SkateSpots/src/hooks/usePayPaymentPointer.ts

122 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 { getSatoshisFromBolt11 } from '@/lib/bolt11';
import { buildReceipt, type PaymentReceipt } from '@/lib/receipts';
import { assertInvoiceAmount } from '@/lib/verifyInvoice';
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<PaymentReceipt> => {
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) SECURITY: never auto-pay an invoice whose amount doesn't match the
// agreed price. A malicious seller can attach a raw BOLT11 or return an
// inflated invoice from their own LNURL server, and an NWC/WebLN wallet
// with a spend budget would pay it silently. `assertInvoiceAmount`
// rejects malformed/amountless/mismatched invoices.
if (typeof amountSats === 'number' && amountSats > 0) {
assertInvoiceAmount(invoice, amountSats);
} else {
// No agreed amount (e.g. an accepted offer with no `amount` tag): never
// auto-pay blind. Require the invoice itself to carry a positive amount.
let sats = 0;
try { sats = getSatoshisFromBolt11(invoice); } catch { sats = 0; }
if (!Number.isFinite(sats) || sats <= 0) {
throw new Error('This payment has no agreed amount — check the invoice before paying.');
}
}
// 3) 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<string, unknown>
| 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 };
}