UNPKG

abstractionkit

Version:

Account Abstraction 4337 SDK by Candidelabs

391 lines (295 loc) 15.4 kB
<!-- PROJECT LOGO --> <div align="center"> <img src="https://github.com/user-attachments/assets/ad202256-d3c2-40d3-ac70-c458f9ab0c1c"> </div> 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 <a href="https://github.com/candidelabs/abstractionkit-examples">Abstractionkit Example Projects</a> ## 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](https://docs.candide.dev/wallet/abstractionkit/introduction). ## Installation Requires Node.js 18 or later. ```bash 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](./CHANGELOG.md) for the full list of new features, renames, type export changes, and fixes. ## Quickstart ### Which account class to use? Default EntryPoint is class-specific. Override `entrypointAddress` only when your account/module deployment matches that EntryPoint. | Class | Default 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](https://dashboard.candide.dev), or use the public endpoint (rate-limited, no key required). ```typescript // 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 ```typescript 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 ```typescript 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 ```typescript 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 { userOperation: sponsoredOp, sponsorMetadata } = await paymaster.createSponsorPaymasterUserOperation( smartAccount, userOp, bundlerRpc, sponsorshipPolicyId, // context (optional — e.g. { signingPhase: "commit" } for EP v0.9 parallel signing) // overrides (optional — gas limits and multipliers) ); // 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 ```typescript 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. // `tokenQuote` carries the exchange rate and max token cost used for the approval. const { userOperation: tokenOp, tokenQuote } = await paymaster.createTokenPaymasterUserOperation( smartAccount, userOp, gasTokenAddress, bundlerRpc, // context (optional) // overrides (optional — gas limits, multipliers, resetApproval) ); tokenOp.signature = smartAccount.signUserOperation(tokenOp, [ownerPrivateKey], chainId); const response = await smartAccount.sendUserOperation(tokenOp, bundlerRpc); ``` ### Pass paymaster context (sponsorship policy, parallel signing) `CandidePaymasterContext` is passed as its own argument, separate from gas overrides. ```typescript const { userOperation: sponsoredOp } = await paymaster.createSponsorPaymasterUserOperation( smartAccount, userOp, bundlerRpc, sponsorshipPolicyId, { // For EntryPoint v0.9 parallel signing flows: // signingPhase: "commit" | "finalize", }, { // gas overrides: callGasLimitPercentageMultiplier: 110, }, ); ``` ### Batch multiple transactions ```typescript 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 ```typescript 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 ``` When you have a stored Safe address and need to pick between the two paths at runtime, use `isDeployed` to avoid emitting redundant factory data (and the `AA10 sender already constructed` error) on subsequent UserOps: ```typescript import { SafeAccountV0_3_0 } from "abstractionkit"; const smartAccount = (await SafeAccountV0_3_0.isDeployed(safeAddress, nodeRpc)) ? new SafeAccountV0_3_0(safeAddress) : SafeAccountV0_3_0.initializeNewAccount(owners); ``` `isDeployed` only checks for bytecode at the address. It does not verify that the deployed code is a Safe or that its on-chain owners match a given set. If your owners may have changed, compare `createAccountAddress(owners)` against the stored address yourself before calling `initializeNewAccount`. ### 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. ```typescript 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 `JsonRpcNode.from(nodeRpc).getDelegatedAddress(eoaAddress)` (import `JsonRpcNode` from `"abstractionkit"`) to check delegation status; it returns the delegatee address, or `null` when the EOA has no delegation. ### Calibur 7702: register a WebAuthn passkey ```typescript 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 ```typescript 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](https://docs.candide.dev/wallet/guides/getting-started) | Learn how to create a smart wallet and to send your first user operation | | [Send a Gasless Transaction](https://docs.candide.dev/wallet/guides/send-gasless-tx) | Learn how to send gasless transactions using a paymaster | | [Pay Gas in ERC-20](https://docs.candide.dev/wallet/guides/pay-gas-in-erc20) | 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](https://claude.ai/code), you can import this README into your project's CLAUDE.md for better AI assistance: ```markdown @node_modules/abstractionkit/README.md ``` ## npm package <a href="https://www.npmjs.com/package/abstractionkit">npm</a> <!-- LICENSE --> ## License MIT <!-- ACKNOWLEDGMENTS --> ## Acknowledgments * <a href='https://eips.ethereum.org/EIPS/eip-4337'>EIP-4337: Account Abstraction via Entry Point Contract specification </a> * <a href='https://safe.global/'>Safe Accounts, Modules, and SGP</a> * <a href='https://github.com/Uniswap/calibur'>Uniswap Calibur Account</a>