Completes the escrow feature on top of the tested core (src/lib/cashuEscrow.ts): - useCashuEscrow: native-only hook wiring createEscrow (cashu-ts mint + NWC/WebLN funding), redeemEscrow, escrow-key derivation (coco keyring) and profile publish - EscrowKeyCard (Wallet): advertise the kind-0 'cashu_escrow' pubkey - EscrowRequestDialog (listing card): buyer locks funds (P2PK to seller, refundable after N days) and sends the 31872 carrying the cashu_token - EscrowPanel + 'Redeem escrow' in the DM request banner (seller side); the panel is offline/parse-based so it renders on web too - Buyer 'Escrow & request' supersedes the plain request when available EXPERIMENTAL: mint-touching paths are unverified on a device (native-only wallet); use small amounts first. Gate: 101 tests, 0 errors.
151 lines
5.4 KiB
TypeScript
151 lines
5.4 KiB
TypeScript
import { useCallback } from 'react';
|
|
import { CashuMint, CashuWallet } from '@cashu/cashu-ts';
|
|
import type { WebLNProvider } from '@webbtc/webln-types';
|
|
import { useCashu } from './useCashu';
|
|
import { useNWC } from '@/hooks/useNWCContext';
|
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
|
import { useAuthor } from '@/hooks/useAuthor';
|
|
import { useNostrPublish } from '@/hooks/useNostrPublish';
|
|
import { isNativePlatform } from '@/lib/platform';
|
|
import {
|
|
createEscrowToken,
|
|
normalizeCashuPubkey,
|
|
parseEscrowToken,
|
|
type EscrowMintWallet,
|
|
type EscrowTokenInfo,
|
|
} from '@/lib/cashuEscrow';
|
|
|
|
/**
|
|
* Cashu escrow — **native-only** (the Cashu wallet is Capacitor-gated).
|
|
*
|
|
* Funds are locked to the seller with a locktime and the buyer as refund key
|
|
* (see `ESCROW.md`). The correctness-critical flow lives in
|
|
* `src/lib/cashuEscrow.ts` (unit-tested); this hook wires it to a real
|
|
* cashu-ts wallet, the NWC/WebLN payment rail, and the coco keyring.
|
|
*
|
|
* EXPERIMENTAL: the mint-touching paths (create/redeem/refund) have not been
|
|
* verified on a device yet — exercise with small amounts first.
|
|
*/
|
|
export function useCashuEscrow() {
|
|
const { coco, isInitialized, mints } = useCashu();
|
|
const { getActiveConnection, sendPayment } = useNWC();
|
|
const { user } = useCurrentUser();
|
|
const author = useAuthor(user?.pubkey);
|
|
const { mutateAsync: publish } = useNostrPublish();
|
|
const webln = (globalThis as { webln?: WebLNProvider }).webln || null;
|
|
|
|
const available = isNativePlatform() && isInitialized && !!coco;
|
|
|
|
/** The active mint URL to fund escrow from (first trusted mint). */
|
|
const defaultMintUrl = mints[0]?.url ?? null;
|
|
|
|
/**
|
|
* The Cashu keyring pubkey used as the P2PK lock/refund key. Nostr signers
|
|
* can't produce Cashu's raw schnorr witness, so escrow uses the Cashu
|
|
* keyring — not the Nostr key.
|
|
*/
|
|
const getEscrowPubkey = useCallback(async (): Promise<string | null> => {
|
|
if (!coco) return null;
|
|
const existing = await coco.keyring.getLatestKeyPair();
|
|
const keypair = existing ?? (await coco.keyring.generateKeyPair(true));
|
|
return normalizeCashuPubkey(keypair.publicKeyHex);
|
|
}, [coco]);
|
|
|
|
/** Pay a mint invoice via NWC, then WebLN. */
|
|
const payInvoice = useCallback(
|
|
async (bolt11: string): Promise<void> => {
|
|
const nwc = getActiveConnection();
|
|
if (nwc?.connectionString && nwc.isConnected) {
|
|
await sendPayment(nwc, bolt11);
|
|
return;
|
|
}
|
|
if (webln) {
|
|
if (typeof webln.enable === 'function') await webln.enable();
|
|
await webln.sendPayment(bolt11);
|
|
return;
|
|
}
|
|
throw new Error('No Lightning wallet available to fund escrow — connect NWC or a WebLN wallet.');
|
|
},
|
|
[getActiveConnection, sendPayment, webln],
|
|
);
|
|
|
|
const makeMintWallet = useCallback((mintUrl: string): EscrowMintWallet => {
|
|
const wallet = new CashuWallet(new CashuMint(mintUrl), { unit: 'sat' });
|
|
return {
|
|
createMintQuote: (amount) => wallet.createMintQuote(amount),
|
|
checkMintQuote: async (quote) => ({ state: (await wallet.checkMintQuote(quote)).state }),
|
|
mintProofs: (amount, quote, options) =>
|
|
wallet.mintProofs(amount, quote, { p2pk: options.p2pk }),
|
|
};
|
|
}, []);
|
|
|
|
/**
|
|
* Create an escrowed token locked to `sellerPubkey` (funded by paying the
|
|
* mint's Lightning invoice). Returns the token to embed in a `31872`.
|
|
*/
|
|
const createEscrow = useCallback(
|
|
async ({
|
|
amountSats,
|
|
sellerPubkey,
|
|
lockDays,
|
|
mintUrl,
|
|
}: {
|
|
amountSats: number;
|
|
sellerPubkey: string;
|
|
lockDays?: number;
|
|
mintUrl?: string;
|
|
}): Promise<{ token: string; locktime: number; info: EscrowTokenInfo }> => {
|
|
if (!coco) throw new Error('Cashu wallet is not ready.');
|
|
|
|
const buyerPubkey = await getEscrowPubkey();
|
|
if (!buyerPubkey) throw new Error('Could not derive your escrow key.');
|
|
|
|
const mint = mintUrl ?? defaultMintUrl;
|
|
if (!mint) throw new Error('No Cashu mint available — add one in your wallet first.');
|
|
|
|
const { token, locktime } = await createEscrowToken(
|
|
{ wallet: makeMintWallet(mint), payInvoice },
|
|
{ mintUrl: mint, amountSats, sellerPubkey, buyerPubkey, lockDays },
|
|
);
|
|
|
|
return { token, locktime, info: parseEscrowToken(token) };
|
|
},
|
|
[coco, getEscrowPubkey, defaultMintUrl, makeMintWallet, payInvoice],
|
|
);
|
|
|
|
/** Redeem an escrow token (seller on delivery, or buyer after the locktime). */
|
|
const redeemEscrow = useCallback(
|
|
async (token: string): Promise<void> => {
|
|
if (!coco) throw new Error('Cashu wallet is not ready.');
|
|
await coco.wallet.receive(token);
|
|
},
|
|
[coco],
|
|
);
|
|
|
|
/**
|
|
* Publish this user's escrow pubkey to their kind 0 profile (`cashu_escrow`)
|
|
* so buyers can lock funds to them.
|
|
*/
|
|
const advertiseEscrowKey = useCallback(async (): Promise<string> => {
|
|
if (!user) throw new Error('Log in first.');
|
|
const pubkey = await getEscrowPubkey();
|
|
if (!pubkey) throw new Error('Could not derive your escrow key.');
|
|
|
|
const current = (author.data?.metadata ?? {}) as Record<string, unknown>;
|
|
await publish({
|
|
kind: 0,
|
|
content: JSON.stringify({ ...current, cashu_escrow: pubkey }),
|
|
});
|
|
return pubkey;
|
|
}, [user, getEscrowPubkey, author.data?.metadata, publish]);
|
|
|
|
return {
|
|
available,
|
|
defaultMintUrl,
|
|
getEscrowPubkey,
|
|
createEscrow,
|
|
redeemEscrow,
|
|
payInvoice,
|
|
advertiseEscrowKey,
|
|
};
|
|
}
|