- 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.
103 lines
4 KiB
TypeScript
103 lines
4 KiB
TypeScript
/**
|
||
* 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 };
|
||
}
|