One advantage of ERC-4337 is that multiple transactions can be sent in a single action. This allows you to turn any experience in crypto into one click.
This example shows one common use case: sending multiple ERC-20 tokens in the same action using a builder’s executeBatch method.
1. Import Libraries
As with other examples, you will import ethers and userop.
import { ethers } from "ethers";
import { Client, Presets } from "userop";
2. Build account preset
Userop.js uses a builder pattern for creating user operations. Rather than create a user operation completely from scratch, we can use one of the presets. In this example we’ll use the simpleAccount preset, which is configured to match the simpleAccount example from the Ethereum Foundation.
const simpleAccount = await Presets.Builder.SimpleAccount.init(
signer, // An ethers.js signer
rpcUrl, // URL for the node
entryPoint, // EntryPoint contract address
simpleAccountFactory, // simpleAccount factory contract address
paymaster // OPTIONAL: paymaster information
);
This initializes a simpleAccount user operation builder. This preset includes an executeBatch function, which can be used to execute multiple transactions.
3. Create the execution data
We will assume that you have a list of recipients, the tokens you are sending, and the amount of the tokens each will receive.
// Vectors containing the transaction information
dest = ["0x000...", "0x000...", ...]; // Addresses of the ERC-20 tokens that will be called
tokenRecipients = ["0x000...", "0x000...", ...]; // Recipients of the ERC-20 tokens
tokenAmounts = [0, 0, ...]; // Amount that will be sent to each recipient
We will loop through the list of recipients and put the transaction data into a data array. Each element of the data array will be encoded function data that will be executed by the smart account.
// First, create a provider that you will use to retrieve each ERC-20 token data
const provider = new ethers.provider.JsonRpcProvider(rpcUrl);
// Loop through each of the recipients, encoding the transaction data into a data array.
let data: Array<string> = [];
for (let i=0; i<tokenAddresses.length(); i++) {
// Get the symbol and decimals of each ERC-20 token
var erc20 = new ethers.Contract(
ethers.utils.getAddress(tokenAddresses[i]),
ERC20_ABI,
provider);
var [symbol, decimals] = await Promise.all([
erc20.symbol(),
erc20.decimals(),
]);
// Encode the data
var to = ethers.utils.getAddress(tokenRecipients[i]);
var amount = ethers.utils.parseUnits(tokenAmounts[i], decimals);
// Check our homework
console.log("Creating transaction to send ${amount} ${symbol} tokens to ${to}")
data = [
...data,
erc20.interface.encodeFunctionData("transfer", [to, amount])
];
}
4. Ship it
Then simply create and send the user operation:
// Create the user operation
const userOp = simpleAccount.executeBatch(dest, data);
// Send the User Operation
const res = await client.sendUserOperation(userOp);
// Check your homework
console.log(`UserOpHash: ${res.userOpHash}`);
console.log("Waiting for transaction...");
const ev = await res.wait();
console.log(`Transaction hash: ${ev?.transactionHash ?? null}`);
Full Example
You can view an example ERC-20 token transfer in the ERC-4337 Example Repository.
import { ethers } from "ethers";
import { Client, Presets } from "userop";
import { ERC20_ABI, CLIOpts } from "../../src";
// @ts-ignore
import config from "../../config.json";
// This example requires several layers of calls:
// EntryPoint
// ┕> sender.executeBatch
// ┕> token.transfer (recipient 1)
// ⋮
// ┕> token.transfer (recipient N)
export default async function main(
tkn: string,
t: Array,
amt: string,
opts: CLIOpts
) {
const paymaster = opts.withPM
? Presets.Middleware.verifyingPaymaster(
config.paymaster.rpcUrl,
config.paymaster.context
)
: undefined;
const simpleAccount = await Presets.Builder.SimpleAccount.init(
new ethers.Wallet(config.signingKey),
config.rpcUrl,
config.entryPoint,
config.simpleAccountFactory,
paymaster
);
const client = await Client.init(config.rpcUrl, config.entryPoint);
const provider = new ethers.providers.JsonRpcProvider(config.rpcUrl);
const token = ethers.utils.getAddress(tkn);
const erc20 = new ethers.Contract(token, ERC20_ABI, provider);
const [symbol, decimals] = await Promise.all([
erc20.symbol(),
erc20.decimals(),
]);
const amount = ethers.utils.parseUnits(amt, decimals);
let dest: Array = [];
let data: Array = [];
t.map((addr) => addr.trim()).forEach((addr) => {
dest = [...dest, erc20.address];
data = [
...data,
erc20.interface.encodeFunctionData("transfer", [
ethers.utils.getAddress(addr),
amount,
]),
];
});
console.log(
`Batch transferring ${amt} ${symbol} to ${dest.length} recipients...`
);
const res = await client.sendUserOperation(
simpleAccount.executeBatch(dest, data),
{
dryRun: opts.dryRun,
onBuild: (op) => console.log("Signed UserOperation:", op),
}
);
console.log(`UserOpHash: ${res.userOpHash}`);
console.log("Waiting for transaction...");
const ev = await res.wait();
console.log(`Transaction hash: ${ev?.transactionHash ?? null}`);
}