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.
Files changed (295) hide show
  1. package/dist/ApiKey.d.ts +2 -1
  2. package/dist/ApiKey.d.ts.map +1 -1
  3. package/dist/ApiKeys.d.ts +4 -2
  4. package/dist/ApiKeys.d.ts.map +1 -1
  5. package/dist/ApiKeys.js +26 -1
  6. package/dist/ApiKeys.js.map +1 -1
  7. package/dist/App.d.ts +6007 -5003
  8. package/dist/App.d.ts.map +1 -1
  9. package/dist/App.js +1 -0
  10. package/dist/App.js.map +1 -1
  11. package/dist/Webhooks.d.ts +1 -1
  12. package/dist/Webhooks.d.ts.map +1 -1
  13. package/dist/Webhooks.js +6 -6
  14. package/dist/Webhooks.js.map +1 -1
  15. package/dist/admin/App.d.ts +91 -84
  16. package/dist/admin/App.d.ts.map +1 -1
  17. package/dist/admin/apps/api-keys.d.ts +22 -13
  18. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  19. package/dist/admin/apps/api-keys.js +6 -0
  20. package/dist/admin/apps/api-keys.js.map +1 -1
  21. package/dist/admin/apps/organizations.d.ts +4 -2
  22. package/dist/admin/apps/organizations.d.ts.map +1 -1
  23. package/dist/admin/ui.generated.d.ts +1 -1
  24. package/dist/admin/ui.generated.js +69 -69
  25. package/dist/admin/ui.generated.js.map +1 -1
  26. package/dist/analytics/Analytics.d.ts.map +1 -1
  27. package/dist/analytics/Analytics.js +232 -0
  28. package/dist/analytics/Analytics.js.map +1 -1
  29. package/dist/analytics/tables/requestEvents.d.ts +4 -0
  30. package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
  31. package/dist/analytics/tables/requestEvents.js +2 -0
  32. package/dist/analytics/tables/requestEvents.js.map +1 -1
  33. package/dist/apps/Zones.d.ts +4266 -82
  34. package/dist/apps/Zones.d.ts.map +1 -1
  35. package/dist/apps/Zones.js +27 -8
  36. package/dist/apps/Zones.js.map +1 -1
  37. package/dist/apps/data/App.d.ts +3467 -2696
  38. package/dist/apps/data/App.d.ts.map +1 -1
  39. package/dist/apps/data/App.js +2 -0
  40. package/dist/apps/data/App.js.map +1 -1
  41. package/dist/apps/data/routes/activities.d.ts +2999 -100
  42. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  43. package/dist/apps/data/routes/activities.js +340 -2
  44. package/dist/apps/data/routes/activities.js.map +1 -1
  45. package/dist/apps/data/routes/addresses.d.ts +514 -0
  46. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  47. package/dist/apps/data/routes/balances.d.ts +26 -26
  48. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  49. package/dist/apps/data/routes/balances.js +7 -3
  50. package/dist/apps/data/routes/balances.js.map +1 -1
  51. package/dist/apps/data/routes/blocks.d.ts +4 -4
  52. package/dist/apps/data/routes/earn.d.ts +75 -75
  53. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  54. package/dist/apps/data/routes/earn.js +65 -36
  55. package/dist/apps/data/routes/earn.js.map +1 -1
  56. package/dist/apps/data/routes/exchanges.d.ts +124 -120
  57. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  58. package/dist/apps/data/routes/exchanges.js +115 -77
  59. package/dist/apps/data/routes/exchanges.js.map +1 -1
  60. package/dist/apps/data/routes/fee-amm.d.ts +24 -24
  61. package/dist/apps/data/routes/indexer.d.ts +2 -1
  62. package/dist/apps/data/routes/indexer.d.ts.map +1 -1
  63. package/dist/apps/data/routes/indexer.js +3 -2
  64. package/dist/apps/data/routes/indexer.js.map +1 -1
  65. package/dist/apps/data/routes/receipts.d.ts +4 -3
  66. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  67. package/dist/apps/data/routes/receipts.js +15 -14
  68. package/dist/apps/data/routes/receipts.js.map +1 -1
  69. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  70. package/dist/apps/data/routes/rpc.js +5 -1
  71. package/dist/apps/data/routes/rpc.js.map +1 -1
  72. package/dist/apps/data/routes/tokenlist.d.ts +12 -12
  73. package/dist/apps/data/routes/tokens.d.ts +1 -1
  74. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  75. package/dist/apps/data/routes/tokens.js +4 -3
  76. package/dist/apps/data/routes/tokens.js.map +1 -1
  77. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  78. package/dist/apps/data/routes/transfers.js +25 -7
  79. package/dist/apps/data/routes/transfers.js.map +1 -1
  80. package/dist/apps/data/routes/verified-tokens.d.ts +37 -37
  81. package/dist/apps/data/routes/webhooks.d.ts +71 -1
  82. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  83. package/dist/apps/data/routes/webhooks.js +177 -23
  84. package/dist/apps/data/routes/webhooks.js.map +1 -1
  85. package/dist/apps/data/routes/zones.js +1 -1
  86. package/dist/apps/data/routes/zones.js.map +1 -1
  87. package/dist/apps/funding/App.d.ts +322 -89
  88. package/dist/apps/funding/App.d.ts.map +1 -1
  89. package/dist/apps/funding/routes/deposit-addresses.d.ts +375 -45
  90. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
  91. package/dist/apps/funding/routes/deposit-addresses.js +208 -12
  92. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  93. package/dist/apps/funding/routes/transfers.d.ts +32 -32
  94. package/dist/apps/funding/routes/transfers.js +1 -1
  95. package/dist/apps/funding/routes/transfers.js.map +1 -1
  96. package/dist/apps/management/App.d.ts +418 -418
  97. package/dist/apps/management/routes/api-keys.d.ts +50 -50
  98. package/dist/apps/management/routes/billing.d.ts +72 -72
  99. package/dist/apps/management/routes/billing.js +1 -1
  100. package/dist/apps/management/routes/billing.js.map +1 -1
  101. package/dist/apps/management/routes/faucet.d.ts +8 -8
  102. package/dist/apps/management/routes/faucet.js +2 -2
  103. package/dist/apps/management/routes/faucet.js.map +1 -1
  104. package/dist/apps/management/routes/invite-links.d.ts +42 -42
  105. package/dist/apps/management/routes/me.d.ts +6 -6
  106. package/dist/apps/management/routes/members.d.ts +14 -14
  107. package/dist/apps/management/routes/projects.d.ts +40 -40
  108. package/dist/apps/management/routes/usage.d.ts +8 -8
  109. package/dist/apps/management/routes/webhooks.d.ts +49 -49
  110. package/dist/apps/mcp.d.ts.map +1 -1
  111. package/dist/apps/mcp.js +4 -1
  112. package/dist/apps/mcp.js.map +1 -1
  113. package/dist/db/Db.d.ts +2 -0
  114. package/dist/db/Db.d.ts.map +1 -1
  115. package/dist/db/Db.js +90 -0
  116. package/dist/db/Db.js.map +1 -1
  117. package/dist/db/Schema.d.ts +19 -0
  118. package/dist/db/Schema.d.ts.map +1 -1
  119. package/dist/db/Schema.js +24 -0
  120. package/dist/db/Schema.js.map +1 -1
  121. package/dist/db/tables/fundingDepositAddresses.d.ts +20 -0
  122. package/dist/db/tables/fundingDepositAddresses.d.ts.map +1 -1
  123. package/dist/db/tables/fundingDepositAddresses.js +17 -0
  124. package/dist/db/tables/fundingDepositAddresses.js.map +1 -1
  125. package/dist/db/tables/fundingDeposits.d.ts +41 -0
  126. package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
  127. package/dist/db/tables/fundingDeposits.js +69 -0
  128. package/dist/db/tables/fundingDeposits.js.map +1 -1
  129. package/dist/db/tables/webhookQueueEvents.d.ts +122 -0
  130. package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -0
  131. package/dist/db/tables/webhookQueueEvents.js +160 -0
  132. package/dist/db/tables/webhookQueueEvents.js.map +1 -0
  133. package/dist/db/tables/webhookSubscriptions.d.ts +2 -0
  134. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  135. package/dist/db/tables/webhookSubscriptions.js +10 -0
  136. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  137. package/dist/internal/Auth.d.ts +3 -26
  138. package/dist/internal/Auth.d.ts.map +1 -1
  139. package/dist/internal/Auth.js +6 -37
  140. package/dist/internal/Auth.js.map +1 -1
  141. package/dist/internal/Credential.d.ts +27 -0
  142. package/dist/internal/Credential.d.ts.map +1 -0
  143. package/dist/internal/Credential.js +34 -0
  144. package/dist/internal/Credential.js.map +1 -0
  145. package/dist/internal/EarnRates.js +11 -7
  146. package/dist/internal/EarnRates.js.map +1 -1
  147. package/dist/internal/Log.d.ts +8 -0
  148. package/dist/internal/Log.d.ts.map +1 -1
  149. package/dist/internal/Log.js +4 -0
  150. package/dist/internal/Log.js.map +1 -1
  151. package/dist/internal/MetricSink.d.ts +51 -0
  152. package/dist/internal/MetricSink.d.ts.map +1 -1
  153. package/dist/internal/MetricSink.js +54 -2
  154. package/dist/internal/MetricSink.js.map +1 -1
  155. package/dist/internal/RateLimit.d.ts +4 -6
  156. package/dist/internal/RateLimit.d.ts.map +1 -1
  157. package/dist/internal/RateLimit.js +15 -14
  158. package/dist/internal/RateLimit.js.map +1 -1
  159. package/dist/internal/Store.d.ts +2 -1
  160. package/dist/internal/Store.d.ts.map +1 -1
  161. package/dist/internal/Store.js +57 -18
  162. package/dist/internal/Store.js.map +1 -1
  163. package/dist/internal/Tidx.d.ts +4 -3
  164. package/dist/internal/Tidx.d.ts.map +1 -1
  165. package/dist/internal/Tidx.js +2 -2
  166. package/dist/internal/Tidx.js.map +1 -1
  167. package/dist/internal/Value.d.ts +2 -0
  168. package/dist/internal/Value.d.ts.map +1 -1
  169. package/dist/internal/Value.js +25 -0
  170. package/dist/internal/Value.js.map +1 -1
  171. package/dist/internal/Viem.d.ts +4 -3
  172. package/dist/internal/Viem.d.ts.map +1 -1
  173. package/dist/internal/Viem.js +3 -2
  174. package/dist/internal/Viem.js.map +1 -1
  175. package/dist/internal/WebhookTransfer.d.ts +20 -0
  176. package/dist/internal/WebhookTransfer.d.ts.map +1 -1
  177. package/dist/internal/WebhookTransfer.js +7 -4
  178. package/dist/internal/WebhookTransfer.js.map +1 -1
  179. package/dist/internal/Webhooks.d.ts +141 -0
  180. package/dist/internal/Webhooks.d.ts.map +1 -1
  181. package/dist/internal/Webhooks.js +139 -10
  182. package/dist/internal/Webhooks.js.map +1 -1
  183. package/dist/internal/funding/providers/relay.d.ts +3 -4
  184. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  185. package/dist/internal/funding/providers/relay.js +59 -8
  186. package/dist/internal/funding/providers/relay.js.map +1 -1
  187. package/dist/internal/index.d.ts +1 -0
  188. package/dist/internal/index.d.ts.map +1 -1
  189. package/dist/internal/index.js +1 -0
  190. package/dist/internal/index.js.map +1 -1
  191. package/package.json +3 -3
  192. package/src/ApiKeys.test.ts +20 -0
  193. package/src/ApiKeys.ts +28 -1
  194. package/src/App.test.ts +40 -8
  195. package/src/App.ts +1 -0
  196. package/src/Client.test-d.ts +34 -1
  197. package/src/Webhooks.ts +20 -18
  198. package/src/admin/apps/api-keys.test.ts +9 -0
  199. package/src/admin/apps/api-keys.ts +15 -0
  200. package/src/admin/ui/src/lib/query-client.ts +3 -2
  201. package/src/admin/ui/src/routes/api-keys.tsx +31 -9
  202. package/src/admin/ui.generated.ts +69 -69
  203. package/src/analytics/Analytics.test.ts +42 -0
  204. package/src/analytics/Analytics.ts +232 -0
  205. package/src/analytics/tables/requestEvents.ts +6 -0
  206. package/src/apps/Zones.test.ts +20 -3
  207. package/src/apps/Zones.ts +28 -8
  208. package/src/apps/composition.test.ts +2 -0
  209. package/src/apps/data/App.test.ts +2 -2
  210. package/src/apps/data/App.ts +2 -0
  211. package/src/apps/data/routes/activities.bench.ts +41 -0
  212. package/src/apps/data/routes/activities.test.ts +103 -2
  213. package/src/apps/data/routes/activities.ts +401 -2
  214. package/src/apps/data/routes/balances.bench.ts +21 -0
  215. package/src/apps/data/routes/balances.ts +7 -3
  216. package/src/apps/data/routes/blocks.bench.ts +34 -0
  217. package/src/apps/data/routes/coingecko.bench.ts +53 -0
  218. package/src/apps/data/routes/earn.bench.ts +71 -0
  219. package/src/apps/data/routes/earn.ts +66 -36
  220. package/src/apps/data/routes/exchanges.bench.ts +85 -0
  221. package/src/apps/data/routes/exchanges.test.ts +8 -3
  222. package/src/apps/data/routes/exchanges.ts +134 -82
  223. package/src/apps/data/routes/fee-amm.bench.ts +13 -0
  224. package/src/apps/data/routes/indexer.bench.ts +8 -0
  225. package/src/apps/data/routes/indexer.test.ts +2 -1
  226. package/src/apps/data/routes/indexer.ts +3 -2
  227. package/src/apps/data/routes/receipts.bench.ts +39 -0
  228. package/src/apps/data/routes/receipts.ts +16 -14
  229. package/src/apps/data/routes/rpc.bench.ts +14 -0
  230. package/src/apps/data/routes/rpc.test.ts +27 -0
  231. package/src/apps/data/routes/rpc.ts +5 -1
  232. package/src/apps/data/routes/tokenlist.bench.ts +3 -0
  233. package/src/apps/data/routes/tokens.bench.ts +76 -0
  234. package/src/apps/data/routes/tokens.ts +3 -2
  235. package/src/apps/data/routes/transactions.bench.ts +48 -0
  236. package/src/apps/data/routes/transfers.bench.ts +28 -0
  237. package/src/apps/data/routes/transfers.ts +36 -10
  238. package/src/apps/data/routes/valuation.bench.ts +14 -0
  239. package/src/apps/data/routes/verified-tokens.bench.ts +8 -0
  240. package/src/apps/data/routes/webhooks.bench.ts +26 -0
  241. package/src/apps/data/routes/webhooks.scan.test.ts +326 -0
  242. package/src/apps/data/routes/webhooks.test.ts +53 -6
  243. package/src/apps/data/routes/webhooks.ts +252 -24
  244. package/src/apps/data/routes/zones.bench.ts +32 -0
  245. package/src/apps/data/routes/zones.test.ts +1 -1
  246. package/src/apps/data/routes/zones.ts +1 -1
  247. package/src/apps/funding/routes/chains.test.ts +1 -1
  248. package/src/apps/funding/routes/deposit-addresses.test.ts +217 -3
  249. package/src/apps/funding/routes/deposit-addresses.ts +255 -19
  250. package/src/apps/funding/routes/providers.test.ts +1 -1
  251. package/src/apps/funding/routes/quotes.test.ts +1 -1
  252. package/src/apps/funding/routes/transfers.test.ts +20 -8
  253. package/src/apps/funding/routes/transfers.ts +1 -1
  254. package/src/apps/management/routes/billing.ts +1 -1
  255. package/src/apps/management/routes/faucet.ts +2 -2
  256. package/src/apps/management/routes/me.test.ts +31 -0
  257. package/src/apps/mcp.ts +4 -1
  258. package/src/db/Db.ts +117 -0
  259. package/src/db/Schema.ts +32 -0
  260. package/src/db/tables/fundingDepositAddresses.test.ts +28 -0
  261. package/src/db/tables/fundingDepositAddresses.ts +39 -0
  262. package/src/db/tables/fundingDeposits.test.ts +82 -0
  263. package/src/db/tables/fundingDeposits.ts +130 -0
  264. package/src/db/tables/webhookQueueEvents.ts +285 -0
  265. package/src/db/tables/webhookSubscriptions.ts +15 -0
  266. package/src/internal/Auth.test.ts +61 -98
  267. package/src/internal/Auth.ts +9 -63
  268. package/src/internal/Credential.test.ts +51 -0
  269. package/src/internal/Credential.ts +58 -0
  270. package/src/internal/EarnRates.ts +13 -7
  271. package/src/internal/EarnVaults.test.ts +2 -2
  272. package/src/internal/Log.test.ts +3 -1
  273. package/src/internal/Log.ts +14 -0
  274. package/src/internal/MetricSink.test.ts +108 -2
  275. package/src/internal/MetricSink.ts +112 -2
  276. package/src/internal/RateLimit.test.ts +23 -18
  277. package/src/internal/RateLimit.ts +23 -15
  278. package/src/internal/Store.test.ts +99 -13
  279. package/src/internal/Store.ts +52 -17
  280. package/src/internal/Tidx.test.ts +4 -4
  281. package/src/internal/Tidx.ts +5 -4
  282. package/src/internal/Value.test.ts +25 -0
  283. package/src/internal/Value.ts +29 -0
  284. package/src/internal/Viem.test.ts +4 -0
  285. package/src/internal/Viem.ts +6 -4
  286. package/src/internal/WebhookTransfer.test.ts +89 -0
  287. package/src/internal/WebhookTransfer.ts +11 -7
  288. package/src/internal/Webhooks.test.ts +488 -0
  289. package/src/internal/Webhooks.ts +291 -10
  290. package/src/internal/funding/Provider.test.ts +1 -1
  291. package/src/internal/funding/providers/index.test.ts +73 -3
  292. package/src/internal/funding/providers/relay.test.ts +1 -5
  293. package/src/internal/funding/providers/relay.ts +56 -12
  294. package/src/internal/index.test.ts +1 -0
  295. 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
- const snapshot =
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
- ? await VerifiedTokens.snapshot(c, chainId).catch(() => undefined)
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
- const pairs = await getPairIndex(c, { chainId })
3005
- const target = pairs.get(baseAddress)
3006
- if (!target) return pairNotFoundError(c, { base: baseAddress, pairIndex: pairs })
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
- const { asks, bids } = await getPairDepth(c, {
3009
- chainId,
3010
- levels: options.levels,
3011
- pair: target,
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 (options.include.includes('tokens')) {
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
- // Inclusive `dex_fills` block range bounding one grouped batch (see
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, `f.block_num >= ${window.lo}`, `f.block_num <= ${window.hi}`].join(' AND ')}
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: from a targeted `dex_pairs` lookup of this
3671
- // batch's bases when indexed, else the base token's on-chain
3672
- // `quoteToken()` — some books predate the indexer's `PairCreated`
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 bookBase of bases) {
3680
- const indexed = pairs.get(bookBase)
3681
- if (indexed) quoteByBase.set(bookBase, indexed.quote)
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 = BlockWindow & { firstMatchingBlock?: number | undefined }
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 = await windowFrom(cursorBlock, bounds.firstMatchingBlock)
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
- const data = pageRows.map((candidate) => candidate.swap)
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
- SELECT topic1, argMax(topic3, (block_num, log_idx)) AS t3,
4082
- argMax(data, (block_num, log_idx)) AS payload,
4083
- toInt64(-1) AS bn, toInt32(-1) AS li
4084
- FROM logs
4085
- WHERE ${eventFilter} AND block_num < ${minBlock}
4086
- GROUP BY topic1
4087
- UNION ALL
4088
- SELECT topic1, topic3 AS t3, data AS payload, block_num AS bn, log_idx AS li
4089
- FROM logs
4090
- WHERE ${eventFilter} AND block_num >= ${minBlock} AND block_num <= ${maxBlock}
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. ClickHouse evaluates the IN-list against the `topic1` bloom-filter
4722
- * index after the TIDX pushdown rewrite re-encodes each literal (see
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 = 1000
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. Aggregation also runs in the app because
4749
- * indexer-side `SUM(...)` over decoded columns is currently unreliable.
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: getPairs.Row
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 `OrderFilled` rows in
5379
- * the snapshot window via batched `WHERE "orderId" IN (...)` queries. Each
5380
- * placement may have many partial fills, so the loader sums them in the app
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['amountFilled'])
5459
+ const filled = Value.toIntegerString(row['amount_filled'])
5408
5460
  if (id === undefined || filled === undefined) continue
5409
- sums.set(id, (sums.get(id) ?? 0n) + BigInt(filled))
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
+ })
@@ -0,0 +1,8 @@
1
+ import * as QueryBench from '../../../../test/QueryBench.js'
2
+
3
+ const sql = 'SELECT num, hash FROM blocks ORDER BY num DESC LIMIT 1'
4
+
5
+ QueryBench.bench({
6
+ name: 'indexerQuery',
7
+ path: `/v1/indexer/query?sql=${encodeURIComponent(sql)}`,
8
+ })
@@ -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.auth,
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
- 'tempo-api-key',
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 (`receipts` indexes `from`
980
- * but not `to`, so the `to` side resolves hashes via the `txs` index), counting
981
- * the deduped keyset so self-receipts (`from = to`) aren't double-counted.
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 toWhere = [
999
- `tx_hash IN (SELECT hash FROM txs WHERE "to" = '${address}')`,
1000
- ...filters,
1001
- ].join(' AND ')
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 feed the
1073
- // block bound the `logs` lookup requires.
1074
- const blocks: number[] = []
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.push(row.blockNumber)
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
- // REQUIRES one: `logs` is sorted by block position, so a bare `tx_hash IN`
1090
- // scans to genesis (see `blockBound` in `transfers.ts`).
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 BETWEEN ${Math.min(...blocks)} AND ${Math.max(...blocks)} AND tx_hash IN (${inList}) ORDER BY block_num ASC, log_idx ASC` as string,
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({ apiKey: { scopes: ['data:read'] }, public: true }),
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.',