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
| Aspect | EOA (Privy) | Privy Smart Wallet | Alchemy Smart Account |
|---|
| Send Method | sendTransaction() | sendTransaction({ calls }) | sendTransactions({ requests }) |
| Batching | No | Yes | Yes |
| Permit Support | Yes | No | No |
| Gas Payment | User | Sponsored | Sponsored |
| Return Value | txHash | txHash | userOpHash |
| Receipt Polling | useWaitForTransactionReceipt | useWaitForTransactionReceipt | waitForUserOperationReceipt |
| USDT Deposit | 2 transactions | 1 batched tx | 1 UserOperation |
| USDC Deposit | 1 tx (permit) | 1 batched tx | 1 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:
// When current allowance is non-zero but insufficient,
// USDT requires resetting to 0 first
if (currentAllowance > 0n && currentAllowance < requiredAmount) {
// Step 1: Reset approval to 0
await sendApproval(tokenAddress, spender, 0n);
// Step 2: Set new approval amount
await sendApproval(tokenAddress, spender, requiredAmount);
// Step 3: Deposit
await sendDeposit(depositTxData);
}
When batching is not possible (e.g., non-smart-wallet USDT deposits with existing partial allowance), these three steps must be sent sequentially.
Token-Specific Behavior
| Token | Supports Permit | Best Flow |
|---|
| USDC | Yes | Permit (EOA) or Batched (Smart) |
| USDG | Yes | Permit (EOA) or Batched (Smart) |
| pyUSD | Yes | Permit (EOA) or Batched (Smart) |
| USD0 | Yes | Permit (EOA) or Batched (Smart) |
| USDT | No | Batched approval + deposit |
Troubleshooting
| Issue | Solution |
|---|
| ”Smart wallet not connected” | Ensure user has a smart wallet configured in Privy/Dynamic |
| UserOperation fails | Check gas estimation, paymaster may need funding |
waitForUserOperationReceipt times out | Bundler may be congested, increase timeout |
| Batched tx reverts | Check individual call data, one call may be failing |
For standard EOA wallet examples, see Deposits.