Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions tests/data-api.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4421,6 +4421,25 @@ test("GET /api/v1/chain/transfers: totals + distinct counts + senders + receiver
expect(body.top_senders[0].address).toBe("5Sender");
});

test("GET /api/v1/chain/transfers caps direct Data API limits at 100", async () => {
mockQueue.current = [
[],
[],
[{ transfer_count: 0, total_volume_tao: 0, newest_observed: null }],
[{ unique_senders: 0 }],
[{ unique_receivers: 0 }],
[],
[],
];
const res = await req("/api/v1/chain/transfers?limit=1000000000");
expect(res.status).toBe(200);
expect(
sqlCalls
.filter((call) => /LIMIT \?$/.test(call.text.trim()))
.map((call) => call.values.at(-1)),
).toEqual([100, 100]);
});

test("GET /api/v1/chain/transfers: a cold store's empty totals row falls back to null", async () => {
mockQueue.current = [
[], // consumed by the session-scoped `SET statement_timeout` call
Expand Down Expand Up @@ -4473,6 +4492,29 @@ test("GET /api/v1/chain/transfer-pairs: default (volume) sort", async () => {
expect(queryText()).toContain("volume_tao DESC, transfer_count DESC");
});

test("GET /api/v1/chain/transfer-pairs caps direct Data API limits at 100", async () => {
mockQueue.current = [
[],
[
{
transfer_count: 0,
total_volume_tao: 0,
unique_pairs: 0,
top_pair_volume_tao: 0,
newest_observed: null,
},
],
[],
[],
];
const res = await req("/api/v1/chain/transfer-pairs?limit=1000000000");
expect(res.status).toBe(200);
const leaderboard = sqlCalls.find((call) =>
/GROUP BY hotkey, "coldkey" ORDER BY/.test(call.text),
);
expect(leaderboard?.values.at(-1)).toBe(100);
});

test("GET /api/v1/chain/transfer-pairs?sort=count uses the count ordering", async () => {
mockQueue.current = [
[], // consumed by the session-scoped `SET statement_timeout` call
Expand Down
15 changes: 8 additions & 7 deletions workers/data-api.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -317,14 +317,15 @@ function windowLabelFor(url, windows, defaultLabel) {
}

// A ?limit= value for the /chain/* network-wide analytics routes (#4832
// Tier 2): by the time tryPostgresTier reaches this route, the D1-side
// handler's own parseLimitParam has ALREADY validated it (absent ->
// defaultLimit, present -> a clean positive integer <= its maxLimit) -- a
// malformed limit 400s before ever reaching here, so this only needs to
// replicate parseLimitParam's success path, not re-validate.
// Tier 2). The main Worker validates these before service-binding here, but
// keep the Postgres-serving Worker safe if it is reached directly: every
// chain-wide analytics leaderboard is capped at the public API's 1..100 range
// before the value is bound into SQL LIMIT clauses.
function chainLimit(url, defaultLimit) {
const raw = url.searchParams.get("limit");
return raw === null ? defaultLimit : Number(raw);
return clampRequestLimit(url.searchParams.get("limit"), {
defaultLimit,
maxLimit: 100,
});
}

// Resolve a ?window= label to a YYYY-MM-DD cutoff date for a neuron_daily
Expand Down
4 changes: 4 additions & 0 deletions wrangler.data.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
"main": "workers/data-api.mjs",
"compatibility_date": "2026-06-06",
"compatibility_flags": ["nodejs_compat"],
// Reached only through the main Worker service binding; do not publish the
// data Worker on workers.dev.
"workers_dev": false,
"preview_urls": false,
// Smart placement: run the Worker close to the Hyperdrive origin (self-hosted indexer
// box Postgres, reached via Cloudflare Tunnel) to minimize the per-request DB round-trip,
// not at the nearest edge to the client.
Expand Down
Loading