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