Skip to content

fix(payout): fail closed on missing Lightning and Cardano transaction IDs #4194

Description

@TaprootFreak

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:

  1. The order is persisted as PAYOUT_DESIGNATED.
  2. LND may successfully execute the payment.
  3. The response contains an empty/missing payment_hash, or mapping it fails.
  4. pendingPayout rejects the empty ID.
  5. The resulting plain error is classified as pre-broadcast and the order is rolled back.
  6. The retry calls getInvoiceByLnurlp or getInvoiceByLndhub again and may receive a new invoice with a different payment hash.
  7. 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:

  1. The order is persisted as PAYOUT_DESIGNATED.
  2. The Blockfrost submit request may be accepted.
  3. A successful but empty/malformed response produces an empty transaction hash.
  4. pendingPayout throws a plain error.
  5. 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

  • Empty/missing Lightning payment hashes after the send call fail closed for LN_URL, LND_HUB, and LN_NID.
  • Post-send Lightning response-mapping failures cannot become plain pre-broadcast errors.
  • The incorrect invoice-dedup test is replaced with a regression test proving no rollback/rebroadcast occurs.
  • Empty Cardano txSubmit results throw TxBroadcastError.
  • A Cardano regression test proves the order remains PAYOUT_DESIGNATED and is not automatically rebroadcast.
  • Existing payout/client tests, type-check, lint, and formatting checks remain green.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions