diff --git a/apps/api/src/cache-control.ts b/apps/api/src/cache-control.ts new file mode 100644 index 0000000..3a6eba5 --- /dev/null +++ b/apps/api/src/cache-control.ts @@ -0,0 +1,64 @@ +// Per-route Cache-Control policy. Most read endpoints serve stable data +// (block headers, finalized txs, address history) but every consumer +// (scan, faucet, dapps) re-fetches the same row hundreds of times per +// minute. A short shared cache at the edge — Caddy + browser — absorbs +// the duplication without hiding live data updates. +// +// Conventions per response shape: +// - live-tip data (chain/info, latest blocks): 2 s. One block-time +// window so users still see new blocks within a tick. +// - finalized objects with a cursor (specific block / tx): 5 min + +// immutable, since block N or tx H never changes once mined. +// - paginated lists keyed off the latest tip (address/txs, whale/tx, +// contracts/recent): 10–30 s. Long enough to dedupe burst traffic, +// short enough the next request lands in roughly real time. +// - aggregate stats (daily counts, validator scores): 30–60 s. The +// materialised view / 5-min in-memory cache already absorbs query +// cost; HTTP cache layer just stops the round-trip. +// +// Routes are free to set their own Cache-Control inside the handler — +// the plugin only fills in a default when none is set, never overrides. + +import type { FastifyInstance } from "fastify"; + +const POLICIES: Array<[RegExp, string]> = [ + [/^\/health$/, "no-store"], + [/^\/chain\/info$/, "public, max-age=2"], + [/^\/blocks$/, "public, max-age=2"], + // Specific block / tx are immutable once finalized; SAFE_LAG (5 by + // default) keeps the indexer behind tip so we never cache an + // unfinalized object. immutable hint lets browsers skip revalidation. + [/^\/blocks\/[0-9]+$/, "public, max-age=300, immutable"], + [/^\/tx\/0x[0-9a-f]+$/i, "public, max-age=300, immutable"], + [/^\/address\/0x[0-9a-f]+\/(txs|transfers)/i, "public, max-age=10"], + [/^\/address\/0x[0-9a-f]+$/i, "public, max-age=10"], + [/^\/stats\/daily$/, "public, max-age=60"], + [/^\/accounts\/active/, "public, max-age=30"], + [/^\/contracts\/recent/, "public, max-age=10"], + [/^\/contracts\/pioneers/, "public, max-age=300, immutable"], + [/^\/contracts\/stats/, "public, max-age=30"], + [/^\/whale\/tx/, "public, max-age=10"], + [/^\/validators$/, "public, max-age=30"], + [/^\/epochs$/, "public, max-age=60"], + [/^\/tokens$/, "public, max-age=30"], + [/^\/tokens\/0x[0-9a-f]+\/holders/i, "public, max-age=30"], +]; + +export function registerCacheControl(app: FastifyInstance) { + app.addHook("onSend", async (req, reply, payload) => { + // Skip errors — 4xx/5xx should always revalidate. + if (reply.statusCode >= 400) return payload; + // Don't override an explicit policy set by the route handler. + if (reply.getHeader("cache-control")) return payload; + for (const [pattern, value] of POLICIES) { + if (pattern.test(req.url.split("?")[0])) { + reply.header("Cache-Control", value); + // Explicit Vary so a future Accept-based variant doesn't + // collide in shared caches. + reply.header("Vary", "Accept, Accept-Encoding"); + break; + } + } + return payload; + }); +} diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index aa7822e..9d53b15 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -10,6 +10,7 @@ import { registerNativeRoutes } from "./routes/native.js"; import { registerEtherscanCompat } from "./routes/etherscan.js"; import { registerHealthRoutes } from "./routes/health.js"; import { registerCoinblastRoutes } from "./routes/coinblast.js"; +import { registerCacheControl } from "./cache-control.js"; const PORT = Number(process.env.API_PORT ?? 8081); const HOST = process.env.API_HOST ?? "0.0.0.0"; @@ -46,6 +47,10 @@ async function main() { const db = createDb(DB_URL); const chain = new SentrixClient({ network: NETWORK }); + // Cache-Control hook before routes so any explicit per-route header + // wins (the hook checks for an existing value before setting). + registerCacheControl(app); + registerHealthRoutes(app, { db, chain, network: NETWORK }); registerNativeRoutes(app, { db, chain }); registerEtherscanCompat(app, { db, chain }); diff --git a/packages/chain/src/index.ts b/packages/chain/src/index.ts index bb4578d..0128827 100644 --- a/packages/chain/src/index.ts +++ b/packages/chain/src/index.ts @@ -174,7 +174,16 @@ export class SentrixClient { cfg.wsUrl ?? chain.rpcUrls.default.webSocket?.[0]; - this.http = createPublicClient({ chain, transport: http(httpUrl) }); + // batch:true coalesces concurrent JSON-RPC calls into a single HTTP + // request (default window 0 ms = same micro-task). Indexer backfill + // fires bursts of getBlock + getLogs + getTransaction per block; with + // batching enabled these collapse from N round-trips to 1 per block, + // cutting tail latency under load and freeing the public LB to serve + // other consumers. wait: 0 keeps single-call latency unchanged. + this.http = createPublicClient({ + chain, + transport: http(httpUrl, { batch: { batchSize: 100, wait: 0 } }), + }); this.ws = wsUrl ? createPublicClient({ chain, transport: webSocket(wsUrl) }) : this.http; diff --git a/packages/db/drizzle/0004_workable_zeigeist.sql b/packages/db/drizzle/0004_workable_zeigeist.sql new file mode 100644 index 0000000..1e939d4 --- /dev/null +++ b/packages/db/drizzle/0004_workable_zeigeist.sql @@ -0,0 +1,14 @@ +-- Composite indexes for paginated address-history queries. +-- +-- Drizzle wraps a migration in a transaction, so we use plain `CREATE +-- INDEX IF NOT EXISTS` rather than `CONCURRENTLY` (the latter can't run +-- inside a transaction). On a quiescent indexer this is fine. On a +-- write-active production indexer with multi-million-row tables, the +-- operator should pre-create each index manually first via psql with +-- `CREATE INDEX CONCURRENTLY IF NOT EXISTS …` to avoid blocking writes; +-- the IF NOT EXISTS guard then makes this migration a no-op when run. + +CREATE INDEX IF NOT EXISTS "transfers_from_block_idx" ON "token_transfers" USING btree ("from_addr","block_height");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "transfers_to_block_idx" ON "token_transfers" USING btree ("to_addr","block_height");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "txs_from_block_idx" ON "transactions" USING btree ("from_addr","block_height");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "txs_to_block_idx" ON "transactions" USING btree ("to_addr","block_height"); diff --git a/packages/db/drizzle/meta/0004_snapshot.json b/packages/db/drizzle/meta/0004_snapshot.json new file mode 100644 index 0000000..47e06e3 --- /dev/null +++ b/packages/db/drizzle/meta/0004_snapshot.json @@ -0,0 +1,1318 @@ +{ + "id": "3d3a13ad-3544-44b6-881a-2e8f011536f3", + "prevId": "69339f6d-4581-419c-b941-5cc0cb718c74", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.addresses": { + "name": "addresses", + "schema": "", + "columns": { + "address": { + "name": "address", + "type": "varchar(42)", + "primaryKey": true, + "notNull": true + }, + "first_seen_block": { + "name": "first_seen_block", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "last_seen_block": { + "name": "last_seen_block", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "balance_cached": { + "name": "balance_cached", + "type": "numeric(78, 0)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "nonce": { + "name": "nonce", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": "0" + }, + "is_contract": { + "name": "is_contract", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "code_hash": { + "name": "code_hash", + "type": "varchar(66)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "addresses_contract_recent_idx": { + "name": "addresses_contract_recent_idx", + "columns": [ + { + "expression": "is_contract", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "first_seen_block", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.blocks": { + "name": "blocks", + "schema": "", + "columns": { + "height": { + "name": "height", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "varchar(66)", + "primaryKey": false, + "notNull": true + }, + "parent_hash": { + "name": "parent_hash", + "type": "varchar(66)", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "validator": { + "name": "validator", + "type": "varchar(42)", + "primaryKey": false, + "notNull": true + }, + "gas_used": { + "name": "gas_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "gas_limit": { + "name": "gas_limit", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "base_fee": { + "name": "base_fee", + "type": "numeric(78, 0)", + "primaryKey": false, + "notNull": false + }, + "tx_count": { + "name": "tx_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "state_root": { + "name": "state_root", + "type": "varchar(66)", + "primaryKey": false, + "notNull": false + }, + "round": { + "name": "round", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "justification_signers": { + "name": "justification_signers", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "blocks_validator_idx": { + "name": "blocks_validator_idx", + "columns": [ + { + "expression": "validator", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "blocks_timestamp_idx": { + "name": "blocks_timestamp_idx", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "blocks_hash_unique": { + "name": "blocks_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cb_tokens": { + "name": "cb_tokens", + "schema": "", + "columns": { + "curve_address": { + "name": "curve_address", + "type": "varchar(42)", + "primaryKey": true, + "notNull": true + }, + "token_address": { + "name": "token_address", + "type": "varchar(42)", + "primaryKey": false, + "notNull": true + }, + "owner_address": { + "name": "owner_address", + "type": "varchar(42)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "symbol": { + "name": "symbol", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "curve_supply": { + "name": "curve_supply", + "type": "numeric(78, 0)", + "primaryKey": false, + "notNull": true + }, + "graduation_threshold": { + "name": "graduation_threshold", + "type": "numeric(78, 0)", + "primaryKey": false, + "notNull": true + }, + "is_graduated": { + "name": "is_graduated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_block": { + "name": "created_block", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_tx_hash": { + "name": "created_tx_hash", + "type": "varchar(66)", + "primaryKey": false, + "notNull": true + }, + "total_volume_srx": { + "name": "total_volume_srx", + "type": "numeric(78, 0)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "trade_count": { + "name": "trade_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_price_srx": { + "name": "last_price_srx", + "type": "numeric(78, 0)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "twitter_url": { + "name": "twitter_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telegram_url": { + "name": "telegram_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website_url": { + "name": "website_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata_updated_at": { + "name": "metadata_updated_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "cb_tokens_owner_idx": { + "name": "cb_tokens_owner_idx", + "columns": [ + { + "expression": "owner_address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cb_tokens_graduated_idx": { + "name": "cb_tokens_graduated_idx", + "columns": [ + { + "expression": "is_graduated", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cb_tokens_created_block_idx": { + "name": "cb_tokens_created_block_idx", + "columns": [ + { + "expression": "created_block", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "cb_tokens_token_address_unique": { + "name": "cb_tokens_token_address_unique", + "nullsNotDistinct": false, + "columns": [ + "token_address" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cb_trades": { + "name": "cb_trades", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "cb_trades_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "curve_address": { + "name": "curve_address", + "type": "varchar(42)", + "primaryKey": false, + "notNull": true + }, + "token_address": { + "name": "token_address", + "type": "varchar(42)", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "trader_address": { + "name": "trader_address", + "type": "varchar(42)", + "primaryKey": false, + "notNull": true + }, + "srx_amount": { + "name": "srx_amount", + "type": "numeric(78, 0)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "token_amount": { + "name": "token_amount", + "type": "numeric(78, 0)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "fee": { + "name": "fee", + "type": "numeric(78, 0)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "block_number": { + "name": "block_number", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "tx_hash": { + "name": "tx_hash", + "type": "varchar(66)", + "primaryKey": false, + "notNull": true + }, + "log_index": { + "name": "log_index", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "cb_trades_uniq_log": { + "name": "cb_trades_uniq_log", + "columns": [ + { + "expression": "tx_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "log_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cb_trades_curve_idx": { + "name": "cb_trades_curve_idx", + "columns": [ + { + "expression": "curve_address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cb_trades_trader_idx": { + "name": "cb_trades_trader_idx", + "columns": [ + { + "expression": "trader_address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cb_trades_block_idx": { + "name": "cb_trades_block_idx", + "columns": [ + { + "expression": "block_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cb_trades_srx_amount_desc_idx": { + "name": "cb_trades_srx_amount_desc_idx", + "columns": [ + { + "expression": "srx_amount", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.epochs": { + "name": "epochs", + "schema": "", + "columns": { + "epoch_number": { + "name": "epoch_number", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "start_height": { + "name": "start_height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "end_height": { + "name": "end_height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "validator_set": { + "name": "validator_set", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_staked": { + "name": "total_staked", + "type": "numeric(78, 0)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_blocks_produced": { + "name": "total_blocks_produced", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": "0" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.logs": { + "name": "logs", + "schema": "", + "columns": { + "block_height": { + "name": "block_height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "tx_hash": { + "name": "tx_hash", + "type": "varchar(66)", + "primaryKey": false, + "notNull": true + }, + "log_index": { + "name": "log_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "varchar(42)", + "primaryKey": false, + "notNull": true + }, + "topic0": { + "name": "topic0", + "type": "varchar(66)", + "primaryKey": false, + "notNull": false + }, + "topic1": { + "name": "topic1", + "type": "varchar(66)", + "primaryKey": false, + "notNull": false + }, + "topic2": { + "name": "topic2", + "type": "varchar(66)", + "primaryKey": false, + "notNull": false + }, + "topic3": { + "name": "topic3", + "type": "varchar(66)", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "logs_address_idx": { + "name": "logs_address_idx", + "columns": [ + { + "expression": "address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "logs_topic0_idx": { + "name": "logs_topic0_idx", + "columns": [ + { + "expression": "topic0", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "logs_tx_idx": { + "name": "logs_tx_idx", + "columns": [ + { + "expression": "tx_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "logs_block_height_blocks_height_fk": { + "name": "logs_block_height_blocks_height_fk", + "tableFrom": "logs", + "tableTo": "blocks", + "columnsFrom": [ + "block_height" + ], + "columnsTo": [ + "height" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "logs_tx_hash_transactions_hash_fk": { + "name": "logs_tx_hash_transactions_hash_fk", + "tableFrom": "logs", + "tableTo": "transactions", + "columnsFrom": [ + "tx_hash" + ], + "columnsTo": [ + "hash" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "logs_block_height_log_index_pk": { + "name": "logs_block_height_log_index_pk", + "columns": [ + "block_height", + "log_index" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._meta": { + "name": "_meta", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.token_transfers": { + "name": "token_transfers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "token_transfers_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "block_height": { + "name": "block_height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "tx_hash": { + "name": "tx_hash", + "type": "varchar(66)", + "primaryKey": false, + "notNull": true + }, + "log_index": { + "name": "log_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "contract": { + "name": "contract", + "type": "varchar(42)", + "primaryKey": false, + "notNull": true + }, + "standard": { + "name": "standard", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "from_addr": { + "name": "from_addr", + "type": "varchar(42)", + "primaryKey": false, + "notNull": true + }, + "to_addr": { + "name": "to_addr", + "type": "varchar(42)", + "primaryKey": false, + "notNull": true + }, + "token_id": { + "name": "token_id", + "type": "numeric(78, 0)", + "primaryKey": false, + "notNull": false + }, + "amount": { + "name": "amount", + "type": "numeric(78, 0)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "transfers_contract_idx": { + "name": "transfers_contract_idx", + "columns": [ + { + "expression": "contract", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "transfers_from_idx": { + "name": "transfers_from_idx", + "columns": [ + { + "expression": "from_addr", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "transfers_to_idx": { + "name": "transfers_to_idx", + "columns": [ + { + "expression": "to_addr", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "transfers_block_idx": { + "name": "transfers_block_idx", + "columns": [ + { + "expression": "block_height", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "transfers_from_block_idx": { + "name": "transfers_from_block_idx", + "columns": [ + { + "expression": "from_addr", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_height", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "transfers_to_block_idx": { + "name": "transfers_to_block_idx", + "columns": [ + { + "expression": "to_addr", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_height", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transactions": { + "name": "transactions", + "schema": "", + "columns": { + "hash": { + "name": "hash", + "type": "varchar(66)", + "primaryKey": true, + "notNull": true + }, + "block_height": { + "name": "block_height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "tx_index": { + "name": "tx_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "from_addr": { + "name": "from_addr", + "type": "varchar(42)", + "primaryKey": false, + "notNull": true + }, + "to_addr": { + "name": "to_addr", + "type": "varchar(42)", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "numeric(78, 0)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "gas_limit": { + "name": "gas_limit", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "gas_used": { + "name": "gas_used", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": "0" + }, + "gas_price": { + "name": "gas_price", + "type": "numeric(78, 0)", + "primaryKey": false, + "notNull": false + }, + "fee": { + "name": "fee", + "type": "numeric(78, 0)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "nonce": { + "name": "nonce", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "contract_address": { + "name": "contract_address", + "type": "varchar(42)", + "primaryKey": false, + "notNull": false + }, + "tx_type": { + "name": "tx_type", + "type": "varchar(24)", + "primaryKey": false, + "notNull": true, + "default": "'native'" + } + }, + "indexes": { + "txs_block_height_idx": { + "name": "txs_block_height_idx", + "columns": [ + { + "expression": "block_height", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "txs_from_idx": { + "name": "txs_from_idx", + "columns": [ + { + "expression": "from_addr", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "txs_to_idx": { + "name": "txs_to_idx", + "columns": [ + { + "expression": "to_addr", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "txs_contract_idx": { + "name": "txs_contract_idx", + "columns": [ + { + "expression": "contract_address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "txs_value_desc_idx": { + "name": "txs_value_desc_idx", + "columns": [ + { + "expression": "value", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "txs_from_block_idx": { + "name": "txs_from_block_idx", + "columns": [ + { + "expression": "from_addr", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_height", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "txs_to_block_idx": { + "name": "txs_to_block_idx", + "columns": [ + { + "expression": "to_addr", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_height", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "transactions_block_height_blocks_height_fk": { + "name": "transactions_block_height_blocks_height_fk", + "tableFrom": "transactions", + "tableTo": "blocks", + "columnsFrom": [ + "block_height" + ], + "columnsTo": [ + "height" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.validators": { + "name": "validators", + "schema": "", + "columns": { + "address": { + "name": "address", + "type": "varchar(42)", + "primaryKey": true, + "notNull": true + }, + "moniker": { + "name": "moniker", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "commission_bp": { + "name": "commission_bp", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "self_stake": { + "name": "self_stake", + "type": "numeric(78, 0)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_delegated": { + "name": "total_delegated", + "type": "numeric(78, 0)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "blocks_proposed": { + "name": "blocks_proposed", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": "0" + }, + "last_active_block": { + "name": "last_active_block", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "is_jailed": { + "name": "is_jailed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "jail_until": { + "name": "jail_until", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 3444cfe..f742f59 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -29,6 +29,13 @@ "when": 1778156171597, "tag": "0003_youthful_prima", "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1778443188386, + "tag": "0004_workable_zeigeist", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index a17f7d5..bc9ec6f 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -80,6 +80,12 @@ export const transactions = pgTable( // /whale/tx — ORDER BY value DESC (numeric(78,0) column). // Default B-tree handles DESC fine via index scan in reverse. valueDescIdx: index("txs_value_desc_idx").on(t.value), + // Composite for paginated address history: WHERE from_addr = X + // ORDER BY block_height DESC LIMIT N. The single-col `from_idx` + // filters but the planner has to sort separately; this composite + // serves filter + sort in one index scan. Same logic for to-side. + fromBlockIdx: index("txs_from_block_idx").on(t.fromAddr, t.blockHeight), + toBlockIdx: index("txs_to_block_idx").on(t.toAddr, t.blockHeight), }) ); @@ -131,6 +137,10 @@ export const tokenTransfers = pgTable( fromIdx: index("transfers_from_idx").on(t.fromAddr), toIdx: index("transfers_to_idx").on(t.toAddr), blockIdx: index("transfers_block_idx").on(t.blockHeight), + // Composite for paginated /address/:addr/transfers — same shape as + // transactions: WHERE from_addr = X ORDER BY block_height DESC. + fromBlockIdx: index("transfers_from_block_idx").on(t.fromAddr, t.blockHeight), + toBlockIdx: index("transfers_to_block_idx").on(t.toAddr, t.blockHeight), }) );