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
@@ -35,8 +35,6 @@ const liveAccounts = {
35
35
  channel: '0x5217c502adf3c96a9551770af4caa5f5144a6984',
36
36
  // TokenCreated + Mint + Transfer.
37
37
  creator: '0x253fb3bf1e3d75ad6ee1554f68f4ea0568f7d24e',
38
- // Token burns.
39
- burner: '0x69519497b59bd64e109d993157d45f14455cc7e6',
40
38
  // KeyAuthorized + KeyRevoked + Approval (+ transfer/mint/exchange).
41
39
  keychain: '0xfe776fd28e06ecc71a157398b2c9991318f1ea2e',
42
40
  // Swaps through the stablecoin DEX (output kept, so it stays a plain `swap`).
@@ -50,27 +48,29 @@ const accounts =
50
48
  ? {
51
49
  channel: localAccounts.keychain,
52
50
  creator: localAccounts.creator,
53
- burner: localAccounts.burner,
54
51
  keychain: localAccounts.keychain,
55
52
  swapper: localAccounts.swapper,
56
53
  }
57
54
  : liveAccounts
58
55
 
59
- /** The account expected to surface each activity type. */
56
+ /**
57
+ * The account expected to surface each activity type. Burns are seeded per run
58
+ * instead: a live burner keeps accruing activity until its last burn falls off
59
+ * the first page.
60
+ */
60
61
  const coverage =
61
- runtime.mode === 'localnet'
62
+ runtime.mode === 'localnet' && localAccounts
62
63
  ? {
63
- approval: accounts.keychain,
64
- burn: accounts.burner,
65
- mint: accounts.creator,
66
- 'token-created': accounts.creator,
67
- transfer: accounts.creator,
64
+ approval: localAccounts.keychain,
65
+ burn: localAccounts.burner,
66
+ mint: localAccounts.creator,
67
+ 'token-created': localAccounts.creator,
68
+ transfer: localAccounts.creator,
68
69
  }
69
70
  : {
70
71
  'access-key-created': accounts.keychain,
71
72
  'access-key-revoked': accounts.keychain,
72
73
  approval: accounts.keychain,
73
- burn: accounts.burner,
74
74
  mint: accounts.creator,
75
75
  'session-closed': accounts.channel,
76
76
  swap: accounts.swapper,
@@ -165,6 +165,54 @@ describe('GET /addresses/:address/activities', () => {
165
165
  expect(find(body.data, type)).toBeDefined()
166
166
  })
167
167
 
168
+ // Localnet seeds its burner through the shared fixtures, so this covers the
169
+ // hosted testnet, where no address stays burn-topped for long.
170
+ test.skipIf(runtime.mode === 'localnet')(
171
+ 'classifies a seeded burn',
172
+ async () => {
173
+ const account = Tempo.accounts[11]
174
+ const wallet = Tempo.getClient({ account })
175
+ await Actions.faucet.fundSync(Tempo.client, { account, timeout: 60_000 })
176
+
177
+ // Burning needs the `issuer` role, so the account burns a token it owns.
178
+ const { token } = await Actions.token.createSync(wallet, {
179
+ currency: 'USD',
180
+ name: 'Seeded Burn USD',
181
+ symbol: 'SBUSD',
182
+ })
183
+ await sendTransactionSync(wallet, {
184
+ account,
185
+ calls: [
186
+ Actions.token.grantRoles.call(wallet, { role: 'issuer', to: account.address, token }),
187
+ Actions.token.mint.call(wallet, { amount: 1_000_000n, to: account.address, token }),
188
+ ],
189
+ })
190
+ const { receipt } = await Actions.token.burnSync(wallet, { amount: 100_000n, token })
191
+
192
+ const deadline = Date.now() + 90_000
193
+ let burn: Activities.Entry | undefined
194
+ while (Date.now() < deadline) {
195
+ const response = await client.v1.addresses[':address'].activities.$get(
196
+ { param: { address: account.address }, query: { limit: '50' } },
197
+ { headers: { 'cache-control': 'no-store', ...TestApp.auth.headers } },
198
+ )
199
+ if (response.status === 200) {
200
+ const body = await TestApp.json(response, Activities.schema.getAddressActivities.Response)
201
+ burn = find(body.data, 'burn')
202
+ if (burn?.type === 'burn' && burn.transactionHash === receipt.transactionHash) break
203
+ }
204
+ await new Promise((resolve) => setTimeout(resolve, 500))
205
+ }
206
+ expect(burn?.type).toBe('burn')
207
+ if (burn?.type !== 'burn') return
208
+ expect(burn.transactionHash).toBe(receipt.transactionHash)
209
+ expect(burn.data.sender.toLowerCase()).toBe(account.address.toLowerCase())
210
+ expect(burn.data.sourceToken.address.toLowerCase()).toBe(token.toLowerCase())
211
+ expect(burn.data.sourceAmount.baseUnits).toBe('100000')
212
+ },
213
+ 180_000,
214
+ )
215
+
168
216
  test('transfer data mirrors the `Transfer` resource', async () => {
169
217
  const transfer = find((await activities(accounts.creator)).data, 'transfer')
170
218
  expect(transfer?.type).toBe('transfer')
@@ -666,6 +714,54 @@ describe('Activities.list classification', () => {
666
714
  expect(log.data).toBe('0x1234')
667
715
  expect(log.topics).toEqual([unknownSelector, pad(viewer)])
668
716
  })
717
+
718
+ test('degrades a topic-encoded approval with a fee transfer to `unknown`', async () => {
719
+ const approvalToken = '0x4120c7ec7e9ead9dcf44b35b5134ebdbfc6e3de9' as Address.Address
720
+ const feeToken = '0x20c0000000000000000000000000000000000000' as Address.Address
721
+ const feeManager = '0xfeec000000000000000000000000000000000000'
722
+ const approvalSelector = '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925'
723
+ const transferSelector = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
724
+ const pad = (value: string) => `0x${value.slice(2).padStart(64, '0')}`
725
+ const txHash = '0xd2439ad8016d4ffd6aebe835734ee4d118a688714a4f9aebcc11f87a2b22e896' as Hex.Hex
726
+ const common = {
727
+ blockNum: 15_781_805,
728
+ blockTimestamp: 1_776_703_284,
729
+ feePayer: viewer,
730
+ source: 'q1' as const,
731
+ txHash,
732
+ txIdx: 0,
733
+ txSender: viewer,
734
+ }
735
+ const { items } = await Activities.list(
736
+ provider([
737
+ {
738
+ ...common,
739
+ data: '0x',
740
+ logAddress: approvalToken,
741
+ logIdx: 0,
742
+ selector: approvalSelector,
743
+ topic1: pad(viewer),
744
+ topic2: pad('0x6063259f720ee125e5bff693779ab8118b0118a7'),
745
+ topic3: '0x2a9063ed52b7d417e441f378325359c9ce274f5d6c3ecf11b7d42f24b9c90b7f',
746
+ },
747
+ {
748
+ ...common,
749
+ data: `0x${(5_815).toString(16).padStart(64, '0')}`,
750
+ logAddress: feeToken,
751
+ logIdx: 1,
752
+ selector: transferSelector,
753
+ topic1: pad(viewer),
754
+ topic2: pad(feeManager),
755
+ topic3: null,
756
+ },
757
+ ]),
758
+ { address: viewer, store: store() },
759
+ )
760
+
761
+ expect(items).toHaveLength(1)
762
+ expect(items[0]?.type).toBe('unknown')
763
+ expect(items[0]?.events).toHaveLength(2)
764
+ })
669
765
  })
670
766
 
671
767
  describe('Activities.list precompile classification', () => {
@@ -2727,15 +2727,46 @@ function classifyFromLogs(
2727
2727
  viewer: Address.Address
2728
2728
  },
2729
2729
  ): RawItem[] {
2730
- const items = classifyFromLogsCore(rows, ctx)
2730
+ const decoded = decodeRows(rows)
2731
+ const items = (() => {
2732
+ try {
2733
+ return classifyFromLogsCore(rows, ctx, decoded)
2734
+ } catch {
2735
+ // Indexed logs can match a known signature without satisfying that event's expected shape.
2736
+ return [
2737
+ unknown(ctx.itemBase(Math.min(...rows.map((row) => row.logIdx))), {
2738
+ events: buildTxEvents(rows, decoded),
2739
+ signer: signerForRows(rows, { fallback: ctx.signer, viewer: ctx.viewer }),
2740
+ }),
2741
+ ]
2742
+ }
2743
+ })()
2731
2744
  // When the caller requested raw logs, attach the full per-transaction log set
2732
2745
  // (decoded when possible) to every classified item so no co-located log is
2733
2746
  // hidden behind the headline. `unknown` items already carry it.
2734
2747
  if (!ctx.includeLogs) return items
2735
- const events = buildTxEvents(rows, decodeRows(rows))
2748
+ const events = buildTxEvents(rows, decoded)
2736
2749
  return items.map((item) => (item.type === 'unknown' ? item : { ...item, events }))
2737
2750
  }
2738
2751
 
2752
+ /** Resolves an access-key signer from raw rows, falling back to the transaction signer. */
2753
+ function signerForRows(
2754
+ rows: readonly Row[],
2755
+ options: { fallback: 'self' | Address.Address; viewer: Address.Address },
2756
+ ) {
2757
+ const accessKeySpendRow = rows.find(
2758
+ (row) =>
2759
+ row.selector === accessKeySpendSelector &&
2760
+ Address.isEqual(row.logAddress, Addresses.accountKeychain),
2761
+ )
2762
+ const accessKeySigner = accessKeySpendRow?.topic2
2763
+ ? (`0x${accessKeySpendRow.topic2.slice(-40)}`.toLowerCase() as Address.Address)
2764
+ : null
2765
+ return accessKeySigner && !Address.isEqual(accessKeySigner, options.viewer)
2766
+ ? accessKeySigner
2767
+ : options.fallback
2768
+ }
2769
+
2739
2770
  /** Classifies a Q1 (log-bearing) group via `parseEventLogs`, picking one headline item. */
2740
2771
  function classifyFromLogsCore(
2741
2772
  rows: Row[],
@@ -2746,24 +2777,11 @@ function classifyFromLogsCore(
2746
2777
  signer: 'self' | Address.Address
2747
2778
  viewer: Address.Address
2748
2779
  },
2780
+ events = decodeRows(rows),
2749
2781
  ): RawItem[] {
2750
2782
  const { blockNum, fingerprintMap, itemBase, viewer } = ctx
2751
2783
 
2752
- // An access-key-signed tx emits AccessKeySpend at the accountKeychain; topic2
2753
- // is the access-key publicKey. Use it as the signer so successive access-key
2754
- // actions group together.
2755
- const accessKeySpendRow = rows.find(
2756
- (r) =>
2757
- r.selector === accessKeySpendSelector &&
2758
- Address.isEqual(r.logAddress, Addresses.accountKeychain),
2759
- )
2760
- const accessKeySigner = accessKeySpendRow?.topic2
2761
- ? (`0x${accessKeySpendRow.topic2.slice(-40)}`.toLowerCase() as Address.Address)
2762
- : null
2763
- const signer: 'self' | Address.Address =
2764
- accessKeySigner && !Address.isEqual(accessKeySigner, viewer) ? accessKeySigner : ctx.signer
2765
-
2766
- const events = decodeRows(rows)
2784
+ const signer = signerForRows(rows, { fallback: ctx.signer, viewer })
2767
2785
 
2768
2786
  const minLogIndex = Math.min(...rows.map((r) => r.logIdx))
2769
2787
 
@@ -3251,6 +3269,14 @@ function classifyFromLogsCore(
3251
3269
  const approvalEvent = finalEvents.find(
3252
3270
  (e) =>
3253
3271
  e.eventName === 'Approval' &&
3272
+ // ERC-721 uses the same selector but indexes its token ID as topic3. In
3273
+ // non-strict mode viem otherwise decodes the missing ERC-20 amount as zero.
3274
+ rows.some(
3275
+ (row) =>
3276
+ row.logIdx === e.logIndex &&
3277
+ row.topic3 === null &&
3278
+ /^0x[0-9a-fA-F]{64}$/.test(row.data ?? ''),
3279
+ ) &&
3254
3280
  !Address.isEqual((e.args as { spender: Address.Address }).spender, Addresses.zoneOutbox),
3255
3281
  )
3256
3282
  if (approvalEvent) {
@@ -4338,21 +4364,27 @@ function unknown(
4338
4364
 
4339
4365
  /** Decodes a group's raw rows into `viem/tempo` events via `parseEventLogs` (non-strict). */
4340
4366
  function decodeRows(rows: readonly Row[]) {
4341
- const viemLogs: Log[] = rows.map((r) => ({
4342
- address: r.logAddress as `0x${string}`,
4343
- blockHash: '0x0' as Hex.Hex,
4344
- blockNumber: BigInt(r.blockNum),
4345
- data: (r.data ?? '0x') as Hex.Hex,
4346
- logIndex: r.logIdx,
4347
- removed: false,
4348
- topics: [r.selector, r.topic1, r.topic2, r.topic3].filter(Boolean) as [Hex.Hex, ...Hex.Hex[]],
4349
- transactionHash: r.txHash as `0x${string}`,
4350
- transactionIndex: r.txIdx,
4351
- }))
4352
- return parseEventLogs({
4353
- abi: [...Abis.abis, ...Abis.earnRouter, ...Abis.earnVault],
4354
- logs: viemLogs,
4355
- strict: false,
4367
+ return rows.flatMap((r) => {
4368
+ const log: Log = {
4369
+ address: r.logAddress as `0x${string}`,
4370
+ blockHash: '0x0' as Hex.Hex,
4371
+ blockNumber: BigInt(r.blockNum),
4372
+ data: (r.data ?? '0x') as Hex.Hex,
4373
+ logIndex: r.logIdx,
4374
+ removed: false,
4375
+ topics: [r.selector, r.topic1, r.topic2, r.topic3].filter(Boolean) as [Hex.Hex, ...Hex.Hex[]],
4376
+ transactionHash: r.txHash as `0x${string}`,
4377
+ transactionIndex: r.txIdx,
4378
+ }
4379
+ try {
4380
+ return parseEventLogs({
4381
+ abi: [...Abis.abis, ...Abis.earnRouter, ...Abis.earnVault],
4382
+ logs: [log],
4383
+ strict: false,
4384
+ })
4385
+ } catch {
4386
+ return []
4387
+ }
4356
4388
  })
4357
4389
  }
4358
4390
 
@@ -375,9 +375,7 @@ export function addresses(options: addresses.Options = {}) {
375
375
  ),
376
376
  )
377
377
  const client = c.get('getClient')(chainId)
378
- const caller = c.get('zones').has(chainId)
379
- ? (await client.zone.getZoneInfo()).sequencers[0]
380
- : undefined
378
+ const caller = c.get('zones').has(chainId) ? address : undefined
381
379
  const [balance, feeTokens, snapshot, resolvedToken] = await Promise.all([
382
380
  Timing.time(c, 'balance_rpc', async () => {
383
381
  const call = Actions.token.getBalance.call(client, {
@@ -516,9 +514,7 @@ async function getAddressBalances(
516
514
  const correctedPromise = holdingsPromise.then(async (holdings) => {
517
515
  if (holdings.length === 0) return []
518
516
  const client = c.get('getClient')(chainId)
519
- const caller = c.get('zones').has(chainId)
520
- ? (await client.zone.getZoneInfo()).sequencers[0]
521
- : undefined
517
+ const caller = c.get('zones').has(chainId) ? address : undefined
522
518
  return Timing.time(c, 'balances_rpc', async () => {
523
519
  // Concurrent reads enter viem's shared batch window and become one
524
520
  // deployless multicall. A failed read fails the request rather than serving
@@ -2034,6 +2034,8 @@ export async function listVaults(c: Context<App.Environment>, options: listVault
2034
2034
  core_EarnVaults.list(db, { chainId: options.chainId }),
2035
2035
  VerifiedTokens.snapshot(c, options.chainId),
2036
2036
  ])
2037
+ const include = vaultInclude(options)
2038
+ const apyByVault = new Map<string, EarnRates.Apy | null>()
2037
2039
  const registry = new Map(records.map((record) => [record.vaultAddress.toLowerCase(), record]))
2038
2040
  const decoded = options.cursor ? Cursor.decode(options.cursor, ['int', 'int']) : undefined
2039
2041
  let position = decoded
@@ -2062,22 +2064,44 @@ export async function listVaults(c: Context<App.Environment>, options: listVault
2062
2064
  }
2063
2065
  scanned += page.items.length
2064
2066
 
2065
- const discovered = await Promise.all(
2066
- page.items.map(async (item) => {
2067
- const record = registry.get(item.vaultAddress.toLowerCase())
2068
- try {
2069
- const discovery = await resolveVaultDiscovery(c, {
2067
+ const apyVaults =
2068
+ apyByVault.size === 0 &&
2069
+ include.includes('apy') &&
2070
+ options.capability.length === 0 &&
2071
+ options['engine.type'] === undefined
2072
+ ? page.items
2073
+ .slice(0, options.limit - matches.length)
2074
+ .map(({ vaultAddress }) => vaultAddress)
2075
+ : []
2076
+ // Rate boundaries and historical quotes depend only on candidate addresses,
2077
+ // so the unfiltered page can resolve them alongside live vault discovery.
2078
+ const [discovered, prefetchedApy] = await Promise.all([
2079
+ Promise.all(
2080
+ page.items.map(async (item) => {
2081
+ const record = registry.get(item.vaultAddress.toLowerCase())
2082
+ try {
2083
+ const discovery = await resolveVaultDiscovery(c, {
2084
+ chainId: options.chainId,
2085
+ include: discoveryInclude(options),
2086
+ vaultAddress: item.vaultAddress,
2087
+ })
2088
+ return { discovery, position: item.position, record }
2089
+ } catch (cause) {
2090
+ if (!record && isIncompatible(cause)) return undefined
2091
+ throw cause
2092
+ }
2093
+ }),
2094
+ ),
2095
+ apyVaults.length > 0
2096
+ ? resolvePageApy(c, {
2070
2097
  chainId: options.chainId,
2071
- include: discoveryInclude(options),
2072
- vaultAddress: item.vaultAddress,
2098
+ include,
2099
+ vaults: apyVaults,
2100
+ window: options['apy.window'],
2073
2101
  })
2074
- return { discovery, position: item.position, record }
2075
- } catch (cause) {
2076
- if (!record && isIncompatible(cause)) return undefined
2077
- throw cause
2078
- }
2079
- }),
2080
- )
2102
+ : undefined,
2103
+ ])
2104
+ for (const [vault, apy] of prefetchedApy ?? []) apyByVault.set(vault, apy)
2081
2105
  for (const match of discovered) {
2082
2106
  if (!match || !matchesVaultFilters(match.discovery, options)) continue
2083
2107
  matches.push(match)
@@ -2089,17 +2113,25 @@ export async function listVaults(c: Context<App.Environment>, options: listVault
2089
2113
 
2090
2114
  const page = matches.slice(0, options.limit)
2091
2115
  const discoveries = page.map(({ discovery }) => discovery)
2092
- const include = vaultInclude(options)
2093
- const [apy, rates, tokens] = await Promise.all([
2094
- resolvePageApy(c, {
2095
- chainId: options.chainId,
2096
- discoveries,
2097
- include,
2098
- window: options['apy.window'],
2099
- }),
2116
+ const missingApyVaults = include.includes('apy')
2117
+ ? discoveries.flatMap(({ vaultAddress }) =>
2118
+ apyByVault.has(vaultAddress.toLowerCase()) ? [] : [vaultAddress],
2119
+ )
2120
+ : []
2121
+ const [missingApy, rates, tokens] = await Promise.all([
2122
+ missingApyVaults.length > 0
2123
+ ? resolvePageApy(c, {
2124
+ chainId: options.chainId,
2125
+ include,
2126
+ vaults: missingApyVaults,
2127
+ window: options['apy.window'],
2128
+ })
2129
+ : undefined,
2100
2130
  resolvePageRates(c, { discoveries, include, oracle: options.oracle, snapshot }),
2101
2131
  resolvePageTokens(c, { chainId: options.chainId, discoveries, include, snapshot }),
2102
2132
  ])
2133
+ for (const [vault, apy] of missingApy ?? []) apyByVault.set(vault, apy)
2134
+ const apy = include.includes('apy') ? apyByVault : undefined
2103
2135
  const data = page.map((match) =>
2104
2136
  resolveVault(c, { ...match, apy, include, rates, snapshot, tokens }),
2105
2137
  )
@@ -2181,8 +2213,8 @@ export async function listVerifiedVaults(
2181
2213
  const [apy, rates, tokens] = await Promise.all([
2182
2214
  resolvePageApy(c, {
2183
2215
  chainId: options.chainId,
2184
- discoveries,
2185
2216
  include,
2217
+ vaults: discoveries.map(({ vaultAddress }) => vaultAddress),
2186
2218
  window: options['apy.window'],
2187
2219
  }),
2188
2220
  resolvePageRates(c, { discoveries, include, oracle: options.oracle, snapshot }),
@@ -2503,8 +2535,8 @@ export async function getVault(c: Context<App.Environment>, options: getVault.Op
2503
2535
  const [apy, rates, tokens] = await Promise.all([
2504
2536
  resolvePageApy(c, {
2505
2537
  chainId: options.chainId,
2506
- discoveries,
2507
2538
  include,
2539
+ vaults: discoveries.map(({ vaultAddress }) => vaultAddress),
2508
2540
  window: options['apy.window'],
2509
2541
  }),
2510
2542
  resolvePageRates(c, { discoveries, include, oracle: options.oracle, snapshot }),
@@ -2616,7 +2648,7 @@ function resolvePageApy(c: Context<App.Environment>, options: resolvePageApy.Opt
2616
2648
  getClient: c.get('getClient'),
2617
2649
  getTidx: c.get('getTidx'),
2618
2650
  store: c.get('store'),
2619
- vaults: options.discoveries.map(({ vaultAddress }) => vaultAddress),
2651
+ vaults: options.vaults,
2620
2652
  window: options.window,
2621
2653
  }),
2622
2654
  )
@@ -2625,8 +2657,8 @@ function resolvePageApy(c: Context<App.Environment>, options: resolvePageApy.Opt
2625
2657
  declare namespace resolvePageApy {
2626
2658
  type Options = {
2627
2659
  chainId: z.output<typeof Schema.ChainId>
2628
- discoveries: readonly EarnVaults.Discovery[]
2629
2660
  include: z.output<typeof schema.getEarnVault.Query>['include']
2661
+ vaults: readonly z.output<typeof Schema.Address>[]
2630
2662
  window: z.output<typeof schema.getEarnVault.Query>['apy.window']
2631
2663
  }
2632
2664
  }
@@ -2837,7 +2869,7 @@ async function queryDeployments(
2837
2869
  c: Context<App.Environment>,
2838
2870
  options: queryDeployments.Options,
2839
2871
  ): Promise<queryDeployments.Result> {
2840
- const filters = ['deployment_rank = 1']
2872
+ const filters: string[] = []
2841
2873
  if (options.asset) filters.push(`lower(asset) = '${options.asset.toLowerCase()}'`)
2842
2874
  if (options.cursor)
2843
2875
  filters.push(
@@ -2855,7 +2887,7 @@ async function queryDeployments(
2855
2887
  ]),
2856
2888
  )
2857
2889
 
2858
- // ClickHouse supports ordering and windowing over decoded event CTEs.
2890
+ // Deduplicate before outer filters so every vault is filtered by its latest deployment.
2859
2891
  const result = await c
2860
2892
  .get('getTidx')(options.chainId)
2861
2893
  .fetch({
@@ -2868,14 +2900,12 @@ async function queryDeployments(
2868
2900
  "earnVault",
2869
2901
  asset,
2870
2902
  block_num,
2871
- log_idx,
2872
- row_number() OVER (
2873
- PARTITION BY lower("earnVault")
2874
- ORDER BY block_num DESC, log_idx DESC
2875
- ) AS deployment_rank
2903
+ log_idx
2876
2904
  FROM EarnStackDeployed
2905
+ ORDER BY block_num DESC, log_idx DESC
2906
+ LIMIT 1 BY lower("earnVault")
2877
2907
  )
2878
- WHERE ${filters.join(' AND ')}
2908
+ ${filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''}
2879
2909
  ORDER BY block_num DESC, log_idx DESC
2880
2910
  LIMIT ${options.limit + 1}
2881
2911
  ` as string,
@@ -531,7 +531,7 @@ describe('GET /exchange/orders?base=… (pair-scoped orders)', () => {
531
531
  })
532
532
 
533
533
  describe('GET /exchange/pairs/:base/depth', () => {
534
- test('returns orderbook depth for a real pair', async () => {
534
+ test('returns orderbook depth when token metadata is requested', async () => {
535
535
  const client = TestApp.client()
536
536
  const pairsResponse = await client.v1.exchange.pairs.$get(
537
537
  { query: { limit: '5' } },
@@ -547,21 +547,25 @@ describe('GET /exchange/pairs/:base/depth', () => {
547
547
  // throttles the multicall response.
548
548
  let body: z.output<typeof Exchanges.schema.getPairDepth.Response> | null = null
549
549
  let pair: (typeof pairs.data)[number] | null = null
550
+ let timing: string | null = null
550
551
  for (const candidate of pairs.data) {
551
552
  const response = await client.v1.exchange.pairs[':base'].depth.$get(
552
- { param: { base: candidate.base.address }, query: {} },
553
+ { param: { base: candidate.base.address }, query: { include: 'tokens' } },
553
554
  TestApp.auth,
554
555
  )
555
556
  if (response.status === 502) return
556
557
  expect(response.status).toBe(200)
557
558
  body = await TestApp.json(response, Exchanges.schema.getPairDepth.Response)
558
559
  pair = candidate
560
+ timing = response.headers.get('server-timing')
559
561
  if (body.bids.length > 0 || body.asks.length > 0) break
560
562
  }
561
563
  if (!body || !pair) return
562
564
 
563
565
  expect(body.base.address).toBe(pair.base.address)
564
566
  expect(body.quote.address).toBe(pair.quote.address)
567
+ expect(timing).toContain('pair_lookup;dur=')
568
+ expect(timing).not.toContain('pair_index;dur=')
565
569
 
566
570
  // Asks are emitted lowest-tick first (best ask outward); bids are
567
571
  // emitted highest-tick first (best bid outward). Per-row `cumulativeSize`
@@ -689,9 +693,9 @@ describe('GET /exchange/swaps', () => {
689
693
  const response = await client.v1.exchange.swaps.$get({ query: { limit: '10' } }, TestApp.auth)
690
694
  expect(response.status).toBe(200)
691
695
  const body = await TestApp.json(response, Exchanges.schema.getSwaps.Response)
696
+ expect(body.data.length).toBeGreaterThan(0)
692
697
  expect(body.data.length).toBeLessThanOrEqual(10)
693
698
  expect(response.headers.get('server-timing')).not.toContain('valuation_rates;dur=')
694
- if (body.data.length === 0) return
695
699
 
696
700
  for (const swap of body.data) {
697
701
  expect(swap.taker).toMatch(/^0x[0-9a-f]{40}$/)
@@ -827,6 +831,7 @@ describe('GET /exchange/swaps', () => {
827
831
  TestApp.auth,
828
832
  )
829
833
  expect(response.status).toBe(200)
834
+ expect(response.headers.get('server-timing')).not.toContain('token_logo_uri;dur=')
830
835
  const body = await TestApp.json(response, Exchanges.schema.getSwaps.Response)
831
836
  if (body.data.length === 0) return
832
837