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. The following API changes are likely to break existing paymaster code:
CandidePaymaster.createSponsorPaymasterUserOperation(...) now takes smartAccount as the first argument: (smartAccount, userOp, bundlerRpc, sponsorshipPolicyId?, context?, overrides?).
CandidePaymaster.createTokenPaymasterUserOperation(...) adds a dedicated context? argument before overrides?: (smartAccount, userOp, tokenAddress, bundlerRpc, context?, overrides?). Callers that previously passed overrides positionally at argument 5 must insert undefined (or an explicit context) so overrides shifts to argument 6.
See CHANGELOG.md for the full list of new features, renames, type export changes, and fixes.
Quickstart
Which account class 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).
const rpc = "https://api.candide.dev/api/v3/11155111/YOUR_API_KEY";
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;
const smartAccount = SafeAccountV0_3_0.initializeNewAccount([ownerPublicAddress]);
console.log("Account address:", smartAccount.accountAddress);
const userOp = await smartAccount.createUserOperation(
[{ to: "0xRecipient", value: 1000000000000000n, data: "0x" }],
nodeRpc,
bundlerRpc,
);
userOp.signature = smartAccount.signUserOperation(userOp, [ownerPrivateKey], chainId);
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";
const transferSelector = getFunctionSelector("transfer(address,uint256)");
const transferCallData = createCallData(
transferSelector,
["address", "uint256"],
["0xRecipient", 1000000n],
);
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");
const userOp = await smartAccount.createUserOperation(
[{ to: "0xRecipient", value: 1000000000000000n, data: "0x" }],
nodeRpc,
bundlerRpc,
);
const { userOperation: sponsoredOp, sponsorMetadata } = await paymaster.createSponsorPaymasterUserOperation(
smartAccount,
userOp,
bundlerRpc,
sponsorshipPolicyId,
);
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";
const userOp = await smartAccount.createUserOperation(
[{ to: "0xRecipient", value: 0n, data: "0x" }],
nodeRpc,
bundlerRpc,
);
const { userOperation: tokenOp, tokenQuote } = await paymaster.createTokenPaymasterUserOperation(
smartAccount,
userOp,
gasTokenAddress,
bundlerRpc,
);
tokenOp.signature = smartAccount.signUserOperation(tokenOp, [ownerPrivateKey], chainId);
const response = await smartAccount.sendUserOperation(tokenOp, bundlerRpc);
CandidePaymasterContext is passed as its own argument, separate from gas overrides.
const { userOperation: sponsoredOp } = await paymaster.createSponsorPaymasterUserOperation(
smartAccount,
userOp,
bundlerRpc,
sponsorshipPolicyId,
{
},
{
callGasLimitPercentageMultiplier: 110,
},
);
Batch multiple transactions
import { SafeAccountV0_3_0, MetaTransaction } from "abstractionkit";
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,
nodeRpc,
bundlerRpc,
);
Connect to an existing (deployed) account
import { SafeAccountV0_3_0 } from "abstractionkit";
const smartAccount = new SafeAccountV0_3_0("0xYourDeployedSafeAddress");
const newAccount = SafeAccountV0_3_0.initializeNewAccount(["0xOwnerAddress"]);
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;
const account = new Calibur7702Account(eoaAddress);
const userOp = await account.createUserOperation(
[{ to: "0xRecipient", value: 1000000000000000n, data: "0x" }],
nodeRpc,
bundlerRpc,
{ eip7702Auth: { chainId } },
);
userOp.eip7702Auth = createAndSignEip7702DelegationAuthorization(
BigInt(userOp.eip7702Auth.chainId),
userOp.eip7702Auth.address,
BigInt(userOp.eip7702Auth.nonce),
privateKey,
);
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";
const webAuthnKey = Calibur7702Account.createWebAuthnP256Key(pubKeyX, pubKeyY);
const keyHash = Calibur7702Account.getKeyHash(webAuthnKey);
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";
const dummySig = Calibur7702Account.createDummyWebAuthnSignature(keyHash);
const userOp = await account.createUserOperation(
[{ to: "0xRecipient", value: 0n, data: "0x" }],
nodeRpc,
bundlerRpc,
{ dummySignature: dummySig },
);
const userOpHash = createUserOperationHash(userOp, entryPointAddress, chainId);
userOp.signature = account.formatWebAuthnSignature(keyHash, {
authenticatorData,
clientDataJSON,
challengeIndex,
typeIndex,
r,
s,
});
const response = await account.sendUserOperation(userOp, bundlerRpc);
Common error codes and solutions
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
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