chain-insights 0.71.4 → 0.73.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -3
- package/dist/buy-97M6dtEu.cjs +195 -0
- package/dist/buy-_mpdRieG.mjs +195 -0
- package/dist/buy-_mpdRieG.mjs.map +1 -0
- package/dist/{capabilities-DGavTSoW.cjs → capabilities-Ds3320iS.cjs} +1 -1
- package/dist/{capabilities-BBVDIECk.mjs → capabilities-DxojbX4D.mjs} +2 -2
- package/dist/{capabilities-BBVDIECk.mjs.map → capabilities-DxojbX4D.mjs.map} +1 -1
- package/dist/{claude-desktop-CTaBCUIm.mjs → claude-desktop-6jwX0Lux.mjs} +18 -3
- package/dist/{claude-desktop-CTaBCUIm.mjs.map → claude-desktop-6jwX0Lux.mjs.map} +1 -1
- package/dist/{claude-desktop-BI_mGkZ6.cjs → claude-desktop-Jmm4PlBZ.cjs} +17 -2
- package/dist/cli.cjs +163 -31
- package/dist/cli.mjs +163 -31
- package/dist/cli.mjs.map +1 -1
- package/dist/{client-gsLi528K.mjs → client-7qOfAdpH.mjs} +31 -12
- package/dist/client-7qOfAdpH.mjs.map +1 -0
- package/dist/{client-Ypw9BNbn.cjs → client-C9uwHnBS.cjs} +33 -14
- package/dist/{dex-registry-KOTJhAQf.cjs → dex-registry-Chs79Ukc.cjs} +11 -8
- package/dist/{dex-registry-05wAhSa0.mjs → dex-registry-rziydXYq.mjs} +12 -9
- package/dist/{dex-registry-05wAhSa0.mjs.map → dex-registry-rziydXYq.mjs.map} +1 -1
- package/dist/{dex-tool-B4T8Czhm.cjs → dex-tool-DLWnff1n.cjs} +6 -4
- package/dist/{dex-tool-B5Qwz1Zd.mjs → dex-tool-DsaAIa-a.mjs} +7 -5
- package/dist/{dex-tool-B5Qwz1Zd.mjs.map → dex-tool-DsaAIa-a.mjs.map} +1 -1
- package/dist/{graph-schema-BoUMa03H.cjs → graph-schema-BGbJumCY.cjs} +63 -25
- package/dist/{graph-schema-DqZUTnP0.mjs → graph-schema-ClqjzgrM.mjs} +59 -27
- package/dist/{graph-schema-DqZUTnP0.mjs.map → graph-schema-ClqjzgrM.mjs.map} +1 -1
- package/dist/index.cjs +5 -5
- package/dist/index.d.cts +7 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +7 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +5 -5
- package/dist/known-keys-B87ZqOgE.mjs +78 -0
- package/dist/known-keys-B87ZqOgE.mjs.map +1 -0
- package/dist/known-keys-PKZF5GYd.cjs +99 -0
- package/dist/mcp-proxy.cjs +86 -39
- package/dist/mcp-proxy.d.cts.map +1 -1
- package/dist/mcp-proxy.d.mts.map +1 -1
- package/dist/mcp-proxy.mjs +86 -39
- package/dist/mcp-proxy.mjs.map +1 -1
- package/dist/{public-tools-Dj130EuJ.cjs → public-tools-Bl_l_rKK.cjs} +8 -5
- package/dist/{public-tools-B77ZT0jt.mjs → public-tools-CQjGt7_q.mjs} +9 -6
- package/dist/{public-tools-B77ZT0jt.mjs.map → public-tools-CQjGt7_q.mjs.map} +1 -1
- package/dist/quote-B9sYNOiv.cjs +70 -0
- package/dist/quote-DfTut0cx.mjs +68 -0
- package/dist/quote-DfTut0cx.mjs.map +1 -0
- package/dist/robinhood-CCTMagwM.mjs +226 -0
- package/dist/robinhood-CCTMagwM.mjs.map +1 -0
- package/dist/robinhood-ChiT69sc.cjs +313 -0
- package/dist/rules-BD-cNHMX.cjs +31 -0
- package/dist/rules-BWdbOtHi.mjs +10 -0
- package/dist/rules-BWdbOtHi.mjs.map +1 -0
- package/dist/{schema-cache-DwddPLcY.cjs → schema-cache-C7v9ml6S.cjs} +1 -1
- package/dist/{schema-cache-BT-kHy_r.mjs → schema-cache-DRObKUvS.mjs} +3 -3
- package/dist/{schema-cache-BT-kHy_r.mjs.map → schema-cache-DRObKUvS.mjs.map} +1 -1
- package/dist/{skill-refresh-FXhQpW94.cjs → skill-refresh-Be2D5FrX.cjs} +2 -2
- package/dist/{skill-refresh-I5MSULwP.mjs → skill-refresh-xYWZ_hua.mjs} +3 -3
- package/dist/{skill-refresh-I5MSULwP.mjs.map → skill-refresh-xYWZ_hua.mjs.map} +1 -1
- package/dist/subscription-status-9tMfD90J.cjs +168 -0
- package/dist/subscription-status-DIuH84fM.mjs +148 -0
- package/dist/subscription-status-DIuH84fM.mjs.map +1 -0
- package/dist/{tools-BHfphgy9.cjs → tools-Cmnq7yip.cjs} +21 -4
- package/dist/{tools-BnThuvE2.mjs → tools-D9ep9E7Q.mjs} +22 -5
- package/dist/tools-D9ep9E7Q.mjs.map +1 -0
- package/dist/topup--nWtDnWP.cjs +43 -0
- package/dist/topup-CfFe1bGS.mjs +45 -0
- package/dist/topup-CfFe1bGS.mjs.map +1 -0
- package/dist/{topup-server-DzeM_YAt.mjs → topup-server-B0VKHslr.mjs} +2 -2
- package/dist/{topup-server-DzeM_YAt.mjs.map → topup-server-B0VKHslr.mjs.map} +1 -1
- package/dist/{topup-server-BIbSfvX5.cjs → topup-server-BkGM8P99.cjs} +1 -1
- package/dist/{topup-server-Bh7MlzRt.cjs → topup-server-C58eWflv.cjs} +284 -69
- package/dist/{topup-server-BrkPhqJG.mjs → topup-server-RQ6-c92F.mjs} +274 -71
- package/dist/topup-server-RQ6-c92F.mjs.map +1 -0
- package/dist/{wallet-B3hAr70M.cjs → wallet-CgIAfdxL.cjs} +34 -0
- package/dist/{wallet-GNDl3FkR.mjs → wallet-QTo5YCFR.mjs} +35 -1
- package/dist/wallet-QTo5YCFR.mjs.map +1 -0
- package/docs/development.md +1 -1
- package/docs/graph-tools.md +2 -1
- package/docs/mcp-proxy.md +30 -4
- package/docs/stability.md +3 -2
- package/package.json +1 -1
- package/skills/chain-insights-cypher/SKILL.md +9 -0
- package/dist/client-gsLi528K.mjs.map +0 -1
- package/dist/subscription-status-DA9DCFrC.mjs +0 -32
- package/dist/subscription-status-DA9DCFrC.mjs.map +0 -1
- package/dist/subscription-status-ru99UCPZ.cjs +0 -40
- package/dist/tools-BnThuvE2.mjs.map +0 -1
- package/dist/topup-server-BrkPhqJG.mjs.map +0 -1
- package/dist/wallet-GNDl3FkR.mjs.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
|
|
2
|
-
const require_client = require("./client-
|
|
3
|
-
const require_capabilities = require("./capabilities-
|
|
2
|
+
const require_client = require("./client-C9uwHnBS.cjs");
|
|
3
|
+
const require_capabilities = require("./capabilities-Ds3320iS.cjs");
|
|
4
4
|
const require_cache_location = require("./cache-location-CBVzUIxq.cjs");
|
|
5
5
|
let node_path = require("node:path");
|
|
6
6
|
node_path = require_rolldown_runtime.__toESM(node_path, 1);
|
|
@@ -45,13 +45,42 @@ let node_crypto = require("node:crypto");
|
|
|
45
45
|
const FLOWS_SCHEMA = "chain-insights.flows.v1";
|
|
46
46
|
const GRAPH_EXPAND_TOOL = "graph_expand";
|
|
47
47
|
const FLOWS_NETWORK = "robinhood";
|
|
48
|
+
/** How many senders and how many receivers one page shows, at most. */
|
|
49
|
+
const FLOWS_PER_SIDE = 250;
|
|
50
|
+
/** The view data of one answer: at most this many addresses, the centre included... */
|
|
51
|
+
const FLOWS_MAX_NODES = 500;
|
|
52
|
+
/**
|
|
53
|
+
* ...and under this many characters of JSON. Hosts drop an app tool result
|
|
54
|
+
* over about 150,000 characters.
|
|
55
|
+
*/
|
|
56
|
+
const FLOWS_MAX_CHARS = 14e4;
|
|
57
|
+
/**
|
|
58
|
+
* The graph endpoint cuts a reply at 200 rows, whatever the LIMIT, so one side
|
|
59
|
+
* of a page is read in reads of at most this many rows.
|
|
60
|
+
*/
|
|
61
|
+
const FLOWS_READ_ROWS = 200;
|
|
48
62
|
/** Where a graph_expand page may start on each side, at most. */
|
|
49
63
|
const FLOWS_MAX_OFFSET = 1e4;
|
|
50
64
|
const TRANSFERS_SCHEMA = "chain-insights.transfers.v1";
|
|
65
|
+
/** A link answer lists at most this many transfers... */
|
|
66
|
+
const TRANSFERS_MAX = 50;
|
|
67
|
+
/**
|
|
68
|
+
* ...from the rows one read brings back. A facts read takes no ORDER BY and
|
|
69
|
+
* the server cuts a reply at 200 rows, so the read asks for the whole cap and
|
|
70
|
+
* the newest rows are picked here.
|
|
71
|
+
*/
|
|
72
|
+
const TRANSFERS_READ_ROWS = 200;
|
|
73
|
+
/** The text of a link answer names at most this many transfers. */
|
|
74
|
+
const TRANSFERS_SUMMARY_ROWS = 5;
|
|
75
|
+
/** 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. */
|
|
76
|
+
const TRANSFERS_MAX_DAYS = 7;
|
|
77
|
+
/** Where a paged link answer stops reading back when the caller names no first_day. */
|
|
78
|
+
const TRANSFERS_FLOOR_DAY = "2015-01-01";
|
|
51
79
|
const ADDRESS_SCHEMA = "chain-insights.address.v1";
|
|
52
80
|
const INVALID_ADDRESS_TEXT = "invalid_address: a full 0x address of 40 hexadecimal characters is needed, for example 0x04911a118f11c75667e4d0dfb8e640af5a353550";
|
|
53
81
|
const OFFSET_TOO_LARGE_TEXT = `invalid_offset: in_offset and out_offset must each be at most ${FLOWS_MAX_OFFSET}`;
|
|
54
82
|
const OFFSET_NEGATIVE_TEXT = "invalid_offset: in_offset and out_offset must each be a whole number, 0 or more";
|
|
83
|
+
const INVALID_DAY_TEXT = "invalid_day: day must be a calendar date written YYYY-MM-DD (UTC), for example 2026-07-10";
|
|
55
84
|
const BOTH_FORMS_TEXT = "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";
|
|
56
85
|
const INVALID_LIMIT_TEXT = "invalid_limit: limit must be a whole number from 1 to 50";
|
|
57
86
|
const INVALID_SKIP_TEXT = "invalid_skip: skip must be a whole number of 0 or more";
|
|
@@ -90,7 +119,7 @@ function normalizeFlowsDay(raw) {
|
|
|
90
119
|
* checks, so they are written into the read as literals.
|
|
91
120
|
*/
|
|
92
121
|
function transfersReadFor(from, to, day) {
|
|
93
|
-
return `USE facts MATCH (a:Address {address: "${from}"})-[t:TRANSFER]->(b:Address {address: "${to}"}) WHERE t.block_date = "${day}" RETURN t.tx_id AS tx_id, t.block_timestamp AS block_timestamp, t.amount AS amount, t.asset_symbol AS asset_symbol, t.amount_usd AS amount_usd LIMIT
|
|
122
|
+
return `USE facts MATCH (a:Address {address: "${from}"})-[t:TRANSFER]->(b:Address {address: "${to}"}) WHERE t.block_date = "${day}" RETURN t.tx_id AS tx_id, t.block_timestamp AS block_timestamp, t.amount AS amount, t.asset_symbol AS asset_symbol, t.amount_usd AS amount_usd LIMIT ${TRANSFERS_READ_ROWS}`;
|
|
94
123
|
}
|
|
95
124
|
/** The UTC day before a YYYY-MM-DD day. */
|
|
96
125
|
function dayBefore(day) {
|
|
@@ -139,9 +168,9 @@ const PAGE_ORDER = " ORDER BY f.last_seen_timestamp DESC, b.address";
|
|
|
139
168
|
function flowsReadsFor(address, inOffset, outOffset) {
|
|
140
169
|
const side = (id, offset, match) => {
|
|
141
170
|
const reads = [];
|
|
142
|
-
for (let done = 0; done < 251; done +=
|
|
171
|
+
for (let done = 0; done < 251; done += FLOWS_READ_ROWS) {
|
|
143
172
|
const skip = offset + done;
|
|
144
|
-
const limit = Math.min(
|
|
173
|
+
const limit = Math.min(FLOWS_READ_ROWS, 251 - done);
|
|
145
174
|
reads.push({
|
|
146
175
|
id,
|
|
147
176
|
query: `USE topology MATCH ${match} RETURN ${PARTY_FIELDS}${PAGE_ORDER}${skip > 0 ? ` SKIP ${skip}` : ""} LIMIT ${limit}`,
|
|
@@ -234,7 +263,7 @@ function assembleFlowView(network, page, senders, receivers, truncated) {
|
|
|
234
263
|
return view;
|
|
235
264
|
}
|
|
236
265
|
function fitsFlowBudget(view) {
|
|
237
|
-
return view.nodes.length <=
|
|
266
|
+
return view.nodes.length <= FLOWS_MAX_NODES && JSON.stringify(view).length < FLOWS_MAX_CHARS;
|
|
238
267
|
}
|
|
239
268
|
/**
|
|
240
269
|
* How many senders and receivers stay after `drops` of the oldest links go,
|
|
@@ -259,9 +288,9 @@ function keptAfterDrops(senders, receivers, drops) {
|
|
|
259
288
|
* per dropped link.
|
|
260
289
|
*/
|
|
261
290
|
function buildFlowView(network, page) {
|
|
262
|
-
const overflow = page.senders.length >
|
|
263
|
-
const senders = page.senders.slice(0,
|
|
264
|
-
const receivers = page.receivers.slice(0,
|
|
291
|
+
const overflow = page.senders.length > FLOWS_PER_SIDE || page.receivers.length > FLOWS_PER_SIDE;
|
|
292
|
+
const senders = page.senders.slice(0, FLOWS_PER_SIDE);
|
|
293
|
+
const receivers = page.receivers.slice(0, FLOWS_PER_SIDE);
|
|
265
294
|
const viewAfter = (drops) => {
|
|
266
295
|
const [s, r] = keptAfterDrops(senders.length, receivers.length, drops);
|
|
267
296
|
return assembleFlowView(network, page, senders.slice(0, s), receivers.slice(0, r), overflow || drops > 0);
|
|
@@ -368,8 +397,8 @@ function buildTransfersView(network, from, to, day, rows) {
|
|
|
368
397
|
from,
|
|
369
398
|
to,
|
|
370
399
|
day,
|
|
371
|
-
transfers: newestFirst.slice(0,
|
|
372
|
-
truncated: newestFirst.length >
|
|
400
|
+
transfers: newestFirst.slice(0, TRANSFERS_MAX),
|
|
401
|
+
truncated: newestFirst.length > TRANSFERS_MAX
|
|
373
402
|
};
|
|
374
403
|
}
|
|
375
404
|
function transferLine(transfer) {
|
|
@@ -381,8 +410,8 @@ function transfersSummary(view) {
|
|
|
381
410
|
const span = paged && view.through_day !== view.day ? `${view.through_day} to ${view.day}` : view.day;
|
|
382
411
|
const head = `Transfers from ${view.from} to ${view.to} on ${span} (UTC) on ${view.network}`;
|
|
383
412
|
if (view.transfers.length === 0) return paged && view.next ? `${head}: none; earlier days remain before ${view.next.day}.` : `${head}: none.`;
|
|
384
|
-
const lines = [`${head}: ${view.truncated ? paged ? `${view.transfers.length}, and a day holds more than one read lists` : `the newest ${view.transfers.length}, and the day holds more` : `${view.transfers.length}`}, newest first.`, ...view.transfers.slice(0,
|
|
385
|
-
const rest = view.transfers.length -
|
|
413
|
+
const lines = [`${head}: ${view.truncated ? paged ? `${view.transfers.length}, and a day holds more than one read lists` : `the newest ${view.transfers.length}, and the day holds more` : `${view.transfers.length}`}, newest first.`, ...view.transfers.slice(0, TRANSFERS_SUMMARY_ROWS).map(transferLine)];
|
|
414
|
+
const rest = view.transfers.length - TRANSFERS_SUMMARY_ROWS;
|
|
386
415
|
if (rest > 0) lines.push(`- and ${rest} more in the picture.`);
|
|
387
416
|
if (paged && view.next) lines.push(`- earlier transfers remain, from ${view.next.day} back.`);
|
|
388
417
|
return lines.join("\n");
|
|
@@ -439,7 +468,7 @@ function rowsOf(answer) {
|
|
|
439
468
|
function checkNetwork(args) {
|
|
440
469
|
const rawNetwork = typeof args.network === "string" ? args.network.trim().toLowerCase() : "";
|
|
441
470
|
if (!rawNetwork) return { error: "invalid_network: network is required; pass robinhood" };
|
|
442
|
-
if (rawNetwork !==
|
|
471
|
+
if (rawNetwork !== FLOWS_NETWORK) return { error: `invalid_network: graph_expand reads the robinhood graph only; got "${String(args.network).trim()}"` };
|
|
443
472
|
return FLOWS_NETWORK;
|
|
444
473
|
}
|
|
445
474
|
/** The address of a node or of one end of a link; `name` says which end in a link refusal. */
|
|
@@ -449,10 +478,10 @@ function checkAddress(raw, name) {
|
|
|
449
478
|
return { error: name ? INVALID_ADDRESS_TEXT.replace("invalid_address:", `invalid_address (${name}):`) : INVALID_ADDRESS_TEXT };
|
|
450
479
|
}
|
|
451
480
|
function checkDay(raw) {
|
|
452
|
-
return normalizeFlowsDay(raw) ?? { error:
|
|
481
|
+
return normalizeFlowsDay(raw) ?? { error: INVALID_DAY_TEXT };
|
|
453
482
|
}
|
|
454
483
|
function checkLimit(value) {
|
|
455
|
-
if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value >
|
|
484
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > TRANSFERS_MAX) return { error: INVALID_LIMIT_TEXT };
|
|
456
485
|
return value;
|
|
457
486
|
}
|
|
458
487
|
function checkSkip(value) {
|
|
@@ -469,7 +498,7 @@ function checkFirstDay(value, day) {
|
|
|
469
498
|
function checkOffset(value) {
|
|
470
499
|
if (value === void 0 || value === null) return 0;
|
|
471
500
|
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) return { error: OFFSET_NEGATIVE_TEXT };
|
|
472
|
-
if (value >
|
|
501
|
+
if (value > FLOWS_MAX_OFFSET) return { error: OFFSET_TOO_LARGE_TEXT };
|
|
473
502
|
return value;
|
|
474
503
|
}
|
|
475
504
|
/**
|
|
@@ -565,7 +594,7 @@ async function readDay(deps, network, from, to, day) {
|
|
|
565
594
|
if (!rows) return { error: "graph_query_failed: the graph endpoint answered without chain-insights.result.v1 rows" };
|
|
566
595
|
return {
|
|
567
596
|
rows: rows.map(transferOf).sort((a, b) => (b.block_timestamp ?? -Infinity) - (a.block_timestamp ?? -Infinity)),
|
|
568
|
-
cut: rows.length >=
|
|
597
|
+
cut: rows.length >= TRANSFERS_READ_ROWS
|
|
569
598
|
};
|
|
570
599
|
}
|
|
571
600
|
/**
|
|
@@ -578,7 +607,7 @@ async function readDay(deps, network, from, to, day) {
|
|
|
578
607
|
async function runLinkPage(deps, network, from, to, day, page) {
|
|
579
608
|
const unavailable = deps.unavailable?.();
|
|
580
609
|
if (unavailable) return errorResult$1(unavailable);
|
|
581
|
-
const floor = page.firstDay ??
|
|
610
|
+
const floor = page.firstDay ?? TRANSFERS_FLOOR_DAY;
|
|
582
611
|
const transfers = [];
|
|
583
612
|
let current = day;
|
|
584
613
|
let skip = page.skip;
|
|
@@ -588,7 +617,7 @@ async function runLinkPage(deps, network, from, to, day, page) {
|
|
|
588
617
|
let next = null;
|
|
589
618
|
for (;;) {
|
|
590
619
|
if (current < floor) break;
|
|
591
|
-
if (daysRead >=
|
|
620
|
+
if (daysRead >= TRANSFERS_MAX_DAYS) {
|
|
592
621
|
next = {
|
|
593
622
|
day: current,
|
|
594
623
|
skip
|
|
@@ -806,12 +835,12 @@ async function saveGraphSchemaCache(network, document, endpoint, complete, now)
|
|
|
806
835
|
* adds a note. It never fails the tool.
|
|
807
836
|
*/
|
|
808
837
|
var graph_schema_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
|
|
809
|
-
ADDRESS_SAMPLE_LIMIT: () =>
|
|
838
|
+
ADDRESS_SAMPLE_LIMIT: () => ADDRESS_SAMPLE_LIMIT,
|
|
810
839
|
FACTS_TRANSFER_COLUMNS: () => FACTS_TRANSFER_COLUMNS,
|
|
811
840
|
GRAPH_SCHEMA_ID: () => GRAPH_SCHEMA_ID,
|
|
812
841
|
GRAPH_SCHEMA_SOURCE: () => GRAPH_SCHEMA_SOURCE,
|
|
813
842
|
GraphSchemaError: () => GraphSchemaError,
|
|
814
|
-
LINK_SAMPLE_LIMIT: () =>
|
|
843
|
+
LINK_SAMPLE_LIMIT: () => LINK_SAMPLE_LIMIT,
|
|
815
844
|
META_SCHEMA_DESCRIPTION: () => META_SCHEMA_DESCRIPTION,
|
|
816
845
|
META_SCHEMA_TITLE: () => META_SCHEMA_TITLE,
|
|
817
846
|
SAMPLED_LINK_TYPES: () => SAMPLED_LINK_TYPES,
|
|
@@ -827,6 +856,9 @@ const GRAPH_SCHEMA_ID = "chain-insights.graph-schema.v1";
|
|
|
827
856
|
const GRAPH_SCHEMA_SOURCE = "live catalog";
|
|
828
857
|
const META_SCHEMA_TITLE = "Graph schema";
|
|
829
858
|
const META_SCHEMA_DESCRIPTION = "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. Call it first when you need field names. It is cached for 24 hours; pass refresh=true to rebuild it.";
|
|
859
|
+
/** Fields of an address come from this many sampled nodes, and of a link from this many sampled links. */
|
|
860
|
+
const ADDRESS_SAMPLE_LIMIT = 20;
|
|
861
|
+
const LINK_SAMPLE_LIMIT = 5;
|
|
830
862
|
/** The link types an agent queries. Each is sampled only when the graph lists it. */
|
|
831
863
|
const SAMPLED_LINK_TYPES = [
|
|
832
864
|
"FLOWS_TO",
|
|
@@ -876,10 +908,10 @@ const CATALOG_READS = {
|
|
|
876
908
|
}
|
|
877
909
|
};
|
|
878
910
|
function addressSampleQuery() {
|
|
879
|
-
return `USE topology MATCH (a:Address) RETURN keys(a) AS keys LIMIT
|
|
911
|
+
return `USE topology MATCH (a:Address) RETURN keys(a) AS keys LIMIT ${ADDRESS_SAMPLE_LIMIT}`;
|
|
880
912
|
}
|
|
881
913
|
function linkSampleQuery(linkType) {
|
|
882
|
-
return `USE topology MATCH ()-[r:${linkType}]->() RETURN keys(r) AS keys LIMIT
|
|
914
|
+
return `USE topology MATCH ()-[r:${linkType}]->() RETURN keys(r) AS keys LIMIT ${LINK_SAMPLE_LIMIT}`;
|
|
883
915
|
}
|
|
884
916
|
/** A refusal with its own words for the caller: a bad argument, an unserved network, an endpoint that is down. */
|
|
885
917
|
var GraphSchemaError = class extends Error {
|
|
@@ -1053,7 +1085,7 @@ async function buildGraphSchema(options) {
|
|
|
1053
1085
|
if (rows) topology.samples[type] = unionOfKeys(rows);
|
|
1054
1086
|
}
|
|
1055
1087
|
const unsampled = topology.link_types.filter((type) => !SAMPLED_LINK_TYPES.includes(type));
|
|
1056
|
-
if (unsampled.length > 0) remarks.push(`Link types with no sample: ${unsampled.join(", ")}. Read the keys of one with USE topology MATCH ()-[r:<TYPE>]->() RETURN keys(r) AS keys LIMIT
|
|
1088
|
+
if (unsampled.length > 0) remarks.push(`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}.`);
|
|
1057
1089
|
}
|
|
1058
1090
|
const facts = {
|
|
1059
1091
|
relationships: [],
|
|
@@ -1231,6 +1263,12 @@ Object.defineProperty(exports, "META_SCHEMA_TITLE", {
|
|
|
1231
1263
|
return META_SCHEMA_TITLE;
|
|
1232
1264
|
}
|
|
1233
1265
|
});
|
|
1266
|
+
Object.defineProperty(exports, "TRANSFERS_MAX", {
|
|
1267
|
+
enumerable: true,
|
|
1268
|
+
get: function() {
|
|
1269
|
+
return TRANSFERS_MAX;
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1234
1272
|
Object.defineProperty(exports, "graph_schema_exports", {
|
|
1235
1273
|
enumerable: true,
|
|
1236
1274
|
get: function() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
|
|
2
|
-
import { t as PaymentRequiredError } from "./client-
|
|
3
|
-
import { n as findNetworkCapability, r as mirrorGraphNetworkCapabilities } from "./capabilities-
|
|
2
|
+
import { t as PaymentRequiredError } from "./client-7qOfAdpH.mjs";
|
|
3
|
+
import { n as findNetworkCapability, r as mirrorGraphNetworkCapabilities } from "./capabilities-DxojbX4D.mjs";
|
|
4
4
|
import { n as chainInsightsHome, t as CACHE_TTL_MS } from "./cache-location-BO2XkfgI.mjs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
@@ -44,13 +44,42 @@ import { createHash } from "node:crypto";
|
|
|
44
44
|
const FLOWS_SCHEMA = "chain-insights.flows.v1";
|
|
45
45
|
const GRAPH_EXPAND_TOOL = "graph_expand";
|
|
46
46
|
const FLOWS_NETWORK = "robinhood";
|
|
47
|
+
/** How many senders and how many receivers one page shows, at most. */
|
|
48
|
+
const FLOWS_PER_SIDE = 250;
|
|
49
|
+
/** The view data of one answer: at most this many addresses, the centre included... */
|
|
50
|
+
const FLOWS_MAX_NODES = 500;
|
|
51
|
+
/**
|
|
52
|
+
* ...and under this many characters of JSON. Hosts drop an app tool result
|
|
53
|
+
* over about 150,000 characters.
|
|
54
|
+
*/
|
|
55
|
+
const FLOWS_MAX_CHARS = 14e4;
|
|
56
|
+
/**
|
|
57
|
+
* The graph endpoint cuts a reply at 200 rows, whatever the LIMIT, so one side
|
|
58
|
+
* of a page is read in reads of at most this many rows.
|
|
59
|
+
*/
|
|
60
|
+
const FLOWS_READ_ROWS = 200;
|
|
47
61
|
/** Where a graph_expand page may start on each side, at most. */
|
|
48
62
|
const FLOWS_MAX_OFFSET = 1e4;
|
|
49
63
|
const TRANSFERS_SCHEMA = "chain-insights.transfers.v1";
|
|
64
|
+
/** A link answer lists at most this many transfers... */
|
|
65
|
+
const TRANSFERS_MAX = 50;
|
|
66
|
+
/**
|
|
67
|
+
* ...from the rows one read brings back. A facts read takes no ORDER BY and
|
|
68
|
+
* the server cuts a reply at 200 rows, so the read asks for the whole cap and
|
|
69
|
+
* the newest rows are picked here.
|
|
70
|
+
*/
|
|
71
|
+
const TRANSFERS_READ_ROWS = 200;
|
|
72
|
+
/** The text of a link answer names at most this many transfers. */
|
|
73
|
+
const TRANSFERS_SUMMARY_ROWS = 5;
|
|
74
|
+
/** 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. */
|
|
75
|
+
const TRANSFERS_MAX_DAYS = 7;
|
|
76
|
+
/** Where a paged link answer stops reading back when the caller names no first_day. */
|
|
77
|
+
const TRANSFERS_FLOOR_DAY = "2015-01-01";
|
|
50
78
|
const ADDRESS_SCHEMA = "chain-insights.address.v1";
|
|
51
79
|
const INVALID_ADDRESS_TEXT = "invalid_address: a full 0x address of 40 hexadecimal characters is needed, for example 0x04911a118f11c75667e4d0dfb8e640af5a353550";
|
|
52
80
|
const OFFSET_TOO_LARGE_TEXT = `invalid_offset: in_offset and out_offset must each be at most ${FLOWS_MAX_OFFSET}`;
|
|
53
81
|
const OFFSET_NEGATIVE_TEXT = "invalid_offset: in_offset and out_offset must each be a whole number, 0 or more";
|
|
82
|
+
const INVALID_DAY_TEXT = "invalid_day: day must be a calendar date written YYYY-MM-DD (UTC), for example 2026-07-10";
|
|
54
83
|
const BOTH_FORMS_TEXT = "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";
|
|
55
84
|
const INVALID_LIMIT_TEXT = "invalid_limit: limit must be a whole number from 1 to 50";
|
|
56
85
|
const INVALID_SKIP_TEXT = "invalid_skip: skip must be a whole number of 0 or more";
|
|
@@ -89,7 +118,7 @@ function normalizeFlowsDay(raw) {
|
|
|
89
118
|
* checks, so they are written into the read as literals.
|
|
90
119
|
*/
|
|
91
120
|
function transfersReadFor(from, to, day) {
|
|
92
|
-
return `USE facts MATCH (a:Address {address: "${from}"})-[t:TRANSFER]->(b:Address {address: "${to}"}) WHERE t.block_date = "${day}" RETURN t.tx_id AS tx_id, t.block_timestamp AS block_timestamp, t.amount AS amount, t.asset_symbol AS asset_symbol, t.amount_usd AS amount_usd LIMIT
|
|
121
|
+
return `USE facts MATCH (a:Address {address: "${from}"})-[t:TRANSFER]->(b:Address {address: "${to}"}) WHERE t.block_date = "${day}" RETURN t.tx_id AS tx_id, t.block_timestamp AS block_timestamp, t.amount AS amount, t.asset_symbol AS asset_symbol, t.amount_usd AS amount_usd LIMIT ${TRANSFERS_READ_ROWS}`;
|
|
93
122
|
}
|
|
94
123
|
/** The UTC day before a YYYY-MM-DD day. */
|
|
95
124
|
function dayBefore(day) {
|
|
@@ -138,9 +167,9 @@ const PAGE_ORDER = " ORDER BY f.last_seen_timestamp DESC, b.address";
|
|
|
138
167
|
function flowsReadsFor(address, inOffset, outOffset) {
|
|
139
168
|
const side = (id, offset, match) => {
|
|
140
169
|
const reads = [];
|
|
141
|
-
for (let done = 0; done < 251; done +=
|
|
170
|
+
for (let done = 0; done < 251; done += FLOWS_READ_ROWS) {
|
|
142
171
|
const skip = offset + done;
|
|
143
|
-
const limit = Math.min(
|
|
172
|
+
const limit = Math.min(FLOWS_READ_ROWS, 251 - done);
|
|
144
173
|
reads.push({
|
|
145
174
|
id,
|
|
146
175
|
query: `USE topology MATCH ${match} RETURN ${PARTY_FIELDS}${PAGE_ORDER}${skip > 0 ? ` SKIP ${skip}` : ""} LIMIT ${limit}`,
|
|
@@ -233,7 +262,7 @@ function assembleFlowView(network, page, senders, receivers, truncated) {
|
|
|
233
262
|
return view;
|
|
234
263
|
}
|
|
235
264
|
function fitsFlowBudget(view) {
|
|
236
|
-
return view.nodes.length <=
|
|
265
|
+
return view.nodes.length <= FLOWS_MAX_NODES && JSON.stringify(view).length < FLOWS_MAX_CHARS;
|
|
237
266
|
}
|
|
238
267
|
/**
|
|
239
268
|
* How many senders and receivers stay after `drops` of the oldest links go,
|
|
@@ -258,9 +287,9 @@ function keptAfterDrops(senders, receivers, drops) {
|
|
|
258
287
|
* per dropped link.
|
|
259
288
|
*/
|
|
260
289
|
function buildFlowView(network, page) {
|
|
261
|
-
const overflow = page.senders.length >
|
|
262
|
-
const senders = page.senders.slice(0,
|
|
263
|
-
const receivers = page.receivers.slice(0,
|
|
290
|
+
const overflow = page.senders.length > FLOWS_PER_SIDE || page.receivers.length > FLOWS_PER_SIDE;
|
|
291
|
+
const senders = page.senders.slice(0, FLOWS_PER_SIDE);
|
|
292
|
+
const receivers = page.receivers.slice(0, FLOWS_PER_SIDE);
|
|
264
293
|
const viewAfter = (drops) => {
|
|
265
294
|
const [s, r] = keptAfterDrops(senders.length, receivers.length, drops);
|
|
266
295
|
return assembleFlowView(network, page, senders.slice(0, s), receivers.slice(0, r), overflow || drops > 0);
|
|
@@ -367,8 +396,8 @@ function buildTransfersView(network, from, to, day, rows) {
|
|
|
367
396
|
from,
|
|
368
397
|
to,
|
|
369
398
|
day,
|
|
370
|
-
transfers: newestFirst.slice(0,
|
|
371
|
-
truncated: newestFirst.length >
|
|
399
|
+
transfers: newestFirst.slice(0, TRANSFERS_MAX),
|
|
400
|
+
truncated: newestFirst.length > TRANSFERS_MAX
|
|
372
401
|
};
|
|
373
402
|
}
|
|
374
403
|
function transferLine(transfer) {
|
|
@@ -380,8 +409,8 @@ function transfersSummary(view) {
|
|
|
380
409
|
const span = paged && view.through_day !== view.day ? `${view.through_day} to ${view.day}` : view.day;
|
|
381
410
|
const head = `Transfers from ${view.from} to ${view.to} on ${span} (UTC) on ${view.network}`;
|
|
382
411
|
if (view.transfers.length === 0) return paged && view.next ? `${head}: none; earlier days remain before ${view.next.day}.` : `${head}: none.`;
|
|
383
|
-
const lines = [`${head}: ${view.truncated ? paged ? `${view.transfers.length}, and a day holds more than one read lists` : `the newest ${view.transfers.length}, and the day holds more` : `${view.transfers.length}`}, newest first.`, ...view.transfers.slice(0,
|
|
384
|
-
const rest = view.transfers.length -
|
|
412
|
+
const lines = [`${head}: ${view.truncated ? paged ? `${view.transfers.length}, and a day holds more than one read lists` : `the newest ${view.transfers.length}, and the day holds more` : `${view.transfers.length}`}, newest first.`, ...view.transfers.slice(0, TRANSFERS_SUMMARY_ROWS).map(transferLine)];
|
|
413
|
+
const rest = view.transfers.length - TRANSFERS_SUMMARY_ROWS;
|
|
385
414
|
if (rest > 0) lines.push(`- and ${rest} more in the picture.`);
|
|
386
415
|
if (paged && view.next) lines.push(`- earlier transfers remain, from ${view.next.day} back.`);
|
|
387
416
|
return lines.join("\n");
|
|
@@ -438,7 +467,7 @@ function rowsOf(answer) {
|
|
|
438
467
|
function checkNetwork(args) {
|
|
439
468
|
const rawNetwork = typeof args.network === "string" ? args.network.trim().toLowerCase() : "";
|
|
440
469
|
if (!rawNetwork) return { error: "invalid_network: network is required; pass robinhood" };
|
|
441
|
-
if (rawNetwork !==
|
|
470
|
+
if (rawNetwork !== FLOWS_NETWORK) return { error: `invalid_network: graph_expand reads the robinhood graph only; got "${String(args.network).trim()}"` };
|
|
442
471
|
return FLOWS_NETWORK;
|
|
443
472
|
}
|
|
444
473
|
/** The address of a node or of one end of a link; `name` says which end in a link refusal. */
|
|
@@ -448,10 +477,10 @@ function checkAddress(raw, name) {
|
|
|
448
477
|
return { error: name ? INVALID_ADDRESS_TEXT.replace("invalid_address:", `invalid_address (${name}):`) : INVALID_ADDRESS_TEXT };
|
|
449
478
|
}
|
|
450
479
|
function checkDay(raw) {
|
|
451
|
-
return normalizeFlowsDay(raw) ?? { error:
|
|
480
|
+
return normalizeFlowsDay(raw) ?? { error: INVALID_DAY_TEXT };
|
|
452
481
|
}
|
|
453
482
|
function checkLimit(value) {
|
|
454
|
-
if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value >
|
|
483
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > TRANSFERS_MAX) return { error: INVALID_LIMIT_TEXT };
|
|
455
484
|
return value;
|
|
456
485
|
}
|
|
457
486
|
function checkSkip(value) {
|
|
@@ -468,7 +497,7 @@ function checkFirstDay(value, day) {
|
|
|
468
497
|
function checkOffset(value) {
|
|
469
498
|
if (value === void 0 || value === null) return 0;
|
|
470
499
|
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) return { error: OFFSET_NEGATIVE_TEXT };
|
|
471
|
-
if (value >
|
|
500
|
+
if (value > FLOWS_MAX_OFFSET) return { error: OFFSET_TOO_LARGE_TEXT };
|
|
472
501
|
return value;
|
|
473
502
|
}
|
|
474
503
|
/**
|
|
@@ -564,7 +593,7 @@ async function readDay(deps, network, from, to, day) {
|
|
|
564
593
|
if (!rows) return { error: "graph_query_failed: the graph endpoint answered without chain-insights.result.v1 rows" };
|
|
565
594
|
return {
|
|
566
595
|
rows: rows.map(transferOf).sort((a, b) => (b.block_timestamp ?? -Infinity) - (a.block_timestamp ?? -Infinity)),
|
|
567
|
-
cut: rows.length >=
|
|
596
|
+
cut: rows.length >= TRANSFERS_READ_ROWS
|
|
568
597
|
};
|
|
569
598
|
}
|
|
570
599
|
/**
|
|
@@ -577,7 +606,7 @@ async function readDay(deps, network, from, to, day) {
|
|
|
577
606
|
async function runLinkPage(deps, network, from, to, day, page) {
|
|
578
607
|
const unavailable = deps.unavailable?.();
|
|
579
608
|
if (unavailable) return errorResult$1(unavailable);
|
|
580
|
-
const floor = page.firstDay ??
|
|
609
|
+
const floor = page.firstDay ?? TRANSFERS_FLOOR_DAY;
|
|
581
610
|
const transfers = [];
|
|
582
611
|
let current = day;
|
|
583
612
|
let skip = page.skip;
|
|
@@ -587,7 +616,7 @@ async function runLinkPage(deps, network, from, to, day, page) {
|
|
|
587
616
|
let next = null;
|
|
588
617
|
for (;;) {
|
|
589
618
|
if (current < floor) break;
|
|
590
|
-
if (daysRead >=
|
|
619
|
+
if (daysRead >= TRANSFERS_MAX_DAYS) {
|
|
591
620
|
next = {
|
|
592
621
|
day: current,
|
|
593
622
|
skip
|
|
@@ -805,12 +834,12 @@ async function saveGraphSchemaCache(network, document, endpoint, complete, now)
|
|
|
805
834
|
* adds a note. It never fails the tool.
|
|
806
835
|
*/
|
|
807
836
|
var graph_schema_exports = /* @__PURE__ */ __exportAll({
|
|
808
|
-
ADDRESS_SAMPLE_LIMIT: () =>
|
|
837
|
+
ADDRESS_SAMPLE_LIMIT: () => ADDRESS_SAMPLE_LIMIT,
|
|
809
838
|
FACTS_TRANSFER_COLUMNS: () => FACTS_TRANSFER_COLUMNS,
|
|
810
839
|
GRAPH_SCHEMA_ID: () => GRAPH_SCHEMA_ID,
|
|
811
840
|
GRAPH_SCHEMA_SOURCE: () => GRAPH_SCHEMA_SOURCE,
|
|
812
841
|
GraphSchemaError: () => GraphSchemaError,
|
|
813
|
-
LINK_SAMPLE_LIMIT: () =>
|
|
842
|
+
LINK_SAMPLE_LIMIT: () => LINK_SAMPLE_LIMIT,
|
|
814
843
|
META_SCHEMA_DESCRIPTION: () => META_SCHEMA_DESCRIPTION,
|
|
815
844
|
META_SCHEMA_TITLE: () => META_SCHEMA_TITLE,
|
|
816
845
|
SAMPLED_LINK_TYPES: () => SAMPLED_LINK_TYPES,
|
|
@@ -826,6 +855,9 @@ const GRAPH_SCHEMA_ID = "chain-insights.graph-schema.v1";
|
|
|
826
855
|
const GRAPH_SCHEMA_SOURCE = "live catalog";
|
|
827
856
|
const META_SCHEMA_TITLE = "Graph schema";
|
|
828
857
|
const META_SCHEMA_DESCRIPTION = "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. Call it first when you need field names. It is cached for 24 hours; pass refresh=true to rebuild it.";
|
|
858
|
+
/** Fields of an address come from this many sampled nodes, and of a link from this many sampled links. */
|
|
859
|
+
const ADDRESS_SAMPLE_LIMIT = 20;
|
|
860
|
+
const LINK_SAMPLE_LIMIT = 5;
|
|
829
861
|
/** The link types an agent queries. Each is sampled only when the graph lists it. */
|
|
830
862
|
const SAMPLED_LINK_TYPES = [
|
|
831
863
|
"FLOWS_TO",
|
|
@@ -875,10 +907,10 @@ const CATALOG_READS = {
|
|
|
875
907
|
}
|
|
876
908
|
};
|
|
877
909
|
function addressSampleQuery() {
|
|
878
|
-
return `USE topology MATCH (a:Address) RETURN keys(a) AS keys LIMIT
|
|
910
|
+
return `USE topology MATCH (a:Address) RETURN keys(a) AS keys LIMIT ${ADDRESS_SAMPLE_LIMIT}`;
|
|
879
911
|
}
|
|
880
912
|
function linkSampleQuery(linkType) {
|
|
881
|
-
return `USE topology MATCH ()-[r:${linkType}]->() RETURN keys(r) AS keys LIMIT
|
|
913
|
+
return `USE topology MATCH ()-[r:${linkType}]->() RETURN keys(r) AS keys LIMIT ${LINK_SAMPLE_LIMIT}`;
|
|
882
914
|
}
|
|
883
915
|
/** A refusal with its own words for the caller: a bad argument, an unserved network, an endpoint that is down. */
|
|
884
916
|
var GraphSchemaError = class extends Error {
|
|
@@ -1052,7 +1084,7 @@ async function buildGraphSchema(options) {
|
|
|
1052
1084
|
if (rows) topology.samples[type] = unionOfKeys(rows);
|
|
1053
1085
|
}
|
|
1054
1086
|
const unsampled = topology.link_types.filter((type) => !SAMPLED_LINK_TYPES.includes(type));
|
|
1055
|
-
if (unsampled.length > 0) remarks.push(`Link types with no sample: ${unsampled.join(", ")}. Read the keys of one with USE topology MATCH ()-[r:<TYPE>]->() RETURN keys(r) AS keys LIMIT
|
|
1087
|
+
if (unsampled.length > 0) remarks.push(`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}.`);
|
|
1056
1088
|
}
|
|
1057
1089
|
const facts = {
|
|
1058
1090
|
relationships: [],
|
|
@@ -1188,6 +1220,6 @@ async function handleMetaSchema(args, deps) {
|
|
|
1188
1220
|
}
|
|
1189
1221
|
}
|
|
1190
1222
|
//#endregion
|
|
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,
|
|
1223
|
+
export { handleMetaSchema as a, GRAPH_EXPAND_TITLE as c, handleGraphExpand as d, 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, TRANSFERS_MAX as u };
|
|
1192
1224
|
|
|
1193
|
-
//# sourceMappingURL=graph-schema-
|
|
1225
|
+
//# sourceMappingURL=graph-schema-ClqjzgrM.mjs.map
|