tapimo 0.10.0 → 0.10.2
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/ApiKey.d.ts +2 -1
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKeys.d.ts +4 -2
- package/dist/ApiKeys.d.ts.map +1 -1
- package/dist/ApiKeys.js +26 -1
- package/dist/ApiKeys.js.map +1 -1
- package/dist/App.d.ts +6007 -5003
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +1 -0
- package/dist/App.js.map +1 -1
- package/dist/Webhooks.d.ts +1 -1
- package/dist/Webhooks.d.ts.map +1 -1
- package/dist/Webhooks.js +6 -6
- package/dist/Webhooks.js.map +1 -1
- package/dist/admin/App.d.ts +91 -84
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +22 -13
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +6 -0
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/organizations.d.ts +4 -2
- package/dist/admin/apps/organizations.d.ts.map +1 -1
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.js +69 -69
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts.map +1 -1
- package/dist/analytics/Analytics.js +232 -0
- package/dist/analytics/Analytics.js.map +1 -1
- package/dist/analytics/tables/requestEvents.d.ts +4 -0
- package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
- package/dist/analytics/tables/requestEvents.js +2 -0
- package/dist/analytics/tables/requestEvents.js.map +1 -1
- package/dist/apps/Zones.d.ts +4266 -82
- package/dist/apps/Zones.d.ts.map +1 -1
- package/dist/apps/Zones.js +27 -8
- package/dist/apps/Zones.js.map +1 -1
- package/dist/apps/data/App.d.ts +3467 -2696
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +2 -0
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +2999 -100
- package/dist/apps/data/routes/activities.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.js +340 -2
- package/dist/apps/data/routes/activities.js.map +1 -1
- package/dist/apps/data/routes/addresses.d.ts +514 -0
- package/dist/apps/data/routes/addresses.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +26 -26
- package/dist/apps/data/routes/balances.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.js +7 -3
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/blocks.d.ts +4 -4
- package/dist/apps/data/routes/earn.d.ts +75 -75
- package/dist/apps/data/routes/earn.d.ts.map +1 -1
- package/dist/apps/data/routes/earn.js +65 -36
- package/dist/apps/data/routes/earn.js.map +1 -1
- package/dist/apps/data/routes/exchanges.d.ts +124 -120
- package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
- package/dist/apps/data/routes/exchanges.js +115 -77
- package/dist/apps/data/routes/exchanges.js.map +1 -1
- package/dist/apps/data/routes/fee-amm.d.ts +24 -24
- package/dist/apps/data/routes/indexer.d.ts +2 -1
- package/dist/apps/data/routes/indexer.d.ts.map +1 -1
- package/dist/apps/data/routes/indexer.js +3 -2
- package/dist/apps/data/routes/indexer.js.map +1 -1
- package/dist/apps/data/routes/receipts.d.ts +4 -3
- package/dist/apps/data/routes/receipts.d.ts.map +1 -1
- package/dist/apps/data/routes/receipts.js +15 -14
- package/dist/apps/data/routes/receipts.js.map +1 -1
- package/dist/apps/data/routes/rpc.d.ts.map +1 -1
- package/dist/apps/data/routes/rpc.js +5 -1
- package/dist/apps/data/routes/rpc.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts +12 -12
- package/dist/apps/data/routes/tokens.d.ts +1 -1
- package/dist/apps/data/routes/tokens.d.ts.map +1 -1
- package/dist/apps/data/routes/tokens.js +4 -3
- package/dist/apps/data/routes/tokens.js.map +1 -1
- package/dist/apps/data/routes/transfers.d.ts.map +1 -1
- package/dist/apps/data/routes/transfers.js +25 -7
- package/dist/apps/data/routes/transfers.js.map +1 -1
- package/dist/apps/data/routes/verified-tokens.d.ts +37 -37
- package/dist/apps/data/routes/webhooks.d.ts +71 -1
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +177 -23
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/data/routes/zones.js +1 -1
- package/dist/apps/data/routes/zones.js.map +1 -1
- package/dist/apps/funding/App.d.ts +322 -89
- package/dist/apps/funding/App.d.ts.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.d.ts +375 -45
- package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.js +208 -12
- package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
- package/dist/apps/funding/routes/transfers.d.ts +32 -32
- package/dist/apps/funding/routes/transfers.js +1 -1
- package/dist/apps/funding/routes/transfers.js.map +1 -1
- package/dist/apps/management/App.d.ts +418 -418
- package/dist/apps/management/routes/api-keys.d.ts +50 -50
- package/dist/apps/management/routes/billing.d.ts +72 -72
- package/dist/apps/management/routes/billing.js +1 -1
- package/dist/apps/management/routes/billing.js.map +1 -1
- package/dist/apps/management/routes/faucet.d.ts +8 -8
- package/dist/apps/management/routes/faucet.js +2 -2
- package/dist/apps/management/routes/faucet.js.map +1 -1
- package/dist/apps/management/routes/invite-links.d.ts +42 -42
- package/dist/apps/management/routes/me.d.ts +6 -6
- package/dist/apps/management/routes/members.d.ts +14 -14
- package/dist/apps/management/routes/projects.d.ts +40 -40
- package/dist/apps/management/routes/usage.d.ts +8 -8
- package/dist/apps/management/routes/webhooks.d.ts +49 -49
- package/dist/apps/mcp.d.ts.map +1 -1
- package/dist/apps/mcp.js +4 -1
- package/dist/apps/mcp.js.map +1 -1
- package/dist/db/Db.d.ts +2 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +90 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +19 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +24 -0
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/fundingDepositAddresses.d.ts +20 -0
- package/dist/db/tables/fundingDepositAddresses.d.ts.map +1 -1
- package/dist/db/tables/fundingDepositAddresses.js +17 -0
- package/dist/db/tables/fundingDepositAddresses.js.map +1 -1
- package/dist/db/tables/fundingDeposits.d.ts +41 -0
- package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
- package/dist/db/tables/fundingDeposits.js +69 -0
- package/dist/db/tables/fundingDeposits.js.map +1 -1
- package/dist/db/tables/webhookQueueEvents.d.ts +122 -0
- package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -0
- package/dist/db/tables/webhookQueueEvents.js +160 -0
- package/dist/db/tables/webhookQueueEvents.js.map +1 -0
- package/dist/db/tables/webhookSubscriptions.d.ts +2 -0
- package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
- package/dist/db/tables/webhookSubscriptions.js +10 -0
- package/dist/db/tables/webhookSubscriptions.js.map +1 -1
- package/dist/internal/Auth.d.ts +3 -26
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +6 -37
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Credential.d.ts +27 -0
- package/dist/internal/Credential.d.ts.map +1 -0
- package/dist/internal/Credential.js +34 -0
- package/dist/internal/Credential.js.map +1 -0
- package/dist/internal/EarnRates.js +11 -7
- package/dist/internal/EarnRates.js.map +1 -1
- package/dist/internal/Log.d.ts +8 -0
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +4 -0
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/MetricSink.d.ts +51 -0
- package/dist/internal/MetricSink.d.ts.map +1 -1
- package/dist/internal/MetricSink.js +54 -2
- package/dist/internal/MetricSink.js.map +1 -1
- package/dist/internal/RateLimit.d.ts +4 -6
- package/dist/internal/RateLimit.d.ts.map +1 -1
- package/dist/internal/RateLimit.js +15 -14
- package/dist/internal/RateLimit.js.map +1 -1
- package/dist/internal/Store.d.ts +2 -1
- package/dist/internal/Store.d.ts.map +1 -1
- package/dist/internal/Store.js +57 -18
- package/dist/internal/Store.js.map +1 -1
- package/dist/internal/Tidx.d.ts +4 -3
- package/dist/internal/Tidx.d.ts.map +1 -1
- package/dist/internal/Tidx.js +2 -2
- package/dist/internal/Tidx.js.map +1 -1
- package/dist/internal/Value.d.ts +2 -0
- package/dist/internal/Value.d.ts.map +1 -1
- package/dist/internal/Value.js +25 -0
- package/dist/internal/Value.js.map +1 -1
- package/dist/internal/Viem.d.ts +4 -3
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Viem.js +3 -2
- package/dist/internal/Viem.js.map +1 -1
- package/dist/internal/WebhookTransfer.d.ts +20 -0
- package/dist/internal/WebhookTransfer.d.ts.map +1 -1
- package/dist/internal/WebhookTransfer.js +7 -4
- package/dist/internal/WebhookTransfer.js.map +1 -1
- package/dist/internal/Webhooks.d.ts +141 -0
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +139 -10
- package/dist/internal/Webhooks.js.map +1 -1
- package/dist/internal/funding/providers/relay.d.ts +3 -4
- package/dist/internal/funding/providers/relay.d.ts.map +1 -1
- package/dist/internal/funding/providers/relay.js +59 -8
- package/dist/internal/funding/providers/relay.js.map +1 -1
- package/dist/internal/index.d.ts +1 -0
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +1 -0
- package/dist/internal/index.js.map +1 -1
- package/package.json +3 -3
- package/src/ApiKeys.test.ts +20 -0
- package/src/ApiKeys.ts +28 -1
- package/src/App.test.ts +40 -8
- package/src/App.ts +1 -0
- package/src/Client.test-d.ts +34 -1
- package/src/Webhooks.ts +20 -18
- package/src/admin/apps/api-keys.test.ts +9 -0
- package/src/admin/apps/api-keys.ts +15 -0
- package/src/admin/ui/src/lib/query-client.ts +3 -2
- package/src/admin/ui/src/routes/api-keys.tsx +31 -9
- package/src/admin/ui.generated.ts +69 -69
- package/src/analytics/Analytics.test.ts +42 -0
- package/src/analytics/Analytics.ts +232 -0
- package/src/analytics/tables/requestEvents.ts +6 -0
- package/src/apps/Zones.test.ts +20 -3
- package/src/apps/Zones.ts +28 -8
- package/src/apps/composition.test.ts +2 -0
- package/src/apps/data/App.test.ts +2 -2
- package/src/apps/data/App.ts +2 -0
- package/src/apps/data/routes/activities.bench.ts +41 -0
- package/src/apps/data/routes/activities.test.ts +103 -2
- package/src/apps/data/routes/activities.ts +401 -2
- package/src/apps/data/routes/balances.bench.ts +21 -0
- package/src/apps/data/routes/balances.ts +7 -3
- package/src/apps/data/routes/blocks.bench.ts +34 -0
- package/src/apps/data/routes/coingecko.bench.ts +53 -0
- package/src/apps/data/routes/earn.bench.ts +71 -0
- package/src/apps/data/routes/earn.ts +66 -36
- package/src/apps/data/routes/exchanges.bench.ts +85 -0
- package/src/apps/data/routes/exchanges.test.ts +8 -3
- package/src/apps/data/routes/exchanges.ts +134 -82
- package/src/apps/data/routes/fee-amm.bench.ts +13 -0
- package/src/apps/data/routes/indexer.bench.ts +8 -0
- package/src/apps/data/routes/indexer.test.ts +2 -1
- package/src/apps/data/routes/indexer.ts +3 -2
- package/src/apps/data/routes/receipts.bench.ts +39 -0
- package/src/apps/data/routes/receipts.ts +16 -14
- package/src/apps/data/routes/rpc.bench.ts +14 -0
- package/src/apps/data/routes/rpc.test.ts +27 -0
- package/src/apps/data/routes/rpc.ts +5 -1
- package/src/apps/data/routes/tokenlist.bench.ts +3 -0
- package/src/apps/data/routes/tokens.bench.ts +76 -0
- package/src/apps/data/routes/tokens.ts +3 -2
- package/src/apps/data/routes/transactions.bench.ts +48 -0
- package/src/apps/data/routes/transfers.bench.ts +28 -0
- package/src/apps/data/routes/transfers.ts +36 -10
- package/src/apps/data/routes/valuation.bench.ts +14 -0
- package/src/apps/data/routes/verified-tokens.bench.ts +8 -0
- package/src/apps/data/routes/webhooks.bench.ts +26 -0
- package/src/apps/data/routes/webhooks.scan.test.ts +326 -0
- package/src/apps/data/routes/webhooks.test.ts +53 -6
- package/src/apps/data/routes/webhooks.ts +252 -24
- package/src/apps/data/routes/zones.bench.ts +32 -0
- package/src/apps/data/routes/zones.test.ts +1 -1
- package/src/apps/data/routes/zones.ts +1 -1
- package/src/apps/funding/routes/chains.test.ts +1 -1
- package/src/apps/funding/routes/deposit-addresses.test.ts +217 -3
- package/src/apps/funding/routes/deposit-addresses.ts +255 -19
- package/src/apps/funding/routes/providers.test.ts +1 -1
- package/src/apps/funding/routes/quotes.test.ts +1 -1
- package/src/apps/funding/routes/transfers.test.ts +20 -8
- package/src/apps/funding/routes/transfers.ts +1 -1
- package/src/apps/management/routes/billing.ts +1 -1
- package/src/apps/management/routes/faucet.ts +2 -2
- package/src/apps/management/routes/me.test.ts +31 -0
- package/src/apps/mcp.ts +4 -1
- package/src/db/Db.ts +117 -0
- package/src/db/Schema.ts +32 -0
- package/src/db/tables/fundingDepositAddresses.test.ts +28 -0
- package/src/db/tables/fundingDepositAddresses.ts +39 -0
- package/src/db/tables/fundingDeposits.test.ts +82 -0
- package/src/db/tables/fundingDeposits.ts +130 -0
- package/src/db/tables/webhookQueueEvents.ts +285 -0
- package/src/db/tables/webhookSubscriptions.ts +15 -0
- package/src/internal/Auth.test.ts +61 -98
- package/src/internal/Auth.ts +9 -63
- package/src/internal/Credential.test.ts +51 -0
- package/src/internal/Credential.ts +58 -0
- package/src/internal/EarnRates.ts +13 -7
- package/src/internal/EarnVaults.test.ts +2 -2
- package/src/internal/Log.test.ts +3 -1
- package/src/internal/Log.ts +14 -0
- package/src/internal/MetricSink.test.ts +108 -2
- package/src/internal/MetricSink.ts +112 -2
- package/src/internal/RateLimit.test.ts +23 -18
- package/src/internal/RateLimit.ts +23 -15
- package/src/internal/Store.test.ts +99 -13
- package/src/internal/Store.ts +52 -17
- package/src/internal/Tidx.test.ts +4 -4
- package/src/internal/Tidx.ts +5 -4
- package/src/internal/Value.test.ts +25 -0
- package/src/internal/Value.ts +29 -0
- package/src/internal/Viem.test.ts +4 -0
- package/src/internal/Viem.ts +6 -4
- package/src/internal/WebhookTransfer.test.ts +89 -0
- package/src/internal/WebhookTransfer.ts +11 -7
- package/src/internal/Webhooks.test.ts +488 -0
- package/src/internal/Webhooks.ts +291 -10
- package/src/internal/funding/Provider.test.ts +1 -1
- package/src/internal/funding/providers/index.test.ts +73 -3
- package/src/internal/funding/providers/relay.test.ts +1 -5
- package/src/internal/funding/providers/relay.ts +56 -12
- package/src/internal/index.test.ts +1 -0
- package/src/internal/index.ts +1 -0
|
@@ -2209,20 +2209,22 @@ export function exchanges(options: exchanges.Options = {}) {
|
|
|
2209
2209
|
transactionHash: options.transactionHash,
|
|
2210
2210
|
})
|
|
2211
2211
|
|
|
2212
|
-
// `route` spans every swap and fill side, so one lookup resolves each
|
|
2213
|
-
// token once for required RPC metadata and requested curated fields.
|
|
2214
|
-
const tokensByAddress = await Tokens.resolveTokens(c, {
|
|
2215
|
-
addresses: page.data.flatMap((swap) => swap.route),
|
|
2216
|
-
chainId,
|
|
2217
|
-
include: options.include,
|
|
2218
|
-
})
|
|
2219
2212
|
// Value amounts via their curated display currency; snapshot or rate
|
|
2220
2213
|
// failures degrade the values to `null`, never the feed.
|
|
2221
2214
|
const denomination = options['valuation.currency']
|
|
2222
|
-
|
|
2215
|
+
// Mode, verified-token, and RPC metadata reads depend only on the
|
|
2216
|
+
// assembled page, so overlap them before enriching its rows.
|
|
2217
|
+
const [, snapshot, tokensByAddress] = await Promise.all([
|
|
2218
|
+
resolveSwapModes(c, { chainId, swaps: page.data }),
|
|
2223
2219
|
denomination !== undefined && page.data.length > 0
|
|
2224
|
-
?
|
|
2225
|
-
: undefined
|
|
2220
|
+
? VerifiedTokens.snapshot(c, chainId).catch(() => undefined)
|
|
2221
|
+
: undefined,
|
|
2222
|
+
Tokens.resolveTokens(c, {
|
|
2223
|
+
addresses: page.data.flatMap((swap) => swap.route),
|
|
2224
|
+
chainId,
|
|
2225
|
+
include: options.include,
|
|
2226
|
+
}),
|
|
2227
|
+
])
|
|
2226
2228
|
const rates = denomination
|
|
2227
2229
|
? await Valuation.ratesFor(c, {
|
|
2228
2230
|
currencies: snapshot
|
|
@@ -3001,27 +3003,43 @@ export function exchanges(options: exchanges.Options = {}) {
|
|
|
3001
3003
|
const chainId = options.chainId ?? c.get('chainId')
|
|
3002
3004
|
|
|
3003
3005
|
try {
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3006
|
+
// Pair definitions are immutable after creation, so cache successful
|
|
3007
|
+
// point lookups without making unknown pairs stale.
|
|
3008
|
+
const target = await Store.memoize(
|
|
3009
|
+
async () =>
|
|
3010
|
+
(await getPairsByBase(c, { bases: [baseAddress], chainId })).get(baseAddress),
|
|
3011
|
+
{
|
|
3012
|
+
key: `exchange:v1:${chainId}:pair-by-base:${baseAddress}:v1`,
|
|
3013
|
+
store: c.get('store'),
|
|
3014
|
+
ttl: Ttl.minutes(5),
|
|
3015
|
+
},
|
|
3016
|
+
)
|
|
3017
|
+
if (!target)
|
|
3018
|
+
return pairNotFoundError(c, {
|
|
3019
|
+
base: baseAddress,
|
|
3020
|
+
pairIndex: await getPairIndex(c, { chainId }),
|
|
3021
|
+
})
|
|
3007
3022
|
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3023
|
+
// Token metadata is opt-in and resolves once per pair onto the top-level
|
|
3024
|
+
// `base`/`quote` slots. Run it alongside the independent depth RPC scan.
|
|
3025
|
+
const [{ asks, bids }, tokensByAddress] = await Promise.all([
|
|
3026
|
+
getPairDepth(c, {
|
|
3027
|
+
chainId,
|
|
3028
|
+
levels: options.levels,
|
|
3029
|
+
pair: target,
|
|
3030
|
+
}),
|
|
3031
|
+
options.include.includes('tokens')
|
|
3032
|
+
? Tokens.resolveTokens(c, {
|
|
3033
|
+
addresses: [target.base, target.quote],
|
|
3034
|
+
chainId,
|
|
3035
|
+
include: tokenFields,
|
|
3036
|
+
})
|
|
3037
|
+
: undefined,
|
|
3038
|
+
])
|
|
3013
3039
|
|
|
3014
|
-
// Token metadata is opt-in. Resolve once per pair and spread onto the
|
|
3015
|
-
// top-level `base`/`quote` PairToken slots. The Level rows carry no
|
|
3016
|
-
// token references so no per-row enrichment is needed.
|
|
3017
3040
|
let base: z.output<typeof schema.PairToken> = { address: target.base }
|
|
3018
3041
|
let quote: z.output<typeof schema.PairToken> = { address: target.quote }
|
|
3019
|
-
if (
|
|
3020
|
-
const tokensByAddress = await Tokens.resolveTokens(c, {
|
|
3021
|
-
addresses: [target.base, target.quote],
|
|
3022
|
-
chainId,
|
|
3023
|
-
include: tokenFields,
|
|
3024
|
-
})
|
|
3042
|
+
if (tokensByAddress) {
|
|
3025
3043
|
const baseMeta = tokensByAddress.get(target.base)
|
|
3026
3044
|
const quoteMeta = tokensByAddress.get(target.quote)
|
|
3027
3045
|
if (baseMeta) base = { ...base, ...baseMeta }
|
|
@@ -3536,9 +3554,9 @@ async function getSwaps(c: Context<App.Environment>, options: getSwaps.Options)
|
|
|
3536
3554
|
// the head page.
|
|
3537
3555
|
const cursor = cursorToken ? Cursor.decode(cursorToken, ['int', 'int', 'int']) : undefined
|
|
3538
3556
|
|
|
3539
|
-
//
|
|
3540
|
-
// `swapWindowBlocks`).
|
|
3541
|
-
type BlockWindow = { hi: number; lo: number }
|
|
3557
|
+
// `dex_fills` block window bounding one grouped batch (see
|
|
3558
|
+
// `swapWindowBlocks`); the head variant derives its lower bound in SQL.
|
|
3559
|
+
type BlockWindow = { hi: number; lo: number } | 'indexedHead'
|
|
3542
3560
|
|
|
3543
3561
|
// Row-level predicates (constant within a group) prune before aggregation.
|
|
3544
3562
|
// These are batch-invariant; the cursor's per-batch block bound is added in
|
|
@@ -3600,6 +3618,12 @@ async function getSwaps(c: Context<App.Environment>, options: getSwaps.Options)
|
|
|
3600
3618
|
// the group timestamp.
|
|
3601
3619
|
const fetchGroups = (activeCursor: typeof cursor, batchLimit: number, window: BlockWindow) => {
|
|
3602
3620
|
const { having, where } = withCursorSql(activeCursor)
|
|
3621
|
+
const windowFilters =
|
|
3622
|
+
window === 'indexedHead'
|
|
3623
|
+
? [
|
|
3624
|
+
`f.block_num >= greatest(toInt128((SELECT max(num) FROM blocks)) - ${swapWindowBlocks - 1}, toInt128(0))`,
|
|
3625
|
+
]
|
|
3626
|
+
: [`f.block_num >= ${window.lo}`, `f.block_num <= ${window.hi}`]
|
|
3603
3627
|
return tidx.fetch({
|
|
3604
3628
|
chainId,
|
|
3605
3629
|
engine: 'clickhouse',
|
|
@@ -3609,7 +3633,7 @@ async function getSwaps(c: Context<App.Environment>, options: getSwaps.Options)
|
|
|
3609
3633
|
groupArray(tuple(f.log_idx, toString(f."orderId"), f.maker,
|
|
3610
3634
|
toString(f."amountFilled"), f."partialFill")) AS fills
|
|
3611
3635
|
FROM dex_fills f
|
|
3612
|
-
WHERE ${[...where,
|
|
3636
|
+
WHERE ${[...where, ...windowFilters].join(' AND ')}
|
|
3613
3637
|
GROUP BY f.block_num, f.tx_hash, f.taker
|
|
3614
3638
|
${having}
|
|
3615
3639
|
ORDER BY block_num ${direction}, first_idx ${direction}
|
|
@@ -3667,19 +3691,16 @@ async function getSwaps(c: Context<App.Environment>, options: getSwaps.Options)
|
|
|
3667
3691
|
}),
|
|
3668
3692
|
)
|
|
3669
3693
|
|
|
3670
|
-
// Each fill's book quote
|
|
3671
|
-
//
|
|
3672
|
-
//
|
|
3673
|
-
// decode (genesis system books) and have no `dex_pairs` row.
|
|
3694
|
+
// Each fill's book quote comes back with its order state when indexed;
|
|
3695
|
+
// otherwise read the base token's on-chain `quoteToken()` — some books
|
|
3696
|
+
// predate the indexer's `PairCreated` decode (genesis system books).
|
|
3674
3697
|
const bases = [
|
|
3675
3698
|
...new Set([...states.values()].flatMap((events) => events.map((event) => event.token))),
|
|
3676
3699
|
]
|
|
3677
|
-
const pairs = await getPairsByBase(c, { bases, chainId })
|
|
3678
3700
|
const quoteByBase = new Map<string, z.output<typeof Schema.TokenAddress>>()
|
|
3679
|
-
for (const
|
|
3680
|
-
const
|
|
3681
|
-
|
|
3682
|
-
}
|
|
3701
|
+
for (const events of states.values())
|
|
3702
|
+
for (const event of events)
|
|
3703
|
+
if (event.quoteToken !== undefined) quoteByBase.set(event.token, event.quoteToken)
|
|
3683
3704
|
const fallback = await Timing.time(c, 'swap_quotes', () =>
|
|
3684
3705
|
resolveBookQuotes(c, {
|
|
3685
3706
|
bases: bases.filter((bookBase) => !quoteByBase.has(bookBase)),
|
|
@@ -3763,7 +3784,12 @@ async function getSwaps(c: Context<App.Environment>, options: getSwaps.Options)
|
|
|
3763
3784
|
return row ? Value.toNumber(row['block_num']) : undefined
|
|
3764
3785
|
}
|
|
3765
3786
|
|
|
3766
|
-
type ScanBounds =
|
|
3787
|
+
type ScanBounds = {
|
|
3788
|
+
firstMatchingBlock?: number | undefined
|
|
3789
|
+
firstWindow?: 'indexedHead' | undefined
|
|
3790
|
+
hi: number
|
|
3791
|
+
lo: number
|
|
3792
|
+
}
|
|
3767
3793
|
const bounds: ScanBounds | undefined = await Timing.time(c, 'swap_bounds', async () => {
|
|
3768
3794
|
if (selective) {
|
|
3769
3795
|
const firstMatchingBlock = await findMatchingBlock(cursorBlock)
|
|
@@ -3775,6 +3801,17 @@ async function getSwaps(c: Context<App.Environment>, options: getSwaps.Options)
|
|
|
3775
3801
|
}
|
|
3776
3802
|
: undefined
|
|
3777
3803
|
}
|
|
3804
|
+
// The newest unfiltered page can derive its 100k-block lower bound from
|
|
3805
|
+
// `blocks` inside the grouped query, avoiding a separate TIDX request.
|
|
3806
|
+
if (
|
|
3807
|
+
order === 'desc' &&
|
|
3808
|
+
cursorBlock === undefined &&
|
|
3809
|
+
fromBlock === undefined &&
|
|
3810
|
+
fromTimestamp === undefined &&
|
|
3811
|
+
toBlock === undefined &&
|
|
3812
|
+
toTimestamp === undefined
|
|
3813
|
+
)
|
|
3814
|
+
return { firstWindow: 'indexedHead', hi: Number.MAX_SAFE_INTEGER, lo: 0 }
|
|
3778
3815
|
const result = await tidx.fetch({
|
|
3779
3816
|
chainId,
|
|
3780
3817
|
engine: 'clickhouse',
|
|
@@ -3802,7 +3839,8 @@ async function getSwaps(c: Context<App.Environment>, options: getSwaps.Options)
|
|
|
3802
3839
|
let activeCursor = cursor
|
|
3803
3840
|
|
|
3804
3841
|
// Selective scans jump to the next populated block through a bounded
|
|
3805
|
-
// lookup. Unfiltered scans retain the fixed-stride min/max sweep
|
|
3842
|
+
// lookup. Unfiltered scans retain the fixed-stride min/max sweep after
|
|
3843
|
+
// the newest page's indexed-head window.
|
|
3806
3844
|
const windowFrom = async (
|
|
3807
3845
|
anchor: number | undefined,
|
|
3808
3846
|
knownMatchingBlock?: number,
|
|
@@ -3821,13 +3859,20 @@ async function getSwaps(c: Context<App.Environment>, options: getSwaps.Options)
|
|
|
3821
3859
|
: undefined
|
|
3822
3860
|
}
|
|
3823
3861
|
|
|
3824
|
-
let window
|
|
3862
|
+
let window: BlockWindow | undefined =
|
|
3863
|
+
bounds.firstWindow ?? (await windowFrom(cursorBlock, bounds.firstMatchingBlock))
|
|
3825
3864
|
|
|
3826
3865
|
// Slide to the next disjoint window away from the newest block and reset
|
|
3827
3866
|
// the cursor: every row in the next window is already past the cursor
|
|
3828
3867
|
// anchor in the requested order, so no anchor refinement is needed.
|
|
3829
|
-
const slideWindow = async () => {
|
|
3868
|
+
const slideWindow = async (indexedHeadAnchor?: number) => {
|
|
3830
3869
|
activeCursor = undefined
|
|
3870
|
+
if (window === 'indexedHead') {
|
|
3871
|
+
const matchingBlock = await findMatchingBlock(indexedHeadAnchor)
|
|
3872
|
+
window =
|
|
3873
|
+
matchingBlock === undefined ? undefined : await windowFrom(matchingBlock, matchingBlock)
|
|
3874
|
+
return
|
|
3875
|
+
}
|
|
3831
3876
|
window =
|
|
3832
3877
|
window === undefined
|
|
3833
3878
|
? undefined
|
|
@@ -3863,7 +3908,7 @@ async function getSwaps(c: Context<App.Environment>, options: getSwaps.Options)
|
|
|
3863
3908
|
// A short batch exhausts this window; a full one may leave more groups
|
|
3864
3909
|
// in it, so advance the keyset cursor and re-query the same window.
|
|
3865
3910
|
if (!fetchedFull) {
|
|
3866
|
-
await slideWindow()
|
|
3911
|
+
await slideWindow(order === 'desc' ? tail.blockNumber - 1 : tail.blockNumber + 1)
|
|
3867
3912
|
continue
|
|
3868
3913
|
}
|
|
3869
3914
|
const next = cursorAfterWholeGroup(tail)
|
|
@@ -3896,13 +3941,7 @@ async function getSwaps(c: Context<App.Environment>, options: getSwaps.Options)
|
|
|
3896
3941
|
])
|
|
3897
3942
|
: null
|
|
3898
3943
|
|
|
3899
|
-
|
|
3900
|
-
// Recover each swap's `mode` from transaction calldata — deferred to here
|
|
3901
|
-
// so the candidate scan never pays the `txs` lookup for rows beyond the
|
|
3902
|
-
// page. Best-effort: a failed lookup leaves `mode: null`.
|
|
3903
|
-
await resolveSwapModes(c, { chainId, swaps: data })
|
|
3904
|
-
|
|
3905
|
-
return { data, nextCursor }
|
|
3944
|
+
return { data: pageRows.map((candidate) => candidate.swap), nextCursor }
|
|
3906
3945
|
})
|
|
3907
3946
|
}
|
|
3908
3947
|
|
|
@@ -4054,6 +4093,8 @@ declare namespace parseGroupRow {
|
|
|
4054
4093
|
* history compressed to the latest event (`argMax`, sentinel position
|
|
4055
4094
|
* `(-1, -1)` so it sorts first) plus every raw in-page event, so an order
|
|
4056
4095
|
* that flips between two of its page fills resolves each fill correctly.
|
|
4096
|
+
* The same query attaches each book's indexed quote token, avoiding a second
|
|
4097
|
+
* TIDX request; callers retain an RPC fallback for books absent from the index.
|
|
4057
4098
|
* `logs` is sorted by `(address, selector, block_num, log_idx)` with a
|
|
4058
4099
|
* `topic1` bloom index, so both halves are pruned range reads. Returned
|
|
4059
4100
|
* per-order event lists are position-sorted.
|
|
@@ -4078,16 +4119,27 @@ export async function resolveOrderStates(
|
|
|
4078
4119
|
chainId,
|
|
4079
4120
|
engine: 'clickhouse',
|
|
4080
4121
|
query: `
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4122
|
+
WITH states AS (
|
|
4123
|
+
SELECT topic1, argMax(topic3, (block_num, log_idx)) AS t3,
|
|
4124
|
+
argMax(data, (block_num, log_idx)) AS payload,
|
|
4125
|
+
toInt64(-1) AS bn, toInt32(-1) AS li
|
|
4126
|
+
FROM logs
|
|
4127
|
+
WHERE ${eventFilter} AND block_num < ${minBlock}
|
|
4128
|
+
GROUP BY topic1
|
|
4129
|
+
UNION ALL
|
|
4130
|
+
SELECT topic1, topic3 AS t3, data AS payload, block_num AS bn, log_idx AS li
|
|
4131
|
+
FROM logs
|
|
4132
|
+
WHERE ${eventFilter} AND block_num >= ${minBlock} AND block_num <= ${maxBlock}
|
|
4133
|
+
)
|
|
4134
|
+
SELECT states.*, pairs.quote AS quote_token
|
|
4135
|
+
FROM states
|
|
4136
|
+
LEFT JOIN (
|
|
4137
|
+
-- Preserve the oldest-pair rule without sorting the complete table.
|
|
4138
|
+
SELECT base, argMin(quote, (block_num, log_idx)) AS quote
|
|
4139
|
+
FROM dex_pairs
|
|
4140
|
+
WHERE address = '${stablecoinDex}'
|
|
4141
|
+
GROUP BY base
|
|
4142
|
+
) AS pairs ON pairs.base = concat('0x', lower(substring(states.t3, 27)))
|
|
4091
4143
|
` as string,
|
|
4092
4144
|
})
|
|
4093
4145
|
|
|
@@ -4123,6 +4175,8 @@ declare namespace resolveOrderStates {
|
|
|
4123
4175
|
isBid: boolean
|
|
4124
4176
|
/** Event log index, or `-1` for the baseline. */
|
|
4125
4177
|
logIndex: number
|
|
4178
|
+
/** Indexed quote token for the state's book, when available. */
|
|
4179
|
+
quoteToken?: z.output<typeof Schema.TokenAddress> | undefined
|
|
4126
4180
|
/** Signed price tick at this state. */
|
|
4127
4181
|
tick: number
|
|
4128
4182
|
/** Book base token (lowercase address). */
|
|
@@ -4144,6 +4198,7 @@ function parseStateRow(
|
|
|
4144
4198
|
const payload = Value.toText(row['payload'])
|
|
4145
4199
|
const blockNumber = Value.toNumber(row['bn'])
|
|
4146
4200
|
const logIndex = Value.toNumber(row['li'])
|
|
4201
|
+
const quoteToken = Schema.TokenAddress.safeParse(row['quote_token'])
|
|
4147
4202
|
if (
|
|
4148
4203
|
topic1 === undefined ||
|
|
4149
4204
|
!/^0x[0-9a-fA-F]{64}$/.test(topic1) ||
|
|
@@ -4166,6 +4221,7 @@ function parseStateRow(
|
|
|
4166
4221
|
blockNumber,
|
|
4167
4222
|
isBid,
|
|
4168
4223
|
logIndex,
|
|
4224
|
+
...(quoteToken.success ? { quoteToken: quoteToken.data } : {}),
|
|
4169
4225
|
tick,
|
|
4170
4226
|
token: `0x${topic3.slice(26).toLowerCase()}`,
|
|
4171
4227
|
},
|
|
@@ -4718,15 +4774,10 @@ const globalOrderScanCap = 2000
|
|
|
4718
4774
|
|
|
4719
4775
|
/**
|
|
4720
4776
|
* Batch size for `OrderCancelled` / `OrderFilled` `WHERE "orderId" IN (...)`
|
|
4721
|
-
* lookups.
|
|
4722
|
-
*
|
|
4723
|
-
* `tempoxyz/ak47` commit `273d88d`); very large lists still inflate SQL parse
|
|
4724
|
-
* cost, but each batch is also an independent transient-error round trip, so
|
|
4725
|
-
* 1000 balances parse cost against keeping the batch count (and thus the
|
|
4726
|
-
* failure surface) low — a typical pair snapshot still resolves in one trip and
|
|
4727
|
-
* the capped global feed in ~two.
|
|
4777
|
+
* lookups. A 2000-ID batch uses indexed state paths while native fill
|
|
4778
|
+
* aggregation keeps the response below SQL and result limits.
|
|
4728
4779
|
*/
|
|
4729
|
-
const orderInBatchSize =
|
|
4780
|
+
const orderInBatchSize = 2000
|
|
4730
4781
|
|
|
4731
4782
|
/**
|
|
4732
4783
|
* Signed-int16 bias used to encode `tick` in the cursor as a non-negative
|
|
@@ -4745,8 +4796,8 @@ const orderCursorTickBias = 32768
|
|
|
4745
4796
|
* when scoped (string column with a bloom-filter index on `topic1`), and the
|
|
4746
4797
|
* fill / cancellation joins use `WHERE "orderId" IN (...)` against the same
|
|
4747
4798
|
* index. The decoded `tick` column has no ClickHouse index, so tick sort is
|
|
4748
|
-
* still applied app-side.
|
|
4749
|
-
*
|
|
4799
|
+
* still applied app-side. ClickHouse aggregates fills per order before the
|
|
4800
|
+
* app computes each remaining amount.
|
|
4750
4801
|
*
|
|
4751
4802
|
* `remaining = OrderPlaced.amount - sum(OrderFilled.amountFilled)`; orders
|
|
4752
4803
|
* with an `OrderCancelled` event are excluded outright. Each row is annotated
|
|
@@ -5012,7 +5063,7 @@ declare namespace getPairDepth {
|
|
|
5012
5063
|
chainId: z.output<typeof Schema.ChainId>
|
|
5013
5064
|
/** Maximum number of non-empty levels to keep per side, in best-first order. */
|
|
5014
5065
|
levels: number
|
|
5015
|
-
pair:
|
|
5066
|
+
pair: getPairsByBase.Pair
|
|
5016
5067
|
}
|
|
5017
5068
|
}
|
|
5018
5069
|
|
|
@@ -5362,7 +5413,7 @@ async function fetchCancelledByIds(
|
|
|
5362
5413
|
query: `
|
|
5363
5414
|
SELECT "orderId"
|
|
5364
5415
|
FROM OrderCancelled
|
|
5365
|
-
WHERE "orderId" IN (${batch.join(', ')})
|
|
5416
|
+
WHERE address = '${stablecoinDex}' AND "orderId" IN (${batch.join(', ')})
|
|
5366
5417
|
`,
|
|
5367
5418
|
signatures: [orderCancelledSignature],
|
|
5368
5419
|
})
|
|
@@ -5375,10 +5426,9 @@ async function fetchCancelledByIds(
|
|
|
5375
5426
|
}
|
|
5376
5427
|
|
|
5377
5428
|
/**
|
|
5378
|
-
* Sums `amountFilled` per `orderId` across all matching `
|
|
5379
|
-
* the snapshot window via batched `WHERE "orderId" IN (...)` queries.
|
|
5380
|
-
*
|
|
5381
|
-
* (indexer-side `SUM(...)` over decoded columns is currently unreliable).
|
|
5429
|
+
* Sums `amountFilled` per `orderId` across all matching native `dex_fills`
|
|
5430
|
+
* rows in the snapshot window via batched `WHERE "orderId" IN (...)` queries.
|
|
5431
|
+
* Native aggregation bounds results, and stringification preserves UInt256 precision through JSON parsing.
|
|
5382
5432
|
*/
|
|
5383
5433
|
async function fetchFillSumsByIds(
|
|
5384
5434
|
c: Context<App.Environment>,
|
|
@@ -5396,17 +5446,19 @@ async function fetchFillSumsByIds(
|
|
|
5396
5446
|
const result = await tidx.fetch({
|
|
5397
5447
|
chainId,
|
|
5398
5448
|
engine: 'clickhouse',
|
|
5449
|
+
// Dynamic order IDs prevent signature-backed row inference; each aggregate is validated below before use.
|
|
5399
5450
|
query: `
|
|
5400
|
-
SELECT "orderId", "amountFilled"
|
|
5451
|
+
SELECT "orderId", toString(sum("amountFilled")) AS amount_filled
|
|
5401
5452
|
FROM dex_fills
|
|
5402
|
-
WHERE "orderId" IN (${batch.join(', ')})
|
|
5453
|
+
WHERE address = '${stablecoinDex}' AND "orderId" IN (${batch.join(', ')})
|
|
5454
|
+
GROUP BY "orderId"
|
|
5403
5455
|
` as string,
|
|
5404
5456
|
})
|
|
5405
5457
|
for (const row of result.rows) {
|
|
5406
5458
|
const id = Value.toIntegerString(row['orderId'])
|
|
5407
|
-
const filled = Value.toIntegerString(row['
|
|
5459
|
+
const filled = Value.toIntegerString(row['amount_filled'])
|
|
5408
5460
|
if (id === undefined || filled === undefined) continue
|
|
5409
|
-
sums.set(id,
|
|
5461
|
+
sums.set(id, BigInt(filled))
|
|
5410
5462
|
}
|
|
5411
5463
|
}
|
|
5412
5464
|
return sums
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as QueryBench from '../../../../test/QueryBench.js'
|
|
2
|
+
|
|
3
|
+
QueryBench.bench({
|
|
4
|
+
name: 'getFeeAmmPools',
|
|
5
|
+
path: '/v1/fee-amm/pools',
|
|
6
|
+
queries: ['include=token.logoUri', 'include=token.verified'],
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
QueryBench.bench({
|
|
10
|
+
name: 'getFeeAmmMints',
|
|
11
|
+
path: '/v1/fee-amm/mints',
|
|
12
|
+
queries: ['include=totalCount'],
|
|
13
|
+
})
|
|
@@ -150,7 +150,7 @@ describe('GET /indexer/query', () => {
|
|
|
150
150
|
const client = TestApp.client()
|
|
151
151
|
const response = await client.v1.indexer.query.$get(
|
|
152
152
|
{ query: { sql: 'select 1', chainId } },
|
|
153
|
-
TestApp.
|
|
153
|
+
{ headers: { 'x-api-key': TestApp.key.token } },
|
|
154
154
|
)
|
|
155
155
|
|
|
156
156
|
expect(response.status).toMatchInlineSnapshot(`200`)
|
|
@@ -161,6 +161,7 @@ describe('GET /indexer/query', () => {
|
|
|
161
161
|
// Tempo API enforces the documented execution bounds upstream even when the
|
|
162
162
|
// caller omits them.
|
|
163
163
|
const upstream = new URL((fetch.mock.calls[0]![0] as Request).url)
|
|
164
|
+
expect((fetch.mock.calls[0]![0] as Request).headers.get('x-api-key')).toBeNull()
|
|
164
165
|
expect(upstream.searchParams.get('timeout_ms')).toMatchInlineSnapshot(`"5000"`)
|
|
165
166
|
expect(upstream.searchParams.get('limit')).toMatchInlineSnapshot(`"10000"`)
|
|
166
167
|
} finally {
|
|
@@ -23,10 +23,11 @@ const strippedHeaders = new Set([
|
|
|
23
23
|
'keep-alive',
|
|
24
24
|
'proxy-authorization',
|
|
25
25
|
'te',
|
|
26
|
+
'tempo-api-key',
|
|
26
27
|
'trailer',
|
|
27
28
|
'transfer-encoding',
|
|
28
29
|
'upgrade',
|
|
29
|
-
'
|
|
30
|
+
'x-api-key',
|
|
30
31
|
ZoneRpcAuthentication.headerName.toLowerCase(),
|
|
31
32
|
])
|
|
32
33
|
|
|
@@ -63,7 +64,7 @@ export namespace schema {
|
|
|
63
64
|
z.meta({ examples: [1] }),
|
|
64
65
|
),
|
|
65
66
|
engine: z
|
|
66
|
-
.optional(z.enum(['postgres', 'clickhouse']))
|
|
67
|
+
.optional(z.enum(['postgres', 'clickhouse', 'tiered']))
|
|
67
68
|
.check(
|
|
68
69
|
z.describe('Indexer engine that ran the query.'),
|
|
69
70
|
z.meta({ examples: ['postgres'] }),
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as QueryBench from '../../../../test/QueryBench.js'
|
|
2
|
+
|
|
3
|
+
const hash = {
|
|
4
|
+
4217: '0x7f98e4b2bf4d1444b257526f7dd294ac3480d73cd91f723bf0df6062d1ba1c82',
|
|
5
|
+
42431: '0x1c8ccebb169f863b72388cc057fcecee240032eb7d2c64aa6f6fb8e5a60d3f9a',
|
|
6
|
+
} as const
|
|
7
|
+
|
|
8
|
+
const address = {
|
|
9
|
+
4217: '0xc24357119432595a547cb2c1e41c51d753645519',
|
|
10
|
+
42431: '0x15496d3123afb2cfadc0ac068eadd11d0f8ae799',
|
|
11
|
+
} as const
|
|
12
|
+
|
|
13
|
+
QueryBench.bench({
|
|
14
|
+
name: 'getReceipts',
|
|
15
|
+
path: '/v1/transactions/receipts',
|
|
16
|
+
queries: [
|
|
17
|
+
'include=feeToken.logoUri',
|
|
18
|
+
'include=feeToken.verified',
|
|
19
|
+
'include=totalCount',
|
|
20
|
+
'valuation.currency=USD',
|
|
21
|
+
],
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
QueryBench.bench({
|
|
25
|
+
name: 'getReceipts?address&include=totalCount',
|
|
26
|
+
path: {
|
|
27
|
+
4217: `/v1/transactions/receipts?address=${address[4217]}&include=totalCount`,
|
|
28
|
+
42431: `/v1/transactions/receipts?address=${address[42431]}&include=totalCount`,
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
QueryBench.bench({
|
|
33
|
+
name: 'getReceipt',
|
|
34
|
+
path: {
|
|
35
|
+
4217: `/v1/transactions/${hash[4217]}/receipt`,
|
|
36
|
+
42431: `/v1/transactions/${hash[42431]}/receipt`,
|
|
37
|
+
},
|
|
38
|
+
queries: ['include=feeToken.logoUri', 'include=feeToken.verified', 'valuation.currency=USD'],
|
|
39
|
+
})
|
|
@@ -976,9 +976,10 @@ function countFilters(options: countReceipts.Options): string[] {
|
|
|
976
976
|
|
|
977
977
|
/**
|
|
978
978
|
* Capped total count of receipts matching the same filters as the page. The
|
|
979
|
-
* address scope reuses the page's `UNION` workaround
|
|
980
|
-
*
|
|
981
|
-
*
|
|
979
|
+
* address scope reuses the page's `UNION` workaround. Fee-payer filters make a
|
|
980
|
+
* direct `receipts.to` predicate selective; otherwise the `to` side resolves
|
|
981
|
+
* hashes via the `txs` index. The deduped keyset avoids double-counting
|
|
982
|
+
* self-receipts (`from = to`).
|
|
982
983
|
* `totalCountCapped: true` is a lower bound — at least {@link Schema.countCap} matches.
|
|
983
984
|
*/
|
|
984
985
|
export function countReceipts(
|
|
@@ -995,10 +996,11 @@ export function countReceipts(
|
|
|
995
996
|
let query: string
|
|
996
997
|
if (address !== undefined) {
|
|
997
998
|
const fromWhere = [`"from" = '${address}'`, ...filters].join(' AND ')
|
|
998
|
-
const
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
999
|
+
const toPredicate =
|
|
1000
|
+
options.feePayer !== undefined
|
|
1001
|
+
? `"to" = '${address}'`
|
|
1002
|
+
: `tx_hash IN (SELECT hash FROM txs WHERE "to" = '${address}')`
|
|
1003
|
+
const toWhere = [toPredicate, ...filters].join(' AND ')
|
|
1002
1004
|
query = `
|
|
1003
1005
|
SELECT count(*) AS total
|
|
1004
1006
|
FROM (
|
|
@@ -1069,13 +1071,13 @@ export async function embedReceipts(
|
|
|
1069
1071
|
const { chainId, include, rows } = options
|
|
1070
1072
|
const found = new Map<string, humanizeReceipt.Output>()
|
|
1071
1073
|
|
|
1072
|
-
// Only mined rows can have a receipt; their block numbers also
|
|
1073
|
-
//
|
|
1074
|
-
const blocks
|
|
1074
|
+
// Only mined rows can have a receipt; their block numbers also constrain the
|
|
1075
|
+
// `logs` lookup to the page's exact blocks.
|
|
1076
|
+
const blocks = new Set<number>()
|
|
1075
1077
|
const hashes = new Set<string>()
|
|
1076
1078
|
for (const row of rows) {
|
|
1077
1079
|
if (row.blockNumber === null) continue
|
|
1078
|
-
blocks.
|
|
1080
|
+
blocks.add(row.blockNumber)
|
|
1079
1081
|
hashes.add(row.hash.toLowerCase())
|
|
1080
1082
|
}
|
|
1081
1083
|
if (hashes.size === 0) return new Map()
|
|
@@ -1086,8 +1088,8 @@ export async function embedReceipts(
|
|
|
1086
1088
|
// Raw-table queries (no signature-decoded CTE), so the inline SQL is cast to
|
|
1087
1089
|
// `string`. The receipts lookup needs no block bound (`receipts.tx_hash` is
|
|
1088
1090
|
// indexed — see the `sideQuery` comment in `getReceipts`); the logs lookup
|
|
1089
|
-
//
|
|
1090
|
-
//
|
|
1091
|
+
// uses the page's exact blocks so sparse transaction pages do not scan every
|
|
1092
|
+
// block between their oldest and newest rows.
|
|
1091
1093
|
const [receipts_result, logs_result] = await Timing.time(c, 'receipts_embed', () =>
|
|
1092
1094
|
Promise.all([
|
|
1093
1095
|
options.receipts
|
|
@@ -1100,7 +1102,7 @@ export async function embedReceipts(
|
|
|
1100
1102
|
tidx.fetch({
|
|
1101
1103
|
chainId,
|
|
1102
1104
|
query:
|
|
1103
|
-
`SELECT tx_hash, block_num, tx_idx, log_idx, address, selector, topic1, topic2, topic3, data, block_timestamp FROM logs WHERE block_num
|
|
1105
|
+
`SELECT tx_hash, block_num, tx_idx, log_idx, address, selector, topic1, topic2, topic3, data, block_timestamp FROM logs WHERE block_num IN (${[...blocks].join(', ')}) AND tx_hash IN (${inList}) ORDER BY block_num ASC, log_idx ASC` as string,
|
|
1104
1106
|
}),
|
|
1105
1107
|
]),
|
|
1106
1108
|
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as QueryBench from '../../../../test/QueryBench.js'
|
|
2
|
+
|
|
3
|
+
QueryBench.bench({
|
|
4
|
+
body: {
|
|
5
|
+
id: 1,
|
|
6
|
+
jsonrpc: '2.0',
|
|
7
|
+
method: 'eth_blockNumber',
|
|
8
|
+
params: [],
|
|
9
|
+
},
|
|
10
|
+
chainId: false,
|
|
11
|
+
method: 'POST',
|
|
12
|
+
name: 'rpcRequest',
|
|
13
|
+
path: { 4217: '/rpc/mainnet', 42431: '/rpc/testnet' },
|
|
14
|
+
})
|
|
@@ -53,6 +53,30 @@ function zoneToken() {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
describe('POST /rpc', () => {
|
|
56
|
+
test('allows 20 public requests per second', async () => {
|
|
57
|
+
vi.useFakeTimers({ now: new Date('2026-08-12T00:00:00.000Z') })
|
|
58
|
+
try {
|
|
59
|
+
const app = TestApp.create()
|
|
60
|
+
const request = () =>
|
|
61
|
+
app.request('/rpc', {
|
|
62
|
+
body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'tempo_blockNumber' }),
|
|
63
|
+
method: 'POST',
|
|
64
|
+
})
|
|
65
|
+
const responses = await Promise.all(
|
|
66
|
+
Array.from({ length: 21 }, () => Promise.resolve(request())),
|
|
67
|
+
)
|
|
68
|
+
const limited = responses.find((response) => response.status === 429)!
|
|
69
|
+
|
|
70
|
+
expect(responses.filter((response) => response.status === 401)).toHaveLength(20)
|
|
71
|
+
expect(limited.headers.get('RateLimit-Limit')).toBe('20')
|
|
72
|
+
|
|
73
|
+
vi.advanceTimersByTime(1_000)
|
|
74
|
+
expect((await Promise.resolve(request())).status).toBe(401)
|
|
75
|
+
} finally {
|
|
76
|
+
vi.useRealTimers()
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
56
80
|
test('routes by principal: a resolver sees the request principal and routes accordingly', async () => {
|
|
57
81
|
const fetch = mockRpc()
|
|
58
82
|
// A resolver that routes API-key principals to the private upstream and
|
|
@@ -113,6 +137,7 @@ describe('POST /rpc', () => {
|
|
|
113
137
|
'content-type': 'application/json',
|
|
114
138
|
cookie: 'session=secret',
|
|
115
139
|
'tempo-api-key': 'caller-key',
|
|
140
|
+
'x-api-key': 'legacy-caller-key',
|
|
116
141
|
'x-authorization-token': 'zone-secret',
|
|
117
142
|
'x-client-trace': 'trace_test',
|
|
118
143
|
},
|
|
@@ -139,6 +164,7 @@ describe('POST /rpc', () => {
|
|
|
139
164
|
expect(call.headers['authorization']).toMatchInlineSnapshot(`"Basic cnBjOnNlY3JldA=="`)
|
|
140
165
|
expect(call.headers['cookie']).toMatchInlineSnapshot(`null`)
|
|
141
166
|
expect(call.headers['tempo-api-key']).toMatchInlineSnapshot(`null`)
|
|
167
|
+
expect(call.headers['x-api-key']).toBeNull()
|
|
142
168
|
expect(call.headers['x-authorization-token']).toBeNull()
|
|
143
169
|
expect(call.headers['x-client-trace']).toMatchInlineSnapshot(`"trace_test"`)
|
|
144
170
|
} finally {
|
|
@@ -1156,6 +1182,7 @@ function mockRpc() {
|
|
|
1156
1182
|
authorization: request.headers.get('authorization'),
|
|
1157
1183
|
cookie: request.headers.get('cookie'),
|
|
1158
1184
|
'tempo-api-key': request.headers.get('tempo-api-key'),
|
|
1185
|
+
'x-api-key': request.headers.get('x-api-key'),
|
|
1159
1186
|
'x-authorization-token': request.headers.get('x-authorization-token'),
|
|
1160
1187
|
'x-client-trace': request.headers.get('x-client-trace'),
|
|
1161
1188
|
},
|
|
@@ -26,6 +26,7 @@ const strippedHeaders = new Set([
|
|
|
26
26
|
'trailer',
|
|
27
27
|
'transfer-encoding',
|
|
28
28
|
'upgrade',
|
|
29
|
+
'x-api-key',
|
|
29
30
|
ZoneRpcAuthentication.headerName.toLowerCase(),
|
|
30
31
|
])
|
|
31
32
|
|
|
@@ -163,7 +164,10 @@ export namespace schema {
|
|
|
163
164
|
export function rpc() {
|
|
164
165
|
return new Hono<App.Environment>().post(
|
|
165
166
|
'/rpc/:chain{(mainnet|testnet|[0-9]+)}?',
|
|
166
|
-
Auth.policy({
|
|
167
|
+
Auth.policy({
|
|
168
|
+
apiKey: { scopes: ['data:read'] },
|
|
169
|
+
public: { rateLimit: { limit: 20, period: 'second' } },
|
|
170
|
+
}),
|
|
167
171
|
OpenApi.describeRoute({
|
|
168
172
|
description:
|
|
169
173
|
'Sends a single or batch Ethereum JSON-RPC request directly to Tempo. Use `/rpc/testnet`, `/rpc/mainnet`, or `/rpc/:chain` to choose a chain. Zone read scopes permit read-only methods; Zone write scopes also permit signed transaction broadcasts. A Zone token in `X-Authorization-Token` is forwarded to the authenticated Zone RPC. This passthrough is not part of the stable API contract and is supported on a best-effort basis only. It has no compatibility, latency, availability, or data-freshness guarantees, and breaking changes may happen with limited notice.',
|