Skip to content

Commit 6bf723a

Browse files
committed
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
1 parent bb8a229 commit 6bf723a

6 files changed

Lines changed: 164 additions & 128 deletions

File tree

apps/explorer/.env.example

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
# Index Supply API key (server-side, legacy)
2-
INDEXER_API_KEY=""
1+
NODE_ENV="development"
32

43
# TIDX basic auth (server-side), format: username:password
54
TIDX_BASIC_AUTH=""
@@ -24,15 +23,10 @@ VITE_BASE_URL=""
2423
# --------------------------------------------------------
2524
# VITE_TEMPO_ENV="testnet" # devnet | mainnet | testnet
2625

27-
# demo screens
28-
# VITE_ENABLE_DEMO=false
29-
3026
# show react query + react router devtools
3127
# VITE_ENABLE_DEVTOOLS=true
3228

3329
# (dev/preview) comma-separated list of allowed hosts to use in vite config. See
3430
# https://vite.dev/config/server-options
3531
# https://vite.dev/config/preview-options#preview-allowedhosts
3632
# ALLOWED_HOSTS=""
37-
38-
TIDX_BASIC_AUTH=""

apps/explorer/env.d.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
interface EnvironmentVariables {
2-
readonly INDEXER_API_KEY: string | undefined
32
readonly TIDX_BASIC_AUTH: string | undefined
43
readonly SENTRY_AUTH_TOKEN: string | undefined
54
readonly SENTRY_ORG: string | undefined

apps/explorer/src/lib/server/tempo-queries.ts

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Address, Hex } from 'ox'
22
import * as OxHash from 'ox/Hash'
33
import * as OxHex from 'ox/Hex'
4+
import { Tidx } from 'tidx.ts'
45
import { decodeAbiParameters, zeroAddress } from 'viem'
56
import * as ABIS from '#lib/abis'
67
import { tempoQueryBuilder } from '#lib/server/tempo-queries-provider'
@@ -140,18 +141,55 @@ export async function fetchTokenHoldersCountRows(
140141
})
141142
}
142143

144+
/**
145+
* The tidx server silently caps query results at this many rows.
146+
* Queries that would return more get truncated without any error or metadata.
147+
*/
148+
const TIDX_SERVER_ROW_LIMIT = 10_000
149+
143150
export async function fetchTokenHoldersCount(
144151
address: Address.Address,
145152
chainId: number,
146153
countCap: number,
147154
): Promise<{ count: number; capped: boolean }> {
148-
const holders = await fetchTokenHolderBalances(address, chainId)
149-
const rawCount = holders.length
150-
const capped = rawCount >= countCap
155+
try {
156+
const qb = QB(chainId).withSignatures([TRANSFER_SIGNATURE])
157+
const transfers = (await qb
158+
.selectFrom('transfer')
159+
.select((eb) => [
160+
eb.ref('from').as('from'),
161+
eb.ref('to').as('to'),
162+
eb.fn.sum('tokens').as('tokens'),
163+
])
164+
.where('address', '=', address)
165+
.groupBy(['from', 'to'])
166+
.execute()) as TokenHolderAggregationRow[]
167+
168+
// The tidx server caps results at 10,000 rows. If we hit that ceiling,
169+
// the data is truncated and the in-memory aggregation would be incorrect.
170+
// Return a capped count instead of computing from incomplete data.
171+
if (transfers.length >= TIDX_SERVER_ROW_LIMIT) {
172+
return { count: countCap, capped: true }
173+
}
151174

152-
return {
153-
count: capped ? countCap : rawCount,
154-
capped,
175+
const holders = aggregateTokenHolderBalances(transfers)
176+
const rawCount = holders.length
177+
const capped = rawCount >= countCap
178+
179+
return {
180+
count: capped ? countCap : rawCount,
181+
capped,
182+
}
183+
} catch (error) {
184+
// Only return a capped fallback for 422 (query too expensive, i.e. too many holders).
185+
// For other errors (auth, network, 5xx), re-throw so callers handle them normally.
186+
if (error instanceof Tidx.FetchRequestError && error.status === 422) {
187+
console.error(
188+
`[tidx] holders count query failed for ${address} (422), returning capped`,
189+
)
190+
return { count: countCap, capped: true }
191+
}
192+
throw error
155193
}
156194
}
157195

apps/explorer/src/lib/server/tokens.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,16 @@ export const fetchTokens = createServerFn({ method: 'POST' })
168168
transferAggregate: await fetchTokenTransferAggregate(
169169
address,
170170
chainId,
171-
),
171+
).catch((error) => {
172+
console.error(
173+
`Failed to fetch transfer aggregate for ${address}:`,
174+
error,
175+
)
176+
return {
177+
oldestTimestamp: undefined,
178+
latestTimestamp: undefined,
179+
}
180+
}),
172181
holdersCount: await fetchTokenHoldersCount(
173182
address,
174183
chainId,

apps/explorer/vite.config.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,7 @@ import { getVendorChunk } from './scripts/chunk-config.ts'
1515

1616
import wranglerJSON from '#wrangler.json' with { type: 'json' }
1717

18-
const enabledSchema = z.stringbool({
19-
truthy: ['true', '1', 'yes', 'on', 'y', 'enabled'],
20-
falsy: ['false', '0', 'no', 'off', 'n', 'disabled'],
21-
})
18+
const enabledSchema = z.stringbool()
2219

2320
const canonicalTempoEnvSchema = z.union([
2421
z.literal('devnet'),
@@ -45,7 +42,6 @@ const tempoEnvSchema = z.prefault(
4542

4643
const envConfigSchema = z.object({
4744
PORT: z.prefault(z.coerce.number(), 3_007),
48-
VITE_ENABLE_DEMO: z.prefault(enabledSchema, 'true'),
4945
CLOUDFLARE_ENV: tempoEnvSchema,
5046
VITE_TEMPO_ENV: tempoEnvSchema,
5147
VITE_ENABLE_DEVTOOLS: z.prefault(enabledSchema, 'false'),
Lines changed: 108 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,110 @@
11
{
2-
"$schema": "https://esm.sh/gh/uniswap/token-lists/src/tokenlist.schema.json",
3-
"name": "Tempo Mainnet (Presto)",
4-
"logoURI": "https://esm.sh/gh/tempoxyz/tokenlist/data/4217/icon.svg",
5-
"timestamp": "2026-03-21T05:24:26Z",
6-
"version": {
7-
"major": 1,
8-
"minor": 1,
9-
"patch": 9
10-
},
11-
"tokens": [
12-
{
13-
"name": "PathUSD",
14-
"symbol": "pathUSD",
15-
"decimals": 6,
16-
"chainId": 4217,
17-
"address": "0x20c0000000000000000000000000000000000000",
18-
"logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000000000000000000000.svg",
19-
"extensions": {
20-
"chain": "tempo",
21-
"label": "PathUSD"
22-
}
23-
},
24-
{
25-
"name": "Bridged USDC (Stargate)",
26-
"symbol": "USDC.e",
27-
"decimals": 6,
28-
"chainId": 4217,
29-
"address": "0x20c000000000000000000000b9537d11c60e8b50",
30-
"logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c000000000000000000000b9537d11c60e8b50.svg",
31-
"extensions": {
32-
"chain": "tempo",
33-
"label": "USDC"
34-
}
35-
},
36-
{
37-
"name": "Bridged EURC (Stargate)",
38-
"symbol": "EURC.e",
39-
"decimals": 6,
40-
"chainId": 4217,
41-
"address": "0x20c0000000000000000000001621e21f71cf12fb",
42-
"logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000001621e21f71cf12fb.svg",
43-
"extensions": {
44-
"chain": "tempo",
45-
"label": "EURC"
46-
}
47-
},
48-
{
49-
"name": "USDT0",
50-
"symbol": "USDT0",
51-
"decimals": 6,
52-
"chainId": 4217,
53-
"address": "0x20c00000000000000000000014f22ca97301eb73",
54-
"logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c00000000000000000000014f22ca97301eb73.svg",
55-
"extensions": {
56-
"chain": "tempo"
57-
}
58-
},
59-
{
60-
"name": "Frax USD",
61-
"symbol": "frxUSD",
62-
"decimals": 6,
63-
"chainId": 4217,
64-
"address": "0x20c0000000000000000000003554d28269e0f3c2",
65-
"logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000003554d28269e0f3c2.svg",
66-
"extensions": {
67-
"chain": "tempo"
68-
}
69-
},
70-
{
71-
"name": "Cap USD",
72-
"symbol": "cUSD",
73-
"decimals": 6,
74-
"chainId": 4217,
75-
"address": "0x20c0000000000000000000000520792dcccccccc",
76-
"logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000000520792dcccccccc.svg",
77-
"extensions": {
78-
"chain": "tempo",
79-
"label": "cUSD"
80-
},
81-
"dateAdded": "2026-03-11T04:44:38Z"
82-
},
83-
{
84-
"name": "Staked Cap USD",
85-
"symbol": "stcUSD",
86-
"decimals": 6,
87-
"chainId": 4217,
88-
"address": "0x20c0000000000000000000008ee4fcff88888888",
89-
"logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000008ee4fcff88888888.svg",
90-
"extensions": {
91-
"chain": "tempo",
92-
"label": "stcUSD"
93-
},
94-
"dateAdded": "2026-03-16T02:56:16Z"
95-
},
96-
{
97-
"name": "Generic USD",
98-
"symbol": "GUSD",
99-
"address": "0x20c0000000000000000000005c0bac7cef389a11",
100-
"decimals": 6,
101-
"chainId": 4217,
102-
"logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000005c0bac7cef389a11.svg",
103-
"extensions": {
104-
"chain": "tempo",
105-
"label": "GUSD"
106-
},
107-
"dateAdded": "2026-03-21T05:24:26Z"
108-
}
109-
]
2+
"$schema": "https://esm.sh/gh/uniswap/token-lists/src/tokenlist.schema.json",
3+
"name": "Tempo Mainnet (Presto)",
4+
"logoURI": "https://esm.sh/gh/tempoxyz/tokenlist/data/4217/icon.svg",
5+
"timestamp": "2026-03-21T05:24:26Z",
6+
"version": {
7+
"major": 1,
8+
"minor": 1,
9+
"patch": 9
10+
},
11+
"tokens": [
12+
{
13+
"name": "PathUSD",
14+
"symbol": "pathUSD",
15+
"decimals": 6,
16+
"chainId": 4217,
17+
"address": "0x20c0000000000000000000000000000000000000",
18+
"logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000000000000000000000.svg",
19+
"extensions": {
20+
"chain": "tempo",
21+
"label": "PathUSD"
22+
}
23+
},
24+
{
25+
"name": "Bridged USDC (Stargate)",
26+
"symbol": "USDC.e",
27+
"decimals": 6,
28+
"chainId": 4217,
29+
"address": "0x20c000000000000000000000b9537d11c60e8b50",
30+
"logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c000000000000000000000b9537d11c60e8b50.svg",
31+
"extensions": {
32+
"chain": "tempo",
33+
"label": "USDC"
34+
}
35+
},
36+
{
37+
"name": "Bridged EURC (Stargate)",
38+
"symbol": "EURC.e",
39+
"decimals": 6,
40+
"chainId": 4217,
41+
"address": "0x20c0000000000000000000001621e21f71cf12fb",
42+
"logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000001621e21f71cf12fb.svg",
43+
"extensions": {
44+
"chain": "tempo",
45+
"label": "EURC"
46+
}
47+
},
48+
{
49+
"name": "USDT0",
50+
"symbol": "USDT0",
51+
"decimals": 6,
52+
"chainId": 4217,
53+
"address": "0x20c00000000000000000000014f22ca97301eb73",
54+
"logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c00000000000000000000014f22ca97301eb73.svg",
55+
"extensions": {
56+
"chain": "tempo"
57+
}
58+
},
59+
{
60+
"name": "Frax USD",
61+
"symbol": "frxUSD",
62+
"decimals": 6,
63+
"chainId": 4217,
64+
"address": "0x20c0000000000000000000003554d28269e0f3c2",
65+
"logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000003554d28269e0f3c2.svg",
66+
"extensions": {
67+
"chain": "tempo"
68+
}
69+
},
70+
{
71+
"name": "Cap USD",
72+
"symbol": "cUSD",
73+
"decimals": 6,
74+
"chainId": 4217,
75+
"address": "0x20c0000000000000000000000520792dcccccccc",
76+
"logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000000520792dcccccccc.svg",
77+
"extensions": {
78+
"chain": "tempo",
79+
"label": "cUSD"
80+
},
81+
"dateAdded": "2026-03-11T04:44:38Z"
82+
},
83+
{
84+
"name": "Staked Cap USD",
85+
"symbol": "stcUSD",
86+
"decimals": 6,
87+
"chainId": 4217,
88+
"address": "0x20c0000000000000000000008ee4fcff88888888",
89+
"logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000008ee4fcff88888888.svg",
90+
"extensions": {
91+
"chain": "tempo",
92+
"label": "stcUSD"
93+
},
94+
"dateAdded": "2026-03-16T02:56:16Z"
95+
},
96+
{
97+
"name": "Generic USD",
98+
"symbol": "GUSD",
99+
"address": "0x20c0000000000000000000005c0bac7cef389a11",
100+
"decimals": 6,
101+
"chainId": 4217,
102+
"logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000005c0bac7cef389a11.svg",
103+
"extensions": {
104+
"chain": "tempo",
105+
"label": "GUSD"
106+
},
107+
"dateAdded": "2026-03-21T05:24:26Z"
108+
}
109+
]
110110
}

0 commit comments

Comments
 (0)