Skip to content

Payments 3/8: Idempotency and duplicate-payment-submission prevention #140

Closed

Description

@wumibals

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 sessionStorage pattern in app/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 into building while any state other than idle/failed is 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 resubmittable idle/failed state 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

  1. yusuftomilola commented on Aug 19, 2026

    @yusuftomilola
    Contributor

    Looked through this project and understood its current implementation, architecture, and overall stage of maturity.

  2. grantfox-oss commented on Aug 19, 2026

    @grantfox-oss

    🦊 GrantFox — @yusuftomilola has been assigned to this issue as part of the Third Campaign campaign!

    Next steps:

    1. Open a Pull Request referencing this issue (e.g., Closes #140)
    2. Your PR will be reviewed by the LadderMine maintainers

    Good luck! Track your progress on GrantFox.

  3. added a commit that references this issue on Aug 22, 2026
    7a980e7
  4. grantfox-oss commented on Aug 22, 2026

    @grantfox-oss

    🎉 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions