Implement cash-out "Sell Bitcoin" flow with denomination-aware amount selection #11
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implement the complete "Sell Bitcoin" (cash-out) flow where users pay sats to receive physical cash from the ATM.
User Flow
Key Requirements
Denomination-Aware Amount Selection
Critical: Users must only be able to request amounts that are physically dispensable given:
For example:
Reference: lamassu-server Implementation
The original
lamassu-serverhas solved this problem. Key files to reference:lamassu-server/lib/cash-out/- Cash-out logic and denomination handlinglamassu-server/lib/dispenser.js- Bill dispenser abstractionlamassu-machine/lib/puloon/- Puloon dispenser driver with cassette managementcash_out_txs,bills,devices(cassette configuration)The solution involves:
UI Components
State Machine States
Services Required
getDispensableAmounts()- Returns valid fiat amounts based on inventorycalculateBillCombination(amount)- Returns which bills to dispensegenerateNoffer(amountSats)- Creates payment QR (already implemented)dispenseCash(bills[])- Interface with HAL dispenser driverupdateInventory(dispensed[])- Track remaining billsAcceptance Criteria
Technical Notes
Related
cf05373@lamassu/clink@lamassu/state-machineImplementation Plan (Refined)
After reviewing lamassu-machine/server and our existing invoice monitoring, here's the refined approach:
Key Decision: Direct BOLT11 Invoice (not spontaneous noffer)
The current implementation shows a "spontaneous" noffer where the user enters the amount in their wallet. This is problematic because:
New approach: ATM-driven amount selection with direct BOLT11 invoice.
State Machine Flow
Implementation Phases
Phase 1 (MVP):
selectingAmountstate to state machinelightningPub.createInvoice()lightningPub.watchInvoice()(already implemented, polls every 2s)PAYMENT_RECEIVEDevent → dispensePhase 2:
computeCashOut()service returningactiveMapfor button enable/disablePhase 3:
Reference: lamassu-machine Key Patterns
From
lamassu-machine/lib/brain.jsandlib/tx.js:Tx.computeCashOut(tx, units, virtualUnits, txLimit)- validates denominationscoin-change.js- greedy solver for bill combinationsactiveMap- dictionary of{denomination: boolean}for UI button statesInvoice Monitoring (Already Available)
Polls
lookupInvoice()via Kind 21000 RPC every 2 seconds.Starting implementation now.
Progress Update: Invoice Payment Detection Fixed
Commit 7defc10 fixes a critical bug in the cash-out flow where invoice payments weren't being detected.
Problem
GetPaymentStateRPC was returning "invoice not found" even after invoices were successfully paid. This caused the ATM to hang after creating an invoice for the customer.Root Cause
GetPaymentStateis for checking outgoing payments (invoices you've paid to others), not incoming payments (invoices you've created that others pay).Lightning.Pub's code path:
Solution
Rewrote
watchInvoice()to use subscription-based detection viaGetLiveUserOperations:requestId === "GetLiveUserOperations"operation.type === "INCOMING_INVOICE"and compare invoice identifiersDocumentation
Added Issue #8 and #9 to
packages/lightning/TROUBLESHOOTING.mddocumenting this gotcha for future reference. Total debugging time documented: ~5+ hours. Following the troubleshooting guide should reduce that to ~15 minutes.Testing
Verified end-to-end with
alice-pay- the ATM now correctly detects when an invoice is paid and completes the cash-out flow.Split this into CLINK-side (noffer) and LNbits-side (invoice + subscription) work
This issue currently mixes two distinct concerns:
Generate a noffer (CLINK kind-21001). noffer is a Lightning.Pub-specific event type in the CLINK protocol (https://github.com/shocknet/CLINK). LNbits has no CLINK support and the
nostr-native-transportwork (see #22) does not add it. Generating and broadcasting noffers stays on LP (or in CLINK-aware client code, depending on which side mints them).Detect that the customer paid the invoice underneath the noffer. This part has clean LNbits coverage post-migration:
create_invoice({amount, memo, extra?})over the nostr transport, against the ATM's LNbits wallet. Returns the BOLT11 the noffer would point to.subscribe_payments({wallet_id: <atm>, payment_hash: <hash>})over the same transport — real-time encrypted push the moment the invoice settles. Already exercised end-to-end in the LNbits driver script--flow subscribe.Recommended split:
create_hold_invoice/settle_hold_invoice/cancel_hold_invoiceto the transport — which is a separate open design decision, currently absent).Optional session correlation: the
extradict oncreate_invoiceis opaque to LNbits; passextra={"session_id": "..."}and the value rides through to thesubscribe_paymentspush (Payment.extra is preserved unchanged). Lets the ATM correlate any of N concurrent cash-out sessions without amount/timing heuristics.Net: the LNbits side of this work is ready to consume. The noffer/CLINK side stays on LP and is not blocked by anything in the LNbits transport.