Skip to main content
Smart wallets enable transaction batching and gas sponsorship, providing a better user experience than traditional EOA wallets. This guide covers integration patterns for both Privy Smart Wallets and Alchemy Smart Accounts (ERC-4337).
Key difference from EOA wallets: Smart wallets cannot sign EIP-712 permit messages, so they must use the approval flow. However, they can batch the approval and deposit into a single user confirmation.

Wallet Type Comparison

AspectEOA (Privy)Privy Smart WalletAlchemy Smart Account
Send MethodsendTransaction()sendTransaction({ calls })sendTransactions({ requests })
BatchingNoYesYes
Permit SupportYesNoNo
Gas PaymentUserSponsoredSponsored
Return ValuetxHashtxHashuserOpHash
Receipt PollinguseWaitForTransactionReceiptuseWaitForTransactionReceiptwaitForUserOperationReceipt
USDT Deposit2 transactions1 batched tx1 UserOperation
USDC Deposit1 tx (permit)1 batched tx1 UserOperation

Privy Smart Wallet

Uses Privy’s embedded smart contract wallet with gas sponsorship and transaction batching.

Imports

// SDK imports
import {
  prepareDepositAuthorization,
  prepareDeposit,
  isApprovalAuth,
  isAlreadyApprovedAuth,
  YieldType,
} from "@paxoslabs/amplify-sdk";

// Privy imports
import { useSmartWallets } from "@privy-io/react-auth/smart-wallets";

// Viem imports
import { encodeFunctionData, formatUnits } from "viem";

usePrivySmartWalletDeposit Hook

// src/hooks/usePrivySmartWalletDeposit.ts
import { useState, useCallback } from "react";
import { useSmartWallets } from "@privy-io/react-auth/smart-wallets";
import { encodeFunctionData, formatUnits } from "viem";
import {
  prepareDepositAuthorization,
  prepareDeposit,
  isApprovalAuth,
  isAlreadyApprovedAuth,
  YieldType,
} from "@paxoslabs/amplify-sdk";

interface DepositParams {
  depositAsset: `0x${string}`;
  amount: bigint;
  signerAddress: `0x${string}`;
  yieldType: YieldType;
}

export function usePrivySmartWalletDeposit() {
  const { client: smartWalletClient } = useSmartWallets();
  const [status, setStatus] = useState<string>("");
  const [isLoading, setIsLoading] = useState(false);

  const deposit = useCallback(
    async ({ depositAsset, amount, signerAddress, yieldType }: DepositParams) => {
      if (!smartWalletClient) {
        throw new Error("Smart wallet not connected");
      }

      setIsLoading(true);

      try {
        const params = {
          yieldType,
          depositAsset,
          depositAmount: formatUnits(amount, 6),
          to: signerAddress,
          chainId: 1,
          forceMethod: "approval" as const, // Smart wallets can't sign permits
        };

        // Step 1: Get authorization (forces approval method)
        setStatus("Preparing transaction...");
        const auth = await prepareDepositAuthorization(params);

        // Step 2: Prepare deposit tx
        const prepared = await prepareDeposit(params);

        // Step 3: Batch approve + deposit in ONE transaction
        if (isApprovalAuth(auth)) {
          const calls = [
            {
              to: auth.txData.address, // Token contract
              data: encodeFunctionData({
                abi: auth.txData.abi,
                functionName: auth.txData.functionName,
                args: auth.txData.args,
              }),
            },
            {
              to: prepared.txData.address, // Depositor contract
              data: encodeFunctionData({
                abi: prepared.txData.abi,
                functionName: prepared.txData.functionName,
                args: prepared.txData.args,
              }),
            },
          ];

          setStatus("Please confirm in your wallet...");

          // Single batched transaction with gas sponsorship
          const hash = await smartWalletClient.sendTransaction(
            { calls },
            {
              sponsor: true, // Gas is sponsored
              uiOptions: {
                description: `Depositing ${formatUnits(amount, 6)} tokens`,
              },
            }
          );

          setStatus("Deposit successful!");
          return hash;
        } else if (isAlreadyApprovedAuth(auth)) {
          // No approval needed, just deposit
          setStatus("Please confirm in your wallet...");

          const hash = await smartWalletClient.sendTransaction(
            {
              calls: [
                {
                  to: prepared.txData.address,
                  data: encodeFunctionData({
                    abi: prepared.txData.abi,
                    functionName: prepared.txData.functionName,
                    args: prepared.txData.args,
                  }),
                },
              ],
            },
            {
              sponsor: true,
              uiOptions: {
                description: `Depositing ${formatUnits(amount, 6)} tokens`,
              },
            }
          );

          setStatus("Deposit successful!");
          return hash;
        }
      } catch (error) {
        setStatus(`Error: ${error instanceof Error ? error.message : "Unknown"}`);
        throw error;
      } finally {
        setIsLoading(false);
      }
    },
    [smartWalletClient]
  );

  return { deposit, status, isLoading };
}

Key Characteristics

  • Uses smartWalletClient.sendTransaction({ calls }) for batching multiple contract calls
  • sponsor: true enables gas sponsorship (users don’t pay gas)
  • Cannot use permit signatures (smart contracts can’t sign EIP-712)
  • Batches approve + deposit into a single user confirmation
  • Returns a standard transaction hash

Alchemy Smart Account (ERC-4337)

Uses ERC-4337 Account Abstraction via Alchemy’s SDK with Dynamic as the auth provider.
Alchemy Smart Accounts return a userOpHash instead of a standard transaction hash. You must use waitForUserOperationReceipt() instead of wagmi’s useWaitForTransactionReceipt.

Imports

// SDK imports
import {
  prepareDepositAuthorization,
  prepareDeposit,
  isApprovalAuth,
  isAlreadyApprovedAuth,
  YieldType,
} from "@paxoslabs/amplify-sdk";

// Your Alchemy provider hook (implementation depends on your setup)
import { useAlchemySmartAccount } from "@/providers/alchemy-smart-account-provider";

// Viem imports
import { encodeFunctionData, formatUnits } from "viem";

useAlchemyDeposit Hook

// src/hooks/useAlchemyDeposit.ts
import { useState, useCallback } from "react";
import { useAlchemySmartAccount } from "@/providers/alchemy-smart-account-provider";
import { encodeFunctionData, formatUnits } from "viem";
import {
  prepareDepositAuthorization,
  prepareDeposit,
  isApprovalAuth,
  isAlreadyApprovedAuth,
  YieldType,
} from "@paxoslabs/amplify-sdk";

interface DepositParams {
  depositAsset: `0x${string}`;
  amount: bigint;
  signerAddress: `0x${string}`;
  yieldType: YieldType;
}

export function useAlchemyDeposit() {
  const { client: alchemyClient } = useAlchemySmartAccount();
  const [status, setStatus] = useState<string>("");
  const [isLoading, setIsLoading] = useState(false);

  const deposit = useCallback(
    async ({ depositAsset, amount, signerAddress, yieldType }: DepositParams) => {
      if (!alchemyClient) {
        throw new Error("Alchemy client not connected");
      }

      setIsLoading(true);

      try {
        const params = {
          yieldType,
          depositAsset,
          depositAmount: formatUnits(amount, 6),
          to: signerAddress,
          chainId: 1,
          forceMethod: "approval" as const,
        };

        // Step 1: Prepare authorization and deposit data
        setStatus("Preparing transaction...");
        const auth = await prepareDepositAuthorization(params);
        const prepared = await prepareDeposit(params);

        // Step 2: Batch via sendTransactions (ERC-4337 UserOperation)
        if (isApprovalAuth(auth)) {
          setStatus("Please confirm in your wallet...");

          const userOpHash = await alchemyClient.sendTransactions({
            requests: [
              {
                to: auth.txData.address,
                data: encodeFunctionData({
                  abi: auth.txData.abi,
                  functionName: auth.txData.functionName,
                  args: auth.txData.args,
                }),
              },
              {
                to: prepared.txData.address,
                data: encodeFunctionData({
                  abi: prepared.txData.abi,
                  functionName: prepared.txData.functionName,
                  args: prepared.txData.args,
                }),
              },
            ],
          });

          // Note: This is a UserOperation hash, NOT a transaction hash
          setStatus("Waiting for confirmation...");
          const receipt = await alchemyClient.waitForUserOperationReceipt({
            hash: userOpHash,
          });

          setStatus("Deposit successful!");
          return receipt.receipt.transactionHash;
        } else if (isAlreadyApprovedAuth(auth)) {
          setStatus("Please confirm in your wallet...");

          const userOpHash = await alchemyClient.sendTransactions({
            requests: [
              {
                to: prepared.txData.address,
                data: encodeFunctionData({
                  abi: prepared.txData.abi,
                  functionName: prepared.txData.functionName,
                  args: prepared.txData.args,
                }),
              },
            ],
          });

          setStatus("Waiting for confirmation...");
          const receipt = await alchemyClient.waitForUserOperationReceipt({
            hash: userOpHash,
          });

          setStatus("Deposit successful!");
          return receipt.receipt.transactionHash;
        }
      } catch (error) {
        setStatus(`Error: ${error instanceof Error ? error.message : "Unknown"}`);
        throw error;
      } finally {
        setIsLoading(false);
      }
    },
    [alchemyClient]
  );

  return { deposit, status, isLoading };
}

Key Characteristics

  • Uses alchemyClient.sendTransactions({ requests }) for batching
  • Returns userOpHash (not a standard transaction hash)
  • Must use waitForUserOperationReceipt() instead of wagmi’s receipt hook
  • Bundler submits the UserOperation to the network
  • Gas can be sponsored via paymaster configuration

USDT Special Handling

USDT requires special handling because its approve() function requires resetting to 0 before setting a new value if there’s an existing non-zero allowance:
// Check if approval reset is needed
const shouldResetApproval =
  !tokenConfig.supportsPermit &&
  currentAllowance > 0n &&
  currentAllowance < approvalAmount;

if (shouldResetApproval) {
  // First reset approval to 0
  await sendApproval(tokenAddress, spender, 0n);
  // Then set new approval amount
  await sendApproval(tokenAddress, spender, approvalAmount);
}
The SDK handles this automatically when using prepareDepositAuthorization() with USDT. The returned txData will include the reset approval if needed.

Token-Specific Behavior

TokenSupports PermitBest Flow
USDCYesPermit (EOA) or Batched (Smart)
USDGYesPermit (EOA) or Batched (Smart)
pyUSDYesPermit (EOA) or Batched (Smart)
USD0YesPermit (EOA) or Batched (Smart)
USDTNoBatched approval + deposit

Troubleshooting

IssueSolution
”Smart wallet not connected”Ensure user has a smart wallet configured in Privy/Dynamic
UserOperation failsCheck gas estimation, paymaster may need funding
waitForUserOperationReceipt times outBundler may be congested, increase timeout
Batched tx revertsCheck individual call data, one call may be failing
For standard EOA wallet examples, see Deposits.