feat(sdk): offline signing — prepare / sign / broadcast for institutional custody (SDK-75)#345
Draft
ghermet wants to merge 45 commits into
Draft
feat(sdk): offline signing — prepare / sign / broadcast for institutional custody (SDK-75)#345ghermet wants to merge 45 commits into
ghermet wants to merge 45 commits into
Conversation
Phase 1 of the deferred-signing v2 refactor. - GenericSigner.writeContract is now optional. Add optional signTransaction(unsignedTx) → Hex for the deferred custodian path. - Add Broadcaster type and BroadcastSigner adapter (capability-bag signer for institutional custody, HSM, and policy-engine workflows). Keys never enter the SDK; the broadcaster owns signing material. - GenericProvider.sendRawTransaction(signedTx) → Hex required. Wired in the viem, ethers, and wagmi adapters. - Add assertWriteContract / assertSignTransaction capability guards using TS asserts predicates so atomic call sites narrow without casts. - SignerCapabilityError carries the missing capability for typed handling at call sites. - All atomic write sites (Token methods, DelegationService) now run the capability assertion before invoking writeContract. Behavioural change: zero. Online signers keep their writeContract method and atomic flows work as before. Broadcast-only signers get a typed SignerCapabilityError instead of a runtime TypeError when atomic methods are called — Phase 4 will replace those throws with a transparent fallback through prepare + sign + broadcast. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 of deferred-signing v2. Adds the prepare → sign → broadcast pipeline
and exposes it on ZamaSDK as five composable methods. Atomic call sites
unchanged.
- New OfflineSigningService (services/offline-signing-service.ts) owns the
full deferred pipeline: build unsigned tx, sign via signer.signTransaction,
broadcast via provider.sendRawTransaction, await receipt, emit *Submitted
event. Lazy-instantiated alongside CredentialService and DecryptionService
when a signer is configured.
- ZamaSDK gains five public methods: prepare, sign, broadcast, execute
(overloaded — PreparedTransaction | TransactionPrepareRequest |
CredentialPermitRequest), completeFromTxHash. All require a configured
signer; methods that need signTransaction surface SignerCapabilityError
via the asserts predicate from Phase 1.
- GenericProvider gains prepareTransaction({ from, call }) returning RLP
unsigned-tx bytes. Wired in viem (encodeFunctionData +
estimateFeesPerGas + serializeTransaction), ethers v6
(Transaction.from(...).unsignedSerialized), and wagmi (via getPublicClient).
- New discriminated types in types/prepared-tx.ts: TransactionKind,
TransactionPrepareRequest, PreparedTransaction<K>, PreparedFor<K>,
ConfidentialTransferRequest, CredentialPermitRequest, ExecuteRequest.
Phase 2 wires the ConfidentialTransfer kind end-to-end; Phase 3 adds the
remaining ERC-7984 write ops and Token-level convenience methods.
- CredentialPermit kind uses the existing CredentialService.allow path —
the same EIP-712 typed-data flow the atomic path runs, but reachable
through execute({ kind: "CredentialPermit", contracts }) so a custodian
signer (signTypedData via Broadcaster) gets the same shape as an EOA.
- Round-trip tests in services/__tests__/offline-signing-service.test.ts
cover prepare/sign/broadcast/execute(prepared)/execute(request)/
completeFromTxHash/execute(permit) via BroadcastSigner + mock provider.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…(SDK-75) Phase 3 of deferred-signing v2. Extends the prepare → sign → broadcast pipeline to every ERC-7984 write op and lifts the SDK-level surface onto Token via paired prepareX / completeX methods. No behavioural change for atomic call sites. - types/prepared-tx.ts gains 10 new discriminated request types: ConfidentialTransferFromRequest, SetOperatorRequest, UnwrapRequest, UnwrapAllRequest, FinalizeUnwrapRequest, ApproveUnderlyingRequest, WrapRequest, TransferAndCallRequest, DelegateDecryptionRequest, RevokeDelegationRequest. TransactionKind / TransactionPrepareRequest cover the full union. - PreparedTransaction is now non-generic (covers the wide union); PreparedFor<K> derives the kind-specific narrowing as an intersection so any narrow form is assignable to the wide form. Removes the variance rejection that came from `request: Extract<...>` being invariant in K. - OfflineSigningService dispatches all 11 kinds via per-kind #buildX methods, owning encryption, balance-handle reads (UnwrapAll), and the publicDecrypt round-trip required by FinalizeUnwrap. SUBMITTED_EVENT_ BY_KIND + ERROR_OPERATION_BY_KIND maps are exhaustive. - Token gains prepareX / completeX for every single-tx kind plus prepareShield(amount, options?) — the multi-step planner that mirrors the atomic shield routing (TransferAndCall single-step on 1363, ApproveUnderlying + Wrap on non-1363). Caller iterates the plan, preparing each step right before signing to keep nonces fresh. - Unshield two-phase stays caller-orchestrated: prepareUnwrap or prepareUnwrapAll, broadcast, wait for the UnwrapRequested event, then prepareFinalizeUnwrap with the handle. - Tests: 17 new — 10 cover each new kind's calldata builder (via BroadcastSigner + the offline-signing service), 7 cover Token-level sugar and the ShieldPlan routing for both 1363 and approve+wrap paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… (SDK-75) Tightens the post-Phase-3 deferred-signing surface based on integration review. No new public API; behavioural changes are additive guards on existing paths. - broadcast/completeFromTxHash re-check chain alignment between the prepared tx and the current provider chain before submitting. The gap between prepare and broadcast is the whole point of deferred signing, and the user may have switched chains in between. - broadcast splits pre-submit failures (chain mismatch, RPC reject) from post-submit ones. Post-submit errors preserve the txHash in the message so callers can resume via completeFromTxHash without losing the in-flight tx. - execute checks CredentialPermit first so the discriminator lookup is safe on the wide ExecuteRequest type; isPreparedTransaction tightened to check all required fields (kind/to/chainId), no longer matches arbitrary shapes that happen to carry `unsignedTx`. - FinalizeUnwrap's publicDecrypt error is wrapped through wrapDecryptError for consistent diagnostic shape. - BroadcastSigner validates the broadcaster's returned signature shape via isHex and throws SigningFailedError instead of letting a malformed response surface as an opaque RPC reject downstream. - prepareShield's payable-path recipientData mirrors the atomic shield's encoding: `0x` for self-shield (wrapper falls back to from), raw 20-byte address otherwise — drops the redundant lowercase-pad-40 normalization. - ShieldPlan exported from packages/sdk and the token barrel for custodian-side use. - Docs cleanup across types/signer.ts, types/prepared-tx.ts, and the service: drop Phase-N labels, document JSON-serialisation gotcha for bigint fields on PreparedTransaction.request, document the PreparedFor<K> intersection rationale (variance of Extract<>), and note the USDT-style two-step approve caveat on ApproveUnderlyingRequest. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…SDK-75)
Round-2 polish on top of the deferred-signing surface — replaces lingering
bare error throws with typed `ZamaError` subclasses and fixes an isPayable
cache footgun that could silently downgrade 1363-capable tokens.
- `Token.isPayable()` now only caches successful results. Contract reverts
(via `isContractCallError`) still cache `false`, but transport errors
bubble up so a transient RPC blip during the probe doesn't permanently
route a 1363 token through the slower approve+wrap path.
- `OfflineSigningService.sign()` wraps broadcaster rejections (HTTP errors,
policy denials, timeouts) in `SigningFailedError` with cause and emits a
`TransactionError` event. Already-typed `ZamaError` causes pass through.
- `EthersProvider` swaps four bare `new Error(...)` for `ConfigurationError`
(block/timestamp/fee data) and `TransactionRevertedError` (receipt not
found). `WagmiProvider` does the same for the two "no public client"
cases. These now survive the `if (error instanceof ZamaError) throw error;`
branch in `broadcast()` instead of being re-wrapped into generic
`TransactionRevertedError("Broadcast failed for …")`.
- `prepareShield` / `prepareApproveUnderlying` now call
`requireAlignedWalletAccount` to fail fast on a wrong-chain signer before
any custodian ceremony runs.
- Lifts the two-phase unshield orchestration WHY out of a hidden `//` block
comment into proper JSDoc on `prepareUnwrap`, `prepareUnwrapAll`, and
`prepareFinalizeUnwrap` — now surfaces in IDE hover.
- Tests: shield.test.ts uses `ContractFunctionRevertedError`-named errors
for revert paths and adds a new test locking in the "transport errors are
NOT cached" contract. ethers.test.ts message-substring assertions match
the new typed-error messages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…h (SDK-75) Follow-on test expansion + small refactors after the deferred-signing pipeline shipped. No new public API, no behavioural change. - packages/react-sdk/src/wagmi/__tests__/wagmi-provider.test.ts: new file covering WagmiProvider.sendRawTransaction and prepareTransaction — happy paths plus the ConfigurationError thrown when no public client is configured for the active chain. - packages/sdk/src/token/__tests__/capability-atomic.test.ts: new file asserting Token atomic methods throw SignerCapabilityError on a broadcast-only signer, with the message pointing callers at the deferred prepare*/complete* surface. - Tighten existing offline-signing-service, prepare-token, and shared test-fixtures coverage for the deferred pipeline (mock broadcaster helpers, hex-shape assertions). - Provider polish: viem, ethers, and wagmi adapters get small doc and error-shape refinements to keep their deferred-signing methods aligned with the GenericProvider contract. - Token + BaseSigner: minor doc and type polish surfaced by the new tests. - types/prepared-tx.ts + types/signer.ts: clarifying JSDoc additions for custodian integrators. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…gner Replaces the anonymous capability-bag arms (WriteContractCapability / SignTransactionCapability) with two named, exported interfaces so adapter authors get a TS class-level error at the declaration when they forget a capability method. - `OnlineSigner` — `writeContract` required, `signTransaction` optional. - `OfflineSigner` — `signTransaction` required, `writeContract` optional. - `GenericSigner = OnlineSigner | OfflineSigner` — unchanged consumer surface; every SDK API still accepts the union and gates the wrong-flavour case at runtime via `SignerCapabilityError`. - Concrete adapters now declare intent via `implements`: `ViemSigner`, `EthersSigner`, `WagmiSigner` → `OnlineSigner`; `BroadcastSigner` → `OfflineSigner`. - `SignerCore` renamed to `CoreSigner` for naming consistency with the two flavour-named interfaces. No runtime behaviour changes; no test changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…deferred-signing-v2 # Conflicts: # packages/sdk/src/token/token.ts # packages/sdk/src/zama-sdk.ts
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Architectural reframe of the deferred-signing path. Real custodian flows split between in-process via viem/ethers adapter (atomic, unchanged) and true cross-process where the SDK process holds no signer at all — the back-end signer service uses the custodian's raw API directly. The synchronous-broker BroadcastSigner shape modeled a third pattern that doesn't exist in production. Breaking changes (alpha.35 → next): - Delete BroadcastSigner + Broadcaster. Custodian-side integration is either viem/ethers adapter (OnlineSigner) or cross-process with no SDK-level signer. In-process OfflineSigner use cases subclass BaseSigner directly; ensureHexSignature moves to signer/util.ts. - Add required from: Address to every PrepareRequest variant. prepare reads from from the request; a configured signer that disagrees throws SignerAddressMismatchError. Rename ConfidentialTransferFromRequest.from to owner to free the slot for the tx-sender. - prepare/broadcast/registerPermit/completeFromTxHash/refreshPrepared drop their signer-required guards — they work signer-optional, which is the canonical shape for cross-process flows. sign/execute keep their guards (in-process flows for tests, dev tools). - Drop execute(PreparedTransaction) overload. Callers chain await broadcast(prepared, await sign(prepared)) so call shapes stay visible at the call site. Additions: - sdk.registerPermit(prepared, signature) — kind-restricted to PermitKind; lands the back-end-of-flow for typed-data permits separately from broadcast (different destination + result shape). - sdk.refreshPrepared(prepared) — re-stamps nonce + fees from current chain state, leaves the original prepared immutable. - OfflineSigningOptions gains nonce, maxFeePerGas, maxPriorityFeePerGas, gasLimit overrides; threaded through to GenericProvider.prepareTransaction. - GenericProvider.prepareTransaction args type soft-extended with the same optional fields; viem/ethers/wagmi adapters honor them. - New types: PreparedCredentialPermit, PreparedPermitFor<K>, CredentialPermitContext, CredentialPermitResult, PermitKind, TxKind, SignerAddressMismatchError. react-sdk deferred-signing hooks (under packages/react-sdk/src/broadcast/): - Tier-1: useExecute(request) — generic mutation mirroring atomic-hook ergonomics for the deferred path. - Tier-2: usePrepare, useSign, useBroadcast, useRegisterPermit, useCompleteFromTxHash, useRefreshPrepared — generic mutations for callers needing explicit phase control (custodian approval UIs, air-gapped tooling). All three-layer (core action → options factory → hook); cache invalidation flows from SDK events, not duplicated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collapse CoreSigner + OnlineSigner + OfflineSigner + GenericSigner into one flat GenericSigner with optional writeContract and signTransaction. Reverses the type-axis split from 1b61033 — the union was paying compile-time rent for a single "at-least-one-capability" guarantee, and the Online/Offline naming mapped poorly to deployment topology (Dfns viem WalletClient is "online" in the type sense but signs off-process at the custodian). Runtime capability guards (assertWriteContract, assertSignTransaction) and SignerCapabilityError remain the source of truth for wrong-flavour-for-the- method errors; the type system was never narrowing them anyway. Updates ViemSigner, EthersSigner, WagmiSigner, and BaseSigner accordingly, prunes the public re-exports, and refreshes the api.md snapshots. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The deferred-signing surface now lives exclusively on ZamaSDK. The
ticket's Layering note is explicit that the build/sign/broadcast
separation is a separate layer from Token's multi-step orchestrators
(prepareShield, prepareUnshield, prepareUnshieldAll) and that the two
concerns should not be conflated. The v2 plan's original "open question
1" resolved the same way; a later commit re-added the sugar; this
reverses that.
Removes 9 prepareX/completeX pairs and the #senderAddress private
helper from Token, plus their direct test cases in prepare-token.test.ts
(prepareConfidentialTransfer, prepareDelegateDecryption blocks).
prepareShield and its routing tests stay — those are layer-1.
Callers wanting deferred-flow ergonomics on Token use
sdk.prepare({ kind, from, ... }) directly; the React-SDK query layer
already targets that surface.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signing belongs to the caller, not the SDK. The ticket's invariant "keys never enter the SDK — the SDK hands out an unsigned transaction and accepts a signed one back" describes a two-method shape: hand-out (prepare) and accept-back (broadcast). Sign is performed between those by whatever holds the keys — the SDK's job is to build and submit, not to invoke signing on the caller's behalf. ZamaSDK.sign() was a thin wrapper around signer.signTransaction() that added no SDK capability. Callers in the deferred flow now compose const prepared = await sdk.prepare(request); const signedTx = await signer.signTransaction(prepared.unsignedTx); const result = await sdk.broadcast(prepared, signedTx); — making the prepare → caller-signs → broadcast call shape visible at the call site, which matches the ticket's "build, sign, and broadcast steps are independently composable" framing. execute() keeps its bundled-flow shape and continues to call signer.signTransaction internally; that's implementation detail for the atomic-shape convenience entry point, not a public signing surface. Removes: - ZamaSDK.sign() public method - signMutationOptions / SignParams from @zama-fhe/sdk/query - useSign hook and its test - 2 sign-specific tests; refactors the SignerNotConfiguredError signer-absent test to drive through execute() Updates JSDoc in zama-sdk, usePrepare, useExecute, useRefreshPrepared to point at signer.signTransaction(prepared.unsignedTx) instead of sdk.sign / useSign. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…oval Captures the public surface narrowing from Q2 (Token per-op prepareX/completeX gone) and Q7 (sdk.sign + useSign + signMutationOptions gone) in the API extractor snapshots. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After removing sdk.sign() from the public API, three JSDoc @link references to it remained dangling on prepare(), refreshPrepared(), and the service's prepare(). Replace with explicit \`signer.signTransaction(...)\` wording so the docs reflect the actual flow (caller signs externally; SDK builds and submits). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restores sdk.sign + useSign with a refined JSDoc making its scope
explicit: in-process convenience that delegates to
signer.signTransaction with capability checks and event/error
integration. The keys-never-enter-the-SDK invariant is structural
(this runs in the caller's process against the caller's signer
object) and stated in the JSDoc.
The Q7 removal traded ergonomics for naming clarity. The trade wasn't
worth it: KMS-can't-sign and cross-process custody callers naturally
route to "prepare → external sign → broadcast" because sdk.sign()
throws SignerNotConfiguredError / SignerCapabilityError in those
cases — i.e. they already weren't using it. The removal only hurt the
in-process case, where the directed errors + event emission +
SigningFailedError wrapping are real value.
Restores:
- ZamaSDK.sign(prepared, options?) with refined JSDoc clarifying scope
(cross-process custody and permit-only KMS use prepare → external
sign → broadcast instead; this method is the in-process convenience)
- signMutationOptions + SignParams in @zama-fhe/sdk/query
- useSign hook + test
- {@link sign} / {@link useSign} JSDoc cross-references on prepare,
refreshPrepared, usePrepare, useExecute, useRefreshPrepared, service
prepare
- Two service tests that exercised sign behavior (success path +
SigningFailedError wrapping on HSM rejection)
- Refreshed api.md snapshots
Reverts: 1aa4179 + 64596e4
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rmit/completeFromTxHash
These four methods carried an _options?: OfflineSigningOptions parameter
that no code path consumed — a forward-compatibility hedge that just
added boilerplate at every call site and tightened the public surface
without buying anything. Removed at the service, ZamaSDK, query-helper,
and mutation-params layers.
prepare(), execute(), and refreshPrepared() keep their options arg —
those actually use the nonce/fee/gas overrides for custodian flows.
API surface tightening for callers; existing code that passed
{ ...options } to broadcast/sign/etc. will get a typecheck error and a
trivial mechanical fix (drop the third arg).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The error class previously embedded a switch on capability that named
specific SDK call shapes ("Use the deferred prepare*/complete* path",
"BaseSigner subclass"). That coupled the error to API surface — every
public-API rename risked rotting the message in errors/signer.ts.
Hoist the per-capability guidance into capabilities.ts as
WRITE_CONTRACT_HINT / SIGN_TRANSACTION_HINT constants — they live next
to the assertion helpers that consume them, which is the file someone
editing the capability surface already touches. SignerCapabilityError
becomes structural:
- accepts hint?: string as a constructor arg (passed by the assertion
helpers, or by callers wanting context-specific guidance)
- exposes readonly hint: string | undefined for programmatic readers
(UIs, telemetry) that want to render headline + suggestion separately
- folds hint into .message when present, so console.error stays useful
cause stays reserved for actual error chaining (SigningFailedError
wrapping the underlying signer's throw, etc.). Decoupling guidance from
the error class doesn't change behaviour — same message text reaches
the caller, same instanceof checks work — but means future API renames
update one file, not two.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The field exposed the hint string for hypothetical programmatic readers (UIs rendering headline + suggestion separately), but no code in the SDK or React-SDK actually reads error.hint — it was speculative API. The hint is still folded into .message via the constructor, so the user-facing behaviour is unchanged. YAGNI cleanup; the structured-field shape can come back when there's a real consumer. Also trims the verbose JSDoc on the error class to one sentence — the design rationale (decoupling from call-shape vocabulary) lives in the commit history, not in public API docs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The fixtures exposed three parallel signer abstractions for tests:
broadcaster: SignerThunks (mock thunk pair)
broadcastSigner: TestOfflineSigner extends BaseSigner (wraps thunks)
signer: GenericSigner from createMockSigner (plain mock)
After the signer flatten removed OfflineSigner, the TestOfflineSigner
class was a structurally-broken vestige (its implements clause pointed
at a type that no longer existed; tsc happens to ignore unresolved
interfaces in implements). The class added nothing tests couldn't get
from createMockSigner — both produce a value that satisfies
GenericSigner; only the latter does so without an unused BaseSigner
subclass + thunks indirection.
Deletes:
- TestOfflineSigner class
- SignerThunks interface
- createMockSignerThunks helper
- (the broadcaster + broadcastSigner fixture properties were already
removed in a prior edit; this completes the cleanup)
Renames in 3 test files (~120 references): broadcastSigner -> signer,
broadcaster.X -> signer.X (assertions now run against the unified
signer fixture's vi.fn() mocks).
createMockSigner now mocks signTransaction by default so tests
exercising the deferred path get a fully-capable signer without
overrides. Tests that need to assert capability errors (e.g.
capability-atomic.test.ts) construct sign-only signers explicitly via
createMockSigner({ writeContract: undefined }).
Net: test-fixtures.ts loses ~45 lines; three test files lose three
import paths and a layer of indirection; the conceptual model
collapses from three signer-shapes to one.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SDK-75 deferred-signing additions (registerPermit, refreshPrepared, new query factories, signer-optional plumbing) pushed brotli sizes ~3 kB over budget. Raise ESM 66→72 kB and CJS 57→64 kB to give ~3 kB headroom; react-sdk stays at 15 kB (well under). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
api-extractor flagged tsdoc-code-span-missing-delimiter because the backtick-quoted `wrapper.unwrap(...)` call wrapped across two lines. Reflow the docstring so the code span stays on a single line and refresh the sdk-query api.md snapshot for the shifted chunk hash. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Public API Changes
|
Narrow CredentialService.registerSignedPermit's keypair param to Pick<StoredKeypair, "publicKey"> so OfflineSigningService can pass a real object instead of casting reconstructed fields through `as never`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace backtick-wrapped symbol references with `{@link}` tags across
SDK and React-SDK tsdoc so API doc generators can resolve them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rtHex Add `assertHex` to utils/assertions and use it in OfflineSigningService.registerPermit so the boundary-input check follows the same TypeError-throwing convention as assertBigint et al. Changes the documented `@throws` from SigningFailedError to TypeError. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…erload bug Adds a vitest integration test that exercises the cross-process custody flow against real DFNS, real Sepolia, and real FHEVM relayer: prepare → DFNS policy-engine approval → broadcast / registerPermit. Env vars are validated through zod/mini; dotenv/config is loaded as a vitest setupFile so `.env` is picked up automatically. Fixes a latent overload-resolution bug surfaced by the test: EthersProvider.prepareTransaction used iface.encodeFunctionData(name) which throws on ERC-7984's overloaded confidentialTransfer (2-arg vs 3-arg). Now resolves the fragment by name + arity via forEachFunction before encoding. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The wallet wrapper from @dfns/lib-ethersjs6 was only used to discover the DFNS wallet address; its signing methods were abandoned when the test switched to the policy-aware async signing API. Fetch the address directly via `dfnsClient.wallets.getWallet`, drop the dependency, and share the evmAddress schema with other env validators. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… (SDK-75)
Move prepare / sign / broadcast / execute / registerPermit /
completeFromTxHash / refreshPrepared off ZamaSDK and onto a dedicated
sub-client at sdk.offline, paired with OfflineClient.
The "offline" naming axis matches the ecosystem-standard term ("offline
signing", as used by ethers, web3.py, and HSM vendors) and aligns with
the existing OnlineSigner / OfflineSigner capability split. Atomic call
sites (Token.confidentialTransfer, etc.) are unchanged and still flow
through the OnlineSigner path on Token.
Also renames the react-sdk broadcast/ directory to offline/ for the
same reason — the directory holds the entire offline-signing hook
surface, not just useBroadcast.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lary
Reshapes the offline-signing surface on `OfflineClient`,
`OfflineSigningService`, and the query/hook layers to match the
"transfer / sign-and-broadcast / sign-only" three-tier vocabulary
common to custody platforms.
- Split `execute` into `signAndBroadcast` (transactions) and
`signAndRegister` (credential permits). Each has its own mutation
factory, hook, and tests; the old umbrella `execute` / `useExecute`
is gone.
- Rename `completeFromTxHash` → `attach` and `refreshPrepared` →
`refresh` on the service and client. The mutation factory and hook
for refresh keep their current names since they remain unique.
- Move per-kind prepare request types from `types/prepared-tx.ts` to
the more accurate `types/offline.ts`.
- Strip vendor-specific names from JSDoc (Dfns, Fireblocks, Fordefi,
Turnkey) — the surface is provider-neutral; vendor mentions stay
only in the DFNS integration test file. Fix stale `{@link execute}`
references introduced by the split.
API extractor reports regenerated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…deferred-signing-v2 # Conflicts: # docs/llm/corpus-manifest.json # packages/react-sdk/etc/react-sdk.api.md # packages/sdk/etc/sdk-query.api.md # packages/sdk/etc/sdk.api.md # packages/sdk/src/index.ts # packages/sdk/src/token/__tests__/shield.test.ts # packages/sdk/src/token/index.ts # packages/sdk/src/token/token.ts # pnpm-lock.yaml
Matches the caller-intent vocabulary of sibling methods (prepare, sign, broadcast, refresh) and frames the cross-process flow as a lifecycle resumption rather than a debugger-style attach. Touches the React hook (useAttach → useResume), the query factory (attachMutationOptions → resumeMutationOptions, AttachParams → ResumeParams, mutation key zama.attach → zama.resume), and downstream JSDoc / error-message references. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Regenerates etc/*.api.md after the attach → resume rename and reflows two JSDoc comments where the `prepare → sign → broadcast` code span spanned two lines — TSDoc treats each comment line independently, so the opening backtick was flagged as unclosed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…deferred-signing-v2 # Conflicts: # packages/sdk/etc/sdk-query.api.md # packages/sdk/src/services/delegation-service.ts # packages/sdk/src/token/token.ts # packages/sdk/src/token/wrapped-token.ts
…deferred-signing-v2
…deferred-signing-v2 # Conflicts: # docs/llm/corpus-manifest.json # packages/sdk/src/credentials/credential-service.ts # packages/sdk/src/errors/index.ts # packages/sdk/src/errors/signer.ts # packages/sdk/src/index.ts # packages/sdk/src/namespaces/index.ts # packages/sdk/src/query/index.ts # packages/sdk/src/test-fixtures/constants.ts # packages/sdk/src/test-fixtures/index.ts # packages/sdk/src/test-fixtures/provider.ts # packages/sdk/src/test-fixtures/signer.ts # packages/sdk/src/token/token.ts # packages/sdk/src/token/wrapped-token.ts # packages/sdk/src/zama-sdk.ts
…deferred-signing-v2 # Conflicts: # docs/llm/corpus-manifest.json # package.json # packages/react-sdk/etc/react-sdk.api.md # packages/sdk/package.json
Refreshes API reports for sdk, sdk-query, and react-sdk to reflect the combined surface after merging prerelease — drops the removed FHE primitive hooks and EIP712 helpers, picks up the deferred-signing-v2 additions.
Renames the public `offline` namespace property to `offlineSigning` for clarity (it's the offline-signing pipeline, not generic offline support). The underlying class file is renamed `offline.ts` -> `offline-signing.ts` and re-exported as `Offline` to keep the published class name stable. All internal call sites, queries, hooks, tests, docstrings, and api-extractor reports are updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
oxlint flags the leading underscore as a dangling identifier. Renaming to `unhandled` preserves the compile-time exhaustiveness check while passing lint.
…deferred-signing-v2 # Conflicts: # docs/llm/corpus-manifest.json # package.json # packages/sdk/etc/sdk-query.api.md # packages/sdk/etc/sdk.api.md # packages/sdk/src/credentials/credential-service.ts # packages/sdk/src/index.ts # pnpm-lock.yaml
…deferred-signing-v2 # Conflicts: # docs/llm/corpus-manifest.json # packages/react-sdk/etc/react-sdk.api.md # packages/sdk/etc/sdk-ethers.api.md # packages/sdk/etc/sdk-query.api.md # packages/sdk/etc/sdk-viem.api.md # packages/sdk/etc/sdk.api.md # packages/sdk/package.json # packages/sdk/src/index.ts # packages/sdk/src/token/token.ts # pnpm-lock.yaml
…SDK-75] ViemProvider and EthersProvider pass `blockTag: "pending"` / the `"pending"` second arg to `getTransactionCount` in `prepareTransaction`. Parallel custodian queues with multiple in-flight approvals against the same wallet no longer collide on a stale "latest" count; callers that own their own nonce manager keep the `options.nonce` bypass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…K-75] The bundled "sign-and-broadcast" tier was one-call sugar over prepare → sign → broadcast, with no flow it unlocked: in-process atomic signing is already covered by Token.* methods, and cross-process custody needs the phase-separated surface anyway. Custody platforms (DFNS, Fireblocks, Fordefi, Turnkey) commonly expose this middle tier; we deliberately omit it to keep the offline-signing surface to two surfaces — atomic in-process (Token.*) and phase-separated. Removes signAndBroadcast/signAndRegister from OfflineSigning namespace and OfflineSigningService, the sign-and-broadcast/sign-and-register query mutations and React hooks, and the corresponding tests (coverage preserved by rewriting CredentialPermit tests as explicit prepare → signTypedData → registerPermit chains). DFNS review doc updated to reflect the two-surface model and the omission rationale. API extractor reports and LLM corpus manifest regenerated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… caveats [SDK-75] Both warnings were referenced as JSDoc-documented by the DFNS partnership review doc but only lived in the prose. Move them to the canonical OfflineSigning namespace and OfflineSigningService JSDoc so out-of-process custody integrators (DFNS, Fireblocks, Fordefi, Turnkey, HSM adapters) encounter them at the call site: - resume(prepared, txHash): the SDK takes the caller's word that txHash corresponds to prepared.unsignedTx; no on-chain check confirms the broadcaster signed this payload rather than a different one from the same from. - refresh(prepared): identity is not stable — returned unsignedTx bytes and tx hash differ; callers keying external approvals by the unsigned-tx bytes must treat the refreshed payload as a new submission. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the offline-signing pipeline for SDK-75 — separates
prepare,sign, andbroadcastfor institutional custody, HSM, and policy-engine workflows where the three phases cannot run synchronously in a single Promise.Closes SDK-75.
Atomic call sites are unchanged. Online signers keep their
writeContractpath and existing flows work identically. The new surface is a parallel route for signers that exposesignTransactioninstead, exposed via a dedicatedsdk.offline.*sub-client.What ships
sdk.offline.*sub-clientThe pipeline lives on a domain sub-client, mirroring the "transfer / sign-and-broadcast / sign-only" three-tier vocabulary custody platforms expose:
Backed by
OfflineSigningService. The atomic path (Token.confidentialTransfer, etc.) is not on this client — it remains onTokenfor online-signer call sites. The surface is provider-neutral (no vendor lock-in in JSDoc); industry vocabulary is the anchor.Signer capability bag
GenericSigner.writeContractis optional; new optionalsignTransaction(unsignedTx)covers the deferred path.BaseSigneris the public base class; wrap a custodian's API client by extending it. Keys never enter the SDK.assertWriteContract/assertSignTransactionTS-assertsguards insigner/capabilities.ts. Capability errors are typed viaSignerCapabilityError.Provider
GenericProvider.sendRawTransaction(signedTx)— required. Wired in viem, ethers, wagmi.GenericProvider.prepareTransaction({ from, call })— required. Builds RLP-encoded EIP-1559 unsigned tx (encode + estimateGas + getTransactionCount + estimateFeesPerGas + serializeTransaction). Wired in all three adapters.EthersProvider.prepareTransaction): resolve overloaded ABI entries by name + arity. Bareiface.encodeFunctionData(name, args)throws on ERC-7984'sconfidentialTransfer(address,bytes32)vsconfidentialTransfer(address,bytes32,bytes)because ethers' overload-disambiguation treats the last value as a potential overrides object.Eleven transaction kinds wired
ConfidentialTransfer,ConfidentialTransferFrom,SetOperator,Unwrap,UnwrapAll,FinalizeUnwrap,ApproveUnderlying,Wrap,TransferAndCall,DelegateDecryption,RevokeDelegation.FinalizeUnwrapdoes thepublicDecryptround-trip duringprepare.UnwrapAllreads the on-chain balance handle instead of encrypting.Token-level sugar
WrappedToken.prepareShield(amount, options?)— multi-step planner that mirrors the atomic shield routing (1363 → singleTransferAndCallstep, otherwiseApproveUnderlying+Wrap). Returns aShieldPlanwhose steps each carry aPreparedFor<K>payload ready to feed intosdk.offline.sign/broadcast(orsignAndBroadcast).EIP-712 permits
sdk.offline.prepare({ kind: "CredentialPermit", contracts, delegator? })returns the typed-data envelope; sign it via the custodian (in-process or out-of-process) and callsdk.offline.registerPermit(prepared, signature). The bundledsdk.offline.signAndRegister(request)does both in one call for signers available in-process.DFNS integration test
packages/sdk/src/services/__tests__/dfns.integration.test.ts— real DFNS + real Sepolia + real FHEVM relayer. Demonstrates the canonical cross-process custody flow with DFNS's async policy-approval API. Env-gated via zod, skips cleanly without creds.pnpm test:integrationruns the suite (withdotenv/configautoload). Satisfies SDK-75 acceptance criterion #7 (reference example against a real custodian environment).What's deliberately not in this PR
token.confidentialTransfer(...)against DFNS). Scoped out — atomic stays unchanged for existing callers.pending-block-tag nonce reads inprepareTransaction. Sequential broadcasts wait for receipt (slow but correct). Flagged as a follow-up for pipelined custodian flows.How callers use it
Two integration patterns, picked by whether the custodian's adapter handles the SDK transparently or you need to model policy approval explicitly.
Path A — in-process via the custodian's viem/ethers adapter
Use when the custodian ships a viem
WalletClientor ethersSigneradapter and policy approval is either disabled or instantaneous. Atomic Token methods just work — no SDK changes.Path B — cross-process deferred (the canonical custodian pattern)
Web initiator runs the SDK without a signer. Back-end signer service holds the custodian's API client. Zama SDK is not on the back-end — it only deals with bytes.
Transactions — using
resumesince the custodian broadcasts via its own infrastructure:Credential permits (EIP-712) — same shape, different leaf method:
Air-gapped tooling is a special case of Path B with manual byte transfer (the back-end queue becomes a USB stick).
React hooks
For the cross-process custodian pattern where the back-end broadcasts directly, swap
useBroadcastforuseResume({ prepared, txHash }).Test plan
pnpm typecheck— clean across the workspacepnpm test:run— 1525 pass, 5 skippedae-forgotten-exportwarningsRisks
GenericProvidergains two new required methods. Custom adapter implementers (none known in the wild) must implement both; viem / ethers / wagmi adapters are wired here.GenericSigner.writeContractflipped from required → optional. Any third-party signer adapter typed viaimplements GenericSigneris unaffected; one typed via a structural assignment will now narrowwriteContractto optional and may need callers to handle the absent case (or just throw via the existingassertWriteContractguard).PreparedTransaction.requestcarriesbigintfields on several kinds — naiveJSON.stringifyon the prepared payload throws. Documented in the type's JSDoc.sdk.prepare/sign/broadcast/etc.moved undersdk.offline.*. Since the deferred-signing surface hasn't shipped yet, no real-world callers exist.🤖 Generated with Claude Code