Repository navigation
Payments 5/8: Failure taxonomy and blockchain/network-failure handling with retry/backoff #142
Description
Problem / Context
app/src/app/deposit/page.tsx has a failed UI state (lines 185-194) but nothing in the app or SDK today ever produces a real failure — it's only reachable by manually forcing state in dev. There is no handling anywhere for RPC timeouts, ledger congestion, expired transaction time-bounds, missing trustlines, or simulation reverts. The relevant contract error surface already exists and is richer than anything the client currently understands: contracts/vault_router/src/error.rs defines InvalidTier, BelowMinDeposit, LockNotExpired, AssetNotAllowed, DepositCapExceeded, ProtocolPaused, Unauthorized; contracts/vault_l3/src/lib.rs additionally defines NotYetMatured and AmountExceedsBalance. The SDK's sdks/typescript/src/errors.ts has exactly two error classes (LockNotExpiredError, BelowMinDepositError); every other failure mode surfaces as an unstructured thrown Error, or is silently swallowed.
Objective
Give every payment operation a complete, typed failure taxonomy and well-defined retry/recovery behavior, so the app (and its users) can distinguish "this failed, and here's why," from "we don't know yet, keep waiting," from "safe to retry automatically."
Technical approach
- Extend
sdks/typescript/src/errors.tsinto a full error hierarchy covering: contract-level reverts (one class perVaultError/VaultL3error variant, decoded from simulate/getTransactiondiagnostic events), network-level failures (RPC unreachable, timeout), transaction-level failures (expired time-bounds, insufficient fee, bad sequence number), and wallet-level failures (user rejected signing, wallet disconnected mid-flow). - Classify each error as retryable-safely / retryable-with-a-new-intent / not-retryable, and encode that classification on the error type itself so the UI doesn't need its own ad hoc branching logic.
- Implement retry-with-exponential-backoff-and-jitter for transient network/timeout failures in the RPC layer (from Payments 4/8: Real transaction status tracking and confirmation lifecycle #141), bounded by a max-attempts/timeout ceiling that hands off to a terminal "failed, needs manual action" state rather than retrying forever.
- Wire the deposit page's and
EarlyExitModal's failure paths to these real, classified errors instead of the currently-unreachable mock state, showing the correct message per error class. - Ensure any resubmission after a recoverable failure (e.g. expired time-bounds) goes through the idempotency guard from Payments 3/8: Idempotency and duplicate-payment-submission prevention #140 rather than blindly resubmitting.
Detailed scope
sdks/typescript/src/errors.ts, the RPC/transaction-submission layer from #139/#141, app/src/app/deposit/page.tsx and app/src/components/EarlyExitModal.tsx failure-state wiring, and app/src/lib/toast.tsx for surfacing non-blocking failure notifications where appropriate.
Important edge cases and failure scenarios
- Partial-success ambiguity:
VaultRouter.withdrawperforms a two-hop transfer (vault call, thentoken::Client::transferfrom vault to user —contracts/vault_router/src/lib.rslines 178-190). Confirm and document Soroban's actual atomicity guarantee for this pattern (a single host-function invocation either commits or reverts as a whole), so the client-side failure taxonomy doesn't invent a "half-completed withdrawal" state that can't actually occur on-chain — and correctly handles the states that can occur. - Network partition: RPC submission succeeds but all subsequent status polls fail — must not be reported to the user as "failed" when the transaction may, in fact, be confirmed on-chain. This requires re-querying rather than trusting local retry-exhaustion as proof of failure.
- Wallet-level rejection (user cancels in Freighter) vs. contract-level rejection (revert) must produce distinct messages and distinct retry affordances — a cancelled signature is trivially retryable, a
BelowMinDepositrevert is not without changing the input. - Distinguish "definitely failed, safe to retry immediately with the same intent" (e.g. a pre-submission simulation revert) from "ambiguous, must query chain state before retrying" (e.g. RPC timeout after the transaction was plausibly already accepted).
Dependencies
Depends on #141 (status tracking, to know when a failure is real vs. merely ambiguous) and #140 (idempotency, to safely gate any retry).
Acceptance criteria
- Every
VaultError/VaultL3error variant has a corresponding typed SDK error with a distinct, actionable, user-facing message. - RPC calls transparently retry transient failures with backoff and terminate with a clear, non-looping failed state after a bounded number of attempts.
- The deposit and early-exit failure UI states are reachable via real, injected failures in tests — not just hardcoded/manual triggers.
Definition of done
PR merged; test suite includes simulated network-timeout, contract-revert, and wallet-rejection scenarios, each producing the correct classified error and UI outcome.
Activity
devwums commentedon Aug 19, 2026
Please can i work on this issue. I will deliver beyond expectations. ETA is 1 day/ A well structured, modular and maintainable pr.
grantfox-oss commentedon Aug 19, 2026
🦊 GrantFox — @devwums has been assigned to this issue as part of the Third Campaign campaign!
Next steps:
Closes #142)Good luck! Track your progress on GrantFox.
feat(payments): typed failure taxonomy, retry/backoff, and real UI wi…
grantfox-oss commentedon Aug 22, 2026
🎉 This issue has been marked as completed on GrantFox as part of the Third Campaign campaign!
@devwums's PR #150 was approved and merged by @wumibals.
🏆 @devwums: You earned 40 FoxPoints for this contribution! Your current tier: Builder (1,225 total points). Track your full progress on GrantFox.
👏 Great work, @devwums! Keep contributing to LadderMine.
feat(wallet): real Freighter signing, trustline detection, real fee e…