Skip to content

Instantly share code, notes, and snippets.

@jonathunne
Created January 23, 2026 18:05
Show Gist options
  • Select an option

  • Save jonathunne/9d225568f767d77e767b0421e1052317 to your computer and use it in GitHub Desktop.

Select an option

Save jonathunne/9d225568f767d77e767b0421e1052317 to your computer and use it in GitHub Desktop.
'use strict'
/**
* Batch Transfers Example
*
* This script demonstrates how to send multiple transfers in a single
* UserOperation using the WDK EVM ERC-4337 wallet.
*
* IMPORTANT: Before running this script:
* 1. Replace the seed phrase with your test wallet seed phrase
* 2. Replace YOUR_CANDIDE_API_KEY with your Candide paymaster API key
* 3. Make sure your Safe smart account has enough funds to cover:
* - The transfer amounts
* - The paymaster token (USDT) for gas fees
* 4. Approve the paymaster contract to spend your USDT (see README)
*
* Run with: node examples/batch-transfers-example.js
*/
import { Interface } from 'ethers'
import WalletManagerEvmErc4337 from '../index.js'
// ============================================================================
// CONFIGURATION - Update these values for your setup
// ============================================================================
// Your BIP-39 seed phrase - DO NOT use in production or commit to git!
const SEED_PHRASE = 'your twelve word seed phrase goes here replace with actual words'
// ============================================================================
// CANDIDE CONFIGURATION FOR SEPOLIA TESTNET
// Get your API key from https://dashboard.candide.dev
// ============================================================================
const CONFIG = {
chainId: 11155111, // Sepolia
provider: 'https://ethereum-sepolia-rpc.publicnode.com', // Public RPC
bundlerUrl: 'https://api.candide.dev/public/v3/11155111', // Public bundler (no API key needed)
paymasterUrl: 'https://api.candide.dev/paymaster/v3/sepolia/YOUR_CANDIDE_API_KEY',
paymasterAddress: '0x8b1f6cb5d062aa2ce8d581942bbb960420d875ba', // Candide ERC-20 Paymaster
entryPointAddress: '0x0000000071727De22E5E9d8BAf0edAc6f37da032', // EntryPoint v0.7
safeModulesVersion: '0.3.0',
paymasterToken: {
address: '0xd077A400968890Eacc75cdc901F0356c943e4fDb' // USDT on Sepolia
}
}
// Example recipient addresses (use your own test addresses)
const RECIPIENTS = [
'0x0000000000000000000000000000000000000001',
'0x0000000000000000000000000000000000000002',
'0x0000000000000000000000000000000000000003'
]
// ============================================================================
// BATCH TRANSFER EXAMPLES
// ============================================================================
/**
* Example 1: Batch USDT Transfers
*
* Sends USDT to multiple recipients in a single UserOperation.
* All transfers are executed atomically - either all succeed or all fail.
*/
async function batchUsdtTransfers (account) {
console.log('\n--- Example 1: Batch USDT Transfers ---\n')
const USDT_ADDRESS = '0xd077A400968890Eacc75cdc901F0356c943e4fDb'
// ERC20 transfer function interface
const erc20Interface = new Interface([
'function transfer(address to, uint256 amount) returns (bool)'
])
// Create an array of USDT transfer transactions
const transactions = [
{
to: USDT_ADDRESS,
value: 0n,
data: erc20Interface.encodeFunctionData('transfer', [RECIPIENTS[0], 100000n]) // 0.1 USDT
},
{
to: USDT_ADDRESS,
value: 0n,
data: erc20Interface.encodeFunctionData('transfer', [RECIPIENTS[1], 200000n]) // 0.2 USDT
},
{
to: USDT_ADDRESS,
value: 0n,
data: erc20Interface.encodeFunctionData('transfer', [RECIPIENTS[2], 300000n]) // 0.3 USDT
}
]
console.log('Transactions to send:')
console.log(` 1. Send 0.1 USDT to ${RECIPIENTS[0]}`)
console.log(` 2. Send 0.2 USDT to ${RECIPIENTS[1]}`)
console.log(` 3. Send 0.3 USDT to ${RECIPIENTS[2]}`)
// Get fee estimate for the batch
console.log('\nEstimating fees...')
const quote = await account.quoteSendTransaction(transactions)
console.log(`Estimated fee: ${Number(quote.fee) / 1e6} USDT`)
// Send the batch transaction
console.log('\nSending batch transaction...')
const result = await account.sendTransaction(transactions)
console.log('\nResult:')
console.log(` UserOperation hash: ${result.hash}`)
console.log(` Fee paid: ${Number(result.fee) / 1e6} USDT`)
return result
}
/**
* Example 2: Batch ERC20 Token Transfers
*
* Sends ERC20 tokens to multiple recipients in a single UserOperation.
* Uses raw transaction data with encoded function calls.
*/
async function batchErc20Transfers (account, tokenAddress) {
console.log('\n--- Example 2: Batch ERC20 Token Transfers ---\n')
// ERC20 transfer function interface
const erc20Interface = new Interface([
'function transfer(address to, uint256 amount) returns (bool)'
])
// Create transfer transactions
const transactions = [
{
to: tokenAddress,
value: 0n,
data: erc20Interface.encodeFunctionData('transfer', [
RECIPIENTS[0],
1000000n // 1 token (assuming 6 decimals)
])
},
{
to: tokenAddress,
value: 0n,
data: erc20Interface.encodeFunctionData('transfer', [
RECIPIENTS[1],
2000000n // 2 tokens
])
},
{
to: tokenAddress,
value: 0n,
data: erc20Interface.encodeFunctionData('transfer', [
RECIPIENTS[2],
3000000n // 3 tokens
])
}
]
console.log(`Token contract: ${tokenAddress}`)
console.log('Transfers to send:')
console.log(` 1. 1 token to ${RECIPIENTS[0]}`)
console.log(` 2. 2 tokens to ${RECIPIENTS[1]}`)
console.log(` 3. 3 tokens to ${RECIPIENTS[2]}`)
// Get fee estimate
console.log('\nEstimating fees...')
const quote = await account.quoteSendTransaction(transactions)
console.log(`Estimated fee: ${Number(quote.fee) / 1e6} USDT`)
// Send the batch
console.log('\nSending batch transaction...')
const result = await account.sendTransaction(transactions)
console.log('\nResult:')
console.log(` UserOperation hash: ${result.hash}`)
console.log(` Fee paid: ${Number(result.fee) / 1e6} USDT`)
return result
}
/**
* Example 3: Mixed Batch (Native + ERC20 + Contract Calls)
*
* Demonstrates combining different transaction types in a single batch.
*/
async function mixedBatchTransactions (account, tokenAddress) {
console.log('\n--- Example 3: Mixed Batch Transactions ---\n')
const erc20Interface = new Interface([
'function transfer(address to, uint256 amount) returns (bool)',
'function approve(address spender, uint256 amount) returns (bool)'
])
const transactions = [
// 1. Send native ETH
{
to: RECIPIENTS[0],
value: 1000000000000000n, // 0.001 ETH
data: '0x'
},
// 2. Transfer ERC20 tokens
{
to: tokenAddress,
value: 0n,
data: erc20Interface.encodeFunctionData('transfer', [
RECIPIENTS[1],
1000000n
])
},
// 3. Approve a spender
{
to: tokenAddress,
value: 0n,
data: erc20Interface.encodeFunctionData('approve', [
RECIPIENTS[2],
5000000n // Approve 5 tokens
])
}
]
console.log('Mixed batch operations:')
console.log(` 1. Send 0.001 ETH to ${RECIPIENTS[0]}`)
console.log(` 2. Transfer 1 token to ${RECIPIENTS[1]}`)
console.log(` 3. Approve 5 tokens for ${RECIPIENTS[2]}`)
// Get fee estimate
console.log('\nEstimating fees...')
const quote = await account.quoteSendTransaction(transactions)
console.log(`Estimated fee: ${Number(quote.fee) / 1e6} USDT`)
// Send the batch
console.log('\nSending batch transaction...')
const result = await account.sendTransaction(transactions)
console.log('\nResult:')
console.log(` UserOperation hash: ${result.hash}`)
console.log(` Fee paid: ${Number(result.fee) / 1e6} USDT`)
return result
}
/**
* Example 4: Fee Estimation Only (Dry Run)
*
* Shows how to estimate fees without actually sending the transaction.
*/
async function estimateBatchFees (account) {
console.log('\n--- Example 4: Fee Estimation Only ---\n')
// Various batch sizes to compare fees
const batchSizes = [1, 3, 5, 10]
for (const size of batchSizes) {
const transactions = Array.from({ length: size }, (_, i) => ({
to: `0x${(i + 1).toString(16).padStart(40, '0')}`, // Valid 20-byte addresses
value: 1000000000000000n, // 0.001 ETH each
data: '0x'
}))
const quote = await account.quoteSendTransaction(transactions)
console.log(`Batch of ${size} transfers: fee = ${Number(quote.fee) / 1e6} USDT`)
}
console.log('\nNote: Batching is more gas-efficient than individual transactions!')
}
// ============================================================================
// MAIN EXECUTION
// ============================================================================
async function main () {
console.log('='.repeat(60))
console.log('WDK EVM ERC-4337 Batch Transfers Demo')
console.log('='.repeat(60))
// Validate configuration
if (CONFIG.paymasterUrl.includes('YOUR_CANDIDE_API_KEY')) {
console.error('\nError: Please update the CONFIG with your Candide API key')
console.log('\nTo run this example, you need:')
console.log(' 1. A Candide API key from https://dashboard.candide.dev')
console.log(' 2. Your seed phrase')
console.log(' 3. USDT in your Safe smart account (for transfers + gas fees)')
console.log(' 4. Approve the paymaster to spend your USDT')
process.exit(1)
}
if (SEED_PHRASE.includes('your twelve word')) {
console.error('\nError: Please update SEED_PHRASE with your actual seed phrase')
process.exit(1)
}
try {
// Create wallet manager
console.log('\nInitializing wallet...')
const wallet = new WalletManagerEvmErc4337(SEED_PHRASE, CONFIG)
// Get the first account
const account = await wallet.getAccount(0)
// Get and display the Safe account address
const address = await account.getAddress()
console.log(`Safe smart account address: ${address}`)
// Check balances
const balance = await account.getBalance()
console.log(`Native token balance: ${Number(balance) / 1e18} ETH`)
const usdtBalance = await account.getTokenBalance(CONFIG.paymasterToken.address)
console.log(`USDT balance: ${Number(usdtBalance) / 1e6} USDT`)
// Run examples (uncomment the one you want to test)
// Example 1: Batch USDT transfers
await batchUsdtTransfers(account)
// Example 2: Batch any ERC20 transfers
// await batchErc20Transfers(account, 'TOKEN_CONTRACT_ADDRESS')
// Example 3: Mixed batch (ETH + tokens + approvals)
// await mixedBatchTransactions(account, 'TOKEN_CONTRACT_ADDRESS')
// Example 4: Fee estimation only (safe to run, no actual transfers)
// await estimateBatchFees(account)
// Clean up
wallet.dispose()
console.log('\n' + '='.repeat(60))
console.log('Demo complete!')
console.log('='.repeat(60))
} catch (error) {
console.error('\nError:', error.message)
if (error.message.includes('AA50')) {
console.log('\nHint: Make sure your Safe account has enough USDT to cover gas fees.')
}
if (error.message.includes('allowance')) {
console.log('\nHint: You need to approve the paymaster to spend your USDT first.')
console.log('Paymaster address:', CONFIG.paymasterAddress)
}
process.exit(1)
}
}
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment