Skip to content

feat(sdk): complete the typed build, sign, submit API for exchange actions - #168

Merged
joeblau merged 1 commit into
mainfrom
feat/157-build-sign-submit
Oct 6, 2026
Merged

joeblau merged 1 commit into
mainfrom
feat/157-build-sign-submit

Conversation

@joeblau

@joeblau joeblau commented Oct 6, 2026

Copy link
Copy Markdown

Summary

Most of the build → sign → submit core landed with #166: canonical builders for every exchange operation, signAction / submitAction / executeAction, ExchangeClient.sign / submit / execute, cached L1 bytes for reused actions, and the stage and contract tests. This PR covers what #157 still needed:

  • Cancellation. The docs now say what an abort does at each stage. An abort before signing consumes no nonce. An abort while the wallet signs burns the nonce and posts nothing. A signed request is not tied to its signing signal. Aborting submit cannot recall a request that was already sent, and resubmitting the same signed request cannot apply it twice. A new test covers each case.
  • Paired measurements. .dev/perf/canonical_reuse.ts compares direct, fresh, reused and staged calls. It uses stub, viem and fast (WASM) wallets over an in-memory transport and the real HttpTransport, and rotates cases inside each round. Results are below.
  • Cheaper fresh builds. immutableCopy now uses plain assignment instead of Object.defineProperty per key, except for own __proto__ keys, which are still defined explicitly. Key order, deep freezing and the prototype are unchanged, and a new test checks this. A fresh 100-order build on Node dropped from about 1.38x to 1.12x of the raw method.
  • Style. All 60 builders now have @param / @return / @throws JSDoc like the method functions; the 6 alias re-exports have no JSDoc of their own. Imports in client.ts and _shell.ts now sit below the module header. executeWithShell documents prepareOnly.

Scope: nothing from #158–#161 is included (no dispatch policy, order batcher or per-operation entry-point changes). bun run perf is untouched, so the CI perf suite fingerprint stays the same.

Acceptance criteria

  • Building performs no wallet calls, nonce allocation, or network requests. Builders are synchronous and take no config (issued stays 0 after buildOrder).
  • Signing consumes exactly one nonce and posts nothing; submitting neither allocates a nonce nor re-signs (_actions.test.ts, plus signCount/nonces in the published consumer check).
  • Response types stay inferred across build, sign, submit and execute (OrderSuccessResponse assignments in tests and in consumer.ts under tsc nodenext).
  • Signed wire bodies and signatures match the existing path for L1 (order), user-signed (usdSend), unnamed and named approveAgent, createVault and agentSendAsset (nonce inside the action), and userSetAbstraction (multi-sig payload transform), each in single and multi-sig mode. l1Cache.test.ts covers hash equality across nonces, vaults and expiry.
  • Caller mutations cannot change a built action or signed request: deep copy and freeze, __proto__-safe. Reconstructed or JSON-roundtripped actions and requests are rejected, and so is a cross-network submit.
  • Existing clients, raw functions and prepareRequest / submitPrepared are unchanged.
  • Focused stage and contract tests (now including cancellation), and paired offline measurements of fresh versus reused actions with stub and real wallets (below).
  • Docs cover lifecycle, reuse (including its cost), cancellation, nonce freshness and serialization (clients.md, "Canonical actions and explicit execution").

Measurements

bun|node .dev/perf/canonical_reuse.ts, Apple M3 Max. Each cell is the median of 11 rotating rounds after 3 warmup rounds, in µs per call, with the ratio to direct. CPU time only, using mocks: no network latency.

  • direct: raw order()
  • fresh: buildOrder() + executeAction() on every call
  • reused: one built action passed to executeAction()
  • staged: one built action, then signAction() + submitAction()

Bun 1.4.0

wallet / wire / orders direct fresh reused staged
stub / memory / 1 5.0 6.0 (1.19x) 3.5 (0.70x) 4.1 (0.81x)
stub / memory / 100 142.5 188.4 (1.32x) 3.7 (0.03x) 4.2 (0.03x)
stub / http / 1 7.0 9.2 (1.30x) 6.0 (0.85x) 6.7 (0.95x)
stub / http / 100 155.9 219.2 (1.41x) 13.2 (0.08x) 14.0 (0.09x)
viem / memory / 1 98.0 100.3 (1.02x) 96.0 (0.98x) 98.5 (1.00x)
viem / memory / 100 234.3 275.9 (1.18x) 96.0 (0.41x) 99.9 (0.43x)
viem / http / 1 99.0 103.6 (1.05x) 100.0 (1.01x) 99.4 (1.00x)
viem / http / 100 246.6 311.3 (1.26x) 98.8 (0.40x) 102.4 (0.42x)
fast / memory / 1 65.8 66.9 (1.02x) 63.8 (0.97x) 64.9 (0.99x)
fast / memory / 100 199.9 238.3 (1.19x) 65.3 (0.33x) 66.6 (0.33x)
fast / http / 1 69.4 72.1 (1.04x) 66.4 (0.96x) 67.4 (0.97x)
fast / http / 100 241.5 315.1 (1.30x) 85.8 (0.36x) 88.1 (0.36x)

Node 24.10.0

wallet / wire / orders direct fresh reused staged
stub / memory / 1 7.0 8.3 (1.19x) 4.7 (0.67x) 5.5 (0.78x)
stub / memory / 100 234.4 262.4 (1.12x) 5.7 (0.02x) 6.5 (0.03x)
stub / http / 1 13.0 14.7 (1.13x) 10.3 (0.79x) 11.4 (0.87x)
stub / http / 100 265.3 294.9 (1.11x) 26.1 (0.10x) 26.9 (0.10x)
viem / memory / 1 184.4 185.0 (1.00x) 180.0 (0.98x) 180.8 (0.98x)
viem / memory / 100 400.6 432.3 (1.08x) 179.0 (0.45x) 179.7 (0.45x)
viem / http / 1 189.9 192.0 (1.01x) 186.2 (0.98x) 188.3 (0.99x)
viem / http / 100 472.9 482.4 (1.02x) 215.9 (0.46x) 219.1 (0.46x)
fast / memory / 1 78.8 80.0 (1.01x) 76.1 (0.97x) 76.8 (0.97x)
fast / memory / 100 302.9 329.7 (1.09x) 77.6 (0.26x) 79.6 (0.26x)
fast / http / 1 85.1 88.0 (1.03x) 82.3 (0.97x) 83.6 (0.98x)
fast / http / 100 338.0 367.6 (1.09x) 98.3 (0.29x) 99.2 (0.29x)

What the numbers show:

  • Reuse is the win. With 100 orders, a reused action costs 0.26–0.46x of the raw call with a real wallet, because it skips validation, copying and MessagePack encoding. With one order, signing time dominates and reuse is about even.
  • Building fresh every call costs 1.0–1.4x of the raw call, because the builder also copies and freezes the action. The docs recommend raw methods for one-off actions.
  • The copy change helped fresh builds. Before it, fresh 100-order calls on Node measured 323 µs (stub/memory, now 262) and 498 µs (viem/memory, now 432).
  • Staging is cheap. Separate sign and submit add no more than about 1 µs per order over executeAction, and the 1-order stub rows stay under 1.5 µs.

Test plan

  • bun run check (format, lint, docs, types, ts7, jsdoc, export, imports)
  • bun run test:offline: 2024 pass, 0 fail (19 in _actions.test.ts, including new immutable-copy and cancellation tests)
  • bun run build, including the published consumer checks on Bun and Node
  • .dev/perf/canonical_reuse.ts on Bun and Node (tables above)

Closes #157

🤖 Generated with Claude Code

…tions

- Document every operation builder with @param/@return/@throws like the method functions.
- Cover cancellation at each stage in the docs and in tests: an abort before signing frees the
  nonce, an abort during signing burns it without posting, and submit forwards its signal.
- Copy builder input with plain assignment (own __proto__ keys still preserved), which makes
  fresh builds cheaper, especially on Node.
- Add .dev/perf/canonical_reuse.ts: paired offline direct/fresh/reused/staged measurements with
  stub, viem and fast wallets over in-memory and HTTP transports.
- Move imports below the module headers in client.ts and _shell.ts and document prepareOnly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@joeblau
joeblau merged commit 183370b into main Oct 6, 2026
6 of 7 checks passed
@joeblau
joeblau deleted the feat/157-build-sign-submit branch October 6, 2026 23:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expose a typed build → sign → submit API for exchange actions

1 participant