chain-insights 0.2.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +165 -0
- package/bin/cli.js +10 -0
- package/bin/install.cjs +252 -0
- package/bin/mcp-proxy.cjs +10 -0
- package/dist/active-BSrxLKwn.mjs +50 -0
- package/dist/active-BSrxLKwn.mjs.map +1 -0
- package/dist/active-Dv7Tu-O4.cjs +68 -0
- package/dist/app-BjjuQM0B.mjs +155 -0
- package/dist/app-BjjuQM0B.mjs.map +1 -0
- package/dist/app-Dq1TdB6p.cjs +161 -0
- package/dist/artifact-server-DoxJ7fCx.cjs +47 -0
- package/dist/artifact-server-Dxz5YbuQ.mjs +48 -0
- package/dist/artifact-server-Dxz5YbuQ.mjs.map +1 -0
- package/dist/assets/bg-pattern.png +0 -0
- package/dist/assets/logo.png +0 -0
- package/dist/call-args-DQA2QcRA.cjs +27 -0
- package/dist/call-args-Lk_wOJxd.mjs +29 -0
- package/dist/call-args-Lk_wOJxd.mjs.map +1 -0
- package/dist/capabilities-CB97WMA5.cjs +83 -0
- package/dist/capabilities-DliMBim-.mjs +84 -0
- package/dist/capabilities-DliMBim-.mjs.map +1 -0
- package/dist/cases-By7INiOa.mjs +6 -0
- package/dist/cases-CDcNU91B.cjs +9 -0
- package/dist/chunk-CZWwpsFl.cjs +43 -0
- package/dist/cli.cjs +752 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +753 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/client-D4Bq0rp9.mjs +111 -0
- package/dist/client-D4Bq0rp9.mjs.map +1 -0
- package/dist/client-D4fZgIaO.cjs +132 -0
- package/dist/config-Bmdl5hdk.cjs +67 -0
- package/dist/config-BwrBYmiC.mjs +44 -0
- package/dist/config-BwrBYmiC.mjs.map +1 -0
- package/dist/data-extractor-BNGj7ECT.cjs +347 -0
- package/dist/data-extractor-DFzsa5CS.mjs +336 -0
- package/dist/data-extractor-DFzsa5CS.mjs.map +1 -0
- package/dist/dossier-BsroDgD3.mjs +76 -0
- package/dist/dossier-BsroDgD3.mjs.map +1 -0
- package/dist/dossier-DtxREpPm.cjs +76 -0
- package/dist/evidence-BGcdKxuV.cjs +200 -0
- package/dist/evidence-BhvFW-y_.mjs +195 -0
- package/dist/evidence-BhvFW-y_.mjs.map +1 -0
- package/dist/format-Ce1RObVl.mjs +22 -0
- package/dist/format-Ce1RObVl.mjs.map +1 -0
- package/dist/format-DOrPvXEr.cjs +20 -0
- package/dist/frontmatter-D8wWCeOa.mjs +26 -0
- package/dist/frontmatter-D8wWCeOa.mjs.map +1 -0
- package/dist/frontmatter-DgAuai7E.cjs +35 -0
- package/dist/graph-normalizer-Cv9yK9Pg.mjs +130 -0
- package/dist/graph-normalizer-Cv9yK9Pg.mjs.map +1 -0
- package/dist/graph-normalizer-DeIj6Ses.cjs +133 -0
- package/dist/graph-reports-C4TBjCkM.mjs +63 -0
- package/dist/graph-reports-C4TBjCkM.mjs.map +1 -0
- package/dist/graph-reports-DU05YCei.cjs +64 -0
- package/dist/html-generator-CAv81IWH.cjs +85 -0
- package/dist/html-generator-V6Bp0uRb.mjs +68 -0
- package/dist/html-generator-V6Bp0uRb.mjs.map +1 -0
- package/dist/index.cjs +31 -0
- package/dist/index.d.cts +187 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +187 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +9 -0
- package/dist/init-BjuFt54X.cjs +232 -0
- package/dist/init-CaOsHTIo.mjs +232 -0
- package/dist/init-CaOsHTIo.mjs.map +1 -0
- package/dist/mcp-proxy.cjs +1257 -0
- package/dist/mcp-proxy.d.cts +12 -0
- package/dist/mcp-proxy.d.cts.map +1 -0
- package/dist/mcp-proxy.d.mts +12 -0
- package/dist/mcp-proxy.d.mts.map +1 -0
- package/dist/mcp-proxy.mjs +1255 -0
- package/dist/mcp-proxy.mjs.map +1 -0
- package/dist/output-root-CFYms3ad.cjs +43 -0
- package/dist/output-root-CmWM7aV2.mjs +33 -0
- package/dist/output-root-CmWM7aV2.mjs.map +1 -0
- package/dist/parser-BUIWW1OH.cjs +182 -0
- package/dist/parser-DO0_SssG.mjs +182 -0
- package/dist/parser-DO0_SssG.mjs.map +1 -0
- package/dist/public-tools-D4UI-Zb0.mjs +2554 -0
- package/dist/public-tools-D4UI-Zb0.mjs.map +1 -0
- package/dist/public-tools-XSpkz2ky.cjs +2556 -0
- package/dist/resolver-C2ZS7oC8.mjs +201 -0
- package/dist/resolver-C2ZS7oC8.mjs.map +1 -0
- package/dist/resolver-zYbu4wDV.cjs +203 -0
- package/dist/rolldown-runtime-wcPFST8Q.mjs +13 -0
- package/dist/runner-1Eq55OYb.cjs +148 -0
- package/dist/runner-BhUHbiHG.mjs +149 -0
- package/dist/runner-BhUHbiHG.mjs.map +1 -0
- package/dist/schema-4XpzDFQM.cjs +55 -0
- package/dist/schema-8d0rVIdZ.mjs +37 -0
- package/dist/schema-8d0rVIdZ.mjs.map +1 -0
- package/dist/schema-cache-9CksD7tX.mjs +34 -0
- package/dist/schema-cache-9CksD7tX.mjs.map +1 -0
- package/dist/schema-cache-CgWRCN2N.cjs +36 -0
- package/dist/selector-CkFcTXzz.cjs +10 -0
- package/dist/selector-xjm6NTHI.mjs +12 -0
- package/dist/selector-xjm6NTHI.mjs.map +1 -0
- package/dist/server-BkM5xrXb.mjs +45 -0
- package/dist/server-BkM5xrXb.mjs.map +1 -0
- package/dist/server-DXowbpfi.cjs +54 -0
- package/dist/session-BpNylyuJ.cjs +115 -0
- package/dist/session-CcTgYxsj.mjs +115 -0
- package/dist/session-CcTgYxsj.mjs.map +1 -0
- package/dist/setup-DOpKPrlx.cjs +81 -0
- package/dist/setup-DyrWHuwQ.mjs +80 -0
- package/dist/setup-DyrWHuwQ.mjs.map +1 -0
- package/dist/store-BiUhQOIf.cjs +230 -0
- package/dist/store-BoWE-Gtl.mjs +225 -0
- package/dist/store-BoWE-Gtl.mjs.map +1 -0
- package/dist/templates/graph.html +1406 -0
- package/dist/tool-visibility-3Z_KvO9Q.mjs +28 -0
- package/dist/tool-visibility-3Z_KvO9Q.mjs.map +1 -0
- package/dist/tool-visibility-CwgY205r.cjs +36 -0
- package/dist/tools-Cp2jAAAb.mjs +100 -0
- package/dist/tools-Cp2jAAAb.mjs.map +1 -0
- package/dist/tools-f_vJUZAF.cjs +139 -0
- package/dist/topup-server-BZuQifvh.cjs +940 -0
- package/dist/topup-server-DUjyFftI.mjs +919 -0
- package/dist/topup-server-DUjyFftI.mjs.map +1 -0
- package/dist/version-1gP19Lhi.mjs +8 -0
- package/dist/version-1gP19Lhi.mjs.map +1 -0
- package/dist/version-BNGtdpmH.cjs +18 -0
- package/dist/viz-BlCJe6Tk.mjs +35 -0
- package/dist/viz-BlCJe6Tk.mjs.map +1 -0
- package/dist/viz-ClezVXrJ.cjs +44 -0
- package/dist/wallet-BMelXBYP.mjs +104 -0
- package/dist/wallet-BMelXBYP.mjs.map +1 -0
- package/dist/wallet-RnvvSpV2.cjs +146 -0
- package/docs/architecture.md +145 -0
- package/docs/contributing.md +68 -0
- package/docs/debugging.md +68 -0
- package/docs/development.md +44 -0
- package/docs/graph-tools.md +251 -0
- package/docs/images/graph-mcp-iframe.png +0 -0
- package/docs/images/graph-visualization.png +0 -0
- package/docs/images/topup-page.png +0 -0
- package/docs/investigation-workspaces.md +151 -0
- package/docs/mcp-proxy.md +180 -0
- package/package.json +59 -0
- package/skills/chain-insights-developer-experience/SKILL.md +101 -0
- package/skills/chain-insights-investigation/SKILL.md +285 -0
- package/skills/chain-insights-investigation/agents/openai.yaml +4 -0
- package/skills/chain-insights-investigation/scripts/run-target-uat.sh +197 -0
- package/skills/chain-insights-trace-funds/SKILL.md +249 -0
- package/skills/ci-case/SKILL.md +43 -0
- package/skills/ci-status/SKILL.md +45 -0
- package/skills/test-chain-insights-graphrag-mcp/SKILL.md +75 -0
- package/skills/test-chain-insights-graphrag-mcp/agents/openai.yaml +4 -0
- package/skills/test-chain-insights-graphrag-mcp/scripts/run-uat.sh +414 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
const require_chunk = require("./chunk-CZWwpsFl.cjs");
|
|
2
|
+
const require_frontmatter = require("./frontmatter-DgAuai7E.cjs");
|
|
3
|
+
const require_active = require("./active-Dv7Tu-O4.cjs");
|
|
4
|
+
let node_path = require("node:path");
|
|
5
|
+
node_path = require_chunk.__toESM(node_path, 1);
|
|
6
|
+
let node_fs_promises = require("node:fs/promises");
|
|
7
|
+
let zod = require("zod");
|
|
8
|
+
zod = require_chunk.__toESM(zod, 1);
|
|
9
|
+
//#region src/viz/graph-model.ts
|
|
10
|
+
const EntityType = zod.enum([
|
|
11
|
+
"eoa",
|
|
12
|
+
"contract",
|
|
13
|
+
"exchange",
|
|
14
|
+
"mixer",
|
|
15
|
+
"unknown"
|
|
16
|
+
]);
|
|
17
|
+
const RiskLevel = zod.enum([
|
|
18
|
+
"low",
|
|
19
|
+
"medium",
|
|
20
|
+
"high",
|
|
21
|
+
"critical",
|
|
22
|
+
"unknown"
|
|
23
|
+
]);
|
|
24
|
+
const GraphNode = zod.object({
|
|
25
|
+
id: zod.string().min(1),
|
|
26
|
+
label: zod.string().optional(),
|
|
27
|
+
entityType: EntityType.default("unknown"),
|
|
28
|
+
riskLevel: RiskLevel.default("unknown"),
|
|
29
|
+
totalIn: zod.number().default(0),
|
|
30
|
+
totalOut: zod.number().default(0),
|
|
31
|
+
txCount: zod.number().int().default(0),
|
|
32
|
+
firstSeen: zod.string().optional(),
|
|
33
|
+
lastSeen: zod.string().optional()
|
|
34
|
+
});
|
|
35
|
+
const GraphEdge = zod.object({
|
|
36
|
+
source: zod.string().min(1),
|
|
37
|
+
target: zod.string().min(1),
|
|
38
|
+
value: zod.number(),
|
|
39
|
+
txHash: zod.string().optional(),
|
|
40
|
+
blockNumber: zod.number().int().optional(),
|
|
41
|
+
timestamp: zod.string().optional()
|
|
42
|
+
});
|
|
43
|
+
const GraphData = zod.object({
|
|
44
|
+
nodes: zod.array(GraphNode),
|
|
45
|
+
edges: zod.array(GraphEdge),
|
|
46
|
+
metadata: zod.object({
|
|
47
|
+
caseId: zod.string().optional(),
|
|
48
|
+
title: zod.string().default("Money Flow"),
|
|
49
|
+
generatedAt: zod.string(),
|
|
50
|
+
truncated: zod.boolean().default(false),
|
|
51
|
+
totalNodes: zod.number().int().optional(),
|
|
52
|
+
hiddenNodes: zod.number().int().optional()
|
|
53
|
+
})
|
|
54
|
+
});
|
|
55
|
+
const MAX_NODES = 100;
|
|
56
|
+
function truncateGraph(data) {
|
|
57
|
+
if (data.nodes.length <= MAX_NODES) return data;
|
|
58
|
+
const kept = [...data.nodes].sort((a, b) => b.totalIn + b.totalOut - (a.totalIn + a.totalOut)).slice(0, MAX_NODES);
|
|
59
|
+
const keptIds = new Set(kept.map((n) => n.id));
|
|
60
|
+
return {
|
|
61
|
+
nodes: kept,
|
|
62
|
+
edges: data.edges.filter((e) => keptIds.has(e.source) && keptIds.has(e.target)),
|
|
63
|
+
metadata: {
|
|
64
|
+
...data.metadata,
|
|
65
|
+
truncated: true,
|
|
66
|
+
totalNodes: data.nodes.length,
|
|
67
|
+
hiddenNodes: data.nodes.length - MAX_NODES
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/viz/data-extractor.ts
|
|
73
|
+
var data_extractor_exports = /* @__PURE__ */ require_chunk.__exportAll({
|
|
74
|
+
extractGraphFromCase: () => extractGraphFromCase,
|
|
75
|
+
extractGraphFromJson: () => extractGraphFromJson,
|
|
76
|
+
parseEvidenceJson: () => parseEvidenceJson
|
|
77
|
+
});
|
|
78
|
+
function caseDir(caseId) {
|
|
79
|
+
if (/[/\\]|^\.\.?$/.test(caseId)) throw new Error(`Invalid case ID: ${caseId}`);
|
|
80
|
+
return node_path.default.join(require_active.activeCasesRoot(), caseId);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Extracts all items from ```json code blocks in a markdown string.
|
|
84
|
+
* If the parsed value is an array, spreads all items.
|
|
85
|
+
* If the parsed value has 'nodes' and 'edges', wraps it as a single item.
|
|
86
|
+
* Returns empty array if no JSON blocks found or parsing fails.
|
|
87
|
+
*/
|
|
88
|
+
function parseEvidenceJson(markdown) {
|
|
89
|
+
const results = [];
|
|
90
|
+
const re = /```json\s*\n([\s\S]*?)```/g;
|
|
91
|
+
let match;
|
|
92
|
+
while ((match = re.exec(markdown)) !== null) {
|
|
93
|
+
const raw = match[1];
|
|
94
|
+
if (!raw) continue;
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(raw);
|
|
97
|
+
if (Array.isArray(parsed)) results.push(...parsed);
|
|
98
|
+
else if (parsed !== null && typeof parsed === "object" && "nodes" in parsed && "edges" in parsed) results.push(parsed);
|
|
99
|
+
} catch {}
|
|
100
|
+
}
|
|
101
|
+
if (results.length > 0) return results;
|
|
102
|
+
const rawJson = extractEmbeddedJson(markdown);
|
|
103
|
+
if (rawJson) try {
|
|
104
|
+
const parsed = JSON.parse(rawJson);
|
|
105
|
+
if (Array.isArray(parsed)) return parsed;
|
|
106
|
+
if (parsed !== null && typeof parsed === "object") return [parsed];
|
|
107
|
+
} catch {}
|
|
108
|
+
return results;
|
|
109
|
+
}
|
|
110
|
+
function extractEmbeddedJson(text) {
|
|
111
|
+
const trimmed = text.trim();
|
|
112
|
+
const start = [...trimmed].map((char, index) => char === "{" || char === "[" ? index : -1).find((index) => index >= 0);
|
|
113
|
+
if (start === void 0) return null;
|
|
114
|
+
return trimmed.slice(start);
|
|
115
|
+
}
|
|
116
|
+
function isSimpleTx(item) {
|
|
117
|
+
return item !== null && typeof item === "object" && "from" in item && "to" in item && "value" in item;
|
|
118
|
+
}
|
|
119
|
+
function isGraphDataLike(input) {
|
|
120
|
+
return input !== null && typeof input === "object" && Array.isArray(input["nodes"]) && Array.isArray(input["edges"]);
|
|
121
|
+
}
|
|
122
|
+
function compactEvidenceToSimpleTxs(item) {
|
|
123
|
+
const compact = item;
|
|
124
|
+
if (!compact || typeof compact !== "object" || compact.schema !== "chain-insights.compact_evidence.v1" || !Array.isArray(compact.outgoing_flows)) return [];
|
|
125
|
+
return compact.outgoing_flows.filter((flow) => typeof flow.src === "string" && typeof flow.dst === "string" && typeof flow.amount_sum === "number").map((flow) => ({
|
|
126
|
+
from: flow.src,
|
|
127
|
+
to: flow.dst,
|
|
128
|
+
value: flow.amount_sum,
|
|
129
|
+
txHash: flow.first_tx_id
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Converts simple [{from, to, value}] transaction arrays into graph nodes.
|
|
134
|
+
* Computes totalIn, totalOut, txCount per node from edges.
|
|
135
|
+
*/
|
|
136
|
+
function buildGraphFromSimpleTxs(items) {
|
|
137
|
+
const edges = items.map((tx) => GraphEdge.parse({
|
|
138
|
+
source: tx.from,
|
|
139
|
+
target: tx.to,
|
|
140
|
+
value: tx.value,
|
|
141
|
+
txHash: tx.txHash,
|
|
142
|
+
blockNumber: tx.blockNumber,
|
|
143
|
+
timestamp: tx.timestamp
|
|
144
|
+
}));
|
|
145
|
+
const addresses = /* @__PURE__ */ new Set();
|
|
146
|
+
for (const tx of items) {
|
|
147
|
+
addresses.add(tx.from);
|
|
148
|
+
addresses.add(tx.to);
|
|
149
|
+
}
|
|
150
|
+
const totals = {};
|
|
151
|
+
for (const addr of addresses) totals[addr] = {
|
|
152
|
+
totalIn: 0,
|
|
153
|
+
totalOut: 0,
|
|
154
|
+
txCount: 0
|
|
155
|
+
};
|
|
156
|
+
for (const tx of items) {
|
|
157
|
+
const out = totals[tx.from];
|
|
158
|
+
const inp = totals[tx.to];
|
|
159
|
+
if (out) {
|
|
160
|
+
out.totalOut += tx.value;
|
|
161
|
+
out.txCount += 1;
|
|
162
|
+
}
|
|
163
|
+
if (inp) {
|
|
164
|
+
inp.totalIn += tx.value;
|
|
165
|
+
inp.txCount += 1;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
nodes: [...addresses].map((addr) => GraphNode.parse({
|
|
170
|
+
id: addr,
|
|
171
|
+
entityType: "unknown",
|
|
172
|
+
riskLevel: "unknown",
|
|
173
|
+
totalIn: totals[addr]?.totalIn ?? 0,
|
|
174
|
+
totalOut: totals[addr]?.totalOut ?? 0,
|
|
175
|
+
txCount: totals[addr]?.txCount ?? 0
|
|
176
|
+
})),
|
|
177
|
+
edges
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Handles two input formats:
|
|
182
|
+
* 1. Full GraphData object (has nodes + edges) — parse with Zod
|
|
183
|
+
* 2. Array of {from, to, value, ...} transaction objects — auto-derive nodes
|
|
184
|
+
*
|
|
185
|
+
* Throws "Invalid transaction data" for any other input.
|
|
186
|
+
*/
|
|
187
|
+
function extractGraphFromJson(input) {
|
|
188
|
+
if (isGraphDataLike(input)) return GraphData.parse(input);
|
|
189
|
+
if (Array.isArray(input)) {
|
|
190
|
+
const simpleTxs = [];
|
|
191
|
+
for (const item of input) if (isSimpleTx(item)) simpleTxs.push(item);
|
|
192
|
+
else simpleTxs.push(...compactEvidenceToSimpleTxs(item));
|
|
193
|
+
const { nodes, edges } = buildGraphFromSimpleTxs(simpleTxs);
|
|
194
|
+
return GraphData.parse({
|
|
195
|
+
nodes,
|
|
196
|
+
edges,
|
|
197
|
+
metadata: {
|
|
198
|
+
title: "Money Flow",
|
|
199
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
throw new Error("Invalid transaction data. The input file must contain a JSON array of transaction objects with `from`, `to`, and `value` fields.");
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Merges two sets of nodes, deduplicating by id.
|
|
207
|
+
* For duplicate nodes, sums totalIn, totalOut, txCount; keeps earliest firstSeen, latest lastSeen.
|
|
208
|
+
*/
|
|
209
|
+
function mergeNodes(existing, incoming) {
|
|
210
|
+
const map = /* @__PURE__ */ new Map();
|
|
211
|
+
for (const node of existing) map.set(node.id, { ...node });
|
|
212
|
+
for (const node of incoming) {
|
|
213
|
+
const prev = map.get(node.id);
|
|
214
|
+
if (prev) map.set(node.id, {
|
|
215
|
+
...prev,
|
|
216
|
+
totalIn: prev.totalIn + node.totalIn,
|
|
217
|
+
totalOut: prev.totalOut + node.totalOut,
|
|
218
|
+
txCount: prev.txCount + node.txCount,
|
|
219
|
+
firstSeen: pickEarlier(prev.firstSeen, node.firstSeen),
|
|
220
|
+
lastSeen: pickLater(prev.lastSeen, node.lastSeen)
|
|
221
|
+
});
|
|
222
|
+
else map.set(node.id, { ...node });
|
|
223
|
+
}
|
|
224
|
+
return [...map.values()];
|
|
225
|
+
}
|
|
226
|
+
function pickEarlier(a, b) {
|
|
227
|
+
if (!a) return b;
|
|
228
|
+
if (!b) return a;
|
|
229
|
+
return a < b ? a : b;
|
|
230
|
+
}
|
|
231
|
+
function pickLater(a, b) {
|
|
232
|
+
if (!a) return b;
|
|
233
|
+
if (!b) return a;
|
|
234
|
+
return a > b ? a : b;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Aggregates edges: for duplicate (source, target) pairs, sums value; keeps last txHash/timestamp.
|
|
238
|
+
*/
|
|
239
|
+
function aggregateEdges(edges) {
|
|
240
|
+
const map = /* @__PURE__ */ new Map();
|
|
241
|
+
for (const edge of edges) {
|
|
242
|
+
const key = `${edge.source}::${edge.target}`;
|
|
243
|
+
const prev = map.get(key);
|
|
244
|
+
if (prev) map.set(key, {
|
|
245
|
+
...edge,
|
|
246
|
+
value: prev.value + edge.value
|
|
247
|
+
});
|
|
248
|
+
else map.set(key, { ...edge });
|
|
249
|
+
}
|
|
250
|
+
return [...map.values()];
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Reads evidence files from a case directory, extracts JSON transaction data
|
|
254
|
+
* from markdown code blocks, enriches nodes with entity types from dossiers,
|
|
255
|
+
* and returns a merged GraphData.
|
|
256
|
+
*/
|
|
257
|
+
async function extractGraphFromCase(caseId) {
|
|
258
|
+
const evidenceDir = node_path.default.join(caseDir(caseId), "evidence");
|
|
259
|
+
let files = [];
|
|
260
|
+
try {
|
|
261
|
+
files = (await (0, node_fs_promises.readdir)(evidenceDir)).filter((f) => f.endsWith(".md"));
|
|
262
|
+
} catch {
|
|
263
|
+
return GraphData.parse({
|
|
264
|
+
nodes: [],
|
|
265
|
+
edges: [],
|
|
266
|
+
metadata: {
|
|
267
|
+
caseId,
|
|
268
|
+
title: `${caseId} - Money Flow`,
|
|
269
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
let allNodes = [];
|
|
274
|
+
let allEdges = [];
|
|
275
|
+
for (const file of files) {
|
|
276
|
+
const { body } = require_frontmatter.parseFrontmatter(await (0, node_fs_promises.readFile)(node_path.default.join(evidenceDir, file), "utf-8"));
|
|
277
|
+
const items = parseEvidenceJson(body);
|
|
278
|
+
if (items.length === 0) continue;
|
|
279
|
+
const graphDataItems = items.filter((item) => isGraphDataLike(item));
|
|
280
|
+
const simpleTxItems = items.flatMap((item) => {
|
|
281
|
+
if (isSimpleTx(item)) return [item];
|
|
282
|
+
return compactEvidenceToSimpleTxs(item);
|
|
283
|
+
});
|
|
284
|
+
if (graphDataItems.length > 0) {
|
|
285
|
+
for (const gd of graphDataItems) try {
|
|
286
|
+
const parsed = GraphData.parse(gd);
|
|
287
|
+
allNodes = mergeNodes(allNodes, parsed.nodes);
|
|
288
|
+
allEdges = [...allEdges, ...parsed.edges];
|
|
289
|
+
} catch {}
|
|
290
|
+
if (simpleTxItems.length > 0) {
|
|
291
|
+
const { nodes, edges } = buildGraphFromSimpleTxs(simpleTxItems);
|
|
292
|
+
allNodes = mergeNodes(allNodes, nodes);
|
|
293
|
+
allEdges = [...allEdges, ...edges];
|
|
294
|
+
}
|
|
295
|
+
} else if (simpleTxItems.length > 0) {
|
|
296
|
+
const { nodes, edges } = buildGraphFromSimpleTxs(simpleTxItems);
|
|
297
|
+
allNodes = mergeNodes(allNodes, nodes);
|
|
298
|
+
allEdges = [...allEdges, ...edges];
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
allEdges = aggregateEdges(allEdges);
|
|
302
|
+
try {
|
|
303
|
+
const { DossierStore } = await Promise.resolve().then(() => require("./cases-CDcNU91B.cjs"));
|
|
304
|
+
const dossiers = await DossierStore.listSummaries(caseId);
|
|
305
|
+
const dossierMap = /* @__PURE__ */ new Map();
|
|
306
|
+
for (const d of dossiers) dossierMap.set(d.address, d.type);
|
|
307
|
+
allNodes = allNodes.map((node) => {
|
|
308
|
+
const dossierType = dossierMap.get(node.id);
|
|
309
|
+
if (dossierType && dossierType !== "unknown") {
|
|
310
|
+
const entityType = [
|
|
311
|
+
"eoa",
|
|
312
|
+
"contract",
|
|
313
|
+
"exchange",
|
|
314
|
+
"mixer",
|
|
315
|
+
"unknown"
|
|
316
|
+
].includes(dossierType) ? dossierType : "unknown";
|
|
317
|
+
return {
|
|
318
|
+
...node,
|
|
319
|
+
entityType
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
return node;
|
|
323
|
+
});
|
|
324
|
+
} catch {}
|
|
325
|
+
return GraphData.parse({
|
|
326
|
+
nodes: allNodes,
|
|
327
|
+
edges: allEdges,
|
|
328
|
+
metadata: {
|
|
329
|
+
caseId,
|
|
330
|
+
title: `${caseId} - Money Flow`,
|
|
331
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
//#endregion
|
|
336
|
+
Object.defineProperty(exports, "data_extractor_exports", {
|
|
337
|
+
enumerable: true,
|
|
338
|
+
get: function() {
|
|
339
|
+
return data_extractor_exports;
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
Object.defineProperty(exports, "truncateGraph", {
|
|
343
|
+
enumerable: true,
|
|
344
|
+
get: function() {
|
|
345
|
+
return truncateGraph;
|
|
346
|
+
}
|
|
347
|
+
});
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
|
|
2
|
+
import { t as parseFrontmatter } from "./frontmatter-D8wWCeOa.mjs";
|
|
3
|
+
import { t as activeCasesRoot } from "./active-BSrxLKwn.mjs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
6
|
+
import * as z from "zod";
|
|
7
|
+
//#region src/viz/graph-model.ts
|
|
8
|
+
const EntityType = z.enum([
|
|
9
|
+
"eoa",
|
|
10
|
+
"contract",
|
|
11
|
+
"exchange",
|
|
12
|
+
"mixer",
|
|
13
|
+
"unknown"
|
|
14
|
+
]);
|
|
15
|
+
const RiskLevel = z.enum([
|
|
16
|
+
"low",
|
|
17
|
+
"medium",
|
|
18
|
+
"high",
|
|
19
|
+
"critical",
|
|
20
|
+
"unknown"
|
|
21
|
+
]);
|
|
22
|
+
const GraphNode = z.object({
|
|
23
|
+
id: z.string().min(1),
|
|
24
|
+
label: z.string().optional(),
|
|
25
|
+
entityType: EntityType.default("unknown"),
|
|
26
|
+
riskLevel: RiskLevel.default("unknown"),
|
|
27
|
+
totalIn: z.number().default(0),
|
|
28
|
+
totalOut: z.number().default(0),
|
|
29
|
+
txCount: z.number().int().default(0),
|
|
30
|
+
firstSeen: z.string().optional(),
|
|
31
|
+
lastSeen: z.string().optional()
|
|
32
|
+
});
|
|
33
|
+
const GraphEdge = z.object({
|
|
34
|
+
source: z.string().min(1),
|
|
35
|
+
target: z.string().min(1),
|
|
36
|
+
value: z.number(),
|
|
37
|
+
txHash: z.string().optional(),
|
|
38
|
+
blockNumber: z.number().int().optional(),
|
|
39
|
+
timestamp: z.string().optional()
|
|
40
|
+
});
|
|
41
|
+
const GraphData = z.object({
|
|
42
|
+
nodes: z.array(GraphNode),
|
|
43
|
+
edges: z.array(GraphEdge),
|
|
44
|
+
metadata: z.object({
|
|
45
|
+
caseId: z.string().optional(),
|
|
46
|
+
title: z.string().default("Money Flow"),
|
|
47
|
+
generatedAt: z.string(),
|
|
48
|
+
truncated: z.boolean().default(false),
|
|
49
|
+
totalNodes: z.number().int().optional(),
|
|
50
|
+
hiddenNodes: z.number().int().optional()
|
|
51
|
+
})
|
|
52
|
+
});
|
|
53
|
+
const MAX_NODES = 100;
|
|
54
|
+
function truncateGraph(data) {
|
|
55
|
+
if (data.nodes.length <= MAX_NODES) return data;
|
|
56
|
+
const kept = [...data.nodes].sort((a, b) => b.totalIn + b.totalOut - (a.totalIn + a.totalOut)).slice(0, MAX_NODES);
|
|
57
|
+
const keptIds = new Set(kept.map((n) => n.id));
|
|
58
|
+
return {
|
|
59
|
+
nodes: kept,
|
|
60
|
+
edges: data.edges.filter((e) => keptIds.has(e.source) && keptIds.has(e.target)),
|
|
61
|
+
metadata: {
|
|
62
|
+
...data.metadata,
|
|
63
|
+
truncated: true,
|
|
64
|
+
totalNodes: data.nodes.length,
|
|
65
|
+
hiddenNodes: data.nodes.length - MAX_NODES
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/viz/data-extractor.ts
|
|
71
|
+
var data_extractor_exports = /* @__PURE__ */ __exportAll({
|
|
72
|
+
extractGraphFromCase: () => extractGraphFromCase,
|
|
73
|
+
extractGraphFromJson: () => extractGraphFromJson,
|
|
74
|
+
parseEvidenceJson: () => parseEvidenceJson
|
|
75
|
+
});
|
|
76
|
+
function caseDir(caseId) {
|
|
77
|
+
if (/[/\\]|^\.\.?$/.test(caseId)) throw new Error(`Invalid case ID: ${caseId}`);
|
|
78
|
+
return path.join(activeCasesRoot(), caseId);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Extracts all items from ```json code blocks in a markdown string.
|
|
82
|
+
* If the parsed value is an array, spreads all items.
|
|
83
|
+
* If the parsed value has 'nodes' and 'edges', wraps it as a single item.
|
|
84
|
+
* Returns empty array if no JSON blocks found or parsing fails.
|
|
85
|
+
*/
|
|
86
|
+
function parseEvidenceJson(markdown) {
|
|
87
|
+
const results = [];
|
|
88
|
+
const re = /```json\s*\n([\s\S]*?)```/g;
|
|
89
|
+
let match;
|
|
90
|
+
while ((match = re.exec(markdown)) !== null) {
|
|
91
|
+
const raw = match[1];
|
|
92
|
+
if (!raw) continue;
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(raw);
|
|
95
|
+
if (Array.isArray(parsed)) results.push(...parsed);
|
|
96
|
+
else if (parsed !== null && typeof parsed === "object" && "nodes" in parsed && "edges" in parsed) results.push(parsed);
|
|
97
|
+
} catch {}
|
|
98
|
+
}
|
|
99
|
+
if (results.length > 0) return results;
|
|
100
|
+
const rawJson = extractEmbeddedJson(markdown);
|
|
101
|
+
if (rawJson) try {
|
|
102
|
+
const parsed = JSON.parse(rawJson);
|
|
103
|
+
if (Array.isArray(parsed)) return parsed;
|
|
104
|
+
if (parsed !== null && typeof parsed === "object") return [parsed];
|
|
105
|
+
} catch {}
|
|
106
|
+
return results;
|
|
107
|
+
}
|
|
108
|
+
function extractEmbeddedJson(text) {
|
|
109
|
+
const trimmed = text.trim();
|
|
110
|
+
const start = [...trimmed].map((char, index) => char === "{" || char === "[" ? index : -1).find((index) => index >= 0);
|
|
111
|
+
if (start === void 0) return null;
|
|
112
|
+
return trimmed.slice(start);
|
|
113
|
+
}
|
|
114
|
+
function isSimpleTx(item) {
|
|
115
|
+
return item !== null && typeof item === "object" && "from" in item && "to" in item && "value" in item;
|
|
116
|
+
}
|
|
117
|
+
function isGraphDataLike(input) {
|
|
118
|
+
return input !== null && typeof input === "object" && Array.isArray(input["nodes"]) && Array.isArray(input["edges"]);
|
|
119
|
+
}
|
|
120
|
+
function compactEvidenceToSimpleTxs(item) {
|
|
121
|
+
const compact = item;
|
|
122
|
+
if (!compact || typeof compact !== "object" || compact.schema !== "chain-insights.compact_evidence.v1" || !Array.isArray(compact.outgoing_flows)) return [];
|
|
123
|
+
return compact.outgoing_flows.filter((flow) => typeof flow.src === "string" && typeof flow.dst === "string" && typeof flow.amount_sum === "number").map((flow) => ({
|
|
124
|
+
from: flow.src,
|
|
125
|
+
to: flow.dst,
|
|
126
|
+
value: flow.amount_sum,
|
|
127
|
+
txHash: flow.first_tx_id
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Converts simple [{from, to, value}] transaction arrays into graph nodes.
|
|
132
|
+
* Computes totalIn, totalOut, txCount per node from edges.
|
|
133
|
+
*/
|
|
134
|
+
function buildGraphFromSimpleTxs(items) {
|
|
135
|
+
const edges = items.map((tx) => GraphEdge.parse({
|
|
136
|
+
source: tx.from,
|
|
137
|
+
target: tx.to,
|
|
138
|
+
value: tx.value,
|
|
139
|
+
txHash: tx.txHash,
|
|
140
|
+
blockNumber: tx.blockNumber,
|
|
141
|
+
timestamp: tx.timestamp
|
|
142
|
+
}));
|
|
143
|
+
const addresses = /* @__PURE__ */ new Set();
|
|
144
|
+
for (const tx of items) {
|
|
145
|
+
addresses.add(tx.from);
|
|
146
|
+
addresses.add(tx.to);
|
|
147
|
+
}
|
|
148
|
+
const totals = {};
|
|
149
|
+
for (const addr of addresses) totals[addr] = {
|
|
150
|
+
totalIn: 0,
|
|
151
|
+
totalOut: 0,
|
|
152
|
+
txCount: 0
|
|
153
|
+
};
|
|
154
|
+
for (const tx of items) {
|
|
155
|
+
const out = totals[tx.from];
|
|
156
|
+
const inp = totals[tx.to];
|
|
157
|
+
if (out) {
|
|
158
|
+
out.totalOut += tx.value;
|
|
159
|
+
out.txCount += 1;
|
|
160
|
+
}
|
|
161
|
+
if (inp) {
|
|
162
|
+
inp.totalIn += tx.value;
|
|
163
|
+
inp.txCount += 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
nodes: [...addresses].map((addr) => GraphNode.parse({
|
|
168
|
+
id: addr,
|
|
169
|
+
entityType: "unknown",
|
|
170
|
+
riskLevel: "unknown",
|
|
171
|
+
totalIn: totals[addr]?.totalIn ?? 0,
|
|
172
|
+
totalOut: totals[addr]?.totalOut ?? 0,
|
|
173
|
+
txCount: totals[addr]?.txCount ?? 0
|
|
174
|
+
})),
|
|
175
|
+
edges
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Handles two input formats:
|
|
180
|
+
* 1. Full GraphData object (has nodes + edges) — parse with Zod
|
|
181
|
+
* 2. Array of {from, to, value, ...} transaction objects — auto-derive nodes
|
|
182
|
+
*
|
|
183
|
+
* Throws "Invalid transaction data" for any other input.
|
|
184
|
+
*/
|
|
185
|
+
function extractGraphFromJson(input) {
|
|
186
|
+
if (isGraphDataLike(input)) return GraphData.parse(input);
|
|
187
|
+
if (Array.isArray(input)) {
|
|
188
|
+
const simpleTxs = [];
|
|
189
|
+
for (const item of input) if (isSimpleTx(item)) simpleTxs.push(item);
|
|
190
|
+
else simpleTxs.push(...compactEvidenceToSimpleTxs(item));
|
|
191
|
+
const { nodes, edges } = buildGraphFromSimpleTxs(simpleTxs);
|
|
192
|
+
return GraphData.parse({
|
|
193
|
+
nodes,
|
|
194
|
+
edges,
|
|
195
|
+
metadata: {
|
|
196
|
+
title: "Money Flow",
|
|
197
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
throw new Error("Invalid transaction data. The input file must contain a JSON array of transaction objects with `from`, `to`, and `value` fields.");
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Merges two sets of nodes, deduplicating by id.
|
|
205
|
+
* For duplicate nodes, sums totalIn, totalOut, txCount; keeps earliest firstSeen, latest lastSeen.
|
|
206
|
+
*/
|
|
207
|
+
function mergeNodes(existing, incoming) {
|
|
208
|
+
const map = /* @__PURE__ */ new Map();
|
|
209
|
+
for (const node of existing) map.set(node.id, { ...node });
|
|
210
|
+
for (const node of incoming) {
|
|
211
|
+
const prev = map.get(node.id);
|
|
212
|
+
if (prev) map.set(node.id, {
|
|
213
|
+
...prev,
|
|
214
|
+
totalIn: prev.totalIn + node.totalIn,
|
|
215
|
+
totalOut: prev.totalOut + node.totalOut,
|
|
216
|
+
txCount: prev.txCount + node.txCount,
|
|
217
|
+
firstSeen: pickEarlier(prev.firstSeen, node.firstSeen),
|
|
218
|
+
lastSeen: pickLater(prev.lastSeen, node.lastSeen)
|
|
219
|
+
});
|
|
220
|
+
else map.set(node.id, { ...node });
|
|
221
|
+
}
|
|
222
|
+
return [...map.values()];
|
|
223
|
+
}
|
|
224
|
+
function pickEarlier(a, b) {
|
|
225
|
+
if (!a) return b;
|
|
226
|
+
if (!b) return a;
|
|
227
|
+
return a < b ? a : b;
|
|
228
|
+
}
|
|
229
|
+
function pickLater(a, b) {
|
|
230
|
+
if (!a) return b;
|
|
231
|
+
if (!b) return a;
|
|
232
|
+
return a > b ? a : b;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Aggregates edges: for duplicate (source, target) pairs, sums value; keeps last txHash/timestamp.
|
|
236
|
+
*/
|
|
237
|
+
function aggregateEdges(edges) {
|
|
238
|
+
const map = /* @__PURE__ */ new Map();
|
|
239
|
+
for (const edge of edges) {
|
|
240
|
+
const key = `${edge.source}::${edge.target}`;
|
|
241
|
+
const prev = map.get(key);
|
|
242
|
+
if (prev) map.set(key, {
|
|
243
|
+
...edge,
|
|
244
|
+
value: prev.value + edge.value
|
|
245
|
+
});
|
|
246
|
+
else map.set(key, { ...edge });
|
|
247
|
+
}
|
|
248
|
+
return [...map.values()];
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Reads evidence files from a case directory, extracts JSON transaction data
|
|
252
|
+
* from markdown code blocks, enriches nodes with entity types from dossiers,
|
|
253
|
+
* and returns a merged GraphData.
|
|
254
|
+
*/
|
|
255
|
+
async function extractGraphFromCase(caseId) {
|
|
256
|
+
const evidenceDir = path.join(caseDir(caseId), "evidence");
|
|
257
|
+
let files = [];
|
|
258
|
+
try {
|
|
259
|
+
files = (await readdir(evidenceDir)).filter((f) => f.endsWith(".md"));
|
|
260
|
+
} catch {
|
|
261
|
+
return GraphData.parse({
|
|
262
|
+
nodes: [],
|
|
263
|
+
edges: [],
|
|
264
|
+
metadata: {
|
|
265
|
+
caseId,
|
|
266
|
+
title: `${caseId} - Money Flow`,
|
|
267
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
let allNodes = [];
|
|
272
|
+
let allEdges = [];
|
|
273
|
+
for (const file of files) {
|
|
274
|
+
const { body } = parseFrontmatter(await readFile(path.join(evidenceDir, file), "utf-8"));
|
|
275
|
+
const items = parseEvidenceJson(body);
|
|
276
|
+
if (items.length === 0) continue;
|
|
277
|
+
const graphDataItems = items.filter((item) => isGraphDataLike(item));
|
|
278
|
+
const simpleTxItems = items.flatMap((item) => {
|
|
279
|
+
if (isSimpleTx(item)) return [item];
|
|
280
|
+
return compactEvidenceToSimpleTxs(item);
|
|
281
|
+
});
|
|
282
|
+
if (graphDataItems.length > 0) {
|
|
283
|
+
for (const gd of graphDataItems) try {
|
|
284
|
+
const parsed = GraphData.parse(gd);
|
|
285
|
+
allNodes = mergeNodes(allNodes, parsed.nodes);
|
|
286
|
+
allEdges = [...allEdges, ...parsed.edges];
|
|
287
|
+
} catch {}
|
|
288
|
+
if (simpleTxItems.length > 0) {
|
|
289
|
+
const { nodes, edges } = buildGraphFromSimpleTxs(simpleTxItems);
|
|
290
|
+
allNodes = mergeNodes(allNodes, nodes);
|
|
291
|
+
allEdges = [...allEdges, ...edges];
|
|
292
|
+
}
|
|
293
|
+
} else if (simpleTxItems.length > 0) {
|
|
294
|
+
const { nodes, edges } = buildGraphFromSimpleTxs(simpleTxItems);
|
|
295
|
+
allNodes = mergeNodes(allNodes, nodes);
|
|
296
|
+
allEdges = [...allEdges, ...edges];
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
allEdges = aggregateEdges(allEdges);
|
|
300
|
+
try {
|
|
301
|
+
const { DossierStore } = await import("./cases-By7INiOa.mjs");
|
|
302
|
+
const dossiers = await DossierStore.listSummaries(caseId);
|
|
303
|
+
const dossierMap = /* @__PURE__ */ new Map();
|
|
304
|
+
for (const d of dossiers) dossierMap.set(d.address, d.type);
|
|
305
|
+
allNodes = allNodes.map((node) => {
|
|
306
|
+
const dossierType = dossierMap.get(node.id);
|
|
307
|
+
if (dossierType && dossierType !== "unknown") {
|
|
308
|
+
const entityType = [
|
|
309
|
+
"eoa",
|
|
310
|
+
"contract",
|
|
311
|
+
"exchange",
|
|
312
|
+
"mixer",
|
|
313
|
+
"unknown"
|
|
314
|
+
].includes(dossierType) ? dossierType : "unknown";
|
|
315
|
+
return {
|
|
316
|
+
...node,
|
|
317
|
+
entityType
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
return node;
|
|
321
|
+
});
|
|
322
|
+
} catch {}
|
|
323
|
+
return GraphData.parse({
|
|
324
|
+
nodes: allNodes,
|
|
325
|
+
edges: allEdges,
|
|
326
|
+
metadata: {
|
|
327
|
+
caseId,
|
|
328
|
+
title: `${caseId} - Money Flow`,
|
|
329
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
//#endregion
|
|
334
|
+
export { truncateGraph as n, data_extractor_exports as t };
|
|
335
|
+
|
|
336
|
+
//# sourceMappingURL=data-extractor-DFzsa5CS.mjs.map
|