Created
January 23, 2026 18:05
-
-
Save jonathunne/9d225568f767d77e767b0421e1052317 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| '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