import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { useTradeReputation } from '@/hooks/useTradeAttestations'; import { useBatchTrustScores } from '@/hooks/useBatchTrustScores'; import { useBatchActivity, useTradeRings } from '@/hooks/useTradeIntegrity'; import { cn } from '@/lib/utils'; import { Handshake } from 'lucide-react'; import { useTranslation } from '@/contexts/language'; function Stat({ label, value, accent }: { label: string; value: number; accent?: boolean }) { return (

{value}

{label}

); } /** * Marketplace reputation for a profile, derived from mutually-signed trade * attestations (kind 31871, see NIP.md → Trade Attestations). * * "Verified" is the strong signal: the seller AND the counterparty both signed * an attestation for the same listing. Renders nothing when there are no trades. */ export function TradeReputationCard({ pubkey, className }: { pubkey: string; className?: string }) { const { t } = useTranslation(); const { data, isError } = useTradeReputation(pubkey); const verifiedWith = data?.verifiedWith ?? []; const partnerPubkeys = verifiedWith.map((v) => v.pubkey); // Trust-weight each mutually-confirmed trade by the counterparty's web-of-trust // score, so a ring of fresh accounts can't inflate reputation. const { data: scores } = useBatchTrustScores(partnerPubkeys); // Anti-sybil depth: weight by REAL activity (spots/listings), and look for a // closed partner cluster (accounts that mostly trade with each other). const { data: activity } = useBatchActivity(partnerPubkeys); const { data: rings } = useTradeRings(pubkey, partnerPubkeys); const total = (data?.sales ?? 0) + (data?.purchases ?? 0); // A relay outage must not silently read as "no reputation". if (isError) { return

{t('marketUi.loadFailed')}

; } if (!data || total === 0) return null; const people = data.verifiedCounterparties; // Cap each counterparty's contribution at 3 so one partner — or a colluding // pair confirming each other over and over — can't inflate the score. // Activity factor: a fresh account with no published content contributes ~0, // so a ring of empty accounts can't move the score even if cross-confirmed. const activityFactor = (pk: string) => Math.min(1, (activity?.get(pk) ?? 0) / 3); const weighted = verifiedWith.reduce( (sum, v) => sum + Math.min(v.count, 3) * ((scores?.get(v.pubkey) ?? 0) / 100) * activityFactor(v.pubkey), 0, ); const trustLabel = weighted >= 5 ? t('marketUi.reputation.highlyTrusted') : weighted >= 1 ? t('marketUi.reputation.building') : t('marketUi.reputation.newTrader'); // The classic fake-reputation pattern: several "trades", all with one account. const singlePartnerWarning = data.verified >= 3 && people <= 1; // A bigger ring: lots of confirmations from only a couple of accounts. const lowDiversityWarning = !singlePartnerWarning && data.verified >= 5 && people <= 2; return ( {t('marketUi.reputation.title')}

{data.verified > 0 ? `${data.verified} ${data.verified === 1 ? t('marketUi.reputation.tradeConfirmedOne') : t('marketUi.reputation.tradeConfirmedMany')} ${t('marketUi.reputation.with')} ${people} ${people === 1 ? t('marketUi.reputation.personOne') : t('marketUi.reputation.personMany')}` : t('marketUi.reputation.awaiting')}

{data.recentVerified > 0 && (

{data.recentVerified}{' '} {t('marketUi.reputation.recentConfirmed')}

)} {data.withPaymentProof > 0 && (

{data.withPaymentProof} {t('marketUi.reputation.withPaymentProof')} {t('marketUi.reputation.selfReported')}

)} {data.paymentVerified > 0 && (

{data.paymentVerified} {t('marketUi.reputation.paymentVerified')} {t('marketUi.reputation.zapReceipt')}

)} {singlePartnerWarning && (

⚠ {t('marketUi.reputation.singlePartnerWarning')}

)} {lowDiversityWarning && (

⚠ {t('marketUi.reputation.lowDiversityWarning')}

)} {rings?.isRing && (

🔁 {t('marketUi.reputation.ringWarning')}

)} {data.verified > 0 && (

{t('marketUi.reputation.weighted')} {weighted.toFixed(1)} {' · '} {trustLabel}

)}

{t('marketUi.reputation.footnote')}

); }