feat(marketplace): buyer escrow refund after locktime

Completes the escrow safety loop: a buyer whose escrow token is past its
locktime can reclaim it from the listing card ('Refund escrow'), which redeems
the P2PK token with their refund key. Native-only, same as the rest of escrow.
This commit is contained in:
SkateSpots 2026-09-26 21:10:59 +07:00
parent 15901976ef
commit c37e21e860

View file

@ -35,6 +35,7 @@ import { useMarkListingSold } from '@/hooks/useMarkListingSold';
import { MarkSoldDialog } from '@/components/MarkSoldDialog'; import { MarkSoldDialog } from '@/components/MarkSoldDialog';
import { EscrowRequestDialog } from '@/components/EscrowRequestDialog'; import { EscrowRequestDialog } from '@/components/EscrowRequestDialog';
import { useCashuEscrow } from '@/hooks/wallet/useCashuEscrow'; import { useCashuEscrow } from '@/hooks/wallet/useCashuEscrow';
import { parseEscrowToken } from '@/lib/cashuEscrow';
import { useListingTrade, useTradeAttestation, useTradeReputation, useListingTradeRequest, useTradeAttestationRequest } from '@/hooks/useTradeAttestations'; import { useListingTrade, useTradeAttestation, useTradeReputation, useListingTradeRequest, useTradeAttestationRequest } from '@/hooks/useTradeAttestations';
import { DeleteEntityDialog } from '@/components/DeleteEntityDialog'; import { DeleteEntityDialog } from '@/components/DeleteEntityDialog';
import { genUserName } from '@/lib/genUserName'; import { genUserName } from '@/lib/genUserName';
@ -224,6 +225,38 @@ export function MarketplaceItemCard({ item, compact = false }: MarketplaceItemCa
!sellerNamedMe && !sellerNamedMe &&
!iRequestedConfirmation; !iRequestedConfirmation;
// The buyer's own escrow token (from their published 31872), refundable after
// the locktime if the seller never delivered.
const myEscrowToken = requestState?.requests
.find((e) => e.pubkey === user?.pubkey)
?.tags.find(([t]) => t === 'cashu_token')?.[1];
const myEscrowRefundable = (() => {
if (!myEscrowToken) return false;
try {
const info = parseEscrowToken(myEscrowToken);
return info.locked && info.refundable;
} catch {
return false;
}
})();
const handleRefundEscrow = async () => {
if (!myEscrowToken) return;
try {
await escrow.redeemEscrow(myEscrowToken);
toast({
title: 'Escrow refunded',
description: 'The funds are back in your Cashu wallet.',
});
} catch (e) {
toast({
title: 'Could not refund escrow',
description: e instanceof Error ? e.message : 'Something went wrong.',
variant: 'destructive',
});
}
};
const handleConfirmTrade = async () => { const handleConfirmTrade = async () => {
if (!user) return; if (!user) return;
try { try {
@ -576,6 +609,19 @@ export function MarketplaceItemCard({ item, compact = false }: MarketplaceItemCa
</p> </p>
)} )}
{/* Buyer: reclaim escrow after the locktime if the seller didn't deliver */}
{escrow.available && myEscrowRefundable && (
<Button
size="sm"
variant="outline"
onClick={handleRefundEscrow}
className="w-full gap-2 border-[#ccff00]/40 text-[#ccff00] hover:bg-[#ccff00]/10"
>
<RotateCcw className="h-4 w-4" />
Refund escrow
</Button>
)}
{/* Contact button */} {/* Contact button */}
<Button <Button
variant="outline" variant="outline"