feat(machine): secure cash-in via server-stamped create_withdraw RPC
Replaces the cash-in LNURL-withdraw creation with the secure create_withdraw
RPC (aiolabs/spirekeeper#31/#32). The ATM now sends only the hardware-attested
gross principal_sats; the operator side verifies the signer, derives fee + NET,
and stamps the link's attribution (source/nostr_sender_pubkey) from the VERIFIED
sender. Closes the dev-stack weakness where the ATM set the withdraw amount +
extra itself (could understate the fee / forge attribution).
- LnbitsClient.createWithdraw(walletId, {principal_sats, fiat_amount?, fiat_code?,
title?, wait_time?, client_ref?}) -> {link_id, lnurl, net_sats, principal_sats,
fee_sats}. Non-idempotent (mints a link) -> not retry-wrapped.
- lightning.ts generateLnurlWithdraw: createWithdrawLink -> createWithdraw; the
ATM no longer computes amount/fee/extra. LNURL-session map re-keyed on link_id
(the secure response carries no unique_hash); settlement-watch half unchanged
(subscribe_payments tag:'withdraw', link_id).
Server RPC is live on the dev stack (spirekeeper#32 registered create_withdraw),
so this is ready for the joint cash-in test. typecheck 12/12, full suite + prod
build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f4e7dcc99e
commit
9c74a28a06
4 changed files with 96 additions and 43 deletions
|
|
@ -42,6 +42,8 @@ import type {
|
|||
PaymentPushCallback,
|
||||
SubscriptionCloseCallback,
|
||||
CreateWithdrawLinkBody,
|
||||
CreateWithdrawBody,
|
||||
CreateWithdrawResult,
|
||||
LnbitsWithdrawLink,
|
||||
UniqueHashesResponse,
|
||||
} from './types.js'
|
||||
|
|
@ -388,6 +390,19 @@ export class LnbitsClient {
|
|||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* Cash-in: create a SERVER-STAMPED LNURL-withdraw via the secure
|
||||
* `create_withdraw` RPC (aiolabs/spirekeeper#31 / #32). The ATM sends only the
|
||||
* hardware-attested `principal_sats`; the operator side verifies the signer,
|
||||
* derives fee + NET, and stamps the link's attribution from the verified
|
||||
* sender — the machine cannot understate the fee or forge attribution. NOT
|
||||
* idempotent (mints a link) → not retry-wrapped; supersedes the direct,
|
||||
* client-amount `createWithdrawLink` for cash-in.
|
||||
*/
|
||||
async createWithdraw(walletId: string, body: CreateWithdrawBody): Promise<CreateWithdrawResult> {
|
||||
return this.sendRpc<CreateWithdrawResult>('create_withdraw', { walletId, body })
|
||||
}
|
||||
|
||||
async getWithdrawLink(walletId: string, id: string): Promise<LnbitsWithdrawLink> {
|
||||
return this.idempotent(() =>
|
||||
this.sendRpc<LnbitsWithdrawLink>('lnurlw_get_link', { walletId, body: { id } }),
|
||||
|
|
|
|||
|
|
@ -73,6 +73,8 @@ export type {
|
|||
PaymentPushCallback,
|
||||
SubscriptionCloseCallback,
|
||||
CreateWithdrawLinkBody,
|
||||
CreateWithdrawBody,
|
||||
CreateWithdrawResult,
|
||||
LnbitsWithdrawLink,
|
||||
UniqueHashEntry,
|
||||
UniqueHashesResponse,
|
||||
|
|
|
|||
|
|
@ -146,6 +146,45 @@ export interface SubscribeClose {
|
|||
// LNURL-withdraw (the `withdraw` extension's transport surface)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Cash-in request for the SECURE `create_withdraw` RPC (aiolabs/spirekeeper#31).
|
||||
* The ATM supplies only the hardware-attested gross principal; the operator
|
||||
* side derives fee + NET and stamps attribution from the *verified* signer, so
|
||||
* the machine cannot understate the fee or forge attribution. Contrast with
|
||||
* `CreateWithdrawLinkBody`, where the amount + extra were client-supplied.
|
||||
*/
|
||||
export interface CreateWithdrawBody {
|
||||
/** Gross principal in sats — the fiat value the ATM measured. REQUIRED. */
|
||||
principal_sats: number
|
||||
/** Fiat amount for the settlement row + display. */
|
||||
fiat_amount?: number
|
||||
/** Fiat code; defaults to the machine's configured currency server-side. */
|
||||
fiat_code?: string
|
||||
/** Link display title. */
|
||||
title?: string
|
||||
/** Seconds between withdraws (default 1). */
|
||||
wait_time?: number
|
||||
/** Audit ref → settlement.nostr_event_id (use the ATM tx id). */
|
||||
client_ref?: string
|
||||
}
|
||||
|
||||
/** Response from `create_withdraw` — server-derived amounts + the LNURL to show. */
|
||||
export interface CreateWithdrawResult {
|
||||
/** Settlement-watch key — `subscribe_payments { tag:'withdraw', link_id }`. */
|
||||
link_id: string
|
||||
/** bech32 LNURL — the QR the ATM displays. */
|
||||
lnurl: string
|
||||
/** Raw callback URL (alternative for QR generation). */
|
||||
lnurl_url?: string
|
||||
/** NET sats the customer receives (principal − fee). */
|
||||
net_sats: number
|
||||
/** Gross principal echoed back. */
|
||||
principal_sats: number
|
||||
/** Fee withheld (server-computed). */
|
||||
fee_sats: number
|
||||
k1?: string
|
||||
}
|
||||
|
||||
export interface CreateWithdrawLinkBody {
|
||||
title: string
|
||||
min_withdrawable: number
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue