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
@@ -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$1(value, fallback, min, max) {
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$2(value) {
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$2(result) {
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$2(result) {
101
- const text = textFromToolResult$2(result).trim();
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$2(remoteClient, network, queries) {
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$2(result) || "graph_query_batch failed");
128
- return parseGraphBatchResult$2(result);
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$2(remoteClient, network, SCHEMA_QUERY_SET));
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$2(address)}"})${relationshipChain}`,
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$2(depositAddress)}"})`,
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$2(address)}"`).join(" OR ")}) AND sender.is_exchange IS NULL AND sender.address <> deposit.address`,
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$2(pair.src)}" AND b.address = "${escapeCypherString$2(pair.dst)}")`).join(" OR ")})`,
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$2(value) {
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$2(terminalEdge["amount_sum"]) ?? numberValue$2(terminalEdge["amount_usd_sum"]);
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$2(terminalEdge["amount_sum"]),
332
- amount_usd_sum: numberValue$2(terminalEdge["amount_usd_sum"]),
333
- hops: numberValue$2(row["hops"]) ?? pathAddresses.length - 1,
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$2(edge["amount_sum"]) ?? numberValue$2(edge["amount_usd_sum"]) ?? 0;
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$2(edge["amount_usd_sum"]),
364
- tx_count: numberValue$2(edge["tx_count"]),
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$2(remoteClient, network, [query]);
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$2(props["amount_sum"]) ?? flow.amount_sum;
395
- flow.amount_usd_sum = numberValue$2(props["amount_usd_sum"]);
396
- flow.tx_count = numberValue$2(props["tx_count"]);
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$2(props["amount_sum"]);
404
- deposit.amount_usd_sum = numberValue$2(props["amount_usd_sum"]);
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$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) => {
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$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)));
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$2(row["hops"]) ?? Math.max(pathAddresses.length - 1, 0),
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$2(remoteClient, options.network, [reverseLeadsQuery(uniqueDepositAddresses)]);
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$2(row["degree_in"]) ?? 0;
450
- const degreeOut = numberValue$2(row["degree_out"]) ?? 0;
451
- const totalVolume = numberValue$2(row["total_volume_usd"]) ?? 0;
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$2(row["amount_usd"]),
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$1(options.maxHops, 3, 1, 5);
823
- const perAddressLimit = clampInt$1(options.perAddressLimit, 5, 1, 10);
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$1(value) {
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$1(value) {
971
+ function escapeCypherString$2(value) {
972
972
  return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
973
973
  }
974
- function textFromToolResult$1(result) {
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$1(result) {
978
- const text = textFromToolResult$1(result).trim();
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$1(remoteClient, network, queries) {
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$1(result) || "graph_query_batch failed");
997
- return parseGraphBatchResult$1(result);
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$1(sourceAddress)}"})-[r:FLOWS_TO]->(dst:Address)`,
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$1(depositAddress)}"})`,
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$1(row["amount_sum"]),
1104
- amount_usd_sum: numberValue$1(row["amount_usd_sum"]),
1105
- tx_count: numberValue$1(row["tx_count"]),
1106
- first_seen_timestamp: numberValue$1(row["first_seen_timestamp"]),
1107
- last_seen_timestamp: numberValue$1(row["last_seen_timestamp"]),
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$1(remoteClient, network, queryChunk);
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$1(remoteClient, network, queryChunk);
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$1(record["confidence"]);
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$1(record["source_incident_timestamp_ms"]) ?? 0
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$1(node[key]) ?? 0) + amount;
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-D6Q5MTcO.mjs.map
2949
+ //# sourceMappingURL=public-tools-B13J0MJZ.mjs.map