/** * usePayPaymentPointer — pay a seller's payment pointer from whatever wallet the * buyer has, mirroring the zap rails: **NWC → WebLN → Cashu melt**. * * The pointer comes from an accepted marketplace offer (`["payment", value, kind]`): * - `lninvoice` / `lnaddress` are payable today; * - `creq` (NUT-18 Cashu request) and `clink` are recognized but not payable yet. * * This NEVER holds funds: it pays the seller's own invoice/address directly. * No Nostr event is published here — recording the trade is the existing `31871` * attestation flow. */ import { useCallback, useState } from 'react'; import type { WebLNProvider } from '@webbtc/webln-types'; import { useNWC } from '@/hooks/useNWCContext'; import { useWallet } from '@/hooks/useWallet'; import { useLightningAddress } from '@/hooks/useLightningAddress'; import { useCashu } from '@/hooks/wallet/useCashu'; import { buildReceipt, type PaymentReceipt } from '@/lib/receipts'; import type { PaymentKind } from '@/hooks/useMarketplaceOffers'; export interface PayPointerArgs { /** The pointer string (BOLT11, Lightning address, `creq1…`, `noffer1…`). */ pointer: string; kind: PaymentKind; /** Required to pay a Lightning address (an invoice carries its own amount). */ amountSats?: number; } export function usePayPaymentPointer() { const { sendPayment } = useNWC(); const { activeNWC, webln } = useWallet(); const { getInvoice } = useLightningAddress(); const { coco, mints } = useCashu(); const [isPaying, setIsPaying] = useState(false); const pay = useCallback( async ({ pointer, kind, amountSats }: PayPointerArgs): Promise => { setIsPaying(true); try { // 1) Resolve the pointer to a BOLT11 invoice. let invoice: string; if (kind === 'lninvoice') { invoice = pointer; } else if (kind === 'lnaddress') { if (!amountSats || amountSats <= 0) { throw new Error('An amount is required to pay a Lightning address.'); } invoice = await getInvoice({ lightningAddress: pointer, amountSats }); } else if (kind === 'creq') { throw new Error( 'Cashu (creq) requests aren’t supported yet — ask the seller for a Lightning invoice or address.', ); } else { throw new Error( 'That payment code isn’t supported yet — ask the seller for a Lightning invoice or address.', ); } // 2) Pay it with the best available rail, capturing the PREIMAGE where the // rail returns one — that is the verifiable receipt. let preimage: string | undefined; if (activeNWC?.connectionString && activeNWC.isConnected) { const res = await sendPayment(activeNWC, invoice); preimage = res?.preimage; } else if (webln) { let provider: WebLNProvider = webln; if (typeof webln.enable === 'function') { const enabled = (await webln.enable()) as WebLNProvider | undefined; if (enabled) provider = enabled; } const res = (await provider.sendPayment(invoice)) as { preimage?: string } | undefined; preimage = res?.preimage; } else { const mint = mints[0]; if (coco && mint) { const quote = await coco.quotes.createMeltQuote(mint.url, invoice); const paid = (await coco.quotes.payMeltQuote(mint.url, quote.quote)) as | Record | undefined; preimage = (paid?.payment_preimage as string | undefined) ?? (paid?.preimage as string | undefined); } else { throw new Error( 'No wallet connected. Add a Lightning wallet (NWC) in Settings, connect a WebLN wallet, or top up your Cashu balance.', ); } } return buildReceipt(invoice, preimage); } finally { setIsPaying(false); } }, [sendPayment, activeNWC, webln, getInvoice, coco, mints], ); return { pay, isPaying }; }