fix(cash-out): settlement watch missed one-tap payments #99

Merged
padreug merged 2 commits from fix/cashout-settlement-race into dev 2026-09-22 16:29:36 +00:00
Owner

A one-tap Bolt Card Complete settles in about a second. Subscribing took two sequential nostr round trips first, roughly eight seconds against a remote relay, because the watch was armed when the invoice was displayed. The settlement push is ephemeral with no replay, so it fired before anything was listening: the machine sat on a paid invoice until it timed out and the sats were taken with no cash dispensed. On sintra 2026-09-22 this hit both one-tap sells. The old two-tap flow only worked because fumbling with the card covered the window.

The watch is now armed during invoice creation, so the invoice cannot reach the screen unwatched. The payment hash comes from the creation response rather than a decode round trip that was half the window. A settlement that still beats the consumer is latched and replayed, and a get_payment poll runs alongside the subscription so a lost push cannot strand a payment either.

Separately, a card pull that is accepted but never settles now logs the txid and shows a notice on screen instead of returning to the amount screen as though nothing happened.

Typecheck clean; 116 tests including seven covering the race; full Electron build green.

A one-tap Bolt Card Complete settles in about a second. Subscribing took two sequential nostr round trips first, roughly eight seconds against a remote relay, because the watch was armed when the invoice was displayed. The settlement push is ephemeral with no replay, so it fired before anything was listening: the machine sat on a paid invoice until it timed out and the sats were taken with no cash dispensed. On sintra 2026-09-22 this hit both one-tap sells. The old two-tap flow only worked because fumbling with the card covered the window. The watch is now armed during invoice creation, so the invoice cannot reach the screen unwatched. The payment hash comes from the creation response rather than a decode round trip that was half the window. A settlement that still beats the consumer is latched and replayed, and a get_payment poll runs alongside the subscription so a lost push cannot strand a payment either. Separately, a card pull that is accepted but never settles now logs the txid and shows a notice on screen instead of returning to the amount screen as though nothing happened. Typecheck clean; 116 tests including seven covering the race; full Electron build green.
A one-tap Bolt Card Complete settles in about a second. Subscribing took
two sequential nostr round trips first — decode_payment to recover the
hash, then subscribe_payments — roughly eight seconds against a remote
relay, because the watch was armed when the invoice was DISPLAYED. The
settlement push is an ephemeral event with no replay, so it fired before
anything was listening: the machine sat on a paid invoice until it timed
out and the customer's sats were taken with no cash dispensed. On sintra
2026-09-22 this hit both one-tap sells (26,660 and 26,500 sats). The old
two-tap flow only ever worked because fumbling with the card covered the
window; at 07:18 the push landed two seconds after the watch went live.

Three layered defences, one mechanism:
- Arm at creation. generateInvoice does not resolve until the watch is
  live, so the invoice cannot reach the screen unwatched.
- Take the payment hash from the create_invoice response instead of
  decoding it back off the bolt11 — the value was already in hand and
  the round trip was half the window (repo guidance says as much).
- Latch and poll. A settlement that still beats the consumer is replayed
  on attach, and get_payment runs alongside the subscription so a push
  that is lost or never sent cannot strand a payment either.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
When a card accepted a cash-out pull and settlement never confirmed, the
machine returned to the amount screen as though nothing had happened —
the customer's wallet had paid and there was nothing on screen or in the
journal to say so. Latch that transition, log it with the txid, and show
a red notice naming the reference an operator can reconcile against.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
padreug deleted branch fix/cashout-settlement-race 2026-09-22 16:29:36 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/bitspire!99
No description provided.