feat(cash-in): secure create_withdraw nostr-transport RPC (#31) #32
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/secure-cashin-rpc"
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?
Implements aiolabs/spirekeeper#31 (the operator-side half). Builds on #30 (merged).
What
A
create_withdrawnostr-transport RPC so the ATM no longer supplies the withdraw amount, fee, or attribution. The ATM sends a bunker-signed kind-21000create_withdrawwith just the grossprincipal_sats(the hardware-attested fiat value); the handler derives everything else server-side:sender_pubkey(never read from the body), matched to an active machine on the authenticated wallet.round(principal × super_cash_in) + round(principal × operator_cash_in)— per-leg rounding so it matchesparse_settlementexactly (fee_mismatch = 0).principal − fee→ the withdraw amount the customer receives.extra={source:bitspire, type:cash_in, principal_sats, fee_sats, nostr_sender_pubkey:<verified>, nostr_event_id}.Customer claims the NET link → payout carries the stamped extra (aiolabs/withdraw#3) →
_handle_paymentrecords thecash_insettlement (#30) with cryptographic attribution. This closes the vector wherelnurlw_create_linklet the ATM setmin/max_withdrawable+extrafreely (gross amount, no fee, forgeable attribution).Why it's safe to register this way
Extensions register transport RPCs directly —
withdraw/__init__.py:56already doesregister_rpc("lnurlw_create_link", …, AUTH_WALLET). We mirror it:register_rpc("create_withdraw", handle_create_withdraw, AUTH_WALLET)inspirekeeper_start, soft-failing on lnbits withoutregister_rpc.Verification
spirekeeper: registered nostr-transport RPC 'create_withdraw'.ruffclean;blackformatted.cash_insettlement + super-fee payout) already proven during the withdraw#3 / #30 validation; this PR moves the stamping from a hand-rolled link to the verified RPC handler.Wire schema
Pinned in #31 (request
principal_sats+ context; amount/fee/attribution server-derived; response{link_id, lnurl, net_sats, principal_sats, fee_sats}). bitspire is buildingLnbitsClient.createWithdrawagainst it.Follow-ups (not in this PR)
super_config.max_cash_in_satsconfig field + UI — the handler already enforces it defensively viagetattr(None = no cap).createWithdrawconsumer over a real signed RPC.🤖 Generated with Claude Code
create_withdrawnostr-transport RPC (#31)Adds a server-side cash-in RPC so the ATM no longer supplies the withdraw amount, fee, or attribution. The ATM sends a bunker-signed kind-21000 `create_withdraw` with just the gross `principal_sats` (the hardware- attested fiat value); the handler derives everything else SERVER-SIDE: - attribution = the VERIFIED transport `sender_pubkey` (never read from the body), matched to an active machine on the authenticated wallet; - fee = round(principal × super_cash_in) + round(principal × operator_cash_in), per-leg rounding so it matches parse_settlement exactly (fee_mismatch=0); - net = principal − fee → the withdraw amount the customer receives; - stamps `extra={source:bitspire, type:cash_in, principal_sats, fee_sats, nostr_sender_pubkey:<verified>, nostr_event_id}` onto the link. The customer claims the NET link; the payout carries the stamped extra (aiolabs/withdraw#3) and `_handle_payment` records the cash_in settlement (spirekeeper#30) with cryptographic attribution — closing the vector where `lnurlw_create_link` let the ATM set amount/fee/attribution freely. Registered via `register_rpc("create_withdraw", …, AUTH_WALLET)` (extensions register RPCs directly — withdraw already does). Soft-fails on lnbits without `register_rpc`. Per-tx cap reads `super_config.max_cash_in_sats` defensively (getattr) — the config field/UI is a fast-follow. Wire schema pinned in #31. Depends on #30 (consumer-side settlement fix).