fix: prevent duplicate transaction submissions with idempotency guard - #925
Merged
llinsss merged 2 commits intoAug 31, 2026
Conversation
- 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)
|
@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! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
mid-request
values before the response arrives
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:
digest)
───────────────────────────────────────────────────────────────────────────────────────────────
Checklist
closes [Frontend] Add transaction idempotency and duplicate-submit protection #859