Skip to content

Commit f0ea68b

Browse files
Merge pull request #227 from candidelabs/dev
chore(release): 0.4.1
2 parents c0a1526 + 8419f0a commit f0ea68b

54 files changed

Lines changed: 2342 additions & 265 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,8 @@ jobs:
4545
- name: Build
4646
run: yarn build
4747

48-
- name: Signer unit tests
49-
run: yarn test:signer
48+
# Runs every suite outside test/integration/ (root jest config
49+
# excludes it): transport, safe, calibur, paymaster, signer, and
50+
# utility tests. All offline — mock transports or loopback servers.
51+
- name: Offline unit tests
52+
run: yarn test

.github/workflows/integration.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ concurrency:
1919

2020
jobs:
2121
integration:
22-
name: integration/chainId
22+
name: integration
2323
runs-on: ubuntu-latest
2424
strategy:
2525
matrix:

CHANGELOG.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,41 @@
11
# Changelog
22

3-
## [UNRELEASED]
3+
## 0.4.1
4+
5+
### API Changes
6+
7+
- **`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)
8+
- **`BundlerErrorCode.INVALID_USEROPERATION_HASH` is deprecated and never produced.** It remains in the union for compatibility. See the bundler error-mapping fix below. (#219)
9+
10+
### Bug Fixes
11+
12+
- **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)
13+
- **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)
14+
- **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)
15+
- **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)
16+
- **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)
17+
- **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)
18+
- **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)
19+
- **`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)
20+
- **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)
21+
- **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)
22+
- **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)
23+
- **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)
24+
- **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)
25+
- **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)
26+
- **`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)
27+
- **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)
28+
- **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)
29+
- **`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)
30+
- **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)
31+
- **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)
32+
- **`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)
33+
- **`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)
34+
- **`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)
35+
- **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)
36+
- **`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)
37+
- **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)
38+
- **`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)
439

540
## 0.4.0
641

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ const response = await account.sendUserOperation(userOp, bundlerRpc);
297297
const receipt = await response.included();
298298
```
299299

300-
After the first UserOp deploys the delegation, subsequent UserOps no longer need `eip7702Auth`. Use `getDelegatedAddress(eoaAddress, nodeRpc)` to check delegation status.
300+
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.
301301

302302
### Calibur 7702: register a WebAuthn passkey
303303

SECURITY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Only the latest stable version published to npm is supported with security updat
66

77
| Version | Supported |
88
| ------- | ------------------ |
9-
| 0.4.0 | :white_check_mark: |
9+
| 0.4.1 | :white_check_mark: |
1010

1111
## Reporting a Vulnerability
1212

jest.config.cjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@ module.exports = {
22
testEnvironment: "node",
33
modulePathIgnorePatterns: ["/.worktrees/"],
44
testPathIgnorePatterns: ["/node_modules/", "/dist/", "/.worktrees/", "/test/integration/"],
5+
// Tests import the built package from dist/; rebuild when src/ is newer.
6+
globalSetup: "<rootDir>/test/_ensureFreshDist.cjs",
57
};

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"name": "Candidelabs",
55
"url": "https://candide.dev"
66
},
7-
"version": "0.4.0",
7+
"version": "0.4.1",
88
"description": "Account Abstraction 4337 SDK by Candidelabs",
99
"main": "dist/index.cjs",
1010
"module": "dist/index.mjs",

0 commit comments

Comments
 (0)