diff --git a/apps/machine/src/services/lightning.ts b/apps/machine/src/services/lightning.ts index 5b60b2f..e975277 100644 --- a/apps/machine/src/services/lightning.ts +++ b/apps/machine/src/services/lightning.ts @@ -109,27 +109,33 @@ const SESSION_SAFETY_TIMEOUT_MS = 15 * 60 * 1000 /** Active LNURL-withdraw session */ interface LnurlSession { sessionId: string - /** Link ID — the management + settlement-watch key (delete/subscribe). */ + /** Link ID for management operations (delete/update) */ linkId: string + uniqueHash: string satsAmount: number status: 'active' | 'claimed' | 'expired' createdAt: number cleanup?: () => void } -/** Map of linkId -> LNURL session data. Keyed on link_id since the secure - * `create_withdraw` response (spirekeeper#31) carries no `unique_hash`. */ +/** Map of uniqueHash -> LNURL session data */ const lnurlSessions = new Map() /** - * Register a new LNURL-withdraw session, keyed by linkId. + * Register a new LNURL-withdraw session */ -function registerLnurlSession(sessionId: string, linkId: string, satsAmount: number): void { - console.log('[LNURL Session] Registering:', linkId, 'for', satsAmount, 'sats') +function registerLnurlSession( + sessionId: string, + linkId: string, + uniqueHash: string, + satsAmount: number, +): void { + console.log('[LNURL Session] Registering:', uniqueHash, 'for', satsAmount, 'sats') - lnurlSessions.set(linkId, { + lnurlSessions.set(uniqueHash, { sessionId, linkId, + uniqueHash, satsAmount, status: 'active', createdAt: Date.now(), @@ -137,10 +143,10 @@ function registerLnurlSession(sessionId: string, linkId: string, satsAmount: num // Safety timeout — normally cleaned up by state machine on idle transition. setTimeout(() => { - const session = lnurlSessions.get(linkId) + const session = lnurlSessions.get(uniqueHash) if (session && session.status === 'active') { - console.warn('[LNURL Session] Safety timeout reached, expiring:', linkId) - expireLnurlSession(linkId) + console.warn('[LNURL Session] Safety timeout reached, expiring:', uniqueHash) + expireLnurlSession(uniqueHash) } }, SESSION_SAFETY_TIMEOUT_MS) } @@ -148,23 +154,23 @@ function registerLnurlSession(sessionId: string, linkId: string, satsAmount: num /** Invalidate an active LNURL session by cash-in sessionId. The session's * cleanup closure unsubscribes from LNbits and deletes the link. */ function invalidateLnurlSessionBySessionId(sessionId: string): void { - for (const [linkId, session] of lnurlSessions.entries()) { + for (const [hash, session] of lnurlSessions.entries()) { if (session.sessionId === sessionId && session.status === 'active') { - console.log('[LNURL Session] Invalidating previous session:', linkId) - expireLnurlSession(linkId) + console.log('[LNURL Session] Invalidating previous session:', hash) + expireLnurlSession(hash) } } } /** Expire a single LNURL session via its cleanup closure. */ -function expireLnurlSession(linkId: string): void { - const session = lnurlSessions.get(linkId) +function expireLnurlSession(uniqueHash: string): void { + const session = lnurlSessions.get(uniqueHash) if (!session || session.status !== 'active') return - console.log('[LNURL Session] Expiring:', linkId) + console.log('[LNURL Session] Expiring:', uniqueHash) session.status = 'expired' if (session.cleanup) session.cleanup() - setTimeout(() => lnurlSessions.delete(linkId), 60000) + setTimeout(() => lnurlSessions.delete(uniqueHash), 60000) } let _lnbitsRef: LnbitsClient | null = null @@ -645,53 +651,50 @@ function createATMServices( invalidateLnurlSessionBySessionId(context.cashInSessionId) } - // Secure cash-in: the ATM sends only the hardware-attested gross - // principal; the operator side verifies the signer, derives fee + NET, - // and stamps attribution (spirekeeper#31/#32). The ATM no longer sets - // the amount or extra. We display the returned LNURL (for NET) and - // watch link_id for settlement. - const link = await lnbits.createWithdraw(lnbitsWalletId, { - principal_sats: context.satsAmount, - fiat_amount: context.fiatCents / 100, - fiat_code: context.currency, + const link = await lnbits.createWithdrawLink(lnbitsWalletId, { title: `bitSpire Cash-In ${context.cashInSessionId?.slice(0, 8) || 'session'}`, - client_ref: context.txid ?? context.cashInSessionId ?? undefined, + min_withdrawable: context.satsAmount, + max_withdrawable: context.satsAmount, + uses: 1, + wait_time: 1, + is_unique: false, }) if (!link.lnurl) { throw new Error( - '[ATM Service] create_withdraw returned no lnurl — check withdraw#3 / LNBITS_BASEURL on the server' + '[ATM Service] LNbits returned link.lnurl=null — check LNBITS_BASEURL on the server (aiolabs/withdraw#1)' ) } const lnurl = link.lnurl.toUpperCase() - console.log( - `[ATM Service] create_withdraw: principal=${link.principal_sats} fee=${link.fee_sats} net=${link.net_sats} link=${link.link_id}` - ) if (context.cashInSessionId) { - // Track the NET (what the customer withdraws); keyed by link_id. - registerLnurlSession(context.cashInSessionId, link.link_id, link.net_sats) + registerLnurlSession( + context.cashInSessionId, + link.id, + link.unique_hash, + context.satsAmount, + ) const subId = await lnbits.subscribePayments( lnbitsWalletId, - { tag: 'withdraw', link_id: link.link_id, max_seconds: 600 }, + { tag: 'withdraw', link_id: link.id, max_seconds: 600 }, (push) => { console.log('[ATM Service] LNURL-withdraw claimed (LNbits push)!') - const session = lnurlSessions.get(link.link_id) + const session = lnurlSessions.get(link.unique_hash) if (session) { session.status = 'claimed' - lnurlSessions.delete(link.link_id) + lnurlSessions.delete(link.unique_hash) } if (onPaymentCallback) { - onPaymentCallback(push.preimage ?? `lnurl-withdraw-${link.link_id}`) + onPaymentCallback(push.preimage ?? `lnurl-withdraw-${link.unique_hash}`) } }, ) // Wire per-session cleanup so abort/expiry tears it down cleanly. - const session = lnurlSessions.get(link.link_id) + const session = lnurlSessions.get(link.unique_hash) if (session) { session.cleanup = () => { void lnbits.unsubscribe(lnbitsWalletId, subId).catch(() => {}) - void lnbits.deleteWithdrawLink(lnbitsWalletId, link.link_id).catch(() => {}) + void lnbits.deleteWithdrawLink(lnbitsWalletId, link.id).catch(() => {}) } } } diff --git a/packages/lnbits/src/client.ts b/packages/lnbits/src/client.ts index f75db3f..de408a1 100644 --- a/packages/lnbits/src/client.ts +++ b/packages/lnbits/src/client.ts @@ -42,8 +42,6 @@ import type { PaymentPushCallback, SubscriptionCloseCallback, CreateWithdrawLinkBody, - CreateWithdrawBody, - CreateWithdrawResult, LnbitsWithdrawLink, UniqueHashesResponse, } from './types.js' @@ -390,19 +388,6 @@ 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 { - return this.sendRpc('create_withdraw', { walletId, body }) - } - async getWithdrawLink(walletId: string, id: string): Promise { return this.idempotent(() => this.sendRpc('lnurlw_get_link', { walletId, body: { id } }), diff --git a/packages/lnbits/src/index.ts b/packages/lnbits/src/index.ts index 9618a2f..2085593 100644 --- a/packages/lnbits/src/index.ts +++ b/packages/lnbits/src/index.ts @@ -73,8 +73,6 @@ export type { PaymentPushCallback, SubscriptionCloseCallback, CreateWithdrawLinkBody, - CreateWithdrawBody, - CreateWithdrawResult, LnbitsWithdrawLink, UniqueHashEntry, UniqueHashesResponse, diff --git a/packages/lnbits/src/types.ts b/packages/lnbits/src/types.ts index 17af86f..c040b3c 100644 --- a/packages/lnbits/src/types.ts +++ b/packages/lnbits/src/types.ts @@ -146,45 +146,6 @@ 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