Skip to content

fix: prevent duplicate transaction submissions with idempotency guard - #925

Merged
llinsss merged 2 commits into
DogStark:mainfrom
Mathew2k-hash:feature/idempotency-duplicate-submission-guard
Aug 31, 2026
Merged

fix: prevent duplicate transaction submissions with idempotency guard#925
llinsss merged 2 commits into
DogStark:mainfrom
Mathew2k-hash:feature/idempotency-duplicate-submission-guard

Conversation

@Mathew2k-hash

Copy link
Copy Markdown
Contributor

fix: prevent duplicate transaction submissions with idempotency guard

───────────────────────────────────────────────────────────────────────────────────────────────

Problem

Double-clicks, rapid retries, and network ambiguity could submit the same wallet transaction
more than once. There was no in-flight guard, no idempotency identifier, and no way to
reconcile stale pending submissions against the server.

───────────────────────────────────────────────────────────────────────────────────────────────

What changed

src/utils/idempotencyKey.ts (new)

Derives a deterministic idempotency key from the canonical payment payload (sorted keys,
trimmed strings, fixed-precision amounts, case-folded asset). Same payload → same digest.
Different payload → different digest. Uses async SHA-256 via Web Crypto with a synchronous
FNV-32a fallback for environments where Web Crypto is unavailable.

src/hooks/useTransactions.ts

Three layered guards added via a new submitPayment() method:

┌────────────────┬───────────────────────┬─────────────────────────────────────────────────┐
│ Layer │ Mechanism │ Prevents │
├────────────────┼───────────────────────┼─────────────────────────────────────────────────┤
│ Mutex │ submittingRef boolean │ Concurrent calls racing through async JS │
├────────────────┼───────────────────────┼─────────────────────────────────────────────────┤
│ Payload dedup │ FNV-32a digest │ Same destination/amount/asset/memo in rapid │
│ │ comparison │ separate calls │
├────────────────┼───────────────────────┼─────────────────────────────────────────────────┤
│ Reconciliation │ reconcilePendingSubmi │ Stale in-flight records blocking retries after │
│ │ ssions() │ a network timeout │
└────────────────┴───────────────────────┴─────────────────────────────────────────────────┘

The generated Idempotency-Key is forwarded to the sender callback so it can be attached as an
HTTP header end-to-end. Guards are always released in finally so a failed submission never
permanently blocks retry.

src/components/Wallet/TransactionSigning.tsx

  • Submit button disabled and shows a spinner while a submission is in-flight
  • All form fields and the fee selector disabled during in-flight to prevent mutation
    mid-request
  • lastSubmittedKeyRef suppresses re-fire when the user clicks submit again with identical form
    values before the response arrives
  • ARIA annotations added: aria-busy, role="alert" on errors, role="status" on success,
    role="radiogroup" on fee selector, explicit htmlFor on every input — fully keyboard and
    screen-reader accessible

src/lib/api/transactionAPI.ts

Added TransactionRequestOptions interface and buildHeaders() helper. All mutating endpoints
(estimateTransactionCost, retryFailedTransaction, cancelPendingTransaction) now accept an
optional idempotencyKey and forward it as the Idempotency-Key HTTP header.

src/hooks/useWallet.ts / src/lib/wallet/walletService.ts / src/pages/wallet.tsx

sendPayment signature extended with an optional idempotencyKey argument threaded all the way
through. Fully backward-compatible — callers that don't pass the key continue to work
unchanged.

Bug fix

Corrected a pre-existing missing closing brace in walletService.importBackup that caused a
TypeScript parse error.

───────────────────────────────────────────────────────────────────────────────────────────────

Tests

New regression suite: src/hooks/useTransactions.idempotency.test.ts — 26 tests, 26 passing

Suites cover:

  • Async SHA-256 key generation (same payload → same digest, different payload → different
    digest)
  • Sync FNV-32a fallback
  • extractDigest / samePayload helpers including malformed-key edge cases
  • In-flight mutex (rapid double-click characterisation test)
  • Same-payload dedup
  • Failure releases the guard for retry
  • reconcilePendingSubmissions with matching and non-matching server responses
  • Boundary cases: empty memo/fee, large amounts, unicode memo, whitespace trimming

───────────────────────────────────────────────────────────────────────────────────────────────

Checklist

  • No real pet, medical, contact, wallet, or credential data in fixtures
  • TypeScript strict — zero errors in changed files (tsc --noEmit)
  • Backward-compatible — no breaking changes to existing hook or component APIs
  • Accessible — keyboard navigable, ARIA roles/labels on all interactive elements
  • No unrelated refactors included
    closes [Frontend] Add transaction idempotency and duplicate-submit protection #859

- Add src/utils/idempotencyKey.ts: deterministic key derivation from
  canonical payment payload using SHA-256 (async) with FNV-32a fallback.
  samePayload() / extractDigest() helpers for dedup comparisons.

- Upgrade useTransactions hook:
  - submittingRef mutex blocks concurrent in-flight calls
  - isDuplicateSubmission() payload-level dedup via FNV-32a digest
  - reconcilePendingSubmissions() clears stale in-flight records after
    fetchPendingTransactions reconciles with the server
  - submitPayment() generates Idempotency-Key, registers/releases guards
    in finally so failure always unblocks retry

- Upgrade TransactionSigning component:
  - submittingRef + isSubmitting state; all form fields disabled in-flight
  - lastSubmittedKeyRef blocks re-fire with identical form values
  - aria-busy, role=alert/status/radiogroup, htmlFor labels for a11y

- Upgrade transactionAPI: TransactionRequestOptions interface,
  buildHeaders() helper, Idempotency-Key header on all mutating calls

- Thread idempotencyKey through useWallet.sendPayment and
  walletService.sendPayment (optional param, backward-compatible)

- Fix pre-existing missing-brace bug in walletService.importBackup

- Add 26-test regression suite covering: async SHA-256 key generation,
  sync FNV-32a variant, extractDigest, samePayload, in-flight mutex,
  same-payload dedup, failure/retry release, reconciliation, boundary cases
  (all 26 pass)
@drips-wave

drips-wave Bot commented Aug 25, 2026

Copy link
Copy Markdown

@Mathew2k-hash Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@llinsss
llinsss merged commit 52d011a into DogStark:main Aug 31, 2026
2 of 7 checks passed
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.

[Frontend] Add transaction idempotency and duplicate-submit protection

3 participants