diff --git a/AGENTS.md b/AGENTS.md index f743fb1..348e562 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,7 +49,9 @@ session with **no other memory of this project**. So: — one implementation). Buyer/seller attestations build mutual, WoT-weighted reputation. **No payments are implemented** — "Buy" is intentionally absent; buyers contact the seller and settle off-app. A **`/trades`** page ("My Trades") - manages incoming requests, pending confirmations, and the user's trade records. + manages incoming requests, pending confirmations, and the user's trade records, + and can **revoke** a record (`s=revoked`, excluded from reputation) for a dispute + or mistake. - **Cashu escrow** (marketplace): time-locked **P2PK** (lock to seller + buyer refund key + locktime) — see **`ESCROW.md`** and `src/lib/cashuEscrow.ts` (unit-tested). Protocol: `cashu_token` on the `31872`; each user advertises a diff --git a/src/pages/TradesPage.tsx b/src/pages/TradesPage.tsx index d77867d..ece0eb3 100644 --- a/src/pages/TradesPage.tsx +++ b/src/pages/TradesPage.tsx @@ -7,6 +7,13 @@ import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; import { LoginArea } from '@/components/auth/LoginArea'; import { TradeReputationCard } from '@/components/TradeReputationCard'; import { useCurrentUser } from '@/hooks/useCurrentUser'; @@ -51,6 +58,12 @@ export default function TradesPage() { const { data: reputation, isLoading: loadingRep } = useTradeReputation(user?.pubkey); const [busy, setBusy] = useState(null); + const [revokeTarget, setRevokeTarget] = useState<{ + listing: { pubkey: string; identifier: string }; + role: 'sold' | 'bought'; + counterparty: string; + } | null>(null); + const [revoking, setRevoking] = useState(false); useSeoMeta({ title: 'My Trades — SkateSpots', @@ -102,6 +115,33 @@ export default function TradesPage() { } }; + const handleRevoke = async () => { + if (!revokeTarget) return; + setRevoking(true); + try { + await publishTrade({ + listing: revokeTarget.listing, + role: revokeTarget.role, + counterparty: revokeTarget.counterparty, + state: 'revoked', + note: 'Retracted by the author.', + }); + toast({ + title: 'Trade record revoked', + description: 'It no longer counts toward reputation.', + }); + setRevokeTarget(null); + } catch (e) { + toast({ + title: 'Could not revoke the trade', + description: e instanceof Error ? e.message : 'Something went wrong.', + variant: 'destructive', + }); + } finally { + setRevoking(false); + } + }; + if (!user) { return ( @@ -240,7 +280,12 @@ export default function TradesPage() { const coord = ev.tags.find(([t]) => t === 'a')?.[1]; const parsed = parseListingCoord(coord); const d = ev.tags.find(([t]) => t === 'd')?.[1] ?? ''; - const role = d.includes(':trade:sold:') ? 'Sold' : d.includes(':trade:bought:') ? 'Bought' : 'Trade'; + const roleTyped: 'sold' | 'bought' | null = d.includes(':trade:sold:') + ? 'sold' + : d.includes(':trade:bought:') + ? 'bought' + : null; + const role = roleTyped === 'sold' ? 'Sold' : roleTyped === 'bought' ? 'Bought' : 'Trade'; const state = ev.tags.find(([t]) => t === 's')?.[1] ?? 'valid'; const counterparty = ev.tags.find(([t]) => t === 'p')?.[1]; return ( @@ -272,12 +317,56 @@ export default function TradesPage() { View listing )} + {roleTyped && counterparty && parsed && ( + + )} ); })} + + !o && setRevokeTarget(null)}> + + + Revoke this trade record? + + It will stop counting toward your reputation. The other party keeps their own + attestation. Use this for a disputed or mistaken deal. + + +
+ + +
+
+
); }