feat(v2): wire bitSpire invoice listener + settlement landing (P1a)
Replaces the no-op tasks.py stub with a real invoice listener that lands
bitSpire settlements idempotently into dca_settlements.
Architecture: satmachineadmin runs *inside* the LNbits process, so it
plugs into LNbits' canonical extension hook (register_invoice_listener
from lnbits.tasks) instead of going through the Nostr transport layer.
External clients like bitSpire use Nostr; internal extensions consume
the resulting Payment objects directly. One invoice_listener queue per
extension, dispatched by invoice_callback_dispatcher.
Flow:
bitSpire ATM (Nostr kind-21000)
→ LNbits nostr_transport handler
→ core Payment system (create_invoice + status=SUCCESS on settle)
→ invoice_callback_dispatcher
→ satmachineadmin's invoice_queue
→ _handle_payment filters by wallet_id → active machine
→ bitspire.parse_settlement reads Payment.extra (or back-derives)
→ create_settlement_idempotent (keyed on payment_hash UNIQUE)
The parser (new bitspire.py module) is bitSpire-specific:
- Happy path (post-aiolabs/lamassu-next#44): Payment.extra carries
{source:"bitspire", net_sats, fee_sats, fee_pct, exchange_rate,
currency, txid, machine_npub, bills, cassettes}. Read directly,
zero back-derivation.
- Fallback path (pre-#44): extra is absent. Back-derive the split
using machine.fallback_commission_pct with the Lamassu-style
formula (calculations.calculate_commission), mark
used_fallback_split=true, log a WARNING that namechecks the
upstream issue so it's findable in logs.
Two-stage commission split (super first, operator remainder) is
computed at land time so the audit row is complete:
platform_fee_sats = round(commission_sats * super_fee_pct)
operator_fee_sats = commission_sats - platform_fee_sats
The actual payout (LP DCA legs + super-fee leg + operator-split legs)
happens in a separate settlement-processor task in P2. P1 only LANDS
the settlement with status='pending'.
Smoke-tested both paths against real LNbits 1.4 (nostr-transport venv):
happy: 266800 gross → 258835 net + 7965 commission
(2390 super @ 30%, 5575 operator)
fallback: 266800 gross → 254095 net + 12705 commission @ 5% default
Also adds crud.get_active_machine_by_wallet_id, the lookup that gates
inbound payments to known machine wallets.
Refs: aiolabs/satmachineadmin#9, aiolabs/lamassu-next#44
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
cba327d0f0
commit
b91e49b642
3 changed files with 258 additions and 13 deletions
13
crud.py
13
crud.py
|
|
@ -118,6 +118,19 @@ async def get_machine_by_npub(machine_npub: str) -> Optional[Machine]:
|
|||
)
|
||||
|
||||
|
||||
async def get_active_machine_by_wallet_id(wallet_id: str) -> Optional[Machine]:
|
||||
"""Used by the invoice listener to route an incoming payment to a machine."""
|
||||
return await db.fetchone(
|
||||
"""
|
||||
SELECT * FROM satoshimachine.dca_machines
|
||||
WHERE wallet_id = :wid AND is_active = true
|
||||
LIMIT 1
|
||||
""",
|
||||
{"wid": wallet_id},
|
||||
Machine,
|
||||
)
|
||||
|
||||
|
||||
async def get_machines_for_operator(operator_user_id: str) -> List[Machine]:
|
||||
return await db.fetchall(
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue