UNPKG

abstractionkit

Version:

Account Abstraction 4337 SDK by Candidelabs

411 lines (328 loc) 59.1 kB
# Changelog ## 0.4.1 ### API Changes - **`AllowanceModule` start-delay parameters removed and renamed.** `createOneTimeAllowanceMetaTransaction` no longer takes a `startAfterInMinutes` argument: it was passed as the contract's `resetBaseMin` with `resetTimeMin = 0`, which reverts on-chain for any nonzero value (`setAllowance` requires `resetTimeMin > 0` whenever `resetBaseMin` is nonzero), so a delayed one-time allowance never worked. `createRecurringAllowanceMetaTransaction`'s last parameter is renamed from `startAfterInMinutes` to `inThePastPeriodStartBaseTimeStamp` and now takes a **unix timestamp in seconds** rather than a relative minute offset; on-chain `resetBaseMin` is an absolute epoch-minutes baseline that must be in the past and only aligns period boundaries, so the old relative value made funds spendable immediately regardless. The value is converted to epoch minutes internally (flooring sub-minute precision) and range-checked against the contract's `uint32` field, which catches millisecond-timestamp mistakes. Callers passing either argument must update; callers relying on the defaults are unaffected. (#217) - **`BundlerErrorCode.INVALID_USEROPERATION_HASH` is deprecated and never produced.** It remains in the union for compatibility. See the bundler error-mapping fix below. (#219) ### Bug Fixes - **Calibur key `expiration` could overflow into the `isAdmin` bit.** `packKeySettings` shifted `expiration` into bits 160+ without validating that it fits the contract's `uint40` field. A millisecond timestamp (>= 2^40) overflowed exactly into bit 200, the `isAdmin` flag, registering the key as an admin and bypassing the explicit guardrail in `createRegisterKeyMetaTransactions`. Both `expiration` and `hook` are now range-checked, with an error hint about seconds versus milliseconds. (#222) - **WebAuthn init UserOperations failed with `GS026` for roughly half of key pairs.** During account init the signature encoder substitutes the WebAuthn shared-signer address for a `WebauthnPublicKey` owner, but `sortSignatures` keyed on the per-owner verifier-proxy address. Whenever the two sorted differently relative to the other owners the encoded signatures were out of ascending owner order and the Safe rejected the operation, after gas estimation had already succeeded. Sorting now keys on the address that actually gets encoded. (#216) - **WebAuthn owners could never be swapped or removed.** `createWebAuthnSignerVerifierAddress` returned a lowercase address while `getOwners()` returns checksummed ones, so the lookups in `createSwapOwnerMetaTransactions` / `createRemoveOwnerMetaTransaction` always threw "not a current owner". The derived address is now checksummed and owner lookups are case-insensitive, so lowercase EOA inputs work too. (#216) - **v0.6 `initCode` override is now resolved before the dummy signature and gas estimation.** `overrides.initCode` was applied only after the base builder had derived `isInit` from `factoryAddress` and estimated gas with a factory-derived `initCode`, so a caller-supplied `"0x"` or custom `initCode` produced a mismatched WebAuthn dummy-signature encoding and estimated an operation that differed from the one being built. The final `initCode` is resolved first and `isInit` derives from it. (#229) - **Safe estimation-path input handling.** User-supplied `dummySignerSignaturePairs` containing raw `WebauthnPublicKey` signers threw "Must define isInit parameter" with no way to supply the flag through the overrides API, in both `createUserOperation` and the public `baseEstimateUserOperationGas`. The init flag is now derived from the operation's own init fields at both call sites, and the WebAuthn verifier-config overrides are forwarded to the signature encoder so deployed accounts with a custom verifier setup encode the correct owner address instead of one derived from the defaults. The `PAYMASTER_SIG_MAGIC` check and its EntryPoint-v0.9 gate also compared case-sensitively, spuriously rejecting uppercase hex `paymasterData` and lowercase entrypoint addresses. (#216) - **Legacy transactions with a leading-zero `r` or `s` were rejected by nodes.** `createAndSignLegacyRawTransaction` encoded `r` and `s` from the 32-byte zero-padded hex, so any signature whose `r` or `s` has a leading zero byte (~0.8% of transactions, deterministic per key/payload under RFC 6979) produced `rlp: non-canonical integer (leading zero bytes)`. Both are now encoded as minimal RLP integers, matching the EIP-7702 path. (#220) - **High-`s` external signatures silently never applied an EIP-7702 delegation.** EIP-7702 requires `s <= n/2` in authorization tuples, and nodes skip high-`s` tuples during set-code processing with no error surfaced, so a signer callback returning a standard high-`s` signature (as KMS/HSM/WebCrypto signers do by design) produced an authorization that never took effect. `parseRawSignature` now normalizes to the complementary low-`s` form (`s' = n - s`, flipped parity), matching the private-key path. (#220) - **`Simple7702Account.createRevokeDelegationTransaction` could revoke the wrong account's delegation.** It fetched delegation status and the transaction nonce for `accountAddress` but signed with `eoaPrivateKey` unconditionally, so a mismatched key produced a transaction whose sender was the other EOA, revoking the signer's delegation instead of the account it checked and reported on. It now applies the same up-front key/address check the Calibur revoke path has. (#220) - **Tenderly v0.6 deployment simulations produced invalid input.** The v0.6 `initCode` was split at 22 characters instead of 42, truncating the factory address to 10 bytes and leaking its second half into an unprefixed `factoryData`. The EIP-7702 factory sentinel check also matched only the short `0x7702` form and not the 20-byte right-padded form accepted by EntryPoint v0.8, skipping the delegation code override or triggering a bogus deployment simulation in the callData path; the `handleOps` encoding likewise concatenated the short sentinel directly with `factoryData`, producing a malformed `initCode` head. The direct call-data simulation now installs the sender's `0xef0100` delegation-code override from `eip7702Auth` and routes non-empty `factoryData` as the SenderCreator-to-sender initialization call. `callTenderlySimulateBundle` additionally typed `value`/`gasPrice` as `number`, silently rounding wei amounts above 2^53 (`bigint` is now accepted and serialized as a decimal string), and rewrote the caller's `stateOverrides` in place during the `stateDiff` to `storage` rename (the conversion now works on a copy). (#223) - **Tenderly state overrides merge case-insensitively by address.** The EIP-7702 delegation-code injection looked up the sender in caller-supplied `stateOverrides` by lowercase key only, so a checksummed caller entry for the same address was missed, producing two `state_objects` keys for one EVM address and leaving resolution up to the server. Both injection sites now merge through a shared helper, and `callTenderlySimulateBundle` normalizes every override key to lowercase, throwing a `RangeError` when two keys differ only in case. (#229) - **v0.6 paymaster verification-gas buffer silently skipped on lowercase entrypoints.** `CandidePaymaster` compared the entrypoint against the checksummed `ENTRYPOINT_V6` constant with strict equality, so a lowercase entrypoint (accepted everywhere else) skipped the 40k v0.6 paymaster verification buffer, under-provisioning `verificationGasLimit` once real `paymasterAndData` replaced the dummy. Both paymasters also added the buffer on top of an explicit `overrides.verificationGasLimit`, which is documented as used verbatim, making it impossible to pin the field (for example to reproduce a previously signed operation). The buffer now applies only to estimated values. (#221) - **WorldId paymaster mutated the caller's UserOperation.** `createPaymasterUserOperation` wrote the paymaster fields and a zeroed `preVerificationGas` directly onto the caller-owned object before calling the bundler, so a failed estimation left the caller's operation corrupted for any retry down another path. It now operates on a shallow copy, like the other paymasters, and returns the copy. (#221) - **Token paymaster flow crashed with a raw `TypeError`.** The token pipeline force-cast the smart account and failed with "is not a function" (wrapped as a generic `PAYMASTER_ERROR`) when the account does not implement `prependTokenPaymasterApproveToCallData`. It now checks the capability up front, before fetching a token quote, and throws a descriptive error. The `Erc7677Context` docs are also corrected: the context object is forwarded to the RPC verbatim (the "reserved, not forwarded" claim contradicted the tested behavior), and the consumed-but-undocumented `paymasterAddress` field is documented. (#221) - **HTTP errors surfaced as opaque `SyntaxError`s.** `HttpTransport.send` never checked the HTTP status and unconditionally parsed JSON, so a 401/429/502 with an HTML or plain-text body lost the status, the actual diagnostic. Non-JSON and non-envelope error bodies now throw a `TransportRpcError` carrying the status, while proper JSON-RPC error envelopes still report the server's error. A non-2xx status carrying a `result` body is contradictory and is no longer resolved as a success. `sendJsonRpcRequest`'s URL path had the same gaps. (#218) - **`error: null` responses crashed response parsing.** `BaseRpcTransport.parseResponse` used a key-presence check, so a successful response carrying `error: null` (sent by some non-strict servers) crashed destructuring `null` instead of returning the result. A non-object `error` field (a bare string, say) previously destructured into `TransportRpcError(undefined, undefined)`, losing the payload, and now falls through to a malformed-response error carrying the raw response. Scalar and `null` JSON bodies in `sendJsonRpcRequest` threw a raw `TypeError` from the `in` operator and now produce a malformed-response `TransportRpcError`. (#218) - **Non-RPC JSON error bodies no longer mask the HTTP status.** `HttpTransport.send` treated any non-null `error` field as a JSON-RPC error envelope, so an HTTP failure carrying a body like `{"error": "rate limited"}` bypassed the HTTP-status branch and surfaced a generic malformed-response error with the status (a 429, say) lost. Only a properly shaped JSON-RPC error object (numeric `code`, string `message`) now falls through to response parsing. (#229) - **Priority fee dropped when `eth_gasPrice` is unsupported.** `getFeeData`'s fallback ladder discarded a successfully fetched `maxPriorityFeePerGas` and pinned both fees to the 1-gwei floor, below the chain's actual required tip and contradicting the documented fallback order. The fetched priority fee is now used, with the existing clamp raising `maxFeePerGas` to match. (#218) - **`eth_getUserOperationByHash` returned wire-format hex strings in `bigint` fields.** Only `blockNumber` was converted, so `nonce`, gas limits, and fees kept hex strings despite the declared type: comparisons like `nonce === 0n` were silently always false and `bigint` arithmetic threw. Numeric fields are now converted, matching `getUserOperationReceipt` and `estimateUserOperationGas`. The result type is also widened from `V6 | V7` to the full V6-V9 union, so EntryPoint v0.8/v0.9 fields such as `eip7702Auth` no longer need an unsafe cast. (#219) - **Bundler JSON-RPC error mapping corrected.** `-32601` was translated to `INVALID_USEROPERATION_HASH`, which nothing assigns that meaning: ERC-7769 defines no error code for invalid hashes on the lookup methods, and Voltaire returns `-32602` for a missing or invalid `userOpHash`. The mapping shadowed the standard method-not-found error and misdirected debugging toward hash issues; `-32601` now always translates to `METHOD_NOT_FOUND`. `-32508` (paymaster balance insufficient for all mempool UserOperations, actively raised by Voltaire's mempool manager) now maps to `PAYMASTER_DEPOSIT_TOO_LOW` instead of `UNKNOWN_ERROR`, and bundler-side `-32603` falls through to the standard `INTERNAL_ERROR`. (#219) - **Safe module helper diagnostics.** `SocialRecoveryModule.getGuardians` and `.nonce` reported empty-result errors under the name `threshold`, misdirecting debugging. `getRecoveryRequestEip712Data` only accepted a URL string while every sibling method accepts `string | Transport | JsonRpcNode`. `AllowanceModule.getDelegates` with `maxNumberOfResults` above 255 or below zero threw an opaque ABI out-of-bounds error (the module's page size is a `uint8`) and now fails with a clear `RangeError` pointing at pagination. (#217) - **`recurringAllowanceValidityPeriodInMinutes` is range-checked against the contract's `uint16`.** An out-of-range value surfaced as the ABI encoder's generic "value out-of-bounds for uint16"; it is now rejected up front with a `RangeError` naming the parameter and its 0 to 65535 minute range. (#229) - **`fetchAccountNonce` `key` parameter widened to `number | bigint`.** The nonce key is a `uint192`, which a JS `number` cannot represent beyond 2^53-1 (keccak-derived parallel-lane keys, for instance). The implementation already coerced via `BigInt(key)`; the declared type now matches. `number` stays accepted. (#224) - **`SafeAccountV1_5_0_M_0_3_0.initializeNewAccount` returns the right runtime type.** The subclass factory delegated to `SafeAccountV0_3_0.initializeNewAccount`, which hard-coded `new SafeAccountV0_3_0(...)`, so the returned instance failed `instanceof SafeAccountV1_5_0_M_0_3_0` and lost subclass behavior. The base factory now instantiates polymorphically through `new this(...)`, so any subclass (including consumer-defined ones) gets its own type back. Detached calls (the factory extracted into a bare function, where strict-mode `this` is undefined) fall back to constructing the base class, preserving the previous behavior for plain-JS callers. (#116) - **Legacy transaction `v` precision for chain IDs above 2^53.** `createAndSignLegacyRawTransaction` computed the EIP-155 `v` field via `Number(chainId)`, silently rounding large chain IDs and producing a transaction whose signature recovers to the wrong sender. The computation now stays in `BigInt`. (#128) - **`sendJsonRpcRequest` accepts Tenderly-style `simulation_results` responses again.** The transport refactor made the URL path return only when the JSON body has a `result` field, so non-standard endpoints answering with `{ simulation_results: ... }` fell through to the (undefined) `error` branch and threw a `TypeError`. The `simulation_results` branch is restored, matching the documented behavior and the `JsonRpcResponse` type. (#182) - **Gas estimation no longer mutates the caller's UserOperation.** `baseEstimateUserOperationGas` on `SafeAccount` and `Simple7702Account` (and their public `estimateUserOperationGas` wrappers) overwrote the operation's `signature` with a dummy and zeroed `maxFeePerGas`/`maxPriorityFeePerGas` in 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.baseEstimateUserOperationGas` per-signer `verificationGasLimit` compensation 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 when `dummySignerSignaturePairs` was passed explicitly. Calls using `expectedSigners` or the default single-EOA dummy got the raw bundler estimate back, underpricing `verificationGasLimit` for multi-signer Safes. Operations that already carry a caller-supplied signature are still returned uncompensated, since the signer count is unknown. (#152) ## 0.4.0 ### New Features - **Safe ERC-4337 module migration helpers.** `SafeAccountV0_3_0.createMigrateToSafeMultiChainSigAccountV1MetaTransactions(nodeRpcUrl, overrides?)` builds the `disableModule` + `enableModule` + `setFallbackHandler` batch that migrates a deployed Safe from the EntryPoint v0.7 module to the v0.9 `Safe4337MultiChainSignatureModule`. 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-chain `AA23`/`AA24` into a clear up-front error. - **New Safe / transport readers.** `SafeAccount.getFallbackHandler(nodeRpcUrl)` (the active 4337 module), `SafeAccount.getSafeVersion(nodeRpcUrl)` (reads `VERSION()`), `JsonRpcNode.getStorageAt(address, slot, blockTag?)`, and the exported `SAFE_FALLBACK_HANDLER_STORAGE_SLOT` constant. - **Safe instance manual signing helpers.** `SafeAccount` and `SafeMultiChainSigAccount` expose 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.** `SignHashFn` and `SignTypedDataFn` return types widen to `Hex | Promise<Hex>`, so local-key signers can return a signature without wrapping it in a `Promise`. Existing async signers are unaffected. - **UserOperation revert-reason decoding and AA-code parsing.** New `decodeUserOperationRevertReason(receipt)` reads the EntryPoint `UserOperationRevertReason` log 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's `userOpHash` so multi-op bundles return the right entry. EntryPoint `AAxx` revert codes (e.g. `AA21`) are parsed into `AbstractionKitError.aaCode` so callers can branch on a stable contract-defined code instead of message-text matching. `UserOperationRevert` type and `parseAaCode` helper are exported. - **`ethers` runtime dependency removed.** Account, paymaster, transport, signer, and utility surfaces now use an internal `ethereUtils` module for ABI encoding/decoding, keccak/hashing, typed-data, and `BigInt`-safe helpers, shrinking the install footprint. Public API shapes are unchanged. ### Breaking Changes - **`UserOperationReceipt.logs` and `UserOperationReceiptResult.logs` are now structured `Log[]`** instead of a JSON-encoded string. Callers that previously did `JSON.parse(receipt.logs)` should drop the parse and read the array directly: ```ts // Before const logs = JSON.parse(receipt.logs); // After const logs = receipt.logs; ``` - **Removed the IIFE / UMD (`unpkg`) browser build.** `dist/index.iife.js` and the `unpkg` field in `package.json` were removed. The `<script>` / CDN build was effectively unusable as a standalone script: it externalized its runtime deps as page globals (`ethers` in 0.3.x, `@noble/*` in 0.4.0), so loading it without first providing those globals threw `ReferenceError`. 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. ### Bug Fixes - **Browser and React Native compatibility.** `generateOnChainIdentifier` used `Buffer` and ABI `string` decoding used `TextDecoder`, both undefined in browsers without a polyfill and in React Native / Hermes, so those paths threw `ReferenceError`. They now use internal pure-JS UTF-8 helpers (`toUtf8Bytes` and the new `fromUtf8Bytes`, not exported from the package), so the SDK runs in browsers (Vite, webpack, esbuild) and React Native out of the box. `fromUtf8Bytes` throws an `invalid UTF-8` error 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 inner `bytes` payload instead of UTF-8-decoding it, which had corrupted the selector and arguments. - **`SafeAccountV0_2_0.createMigrateToSafeAccountV0_3_0MetaTransactions` predecessor wiring.** When `safeV06ModuleAddress` was set explicitly, the v0.6 → v0.7 migration passed the module being disabled as its own linked-list predecessor, producing `disableModule(prev = module, module)`. It now exposes a dedicated `prevModuleAddress` override (defaulting to the on-chain lookup) and shares the generic migration implementation. Default callers are unaffected. - **v0.6 `verificationGasLimit` multiplier in `calculateUserOperationMaxGasCost`.** The paymaster multiplier was applied to the wrong factor on v0.6 UserOperations, undercounting the maximum gas cost. Fixed. - **ERC-7677 / Candide paymaster `tokenCost` rounds-to-zero on cheap-gas chains.** On chains where `exchangeRate * maxGasCostWei < 10^18`, the floor division collapsed `tokenCost` to `0n` and the prepended ERC-20 approval was also `0n`, causing the UserOperation to revert or the paymaster to absorb the full cost. The two `Erc7677Paymaster` / `CandidePaymaster` cost paths and the public `calculateUserOperationErc20TokenMaxGasCost` helper now floor `tokenCost` to a minimum of `1` token smallest-unit. `pimlico_getTokenQuotes` and `pm_supportedERC20Tokens` responses with a non-positive `exchangeRate` now throw `PAYMASTER_ERROR` instead of silently feeding a zero rate into the math. - **`createDisableModuleMetaTransaction` module lookup now paginates.** The predecessor lookup read a single `getModulesPaginated` page, 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-reverting `disableModule`. It now walks every page with a cursor. - **Tenderly API key masked in error context.** `callTenderlySimulateBundle` errors no longer surface the raw access key in their context payload. ## 0.3.8 ### New Features - **EIP-712 UserOperation helpers aligned across account families.** `Simple7702Account`, `Simple7702AccountV09`, and `Calibur7702Account` now expose public static `getUserOperationEip712Data(userOp, chainId, overrides?)` and `getUserOperationEip712Hash(userOp, chainId, overrides?)` helpers. This matches the Safe helper naming and supports EntryPoint override via `overrides.entrypointAddress`. - **EIP-712 typed-data signing support expanded.** `Calibur7702Account.signUserOperationWithSigner` now accepts `signTypedData` signers, and `Calibur7702Account.formatEip712SingleSignatureToUseroperationSignature(signature, overrides?)` wraps raw typed-data signatures into Calibur's `(keyHash, sig, hookData)` layout. `SafeMultiChainSigAccountV1.signUserOperationsWithSigners` now accepts typed-data-only signers for multi-operation Merkle bundles. - **Pluggable `Transport` abstraction.** Node, bundler, and paymaster traffic now accepts EIP-1193-shaped transports: ```ts interface Transport { request<T = unknown>( args: { method: string; params?: readonly unknown[] | object }, options?: { signal?: AbortSignal }, ): Promise<T>; } ``` New exports: `Transport`, `EventfulTransport`, `isEventfulTransport`, `BaseRpcTransport`, `HttpTransport`, `isHttpTransport`, `JsonRpcNode`, `TransportRpcError`, `RequestArgs`, `RequestOptions`, `ProviderRpcError`, `HttpTransportOptions`, `JsonRpcEnvelope`, and `EthCallTransaction`. - **`JsonRpcNode` service class added.** Methods: `chainId()`, `blockNumber()`, `getCode()`, `call()`, `getTransactionCount()`, `getFeeData()`, `getDelegatedAddress()`, `getEntryPointNonce()`, `getEntryPointDeposit()`, `getEntryPointDepositInfo()`, and `request()`. `getFeeData()` no longer depends on `ethers.JsonRpcProvider`. - **RPC inputs widened.** `Bundler`, `CandidePaymaster`, and `Erc7677Paymaster` constructors now accept `string | Transport`; each class implements `Transport` and exposes `.from(input)`. Public `providerRpc?`, `bundlerRpc?`, and `nodeRpcUrl` parameters now accept `string | Transport | JsonRpcNode` for node calls and `string | Transport | Bundler` for bundler calls across Safe, Simple7702, Calibur, `AllowanceModule`, `SocialRecoveryModule`, and paymaster methods. - **Request cancellation and service error mapping added.** `Transport.request` accepts `options?: { signal?: AbortSignal }`; `HttpTransport` forwards it to `fetch`. `NODE_ERROR` was added to `BasicErrorCode` for `JsonRpcNode`, and `sendJsonRpcRequest` now throws `TransportRpcError` while service classes translate into their own domain errors. - **Tenderly helpers no longer depend on `sendJsonRpcRequest`.** `callTenderlySimulateBundle` now uses an inline `fetch` call; public Tenderly helper signatures are unchanged. ### Breaking Changes - **`BaseSimple7702Account#getUserOperationEip712TypedData` moved from an instance method to a static helper and was renamed to `getUserOperationEip712Data`.** The returned typed-data payload shape is unchanged, but callers must switch from `account.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: ```ts // 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.rpcUrl` field removed.** The `readonly rpcUrl: string` field on `Bundler`, `CandidePaymaster`, and `Erc7677Paymaster` is replaced by `readonly transport: Transport`. Migration: ```ts // Before const url = bundler.rpcUrl; // After import { isHttpTransport } from "abstractionkit"; const url = isHttpTransport(bundler.transport) ? bundler.transport.url : null; ``` Keeping `rpcUrl` made no sense once the input could be a non-HTTP `Transport` — the field would have been `undefined` for `Transport` inputs 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 a `JsonRpcNode` method (in some cases renamed for clarity). Migration: ```ts // 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); ``` The `DepositInfo` type continues to be exported from the package root. `fetchAccountNonce` and `sendJsonRpcRequest` are explicitly **kept** as top-level exports (both have their first parameter widened to `string | Transport | JsonRpcNode`); they're the only loose helpers preserved. ### Bug Fixes - **Social recovery multi-confirm transactions now encode and validate signer data correctly.** `createMultiConfirmRecoveryMetaTransaction` now 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.getFeeData` preserves `bigint` precision above `Number.MAX_SAFE_INTEGER`.** Gas-level multipliers are now applied in `bigint` space instead of via `Number(gasPrice)`, avoiding precision loss for large gas prices. ### Documentation - **EIP-7702 delegation authorization signer docs are clearer.** `createAndSignEip7702DelegationAuthorization` now 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 `chainId` placement.** The single-operation signing JSDoc points out that `chainId` is passed positionally, while multi-operation signing carries `chainId` inside each `UserOperationToSign` item. ### Testing and CI - **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, `Simple7702Account` on EntryPoint v0.8, and `Simple7702AccountV09` on 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`. ## 0.3.7 ### Fixes - **Safe multi-chain single-operation signing now hashes the correct payload.** Single-op `SafeMultiChainSigAccountV1` flows previously hashed through the Merkle wrapper and were rejected on-chain with `AA24`, since `Safe4337MultiChainSignatureModule` verifies against `keccak256(SafeOp)` directly when `merkleTreeDepth == 0`. The per-op SafeOp digest is now reachable via `SafeMultiChainSigAccountV1.getUserOperationEip712Hash` / `getUserOperationEip712Data`, which default `safe4337ModuleAddress` to 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.** `signUserOperationsWithSigners` now preserves the `"contract"` signer type when formatting multi-operation signatures, so dynamic Safe contract-signature segments are emitted correctly. ### API changes - **`getMultiChainSingleSignatureUserOperationsEip712Hash` / `...Eip712Data` are wrapper-only and throw for `length < 2`.** They now do one thing: hash the Merkle-wrapped multi-op payload. Single-op callers must use `SafeMultiChainSigAccountV1.getUserOperationEip712Hash` / `getUserOperationEip712Data` (which default `safe4337ModuleAddress` to the multi-chain module). If you call the parent `SafeAccount` helpers directly, pass `safe4337ModuleAddress` explicitly. Their default points at the standard 4337 module, not the multi-chain one. ### Maintenance - Added signer API documentation, type-level signer tests, signer unit-test coverage, and CI checks for linting, type tests, build, and signer tests. ## 0.3.5 ### New Features - **EIP-712 typed-data signing for `Simple7702Account` / `Simple7702AccountV09`**: `signUserOperationWithSigner` now accepts `signTypedData`-only signers (JSON-RPC wallets, viem `WalletClient`) in addition to existing `signHash` signers. The v0.8/v0.9 `userOpHash` IS the EIP-712 digest of `PackedUserOperation` under the EntryPoint's domain, so both schemes produce signatures that validate against the same hash. Throws on EntryPoint v0.7 (different signing scheme). Adds `getUserOperationEip712TypedData(userOp, chainId)` on `BaseSimple7702Account` as the lower-level escape hatch for integrators driving `signTypedData` with their own primitive (HSM, MPC, custom wallet abstraction). ```ts // 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. `fromJson` accepts a JSON string or a pre-parsed object, and either hex or decimal string coords. - `webauthnSignatureFromAssertion(response)`: turns a structural assertion shape (browser `AuthenticatorAssertionResponse`, `ox/WebAuthnP256` sign output, or `@simplewebauthn/browser`) into the `WebauthnSignatureData` that `fromSafeWebauthn` and `createWebAuthnSignature` already accept. Replaces the ~13-line parser pipeline every Safe-passkeys consumer was writing in their `getAssertion` callback. - **`fromSafeWebauthn` adapter**: package-root factory that produces an `ExternalSigner` from a WebAuthn credential, ready to pass into `safe.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), the `type: "contract"` tag, and the Safe-specific signature encoding. Required `accountClass` parameter (the same Safe subclass used at `initializeNewAccount`) sources the Passkey module defaults — `SafeAccountV0_2_0` / `SafeAccountV0_3_0` for v0.2.0 (FCL P256), `SafeMultiChainSigAccountV1` for 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" (`GS026` on-chain), so the param is required to surface this choice at compile time. Caller supplies a `getAssertion(challenge: Uint8Array) => Promise<WebauthnSignatureData>` callback that runs `navigator.credentials.get(...)` (browser) or an equivalent native bridge — the SDK doesn't import `navigator` itself, so the adapter stays environment-agnostic. Pass `expectedSigners: [{ x, y }]` to `createUserOperation` so 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. The `FromSafeWebauthnParams` and `WebauthnAssertionFetcher` types are also exported from the package root. ```ts 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.type` field** (`"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 single `signUserOperationWithSigners([...])` 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. `fromSafeWebauthn` sets this internally. ### Breaking Changes - **`UserOperationToSignWithOverrides.overrides` is split into `options` and `webAuthnSignatureOverrides`.** The previous kitchen-sink `overrides` field 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 of `SafeMultiChainSigAccountV1.signUserOperations` and `signUserOperationsWithSigners`. Migration: ```ts // 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], ); ``` ## 0.3.4 ### New Features - **`SafeAccount.isDeployed(accountAddress, nodeRpcUrl)`**: static method that checks whether a Safe account is already deployed on-chain. Returns `true` when `accountAddress` has non-empty bytecode, `false` otherwise. Useful for branching between `new SafeAccountV0_3_0(address)` (existing account) and `SafeAccountV0_3_0.initializeNewAccount([owners])` (counterfactual) without inspecting `eth_getCode` manually. ## 0.3.3 ### New Features - **`TokenQuote` type** 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.createTokenPaymasterUserOperation` and `Erc7677Paymaster.createPaymasterUserOperation` now return `tokenQuote`** alongside the UserOperation. Populated on the token-payment flow; absent on sponsored flows and on Candide's `signingPhase: "finalize"` path (no gas estimation → no cost computation). - **`skipGasEstimation` flag on `createUserOperation` overrides** for `SafeAccount`, `Calibur7702Account`, and `Simple7702Account`. When set, the UserOperation is returned with a dummy signature and zero (or override-provided) gas limits, skipping the bundler's `eth_estimateUserOperationGas` roundtrip. Useful when gas estimation is run separately, for example by a paymaster sponsorship call that returns its own gas limits. - **`SponsorInfo` type** exported from the package root. Represents the raw `{ name, icon? }` shape returned by paymasters per ERC-7677; `CandidePaymaster` normalizes it into the public `SponsorMetadata` shape. ### Breaking Changes - **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? }` (was `SameUserOp<T>`). - `CandidePaymaster.createSponsorPaymasterUserOperation` — returns `{ userOperation, sponsorMetadata? }` (was `[SameUserOp<T>, SponsorMetadata | undefined]`). - `Erc7677Paymaster.createPaymasterUserOperation` — returns `{ userOperation, tokenQuote? }` (was `SameUserOp<T>`). Migration: ```ts // 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(...); ``` - **`CandidePaymasterContext` moved back to a dedicated parameter** on `CandidePaymaster.createSponsorPaymasterUserOperation` and `createTokenPaymasterUserOperation`. The `context` field was removed from `GasPaymasterUserOperationOverrides`, and `context` is now the second-to-last argument (optional) on both methods, with `overrides` as the last argument. Migration: ```ts // 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 }, ); ``` ### Bug Fixes - **`CandidePaymaster` now parses sponsor info per ERC-7677.** Paymasters return sponsor info under `sponsor: { name, icon? }` (singular `icon`); the previous code read a non-standard `sponsorMetadata` key and therefore always returned `undefined`. The raw response is now normalized into the public `SponsorMetadata` shape (`{ name, description, url, icons[] }`). ## 0.3.2 ### New Features - **`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 static `ACCEPTED_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: ```ts import { fromViem } from "abstractionkit" userOp.signature = await safe.signUserOperationWithSigners( userOp, [fromViem(account)], chainId, ) ``` - **`ExternalSigner` interface**: `{ 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 an `ExternalSigner`. Structural types only. `fromViem` / `fromViemWalletClient` require viem ≥ 2.0; `fromEthersWallet` requires ethers ≥ 6.0. - **`SignHashFn` / `SignTypedDataFn` / `TypedData` / `SigningScheme` / `SignContext` / `MultiOpSignContext` types** exported from the package root for implementers of custom signers. - **`SignContext` forwarded to signers, narrowly typed per signing path**: signers receive a context as the second arg of `signHash` / `signTypedData` so custom validator implementations can inspect the userOp. `Signer<C>` is generic over context (default `C = SignContext` for single-op `{userOperation, chainId, entryPoint}`; opt into `ExternalSigner<MultiOpSignContext>` for `signUserOperationsWithSigners`'s `{userOperations[], entryPoint}`). Built-in adapters return `Signer<unknown>` and work everywhere. See `src/signer/types.ts`. - **`SafeMultiChainSigAccountV1.signUserOperationsWithSigners`**: new async multi-op variant that signs a Merkle-rooted bundle of UserOperations with a single signature across chains, using `ExternalSigner[]`. ### Breaking Changes > **Note on versioning.** The callback-API removal below is a breaking change for callers of `signUserOperationWithSigner`'s prior callback shape on `Calibur7702Account`. 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.baseSignSingleUserOperation` is now `protected static`.** Previously `public 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: ```ts // 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 also `protected static` for the same reason; no migration needed since it was never on a released `latest` tag. - **`ViemLocalAccountLike` / `ViemWalletClientLike` / `EthersWalletLike` are no longer exported.** They're internal structural shapes the adapters match against; pass concrete viem / ethers instances directly to `fromViem` / `fromViemWalletClient` / `fromEthersWallet`. If you need to type a wrapper, use `Parameters<typeof fromViem>[0]` (etc.). - **Callback signing API removed.** `signUserOperationWithSigner(op, callback, chainId)` as introduced in the original signer PR is gone, along with the `SignerFunction`, `AddressedSignerFunction`, `SignerInput`, `SignerResult`, and `SignerTypedData` types. 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: ```ts // 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, ); ``` ### Migration: Signing with a raw private key 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: ```ts import { fromPrivateKey } from "abstractionkit"; userOp.signature = await safe.signUserOperationWithSigners( userOp, [fromPrivateKey(pk)], chainId, ); ``` ## 0.3.1 ### New Features - **`Erc7677Paymaster`**: provider-agnostic [ERC-7677](https://eips.ethereum.org/EIPS/eip-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.estimateUserOperationGas` now forwards `paymasterVerificationGasLimit` and `paymasterPostOpGasLimit` when returned by the bundler. ### Breaking Changes - **`SafeMultiChainSigAccountV1.formatSignaturesToUseroperationsSignatures`**: the third `overrides` argument has been removed. Overrides are now per-operation via a new optional `overrides` field on each `UserOperationToSignWithOverrides` element of the first argument. Migration: `ops.map(op => ({ ...op, overrides: {...} }))` and drop the third argument. ### Other - Minor type tightening across Calibur, Simple7702, and Tenderly helpers. ## 0.3.0 **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. ### Breaking Changes #### Build & Runtime - **Node.js >= 18 required.** Native `fetch` is now used; `isomorphic-unfetch` has 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.cjs` - `dist/index.m.js` -> `dist/index.mjs` - `dist/index.umd.js` -> `dist/index.iife.js` - `dist/index.d.ts` -> `dist/index.d.cts` - A proper `exports` map has been added to `package.json` for ESM/CJS resolution, so normal `import { X } from "abstractionkit"` consumers are unaffected. #### Paymaster API - **`CandidePaymaster.createSponsorPaymasterUserOperation(...)` signature changed.** The method now takes `smartAccount` as the **first** argument. Migration: ```ts // Before (0.2.30)