Documentation Index
Fetch the complete documentation index at: https://developers.paxoslabs.com/llms.txt
Use this file to discover all available pages before exploring further.
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}`
vaultName: string
}
export function usePrivySmartWalletDeposit() {
const { client: smartWalletClient } = useSmartWallets()
const [status, setStatus] = useState<string>('')
const [isLoading, setIsLoading] = useState(false)
const deposit = useCallback(
async ({
depositAsset,
amount,
signerAddress,
vaultName,
}: DepositParams) => {
if (!smartWalletClient) {
throw new Error('Smart wallet not connected')
}
setIsLoading(true)
try {
const params = {
vaultName,
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}`
vaultName: string
}
export function useAlchemyDeposit() {
const { client: alchemyClient } = useAlchemySmartAccount()
const [status, setStatus] = useState<string>('')
const [isLoading, setIsLoading] = useState(false)
const deposit = useCallback(
async ({
depositAsset,
amount,
signerAddress,
vaultName,
}: DepositParams) => {
if (!alchemyClient) {
throw new Error('Alchemy client not connected')
}
setIsLoading(true)
try {
const params = {
vaultName,
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.