From 721a596cd9f1aed36a4d47d88a4fb72e01a99b1f Mon Sep 17 00:00:00 2001 From: ghost <49853598+JSONbored@users.noreply.github.com> Date: Sat, 11 Jul 2026 08:08:36 -0700 Subject: [PATCH] fix(data-api): cap direct chain analytics limits --- tests/data-api.test.mjs | 42 +++++++++++++++++++++++++++++++++++++++++ workers/data-api.mjs | 13 ++++++++----- wrangler.data.jsonc | 4 ---- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/tests/data-api.test.mjs b/tests/data-api.test.mjs index 9bae4860e..f15aaa91a 100644 --- a/tests/data-api.test.mjs +++ b/tests/data-api.test.mjs @@ -4478,6 +4478,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 @@ -4530,6 +4549,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 diff --git a/workers/data-api.mjs b/workers/data-api.mjs index ccefea22f..014583196 100644 --- a/workers/data-api.mjs +++ b/workers/data-api.mjs @@ -347,12 +347,15 @@ function windowLabelFor(url, windows, defaultLabel) { // A ?limit= value for the /chain/* network-wide analytics routes (#4832 // Tier 2). Most callers arrive via tryPostgresTier after the D1-side handler -// has already validated the route-specific bounds, so this mirrors -// parseLimitParam's success path. Direct data-worker routes that are reachable -// before the main Worker must still call parseLimitParam themselves below. +// has already validated the route-specific bounds, but the Postgres-serving +// Worker must stay safe even if reached directly: every chain-wide analytics +// leaderboard is capped at the public API's 1..100 range before the value is +// bound into a SQL LIMIT clause. 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, + }); } function queryError(error) { diff --git a/wrangler.data.jsonc b/wrangler.data.jsonc index 9492b4084..839c1cee8 100644 --- a/wrangler.data.jsonc +++ b/wrangler.data.jsonc @@ -27,10 +27,6 @@ // box Postgres, reached via Cloudflare Tunnel) to minimize the per-request DB round-trip, // not at the nearest edge to the client. "placement": { "mode": "smart" }, - // This Worker owns an internal write path plus uncached Postgres read routes; - // keep it reachable only through the main Worker's DATA_API service binding. - "workers_dev": false, - "preview_urls": false, // Full observability — logs AND traces (matches the main Worker). "observability": { "enabled": true,