ERC-20 Transfers
Send and receive ERC-20 tokens with your smart wallet.
This tutorial covers transferring ERC-20 tokens using both the Core SDK (backend) and the UI Kit (frontend). You will use the encodeERC20Transfer helper for backend transfers and the useSendTransaction hook with assetType: 'erc20' for frontend transfers.
Prerequisites
- Node.js 18+ installed
- An EmbarkAI API key from the Dashboard
- A deployed wallet with ERC-20 tokens (see Deploy a Smart Wallet)
- The token contract address and decimals for the ERC-20 you want to transfer
Backend Approach
Step 1: Encode the Transfer
Use encodeERC20Transfer to generate the calldata for a standard transfer(address,uint256) call:
import {
createServerWalletManager,
MemoryKeyshareStorage,
encodeERC20Transfer,
} from '@embarkai/core'
const tokenAddress = '0xTokenContractAddress'
const recipientAddress = '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb'
const amount = '1000000' // 1 USDC (6 decimals)
const calldata = encodeERC20Transfer(recipientAddress, amount)Step 2: Send the UserOperation
Pass the encoded calldata to sendUserOperation with the token contract as the to address and 0 as the value (ERC-20 transfers do not send native tokens):
const manager = createServerWalletManager({
apiKey: process.env.EMBARK_API_KEY!,
chainId: 994873017,
storage: new MemoryKeyshareStorage(),
})
const wallet = await manager.getWallet('my-wallet')
const userOpHash = await wallet.sendUserOperation(
tokenAddress, // to: the token contract
'0', // value: 0 (no native transfer)
calldata, // data: encoded transfer call
)
const receipt = await wallet.waitForUserOperationReceipt(userOpHash)
console.log('Transfer TX:', receipt.transactionHash)Frontend Approach
Step 1: Set Up the Provider
import { Provider, ConnectWalletButton } from '@embarkai/ui-kit'
function App() {
return (
<Provider
projectId={process.env.NEXT_PUBLIC_PROJECT_ID!}
chainId={994873017}
>
<ConnectWalletButton />
<TokenTransfer />
</Provider>
)
}Step 2: Send an ERC-20 Transfer
Use useSendTransaction with assetType: 'erc20'. The hook handles encoding internally:
import { useSendTransaction } from '@embarkai/ui-kit'
function TokenTransfer() {
const { sendTransaction, isLoading, userOpHash, error } = useSendTransaction()
const handleTransfer = async () => {
await sendTransaction({
to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
value: '1000000', // Amount in token's smallest unit
assetType: 'erc20',
tokenAddress: '0xTokenContractAddress',
decimals: 6, // Token decimals (e.g., 6 for USDC)
})
}
return (
<div>
<button onClick={handleTransfer} disabled={isLoading}>
{isLoading ? 'Transferring...' : 'Send 1 USDC'}
</button>
{userOpHash && <p>UserOp: {userOpHash}</p>}
{error && <p>Error: {error.message}</p>}
</div>
)
}SendTransactionParams for ERC-20
| Parameter | Type | Description |
|---|---|---|
to | string | Recipient address |
value | string | Amount in the token’s smallest unit (wei equivalent) |
assetType | 'erc20' | Indicates an ERC-20 transfer |
tokenAddress | string | The ERC-20 contract address |
decimals | number | Token decimal places (e.g., 18 for most tokens, 6 for USDC) |
Approve + TransferFrom Pattern
Some use cases require the two-step approve/transferFrom flow — for example, when interacting with a DEX or staking contract.
Step 1: Approve the Spender
Use encodeContractCall to encode an approve call:
import { encodeContractCall } from '@embarkai/core'
const approveCalldata = encodeContractCall({
abi: ['function approve(address spender, uint256 amount) returns (bool)'],
functionName: 'approve',
args: ['0xSpenderContractAddress', BigInt('1000000')],
})
const approveHash = await wallet.sendUserOperation(
tokenAddress,
'0',
approveCalldata,
)
await wallet.waitForUserOperationReceipt(approveHash)
console.log('Approval confirmed')Step 2: Execute TransferFrom
The spender contract can now call transferFrom on behalf of your wallet. If you need to trigger this from your own wallet (e.g., in a batch), encode it similarly:
const transferFromCalldata = encodeContractCall({
abi: ['function transferFrom(address from, address to, uint256 amount) returns (bool)'],
functionName: 'transferFrom',
args: [wallet.smartAccountAddress, '0xRecipientAddress', BigInt('1000000')],
})
const txHash = await wallet.sendUserOperation(
tokenAddress,
'0',
transferFromCalldata,
)Complete Backend Code
import {
createServerWalletManager,
MemoryKeyshareStorage,
encodeERC20Transfer,
} from '@embarkai/core'
async function main() {
const manager = createServerWalletManager({
apiKey: process.env.EMBARK_API_KEY!,
chainId: 994873017,
storage: new MemoryKeyshareStorage(),
})
const wallet = await manager.getWallet('my-wallet')
const tokenAddress = '0xTokenContractAddress'
// Encode and send an ERC-20 transfer
const calldata = encodeERC20Transfer(
'0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
'1000000', // 1 USDC
)
const userOpHash = await wallet.sendUserOperation(
tokenAddress,
'0',
calldata,
)
const receipt = await wallet.waitForUserOperationReceipt(userOpHash)
if (receipt.success) {
console.log('ERC-20 transfer confirmed:', receipt.transactionHash)
}
}
main().catch(console.error)Next Steps
- Contract Encoding — Encode arbitrary contract calls (ERC-721, ERC-1155, custom ABIs)
- Gasless Transactions — Combine ERC-20 transfers with gas sponsorship
- Send Your First Transaction — Native token transfers
- Server Wallets — Full API reference for batch operations