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 calling resetOp. This could be for fields like sender 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 a builder will also call resetOp 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

These 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.