Quickstart

A quick technical guide for getting started with userop.js

This guide provides a quick walkthrough for integrating userop.js and explanation of high level concepts.

Install

The userop.js package is hosted on npm and can be installed using one of the common package managers.

npm install userop
pnpm install userop
yarn add userop

πŸ“˜

Supported viem and ethers.js versions

Userop.js has been tested against the following versions of viem and ethers.js. Using a lower version in your application may result in type errors and other incompatibilities.

  • viem: ^2.9.12
  • ethers.js: ^6.11.1

Sending a UserOperation

Below is a full code snippet for sending ETH from your Smart Account to another address with the following configurations.

  • Using v0.6 of the ERC-4337 EntryPoint
  • SimpleAccount implementation
  • viem PublicClient or ethers JsonRpcProvider
  • EOA owner using a viem WalletClient or ethers Signer

πŸ“˜

Looking to get your hands on some code samples?

Checkout the ERC-4337 examples repository for a collection of userop.js related use cases.

import { createPublicClient, createWalletClient } from "viem"; 
import { V06 } from "userop";

// 1. Initialize clients to eth node and wallet.
const ethClient = createPublicClient({
  // See https://viem.sh/docs/clients/public
});

const walletClient = createWalletClient({
  // See https://viem.sh/docs/clients/wallet
});

// 2. Initialize client to smart account.
const account = new V06.Account.Instance({
  ...V06.Account.Common.SimpleAccount.base(ethClient, walletClient),
})

// 3. Initiate an ETH transfer from your smart account.
const send = await account
  .encodeCallData("execute", [TO_ADDRESS, VALUE, "0x"])
  .sendUserOperation();

// 4. Wait for UserOperation to be included onchain.
const receipt = await send.wait();
import { JsonRpcProvider, BaseWallet } from "ethers"; 
import { V06 } from "userop";

// 1. Initialize clients to eth node and wallet.
const ethClient = new JsonRpcProvider(
  // See https://docs.ethers.org/v6/api/providers/jsonrpc
);

const walletClient = BaseWallet(
  // See https://docs.ethers.org/v6/api/wallet
);

// 2. Initialize client to smart account.
const account = new V06.Account.Instance({
  ...V06.Account.CommonConfigs.SimpleAccount.base(
    ethClient,
    walletClient,
  ),
})

// 3. Initiate an ETH transfer from your smart account.
const send = await acc
	.encodeCallData("execute", [TO_ADDRESS, VALUE, "0x"])
	.sendUserOperation();

// 4. Wait for UserOperation to be included onchain.
const receipt = await send.wait();

V06 namespace

In the code snippet above, all userop.js modules are organised under the root namespace V06. This simply represents v0.6 of the ERC-4337 EntryPoint. Similarly, future protocol versions will have its own root namespace (e.g. V07 for v0.7).

Account instance

The core component of userop.js is the Account instance which represents an interface to a user's Smart Account. The Account instance is compatible with both viem and ethers.js and used to interact with all the various ERC-4337 components.

At its core, an Account can also be configured to support any arbitrary combination of node, bundler, account, and paymaster. In the above code snippet we've used a helper function in the Common Configs module, to create a minimum configuration for SimpleAccount. Under the hood this function returns an object of the following type.

export interface AccountOpts<A extends Abi, F extends Abi> {
  // Required global values
  accountAbi: A;
  factoryAbi: F;
  factoryAddress: Address;
  ethClient: PublicClient | JsonRpcProvider;

  // Optional global values
  bundlerClient?: PublicClient | JsonRpcProvider;
  entryPointAddress?: Address;
  salt?: bigint;
  waitTimeoutMs?: number;
  waitIntervalMs?: number;

  // Required hook methods
  setFactoryData: Hooks.SetFactoryDataFunc<F>;
  requestSignature: Hooks.RequestSignatureFunc;

  // Optional hook methods
  requestGasPrice?: Hooks.RequestGasPriceFunc;
  requestGasValues?: Hooks.RequestGasValuesFunc;
  requestPaymaster?: Hooks.RequestPaymasterFunc;
  onBuild?: Hooks.OnBuildFunc;
}

It's useful to understand how Account Options work if you intend on extending an account's configuration beyond what's available in Common Configs. We'll go into details in later sections, but the easiest example of this would be extending the base SimpleAccount config to add a Stackup paymaster.

const account = new V06.Account.Instance({
  ...V06.Account.Common.SimpleAccount.base(
    ethClient,
    walletClient,
  ),

  requestPaymaster: V06.Account.Hooks.RequestPaymaster.withCommon({
    variant: "stackupV1",
    parameters: { rpcUrl: STACKUP_V1_PM_RPC, type: "payg" },
  }),
})

You can also create AccountOpts for any ERC-4337 compliant implementation that isn't just SimpleAccount. By providing the relevant ABIs, addresses, and hook functions, the Account instance will automatically know how to build the correct User Operation while providing intelligent typing.

Account hooks

Regardless of the account implementation used, most User Operations follow a similar build lifecycle.

  1. Generate the sender address
  2. Set the initCode if applicable
  3. Resolve nonce
  4. Fetch the gas prices
  5. Estimate the gas values
  6. Request paymaster data if applicable
  7. Sign the final userOpHash
  8. Send the User Operation

Throughout the build lifecycle, an Account instance will call into various hook functions to fill in the required fields on a User Operation. For V06, these hooks include:

  • setFactoryData
  • requestSignature
  • requestGasPrice
  • requestGasValues
  • requestPaymaster
  • onBuild

For details on each hook function see Account Options. As long as it follows the defined interface, these functions can be implemented in any arbitrary way to return values suited for a particular Smart Account implementation. For example, the requestSignature for SimpleAccount would return a signature of the userOpHash that was signed by the account's EOA owner.