Skip to content

Key duplicate-payment protection by operation, not by transaction - #177

Open
collinsezedike wants to merge 2 commits into
determined-001:mainfrom
collinsezedike:fix/dedup-by-operation
Open

Key duplicate-payment protection by operation, not by transaction#177
collinsezedike wants to merge 2 commits into
determined-001:mainfrom
collinsezedike:fix/dedup-by-operation

Conversation

@collinsezedike

Copy link
Copy Markdown
Contributor

What

processed_tx (the watcher's dedup ledger) and link_payments (the cumulative-accounting ledger from issue 1.4) both keyed uniqueness on tx_hash alone. A Stellar transaction can carry up to 100 operations, and a payment is one operation, not the whole transaction — so a second payment operation sharing a transaction with the first was silently discarded as a false duplicate, permanently: there is no re-processing path once a hash is marked done.

Why

Two concrete failure modes, both silent fund loss from the seller's point of view:

  • A wallet splits one payment across two operations to the same destination in one atomic transaction. Operation 1 is credited, the hash is marked processed, operation 2 is dropped — the link sits underpaid forever even though the full amount arrived.
  • One transaction pays two different watched sellers' destinations (ordinary fee-saving batching). Whichever seller's poll tick reaches the hash first claims it system-wide; the other seller's operation is invisibly dropped. That link never reaches paid, no webhook fires, and the payment is fully verifiable on-chain yet invisible to the app.

Closes #152

How

  • Re-keyed processed_tx on (tx_hash, operation_id)operation_id is Horizon's per-operation pagingToken. WatcherStateRepository.isProcessed/markProcessed both take it now, and watcher-loop.ts passes payment.pagingToken through.
  • Also re-keyed link_payments's uniqueness the same way: recordPayment's onConflictDoNothing was silently dropping a split payment's second operation even after the processed_tx fix, since it enforced uniqueness on tx_hash alone too (this wasn't explicitly called out in the issue text, but the "done when" criteria don't hold without it — the cumulative-payment ledger from issue 1.4 sits directly downstream of the same bug).
  • Both are full table rebuilds (migrateLegacyProcessedTxTable, migrateLegacyLinkPaymentsTable in db/client.ts), not ALTER TABLE ADD COLUMN — SQLite can't move a column into/out of a PRIMARY KEY, and the whole point is that tx_hash stops being unique on its own. Legacy rows get operation_id = NULL; isProcessed treats a NULL row as "the whole transaction was processed", preserving today's dedup behavior for anything already settled. Migrations are idempotent and no-op on a fresh database.

Test plan

  • pnpm typecheck
  • pnpm test
  • pnpm build
  • Added regression tests for both scenarios in apps/api/test/worker/watcher-loop.test.ts
  • apps/api/test/bootstrap-migrations.test.ts still passes (its legacy link_payments fixture predates the ledger column, which caught a real bug in my first migration draft — it assumed ledger always existed on a legacy table)
  • Added a docs/FIXLOG.md row (BUG-152)

One thing worth flagging: the FIXLOG "Fix commit" cell references this branch's current HEAD (9650020), which won't match the final commit hash once this squash-merges. Please update that cell to the actual merge commit when you land this, same as the other rows in the table.

@drips-wave

drips-wave Bot commented Aug 28, 2026

Copy link
Copy Markdown

@collinsezedike 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

@vercel

vercel Bot commented Aug 28, 2026

Copy link
Copy Markdown

@collinsezedike is attempting to deploy a commit to the determined's projects Team on Vercel.

A member of the Team first needs to authorize it.

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.

4.11 - Key duplicate-payment protection by operation, not by transaction

1 participant