Skip to content
Merged
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
34 changes: 28 additions & 6 deletions apps/api/src/routes/coinblast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,25 @@ import type { SentrixClient } from "@sentriscloud/indexer-chain";

const MAX_PAGE = 100;

// Safe BigInt parse — returns parsed value or throws InvalidQueryError so
// the route handler can convert to a 400 response. Pre-fix sites used
// raw BigInt(req.query.*) which threw SyntaxError on non-numeric input,
// surfacing as Fastify 500 instead of an actionable 400.
function parseBigIntOrThrow(raw: string, field: string): bigint {
try {
return BigInt(raw);
} catch {
throw new InvalidQueryError(`invalid ${field}: must be a non-negative integer`);
}
}

class InvalidQueryError extends Error {
constructor(msg: string) {
super(msg);
this.name = "InvalidQueryError";
}
}

// Reasonable upper-bound caps on metadata field lengths. The DB schema
// uses unbounded `text` so the limit lives at the API edge.
const MAX_IMAGE_URL = 256;
Expand All @@ -39,7 +58,7 @@ export function registerCoinblastRoutes(
owner?: string;
before?: string;
};
}>("/coinblast/tokens", async (req) => {
}>("/coinblast/tokens", async (req, reply) => {
const limit = clampLimit(req.query.limit);
const conds = [];
if (req.query.graduated === "true") {
Expand All @@ -51,7 +70,8 @@ export function registerCoinblastRoutes(
conds.push(eq(cbTokens.ownerAddress, req.query.owner.toLowerCase()));
}
if (req.query.before) {
conds.push(lt(cbTokens.createdBlock, BigInt(req.query.before)));
try { conds.push(lt(cbTokens.createdBlock, parseBigIntOrThrow(req.query.before, "before"))); }
catch (e) { return reply.code(400).send({ error: (e as Error).message }); }
}
const rows = await ctx.db
.select()
Expand Down Expand Up @@ -87,7 +107,7 @@ export function registerCoinblastRoutes(
type?: string;
before?: string; // block number
};
}>("/coinblast/trades", async (req) => {
}>("/coinblast/trades", async (req, reply) => {
const limit = clampLimit(req.query.limit);
const conds = [];
if (req.query.curve) {
Expand All @@ -100,7 +120,8 @@ export function registerCoinblastRoutes(
conds.push(eq(cbTrades.type, req.query.type));
}
if (req.query.before) {
conds.push(lt(cbTrades.blockNumber, BigInt(req.query.before)));
try { conds.push(lt(cbTrades.blockNumber, parseBigIntOrThrow(req.query.before, "before"))); }
catch (e) { return reply.code(400).send({ error: (e as Error).message }); }
}
const rows = await ctx.db
.select()
Expand Down Expand Up @@ -255,11 +276,12 @@ export function registerCoinblastRoutes(
app.get<{
Params: { curve: string };
Querystring: { limit?: string; after?: string };
}>("/coinblast/trades/by-curve/:curve", async (req) => {
}>("/coinblast/trades/by-curve/:curve", async (req, reply) => {
const limit = clampLimit(req.query.limit);
const conds = [eq(cbTrades.curveAddress, req.params.curve.toLowerCase())];
if (req.query.after) {
conds.push(eq(cbTrades.blockNumber, BigInt(req.query.after)));
try { conds.push(eq(cbTrades.blockNumber, parseBigIntOrThrow(req.query.after, "after"))); }
catch (e) { return reply.code(400).send({ error: (e as Error).message }); }
}
const rows = await ctx.db
.select()
Expand Down
32 changes: 29 additions & 3 deletions apps/api/src/routes/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,26 @@ import type { SentrixClient } from "@sentriscloud/indexer-chain";

const MAX_PAGE = 100;

// Safe BigInt parse — returns undefined on malformed input rather than
// throwing. Pre-fix sites used raw `BigInt(req.query.before)` which threw
// SyntaxError on non-numeric strings, surfaced as a Fastify 500 with no
// actionable error message. This wraps the throw so endpoints can return
// 400 with a clear "invalid cursor" message and stay alerter-quiet.
function parseBigIntOrThrow(raw: string, field: string): bigint {
try {
return BigInt(raw);
} catch {
throw new InvalidQueryError(`invalid ${field}: must be a non-negative integer`);
}
}

class InvalidQueryError extends Error {
constructor(msg: string) {
super(msg);
this.name = "InvalidQueryError";
}
}

// /stats/daily moved off the chain native API in 2026-05-05 — at h~1.55M the
// on-chain handler scanned every block from genesis under the state read lock
// and hung the LB. Indexer side: a single GROUP BY against the timestamp-indexed
Expand All @@ -32,9 +52,13 @@ export function registerNativeRoutes(
// ── /blocks ───────────────────────────────────────────────
app.get<{ Querystring: { limit?: string; before?: string } }>(
"/blocks",
async (req) => {
async (req, reply) => {
const limit = clampLimit(req.query.limit);
const before = req.query.before ? BigInt(req.query.before) : undefined;
let before: bigint | undefined;
if (req.query.before) {
try { before = parseBigIntOrThrow(req.query.before, "before"); }
catch (e) { return reply.code(400).send({ error: (e as Error).message }); }
}
const rows = await ctx.db
.select()
.from(blocks)
Expand All @@ -47,7 +71,9 @@ export function registerNativeRoutes(

// ── /blocks/:height ───────────────────────────────────────
app.get<{ Params: { height: string } }>("/blocks/:height", async (req, reply) => {
const h = BigInt(req.params.height);
let h: bigint;
try { h = parseBigIntOrThrow(req.params.height, "height"); }
catch (e) { return reply.code(400).send({ error: (e as Error).message }); }
const row = await ctx.db.select().from(blocks).where(eq(blocks.height, h)).limit(1);
if (!row[0]) return reply.code(404).send({ error: "block not found" });
const txs = await ctx.db
Expand Down