diff --git a/MARKETPLACE_FLOW_AUDIT.md b/MARKETPLACE_FLOW_AUDIT.md index 3da151d..8f0ea57 100644 --- a/MARKETPLACE_FLOW_AUDIT.md +++ b/MARKETPLACE_FLOW_AUDIT.md @@ -65,6 +65,16 @@ visibility, terminology, translation). runs `markSold` then `publishTrade` non-atomically; if the second fails (or is queued offline, which resolves — `useNostrPublish.ts:77-89`), the listing is sold but no `31871` exists, so no reputation and no confirmation path. +- **[verified] Escrow mint is chosen by the buyer, not the payee.** + `useCashuEscrow` resolves the mint as `prefs.preferredMintUrl || mints[0].url` + (the buyer's first trusted mint, `useCashuEscrow.ts:40,103`), and + `EscrowRequestDialog` did not show or offer a choice. The token is P2PK-locked + to the seller but lives at the buyer's mint, so a seller who does not + trust/reach that mint may fail to redeem. There was also no P2PK (NUT-11) + capability check before minting (`cashuEscrow.ts:261-282`). **Partly fixed this + pass**: the buyer now sees and picks the mint in the dialog, and a mint that + advertises no NUT-11 is blocked with a clear message. Still open: consult a + seller-advertised mint, and seller-side receive from an unfamiliar mint. --- @@ -190,6 +200,10 @@ Second batch (payment, attestation, escrow): (`lib/pendingEscrow.ts`) *before* publishing the request, so a failed send cannot strand the locked funds, and the failure message says so. (Still a mitigation: there is no re-send/recover UI yet.) +10. **Escrow mint selection:** the escrow dialog now shows the mint and lets the + buyer pick from their trusted mints; a mint whose NUT-06 info advertises no + NUT-11 (P2PK) support is blocked with a clear message + (`cashuEscrow.mintSupportsP2PK`), instead of failing deep in minting. ## 7. Recommended order diff --git a/src/components/EscrowRequestDialog.tsx b/src/components/EscrowRequestDialog.tsx index 3eb938d..46435bc 100644 --- a/src/components/EscrowRequestDialog.tsx +++ b/src/components/EscrowRequestDialog.tsx @@ -13,7 +13,7 @@ import { useCashuEscrow } from '@/hooks/wallet/useCashuEscrow'; import { useEscrowPrefs } from '@/hooks/wallet/useEscrowPrefs'; import { useTradeAttestationRequest } from '@/hooks/useTradeAttestations'; import { useToast } from '@/hooks/useToast'; -import { ESCROW_MIN_LOCK_DAYS } from '@/lib/cashuEscrow'; +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'; @@ -48,6 +48,13 @@ export function EscrowRequestDialog({ 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 mintOptions = escrow.mints; + const effectiveMint = mintChoice || prefs.preferredMintUrl || mintOptions[0]?.url || ''; + const p2pkBlocked = + mintSupportsP2PK(mintOptions.find((m) => m.url === effectiveMint)?.info) === false; const submit = async () => { const amountSats = parseInt(amount, 10); @@ -65,7 +72,7 @@ export function EscrowRequestDialog({ setBusy(true); let mintedToken: string | undefined; try { - const { token } = await escrow.createEscrow({ amountSats, sellerPubkey, lockDays, mintUrl: prefs.preferredMintUrl || undefined }); + 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. @@ -124,6 +131,28 @@ export function EscrowRequestDialog({ /> +
{t('marketUi.escrowRequest.mintHint')}
+