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:
parent
15901976ef
commit
c37e21e860
1 changed files with 46 additions and 0 deletions
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue