Skip to content

feat(evm): share browser payment APIs and durable payment recovery - #17

Draft
mickvandijke wants to merge 9 commits into
mainfrom
web-support
Draft

mickvandijke wants to merge 9 commits into
mainfrom
web-support

Conversation

@mickvandijke

@mickvandijke mickvandijke commented Sep 15, 2026

Copy link
Copy Markdown
Member

Summary

Allows browser clients to use the same EVM payment types, quote hashing, pricing, Merkle algorithms, and vault encodings as native clients. The portable layer excludes native process and environment facilities; optional RPC uses browser Fetch and timers, while the default native feature retains Anvil and native wallet support.

Merkle checkpoints preserve the original salts and random padding so recovery reconstructs the same payment intent. Native wallets can prepare and sign a payment without broadcasting, persist its exact transaction bytes, then submit or observe that same transaction after interruption. Journal validation binds the transaction to its wallet, chain, vault, and intended calldata.

Recovery distinguishes pending transactions, unfinalized failures, finalized reverts, and finalized nonce replacements. A same-intent fee replacement returns its successful receipt and settlement hash; a different finalized nonce consumer permits a fresh attempt. Missing history, RPC errors, and unexplained nonce advances retain the journal. Failed-payment retry decisions use canonical receipts and the RPC finalized tag; successful payments keep the existing optimistic canonical-inclusion semantics. See PAYMENT_RECOVERY.md for finality and historical-RPC requirements.

Native-only tests are feature-gated, and CI covers both portable and RPC-only test configurations.

Linear issue

Closes V2-803 — Linear issue

Risk tier

  • T0 — docs / tooling / CI / pure UX-output. Repo CI only.
  • T1 — client-only, no network-facing behavior change. CI + prod compat smoke.
  • T2 — node/client logic with behavioral surface, no protocol/format/economics change. Dev testnet + ADR.
  • T3 — protocol / storage format / payments / routing. T2 evidence + adversarial testing.

Proposed for human review: changes payment submission and recovery behavior.

Compatibility

  • Wire: Existing quote hashes, vault ABI, payment proofs, and native serialization remain shared and unchanged. External signers reuse the native Merkle call encoder and event decoder.
  • Storage: Adds serializable Merkle checkpoints preserving salts and random padding; no on-chain storage change. Native signed-payment journals retain exact transaction bytes.
  • API: Default native wallet APIs remain available. Adds portable/RPC feature selection, shared encoding helpers, Merkle checkpoint serialization, and native prepare/broadcast/observe payment APIs.

Consumers of the draft journal API must handle PaymentStatus::Finalizing and PaymentStatus::Replaced. The stored SignedPayment representation is unchanged.

Semver impact

  • breaking
  • feature
  • fix

Test evidence

Validated on the current revision during this refresh:

  • cargo test --test wallet journaled: 7 passed, covering canonical success, Merkle winner recovery, reverted transactions across a reorg, finality before retry, finalized nonce replacement, successful fee replacement, and unexplained nonce consumption.
  • cargo test --no-default-features: 10 tests passed; native-only integration targets are gated out.
  • cargo test --lib --test cryptography --no-default-features --features rpc,external-signer: 19 passed.
  • cargo clippy --all-targets --all-features -- -D warnings: passed.
  • cargo check --lib --no-default-features --features rpc,external-signer --target wasm32-unknown-unknown: passed.

The coordinated ant-client recovery suite also exercises the native caller's handling of finalized replacements. Local results do not replace CI or production RPC validation.

New dependency

Adds web-time for portable clocks and WASM-target gloo-timers, futures-util, and JavaScript-enabled getrandom. Existing Alloy and Tokio dependencies use explicit feature gates. No new standalone crate or parallel browser payment implementation is introduced.

ADR

ant-client ADR-0004: Direct browser immutable-data client documents shared payment preparation, journals, and recovery across native/browser adapters. Saorsa Core ADR-019 covers the shared portable dependency architecture.

Mitigation / rollback

Keep consumers on the default native feature or revert the coordinated client/EVM revisions together. Preserve unresolved transaction journals; a missing receipt is not proof that another payment is safe.

Coordinated consumers

Current stack revisions

All PRs link to V2-803. Immutable Git revisions used by the validated stack:

PR Revision
saorsa-pqc#7 29a2b272
evmlib#17 cf424c04
saorsa-transport#160 6f0b1f62
saorsa-core#158 0df7853e
ant-protocol#29 cdae7d19
ant-node#220 26f5fb79
ant-client#186 f1303e1a

EVM recovery now retains ambiguous journals until finalized failure/replacement evidence is available. The protocol and native client include the matching consumer changes. Existing browser wire and payment formats are retained.

Measured on DEV-03 run 589 (2026-09-16) against sepolia-rollup.arbitrum.io:
26 of 938 native uploads failed at the payment step, none of them for a
network reason, all in the journal path introduced by a5f3d61:

- `observe_payment` reported `Finalizing` when a `latest` nonce read was
  higher than the nonce just signed from a `pending` read. The two reads are
  separate HTTP requests and the endpoint's replicas lag each other
  (reproduced: `pending=98` then `latest=99`, 1 pair in 150), and finality
  on Arbitrum Sepolia trails by ~28 nonces, so every stale read became
  "awaiting chain finality" on bytes that were never broadcast.
- Every read in `prepare_payment`/`observe_payment` and the broadcast itself
  were single-shot, where the legacy `send_transaction_with_retries` path
  retried three times. One `429` or `-32000 context deadline exceeded` on
  any of ~9 calls per wave failed the upload, after earlier waves had paid.

Now:

- `prepare_payment` takes the higher of a `pending` and a `latest` read, so
  one lagging replica cannot sign a consumed nonce, and sets the chain id
  explicitly so `fill` makes no RPC call.
- Reads go through `retry()`. Reads made while a caller polls use a 500ms
  base (the native client observes inside a 30s window).
- A consumed-but-unfinalised nonce with no receipt reads as `Pending`, not
  `Finalizing`: it is indistinguishable from a stale read, re-sending the
  journaled bytes is refused by the node rather than paid twice, and
  finality still resolves a real replacement into `Replaced`. `Finalizing`
  is kept for a mined revert that is not yet final.
- `broadcast_payment` retries the same bytes on transport-level failures
  (HTTP status, connection loss, timeouts, backend-deadline responses) and
  treats "already known" as success. Definitive rejections return at once.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

2 participants