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 => { 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 => { 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 => { 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 => { 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; 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, }; }