/** * Exchange reputation scoring and badge calculation. * * Score formula (0-100): * completionRate × 50 + (1 - disputeRate) × 25 + (1 - disputeLossRate) × 15 + confirmSpeed × 10 * * Badges: * verified_seller — 5+ completed trades, score ≥ 70 * liquidity_provider — has standing orders * top_trader — ≥ $10k equivalent volume */ import type { ExchangeReputationRecord, ExchangeReputationDoc, ExchangeTradesDoc, LiquidityPosition } from './schemas'; export const DEFAULT_REPUTATION: ExchangeReputationRecord = { did: '', tradesCompleted: 0, tradesCancelled: 0, disputesRaised: 0, disputesLost: 0, totalVolumeBase: 0, avgConfirmTimeMs: 0, score: 50, badges: [], }; /** * Calculate reputation score from raw stats. */ export function calculateScore(rec: ExchangeReputationRecord): number { const totalTrades = rec.tradesCompleted + rec.tradesCancelled; if (totalTrades === 0) return 50; // neutral default const completionRate = rec.tradesCompleted / totalTrades; const totalDisputes = rec.disputesRaised; const disputeRate = totalTrades > 0 ? totalDisputes / totalTrades : 0; const disputeLossRate = totalDisputes > 0 ? rec.disputesLost / totalDisputes : 0; // Confirm speed: normalize to 0-1 (faster = higher). // Target: < 1hr = perfect, > 24hr = 0. avgConfirmTimeMs capped at 24h. const oneHour = 3600_000; const twentyFourHours = 86400_000; const speedScore = rec.avgConfirmTimeMs <= 0 ? 0.5 : rec.avgConfirmTimeMs <= oneHour ? 1.0 : Math.max(0, 1 - (rec.avgConfirmTimeMs - oneHour) / (twentyFourHours - oneHour)); const score = completionRate * 50 + (1 - disputeRate) * 25 + (1 - disputeLossRate) * 15 + speedScore * 10; return Math.round(Math.max(0, Math.min(100, score))); } /** * Compute badges based on reputation stats. * LP badge awarded for standing orders OR active LP positions. */ export function computeBadges( rec: ExchangeReputationRecord, hasStandingOrders: boolean, hasActiveLpPositions: boolean = false, ): string[] { const badges: string[] = []; if (rec.tradesCompleted >= 5 && rec.score >= 70) { badges.push('verified_seller'); } if (hasStandingOrders || hasActiveLpPositions) { badges.push('liquidity_provider'); } // $10k volume threshold — base units with 6 decimals → 10_000 * 1_000_000 if (rec.totalVolumeBase >= 10_000_000_000) { badges.push('top_trader'); } return badges; } /** * Update reputation for a DID after a completed trade. */ export function updateReputationAfterTrade( rec: ExchangeReputationRecord, tokenAmount: number, confirmTimeMs: number, ): ExchangeReputationRecord { const newCompleted = rec.tradesCompleted + 1; const newVolume = rec.totalVolumeBase + tokenAmount; // Running average of confirm time const prevTotal = rec.avgConfirmTimeMs * rec.tradesCompleted; const newAvg = newCompleted > 0 ? (prevTotal + confirmTimeMs) / newCompleted : 0; const updated: ExchangeReputationRecord = { ...rec, tradesCompleted: newCompleted, totalVolumeBase: newVolume, avgConfirmTimeMs: newAvg, }; updated.score = calculateScore(updated); return updated; } /** * Get reputation for a DID from the doc, or return defaults. */ export function getReputation(did: string, doc: ExchangeReputationDoc): ExchangeReputationRecord { return doc.records[did] || { ...DEFAULT_REPUTATION, did }; } /** * Check if a DID has standing orders in the intents doc. */ export function hasStandingOrders( did: string, intentsDoc: { intents: Record }, ): boolean { return Object.values(intentsDoc.intents).some( i => i.creatorDid === did && i.isStandingOrder && i.status === 'active', ); } /** * Check if a DID has active liquidity pool positions. */ export function hasActiveLpPositions( did: string, poolsDoc: { positions: Record } | null | undefined, ): boolean { if (!poolsDoc) return false; return Object.values(poolsDoc.positions).some( p => p.creatorDid === did && p.status === 'active', ); }