chainhint-mcp 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +77 -11
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -105,14 +105,78 @@ function formatRiskLevel(score) {
|
|
|
105
105
|
function truncateAddr(addr) {
|
|
106
106
|
return addr.length > 12 ? `${addr.slice(0, 8)}...${addr.slice(-6)}` : addr;
|
|
107
107
|
}
|
|
108
|
+
// Same tiers as the site's formatUsd (src/lib/utils.ts): $1.4B, $115.0M, $629.4K.
|
|
109
|
+
// KIR-45: this used to stop at "M" and print "$1409.97M" for the Bybit loss.
|
|
108
110
|
function usd(n) {
|
|
109
111
|
if (n == null || !Number.isFinite(n))
|
|
110
112
|
return "Unknown";
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
const abs = Math.abs(n);
|
|
114
|
+
if (abs >= 1e15)
|
|
115
|
+
return "Unknown";
|
|
116
|
+
if (abs >= 1e12)
|
|
117
|
+
return `$${(n / 1e12).toFixed(1)}T`;
|
|
118
|
+
if (abs >= 1e9)
|
|
119
|
+
return `$${(n / 1e9).toFixed(1)}B`;
|
|
120
|
+
if (abs >= 1e6)
|
|
121
|
+
return `$${(n / 1e6).toFixed(1)}M`;
|
|
122
|
+
if (abs >= 1e3)
|
|
123
|
+
return `$${(n / 1e3).toFixed(1)}K`;
|
|
124
|
+
return `$${n.toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
|
|
125
|
+
}
|
|
126
|
+
// Hop count as the site counts it (src/lib/traceReconciliation deriveDepthMap):
|
|
127
|
+
// shortest-path depth from the attacker over the edges, max over reachable
|
|
128
|
+
// nodes — NOT max(edge.depth), which the tracer assigns as it walks (Bybit:
|
|
129
|
+
// 6 vs the site's 5).
|
|
130
|
+
function hopCount(edges, attacker) {
|
|
131
|
+
const src = (attacker ?? "").toLowerCase();
|
|
132
|
+
if (!src || !edges.length)
|
|
133
|
+
return 0;
|
|
134
|
+
const out = new Map();
|
|
135
|
+
for (const e of edges) {
|
|
136
|
+
const f = (e.from ?? "").toLowerCase(), t = (e.to ?? "").toLowerCase();
|
|
137
|
+
if (!f || !t)
|
|
138
|
+
continue;
|
|
139
|
+
(out.get(f) ?? out.set(f, []).get(f)).push(t);
|
|
140
|
+
}
|
|
141
|
+
const depth = new Map([[src, 0]]);
|
|
142
|
+
const queue = [src];
|
|
143
|
+
let max = 0;
|
|
144
|
+
while (queue.length) {
|
|
145
|
+
const a = queue.shift();
|
|
146
|
+
const d = depth.get(a);
|
|
147
|
+
for (const b of out.get(a) ?? []) {
|
|
148
|
+
if (depth.has(b))
|
|
149
|
+
continue;
|
|
150
|
+
depth.set(b, d + 1);
|
|
151
|
+
if (d + 1 > max)
|
|
152
|
+
max = d + 1;
|
|
153
|
+
queue.push(b);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return max;
|
|
157
|
+
}
|
|
158
|
+
// Endpoint buckets as the site's Flow Summary (src/lib/flowSummary.ts): the
|
|
159
|
+
// entity CATEGORY decides — an attacker wallet retyped "exchange" is not an
|
|
160
|
+
// exchange, a no-KYC swap is not a freeze target.
|
|
161
|
+
const ATTACKER_CATEGORIES = new Set(["hacker", "scam", "exploit"]);
|
|
162
|
+
const NO_KYC_TOKENS = ["changenow", "fixedfloat", "simpleswap", "sideshift", "stealthex", "changelly", "letsexchange", "godex", "swapuz", "exolix", "exch.cx", "exch.sc", "exch.net", "ff.io"];
|
|
163
|
+
function endpointBucket(category, type, name) {
|
|
164
|
+
const c = (category ?? "").toLowerCase();
|
|
165
|
+
const t = (type ?? "").toLowerCase();
|
|
166
|
+
const n = (name ?? "").toLowerCase().replace(/[\s_-]/g, "");
|
|
167
|
+
if (ATTACKER_CATEGORIES.has(c))
|
|
168
|
+
return "attacker-attributed";
|
|
169
|
+
if (c === "sanctioned" || c === "high_risk_exchange" || NO_KYC_TOKENS.some((k) => n.includes(k.replace(/[\s_-]/g, ""))))
|
|
170
|
+
return "risky";
|
|
171
|
+
if (c === "mixer" || t === "mixer")
|
|
172
|
+
return "mixer";
|
|
173
|
+
if (c === "bridge" || t === "bridge")
|
|
174
|
+
return "bridge";
|
|
175
|
+
if (c === "exchange" || t === "exchange")
|
|
176
|
+
return "exchange";
|
|
177
|
+
if (c === "defi" || c === "dex" || t === "defi" || t === "dex")
|
|
178
|
+
return "defi";
|
|
179
|
+
return "unknown";
|
|
116
180
|
}
|
|
117
181
|
// EVM addresses are case-insensitive and stored lowercase; base58 chains
|
|
118
182
|
// (BTC/TRON/SOL/TON) are case-sensitive — never lowercase those.
|
|
@@ -323,7 +387,8 @@ server.tool("get_trace_status", "Fund-trace summary for a publicly tracked crypt
|
|
|
323
387
|
};
|
|
324
388
|
}
|
|
325
389
|
const inc = rows[0];
|
|
326
|
-
|
|
390
|
+
// The site's figure: stored estimated_loss_usd first (TraceFacts.displayedLossUsd), amount_usd as fallback.
|
|
391
|
+
const lossUsd = inc.estimated_loss_usd ?? inc.amount_usd;
|
|
327
392
|
const date = inc.display_date ?? inc.hack_date ?? inc.created_at;
|
|
328
393
|
const status = (inc.status ?? "unknown").toLowerCase();
|
|
329
394
|
const lines = [
|
|
@@ -346,15 +411,15 @@ server.tool("get_trace_status", "Fund-trace summary for a publicly tracked crypt
|
|
|
346
411
|
const edges = inc.flow_graph?.edges ?? [];
|
|
347
412
|
const nodes = inc.flow_graph?.nodes ?? [];
|
|
348
413
|
if (edges.length) {
|
|
349
|
-
const
|
|
414
|
+
const hops = hopCount(edges, inc.attacker_address);
|
|
350
415
|
lines.push(``, `### Trace Graph`);
|
|
351
|
-
lines.push(`**Hops traced:** ${
|
|
416
|
+
lines.push(`**Hops traced:** ${hops} · **Addresses:** ${nodes.length} · **Transfers:** ${edges.length}`);
|
|
352
417
|
}
|
|
353
418
|
const endpoints = inc.endpoints ?? [];
|
|
354
419
|
if (endpoints.length) {
|
|
355
420
|
const byType = new Map();
|
|
356
421
|
for (const e of endpoints) {
|
|
357
|
-
const t = e.type ??
|
|
422
|
+
const t = endpointBucket(e.entity_category, e.type, e.entity_name ?? e.entity);
|
|
358
423
|
const b = byType.get(t) ?? { usd: 0, n: 0, entities: new Map() };
|
|
359
424
|
b.usd += e.amount_usd ?? 0;
|
|
360
425
|
b.n += 1;
|
|
@@ -364,7 +429,8 @@ server.tool("get_trace_status", "Fund-trace summary for a publicly tracked crypt
|
|
|
364
429
|
byType.set(t, b);
|
|
365
430
|
}
|
|
366
431
|
const totalUsd = [...byType.values()].reduce((s, b) => s + b.usd, 0);
|
|
367
|
-
|
|
432
|
+
// Σ endpoint amounts counts every hop's inflow (multi-hop), so it is larger than the loss — name the base.
|
|
433
|
+
lines.push(``, `### Where the funds went (${endpoints.length} endpoints, ${usd(totalUsd)} observed at endpoints — not the loss figure)`);
|
|
368
434
|
for (const [t, b] of [...byType.entries()].sort((a, b) => b[1].usd - a[1].usd)) {
|
|
369
435
|
const top = [...b.entities.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([n]) => n);
|
|
370
436
|
const pct = totalUsd > 0 ? ` (${((b.usd / totalUsd) * 100).toFixed(1)}%)` : "";
|
|
@@ -378,7 +444,7 @@ server.tool("get_trace_status", "Fund-trace summary for a publicly tracked crypt
|
|
|
378
444
|
lines.push(`**Largest endpoints:**`);
|
|
379
445
|
for (const e of topEndpoints) {
|
|
380
446
|
const name = e.entity_name ?? e.entity ?? "unattributed";
|
|
381
|
-
lines.push(`- ${truncateAddr(e.address)} — ${name} (${e.type ??
|
|
447
|
+
lines.push(`- ${truncateAddr(e.address)} — ${name} (${endpointBucket(e.entity_category, e.type, e.entity_name ?? e.entity)}): ${usd(e.amount_usd)}`);
|
|
382
448
|
}
|
|
383
449
|
}
|
|
384
450
|
}
|