Skip to content

candidelabs/abstractionkit

Repository files navigation

A Typescript Library to easily build standard Ethereum Smart Wallets, with first class support for Safe Accounts.

AbstractionKit is agnostic of:

  • Ethereum interface libraries: ethers, web3.js, viem/wagmi
  • Bundlers: Plug and play a Bundler URL from any provider, or self-host your own
  • Paymasters: Candide Paymaster is supported, but you can use any 3rd party paymaster to sponsor gas
  • Accounts: The Safe Account is first class supported, but you can use Bundlers and Paymasters with any account

Examples

Abstractionkit Example Projects

Features

  • Safe Accounts with passkey authentication, social recovery, multisig, and allowance management
  • EIP-7702 support via Calibur7702Account and Simple7702Account
  • Gas abstraction with sponsored UserOperations and ERC-20 gas payment via CandidePaymaster
  • Multichain signatures via SafeMultiChainSigAccountV1 (sign once, replay across chains)
  • Bundler client compatible with standard ERC-4337 methods
  • EntryPoint v0.6, v0.7, v0.8, and v0.9 support with a version-safe account/UserOp mapping

Docs

For full detailed documentation visit our docs page.

Installation

Requires Node.js 18 or later.

npm install abstractionkit

Upgrading to v0.3.0

v0.3.0 is a major release. Two API changes are likely to break existing paymaster code:

  • CandidePaymaster.createSponsorPaymasterUserOperation(...) now takes smartAccount as the first argument: (smartAccount, userOp, bundlerRpc, sponsorshipPolicyId?, overrides?).
  • CandidePaymasterContext is no longer a separate argument. Pass it via overrides.context on GasPaymasterUserOperationOverrides.

See CHANGELOG.md for the full list of new features, renames, type export changes, and fixes.

Quickstart

Which account class to use?

Class EntryPoint Account Type When to use
SafeAccountV0_3_0 EP v0.7 Safe (counterfactual) Recommended for most new projects
SafeAccountV1_5_0_M_0_3_0 EP v0.7 Safe v1.5.0 (counterfactual) Safe v1.5.0 with EIP-7951 / Daimo P256 verifier for WebAuthn
SafeAccountV0_2_0 EP v0.6 Safe (counterfactual) Legacy support for EntryPoint v0.6
SafeMultiChainSigAccountV1 EP v0.9 Safe multichain Sign once, replay across chains.
Calibur7702Account EP v0.8 EIP-7702 (Uniswap Calibur) Upgrade an EOA in place. Supports EOA, P256, and WebAuthn keys
Simple7702Account EP v0.8 EIP-7702 (minimal) Minimal reference EIP-7702 account
Simple7702AccountV09 EP v0.9 EIP-7702 (minimal, parallel paymaster) EntryPoint v0.9 with parallel paymaster signing

Endpoints

Candide hosts both bundler and paymaster under the same base URL. Get an API key from the dashboard, or use the public endpoint (rate-limited, no key required).

// Authenticated
const rpc = "https://api.candide.dev/api/v3/11155111/YOUR_API_KEY";

// Or public (no key required)
// const rpc = "https://api.candide.dev/public/v3/11155111";

Recipes

Copy paste patterns for common tasks. Examples use SafeAccountV0_3_0 (EntryPoint v0.7). For EntryPoint v0.6, replace with SafeAccountV0_2_0.

Send ETH from a new Safe account

import { SafeAccountV0_3_0 } from "abstractionkit";

const ownerPublicAddress = "0xOwner";
const ownerPrivateKey = "0xPrivateKey";
const nodeRpc = "https://rpc.example.com";
const bundlerRpc = "https://api.candide.dev/api/v3/11155111/YOUR_API_KEY";
const chainId = 11155111n; // Sepolia

// Initialize new account (deploys on first UserOp)
const smartAccount = SafeAccountV0_3_0.initializeNewAccount([ownerPublicAddress]);
console.log("Account address:", smartAccount.accountAddress);
// Fund this address with ETH before sending the first UserOp

// Create UserOp
const userOp = await smartAccount.createUserOperation(
  [{ to: "0xRecipient", value: 1000000000000000n, data: "0x" }], // 0.001 ETH
  nodeRpc,
  bundlerRpc,
);

// Sign
userOp.signature = smartAccount.signUserOperation(userOp, [ownerPrivateKey], chainId);

// Send and wait for receipt
const response = await smartAccount.sendUserOperation(userOp, bundlerRpc);
const receipt = await response.included();
console.log("Tx hash:", receipt?.receipt.transactionHash);

Send an ERC-20 token transfer

import { SafeAccountV0_3_0, createCallData, getFunctionSelector } from "abstractionkit";

// Encode ERC-20 transfer(address,uint256)
const transferSelector = getFunctionSelector("transfer(address,uint256)");
const transferCallData = createCallData(
  transferSelector,
  ["address", "uint256"],
  ["0xRecipient", 1000000n], // amount in token's smallest unit
);

const userOp = await smartAccount.createUserOperation(
  [{ to: "0xTokenContractAddress", value: 0n, data: transferCallData }],
  nodeRpc,
  bundlerRpc,
);

Sponsor gas with CandidePaymaster

import { SafeAccountV0_3_0, CandidePaymaster } from "abstractionkit";

const paymaster = new CandidePaymaster("https://api.candide.dev/api/v3/11155111/YOUR_API_KEY");

// Create the UserOp first (without paymaster)
const userOp = await smartAccount.createUserOperation(
  [{ to: "0xRecipient", value: 1000000000000000n, data: "0x" }],
  nodeRpc,
  bundlerRpc,
);

// Sponsor it. Sets paymaster fields and re-estimates gas.
// Note: as of v0.3.0, smartAccount is the first argument.
const [sponsoredOp] = await paymaster.createSponsorPaymasterUserOperation(
  smartAccount,
  userOp,
  bundlerRpc,
  sponsorshipPolicyId,
  // overrides (optional, includes context for parallel signing)
);

// Sign and send as usual
sponsoredOp.signature = smartAccount.signUserOperation(sponsoredOp, [ownerPrivateKey], chainId);
const response = await smartAccount.sendUserOperation(sponsoredOp, bundlerRpc);

Pay gas with ERC-20 tokens

import { SafeAccountV0_3_0, CandidePaymaster } from "abstractionkit";

const paymaster = new CandidePaymaster("https://api.candide.dev/api/v3/11155111/YOUR_API_KEY");
const gasTokenAddress = "0xERC20TokenAddress"; // must be supported by paymaster

const userOp = await smartAccount.createUserOperation(
  [{ to: "0xRecipient", value: 0n, data: "0x" }],
  nodeRpc,
  bundlerRpc,
);

// Automatically prepends token approval + sets paymaster fields.
// For tokens like USDT that require resetting allowance to 0 first, pass
// { resetApproval: true } in the overrides.
const tokenOp = await paymaster.createTokenPaymasterUserOperation(
  smartAccount,
  userOp,
  gasTokenAddress,
  bundlerRpc,
  // overrides (optional)
);

tokenOp.signature = smartAccount.signUserOperation(tokenOp, [ownerPrivateKey], chainId);
const response = await smartAccount.sendUserOperation(tokenOp, bundlerRpc);

Pass paymaster context (sponsorship policy, parallel signing)

As of v0.3.0, CandidePaymasterContext is passed via the overrides.context field on GasPaymasterUserOperationOverrides. Previously it was a separate top level argument.

const [sponsoredOp] = await paymaster.createSponsorPaymasterUserOperation(
  smartAccount,
  userOp,
  bundlerRpc,
  sponsorshipPolicyId,
  {
    context: {
      // For EntryPoint v0.9 parallel signing flows:
      // signingPhase: "commit" | "finalize",
    },
    // gas overrides also live here:
    callGasLimitPercentageMultiplier: 110,
  },
);

Batch multiple transactions

import { SafeAccountV0_3_0, MetaTransaction } from "abstractionkit";

// Pass an array of MetaTransactions. Automatically encoded via MultiSend.
const transactions: MetaTransaction[] = [
  { to: "0xRecipientA", value: 1000000000000000n, data: "0x" },
  { to: "0xRecipientB", value: 2000000000000000n, data: "0x" },
  { to: "0xTokenContract", value: 0n, data: transferCallData },
];

const userOp = await smartAccount.createUserOperation(
  transactions, // automatically batched via MultiSend when length > 1
  nodeRpc,
  bundlerRpc,
);

Connect to an existing (deployed) account

import { SafeAccountV0_3_0 } from "abstractionkit";

// Use the constructor for an already-deployed account
const smartAccount = new SafeAccountV0_3_0("0xYourDeployedSafeAddress");

// vs. initializeNewAccount which sets factory data for first-time deployment
const newAccount = SafeAccountV0_3_0.initializeNewAccount(["0xOwnerAddress"]);
// newAccount.accountAddress is the counterfactual address
// First UserOp will deploy it automatically

Calibur 7702: delegate an EOA and send a transfer

Calibur7702Account is Uniswap's EIP-7702 smart account. It upgrades a regular EOA in place so the same address becomes a programmable smart account on EntryPoint v0.8.

import {
  Calibur7702Account,
  createAndSignEip7702DelegationAuthorization,
} from "abstractionkit";

const eoaAddress = "0xYourEOA";
const privateKey = "0xYourPrivateKey";
const nodeRpc = "https://rpc.example.com";
const bundlerRpc = "https://api.candide.dev/api/v3/11155111/YOUR_API_KEY";
const chainId = 11155111n;

// The EOA address becomes the smart account address after delegation.
const account = new Calibur7702Account(eoaAddress);

// Create UserOp with EIP-7702 delegation (only required the first time).
const userOp = await account.createUserOperation(
  [{ to: "0xRecipient", value: 1000000000000000n, data: "0x" }],
  nodeRpc,
  bundlerRpc,
  { eip7702Auth: { chainId } },
);

// Sign the delegation authorization.
userOp.eip7702Auth = createAndSignEip7702DelegationAuthorization(
  BigInt(userOp.eip7702Auth.chainId),
  userOp.eip7702Auth.address,
  BigInt(userOp.eip7702Auth.nonce),
  privateKey,
);

// Sign and send.
userOp.signature = account.signUserOperation(userOp, privateKey, chainId);
const response = await account.sendUserOperation(userOp, bundlerRpc);
const receipt = await response.included();

After the first UserOp deploys the delegation, subsequent UserOps no longer need eip7702Auth. Use getDelegatedAddress(eoaAddress, nodeRpc) to check delegation status.

Calibur 7702: register a WebAuthn passkey

import { Calibur7702Account } from "abstractionkit";

// Build a P256 key from the WebAuthn public key coordinates.
const webAuthnKey = Calibur7702Account.createWebAuthnP256Key(pubKeyX, pubKeyY);
const keyHash = Calibur7702Account.getKeyHash(webAuthnKey);

// Register with a 1-year expiration.
const registerTxs = Calibur7702Account.createRegisterKeyMetaTransactions(
  webAuthnKey,
  { expiration: Math.floor(Date.now() / 1000) + 86400 * 365 },
);

const userOp = await account.createUserOperation(registerTxs, nodeRpc, bundlerRpc);
userOp.signature = account.signUserOperation(userOp, privateKey, chainId);
const response = await account.sendUserOperation(userOp, bundlerRpc);

Calibur 7702: sign a UserOp with a registered passkey

import { Calibur7702Account, createUserOperationHash } from "abstractionkit";

// Use a WebAuthn dummy signature for accurate gas estimation.
const dummySig = Calibur7702Account.createDummyWebAuthnSignature(keyHash);

const userOp = await account.createUserOperation(
  [{ to: "0xRecipient", value: 0n, data: "0x" }],
  nodeRpc,
  bundlerRpc,
  { dummySignature: dummySig },
);

// Compute the hash, sign with the passkey off-chain, then format the signature.
const userOpHash = createUserOperationHash(userOp, entryPointAddress, chainId);
userOp.signature = account.formatWebAuthnSignature(keyHash, {
  authenticatorData,
  clientDataJSON,
  challengeIndex,
  typeIndex,
  r,
  s, // P256 signature components
});

const response = await account.sendUserOperation(userOp, bundlerRpc);

Common error codes and solutions

Error Code Meaning Fix
AA10 Sender already constructed (initCode not needed) Use new SafeAccountV0_3_0(address) instead of initializeNewAccount for deployed accounts
AA21 Didn't pay prefund Fund the sender address with enough ETH to cover gas, or use a paymaster
AA25 Nonce mismatch Don't override nonce, or fetch latest via fetchAccountNonce()
AA40 Paymaster deposit too low Contact paymaster provider or use a different paymaster
AA41 Paymaster postOp reverted Check paymaster-specific requirements (token balance, approval amount)

Guides

Title Description
Send your first user operation Learn how to create a smart wallet and to send your first user operation
Send a Gasless Transaction Learn how to send gasless transactions using a paymaster
Pay Gas in ERC-20 Learn how to offer the ability for users to pay gas in ERC-20s using a Paymaster

AI Agent Integration

If you use Claude Code, you can import this README into your project's CLAUDE.md for better AI assistance:

@node_modules/abstractionkit/README.md

npm package

npm

License

MIT

Acknowledgments

About

A Typescript library to build Account Abstraction Smart Wallets

Topics

Resources

License

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors