diff --git a/MARKETPLACE_FLOW_AUDIT.md b/MARKETPLACE_FLOW_AUDIT.md index 20ec4d4..e22040c 100644 --- a/MARKETPLACE_FLOW_AUDIT.md +++ b/MARKETPLACE_FLOW_AUDIT.md @@ -218,6 +218,10 @@ Second batch (payment, attestation, escrow): documented in NIP.md; the buyer's escrow dialog includes that mint in the picker and prefers it, so the buyer locks at a mint the seller can redeem from. (Still open: seller-side receive from a mint they do not already have.) +13. **Correctness guards:** fixed-price Buy is disabled when the seller requires + escrow (`item.escrow === 'required'`); ask-seller is available on an active + listing (not only once sold); payment receipts are scoped to the payer's + pubkey, so a stale/foreign receipt cannot be attached by another account. ## 7. Recommended order diff --git a/src/components/ListingOffersPanel.tsx b/src/components/ListingOffersPanel.tsx index ee016da..3f8863a 100644 --- a/src/components/ListingOffersPanel.tsx +++ b/src/components/ListingOffersPanel.tsx @@ -338,7 +338,7 @@ export function ListingOffersPanel({ item, isOwner }: { item: MarketplaceItem; i currency={payFor.latest.currency} onPaid={(receipt) => { // Carry the verifiable receipt to the confirm step (same coord key). - if (receipt) saveReceipt(payFor.listingCoord, receipt); + if (receipt) saveReceipt(payFor.listingCoord, receipt, 'payment', user?.pubkey ?? ''); setPaidThreads((prev) => new Set(prev).add(`${payFor.listingCoord}:${payFor.buyer}`)); }} /> diff --git a/src/components/ListingTradeActions.tsx b/src/components/ListingTradeActions.tsx index 581af2d..96102d5 100644 --- a/src/components/ListingTradeActions.tsx +++ b/src/components/ListingTradeActions.tsx @@ -94,6 +94,8 @@ export function ListingTradeActions({ item }: { item: MarketplaceItem }) { !isSold && !isExpired && !iConfirmedTrade && + // A seller who requires escrow must not be paid directly. + item.escrow !== 'required' && item.priceType === 'fixed' && typeof buyAmountSats === 'number' && buyAmountSats > 0; @@ -134,12 +136,12 @@ export function ListingTradeActions({ item }: { item: MarketplaceItem }) { // Attach the Lightning receipts captured at Pay time, if any — this is what // makes the trade payment-VERIFIED (sha256(preimage) === payment_hash), and // gives a barter a verifiable DEPOSIT cost. - const receipt = getReceipt(coord); + const receipt = getReceipt(coord, 'payment', user?.pubkey ?? ''); const paymentProof = receipt?.preimage && receipt.paymentHash ? { preimage: receipt.preimage, paymentHash: receipt.paymentHash } : undefined; - const depositReceipt = getReceipt(coord, 'deposit'); + const depositReceipt = getReceipt(coord, 'deposit', user?.pubkey ?? ''); const depositProof = depositReceipt?.preimage && depositReceipt.paymentHash ? { preimage: depositReceipt.preimage, paymentHash: depositReceipt.paymentHash } @@ -154,8 +156,8 @@ export function ListingTradeActions({ item }: { item: MarketplaceItem }) { paymentProof, depositProof, }); - clearReceipt(coord); - clearReceipt(coord, 'deposit'); + clearReceipt(coord, 'payment', user?.pubkey ?? ''); + clearReceipt(coord, 'deposit', user?.pubkey ?? ''); toast({ title: t('marketUi.tradeActions.tradeConfirmed'), description: @@ -251,7 +253,7 @@ export function ListingTradeActions({ item }: { item: MarketplaceItem }) { )} {/* Buyer: ask the seller to record the sale (kind 31872 request) */} - {!canEscrow && user && !isOwnListing && isSold && !iConfirmedTrade && !sellerNamedMe && !iRequestedConfirmation && ( + {!canEscrow && user && !isOwnListing && !iConfirmedTrade && !sellerNamedMe && !iRequestedConfirmation && (