tapimo 0.10.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/dist/App.d.ts +5249 -3572
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js.map +1 -1
  4. package/dist/Webhooks.d.ts +1 -76
  5. package/dist/Webhooks.d.ts.map +1 -1
  6. package/dist/Webhooks.js +2 -144
  7. package/dist/Webhooks.js.map +1 -1
  8. package/dist/admin/App.d.ts +74 -74
  9. package/dist/admin/apps/earn-vaults.d.ts +1 -1
  10. package/dist/admin/apps/organizations.d.ts +3 -3
  11. package/dist/admin/apps/verified-tokens.d.ts +74 -74
  12. package/dist/analytics/Analytics.d.ts.map +1 -1
  13. package/dist/analytics/Analytics.js +206 -0
  14. package/dist/analytics/Analytics.js.map +1 -1
  15. package/dist/analytics/tables/requestEvents.d.ts +4 -0
  16. package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
  17. package/dist/analytics/tables/requestEvents.js +2 -0
  18. package/dist/analytics/tables/requestEvents.js.map +1 -1
  19. package/dist/apps/data/App.d.ts +3618 -2954
  20. package/dist/apps/data/App.d.ts.map +1 -1
  21. package/dist/apps/data/App.js +7 -2
  22. package/dist/apps/data/App.js.map +1 -1
  23. package/dist/apps/data/routes/activities.d.ts +1 -1
  24. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  25. package/dist/apps/data/routes/activities.js +55 -27
  26. package/dist/apps/data/routes/activities.js.map +1 -1
  27. package/dist/apps/data/routes/balances.d.ts +11 -11
  28. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  29. package/dist/apps/data/routes/balances.js +2 -6
  30. package/dist/apps/data/routes/balances.js.map +1 -1
  31. package/dist/apps/data/routes/blocks.d.ts +5 -5
  32. package/dist/apps/data/routes/earn.d.ts +4 -4
  33. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  34. package/dist/apps/data/routes/earn.js +63 -34
  35. package/dist/apps/data/routes/earn.js.map +1 -1
  36. package/dist/apps/data/routes/exchanges.d.ts +8 -4
  37. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  38. package/dist/apps/data/routes/exchanges.js +115 -77
  39. package/dist/apps/data/routes/exchanges.js.map +1 -1
  40. package/dist/apps/data/routes/fee-amm.d.ts +26 -26
  41. package/dist/apps/data/routes/receipts.d.ts +240 -239
  42. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  43. package/dist/apps/data/routes/receipts.js +15 -14
  44. package/dist/apps/data/routes/receipts.js.map +1 -1
  45. package/dist/apps/data/routes/tokenlist.d.ts +12 -12
  46. package/dist/apps/data/routes/tokens.d.ts +4 -4
  47. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  48. package/dist/apps/data/routes/tokens.js +4 -3
  49. package/dist/apps/data/routes/tokens.js.map +1 -1
  50. package/dist/apps/data/routes/transactions.d.ts +282 -282
  51. package/dist/apps/data/routes/transfers.d.ts +13 -13
  52. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  53. package/dist/apps/data/routes/transfers.js +25 -7
  54. package/dist/apps/data/routes/transfers.js.map +1 -1
  55. package/dist/apps/data/routes/valuation.d.ts +21 -21
  56. package/dist/apps/data/routes/verified-tokens.d.ts +5 -5
  57. package/dist/apps/data/routes/webhooks.d.ts +1519 -643
  58. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  59. package/dist/apps/data/routes/webhooks.js +134 -740
  60. package/dist/apps/data/routes/webhooks.js.map +1 -1
  61. package/dist/apps/data/routes/zones.d.ts +3 -3
  62. package/dist/apps/funding/App.d.ts +233 -0
  63. package/dist/apps/funding/App.d.ts.map +1 -1
  64. package/dist/apps/funding/routes/deposit-addresses.d.ts +331 -1
  65. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
  66. package/dist/apps/funding/routes/deposit-addresses.js +207 -11
  67. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  68. package/dist/apps/funding/routes/transfers.d.ts +33 -33
  69. package/dist/apps/management/App.d.ts +793 -16
  70. package/dist/apps/management/App.d.ts.map +1 -1
  71. package/dist/apps/management/routes/api-keys.d.ts +50 -50
  72. package/dist/apps/management/routes/faucet.d.ts +2 -2
  73. package/dist/apps/management/routes/invite-links.d.ts +42 -42
  74. package/dist/apps/management/routes/members.d.ts +2 -2
  75. package/dist/apps/management/routes/projects.d.ts +40 -40
  76. package/dist/apps/management/routes/webhooks.d.ts +1031 -59
  77. package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
  78. package/dist/apps/management/routes/webhooks.js +27 -4
  79. package/dist/apps/management/routes/webhooks.js.map +1 -1
  80. package/dist/db/Db.d.ts.map +1 -1
  81. package/dist/db/Db.js +53 -0
  82. package/dist/db/Db.js.map +1 -1
  83. package/dist/db/Schema.d.ts +7 -1
  84. package/dist/db/Schema.d.ts.map +1 -1
  85. package/dist/db/Schema.js +9 -0
  86. package/dist/db/Schema.js.map +1 -1
  87. package/dist/db/tables/fundingDepositAddresses.d.ts +20 -0
  88. package/dist/db/tables/fundingDepositAddresses.d.ts.map +1 -1
  89. package/dist/db/tables/fundingDepositAddresses.js +17 -0
  90. package/dist/db/tables/fundingDepositAddresses.js.map +1 -1
  91. package/dist/db/tables/fundingDeposits.d.ts +41 -0
  92. package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
  93. package/dist/db/tables/fundingDeposits.js +69 -0
  94. package/dist/db/tables/fundingDeposits.js.map +1 -1
  95. package/dist/db/tables/webhookQueueEvents.d.ts +3 -1
  96. package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -1
  97. package/dist/db/tables/webhookQueueEvents.js +4 -0
  98. package/dist/db/tables/webhookQueueEvents.js.map +1 -1
  99. package/dist/db/tables/webhookSubscriptions.d.ts +33 -69
  100. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  101. package/dist/db/tables/webhookSubscriptions.js +63 -100
  102. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  103. package/dist/internal/Auth.d.ts +0 -26
  104. package/dist/internal/Auth.d.ts.map +1 -1
  105. package/dist/internal/Auth.js +0 -33
  106. package/dist/internal/Auth.js.map +1 -1
  107. package/dist/internal/Credential.d.ts +27 -0
  108. package/dist/internal/Credential.d.ts.map +1 -0
  109. package/dist/internal/Credential.js +34 -0
  110. package/dist/internal/Credential.js.map +1 -0
  111. package/dist/internal/EarnRates.js +11 -7
  112. package/dist/internal/EarnRates.js.map +1 -1
  113. package/dist/internal/Log.d.ts +8 -0
  114. package/dist/internal/Log.d.ts.map +1 -1
  115. package/dist/internal/Log.js +4 -0
  116. package/dist/internal/Log.js.map +1 -1
  117. package/dist/internal/MetricSink.d.ts +50 -116
  118. package/dist/internal/MetricSink.d.ts.map +1 -1
  119. package/dist/internal/MetricSink.js +60 -106
  120. package/dist/internal/MetricSink.js.map +1 -1
  121. package/dist/internal/Schema.d.ts +3 -1
  122. package/dist/internal/Schema.d.ts.map +1 -1
  123. package/dist/internal/Schema.js +4 -2
  124. package/dist/internal/Schema.js.map +1 -1
  125. package/dist/internal/Tidx.d.ts +4 -3
  126. package/dist/internal/Tidx.d.ts.map +1 -1
  127. package/dist/internal/Tidx.js +2 -2
  128. package/dist/internal/Tidx.js.map +1 -1
  129. package/dist/internal/Viem.d.ts +4 -3
  130. package/dist/internal/Viem.d.ts.map +1 -1
  131. package/dist/internal/Viem.js +3 -2
  132. package/dist/internal/Viem.js.map +1 -1
  133. package/dist/internal/WebhookTransfer.d.ts +1 -55
  134. package/dist/internal/WebhookTransfer.d.ts.map +1 -1
  135. package/dist/internal/WebhookTransfer.js +1 -239
  136. package/dist/internal/WebhookTransfer.js.map +1 -1
  137. package/dist/internal/Webhooks.d.ts +85 -44
  138. package/dist/internal/Webhooks.d.ts.map +1 -1
  139. package/dist/internal/Webhooks.js +94 -73
  140. package/dist/internal/Webhooks.js.map +1 -1
  141. package/dist/internal/funding/DepositWebhook.d.ts +25 -0
  142. package/dist/internal/funding/DepositWebhook.d.ts.map +1 -0
  143. package/dist/internal/funding/DepositWebhook.js +74 -0
  144. package/dist/internal/funding/DepositWebhook.js.map +1 -0
  145. package/dist/internal/funding/Reconciliation.d.ts +6 -0
  146. package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
  147. package/dist/internal/funding/Reconciliation.js +42 -10
  148. package/dist/internal/funding/Reconciliation.js.map +1 -1
  149. package/dist/internal/funding/index.d.ts +1 -0
  150. package/dist/internal/funding/index.d.ts.map +1 -1
  151. package/dist/internal/funding/index.js +1 -0
  152. package/dist/internal/funding/index.js.map +1 -1
  153. package/dist/internal/index.d.ts +1 -0
  154. package/dist/internal/index.d.ts.map +1 -1
  155. package/dist/internal/index.js +1 -0
  156. package/dist/internal/index.js.map +1 -1
  157. package/dist/internal/webhookDestinations/slack.d.ts.map +1 -1
  158. package/dist/internal/webhookDestinations/slack.js +16 -0
  159. package/dist/internal/webhookDestinations/slack.js.map +1 -1
  160. package/package.json +1 -1
  161. package/src/App.test.ts +30 -0
  162. package/src/App.ts +3 -0
  163. package/src/Client.test-d.ts +55 -2
  164. package/src/Webhooks.ts +2 -221
  165. package/src/analytics/Analytics.test.ts +34 -0
  166. package/src/analytics/Analytics.ts +206 -0
  167. package/src/analytics/tables/requestEvents.ts +6 -0
  168. package/src/apps/composition.test.ts +1 -0
  169. package/src/apps/data/App.ts +10 -2
  170. package/src/apps/data/routes/activities.test.ts +107 -11
  171. package/src/apps/data/routes/activities.ts +64 -32
  172. package/src/apps/data/routes/balances.ts +2 -6
  173. package/src/apps/data/routes/earn.ts +64 -34
  174. package/src/apps/data/routes/exchanges.test.ts +8 -3
  175. package/src/apps/data/routes/exchanges.ts +134 -82
  176. package/src/apps/data/routes/receipts.ts +16 -14
  177. package/src/apps/data/routes/tokens.ts +3 -2
  178. package/src/apps/data/routes/transactions.bench.ts +8 -0
  179. package/src/apps/data/routes/transfers.ts +36 -10
  180. package/src/apps/data/routes/webhooks.test.ts +179 -1497
  181. package/src/apps/data/routes/webhooks.ts +172 -975
  182. package/src/apps/funding/routes/deposit-addresses.test.ts +216 -2
  183. package/src/apps/funding/routes/deposit-addresses.ts +254 -18
  184. package/src/apps/funding/routes/transfers.test.ts +17 -2
  185. package/src/apps/management/routes/webhooks.test.ts +79 -1
  186. package/src/apps/management/routes/webhooks.ts +40 -4
  187. package/src/db/Db.test.ts +1 -5
  188. package/src/db/Db.ts +59 -0
  189. package/src/db/Schema.ts +13 -0
  190. package/src/db/tables/fundingDepositAddresses.test.ts +28 -0
  191. package/src/db/tables/fundingDepositAddresses.ts +39 -0
  192. package/src/db/tables/fundingDeposits.test.ts +82 -0
  193. package/src/db/tables/fundingDeposits.ts +130 -0
  194. package/src/db/tables/organizations.test.ts +5 -2
  195. package/src/db/tables/webhookQueueEvents.ts +7 -1
  196. package/src/db/tables/webhookSubscriptions.ts +89 -138
  197. package/src/internal/Auth.test.ts +0 -48
  198. package/src/internal/Auth.ts +0 -59
  199. package/src/internal/Credential.test.ts +51 -0
  200. package/src/internal/Credential.ts +58 -0
  201. package/src/internal/EarnRates.ts +13 -7
  202. package/src/internal/Log.test.ts +3 -1
  203. package/src/internal/Log.ts +14 -0
  204. package/src/internal/MetricSink.test.ts +75 -358
  205. package/src/internal/MetricSink.ts +117 -246
  206. package/src/internal/Schema.test.ts +18 -0
  207. package/src/internal/Schema.ts +13 -2
  208. package/src/internal/Tidx.ts +5 -4
  209. package/src/internal/Viem.test.ts +4 -0
  210. package/src/internal/Viem.ts +6 -4
  211. package/src/internal/WebhookTransfer.ts +1 -351
  212. package/src/internal/Webhooks.test.ts +111 -139
  213. package/src/internal/Webhooks.ts +181 -105
  214. package/src/internal/funding/DepositWebhook.test.ts +131 -0
  215. package/src/internal/funding/DepositWebhook.ts +101 -0
  216. package/src/internal/funding/Reconciliation.test.ts +14 -0
  217. package/src/internal/funding/Reconciliation.ts +161 -86
  218. package/src/internal/funding/index.test.ts +1 -0
  219. package/src/internal/funding/index.ts +1 -0
  220. package/src/internal/index.test.ts +1 -0
  221. package/src/internal/index.ts +1 -0
  222. package/src/internal/webhookDestinations/slack.ts +15 -0
  223. package/src/Webhooks.bench.ts +0 -714
  224. package/src/Webhooks.memory.bench.ts +0 -250
  225. package/src/Webhooks.test.ts +0 -146
@@ -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
@@ -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
  )
@@ -1228,12 +1228,13 @@ export async function resolveTokens(
1228
1228
  const logos = new Map(logoByAddress)
1229
1229
  const metadataMap = new Map(metadataByAddress)
1230
1230
  const onchainLogos = new Map(
1231
- includeLogoUri
1231
+ includeLogoUri && options.metadata !== undefined
1232
1232
  ? await Timing.time(c, 'token_logo_uri', () =>
1233
1233
  Promise.all(
1234
1234
  addresses
1235
1235
  .filter(
1236
1236
  (address) =>
1237
+ options.metadata?.has(address) === true &&
1237
1238
  !logos.get(address) &&
1238
1239
  !snapshot?.byAddress.get(address)?.logoUri &&
1239
1240
  !metadataMap.get(address)?.logoUri,
@@ -1321,7 +1322,7 @@ function getTokenLogoUri(c: Context<App.Environment>, options: getTokenMetadata.
1321
1322
 
1322
1323
  /**
1323
1324
  * Resolves a token's on-chain metadata (`name`/`symbol`/`decimals`/`currency`/
1324
- * `totalSupply`) via a single `token.getMetadata` RPC, memoized under the shared
1325
+ * `logoUri`/`totalSupply`) via a single `token.getMetadata` RPC, memoized under the shared
1325
1326
  * `token:v1:{chainId}:{address}:metadata` key. Exported so feed enrichers (e.g.
1326
1327
  * `activities`) reuse the same cache entry as the token resource rather than
1327
1328
  * fetching metadata through a parallel path.
@@ -30,6 +30,14 @@ QueryBench.bench({
30
30
  },
31
31
  })
32
32
 
33
+ QueryBench.bench({
34
+ name: 'getTransactions?address&include=receipt',
35
+ path: {
36
+ 4217: `/v1/transactions?address=${address[4217]}&include=receipt`,
37
+ 42431: `/v1/transactions?address=${address[42431]}&include=receipt`,
38
+ },
39
+ })
40
+
33
41
  QueryBench.bench({
34
42
  name: 'getTransaction',
35
43
  path: {
@@ -437,10 +437,10 @@ export function transfers(options: transfers.Options = {}) {
437
437
  transactionHash: row.transactionHash,
438
438
  }))
439
439
 
440
- // Memo/attribution are opt-in: one `TransferWithMemo` lookup bounded
441
- // to the page's transactions resolves both — the decoded text `memo`,
442
- // and `attribution` when the memo is a known MPP service fingerprint.
443
- if (options.include.includes('memo') && data.length > 0) {
440
+ const includeCrossToken = options.include.includes('crossToken')
441
+ const includeMemo = options.include.includes('memo')
442
+ // Attach memos before cross-token folding changes the matching fields.
443
+ if (includeMemo && includeCrossToken && data.length > 0) {
444
444
  const memoByRow = await memoMap(c, { chainId, rows: data })
445
445
  data = data.map((transfer) => {
446
446
  const entry = memoByRow.get(
@@ -469,7 +469,7 @@ export function transfers(options: transfers.Options = {}) {
469
469
  // own event token is the delivered token), so a `token=` filter matches
470
470
  // cross-token rows by their destination token, and each transfer still
471
471
  // appears exactly once across pages.
472
- if (options.include.includes('crossToken') && data.length > 0) {
472
+ if (includeCrossToken && data.length > 0) {
473
473
  const { fold, suppress } = await crossTokenMap(c, { chainId, rows: data })
474
474
  data = data.flatMap((transfer) => {
475
475
  const key = eventKey(transfer.transactionHash, transfer.logIndex)
@@ -502,11 +502,17 @@ export function transfers(options: transfers.Options = {}) {
502
502
  ]),
503
503
  ),
504
504
  )
505
- const tokensByAddress = new Map<
506
- string,
507
- Awaited<ReturnType<typeof Tokens.getTokenMetadata>>
508
- >()
509
- await Timing.time(c, 'tokens', async () => {
505
+ // Memo and token metadata depend only on the page rows, so resolve
506
+ // them concurrently when cross-token folding is not requested.
507
+ const memoPromise =
508
+ includeMemo && !includeCrossToken && data.length > 0
509
+ ? memoMap(c, { chainId, rows: data })
510
+ : undefined
511
+ const tokensPromise = Timing.time(c, 'tokens', async () => {
512
+ const tokensByAddress = new Map<
513
+ string,
514
+ Awaited<ReturnType<typeof Tokens.getTokenMetadata>>
515
+ >()
510
516
  for (let index = 0; index < uniqueTokens.length; index += tokenReadChunkSize) {
511
517
  const group = uniqueTokens.slice(index, index + tokenReadChunkSize)
512
518
  const entries = await Promise.all(
@@ -536,7 +542,27 @@ export function transfers(options: transfers.Options = {}) {
536
542
  )
537
543
  for (const entry of entries) tokensByAddress.set(...entry)
538
544
  }
545
+ return tokensByAddress
539
546
  })
547
+ const [memoByRow, tokensByAddress] = await Promise.all([memoPromise, tokensPromise])
548
+ if (memoByRow)
549
+ data = data.map((transfer) => {
550
+ const entry = memoByRow.get(
551
+ transferKey(
552
+ transfer.transactionHash,
553
+ transfer.sender,
554
+ transfer.recipient,
555
+ transfer.sourceToken.amount,
556
+ transfer.sourceToken.address,
557
+ ),
558
+ )
559
+ if (!entry) return transfer
560
+ return {
561
+ ...transfer,
562
+ ...(entry.attribution ? { attribution: entry.attribution } : {}),
563
+ ...(entry.memo ? { memo: entry.memo } : {}),
564
+ }
565
+ })
540
566
 
541
567
  // Value each leg via its curated display currency, in the requested
542
568
  // denomination.