Skip to Content
TutorialsERC-20 Transfers

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 = '0x742d35Cc6634C0532925a3b844Bc454e4438f44e' // the ERC-20 contract const recipientAddress = '0x742d35Cc6634C0532925a3b844Bc454e4438f44e' const amount = 1_000_000n // 1 USDC (6 decimals), as bigint in the smallest unit const calldata = await encodeERC20Transfer(recipientAddress, amount)

encodeERC20Transfer(to, amount) is asynchronous and takes the amount as a bigint in the token’s smallest unit.

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!} initialConfig={{ network: { 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: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', value: '1', // human-readable amount, parsed with decimals assetType: 'erc20', tokenAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', 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}</p>} </div> ) }

SendTransactionParams for ERC-20

ParameterTypeDescription
tostringRecipient address
valuestringHuman-readable amount (e.g. "1.5"); the hook applies decimals
assetType'erc20'Indicates an ERC-20 transfer
tokenAddressstringThe ERC-20 contract address
decimalsnumberToken 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 encodeERC20Approve to encode an approve call:

import { createServerWalletManager, MemoryKeyshareStorage, encodeERC20Approve } from '@embarkai/core' const manager = createServerWalletManager({ apiKey: process.env.EMBARK_API_KEY!, chainId: 994873017, storage: new MemoryKeyshareStorage(), }) const wallet = await manager.getWallet('my-wallet') const tokenAddress = '0x742d35Cc6634C0532925a3b844Bc454e4438f44e' const approveCalldata = await encodeERC20Approve( '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', // spender contract 1_000_000n, ) 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), use encodeERC20TransferFrom:

import { createServerWalletManager, MemoryKeyshareStorage, encodeERC20TransferFrom } from '@embarkai/core' const manager = createServerWalletManager({ apiKey: process.env.EMBARK_API_KEY!, chainId: 994873017, storage: new MemoryKeyshareStorage(), }) const wallet = await manager.getWallet('my-wallet') const tokenAddress = '0x742d35Cc6634C0532925a3b844Bc454e4438f44e' const transferFromCalldata = await encodeERC20TransferFrom( wallet.smartAccountAddress, '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', // recipient 1_000_000n, ) const txHash = await wallet.sendUserOperation( tokenAddress, '0', transferFromCalldata, )

For arbitrary ABIs use encodeContractCall(abi, functionName, args) from the Contract Encoding page.

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 = '0x742d35Cc6634C0532925a3b844Bc454e4438f44e' // the ERC-20 contract // Encode and send an ERC-20 transfer const calldata = await encodeERC20Transfer( '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', 1_000_000n, // 1 USDC (6 decimals) ) 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

Last updated on