SafeAccountV1_5_0_M_0_3_0.initializeNewAccountreturns the right runtime type. The subclass factory delegated toSafeAccountV0_3_0.initializeNewAccount, which hard-codednew SafeAccountV0_3_0(...), so the returned instance failedinstanceof SafeAccountV1_5_0_M_0_3_0and lost subclass behavior. The base factory now instantiates polymorphically throughnew this(...), so any subclass (including consumer-defined ones) gets its own type back. Detached calls (the factory extracted into a bare function, where strict-modethisis undefined) fall back to constructing the base class, preserving the previous behavior for plain-JS callers. (#116)- Legacy transaction
vprecision for chain IDs above 2^53.createAndSignLegacyRawTransactioncomputed the EIP-155vfield viaNumber(chainId), silently rounding large chain IDs and producing a transaction whose signature recovers to the wrong sender. The computation now stays inBigInt. (#128) sendJsonRpcRequestaccepts Tenderly-stylesimulation_resultsresponses again. The transport refactor made the URL path return only when the JSON body has aresultfield, so non-standard endpoints answering with{ simulation_results: ... }fell through to the (undefined)errorbranch and threw aTypeError. Thesimulation_resultsbranch is restored, matching the documented behavior and theJsonRpcResponsetype. (#182)- Gas estimation no longer mutates the caller's UserOperation.
baseEstimateUserOperationGasonSafeAccountandSimple7702Account(and their publicestimateUserOperationGaswrappers) overwrote the operation'ssignaturewith a dummy and zeroedmaxFeePerGas/maxPriorityFeePerGasin place, restoring the fees only on success. A bundler error left the caller's operation corrupted (zeroed fees, dummy signature). Estimation now runs on an internal shallow copy and the passed operation is never touched. (#132) SafeAccount.baseEstimateUserOperationGasper-signerverificationGasLimitcompensation now applies on every dummy-signature path. The ~55k-per-signer margin (covering signature verification cost that bundler simulation skips with short-circuiting dummy signatures) was only added whendummySignerSignaturePairswas passed explicitly. Calls usingexpectedSignersor the default single-EOA dummy got the raw bundler estimate back, underpricingverificationGasLimitfor multi-signer Safes. Operations that already carry a caller-supplied signature are still returned uncompensated, since the signer count is unknown. (#152)
- Safe ERC-4337 module migration helpers.
SafeAccountV0_3_0.createMigrateToSafeMultiChainSigAccountV1MetaTransactions(nodeRpcUrl, overrides?)builds thedisableModule+enableModule+setFallbackHandlerbatch that migrates a deployed Safe from the EntryPoint v0.7 module to the v0.9Safe4337MultiChainSignatureModule. Both modules are stateless, so no storage clearing is required. Unless{ skipPreflight: true }is passed, it first verifies on-chain that the account is actually a Safe running the old module (the module is enabled and is the current fallback handler) on a Safe version>= 1.4.1, turning a would-be cryptic on-chainAA23/AA24into a clear up-front error. - New Safe / transport readers.
SafeAccount.getFallbackHandler(nodeRpcUrl)(the active 4337 module),SafeAccount.getSafeVersion(nodeRpcUrl)(readsVERSION()),JsonRpcNode.getStorageAt(address, slot, blockTag?), and the exportedSAFE_FALLBACK_HANDLER_STORAGE_SLOTconstant. - Safe instance manual signing helpers.
SafeAccountandSafeMultiChainSigAccountexpose instance-level manual signing helpers that match the existing static EIP-712 helper API, so apps that already hold a Safe instance can sign without re-deriving the static call surface. SafeMultiChainSigAccount.estimateUserOperationGas. New instance method that estimates gas for a multi-chain-signature UserOperation against a bundler, matching the estimation surface already available on the other account classes.- Signer functions may return synchronously.
SignHashFnandSignTypedDataFnreturn types widen toHex | Promise<Hex>, so local-key signers can return a signature without wrapping it in aPromise. Existing async signers are unaffected. - UserOperation revert-reason decoding and AA-code parsing. New
decodeUserOperationRevertReason(receipt)reads the EntryPointUserOperationRevertReasonlog directly from a mined receipt and returns the decoded reason (Error string, Panic code, or empty for likely out-of-gas) with no extra RPC call, matching the receipt'suserOpHashso multi-op bundles return the right entry. EntryPointAAxxrevert codes (e.g.AA21) are parsed intoAbstractionKitError.aaCodeso callers can branch on a stable contract-defined code instead of message-text matching.UserOperationReverttype andparseAaCodehelper are exported. ethersruntime dependency removed. Account, paymaster, transport, signer, and utility surfaces now use an internalethereUtilsmodule for ABI encoding/decoding, keccak/hashing, typed-data, andBigInt-safe helpers, shrinking the install footprint. Public API shapes are unchanged.
UserOperationReceipt.logsandUserOperationReceiptResult.logsare now structuredLog[]instead of a JSON-encoded string. Callers that previously didJSON.parse(receipt.logs)should drop the parse and read the array directly:// Before const logs = JSON.parse(receipt.logs); // After const logs = receipt.logs;
- Removed the IIFE / UMD (
unpkg) browser build.dist/index.iife.jsand theunpkgfield inpackage.jsonwere removed. The<script>/ CDN build was effectively unusable as a standalone script: it externalized its runtime deps as page globals (ethersin 0.3.x,@noble/*in 0.4.0), so loading it without first providing those globals threwReferenceError. It was unused, so it was removed rather than maintained. Install via npm and use a bundler; the ESM (dist/index.mjs) and CJS (dist/index.cjs) entries are unchanged.
- Browser and React Native compatibility.
generateOnChainIdentifierusedBufferand ABIstringdecoding usedTextDecoder, both undefined in browsers without a polyfill and in React Native / Hermes, so those paths threwReferenceError. They now use internal pure-JS UTF-8 helpers (toUtf8Bytesand the newfromUtf8Bytes, not exported from the package), so the SDK runs in browsers (Vite, webpack, esbuild) and React Native out of the box.fromUtf8Bytesthrows aninvalid UTF-8error on malformed input (overlong, surrogate-range, out-of-range, or truncated sequences) rather than silently coercing the bytes into other characters, so consumers should not expect U+FFFD replacement characters. The Calibur, Safe, and Simple7702 executor-calldata decode paths now hex-encode the innerbytespayload instead of UTF-8-decoding it, which had corrupted the selector and arguments. SafeAccountV0_2_0.createMigrateToSafeAccountV0_3_0MetaTransactionspredecessor wiring. WhensafeV06ModuleAddresswas set explicitly, the v0.6 → v0.7 migration passed the module being disabled as its own linked-list predecessor, producingdisableModule(prev = module, module). It now exposes a dedicatedprevModuleAddressoverride (defaulting to the on-chain lookup) and shares the generic migration implementation. Default callers are unaffected.- v0.6
verificationGasLimitmultiplier incalculateUserOperationMaxGasCost. The paymaster multiplier was applied to the wrong factor on v0.6 UserOperations, undercounting the maximum gas cost. Fixed. - ERC-7677 / Candide paymaster
tokenCostrounds-to-zero on cheap-gas chains. On chains whereexchangeRate * maxGasCostWei < 10^18, the floor division collapsedtokenCostto0nand the prepended ERC-20 approval was also0n, causing the UserOperation to revert or the paymaster to absorb the full cost. The twoErc7677Paymaster/CandidePaymastercost paths and the publiccalculateUserOperationErc20TokenMaxGasCosthelper now floortokenCostto a minimum of1token smallest-unit.pimlico_getTokenQuotesandpm_supportedERC20Tokensresponses with a non-positiveexchangeRatenow throwPAYMASTER_ERRORinstead of silently feeding a zero rate into the math. createDisableModuleMetaTransactionmodule lookup now paginates. The predecessor lookup read a singlegetModulesPaginatedpage, so on a Safe with more enabled modules than the page size it could miss the target module or compute the wrong linked-list predecessor and produce an on-chain-revertingdisableModule. It now walks every page with a cursor.- Tenderly API key masked in error context.
callTenderlySimulateBundleerrors no longer surface the raw access key in their context payload.
- EIP-712 UserOperation helpers aligned across account families.
Simple7702Account,Simple7702AccountV09, andCalibur7702Accountnow expose public staticgetUserOperationEip712Data(userOp, chainId, overrides?)andgetUserOperationEip712Hash(userOp, chainId, overrides?)helpers. This matches the Safe helper naming and supports EntryPoint override viaoverrides.entrypointAddress. - EIP-712 typed-data signing support expanded.
Calibur7702Account.signUserOperationWithSignernow acceptssignTypedDatasigners, andCalibur7702Account.formatEip712SingleSignatureToUseroperationSignature(signature, overrides?)wraps raw typed-data signatures into Calibur's(keyHash, sig, hookData)layout.SafeMultiChainSigAccountV1.signUserOperationsWithSignersnow accepts typed-data-only signers for multi-operation Merkle bundles. - Pluggable
Transportabstraction. Node, bundler, and paymaster traffic now accepts EIP-1193-shaped transports:New exports:interface Transport { request<T = unknown>( args: { method: string; params?: readonly unknown[] | object }, options?: { signal?: AbortSignal }, ): Promise<T>; }
Transport,EventfulTransport,isEventfulTransport,BaseRpcTransport,HttpTransport,isHttpTransport,JsonRpcNode,TransportRpcError,RequestArgs,RequestOptions,ProviderRpcError,HttpTransportOptions,JsonRpcEnvelope, andEthCallTransaction. JsonRpcNodeservice class added. Methods:chainId(),blockNumber(),getCode(),call(),getTransactionCount(),getFeeData(),getDelegatedAddress(),getEntryPointNonce(),getEntryPointDeposit(),getEntryPointDepositInfo(), andrequest().getFeeData()no longer depends onethers.JsonRpcProvider.- RPC inputs widened.
Bundler,CandidePaymaster, andErc7677Paymasterconstructors now acceptstring | Transport; each class implementsTransportand exposes.from(input). PublicproviderRpc?,bundlerRpc?, andnodeRpcUrlparameters now acceptstring | Transport | JsonRpcNodefor node calls andstring | Transport | Bundlerfor bundler calls across Safe, Simple7702, Calibur,AllowanceModule,SocialRecoveryModule, and paymaster methods. - Request cancellation and service error mapping added.
Transport.requestacceptsoptions?: { signal?: AbortSignal };HttpTransportforwards it tofetch.NODE_ERRORwas added toBasicErrorCodeforJsonRpcNode, andsendJsonRpcRequestnow throwsTransportRpcErrorwhile service classes translate into their own domain errors. - Tenderly helpers no longer depend on
sendJsonRpcRequest.callTenderlySimulateBundlenow uses an inlinefetchcall; public Tenderly helper signatures are unchanged.
BaseSimple7702Account#getUserOperationEip712TypedDatamoved from an instance method to a static helper and was renamed togetUserOperationEip712Data. The returned typed-data payload shape is unchanged, but callers must switch fromaccount.getUserOperationEip712TypedData(...)to the account class' static helper. This aligns Simple7702 with the Safe and Calibur EIP-712 helper API and lets callers override the EntryPoint explicitly when needed. Migration:// Before const typedData = account.getUserOperationEip712TypedData(userOp, chainId); // After const typedData = Simple7702Account.getUserOperationEip712Data(userOp, chainId); // For EntryPoint v0.9 const typedDataV9 = Simple7702AccountV09.getUserOperationEip712Data(userOp, chainId);
bundler.rpcUrl/paymaster.rpcUrlfield removed. Thereadonly rpcUrl: stringfield onBundler,CandidePaymaster, andErc7677Paymasteris replaced byreadonly transport: Transport. Migration:Keeping// Before const url = bundler.rpcUrl; // After import { isHttpTransport } from "abstractionkit"; const url = isHttpTransport(bundler.transport) ? bundler.transport.url : null;
rpcUrlmade no sense once the input could be a non-HTTPTransport— the field would have beenundefinedforTransportinputs and silently misleading.- Top-level helpers removed from the public API:
fetchGasPrice,getBalanceOf,getDepositInfo,getDelegatedAddress. Each was a thin URL-string wrapper around what is now aJsonRpcNodemethod (in some cases renamed for clarity). Migration:The// Before import { fetchGasPrice, getBalanceOf, getDepositInfo, getDelegatedAddress } from "abstractionkit"; const [maxFee, priority] = await fetchGasPrice(nodeUrl, GasOption.Medium); const deposit = await getBalanceOf(nodeUrl, address, entryPoint); const info = await getDepositInfo(nodeUrl, address, entryPoint); const delegatee = await getDelegatedAddress(eoaAddress, nodeUrl); // After import { JsonRpcNode, GasOption } from "abstractionkit"; const node = new JsonRpcNode(nodeUrl); const [maxFee, priority] = await node.getFeeData(GasOption.Medium); const deposit = await node.getEntryPointDeposit(address, entryPoint); const info = await node.getEntryPointDepositInfo(address, entryPoint); const delegatee = await node.getDelegatedAddress(eoaAddress);
DepositInfotype continues to be exported from the package root.fetchAccountNonceandsendJsonRpcRequestare explicitly kept as top-level exports (both have their first parameter widened tostring | Transport | JsonRpcNode); they're the only loose helpers preserved.
- Social recovery multi-confirm transactions now encode and validate signer data correctly.
createMultiConfirmRecoveryMetaTransactionnow uses the correct ABI tuple shape, sorts signer addresses with a spec-compliant comparator, and rejects duplicate signers before producing calldata. This prevents malformed recovery confirmations and catches invalid guardian input earlier. JsonRpcNode.getFeeDatapreservesbigintprecision aboveNumber.MAX_SAFE_INTEGER. Gas-level multipliers are now applied inbigintspace instead of viaNumber(gasPrice), avoiding precision loss for large gas prices.
- EIP-7702 delegation authorization signer docs are clearer.
createAndSignEip7702DelegationAuthorizationnow documents that callback signers sign the authorization hash directly, without an EIP-191 / EIP-712 prefix, and that both 65-byte standard signatures and 64-byte EIP-2098 compact signatures are accepted. - Safe multi-chain signing docs clarify
chainIdplacement. The single-operation signing JSDoc points out thatchainIdis passed positionally, while multi-operation signing carrieschainIdinside eachUserOperationToSignitem.
- Docker-backed integration test suite added. The repo now includes an integration Jest config, global setup / teardown, chain matrix helpers, and e2e coverage for passkeys, social recovery, batch transactions, multisig, spend permissions, signer adapters, on-chain identifiers, bundler behavior, and EIP-712 signing.
- EIP-7702 accounts added to the e2e matrix. Calibur,
Simple7702Accounton EntryPoint v0.8, andSimple7702AccountV09on EntryPoint v0.9 now run through the account-agnostic integration suites. The local bundler matrix was expanded to run v0.8 with--eip7702. - Integration CI expanded. The integration workflow now supports manual dispatch, fork RPC overrides, per-entrypoint bundlers, cached anvil images, fail-fast chain startup, and cleanup on
SIGINT/SIGTERM.
- Safe multi-chain single-operation signing now hashes the correct payload. Single-op
SafeMultiChainSigAccountV1flows previously hashed through the Merkle wrapper and were rejected on-chain withAA24, sinceSafe4337MultiChainSignatureModuleverifies againstkeccak256(SafeOp)directly whenmerkleTreeDepth == 0. The per-op SafeOp digest is now reachable viaSafeMultiChainSigAccountV1.getUserOperationEip712Hash/getUserOperationEip712Data, which defaultsafe4337ModuleAddressto the multi-chain module so the digest matches the on-chain verifier without manual override. - Safe multi-chain WebAuthn / EIP-1271 signatures keep contract-signer formatting.
signUserOperationsWithSignersnow preserves the"contract"signer type when formatting multi-operation signatures, so dynamic Safe contract-signature segments are emitted correctly.
getMultiChainSingleSignatureUserOperationsEip712Hash/...Eip712Dataare wrapper-only and throw forlength < 2. They now do one thing: hash the Merkle-wrapped multi-op payload. Single-op callers must useSafeMultiChainSigAccountV1.getUserOperationEip712Hash/getUserOperationEip712Data(which defaultsafe4337ModuleAddressto the multi-chain module). If you call the parentSafeAccounthelpers directly, passsafe4337ModuleAddressexplicitly. Their default points at the standard 4337 module, not the multi-chain one.
- Added signer API documentation, type-level signer tests, signer unit-test coverage, and CI checks for linting, type tests, build, and signer tests.
- EIP-712 typed-data signing for
Simple7702Account/Simple7702AccountV09:signUserOperationWithSignernow acceptssignTypedData-only signers (JSON-RPC wallets, viemWalletClient) in addition to existingsignHashsigners. The v0.8/v0.9userOpHashIS the EIP-712 digest ofPackedUserOperationunder the EntryPoint's domain, so both schemes produce signatures that validate against the same hash. Throws on EntryPoint v0.7 (different signing scheme). AddsgetUserOperationEip712TypedData(userOp, chainId)onBaseSimple7702Accountas the lower-level escape hatch for integrators drivingsignTypedDatawith their own primitive (HSM, MPC, custom wallet abstraction).// Path A: signTypedData-only signer const signer = { address: eoaAddress, signTypedData: async (td) => walletClient.signTypedData(td), }; userOp.signature = await account.signUserOperationWithSigner(userOp, signer, chainId); // Path B: drive signTypedData yourself const td = account.getUserOperationEip712TypedData(userOp, chainId); userOp.signature = await wallet.signTypedData(td.domain, td.types, td.message);
- WebAuthn pubkey JSON helpers + assertion normalizer (3 new exports from package root):
pubkeyCoordinatesToJson(pubkey)/pubkeyCoordinatesFromJson(input): bigint-safe JSON round-trip for{ x, y }coordinates. Hex on the wire, canonical{ x: bigint, y: bigint }after parse.fromJsonaccepts a JSON string or a pre-parsed object, and either hex or decimal string coords.webauthnSignatureFromAssertion(response): turns a structural assertion shape (browserAuthenticatorAssertionResponse,ox/WebAuthnP256sign output, or@simplewebauthn/browser) into theWebauthnSignatureDatathatfromSafeWebauthnandcreateWebAuthnSignaturealready accept. Replaces the ~13-line parser pipeline every Safe-passkeys consumer was writing in theirgetAssertioncallback.
fromSafeWebauthnadapter: package-root factory that produces anExternalSignerfrom a WebAuthn credential, ready to pass intosafe.signUserOperationWithSigners(op, [signer], chainId). Hides three Safe-specific concerns: address routing (the WebAuthn shared signer for the deployment UserOp, the deterministic verifier-proxy address derived from(x, y)afterward), thetype: "contract"tag, and the Safe-specific signature encoding. RequiredaccountClassparameter (the same Safe subclass used atinitializeNewAccount) sources the Passkey module defaults —SafeAccountV0_2_0/SafeAccountV0_3_0for v0.2.0 (FCL P256),SafeMultiChainSigAccountV1for v0.2.1 (Daimo P256 + RIP-7951). Picking the wrong class would derive an address that isn't an on-chain owner and the bundler would reject with a generic "Invalid UserOp signature" (GS026on-chain), so the param is required to surface this choice at compile time. Caller supplies agetAssertion(challenge: Uint8Array) => Promise<WebauthnSignatureData>callback that runsnavigator.credentials.get(...)(browser) or an equivalent native bridge — the SDK doesn't importnavigatoritself, so the adapter stays environment-agnostic. PassexpectedSigners: [{ x, y }]tocreateUserOperationso the bundler estimates verification gas against the WebAuthn dummy signature (~400 bytes) instead of the EOA dummy (~65 bytes); without it, the real signed UserOp is rejected at submit. TheFromSafeWebauthnParamsandWebauthnAssertionFetchertypes are also exported from the package root.import { fromSafeWebauthn, SafeAccountV0_3_0 } from "abstractionkit"; let userOperation = await safe.createUserOperation( transactions, nodeUrl, bundlerUrl, { expectedSigners: [{ x, y }] }, ); const signer = fromSafeWebauthn({ publicKey: { x, y }, isInit: userOperation.nonce === 0n, accountClass: SafeAccountV0_3_0, // SafeMultiChainSigAccountV1 for multi-chain getAssertion: async (challenge) => { const assertion = await navigator.credentials.get({ publicKey: { challenge, rpId, allowCredentials, userVerification }, }); return { authenticatorData: assertion.response.authenticatorData, clientDataFields: extractClientDataFields(assertion.response), rs: extractSignature(assertion.response), }; }, }); userOperation.signature = await safe.signUserOperationWithSigners( userOperation, [signer], chainId, );
ExternalSigner.typefield ("ecdsa" | "contract", optional, defaults to"ecdsa"). When"contract", the signer's signature is encoded as a dynamic-length EIP-1271 contract-signature segment instead of a raw 65-byte ECDSA blob. Lets a singlesignUserOperationWithSigners([...])call mix ECDSA owners and contract-signature owners (WebAuthn, smart-contract owners) in the same Safe multisig batch. Account-agnostic: ignored by non-Safe accounts that don't model contract signatures.fromSafeWebauthnsets this internally.
UserOperationToSignWithOverrides.overridesis split intooptionsandwebAuthnSignatureOverrides. The previous kitchen-sinkoverridesfield carried both per-call signing options (timing, multi-chain, module address) and WebAuthn-specific encoding overrides (verifier addresses, init flag); these now live on dedicated fields. Affects callers ofSafeMultiChainSigAccountV1.signUserOperationsandsignUserOperationsWithSigners. Migration:// Before await safe.signUserOperations( [{ userOperation, chainId, validAfter, validUntil, overrides: { isInit: true, webAuthnSharedSigner, safe4337ModuleAddress }, }], [pk], ); // After await safe.signUserOperations( [{ userOperation, chainId, validAfter, validUntil, options: { safe4337ModuleAddress }, webAuthnSignatureOverrides: { isInit: true, webAuthnSharedSigner }, }], [pk], );
SafeAccount.isDeployed(accountAddress, nodeRpcUrl): static method that checks whether a Safe account is already deployed on-chain. ReturnstruewhenaccountAddresshas non-empty bytecode,falseotherwise. Useful for branching betweennew SafeAccountV0_3_0(address)(existing account) andSafeAccountV0_3_0.initializeNewAccount([owners])(counterfactual) without inspectingeth_getCodemanually.
TokenQuotetype exported from the package root:{ token: string; exchangeRate: bigint; tokenCost: bigint }. Surfaces the exchange rate and maximum token cost the paymaster applied when paying gas with an ERC-20 token, so consumers can display the cost to users or log/meter it without a second RPC round-trip.CandidePaymaster.createTokenPaymasterUserOperationandErc7677Paymaster.createPaymasterUserOperationnow returntokenQuotealongside the UserOperation. Populated on the token-payment flow; absent on sponsored flows and on Candide'ssigningPhase: "finalize"path (no gas estimation → no cost computation).skipGasEstimationflag oncreateUserOperationoverrides forSafeAccount,Calibur7702Account, andSimple7702Account. When set, the UserOperation is returned with a dummy signature and zero (or override-provided) gas limits, skipping the bundler'seth_estimateUserOperationGasroundtrip. Useful when gas estimation is run separately, for example by a paymaster sponsorship call that returns its own gas limits.SponsorInfotype exported from the package root. Represents the raw{ name, icon? }shape returned by paymasters per ERC-7677;CandidePaymasternormalizes it into the publicSponsorMetadatashape.
-
Three paymaster methods changed return shape from a raw UserOperation / tuple to a named-field object. All now return
{ userOperation, tokenQuote? | sponsorMetadata? }:CandidePaymaster.createTokenPaymasterUserOperation— returns{ userOperation, tokenQuote? }(wasSameUserOp<T>).CandidePaymaster.createSponsorPaymasterUserOperation— returns{ userOperation, sponsorMetadata? }(was[SameUserOp<T>, SponsorMetadata | undefined]).Erc7677Paymaster.createPaymasterUserOperation— returns{ userOperation, tokenQuote? }(wasSameUserOp<T>).
Migration:
// Before const [sponsoredOp, sponsorMetadata] = await paymaster.createSponsorPaymasterUserOperation(...); const tokenOp = await paymaster.createTokenPaymasterUserOperation(...); const userOp = await erc7677.createPaymasterUserOperation(...); // After const { userOperation: sponsoredOp, sponsorMetadata } = await paymaster.createSponsorPaymasterUserOperation(...); const { userOperation: tokenOp, tokenQuote } = await paymaster.createTokenPaymasterUserOperation(...); const { userOperation, tokenQuote } = await erc7677.createPaymasterUserOperation(...);
-
CandidePaymasterContextmoved back to a dedicated parameter onCandidePaymaster.createSponsorPaymasterUserOperationandcreateTokenPaymasterUserOperation. Thecontextfield was removed fromGasPaymasterUserOperationOverrides, andcontextis now the second-to-last argument (optional) on both methods, withoverridesas the last argument. Migration:// Before (0.3.2): context nested inside overrides await paymaster.createSponsorPaymasterUserOperation( smartAccount, userOp, bundlerRpc, sponsorshipPolicyId, { context: { signingPhase: "commit" }, maxFeePerGasMultiplier: 110n }, ); await paymaster.createTokenPaymasterUserOperation( smartAccount, userOp, tokenAddress, bundlerRpc, { context: { signingPhase: "commit" }, maxFeePerGasMultiplier: 110n }, ); // After (0.3.3): context is a dedicated argument await paymaster.createSponsorPaymasterUserOperation( smartAccount, userOp, bundlerRpc, sponsorshipPolicyId, { signingPhase: "commit" }, { maxFeePerGasMultiplier: 110n }, ); // For createTokenPaymasterUserOperation, `context` is optional: the method // always derives `context.token` from the `tokenAddress` argument, so pass // `undefined` unless you need other context fields (e.g. `signingPhase`). await paymaster.createTokenPaymasterUserOperation( smartAccount, userOp, tokenAddress, bundlerRpc, undefined, { maxFeePerGasMultiplier: 110n }, );
CandidePaymasternow parses sponsor info per ERC-7677. Paymasters return sponsor info undersponsor: { name, icon? }(singularicon); the previous code read a non-standardsponsorMetadatakey and therefore always returnedundefined. The raw response is now normalized into the publicSponsorMetadatashape ({ name, description, url, icons[] }).
-
signUserOperationWithSigner(s)+ExternalSigner(capability-oriented signing API): new async method on every account class for integrating viem, ethers Signers, hardware wallets, HSMs, MPC, WebAuthn, or Uint8Array-only signers without passing raw private keys. Each account declares its accepted schemes via a staticACCEPTED_SIGNING_SCHEMES: ReadonlyArray<"hash" | "typedData">, and incompatible signers fail offline with an actionable error. The method naming mirrors the parameter arity:- Safe accounts (multi-signer):
signUserOperationWithSigners(op, signers[], chainId)— plural. - Simple7702 / Calibur (single signer):
signUserOperationWithSigner(op, signer, chainId)— singular.
Call-site is one line:
import { fromViem } from "abstractionkit" userOp.signature = await safe.signUserOperationWithSigners( userOp, [fromViem(account)], chainId, )
- Safe accounts (multi-signer):
-
ExternalSignerinterface:{ address, signHash?, signTypedData? }discriminated union that enforces at least one of the two methods at compile time. Accepts any signer that matches the shape (viem local account, viem WalletClient, ethers Wallet, hardware wallet, MPC, WebAuthn, Uint8Array-held keys). The library has zero runtime dependency on viem or ethers for this surface. -
fromPrivateKey(pk)/fromViem(account)/fromEthersWallet(wallet)/fromViemWalletClient(client)adapters: one-line factories returning anExternalSigner. Structural types only.fromViem/fromViemWalletClientrequire viem ≥ 2.0;fromEthersWalletrequires ethers ≥ 6.0. -
SignHashFn/SignTypedDataFn/TypedData/SigningScheme/SignContext/MultiOpSignContexttypes exported from the package root for implementers of custom signers. -
SignContextforwarded to signers, narrowly typed per signing path: signers receive a context as the second arg ofsignHash/signTypedDataso custom validator implementations can inspect the userOp.Signer<C>is generic over context (defaultC = SignContextfor single-op{userOperation, chainId, entryPoint}; opt intoExternalSigner<MultiOpSignContext>forsignUserOperationsWithSigners's{userOperations[], entryPoint}). Built-in adapters returnSigner<unknown>and work everywhere. Seesrc/signer/types.ts. -
SafeMultiChainSigAccountV1.signUserOperationsWithSigners: new async multi-op variant that signs a Merkle-rooted bundle of UserOperations with a single signature across chains, usingExternalSigner[].
Note on versioning. The callback-API removal below is a breaking change for callers of
signUserOperationWithSigner's prior callback shape onCalibur7702Account. Calibur is not yet in use in any production environment; we're communicating directly with the developers currently building against it to coordinate the migration.
SafeAccount.baseSignSingleUserOperationis nowprotected static. Previouslypublic static, which leaked an internal helper into the package surface. All callers should use the version-specific subclass methods that wrap it (SafeAccountV0_2_0#signUserOperation,SafeAccountV0_3_0#signUserOperation, etc.) — they auto-inject the correct entrypoint and 4337 module addresses. Migration:// Before: const sig = SafeAccount.baseSignSingleUserOperation( op, [pk], chainId, SafeAccountV0_3_0.DEFAULT_ENTRYPOINT_ADDRESS, SafeAccountV0_3_0.DEFAULT_SAFE_4337_MODULE_ADDRESS, ); // After: const sig = safeV3.signUserOperation(op, [pk], chainId);
baseSignUserOperationWithSigners(introduced earlier in this Unreleased window) is alsoprotected staticfor the same reason; no migration needed since it was never on a releasedlatesttag.ViemLocalAccountLike/ViemWalletClientLike/EthersWalletLikeare no longer exported. They're internal structural shapes the adapters match against; pass concrete viem / ethers instances directly tofromViem/fromViemWalletClient/fromEthersWallet. If you need to type a wrapper, useParameters<typeof fromViem>[0](etc.).- Callback signing API removed.
signUserOperationWithSigner(op, callback, chainId)as introduced in the original signer PR is gone, along with theSignerFunction,AddressedSignerFunction,SignerInput,SignerResult, andSignerTypedDatatypes. The callback method name is now reused for the new capability-oriented API on single-signer accounts (Simple7702, Calibur) with a different parameter shape. Migration:// Before: const signer = async ({ userOpHash }) => ({ signature: wallet.signingKey.sign(userOpHash).serialized, }); userOp.signature = await account.signUserOperationWithSigner(userOp, signer, chainId); // After — Simple7702 / Calibur (single signer): import { fromEthersWallet } from "abstractionkit"; userOp.signature = await account.signUserOperationWithSigner( userOp, fromEthersWallet(wallet), chainId, ); // After — Safe accounts (multi-signer, plural method name): userOp.signature = await safe.signUserOperationWithSigners( userOp, [fromEthersWallet(wallet)], chainId, );
The existing sync signUserOperation(op, pk[] | pk, chainId): string method on every account is untouched. If your code passes a hex private-key string directly, no change needed. The new signUserOperationWithSigner(s) methods are Signers-only — they do NOT accept bare pk strings. To sign with a pk string via the new API, wrap explicitly:
import { fromPrivateKey } from "abstractionkit";
userOp.signature = await safe.signUserOperationWithSigners(
userOp, [fromPrivateKey(pk)], chainId,
);Erc7677Paymaster: provider-agnostic ERC-7677 paymaster client. Works with any compliant provider (Candide, Pimlico, Alchemy, ...). Auto-detects Candide/Pimlico from the URL and runs the full stub, estimate, and final pipeline in one call. Passing{ token }in context triggers the ERC-20 gas flow automatically.Bundler.estimateUserOperationGasnow forwardspaymasterVerificationGasLimitandpaymasterPostOpGasLimitwhen returned by the bundler.
SafeMultiChainSigAccountV1.formatSignaturesToUseroperationsSignatures: the thirdoverridesargument has been removed. Overrides are now per-operation via a new optionaloverridesfield on eachUserOperationToSignWithOverrideselement of the first argument. Migration:ops.map(op => ({ ...op, overrides: {...} }))and drop the third argument.
- Minor type tightening across Calibur, Simple7702, and Tenderly helpers.
This is a major release. The canonical upgrade path is from 0.2.30 (previous stable) to 0.3.0 (current stable). Versions 0.2.31 through 0.2.41 were experimental pre-releases and are not on the latest dist-tag.
- Node.js >= 18 required. Native
fetchis now used;isomorphic-unfetchhas been removed as a dependency. - Build system switched from microbundle to tsdown. Dist output paths have changed. If you import from a subpath, update your references:
dist/index.js->dist/index.cjsdist/index.m.js->dist/index.mjsdist/index.umd.js->dist/index.iife.jsdist/index.d.ts->dist/index.d.cts- A proper
exportsmap has been added topackage.jsonfor ESM/CJS resolution, so normalimport { X } from "abstractionkit"consumers are unaffected.
CandidePaymaster.createSponsorPaymasterUserOperation(...)signature changed. The method now takessmartAccountas the first argument. Migration:The// Before (0.2.30): await paymaster.createSponsorPaymasterUserOperation(userOp, bundlerRpc, sponsorshipPolicyId, overrides); // After (0.3.0): await paymaster.createSponsorPaymasterUserOperation(smartAccount, userOp, bundlerRpc, sponsorshipPolicyId, overrides);
overridesparameter type is also richer: it now accepts acontext?: CandidePaymasterContextfield for passingsponsorshipPolicyIdand the new parallel-signingsigningPhaseoption through overrides.createPaymasterUserOperationhas been removed. UsecreateSponsorPaymasterUserOperationorcreateTokenPaymasterUserOperationdirectly.- CandidePaymaster now uses the
pm_getPaymasterDataJSON-RPC method internally. Paymaster types have been unified and restructured. PaymasterInitValuesrenamed toParallelPaymasterInitValues.
Many interfaces and types are now exported with export type instead of export. This is only breaking if you re-export them yourself with export { X } from "abstractionkit", in which case change to export type { X }. Affected identifiers include:
RecoveryRequest,RecoverySignaturePair,RecoveryRequestTypedDataDomain,RecoveryRequestTypedMessageValueAllowanceDepositInfoAuthorization7702Hex,Authorization7702CandidePaymasterContext,PrependTokenPaymasterApproveAccountUserOperationV6,UserOperationV7,UserOperationV8,UserOperationV9,AbiInputValue,JsonRpcParam,JsonRpcResponse,MetaTransaction,StateOverrideSet, and other non-runtime types from./typesCreateUserOperationV6Overrides,CreateUserOperationV7Overrides,CreateUserOperationV9Overrides,ECDSAPublicAddress,InitCodeOverrides,SafeUserOperationTypedDataDomain,WebauthnPublicKey,WebauthnSignatureData,SignerSignaturePair,SignerSafeMessageTypedDataDomain,SafeMessageTypedMessageValue
The wildcard re-export export * from "./account/Safe/safeMessage" has been replaced with explicit named exports (SAFE_MESSAGE_PRIMARY_TYPE, SAFE_MESSAGE_MODULE_TYPE, getSafeMessageEip712Data).
Calibur7702Account: full-featured EIP-7702 smart account for EntryPoint v0.8, ported from Uniswap's Calibur. Supports secp256k1, P256, and WebAuthn P256 keys with per-key permissions and expirations. Includes key management (register, revoke, update settings via self-calls), automatic EIP-7702 delegation authoring and checking, and delegation revocation. Also exportsCaliburKeyTypeand theCaliburKey,CaliburKeySettings,CaliburKeySettingsResult,WebAuthnSignatureData,CaliburCreateUserOperationOverrides,CaliburSignatureOverrides, andSignerFunctiontypes.Simple7702AccountV09: minimal EIP-7702 account targeting EntryPoint v0.9, with parallel paymaster signing support.SafeMultiChainSigAccountV1: audited multi-chain signature account. Sign once, replay across chains via a merkle-proof structure. Promoted from experimental.SafeAccountV1_5_0_M_0_3_0: Safe contract v1.5.0 support with EIP-7951 and the Daimo P256 verifier for WebAuthn.
UserOperationV9type andCreateUserOperationV9Overridesadded.ENTRYPOINT_V6,ENTRYPOINT_V7,ENTRYPOINT_V8,ENTRYPOINT_V9address constants exported.- Bundler, CandidePaymaster, and Tenderly simulation helpers updated to handle all four EntryPoint versions.
- Entrypoint version resolution has been centralized in
CandidePaymaster: a new privateresolveEntrypointhelper reads the target entrypoint from the smart account instance at the top of each public method, replacing the per-methodUserOperation vX.YZ is not supportedchecks from 0.2.30. The guard itself is not new, but unsupported-version errors are now surfaced earlier and more consistently.
ExperimentalAllowAllParallelPaymaster: an experimental paymaster for the parallel-signing flow.signingPhaseadded toCandidePaymasterContext, with values"commit"and"finalize". Enables parallel-signing flows where owner signing and the paymaster's final signature can happen independently, via thePAYMASTER_SIG_MAGICconvention onpaymasterData. Works with EntryPoint v0.9 only.CandidePaymastersupports both v0.9 parallel flows and the existing sequential flow.
createChangeThresholdMetaTransaction,createApproveHashMetaTransaction, andgetThresholdadded toSafeAccount. Makes multi-sig threshold management and offchain approval flows first-class.- Auto-prepend
approve(0)before setting a new ERC-20 allowance for tokens like USDT that disallow changing a non-zero allowance directly. Opt in via{ resetApproval: true }on the token paymaster overrides. MerkleTreehelper utilities added for multi-chain operations.
- Allowance module updated to v1.0.0. The legacy address is exported as
ALLOWANCE_MODULE_V0_1_0_ADDRESSfor migration purposes.
CALIBUR_UNISWAP_V1_0_0_SINGLETON_ADDRESSandCALIBUR_CANDIDE_V0_1_0_SINGLETON_ADDRESSexported as constants.
getDelegatedAddress(eoaAddress, nodeRpc)utility for checking the current EIP-7702 delegation target of an EOA.- Calibur delegation and key revocation:
Calibur7702Account.createRevokeKeyMetaTransactionandcreateRevokeAllKeysMetaTransactionsfor revoking individual or all registered keys, pluscreateRevokeDelegationRawTransactionfor revoking the EIP-7702 delegation itself. Complements automatic delegation checking during UserOperation creation.
- EIP-2098 compact signature support in
parseRawSignature. EIP712_SAFE_OPERATION_PRIMARY_TYPEandEIP712_MULTI_CHAIN_OPERATIONS_PRIMARY_TYPEconstants added alongside the existing EIP-712 type constants.EIP712_MULTI_CHAIN_OPERATIONS_TYPE(previouslyEIP712_MULTI_SAFE_OPERATIONS_TYPE, renamed).- New paymaster-type exports:
AnyUserOperation,SameUserOp.
- Tenderly simulation helpers updated to support EntryPoint v0.9 and
IAccountExecute.executeUserOpcallData rewriting.
| Before | After |
|---|---|
ExperimentalSafeMultiChainSigAccount |
SafeMultiChainSigAccountV1 |
ExperimentalAllowAllPaymaster |
ExperimentalAllowAllParallelPaymaster |
EIP712_MULTI_SAFE_OPERATIONS_TYPE |
EIP712_MULTI_CHAIN_OPERATIONS_TYPE |
PaymasterInitValues |
ParallelPaymasterInitValues |
listKeys (Calibur) |
getKeys |
These renames only apply to code built on intermediate experimental versions (0.2.31 through 0.2.41). Code on 0.2.30 does not reference these identifiers.
Fixes listed here apply to APIs that already existed at 0.2.30. Bugs that were fixed within new-in-0.3.0 features during their pre-release development are not listed separately; those features are shipped in their final form as part of the "New Features" section.
- Gas estimation: fixed gas overrides calculations, BigInt gas scaling, and handling of fractional percentage multipliers in
applyMultiplier. - SafeAccount multisend: fixed a bug where token paymaster approvals were prepended after existing calls instead of before them.
- WebAuthn passkeys: fixed compatibility with the v0.2.1 shared-signer contracts when using custom contract addresses.
- EIP-7702 utilities: fixed
CHAIN_IDBigInt crash in signing helpers and exposedDEFAULT_DELEGATEE_ADDRESSas a static property. - Safe v0.3.0 account: fixed
safeAccountSingletonforwarding and added missingwebAuthnSignerProxyCreationCodehandling. - CandidePaymaster: fixed
paymasterMetadatahex-field normalization infetchSupportedERC20TokensAndPaymasterMetadata; fixed several instances of in-place mutation via aliasing on the user-passed UserOperation. - Constructor forwarding and lifecycle: fixed unhandled promises, timeout tracking, and constructor argument forwarding across pre-existing classes.
- Miscellaneous: typo fixes in error messages, removal of unused imports and dead guards, unused
safeV06PrevModuleAddressremoved, chainId validation tightened in pre-existing helpers.
- Build system migrated from microbundle to tsdown. Output paths updated (see Breaking Changes).
Simple7702Accountrefactored into aBaseSimple7702Accountpattern to enable the newSimple7702AccountV09subclass. No user-facing API changes onSimple7702Accountitself.- Removed
isomorphic-unfetchandrimrafdependencies;rimrafreplaced with a cross-platform inline Node script. - Added CI workflow (
.github/workflows/ci.yml) using yarn. - Added
SECURITY.mdwith vulnerability reporting policy. - Added
preparescript for GitHub-based installs. - Extensive JSDoc coverage added across public methods and types.