Skip to content

fix(platform-wallet): deliver typed AddressNonceMismatch error across wallet, FFI, and Swift host#4047

Merged
lklimek merged 6 commits into
v4.1-devfrom
fix/platform-wallet-address-nonce-mismatch
Jul 9, 2026
Merged

fix(platform-wallet): deliver typed AddressNonceMismatch error across wallet, FFI, and Swift host#4047
lklimek merged 6 commits into
v4.1-devfrom
fix/platform-wallet-address-nonce-mismatch

Conversation

@Claudius-Maginificent

@Claudius-Maginificent Claudius-Maginificent commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Issue being fixed or feature implemented

Extracted from #3692 (feat/platform-wallet-rehydration) at the user's request: these three commits fix address-nonce mismatch error handling and are unrelated to watch-only wallet rehydration, so they belong in their own PR on top of v4.1-dev.

The optimistic address-nonce path submits fetched + 1 by design and relies on the caller retrying when Platform rejects a stale/replayed value under a lagging DAPI replica read (Found-033). The typed AddressInvalidNonceError (consensus code 40603) was getting flattened to a string at several mapping seams, so callers (including the FFI boundary and the Swift host binding) lost the ability to recognize this self-healing race and retry correctly.

What was done?

  • packages/rs-platform-wallet: added a first-class PlatformWalletError::AddressNonceMismatch { address, provided_nonce, expected_nonce } variant, a public as_address_invalid_nonce extractor, and a promote_address_nonce_error helper. Rewired the lossy string-collapse seams (shield enrich, broadcast_shielded_spend, classify_spend_wait_failure, identity top_up_from_addresses) to promote via the helper.
  • packages/rs-platform-wallet-ffi: added a dedicated ErrorAddressNonceMismatch = 21 FFI result code (additive), mapped in both map_spend_result and the blanket From<PlatformWalletError>, preserving the definitively-failed / notes-released / safe-to-retry contract. as_address_invalid_nonce now also recurses through dash_sdk::Error::NoAvailableAddressesToRetry.
  • packages/swift-sdk: mirrored the new code 21 in PlatformWalletResult.swift (PlatformWalletResultCode.errorAddressNonceMismatch, init(ffi:), PlatformWalletError.addressNonceMismatch(String), init(result:)), and pinned the FFI nonce tests to exact rendered substrings instead of bare-digit substring checks.

Nonce values are intentionally NOT exposed as structured FFI out-fields (ABI-frozen by-value result struct); they travel in the typed Display message, and a retry re-fetches the nonce anyway.

How Has This Been Tested?

  • cargo check -p platform-wallet -p platform-wallet-ffi
  • cargo fmt --check -p platform-wallet -p platform-wallet-ffi
  • cargo clippy -p platform-wallet -p platform-wallet-ffi --all-targets
  • Rust unit tests covering AddressNonceMismatch promotion through both the blanket From and map_spend_result, and extractor recursion through the retry envelope.
  • Swift compilation was NOT independently verified in this PR (cbindgen header + DashSDKFFI module require the iOS toolchain, unavailable in this environment); the change is pattern-exact against the existing errorShieldedBroadcastFailed arm.

Breaking Changes

None. The new FFI result code is an additive enum slot; existing codes are unchanged.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

Summary by CodeRabbit

  • New Features

    • Added a new typed error for address nonce mismatches, with clearer messages that include the submitted and expected nonce values.
    • Improved error reporting so retryable nonce-related failures are surfaced consistently across wallet flows.
  • Bug Fixes

    • Updated top-up, shielded send, and related transaction paths to preserve nonce-mismatch details instead of showing generic wallet errors.
    • Added matching support in the Swift SDK so apps can recognize the new error code and message.

lklimek and others added 3 commits July 9, 2026 06:59
…llers

The optimistic address-nonce path submits `fetched + 1` by design and
relies on the caller retrying when Platform rejects a stale/replayed
value under a lagging DAPI replica read (Found-033). rs-sdk carries the
typed `AddressInvalidNonceError` (consensus code 40603) end-to-end
intact, but platform-wallet flattened it to a string at its
error-mapping seams, so callers could not recover `expected_nonce` to
honor their half of the optimistic-concurrency contract.

Add a public `as_address_invalid_nonce` extractor (mirroring
`as_asset_lock_proof_cl_height_too_low`, matching both the
`Protocol(ConsensusError)` and `StateTransitionBroadcastError` SDK
shapes), a first-class `PlatformWalletError::AddressNonceMismatch`
variant carrying `{address, provided_nonce, expected_nonce}`, and a
`promote_address_nonce_error` helper. Rewire the lossy string-collapse
seams (shield `enrich`, `broadcast_shielded_spend`,
`classify_spend_wait_failure`, identity `top_up_from_addresses`) to
promote via the helper when it matches, else keep today's exact string
fallback.

rs-sdk is untouched. The plain transfer/withdrawal paths already
preserve the typed error via `PlatformWalletError::Sdk`, from which the
now-public extractor can recover the nonce.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ecurse nonce extractor

QA-001 (HIGH): the AddressNonceMismatch variant added at the wallet layer
had no FFI mapping, so it both regressed and under-delivered at the C
boundary hosts actually consume.
- Regression: a shield nonce-rejection that used to surface as
  `ErrorShieldedBroadcastFailed` (code 16 — definitively failed, notes
  released, safe to retry) fell through `map_spend_result`'s catch-all to
  `ErrorWalletOperation` (code 6), dropping the retry-safety contract.
- Fix: add a dedicated `ErrorAddressNonceMismatch = 21` result code (an
  additive enum slot) carrying the SAME definitively-failed / notes-
  released / safe-to-retry contract, and map to it in BOTH `map_spend_result`
  (covers shield/unshield/transfer/withdraw) AND the blanket
  `From<PlatformWalletError>` (covers identity `top_up_from_addresses`,
  which propagates via `?`/`.into()`). Hosts can now recognize the self-
  healing nonce race and retry — the retry re-fetches the address nonce.

Nonce values are NOT exposed as structured out-fields: the shared
`PlatformWalletFFIResult` is returned by value and mirrored by every
language binding, so adding fields (or send-function out-params) would be
an ABI-breaking change for all consumers. The submitted/expected nonces
still travel in the result `message` (typed Display), and an FFI retry
re-fetches the nonce anyway, so the practical loss is nil.

QA-002 (LOW): `as_address_invalid_nonce` now recurses through
`dash_sdk::Error::NoAvailableAddressesToRetry(inner)`, matching its sibling
predicate `broadcast_definitely_failed` so the two stay in lockstep
(latent today — the retry envelope never wraps a ConsensusError yet).

Tests: AddressNonceMismatch -> ErrorAddressNonceMismatch via both the
blanket From and map_spend_result; extractor unwraps the retry envelope.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… test assertions

QA-R2 #1 (MEDIUM): the Swift host binding — the only in-repo host —
stopped at result code 20, so the new ErrorAddressNonceMismatch (=21)
collapsed to `.errorUnknown` → `.unknown`, and the typed nonce-race fix
never reached the shipped host. Mirror it in PlatformWalletResult.swift:
add `case errorAddressNonceMismatch = 21` to `PlatformWalletResultCode`,
its `init(ffi:)` arm (matching the cbindgen constant
`PLATFORM_WALLET_FFI_RESULT_CODE_ERROR_ADDRESS_NONCE_MISMATCH`), a
dedicated `PlatformWalletError.addressNonceMismatch(String)` case, and the
`init(result:)` mapping — mirroring `errorShieldedBroadcastFailed`'s
definitively-failed / notes-released / safe-to-retry semantics. Swift
compilation was NOT verified: the cbindgen-generated header + DashSDKFFI
module are produced by build_ios.sh (iOS toolchain, unavailable here);
the change is pattern-exact against the existing arms and the
`prefix_with_name + ScreamingSnakeCase` cbindgen config.

QA-R2 #2 (LOW): the two new nonce-value FFI tests asserted bare digits
(`contains('1') && contains('2')`), which a provided/expected transposition
would pass. Pin the exact rendered substrings instead (`submitted nonce 1`
/ `Platform expected 2`) so a transposition fails.

Per the ABI decision (keep dedicated-code-only), document at the blanket
`From` mapping arm that structured provided/expected nonce out-fields are
intentionally out of scope — PlatformWalletFFIResult is by-value/ABI-frozen;
the values travel in the message and an FFI retry re-fetches the nonce.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@lklimek, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 5 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b6b20ee2-c777-49f6-bb32-205d0c25bbfd

📥 Commits

Reviewing files that changed from the base of the PR and between 2345876 and 61d95f2.

📒 Files selected for processing (5)
  • packages/rs-platform-wallet/src/error.rs
  • packages/rs-platform-wallet/src/wallet/identity/network/register_from_addresses.rs
  • packages/rs-platform-wallet/src/wallet/platform_addresses/transfer.rs
  • packages/rs-platform-wallet/src/wallet/platform_addresses/withdrawal.rs
  • packages/rs-platform-wallet/src/wallet/shielded/operations.rs
📝 Walkthrough

Walkthrough

Adds a new AddressNonceMismatch error variant to PlatformWalletError, plus helper functions to extract and promote address-nonce consensus errors from SDK errors. Wires this promotion into wallet operations, adds a dedicated FFI result code (ErrorAddressNonceMismatch), and surfaces the corresponding error/case in the Swift SDK.

Changes

Address Nonce Mismatch Error Handling

Layer / File(s) Summary
Core error type and promotion helpers
packages/rs-platform-wallet/src/error.rs
Adds AddressNonceMismatch variant with address/provided/expected nonce fields, as_address_invalid_nonce and promote_address_nonce_error helper functions, plus unit tests.
Wallet operation wiring
packages/rs-platform-wallet/src/wallet/identity/network/top_up_from_addresses.rs, packages/rs-platform-wallet/src/wallet/shielded/operations.rs
Updates top-up and shielded broadcast/wait-failure error handling to attempt promotion before falling back to generic errors.
FFI error code and result mapping
packages/rs-platform-wallet-ffi/src/error.rs, packages/rs-platform-wallet-ffi/src/shielded_send.rs
Adds ErrorAddressNonceMismatch = 21 FFI code, maps AddressNonceMismatch in the From impl and map_spend_result, with unit tests.
Swift SDK error surfacing
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletResult.swift
Adds errorAddressNonceMismatch code and addressNonceMismatch(String) error case, wired into init(ffi:), errorDescription, and init(result:).

Estimated code review effort: 2 (Simple) | ~15 minutes

Possibly related issues

Suggested labels: ready for final review

Suggested reviewers: shumkov, lklimek, llbartekll, ZocoLini, thepastaclaw

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: typed AddressNonceMismatch propagation across wallet, FFI, and Swift host.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/platform-wallet-address-nonce-mismatch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thepastaclaw

thepastaclaw commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

✅ Review complete (commit 61d95f2)

@github-actions github-actions Bot added this to the v4.1.0 milestone Jul 9, 2026

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Source: reviewers: claude opus general (failed), codex gpt-5.5 general (ok), claude opus security-auditor (failed), codex gpt-5.5 security-auditor (ok), claude opus rust-quality (failed), codex gpt-5.5 rust-quality (ok), claude opus ffi-engineer (failed), codex gpt-5.5 ffi-engineer (ok). Verifier: codex gpt-5.5.

The nonce-mismatch error plumbing is directionally correct, but it is incomplete across the address-funded wallet surface. Several public FFI-backed paths can still flatten the same retryable consensus error into generic wallet errors, and one shielded wait-side classifier fails to unwrap the retry envelope that the new extractor explicitly supports.

🔴 1 blocking | 🟡 1 suggestion(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-platform-wallet/src/wallet/platform_addresses/transfer.rs`:
- [BLOCKING] packages/rs-platform-wallet/src/wallet/platform_addresses/transfer.rs:228-230: Preserve nonce mismatch errors across all address-funded paths
  This transfer path still calls the auto-nonce SDK helper and propagates its error with bare `?`, so an `AddressInvalidNonceError` from `transfer_address_funds` is converted through `PlatformWalletError::Sdk` instead of the new `AddressNonceMismatch` variant. The same problem remains in withdrawal (`packages/rs-platform-wallet/src/wallet/platform_addresses/withdrawal.rs:125`) and identity registration from addresses (`packages/rs-platform-wallet/src/wallet/identity/network/register_from_addresses.rs:100`, where it is wrapped as `InvalidIdentityData`). These paths use the same `fetch_inputs_with_nonce` plus `nonce_inc` optimistic nonce flow that this PR is making retryable for shield/top-up, and they are exposed through FFI/Swift entry points, so callers still receive `ErrorUnknown` or a generic Swift error for the same retryable race. Promote `AddressInvalidNonceError` at these SDK error seams too, including the explicit-nonce variants where Platform can reject a caller-supplied stale nonce.

In `packages/rs-platform-wallet/src/wallet/shielded/operations.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/shielded/operations.rs:2295-2301: Unwrap retry envelope before classifying nonce rejection
  `as_address_invalid_nonce` now recurses through `dash_sdk::Error::NoAvailableAddressesToRetry`, but shield wait-side handling only calls `promote_address_nonce_error` after `carries_consensus_rejection` returns true. Since this predicate does not unwrap `NoAvailableAddressesToRetry`, a wait error shaped like `NoAvailableAddressesToRetry(Protocol(ConsensusError::AddressInvalidNonceError(_)))` is treated as ambiguous `ShieldedSpendUnconfirmed` instead of the new retryable `AddressNonceMismatch`. Make the classifier compositional over the same wrapper so the new extractor can actually run on wrapped consensus rejections.

…ddress-funded call sites

Deliver Platform's typed AddressInvalidNonceError (consensus 40603) as
PlatformWalletError::AddressNonceMismatch consistently, matching the
shield/top-up path, so callers can retry with expected_nonce instead of
parsing a flattened string.

- transfer.rs / withdrawal.rs: promote the nonce rejection at all six
  Explicit / ExplicitWithNonces / Auto call sites via a shared
  promote_address_nonce_error_or_sdk helper (Sdk fallback preserved).
- register_from_addresses.rs: promote on top of the existing
  InvalidIdentityData fallback (matching its top_up sibling).
- operations.rs: carries_consensus_rejection now recurses through
  NoAvailableAddressesToRetry, in lockstep with as_address_invalid_nonce,
  so a nonce mismatch in a retry envelope promotes instead of falling
  through to the ambiguous ShieldedSpendUnconfirmed bucket.

Adds unit tests for the helper (both branches + retry envelope) and the
classifier recursion (wrapped consensus / transport / nonce shapes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Claudius-Maginificent

Copy link
Copy Markdown
Collaborator Author

@thepastaclaw — fixed in d3103c8. Ripped out the last few spots where a 40603 nonce rejection was still leaking out as a flat string instead of the typed AddressNonceMismatch:

  • transfer.rs / withdrawal.rs — all six Explicit / ExplicitWithNonces / Auto call sites now promote via a shared promote_address_nonce_error_or_sdk helper (keeps the Sdk(e) fallback for everything else).
  • register_from_addresses.rs — promotes on top of the existing InvalidIdentityData fallback, matching its top_up_from_addresses sibling.
  • operations.rscarries_consensus_rejection now recurses through NoAvailableAddressesToRetry, in lockstep with as_address_invalid_nonce, so a nonce mismatch wrapped in a retry envelope reaches promotion instead of the ambiguous ShieldedSpendUnconfirmed bucket.

New unit tests cover the helper (both branches + retry envelope) and the classifier recursion (wrapped consensus / transport / nonce shapes). cargo clippy --all-targets --features shielded -- -D warnings clean, all 536 rs-platform-wallet tests green, cargo fmt applied.

lklimek and others added 2 commits July 9, 2026 08:11
…ngth budget

Apply coding-best-practices comment-length rules to the error.rs items and the
operations.rs classifier note touched by the promotion fix: tighten the
AddressNonceMismatch variant doc and promote_address_nonce_error_or_sdk to the
5-10 line pub-API budget, cut as_address_invalid_nonce from ~18 lines to
essentials, and shrink the carries_consensus_rejection recursion note plus the
inline retry-envelope comment to the internal budget. No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Trim the private helper's recursion note from 3 to 2 lines (internal-comment
budget), dropping the outcome-narrative tail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@thepastaclaw

Copy link
Copy Markdown
Collaborator

Verified at current head 61d95f2d9789d0d0f004ee62ed5c1884318ed6fe: the prior AddressNonceMismatch findings are addressed.

What I checked:

  • transfer and withdrawal now map all explicit / explicit-with-nonces / auto SDK submit errors through promote_address_nonce_error_or_sdk
  • register_from_addresses now promotes address nonce mismatches before falling back to InvalidIdentityData
  • carries_consensus_rejection now recurses through NoAvailableAddressesToRetry, so wrapped nonce rejections reach the typed AddressNonceMismatch path instead of ShieldedSpendUnconfirmed

Local verification:

  • cargo test -p platform-wallet nonce --features shielded
  • cargo test -p platform-wallet wrapped --features shielded
  • cargo fmt --check -p platform-wallet

@lklimek lklimek merged commit aae79a8 into v4.1-dev Jul 9, 2026
16 checks passed
@lklimek lklimek deleted the fix/platform-wallet-address-nonce-mismatch branch July 9, 2026 08:48
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.

4 participants