tapimo 0.9.0 → 0.10.1

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 (319) 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 +4292 -3708
  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 +21 -20
  12. package/dist/Webhooks.d.ts.map +1 -1
  13. package/dist/Webhooks.js +66 -23
  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 +26 -17
  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/apps/verified-tokens.d.ts +168 -168
  24. package/dist/admin/ui.generated.d.ts +1 -1
  25. package/dist/admin/ui.generated.js +69 -69
  26. package/dist/admin/ui.generated.js.map +1 -1
  27. package/dist/analytics/Analytics.d.ts.map +1 -1
  28. package/dist/analytics/Analytics.js +26 -0
  29. package/dist/analytics/Analytics.js.map +1 -1
  30. package/dist/apps/Zones.d.ts +4280 -84
  31. package/dist/apps/Zones.d.ts.map +1 -1
  32. package/dist/apps/Zones.js +30 -10
  33. package/dist/apps/Zones.js.map +1 -1
  34. package/dist/apps/data/App.d.ts +5799 -4929
  35. package/dist/apps/data/App.d.ts.map +1 -1
  36. package/dist/apps/data/FxOracle.d.ts +4 -5
  37. package/dist/apps/data/FxOracle.d.ts.map +1 -1
  38. package/dist/apps/data/FxOracle.js +19 -4
  39. package/dist/apps/data/FxOracle.js.map +1 -1
  40. package/dist/apps/data/routes/activities.d.ts +3391 -492
  41. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  42. package/dist/apps/data/routes/activities.js +372 -29
  43. package/dist/apps/data/routes/activities.js.map +1 -1
  44. package/dist/apps/data/routes/addresses.d.ts +853 -339
  45. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  46. package/dist/apps/data/routes/balances.d.ts +10 -10
  47. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  48. package/dist/apps/data/routes/balances.js +145 -83
  49. package/dist/apps/data/routes/balances.js.map +1 -1
  50. package/dist/apps/data/routes/blocks.d.ts +123 -123
  51. package/dist/apps/data/routes/coingecko.d.ts +169 -169
  52. package/dist/apps/data/routes/earn.d.ts +455 -1
  53. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  54. package/dist/apps/data/routes/earn.js +369 -45
  55. package/dist/apps/data/routes/earn.js.map +1 -1
  56. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  57. package/dist/apps/data/routes/exchanges.js +114 -118
  58. package/dist/apps/data/routes/exchanges.js.map +1 -1
  59. package/dist/apps/data/routes/fee-amm.d.ts +24 -24
  60. package/dist/apps/data/routes/indexer.d.ts +35 -34
  61. package/dist/apps/data/routes/indexer.d.ts.map +1 -1
  62. package/dist/apps/data/routes/indexer.js +3 -2
  63. package/dist/apps/data/routes/indexer.js.map +1 -1
  64. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  65. package/dist/apps/data/routes/receipts.js +68 -47
  66. package/dist/apps/data/routes/receipts.js.map +1 -1
  67. package/dist/apps/data/routes/rpc.d.ts +31 -31
  68. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  69. package/dist/apps/data/routes/rpc.js +5 -1
  70. package/dist/apps/data/routes/rpc.js.map +1 -1
  71. package/dist/apps/data/routes/tokenlist.d.ts +12 -12
  72. package/dist/apps/data/routes/tokens.d.ts +461 -459
  73. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  74. package/dist/apps/data/routes/transactions.d.ts +313 -305
  75. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  76. package/dist/apps/data/routes/transactions.js +25 -23
  77. package/dist/apps/data/routes/transactions.js.map +1 -1
  78. package/dist/apps/data/routes/transfers.d.ts +108 -86
  79. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  80. package/dist/apps/data/routes/transfers.js +618 -105
  81. package/dist/apps/data/routes/transfers.js.map +1 -1
  82. package/dist/apps/data/routes/valuation.d.ts +21 -21
  83. package/dist/apps/data/routes/valuation.d.ts.map +1 -1
  84. package/dist/apps/data/routes/valuation.js +2 -2
  85. package/dist/apps/data/routes/valuation.js.map +1 -1
  86. package/dist/apps/data/routes/webhooks.d.ts +519 -868
  87. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  88. package/dist/apps/data/routes/webhooks.js +642 -353
  89. package/dist/apps/data/routes/webhooks.js.map +1 -1
  90. package/dist/apps/data/routes/zones.d.ts +31 -31
  91. package/dist/apps/data/routes/zones.d.ts.map +1 -1
  92. package/dist/apps/data/routes/zones.js +4 -2
  93. package/dist/apps/data/routes/zones.js.map +1 -1
  94. package/dist/apps/funding/routes/deposit-addresses.js +1 -1
  95. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  96. package/dist/apps/funding/routes/providers.d.ts +13 -13
  97. package/dist/apps/funding/routes/quotes.d.ts +89 -89
  98. package/dist/apps/funding/routes/transfers.d.ts +36 -36
  99. package/dist/apps/funding/routes/transfers.js +1 -1
  100. package/dist/apps/funding/routes/transfers.js.map +1 -1
  101. package/dist/apps/management/App.d.ts +11 -309
  102. package/dist/apps/management/App.d.ts.map +1 -1
  103. package/dist/apps/management/routes/api-keys.d.ts +74 -74
  104. package/dist/apps/management/routes/billing.js +1 -1
  105. package/dist/apps/management/routes/billing.js.map +1 -1
  106. package/dist/apps/management/routes/faucet.d.ts +35 -35
  107. package/dist/apps/management/routes/faucet.js +2 -2
  108. package/dist/apps/management/routes/faucet.js.map +1 -1
  109. package/dist/apps/management/routes/invitations.d.ts +134 -134
  110. package/dist/apps/management/routes/invite-links.d.ts +80 -80
  111. package/dist/apps/management/routes/members.d.ts +69 -69
  112. package/dist/apps/management/routes/orgs.d.ts +98 -98
  113. package/dist/apps/management/routes/projects.d.ts +60 -60
  114. package/dist/apps/management/routes/usage.d.ts +72 -72
  115. package/dist/apps/management/routes/verified-token-requests.d.ts +83 -83
  116. package/dist/apps/management/routes/webhooks.d.ts +203 -618
  117. package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
  118. package/dist/apps/mcp.d.ts.map +1 -1
  119. package/dist/apps/mcp.js +4 -1
  120. package/dist/apps/mcp.js.map +1 -1
  121. package/dist/apps/mpp/App.d.ts +56 -44
  122. package/dist/apps/mpp/App.d.ts.map +1 -1
  123. package/dist/apps/mpp/Mpp.d.ts +3 -0
  124. package/dist/apps/mpp/Mpp.d.ts.map +1 -1
  125. package/dist/apps/mpp/Mpp.js +3 -0
  126. package/dist/apps/mpp/Mpp.js.map +1 -1
  127. package/dist/apps/mpp/internal/Adapter.d.ts.map +1 -1
  128. package/dist/apps/mpp/internal/Adapter.js +9 -2
  129. package/dist/apps/mpp/internal/Adapter.js.map +1 -1
  130. package/dist/db/Db.d.ts +4 -2
  131. package/dist/db/Db.d.ts.map +1 -1
  132. package/dist/db/Db.js +75 -15
  133. package/dist/db/Db.js.map +1 -1
  134. package/dist/db/Schema.d.ts +19 -0
  135. package/dist/db/Schema.d.ts.map +1 -1
  136. package/dist/db/Schema.js +24 -0
  137. package/dist/db/Schema.js.map +1 -1
  138. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  139. package/dist/db/tables/sponsoredTransactions.js +4 -1
  140. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  141. package/dist/db/tables/webhookQueueEvents.d.ts +122 -0
  142. package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -0
  143. package/dist/db/tables/webhookQueueEvents.js +160 -0
  144. package/dist/db/tables/webhookQueueEvents.js.map +1 -0
  145. package/dist/db/tables/webhookSubscriptions.d.ts +58 -3
  146. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  147. package/dist/db/tables/webhookSubscriptions.js +86 -3
  148. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  149. package/dist/internal/Auth.d.ts +3 -0
  150. package/dist/internal/Auth.d.ts.map +1 -1
  151. package/dist/internal/Auth.js +6 -4
  152. package/dist/internal/Auth.js.map +1 -1
  153. package/dist/internal/MetricSink.d.ts +139 -3
  154. package/dist/internal/MetricSink.d.ts.map +1 -1
  155. package/dist/internal/MetricSink.js +144 -4
  156. package/dist/internal/MetricSink.js.map +1 -1
  157. package/dist/internal/RateLimit.d.ts +4 -6
  158. package/dist/internal/RateLimit.d.ts.map +1 -1
  159. package/dist/internal/RateLimit.js +15 -14
  160. package/dist/internal/RateLimit.js.map +1 -1
  161. package/dist/internal/Schema.js +1 -1
  162. package/dist/internal/Schema.js.map +1 -1
  163. package/dist/internal/Store.d.ts +2 -1
  164. package/dist/internal/Store.d.ts.map +1 -1
  165. package/dist/internal/Store.js +57 -18
  166. package/dist/internal/Store.js.map +1 -1
  167. package/dist/internal/Tidx.d.ts +1 -3
  168. package/dist/internal/Tidx.d.ts.map +1 -1
  169. package/dist/internal/Tidx.js +1 -3
  170. package/dist/internal/Tidx.js.map +1 -1
  171. package/dist/internal/Value.d.ts +2 -0
  172. package/dist/internal/Value.d.ts.map +1 -1
  173. package/dist/internal/Value.js +25 -0
  174. package/dist/internal/Value.js.map +1 -1
  175. package/dist/internal/Viem.d.ts.map +1 -1
  176. package/dist/internal/Viem.js +2 -1
  177. package/dist/internal/Viem.js.map +1 -1
  178. package/dist/internal/WebhookDestination.d.ts +2 -0
  179. package/dist/internal/WebhookDestination.d.ts.map +1 -1
  180. package/dist/internal/WebhookDestination.js +11 -0
  181. package/dist/internal/WebhookDestination.js.map +1 -1
  182. package/dist/internal/WebhookTransfer.d.ts +75 -0
  183. package/dist/internal/WebhookTransfer.d.ts.map +1 -0
  184. package/dist/internal/WebhookTransfer.js +299 -0
  185. package/dist/internal/WebhookTransfer.js.map +1 -0
  186. package/dist/internal/Webhooks.d.ts +176 -0
  187. package/dist/internal/Webhooks.d.ts.map +1 -1
  188. package/dist/internal/Webhooks.js +216 -20
  189. package/dist/internal/Webhooks.js.map +1 -1
  190. package/dist/internal/funding/Provider.d.ts +4 -0
  191. package/dist/internal/funding/Provider.d.ts.map +1 -1
  192. package/dist/internal/funding/Provider.js +11 -7
  193. package/dist/internal/funding/Provider.js.map +1 -1
  194. package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
  195. package/dist/internal/funding/Reconciliation.js +128 -0
  196. package/dist/internal/funding/Reconciliation.js.map +1 -1
  197. package/dist/internal/funding/providers/relay.d.ts +6 -4
  198. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  199. package/dist/internal/funding/providers/relay.js +71 -9
  200. package/dist/internal/funding/providers/relay.js.map +1 -1
  201. package/package.json +4 -4
  202. package/src/ApiKeys.test.ts +20 -0
  203. package/src/ApiKeys.ts +28 -1
  204. package/src/App.test.ts +10 -8
  205. package/src/App.ts +1 -0
  206. package/src/Client.test-d.ts +61 -5
  207. package/src/Webhooks.bench.ts +714 -0
  208. package/src/Webhooks.memory.bench.ts +250 -0
  209. package/src/Webhooks.test.ts +77 -6
  210. package/src/Webhooks.ts +85 -29
  211. package/src/admin/apps/api-keys.test.ts +9 -0
  212. package/src/admin/apps/api-keys.ts +15 -0
  213. package/src/admin/ui/src/lib/query-client.ts +3 -2
  214. package/src/admin/ui/src/routes/api-keys.tsx +31 -9
  215. package/src/admin/ui.generated.ts +69 -69
  216. package/src/analytics/Analytics.test.ts +8 -0
  217. package/src/analytics/Analytics.ts +26 -0
  218. package/src/apps/Zones.test.ts +23 -5
  219. package/src/apps/Zones.ts +31 -10
  220. package/src/apps/composition.test.ts +1 -0
  221. package/src/apps/data/App.test.ts +2 -2
  222. package/src/apps/data/FxOracle.test.ts +18 -0
  223. package/src/apps/data/FxOracle.ts +28 -5
  224. package/src/apps/data/routes/activities.bench.ts +41 -0
  225. package/src/apps/data/routes/activities.test.ts +122 -2
  226. package/src/apps/data/routes/activities.ts +435 -32
  227. package/src/apps/data/routes/balances.bench.ts +21 -0
  228. package/src/apps/data/routes/balances.test.ts +119 -10
  229. package/src/apps/data/routes/balances.ts +164 -97
  230. package/src/apps/data/routes/blocks.bench.ts +34 -0
  231. package/src/apps/data/routes/blocks.test.ts +12 -6
  232. package/src/apps/data/routes/coingecko.bench.ts +53 -0
  233. package/src/apps/data/routes/earn.bench.ts +71 -0
  234. package/src/apps/data/routes/earn.test.ts +197 -0
  235. package/src/apps/data/routes/earn.ts +487 -45
  236. package/src/apps/data/routes/exchanges.bench.ts +85 -0
  237. package/src/apps/data/routes/exchanges.test.ts +7 -6
  238. package/src/apps/data/routes/exchanges.ts +124 -117
  239. package/src/apps/data/routes/fee-amm.bench.ts +13 -0
  240. package/src/apps/data/routes/indexer.bench.ts +8 -0
  241. package/src/apps/data/routes/indexer.test.ts +2 -1
  242. package/src/apps/data/routes/indexer.ts +3 -2
  243. package/src/apps/data/routes/receipts.bench.ts +39 -0
  244. package/src/apps/data/routes/receipts.test.ts +24 -7
  245. package/src/apps/data/routes/receipts.ts +67 -50
  246. package/src/apps/data/routes/rpc.bench.ts +14 -0
  247. package/src/apps/data/routes/rpc.test.ts +27 -0
  248. package/src/apps/data/routes/rpc.ts +5 -1
  249. package/src/apps/data/routes/tokenlist.bench.ts +3 -0
  250. package/src/apps/data/routes/tokens.bench.ts +76 -0
  251. package/src/apps/data/routes/tokens.test.ts +51 -0
  252. package/src/apps/data/routes/transactions.bench.ts +40 -0
  253. package/src/apps/data/routes/transactions.test.ts +42 -0
  254. package/src/apps/data/routes/transactions.ts +44 -39
  255. package/src/apps/data/routes/transfers.bench.ts +28 -0
  256. package/src/apps/data/routes/transfers.test.ts +402 -24
  257. package/src/apps/data/routes/transfers.ts +708 -101
  258. package/src/apps/data/routes/valuation.bench.ts +14 -0
  259. package/src/apps/data/routes/valuation.test.ts +26 -0
  260. package/src/apps/data/routes/valuation.ts +6 -2
  261. package/src/apps/data/routes/verified-tokens.bench.ts +8 -0
  262. package/src/apps/data/routes/webhooks.bench.ts +26 -0
  263. package/src/apps/data/routes/webhooks.scan.test.ts +326 -0
  264. package/src/apps/data/routes/webhooks.test.ts +466 -135
  265. package/src/apps/data/routes/webhooks.ts +820 -429
  266. package/src/apps/data/routes/zones.bench.ts +32 -0
  267. package/src/apps/data/routes/zones.test.ts +1 -1
  268. package/src/apps/data/routes/zones.ts +7 -2
  269. package/src/apps/funding/routes/chains.test.ts +1 -1
  270. package/src/apps/funding/routes/deposit-addresses.test.ts +1 -1
  271. package/src/apps/funding/routes/deposit-addresses.ts +1 -1
  272. package/src/apps/funding/routes/providers.test.ts +1 -1
  273. package/src/apps/funding/routes/quotes.test.ts +2 -2
  274. package/src/apps/funding/routes/transfers.test.ts +3 -6
  275. package/src/apps/funding/routes/transfers.ts +1 -1
  276. package/src/apps/management/routes/billing.ts +1 -1
  277. package/src/apps/management/routes/faucet.ts +2 -2
  278. package/src/apps/management/routes/me.test.ts +31 -0
  279. package/src/apps/mcp.ts +4 -1
  280. package/src/apps/mpp/Mpp.test.ts +13 -1
  281. package/src/apps/mpp/Mpp.ts +8 -0
  282. package/src/apps/mpp/internal/Adapter.ts +10 -2
  283. package/src/db/Db.test.ts +40 -6
  284. package/src/db/Db.ts +114 -19
  285. package/src/db/Schema.ts +32 -0
  286. package/src/db/tables/sponsoredTransactions.ts +9 -2
  287. package/src/db/tables/webhookQueueEvents.ts +285 -0
  288. package/src/db/tables/webhookSubscriptions.ts +131 -4
  289. package/src/internal/Auth.test.ts +61 -50
  290. package/src/internal/Auth.ts +9 -4
  291. package/src/internal/EarnVaults.test.ts +2 -2
  292. package/src/internal/MetricSink.test.ts +575 -0
  293. package/src/internal/MetricSink.ts +325 -10
  294. package/src/internal/RateLimit.test.ts +23 -18
  295. package/src/internal/RateLimit.ts +23 -15
  296. package/src/internal/Schema.ts +1 -1
  297. package/src/internal/Store.test.ts +99 -13
  298. package/src/internal/Store.ts +52 -17
  299. package/src/internal/Tidx.test.ts +4 -4
  300. package/src/internal/Tidx.ts +1 -3
  301. package/src/internal/Value.test.ts +25 -0
  302. package/src/internal/Value.ts +29 -0
  303. package/src/internal/Viem.test.ts +8 -0
  304. package/src/internal/Viem.ts +2 -1
  305. package/src/internal/WebhookDestination.test.ts +20 -0
  306. package/src/internal/WebhookDestination.ts +10 -0
  307. package/src/internal/WebhookTransfer.test.ts +89 -0
  308. package/src/internal/WebhookTransfer.ts +430 -0
  309. package/src/internal/Webhooks.test.ts +616 -0
  310. package/src/internal/Webhooks.ts +412 -24
  311. package/src/internal/funding/Provider.test.ts +48 -8
  312. package/src/internal/funding/Provider.ts +12 -7
  313. package/src/internal/funding/Reconciliation.local.test.ts +3 -1
  314. package/src/internal/funding/Reconciliation.test.ts +1240 -0
  315. package/src/internal/funding/Reconciliation.ts +136 -0
  316. package/src/internal/funding/SourceObservation.test.ts +1 -0
  317. package/src/internal/funding/providers/index.test.ts +73 -3
  318. package/src/internal/funding/providers/relay.test.ts +2 -9
  319. package/src/internal/funding/providers/relay.ts +72 -13
@@ -60,6 +60,8 @@ describe('GET /transactions/:transactionHash/receipt', () => {
60
60
  expect(body.feeAmount.currency).toBe('USD')
61
61
  expect(body.feeAmount.decimals).toBe(6)
62
62
  expect(body.feeAmount.formatted).toMatch(/^\d+(\.\d+)?$/)
63
+ expect('valuation' in body.feeAmount).toBe(false)
64
+ expect(response.headers.get('server-timing')).not.toContain('valuation_rates;dur=')
63
65
  // Core fee-token metadata is present by default while the raw address stays
64
66
  // under `meta.rpc`. Curated fields remain opt-in.
65
67
  if (body.meta.rpc.feeToken) {
@@ -272,6 +274,27 @@ describe('GET /transactions/receipts', () => {
272
274
  for (const receipt of body.data) expect(receipt.status).toBe('success')
273
275
  })
274
276
 
277
+ test('narrows receipts by fee token', async () => {
278
+ const client = TestApp.client()
279
+ const response = await client.v1.transactions.receipts.$get(
280
+ {
281
+ query: {
282
+ feeToken: TestApp.tokenWithHolders,
283
+ include: 'totalCount',
284
+ limit: '5',
285
+ },
286
+ },
287
+ TestApp.auth,
288
+ )
289
+ const body = await TestApp.json(response, Receipts.schema.getReceipts.Response)
290
+ expect(response.status).toBe(200)
291
+ expect(body.data.length).toBeGreaterThan(0)
292
+ expect(body.meta).toBeDefined()
293
+ expect(body.meta!.totalCount).toBeGreaterThanOrEqual(body.data.length)
294
+ for (const receipt of body.data)
295
+ expect(receipt.meta.rpc.feeToken).toBe(TestApp.tokenWithHolders)
296
+ })
297
+
275
298
  test('embeds a capped total count on demand via include=totalCount', async () => {
276
299
  const client = TestApp.client()
277
300
 
@@ -294,13 +317,7 @@ describe('GET /transactions/receipts', () => {
294
317
  expect(body.meta!.totalCount).toBeGreaterThanOrEqual(body.data.length)
295
318
  expect(body.meta!.totalCount).toBeLessThanOrEqual(Schema.countCap)
296
319
  expect(body.meta!.totalCountCapped).toBe(body.meta!.totalCount! >= Schema.countCap)
297
- // Fees are USD, so the default denomination values them at identity with
298
- // no rate provenance.
299
- for (const receipt of body.data)
300
- expect(receipt.feeAmount.valuation).toEqual({
301
- amount: receipt.feeAmount.formatted,
302
- currency: 'USD',
303
- })
320
+ for (const receipt of body.data) expect('valuation' in receipt.feeAmount).toBe(false)
304
321
  expect(body.meta!.valuation).toBeUndefined()
305
322
 
306
323
  // Without `include=totalCount`, the same page omits `meta` and the count.
@@ -553,22 +553,28 @@ export function receipts(options: receipts.Options = {}) {
553
553
 
554
554
  // Value each fee via its own currency tag, in the requested
555
555
  // denomination.
556
- const denomination = query['valuation.currency'] ?? 'USD'
557
- const rates = await Valuation.ratesFor(c, {
558
- currencies: receipts.data.map((receipt) => receipt.feeAmount.currency),
559
- denomination,
560
- oracle,
561
- })
556
+ const denomination = query['valuation.currency']
557
+ const rates = denomination
558
+ ? await Valuation.ratesFor(c, {
559
+ currencies: receipts.data.map((receipt) => receipt.feeAmount.currency),
560
+ denomination,
561
+ oracle,
562
+ })
563
+ : undefined
562
564
  const data = receipts.data.map((receipt) => ({
563
565
  ...receipt,
564
566
  feeAmount: {
565
567
  ...receipt.feeAmount,
566
- valuation: Valuation.valuationFor({
567
- amount: BigInt(receipt.feeAmount.baseUnits),
568
- denomination,
569
- rates,
570
- token: receipt.feeAmount,
571
- }),
568
+ ...(denomination
569
+ ? {
570
+ valuation: Valuation.valuationFor({
571
+ amount: BigInt(receipt.feeAmount.baseUnits),
572
+ denomination,
573
+ rates,
574
+ token: receipt.feeAmount,
575
+ }),
576
+ }
577
+ : {}),
572
578
  },
573
579
  }))
574
580
 
@@ -680,22 +686,28 @@ export function receipts(options: receipts.Options = {}) {
680
686
 
681
687
  // Value the fee via its own currency tag, in the requested
682
688
  // denomination.
683
- const denomination = query['valuation.currency'] ?? 'USD'
684
- const rates = await Valuation.ratesFor(c, {
685
- currencies: [enriched.feeAmount.currency],
686
- denomination,
687
- oracle,
688
- })
689
+ const denomination = query['valuation.currency']
690
+ const rates = denomination
691
+ ? await Valuation.ratesFor(c, {
692
+ currencies: [enriched.feeAmount.currency],
693
+ denomination,
694
+ oracle,
695
+ })
696
+ : undefined
689
697
  const valued = {
690
698
  ...enriched,
691
699
  feeAmount: {
692
700
  ...enriched.feeAmount,
693
- valuation: Valuation.valuationFor({
694
- amount: BigInt(enriched.feeAmount.baseUnits),
695
- denomination,
696
- rates,
697
- token: enriched.feeAmount,
698
- }),
701
+ ...(denomination
702
+ ? {
703
+ valuation: Valuation.valuationFor({
704
+ amount: BigInt(enriched.feeAmount.baseUnits),
705
+ denomination,
706
+ rates,
707
+ token: enriched.feeAmount,
708
+ }),
709
+ }
710
+ : {}),
699
711
  },
700
712
  meta: {
701
713
  ...enriched.meta,
@@ -771,9 +783,9 @@ export function getReceipts(c: Context<App.Environment>, options: getReceipts.Op
771
783
  return Timing.time(c, 'receipts', () =>
772
784
  Store.memoize(
773
785
  async () => {
774
- // Page the `receipts` table alone, then enrich each row with the
775
- // `type`/`fee_token` it does not store via a follow-up `txs` lookup
776
- // (the blocks-feed pattern). Paging straight off `receipts` avoids the
786
+ // Page the `receipts` table alone. Fee-token queries read metadata
787
+ // denormalized on ClickHouse; other queries hydrate it from `txs`.
788
+ // Paging straight off `receipts` avoids the
777
789
  // `receipts JOIN txs` shape, which TIDX's planner cannot execute under
778
790
  // address scope: a `UNION` of two JOINs (or the `(from = X OR to = X)`
779
791
  // form) reliably returns `db error` (HTTP 422) for high-traffic
@@ -785,10 +797,9 @@ export function getReceipts(c: Context<App.Environment>, options: getReceipts.Op
785
797
  if (sender !== undefined) filters.push(`"from" = '${sender}'`)
786
798
  if (recipient !== undefined) filters.push(`"to" = '${recipient}'`)
787
799
  if (feePayer !== undefined) filters.push(`fee_payer = '${feePayer}'`)
788
- // `fee_token` lives on `txs`, not `receipts`; filter via a subquery so
789
- // the page query stays single-table.
790
- if (feeToken !== undefined)
791
- filters.push(`tx_hash IN (SELECT hash FROM txs WHERE fee_token = '${feeToken}')`)
800
+ // ClickHouse receipts denormalize fee metadata specifically to avoid
801
+ // an unbounded transaction-hash semi-join for this filter.
802
+ if (feeToken !== undefined) filters.push(`fee_token = '${feeToken}'`)
792
803
  if (fromBlock !== undefined) filters.push(`block_num >= ${fromBlock}`)
793
804
  if (toBlock !== undefined) filters.push(`block_num <= ${toBlock}`)
794
805
  if (fromTimestamp !== undefined) filters.push(`block_timestamp >= '${fromTimestamp}'`)
@@ -804,7 +815,7 @@ export function getReceipts(c: Context<App.Environment>, options: getReceipts.Op
804
815
 
805
816
  const columns = `block_num, block_timestamp, tx_idx, tx_hash, "from", "to",
806
817
  contract_address, gas_used, cumulative_gas_used, effective_gas_price,
807
- status, fee_payer`
818
+ status, fee_payer${feeToken === undefined ? '' : ', type, fee_token'}`
808
819
 
809
820
  // For address-scoped queries, run one branch per side and `UNION` so
810
821
  // each branch returns at most `limit + 1` index-ordered rows, the outer
@@ -814,14 +825,15 @@ export function getReceipts(c: Context<App.Environment>, options: getReceipts.Op
814
825
  //
815
826
  // The `receipts` table only indexes `from`, not `to` (a `to` scan over
816
827
  // ~25M rows hits the indexer's statement timeout and returns `db error`,
817
- // HTTP 422). The `from` branch filters `receipts` directly; the `to`
818
- // branch resolves matching transaction hashes from `txs` (which indexes
819
- // `to`) and looks them up by the indexed `receipts.tx_hash`.
828
+ // HTTP 422). A fee-payer filter bounds the direct `to` scan through the
829
+ // receipt fee-payer index. Other `to` branches resolve matching hashes
830
+ // through the indexed `txs.to` and `receipts.tx_hash` columns.
820
831
  const sideQuery = (side: 'from' | 'to', addr: string) => {
821
- const predicate =
822
- side === 'from'
823
- ? `"from" = '${addr}'`
824
- : `tx_hash IN (SELECT hash FROM txs WHERE "to" = '${addr}')`
832
+ const predicate = (() => {
833
+ if (side === 'from') return `"from" = '${addr}'`
834
+ if (feePayer !== undefined) return `"to" = '${addr}'`
835
+ return `tx_hash IN (SELECT hash FROM txs WHERE "to" = '${addr}')`
836
+ })()
825
837
  const where = [predicate, ...filters].join(' AND ')
826
838
  // In the offset lane each branch over-fetches the full window
827
839
  // (`offset + limit + 1` rows) because the skip applies to the merged
@@ -847,7 +859,11 @@ export function getReceipts(c: Context<App.Environment>, options: getReceipts.Op
847
859
  LIMIT ${limit + 1}${skip}
848
860
  `
849
861
 
850
- const result = await tidx.fetch({ chainId, query: query as string })
862
+ const result = await tidx.fetch({
863
+ chainId,
864
+ ...(feeToken !== undefined ? { engine: 'clickhouse' } : {}),
865
+ query: query as string,
866
+ })
851
867
 
852
868
  // The next page anchors below the last fetched row's `(block, tx_idx)`.
853
869
  const page = Cursor.paginate({
@@ -860,11 +876,8 @@ export function getReceipts(c: Context<App.Environment>, options: getReceipts.Op
860
876
  },
861
877
  })
862
878
 
863
- // Enrich the page with `type`/`fee_token` from `txs` in a single
864
- // `IN (...)` lookup scoped to the page's transaction hashes. `txs` is
865
- // sorted by `(block_num, idx)` with no hash index, so the hash list
866
- // must be paired with the page's block numbers — a bare `hash IN`
867
- // full-scans the table.
879
+ // Enrich non-fee-token pages with metadata from `txs`. Pairing hashes
880
+ // with their blocks avoids a full-table hash scan.
868
881
  const hashes = [
869
882
  ...new Set(
870
883
  page.rows
@@ -880,7 +893,7 @@ export function getReceipts(c: Context<App.Environment>, options: getReceipts.Op
880
893
  ),
881
894
  ]
882
895
  const txByHash = new Map<string, { feeToken: unknown; type: unknown }>()
883
- if (hashes.length > 0 && blocks.length > 0) {
896
+ if (feeToken === undefined && hashes.length > 0 && blocks.length > 0) {
884
897
  const txs = await tidx.fetch({
885
898
  chainId,
886
899
  query: `SELECT hash, type, fee_token FROM txs WHERE block_num IN (${blocks.join(
@@ -900,7 +913,8 @@ export function getReceipts(c: Context<App.Environment>, options: getReceipts.Op
900
913
  const data: humanizeReceipt.Output[] = []
901
914
  for (const row of page.rows) {
902
915
  const tx = typeof row['tx_hash'] === 'string' ? txByHash.get(row['tx_hash']) : undefined
903
- const merged = { ...row, fee_token: tx?.feeToken, type: tx?.type }
916
+ const merged =
917
+ feeToken === undefined ? { ...row, fee_token: tx?.feeToken, type: tx?.type } : row
904
918
  const rpc = schema.Rpc.Receipt.safeParse(toRpcReceipt(merged))
905
919
  if (rpc.success) data.push(humanizeReceipt(rpc.data))
906
920
  }
@@ -950,8 +964,7 @@ function countFilters(options: countReceipts.Options): string[] {
950
964
  if (options.sender !== undefined) filters.push(`"from" = '${options.sender}'`)
951
965
  if (options.recipient !== undefined) filters.push(`"to" = '${options.recipient}'`)
952
966
  if (options.feePayer !== undefined) filters.push(`fee_payer = '${options.feePayer}'`)
953
- if (options.feeToken !== undefined)
954
- filters.push(`tx_hash IN (SELECT hash FROM txs WHERE fee_token = '${options.feeToken}')`)
967
+ if (options.feeToken !== undefined) filters.push(`fee_token = '${options.feeToken}'`)
955
968
  if (options.fromBlock !== undefined) filters.push(`block_num >= ${options.fromBlock}`)
956
969
  if (options.toBlock !== undefined) filters.push(`block_num <= ${options.toBlock}`)
957
970
  if (options.fromTimestamp !== undefined)
@@ -1003,7 +1016,11 @@ export function countReceipts(
1003
1016
  ) AS capped
1004
1017
  `
1005
1018
  }
1006
- const result = await tidx.fetch({ chainId, query: query as string })
1019
+ const result = await tidx.fetch({
1020
+ chainId,
1021
+ ...(options.feeToken !== undefined ? { engine: 'clickhouse' } : {}),
1022
+ query: query as string,
1023
+ })
1007
1024
  const totalCount = Value.toNumber(result.rows[0]?.['total']) ?? 0
1008
1025
  return { totalCountCapped: totalCount >= Schema.countCap, totalCount }
1009
1026
  },
@@ -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.',
@@ -0,0 +1,3 @@
1
+ import * as QueryBench from '../../../../test/QueryBench.js'
2
+
3
+ QueryBench.bench({ name: 'getTokenList', path: '/v1/tokenlist' })
@@ -0,0 +1,76 @@
1
+ import * as QueryBench from '../../../../test/QueryBench.js'
2
+
3
+ const address = {
4
+ 4217: '0xc24357119432595a547cb2c1e41c51d753645519',
5
+ 42431: '0x15496d3123afb2cfadc0ac068eadd11d0f8ae799',
6
+ } as const
7
+
8
+ QueryBench.bench({
9
+ name: 'getTokens',
10
+ path: '/v1/tokens',
11
+ queries: [
12
+ `addresses=${QueryBench.pathUsd}`,
13
+ `addresses=${QueryBench.pathUsd}&include=admin,createdAt,holderCount,quoteToken,transferStats`,
14
+ 'include=admin',
15
+ 'include=createdAt',
16
+ 'include=holderCount',
17
+ 'include=quoteToken',
18
+ 'include=transferStats',
19
+ 'verified=true',
20
+ 'verified=true&include=admin,createdAt,holderCount,quoteToken,transferStats',
21
+ ],
22
+ })
23
+
24
+ QueryBench.bench({
25
+ name: 'getTokenBySymbol',
26
+ path: '/v1/tokens/pathUSD',
27
+ queries: [
28
+ 'include=admin',
29
+ 'include=createdAt',
30
+ 'include=holderCount',
31
+ 'include=quoteToken',
32
+ 'include=transferStats',
33
+ ],
34
+ })
35
+
36
+ QueryBench.bench({
37
+ name: 'getToken',
38
+ path: `/v1/tokens/${QueryBench.pathUsd}`,
39
+ queries: [
40
+ 'include=admin',
41
+ 'include=createdAt',
42
+ 'include=holderCount',
43
+ 'include=quoteToken',
44
+ 'include=transferStats',
45
+ ],
46
+ })
47
+
48
+ QueryBench.bench({
49
+ name: 'getTokenLogo',
50
+ path: `/v1/tokens/${QueryBench.pathUsd}/logo`,
51
+ })
52
+
53
+ QueryBench.bench({
54
+ name: 'getTokenHolders',
55
+ path: `/v1/tokens/${QueryBench.pathUsd}/holders`,
56
+ queries: ['include=token', 'include=totalCount'],
57
+ })
58
+
59
+ QueryBench.bench({
60
+ name: 'getTokenTransactions',
61
+ path: `/v1/tokens/${QueryBench.pathUsd}/transactions`,
62
+ queries: [
63
+ 'include=feeToken.logoUri',
64
+ 'include=feeToken.verified',
65
+ 'include=token',
66
+ 'include=totalCount',
67
+ ],
68
+ })
69
+
70
+ QueryBench.bench({
71
+ name: 'getTokenTransactions?feePayer&include=totalCount',
72
+ path: {
73
+ 4217: `/v1/tokens/${QueryBench.pathUsd}/transactions?feePayer=${address[4217]}&include=totalCount`,
74
+ 42431: `/v1/tokens/${QueryBench.pathUsd}/transactions?feePayer=${address[42431]}&include=totalCount`,
75
+ },
76
+ })
@@ -1121,6 +1121,57 @@ describe('GET /tokens/:token/transactions', () => {
1121
1121
  expect(secondBody.data.every((entry) => !firstHashes.has(entry.hash))).toBe(true)
1122
1122
  })
1123
1123
 
1124
+ test('filters and counts by fee payer through receipts with pagination', async () => {
1125
+ const client = app()
1126
+ const seedResponse = await client.v1.tokens[':token'].transactions.$get(
1127
+ { param: { token: TestApp.token }, query: { limit: '50' } },
1128
+ TestApp.auth,
1129
+ )
1130
+ const seed = await TestApp.json(seedResponse, Tokens.schema.getTokenTransactions.Response)
1131
+ const feePayers = seed.data.flatMap((transaction) => {
1132
+ const feePayer = transaction.meta.rpc.feePayer
1133
+ return typeof feePayer === 'string' ? [feePayer] : []
1134
+ })
1135
+ const feePayer = feePayers.find(
1136
+ (candidate) => feePayers.filter((value) => value === candidate).length > 5,
1137
+ )
1138
+ if (!feePayer) return
1139
+
1140
+ const first = await client.v1.tokens[':token'].transactions.$get(
1141
+ {
1142
+ param: { token: TestApp.token },
1143
+ query: { feePayer, include: 'totalCount', limit: '5' },
1144
+ },
1145
+ TestApp.auth,
1146
+ )
1147
+ const firstBody = await TestApp.json(first, Tokens.schema.getTokenTransactions.Response)
1148
+ expect(first.status).toBe(200)
1149
+ expect(first.headers.get('server-timing')).toContain('receipts;dur=')
1150
+ expect(first.headers.get('server-timing')).toContain('receipts_count;dur=')
1151
+ expect(firstBody.data).toHaveLength(5)
1152
+ expect(firstBody.meta?.totalCount).toBeGreaterThanOrEqual(firstBody.data.length)
1153
+ expect(firstBody.data.every((transaction) => transaction.meta.rpc.feePayer === feePayer)).toBe(
1154
+ true,
1155
+ )
1156
+ if (!firstBody.nextCursor) return
1157
+
1158
+ const second = await client.v1.tokens[':token'].transactions.$get(
1159
+ {
1160
+ param: { token: TestApp.token },
1161
+ query: { cursor: firstBody.nextCursor, feePayer, limit: '5' },
1162
+ },
1163
+ TestApp.auth,
1164
+ )
1165
+ const secondBody = await TestApp.json(second, Tokens.schema.getTokenTransactions.Response)
1166
+ expect(second.status).toBe(200)
1167
+ expect(secondBody.data.length).toBeGreaterThan(0)
1168
+ expect(secondBody.data.every((transaction) => transaction.meta.rpc.feePayer === feePayer)).toBe(
1169
+ true,
1170
+ )
1171
+ const firstHashes = new Set(firstBody.data.map((transaction) => transaction.hash))
1172
+ expect(secondBody.data.every((transaction) => !firstHashes.has(transaction.hash))).toBe(true)
1173
+ })
1174
+
1124
1175
  test('returns 400 for an invalid token address', async () => {
1125
1176
  // The typed client requires `token: `Hex.Hex`` so an invalid literal
1126
1177
  // cannot pass through it; exercise the validator via `app.request`.
@@ -0,0 +1,40 @@
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: 'getTransactions',
15
+ path: '/v1/transactions',
16
+ queries: [
17
+ 'include=feeToken.logoUri',
18
+ 'include=feeToken.verified',
19
+ 'include=receipt',
20
+ 'include=totalCount',
21
+ 'status=success&include=totalCount',
22
+ ],
23
+ })
24
+
25
+ QueryBench.bench({
26
+ name: 'getTransactions?address&include=totalCount',
27
+ path: {
28
+ 4217: `/v1/transactions?address=${address[4217]}&include=totalCount`,
29
+ 42431: `/v1/transactions?address=${address[42431]}&include=totalCount`,
30
+ },
31
+ })
32
+
33
+ QueryBench.bench({
34
+ name: 'getTransaction',
35
+ path: {
36
+ 4217: `/v1/transactions/${hash[4217]}`,
37
+ 42431: `/v1/transactions/${hash[42431]}`,
38
+ },
39
+ queries: ['include=feeToken.logoUri', 'include=feeToken.verified', 'include=receipt'],
40
+ })
@@ -311,6 +311,48 @@ describe('GET /transactions', () => {
311
311
  `)
312
312
  })
313
313
 
314
+ test('filters and counts by fee payer through receipts with pagination', async () => {
315
+ const client = TestApp.client()
316
+ const seedResponse = await client.v1.transactions.$get({ query: { limit: '50' } }, TestApp.auth)
317
+ const seed = await TestApp.json(seedResponse, Transactions.schema.getTransactions.Response)
318
+ const feePayers = seed.data.flatMap((transaction) => {
319
+ const feePayer = transaction.meta.rpc.feePayer
320
+ return typeof feePayer === 'string' ? [feePayer] : []
321
+ })
322
+ const feePayer = feePayers.find(
323
+ (candidate) => feePayers.filter((value) => value === candidate).length > 5,
324
+ )
325
+ if (!feePayer) return
326
+
327
+ const first = await client.v1.transactions.$get(
328
+ { query: { feePayer, include: 'totalCount', limit: '5' } },
329
+ TestApp.auth,
330
+ )
331
+ const firstBody = await TestApp.json(first, Transactions.schema.getTransactions.Response)
332
+ expect(first.status).toBe(200)
333
+ expect(first.headers.get('server-timing')).toContain('receipts;dur=')
334
+ expect(first.headers.get('server-timing')).toContain('receipts_count;dur=')
335
+ expect(firstBody.data).toHaveLength(5)
336
+ expect(firstBody.meta?.totalCount).toBeGreaterThanOrEqual(firstBody.data.length)
337
+ expect(firstBody.data.every((transaction) => transaction.meta.rpc.feePayer === feePayer)).toBe(
338
+ true,
339
+ )
340
+ if (!firstBody.nextCursor) return
341
+
342
+ const second = await client.v1.transactions.$get(
343
+ { query: { cursor: firstBody.nextCursor, feePayer, limit: '5' } },
344
+ TestApp.auth,
345
+ )
346
+ const secondBody = await TestApp.json(second, Transactions.schema.getTransactions.Response)
347
+ expect(second.status).toBe(200)
348
+ expect(secondBody.data.length).toBeGreaterThan(0)
349
+ expect(secondBody.data.every((transaction) => transaction.meta.rpc.feePayer === feePayer)).toBe(
350
+ true,
351
+ )
352
+ const firstHashes = new Set(firstBody.data.map((transaction) => transaction.hash))
353
+ expect(secondBody.data.every((transaction) => !firstHashes.has(transaction.hash))).toBe(true)
354
+ })
355
+
314
356
  test('filters reverted transactions by execution status', async () => {
315
357
  const client = TestApp.client()
316
358
  const response = await client.v1.transactions.$get(