SkateSpots/src/hooks/usePayPaymentPointer.ts
SkateSpots e7c6bb1ece feat(receipts): verifiable Lightning preimage receipts (money legs)
- getPaymentHashFromBolt11 (bolt11.ts, dependency-free; matches the BOLT11 spec
  test vector) + verifyPreimage (sha256(preimage) === payment_hash).
- usePayPaymentPointer captures the preimage from NWC/WebLN/melt and returns a
  PaymentReceipt; PayWithSats.onPaid carries it; stored device-locally so the
  later confirm step attaches it; publishTrade writes
  ['proof','lnpreimage',preimage,hash] (and ['proof','deposit',...] for barter).
- useTradeReputation verifies and counts it in paymentVerified.
- Honest scope: proves PAYMENT, not delivery; money legs only - a pure hardware
  swap has no invoice, stays attested; a deposit receipt gives barter real cost.
2026-10-01 17:25:16 +07:00

103 lines
4 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 { 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<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) 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 };
}