chain-insights 0.2.18 → 0.2.21
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/README.md +54 -12
- package/bin/cli.js +2 -3
- package/bin/install.cjs +0 -1
- package/dist/{app-DxlQE_P5.cjs → app-BxojXjtB.cjs} +1 -1
- package/dist/{app-DdWQF_zb.mjs → app-CRd39JJ8.mjs} +2 -2
- package/dist/{app-DdWQF_zb.mjs.map → app-CRd39JJ8.mjs.map} +1 -1
- package/dist/{artifact-server-4DiMvwhC.mjs → artifact-server-CP6LXQ9d.mjs} +2 -2
- package/dist/{artifact-server-4DiMvwhC.mjs.map → artifact-server-CP6LXQ9d.mjs.map} +1 -1
- package/dist/{artifact-server-B-3ho4bk.cjs → artifact-server-XbN16DwU.cjs} +1 -1
- package/dist/cli.cjs +66 -25
- package/dist/cli.mjs +66 -25
- package/dist/cli.mjs.map +1 -1
- package/dist/{config-BhYbhLDI.cjs → config-BwVx19Og.cjs} +48 -15
- package/dist/config-Drgc2HuF.mjs +77 -0
- package/dist/config-Drgc2HuF.mjs.map +1 -0
- package/dist/frontmatter-D0ccQnUM.mjs.map +1 -1
- package/dist/index.cjs +4 -4
- package/dist/index.d.cts +3 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +4 -4
- package/dist/{init-CZbZegIW.mjs → init-4tn7jfhN.mjs} +3 -2
- package/dist/init-4tn7jfhN.mjs.map +1 -0
- package/dist/{init-BvpZtFiT.cjs → init-TCQY5RDJ.cjs} +2 -1
- package/dist/mcp-endpoint-BaV8h_lq.cjs +60 -0
- package/dist/mcp-endpoint-DHs1cRFH.mjs +39 -0
- package/dist/mcp-endpoint-DHs1cRFH.mjs.map +1 -0
- package/dist/mcp-proxy.cjs +108 -9
- package/dist/mcp-proxy.d.cts.map +1 -1
- package/dist/mcp-proxy.d.mts.map +1 -1
- package/dist/mcp-proxy.mjs +108 -9
- package/dist/mcp-proxy.mjs.map +1 -1
- package/dist/{public-tools-D6Q5MTcO.mjs → public-tools-B13J0MJZ.mjs} +465 -70
- package/dist/public-tools-B13J0MJZ.mjs.map +1 -0
- package/dist/{public-tools-V7ON7goq.cjs → public-tools-BC1fi0DV.cjs} +464 -68
- package/dist/resolver-D7VBb0uB.mjs.map +1 -1
- package/dist/{runner-BatyCxv7.mjs → runner-DIs04IhN.mjs} +2 -2
- package/dist/{runner-BatyCxv7.mjs.map → runner-DIs04IhN.mjs.map} +1 -1
- package/dist/{runner-CCA7SJ7X.cjs → runner-ZYowxCVl.cjs} +1 -1
- package/dist/schema-BFEWhzg7.mjs +60 -0
- package/dist/schema-BFEWhzg7.mjs.map +1 -0
- package/dist/{schema-DN-KLkYN.cjs → schema-Vl9yuOFO.cjs} +31 -8
- package/dist/{server-BDlbmGbL.mjs → server-BXLX2j_A.mjs} +2 -2
- package/dist/{server-BDlbmGbL.mjs.map → server-BXLX2j_A.mjs.map} +1 -1
- package/dist/{server-C3y1gQmZ.cjs → server-BqVdWath.cjs} +1 -1
- package/dist/{topup-server-6MH7q73X.mjs → topup-server-BJgVw6Jt.mjs} +100 -42
- package/dist/topup-server-BJgVw6Jt.mjs.map +1 -0
- package/dist/{topup-server-DjUjhNjv.cjs → topup-server-yAaXYkJP.cjs} +98 -40
- package/docs/architecture.md +4 -0
- package/docs/contributing.md +1 -0
- package/docs/debugging.md +10 -14
- package/docs/graph-tools.md +60 -2
- package/docs/mcp-proxy.md +44 -0
- package/package.json +2 -2
- package/skills/chain-insights-developer-experience/SKILL.md +4 -2
- package/skills/chain-insights-investigation/SKILL.md +1 -1
- package/skills/test-chain-insights-graphrag-mcp/SKILL.md +4 -5
- package/skills/test-chain-insights-graphrag-mcp/scripts/run-uat.sh +5 -24
- package/dist/config-9KYXaAv-.mjs +0 -44
- package/dist/config-9KYXaAv-.mjs.map +0 -1
- package/dist/init-CZbZegIW.mjs.map +0 -1
- package/dist/public-tools-D6Q5MTcO.mjs.map +0 -1
- package/dist/schema-BbQVXp36.mjs +0 -37
- package/dist/schema-BbQVXp36.mjs.map +0 -1
- package/dist/topup-server-6MH7q73X.mjs.map +0 -1
|
@@ -62,11 +62,11 @@ const SCHEMA_QUERY_SET = [
|
|
|
62
62
|
query: "MATCH (:Address)-[r:FLOWS_TO]->(:Address) RETURN \"amount_sum\" AS property_key, count(r) AS sample_count LIMIT 1"
|
|
63
63
|
}
|
|
64
64
|
];
|
|
65
|
-
function clampInt$
|
|
65
|
+
function clampInt$2(value, fallback, min, max) {
|
|
66
66
|
if (!Number.isFinite(value)) return fallback;
|
|
67
67
|
return Math.max(min, Math.min(max, Math.trunc(value)));
|
|
68
68
|
}
|
|
69
|
-
function escapeCypherString$
|
|
69
|
+
function escapeCypherString$3(value) {
|
|
70
70
|
return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
|
|
71
71
|
}
|
|
72
72
|
function sanitizeSegment$1(value) {
|
|
@@ -94,11 +94,11 @@ async function ensureDirs(paths) {
|
|
|
94
94
|
mode: 448
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
|
-
function textFromToolResult$
|
|
97
|
+
function textFromToolResult$3(result) {
|
|
98
98
|
return (result.content ?? []).filter((item) => item.type === "text").map((item) => item.text).join("\n");
|
|
99
99
|
}
|
|
100
|
-
function parseGraphBatchResult$
|
|
101
|
-
const text = textFromToolResult$
|
|
100
|
+
function parseGraphBatchResult$3(result) {
|
|
101
|
+
const text = textFromToolResult$3(result).trim();
|
|
102
102
|
if (!text) throw new Error("graph_query_batch returned no text content");
|
|
103
103
|
const parsed = JSON.parse(text);
|
|
104
104
|
if (!parsed.facts?.queries) throw new Error("graph_query_batch response did not include facts.queries");
|
|
@@ -109,7 +109,7 @@ function topologyGraphQuery$1(query) {
|
|
|
109
109
|
if (/^USE\s+/i.test(trimmed)) return trimmed;
|
|
110
110
|
return `USE live_topology ${trimmed}`;
|
|
111
111
|
}
|
|
112
|
-
async function callGraphBatch$
|
|
112
|
+
async function callGraphBatch$3(remoteClient, network, queries) {
|
|
113
113
|
const result = await remoteClient.callTool({
|
|
114
114
|
name: "graph_query_batch",
|
|
115
115
|
arguments: {
|
|
@@ -124,8 +124,8 @@ async function callGraphBatch$2(remoteClient, network, queries) {
|
|
|
124
124
|
timeout: GRAPH_QUERY_BATCH_REQUEST_TIMEOUT_MS$1,
|
|
125
125
|
maxTotalTimeout: GRAPH_QUERY_BATCH_REQUEST_TIMEOUT_MS$1
|
|
126
126
|
});
|
|
127
|
-
if (result.isError) throw new Error(textFromToolResult$
|
|
128
|
-
return parseGraphBatchResult$
|
|
127
|
+
if (result.isError) throw new Error(textFromToolResult$3(result) || "graph_query_batch failed");
|
|
128
|
+
return parseGraphBatchResult$3(result);
|
|
129
129
|
}
|
|
130
130
|
function resultsFor(batch, id) {
|
|
131
131
|
const query = batch.facts?.queries?.find((entry) => entry.id === id);
|
|
@@ -166,7 +166,7 @@ async function loadOrCaptureTopologySchema(remoteClient, paths, network) {
|
|
|
166
166
|
} catch (err) {
|
|
167
167
|
if (err.code !== "ENOENT") throw err;
|
|
168
168
|
}
|
|
169
|
-
const schema = schemaFromGraphBatch(network, await callGraphBatch$
|
|
169
|
+
const schema = schemaFromGraphBatch(network, await callGraphBatch$3(remoteClient, network, SCHEMA_QUERY_SET));
|
|
170
170
|
await writeFile(filePath, JSON.stringify(schema, null, 2) + "\n", { mode: 384 });
|
|
171
171
|
return {
|
|
172
172
|
schema,
|
|
@@ -204,7 +204,7 @@ function forwardExchangeQueryAtDepth(address, limit, minAmountSum, depth) {
|
|
|
204
204
|
return {
|
|
205
205
|
id: `forward_exchange_paths_${depth}`,
|
|
206
206
|
query: [
|
|
207
|
-
`MATCH (s:Address {address: "${escapeCypherString$
|
|
207
|
+
`MATCH (s:Address {address: "${escapeCypherString$3(address)}"})${relationshipChain}`,
|
|
208
208
|
`WHERE ${predicates.join(" AND ")}`,
|
|
209
209
|
`RETURN [${nodeVariables.map((nodeVariable) => `${nodeVariable}.address`).join(", ")}] AS addresses, [${nodeVariables.map((nodeVariable) => `${nodeVariable}.labels`).join(", ")}] AS node_labels, [${nodeVariables.map(pathNodeMap$1).join(", ")}] AS path_nodes, [${edgeVariables.map(flowEdgeMap$1).join(", ")}] AS edge_props, t.address AS exchange_address, t.labels AS exchange_display_labels, t.labels AS exchange_labels, t.address_type AS exchange_address_type, t.address_subtypes AS exchange_address_subtypes, ${depositVariable}.address AS deposit_address, ${depth} AS hops`,
|
|
210
210
|
"ORDER BY hops ASC",
|
|
@@ -230,7 +230,7 @@ function backwardSourceQueryAtDepth(id, depositAddress, depth) {
|
|
|
230
230
|
return {
|
|
231
231
|
id,
|
|
232
232
|
query: [
|
|
233
|
-
`MATCH (dep:Address {address: "${escapeCypherString$
|
|
233
|
+
`MATCH (dep:Address {address: "${escapeCypherString$3(depositAddress)}"})`,
|
|
234
234
|
`MATCH (dep)${relationshipChain}`,
|
|
235
235
|
`WHERE source <> dep AND source.is_exchange IS NOT NULL${intermediatePredicates.length > 0 ? ` AND ${intermediatePredicates.join(" AND ")}` : ""}`,
|
|
236
236
|
`RETURN dep.address AS deposit_address, source.address AS source_exchange, source.labels AS source_display_labels, source.labels AS source_labels, source.address_type AS source_address_type, source.address_subtypes AS source_address_subtypes, ${depth} AS hops, [${nodeVariables.map((nodeVariable) => `${nodeVariable}.address`).join(", ")}] AS addresses, [${nodeVariables.map((nodeVariable) => `${nodeVariable}.labels`).join(", ")}] AS node_labels, [${nodeVariables.map(pathNodeMap$1).join(", ")}] AS path_nodes`,
|
|
@@ -243,7 +243,7 @@ function reverseLeadsQuery(depositAddresses) {
|
|
|
243
243
|
id: "reverse_1hop",
|
|
244
244
|
query: [
|
|
245
245
|
"MATCH (sender:Address)-[r:FLOWS_TO]->(deposit:Address)",
|
|
246
|
-
`WHERE (${depositAddresses.map((address) => `deposit.address = "${escapeCypherString$
|
|
246
|
+
`WHERE (${depositAddresses.map((address) => `deposit.address = "${escapeCypherString$3(address)}"`).join(" OR ")}) AND sender.is_exchange IS NULL AND sender.address <> deposit.address`,
|
|
247
247
|
"RETURN DISTINCT sender.address AS address, sender.labels AS display_labels, sender.labels AS system_labels, sender.address_type AS address_type, sender.address_subtypes AS address_subtypes, coalesce(sender.lifetime_degree_in, 0) AS degree_in, coalesce(sender.lifetime_degree_out, 0) AS degree_out, coalesce(sender.total_volume_usd, 0) AS total_volume_usd, deposit.address AS deposit_address, r.amount_usd_sum AS amount_usd",
|
|
248
248
|
"ORDER BY r.amount_usd_sum DESC",
|
|
249
249
|
`LIMIT ${Math.max(50, depositAddresses.length * 50)}`
|
|
@@ -263,13 +263,13 @@ function directEdgePropsQuery(flows) {
|
|
|
263
263
|
id: "direct_edge_props",
|
|
264
264
|
query: [
|
|
265
265
|
"MATCH (a:Address)-[r:FLOWS_TO]->(b:Address)",
|
|
266
|
-
`WHERE (${pairs.map((pair) => `(a.address = "${escapeCypherString$
|
|
266
|
+
`WHERE (${pairs.map((pair) => `(a.address = "${escapeCypherString$3(pair.src)}" AND b.address = "${escapeCypherString$3(pair.dst)}")`).join(" OR ")})`,
|
|
267
267
|
"RETURN a.address AS src, b.address AS dst, r.amount_sum AS amount_sum, r.amount_usd_sum AS amount_usd_sum, r.tx_count AS tx_count, r.first_tx_id AS first_tx_id, r.last_tx_id AS last_tx_id",
|
|
268
268
|
`LIMIT ${pairs.length}`
|
|
269
269
|
].join(" ")
|
|
270
270
|
};
|
|
271
271
|
}
|
|
272
|
-
function numberValue$
|
|
272
|
+
function numberValue$3(value) {
|
|
273
273
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
274
274
|
if (typeof value === "string" && value.trim()) {
|
|
275
275
|
const parsed = Number(value);
|
|
@@ -280,7 +280,7 @@ function rowTerminalAmount(row) {
|
|
|
280
280
|
const edgeProps = Array.isArray(row["edge_props"]) ? row["edge_props"] : [];
|
|
281
281
|
const terminalEdge = edgeProps[edgeProps.length - 1];
|
|
282
282
|
if (!terminalEdge) return void 0;
|
|
283
|
-
return numberValue$
|
|
283
|
+
return numberValue$3(terminalEdge["amount_sum"]) ?? numberValue$3(terminalEdge["amount_usd_sum"]);
|
|
284
284
|
}
|
|
285
285
|
function rowsMatchingMinimumAmount(rows, minAmountSum) {
|
|
286
286
|
if (minAmountSum <= 0) return rows;
|
|
@@ -328,9 +328,9 @@ function depositFromRow(row) {
|
|
|
328
328
|
exchangeAddress,
|
|
329
329
|
exchangeLabels: stringArrayValue$1(row["exchange_labels"]),
|
|
330
330
|
exchangeNode,
|
|
331
|
-
amount_sum: numberValue$
|
|
332
|
-
amount_usd_sum: numberValue$
|
|
333
|
-
hops: numberValue$
|
|
331
|
+
amount_sum: numberValue$3(terminalEdge["amount_sum"]),
|
|
332
|
+
amount_usd_sum: numberValue$3(terminalEdge["amount_usd_sum"]),
|
|
333
|
+
hops: numberValue$3(row["hops"]) ?? pathAddresses.length - 1,
|
|
334
334
|
path: pathAddresses,
|
|
335
335
|
pathNodes
|
|
336
336
|
};
|
|
@@ -350,7 +350,7 @@ function flowsFromForwardRows(rows) {
|
|
|
350
350
|
const src = pathAddresses[index];
|
|
351
351
|
const dst = pathAddresses[index + 1];
|
|
352
352
|
const edge = edgeProps[index] ?? {};
|
|
353
|
-
const amount = numberValue$
|
|
353
|
+
const amount = numberValue$3(edge["amount_sum"]) ?? numberValue$3(edge["amount_usd_sum"]) ?? 0;
|
|
354
354
|
const terminal = index === pathAddresses.length - 2;
|
|
355
355
|
const key = `${src}->${dst}`;
|
|
356
356
|
if (seenEdges.has(key)) continue;
|
|
@@ -360,8 +360,8 @@ function flowsFromForwardRows(rows) {
|
|
|
360
360
|
src,
|
|
361
361
|
dst,
|
|
362
362
|
amount_sum: amount,
|
|
363
|
-
amount_usd_sum: numberValue$
|
|
364
|
-
tx_count: numberValue$
|
|
363
|
+
amount_usd_sum: numberValue$3(edge["amount_usd_sum"]),
|
|
364
|
+
tx_count: numberValue$3(edge["tx_count"]),
|
|
365
365
|
first_tx_id: typeof edge["first_tx_id"] === "string" ? edge["first_tx_id"] : void 0,
|
|
366
366
|
last_tx_id: typeof edge["last_tx_id"] === "string" ? edge["last_tx_id"] : void 0,
|
|
367
367
|
src_labels: nodeLabels[index],
|
|
@@ -380,7 +380,7 @@ function flowsFromForwardRows(rows) {
|
|
|
380
380
|
async function hydrateDirectEdgeProps(remoteClient, network, flows, deposits) {
|
|
381
381
|
const query = directEdgePropsQuery(flows);
|
|
382
382
|
if (!query) return;
|
|
383
|
-
const batch = await callGraphBatch$
|
|
383
|
+
const batch = await callGraphBatch$3(remoteClient, network, [query]);
|
|
384
384
|
const edgeProps = /* @__PURE__ */ new Map();
|
|
385
385
|
for (const row of resultsFor(batch, "direct_edge_props")) {
|
|
386
386
|
const src = typeof row["src"] === "string" ? row["src"] : "";
|
|
@@ -391,21 +391,21 @@ async function hydrateDirectEdgeProps(remoteClient, network, flows, deposits) {
|
|
|
391
391
|
for (const flow of flows) {
|
|
392
392
|
const props = edgeProps.get(edgeKey$1(flow.src, flow.dst));
|
|
393
393
|
if (!props) continue;
|
|
394
|
-
flow.amount_sum = numberValue$
|
|
395
|
-
flow.amount_usd_sum = numberValue$
|
|
396
|
-
flow.tx_count = numberValue$
|
|
394
|
+
flow.amount_sum = numberValue$3(props["amount_sum"]) ?? flow.amount_sum;
|
|
395
|
+
flow.amount_usd_sum = numberValue$3(props["amount_usd_sum"]);
|
|
396
|
+
flow.tx_count = numberValue$3(props["tx_count"]);
|
|
397
397
|
flow.first_tx_id = typeof props["first_tx_id"] === "string" ? props["first_tx_id"] : void 0;
|
|
398
398
|
flow.last_tx_id = typeof props["last_tx_id"] === "string" ? props["last_tx_id"] : void 0;
|
|
399
399
|
}
|
|
400
400
|
for (const deposit of deposits) {
|
|
401
401
|
const props = edgeProps.get(edgeKey$1(deposit.address, deposit.exchangeAddress));
|
|
402
402
|
if (!props) continue;
|
|
403
|
-
deposit.amount_sum = numberValue$
|
|
404
|
-
deposit.amount_usd_sum = numberValue$
|
|
403
|
+
deposit.amount_sum = numberValue$3(props["amount_sum"]);
|
|
404
|
+
deposit.amount_usd_sum = numberValue$3(props["amount_usd_sum"]);
|
|
405
405
|
}
|
|
406
406
|
}
|
|
407
407
|
async function collectProbeTrace(remoteClient, options) {
|
|
408
|
-
const { flows, deposits } = flowsFromForwardRows(rowsMatchingMinimumAmount(((await callGraphBatch$
|
|
408
|
+
const { flows, deposits } = flowsFromForwardRows(rowsMatchingMinimumAmount(((await callGraphBatch$3(remoteClient, options.network, [...forwardExchangeQueries(options.seedAddress, Math.max(options.perAddressLimit * 20, 200), options.minAmountSum, options.maxHops)])).facts?.queries ?? []).filter((query) => query.id?.startsWith("forward_exchange_paths_")).flatMap((query) => {
|
|
409
409
|
if (query.ok === false) throw new Error(query.error || `Query failed: ${query.id}`);
|
|
410
410
|
return query.results ?? [];
|
|
411
411
|
}), options.minAmountSum));
|
|
@@ -413,7 +413,7 @@ async function collectProbeTrace(remoteClient, options) {
|
|
|
413
413
|
const uniqueDepositAddresses = [...new Set(deposits.map((deposit) => deposit.address))];
|
|
414
414
|
const sourceMatches = [];
|
|
415
415
|
if (uniqueDepositAddresses.length > 0) {
|
|
416
|
-
const backwardBatch = await callGraphBatch$
|
|
416
|
+
const backwardBatch = await callGraphBatch$3(remoteClient, options.network, uniqueDepositAddresses.slice(0, Math.max(1, Math.floor(20 / options.maxHops))).flatMap((address, index) => backwardSourceQueries(`backward_from_deposit_${index + 1}`, address, options.maxHops)));
|
|
417
417
|
for (const query of backwardBatch.facts?.queries ?? []) for (const row of query.results ?? []) {
|
|
418
418
|
const pathAddresses = stringArrayValue$1(row["addresses"]) ?? [];
|
|
419
419
|
const pathNodes = Array.isArray(row["path_nodes"]) ? row["path_nodes"].map((node, index) => nodeMetadataFromValue(node, pathAddresses[index])).filter((node) => Boolean(node)) : void 0;
|
|
@@ -432,7 +432,7 @@ async function collectProbeTrace(remoteClient, options) {
|
|
|
432
432
|
source_exchange: sourceExchange,
|
|
433
433
|
source_labels: stringArrayValue$1(row["source_labels"]),
|
|
434
434
|
sourceNode,
|
|
435
|
-
hops: numberValue$
|
|
435
|
+
hops: numberValue$3(row["hops"]) ?? Math.max(pathAddresses.length - 1, 0),
|
|
436
436
|
path: pathAddresses,
|
|
437
437
|
pathNodes
|
|
438
438
|
});
|
|
@@ -440,15 +440,15 @@ async function collectProbeTrace(remoteClient, options) {
|
|
|
440
440
|
}
|
|
441
441
|
const reverseLeads = [];
|
|
442
442
|
if (uniqueDepositAddresses.length > 0) {
|
|
443
|
-
const reverseBatch = await callGraphBatch$
|
|
443
|
+
const reverseBatch = await callGraphBatch$3(remoteClient, options.network, [reverseLeadsQuery(uniqueDepositAddresses)]);
|
|
444
444
|
for (const row of resultsFor(reverseBatch, "reverse_1hop")) {
|
|
445
445
|
const address = typeof row["address"] === "string" ? row["address"] : "";
|
|
446
446
|
const depositAddress = typeof row["deposit_address"] === "string" ? row["deposit_address"] : "";
|
|
447
447
|
if (!address || !depositAddress) continue;
|
|
448
448
|
const labels = stringArrayValue$1(row["display_labels"]) ?? stringArrayValue$1(row["labels"]) ?? [];
|
|
449
|
-
const degreeIn = numberValue$
|
|
450
|
-
const degreeOut = numberValue$
|
|
451
|
-
const totalVolume = numberValue$
|
|
449
|
+
const degreeIn = numberValue$3(row["degree_in"]) ?? 0;
|
|
450
|
+
const degreeOut = numberValue$3(row["degree_out"]) ?? 0;
|
|
451
|
+
const totalVolume = numberValue$3(row["total_volume_usd"]) ?? 0;
|
|
452
452
|
const reason = labels.length > 0 ? "labeled_entity" : degreeIn > 50 ? "fan_in_hub" : degreeOut > 50 ? "fan_out_hub" : totalVolume > 1e5 ? "high_volume_sender" : "";
|
|
453
453
|
if (!reason) continue;
|
|
454
454
|
reverseLeads.push({
|
|
@@ -465,7 +465,7 @@ async function collectProbeTrace(remoteClient, options) {
|
|
|
465
465
|
degree_out: degreeOut,
|
|
466
466
|
total_volume_usd: totalVolume,
|
|
467
467
|
deposit_address: depositAddress,
|
|
468
|
-
amount_usd: numberValue$
|
|
468
|
+
amount_usd: numberValue$3(row["amount_usd"]),
|
|
469
469
|
reason
|
|
470
470
|
});
|
|
471
471
|
}
|
|
@@ -819,8 +819,8 @@ async function runFundFlowProbe(remoteClient, _config, options) {
|
|
|
819
819
|
const network = options.network.trim();
|
|
820
820
|
if (!seedAddress) throw new Error("seed_address is required");
|
|
821
821
|
if (!network) throw new Error("network is required");
|
|
822
|
-
const maxHops = clampInt$
|
|
823
|
-
const perAddressLimit = clampInt$
|
|
822
|
+
const maxHops = clampInt$2(options.maxHops, 3, 1, 5);
|
|
823
|
+
const perAddressLimit = clampInt$2(options.perAddressLimit, 5, 1, 10);
|
|
824
824
|
const minAmountSum = Math.max(0, options.minAmountSum ?? 0);
|
|
825
825
|
const paths = workspaceOutputPaths();
|
|
826
826
|
await ensureDirs(paths);
|
|
@@ -932,17 +932,17 @@ function stringArray(value) {
|
|
|
932
932
|
}
|
|
933
933
|
return [];
|
|
934
934
|
}
|
|
935
|
-
function stringValue(value) {
|
|
935
|
+
function stringValue$1(value) {
|
|
936
936
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
937
937
|
}
|
|
938
|
-
function numberValue$
|
|
938
|
+
function numberValue$2(value) {
|
|
939
939
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
940
940
|
if (typeof value === "string" && value.trim()) {
|
|
941
941
|
const parsed = Number(value);
|
|
942
942
|
if (Number.isFinite(parsed)) return parsed;
|
|
943
943
|
}
|
|
944
944
|
}
|
|
945
|
-
function clampInt(value, fallback, min, max) {
|
|
945
|
+
function clampInt$1(value, fallback, min, max) {
|
|
946
946
|
if (!Number.isFinite(value)) return fallback;
|
|
947
947
|
return Math.max(min, Math.min(max, Math.trunc(value)));
|
|
948
948
|
}
|
|
@@ -968,20 +968,20 @@ async function ensureScamTopologyDirs(paths) {
|
|
|
968
968
|
mode: 448
|
|
969
969
|
});
|
|
970
970
|
}
|
|
971
|
-
function escapeCypherString$
|
|
971
|
+
function escapeCypherString$2(value) {
|
|
972
972
|
return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
|
|
973
973
|
}
|
|
974
|
-
function textFromToolResult$
|
|
974
|
+
function textFromToolResult$2(result) {
|
|
975
975
|
return (result.content ?? []).filter((item) => item.type === "text").map((item) => item.text).join("\n");
|
|
976
976
|
}
|
|
977
|
-
function parseGraphBatchResult$
|
|
978
|
-
const text = textFromToolResult$
|
|
977
|
+
function parseGraphBatchResult$2(result) {
|
|
978
|
+
const text = textFromToolResult$2(result).trim();
|
|
979
979
|
if (!text) throw new Error("graph_query_batch returned no text content");
|
|
980
980
|
const parsed = JSON.parse(text);
|
|
981
981
|
if (!parsed.facts?.queries) throw new Error("graph_query_batch response did not include facts.queries");
|
|
982
982
|
return parsed;
|
|
983
983
|
}
|
|
984
|
-
async function callGraphBatch$
|
|
984
|
+
async function callGraphBatch$2(remoteClient, network, queries) {
|
|
985
985
|
const result = await remoteClient.callTool({
|
|
986
986
|
name: "graph_query_batch",
|
|
987
987
|
arguments: {
|
|
@@ -993,8 +993,8 @@ async function callGraphBatch$1(remoteClient, network, queries) {
|
|
|
993
993
|
timeout: SCAM_TOPOLOGY_GRAPH_BATCH_REQUEST_TIMEOUT_MS,
|
|
994
994
|
maxTotalTimeout: SCAM_TOPOLOGY_GRAPH_BATCH_REQUEST_TIMEOUT_MS
|
|
995
995
|
});
|
|
996
|
-
if (result.isError) throw new Error(textFromToolResult$
|
|
997
|
-
return parseGraphBatchResult$
|
|
996
|
+
if (result.isError) throw new Error(textFromToolResult$2(result) || "graph_query_batch failed");
|
|
997
|
+
return parseGraphBatchResult$2(result);
|
|
998
998
|
}
|
|
999
999
|
function graphForScope(graphScope) {
|
|
1000
1000
|
return graphScope === "history" ? "archive_topology" : "live_topology";
|
|
@@ -1047,7 +1047,7 @@ function frontierQuery(graphScope, sourceAddress, hop, sourceIndex, perAddressLi
|
|
|
1047
1047
|
id: sourceIndex === void 0 ? `${graphScope}_hop_${hop}` : `${graphScope}_hop_${hop}_source_${sourceIndex}`,
|
|
1048
1048
|
query: [
|
|
1049
1049
|
`USE ${graphForScope(graphScope)}`,
|
|
1050
|
-
`MATCH (src:Address {address: "${escapeCypherString$
|
|
1050
|
+
`MATCH (src:Address {address: "${escapeCypherString$2(sourceAddress)}"})-[r:FLOWS_TO]->(dst:Address)`,
|
|
1051
1051
|
`WHERE ${where.join(" AND ")}`,
|
|
1052
1052
|
`RETURN ${traversalProjection()}`,
|
|
1053
1053
|
"ORDER BY r.amount_sum DESC",
|
|
@@ -1072,7 +1072,7 @@ function depositClusterQuery(graphScope, depositAddress, index, minAmountSum) {
|
|
|
1072
1072
|
id: `${graphScope}_deposit_cluster_${index}`,
|
|
1073
1073
|
query: [
|
|
1074
1074
|
`USE ${graphForScope(graphScope)}`,
|
|
1075
|
-
`MATCH (src:Address)-[r:FLOWS_TO]->(dst:Address {address: "${escapeCypherString$
|
|
1075
|
+
`MATCH (src:Address)-[r:FLOWS_TO]->(dst:Address {address: "${escapeCypherString$2(depositAddress)}"})`,
|
|
1076
1076
|
`WHERE ${where.join(" AND ")}`,
|
|
1077
1077
|
`RETURN ${traversalProjection()}`,
|
|
1078
1078
|
"ORDER BY r.amount_sum DESC",
|
|
@@ -1081,8 +1081,8 @@ function depositClusterQuery(graphScope, depositAddress, index, minAmountSum) {
|
|
|
1081
1081
|
};
|
|
1082
1082
|
}
|
|
1083
1083
|
function edgeFromRow(row, graphScope, hop, context) {
|
|
1084
|
-
const src = stringValue(row["src"]) ?? stringValue(row["from_address"]);
|
|
1085
|
-
const dst = stringValue(row["dst"]) ?? stringValue(row["to_address"]);
|
|
1084
|
+
const src = stringValue$1(row["src"]) ?? stringValue$1(row["from_address"]);
|
|
1085
|
+
const dst = stringValue$1(row["dst"]) ?? stringValue$1(row["to_address"]);
|
|
1086
1086
|
if (!src || !dst || src === dst) return null;
|
|
1087
1087
|
const srcLabels = stringArray(row["src_labels"]);
|
|
1088
1088
|
const dstLabels = stringArray(row["dst_labels"]);
|
|
@@ -1100,13 +1100,13 @@ function edgeFromRow(row, graphScope, hop, context) {
|
|
|
1100
1100
|
topology_graph: graphForScope(graphScope),
|
|
1101
1101
|
seed_address: context.seedAddress,
|
|
1102
1102
|
seed_role: context.seedRole,
|
|
1103
|
-
amount_sum: numberValue$
|
|
1104
|
-
amount_usd_sum: numberValue$
|
|
1105
|
-
tx_count: numberValue$
|
|
1106
|
-
first_seen_timestamp: numberValue$
|
|
1107
|
-
last_seen_timestamp: numberValue$
|
|
1108
|
-
first_tx_id: stringValue(row["first_tx_id"]),
|
|
1109
|
-
last_tx_id: stringValue(row["last_tx_id"]),
|
|
1103
|
+
amount_sum: numberValue$2(row["amount_sum"]),
|
|
1104
|
+
amount_usd_sum: numberValue$2(row["amount_usd_sum"]),
|
|
1105
|
+
tx_count: numberValue$2(row["tx_count"]),
|
|
1106
|
+
first_seen_timestamp: numberValue$2(row["first_seen_timestamp"]),
|
|
1107
|
+
last_seen_timestamp: numberValue$2(row["last_seen_timestamp"]),
|
|
1108
|
+
first_tx_id: stringValue$1(row["first_tx_id"]),
|
|
1109
|
+
last_tx_id: stringValue$1(row["last_tx_id"]),
|
|
1110
1110
|
src_labels: srcLabels,
|
|
1111
1111
|
dst_labels: dstLabels,
|
|
1112
1112
|
src_is_exchange: srcIsExchange,
|
|
@@ -1147,7 +1147,7 @@ async function runDirectedTraversal(remoteClient, network, seeds, graphScope, ac
|
|
|
1147
1147
|
for (const queryChunk of chunks(queries, maxBatchQueries)) {
|
|
1148
1148
|
let batch;
|
|
1149
1149
|
try {
|
|
1150
|
-
batch = await callGraphBatch$
|
|
1150
|
+
batch = await callGraphBatch$2(remoteClient, network, queryChunk);
|
|
1151
1151
|
} catch (err) {
|
|
1152
1152
|
if (hop === 1) throw err;
|
|
1153
1153
|
for (const query of queryChunk) skippedQueryErrors.push({
|
|
@@ -1170,7 +1170,7 @@ async function runDirectedTraversal(remoteClient, network, seeds, graphScope, ac
|
|
|
1170
1170
|
continue;
|
|
1171
1171
|
}
|
|
1172
1172
|
for (const row of queryResult.results ?? []) {
|
|
1173
|
-
const src = stringValue(row["src"]) ?? stringValue(row["from_address"]);
|
|
1173
|
+
const src = stringValue$1(row["src"]) ?? stringValue$1(row["from_address"]);
|
|
1174
1174
|
if (!src) continue;
|
|
1175
1175
|
const contexts = frontierByAddress.get(src) ?? [];
|
|
1176
1176
|
for (const context of contexts) {
|
|
@@ -1239,7 +1239,7 @@ async function expandDepositClusters(remoteClient, network, run, minAmountSum) {
|
|
|
1239
1239
|
for (const queryChunk of chunks(queries, maxBatchQueries)) {
|
|
1240
1240
|
let batch;
|
|
1241
1241
|
try {
|
|
1242
|
-
batch = await callGraphBatch$
|
|
1242
|
+
batch = await callGraphBatch$2(remoteClient, network, queryChunk);
|
|
1243
1243
|
} catch (err) {
|
|
1244
1244
|
for (const query of queryChunk) run.skippedQueryErrors.push({
|
|
1245
1245
|
id: query.id,
|
|
@@ -1588,16 +1588,16 @@ function scamLabelsByAddress(facts) {
|
|
|
1588
1588
|
for (const label of labels) {
|
|
1589
1589
|
if (!label || typeof label !== "object" || Array.isArray(label)) continue;
|
|
1590
1590
|
const record = label;
|
|
1591
|
-
const address = stringValue(record["address"]);
|
|
1592
|
-
const confidence = numberValue$
|
|
1591
|
+
const address = stringValue$1(record["address"]);
|
|
1592
|
+
const confidence = numberValue$2(record["confidence"]);
|
|
1593
1593
|
if (!address || confidence === void 0) continue;
|
|
1594
1594
|
result.set(address, {
|
|
1595
1595
|
address,
|
|
1596
1596
|
scam: true,
|
|
1597
1597
|
confidence,
|
|
1598
1598
|
source: "scam_topology",
|
|
1599
|
-
source_victim_address: stringValue(record["source_victim_address"]) ?? "",
|
|
1600
|
-
source_incident_timestamp_ms: numberValue$
|
|
1599
|
+
source_victim_address: stringValue$1(record["source_victim_address"]) ?? "",
|
|
1600
|
+
source_incident_timestamp_ms: numberValue$2(record["source_incident_timestamp_ms"]) ?? 0
|
|
1601
1601
|
});
|
|
1602
1602
|
}
|
|
1603
1603
|
return result;
|
|
@@ -1644,7 +1644,7 @@ function buildGraph(seeds, edges, rolesByAddress, facts) {
|
|
|
1644
1644
|
const addFlowTotals = (address, direction, amount) => {
|
|
1645
1645
|
const node = nodesById.get(address) ?? mergeNode(address, []);
|
|
1646
1646
|
const key = direction === "in" ? "flow_in_usd" : "flow_out_usd";
|
|
1647
|
-
node[key] = (numberValue$
|
|
1647
|
+
node[key] = (numberValue$2(node[key]) ?? 0) + amount;
|
|
1648
1648
|
nodesById.set(address, node);
|
|
1649
1649
|
};
|
|
1650
1650
|
for (const edge of edges) {
|
|
@@ -1851,7 +1851,7 @@ function buildScamTopologyReport(facts, files) {
|
|
|
1851
1851
|
"| Deposit | Exchange | Names | Hop | amount_sum | tx_count |",
|
|
1852
1852
|
"|---|---|---|---:|---:|---:|",
|
|
1853
1853
|
...facts.exchange_deposits.map((entry) => {
|
|
1854
|
-
return `| \`${stringValue(entry["deposit_address"]) ?? ""}\` | \`${stringValue(entry["exchange_address"]) ?? ""}\` | ${stringArray(entry["exchange_names"]).join(", ") || ""} | ${entry["hop"] ?? ""} | ${entry["amount_sum"] ?? ""} | ${entry["tx_count"] ?? ""} |`;
|
|
1854
|
+
return `| \`${stringValue$1(entry["deposit_address"]) ?? ""}\` | \`${stringValue$1(entry["exchange_address"]) ?? ""}\` | ${stringArray(entry["exchange_names"]).join(", ") || ""} | ${entry["hop"] ?? ""} | ${entry["amount_sum"] ?? ""} | ${entry["tx_count"] ?? ""} |`;
|
|
1855
1855
|
}),
|
|
1856
1856
|
"",
|
|
1857
1857
|
"## Label Candidates",
|
|
@@ -1923,7 +1923,7 @@ async function scamTopology(remoteClient, config, options) {
|
|
|
1923
1923
|
const victimAddresses = parseAddressList$1(options.victimAddress ?? legacyOptions.victimAddresses);
|
|
1924
1924
|
const scammerAddresses = parseAddressList$1(legacyOptions.scammerAddresses);
|
|
1925
1925
|
const incidentTimestampMs = validateNonNegativeNumber(options.incidentTimestampMs, "incident_timestamp_ms");
|
|
1926
|
-
const maxHops = clampInt(options.maxHops, SCAM_TOPOLOGY_DEFAULT_MAX_HOPS, 1, SCAM_TOPOLOGY_MAX_HOPS);
|
|
1926
|
+
const maxHops = clampInt$1(options.maxHops, SCAM_TOPOLOGY_DEFAULT_MAX_HOPS, 1, SCAM_TOPOLOGY_MAX_HOPS);
|
|
1927
1927
|
const perAddressLimit = SCAM_TOPOLOGY_FRONTIER_LIMIT;
|
|
1928
1928
|
const minAmountSum = void 0;
|
|
1929
1929
|
const activityPolicyMode = validateActivityPolicyMode(options.activityPolicyMode);
|
|
@@ -2026,6 +2026,401 @@ async function scamTopology(remoteClient, config, options) {
|
|
|
2026
2026
|
};
|
|
2027
2027
|
}
|
|
2028
2028
|
//#endregion
|
|
2029
|
+
//#region src/investigation/stake-insights.ts
|
|
2030
|
+
const STAKE_INSIGHTS_QUERY_TIMEOUT_SECONDS = 120;
|
|
2031
|
+
const STAKE_INSIGHTS_REQUEST_TIMEOUT_MS = 300 * 1e3;
|
|
2032
|
+
function escapeCypherString$1(value) {
|
|
2033
|
+
return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
|
|
2034
|
+
}
|
|
2035
|
+
function textFromToolResult$1(result) {
|
|
2036
|
+
return (result.content ?? []).filter((item) => item.type === "text").map((item) => item.text).join("\n");
|
|
2037
|
+
}
|
|
2038
|
+
function parseGraphBatchResult$1(result) {
|
|
2039
|
+
const text = textFromToolResult$1(result).trim();
|
|
2040
|
+
if (!text) throw new Error("graph_query_batch returned no text content");
|
|
2041
|
+
const parsed = JSON.parse(text);
|
|
2042
|
+
if (!parsed.facts?.queries) throw new Error("graph_query_batch response did not include facts.queries");
|
|
2043
|
+
return parsed;
|
|
2044
|
+
}
|
|
2045
|
+
function stringValue(value) {
|
|
2046
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
2047
|
+
}
|
|
2048
|
+
function numberValue$1(value) {
|
|
2049
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
2050
|
+
if (typeof value === "string" && value.trim()) {
|
|
2051
|
+
const parsed = Number(value);
|
|
2052
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
function nonZeroNumber(value) {
|
|
2056
|
+
const parsed = numberValue$1(value);
|
|
2057
|
+
return parsed !== void 0 && parsed !== 0 ? parsed : void 0;
|
|
2058
|
+
}
|
|
2059
|
+
function clampInt(value, fallback, min, max) {
|
|
2060
|
+
if (!Number.isFinite(value)) return fallback;
|
|
2061
|
+
return Math.max(min, Math.min(max, Math.trunc(value)));
|
|
2062
|
+
}
|
|
2063
|
+
function resolveSubject(options) {
|
|
2064
|
+
const candidates = [
|
|
2065
|
+
["address", options.address],
|
|
2066
|
+
["coldkey", options.coldkey],
|
|
2067
|
+
["hotkey", options.hotkey]
|
|
2068
|
+
].filter((entry) => !!entry[1]?.trim());
|
|
2069
|
+
if (candidates.length !== 1) throw new Error("Provide exactly one of address, coldkey, or hotkey");
|
|
2070
|
+
return {
|
|
2071
|
+
role: candidates[0][0],
|
|
2072
|
+
address: candidates[0][1].trim()
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
function validateOptions(options) {
|
|
2076
|
+
const network = options.network.trim();
|
|
2077
|
+
if (!network) throw new Error("network is required");
|
|
2078
|
+
if (options.startBlock !== void 0 || options.endBlock !== void 0) throw new Error("Block windows are not available on the current stake graph surface; use start_timestamp_ms/end_timestamp_ms");
|
|
2079
|
+
return {
|
|
2080
|
+
network,
|
|
2081
|
+
subject: resolveSubject(options),
|
|
2082
|
+
depth: clampInt(options.depth ?? options.maxHops, 1, 1, 3)
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
function subjectPredicate(subject) {
|
|
2086
|
+
const address = escapeCypherString$1(subject.address);
|
|
2087
|
+
if (subject.role === "coldkey") return `coldkey.address = "${address}"`;
|
|
2088
|
+
if (subject.role === "hotkey") return `hotkey.address = "${address}"`;
|
|
2089
|
+
return `(coldkey.address = "${address}" OR hotkey.address = "${address}")`;
|
|
2090
|
+
}
|
|
2091
|
+
function stakeRelationshipQuery(topologyGraph, subject, options, depth) {
|
|
2092
|
+
const predicates = [subjectPredicate(subject)];
|
|
2093
|
+
if (options.netuid !== void 0) predicates.push(`stake.netuid = ${Math.trunc(options.netuid)}`);
|
|
2094
|
+
if (options.startTimestampMs !== void 0) predicates.push(`stake.last_activity_timestamp >= ${Math.trunc(options.startTimestampMs)}`);
|
|
2095
|
+
if (options.endTimestampMs !== void 0) predicates.push(`stake.first_activity_timestamp <= ${Math.trunc(options.endTimestampMs)}`);
|
|
2096
|
+
const limit = Math.min(500, Math.max(50, depth * 100));
|
|
2097
|
+
return {
|
|
2098
|
+
id: topologyGraph === "live_topology" ? "live_stake_relationships" : "archive_stake_relationships",
|
|
2099
|
+
query: [
|
|
2100
|
+
`USE ${topologyGraph}`,
|
|
2101
|
+
"MATCH (coldkey:Address)-[stake:STAKES_IN]->(hotkey:Address)",
|
|
2102
|
+
`WHERE ${predicates.join(" AND ")}`,
|
|
2103
|
+
[
|
|
2104
|
+
"RETURN coldkey.address AS coldkey",
|
|
2105
|
+
"hotkey.address AS hotkey",
|
|
2106
|
+
"stake.netuid AS netuid",
|
|
2107
|
+
"stake.amount AS amount",
|
|
2108
|
+
"stake.source_role AS source_role",
|
|
2109
|
+
"stake.destination_role AS destination_role",
|
|
2110
|
+
"stake.stake_added_amount AS stake_added_amount",
|
|
2111
|
+
"stake.stake_removed_amount AS stake_removed_amount",
|
|
2112
|
+
"stake.stake_moved_in_amount AS stake_moved_in_amount",
|
|
2113
|
+
"stake.stake_moved_out_amount AS stake_moved_out_amount",
|
|
2114
|
+
"stake.net_stake_change AS net_stake_change",
|
|
2115
|
+
"stake.stake_event_count AS stake_event_count",
|
|
2116
|
+
"stake.first_seen_timestamp AS first_seen_timestamp",
|
|
2117
|
+
"stake.last_seen_timestamp AS last_seen_timestamp",
|
|
2118
|
+
"stake.first_activity_timestamp AS first_activity_timestamp",
|
|
2119
|
+
"stake.last_activity_timestamp AS last_activity_timestamp",
|
|
2120
|
+
"stake.first_tx_id AS first_tx_id",
|
|
2121
|
+
"stake.last_tx_id AS last_tx_id",
|
|
2122
|
+
"stake.active_days AS active_days",
|
|
2123
|
+
"stake.granularity AS granularity",
|
|
2124
|
+
"stake.source_stake_rows AS source_stake_rows",
|
|
2125
|
+
"stake.source_backend AS source_backend",
|
|
2126
|
+
`"${topologyGraph}" AS topology_graph`
|
|
2127
|
+
].join(", "),
|
|
2128
|
+
"ORDER BY stake.amount DESC",
|
|
2129
|
+
`LIMIT ${limit}`
|
|
2130
|
+
].join(" ")
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2133
|
+
async function callGraphBatch$1(remoteClient, network, queries) {
|
|
2134
|
+
const result = await remoteClient.callTool({
|
|
2135
|
+
name: "graph_query_batch",
|
|
2136
|
+
arguments: {
|
|
2137
|
+
network,
|
|
2138
|
+
queries,
|
|
2139
|
+
per_query_timeout_seconds: STAKE_INSIGHTS_QUERY_TIMEOUT_SECONDS
|
|
2140
|
+
}
|
|
2141
|
+
}, void 0, {
|
|
2142
|
+
timeout: STAKE_INSIGHTS_REQUEST_TIMEOUT_MS,
|
|
2143
|
+
maxTotalTimeout: STAKE_INSIGHTS_REQUEST_TIMEOUT_MS
|
|
2144
|
+
});
|
|
2145
|
+
if (result.isError) throw new Error(textFromToolResult$1(result) || "graph_query_batch failed");
|
|
2146
|
+
return parseGraphBatchResult$1(result);
|
|
2147
|
+
}
|
|
2148
|
+
function topologyGraphForQueryId(id) {
|
|
2149
|
+
return id.startsWith("archive_") ? "archive_topology" : "live_topology";
|
|
2150
|
+
}
|
|
2151
|
+
function collectRelationships(batch) {
|
|
2152
|
+
const failures = [];
|
|
2153
|
+
const evidence = [];
|
|
2154
|
+
const live = [];
|
|
2155
|
+
const archive = [];
|
|
2156
|
+
for (const query of batch.facts?.queries ?? []) {
|
|
2157
|
+
const id = query.id ?? "unknown";
|
|
2158
|
+
const topologyGraph = topologyGraphForQueryId(id);
|
|
2159
|
+
if (query.ok === false) {
|
|
2160
|
+
failures.push({
|
|
2161
|
+
id,
|
|
2162
|
+
error: query.error || "unknown error"
|
|
2163
|
+
});
|
|
2164
|
+
evidence.push({
|
|
2165
|
+
id,
|
|
2166
|
+
topology_graph: topologyGraph,
|
|
2167
|
+
ok: false,
|
|
2168
|
+
row_count: 0,
|
|
2169
|
+
error: query.error || "unknown error"
|
|
2170
|
+
});
|
|
2171
|
+
continue;
|
|
2172
|
+
}
|
|
2173
|
+
const rows = (query.results ?? []).map((row) => normalizeRelationship(row, topologyGraph));
|
|
2174
|
+
if (topologyGraph === "live_topology") live.push(...rows);
|
|
2175
|
+
else archive.push(...rows);
|
|
2176
|
+
evidence.push({
|
|
2177
|
+
id,
|
|
2178
|
+
topology_graph: topologyGraph,
|
|
2179
|
+
ok: true,
|
|
2180
|
+
row_count: rows.length,
|
|
2181
|
+
source_backends: [...new Set(rows.map((row) => row.source_backend).filter(Boolean))]
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
return {
|
|
2185
|
+
live,
|
|
2186
|
+
archive,
|
|
2187
|
+
failures,
|
|
2188
|
+
evidence
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
function normalizeRelationship(row, topologyGraph) {
|
|
2192
|
+
return {
|
|
2193
|
+
coldkey: String(row["coldkey"] ?? ""),
|
|
2194
|
+
hotkey: String(row["hotkey"] ?? ""),
|
|
2195
|
+
netuid: numberValue$1(row["netuid"]),
|
|
2196
|
+
amount: numberValue$1(row["amount"]),
|
|
2197
|
+
source_role: stringValue(row["source_role"]),
|
|
2198
|
+
destination_role: stringValue(row["destination_role"]),
|
|
2199
|
+
stake_added_amount: numberValue$1(row["stake_added_amount"]),
|
|
2200
|
+
stake_removed_amount: numberValue$1(row["stake_removed_amount"]),
|
|
2201
|
+
stake_moved_in_amount: numberValue$1(row["stake_moved_in_amount"]),
|
|
2202
|
+
stake_moved_out_amount: numberValue$1(row["stake_moved_out_amount"]),
|
|
2203
|
+
net_stake_change: numberValue$1(row["net_stake_change"]),
|
|
2204
|
+
stake_event_count: numberValue$1(row["stake_event_count"]),
|
|
2205
|
+
first_seen_timestamp: numberValue$1(row["first_seen_timestamp"]),
|
|
2206
|
+
last_seen_timestamp: numberValue$1(row["last_seen_timestamp"]),
|
|
2207
|
+
first_activity_timestamp: numberValue$1(row["first_activity_timestamp"]),
|
|
2208
|
+
last_activity_timestamp: numberValue$1(row["last_activity_timestamp"]),
|
|
2209
|
+
first_tx_id: stringValue(row["first_tx_id"]),
|
|
2210
|
+
last_tx_id: stringValue(row["last_tx_id"]),
|
|
2211
|
+
active_days: numberValue$1(row["active_days"]),
|
|
2212
|
+
granularity: stringValue(row["granularity"]),
|
|
2213
|
+
source_stake_rows: numberValue$1(row["source_stake_rows"]),
|
|
2214
|
+
source_backend: stringValue(row["source_backend"]) ?? (topologyGraph === "live_topology" ? "memgraph_live" : "starrocks_archive"),
|
|
2215
|
+
topology_graph: topologyGraph
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
function firstTimestamp(rows) {
|
|
2219
|
+
const timestamps = rows.map((row) => row.first_activity_timestamp).filter((value) => value !== void 0);
|
|
2220
|
+
return timestamps.length > 0 ? Math.min(...timestamps) : void 0;
|
|
2221
|
+
}
|
|
2222
|
+
function lastTimestamp(rows) {
|
|
2223
|
+
const timestamps = rows.map((row) => row.last_activity_timestamp).filter((value) => value !== void 0);
|
|
2224
|
+
return timestamps.length > 0 ? Math.max(...timestamps) : void 0;
|
|
2225
|
+
}
|
|
2226
|
+
function sum(rows, selector) {
|
|
2227
|
+
return rows.reduce((total, row) => total + (selector(row) ?? 0), 0);
|
|
2228
|
+
}
|
|
2229
|
+
function stakeTotals(rows) {
|
|
2230
|
+
return {
|
|
2231
|
+
amount_unit: "tao",
|
|
2232
|
+
total_staked: sum(rows, (row) => row.stake_added_amount),
|
|
2233
|
+
total_unstaked: sum(rows, (row) => row.stake_removed_amount),
|
|
2234
|
+
total_moved_in: sum(rows, (row) => row.stake_moved_in_amount),
|
|
2235
|
+
total_moved_out: sum(rows, (row) => row.stake_moved_out_amount),
|
|
2236
|
+
net_staked: rows.some((row) => row.net_stake_change !== void 0) ? sum(rows, (row) => row.net_stake_change) : sum(rows, (row) => row.amount),
|
|
2237
|
+
relationship_count: rows.length,
|
|
2238
|
+
first_activity_timestamp: firstTimestamp(rows),
|
|
2239
|
+
last_activity_timestamp: lastTimestamp(rows)
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
function movementRows(rows) {
|
|
2243
|
+
const movements = [];
|
|
2244
|
+
for (const row of rows) {
|
|
2245
|
+
const base = {
|
|
2246
|
+
coldkey: row.coldkey,
|
|
2247
|
+
hotkey: row.hotkey,
|
|
2248
|
+
netuid: row.netuid,
|
|
2249
|
+
source_backend: row.source_backend,
|
|
2250
|
+
first_activity_timestamp: row.first_activity_timestamp,
|
|
2251
|
+
last_activity_timestamp: row.last_activity_timestamp
|
|
2252
|
+
};
|
|
2253
|
+
const added = nonZeroNumber(row.stake_added_amount);
|
|
2254
|
+
if (added !== void 0) movements.push({
|
|
2255
|
+
...base,
|
|
2256
|
+
movement_type: "stake_added",
|
|
2257
|
+
direction: "coldkey_to_hotkey",
|
|
2258
|
+
amount: added
|
|
2259
|
+
});
|
|
2260
|
+
const removed = nonZeroNumber(row.stake_removed_amount);
|
|
2261
|
+
if (removed !== void 0) movements.push({
|
|
2262
|
+
...base,
|
|
2263
|
+
movement_type: "stake_removed",
|
|
2264
|
+
direction: "hotkey_to_coldkey",
|
|
2265
|
+
amount: removed
|
|
2266
|
+
});
|
|
2267
|
+
const movedIn = nonZeroNumber(row.stake_moved_in_amount);
|
|
2268
|
+
if (movedIn !== void 0) movements.push({
|
|
2269
|
+
...base,
|
|
2270
|
+
movement_type: "stake_moved_in",
|
|
2271
|
+
direction: "counterparty_to_relationship",
|
|
2272
|
+
amount: movedIn
|
|
2273
|
+
});
|
|
2274
|
+
const movedOut = nonZeroNumber(row.stake_moved_out_amount);
|
|
2275
|
+
if (movedOut !== void 0) movements.push({
|
|
2276
|
+
...base,
|
|
2277
|
+
movement_type: "stake_moved_out",
|
|
2278
|
+
direction: "relationship_to_counterparty",
|
|
2279
|
+
amount: movedOut
|
|
2280
|
+
});
|
|
2281
|
+
}
|
|
2282
|
+
return movements;
|
|
2283
|
+
}
|
|
2284
|
+
function topCounterparties(subject, rows) {
|
|
2285
|
+
const byAddress = /* @__PURE__ */ new Map();
|
|
2286
|
+
for (const row of rows) {
|
|
2287
|
+
const counterparties = [];
|
|
2288
|
+
if (subject.role === "coldkey") counterparties.push({
|
|
2289
|
+
address: row.hotkey,
|
|
2290
|
+
role: "hotkey"
|
|
2291
|
+
});
|
|
2292
|
+
else if (subject.role === "hotkey") counterparties.push({
|
|
2293
|
+
address: row.coldkey,
|
|
2294
|
+
role: "coldkey"
|
|
2295
|
+
});
|
|
2296
|
+
else {
|
|
2297
|
+
if (row.coldkey === subject.address) counterparties.push({
|
|
2298
|
+
address: row.hotkey,
|
|
2299
|
+
role: "hotkey"
|
|
2300
|
+
});
|
|
2301
|
+
if (row.hotkey === subject.address) counterparties.push({
|
|
2302
|
+
address: row.coldkey,
|
|
2303
|
+
role: "coldkey"
|
|
2304
|
+
});
|
|
2305
|
+
}
|
|
2306
|
+
for (const counterparty of counterparties.filter((entry) => entry.address)) {
|
|
2307
|
+
const current = byAddress.get(counterparty.address) ?? {
|
|
2308
|
+
address: counterparty.address,
|
|
2309
|
+
role: counterparty.role,
|
|
2310
|
+
amount: 0,
|
|
2311
|
+
relationship_count: 0,
|
|
2312
|
+
stake_event_count: 0
|
|
2313
|
+
};
|
|
2314
|
+
current.amount += row.amount ?? row.net_stake_change ?? 0;
|
|
2315
|
+
current.relationship_count += 1;
|
|
2316
|
+
current.stake_event_count += row.stake_event_count ?? 0;
|
|
2317
|
+
byAddress.set(counterparty.address, current);
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
return [...byAddress.values()].sort((left, right) => Math.abs(right.amount) - Math.abs(left.amount)).slice(0, 10);
|
|
2321
|
+
}
|
|
2322
|
+
function graphData(rows, subject, network) {
|
|
2323
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
2324
|
+
const ensureNode = (address, role) => {
|
|
2325
|
+
const existing = nodes.get(address) ?? {
|
|
2326
|
+
id: address,
|
|
2327
|
+
address,
|
|
2328
|
+
node_type: "address",
|
|
2329
|
+
labels: [],
|
|
2330
|
+
roles: []
|
|
2331
|
+
};
|
|
2332
|
+
const roles = Array.isArray(existing["roles"]) ? existing["roles"].map(String) : [];
|
|
2333
|
+
nodes.set(address, {
|
|
2334
|
+
...existing,
|
|
2335
|
+
roles: [...new Set([...roles, role])]
|
|
2336
|
+
});
|
|
2337
|
+
};
|
|
2338
|
+
ensureNode(subject.address, "subject");
|
|
2339
|
+
const edges = rows.map((row) => {
|
|
2340
|
+
ensureNode(row.coldkey, "coldkey");
|
|
2341
|
+
ensureNode(row.hotkey, "hotkey");
|
|
2342
|
+
return {
|
|
2343
|
+
source: row.coldkey,
|
|
2344
|
+
target: row.hotkey,
|
|
2345
|
+
edge_type: "stakes_in",
|
|
2346
|
+
amount: row.amount ?? row.net_stake_change ?? 0,
|
|
2347
|
+
netuid: row.netuid,
|
|
2348
|
+
source_backend: row.source_backend,
|
|
2349
|
+
topology_graph: row.topology_graph,
|
|
2350
|
+
first_activity_timestamp: row.first_activity_timestamp,
|
|
2351
|
+
last_activity_timestamp: row.last_activity_timestamp
|
|
2352
|
+
};
|
|
2353
|
+
});
|
|
2354
|
+
return normalizeGraphPayload({
|
|
2355
|
+
schema: "chain-insights.graph.v1",
|
|
2356
|
+
nodes: [...nodes.values()],
|
|
2357
|
+
edges,
|
|
2358
|
+
flows: [],
|
|
2359
|
+
edge_anchors: [],
|
|
2360
|
+
metadata: {
|
|
2361
|
+
network,
|
|
2362
|
+
subject_address: subject.address,
|
|
2363
|
+
subject_role: subject.role,
|
|
2364
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2365
|
+
}
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
function summaryLines(network, subject, rows, totals, failures) {
|
|
2369
|
+
const lines = [
|
|
2370
|
+
`Stake insights for ${network}:${subject.address}`,
|
|
2371
|
+
"",
|
|
2372
|
+
`Subject role: ${subject.role}`,
|
|
2373
|
+
`Relationships: ${rows.length}`,
|
|
2374
|
+
`Net staked: ${totals["net_staked"] ?? 0} TAO`,
|
|
2375
|
+
`Total staked: ${totals["total_staked"] ?? 0} TAO`,
|
|
2376
|
+
`Total unstaked: ${totals["total_unstaked"] ?? 0} TAO`,
|
|
2377
|
+
`First activity: ${totals["first_activity_timestamp"] ?? "unknown"}`,
|
|
2378
|
+
`Last activity: ${totals["last_activity_timestamp"] ?? "unknown"}`
|
|
2379
|
+
];
|
|
2380
|
+
if (rows.length > 0) {
|
|
2381
|
+
lines.push("", "Top staking relationships");
|
|
2382
|
+
for (const row of rows.slice(0, 10)) lines.push(`- ${row.coldkey} -> ${row.hotkey} netuid ${row.netuid ?? "unknown"} amount ${row.amount ?? row.net_stake_change ?? "unknown"} (${row.source_backend})`);
|
|
2383
|
+
} else lines.push("", "No stake relationships matched the requested filters.");
|
|
2384
|
+
if (failures.length > 0) lines.push("", "Partial query failures", failures.map((failure) => `- ${failure.id}: ${failure.error}`).join("\n"));
|
|
2385
|
+
return lines.join("\n");
|
|
2386
|
+
}
|
|
2387
|
+
async function stakeInsights(remoteClient, options) {
|
|
2388
|
+
const { network, subject, depth } = validateOptions(options);
|
|
2389
|
+
const { live, archive, failures, evidence } = collectRelationships(await callGraphBatch$1(remoteClient, network, [stakeRelationshipQuery("live_topology", subject, options, depth), stakeRelationshipQuery("archive_topology", subject, options, depth)]));
|
|
2390
|
+
if (live.length === 0 && archive.length === 0 && failures.length > 0) throw new Error(`Stake insights unavailable: ${failures.map((failure) => `${failure.id}: ${failure.error}`).join("; ")}`);
|
|
2391
|
+
const rows = live.length > 0 ? live : archive;
|
|
2392
|
+
const totals = stakeTotals(rows);
|
|
2393
|
+
const facts = {
|
|
2394
|
+
subject: {
|
|
2395
|
+
network,
|
|
2396
|
+
address: subject.address,
|
|
2397
|
+
role: subject.role,
|
|
2398
|
+
netuid: options.netuid,
|
|
2399
|
+
start_timestamp_ms: options.startTimestampMs,
|
|
2400
|
+
end_timestamp_ms: options.endTimestampMs,
|
|
2401
|
+
depth
|
|
2402
|
+
},
|
|
2403
|
+
backend_used: [...new Set(rows.map((row) => row.source_backend).filter(Boolean))],
|
|
2404
|
+
primary_topology_graph: live.length > 0 ? "live_topology" : "archive_topology",
|
|
2405
|
+
stake_totals: totals,
|
|
2406
|
+
active_relationships: rows,
|
|
2407
|
+
stake_movements: movementRows(rows),
|
|
2408
|
+
top_counterparties: topCounterparties(subject, rows),
|
|
2409
|
+
query_evidence: evidence,
|
|
2410
|
+
partial_query_errors: failures.length > 0 ? failures : void 0
|
|
2411
|
+
};
|
|
2412
|
+
return {
|
|
2413
|
+
summaryText: summaryLines(network, subject, rows, totals, failures),
|
|
2414
|
+
structuredContent: {
|
|
2415
|
+
schema: "chain-insights.result.v1",
|
|
2416
|
+
tool: "stake_insights",
|
|
2417
|
+
facts,
|
|
2418
|
+
hint: rows.length > 0 ? "Review active_relationships and stake_movements before treating stake behavior as generic money flow." : "No matching stake relationships were found; confirm the address role, netuid, and time window."
|
|
2419
|
+
},
|
|
2420
|
+
graphData: graphData(rows, subject, network)
|
|
2421
|
+
};
|
|
2422
|
+
}
|
|
2423
|
+
//#endregion
|
|
2029
2424
|
//#region src/investigation/public-tools.ts
|
|
2030
2425
|
const GRAPH_QUERY_BATCH_TIMEOUT_SECONDS = 120;
|
|
2031
2426
|
const GRAPH_QUERY_BATCH_REQUEST_TIMEOUT_MS = 300 * 1e3;
|
|
@@ -2549,6 +2944,6 @@ async function trackFunds(remoteClient, config, options) {
|
|
|
2549
2944
|
};
|
|
2550
2945
|
}
|
|
2551
2946
|
//#endregion
|
|
2552
|
-
export { addressRisk, scamTopology, trackFunds };
|
|
2947
|
+
export { addressRisk, scamTopology, stakeInsights, trackFunds };
|
|
2553
2948
|
|
|
2554
|
-
//# sourceMappingURL=public-tools-
|
|
2949
|
+
//# sourceMappingURL=public-tools-B13J0MJZ.mjs.map
|