From 6bf723a701a88d798ae370be73578632b681162f Mon Sep 17 00:00:00 2001 From: o-az Date: Sat, 28 Mar 2026 06:36:24 -0700 Subject: [PATCH] fix: show '> 10,000' for token holders when indexer query fails The tidx server silently caps query results at 10,000 rows and returns 422 for extremely large aggregation queries. Both cases caused the /tokens page to show incorrect holder counts: - pathUSD (422 error): showed '0' instead of '> 10,000' - USDC.e (10k row truncation): showed '3,260' from incomplete data Changes: - Catch 422 errors in fetchTokenHoldersCount and return capped count - Detect when raw transfer pair rows hit the 10k server ceiling and treat the result as capped instead of aggregating incomplete data - Add .catch() for fetchTokenTransferAggregate so a single failed query doesn't reject the entire per-token promise --- apps/explorer/.env.example | 8 +- apps/explorer/env.d.ts | 1 - apps/explorer/src/lib/server/tempo-queries.ts | 50 +++- apps/explorer/src/lib/server/tokens.ts | 11 +- apps/explorer/vite.config.ts | 6 +- apps/tokenlist/data/4217/tokenlist.json | 216 +++++++++--------- 6 files changed, 164 insertions(+), 128 deletions(-) diff --git a/apps/explorer/.env.example b/apps/explorer/.env.example index 42981c557..c4f7627a7 100644 --- a/apps/explorer/.env.example +++ b/apps/explorer/.env.example @@ -1,5 +1,4 @@ -# Index Supply API key (server-side, legacy) -INDEXER_API_KEY="" +NODE_ENV="development" # TIDX basic auth (server-side), format: username:password TIDX_BASIC_AUTH="" @@ -24,9 +23,6 @@ VITE_BASE_URL="" # -------------------------------------------------------- # VITE_TEMPO_ENV="testnet" # devnet | mainnet | testnet -# demo screens -# VITE_ENABLE_DEMO=false - # show react query + react router devtools # VITE_ENABLE_DEVTOOLS=true @@ -34,5 +30,3 @@ VITE_BASE_URL="" # https://vite.dev/config/server-options # https://vite.dev/config/preview-options#preview-allowedhosts # ALLOWED_HOSTS="" - -TIDX_BASIC_AUTH="" diff --git a/apps/explorer/env.d.ts b/apps/explorer/env.d.ts index 6f27cc9fe..895a9da89 100644 --- a/apps/explorer/env.d.ts +++ b/apps/explorer/env.d.ts @@ -1,5 +1,4 @@ interface EnvironmentVariables { - readonly INDEXER_API_KEY: string | undefined readonly TIDX_BASIC_AUTH: string | undefined readonly SENTRY_AUTH_TOKEN: string | undefined readonly SENTRY_ORG: string | undefined diff --git a/apps/explorer/src/lib/server/tempo-queries.ts b/apps/explorer/src/lib/server/tempo-queries.ts index 50906784a..3c4e034c9 100644 --- a/apps/explorer/src/lib/server/tempo-queries.ts +++ b/apps/explorer/src/lib/server/tempo-queries.ts @@ -1,6 +1,7 @@ import type { Address, Hex } from 'ox' import * as OxHash from 'ox/Hash' import * as OxHex from 'ox/Hex' +import { Tidx } from 'tidx.ts' import { decodeAbiParameters, zeroAddress } from 'viem' import * as ABIS from '#lib/abis' import { tempoQueryBuilder } from '#lib/server/tempo-queries-provider' @@ -140,18 +141,55 @@ export async function fetchTokenHoldersCountRows( }) } +/** + * The tidx server silently caps query results at this many rows. + * Queries that would return more get truncated without any error or metadata. + */ +const TIDX_SERVER_ROW_LIMIT = 10_000 + export async function fetchTokenHoldersCount( address: Address.Address, chainId: number, countCap: number, ): Promise<{ count: number; capped: boolean }> { - const holders = await fetchTokenHolderBalances(address, chainId) - const rawCount = holders.length - const capped = rawCount >= countCap + try { + const qb = QB(chainId).withSignatures([TRANSFER_SIGNATURE]) + const transfers = (await qb + .selectFrom('transfer') + .select((eb) => [ + eb.ref('from').as('from'), + eb.ref('to').as('to'), + eb.fn.sum('tokens').as('tokens'), + ]) + .where('address', '=', address) + .groupBy(['from', 'to']) + .execute()) as TokenHolderAggregationRow[] + + // The tidx server caps results at 10,000 rows. If we hit that ceiling, + // the data is truncated and the in-memory aggregation would be incorrect. + // Return a capped count instead of computing from incomplete data. + if (transfers.length >= TIDX_SERVER_ROW_LIMIT) { + return { count: countCap, capped: true } + } - return { - count: capped ? countCap : rawCount, - capped, + const holders = aggregateTokenHolderBalances(transfers) + const rawCount = holders.length + const capped = rawCount >= countCap + + return { + count: capped ? countCap : rawCount, + capped, + } + } catch (error) { + // Only return a capped fallback for 422 (query too expensive, i.e. too many holders). + // For other errors (auth, network, 5xx), re-throw so callers handle them normally. + if (error instanceof Tidx.FetchRequestError && error.status === 422) { + console.error( + `[tidx] holders count query failed for ${address} (422), returning capped`, + ) + return { count: countCap, capped: true } + } + throw error } } diff --git a/apps/explorer/src/lib/server/tokens.ts b/apps/explorer/src/lib/server/tokens.ts index fe5dd7194..96144c64f 100644 --- a/apps/explorer/src/lib/server/tokens.ts +++ b/apps/explorer/src/lib/server/tokens.ts @@ -168,7 +168,16 @@ export const fetchTokens = createServerFn({ method: 'POST' }) transferAggregate: await fetchTokenTransferAggregate( address, chainId, - ), + ).catch((error) => { + console.error( + `Failed to fetch transfer aggregate for ${address}:`, + error, + ) + return { + oldestTimestamp: undefined, + latestTimestamp: undefined, + } + }), holdersCount: await fetchTokenHoldersCount( address, chainId, diff --git a/apps/explorer/vite.config.ts b/apps/explorer/vite.config.ts index f3f03b0b2..626c5bd60 100644 --- a/apps/explorer/vite.config.ts +++ b/apps/explorer/vite.config.ts @@ -15,10 +15,7 @@ import { getVendorChunk } from './scripts/chunk-config.ts' import wranglerJSON from '#wrangler.json' with { type: 'json' } -const enabledSchema = z.stringbool({ - truthy: ['true', '1', 'yes', 'on', 'y', 'enabled'], - falsy: ['false', '0', 'no', 'off', 'n', 'disabled'], -}) +const enabledSchema = z.stringbool() const canonicalTempoEnvSchema = z.union([ z.literal('devnet'), @@ -45,7 +42,6 @@ const tempoEnvSchema = z.prefault( const envConfigSchema = z.object({ PORT: z.prefault(z.coerce.number(), 3_007), - VITE_ENABLE_DEMO: z.prefault(enabledSchema, 'true'), CLOUDFLARE_ENV: tempoEnvSchema, VITE_TEMPO_ENV: tempoEnvSchema, VITE_ENABLE_DEVTOOLS: z.prefault(enabledSchema, 'false'), diff --git a/apps/tokenlist/data/4217/tokenlist.json b/apps/tokenlist/data/4217/tokenlist.json index 27a667e44..db26a791c 100644 --- a/apps/tokenlist/data/4217/tokenlist.json +++ b/apps/tokenlist/data/4217/tokenlist.json @@ -1,110 +1,110 @@ { - "$schema": "https://esm.sh/gh/uniswap/token-lists/src/tokenlist.schema.json", - "name": "Tempo Mainnet (Presto)", - "logoURI": "https://esm.sh/gh/tempoxyz/tokenlist/data/4217/icon.svg", - "timestamp": "2026-03-21T05:24:26Z", - "version": { - "major": 1, - "minor": 1, - "patch": 9 - }, - "tokens": [ - { - "name": "PathUSD", - "symbol": "pathUSD", - "decimals": 6, - "chainId": 4217, - "address": "0x20c0000000000000000000000000000000000000", - "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000000000000000000000.svg", - "extensions": { - "chain": "tempo", - "label": "PathUSD" - } - }, - { - "name": "Bridged USDC (Stargate)", - "symbol": "USDC.e", - "decimals": 6, - "chainId": 4217, - "address": "0x20c000000000000000000000b9537d11c60e8b50", - "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c000000000000000000000b9537d11c60e8b50.svg", - "extensions": { - "chain": "tempo", - "label": "USDC" - } - }, - { - "name": "Bridged EURC (Stargate)", - "symbol": "EURC.e", - "decimals": 6, - "chainId": 4217, - "address": "0x20c0000000000000000000001621e21f71cf12fb", - "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000001621e21f71cf12fb.svg", - "extensions": { - "chain": "tempo", - "label": "EURC" - } - }, - { - "name": "USDT0", - "symbol": "USDT0", - "decimals": 6, - "chainId": 4217, - "address": "0x20c00000000000000000000014f22ca97301eb73", - "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c00000000000000000000014f22ca97301eb73.svg", - "extensions": { - "chain": "tempo" - } - }, - { - "name": "Frax USD", - "symbol": "frxUSD", - "decimals": 6, - "chainId": 4217, - "address": "0x20c0000000000000000000003554d28269e0f3c2", - "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000003554d28269e0f3c2.svg", - "extensions": { - "chain": "tempo" - } - }, - { - "name": "Cap USD", - "symbol": "cUSD", - "decimals": 6, - "chainId": 4217, - "address": "0x20c0000000000000000000000520792dcccccccc", - "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000000520792dcccccccc.svg", - "extensions": { - "chain": "tempo", - "label": "cUSD" - }, - "dateAdded": "2026-03-11T04:44:38Z" - }, - { - "name": "Staked Cap USD", - "symbol": "stcUSD", - "decimals": 6, - "chainId": 4217, - "address": "0x20c0000000000000000000008ee4fcff88888888", - "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000008ee4fcff88888888.svg", - "extensions": { - "chain": "tempo", - "label": "stcUSD" - }, - "dateAdded": "2026-03-16T02:56:16Z" - }, - { - "name": "Generic USD", - "symbol": "GUSD", - "address": "0x20c0000000000000000000005c0bac7cef389a11", - "decimals": 6, - "chainId": 4217, - "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000005c0bac7cef389a11.svg", - "extensions": { - "chain": "tempo", - "label": "GUSD" - }, - "dateAdded": "2026-03-21T05:24:26Z" - } - ] + "$schema": "https://esm.sh/gh/uniswap/token-lists/src/tokenlist.schema.json", + "name": "Tempo Mainnet (Presto)", + "logoURI": "https://esm.sh/gh/tempoxyz/tokenlist/data/4217/icon.svg", + "timestamp": "2026-03-21T05:24:26Z", + "version": { + "major": 1, + "minor": 1, + "patch": 9 + }, + "tokens": [ + { + "name": "PathUSD", + "symbol": "pathUSD", + "decimals": 6, + "chainId": 4217, + "address": "0x20c0000000000000000000000000000000000000", + "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000000000000000000000.svg", + "extensions": { + "chain": "tempo", + "label": "PathUSD" + } + }, + { + "name": "Bridged USDC (Stargate)", + "symbol": "USDC.e", + "decimals": 6, + "chainId": 4217, + "address": "0x20c000000000000000000000b9537d11c60e8b50", + "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c000000000000000000000b9537d11c60e8b50.svg", + "extensions": { + "chain": "tempo", + "label": "USDC" + } + }, + { + "name": "Bridged EURC (Stargate)", + "symbol": "EURC.e", + "decimals": 6, + "chainId": 4217, + "address": "0x20c0000000000000000000001621e21f71cf12fb", + "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000001621e21f71cf12fb.svg", + "extensions": { + "chain": "tempo", + "label": "EURC" + } + }, + { + "name": "USDT0", + "symbol": "USDT0", + "decimals": 6, + "chainId": 4217, + "address": "0x20c00000000000000000000014f22ca97301eb73", + "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c00000000000000000000014f22ca97301eb73.svg", + "extensions": { + "chain": "tempo" + } + }, + { + "name": "Frax USD", + "symbol": "frxUSD", + "decimals": 6, + "chainId": 4217, + "address": "0x20c0000000000000000000003554d28269e0f3c2", + "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000003554d28269e0f3c2.svg", + "extensions": { + "chain": "tempo" + } + }, + { + "name": "Cap USD", + "symbol": "cUSD", + "decimals": 6, + "chainId": 4217, + "address": "0x20c0000000000000000000000520792dcccccccc", + "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000000520792dcccccccc.svg", + "extensions": { + "chain": "tempo", + "label": "cUSD" + }, + "dateAdded": "2026-03-11T04:44:38Z" + }, + { + "name": "Staked Cap USD", + "symbol": "stcUSD", + "decimals": 6, + "chainId": 4217, + "address": "0x20c0000000000000000000008ee4fcff88888888", + "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000008ee4fcff88888888.svg", + "extensions": { + "chain": "tempo", + "label": "stcUSD" + }, + "dateAdded": "2026-03-16T02:56:16Z" + }, + { + "name": "Generic USD", + "symbol": "GUSD", + "address": "0x20c0000000000000000000005c0bac7cef389a11", + "decimals": 6, + "chainId": 4217, + "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000005c0bac7cef389a11.svg", + "extensions": { + "chain": "tempo", + "label": "GUSD" + }, + "dateAdded": "2026-03-21T05:24:26Z" + } + ] }