Builder
Learn how to use userop.js to build ERC-4337 UserOperations.
A UserOperation is a pseudo-transaction object used to execute actions through a smart contract account. Although it can be quite complex to create, the UserOperationBuilder
simplifies this process using the builder pattern. The interface is also agnostic to any ERC-4337 Smart Account or Paymaster implementation.
View the source code on GitHub.
Interfaces
These interfaces are built using common ethers.js types. More specifically BigNumberish
and BytesLike
.
UserOperation
An interface for an ERC-4337 User Operation. Building a UserOperation
involves constructing multiple parts and merging them together.
interface IUserOperation {
sender: string;
nonce: BigNumberish;
initCode: BytesLike;
callData: BytesLike;
callGasLimit: BigNumberish;
verificationGasLimit: BigNumberish;
preVerificationGas: BigNumberish;
maxFeePerGas: BigNumberish;
maxPriorityFeePerGas: BigNumberish;
paymasterAndData: BytesLike;
signature: BytesLike;
}
UserOperationBuilder
An instance of UserOperationBuilder
can help build a UserOperation
that can be passed to the client.
interface IUserOperationBuilder {
// get methods.
getSender: () => string;
getNonce: () => BigNumberish;
getInitCode: () => BytesLike;
getCallData: () => BytesLike;
getCallGasLimit: () => BigNumberish;
getVerificationGasLimit: () => BigNumberish;
getPreVerificationGas: () => BigNumberish;
getMaxFeePerGas: () => BigNumberish;
getMaxPriorityFeePerGas: () => BigNumberish;
getPaymasterAndData: () => BytesLike;
getSignature: () => BytesLike;
getOp: () => IUserOperation;
// set methods.
setSender: (address: string) => IUserOperationBuilder;
setNonce: (nonce: BigNumberish) => IUserOperationBuilder;
setInitCode: (code: BytesLike) => IUserOperationBuilder;
setCallData: (data: BytesLike) => IUserOperationBuilder;
setCallGasLimit: (gas: BigNumberish) => IUserOperationBuilder;
setVerificationGasLimit: (gas: BigNumberish) => IUserOperationBuilder;
setPreVerificationGas: (gas: BigNumberish) => IUserOperationBuilder;
setMaxFeePerGas: (fee: BigNumberish) => IUserOperationBuilder;
setMaxPriorityFeePerGas: (fee: BigNumberish) => IUserOperationBuilder;
setPaymasterAndData: (data: BytesLike) => IUserOperationBuilder;
setSignature: (bytes: BytesLike) => IUserOperationBuilder;
setPartial: (partialOp: Partial<IUserOperation>) => IUserOperationBuilder;
// Sets the default values that won't be wiped on reset.
useDefaults: (partialOp: Partial<IUserOperation>) => IUserOperationBuilder;
resetDefaults: () => IUserOperationBuilder;
// Some fields may require arbitrary logic to build an op.
// Middleware functions allow you to set custom logic for building op fragments.
useMiddleware: (fn: UserOperationMiddlewareFn) => IUserOperationBuilder;
resetMiddleware: () => IUserOperationBuilder;
// This will construct a UserOperation that can be sent to a client.
// It will run through your entire middleware stack in the process.
buildOp: (
entryPoint: string,
chainId: BigNumberish
) => Promise<IUserOperation>;
// Will reset all fields back to default value.
resetOp: () => IUserOperationBuilder;
}
Usage
import { UserOperationBuilder } from "userop";
const builder = new UserOperationBuilder().useDefaults({ sender });
Using Defaults
The
useDefaults
method will set fields that will persist after callingresetOp
. This could be for fields likesender
which you don't expect to change across different operations.
Building a UserOperation
A UserOperation is built using the buildOp
method once it is properly configured. The easiest way to avoid passing around entryPoint and chainID values is to use the client as a director.
// If you only want to build.
const userOp = await client.buildUserOperation(builder);
// If you want to build and send.
const result = await client.sendUserOperation(builder);
Note
Using the above methods on a
client
to direct abuilder
will also callresetOp
if successful.
Alternatively, if you want to control the build process:
// Build op with the middleware stack.
let userOp = await builder.buildOp(entryPoint, chainId);
// Or get the latest built op. Will not use the middleware stack.
let userOp = await builder.getOp();
// Reset op back to default values when you're done.
builder.resetOp();
get
and set
Functions
get
and set
FunctionsThese are basic getters and setters for all fields on a UserOperation. Getters return the field type whereas setters will return the instance to enable chaining.
For example:
const builder = new UserOperationBuilder()
.setCallData(callData)
.setCallGasLimit(callGas);
Careful when setting gas values
Avoid setting hard-coded gas prices in your user operations. We recommend using the built-in gas price middleware to set gas prices or a gas price oracle.
Middleware Functions
Some fragments on a UserOperation may depend on custom logic in order to be built. For example, based on your Smart Account, there might be a specific ways to sign an operation which aren't specified in the standard.
For such cases we can set custom middleware functions. During buildOp
, a middleware will be called in the order they are set. Here is a example of middleware functions you might have in your application:
const resolveAccount = async (ctx) => {
// Fetch the latest nonce and initCode if required.
ctx.op.nonce = nonce;
ctx.op.initCode = initCode;
};
const fetchGasPrice = async (ctx) => {
// Fetch the latest gas prices.
ctx.op.maxFeePerGas = maxFeePerGas;
ctx.op.maxFeePerGas = maxPriorityFeePerGas;
};
const verifyingPaymaster = async (ctx) => {
// Request gas sponsorship from a paymaster provider.
ctx.op.paymasterAndData = paymasterAndData;
ctx.op.preVerificationGas = preVerificationGas;
ctx.op.verificationGasLimit = verificationGasLimit;
ctx.op.callGasLimit = callGasLimit;
};
const signUserOperation = async (ctx) => {
// Use the required signature scheme based on your wallet.
// ctx.getRequestId() will generate the required hash for verification.
// Multisig, ECDSA, etc.
ctx.op.signature = signature;
};
const builder = new UserOperationBuilder()
.useMiddleware(resolveAccount)
.useMiddleware(fetchGasPrice)
.useMiddleware(verifyingPaymaster)
.useMiddleware(signUserOperation);
Looking for a concrete example?
Checkout the SimpleAccount preset based on SimpleAccount.sol.
Updated about 1 year ago