/** * Invoice amount verification for auto-pay flows. * * SECURITY (HIGH): Any invoice returned by a third-party LNURL / Lightning * address server MUST be verified before it is paid programmatically (NWC / * WebLN). Otherwise a malicious zap target can return a 1,000,000-sat invoice * for a 21-sat zap and the app pays it silently. We decode the bolt11 amount * ourselves and refuse to auto-pay if it doesn't match what the user intended. */ import { getSatoshisFromBolt11 } from '@/lib/bolt11'; /** * Assert that a bolt11 invoice encodes an amount matching `expectedSats`. * * - Rejects a malformed / unparseable invoice. * - Rejects an amountless invoice (0) when a concrete amount was expected — an * amountless invoice would let the wallet be prompted for an arbitrary amount. * - Allows a tiny tolerance for rounding between millisat/sat conversions. * * @throws Error with a clear message if the invoice must NOT be auto-paid. */ export function assertInvoiceAmount(invoice: string, expectedSats: number): void { if (!invoice || typeof invoice !== 'string') { throw new Error('The Lightning service returned an invalid invoice.'); } let invoiceSats: number; try { invoiceSats = getSatoshisFromBolt11(invoice); } catch { throw new Error( 'Could not verify the invoice amount — refusing to auto-pay for your safety.', ); } // Amountless invoice: never auto-pay when we intended a specific amount. if (invoiceSats === 0 && expectedSats > 0) { throw new Error( 'The invoice has no fixed amount — refusing to auto-pay for your safety.', ); } // Allow 1 sat of rounding slack (millisat↔sat conversions). const diff = Math.abs(invoiceSats - expectedSats); if (diff > 1) { throw new Error( `Invoice amount mismatch: expected ${expectedSats} sats but the invoice ` + `requests ${invoiceSats} sats. Payment blocked for your safety.`, ); } }