fix: make wallet creation and pending debits idempotent (EN-1204)#109
fix: make wallet creation and pending debits idempotent (EN-1204)#109flemzord wants to merge 3 commits into
Conversation
Creation paths generated a fresh UUID on every request, so retrying with the same Idempotency-Key created duplicates (and for pending debits the ledger rejected the retry, since the hold ID baked into the request body changed each time). - Derive the wallet ID and the pending-debit hold ID from the Idempotency-Key (UUIDv5) when one is provided; keep random IDs otherwise - Forward the Idempotency-Key to the ledger when creating a wallet Adds tests asserting stable IDs and key forwarding across retries.
|
Warning Review limit reached
More reviews will be available in 50 minutes and 19 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more credits in the billing tab to continue. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
flemzord
left a comment
There was a problem hiding this comment.
Revue inline: l’idempotence est améliorée, mais deux cas restent fragiles dans les tests et dans les sources expirables.
| } | ||
|
|
||
| hold = Ptr(debit.newHold()) | ||
| // Derive the hold ID from the Idempotency-Key so a retry produces an |
There was a problem hiding this comment.
Ça stabilise le hold ID, mais pas forcément toute la requête ledger. Plus bas, les sources sont recalculées puis filtrées avec time.Now(); pour un pending debit avec balances:["*"] ou une balance nommée qui expire entre deux essais, le retry avec la même Idempotency-Key peut produire un script/body différent et être rejeté par le ledger. Il faut rendre la résolution des sources déterministe pour une clé/requête donnée, ou au minimum couvrir ce cas par un test d’expiration.
There was a problem hiding this comment.
You're right — the deterministic hold ID stabilises the ID but not the whole body. I documented this explicitly at the call site and strengthened TestWalletsDebitPendingIdempotency to assert a byte-identical PostTransaction across retries for the stable case (explicit, non-expiring source set). The wildcard (balances:["*"]) and expiry-crossing cases remain non-deterministic because sources are resolved against live ledger state with time.Now(); fully fixing those needs deterministic source resolution (e.g. persisting the resolved request per key), which I called out in the comment as separate follow-up rather than solve in this PR.
Address review feedback: - Wallet-create idempotency test now replays a fixed payload (a regenerated name would be a different body for the same key, i.e. a conflict, not a replay) and asserts the targeted account is stable across retries. - Strengthen the pending-debit idempotency test to assert a byte-identical ledger body across retries for a stable source set. - Document that pending-debit idempotency only holds for an explicit, non-expiring source set: the wildcard / expiry-crossing case is resolved against live state with time.Now() and may still differ on retry.
🛑 Changes requested — multi-model reviewThe PR stabilizes wallet and hold IDs by deriving them deterministically from the Idempotency-Key via UUIDv5, and forwards the key to the ledger for wallet creation. However, pending-debit idempotency is only partially achieved: the hold ID is now stable, but the ledger request body is still rebuilt from live balance state on every retry. A retry crossing an expiry boundary or involving wildcard source resolution will produce a different Numscript body and be rejected by the ledger as a conflict rather than replayed. The new test 🟠 [major] Pending debit retries can still yield a different ledger body, breaking idempotency
Only the hold ID is made deterministic; the full ledger request body is still reconstructed from live balance state (source balances fetched and filtered against Suggestion: Make the entire pending-debit ledger body deterministic for a given idempotency key. For example, persist or cache the resolved transaction body (or at minimum the resolved source set) keyed by the idempotency key before submitting to the ledger, and reuse it on retry. As a short-term alternative, explicitly reject idempotent pending-debit retries that depend on expiring or wildcard source resolution with a clear error until deterministic body construction is implemented. 🟡 [minor] Wallet ID and hold ID derived from the same namespace without a type discriminator
Suggestion: Prefix the key with a resource-type discriminator before hashing, e.g. 🟡 [minor] Documented idempotency limitation for expiring/wildcard balances needs follow-up tracking
The NOTE comment and the Suggestion: Ensure the limitation is surfaced clearly in user-facing API documentation and that a follow-up issue (e.g. for deterministic source resolution) is filed and linked from the NOTE comment, so the production gap is not forgotten. ⚪ [nit] testEnv captured by closure before assignment is a fragile pattern
Suggestion: Optionally extract Reviewed in parallel by claude (anthropic/claude-opus-4-8) and gpt (openai/gpt-5.5), then consolidated. This comment is updated on each push. |
Problem (Medium — M1, M7-wallet)
uuid.NewString()for the hold per request and baked it into the Numscript and metadata. A retry with the sameIdempotency-Keytherefore produced a different ledger request body, so the ledger (which hashes the body to enforce idempotency) rejected the retry instead of returning the original result.POST /walletsignored theIdempotency-Keyand minted a fresh wallet UUID on every call, so a retry created a duplicate wallet.Fix
Idempotency-Key(UUIDv5 over a fixed namespace) when one is provided; keep random IDs when it is absent.Idempotency-Keyto the ledger when creating a wallet.Tests
TestWalletsCreateIdempotency: twoPOST /walletswith the same key forward the key to the ledger and resolve to the same wallet ID.TestWalletsDebitPendingIdempotency: two pending debits with the same key yield the same hold ID.Note
The balance-creation idempotency (M7-balance) is handled together with the
CreateBalanceconcurrency fix (M5) in a sibling PR, since both touch the same function.From the in-depth repository review.