tapimo 0.4.3 → 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.
- package/dist/App.d.ts +7222 -6316
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +4 -0
- package/dist/App.js.map +1 -1
- package/dist/Scope.d.ts +18 -1
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +24 -3
- package/dist/Scope.js.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +116 -116
- package/dist/apps/data/App.d.ts +2541 -2607
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +135 -44
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/FundingQuoteProviders.d.ts +36 -0
- package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -0
- package/dist/apps/data/FundingQuoteProviders.js +190 -0
- package/dist/apps/data/FundingQuoteProviders.js.map +1 -0
- package/dist/apps/data/index.d.ts +1 -1
- package/dist/apps/data/index.js +1 -1
- package/dist/apps/data/routes/activities.d.ts +242 -242
- package/dist/apps/data/routes/addresses.d.ts +227 -227
- package/dist/apps/data/routes/balances.d.ts +12 -12
- package/dist/apps/data/routes/balances.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.js +67 -149
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/blocks.d.ts +125 -126
- package/dist/apps/data/routes/blocks.d.ts.map +1 -1
- package/dist/apps/data/routes/blocks.js +3 -5
- package/dist/apps/data/routes/blocks.js.map +1 -1
- package/dist/apps/data/routes/coingecko.d.ts +55 -54
- package/dist/apps/data/routes/coingecko.d.ts.map +1 -1
- package/dist/apps/data/routes/coingecko.js +3 -8
- package/dist/apps/data/routes/coingecko.js.map +1 -1
- package/dist/apps/data/routes/exchanges.d.ts +455 -455
- package/dist/apps/data/routes/fee-amm.d.ts +92 -92
- package/dist/apps/data/routes/funding.d.ts +91 -864
- package/dist/apps/data/routes/funding.d.ts.map +1 -1
- package/dist/apps/data/routes/funding.js +138 -450
- package/dist/apps/data/routes/funding.js.map +1 -1
- package/dist/apps/data/routes/rpc.d.ts.map +1 -1
- package/dist/apps/data/routes/rpc.js +86 -16
- package/dist/apps/data/routes/rpc.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts +36 -36
- package/dist/apps/data/routes/webhooks.d.ts +340 -0
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +13 -1
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/index.d.ts +1 -1
- package/dist/apps/index.js +1 -1
- package/dist/apps/management/App.d.ts +1843 -861
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/App.js +4 -0
- package/dist/apps/management/App.js.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +121 -121
- package/dist/apps/management/routes/api-keys.js +2 -2
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/billing.d.ts +2 -2
- package/dist/apps/management/routes/invite-links.d.ts +1100 -0
- package/dist/apps/management/routes/invite-links.d.ts.map +1 -0
- package/dist/apps/management/routes/invite-links.js +411 -0
- package/dist/apps/management/routes/invite-links.js.map +1 -0
- package/dist/apps/management/routes/me.d.ts +13 -13
- package/dist/apps/management/routes/members.d.ts +77 -77
- package/dist/apps/management/routes/orgs.d.ts +96 -96
- package/dist/apps/management/routes/projects.d.ts +62 -62
- package/dist/apps/management/routes/scopes.d.ts +11 -11
- package/dist/apps/management/routes/scopes.d.ts.map +1 -1
- package/dist/apps/management/routes/scopes.js +2 -2
- package/dist/apps/management/routes/scopes.js.map +1 -1
- package/dist/apps/management/routes/usage.d.ts +79 -79
- package/dist/apps/mcp.d.ts.map +1 -1
- package/dist/apps/mcp.js +3 -1
- package/dist/apps/mcp.js.map +1 -1
- package/dist/apps/relay/App.d.ts.map +1 -1
- package/dist/apps/relay/App.js +42 -4
- package/dist/apps/relay/App.js.map +1 -1
- package/dist/cloudflare.d.ts +2 -0
- package/dist/cloudflare.d.ts.map +1 -1
- package/dist/cloudflare.js +4 -0
- package/dist/cloudflare.js.map +1 -1
- package/dist/db/Db.d.ts +3 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +57 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +31 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +31 -0
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/inviteLinks.d.ts +42 -0
- package/dist/db/tables/inviteLinks.d.ts.map +1 -0
- package/dist/db/tables/inviteLinks.js +148 -0
- package/dist/db/tables/inviteLinks.js.map +1 -0
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +2 -0
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/handlers/relay.d.ts.map +1 -1
- package/dist/handlers/relay.js +13 -3
- package/dist/handlers/relay.js.map +1 -1
- package/dist/internal/Auth.d.ts +42 -23
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +169 -20
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/EdgeCache.d.ts +3 -1
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +17 -2
- package/dist/internal/EdgeCache.js.map +1 -1
- package/dist/internal/Errors.d.ts +6 -0
- package/dist/internal/Errors.d.ts.map +1 -0
- package/dist/internal/Errors.js +16 -0
- package/dist/internal/Errors.js.map +1 -0
- package/dist/internal/FundingQuotes.d.ts +644 -0
- package/dist/internal/FundingQuotes.d.ts.map +1 -0
- package/dist/internal/{FundingRoutes.js → FundingQuotes.js} +93 -289
- package/dist/internal/FundingQuotes.js.map +1 -0
- package/dist/internal/Store.d.ts +61 -16
- package/dist/internal/Store.d.ts.map +1 -1
- package/dist/internal/Store.js +72 -17
- package/dist/internal/Store.js.map +1 -1
- package/dist/internal/Viem.d.ts +2 -0
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Viem.js +11 -2
- package/dist/internal/Viem.js.map +1 -1
- package/dist/internal/index.d.ts +27 -0
- package/dist/internal/index.d.ts.map +1 -0
- package/dist/internal/index.js +27 -0
- package/dist/internal/index.js.map +1 -0
- package/package.json +8 -3
- package/src/ApiKeys.test.ts +23 -0
- package/src/App.test-d.ts +2 -0
- package/src/App.test.ts +60 -4
- package/src/App.ts +6 -0
- package/src/Client.test-d.ts +16 -54
- package/src/Scope.test.ts +1 -0
- package/src/Scope.ts +30 -4
- package/src/apps/data/App.test.ts +354 -0
- package/src/apps/data/App.ts +159 -52
- package/src/apps/data/FundingQuoteProviders.test.ts +151 -0
- package/src/apps/data/FundingQuoteProviders.ts +251 -0
- package/src/apps/data/index.ts +1 -1
- package/src/apps/data/routes/balances.test.ts +64 -18
- package/src/apps/data/routes/balances.ts +76 -177
- package/src/apps/data/routes/blocks.ts +3 -5
- package/src/apps/data/routes/coingecko.ts +3 -8
- package/src/apps/data/routes/funding.test.ts +149 -306
- package/src/apps/data/routes/funding.ts +154 -604
- package/src/apps/data/routes/indexer.test.ts +99 -3
- package/src/apps/data/routes/rpc.test.ts +219 -0
- package/src/apps/data/routes/rpc.ts +92 -17
- package/src/apps/data/routes/webhooks.test.ts +23 -2
- package/src/apps/data/routes/webhooks.ts +13 -1
- package/src/apps/index.ts +1 -1
- package/src/apps/management/App.ts +4 -0
- package/src/apps/management/routes/api-keys.test.ts +47 -3
- package/src/apps/management/routes/api-keys.ts +2 -2
- package/src/apps/management/routes/invite-links.test.ts +385 -0
- package/src/apps/management/routes/invite-links.ts +570 -0
- package/src/apps/management/routes/scopes.test.ts +3 -2
- package/src/apps/management/routes/scopes.ts +4 -2
- package/src/apps/mcp.test.ts +55 -14
- package/src/apps/mcp.ts +3 -1
- package/src/apps/relay/App.test.ts +324 -4
- package/src/apps/relay/App.ts +40 -4
- package/src/cloudflare.ts +5 -0
- package/src/db/Db.ts +65 -0
- package/src/db/Schema.ts +39 -0
- package/src/db/tables/inviteLinks.ts +187 -0
- package/src/db/tables/organizations.ts +2 -0
- package/src/handlers/relay.ts +16 -4
- package/src/internal/Auth.test.ts +528 -37
- package/src/internal/Auth.ts +246 -45
- package/src/internal/EdgeCache.test.ts +24 -3
- package/src/internal/EdgeCache.ts +18 -2
- package/src/internal/FundingQuotes.test.ts +174 -0
- package/src/internal/FundingQuotes.ts +735 -0
- package/src/internal/Log.test.ts +4 -4
- package/src/internal/Store.test-d.ts +15 -0
- package/src/internal/Store.test.ts +153 -2
- package/src/internal/Store.ts +157 -41
- package/src/internal/Viem.ts +14 -5
- package/src/internal/index.test.ts +34 -0
- package/src/internal/index.ts +26 -0
- package/dist/apps/data/FundingRouteProviders.d.ts +0 -143
- package/dist/apps/data/FundingRouteProviders.d.ts.map +0 -1
- package/dist/apps/data/FundingRouteProviders.js +0 -546
- package/dist/apps/data/FundingRouteProviders.js.map +0 -1
- package/dist/internal/FundingRoutes.d.ts +0 -1085
- package/dist/internal/FundingRoutes.d.ts.map +0 -1
- package/dist/internal/FundingRoutes.js.map +0 -1
- package/src/apps/data/FundingRouteProviders.test.ts +0 -485
- package/src/apps/data/FundingRouteProviders.ts +0 -744
- package/src/internal/FundingRoutes.test.ts +0 -239
- package/src/internal/FundingRoutes.ts +0 -1067
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Hono, type Context } from 'hono'
|
|
2
|
-
import {
|
|
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
|
-
//
|
|
22
|
-
//
|
|
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
|
-
|
|
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 } =
|
|
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
|
-
|
|
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)
|
|
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, '
|
|
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
|
-
//
|
|
320
|
-
//
|
|
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
|
|
327
|
-
FROM
|
|
328
|
-
WHERE holder = '${address}' AND startsWith(token, '${tip20AddressPrefix}')
|
|
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
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
return { data, nextCursor: page.nextCursor }
|
|
281
|
+
return token.success ? [{ address: token.data }] : []
|
|
282
|
+
})
|
|
353
283
|
})
|
|
354
284
|
|
|
355
|
-
if (holdings.
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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.
|
|
588
|
-
*
|
|
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
|
-
//
|
|
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
|
-
*
|
|
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)
|