Repository navigation
Reserve payouts exactly and record chain transfers before broadcast - #381
Merged
Merged
Conversation
added 2 commits
October 5, 2026 11:30
…work Compose the corrected receipts and transfers head with the corrected Sui adapter head (prepared transfers with a persisted identity, coin selection without repeated objects) as the base of the payout reservation and refund change, which uses both. GitHub issue #80.
Executor payouts zeroed the balance after the transfer, so earnings credited in between were lost; refunds and payouts held a SQL transaction across the chain call, and a transaction-wide refund wrote order states unconditionally and refunded credited orders. Give payouts and refunds one lifecycle: prepare the transfer (a chain read), commit the local decision together with a reserved chain transfer row carrying the digest, the signed transaction and its signature, broadcast the signed transaction under a two-minute bound, and record the outcome by a conditional write: sent, failed (a failed payout releases its amount in the same write) or unknown. Reconcile sent, unknown and stale reserved rows on the payout loop's ticker with one lookup: confirmed with the expected effects, failed, or resubmitted once from the identical stored bytes when the chain does not know the transaction; anything unverified stays unknown. A payout reserves exactly the balance it read and is refused while another is open; refunds change the transaction status and each outstanding order conditionally, with a refund settlement row, before the chain call. The maintenance pass settles chain refunds of failed runs through the same lifecycle and never selects an order with a refund transfer again. GitHub issues #80 and #81.
ctfbruce
marked this pull request as ready for review
October 5, 2026 10:01
This was referenced Oct 5, 2026
ctfbruce
marked this pull request as draft
October 5, 2026 11:07
ctfbruce
marked this pull request as ready for review
October 5, 2026 11:21
ctfbruce
marked this pull request as draft
October 5, 2026 11:58
After a refund decision was committed, the transfer lifecycle returned no outcome to its caller, so a buyer refused during maintenance was told the payment had been refunded even when the transfer failed or its outcome was unknown, and later requests repeated it. Return the recorded outcome and word the refusal as sent, pending confirmation, or recorded but not sent and owed; a repeated request reads the refund transfer's state. The order stays unspendable after the decision. A reconciliation lookup ran under the daemon's lifetime context on an SDK path with no deadline, so one lookup that never completed stopped every later row and all payouts. Bound each lookup; a timeout is recorded as an unknown outcome with the reservation kept, and the pass continues with the next row. GitHub issues #80 and #81.
ctfbruce
marked this pull request as ready for review
October 5, 2026 12:32
ctfbruce
marked this pull request as draft
October 5, 2026 13:53
ctfbruce
marked this pull request as ready for review
October 5, 2026 14:03
This was referenced Oct 5, 2026
5 tasks
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.
Behaviour
Executor payouts and refunds go through one durable transfer lifecycle (
internal/dispatcher/payments/transfers.go):reservedrow ofchain_transferscarrying the transaction digest, the signed transaction and its signature are committed, then the signed transaction is broadcast (bounded by two minutes), and the outcome is recorded by a conditional write on the state the caller read:sent,failed(not broadcast, or failed on chain; a failed payout releases its amount in the same write) orunknown(lost response or timeout). No SQL transaction spans a chain call.ReconcileTransfers, on the payout loop's one-minute ticker) resolvessentandunknownrows andreservedrows older than ten minutes with one lookup: a confirmed transaction with the expected effects becomesconfirmed; a failed onefailed(payout released); a transaction the chain does not know is resubmitted once from the identical stored bytes, only fromreservedorunknown; an unverified or inconclusive result staysunknownwith the last check indetail.PayoutExecutorreserves exactly the balance it read (ReservePayout, one conditional update requiring the balance and no open payout of that executor and currency); earnings credited during the transfer are kept; zero balances and non-chain currencies are skipped; executors without a wallet produce one warning per pass.Outstanding -> Refundedtransition per order and arefundsettlement row commit before the chain call; credited or refunded orders are never refunded; a repeated request sends nothing; the terminal USDC refund andRefundDebugletOrderuse the same path; TEST behaviour is unchanged (bookkeeping only).docs/operations/payments.mddescribes the lifecycle, the reconciliation table and what an operator sees forfailedandunknownrows.Proof
-race.Upgrade
Dispatcher schema 22 (from the receipts change) is required; this change adds no migration.
Stacked on #374 (receipts and the
chain_transferstable) and #369 (prepared transfers); both are merged into this branch, so the diff againstmainshows their commits until they merge.Limits
failedrefund is not re-sent as a new row; the row is the record of what is owed.unknownand blocks that executor's payouts until an operator acts (documented on the operations page).SettlePendingOrderswith the scripted chain; the dispatcher's timed sweep is tested for its forwarding only.reservedorunknownon every reconciliation pass; if the bytes can no longer execute and the node's rejection is not reported as not broadcast, the row returns tounknownand is resubmitted each minute without a bound. The digest never changes, so nothing is paid twice, but anunknownpayout keeps that executor's later payouts waiting until an operator resolves it. A bound on resubmission is a following change (Bound the resubmission of a chain transfer the node reports as not found #385).TransferUSDCsends coins outside the lifecycle and has no production caller; it is left for the same trim.SettleEarningand the backend'sRefundDebugletandTransferCoinsare no longer used by payouts and refunds and are left for a later trim.GitHub issues #80 and #81.
Candidate CI
Run 37292787301 on 4672277: 16 of 16 lanes and
requiredsucceeded. The branch then took the adapter's second correction by merge (76a70bd): run 37300726652, 16 of 16 lanes andrequiredsucceeded on attempt 2 (attempt 1 failed in theraceandtestlanes on a data race inside an executor test and an export deadline, both outside the diff). Corrections 66fe628: the maintenance refusal now says whether the recorded refund was sent, is pending confirmation, or could not be sent and is owed, on the deciding and on later requests, instead of "has been refunded"; each reconciliation lookup and the preparation read are bounded (30 s), a lookup that times out leaves the transferunknownwith its reservation kept and the pass continues. Run 37307615519 on 66fe628: 16 of 16 lanes andrequiredsucceeded.