permissionless.js: ERC-4337 Account Abstraction Utilities
permissionless.js is a robust, TypeScript-first utility library designed to simplify interaction with Ethereum's ERC-4337 Account Abstraction standard. It provides a comprehensive set of client interfaces and helper functions for integrating with ERC-4337 Bundlers and Paymasters, as well as tools for creating and managing Smart Accounts. This enables developers to build dApps with advanced features like gasless transactions, multi-signature accounts, and custom validation logic. The library leverages `viem` for core Ethereum interactions and `ox` for cryptographic operations, offering a type-safe and modular approach to account abstraction development. The current stable version is 0.3.5, with frequent patch releases addressing bug fixes and minor feature enhancements, alongside occasional minor releases for larger feature sets or dependency upgrades.
Common errors
-
Error: The client must be configured with a chain. Ensure that `chain` is not `undefined`.
cause Attempting to create a `permissionless` client (e.g., `createBundlerClient`) without providing a valid `chain` object from `viem/chains`.fixWhen initializing `createPublicClient` or `createBundlerClient`, `createSmartAccountClient`, ensure a `chain` object (e.g., `optimismSepolia`) is passed in the configuration. -
Error: Invalid user operation signature
cause The `UserOperation` was signed incorrectly, or the `signature` field does not match the expected format or validation logic of the smart account.fixVerify that the private key or signer used for signing is correct, the `UserOperation` fields were not mutated after signing, and that the smart account's `validateUserOp` logic on-chain is correctly implemented. -
Error: Paymaster returned an invalid response.
cause The connected Paymaster service responded with an error or an incorrectly formatted object during the `sponsorUserOperation` call, often due to insufficient funds, an invalid `UserOperation`, or rate limiting.fixCheck the Paymaster's specific API documentation for error codes. Ensure the `UserOperation` is valid and that your Paymaster account has sufficient balance or allowance for the transaction. Also, verify your API key and endpoint URL. -
Error: Could not estimate gas for UserOperation
cause The bundler failed to estimate gas for the `UserOperation`, often indicating a revert on execution, an invalid `callData`, or an issue with the gas parameters.fixInspect the `callData` for correctness. Use a debugger (if available for your bundler) or manually trace the `UserOperation` execution to identify the revert reason. Ensure `sender` account has enough ETH for the `preVerificationGas` or that the paymaster is properly sponsoring.
Warnings
- breaking Version 0.3.0 introduced a critical fix for a vulnerability in previous Kernel validator versions. Users on older Kernel-based smart accounts should upgrade immediately to patch 'unsafe to use' functionality.
- gotcha `permissionless` has `viem` and `ox` as peer dependencies. Ensure you install compatible versions as specified in `package.json` to avoid runtime errors or type mismatches. Regular updates to `permissionless` often include peer dependency version bumps (e.g., v0.3.4 updated `viem` to `^2.44.4` and `ox` to `^0.11.3`).
- gotcha The `ox` library, a dependency for cryptographic operations, is dynamically loaded. In certain environments or bundler configurations, this might lead to 'Cannot find module 'ox'' errors if not properly handled by your build system.
- gotcha The `entryPoint` address is a critical configuration parameter for all ERC-4337 clients (`BundlerClient`, `PaymasterClient`, `SmartAccountClient`). Using an incorrect or outdated entryPoint address will lead to transaction failures.
Install
-
npm install permissionless -
yarn add permissionless -
pnpm add permissionless
Imports
- createSmartAccountClient
const createSmartAccountClient = require('permissionless');import { createSmartAccountClient } from 'permissionless'; - createBundlerClient
import createBundlerClient from 'permissionless/clients/createBundlerClient';
import { createBundlerClient } from 'permissionless'; - UserOperation
import { UserOperation } from 'permissionless/types';import type { UserOperation } from 'permissionless/types';
Quickstart
import { createPublicClient, http, privateKeyToAccount, Address } from 'viem';
import { optimismSepolia } from 'viem/chains';
import { createBundlerClient, createSmartAccountClient, createPimlicoPaymasterClient, ENTRYPOINT_ADDRESS_V07, signerToSimpleSmartAccount } from 'permissionless';
import { type UserOperation } from 'permissionless/types';
const privateKey = (process.env.PRIVATE_KEY as `0x${string}`) || '0x...'; // Replace with your private key
const pimlicoRpcUrl = process.env.PIMLICO_RPC_URL || 'https://api.pimlico.io/v2/optimism-sepolia/rpc?apikey=YOUR_PIMLICO_API_KEY';
const chain = optimismSepolia;
const publicClient = createPublicClient({
chain,
transport: http(chain.rpcUrls.default.http[0]),
});
const bundlerClient = createBundlerClient({
chain,
transport: http(pimlicoRpcUrl),
entryPoint: ENTRYPOINT_ADDRESS_V07
});
const paymasterClient = createPimlicoPaymasterClient({
chain,
transport: http(pimlicoRpcUrl),
entryPoint: ENTRYPOINT_ADDRESS_V07
});
async function sendSimpleUserOperation() {
const signer = privateKeyToAccount(privateKey);
const simpleSmartAccountClient = await signerToSimpleSmartAccount(publicClient, {
signer,
entryPoint: ENTRYPOINT_ADDRESS_V07,
factoryAddress: '0x9406Cc6185a346906296840746125a0E447645E4' // Example SimpleAccountFactory
});
const smartAccountClient = createSmartAccountClient({
account: simpleSmartAccountClient,
chain,
transport: http(pimlicoRpcUrl),
entryPoint: ENTRYPOINT_ADDRESS_V07
});
console.log(`Smart account address: ${smartAccountClient.account.address}`);
const userOperation = await smartAccountClient.prepareUserOperation({
userOperation: {
callData: await smartAccountClient.account.encodeCallData({
to: smartAccountClient.account.address,
value: 0n,
data: '0x'
})
},
sponsorUserOperation: async ({ userOperation }) => {
return paymasterClient.sponsorUserOperation({
userOperation,
entryPoint: ENTRYPOINT_ADDRESS_V07
});
}
});
// For demonstration, not actually sending a transaction
console.log('Prepared UserOperation:', userOperation);
// To send, uncomment:
// const userOpHash = await smartAccountClient.sendUserOperation({
// userOperation: userOperation,
// entryPoint: ENTRYPOINT_ADDRESS_V07
// });
// console.log(`UserOperation sent, hash: ${userOpHash}`);
// const receipt = await bundlerClient.waitForUserOperationReceipt({ hash: userOpHash });
// console.log('UserOperation receipt:', receipt);
}
sendSimpleUserOperation().catch(console.error);