Skip to main content
Complete deposit flow example using wagmi (React hooks) and the Earn SDK.

Overview

This example demonstrates how to:
  • Connect wallet with wagmi in React
  • Check deposit approval status
  • Approve deposit tokens when needed
  • Execute deposit transactions with slippage protection
  • Create custom React hooks for reusable deposit logic
  • Handle loading states and transaction confirmation tracking
  • Display error messages and transaction results

Prerequisites

Before running this example, ensure you have:
  • Node.js >= 20 installed
  • wagmi >= 2.0.0 and viem >= 2.0.0 installed
  • @paxoslabs/earn-sdk installed
  • @tanstack/react-query installed
  • Modern web browser with Web3 wallet extension (MetaMask, Rainbow, etc.)
  • ETH for gas fees
  • Deposit tokens (e.g., USDC)

Complete Code Example

Custom Hook: useVaultDeposit.ts

First, create a custom hook that encapsulates all deposit logic:
/**
 * useVaultDeposit Hook
 *
 * Custom React hook that encapsulates the complete vault deposit flow:
 * - Check approval status
 * - Approve deposit tokens
 * - Execute deposit transaction
 */

import {
  isDepositSpendApproved,
  prepareApproveDepositToken,
  prepareDepositTransactionData,
} from "@paxoslabs/earn-sdk";
import { useCallback, useState } from "react";
import type { Address } from "viem";
import { useWaitForTransactionReceipt, useWriteContract } from "wagmi";

interface CheckApprovalParams {
  yieldType: string;
  depositToken: Address;
  depositAmount: string;
  recipientAddress: Address;
  chainId: number;
}

interface ApproveParams {
  yieldType: string;
  depositToken: Address;
  depositAmount: string;
  chainId: number;
}

interface DepositParams {
  yieldType: string;
  recipientAddress: Address;
  depositToken: Address;
  depositAmount: string;
  chainId: number;
  slippage: number;
}

interface UseVaultDepositReturn {
  // Actions
  checkApproval: (params: CheckApprovalParams) => Promise<void>;
  approve: (params: ApproveParams) => Promise<void>;
  deposit: (params: DepositParams) => Promise<void>;
  reset: () => void;

  // State
  isApproved: boolean | null;
  isCheckingApproval: boolean;
  isApprovingToken: boolean;
  isDepositing: boolean;
  error: Error | null;
  approvalHash: Address | null;
  depositHash: Address | null;
}

export function useDeposit(): UseVaultDepositReturn {
  // State management
  const [isApproved, setIsApproved] = useState<boolean | null>(null);
  const [isCheckingApproval, setIsCheckingApproval] = useState(false);
  const [error, setError] = useState<Error | null>(null);

  // Wagmi hooks for writing contracts
  const {
    writeContractAsync: writeApproval,
    data: approvalHash,
    isPending: isApprovingToken,
  } = useWriteContract();

  const {
    writeContractAsync: writeDeposit,
    data: depositHash,
    isPending: isDepositing,
  } = useWriteContract();

  // Wait for transaction confirmations
  const { isLoading: isWaitingForApproval } = useWaitForTransactionReceipt({
    hash: approvalHash,
  });

  const { isLoading: isWaitingForDeposit } = useWaitForTransactionReceipt({
    hash: depositHash,
  });

  /**
   * Check if deposit tokens are approved
   */
  const checkApproval = useCallback(async (params: CheckApprovalParams) => {
    setIsCheckingApproval(true);
    setError(null);

    try {
      const approved = await isDepositSpendApproved({
        yieldType: params.yieldType,
        depositToken: params.depositToken,
        depositAmount: params.depositAmount,
        recipientAddress: params.recipientAddress,
        chainId: params.chainId,
      });

      setIsApproved(approved);
    } catch (err) {
      const error = err instanceof Error ? err : new Error(String(err));
      setError(error);
      setIsApproved(null);
    } finally {
      setIsCheckingApproval(false);
    }
  }, []);

  /**
   * Approve deposit tokens
   */
  const approve = useCallback(
    async (params: ApproveParams) => {
      setError(null);

      try {
        // Prepare approval transaction
        const approvalTx = await prepareApproveDepositTxData({
          yieldType: params.yieldType,
          depositToken: params.depositToken,
          depositAmount: params.depositAmount,
          chainId: params.chainId,
        });

        // Execute approval using wagmi
        await writeApproval(approvalTx);

        // Update approval state after successful transaction
        setIsApproved(true);
      } catch (err) {
        const error = err instanceof Error ? err : new Error(String(err));
        setError(error);
        throw error;
      }
    },
    [writeApproval]
  );

  /**
   * Execute deposit transaction
   */
  const deposit = useCallback(
    async (params: DepositParams) => {
      setError(null);

      try {
        // Prepare deposit transaction
        const depositTx = await prepareDepositTxData({
          yieldType: params.yieldType,
          recipientAddress: params.recipientAddress,
          depositToken: params.depositToken,
          depositAmount: params.depositAmount,
          chainId: params.chainId,
          slippage: params.slippage,
        });

        // Execute deposit using wagmi
        await writeDeposit(depositTx);
      } catch (err) {
        const error = err instanceof Error ? err : new Error(String(err));
        setError(error);
        throw error;
      }
    },
    [writeDeposit]
  );

  /**
   * Reset hook state
   */
  const reset = useCallback(() => {
    setIsApproved(null);
    setError(null);
  }, []);

  return {
    // Actions
    checkApproval,
    approve,
    deposit,
    reset,

    // State
    isApproved,
    isCheckingApproval,
    isApprovingToken:
      isApprovingToken || isWaitingForApproval || Boolean(approvalHash),
    isDepositing: isDepositing || isWaitingForDeposit || Boolean(depositHash),
    error,
    approvalHash: (approvalHash as Address) || null,
    depositHash: (depositHash as Address) || null,
  };
}

Component: DepositComponent.tsx

Now create the React component that uses the custom hook:
/**
 * Deposit Component
 *
 * React component demonstrating vault deposit using wagmi hooks
 * and the custom useVaultDeposit hook.
 */

import { useState } from "react";
import type { Address } from "viem";
import { useAccount, useConnect, useDisconnect } from "wagmi";
import { injected } from "wagmi/connectors";
import { useVaultDeposit } from "./hooks/useVaultDeposit";

interface DepositConfig {
  yieldType: "PRIME" | "TBILL" | "LENDING";
  depositToken: Address;
  depositAmount: string;
  chainId: number;
  slippage: number;
}

const DEFAULT_CONFIG: DepositConfig = {
  yieldType: "PRIME",
  depositToken: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum
  depositAmount: "1000.0", // $1000 USDC
  chainId: 1, // Ethereum mainnet
  slippage: 100, // 1% (100 basis points)
};

export function DepositComponent() {
  // Wagmi wallet connection hooks
  const { address, isConnected } = useAccount();
  const { connect } = useConnect();
  const { disconnect } = useDisconnect();

  // Local state for deposit configuration
  const [config, setConfig] = useState<DepositConfig>(DEFAULT_CONFIG);

  // Custom deposit hook
  const {
    deposit,
    approve,
    checkApproval,
    isApproved,
    isCheckingApproval,
    isApprovingToken,
    isDepositing,
    error,
    approvalHash,
    depositHash,
    reset,
  } = useVaultDeposit();

  /**
   * Handle wallet connection
   */
  const handleConnect = () => {
    connect({ connector: injected() });
  };

  /**
   * Handle check approval
   */
  const handleCheckApproval = async () => {
    if (!address) return;

    await checkApproval({
      yieldType: config.yieldType,
      depositToken: config.depositToken,
      depositAmount: config.depositAmount,
      recipientAddress: address,
      chainId: config.chainId,
    });
  };

  /**
   * Handle approve tokens
   */
  const handleApprove = async () => {
    await approve({
      yieldType: config.yieldType,
      depositToken: config.depositToken,
      depositAmount: config.depositAmount,
      chainId: config.chainId,
    });
  };

  /**
   * Handle deposit
   */
  const handleDeposit = async () => {
    if (!address) return;

    await deposit({
      yieldType: config.yieldType,
      recipientAddress: address,
      depositToken: config.depositToken,
      depositAmount: config.depositAmount,
      chainId: config.chainId,
      slippage: config.slippage,
    });
  };

  // Render wallet connection UI if not connected
  if (!isConnected) {
    return (
      <div className="deposit-container">
        <div className="connect-section">
          <h2>Connect Wallet</h2>
          <p>Connect your wallet to deposit into Earn Vaults</p>
          <button onClick={handleConnect} className="btn btn-primary">
            Connect Wallet
          </button>
        </div>
      </div>
    );
  }

  // Render deposit form
  return (
    <div className="deposit-container">
      <div className="wallet-section">
        <p className="wallet-address">
          Connected: {address?.slice(0, 6)}...{address?.slice(-4)}
        </p>
        <button onClick={() => disconnect()} className="btn btn-secondary">
          Disconnect
        </button>
      </div>

      <div className="deposit-form">
        <h2>Deposit to Vault</h2>

        <div className="form-group">
          <label htmlFor="yieldType">Yield Type</label>
          <select
            id="yieldType"
            value={config.yieldType}
            onChange={(e) =>
              setConfig((prev) => ({
                ...prev,
                yieldType: e.target.value as "PRIME" | "TBILL" | "LENDING",
              }))
            }
            disabled={isApprovingToken || isDepositing}
          >
            <option value="PRIME">PRIME</option>
            <option value="TBILL">TBILL</option>
            <option value="LENDING">LENDING</option>
          </select>
        </div>

        <div className="form-group">
          <label htmlFor="amount">Deposit Amount (USDC)</label>
          <input
            id="amount"
            type="text"
            value={config.depositAmount}
            onChange={(e) =>
              setConfig((prev) => ({
                ...prev,
                depositAmount: e.target.value,
              }))
            }
            disabled={isApprovingToken || isDepositing}
            placeholder="1000.0"
          />
        </div>

        <div className="form-group">
          <label htmlFor="slippage">Slippage (basis points)</label>
          <input
            id="slippage"
            type="number"
            value={config.slippage}
            onChange={(e) =>
              setConfig((prev) => ({
                ...prev,
                slippage: Number(e.target.value),
              }))
            }
            disabled={isApprovingToken || isDepositing}
            placeholder="100"
          />
          <small>100 basis points = 1%</small>
        </div>

        <div className="button-group">
          <button
            onClick={handleCheckApproval}
            disabled={isCheckingApproval || isApprovingToken || isDepositing}
            className="btn btn-secondary"
          >
            {isCheckingApproval ? "Checking..." : "Check Approval"}
          </button>

          {isApproved !== null && (
            <div className="approval-status">
              {isApproved ? (
                <span className="status-approved">Approved</span>
              ) : (
                <span className="status-not-approved">Not Approved</span>
              )}
            </div>
          )}

          {isApproved === false && (
            <button
              onClick={handleApprove}
              disabled={isApprovingToken || isDepositing}
              className="btn btn-warning"
            >
              {isApprovingToken ? "Approving..." : "Approve Tokens"}
            </button>
          )}

          {isApproved === true && (
            <button
              onClick={handleDeposit}
              disabled={isDepositing}
              className="btn btn-primary"
            >
              {isDepositing ? "Depositing..." : "Deposit"}
            </button>
          )}

          {(approvalHash || depositHash) && (
            <button onClick={reset} className="btn btn-secondary">
              Reset
            </button>
          )}
        </div>

        {error && (
          <div className="error-message">
            <strong>Error:</strong> {error.message}
          </div>
        )}

        {approvalHash && (
          <div className="success-message">
            <strong>Approval Transaction:</strong>
            <br />
            <a
              href={`https://etherscan.io/tx/${approvalHash}`}
              target="_blank"
              rel="noopener noreferrer"
            >
              {approvalHash.slice(0, 10)}...{approvalHash.slice(-8)}
            </a>
          </div>
        )}

        {depositHash && (
          <div className="success-message">
            <strong>Deposit Transaction:</strong>
            <br />
            <a
              href={`https://etherscan.io/tx/${depositHash}`}
              target="_blank"
              rel="noopener noreferrer"
            >
              {depositHash.slice(0, 10)}...{depositHash.slice(-8)}
            </a>
            <br />
            <br />
            <strong>✓ Deposit completed successfully!</strong>
          </div>
        )}
      </div>
    </div>
  );
}

What This Code Does

Custom Hook (useVaultDeposit)

The custom hook encapsulates all deposit logic and provides a clean API:
  1. State Management: Tracks approval status, loading states, errors, and transaction hashes
  2. Approval Checking: checkApproval() verifies if tokens are already approved
  3. Token Approval: approve() prepares and executes approval transaction
  4. Deposit Execution: deposit() prepares and executes deposit transaction
  5. Transaction Tracking: Uses useWaitForTransactionReceipt to track confirmations
  6. Reset Functionality: reset() clears state for new deposits

Component (DepositComponent)

The React component provides the UI and orchestrates the deposit flow:
  1. Wallet Connection: Uses wagmi’s useConnect and useAccount hooks
  2. Form State: Manages deposit configuration (yield type, amount, slippage)
  3. Approval Workflow: Check approval → Approve if needed → Deposit
  4. Loading States: Disables buttons and shows loading indicators during transactions
  5. Error Handling: Displays error messages from hook
  6. Transaction Results: Shows Etherscan links for approval and deposit transactions

Key Features

Custom Hook Pattern

The useVaultDeposit hook provides a reusable, testable pattern for vault deposits:
const {
  deposit,
  approve,
  checkApproval,
  isApproved,
  isCheckingApproval,
  isApprovingToken,
  isDepositing,
  error,
  approvalHash,
  depositHash,
  reset,
} = useVaultDeposit();
Benefits:
  • Separation of Concerns: Business logic separated from UI
  • Reusability: Use the same hook in multiple components
  • Testability: Test logic independently from UI
  • Type Safety: Full TypeScript support with interfaces

Approval-First Workflow

Loading States

The component shows appropriate loading states during:
  • Approval checking: isCheckingApproval
  • Token approval: isApprovingToken (includes waiting for confirmation)
  • Deposit execution: isDepositing (includes waiting for confirmation)
Buttons are disabled during these operations to prevent duplicate transactions.

Error Handling

Errors are caught at the hook level and exposed to components:
if (error) {
  // Display user-friendly error message
  console.error("Deposit failed:", error.message);
}

Slippage Protection

Configuration Options

Change Deposit Amount

const DEFAULT_CONFIG: DepositConfig = {
  // ...
  depositAmount: "5000.0", // Change to $5000 USDC
};

Change Yield Type

const DEFAULT_CONFIG: DepositConfig = {
  yieldType: "TBILL", // Switch to T-Bill vault
  // ...
};

Change Slippage Tolerance

const DEFAULT_CONFIG: DepositConfig = {
  // ...
  slippage: 50, // 0.5% slippage (50 basis points)
};

Use Different Token

const DEFAULT_CONFIG: DepositConfig = {
  depositToken: "0x6B175474E89094C44Da98b954EedeAC495271d0F", // DAI
  // ...
};

Add More Chains

Update your wagmi config to support additional chains:
import { createConfig, http } from "wagmi";
import { mainnet, boba, sei } from "wagmi/chains";

const config = createConfig({
  chains: [mainnet, boba, sei],
  transports: {
    [mainnet.id]: http(),
    [boba.id]: http(),
    [sei.id]: http(),
  },
});

Troubleshooting

”Connect Wallet” button not working

Possible causes:
  • No Web3 wallet extension installed
  • Wallet extension disabled or locked
Solutions:
  1. Install MetaMask, Rainbow, or another Web3 wallet
  2. Unlock your wallet extension
  3. Refresh the page and try again

”Approval transaction failed”

Possible causes:
  • Insufficient ETH for gas fees
  • Incorrect deposit token address
  • Wrong network selected in wallet
Solutions:
  1. Check ETH balance for gas fees
  2. Verify deposit token address is correct
  3. Ensure wallet is connected to Ethereum mainnet

”Deposit transaction failed”

Possible causes:
  • Approval not successful
  • Insufficient deposit tokens
  • Slippage exceeded
Solutions:
  1. Wait for approval confirmation before depositing
  2. Check token balance in wallet
  3. Increase slippage tolerance if needed

Wagmi hook errors

Possible causes:
  • Wagmi or React Query providers not configured
  • Incompatible wagmi version
  • Missing chains in wagmi config
Solutions:
  1. Ensure WagmiProvider and QueryClientProvider wrap your app
  2. Use wagmi >= 2.0.0
  3. Add chains to wagmi config that you’re using

Next Steps