Improve transaction limit enforcement for cash-in and cash-out #35
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?
lamassu-next has basic per-bill validation for cash-in and per-denomination inventory checks for cash-out, but there are gaps compared to the legacy lamassu-machine that could lead to users inserting cash beyond available BTC or selecting undispensable amounts.
Cash-In Issues
1. Bills accepted when rate/balance unknown
When
exchangeRate === 0oravailableBalance <= 0, the guard returnstrue— bills are accepted without limit.File:
apps/machine/src/views/CashInView.vue:137-145Fix: Reject bills if rate or balance is unknown. Show "Temporarily unavailable" instead of accepting unlimited cash.
2. Weak UX on bill rejection
When a bill exceeds the remaining balance, it's physically rejected but the user gets minimal feedback.
Legacy behavior (
brain.js:3014-3020): Shows a "highBill" dialog telling the user the maximum denomination they can still insert, with reason'lowBalance'.lamassu-next: Just shows "Maximum amount reached" text. No indication of which bills ARE still accepted.
Fix: Calculate and display the highest acceptable denomination when rejecting a bill.
3. Balance fetched once, never refreshed
lamassu-next fetches
availableBalanceonce at the start of the transaction (machine.ts:74-84). If the balance changes during a long transaction (e.g., another user is also cashing in on the same LP), the local check becomes stale.Legacy behavior (
brain.js:2701-2706): Continuously polls balance from the trader/server during the transaction.Fix: Poll LP balance periodically during
insertingBillsstate, or at minimum re-check before each bill acceptance.Cash-Out Issues
4. No coin-change validation
lamassu-next uses simple per-denomination inventory checks:
selected[denom] < inventory[denom]. This doesn't validate that the total selected amount is actually dispensable as a combination of available bills.Legacy behavior (
tx.js:166-190): Uses a coin-change algorithm that:Fix: Add coin-change validation when the user confirms their selection, or better yet, pre-validate on each denomination addition (like legacy does).
5. No "out of cash" state
If all cassettes are empty, lamassu-next stays in
selectingAmountwith all buttons disabled but no explicit message.Legacy behavior (
brain.js:3871): Transitions to a dedicatedoutOfCashtimed state with a clear message, then returns to idle.Fix: Add an
outOfCashstate or guard that prevents entering cash-out flow when inventory is empty.Edge Cases
Proposed Fixes
trueinsertingBillsstategetAmountToHardLimit()for regulatory caps; lamassu-next has noneReferences
packages/state-machine/src/machine.ts:350-357apps/machine/src/views/CashOutView.vue:48-77lamassu-machine/lib/brain.js:2964-3070lamassu-machine/lib/tx.js:166-190lamassu-machine/lib/brain.js:3847-3871LNbits side:
get_wallet+ push events fromsubscribe_paymentsThe "periodic balance refresh during cash-in" need maps cleanly to the LNbits transport (see #22):
Pull side —
get_walletover nostr:Returns the wallet's current
balance_msat. Cheap call; safe to invoke once per bill accept (or on a 5-10 second interval during cash-in). Equivalent of LP'sGetUserInfofor the balance-only case.Push side —
subscribe_paymentsalready covers the right surface:The recently-shipped outgoing-payment fanout (LNbits commit
085fd501) means the ATM's subscription matches both directions on a wallet. Sosubscribe_payments({wallet_id: <atm_wallet>})(with nopayment_hashfilter — just wallet scope) streams every settlement on the wallet in real time. Each push carries the fullPaymentobject including amount and direction; the ATM derives the new balance from the running tally without a separateget_walletpoll between bills.Recommended cash-in flow:
get_wallet→ cache the baselinebalance_msat.subscribe_payments({wallet_id})for the duration of the session.This eliminates the "bills accepted without a fresh rate/balance check" race the issue describes — the balance is always current to the last settled payment, with no polling latency.
Optional safety net: still call
get_walletperiodically (every 60-120s) as a reconciliation guard against any subscription drop / missed event. Theextradict on each payment also gives you a place to thread session correlation if multiple ATMs share a wallet.Net: LNbits side is in place. No new RPCs needed for this issue.
Status check against current
dev(2026-07-04 review): the cash-in section is now resolved.CashInView.canAcceptBillwere already fail-closed on dev (both reject whenexchangeRate === 0 || availableBalance <= 0) — the issue's cited fail-open guard no longer matches the code.stores/atm.ts(onHalBillRead: "no rate yet, accept anyway" → stack) — the layer that physically takes the bill. PR #77 makes it fail-closed (unknown rate/balance, or machine not ininsertingBills→ bill returned to customer) and moves balance gating to the pre-stackBILL_PENDINGguard.Cash-out-side items in this issue (denomination-aware selection limits etc.) were not re-verified in this pass and may still stand.