Repository navigation
Payments 3/8: Idempotency and duplicate-payment-submission prevention #140
Description
Problem / Context
Nothing in the SDK or app currently prevents a duplicate payment submission. In app/src/app/deposit/page.tsx, the step-4 handleConfirm handler (line 48-53) has no submitting/disabled guard, so a double-click — or a retry after a dropped network call — can trigger two separate transactions. app/src/components/EarlyExitModal.tsx's "Exit Early" button (lines 88-98) currently has no onClick at all, meaning today it can't be duplicated only because it does nothing; once wired up (see the SDK pipeline work in #139), it needs the same guard from day one.
Soroban's ledger deduplicates identical transaction envelopes by hash, but a resigned or resubmitted transaction — e.g. after a client-side retry that rebuilds with a fresh sequence number or fee — is a distinct hash and will execute a second time, debiting the user twice. There is no application-level idempotency concept anywhere in this codebase today.
Objective
Guarantee that a single user-intended payment action (one deposit, one withdrawal, one early-exit) cannot execute more than once — across UI double-clicks, network retries, or a page reload mid-flight.
Technical approach
- Introduce a client-side idempotency key per payment intent, generated once when the user reaches the confirm step (derived from user address + tier + asset + amount + a monotonic nonce/timestamp), and persist it (reusing the existing
sessionStoragepattern inapp/src/lib/wallet/session.ts) until the operation resolves. - Define an explicit state machine per pending intent:
idle -> building -> awaiting_signature -> submitted -> confirmed | failed, and refuse re-entry intobuildingwhile any state other thanidle/failedis active for that key. - Disable submit controls for the duration of an in-flight submission: the deposit confirm button and
EarlyExitModal's currently-unwired submit button both need this. - On page reload while an intent is in
submitted(or later) state, rehydrate the persisted intent and resume status polling (built in the next issue) instead of allowing a fresh submission of the same intent.
Detailed scope
app/src/app/deposit/page.tsx, app/src/components/EarlyExitModal.tsx, a new shared module (e.g. app/src/lib/paymentIntent.ts) usable by both flows, and any SDK-level dedup guard in sdks/typescript that consumers should be able to rely on directly (not just the app).
Important edge cases and failure scenarios
- User closes the tab and reopens before confirmation completes — the pending intent must be recoverable, not silently dropped (which invites a duplicate submission) or stuck forever.
- Two browser tabs open to the same wallet, both attempting the same deposit concurrently.
- Wallet extension rejects/cancels signing after the intent was marked
awaiting_signature— must cleanly return to a resubmittableidle/failedstate without leaving a stuck lock that blocks all future submissions for that key. - User changes the amount or tier after an intent key was generated but before submission — the key must be regenerated, not silently reused for a now-different payment.
- Idempotency key collisions across genuinely different intents (e.g. same user depositing the same amount into the same tier twice, deliberately, in quick succession) must not be conflated — the key must distinguish deliberate repeat actions from accidental duplicates.
Dependencies
Depends on #139 (SDK transaction pipeline) — there is no real submission to guard until that exists. Also establishes the intent identifier that the status-tracking issue below will use to correlate a submission with its on-chain outcome.
Acceptance criteria
- An automated test demonstrates that rapid double-invocation of deposit/withdraw/earlyExit for the same intent results in exactly one submitted transaction.
EarlyExitModal's submit button is wired to a real, idempotency-guarded submission path (no longer dead).- Reloading the page mid-submission does not allow a duplicate submission of the same in-flight intent.
Definition of done
PR merged with tests covering the double-click and reload-during-flight scenarios; no user-facing payment action in the app is submittable without idempotency guarding.
Activity
yusuftomilola commentedon Aug 19, 2026
Looked through this project and understood its current implementation, architecture, and overall stage of maturity.
grantfox-oss commentedon Aug 19, 2026
🦊 GrantFox — @yusuftomilola has been assigned to this issue as part of the Third Campaign campaign!
Next steps:
Closes #140)Good luck! Track your progress on GrantFox.
feat(payments): idempotency guard against duplicate payment submission
grantfox-oss commentedon Aug 22, 2026
🎉 This issue has been marked as completed on GrantFox as part of the Third Campaign campaign!
@yusuftomilola's PR #148 was approved and merged by @wumibals.
🏆 @yusuftomilola: You earned 40 FoxPoints for this contribution! Your current tier: Builder (1,305 total points). Track your full progress on GrantFox.
👏 Great work, @yusuftomilola! Keep contributing to LadderMine.