chain-insights 0.69.0 → 0.70.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apps/claude-view.html +40 -39
- package/dist/cli.cjs +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/{graph-schema-C_7aZ8RL.cjs → graph-schema-BoUMa03H.cjs} +19 -6
- package/dist/{graph-schema-DHd2fGbr.mjs → graph-schema-DqZUTnP0.mjs} +20 -7
- package/dist/graph-schema-DqZUTnP0.mjs.map +1 -0
- package/dist/mcp-proxy.cjs +60 -20
- package/dist/mcp-proxy.d.cts +2 -0
- package/dist/mcp-proxy.d.cts.map +1 -1
- package/dist/mcp-proxy.d.mts +2 -0
- package/dist/mcp-proxy.d.mts.map +1 -1
- package/dist/mcp-proxy.mjs +60 -21
- package/dist/mcp-proxy.mjs.map +1 -1
- package/package.json +1 -1
- package/skills/chain-insights-cypher/SKILL.md +9 -2
- package/skills/chain-insights-dex/SKILL.md +3 -0
- package/dist/graph-schema-DHd2fGbr.mjs.map +0 -1
package/dist/cli.cjs
CHANGED
|
@@ -152,7 +152,7 @@ async function printNetworkCapability(name, opts) {
|
|
|
152
152
|
async function printNetworkSchema(name, opts) {
|
|
153
153
|
const { loadConfig } = await Promise.resolve().then(() => require("./config-BxtMoo9c.cjs")).then((n) => n.config_exports);
|
|
154
154
|
const { resolveGraphMcpEndpoint } = await Promise.resolve().then(() => require("./client-Ypw9BNbn.cjs")).then((n) => n.client_exports);
|
|
155
|
-
const { formatGraphSchemaText, getGraphSchema } = await Promise.resolve().then(() => require("./graph-schema-
|
|
155
|
+
const { formatGraphSchemaText, getGraphSchema } = await Promise.resolve().then(() => require("./graph-schema-BoUMa03H.cjs")).then((n) => n.graph_schema_exports);
|
|
156
156
|
const config = await loadConfig();
|
|
157
157
|
const schema = await getGraphSchema({
|
|
158
158
|
network: name,
|
package/dist/cli.mjs
CHANGED
|
@@ -150,7 +150,7 @@ async function printNetworkCapability(name, opts) {
|
|
|
150
150
|
async function printNetworkSchema(name, opts) {
|
|
151
151
|
const { loadConfig } = await import("./config-9MabJh5R.mjs").then((n) => n.t);
|
|
152
152
|
const { resolveGraphMcpEndpoint } = await import("./client-gsLi528K.mjs").then((n) => n.r);
|
|
153
|
-
const { formatGraphSchemaText, getGraphSchema } = await import("./graph-schema-
|
|
153
|
+
const { formatGraphSchemaText, getGraphSchema } = await import("./graph-schema-DqZUTnP0.mjs").then((n) => n.i);
|
|
154
154
|
const config = await loadConfig();
|
|
155
155
|
const schema = await getGraphSchema({
|
|
156
156
|
network: name,
|
|
@@ -125,7 +125,7 @@ function buildAddressView(network, address, row) {
|
|
|
125
125
|
}
|
|
126
126
|
/** The one line of a details answer. */
|
|
127
127
|
function addressSummary(view) {
|
|
128
|
-
return `${view.address} on ${view.network}: ${view.role}; lifetime ${formatFlowsUsd(view.total_in_usd)} in, ${formatFlowsUsd(view.total_out_usd)} out; ${view.degree_in} senders and ${view.degree_out} receivers in the topology graph` + (view.labels.length ? `; labels ${view.labels.join(", ")}` : "") + ".";
|
|
128
|
+
return `${view.address} on ${view.network}: ${roleWord(view.role)}; lifetime ${formatFlowsUsd(view.total_in_usd)} in, ${formatFlowsUsd(view.total_out_usd)} out; ${view.degree_in} senders and ${view.degree_out} receivers in the topology graph` + (view.labels.length ? `; labels ${view.labels.join(", ")}` : "") + ".";
|
|
129
129
|
}
|
|
130
130
|
const NODE_FIELDS = "b.address AS address, b.labels AS labels, b.is_exchange AS is_exchange, b.is_sanctioned AS is_sanctioned, b.is_scam AS is_scam, b.is_victim AS is_victim, b:Pool AS pool, b.total_in_usd AS total_in_usd, b.total_out_usd AS total_out_usd, b.degree_in AS degree_in, b.degree_out AS degree_out";
|
|
131
131
|
const PARTY_FIELDS = NODE_FIELDS + ", f.amount_usd_sum AS usd, f.tx_count AS tx_count, " + "f.first_seen_timestamp AS first_seen, f.last_seen_timestamp AS last_seen";
|
|
@@ -169,8 +169,13 @@ function flowsRole(row) {
|
|
|
169
169
|
if (row.is_scam === true) return "scam";
|
|
170
170
|
if (row.is_victim === true) return "victim";
|
|
171
171
|
if (row.pool === true) return "pool";
|
|
172
|
+
if (Array.isArray(row.labels) && row.labels.includes("SelfTrader")) return "self_trader";
|
|
172
173
|
return "other";
|
|
173
174
|
}
|
|
175
|
+
/** The role as the text answers write it: a role code becomes words. */
|
|
176
|
+
function roleWord(role) {
|
|
177
|
+
return role === "self_trader" ? "self-trading contract" : role;
|
|
178
|
+
}
|
|
174
179
|
function flowsFloat(value) {
|
|
175
180
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
176
181
|
}
|
|
@@ -289,8 +294,9 @@ function formatFlowsAge(lastSeenMs, now) {
|
|
|
289
294
|
/** The role word, with up to three address labels beside it. */
|
|
290
295
|
function roleText(node) {
|
|
291
296
|
if (!node) return "other";
|
|
292
|
-
|
|
293
|
-
|
|
297
|
+
const word = roleWord(node.role);
|
|
298
|
+
if (node.labels.length === 0) return word;
|
|
299
|
+
return `${word} (${node.labels.slice(0, 3).join(", ")})`;
|
|
294
300
|
}
|
|
295
301
|
function sideLines(title, parties) {
|
|
296
302
|
if (parties.length === 0) return [`${title}, newest first: none.`];
|
|
@@ -390,6 +396,13 @@ function errorResult$1(text) {
|
|
|
390
396
|
isError: true
|
|
391
397
|
};
|
|
392
398
|
}
|
|
399
|
+
/** The result of a failed read: the structured refusal when the proxy builds one, else the text. */
|
|
400
|
+
async function failedRead(deps, err) {
|
|
401
|
+
if (deps.refusal) try {
|
|
402
|
+
return await deps.refusal(err);
|
|
403
|
+
} catch {}
|
|
404
|
+
return errorResult$1(deps.describeFailure(err));
|
|
405
|
+
}
|
|
393
406
|
function isRecord$1(value) {
|
|
394
407
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
395
408
|
}
|
|
@@ -485,7 +498,7 @@ async function runNode(deps, network, address, inOffset, outOffset) {
|
|
|
485
498
|
query: read.query
|
|
486
499
|
});
|
|
487
500
|
} catch (err) {
|
|
488
|
-
return
|
|
501
|
+
return failedRead(deps, err);
|
|
489
502
|
}
|
|
490
503
|
if (answer.isError === true) return errorResult$1(refusalText(answer));
|
|
491
504
|
const found = rowsOf(answer);
|
|
@@ -521,7 +534,7 @@ async function runLink(deps, network, from, to, day) {
|
|
|
521
534
|
query: transfersReadFor(from, to, day)
|
|
522
535
|
});
|
|
523
536
|
} catch (err) {
|
|
524
|
-
return
|
|
537
|
+
return failedRead(deps, err);
|
|
525
538
|
}
|
|
526
539
|
if (answer.isError === true) return errorResult$1(refusalText(answer));
|
|
527
540
|
const rows = rowsOf(answer);
|
|
@@ -641,7 +654,7 @@ async function runDetails(deps, network, address) {
|
|
|
641
654
|
query: detailsReadFor(address)
|
|
642
655
|
});
|
|
643
656
|
} catch (err) {
|
|
644
|
-
return
|
|
657
|
+
return failedRead(deps, err);
|
|
645
658
|
}
|
|
646
659
|
if (answer.isError === true) return errorResult$1(refusalText(answer));
|
|
647
660
|
const rows = rowsOf(answer);
|
|
@@ -124,7 +124,7 @@ function buildAddressView(network, address, row) {
|
|
|
124
124
|
}
|
|
125
125
|
/** The one line of a details answer. */
|
|
126
126
|
function addressSummary(view) {
|
|
127
|
-
return `${view.address} on ${view.network}: ${view.role}; lifetime ${formatFlowsUsd(view.total_in_usd)} in, ${formatFlowsUsd(view.total_out_usd)} out; ${view.degree_in} senders and ${view.degree_out} receivers in the topology graph` + (view.labels.length ? `; labels ${view.labels.join(", ")}` : "") + ".";
|
|
127
|
+
return `${view.address} on ${view.network}: ${roleWord(view.role)}; lifetime ${formatFlowsUsd(view.total_in_usd)} in, ${formatFlowsUsd(view.total_out_usd)} out; ${view.degree_in} senders and ${view.degree_out} receivers in the topology graph` + (view.labels.length ? `; labels ${view.labels.join(", ")}` : "") + ".";
|
|
128
128
|
}
|
|
129
129
|
const NODE_FIELDS = "b.address AS address, b.labels AS labels, b.is_exchange AS is_exchange, b.is_sanctioned AS is_sanctioned, b.is_scam AS is_scam, b.is_victim AS is_victim, b:Pool AS pool, b.total_in_usd AS total_in_usd, b.total_out_usd AS total_out_usd, b.degree_in AS degree_in, b.degree_out AS degree_out";
|
|
130
130
|
const PARTY_FIELDS = NODE_FIELDS + ", f.amount_usd_sum AS usd, f.tx_count AS tx_count, " + "f.first_seen_timestamp AS first_seen, f.last_seen_timestamp AS last_seen";
|
|
@@ -168,8 +168,13 @@ function flowsRole(row) {
|
|
|
168
168
|
if (row.is_scam === true) return "scam";
|
|
169
169
|
if (row.is_victim === true) return "victim";
|
|
170
170
|
if (row.pool === true) return "pool";
|
|
171
|
+
if (Array.isArray(row.labels) && row.labels.includes("SelfTrader")) return "self_trader";
|
|
171
172
|
return "other";
|
|
172
173
|
}
|
|
174
|
+
/** The role as the text answers write it: a role code becomes words. */
|
|
175
|
+
function roleWord(role) {
|
|
176
|
+
return role === "self_trader" ? "self-trading contract" : role;
|
|
177
|
+
}
|
|
173
178
|
function flowsFloat(value) {
|
|
174
179
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
175
180
|
}
|
|
@@ -288,8 +293,9 @@ function formatFlowsAge(lastSeenMs, now) {
|
|
|
288
293
|
/** The role word, with up to three address labels beside it. */
|
|
289
294
|
function roleText(node) {
|
|
290
295
|
if (!node) return "other";
|
|
291
|
-
|
|
292
|
-
|
|
296
|
+
const word = roleWord(node.role);
|
|
297
|
+
if (node.labels.length === 0) return word;
|
|
298
|
+
return `${word} (${node.labels.slice(0, 3).join(", ")})`;
|
|
293
299
|
}
|
|
294
300
|
function sideLines(title, parties) {
|
|
295
301
|
if (parties.length === 0) return [`${title}, newest first: none.`];
|
|
@@ -389,6 +395,13 @@ function errorResult$1(text) {
|
|
|
389
395
|
isError: true
|
|
390
396
|
};
|
|
391
397
|
}
|
|
398
|
+
/** The result of a failed read: the structured refusal when the proxy builds one, else the text. */
|
|
399
|
+
async function failedRead(deps, err) {
|
|
400
|
+
if (deps.refusal) try {
|
|
401
|
+
return await deps.refusal(err);
|
|
402
|
+
} catch {}
|
|
403
|
+
return errorResult$1(deps.describeFailure(err));
|
|
404
|
+
}
|
|
392
405
|
function isRecord$1(value) {
|
|
393
406
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
394
407
|
}
|
|
@@ -484,7 +497,7 @@ async function runNode(deps, network, address, inOffset, outOffset) {
|
|
|
484
497
|
query: read.query
|
|
485
498
|
});
|
|
486
499
|
} catch (err) {
|
|
487
|
-
return
|
|
500
|
+
return failedRead(deps, err);
|
|
488
501
|
}
|
|
489
502
|
if (answer.isError === true) return errorResult$1(refusalText(answer));
|
|
490
503
|
const found = rowsOf(answer);
|
|
@@ -520,7 +533,7 @@ async function runLink(deps, network, from, to, day) {
|
|
|
520
533
|
query: transfersReadFor(from, to, day)
|
|
521
534
|
});
|
|
522
535
|
} catch (err) {
|
|
523
|
-
return
|
|
536
|
+
return failedRead(deps, err);
|
|
524
537
|
}
|
|
525
538
|
if (answer.isError === true) return errorResult$1(refusalText(answer));
|
|
526
539
|
const rows = rowsOf(answer);
|
|
@@ -640,7 +653,7 @@ async function runDetails(deps, network, address) {
|
|
|
640
653
|
query: detailsReadFor(address)
|
|
641
654
|
});
|
|
642
655
|
} catch (err) {
|
|
643
|
-
return
|
|
656
|
+
return failedRead(deps, err);
|
|
644
657
|
}
|
|
645
658
|
if (answer.isError === true) return errorResult$1(refusalText(answer));
|
|
646
659
|
const rows = rowsOf(answer);
|
|
@@ -1177,4 +1190,4 @@ async function handleMetaSchema(args, deps) {
|
|
|
1177
1190
|
//#endregion
|
|
1178
1191
|
export { handleMetaSchema as a, GRAPH_EXPAND_TITLE as c, graph_schema_exports as i, GRAPH_EXPAND_TOOL as l, META_SCHEMA_DESCRIPTION as n, FLOWS_MAX_OFFSET as o, META_SCHEMA_TITLE as r, GRAPH_EXPAND_DESCRIPTION as s, GraphSchemaError as t, handleGraphExpand as u };
|
|
1179
1192
|
|
|
1180
|
-
//# sourceMappingURL=graph-schema-
|
|
1193
|
+
//# sourceMappingURL=graph-schema-DqZUTnP0.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph-schema-DqZUTnP0.mjs","names":["errorResult","isRecord"],"sources":["../src/mcp/flows.ts","../src/mcp/graph-schema-cache.ts","../src/mcp/graph-schema.ts"],"sourcesContent":["/**\n * graph_expand, the one dedicated tool of the local proxy, called by the Chain\n * Insights view (ui://chain-insights/view) when the investigator clicks. Its\n * _meta says visibility [\"app\"], so a host keeps it away from the model: the\n * model writes its own graph_query, and the view draws the rows by their column\n * names.\n *\n * Two forms, told apart by the arguments:\n *\n * - node, {network, address, in_offset?, out_offset?}: one address with up to\n * 250 newest senders and 250 newest receivers, a text summary and\n * chain-insights.flows.v1 in structuredContent. Composed from at most five\n * anchored graph_query reads on the topology graph: the node, then its\n * outgoing and its incoming FLOWS_TO, newest first, two reads a side because\n * a reply holds at most 200 rows.\n * - link, {network, from, to, day}: the transfers between two known addresses\n * on one UTC day, newest first, at most 50, a short text summary and\n * chain-insights.transfers.v1 in structuredContent. One anchored USE facts\n * graph_query read.\n * - link page, {network, from, to, day, limit, skip?, first_day?}: a page of\n * `limit` transfers (1 to 50) newest first, read from `day` back one UTC day\n * at a time, at most TRANSFERS_MAX_DAYS (a week of) reads a call, never before\n * first_day. The answer carries `next` ({day, skip}, or null when every\n * transfer back to first_day is listed) which the next call sends back as\n * day and skip. The view's \"More transfers\".\n * - details, {network, address, details: true}: the lifetime totals, counts,\n * role and labels of one address, chain-insights.address.v1, one anchored\n * topology read. The view fills its panel with it when the rows of a query\n * carried no totals.\n *\n * Every read goes through the proxy's remote client, the same path and payment\n * wrapping a graph_query call takes, and is billed as a graph_query. Every\n * argument is checked before the first read, so a refused call reaches no graph\n * endpoint and costs nothing.\n */\n\nexport const FLOWS_SCHEMA = 'chain-insights.flows.v1'\nexport const GRAPH_EXPAND_TOOL = 'graph_expand'\nexport const FLOWS_NETWORK = 'robinhood'\n\n/** How many senders and how many receivers one page shows, at most. */\nexport const FLOWS_PER_SIDE = 250\n/** The view data of one answer: at most this many addresses, the centre included... */\nexport const FLOWS_MAX_NODES = 500\n/**\n * ...and under this many characters of JSON. Hosts drop an app tool result\n * over about 150,000 characters.\n */\nexport const FLOWS_MAX_CHARS = 140_000\n/**\n * The graph endpoint cuts a reply at 200 rows, whatever the LIMIT, so one side\n * of a page is read in reads of at most this many rows.\n */\nexport const FLOWS_READ_ROWS = 200\n/** The text of a node answer holds at most this many lines. */\nexport const FLOWS_SUMMARY_MAX_LINES = 20\n/** Where a graph_expand page may start on each side, at most. */\nexport const FLOWS_MAX_OFFSET = 10_000\n\nexport const TRANSFERS_SCHEMA = 'chain-insights.transfers.v1'\n/** A link answer lists at most this many transfers... */\nexport const TRANSFERS_MAX = 50\n/**\n * ...from the rows one read brings back. A facts read takes no ORDER BY and\n * the server cuts a reply at 200 rows, so the read asks for the whole cap and\n * the newest rows are picked here.\n */\nexport const TRANSFERS_READ_ROWS = 200\n/** The text of a link answer names at most this many transfers. */\nexport const TRANSFERS_SUMMARY_ROWS = 5\n/** A paged link answer reads at most this many days (a week) in one call: a sparse link costs at most this many reads a page, and a week of it comes back at once. */\nexport const TRANSFERS_MAX_DAYS = 7\n/** Where a paged link answer stops reading back when the caller names no first_day. */\nexport const TRANSFERS_FLOOR_DAY = '2015-01-01'\n\nexport const ADDRESS_SCHEMA = 'chain-insights.address.v1'\n\nexport type FlowNode = {\n address: string\n role: string\n labels: string[]\n total_in_usd: number\n total_out_usd: number\n degree_in: number\n degree_out: number\n}\n\nexport type FlowEdge = {\n from: string\n to: string\n usd: number\n tx_count: number\n first_seen_ms: number\n last_seen_ms: number\n}\n\nexport type FlowCursor = { in_offset: number; out_offset: number }\n\n/** The links of the centre not loaded yet, past the cursor, on each side. */\nexport type FlowRemaining = { senders: number; receivers: number }\n\nexport type FlowView = {\n schema: typeof FLOWS_SCHEMA\n network: string\n center: string\n nodes: FlowNode[]\n edges: FlowEdge[]\n cursor: FlowCursor\n truncated: boolean\n /** Set on every node page. A view that does not know it ignores it. */\n remaining?: FlowRemaining\n}\n\ntype Row = Record<string, unknown>\n\nexport type FlowsPage = {\n center: Row\n senders: Row[]\n receivers: Row[]\n inOffset: number\n outOffset: number\n}\n\n/** One graph_query answer as the remote client returns it. */\nexport type GraphQueryAnswer = {\n content?: Array<{ type: string; text?: string }>\n structuredContent?: Record<string, unknown>\n isError?: boolean\n}\n\nexport type FlowsToolResult = {\n content: Array<{ type: 'text'; text: string }>\n structuredContent?: Record<string, unknown>\n isError: boolean\n}\n\nexport type FlowsDependencies = {\n /** One graph_query call through the proxy's remote client. */\n graphQuery: (args: { network: string; query: string }) => Promise<GraphQueryAnswer>\n /**\n * The whole refusal of a failed read, text and structured facts\n * (chain-insights.refusal.v1: the free tier spent, a wallet that cannot\n * pay). When given it replaces describeFailure, so the view can say why in\n * the viewer's own words and time.\n */\n refusal?: (err: unknown) => Promise<FlowsToolResult>\n /** The text of a call that threw (payment required, transport failure). */\n describeFailure: (err: unknown) => string\n /** Why no read can run (the graph endpoint is not connected), checked after the arguments. */\n unavailable?: () => string | undefined\n now?: () => Date\n}\n\nexport const INVALID_ADDRESS_TEXT =\n 'invalid_address: a full 0x address of 40 hexadecimal characters is needed, for example 0x04911a118f11c75667e4d0dfb8e640af5a353550'\nexport const OFFSET_TOO_LARGE_TEXT = `invalid_offset: in_offset and out_offset must each be at most ${FLOWS_MAX_OFFSET}`\nexport const OFFSET_NEGATIVE_TEXT =\n 'invalid_offset: in_offset and out_offset must each be a whole number, 0 or more'\n\nexport const INVALID_DAY_TEXT =\n 'invalid_day: day must be a calendar date written YYYY-MM-DD (UTC), for example 2026-07-10'\nexport const BOTH_FORMS_TEXT =\n 'invalid_arguments: pass address (with in_offset and out_offset) to expand a node, or from, to and day to list the transfers of a link, not both'\nexport const INVALID_LIMIT_TEXT = 'invalid_limit: limit must be a whole number from 1 to 50'\nexport const INVALID_SKIP_TEXT = 'invalid_skip: skip must be a whole number of 0 or more'\nexport const INVALID_FIRST_DAY_TEXT =\n 'invalid_first_day: first_day must be a calendar date written YYYY-MM-DD (UTC), not after day'\nexport const DETAILS_FORM_TEXT =\n 'invalid_arguments: details takes address alone; pass no from, to, day or offsets with it'\n\nconst ADDRESS_PATTERN = /^0x[0-9a-f]{40}$/\nconst DAY_PATTERN = /^\\d{4}-\\d{2}-\\d{2}$/\nconst CODE_PREFIX = /^([a-z][a-z0-9_]*):\\s*/\n\n/**\n * The address lower-cased, or null when it is not 0x and 40 hexadecimal\n * characters. Only an address that passes is ever written into a read, so the\n * reads can carry it as a literal.\n */\nexport function normalizeFlowsAddress(raw: unknown): string | null {\n if (typeof raw !== 'string') return null\n const address = raw.trim().toLowerCase()\n return ADDRESS_PATTERN.test(address) ? address : null\n}\n\n/**\n * The day as YYYY-MM-DD when it is a real calendar date, or null. Only a day\n * that passes is ever written into a read.\n */\nexport function normalizeFlowsDay(raw: unknown): string | null {\n if (typeof raw !== 'string') return null\n const day = raw.trim()\n if (!DAY_PATTERN.test(day)) return null\n const time = Date.parse(`${day}T00:00:00Z`)\n if (Number.isNaN(time) || new Date(time).toISOString().slice(0, 10) !== day) return null\n return day\n}\n\n/**\n * The one facts read of a link: the transfers from one address to another on\n * one day. A facts read names the pair with one day, takes no ORDER BY and\n * holds at most 200 rows, so the read asks for the whole cap and the order is\n * made in buildTransfersView. Both addresses and the day have passed their\n * checks, so they are written into the read as literals.\n */\nexport function transfersReadFor(from: string, to: string, day: string): string {\n return (\n `USE facts MATCH (a:Address {address: \"${from}\"})-[t:TRANSFER]->(b:Address {address: \"${to}\"}) ` +\n `WHERE t.block_date = \"${day}\" ` +\n 'RETURN t.tx_id AS tx_id, t.block_timestamp AS block_timestamp, t.amount AS amount, ' +\n `t.asset_symbol AS asset_symbol, t.amount_usd AS amount_usd LIMIT ${TRANSFERS_READ_ROWS}`\n )\n}\n\n/** The UTC day before a YYYY-MM-DD day. */\nexport function dayBefore(day: string): string {\n return new Date(Date.parse(`${day}T00:00:00Z`) - 86_400_000).toISOString().slice(0, 10)\n}\n\n/**\n * The one topology read of a details call: the address alone, with its\n * lifetime rollups and activity span. The address has passed its check, so it\n * is written into the read as a literal.\n */\nexport function detailsReadFor(address: string): string {\n return (\n `USE topology MATCH (b:Address {address: \"${address}\"}) RETURN ${NODE_FIELDS}, ` +\n 'b.first_activity_timestamp AS first_activity_timestamp, b.last_activity_timestamp AS last_activity_timestamp LIMIT 1'\n )\n}\n\nexport type AddressView = FlowNode & {\n schema: typeof ADDRESS_SCHEMA\n network: string\n is_exchange: boolean\n is_sanctioned: boolean\n is_scam: boolean\n is_victim: boolean\n pool: boolean\n first_activity_timestamp: number | null\n last_activity_timestamp: number | null\n}\n\n/** chain-insights.address.v1 from the one row of a details read. */\nexport function buildAddressView(network: string, address: string, row: Row): AddressView {\n const node = flowsNodeOf({ ...row, address })\n return {\n schema: ADDRESS_SCHEMA,\n network,\n ...node,\n is_exchange: row.is_exchange === true,\n is_sanctioned: row.is_sanctioned === true,\n is_scam: row.is_scam === true,\n is_victim: row.is_victim === true,\n pool: row.pool === true,\n first_activity_timestamp: flowsNumberOrNull(row.first_activity_timestamp),\n last_activity_timestamp: flowsNumberOrNull(row.last_activity_timestamp),\n }\n}\n\n/** The one line of a details answer. */\nexport function addressSummary(view: AddressView): string {\n return (\n `${view.address} on ${view.network}: ${roleWord(view.role)}; lifetime ${formatFlowsUsd(view.total_in_usd)} in, ` +\n `${formatFlowsUsd(view.total_out_usd)} out; ${view.degree_in} senders and ${view.degree_out} receivers in the topology graph` +\n (view.labels.length ? `; labels ${view.labels.join(', ')}` : '') +\n '.'\n )\n}\n\nconst NODE_FIELDS =\n 'b.address AS address, b.labels AS labels, b.is_exchange AS is_exchange, ' +\n 'b.is_sanctioned AS is_sanctioned, b.is_scam AS is_scam, b.is_victim AS is_victim, b:Pool AS pool, ' +\n 'b.total_in_usd AS total_in_usd, b.total_out_usd AS total_out_usd, b.degree_in AS degree_in, b.degree_out AS degree_out'\nconst PARTY_FIELDS =\n NODE_FIELDS +\n ', f.amount_usd_sum AS usd, f.tx_count AS tx_count, ' +\n 'f.first_seen_timestamp AS first_seen, f.last_seen_timestamp AS last_seen'\n// Newest link first, the address breaking a tie, so a page and the next one\n// never share or skip a link.\nconst PAGE_ORDER = ' ORDER BY f.last_seen_timestamp DESC, b.address'\n\n/** One anchored read of a page, with the SKIP and LIMIT it carries. */\nexport type FlowsRead = { id: 'node' | 'out' | 'in'; query: string; skip: number; limit: number }\n\n/**\n * The anchored reads of one page: the node, then the receivers, then the\n * senders. Each side asks for one row more than it shows: the extra row says\n * another page exists. A reply holds at most FLOWS_READ_ROWS rows, so a side\n * is read in chunks on the same order, each starting where the last one ends.\n */\nexport function flowsReadsFor(address: string, inOffset: number, outOffset: number): FlowsRead[] {\n const side = (id: 'out' | 'in', offset: number, match: string): FlowsRead[] => {\n const reads: FlowsRead[] = []\n for (let done = 0; done < FLOWS_PER_SIDE + 1; done += FLOWS_READ_ROWS) {\n const skip = offset + done\n const limit = Math.min(FLOWS_READ_ROWS, FLOWS_PER_SIDE + 1 - done)\n reads.push({\n id,\n query: `USE topology MATCH ${match} RETURN ${PARTY_FIELDS}${PAGE_ORDER}${skip > 0 ? ` SKIP ${skip}` : ''} LIMIT ${limit}`,\n skip,\n limit,\n })\n }\n return reads\n }\n return [\n {\n id: 'node',\n query: `USE topology MATCH (b:Address {address: '${address}'}) RETURN ${NODE_FIELDS} LIMIT 1`,\n skip: 0,\n limit: 1,\n },\n ...side('out', outOffset, `(a:Address {address: '${address}'})-[f:FLOWS_TO]->(b:Address)`),\n ...side('in', inOffset, `(b:Address)-[f:FLOWS_TO]->(a:Address {address: '${address}'})`),\n ]\n}\n\n/** The role word of an address, first match wins. */\nexport function flowsRole(row: Row): string {\n if (row.is_exchange === true) return 'exchange'\n if (row.is_sanctioned === true) return 'sanctioned'\n if (row.is_scam === true) return 'scam'\n if (row.is_victim === true) return 'victim'\n if (row.pool === true) return 'pool'\n if (Array.isArray(row.labels) && row.labels.includes('SelfTrader')) return 'self_trader'\n return 'other'\n}\n\n/** The role as the text answers write it: a role code becomes words. */\nfunction roleWord(role: string): string {\n return role === 'self_trader' ? 'self-trading contract' : role\n}\n\nfunction flowsFloat(value: unknown): number {\n return typeof value === 'number' && Number.isFinite(value) ? value : 0\n}\n\nfunction flowsInt(value: unknown): number {\n return typeof value === 'number' && Number.isFinite(value) ? Math.trunc(value) : 0\n}\n\nfunction flowsNodeOf(row: Row): FlowNode {\n const labels = Array.isArray(row.labels)\n ? row.labels.filter((label): label is string => typeof label === 'string')\n : []\n return {\n address: typeof row.address === 'string' ? row.address : '',\n role: flowsRole(row),\n labels,\n total_in_usd: flowsFloat(row.total_in_usd),\n total_out_usd: flowsFloat(row.total_out_usd),\n degree_in: flowsInt(row.degree_in),\n degree_out: flowsInt(row.degree_out),\n }\n}\n\nfunction assembleFlowView(\n network: string,\n page: FlowsPage,\n senders: Row[],\n receivers: Row[],\n truncated: boolean\n): FlowView {\n const center = flowsNodeOf(page.center)\n const cursor = {\n in_offset: page.inOffset + senders.length,\n out_offset: page.outOffset + receivers.length,\n }\n const view: FlowView = {\n schema: FLOWS_SCHEMA,\n network,\n center: center.address,\n nodes: [center],\n edges: [],\n cursor,\n truncated,\n remaining: {\n senders: Math.max(0, center.degree_in - cursor.in_offset),\n receivers: Math.max(0, center.degree_out - cursor.out_offset),\n },\n }\n const seen = new Set([center.address])\n const add = (row: Row, incoming: boolean) => {\n const node = flowsNodeOf(row)\n if (!seen.has(node.address)) {\n seen.add(node.address)\n view.nodes.push(node)\n }\n view.edges.push({\n from: incoming ? node.address : center.address,\n to: incoming ? center.address : node.address,\n usd: flowsFloat(row.usd),\n tx_count: flowsInt(row.tx_count),\n first_seen_ms: flowsInt(row.first_seen),\n last_seen_ms: flowsInt(row.last_seen),\n })\n }\n for (const row of senders) add(row, true)\n for (const row of receivers) add(row, false)\n return view\n}\n\nfunction fitsFlowBudget(view: FlowView): boolean {\n return view.nodes.length <= FLOWS_MAX_NODES && JSON.stringify(view).length < FLOWS_MAX_CHARS\n}\n\n/**\n * How many senders and receivers stay after `drops` of the oldest links go,\n * one at a time from the longer side, the senders first on a tie.\n */\nfunction keptAfterDrops(senders: number, receivers: number, drops: number): [number, number] {\n let s = senders\n let r = receivers\n for (let left = Math.min(drops, s + r); left > 0; left -= 1) {\n if (s >= r) s -= 1\n else r -= 1\n }\n return [s, r]\n}\n\n/**\n * flows.v1 from one page: the centre first, then the senders and the\n * receivers, each newest first, at most FLOWS_PER_SIDE a side. When the view\n * data would hold more than FLOWS_MAX_NODES addresses or reach FLOWS_MAX_CHARS\n * characters, the oldest links go first, the view is marked truncated, and the\n * cursor still says where the next page starts.\n *\n * Each dropped link only makes the view smaller, so the fewest drops that fit\n * are found by halving: about ten views are measured for a full page, not one\n * per dropped link.\n */\nexport function buildFlowView(network: string, page: FlowsPage): FlowView {\n const overflow = page.senders.length > FLOWS_PER_SIDE || page.receivers.length > FLOWS_PER_SIDE\n const senders = page.senders.slice(0, FLOWS_PER_SIDE)\n const receivers = page.receivers.slice(0, FLOWS_PER_SIDE)\n const viewAfter = (drops: number) => {\n const [s, r] = keptAfterDrops(senders.length, receivers.length, drops)\n return assembleFlowView(\n network,\n page,\n senders.slice(0, s),\n receivers.slice(0, r),\n overflow || drops > 0\n )\n }\n // Dropping every link is the floor: that view is answered even when it does\n // not fit.\n let low = 0\n let high = senders.length + receivers.length\n while (low < high) {\n const mid = (low + high) >> 1\n if (fitsFlowBudget(viewAfter(mid))) high = mid\n else low = mid + 1\n }\n return viewAfter(low)\n}\n\nexport function formatFlowsUsd(value: number): string {\n if (!(value > 0)) return '$0'\n if (value < 0.01) return 'under $0.01'\n const whole = Math.floor(value)\n const cents = Math.round((value - whole) * 100)\n if (cents === 100) return formatFlowsUsd(whole + 1)\n const grouped = whole.toFixed(0).replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')\n return `$${grouped}.${String(cents).padStart(2, '0')}`\n}\n\nfunction formatFlowsAge(lastSeenMs: number, now: Date): string {\n if (lastSeenMs <= 0) return 'at an unknown time'\n const date = new Date(lastSeenMs).toISOString().slice(0, 10)\n const days = Math.trunc((now.getTime() - lastSeenMs) / 86_400_000)\n if (days < 1) return `${date} (less than a day ago)`\n if (days === 1) return `${date} (1 day ago)`\n return `${date} (${days} days ago)`\n}\n\n/** The role word, with up to three address labels beside it. */\nfunction roleText(node: FlowNode | undefined): string {\n if (!node) return 'other'\n const word = roleWord(node.role)\n if (node.labels.length === 0) return word\n return `${word} (${node.labels.slice(0, 3).join(', ')})`\n}\n\nfunction sideLines(title: string, parties: string[]): string[] {\n if (parties.length === 0) return [`${title}, newest first: none.`]\n const lines = [`${title}, newest first (${parties.length}):`]\n // A side with more than six counterparties lists two on a line, so a full\n // page stays within the line budget.\n const perLine = parties.length > 6 ? 2 : 1\n for (let start = 0; start < parties.length; start += perLine) {\n lines.push(`- ${parties.slice(start, start + perLine).join('; ')}`)\n }\n return lines\n}\n\n/**\n * The text of a node answer: the centre, its role and lifetime totals, then\n * every counterparty of the view with its USD, transaction count and last-seen\n * date and age. At most FLOWS_SUMMARY_MAX_LINES lines.\n */\nexport function flowsSummary(view: FlowView, now: Date): string {\n const nodes = new Map(view.nodes.map((node) => [node.address, node]))\n const center = nodes.get(view.center)\n const party = (address: string, edge: FlowEdge) =>\n `${address} ${roleText(nodes.get(address))}, ${formatFlowsUsd(edge.usd)} in ${edge.tx_count} tx, last seen ${formatFlowsAge(edge.last_seen_ms, now)}`\n const senders: string[] = []\n const receivers: string[] = []\n for (const edge of view.edges) {\n if (edge.to === view.center) senders.push(party(edge.from, edge))\n else receivers.push(party(edge.to, edge))\n }\n const lines = [\n `Money flows of ${view.center} on ${view.network}: ${roleText(center)}; lifetime ${formatFlowsUsd(center?.total_in_usd ?? 0)} in, ${formatFlowsUsd(center?.total_out_usd ?? 0)} out; ${center?.degree_in ?? 0} senders and ${center?.degree_out ?? 0} receivers in the topology graph.`,\n ...sideLines('Recent senders', senders),\n ...sideLines('Recent receivers', receivers),\n ]\n const body = lines.slice(0, FLOWS_SUMMARY_MAX_LINES - 2)\n const footer = pagingLines(view, senders.length, receivers.length, center)\n return [...body, ...footer].join('\\n')\n}\n\n/** Where this page sits, and that the picture loads the rest when an address is clicked. */\nfunction pagingLines(\n view: FlowView,\n shownSenders: number,\n shownReceivers: number,\n center: FlowNode | undefined\n): string[] {\n const totalIn = center?.degree_in ?? 0\n const totalOut = center?.degree_out ?? 0\n const { in_offset: nextIn, out_offset: nextOut } = view.cursor\n const range = (shown: number, next: number, total: number) =>\n shown === 0 ? `none of ${total}` : `${next - shown + 1} to ${next} of ${total}`\n const where = `Showing senders ${range(shownSenders, nextIn, totalIn)} and receivers ${range(shownReceivers, nextOut, totalOut)}, newest first.`\n if (!view.truncated && nextIn >= totalIn && nextOut >= totalOut) return [where]\n return [\n where,\n 'More senders and receivers load when an address in the picture is double-clicked.',\n ]\n}\n\nexport type TransferRow = {\n tx_id: string\n block_timestamp: number | null\n amount: number | string | null\n asset_symbol: string | null\n amount_usd: number | null\n}\n\nexport type TransfersCursor = { day: string; skip: number }\n\nexport type TransfersView = {\n schema: typeof TRANSFERS_SCHEMA\n network: string\n from: string\n to: string\n day: string\n transfers: TransferRow[]\n truncated: boolean\n /** Paged answers only: the earliest day read, how many days the call read, and where the next page starts (null: every transfer back to first_day is listed). */\n through_day?: string\n days_read?: number\n next?: TransfersCursor | null\n}\n\nexport type TransfersPageRequest = { skip: number; firstDay: string | null; limit: number }\n\n/** A finite number, or a decimal string read as one, or null. */\nfunction flowsNumberOrNull(value: unknown): number | null {\n if (typeof value === 'number') return Number.isFinite(value) ? value : null\n if (typeof value === 'string' && value.trim() !== '') {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : null\n }\n return null\n}\n\nfunction transferOf(row: Row): TransferRow {\n const amount =\n typeof row.amount === 'number' && Number.isFinite(row.amount)\n ? row.amount\n : typeof row.amount === 'string' && row.amount.trim() !== ''\n ? row.amount\n : null\n return {\n tx_id: typeof row.tx_id === 'string' ? row.tx_id : '',\n block_timestamp: flowsNumberOrNull(row.block_timestamp),\n amount,\n asset_symbol:\n typeof row.asset_symbol === 'string' && row.asset_symbol !== '' ? row.asset_symbol : null,\n amount_usd: flowsNumberOrNull(row.amount_usd),\n }\n}\n\n/**\n * chain-insights.transfers.v1 from the rows of one facts read: newest first,\n * at most TRANSFERS_MAX, truncated when more rows than that came back. The\n * facts layer takes no ORDER BY, so the order is made here.\n */\nexport function buildTransfersView(\n network: string,\n from: string,\n to: string,\n day: string,\n rows: Row[]\n): TransfersView {\n const newestFirst = rows\n .map(transferOf)\n .sort((a, b) => (b.block_timestamp ?? -Infinity) - (a.block_timestamp ?? -Infinity))\n return {\n schema: TRANSFERS_SCHEMA,\n network,\n from,\n to,\n day,\n transfers: newestFirst.slice(0, TRANSFERS_MAX),\n truncated: newestFirst.length > TRANSFERS_MAX,\n }\n}\n\nfunction transferLine(transfer: TransferRow): string {\n const time =\n transfer.block_timestamp === null\n ? 'at an unknown time'\n : `${new Date(transfer.block_timestamp).toISOString().slice(0, 19).replace('T', ' ')} UTC`\n const amount = transfer.amount === null ? 'an unknown amount' : String(transfer.amount)\n const asset = transfer.asset_symbol ? ` ${transfer.asset_symbol}` : ''\n const usd = transfer.amount_usd === null ? 'no USD price' : formatFlowsUsd(transfer.amount_usd)\n return `- ${time}, ${amount}${asset}, ${usd}, tx ${transfer.tx_id}`\n}\n\n/** The short text of a link answer: the pair, the day, the count and the newest few transfers. */\nexport function transfersSummary(view: TransfersView): string {\n const paged = view.through_day !== undefined\n const span = paged && view.through_day !== view.day ? `${view.through_day} to ${view.day}` : view.day\n const head = `Transfers from ${view.from} to ${view.to} on ${span} (UTC) on ${view.network}`\n if (view.transfers.length === 0) {\n return paged && view.next ? `${head}: none; earlier days remain before ${view.next.day}.` : `${head}: none.`\n }\n const count = view.truncated\n ? paged\n ? `${view.transfers.length}, and a day holds more than one read lists`\n : `the newest ${view.transfers.length}, and the day holds more`\n : `${view.transfers.length}`\n const lines = [\n `${head}: ${count}, newest first.`,\n ...view.transfers.slice(0, TRANSFERS_SUMMARY_ROWS).map(transferLine),\n ]\n const rest = view.transfers.length - TRANSFERS_SUMMARY_ROWS\n if (rest > 0) lines.push(`- and ${rest} more in the picture.`)\n if (paged && view.next) lines.push(`- earlier transfers remain, from ${view.next.day} back.`)\n return lines.join('\\n')\n}\n\nfunction errorResult(text: string): FlowsToolResult {\n return { content: [{ type: 'text', text }], isError: true }\n}\n\n/** The result of a failed read: the structured refusal when the proxy builds one, else the text. */\nasync function failedRead(deps: FlowsDependencies, err: unknown): Promise<FlowsToolResult> {\n if (deps.refusal) {\n try {\n return await deps.refusal(err)\n } catch {\n // The refusal builder itself failed: the plain text still names the cause.\n }\n }\n return errorResult(deps.describeFailure(err))\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return !!value && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction answerText(answer: GraphQueryAnswer): string {\n return (answer.content ?? [])\n .map((block) => (block.type === 'text' && typeof block.text === 'string' ? block.text : ''))\n .filter(Boolean)\n .join('\\n')\n .trim()\n}\n\n/**\n * The refusal of a read that did not answer, naming its code first:\n * error_detail.code when the graph endpoint sent its typed refusal, the code\n * its text starts with otherwise, and graph_query_failed when it names none.\n * The view marks the address it was expanding.\n */\nexport function refusalText(answer: GraphQueryAnswer): string {\n const text = answerText(answer)\n const detail = isRecord(answer.structuredContent)\n ? answer.structuredContent.error_detail\n : undefined\n const detailCode = isRecord(detail) && typeof detail.code === 'string' ? detail.code : ''\n const textCode = CODE_PREFIX.exec(text)?.[1] ?? ''\n const code = detailCode || textCode || 'graph_query_failed'\n const rest = textCode === code ? text.replace(CODE_PREFIX, '') : text\n return rest ? `${code}: ${rest}` : `${code}: the graph endpoint refused the read`\n}\n\n/** The endpoint says it cut the reply short (facts.query.truncated). */\nfunction cutByEndpoint(answer: GraphQueryAnswer): boolean {\n const facts = isRecord(answer.structuredContent) ? answer.structuredContent.facts : undefined\n const query = isRecord(facts) ? facts.query : undefined\n return isRecord(query) && query.truncated === true\n}\n\nfunction rowsOf(answer: GraphQueryAnswer): Row[] | null {\n const facts = isRecord(answer.structuredContent) ? answer.structuredContent.facts : undefined\n const query = isRecord(facts) ? facts.query : undefined\n const results = isRecord(query) ? query.results : undefined\n return Array.isArray(results) ? results.filter(isRecord) : null\n}\n\ntype Checked<T> = T | { error: string }\n\nfunction checkNetwork(args: Record<string, unknown>): Checked<string> {\n const rawNetwork = typeof args.network === 'string' ? args.network.trim().toLowerCase() : ''\n if (!rawNetwork) return { error: 'invalid_network: network is required; pass robinhood' }\n if (rawNetwork !== FLOWS_NETWORK) {\n return {\n error: `invalid_network: graph_expand reads the robinhood graph only; got \"${String(args.network).trim()}\"`,\n }\n }\n return FLOWS_NETWORK\n}\n\n/** The address of a node or of one end of a link; `name` says which end in a link refusal. */\nfunction checkAddress(raw: unknown, name?: string): Checked<string> {\n const address = normalizeFlowsAddress(raw)\n if (address) return address\n return {\n error: name\n ? INVALID_ADDRESS_TEXT.replace('invalid_address:', `invalid_address (${name}):`)\n : INVALID_ADDRESS_TEXT,\n }\n}\n\nfunction checkDay(raw: unknown): Checked<string> {\n const day = normalizeFlowsDay(raw)\n return day ?? { error: INVALID_DAY_TEXT }\n}\n\nfunction checkLimit(value: unknown): Checked<number> {\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 1 || value > TRANSFERS_MAX) {\n return { error: INVALID_LIMIT_TEXT }\n }\n return value\n}\n\nfunction checkSkip(value: unknown): Checked<number> {\n if (value === undefined || value === null) return 0\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) return { error: INVALID_SKIP_TEXT }\n return value\n}\n\nfunction checkFirstDay(value: unknown, day: string): Checked<string | null> {\n if (value === undefined || value === null) return null\n const first = normalizeFlowsDay(value)\n if (!first || first > day) return { error: INVALID_FIRST_DAY_TEXT }\n return first\n}\n\nfunction checkOffset(value: unknown): Checked<number> {\n if (value === undefined || value === null) return 0\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {\n return { error: OFFSET_NEGATIVE_TEXT }\n }\n if (value > FLOWS_MAX_OFFSET) return { error: OFFSET_TOO_LARGE_TEXT }\n return value\n}\n\n/**\n * Read and answer one node page. The reads run one after another: the node\n * first, so an address the graph does not hold costs one read; then the\n * receivers and the senders, the chunks of a side joined in order. A chunk that\n * comes back short, and not cut by the endpoint, ends its side, so the next\n * chunk of that side is not read or billed. The first read that does not\n * answer ends the call.\n */\nasync function runNode(\n deps: FlowsDependencies,\n network: string,\n address: string,\n inOffset: number,\n outOffset: number\n): Promise<FlowsToolResult> {\n const unavailable = deps.unavailable?.()\n if (unavailable) return errorResult(unavailable)\n const rows: Record<FlowsRead['id'], Row[]> = { node: [], out: [], in: [] }\n const ended = new Set<FlowsRead['id']>()\n for (const read of flowsReadsFor(address, inOffset, outOffset)) {\n if (ended.has(read.id)) continue\n let answer: GraphQueryAnswer\n try {\n answer = await deps.graphQuery({ network, query: read.query })\n } catch (err) {\n return failedRead(deps, err)\n }\n if (answer.isError === true) return errorResult(refusalText(answer))\n const found = rowsOf(answer)\n if (!found) {\n return errorResult(\n 'graph_query_failed: the graph endpoint answered without chain-insights.result.v1 rows'\n )\n }\n rows[read.id].push(...found)\n if (read.id === 'node' && found.length === 0) {\n return errorResult(`${address} is not in the ${network} topology graph`)\n }\n if (found.length < read.limit && !cutByEndpoint(answer)) ended.add(read.id)\n }\n const view = buildFlowView(network, {\n center: rows.node[0] ?? {},\n senders: rows.in,\n receivers: rows.out,\n inOffset,\n outOffset,\n })\n const now = deps.now ? deps.now() : new Date()\n return {\n content: [{ type: 'text', text: flowsSummary(view, now) }],\n structuredContent: view as unknown as Record<string, unknown>,\n isError: false,\n }\n}\n\n/** Read and answer the transfers of one link on one day: a single facts read. */\nasync function runLink(\n deps: FlowsDependencies,\n network: string,\n from: string,\n to: string,\n day: string\n): Promise<FlowsToolResult> {\n const unavailable = deps.unavailable?.()\n if (unavailable) return errorResult(unavailable)\n let answer: GraphQueryAnswer\n try {\n answer = await deps.graphQuery({ network, query: transfersReadFor(from, to, day) })\n } catch (err) {\n return failedRead(deps, err)\n }\n if (answer.isError === true) return errorResult(refusalText(answer))\n const rows = rowsOf(answer)\n if (!rows) {\n return errorResult(\n 'graph_query_failed: the graph endpoint answered without chain-insights.result.v1 rows'\n )\n }\n const view = buildTransfersView(network, from, to, day, rows)\n return {\n content: [{ type: 'text', text: transfersSummary(view) }],\n structuredContent: view as unknown as Record<string, unknown>,\n isError: false,\n }\n}\n\n/** One facts read of a day, as the paged walk needs it: the rows newest first, or the error that ends the call. */\nasync function readDay(\n deps: FlowsDependencies,\n network: string,\n from: string,\n to: string,\n day: string\n): Promise<{ rows: TransferRow[]; cut: boolean } | { error: string }> {\n let answer: GraphQueryAnswer\n try {\n answer = await deps.graphQuery({ network, query: transfersReadFor(from, to, day) })\n } catch (err) {\n return { error: deps.describeFailure(err) }\n }\n if (answer.isError === true) return { error: refusalText(answer) }\n const rows = rowsOf(answer)\n if (!rows) {\n return { error: 'graph_query_failed: the graph endpoint answered without chain-insights.result.v1 rows' }\n }\n const newestFirst = rows\n .map(transferOf)\n .sort((a, b) => (b.block_timestamp ?? -Infinity) - (a.block_timestamp ?? -Infinity))\n return { rows: newestFirst, cut: rows.length >= TRANSFERS_READ_ROWS }\n}\n\n/**\n * Read and answer one page of a link's transfers: from `day` back, one day a\n * read, until the page holds `limit` transfers, TRANSFERS_MAX_DAYS days were\n * read, or the day before first_day is reached. `skip` passes over the\n * transfers of `day` an earlier page listed. The first read that does not\n * answer ends the call.\n */\nasync function runLinkPage(\n deps: FlowsDependencies,\n network: string,\n from: string,\n to: string,\n day: string,\n page: TransfersPageRequest\n): Promise<FlowsToolResult> {\n const unavailable = deps.unavailable?.()\n if (unavailable) return errorResult(unavailable)\n const floor = page.firstDay ?? TRANSFERS_FLOOR_DAY\n const transfers: TransferRow[] = []\n let current = day\n let skip = page.skip\n let daysRead = 0\n let through = day\n let truncated = false\n let next: TransfersCursor | null = null\n for (;;) {\n if (current < floor) break\n if (daysRead >= TRANSFERS_MAX_DAYS) {\n next = { day: current, skip }\n break\n }\n const read = await readDay(deps, network, from, to, current)\n if ('error' in read) return errorResult(read.error)\n daysRead += 1\n through = current\n if (read.cut) truncated = true\n const rest = read.rows.slice(skip)\n const room = page.limit - transfers.length\n if (rest.length > room) {\n transfers.push(...rest.slice(0, room))\n next = { day: current, skip: skip + room }\n break\n }\n transfers.push(...rest)\n const earlier = dayBefore(current)\n if (transfers.length >= page.limit) {\n next = earlier < floor ? null : { day: earlier, skip: 0 }\n break\n }\n current = earlier\n skip = 0\n }\n const view: TransfersView = {\n schema: TRANSFERS_SCHEMA,\n network,\n from,\n to,\n day,\n transfers,\n truncated,\n through_day: through,\n days_read: daysRead,\n next,\n }\n return {\n content: [{ type: 'text', text: transfersSummary(view) }],\n structuredContent: view as unknown as Record<string, unknown>,\n isError: false,\n }\n}\n\n/** Read and answer the details of one address: a single anchored topology read. */\nasync function runDetails(\n deps: FlowsDependencies,\n network: string,\n address: string\n): Promise<FlowsToolResult> {\n const unavailable = deps.unavailable?.()\n if (unavailable) return errorResult(unavailable)\n let answer: GraphQueryAnswer\n try {\n answer = await deps.graphQuery({ network, query: detailsReadFor(address) })\n } catch (err) {\n return failedRead(deps, err)\n }\n if (answer.isError === true) return errorResult(refusalText(answer))\n const rows = rowsOf(answer)\n if (!rows) {\n return errorResult(\n 'graph_query_failed: the graph endpoint answered without chain-insights.result.v1 rows'\n )\n }\n if (rows.length === 0) return errorResult(`${address} is not in the ${network} topology graph`)\n const view = buildAddressView(network, address, rows[0]!)\n return {\n content: [{ type: 'text', text: addressSummary(view) }],\n structuredContent: view as unknown as Record<string, unknown>,\n isError: false,\n }\n}\n\nfunction given(record: Record<string, unknown>, keys: string[]): boolean {\n return keys.some((key) => record[key] !== undefined && record[key] !== null)\n}\n\n/**\n * graph_expand, the node form {network, address, in_offset?, out_offset?} or the\n * link form {network, from, to, day}. Every argument is checked before the\n * first read.\n */\nexport async function handleGraphExpand(\n args: unknown,\n deps: FlowsDependencies\n): Promise<FlowsToolResult> {\n const record = isRecord(args) ? args : {}\n const network = checkNetwork(record)\n if (typeof network !== 'string') return errorResult(network.error)\n\n const isLink = given(record, ['from', 'to', 'day'])\n if (isLink && given(record, ['address', 'in_offset', 'out_offset', 'details']))\n return errorResult(BOTH_FORMS_TEXT)\n\n if (isLink) {\n const from = checkAddress(record.from, 'from')\n if (typeof from !== 'string') return errorResult(from.error)\n const to = checkAddress(record.to, 'to')\n if (typeof to !== 'string') return errorResult(to.error)\n const day = checkDay(record.day)\n if (typeof day !== 'string') return errorResult(day.error)\n // A limit, a skip or a first_day asks for a page across days; without them the call is the one-day read.\n if (given(record, ['limit', 'skip', 'first_day'])) {\n const limit = checkLimit(record.limit)\n if (typeof limit !== 'number') return errorResult(limit.error)\n const skip = checkSkip(record.skip)\n if (typeof skip !== 'number') return errorResult(skip.error)\n const firstDay = checkFirstDay(record.first_day, day)\n if (firstDay !== null && typeof firstDay !== 'string') return errorResult(firstDay.error)\n return runLinkPage(deps, network, from, to, day, { skip, firstDay, limit })\n }\n return runLink(deps, network, from, to, day)\n }\n\n if (record.details !== undefined && record.details !== null) {\n if (record.details !== true) return errorResult(DETAILS_FORM_TEXT)\n if (given(record, ['in_offset', 'out_offset'])) return errorResult(DETAILS_FORM_TEXT)\n const address = checkAddress(record.address)\n if (typeof address !== 'string') return errorResult(address.error)\n return runDetails(deps, network, address)\n }\n\n const address = checkAddress(record.address)\n if (typeof address !== 'string') return errorResult(address.error)\n const inOffset = checkOffset(record.in_offset)\n if (typeof inOffset !== 'number') return errorResult(inOffset.error)\n const outOffset = checkOffset(record.out_offset)\n if (typeof outOffset !== 'number') return errorResult(outOffset.error)\n return runNode(deps, network, address, inOffset, outOffset)\n}\n\nexport const GRAPH_EXPAND_TITLE = 'Expand a node or a link in the picture'\n\nexport const GRAPH_EXPAND_DESCRIPTION =\n 'Expand an open Chain Insights picture. Called by the view when the investigator clicks, never by the model. ' +\n 'A node, {network, address, in_offset?, out_offset?}, loads the newest senders and receivers of one address ' +\n '(up to 250 a side, from the offsets); billed as at most five graph_query topology reads. A link, {network, from, to, day}, ' +\n 'lists the transfers between two addresses on one UTC day (YYYY-MM-DD), newest first, at most 50; billed as ' +\n 'one graph_query facts read. A link page, {network, from, to, day, limit, skip?, first_day?}, lists `limit` (1-50) ' +\n 'transfers from `day` back across days, newest first, and answers a `next` cursor ({day, skip}) while older ones ' +\n 'remain; billed as at most seven facts reads (a week). Details, {network, address, details: true}, answer the lifetime totals, ' +\n 'counts, role and labels of one address; billed as one topology read. Read-only; an argument refused before a read costs nothing.'\n","import { createHash } from 'node:crypto'\nimport { mkdir, readFile, rename, writeFile } from 'node:fs/promises'\nimport path from 'node:path'\nimport { CACHE_TTL_MS, chainInsightsHome } from './cache-location.js'\nimport type { GraphSchemaDocument } from './graph-schema.js'\n\n// The graph schema of one network on one endpoint, kept on disk beside the tool\n// list cache (src/mcp/schema-cache.ts) with the same home and the same 24 hour\n// lifetime: ~/.chain-insights/cache/schema-<network>-<endpoint hash>.json.\n//\n// A schema built while a read failed is kept for a short time only. Kept for\n// 24 hours, one refused or rate-limited read would hide a section of the schema\n// for a whole day.\n\n/** How long a schema with a failed read stays fresh. */\nexport const PARTIAL_SCHEMA_TTL_MS = 10 * 60 * 1000\n\ninterface GraphSchemaCacheFile {\n cached_at: number\n complete: boolean\n endpoint: string\n document: GraphSchemaDocument\n}\n\nfunction endpointHash(endpoint: string): string {\n return createHash('sha256').update(endpoint).digest('hex').slice(0, 12)\n}\n\n/** The cache file of a network on an endpoint. The network is an identifier already checked by the caller. */\nexport function graphSchemaCachePath(network: string, endpoint: string): string {\n return path.join(chainInsightsHome(), 'cache', `schema-${network}-${endpointHash(endpoint)}.json`)\n}\n\n/**\n * The cached schema, marked cached, or null when there is none, it is stale, or\n * the file cannot be read. A cache is derived data: a damaged file is a miss and\n * the next build replaces it.\n */\nexport async function loadGraphSchemaCache(\n network: string,\n endpoint: string,\n now: Date\n): Promise<GraphSchemaDocument | null> {\n let parsed: unknown\n try {\n parsed = JSON.parse(await readFile(graphSchemaCachePath(network, endpoint), 'utf8'))\n } catch {\n return null\n }\n const file = parsed as Partial<GraphSchemaCacheFile> | null\n if (\n !file ||\n typeof file.cached_at !== 'number' ||\n file.endpoint !== endpoint ||\n file.document?.schema !== 'chain-insights.graph-schema.v1'\n ) {\n return null\n }\n const lifetime = file.complete === true ? CACHE_TTL_MS : PARTIAL_SCHEMA_TTL_MS\n const age = now.getTime() - file.cached_at\n if (age < 0 || age > lifetime) return null\n return { ...file.document, cached: true }\n}\n\n/** `network` is the name the caller asked for, the same one a later load asks with. */\nexport async function saveGraphSchemaCache(\n network: string,\n document: GraphSchemaDocument,\n endpoint: string,\n complete: boolean,\n now: Date\n): Promise<void> {\n const target = graphSchemaCachePath(network, endpoint)\n const file: GraphSchemaCacheFile = {\n cached_at: now.getTime(),\n complete,\n endpoint,\n document: { ...document, cached: false },\n }\n await mkdir(path.dirname(target), { recursive: true })\n // Written beside the target and renamed, so a second proxy reading the file\n // never sees half of it.\n const scratch = `${target}.${process.pid}.tmp`\n await writeFile(scratch, JSON.stringify(file, null, 2) + '\\n', { mode: 0o600 })\n await rename(scratch, target)\n}\n","/**\n * meta_schema: the live graph schema of one network, read from the graph\n * endpoint and kept on disk for 24 hours.\n *\n * The graph serves four read-only catalog statements on `USE topology` through\n * the ordinary graph_query tool: `CALL db.labels()`, `CALL db.relationshipTypes()`,\n * `CALL db.propertyKeys()` and `SHOW INDEXES`. They name the labels, the link\n * types, every property key and the indexes. They do not say which keys belong to\n * which kind, so the main kinds are sampled with the two probe forms the server\n * admits (`keys(a)` on addresses, `keys(r)` on a link). The facts relationships\n * and the chain lookups come from the network's published capabilities.\n *\n * One build is one MCP session and runs its reads one after another: the public\n * endpoint allows about 30 requests a minute for each IP address, and opening a\n * session costs three of them. A read that fails leaves its section empty and\n * adds a note. It never fails the tool.\n */\n\nimport { findNetworkCapability, mirrorGraphNetworkCapabilities } from './capabilities.js'\nimport type { NetworkCapability } from './capabilities.js'\nimport { PaymentRequiredError } from './client.js'\nimport { refusalText, type GraphQueryAnswer } from './flows.js'\nimport { loadGraphSchemaCache, saveGraphSchemaCache } from './graph-schema-cache.js'\n\nexport const GRAPH_SCHEMA_ID = 'chain-insights.graph-schema.v1' as const\nexport const GRAPH_SCHEMA_SOURCE = 'live catalog' as const\nexport const META_SCHEMA_TITLE = 'Graph schema'\nexport const META_SCHEMA_DESCRIPTION =\n 'Return the live graph schema of one network: the labels, the link types, the field names of an address and of each main link, the indexes, the USE facts relationships with the TRANSFER columns, and the USE chain lookups. ' +\n 'Call it first when you need field names. It is cached for 24 hours; pass refresh=true to rebuild it.'\n\n/** Fields of an address come from this many sampled nodes, and of a link from this many sampled links. */\nexport const ADDRESS_SAMPLE_LIMIT = 20\nexport const LINK_SAMPLE_LIMIT = 5\n\n/** The link types an agent queries. Each is sampled only when the graph lists it. */\nexport const SAMPLED_LINK_TYPES = [\n 'FLOWS_TO',\n 'OPERATED_BY',\n 'LINKED',\n 'SWAPPED',\n 'BRIDGED',\n 'ADDED_LIQUIDITY',\n 'REMOVED_LIQUIDITY',\n] as const\n\n// The TRANSFER columns the cypher skill lists, in its order. tests/graph-schema.test.ts\n// holds this list to the skill and to the columns the graph server maps on TRANSFER\n// (tests/fixtures/facts-columns.json).\nexport const FACTS_TRANSFER_COLUMNS = [\n 'tx_id',\n 'block_date',\n 'block_height',\n 'block_timestamp',\n 'event_index',\n 'edge_index',\n 'kind',\n 'asset_contract',\n 'asset_symbol',\n 'amount',\n 'amount_usd',\n 'price_usd',\n 'price_missing',\n 'token_id',\n 'token_standard',\n 'operator_address',\n 'raw_amount',\n 'decimals',\n] as const\n\n// `CALL db.schema.visualization()` is not read. It bills about 427 units where\n// the four statements below bill 13 to 133 each, and it adds only which labels a\n// link type joins, which the cypher skill and the served hints already say.\nconst CATALOG_READS = {\n labels: { label: 'labels', query: 'USE topology CALL db.labels()' },\n linkTypes: { label: 'link types', query: 'USE topology CALL db.relationshipTypes()' },\n propertyKeys: { label: 'property keys', query: 'USE topology CALL db.propertyKeys()' },\n indexes: { label: 'indexes', query: 'USE topology SHOW INDEXES' },\n} as const\n\nexport function addressSampleQuery(): string {\n return `USE topology MATCH (a:Address) RETURN keys(a) AS keys LIMIT ${ADDRESS_SAMPLE_LIMIT}`\n}\n\nexport function linkSampleQuery(linkType: string): string {\n return `USE topology MATCH ()-[r:${linkType}]->() RETURN keys(r) AS keys LIMIT ${LINK_SAMPLE_LIMIT}`\n}\n\nexport interface GraphSchemaIndex {\n name: string\n entity: 'node' | 'link'\n /** The labels of a node index, the link types of a link index. */\n types: string[]\n properties: string[]\n state: string\n}\n\nexport interface GraphSchemaDocument {\n schema: typeof GRAPH_SCHEMA_ID\n network: string\n built_at: string\n source: typeof GRAPH_SCHEMA_SOURCE\n cached: boolean\n /** Units the reads of the build billed, summed from the replies. A cached document keeps the units of its build. */\n billed_units: number\n layers: {\n topology: {\n labels: string[]\n link_types: string[]\n property_keys: string[]\n indexes: GraphSchemaIndex[]\n samples: Record<string, string[]>\n }\n facts: { relationships: string[]; transfer_columns: string[] }\n chain: { lookups: string[] }\n }\n notes: string[]\n}\n\n/** A refusal with its own words for the caller: a bad argument, an unserved network, an endpoint that is down. */\nexport class GraphSchemaError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'GraphSchemaError'\n }\n}\n\nexport interface GraphSchemaClient {\n callTool(request: { name: string; arguments: Record<string, unknown> }): Promise<unknown>\n}\n\ntype Row = Record<string, unknown>\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return !!value && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction sortedUnique(values: Iterable<string>): string[] {\n return [...new Set(values)].sort()\n}\n\nfunction stringList(value: unknown): string[] {\n return Array.isArray(value)\n ? value.filter((entry): entry is string => typeof entry === 'string' && entry !== '')\n : []\n}\n\nconst NETWORK_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/\n\n/**\n * The network as a lower-case identifier. It becomes part of a file name and of\n * every query, so only an identifier passes.\n */\nexport function normalizeSchemaNetwork(raw: unknown): string {\n const network = typeof raw === 'string' ? raw.trim().toLowerCase() : ''\n if (!network) {\n throw new GraphSchemaError(\n 'invalid_network: network is required. Pass the identifier of a served network, for example robinhood.'\n )\n }\n if (!NETWORK_PATTERN.test(network)) {\n throw new GraphSchemaError(\n `invalid_network: \"${String(raw).trim()}\" is not a network identifier. Pass the identifier of a served network, for example robinhood.`\n )\n }\n return network\n}\n\n// ─── Reading ──────────────────────────────────────────────────────────────────\n\nfunction queryBlock(structured: unknown): Row | null {\n const facts = isRecord(structured) ? structured.facts : undefined\n const query = isRecord(facts) ? facts.query : undefined\n return isRecord(query) ? query : null\n}\n\n/** The `facts.query` block of one graph_query reply, from structuredContent or from the JSON text. */\nfunction answerQuery(answer: GraphQueryAnswer): Row | null {\n const fromStructured = queryBlock(answer.structuredContent)\n if (fromStructured) return fromStructured\n for (const block of answer.content ?? []) {\n if (block.type !== 'text' || typeof block.text !== 'string') continue\n try {\n const parsed = queryBlock(JSON.parse(block.text))\n if (parsed) return parsed\n } catch {\n /* not JSON: try the next block */\n }\n }\n return null\n}\n\n// A failure after which every later read would fail the same way: the endpoint\n// limits requests, or it asks for payment. The build stops sending reads.\ntype Halt = 'rate_limit' | 'payment'\n\nfunction haltOfError(err: unknown): Halt | undefined {\n if (err instanceof PaymentRequiredError) return 'payment'\n const code = isRecord(err) ? err.code : undefined\n const message = err instanceof Error ? err.message : String(err)\n if (code === 429 || /\\b429\\b|too many requests|rate.?limit/i.test(message)) return 'rate_limit'\n if (/\\b402\\b|payment required/i.test(message)) return 'payment'\n return undefined\n}\n\nfunction haltOfAnswer(answer: GraphQueryAnswer): Halt | undefined {\n const detail = isRecord(answer.structuredContent)\n ? answer.structuredContent.error_detail\n : undefined\n return isRecord(detail) && detail.class === 'capacity' ? 'rate_limit' : undefined\n}\n\nexport interface BuildOptions {\n client: GraphSchemaClient\n /** A normalized network identifier. */\n network: string\n now?: () => Date\n /** A wait between two reads, in milliseconds. One build needs none: it stays under the rate limit. */\n pauseMs?: number\n sleep?: (ms: number) => Promise<void>\n}\n\nexport interface BuildResult {\n document: GraphSchemaDocument\n /** False when a read failed: the document has a section missing. */\n complete: boolean\n}\n\nfunction describeIndex(row: Row): GraphSchemaIndex | null {\n const name = typeof row.name === 'string' ? row.name : ''\n const entity =\n row.entityType === 'NODE' ? 'node' : row.entityType === 'RELATIONSHIP' ? 'link' : null\n const properties = stringList(row.properties)\n // A lookup index names no label and no property: it tells an agent nothing.\n if (!name || !entity || properties.length === 0) return null\n return {\n name,\n entity,\n types: stringList(row.labelsOrTypes),\n properties,\n state: typeof row.state === 'string' && row.state ? row.state : 'unknown',\n }\n}\n\nexport async function buildGraphSchema(options: BuildOptions): Promise<BuildResult> {\n const { client } = options\n const now = options.now ?? (() => new Date())\n const pauseMs = options.pauseMs ?? 0\n const sleep = options.sleep ?? ((ms: number) => new Promise<void>((done) => setTimeout(done, ms)))\n\n const problems: string[] = []\n const remarks: string[] = []\n let billed = 0\n let calls = 0\n let halted = false\n\n async function pace(): Promise<void> {\n if (calls > 0 && pauseMs > 0) await sleep(pauseMs)\n calls += 1\n }\n\n function fail(label: string, reason: string, halt?: Halt): null {\n problems.push(`${label} could not be read: ${reason}`)\n if (halt && !halted) {\n halted = true\n if (halt === 'payment') {\n problems.push(\n 'The endpoint asks for payment, so the reads after it were not sent. Fix the payment setup, then call again with refresh=true.'\n )\n } else {\n // The endpoint says how long to wait: \"retry after 23 seconds\".\n const wait = /retry after (\\d+) seconds?/i.exec(reason)?.[1]\n problems.push(\n `The endpoint is limiting requests, so the reads after it were not sent. Call again after ${wait ? `${wait} seconds` : 'a minute'} with refresh=true.`\n )\n }\n }\n return null\n }\n\n async function graphRead(network: string, label: string, query: string): Promise<Row[] | null> {\n if (halted) return null\n await pace()\n let answer: GraphQueryAnswer\n try {\n answer = (await client.callTool({\n name: 'graph_query',\n arguments: { network, query },\n })) as GraphQueryAnswer\n } catch (err) {\n return fail(label, err instanceof Error ? err.message : String(err), haltOfError(err))\n }\n const block = answerQuery(answer ?? {})\n if (block && typeof block.billable_units === 'number') billed += block.billable_units\n if (answer?.isError === true) {\n return fail(label, refusalText(answer), haltOfAnswer(answer))\n }\n if (!block || !Array.isArray(block.results)) {\n return fail(label, 'the reply holds no rows')\n }\n return block.results.filter(isRecord)\n }\n\n // ── The network: capabilities first, so an unserved name costs no graph read.\n let network = options.network\n let capability: NetworkCapability | undefined\n await pace()\n try {\n const answer = (await client.callTool({\n name: 'network_capabilities',\n arguments: {},\n })) as GraphQueryAnswer\n if (answer?.isError === true) {\n fail('network capabilities', refusalText(answer), haltOfAnswer(answer))\n } else {\n const facts = isRecord(answer?.structuredContent) ? answer.structuredContent.facts : undefined\n const capabilities = isRecord(facts) ? facts.capabilities : undefined\n const networks =\n isRecord(capabilities) && Array.isArray(capabilities.networks)\n ? capabilities.networks\n : null\n if (!networks) {\n fail('network capabilities', 'the reply lists no networks')\n } else {\n const document = mirrorGraphNetworkCapabilities({ networks })\n capability = findNetworkCapability(document, options.network)\n if (!capability) {\n const served = document.networks.map((entry) => entry.network).join(', ')\n throw new GraphSchemaError(\n `network_not_served: \"${options.network}\" is not a network this endpoint serves. Served networks: ${served || 'none'}.`\n )\n }\n network = capability.network\n }\n }\n } catch (err) {\n if (err instanceof GraphSchemaError) throw err\n fail('network capabilities', err instanceof Error ? err.message : String(err), haltOfError(err))\n }\n\n // ── Topology: the catalog, then a sample of the main kinds.\n const topology: GraphSchemaDocument['layers']['topology'] = {\n labels: [],\n link_types: [],\n property_keys: [],\n indexes: [],\n samples: {},\n }\n const topologyOff = capability?.layers?.topology?.enabled === false\n if (topologyOff) {\n remarks.push('The topology layer is off on this network.')\n } else {\n const labels = await graphRead(network, CATALOG_READS.labels.label, CATALOG_READS.labels.query)\n if (labels) topology.labels = sortedUnique(labels.flatMap((row) => stringList([row.label])))\n\n const linkTypes = await graphRead(\n network,\n CATALOG_READS.linkTypes.label,\n CATALOG_READS.linkTypes.query\n )\n if (linkTypes) {\n topology.link_types = sortedUnique(\n linkTypes.flatMap((row) => stringList([row.relationshipType]))\n )\n }\n\n const keys = await graphRead(\n network,\n CATALOG_READS.propertyKeys.label,\n CATALOG_READS.propertyKeys.query\n )\n if (keys) {\n topology.property_keys = sortedUnique(keys.flatMap((row) => stringList([row.propertyKey])))\n }\n\n const indexes = await graphRead(\n network,\n CATALOG_READS.indexes.label,\n CATALOG_READS.indexes.query\n )\n if (indexes) {\n topology.indexes = indexes\n .map(describeIndex)\n .filter((index): index is GraphSchemaIndex => index !== null)\n .sort((a, b) => a.name.localeCompare(b.name))\n }\n\n // An address is sampled unless the label list was read and has no Address.\n const labelsRead = labels !== null\n if (!labelsRead || topology.labels.includes('Address')) {\n const rows = await graphRead(network, 'the Address sample', addressSampleQuery())\n if (rows) topology.samples['Address'] = unionOfKeys(rows)\n }\n\n const sampled = SAMPLED_LINK_TYPES.filter((type) => topology.link_types.includes(type))\n if (linkTypes === null && !halted) {\n problems.push('The link samples were skipped: the link types are unknown.')\n }\n for (const type of sampled) {\n const rows = await graphRead(network, `the ${type} sample`, linkSampleQuery(type))\n if (rows) topology.samples[type] = unionOfKeys(rows)\n }\n\n const unsampled = topology.link_types.filter(\n (type) => !(SAMPLED_LINK_TYPES as readonly string[]).includes(type)\n )\n if (unsampled.length > 0) {\n remarks.push(\n `Link types with no sample: ${unsampled.join(', ')}. Read the keys of one with USE topology MATCH ()-[r:<TYPE>]->() RETURN keys(r) AS keys LIMIT ${LINK_SAMPLE_LIMIT}.`\n )\n }\n }\n\n // ── Facts and chain: what the network publishes.\n const facts: GraphSchemaDocument['layers']['facts'] = { relationships: [], transfer_columns: [] }\n const chain: GraphSchemaDocument['layers']['chain'] = { lookups: [] }\n if (capability) {\n if (capability.layers?.facts?.enabled === false) {\n remarks.push('The facts layer is off on this network.')\n } else {\n facts.relationships = stringList(capability.layers?.facts?.relationships)\n if (facts.relationships.includes('TRANSFER')) {\n facts.transfer_columns = [...FACTS_TRANSFER_COLUMNS]\n }\n }\n const admission = capability.chain_admission\n if (admission?.enabled === false) {\n remarks.push(`The chain layer is off (status ${admission.status ?? 'unknown'}).`)\n } else {\n chain.lookups = stringList(admission?.lookups)\n }\n }\n\n const words = [...topology.property_keys, ...Object.values(topology.samples).flat()]\n if (words.includes('synced_through_height') || words.includes('pair_key')) {\n remarks.push(\n 'synced_through_height and pair_key are sync bookkeeping: never filter or sort on them.'\n )\n }\n remarks.push('Fields come from a sample of the live graph. A rare field may be missing.')\n\n return {\n complete: problems.length === 0,\n document: {\n schema: GRAPH_SCHEMA_ID,\n network,\n built_at: now().toISOString(),\n source: GRAPH_SCHEMA_SOURCE,\n cached: false,\n billed_units: billed,\n layers: { topology, facts, chain },\n notes: [...problems, ...remarks],\n },\n }\n}\n\nfunction unionOfKeys(rows: Row[]): string[] {\n return sortedUnique(rows.flatMap((row) => stringList(row.keys)))\n}\n\n// ─── Reading from the cache, or building ──────────────────────────────────────\n\nexport interface GetGraphSchemaOptions {\n /** As the caller wrote it: normalized here. */\n network: unknown\n refresh?: boolean\n endpoint: string\n /**\n * Runs `fn` with one connected client. It is called once for a build and not at\n * all when the cache answers, so a cache hit opens no session.\n */\n withClient: <T>(fn: (client: GraphSchemaClient) => Promise<T>) => Promise<T>\n now?: () => Date\n pauseMs?: number\n sleep?: (ms: number) => Promise<void>\n}\n\n// Builds in flight, by endpoint and network: two callers that ask for one\n// schema at once share one build and pay for it once.\nconst building = new Map<string, Promise<GraphSchemaDocument>>()\n\nexport async function getGraphSchema(options: GetGraphSchemaOptions): Promise<GraphSchemaDocument> {\n const network = normalizeSchemaNetwork(options.network)\n const now = options.now ?? (() => new Date())\n if (options.refresh !== true) {\n const cached = await loadGraphSchemaCache(network, options.endpoint, now())\n if (cached) return cached\n }\n const key = `${options.endpoint}\\n${network}`\n const running = building.get(key)\n if (running) return running\n const build = (async () => {\n const { document, complete } = await options.withClient((client) =>\n buildGraphSchema({\n client,\n network,\n now,\n ...(options.pauseMs === undefined ? {} : { pauseMs: options.pauseMs }),\n ...(options.sleep ? { sleep: options.sleep } : {}),\n })\n )\n try {\n await saveGraphSchemaCache(network, document, options.endpoint, complete, now())\n } catch (err) {\n document.notes.push(\n `The schema could not be saved to the cache: ${err instanceof Error ? err.message : String(err)}`\n )\n }\n return document\n })()\n building.set(key, build)\n try {\n return await build\n } finally {\n building.delete(key)\n }\n}\n\n// ─── Text ─────────────────────────────────────────────────────────────────────\n\nfunction listLine(title: string, values: string[], empty = 'none read'): string {\n return `${title}: ${values.length > 0 ? values.join(', ') : empty}`\n}\n\n/** The schema as the model and the terminal read it. */\nexport function formatGraphSchemaText(document: GraphSchemaDocument): string {\n const { topology, facts, chain } = document.layers\n const origin = document.cached\n ? 'from the cache, kept 24 hours; refresh=true rebuilds it'\n : `read now for ${document.billed_units} billed units and cached for 24 hours`\n const lines = [\n `Graph schema, network ${document.network}`,\n `Source: ${document.source}, built ${document.built_at} (${origin})`,\n '',\n 'USE topology',\n listLine('Labels', topology.labels),\n listLine('Link types', topology.link_types),\n listLine(`Property keys (${topology.property_keys.length})`, topology.property_keys),\n ]\n lines.push(topology.indexes.length > 0 ? 'Indexes:' : 'Indexes: none read')\n for (const index of topology.indexes) {\n lines.push(\n `- ${index.entity} ${index.types.join('|')}(${index.properties.join(', ')}) ${index.state}`\n )\n }\n const sampled = Object.entries(topology.samples)\n lines.push(sampled.length > 0 ? 'Fields, from a sample:' : 'Fields, from a sample: none read')\n for (const [kind, fields] of sampled) lines.push(`- ${kind}: ${fields.join(', ')}`)\n lines.push(\n '',\n 'USE facts',\n listLine('Relationships', facts.relationships),\n listLine('TRANSFER columns', facts.transfer_columns),\n '',\n 'USE chain',\n listLine('Lookups', chain.lookups)\n )\n if (document.notes.length > 0) {\n lines.push('', 'Notes:', ...document.notes.map((note) => `- ${note}`))\n }\n return lines.join('\\n')\n}\n\n// ─── The MCP tool ─────────────────────────────────────────────────────────────\n\nexport interface GraphSchemaDependencies {\n endpoint: string\n withClient: GetGraphSchemaOptions['withClient']\n /** The text of a failure that is not a GraphSchemaError: payment required, transport. */\n describeFailure: (err: unknown) => string\n now?: () => Date\n pauseMs?: number\n sleep?: (ms: number) => Promise<void>\n}\n\nexport type MetaSchemaResult = {\n content: Array<{ type: 'text'; text: string }>\n structuredContent?: Record<string, unknown>\n isError: boolean\n}\n\nfunction errorResult(text: string): MetaSchemaResult {\n return { content: [{ type: 'text', text }], isError: true }\n}\n\n/** meta_schema, {network, refresh?}. Every argument is checked before the first read. */\nexport async function handleMetaSchema(\n args: unknown,\n deps: GraphSchemaDependencies\n): Promise<MetaSchemaResult> {\n const record = isRecord(args) ? args : {}\n const refresh = record.refresh\n if (refresh !== undefined && refresh !== null && typeof refresh !== 'boolean') {\n return errorResult('invalid_refresh: refresh must be true or false')\n }\n try {\n const document = await getGraphSchema({\n network: record.network,\n refresh: refresh === true,\n endpoint: deps.endpoint,\n withClient: deps.withClient,\n ...(deps.now ? { now: deps.now } : {}),\n ...(deps.pauseMs === undefined ? {} : { pauseMs: deps.pauseMs }),\n ...(deps.sleep ? { sleep: deps.sleep } : {}),\n })\n return {\n content: [{ type: 'text', text: formatGraphSchemaText(document) }],\n structuredContent: document as unknown as Record<string, unknown>,\n isError: false,\n }\n } catch (err) {\n return errorResult(err instanceof GraphSchemaError ? err.message : deps.describeFailure(err))\n }\n}\n"],"mappings":"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"}
|
package/dist/mcp-proxy.cjs
CHANGED
|
@@ -6,7 +6,7 @@ const require_tool_visibility = require("./tool-visibility-BopFnGsT.cjs");
|
|
|
6
6
|
const require_usage_status = require("./usage-status-DonwoLR3.cjs");
|
|
7
7
|
const require_subscription_status = require("./subscription-status-ru99UCPZ.cjs");
|
|
8
8
|
const require_capabilities = require("./capabilities-DGavTSoW.cjs");
|
|
9
|
-
const require_graph_schema = require("./graph-schema-
|
|
9
|
+
const require_graph_schema = require("./graph-schema-BoUMa03H.cjs");
|
|
10
10
|
const require_action_log = require("./action-log-HniMMF7f.cjs");
|
|
11
11
|
const require_dex_tool = require("./dex-tool-DIKN4dXB.cjs");
|
|
12
12
|
const require_dex_registry = require("./dex-registry-KOTJhAQf.cjs");
|
|
@@ -273,7 +273,7 @@ function routingHintLines() {
|
|
|
273
273
|
return [`- Pick the layer first. ${ROUTING_RULE} 1. ${ROUTING_LINES[0]} 2. ${ROUTING_LINES[1]} 3. ${ROUTING_LINES[2]} ${ROUTING_NOT_SERVED} ${ROUTING_ADDRESS_ON_CHAIN} ${ROUTING_TWO_FIT} ${ROUTING_HANDOFF} The lookups that USE chain serves are listed in chain_admission.lookups of meta_network_capabilities. Read every limit that the server publishes from meta_network_capabilities (chain_admission and, when the server sends them, topology_admission and facts_admission). Never write a limit down.`, `- Act on a refusal by its class. ${moveLines().join(" ")}`];
|
|
274
274
|
}
|
|
275
275
|
const VIEW_BODY_LIMIT_BYTES = 65536;
|
|
276
|
-
const VIEW_PAGE_TOOLS = /* @__PURE__ */ new Set(["graph_expand"]);
|
|
276
|
+
const VIEW_PAGE_TOOLS = /* @__PURE__ */ new Set(["graph_expand", "meta_usage_status"]);
|
|
277
277
|
/** A fresh answer id: 12 random bytes, URL-safe base64 (16 characters). */
|
|
278
278
|
function newViewId() {
|
|
279
279
|
return (0, node_crypto.randomBytes)(12).toString("base64url");
|
|
@@ -622,6 +622,8 @@ const LOCAL_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
|
622
622
|
require_graph_schema.GRAPH_EXPAND_TOOL
|
|
623
623
|
]);
|
|
624
624
|
const APP_ONLY_LOCAL_TOOL_NAMES = /* @__PURE__ */ new Set([require_graph_schema.GRAPH_EXPAND_TOOL]);
|
|
625
|
+
/** The structured refusal of a click refused for money; the view reads it (chain-insights-ui apps/claude-view/credits.ts). */
|
|
626
|
+
const REFUSAL_SCHEMA = "chain-insights.refusal.v1";
|
|
625
627
|
const GRAPH_ARRAY_KEYS = [
|
|
626
628
|
"nodes",
|
|
627
629
|
"edges",
|
|
@@ -1298,22 +1300,56 @@ async function createProxy() {
|
|
|
1298
1300
|
});
|
|
1299
1301
|
}
|
|
1300
1302
|
const remoteToolNames = new Set((tools ?? []).map((tool) => tool.name));
|
|
1301
|
-
async function
|
|
1303
|
+
async function paymentRefusal(toolName, err) {
|
|
1302
1304
|
const text = remoteCallFailureText(toolName, err);
|
|
1303
|
-
|
|
1305
|
+
const isPayment = err instanceof require_client.PaymentRequiredError || /\b402\b|payment/i.test(err.message ?? "");
|
|
1306
|
+
if (!isPayment || !remoteConnected || !remoteToolNames.has("usage_status")) return {
|
|
1307
|
+
text,
|
|
1308
|
+
payment: isPayment,
|
|
1309
|
+
spent: false
|
|
1310
|
+
};
|
|
1304
1311
|
try {
|
|
1305
1312
|
const result = await remoteClient.callTool({
|
|
1306
1313
|
name: "usage_status",
|
|
1307
1314
|
arguments: {}
|
|
1308
1315
|
});
|
|
1309
1316
|
const facts = isRecord(result.structuredContent) ? result.structuredContent["facts"] : void 0;
|
|
1310
|
-
const usage = isRecord(facts) ? facts["usage"] : void 0;
|
|
1317
|
+
const usage = isRecord(facts) && isRecord(facts["usage"]) ? facts["usage"] : void 0;
|
|
1311
1318
|
const spent = require_usage_status.freeTierSpentText(usage);
|
|
1312
|
-
return
|
|
1319
|
+
return {
|
|
1320
|
+
text: spent ? `${spent} ${text}` : text,
|
|
1321
|
+
payment: true,
|
|
1322
|
+
usage,
|
|
1323
|
+
spent: Boolean(spent)
|
|
1324
|
+
};
|
|
1313
1325
|
} catch {
|
|
1314
|
-
return
|
|
1326
|
+
return {
|
|
1327
|
+
text,
|
|
1328
|
+
payment: true,
|
|
1329
|
+
spent: false
|
|
1330
|
+
};
|
|
1315
1331
|
}
|
|
1316
1332
|
}
|
|
1333
|
+
async function paymentFailureText(toolName, err) {
|
|
1334
|
+
return (await paymentRefusal(toolName, err)).text;
|
|
1335
|
+
}
|
|
1336
|
+
async function clickRefusal(toolName, err) {
|
|
1337
|
+
const refusal = await paymentRefusal(toolName, err);
|
|
1338
|
+
const structuredContent = refusal.payment ? {
|
|
1339
|
+
schema: REFUSAL_SCHEMA,
|
|
1340
|
+
tool: toolName,
|
|
1341
|
+
code: refusal.spent ? "free_tier_spent" : "payment_failed",
|
|
1342
|
+
...refusal.usage ? { usage: refusal.usage } : {}
|
|
1343
|
+
} : void 0;
|
|
1344
|
+
return {
|
|
1345
|
+
content: [{
|
|
1346
|
+
type: "text",
|
|
1347
|
+
text: refusal.text
|
|
1348
|
+
}],
|
|
1349
|
+
isError: true,
|
|
1350
|
+
...structuredContent ? { structuredContent } : {}
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1317
1353
|
const server = new _modelcontextprotocol_sdk_server_mcp_js.McpServer({
|
|
1318
1354
|
name: "chain-insights",
|
|
1319
1355
|
version: require_print_result.PACKAGE_VERSION
|
|
@@ -1411,18 +1447,7 @@ async function createProxy() {
|
|
|
1411
1447
|
},
|
|
1412
1448
|
annotations: require_dex_tool.META_DEX_LOOKUP_ANNOTATIONS
|
|
1413
1449
|
}, async (args) => require_dex_tool.handleMetaDexLookup(args, dexDependencies));
|
|
1414
|
-
|
|
1415
|
-
title: "Usage Status",
|
|
1416
|
-
description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.meta_usage_status,
|
|
1417
|
-
inputSchema: EMPTY_INPUT_SCHEMA,
|
|
1418
|
-
_meta: VIEW_TOOL_META,
|
|
1419
|
-
annotations: {
|
|
1420
|
-
readOnlyHint: true,
|
|
1421
|
-
destructiveHint: false,
|
|
1422
|
-
idempotentHint: true,
|
|
1423
|
-
openWorldHint: true
|
|
1424
|
-
}
|
|
1425
|
-
}, async () => {
|
|
1450
|
+
const readUsageStatus = async () => {
|
|
1426
1451
|
try {
|
|
1427
1452
|
await ensureRemote();
|
|
1428
1453
|
if (!remoteConnected) return {
|
|
@@ -1459,7 +1484,19 @@ async function createProxy() {
|
|
|
1459
1484
|
isError: true
|
|
1460
1485
|
};
|
|
1461
1486
|
}
|
|
1462
|
-
}
|
|
1487
|
+
};
|
|
1488
|
+
server.registerTool("meta_usage_status", {
|
|
1489
|
+
title: "Usage Status",
|
|
1490
|
+
description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.meta_usage_status,
|
|
1491
|
+
inputSchema: EMPTY_INPUT_SCHEMA,
|
|
1492
|
+
_meta: VIEW_TOOL_META,
|
|
1493
|
+
annotations: {
|
|
1494
|
+
readOnlyHint: true,
|
|
1495
|
+
destructiveHint: false,
|
|
1496
|
+
idempotentHint: true,
|
|
1497
|
+
openWorldHint: true
|
|
1498
|
+
}
|
|
1499
|
+
}, async () => readUsageStatus());
|
|
1463
1500
|
server.registerTool("meta_subscription_status", {
|
|
1464
1501
|
title: "Subscription Status",
|
|
1465
1502
|
description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.meta_subscription_status,
|
|
@@ -1545,6 +1582,7 @@ async function createProxy() {
|
|
|
1545
1582
|
}, void 0, options));
|
|
1546
1583
|
},
|
|
1547
1584
|
describeFailure: (err) => remoteCallFailureText("graph_query", err),
|
|
1585
|
+
refusal: (err) => clickRefusal("graph_query", err),
|
|
1548
1586
|
unavailable: () => remoteConnected ? void 0 : `${remoteUnavailableMessage ?? `Chain Insights Graph is not connected at ${graphMcpEndpoint}`}. Restart the Chain Insights MCP proxy after the endpoint is reachable.`
|
|
1549
1587
|
};
|
|
1550
1588
|
const runGraphExpand = (args) => require_graph_schema.handleGraphExpand(args, flowsDependencies);
|
|
@@ -1554,6 +1592,7 @@ async function createProxy() {
|
|
|
1554
1592
|
html: readClaudeViewHtml,
|
|
1555
1593
|
runTool: async (name, args) => {
|
|
1556
1594
|
if (name === "graph_expand") return runGraphExpand(args);
|
|
1595
|
+
if (name === "meta_usage_status") return readUsageStatus();
|
|
1557
1596
|
throw new Error(`The browser page cannot call ${name}`);
|
|
1558
1597
|
},
|
|
1559
1598
|
log: (event, fields) => {
|
|
@@ -1789,6 +1828,7 @@ exports.KNOWN_PUBLIC_TOOL_DESCRIPTIONS = KNOWN_PUBLIC_TOOL_DESCRIPTIONS;
|
|
|
1789
1828
|
exports.LOCAL_TOOL_NAMES = LOCAL_TOOL_NAMES;
|
|
1790
1829
|
exports.MCP_APP_MIME_TYPE = MCP_APP_MIME_TYPE;
|
|
1791
1830
|
exports.PICTURE_RULES = PICTURE_RULES;
|
|
1831
|
+
exports.REFUSAL_SCHEMA = REFUSAL_SCHEMA;
|
|
1792
1832
|
exports.createProxy = createProxy;
|
|
1793
1833
|
exports.knownPublicToolInputSchema = knownPublicToolInputSchema;
|
|
1794
1834
|
exports.readClaudeViewHtml = readClaudeViewHtml;
|
package/dist/mcp-proxy.d.cts
CHANGED
|
@@ -2,6 +2,8 @@ import * as z from "zod";
|
|
|
2
2
|
//#region src/mcp/proxy.d.ts
|
|
3
3
|
export declare const LOCAL_TOOL_NAMES: Set<string>;
|
|
4
4
|
export declare const APP_ONLY_LOCAL_TOOL_NAMES: Set<string>;
|
|
5
|
+
/** The structured refusal of a click refused for money; the view reads it (chain-insights-ui apps/claude-view/credits.ts). */
|
|
6
|
+
export declare const REFUSAL_SCHEMA = "chain-insights.refusal.v1";
|
|
5
7
|
export declare const CLAUDE_VIEW_URI = "ui://chain-insights/view";
|
|
6
8
|
export declare const MCP_APP_MIME_TYPE = "text/html;profile=mcp-app";
|
|
7
9
|
/**
|
package/dist/mcp-proxy.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-proxy.d.cts","names":[],"sources":["../src/mcp/proxy.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"mcp-proxy.d.cts","names":[],"sources":["../src/mcp/proxy.ts"],"mappings":";;qBA6Da,kBAAgB;qBAahB,2BAAyB;;qBAEzB;qBAUA;qBACA;;;;;wBAyBG;YAkBJ;wBAEI,oBAAoB,MAAK,OAAO,aAA2B;qBAiB9D;qBAGA,gCAAgC;KAuBxC,iBAAiB,eAAe,EAAE;qBAgB1B;wBA2GG,2BAA2B,mBAAmB;;;;;;;;wBAomBxC,eAAe"}
|
package/dist/mcp-proxy.d.mts
CHANGED
|
@@ -2,6 +2,8 @@ import * as z from "zod";
|
|
|
2
2
|
//#region src/mcp/proxy.d.ts
|
|
3
3
|
export declare const LOCAL_TOOL_NAMES: Set<string>;
|
|
4
4
|
export declare const APP_ONLY_LOCAL_TOOL_NAMES: Set<string>;
|
|
5
|
+
/** The structured refusal of a click refused for money; the view reads it (chain-insights-ui apps/claude-view/credits.ts). */
|
|
6
|
+
export declare const REFUSAL_SCHEMA = "chain-insights.refusal.v1";
|
|
5
7
|
export declare const CLAUDE_VIEW_URI = "ui://chain-insights/view";
|
|
6
8
|
export declare const MCP_APP_MIME_TYPE = "text/html;profile=mcp-app";
|
|
7
9
|
/**
|
package/dist/mcp-proxy.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-proxy.d.mts","names":[],"sources":["../src/mcp/proxy.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"mcp-proxy.d.mts","names":[],"sources":["../src/mcp/proxy.ts"],"mappings":";;qBA6Da,kBAAgB;qBAahB,2BAAyB;;qBAEzB;qBAUA;qBACA;;;;;wBAyBG;YAkBJ;wBAEI,oBAAoB,MAAK,OAAO,aAA2B;qBAiB9D;qBAGA,gCAAgC;KAuBxC,iBAAiB,eAAe,EAAE;qBAgB1B;wBA2GG,2BAA2B,mBAAmB;;;;;;;;wBAomBxC,eAAe"}
|