125 lines
3.5 KiB
TypeScript
125 lines
3.5 KiB
TypeScript
/**
|
||
* 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 } 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.
|
||
*/
|
||
export function computeBadges(
|
||
rec: ExchangeReputationRecord,
|
||
hasStandingOrders: boolean,
|
||
): string[] {
|
||
const badges: string[] = [];
|
||
|
||
if (rec.tradesCompleted >= 5 && rec.score >= 70) {
|
||
badges.push('verified_seller');
|
||
}
|
||
|
||
if (hasStandingOrders) {
|
||
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<string, { creatorDid: string; isStandingOrder: boolean; status: string }> },
|
||
): boolean {
|
||
return Object.values(intentsDoc.intents).some(
|
||
i => i.creatorDid === did && i.isStandingOrder && i.status === 'active',
|
||
);
|
||
}
|