Repository navigation
fix(payout-hop): answer with the hash and repair bookkeeping when post-send writes fail (#49) - #1102
Merged
DevTobis merged 3 commits intoOct 23, 2026
Conversation
…t-send writes fail (DevTobis#49) After the anchor was paid, a failure in the sent update, exit_fill insert or journal pair surfaced as 502. The caller saw a failure, a retry got 409, and the payout stayed paid but unbooked with nothing to reconcile it. - The journal ref_id (quoteId, else a fresh uuid) and quote_id are stored on the payout_forward claim before sending (migration 0010), so the booking id is fixed and a repair reuses it. - Marking the row sent and booking exit_fill + the journal pair share one transaction, so sent always means booked. - If that transaction fails, the row becomes bookkeeping_pending, a payout_unbooked alert fires, and the response is still 200 with the hash and booked: false. - bookPendingPayouts books pending rows idempotently (conditional update per row; a row whose journal pair already exists is only marked sent). It runs at startup and every PAYOUT_HOP_REPAIR_INTERVAL_S.
|
@Hussman256 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! 🚀 |
Resolve against the first-leg verification (DevTobis#1101): the verifier runs before the claim and the bookkeeping repair keeps its own alerter. Renumber the payout_forward booking migration to 0012 and regenerate it with drizzle-kit.
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.
Closes #49
Problem
After the anchor was paid,
forwardPayoutdid three more writes outside the error handling: thesentupdate, then a transaction withexit_filland the journal pair. If any of them failed, the API answered 502. The caller treated the payout as failed, a retry got 409, and the ledger never got itsusdc_cash/operator:payout_hoprows.Change
refId. The journalref_id(thequoteIdwhen given, otherwise a fresh uuid) and thequote_idare written onto thepayout_forwardclaim before sending. Migration0010_payout_forward_bookingaddsref_id uuid NOT NULL DEFAULT gen_random_uuid()(which backfills existing rows) andquote_id uuid. The original attempt and any repair book under the same id.sentmeans booked. Setting the status tosent, insertingexit_filland writing the journal pair now happen in one transaction (bookPayout).bookkeeping_pendingwith the tx hash, apayout_unbookedalert fires (newAlertKind), and the response is200 { hash, status, booked: false }.claimedand carries the tx hash.bookPendingPayouts(db, alerter)insrc/repair.tsruns at startup and everyPAYOUT_HOP_REPAIR_INTERVAL_S(default 300).UPDATE … SET status='sent' WHERE from_tx_hash=? AND status='bookkeeping_pending' RETURNINGinside its own transaction. That locks the row, so repairs running in parallel can't book it twice.payout_hopjournal row already exists for thatref_id, the original commit landed and only its acknowledgement was lost. The row is just markedsent.exit_fill(if there's a quote) and the pair. A row that still fails stays pending and alerts again.Tests (25 passing)
The hop test's fake database was rewritten to keep real rows, honour the
eq/andfilters, apply thepayout_forwardprimary key, and roll back a transaction that throws. New cases:{ hash, booked: false }, the row isbookkeeping_pendingwith the hash andref_id, nothing is half-written,payout_unbookedfires with the reason, and a retry still getsDuplicatePayoutError(the anchor is not paid twice).claimed, and the alert hasflagged: false.ref_id, both the quote case and the generated case, and writesexit_fill;senta row whose pair already exists;claimed,failedandsentrows.pnpm --filter @ballast/service-payout-hop test,typecheckandbuildpass. The@ballast/observabilitytests also pass.Note: this overlaps
forwardPayoutwith #1101 (the first-leg verification for #50). Whichever merges second will need a small rebase.