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
Abstractionkit Example Projects
- Safe Accounts with passkey authentication, social recovery, multisig, and allowance management
- EIP-7702 support via
Calibur7702AccountandSimple7702Account - 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
For full detailed documentation visit our docs page.
Requires Node.js 18 or later.
npm install abstractionkitv0.3.0 is a major release. Two API changes are likely to break existing paymaster code:
CandidePaymaster.createSponsorPaymasterUserOperation(...)now takessmartAccountas the first argument:(smartAccount, userOp, bundlerRpc, sponsorshipPolicyId?, overrides?).CandidePaymasterContextis no longer a separate argument. Pass it viaoverrides.contextonGasPaymasterUserOperationOverrides.
See CHANGELOG.md for the full list of new features, renames, type export changes, and fixes.
| 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 |
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";Copy paste patterns for common tasks. Examples use SafeAccountV0_3_0 (EntryPoint v0.7). For EntryPoint v0.6, replace with SafeAccountV0_2_0.
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);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,
);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);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);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,
},
);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,
);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 automaticallyCalibur7702Account 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.
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);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);| 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) |
| 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 |
If you use Claude Code, you can import this README into your project's CLAUDE.md for better AI assistance:
@node_modules/abstractionkit/README.mdMIT
