Context
Review of #4181 found two remaining paths where a broadcast can succeed but an empty or missing transaction identifier is handled as a pre-broadcast failure. PayoutStrategy.handleBroadcastError then rolls the order back to PREPARATION_CONFIRMED, allowing the payout cron to broadcast again.
These are merge blockers for #4181 because they preserve the double-payout class the PR is intended to eliminate.
1. Lightning invoice payouts can be retried with a new invoice
PayoutLightningService.sendPayment treats an empty transaction ID as ambiguous only for LN_NID keysend payouts. For LN_URL and LND_HUB, an empty hash is returned unchanged and a missing payment_hash can throw a plain mapping error after the LND send call.
Current failure sequence:
- The order is persisted as
PAYOUT_DESIGNATED.
- LND may successfully execute the payment.
- The response contains an empty/missing
payment_hash, or mapping it fails.
pendingPayout rejects the empty ID.
- The resulting plain error is classified as pre-broadcast and the order is rolled back.
- The retry calls
getInvoiceByLnurlp or getInvoiceByLndhub again and may receive a new invoice with a different payment hash.
- LND deduplication of the original invoice therefore does not reliably prevent a second payment.
The existing test named returns an empty payment hash unchanged for a non-keysend (invoice payment_hash dedup covers a retry) codifies the unsafe assumption and should be replaced.
Expected behavior
After the LND send call has been reached, a missing, empty, or unparseable payment hash must be classified as an ambiguous broadcast result and surface as TxBroadcastError / PayoutBroadcastException for all Lightning payout types.
An explicit in-band payment_error may remain separately classified when it proves the payment was not routed.
2. Cardano accepts an empty txSubmit result
CardanoClient.sendNativeCoin returns the result of blockFrostApi.txSubmit without validating that a non-empty transaction hash was returned.
Current failure sequence:
- The order is persisted as
PAYOUT_DESIGNATED.
- The Blockfrost submit request may be accepted.
- A successful but empty/malformed response produces an empty transaction hash.
pendingPayout throws a plain error.
- The error is classified as pre-broadcast, the designation is rolled back, and the next cron run can submit another payout.
Expected behavior
An empty Cardano transaction hash returned after txSubmit must throw TxBroadcastError inside the broadcast boundary so the order remains fail-closed.
Acceptance criteria
References
Context
Review of #4181 found two remaining paths where a broadcast can succeed but an empty or missing transaction identifier is handled as a pre-broadcast failure.
PayoutStrategy.handleBroadcastErrorthen rolls the order back toPREPARATION_CONFIRMED, allowing the payout cron to broadcast again.These are merge blockers for #4181 because they preserve the double-payout class the PR is intended to eliminate.
1. Lightning invoice payouts can be retried with a new invoice
PayoutLightningService.sendPaymenttreats an empty transaction ID as ambiguous only forLN_NIDkeysend payouts. ForLN_URLandLND_HUB, an empty hash is returned unchanged and a missingpayment_hashcan throw a plain mapping error after the LND send call.Current failure sequence:
PAYOUT_DESIGNATED.payment_hash, or mapping it fails.pendingPayoutrejects the empty ID.getInvoiceByLnurlporgetInvoiceByLndhubagain and may receive a new invoice with a different payment hash.The existing test named
returns an empty payment hash unchanged for a non-keysend (invoice payment_hash dedup covers a retry)codifies the unsafe assumption and should be replaced.Expected behavior
After the LND send call has been reached, a missing, empty, or unparseable payment hash must be classified as an ambiguous broadcast result and surface as
TxBroadcastError/PayoutBroadcastExceptionfor all Lightning payout types.An explicit in-band
payment_errormay remain separately classified when it proves the payment was not routed.2. Cardano accepts an empty
txSubmitresultCardanoClient.sendNativeCoinreturns the result ofblockFrostApi.txSubmitwithout validating that a non-empty transaction hash was returned.Current failure sequence:
PAYOUT_DESIGNATED.pendingPayoutthrows a plain error.Expected behavior
An empty Cardano transaction hash returned after
txSubmitmust throwTxBroadcastErrorinside the broadcast boundary so the order remains fail-closed.Acceptance criteria
LN_URL,LND_HUB, andLN_NID.txSubmitresults throwTxBroadcastError.PAYOUT_DESIGNATEDand is not automatically rebroadcast.References
src/subdomains/supporting/payout/services/payout-lightning.service.tssrc/integration/lightning/services/lightning.service.tssrc/integration/blockchain/cardano/cardano-client.tssrc/subdomains/supporting/payout/strategies/payout/impl/base/payout.strategy.ts