import { useState } from 'react'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { useCashuEscrow } from '@/hooks/wallet/useCashuEscrow'; import { useEscrowPrefs } from '@/hooks/wallet/useEscrowPrefs'; import { useAuthor } from '@/hooks/useAuthor'; import { useTradeAttestationRequest } from '@/hooks/useTradeAttestations'; import { useToast } from '@/hooks/useToast'; import { ESCROW_MIN_LOCK_DAYS, mintSupportsP2PK } from '@/lib/cashuEscrow'; import { savePendingEscrow, clearPendingEscrow } from '@/lib/pendingEscrow'; import { Loader2, Lock } from 'lucide-react'; import { useTranslation } from '@/contexts/language'; /** * Buyer: lock funds in Cashu escrow (P2PK to the seller, refundable after a * deadline) and send the `31872` confirmation request carrying the token. * * Native-only (the Cashu wallet is Capacitor-gated). EXPERIMENTAL — the * mint-touching path has not been verified on a device; start with small * amounts. */ export function EscrowRequestDialog({ open, onOpenChange, listing, sellerPubkey, sellerName, }: { open: boolean; onOpenChange: (open: boolean) => void; listing: { pubkey: string; identifier: string }; sellerPubkey: string; sellerName: string; }) { const { t } = useTranslation(); const escrow = useCashuEscrow(); const { prefs } = useEscrowPrefs(); const { publishRequest, isPending: publishing } = useTradeAttestationRequest(); const { toast } = useToast(); const [amount, setAmount] = useState(''); const [days, setDays] = useState(String(prefs.defaultLockDays)); const [busy, setBusy] = useState(false); // Buyer-facing mint choice: default to their preference, else the first trusted // mint. Shown explicitly so the buyer knows where the funds will be locked. const [mintChoice, setMintChoice] = useState(''); const sellerAuthor = useAuthor(sellerPubkey); const sellerMintRaw = (sellerAuthor.data?.metadata as Record | undefined)?.cashu_mint; const sellerMint = typeof sellerMintRaw === 'string' && sellerMintRaw.trim() ? sellerMintRaw.trim() : undefined; const mintOptions = escrow.mints; // Include the seller's advertised mint even if it is not in our trust list, and // prefer it — a mint the seller actually uses is one they can redeem from. const mintChoices = sellerMint && !mintOptions.some((m) => m.url === sellerMint) ? [...mintOptions, { url: sellerMint }] : mintOptions; const effectiveMint = mintChoice || sellerMint || prefs.preferredMintUrl || mintChoices[0]?.url || ''; const p2pkBlocked = mintSupportsP2PK(mintChoices.find((m) => m.url === effectiveMint)?.info) === false; const submit = async () => { const amountSats = parseInt(amount, 10); const lockDays = parseInt(days, 10); if (!Number.isFinite(amountSats) || amountSats <= 0) { toast({ title: t('marketUi.escrowRequest.enterAmount'), variant: 'destructive' }); return; } if (!Number.isFinite(lockDays) || lockDays < ESCROW_MIN_LOCK_DAYS) { toast({ title: t('marketUi.escrowRequest.minLock', { days: ESCROW_MIN_LOCK_DAYS }), variant: 'destructive' }); return; } setBusy(true); let mintedToken: string | undefined; try { const { token } = await escrow.createEscrow({ amountSats, sellerPubkey, lockDays, mintUrl: effectiveMint || undefined }); mintedToken = token; // Persist BEFORE publishing: if the request fails to send, the P2PK-locked // funds stay recoverable from this device instead of being lost. savePendingEscrow(listing, token, amountSats, lockDays); await publishRequest({ listing, attestor: sellerPubkey, cashuToken: token, note: t('marketUi.escrowRequest.requestNote', { amount: amountSats }), }); clearPendingEscrow(listing); toast({ title: t('marketUi.escrowRequest.created'), description: t('marketUi.escrowRequest.createdBody', { amount: amountSats, days: lockDays, sellerName }), }); onOpenChange(false); } catch (e) { toast({ title: t('marketUi.escrowRequest.createFailed'), description: mintedToken ? t('marketUi.escrowRequest.lockedNotSent') : e instanceof Error ? e.message : t('marketUi.escrowRequest.somethingWrong'), variant: 'destructive', }); } finally { setBusy(false); } }; const pending = busy || publishing; return ( {t('marketUi.escrowRequest.title')} {t('marketUi.escrowRequest.description', { sellerName })}
setAmount(e.target.value)} inputMode="numeric" placeholder={t('marketUi.offer.amountPlaceholder')} className="h-11 border-gray-700 bg-gray-900 text-white" />

{t('marketUi.escrowRequest.mintHint')}

{sellerMint && (

{t('marketUi.escrowRequest.sellerMintHint')}

)}
setDays(e.target.value)} inputMode="numeric" className="h-11 border-gray-700 bg-gray-900 text-white" />

{t('marketUi.escrowRequest.daysHint')}

{!escrow.available && (

{t('marketUi.escrowRequest.nativeOnly')}

)} {p2pkBlocked && (

{t('marketUi.escrowRequest.p2pkUnsupported')}

)}
); }