S8 — Wire cash‑in path (LNURL‑withdraw outbound) into settlements #23
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?
Gap: G10 — cash‑in settlements are never recorded.
Today
tasks.py:_handle_paymentfiltersis_in=Trueonly:Cash‑in (customer hands the ATM fiat in exchange for BTC) is implemented as
an LNURL‑withdraw the customer scans at the ATM — so the resulting
LNbits
Paymentis outbound from the operator's wallet(
is_in=False). The current listener drops it on the floor, so nodca_settlementsrow is created and no super‑fee / DCA distributionfires. From the operator's books, cash‑in does not exist.
This was confirmed during dev: a redemption from a different LNbits user
landed on greg's wallet (the operator), the payment row exists, but
satmachineadmin's "Settlements" tab for the machine stayed empty.
Naming hazard
Before extending the handler, rename the conditional so we stop conflating
two inverted axes:
Payment.is_in)TrueFalsePayment.is_inis inbound at the operator's wallet on the Lightningnetwork, which is the opposite of the bitSpire business direction.
Local variables in
tasks.py/bitspire.pythat readpayment.is_inshould be aliased to
is_lightning_inbound(and its counterpartis_lightning_outboundwherever the opposite branch is named) — alwaysin pairs, because using one name without the other re‑introduces the
ambiguity the rename was meant to kill. Any branch that conditions on
payment.is_inshould carry a one‑line docstring spelling out whichbusiness direction it is catching.
Design
is_infilter. Process both directions; branch ontx_typefromPayment.extra(the bitSpire business‑direction sourceof truth), with
is_lightning_inbound/is_lightning_outboundonlyas a sanity cross‑check (
cash_out ↔ inbound,cash_in ↔ outbound).get_active_machine_by_wallet_id(payment.wallet_id).For cash‑in the wallet is the operator's wallet (LNURL‑withdraw drains
from it), so the same 1:1 machine↔wallet mapping holds — until S4
(NIP‑78 fleet roster) takes over routing.
bitspire.parse_settlementalready acceptstx_type ∈ {cash_in, cash_out}. Verify it produces sane numbers for the cash‑in case(gross/net inversion, fee direction, ATM‑side vs operator‑side fee).
rulesets don't yet distinguish — pick a default (most likely "same
split table applies") and document it. Open for product input.
coming in, not going out to LPs). The leg should be skipped, not
zero‑sized.
physical session (real incident: ~7 h between issue and redemption,
internet outage in between). Tie the LNURL TTL to the kind‑21000
RPC's NIP‑40 expiration (#15 / S1) — same window, same trust story.
On TTL expiry, the link must be revoked at LNbits so a late
redemption fails closed instead of silently draining the wallet.
reference the LNURL
k1(or its hash) and the eventual paymentpreimage so cash‑in is auditable on the same footing as cash‑out.
Acceptance criteria
tasks.pylistener no longer pre‑filters onis_in; processescash_inandcash_outviatx_typediscriminator.payment.is_inrenamed tois_lightning_inbound/is_lightning_outbound(paired), with adocstring on each conditional branch.
bitspire.parse_settlementcash‑in branch verified end‑to‑end:gross/net/fee numbers match what bitSpire reported in
Payment.extra.tx_type=cash_in.distribution.process_settlementskips the DCA leg forcash_in(skip, not zero‑sized).
expires (S1 / #15 dependency — gate this acceptance criterion on #15
landing).
(icon + tx_type chip) so operators don't have to read JSON to tell
the two flows apart.
LNbits user redeems it; the operator's machine page shows a
cash_insettlement row with correct sats and a fired super‑fee leg.
Out of scope
fiat to fund operator liquidity). Separate product workstream; this
issue is just about recording the settlement.
References
hardening step — it is a functional gap surfaced by the security
audit. Filed standalone so it can land independently and doesn't
block S0/S1/S5.
~/dev/shared/extensions/satmachineadmin/tasks.py:57(the filter)~/dev/shared/extensions/satmachineadmin/bitspire.py:parse_settlement~/dev/shared/extensions/satmachineadmin/distribution.py:process_settlementSequencing
Can land independently of the security epic, but S1 (NIP‑40 expiration,
#15) is a hard dependency for the LNURL‑withdraw lifecycle piece — file
the listener + naming + parse_settlement work as a first PR; gate the
LNURL revocation PR on #15.
Closing as duplicate of #22. Both filed within ~10 minutes on 2026-05-15 with the same scope (G10 cash-in path + naming hygiene).
The epic body (#13) references #22 as the canonical S8 tracker, so consolidating there.
Content from this issue worth merging into #22:
k1(or its hash) + payment preimage referenced from the S3 receipt event for cash-in (per #17 once that lands)Will fold these into #22's body when picking up S8 work. Until then they're preserved in this comment as a search hit.