Send a user operation
User operations are the ERC-4337 counterpart to traditional blockchain transactions. They incorporate significant enhancements that improve user experience and provide greater flexibility in account management and transaction execution.
Viem's Account Abstraction API allows a developer to specify an array of Calls
that will be executed as a user operation via Viem's sendUserOperation
method.
The Delegation Toolkit encodes and executes the provided calls.
User operations are not directly sent to the network.
Instead, they are sent to a bundler, which validates, optimizes, and aggregates them before network submission.
See Viem's BundlerClient
for details on how to interact with the bundler.
If a user operation is sent from a smart contract account that has not been deployed, the toolkit configures the user operation to automatically deploy the account.
Prerequisites
- Install and set up the MetaMask Delegation Toolkit.
- Configure the toolkit.
- Create a delegator account.
Send a user operation
The following is a simplified example of sending a user operation using Viem Core SDK. Viem Core SDK offers more granular control for developers who require it.
In the example, a user operation is created with the necessary gas limits.
This user operation is passed to a bundler instance, and the EntryPoint
address is retrieved from the client.
- example.ts
- config.ts
import { bundlerClient, smartAccount } from "./config.ts";
// Appropriate fee per gas must be determined for the specific bundler being used.
const maxFeePerGas = 1n;
const maxPriorityFeePerGas = 1n;
const userOperationHash = await bundlerClient.sendUserOperation({
account: smartAccount,
calls: [
{
to: "0x1234567890123456789012345678901234567890",
value: parseEther("1")
}
],
maxFeePerGas,
maxPriortyFeePerGas
});
import { createPublicClient, http } from "viem";
import { createBundlerClient } from "viem/account-abstraction";
import { lineaSepolia as chain } from "viem/chains";
import {
Implementation,
toMetaMaskSmartAccount,
} from "@metamask-private/delegator-core-viem";
const publicClient = createPublicClient({
chain,
transport: http()
});
const privateKey = generatePrivateKey();
const account = privateKeyToAccount(privateKey);
export const smartAccount = await toMetaMaskSmartAccount({
client: publicClient,
implementation: Implementation.Hybrid,
deployParams: [account.address, [], [], []],
deploySalt: "0x",
signatory: { account },
});
export const bundlerClient = createBundlerClient({
publicClient,
transport: http("https://public.pimlico.io/v2/1/rpc")
});
Estimate fee per gas
Different bundlers have different ways to estimate maxFeePerGas
and maxPriorityFeePerGas
, and can reject requests with insufficient values.
The following example uses constant values, but the Hello Gator example use Pimlico's Alto bundler,
which fetches user operation gas price using the RPC method pimlico_getUserOperationPrice
.
import { createPimlicoClient } from "permissionless/clients/pimlico";
// The config.ts is the same as in the previous example.
import { bundlerClient, smartAccount } from "./config.ts"
// You can get the API Key from the Pimlico's dashboard
const pimlicoClient = createPimlicoClient({
transport: http("https://api.pimlico.io/v2/59141/rpc"),
});
const { fast: fee } = await pimlicoClient.getUserOperationGasPrice();
const userOperationHash = await bundlerClient.sendUserOperation({
account: smartAccount,
calls: [
{
to: "0x1234567890123456789012345678901234567890",
value: parseEther("1")
}
],
...fee
});
Wait for the transaction receipt
After submitting the user operation, it's crucial to wait for the receipt to ensure that it has been successfully included in the blockchain. Use the waitForUserOperationReceipt
method provided by the bundler client.
import { bundlerClient } from "./config.ts";
// Use the userOperationHash from the previous code snippet.
const { receipt } = await bundlerClient.waitForUserOperationReceipt({
hash: userOperationHash
});
console.log(receipt.transactionHash);