Skip to content

feat(core): type a wallet balance shortfall as Error::InsufficientFunds (#152) - #224

Open
Nic-dorman wants to merge 1 commit into
mainfrom
fix/typed-insufficient-funds
Open

Nic-dorman wants to merge 1 commit into
mainfrom
fix/typed-insufficient-funds

Conversation

@Nic-dorman

@Nic-dorman Nic-dorman commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

Fixes #152. Upstream half of V2-601 (mobile SDK).

A wallet-backed payment with too few payment tokens failed with the generic data::Error::Payment(String). Consumers such as ant-ffi, the mobile SDK and the CLI could only tell "top up your wallet" apart from any other payment failure by matching message text.

Why it isn't the fix #152 describes

#152 proposed matching evmlib::wallet::Error::InsufficientTokensForQuotes(have, need) by type before the generic stringify. That's no longer possible on any path:

  • File and data uploads now go through evmlib's journaled API (native_payment.rs → Wallet::prepare_payment). It returns Result<_, String> and already calls .to_string() on that variant (evmlib-0.10.0/src/wallet/journal.rs:214).
  • ant-protocol re-exports neither evmlib::wallet::Error nor its variant, only PayForQuotesError and Wallet. ant-core's Cargo.toml forbids a direct evmlib dependency, so even the paths that still hold the typed error can't name it.

That leaves evmlib's report text as the only contract, so the fix pins it with a test.

Change

  • New data::Error::InsufficientFunds { have: Amount, need: Amount } (atto). Display: insufficient funds: wallet has {have} atto, payment needs {need} atto.
  • Error::from_payment_failure(String) parses evmlib's Insufficient tokens to pay for quotes. Have: … atto, need: … atto wherever it appears in the message (callers prefix context). Anything else stays Payment with the message verbatim, and truncated or malformed reports are not guessed at.
  • It's used at every wallet-backed payment site:
    • the journaled native path (native_payment.rs, used by file and data uploads)
    • wave-batch pay_for_quotes (batch.rs ×2)
    • Merkle pay_for_merkle_tree (merkle.rs)
    • the non-native upload adapter (upload.rs ×2)
    • single-chunk payment: payment.rs now passes the typed error through instead of re-wrapping it as Payment("On-chain payment failed: …")
  • classify_error treats it as ApplicationError, so the adaptive limiter isn't pushed down. The exhaustiveness guard and the covers-all-variants table are extended.
  • Behaviour to note:

Linear issue

Closes V2-1507

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.

Client-side error typing only. The payment flow, amounts, wire messages and node interaction are unchanged.

Compatibility

  • Wire: none
  • Storage: none
  • API: breaking for ant_core::data::Error, which gains a variant and is not #[non_exhaustive]. A downstream exhaustive match must add an arm.
    • ant-ffi (ant-sdk/ffi/rust/ant-ffi/src/lib.rs From<ant_core::data::Error>) has an other => catch-all, so it still compiles. But until it adds an arm, a shortfall maps to ClientError::InternalError where it used to map to PaymentError. That arm (→ a new ClientError::InsufficientFunds) is V2-601's follow-up.
    • The CLI message changes from payment error: Insufficient tokens to pay for quotes. Have: … to insufficient funds: wallet has … atto, payment needs … atto.

Semver impact

  • breaking
  • feature
  • fix

Test evidence

Rebased 2026-10-23 onto main at 50bc653 (0.3.12 promoted), 66 commits newer. Two changes came with it:

Every direct wallet payment call site on the new main still maps errors through Error::from_payment_failure. Re-run on Linux (dev2, stable):

  • cargo fmt --all -- --check, cargo clippy --all-targets --all-features -- -D warnings and the browser-wasm clippy: pass.
  • cargo test -p ant-core --lib data: pass.
  • e2e_native_payment: 4 passed, including insufficient_funds_then_funding_unblocks_native_payment.

Original evidence follows.

  • Real-chain guard: e2e_native_payment::insufficient_funds_then_funding_unblocks_native_payment (Anvil) now asserts Err(InsufficientFunds { have: 0, need: 100 }) for an unfunded wallet through the real journaled path. With the old map_err(Error::Payment) restored, it fails with expected InsufficientFunds, got Err(Payment("Insufficient tokens to pay for quotes. Have: 0 atto, need: 100 atto")). That confirms both evmlib's live wording and that the test catches a regression. cargo test -p ant-core --features native,test-utils --test e2e_native_payment: 4/4 pass.

  • Unit tests:

    • Display.
    • Parsing the report bare and under each caller's context prefix, with have/need above u64::MAX to prove the U256 round-trip.
    • Non-shortfall messages (nonce errors, an RPC gas "insufficient funds for gas", truncated or malformed reports) stay Payment verbatim.
    • classify_error_covers_all_variants.
  • cargo fmt --all -- --check ✓ · cargo clippy --all-targets --all-features -- -D warnings ✓ · cargo test -p ant-core --lib: 804 passed · cargo test -p ant-cli: 30 passed. All run on Windows 11.

  • Prod compat smoke on mainnet (2026-10-23), rebased head 388c3814 against main ccd4106d. Run on Linux (dev2, stable 1.99.1) with --locked release builds.

    • The wallet is a freshly generated key, and ant wallet balance reports 0. It holds neither tokens nor gas, so nothing can be paid.

    • Uploading a random 3 MiB file (3 chunks) collects real mainnet quotes and then fails at payment:

      Path main this branch
      --no-merkle File upload failed: payment error: Insufficient tokens to pay for quotes. Have: 0 atto, need: 228374654296875000 atto File upload failed: insufficient funds: wallet has 0 atto, payment needs 228374654296875000 atto
      --merkle File upload failed: payment error: Insufficient tokens to pay for quotes. Have: 0 atto, need: 185711296875000000 atto File upload failed: insufficient funds: wallet has 0 atto, payment needs 294520417968750000 atto
    • Both builds exit 1 before sending any transaction. The need amounts differ between runs because quotes vary.

    • This confirms that evmlib's wording on the live network still matches what from_payment_failure parses, on both payment paths.

    • A mainnet ant file download of a public canary file (3cba93c8…, 5,277,908 B) with this build also succeeded, and the byte count matches.

New dependency

none

ADR

n/a

Mitigation / rollback

Revert the commit. Consumers that already match InsufficientFunds go back to seeing Payment(String).

🤖 Generated with Claude Code

@Nic-dorman
Nic-dorman force-pushed the fix/typed-insufficient-funds branch from f6fc3ab to 795606a Compare October 23, 2026 09:36
…ds (#152)

A wallet without enough payment tokens failed with the generic
Error::Payment(String), so consumers (the mobile FFI/SDK, the CLI) could
only tell "top up your wallet" from any other payment failure by
matching message text.

Since #152 was filed, file and data uploads moved to evmlib's journaled
payment API, which returns InsufficientTokensForQuotes already rendered
as a String, and ant-protocol re-exports neither evmlib::wallet::Error
nor its variant (and ant-core may not depend on evmlib directly). So the
variant cannot be matched by type on any path; evmlib's report text is
the contract. Error::from_payment_failure parses its have/need amounts
wherever the report sits in the message and is used at every
wallet-backed payment site: the journaled native path, wave-batch and
Merkle batch payment, the non-native upload adapter, and single-chunk
payment (whose caller no longer re-wraps the typed error).

The e2e_native_payment Anvil test now asserts InsufficientFunds { have:
0, need: 100 } for an unfunded wallet, pinning evmlib's wording through
the real path: with the old mapping it fails with the Payment string.
classify_error treats the new variant as an application error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Nic-dorman
Nic-dorman force-pushed the fix/typed-insufficient-funds branch from 795606a to 388c381 Compare October 23, 2026 18: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.

Payment errors collapse insufficient-funds into generic Error::Payment(String) — add a typed InsufficientFunds variant

1 participant