tapimo 0.4.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/dist/App.d.ts +7403 -6127
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +4 -0
  4. package/dist/App.js.map +1 -1
  5. package/dist/Scope.d.ts +18 -1
  6. package/dist/Scope.d.ts.map +1 -1
  7. package/dist/Scope.js +24 -3
  8. package/dist/Scope.js.map +1 -1
  9. package/dist/admin/apps/verified-tokens.d.ts +115 -115
  10. package/dist/apps/data/App.d.ts +1838 -1534
  11. package/dist/apps/data/App.d.ts.map +1 -1
  12. package/dist/apps/data/App.js +135 -44
  13. package/dist/apps/data/App.js.map +1 -1
  14. package/dist/apps/data/{FundingRouteProviders.d.ts → FundingQuoteProviders.d.ts} +7 -7
  15. package/dist/apps/data/{FundingRouteProviders.d.ts.map → FundingQuoteProviders.d.ts.map} +1 -1
  16. package/dist/apps/data/{FundingRouteProviders.js → FundingQuoteProviders.js} +36 -91
  17. package/dist/apps/data/FundingQuoteProviders.js.map +1 -0
  18. package/dist/apps/data/index.d.ts +1 -1
  19. package/dist/apps/data/index.js +1 -1
  20. package/dist/apps/data/routes/activities.d.ts +242 -242
  21. package/dist/apps/data/routes/addresses.d.ts +227 -227
  22. package/dist/apps/data/routes/balances.d.ts +12 -12
  23. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  24. package/dist/apps/data/routes/balances.js +67 -149
  25. package/dist/apps/data/routes/balances.js.map +1 -1
  26. package/dist/apps/data/routes/blocks.d.ts +125 -126
  27. package/dist/apps/data/routes/blocks.d.ts.map +1 -1
  28. package/dist/apps/data/routes/blocks.js +3 -5
  29. package/dist/apps/data/routes/blocks.js.map +1 -1
  30. package/dist/apps/data/routes/coingecko.d.ts +55 -54
  31. package/dist/apps/data/routes/coingecko.d.ts.map +1 -1
  32. package/dist/apps/data/routes/coingecko.js +3 -8
  33. package/dist/apps/data/routes/coingecko.js.map +1 -1
  34. package/dist/apps/data/routes/funding.d.ts +207 -388
  35. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  36. package/dist/apps/data/routes/funding.js +143 -221
  37. package/dist/apps/data/routes/funding.js.map +1 -1
  38. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  39. package/dist/apps/data/routes/rpc.js +86 -16
  40. package/dist/apps/data/routes/rpc.js.map +1 -1
  41. package/dist/apps/data/routes/tokenlist.d.ts +33 -33
  42. package/dist/apps/data/routes/transactions.d.ts +281 -281
  43. package/dist/apps/data/routes/transfers.d.ts +58 -58
  44. package/dist/apps/data/routes/verified-tokens.d.ts +86 -86
  45. package/dist/apps/data/routes/webhooks.d.ts +340 -0
  46. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  47. package/dist/apps/data/routes/webhooks.js +13 -1
  48. package/dist/apps/data/routes/webhooks.js.map +1 -1
  49. package/dist/apps/index.d.ts +1 -1
  50. package/dist/apps/index.js +1 -1
  51. package/dist/apps/management/App.d.ts +1090 -108
  52. package/dist/apps/management/App.d.ts.map +1 -1
  53. package/dist/apps/management/App.js +4 -0
  54. package/dist/apps/management/App.js.map +1 -1
  55. package/dist/apps/management/routes/api-keys.d.ts +118 -118
  56. package/dist/apps/management/routes/api-keys.js +2 -2
  57. package/dist/apps/management/routes/api-keys.js.map +1 -1
  58. package/dist/apps/management/routes/billing.d.ts +207 -207
  59. package/dist/apps/management/routes/invite-links.d.ts +1100 -0
  60. package/dist/apps/management/routes/invite-links.d.ts.map +1 -0
  61. package/dist/apps/management/routes/invite-links.js +411 -0
  62. package/dist/apps/management/routes/invite-links.js.map +1 -0
  63. package/dist/apps/management/routes/me.d.ts +17 -17
  64. package/dist/apps/management/routes/members.d.ts +71 -71
  65. package/dist/apps/management/routes/orgs.d.ts +12 -12
  66. package/dist/apps/management/routes/projects.d.ts +70 -70
  67. package/dist/apps/management/routes/scopes.d.ts +11 -11
  68. package/dist/apps/management/routes/scopes.d.ts.map +1 -1
  69. package/dist/apps/management/routes/scopes.js +2 -2
  70. package/dist/apps/management/routes/scopes.js.map +1 -1
  71. package/dist/apps/management/routes/usage.d.ts +75 -75
  72. package/dist/apps/relay/App.d.ts.map +1 -1
  73. package/dist/apps/relay/App.js +42 -4
  74. package/dist/apps/relay/App.js.map +1 -1
  75. package/dist/db/Db.d.ts +3 -0
  76. package/dist/db/Db.d.ts.map +1 -1
  77. package/dist/db/Db.js +57 -0
  78. package/dist/db/Db.js.map +1 -1
  79. package/dist/db/Schema.d.ts +31 -0
  80. package/dist/db/Schema.d.ts.map +1 -1
  81. package/dist/db/Schema.js +31 -0
  82. package/dist/db/Schema.js.map +1 -1
  83. package/dist/db/tables/inviteLinks.d.ts +42 -0
  84. package/dist/db/tables/inviteLinks.d.ts.map +1 -0
  85. package/dist/db/tables/inviteLinks.js +148 -0
  86. package/dist/db/tables/inviteLinks.js.map +1 -0
  87. package/dist/db/tables/organizations.d.ts.map +1 -1
  88. package/dist/db/tables/organizations.js +2 -0
  89. package/dist/db/tables/organizations.js.map +1 -1
  90. package/dist/handlers/relay.d.ts.map +1 -1
  91. package/dist/handlers/relay.js +13 -3
  92. package/dist/handlers/relay.js.map +1 -1
  93. package/dist/internal/Auth.d.ts +9 -1
  94. package/dist/internal/Auth.d.ts.map +1 -1
  95. package/dist/internal/Auth.js +130 -10
  96. package/dist/internal/Auth.js.map +1 -1
  97. package/dist/internal/EdgeCache.d.ts +3 -1
  98. package/dist/internal/EdgeCache.d.ts.map +1 -1
  99. package/dist/internal/EdgeCache.js +17 -2
  100. package/dist/internal/EdgeCache.js.map +1 -1
  101. package/dist/internal/{FundingRoutes.d.ts → FundingQuotes.d.ts} +185 -428
  102. package/dist/internal/FundingQuotes.d.ts.map +1 -0
  103. package/dist/internal/{FundingRoutes.js → FundingQuotes.js} +93 -218
  104. package/dist/internal/FundingQuotes.js.map +1 -0
  105. package/dist/internal/Viem.d.ts +3 -1
  106. package/dist/internal/Viem.d.ts.map +1 -1
  107. package/dist/internal/Viem.js +11 -2
  108. package/dist/internal/Viem.js.map +1 -1
  109. package/dist/internal/index.d.ts +1 -1
  110. package/dist/internal/index.js +1 -1
  111. package/package.json +1 -1
  112. package/src/ApiKeys.test.ts +23 -0
  113. package/src/App.test-d.ts +2 -0
  114. package/src/App.ts +6 -0
  115. package/src/Client.test-d.ts +20 -11
  116. package/src/Scope.test.ts +1 -0
  117. package/src/Scope.ts +30 -4
  118. package/src/apps/data/App.test.ts +354 -0
  119. package/src/apps/data/App.ts +159 -52
  120. package/src/apps/data/FundingQuoteProviders.test.ts +151 -0
  121. package/src/apps/data/{FundingRouteProviders.ts → FundingQuoteProviders.ts} +40 -101
  122. package/src/apps/data/index.ts +1 -1
  123. package/src/apps/data/routes/balances.test.ts +64 -18
  124. package/src/apps/data/routes/balances.ts +76 -177
  125. package/src/apps/data/routes/blocks.ts +3 -5
  126. package/src/apps/data/routes/coingecko.ts +3 -8
  127. package/src/apps/data/routes/funding.test.ts +151 -206
  128. package/src/apps/data/routes/funding.ts +156 -291
  129. package/src/apps/data/routes/indexer.test.ts +97 -1
  130. package/src/apps/data/routes/rpc.test.ts +219 -0
  131. package/src/apps/data/routes/rpc.ts +92 -17
  132. package/src/apps/data/routes/webhooks.test.ts +21 -0
  133. package/src/apps/data/routes/webhooks.ts +13 -1
  134. package/src/apps/index.ts +1 -1
  135. package/src/apps/management/App.ts +4 -0
  136. package/src/apps/management/routes/api-keys.test.ts +47 -3
  137. package/src/apps/management/routes/api-keys.ts +2 -2
  138. package/src/apps/management/routes/invite-links.test.ts +385 -0
  139. package/src/apps/management/routes/invite-links.ts +570 -0
  140. package/src/apps/management/routes/scopes.test.ts +3 -2
  141. package/src/apps/management/routes/scopes.ts +4 -2
  142. package/src/apps/relay/App.test.ts +324 -4
  143. package/src/apps/relay/App.ts +40 -4
  144. package/src/db/Db.ts +65 -0
  145. package/src/db/Schema.ts +39 -0
  146. package/src/db/tables/inviteLinks.ts +187 -0
  147. package/src/db/tables/organizations.ts +2 -0
  148. package/src/handlers/relay.ts +16 -4
  149. package/src/internal/Auth.test.ts +132 -22
  150. package/src/internal/Auth.ts +171 -14
  151. package/src/internal/EdgeCache.test.ts +24 -3
  152. package/src/internal/EdgeCache.ts +18 -2
  153. package/src/internal/FundingQuotes.test.ts +174 -0
  154. package/src/internal/{FundingRoutes.ts → FundingQuotes.ts} +153 -312
  155. package/src/internal/Viem.ts +14 -5
  156. package/src/internal/index.test.ts +1 -1
  157. package/src/internal/index.ts +1 -1
  158. package/dist/apps/data/FundingRouteProviders.js.map +0 -1
  159. package/dist/internal/FundingRoutes.d.ts.map +0 -1
  160. package/dist/internal/FundingRoutes.js.map +0 -1
  161. package/src/apps/data/FundingRouteProviders.test.ts +0 -225
  162. package/src/internal/FundingRoutes.test.ts +0 -213
@@ -1,5 +1,5 @@
1
1
  import { Hono, type Context } from 'hono'
2
- import { Hex, Value as core_Value } from 'ox'
2
+ import { Value as core_Value } from 'ox'
3
3
  import { Actions } from 'viem/tempo'
4
4
  import * as z from 'zod/mini'
5
5
 
@@ -11,16 +11,14 @@ import * as OpenApi from '../../../internal/OpenApi.js'
11
11
  import * as Response from '../../../internal/Response.js'
12
12
  import * as Schema from '../../../internal/Schema.js'
13
13
  import * as Timing from '../../../internal/Timing.js'
14
- import * as Value from '../../../internal/Value.js'
15
14
  import * as VerifiedTokens from '../../../internal/VerifiedTokens.js'
16
15
  import * as FeeAmm from './fee-amm.js'
17
16
  import * as Tokens from './tokens.js'
18
17
 
19
18
  // TIP-20 tokens are minted by the TIP-20 factory and live at deterministic
20
19
  // `0x20c0…`-prefixed addresses (e.g. pathUSD `0x20c0…0000`). The indexer's
21
- // `token_balances_snapshot` view, by contrast, records balances for every
22
- // Transfer-emitting contract, so balance queries filter on this prefix to keep
23
- // non-TIP-20 tokens (which have no resolvable TIP-20 metadata) out of results.
20
+ // balance sources include every Transfer-emitting contract, so discovery
21
+ // filters on this prefix to keep non-TIP-20 tokens out of results.
24
22
  const tip20AddressPrefix = '0x20c0'
25
23
 
26
24
  /** Zod schemas owned by the balance handlers. */
@@ -215,20 +213,6 @@ export function addresses() {
215
213
  return undefined
216
214
  })
217
215
 
218
- // `totalCount` (the number of matching holdings) is opt-in and shares
219
- // the page's filters; run it concurrently with the page. Best-effort: a
220
- // count failure omits `meta` rather than failing the page.
221
- const countPromise = query.include.includes('totalCount')
222
- ? countAddressBalances(c, {
223
- address,
224
- chainId,
225
- currency: query.currency,
226
- feeEligible,
227
- feeTokens,
228
- verified,
229
- }).catch(() => undefined)
230
- : undefined
231
-
232
216
  const balances = await getAddressBalances(c, {
233
217
  address,
234
218
  chainId,
@@ -236,20 +220,13 @@ export function addresses() {
236
220
  cursor: query.cursor,
237
221
  feeEligible,
238
222
  feeTokens,
223
+ includeTotalCount: query.include.includes('totalCount'),
239
224
  limit: query.limit,
240
225
  page: query.page,
241
226
  verified,
242
227
  })
243
228
 
244
- const meta = countPromise ? await countPromise : undefined
245
-
246
- return c.json(
247
- Response.validated(schema.getAddressBalances.Response, {
248
- ...balances,
249
- ...(meta ? { meta } : {}),
250
- }),
251
- 200,
252
- )
229
+ return c.json(Response.validated(schema.getAddressBalances.Response, balances), 200)
253
230
  } catch (cause) {
254
231
  return Response.upstream(c, cause)
255
232
  }
@@ -261,18 +238,11 @@ async function getAddressBalances(
261
238
  c: Context<App.Environment>,
262
239
  options: getAddressBalances.Options,
263
240
  ) {
264
- const { address, chainId, currency, feeEligible, feeTokens, limit, verified } = options
241
+ const { address, chainId, currency, feeEligible, feeTokens, includeTotalCount, limit, verified } =
242
+ options
265
243
  const tidx = c.get('getTidx')(chainId)
266
- // Keyset pagination on `(balance, token)`: balance is descending and `token`
267
- // is a stable tiebreaker for the many holdings sharing a balance. A malformed
268
- // cursor falls back to the head page. Note: because `balance` is mutable, this
269
- // is best-effort across balance changes (the inherent limit of ranking feeds).
270
244
  const cursor = options.cursor ? Cursor.decode(options.cursor, ['uint', 'address']) : undefined
271
- // Bounded positional lane (exclusive with `cursor` at the schema): page 1 is
272
- // the head page, so it shares the head's SQL and cache entries; deeper pages
273
- // translate to a SQL OFFSET of `(page - 1) * limit` rows.
274
- const offset =
275
- options.page !== undefined && options.page > 1 ? (options.page - 1) * limit : undefined
245
+ const offset = options.page !== undefined && options.page > 1 ? (options.page - 1) * limit : 0
276
246
 
277
247
  // Build the optional `token IN (…)` restriction before hitting TIDX. The
278
248
  // verified/`currency` filter and the `feeEligible` filter each contribute a set
@@ -284,98 +254,56 @@ async function getAddressBalances(
284
254
  const snapshot = await VerifiedTokens.snapshot(c, chainId)
285
255
  const allowed = allowedTokens({ currency, feeEligible, feeTokens, snapshot, verified })
286
256
  const tokenFilter = allowed?.map((address) => `'${address}'`)
287
- if (tokenFilter?.length === 0) return { data: [], nextCursor: null }
257
+ if (tokenFilter?.length === 0)
258
+ return {
259
+ data: [],
260
+ ...(includeTotalCount ? { meta: { totalCount: 0, totalCountCapped: false } } : {}),
261
+ nextCursor: null,
262
+ }
288
263
 
289
- const holdings = await Timing.time(c, 'address_balances', async () => {
290
- // A held token is one this address holds a positive amount of. Read the
291
- // live `address_balances` view (`holder`-keyed: one row per
292
- // `(holder, token, balance)`), which aggregates `address_holder_deltas`
293
- // directly so balances reflect the indexer's chain head (no periodic
294
- // snapshot refresh — see the indexer's `address_holder_deltas` MV). The
295
- // `holder` equality prunes the view by its leading sort key, so this stays
296
- // a bounded read per request. Fetch one extra row to detect `hasMore`
297
- // without a separate count query. `startsWith` restricts results to TIP-20
298
- // tokens (see `tip20AddressPrefix`) before pagination, so `hasMore`/page
299
- // sizes stay correct and every returned token has resolvable metadata. The
300
- // inline query is cast to `string` so TIDX treats it as a dynamic
301
- // ClickHouse query that needs no event signature for the
302
- // `address_balances` view.
303
- //
304
- // Let upstream failures propagate: this is a dedicated endpoint, so a
305
- // failure should surface as the declared `502` rather than masquerade as
306
- // an address holding zero tokens.
307
- const keyset =
308
- cursor !== undefined
309
- ? ` AND ${Cursor.keyset([
310
- { literal: Cursor.literal(cursor[0]!, 'uint'), name: 'balance', order: 'desc' },
311
- { literal: Cursor.literal(cursor[1]!, 'address'), name: 'token', order: 'asc' },
312
- ])}`
313
- : ''
264
+ const holdings = await Timing.time(c, 'balances_discovery', async () => {
314
265
  const tokenIn = tokenFilter ? ` AND token IN (${tokenFilter.join(', ')})` : ''
315
266
 
316
267
  const result = await tidx.fetch({
317
268
  chainId,
318
269
  engine: 'clickhouse',
319
- // `balance` is UInt256; SELECT it as a string so values > 2^53
320
- // survive JSON without precision loss. The keyset cursor encodes the
321
- // row's exact balance, so any rounding would re-include the cursor
322
- // row on the next page. Alias is `balance_str` (not `balance`)
323
- // because TIDX rejects expression aliases that shadow the source
324
- // column (422).
270
+ // Discovery tolerates stale and reorged rows: RPC removes candidates
271
+ // with no current balance, so avoid the cost of `FINAL` and aggregation.
325
272
  query: `
326
- SELECT token, toString(balance) AS balance_str
327
- FROM address_balances
328
- WHERE holder = '${address}' AND startsWith(token, '${tip20AddressPrefix}') AND balance > 0${tokenIn}${keyset}
329
- ORDER BY balance DESC, token ASC
330
- LIMIT ${limit + 1}${offset !== undefined ? ` OFFSET ${offset}` : ''}
273
+ SELECT DISTINCT token
274
+ FROM address_holder_deltas
275
+ WHERE holder = '${address}' AND startsWith(token, '${tip20AddressPrefix}')${tokenIn}
331
276
  ` as string,
332
277
  })
333
278
 
334
- // The next page anchors below the last fetched row's `(balance, token)`.
335
- const page = Cursor.paginate({
336
- rows: result.rows,
337
- limit,
338
- key: (row) => {
339
- const balance = Value.toIntegerString(row['balance_str'])
340
- const token = Schema.Address.safeParse(row['token'])
341
- return balance !== undefined && token.success ? [balance, token.data] : undefined
342
- },
343
- })
344
-
345
- const data: { address: Hex.Hex; amount: string }[] = []
346
- for (const row of page.rows) {
279
+ return result.rows.flatMap((row) => {
347
280
  const token = Schema.Address.safeParse(row['token'])
348
- const amount = Value.toIntegerString(row['balance_str'])
349
- if (token.success && amount !== undefined) data.push({ address: token.data, amount })
350
- }
351
-
352
- return { data, nextCursor: page.nextCursor }
281
+ return token.success ? [{ address: token.data }] : []
282
+ })
353
283
  })
354
284
 
355
- if (holdings.data.length === 0) return { data: [], nextCursor: holdings.nextCursor }
285
+ if (holdings.length === 0)
286
+ return {
287
+ data: [],
288
+ ...(includeTotalCount ? { meta: { totalCount: 0, totalCountCapped: false } } : {}),
289
+ nextCursor: null,
290
+ }
356
291
 
357
292
  const client = c.get('getClient')(chainId)
358
- // TODO: remove once https://github.com/tempoxyz/tidx/commit/2f37b44172d381fd00234ef23f79ed1e1d69c470 deployed.
359
293
  const corrected = await Timing.time(c, 'balances_rpc', async () => {
360
- const results = await client
361
- .multicall({
362
- allowFailure: true,
363
- contracts: holdings.data.map((holding) =>
364
- Actions.token.getBalance.call(client, { account: address, token: holding.address }),
365
- ),
366
- })
367
- .catch(() => undefined)
368
-
369
- const rows: { address: Hex.Hex; amount: string }[] = []
370
- for (const [index, holding] of holdings.data.entries()) {
371
- const result = results?.[index]
372
- if (!result || result.status !== 'success' || typeof result.result !== 'bigint') {
373
- rows.push(holding)
374
- continue
375
- }
376
- if (result.result === 0n) continue
377
- rows.push({ address: holding.address, amount: result.result.toString() })
378
- }
294
+ // Concurrent reads enter viem's shared batch window and become one
295
+ // deployless multicall. A failed read fails the request rather than serving
296
+ // a stale or partial balance page.
297
+ const results = await Promise.all(
298
+ holdings.map(async (holding) => {
299
+ const { amount } = await Actions.token.getBalance(client, {
300
+ account: address,
301
+ token: holding.address,
302
+ })
303
+ return amount === 0n ? undefined : { address: holding.address, amount: amount.toString() }
304
+ }),
305
+ )
306
+ const rows = results.filter((holding) => holding !== undefined)
379
307
 
380
308
  rows.sort((a, b) => {
381
309
  const delta = BigInt(b.amount) - BigInt(a.amount)
@@ -385,7 +313,30 @@ async function getAddressBalances(
385
313
  return rows
386
314
  })
387
315
 
388
- if (corrected.length === 0) return { data: [], nextCursor: holdings.nextCursor }
316
+ const positioned = (() => {
317
+ if (!cursor) return corrected.slice(offset)
318
+ const amount = BigInt(cursor[0]!)
319
+ const address = String(cursor[1]!)
320
+ return corrected.filter(
321
+ (holding) =>
322
+ BigInt(holding.amount) < amount ||
323
+ (BigInt(holding.amount) === amount && holding.address > address),
324
+ )
325
+ })()
326
+ const page = Cursor.paginate({
327
+ rows: positioned,
328
+ limit,
329
+ key: (holding) => [holding.amount, holding.address],
330
+ })
331
+
332
+ if (page.rows.length === 0)
333
+ return {
334
+ data: [],
335
+ ...(includeTotalCount
336
+ ? { meta: { totalCount: corrected.length, totalCountCapped: false } }
337
+ : {}),
338
+ nextCursor: page.nextCursor,
339
+ }
389
340
 
390
341
  // Resolve token metadata for each balance. The endpoint's contract is a
391
342
  // balance with its `token` reference inlined per row (and `formatted` needs
@@ -399,7 +350,7 @@ async function getAddressBalances(
399
350
  // so it is never fetched here.
400
351
  const resolved = await Timing.time(c, 'tokens', () =>
401
352
  Promise.all(
402
- corrected.map((holding) =>
353
+ page.rows.map((holding) =>
403
354
  Tokens.resolveToken(c, {
404
355
  address: holding.address,
405
356
  chainId,
@@ -434,7 +385,13 @@ async function getAddressBalances(
434
385
  })
435
386
  }
436
387
 
437
- return { data, nextCursor: holdings.nextCursor }
388
+ return {
389
+ data,
390
+ ...(includeTotalCount
391
+ ? { meta: { totalCount: corrected.length, totalCountCapped: false } }
392
+ : {}),
393
+ nextCursor: page.nextCursor,
394
+ }
438
395
  }
439
396
 
440
397
  declare namespace getAddressBalances {
@@ -453,6 +410,8 @@ declare namespace getAddressBalances {
453
410
  * failed, in which case `feeEligible` is omitted from rows.
454
411
  */
455
412
  feeTokens?: ReadonlySet<string> | undefined
413
+ /** When true, include the corrected candidate count in response metadata. */
414
+ includeTotalCount: boolean
456
415
  limit: number
457
416
  /** 1-indexed page number (positional pagination; exclusive with `cursor`). */
458
417
  page?: number | undefined
@@ -505,63 +464,3 @@ declare namespace allowedTokens {
505
464
  verified?: boolean | undefined
506
465
  }
507
466
  }
508
-
509
- /**
510
- * Exact count of an address's matching TIP-20 holdings, sharing
511
- * {@link getAddressBalances}'s `holder`/`verified`/`currency` filters (minus
512
- * pagination) so the count matches the page it annotates. The `holder` equality
513
- * prunes the `token_balances_snapshot` view by its sort key, so ClickHouse
514
- * counts via the sparse primary index without a row scan — the count is exact
515
- * and cheap with no cap (`totalCountCapped` is always `false`), verified at
516
- * ~370k holdings in <1s. Feeds the opt-in `meta.totalCount`.
517
- */
518
- async function countAddressBalances(
519
- c: Context<App.Environment>,
520
- options: countAddressBalances.Options,
521
- ): Promise<{ totalCountCapped: boolean; totalCount: number }> {
522
- const { address, chainId, currency, feeEligible, feeTokens, verified } = options
523
- const tidx = c.get('getTidx')(chainId)
524
-
525
- // Same verified/currency/feeEligible `token IN (…)` restriction as the page; an
526
- // empty filter means nothing matches, so the count is zero without a round
527
- // trip.
528
- const snapshot = await VerifiedTokens.snapshot(c, chainId)
529
- const allowed = allowedTokens({ currency, feeEligible, feeTokens, snapshot, verified })
530
- const tokenFilter = allowed?.map((address) => `'${address}'`)
531
- if (tokenFilter?.length === 0) return { totalCountCapped: false, totalCount: 0 }
532
- const tokenIn = tokenFilter ? ` AND token IN (${tokenFilter.join(', ')})` : ''
533
-
534
- return Timing.time(c, 'address_balances_count', async () => {
535
- // Exact count over the holder-pruned `address_balances` rows (mirrors the
536
- // page's WHERE, minus the keyset cursor). The `holder` sort-key prune lets
537
- // ClickHouse count via the sparse index, so no cap is needed. The inline
538
- // query is cast to `string` so TIDX treats it as a dynamic ClickHouse
539
- // query needing no signature.
540
- const result = await tidx.fetch({
541
- chainId,
542
- engine: 'clickhouse',
543
- query: `
544
- SELECT count(*) AS total
545
- FROM address_balances
546
- WHERE holder = '${address}' AND startsWith(token, '${tip20AddressPrefix}') AND balance > 0${tokenIn}
547
- ` as string,
548
- })
549
- const totalCount = Value.toNumber(result.rows[0]?.['total']) ?? 0
550
- return { totalCountCapped: false, totalCount }
551
- })
552
- }
553
-
554
- declare namespace countAddressBalances {
555
- type Options = {
556
- address: z.output<typeof Schema.Address>
557
- chainId: z.output<typeof Schema.ChainId>
558
- /** Only include verified tokens denominated in this currency (implies `verified`). */
559
- currency?: string | undefined
560
- /** When true, restrict the count to fee-usable tokens (members of `feeTokens`). */
561
- feeEligible?: boolean | undefined
562
- /** The chain's fee-token set (best-effort), backing the `feeEligible` restriction. */
563
- feeTokens?: ReadonlySet<string> | undefined
564
- /** When true, restrict the count to addresses in the curated verified list. */
565
- verified?: boolean | undefined
566
- }
567
- }
@@ -584,9 +584,8 @@ export function blocks() {
584
584
  * `GET /v1/blocks/:block` with an ISO 8601 selector.
585
585
  *
586
586
  * Like the original it searches a ±{@link byTimestampWindowSeconds}s window and
587
- * returns `404` when no indexed block falls within it. Because the chain id is a
588
- * path parameter (not the `chainId` query the global guard inspects), this
589
- * validates it against the deployment's supported chains itself.
587
+ * returns `404` when no indexed block falls within it. A local supported-chain
588
+ * check keeps standalone composition safe.
590
589
  */
591
590
  export function blocksByTimestampCompat() {
592
591
  return new Hono<App.Environment>().get(
@@ -635,8 +634,7 @@ export function blocksByTimestampCompat() {
635
634
  const { chainId } = c.req.valid('param')
636
635
  const { closest, timestamp } = c.req.valid('query')
637
636
 
638
- // The global unsupported-chain guard only inspects the `chainId` query, so
639
- // reject an unserved chain here before it reaches the indexer.
637
+ // Keep this route safe when mounted without the shared data app.
640
638
  const supportedChainIds = c.get('supportedChainIds')
641
639
  if (!supportedChainIds.has(chainId))
642
640
  return Response.unsupportedChainId(c, chainId, supportedChainIds)
@@ -466,14 +466,8 @@ export namespace schema {
466
466
  }
467
467
 
468
468
  /**
469
- * Composes the GeckoTerminal/CoinGecko DEX-integration compatibility adapter.
470
- */
471
- /**
472
- * Rejects a path `chainId` this deployment does not serve. GeckoTerminal
473
- * templates `chainId` into the path, so the App-level query-param guard never
474
- * sees it; reject a well-formed-but-unsupported chain here before it reaches an
475
- * upstream (which would otherwise surface as an opaque `upstream_error`).
476
- * Malformed values fall through to each route's own `param` validation.
469
+ * Keeps a local supported-chain check when this route group is composed
470
+ * independently of the shared data app.
477
471
  */
478
472
  const guardChainId = createMiddleware<App.Environment>(async (c, next) => {
479
473
  const parsed = Schema.ChainId.safeParse(c.req.param('chainId'))
@@ -482,6 +476,7 @@ const guardChainId = createMiddleware<App.Environment>(async (c, next) => {
482
476
  return Response.unsupportedChainId(c, parsed.data, supported)
483
477
  })
484
478
 
479
+ /** Composes the GeckoTerminal/CoinGecko DEX-integration compatibility adapter. */
485
480
  export function coingecko() {
486
481
  return new Hono<App.Environment>()
487
482
  .use('/gecko/:chainId/*', guardChainId)