tapimo 0.0.0 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (422) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +119 -0
  3. package/dist/ApiKey.d.ts +65 -0
  4. package/dist/ApiKey.d.ts.map +1 -0
  5. package/dist/ApiKey.js +105 -0
  6. package/dist/ApiKey.js.map +1 -0
  7. package/dist/ApiKeySource.d.ts +166 -0
  8. package/dist/ApiKeySource.d.ts.map +1 -0
  9. package/dist/ApiKeySource.js +234 -0
  10. package/dist/ApiKeySource.js.map +1 -0
  11. package/dist/App.d.ts +19317 -0
  12. package/dist/App.d.ts.map +1 -0
  13. package/dist/App.js +612 -0
  14. package/dist/App.js.map +1 -0
  15. package/dist/Assets.d.ts +44 -0
  16. package/dist/Assets.d.ts.map +1 -0
  17. package/dist/Assets.js +23 -0
  18. package/dist/Assets.js.map +1 -0
  19. package/dist/Auth.d.ts +2 -0
  20. package/dist/Auth.d.ts.map +1 -0
  21. package/dist/Auth.js +2 -0
  22. package/dist/Auth.js.map +1 -0
  23. package/dist/Cli.d.ts +21 -0
  24. package/dist/Cli.d.ts.map +1 -0
  25. package/dist/Cli.js +13 -0
  26. package/dist/Cli.js.map +1 -0
  27. package/dist/Client.d.ts +20 -0
  28. package/dist/Client.d.ts.map +1 -0
  29. package/dist/Client.js +24 -0
  30. package/dist/Client.js.map +1 -0
  31. package/dist/Metrics.d.ts +70 -0
  32. package/dist/Metrics.d.ts.map +1 -0
  33. package/dist/Metrics.js +91 -0
  34. package/dist/Metrics.js.map +1 -0
  35. package/dist/Scope.d.ts +35 -0
  36. package/dist/Scope.d.ts.map +1 -0
  37. package/dist/Scope.js +38 -0
  38. package/dist/Scope.js.map +1 -0
  39. package/dist/Webhooks.d.ts +79 -0
  40. package/dist/Webhooks.d.ts.map +1 -0
  41. package/dist/Webhooks.js +107 -0
  42. package/dist/Webhooks.js.map +1 -0
  43. package/dist/admin/Access.d.ts +41 -0
  44. package/dist/admin/Access.d.ts.map +1 -0
  45. package/dist/admin/Access.js +75 -0
  46. package/dist/admin/Access.js.map +1 -0
  47. package/dist/admin/App.d.ts +1221 -0
  48. package/dist/admin/App.d.ts.map +1 -0
  49. package/dist/admin/App.js +113 -0
  50. package/dist/admin/App.js.map +1 -0
  51. package/dist/admin/apps/api-keys.d.ts +209 -0
  52. package/dist/admin/apps/api-keys.d.ts.map +1 -0
  53. package/dist/admin/apps/api-keys.js +129 -0
  54. package/dist/admin/apps/api-keys.js.map +1 -0
  55. package/dist/admin/apps/scopes.d.ts +46 -0
  56. package/dist/admin/apps/scopes.d.ts.map +1 -0
  57. package/dist/admin/apps/scopes.js +41 -0
  58. package/dist/admin/apps/scopes.js.map +1 -0
  59. package/dist/admin/apps/verified-tokens.d.ts +1206 -0
  60. package/dist/admin/apps/verified-tokens.d.ts.map +1 -0
  61. package/dist/admin/apps/verified-tokens.js +485 -0
  62. package/dist/admin/apps/verified-tokens.js.map +1 -0
  63. package/dist/admin/index.d.ts +3 -0
  64. package/dist/admin/index.d.ts.map +1 -0
  65. package/dist/admin/index.js +3 -0
  66. package/dist/admin/index.js.map +1 -0
  67. package/dist/admin/ui.generated.d.ts +11 -0
  68. package/dist/admin/ui.generated.d.ts.map +1 -0
  69. package/dist/admin/ui.generated.js +43 -0
  70. package/dist/admin/ui.generated.js.map +1 -0
  71. package/dist/apps/activities.d.ts +18725 -0
  72. package/dist/apps/activities.d.ts.map +1 -0
  73. package/dist/apps/activities.js +3138 -0
  74. package/dist/apps/activities.js.map +1 -0
  75. package/dist/apps/addresses.d.ts +3662 -0
  76. package/dist/apps/addresses.d.ts.map +1 -0
  77. package/dist/apps/addresses.js +22 -0
  78. package/dist/apps/addresses.js.map +1 -0
  79. package/dist/apps/balances.d.ts +342 -0
  80. package/dist/apps/balances.d.ts.map +1 -0
  81. package/dist/apps/balances.js +393 -0
  82. package/dist/apps/balances.js.map +1 -0
  83. package/dist/apps/blocks.d.ts +931 -0
  84. package/dist/apps/blocks.d.ts.map +1 -0
  85. package/dist/apps/blocks.js +689 -0
  86. package/dist/apps/blocks.js.map +1 -0
  87. package/dist/apps/coingecko.d.ts +1053 -0
  88. package/dist/apps/coingecko.d.ts.map +1 -0
  89. package/dist/apps/coingecko.js +1015 -0
  90. package/dist/apps/coingecko.js.map +1 -0
  91. package/dist/apps/exchanges.d.ts +3000 -0
  92. package/dist/apps/exchanges.d.ts.map +1 -0
  93. package/dist/apps/exchanges.js +3570 -0
  94. package/dist/apps/exchanges.js.map +1 -0
  95. package/dist/apps/fee-amm.d.ts +640 -0
  96. package/dist/apps/fee-amm.d.ts.map +1 -0
  97. package/dist/apps/fee-amm.js +815 -0
  98. package/dist/apps/fee-amm.js.map +1 -0
  99. package/dist/apps/indexer.d.ts +116 -0
  100. package/dist/apps/indexer.d.ts.map +1 -0
  101. package/dist/apps/indexer.js +270 -0
  102. package/dist/apps/indexer.js.map +1 -0
  103. package/dist/apps/receipts.d.ts +1438 -0
  104. package/dist/apps/receipts.d.ts.map +1 -0
  105. package/dist/apps/receipts.js +874 -0
  106. package/dist/apps/receipts.js.map +1 -0
  107. package/dist/apps/rpc.d.ts +173 -0
  108. package/dist/apps/rpc.d.ts.map +1 -0
  109. package/dist/apps/rpc.js +191 -0
  110. package/dist/apps/rpc.js.map +1 -0
  111. package/dist/apps/tokenlist.d.ts +192 -0
  112. package/dist/apps/tokenlist.d.ts.map +1 -0
  113. package/dist/apps/tokenlist.js +153 -0
  114. package/dist/apps/tokenlist.js.map +1 -0
  115. package/dist/apps/tokens.d.ts +2246 -0
  116. package/dist/apps/tokens.d.ts.map +1 -0
  117. package/dist/apps/tokens.js +1789 -0
  118. package/dist/apps/tokens.js.map +1 -0
  119. package/dist/apps/transactions.d.ts +2327 -0
  120. package/dist/apps/transactions.d.ts.map +1 -0
  121. package/dist/apps/transactions.js +975 -0
  122. package/dist/apps/transactions.js.map +1 -0
  123. package/dist/apps/transfers.d.ts +589 -0
  124. package/dist/apps/transfers.d.ts.map +1 -0
  125. package/dist/apps/transfers.js +769 -0
  126. package/dist/apps/transfers.js.map +1 -0
  127. package/dist/apps/verified-tokens.d.ts +544 -0
  128. package/dist/apps/verified-tokens.d.ts.map +1 -0
  129. package/dist/apps/verified-tokens.js +234 -0
  130. package/dist/apps/verified-tokens.js.map +1 -0
  131. package/dist/apps/webhooks.d.ts +7684 -0
  132. package/dist/apps/webhooks.d.ts.map +1 -0
  133. package/dist/apps/webhooks.js +2112 -0
  134. package/dist/apps/webhooks.js.map +1 -0
  135. package/dist/cloudflare.d.ts +32 -0
  136. package/dist/cloudflare.d.ts.map +1 -0
  137. package/dist/cloudflare.js +45 -0
  138. package/dist/cloudflare.js.map +1 -0
  139. package/dist/index.d.ts +16 -0
  140. package/dist/index.d.ts.map +1 -0
  141. package/dist/index.js +16 -0
  142. package/dist/index.js.map +1 -0
  143. package/dist/internal/Auth.d.ts +298 -0
  144. package/dist/internal/Auth.d.ts.map +1 -0
  145. package/dist/internal/Auth.js +618 -0
  146. package/dist/internal/Auth.js.map +1 -0
  147. package/dist/internal/Cache.d.ts +74 -0
  148. package/dist/internal/Cache.d.ts.map +1 -0
  149. package/dist/internal/Cache.js +215 -0
  150. package/dist/internal/Cache.js.map +1 -0
  151. package/dist/internal/Cursor.d.ts +79 -0
  152. package/dist/internal/Cursor.d.ts.map +1 -0
  153. package/dist/internal/Cursor.js +116 -0
  154. package/dist/internal/Cursor.js.map +1 -0
  155. package/dist/internal/EdgeCache.d.ts +59 -0
  156. package/dist/internal/EdgeCache.d.ts.map +1 -0
  157. package/dist/internal/EdgeCache.js +174 -0
  158. package/dist/internal/EdgeCache.js.map +1 -0
  159. package/dist/internal/Log.d.ts +104 -0
  160. package/dist/internal/Log.d.ts.map +1 -0
  161. package/dist/internal/Log.js +118 -0
  162. package/dist/internal/Log.js.map +1 -0
  163. package/dist/internal/MetricSink.d.ts +65 -0
  164. package/dist/internal/MetricSink.d.ts.map +1 -0
  165. package/dist/internal/MetricSink.js +146 -0
  166. package/dist/internal/MetricSink.js.map +1 -0
  167. package/dist/internal/Mpp.d.ts +71 -0
  168. package/dist/internal/Mpp.d.ts.map +1 -0
  169. package/dist/internal/Mpp.js +105 -0
  170. package/dist/internal/Mpp.js.map +1 -0
  171. package/dist/internal/OpenApi.d.ts +313 -0
  172. package/dist/internal/OpenApi.d.ts.map +1 -0
  173. package/dist/internal/OpenApi.fonts.d.ts +2 -0
  174. package/dist/internal/OpenApi.fonts.d.ts.map +1 -0
  175. package/dist/internal/OpenApi.fonts.js +39 -0
  176. package/dist/internal/OpenApi.fonts.js.map +1 -0
  177. package/dist/internal/OpenApi.js +335 -0
  178. package/dist/internal/OpenApi.js.map +1 -0
  179. package/dist/internal/OpenApi.logo.d.ts +6 -0
  180. package/dist/internal/OpenApi.logo.d.ts.map +1 -0
  181. package/dist/internal/OpenApi.logo.js +19 -0
  182. package/dist/internal/OpenApi.logo.js.map +1 -0
  183. package/dist/internal/Path.d.ts +5 -0
  184. package/dist/internal/Path.d.ts.map +1 -0
  185. package/dist/internal/Path.js +16 -0
  186. package/dist/internal/Path.js.map +1 -0
  187. package/dist/internal/RateLimit.d.ts +60 -0
  188. package/dist/internal/RateLimit.d.ts.map +1 -0
  189. package/dist/internal/RateLimit.js +48 -0
  190. package/dist/internal/RateLimit.js.map +1 -0
  191. package/dist/internal/Response.d.ts +132 -0
  192. package/dist/internal/Response.d.ts.map +1 -0
  193. package/dist/internal/Response.js +72 -0
  194. package/dist/internal/Response.js.map +1 -0
  195. package/dist/internal/Schema.d.ts +257 -0
  196. package/dist/internal/Schema.d.ts.map +1 -0
  197. package/dist/internal/Schema.js +360 -0
  198. package/dist/internal/Schema.js.map +1 -0
  199. package/dist/internal/Store.d.ts +204 -0
  200. package/dist/internal/Store.d.ts.map +1 -0
  201. package/dist/internal/Store.js +291 -0
  202. package/dist/internal/Store.js.map +1 -0
  203. package/dist/internal/Tidx.d.ts +120 -0
  204. package/dist/internal/Tidx.d.ts.map +1 -0
  205. package/dist/internal/Tidx.js +156 -0
  206. package/dist/internal/Tidx.js.map +1 -0
  207. package/dist/internal/Timing.d.ts +22 -0
  208. package/dist/internal/Timing.d.ts.map +1 -0
  209. package/dist/internal/Timing.js +32 -0
  210. package/dist/internal/Timing.js.map +1 -0
  211. package/dist/internal/Ttl.d.ts +25 -0
  212. package/dist/internal/Ttl.d.ts.map +1 -0
  213. package/dist/internal/Ttl.js +49 -0
  214. package/dist/internal/Ttl.js.map +1 -0
  215. package/dist/internal/Value.d.ts +47 -0
  216. package/dist/internal/Value.d.ts.map +1 -0
  217. package/dist/internal/Value.js +122 -0
  218. package/dist/internal/Value.js.map +1 -0
  219. package/dist/internal/VerifiedTokens.d.ts +228 -0
  220. package/dist/internal/VerifiedTokens.d.ts.map +1 -0
  221. package/dist/internal/VerifiedTokens.js +450 -0
  222. package/dist/internal/VerifiedTokens.js.map +1 -0
  223. package/dist/internal/Viem.d.ts +2082 -0
  224. package/dist/internal/Viem.d.ts.map +1 -0
  225. package/dist/internal/Viem.js +63 -0
  226. package/dist/internal/Viem.js.map +1 -0
  227. package/dist/internal/WebhookDestination.d.ts +198 -0
  228. package/dist/internal/WebhookDestination.d.ts.map +1 -0
  229. package/dist/internal/WebhookDestination.js +170 -0
  230. package/dist/internal/WebhookDestination.js.map +1 -0
  231. package/dist/internal/Webhooks.d.ts +433 -0
  232. package/dist/internal/Webhooks.d.ts.map +1 -0
  233. package/dist/internal/Webhooks.js +489 -0
  234. package/dist/internal/Webhooks.js.map +1 -0
  235. package/dist/internal/webhookDestinations/betterStack.d.ts +26 -0
  236. package/dist/internal/webhookDestinations/betterStack.d.ts.map +1 -0
  237. package/dist/internal/webhookDestinations/betterStack.js +76 -0
  238. package/dist/internal/webhookDestinations/betterStack.js.map +1 -0
  239. package/dist/internal/webhookDestinations/slack.d.ts +59 -0
  240. package/dist/internal/webhookDestinations/slack.d.ts.map +1 -0
  241. package/dist/internal/webhookDestinations/slack.js +422 -0
  242. package/dist/internal/webhookDestinations/slack.js.map +1 -0
  243. package/dist/internal/webhookDestinations/url.d.ts +17 -0
  244. package/dist/internal/webhookDestinations/url.d.ts.map +1 -0
  245. package/dist/internal/webhookDestinations/url.js +130 -0
  246. package/dist/internal/webhookDestinations/url.js.map +1 -0
  247. package/dist/openrpc.json +5302 -0
  248. package/package.json +46 -4
  249. package/seeds/31318/icons/0x20c0000000000000000000000000000000000000.svg +1 -0
  250. package/seeds/31318/icons/0x20c0000000000000000000000000000000000001.svg +1 -0
  251. package/seeds/31318/icons/0x20c0000000000000000000000000000000000002.svg +1 -0
  252. package/seeds/31318/icons/0x20c0000000000000000000000000000000000003.svg +1 -0
  253. package/seeds/31318/verified-tokens.json +30 -0
  254. package/seeds/4217/icons/0x20c0000000000000000000000000000000000000.svg +25 -0
  255. package/seeds/4217/icons/0x20c000000000000000000000048c8f36df1c9a4a.svg +1 -0
  256. package/seeds/4217/icons/0x20c0000000000000000000000520792dcccccccc.svg +1 -0
  257. package/seeds/4217/icons/0x20c0000000000000000000000a6da882d075a4c3.svg +4 -0
  258. package/seeds/4217/icons/0x20c000000000000000000000111111111e910f0f.svg +30 -0
  259. package/seeds/4217/icons/0x20c00000000000000000000014f22ca97301eb73.svg +1 -0
  260. package/seeds/4217/icons/0x20c0000000000000000000001621e21f71cf12fb.svg +1 -0
  261. package/seeds/4217/icons/0x20c0000000000000000000002f52d5cc21a3207b.svg +1 -0
  262. package/seeds/4217/icons/0x20c0000000000000000000003158081efd85bfc2.svg +11 -0
  263. package/seeds/4217/icons/0x20c0000000000000000000003554d28269e0f3c2.svg +1 -0
  264. package/seeds/4217/icons/0x20c000000000000000000000383a23bacb546ab9.svg +1 -0
  265. package/seeds/4217/icons/0x20c00000000000000000000042109aef2f8b28e1.png +0 -0
  266. package/seeds/4217/icons/0x20c0000000000000000000005c0bac7cef389a11.svg +1 -0
  267. package/seeds/4217/icons/0x20c0000000000000000000006fd9a167923ba194.png +0 -0
  268. package/seeds/4217/icons/0x20c0000000000000000000007f7ba549dd0251b9.svg +1 -0
  269. package/seeds/4217/icons/0x20c0000000000000000000008191667423f70e67.svg +5 -0
  270. package/seeds/4217/icons/0x20c0000000000000000000008ee4fcff88888888.svg +1 -0
  271. package/seeds/4217/icons/0x20c0000000000000000000009a4a4b17e0dc6651.svg +1 -0
  272. package/seeds/4217/icons/0x20c000000000000000000000ab02d39df30bd17e.svg +1 -0
  273. package/seeds/4217/icons/0x20c000000000000000000000ae247a1130450f09.svg +1 -0
  274. package/seeds/4217/icons/0x20c000000000000000000000aeed2ec36a54d0e5.svg +1 -0
  275. package/seeds/4217/icons/0x20c000000000000000000000b9537d11c60e8b50.svg +1 -0
  276. package/seeds/4217/icons/0x20c000000000000000000000bd95bfb69fbe6ce3.svg +1 -0
  277. package/seeds/4217/icons/0x20c000000000000000000000c412ec89d0c08be5.svg +1 -0
  278. package/seeds/4217/icons/0x20c000000000000000000000f047dd7018e50367.png +0 -0
  279. package/seeds/4217/verified-tokens.json +177 -0
  280. package/seeds/42431/icons/0x20c0000000000000000000000000000000000000.svg +25 -0
  281. package/seeds/42431/icons/0x20c0000000000000000000000000000000000001.svg +1 -0
  282. package/seeds/42431/icons/0x20c0000000000000000000000000000000000002.svg +1 -0
  283. package/seeds/42431/icons/0x20c0000000000000000000000000000000000003.svg +1 -0
  284. package/seeds/42431/icons/0x20c0000000000000000000009e8d7eb59b783726.svg +1 -0
  285. package/seeds/42431/icons/0x20c000000000000000000000d72572838bbee59c.svg +1 -0
  286. package/seeds/42431/verified-tokens.json +44 -0
  287. package/seeds/openrpc/README.md +27 -0
  288. package/seeds/openrpc/execution-apis.json +8989 -0
  289. package/src/ApiKey.test.ts +91 -0
  290. package/src/ApiKey.ts +122 -0
  291. package/src/ApiKeySource.test-d.ts +11 -0
  292. package/src/ApiKeySource.test.ts +295 -0
  293. package/src/ApiKeySource.ts +303 -0
  294. package/src/App.test-d.ts +38 -0
  295. package/src/App.test.ts +65927 -0
  296. package/src/App.ts +855 -0
  297. package/src/Assets.ts +58 -0
  298. package/src/Auth.ts +1 -0
  299. package/src/Cli.test.ts +81 -0
  300. package/src/Cli.ts +38 -0
  301. package/src/Client.test-d.ts +1438 -0
  302. package/src/Client.test.ts +134 -0
  303. package/src/Client.ts +45 -0
  304. package/src/Metrics.ts +154 -0
  305. package/src/Scope.ts +50 -0
  306. package/src/Webhooks.test.ts +49 -0
  307. package/src/Webhooks.ts +172 -0
  308. package/src/admin/Access.test.ts +130 -0
  309. package/src/admin/Access.ts +87 -0
  310. package/src/admin/App.test.ts +225 -0
  311. package/src/admin/App.ts +216 -0
  312. package/src/admin/apps/api-keys.test.ts +156 -0
  313. package/src/admin/apps/api-keys.ts +157 -0
  314. package/src/admin/apps/scopes.test.ts +52 -0
  315. package/src/admin/apps/scopes.ts +47 -0
  316. package/src/admin/apps/verified-tokens.test.ts +415 -0
  317. package/src/admin/apps/verified-tokens.ts +566 -0
  318. package/src/admin/index.ts +2 -0
  319. package/src/admin/ui/plugin.ts +135 -0
  320. package/src/admin/ui/src/components/Checkbox.tsx +22 -0
  321. package/src/admin/ui/src/components/Dialog.tsx +32 -0
  322. package/src/admin/ui/src/components/MobileProfile.tsx +31 -0
  323. package/src/admin/ui/src/components/Page.tsx +53 -0
  324. package/src/admin/ui/src/components/TempoLogo.tsx +37 -0
  325. package/src/admin/ui/src/fonts/Pilat-Demi.woff2 +0 -0
  326. package/src/admin/ui/src/fonts/Pilat-Regular.woff2 +0 -0
  327. package/src/admin/ui/src/lib/api-keys.ts +63 -0
  328. package/src/admin/ui/src/lib/client.ts +15 -0
  329. package/src/admin/ui/src/lib/config.ts +57 -0
  330. package/src/admin/ui/src/lib/query-client.ts +26 -0
  331. package/src/admin/ui/src/lib/scopes.ts +25 -0
  332. package/src/admin/ui/src/lib/verified-tokens.ts +89 -0
  333. package/src/admin/ui/src/lib/wagmi.ts +18 -0
  334. package/src/admin/ui/src/routeTree.gen.ts +104 -0
  335. package/src/admin/ui/src/router.tsx +25 -0
  336. package/src/admin/ui/src/routes/__root.tsx +219 -0
  337. package/src/admin/ui/src/routes/api-keys.tsx +329 -0
  338. package/src/admin/ui/src/routes/index.tsx +7 -0
  339. package/src/admin/ui/src/routes/verified-tokens.tsx +331 -0
  340. package/src/admin/ui/src/styles.css +20 -0
  341. package/src/admin/ui/tsconfig.json +16 -0
  342. package/src/admin/ui/vite.config.ts +29 -0
  343. package/src/admin/ui.generated.ts +60 -0
  344. package/src/apps/activities.test.ts +1020 -0
  345. package/src/apps/activities.ts +3934 -0
  346. package/src/apps/addresses.ts +24 -0
  347. package/src/apps/balances.test.ts +274 -0
  348. package/src/apps/balances.ts +533 -0
  349. package/src/apps/blocks.test.ts +385 -0
  350. package/src/apps/blocks.ts +896 -0
  351. package/src/apps/coingecko.test.ts +218 -0
  352. package/src/apps/coingecko.ts +1383 -0
  353. package/src/apps/exchanges.test.ts +958 -0
  354. package/src/apps/exchanges.ts +4773 -0
  355. package/src/apps/fee-amm.test.ts +321 -0
  356. package/src/apps/fee-amm.ts +1121 -0
  357. package/src/apps/indexer.test.ts +153 -0
  358. package/src/apps/indexer.ts +330 -0
  359. package/src/apps/receipts.test.ts +285 -0
  360. package/src/apps/receipts.ts +1193 -0
  361. package/src/apps/rpc.test.ts +302 -0
  362. package/src/apps/rpc.ts +222 -0
  363. package/src/apps/tokenlist.test.ts +80 -0
  364. package/src/apps/tokenlist.ts +207 -0
  365. package/src/apps/tokens.test.ts +1062 -0
  366. package/src/apps/tokens.ts +2292 -0
  367. package/src/apps/transactions.test.ts +408 -0
  368. package/src/apps/transactions.ts +1363 -0
  369. package/src/apps/transfers.test.ts +371 -0
  370. package/src/apps/transfers.ts +1116 -0
  371. package/src/apps/verified-tokens.test.ts +114 -0
  372. package/src/apps/verified-tokens.ts +295 -0
  373. package/src/apps/webhooks.test.ts +1937 -0
  374. package/src/apps/webhooks.ts +2926 -0
  375. package/src/cloudflare.ts +55 -0
  376. package/src/index.ts +15 -0
  377. package/src/internal/Auth.test.ts +980 -0
  378. package/src/internal/Auth.ts +1012 -0
  379. package/src/internal/Cache.test.ts +240 -0
  380. package/src/internal/Cache.ts +262 -0
  381. package/src/internal/Cursor.test.ts +100 -0
  382. package/src/internal/Cursor.ts +149 -0
  383. package/src/internal/EdgeCache.test.ts +234 -0
  384. package/src/internal/EdgeCache.ts +224 -0
  385. package/src/internal/Errors.ts +14 -0
  386. package/src/internal/Log.test.ts +244 -0
  387. package/src/internal/Log.ts +216 -0
  388. package/src/internal/MetricSink.ts +220 -0
  389. package/src/internal/Mpp.test.ts +64 -0
  390. package/src/internal/Mpp.ts +141 -0
  391. package/src/internal/OpenApi.fonts.ts +38 -0
  392. package/src/internal/OpenApi.logo.ts +18 -0
  393. package/src/internal/OpenApi.ts +488 -0
  394. package/src/internal/Path.test.ts +14 -0
  395. package/src/internal/Path.ts +15 -0
  396. package/src/internal/RateLimit.test.ts +93 -0
  397. package/src/internal/RateLimit.ts +93 -0
  398. package/src/internal/Response.ts +131 -0
  399. package/src/internal/Schema.test.ts +34 -0
  400. package/src/internal/Schema.ts +523 -0
  401. package/src/internal/Store.test.ts +545 -0
  402. package/src/internal/Store.ts +526 -0
  403. package/src/internal/Tidx.local.test.ts +33 -0
  404. package/src/internal/Tidx.test.ts +294 -0
  405. package/src/internal/Tidx.ts +238 -0
  406. package/src/internal/Timing.test.ts +22 -0
  407. package/src/internal/Timing.ts +62 -0
  408. package/src/internal/Ttl.test.ts +18 -0
  409. package/src/internal/Ttl.ts +59 -0
  410. package/src/internal/Value.ts +114 -0
  411. package/src/internal/VerifiedTokens.test.ts +333 -0
  412. package/src/internal/VerifiedTokens.ts +645 -0
  413. package/src/internal/Viem.test.ts +115 -0
  414. package/src/internal/Viem.ts +165 -0
  415. package/src/internal/WebhookDestination.test.ts +648 -0
  416. package/src/internal/WebhookDestination.ts +331 -0
  417. package/src/internal/Webhooks.test.ts +668 -0
  418. package/src/internal/Webhooks.ts +940 -0
  419. package/src/internal/webhookDestinations/betterStack.ts +91 -0
  420. package/src/internal/webhookDestinations/slack.ts +496 -0
  421. package/src/internal/webhookDestinations/url.ts +128 -0
  422. package/src/openrpc.json +5302 -0
@@ -0,0 +1,958 @@
1
+ import type * as z from 'zod/mini'
2
+ import { Schema } from 'cadent'
3
+
4
+ import * as TestApp from '../../test/App.js'
5
+ import * as Exchanges from './exchanges.js'
6
+
7
+ type OrdersResponse = z.output<typeof Exchanges.schema.getOrders.Response>
8
+ type PairsResponse = z.output<typeof Exchanges.schema.getPairs.Response>
9
+
10
+ type Client = ReturnType<typeof TestApp.client>
11
+ type OrdersQuery = {
12
+ chainId?: string
13
+ cursor?: string
14
+ include?: string
15
+ limit?: string
16
+ maker?: string
17
+ order?: 'asc' | 'desc'
18
+ side?: 'bid' | 'ask'
19
+ sort?: 'tick' | 'time'
20
+ }
21
+
22
+ /**
23
+ * Find a real DEX pair that currently has at least one resting order. The
24
+ * exchange's small pair set means most pairs have empty books at any given
25
+ * moment, so we scan the pairs listing until one returns orders for the
26
+ * supplied query (scoped via `?base=`). Returns `null` when *every* scanned
27
+ * pair returns a 200 with an empty page, which is rare but valid on quiet
28
+ * testnets — callers should skip the assertion rather than fail.
29
+ *
30
+ * A non-200 response is treated as a hard failure: it surfaces real bugs (such
31
+ * as upstream schema mismatches) instead of letting them silently degrade to a
32
+ * skipped test. Empty 200 responses are tolerated and counted, with a final
33
+ * `expect` ensuring at least one pair was scanned so a 0-pair listing also
34
+ * fails loudly.
35
+ */
36
+ async function findPairWithOrders(
37
+ client: Client,
38
+ query: OrdersQuery = {},
39
+ ): Promise<{ body: OrdersResponse; pair: PairsResponse['data'][number] } | null> {
40
+ const pairsResponse = await client.v1.exchange.pairs.$get(
41
+ { query: { limit: '10' } },
42
+ TestApp.auth,
43
+ )
44
+ const pairs = await TestApp.json(pairsResponse, Exchanges.schema.getPairs.Response)
45
+ expect(pairs.data.length).toBeGreaterThan(0)
46
+ for (const pair of pairs.data) {
47
+ const response = await client.v1.exchange.orders.$get(
48
+ { query: { ...query, base: pair.base.address } },
49
+ TestApp.auth,
50
+ )
51
+ // A 5xx from the orders endpoint indicates a real regression (e.g. an
52
+ // upstream schema mismatch). Fail fast with the error body for context
53
+ // instead of silently skipping the pair.
54
+ if (response.status !== 200) {
55
+ const body = await response.text()
56
+ throw new Error(`orders ${response.status} for base=${pair.base.address}: ${body}`)
57
+ }
58
+ const body = await TestApp.json(response, Exchanges.schema.getOrders.Response)
59
+ if (body.data.length > 0) return { body, pair }
60
+ }
61
+ return null
62
+ }
63
+
64
+ /**
65
+ * Convenience wrapper around `findPairWithOrders` for tests that only care
66
+ * about the response body under a given query. Returns `null` when no pair
67
+ * had orders.
68
+ */
69
+ async function fetchOrdersForBookedPair(query: OrdersQuery): Promise<OrdersResponse | null> {
70
+ const found = await findPairWithOrders(TestApp.client(), query)
71
+ return found?.body ?? null
72
+ }
73
+
74
+ describe('GET /exchange/pairs', () => {
75
+ test('returns a page of trading pairs ordered newest-first by default', async () => {
76
+ const client = TestApp.client()
77
+ const response = await client.v1.exchange.pairs.$get({ query: { limit: '5' } }, TestApp.auth)
78
+ const body = await TestApp.json(response, Exchanges.schema.getPairs.Response)
79
+
80
+ expect(response.status).toBe(200)
81
+ expect(body.data.length).toBeLessThanOrEqual(5)
82
+ if (body.data.length > 0) {
83
+ const pair = body.data[0]!
84
+ expect(pair.key).toMatch(/^0x[0-9a-f]{64}$/)
85
+ expect(pair.base.address).toMatch(/^0x[0-9a-f]{40}$/)
86
+ expect(pair.quote.address).toMatch(/^0x[0-9a-f]{40}$/)
87
+ // Newest-first by default.
88
+ for (let i = 1; i < body.data.length; i++) {
89
+ expect(body.data[i - 1]!.blockNumber).toBeGreaterThanOrEqual(body.data[i]!.blockNumber)
90
+ }
91
+ }
92
+ }, 30000)
93
+
94
+ test('embeds an exact total count on demand via include=totalCount', async () => {
95
+ const client = TestApp.client()
96
+ const response = await client.v1.exchange.pairs.$get(
97
+ { query: { include: 'totalCount', limit: '5' } },
98
+ TestApp.auth,
99
+ )
100
+ const body = await TestApp.json(response, Exchanges.schema.getPairs.Response)
101
+
102
+ expect(response.status).toBe(200)
103
+ expect(response.headers.get('server-timing')?.includes('pairs_count;dur=')).toBe(true)
104
+ expect(body.meta).toBeDefined()
105
+ expect(Number.isInteger(body.meta?.totalCount)).toBe(true)
106
+ expect(body.meta!.totalCount).toBeGreaterThanOrEqual(body.data.length)
107
+ // The count is sort-key-pruned on ClickHouse, so it is exact (never capped).
108
+ expect(body.meta!.totalCountCapped).toBe(false)
109
+
110
+ const bare = await client.v1.exchange.pairs.$get({ query: { limit: '5' } }, TestApp.auth)
111
+ const bareBody = await TestApp.json(bare, Exchanges.schema.getPairs.Response)
112
+ expect(bare.status).toBe(200)
113
+ expect(bareBody.meta).toBeUndefined()
114
+ }, 30000)
115
+
116
+ test('serves a bounded positional page via `page`', async () => {
117
+ const client = TestApp.client()
118
+ // Ascending creation order anchors the feed at the first pair, so
119
+ // positional pages are deterministic against the head page.
120
+ const headResponse = await client.v1.exchange.pairs.$get(
121
+ { query: { limit: '10', order: 'asc' } },
122
+ TestApp.auth,
123
+ )
124
+ const head = await TestApp.json(headResponse, Exchanges.schema.getPairs.Response)
125
+ expect(headResponse.status).toBe(200)
126
+ if (head.data.length < 10) return
127
+
128
+ const offsetResponse = await client.v1.exchange.pairs.$get(
129
+ { query: { limit: '5', order: 'asc', page: '2' } },
130
+ TestApp.auth,
131
+ )
132
+ const page = await TestApp.json(offsetResponse, Exchanges.schema.getPairs.Response)
133
+ expect(offsetResponse.status).toBe(200)
134
+ expect(page.data.map((pair) => pair.key)).toEqual(
135
+ head.data.slice(5, 10).map((pair) => pair.key),
136
+ )
137
+ }, 30000)
138
+
139
+ test('embeds token metadata for both sides when requested via `include=tokens`', async () => {
140
+ const client = TestApp.client()
141
+ const response = await client.v1.exchange.pairs.$get(
142
+ { query: { limit: '5', include: 'tokens' } },
143
+ TestApp.auth,
144
+ )
145
+ const body = await TestApp.json(response, Exchanges.schema.getPairs.Response)
146
+
147
+ expect(response.status).toBe(200)
148
+ if (body.data.length === 0) return
149
+ const enriched = body.data.find((pair) => typeof pair.base.symbol === 'string')
150
+ // At least one row should resolve metadata against the live RPC; both sides
151
+ // share the same metadata path so when `base.symbol` is present, so is the
152
+ // rest of the trimmed token reference.
153
+ if (enriched) {
154
+ expect(typeof enriched.base.symbol).toBe('string')
155
+ expect(typeof enriched.base.decimals).toBe('number')
156
+ expect(typeof enriched.quote.symbol).toBe('string')
157
+ }
158
+ }, 60000)
159
+
160
+ test('ranks pairs by DEX-escrow base balance when `sort=liquidity`', async () => {
161
+ const client = TestApp.client()
162
+ const response = await client.v1.exchange.pairs.$get(
163
+ { query: { limit: '5', sort: 'liquidity' } },
164
+ TestApp.auth,
165
+ )
166
+ const body = await TestApp.json(response, Exchanges.schema.getPairs.Response)
167
+
168
+ expect(response.status).toBe(200)
169
+ expect(body.data.length).toBeLessThanOrEqual(5)
170
+ if (body.data.length > 0) {
171
+ for (const pair of body.data) {
172
+ expect(typeof pair.liquidity).toBe('string')
173
+ expect(pair.liquidity).toMatch(/^\d+$/)
174
+ expect(BigInt(pair.liquidity!)).toBeGreaterThan(0n)
175
+ }
176
+ // Newest-first balance ordering.
177
+ for (let i = 1; i < body.data.length; i++) {
178
+ expect(BigInt(body.data[i - 1]!.liquidity!)).toBeGreaterThanOrEqual(
179
+ BigInt(body.data[i]!.liquidity!),
180
+ )
181
+ }
182
+ }
183
+ }, 30000)
184
+ })
185
+
186
+ describe('GET /exchange/pairs/:base', () => {
187
+ test('returns the pair identified by its base token', async () => {
188
+ const client = TestApp.client()
189
+ const pairsResponse = await client.v1.exchange.pairs.$get(
190
+ { query: { limit: '5' } },
191
+ TestApp.auth,
192
+ )
193
+ const pairs = await TestApp.json(pairsResponse, Exchanges.schema.getPairs.Response)
194
+ if (pairs.data.length === 0) return
195
+ const pair = pairs.data[0]!
196
+
197
+ const response = await client.v1.exchange.pairs[':base'].$get(
198
+ { param: { base: pair.base.address }, query: {} },
199
+ TestApp.auth,
200
+ )
201
+ expect(response.status).toBe(200)
202
+ const body = await TestApp.json(response, Exchanges.schema.getPair.Response)
203
+
204
+ expect(body.key).toBe(pair.key)
205
+ expect(body.base.address).toBe(pair.base.address)
206
+ expect(body.quote.address).toBe(pair.quote.address)
207
+ expect(Number.isInteger(body.blockNumber)).toBe(true)
208
+ expect(body.transactionHash).toMatch(/^0x[0-9a-f]{64}$/)
209
+ expect(Number.isNaN(Date.parse(body.timestamp))).toBe(false)
210
+ // Token metadata is opt-in: the base response carries bare addresses.
211
+ expect(body.base.symbol).toBeUndefined()
212
+ }, 30000)
213
+
214
+ test('embeds token metadata when requested via `include=tokens`', async () => {
215
+ const client = TestApp.client()
216
+ const pairsResponse = await client.v1.exchange.pairs.$get(
217
+ { query: { limit: '5' } },
218
+ TestApp.auth,
219
+ )
220
+ const pairs = await TestApp.json(pairsResponse, Exchanges.schema.getPairs.Response)
221
+ if (pairs.data.length === 0) return
222
+ const pair = pairs.data[0]!
223
+
224
+ const response = await client.v1.exchange.pairs[':base'].$get(
225
+ { param: { base: pair.base.address }, query: { include: 'tokens' } },
226
+ TestApp.auth,
227
+ )
228
+ expect(response.status).toBe(200)
229
+ const body = await TestApp.json(response, Exchanges.schema.getPair.Response)
230
+ // Best-effort: at least one side should resolve metadata for a real pair.
231
+ const sides = [body.base, body.quote]
232
+ expect(sides.some((side) => typeof side.decimals === 'number')).toBe(true)
233
+ }, 60000)
234
+
235
+ test('returns 404 for an unknown base token', async () => {
236
+ const client = TestApp.client()
237
+ const response = await client.v1.exchange.pairs[':base'].$get(
238
+ { param: { base: `0x${'1'.repeat(40)}` as const }, query: {} },
239
+ TestApp.auth,
240
+ )
241
+ expect(response.status).toBe(404)
242
+ }, 30000)
243
+ })
244
+
245
+ describe('GET /exchange/pairs/:base/ohlc', () => {
246
+ test('returns OHLC buckets for a real pair', async () => {
247
+ const client = TestApp.client()
248
+ const pairsResponse = await client.v1.exchange.pairs.$get(
249
+ { query: { limit: '5' } },
250
+ TestApp.auth,
251
+ )
252
+ const pairs = await TestApp.json(pairsResponse, Exchanges.schema.getPairs.Response)
253
+ if (pairs.data.length === 0) return
254
+ const pair = pairs.data[0]!
255
+
256
+ const response = await client.v1.exchange.pairs[':base'].ohlc.$get(
257
+ { param: { base: pair.base.address }, query: { interval: '1h', window: '7d' } },
258
+ TestApp.auth,
259
+ )
260
+ expect(response.status).toBe(200)
261
+ const body = await TestApp.json(response, Exchanges.schema.getPairOhlc.Response)
262
+
263
+ expect(body.interval).toBe('1h')
264
+ expect(body.window).toBe('7d')
265
+ expect(body.base.address).toBe(pair.base.address)
266
+ expect(body.quote.address).toBe(pair.quote.address)
267
+ expect(typeof body.truncated).toBe('boolean')
268
+ expect(Array.isArray(body.data)).toBe(true)
269
+
270
+ // Buckets, if any, are ordered oldest → newest with strictly increasing
271
+ // timestamps and well-formed rate / volume shapes.
272
+ for (let i = 0; i < body.data.length; i++) {
273
+ const bucket = body.data[i]!
274
+ expect(bucket.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/)
275
+ expect(bucket.open).toMatch(/^\d+(\.\d+)?$/)
276
+ expect(bucket.close).toMatch(/^\d+(\.\d+)?$/)
277
+ expect(bucket.high).toMatch(/^\d+(\.\d+)?$/)
278
+ expect(bucket.low).toMatch(/^\d+(\.\d+)?$/)
279
+ expect(Number(bucket.high)).toBeGreaterThanOrEqual(Number(bucket.low))
280
+ expect(bucket.fillCount).toBeGreaterThan(0)
281
+ expect(BigInt(bucket.volume.base)).toBeGreaterThanOrEqual(0n)
282
+ expect(BigInt(bucket.volume.quote)).toBeGreaterThanOrEqual(0n)
283
+ if (i > 0) {
284
+ expect(Date.parse(bucket.timestamp)).toBeGreaterThan(
285
+ Date.parse(body.data[i - 1]!.timestamp),
286
+ )
287
+ }
288
+ }
289
+ }, 60000)
290
+
291
+ test('rejects OHLC request whose `interval × window` exceeds the bucket cap', async () => {
292
+ const client = TestApp.client()
293
+ const response = await client.v1.exchange.pairs[':base'].ohlc.$get(
294
+ {
295
+ // `1m` × `24h` = 1440 buckets, well above the 500-bucket cap.
296
+ param: { base: `0x${'1'.repeat(40)}` as const },
297
+ query: { interval: '1m', window: '24h' },
298
+ },
299
+ TestApp.auth,
300
+ )
301
+ expect(response.status).toBe(400)
302
+ }, 15000)
303
+
304
+ test('returns 404 OHLC for an unknown pair', async () => {
305
+ const client = TestApp.client()
306
+ const response = await client.v1.exchange.pairs[':base'].ohlc.$get(
307
+ { param: { base: `0x${'1'.repeat(40)}` as const }, query: {} },
308
+ TestApp.auth,
309
+ )
310
+ expect(response.status).toBe(404)
311
+ }, 30000)
312
+
313
+ test('embeds token metadata on OHLC when requested via `include=tokens`', async () => {
314
+ const client = TestApp.client()
315
+ const pairsResponse = await client.v1.exchange.pairs.$get(
316
+ { query: { limit: '5' } },
317
+ TestApp.auth,
318
+ )
319
+ const pairs = await TestApp.json(pairsResponse, Exchanges.schema.getPairs.Response)
320
+ if (pairs.data.length === 0) return
321
+ const pair = pairs.data[0]!
322
+
323
+ const response = await client.v1.exchange.pairs[':base'].ohlc.$get(
324
+ {
325
+ param: { base: pair.base.address },
326
+ query: { include: 'tokens', interval: '1h', window: '24h' },
327
+ },
328
+ TestApp.auth,
329
+ )
330
+ expect(response.status).toBe(200)
331
+ const body = await TestApp.json(response, Exchanges.schema.getPairOhlc.Response)
332
+ // Best-effort: at least one side should resolve metadata for a real pair.
333
+ const sides = [body.base, body.quote]
334
+ expect(sides.some((side) => typeof side.decimals === 'number')).toBe(true)
335
+ }, 60000)
336
+ })
337
+
338
+ describe('GET /exchange/orders?base=… (pair-scoped orders)', () => {
339
+ test('returns a page of resting orders for a real pair', async () => {
340
+ const client = TestApp.client()
341
+ const found = await findPairWithOrders(client, { limit: '5' })
342
+ if (!found) return
343
+ const { body, pair } = found
344
+
345
+ expect(body.data.length).toBeGreaterThan(0)
346
+ expect(body.data.length).toBeLessThanOrEqual(5)
347
+ expect(typeof body.truncated).toBe('boolean')
348
+ // `nextCursor` is `null` when the page exhausts the snapshot.
349
+ expect(body.nextCursor === null || typeof body.nextCursor === 'string').toBe(true)
350
+
351
+ for (const order of body.data) {
352
+ expect(order.orderId).toMatch(/^\d+$/)
353
+ expect(order.maker).toMatch(/^0x[0-9a-f]{40}$/)
354
+ expect(order.amount).toMatch(/^\d+$/)
355
+ expect(order.remaining).toMatch(/^\d+$/)
356
+ expect(BigInt(order.remaining)).toBeGreaterThan(0n)
357
+ expect(BigInt(order.remaining)).toBeLessThanOrEqual(BigInt(order.amount))
358
+ expect(['bid', 'ask']).toContain(order.side)
359
+ expect(Number.isInteger(order.tick)).toBe(true)
360
+ expect(order.rate).toMatch(/^\d+(\.\d+)?$/)
361
+ // `price` is quote-per-base regardless of side; `rate` is the
362
+ // taker-perspective ratio (price for bids, 1/price for asks). A tick=0
363
+ // order — peg — must render both as exactly 1.00000.
364
+ expect(order.price).toMatch(/^\d+(\.\d+)?$/)
365
+ if (order.tick === 0) {
366
+ expect(Number(order.price)).toBe(1)
367
+ expect(Number(order.rate)).toBe(1)
368
+ }
369
+ expect(order.transactionHash).toMatch(/^0x[0-9a-f]{64}$/)
370
+ expect(Number.isInteger(order.blockNumber)).toBe(true)
371
+ expect(order.blockNumber).toBeGreaterThanOrEqual(0)
372
+ expect(Number.isInteger(order.logIndex)).toBe(true)
373
+ expect(order.logIndex).toBeGreaterThanOrEqual(0)
374
+ expect(Number.isNaN(Date.parse(order.placedAt))).toBe(false)
375
+ // Every row is annotated with the pair we scoped to.
376
+ expect(order.pair.key).toBe(pair.key)
377
+ expect(order.pair.base.address).toBe(pair.base.address)
378
+ expect(order.pair.quote.address).toBe(pair.quote.address)
379
+ }
380
+ }, 120000)
381
+
382
+ test('sorts resting orders by tick descending by default', async () => {
383
+ const body = await fetchOrdersForBookedPair({ limit: '10' })
384
+ if (!body) return
385
+ for (let i = 1; i < body.data.length; i++)
386
+ expect(body.data[i - 1]!.tick).toBeGreaterThanOrEqual(body.data[i]!.tick)
387
+ }, 120000)
388
+
389
+ test('sorts resting orders by tick ascending when `order=asc`', async () => {
390
+ const body = await fetchOrdersForBookedPair({ limit: '10', order: 'asc' })
391
+ if (!body) return
392
+ for (let i = 1; i < body.data.length; i++)
393
+ expect(body.data[i - 1]!.tick).toBeLessThanOrEqual(body.data[i]!.tick)
394
+ }, 120000)
395
+
396
+ test('sorts resting orders by placement time when `sort=time`', async () => {
397
+ const body = await fetchOrdersForBookedPair({ limit: '10', sort: 'time' })
398
+ if (!body) return
399
+ for (let i = 1; i < body.data.length; i++) {
400
+ const previous = body.data[i - 1]!
401
+ const current = body.data[i]!
402
+ // Default `order=desc` against `sort=time`: newest placement first,
403
+ // tie-breaking on log index.
404
+ expect(
405
+ previous.blockNumber > current.blockNumber ||
406
+ (previous.blockNumber === current.blockNumber && previous.logIndex >= current.logIndex),
407
+ ).toBe(true)
408
+ }
409
+ }, 120000)
410
+
411
+ test('filters resting orders by `side`', async () => {
412
+ const bids = await fetchOrdersForBookedPair({ limit: '10', side: 'bid' })
413
+ if (bids) for (const order of bids.data) expect(order.side).toBe('bid')
414
+
415
+ // Asks are not always present alongside bids on the same pair, so this
416
+ // assertion runs against whichever pair happens to have ask orders.
417
+ const asks = await fetchOrdersForBookedPair({ limit: '10', side: 'ask' })
418
+ if (asks) for (const order of asks.data) expect(order.side).toBe('ask')
419
+ }, 180000)
420
+
421
+ test('filters resting orders to a single maker', async () => {
422
+ // Seed off the default-sort head page to discover a maker that has at
423
+ // least one resting order on a real pair, then filter to just them.
424
+ const seed = await findPairWithOrders(TestApp.client(), { limit: '5' })
425
+ if (!seed) return
426
+ const maker = seed.body.data[0]!.maker
427
+ const client = TestApp.client()
428
+ const response = await client.v1.exchange.orders.$get(
429
+ { query: { base: seed.pair.base.address, limit: '10', maker } },
430
+ TestApp.auth,
431
+ )
432
+ expect(response.status).toBe(200)
433
+ const body = await TestApp.json(response, Exchanges.schema.getOrders.Response)
434
+ expect(body.data.length).toBeGreaterThan(0)
435
+ for (const order of body.data) expect(order.maker).toBe(maker)
436
+ }, 120000)
437
+
438
+ test('embeds pair token metadata per row when requested via `include=tokens`', async () => {
439
+ const seed = await findPairWithOrders(TestApp.client(), {
440
+ include: 'tokens',
441
+ limit: '5',
442
+ })
443
+ if (!seed) return
444
+ const { body, pair } = seed
445
+ // At least one side resolves metadata against the live RPC; both sides
446
+ // share the same resolution path so this gates the embedding shape.
447
+ const row = body.data[0]!
448
+ expect(row.pair.base.address).toBe(pair.base.address)
449
+ expect(row.pair.quote.address).toBe(pair.quote.address)
450
+ const sides = [row.pair.base, row.pair.quote]
451
+ expect(sides.some((side) => typeof side.symbol === 'string')).toBe(true)
452
+ }, 120000)
453
+
454
+ test('paginates resting orders without skipping or duplicating rows', async () => {
455
+ const client = TestApp.client()
456
+ const seed = await findPairWithOrders(client, { limit: '5' })
457
+ if (!seed) return
458
+ if (seed.body.nextCursor === null) return
459
+
460
+ const next = await client.v1.exchange.orders.$get(
461
+ { query: { base: seed.pair.base.address, cursor: seed.body.nextCursor, limit: '5' } },
462
+ TestApp.auth,
463
+ )
464
+ expect(next.status).toBe(200)
465
+ const second = await TestApp.json(next, Exchanges.schema.getOrders.Response)
466
+ const firstIds = new Set(seed.body.data.map((order) => order.orderId))
467
+ for (const order of second.data) expect(firstIds.has(order.orderId)).toBe(false)
468
+ }, 180000)
469
+
470
+ test('returns 404 for an unknown pair on the orders endpoint', async () => {
471
+ const client = TestApp.client()
472
+ const response = await client.v1.exchange.orders.$get(
473
+ { query: { base: `0x${'1'.repeat(40)}` as const } },
474
+ TestApp.auth,
475
+ )
476
+ expect(response.status).toBe(404)
477
+ }, 30000)
478
+ })
479
+
480
+ describe('GET /exchange/pairs/:base/depth', () => {
481
+ test('returns orderbook depth for a real pair', async () => {
482
+ const client = TestApp.client()
483
+ const pairsResponse = await client.v1.exchange.pairs.$get(
484
+ { query: { limit: '5' } },
485
+ TestApp.auth,
486
+ )
487
+ const pairs = await TestApp.json(pairsResponse, Exchanges.schema.getPairs.Response)
488
+ if (pairs.data.length === 0) return
489
+
490
+ // The RPC fans out ~400 `getTickLevel` reads through a single deployless
491
+ // multicall; on testnet that occasionally exceeds the upstream RPC's
492
+ // multicall return-size budget and surfaces as 502 from our handler.
493
+ // Treat that as "skip" so the test stays green even when the upstream
494
+ // throttles the multicall response.
495
+ let body: z.output<typeof Exchanges.schema.getPairDepth.Response> | null = null
496
+ let pair: (typeof pairs.data)[number] | null = null
497
+ for (const candidate of pairs.data) {
498
+ const response = await client.v1.exchange.pairs[':base'].depth.$get(
499
+ { param: { base: candidate.base.address }, query: {} },
500
+ TestApp.auth,
501
+ )
502
+ if (response.status === 502) return
503
+ expect(response.status).toBe(200)
504
+ body = await TestApp.json(response, Exchanges.schema.getPairDepth.Response)
505
+ pair = candidate
506
+ if (body.bids.length > 0 || body.asks.length > 0) break
507
+ }
508
+ if (!body || !pair) return
509
+
510
+ expect(body.base.address).toBe(pair.base.address)
511
+ expect(body.quote.address).toBe(pair.quote.address)
512
+
513
+ // Asks are emitted lowest-tick first (best ask outward); bids are
514
+ // emitted highest-tick first (best bid outward). Per-row `cumulativeSize`
515
+ // must equal the running sum of `size` from the head of the same side.
516
+ const checkSide = (side: typeof body.asks, label: 'asks' | 'bids') => {
517
+ let expected = 0n
518
+ let previousTick: number | undefined
519
+ for (const level of side) {
520
+ expect(level.size).toMatch(/^\d+$/)
521
+ expect(level.price).toMatch(/^\d+(\.\d+)?$/)
522
+ expect(BigInt(level.size)).toBeGreaterThan(0n)
523
+ expected += BigInt(level.size)
524
+ expect(BigInt(level.cumulativeSize)).toBe(expected)
525
+ // Ticks are monotone outward from peg per side.
526
+ if (previousTick !== undefined) {
527
+ if (label === 'asks') expect(level.tick).toBeGreaterThan(previousTick)
528
+ else expect(level.tick).toBeLessThan(previousTick)
529
+ }
530
+ previousTick = level.tick
531
+ }
532
+ }
533
+ checkSide(body.asks, 'asks')
534
+ checkSide(body.bids, 'bids')
535
+ }, 60000)
536
+
537
+ test('caps depth levels per side when `levels` is set', async () => {
538
+ const client = TestApp.client()
539
+ const pairsResponse = await client.v1.exchange.pairs.$get(
540
+ { query: { limit: '5' } },
541
+ TestApp.auth,
542
+ )
543
+ const pairs = await TestApp.json(pairsResponse, Exchanges.schema.getPairs.Response)
544
+ if (pairs.data.length === 0) return
545
+
546
+ for (const pair of pairs.data) {
547
+ const response = await client.v1.exchange.pairs[':base'].depth.$get(
548
+ { param: { base: pair.base.address }, query: { levels: '3' } },
549
+ TestApp.auth,
550
+ )
551
+ if (response.status === 502) return
552
+ expect(response.status).toBe(200)
553
+ const body = await TestApp.json(response, Exchanges.schema.getPairDepth.Response)
554
+ expect(body.asks.length).toBeLessThanOrEqual(3)
555
+ expect(body.bids.length).toBeLessThanOrEqual(3)
556
+ }
557
+ }, 60000)
558
+
559
+ test('returns 404 for an unknown depth pair', async () => {
560
+ const client = TestApp.client()
561
+ const response = await client.v1.exchange.pairs[':base'].depth.$get(
562
+ { param: { base: `0x${'1'.repeat(40)}` as const }, query: {} },
563
+ TestApp.auth,
564
+ )
565
+ expect(response.status).toBe(404)
566
+ }, 30000)
567
+ })
568
+
569
+ describe('GET /exchange/swaps', () => {
570
+ type GlobalSwapsResponse = z.output<typeof Exchanges.schema.getSwaps.Response>
571
+
572
+ test('returns a global, newest-first page of swaps across all pairs', async () => {
573
+ const client = TestApp.client()
574
+ const response = await client.v1.exchange.swaps.$get({ query: { limit: '10' } }, TestApp.auth)
575
+ expect(response.status).toBe(200)
576
+ const body = await TestApp.json(response, Exchanges.schema.getSwaps.Response)
577
+ expect(body.data.length).toBeLessThanOrEqual(10)
578
+ if (body.data.length === 0) return
579
+
580
+ for (const swap of body.data) {
581
+ expect(swap.taker).toMatch(/^0x[0-9a-f]{40}$/)
582
+ expect(swap.transactionHash).toMatch(/^0x[0-9a-f]{64}$/)
583
+ expect(['exactSource', 'exactDestination', null]).toContain(swap.mode)
584
+ expect(swap.sourceToken.address).toMatch(/^0x[0-9a-f]{40}$/)
585
+ expect(swap.sourceToken.amount).toMatch(/^\d+$/)
586
+ expect(swap.destinationToken.address).toMatch(/^0x[0-9a-f]{40}$/)
587
+ expect(swap.destinationToken.amount).toMatch(/^\d+$/)
588
+ expect(swap.rate).toMatch(/^\d+(\.\d+)?$/)
589
+ expect(swap.filledAt).toMatch(/^\d{4}-\d{2}-\d{2}T/)
590
+ // `route` spans source → destination; its ends mirror the side tokens.
591
+ expect(swap.route.length).toBeGreaterThanOrEqual(2)
592
+ expect(swap.route[0]).toBe(swap.sourceToken.address)
593
+ expect(swap.route.at(-1)).toBe(swap.destinationToken.address)
594
+ // Per-fill detail is nested, in execution order, and chains hop-wise:
595
+ // every fill trades adjacent tokens of the route.
596
+ expect(swap.fills.length).toBeGreaterThan(0)
597
+ for (const fill of swap.fills) {
598
+ expect(fill.orderId).toMatch(/^\d+$/)
599
+ expect(fill.maker).toMatch(/^0x[0-9a-f]{40}$/)
600
+ expect(fill.price).toMatch(/^\d+(\.\d+)?$/)
601
+ expect(swap.route).toContain(fill.sourceToken.address)
602
+ expect(swap.route).toContain(fill.destinationToken.address)
603
+ }
604
+ for (let i = 1; i < swap.fills.length; i++)
605
+ expect(swap.fills[i]!.logIndex).toBeGreaterThan(swap.fills[i - 1]!.logIndex)
606
+ }
607
+ // Newest-first (rows order by block, then first fill within the block).
608
+ for (let i = 1; i < body.data.length; i++) {
609
+ const prev = body.data[i - 1]!
610
+ const curr = body.data[i]!
611
+ expect(
612
+ prev.blockNumber > curr.blockNumber ||
613
+ (prev.blockNumber === curr.blockNumber &&
614
+ prev.fills[0]!.logIndex >= curr.fills[0]!.logIndex),
615
+ ).toBe(true)
616
+ }
617
+ }, 60000)
618
+
619
+ test('paginates with an opaque cursor without skipping or duplicating rows', async () => {
620
+ const client = TestApp.client()
621
+ const first = await client.v1.exchange.swaps.$get({ query: { limit: '5' } }, TestApp.auth)
622
+ const firstBody = await TestApp.json(first, Exchanges.schema.getSwaps.Response)
623
+ if (firstBody.nextCursor === null) return
624
+
625
+ const second = await client.v1.exchange.swaps.$get(
626
+ { query: { cursor: firstBody.nextCursor, limit: '5' } },
627
+ TestApp.auth,
628
+ )
629
+ const secondBody = await TestApp.json(second, Exchanges.schema.getSwaps.Response)
630
+
631
+ // A swap row is unique on `(block, first fill logIndex)`.
632
+ const key = (swap: GlobalSwapsResponse['data'][number]) =>
633
+ `${swap.blockNumber}:${swap.fills[0]!.logIndex}`
634
+ const firstKeys = new Set(firstBody.data.map(key))
635
+ for (const swap of secondBody.data) expect(firstKeys.has(key(swap))).toBe(false)
636
+ }, 60000)
637
+
638
+ test('filters the global feed by transactionHash', async () => {
639
+ const client = TestApp.client()
640
+ const seed = await client.v1.exchange.swaps.$get({ query: { limit: '5' } }, TestApp.auth)
641
+ const seedBody = await TestApp.json(seed, Exchanges.schema.getSwaps.Response)
642
+ const row = seedBody.data[0]
643
+ if (!row) return
644
+
645
+ const response = await client.v1.exchange.swaps.$get(
646
+ { query: { limit: '5', transactionHash: row.transactionHash } },
647
+ TestApp.auth,
648
+ )
649
+ const body = await TestApp.json(response, Exchanges.schema.getSwaps.Response)
650
+ // The seeded swap's transaction is on the head page, so it must match,
651
+ // and every returned swap belongs to that transaction.
652
+ expect(body.data.length).toBeGreaterThan(0)
653
+ for (const swap of body.data) expect(swap.transactionHash).toBe(row.transactionHash)
654
+ }, 60000)
655
+
656
+ test('filters the global feed by maker', async () => {
657
+ const client = TestApp.client()
658
+ const seed = await client.v1.exchange.swaps.$get({ query: { limit: '5' } }, TestApp.auth)
659
+ const seedBody = await TestApp.json(seed, Exchanges.schema.getSwaps.Response)
660
+ const row = seedBody.data[0]
661
+ if (!row) return
662
+ const maker = row.fills[0]!.maker
663
+
664
+ const response = await client.v1.exchange.swaps.$get(
665
+ { query: { limit: '5', maker } },
666
+ TestApp.auth,
667
+ )
668
+ const body = await TestApp.json(response, Exchanges.schema.getSwaps.Response)
669
+ for (const swap of body.data) expect(swap.fills.some((fill) => fill.maker === maker)).toBe(true)
670
+ }, 60000)
671
+
672
+ test('embeds token metadata on swap sides when requested via `include=tokens`', async () => {
673
+ const client = TestApp.client()
674
+ const response = await client.v1.exchange.swaps.$get(
675
+ { query: { include: 'tokens', limit: '10' } },
676
+ TestApp.auth,
677
+ )
678
+ expect(response.status).toBe(200)
679
+ const body = await TestApp.json(response, Exchanges.schema.getSwaps.Response)
680
+ if (body.data.length === 0) return
681
+
682
+ const enriched = body.data.find((swap) => typeof swap.sourceToken.symbol === 'string')
683
+ if (enriched) {
684
+ expect(typeof enriched.sourceToken.symbol).toBe('string')
685
+ expect(typeof enriched.sourceToken.decimals).toBe('number')
686
+ }
687
+ }, 60000)
688
+ })
689
+
690
+ describe('GET /exchange/orders', () => {
691
+ type GlobalOrdersResponse = z.output<typeof Exchanges.schema.getOrders.Response>
692
+
693
+ test('returns a global page of resting orders across all pairs', async () => {
694
+ const client = TestApp.client()
695
+ const response = await client.v1.exchange.orders.$get({ query: { limit: '10' } }, TestApp.auth)
696
+ expect(response.status).toBe(200)
697
+ const body = await TestApp.json(response, Exchanges.schema.getOrders.Response)
698
+ expect(body.data.length).toBeLessThanOrEqual(10)
699
+ if (body.data.length === 0) return
700
+
701
+ for (const order of body.data) {
702
+ expect(order.orderId).toMatch(/^\d+$/)
703
+ expect(order.maker).toMatch(/^0x[0-9a-f]{40}$/)
704
+ expect(order.transactionHash).toMatch(/^0x[0-9a-f]{64}$/)
705
+ expect(order.amount).toMatch(/^\d+$/)
706
+ expect(order.remaining).toMatch(/^\d+$/)
707
+ expect(['bid', 'ask']).toContain(order.side)
708
+ expect(order.rate).toMatch(/^\d+(\.\d+)?$/)
709
+ expect(order.placedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/)
710
+ // Each row carries the pair it belongs to (a global page spans many pairs).
711
+ expect(order.pair.key).toMatch(/^0x[0-9a-f]{64}$/)
712
+ expect(order.pair.base.address).toMatch(/^0x[0-9a-f]{40}$/)
713
+ expect(order.pair.quote.address).toMatch(/^0x[0-9a-f]{40}$/)
714
+ }
715
+ }, 60000)
716
+
717
+ test('embeds a capped total count on demand via include=totalCount', async () => {
718
+ const client = TestApp.client()
719
+ const response = await client.v1.exchange.orders.$get(
720
+ { query: { include: 'totalCount', limit: '5' } },
721
+ TestApp.auth,
722
+ )
723
+ expect(response.status).toBe(200)
724
+ const body = await TestApp.json(response, Exchanges.schema.getOrders.Response)
725
+ expect(body.meta).toBeDefined()
726
+ expect(Number.isInteger(body.meta?.totalCount)).toBe(true)
727
+ expect(body.meta!.totalCount).toBeGreaterThanOrEqual(body.data.length)
728
+ expect(body.meta!.totalCount).toBeLessThanOrEqual(Schema.countCap)
729
+ // The in-memory total is capped when the page was truncated or hit the cap.
730
+ expect(body.meta!.totalCountCapped).toBe(
731
+ body.meta!.totalCount >= Schema.countCap || body.truncated,
732
+ )
733
+
734
+ const bare = await client.v1.exchange.orders.$get({ query: { limit: '5' } }, TestApp.auth)
735
+ const bareBody = await TestApp.json(bare, Exchanges.schema.getOrders.Response)
736
+ expect(bare.status).toBe(200)
737
+ expect(bareBody.meta).toBeUndefined()
738
+ }, 60000)
739
+
740
+ test('paginates with an opaque cursor without skipping or duplicating rows', async () => {
741
+ const client = TestApp.client()
742
+ const first = await client.v1.exchange.orders.$get({ query: { limit: '5' } }, TestApp.auth)
743
+ const firstBody = await TestApp.json(first, Exchanges.schema.getOrders.Response)
744
+ if (firstBody.nextCursor === null) return
745
+
746
+ const second = await client.v1.exchange.orders.$get(
747
+ { query: { cursor: firstBody.nextCursor, limit: '5' } },
748
+ TestApp.auth,
749
+ )
750
+ const secondBody = await TestApp.json(second, Exchanges.schema.getOrders.Response)
751
+
752
+ const key = (order: GlobalOrdersResponse['data'][number]) => order.orderId
753
+ const firstKeys = new Set(firstBody.data.map(key))
754
+ for (const order of secondBody.data) expect(firstKeys.has(key(order))).toBe(false)
755
+ }, 60000)
756
+
757
+ test('filters the global feed by maker', async () => {
758
+ const client = TestApp.client()
759
+ const seed = await client.v1.exchange.orders.$get({ query: { limit: '5' } }, TestApp.auth)
760
+ const seedBody = await TestApp.json(seed, Exchanges.schema.getOrders.Response)
761
+ const row = seedBody.data[0]
762
+ if (!row) return
763
+
764
+ const response = await client.v1.exchange.orders.$get(
765
+ { query: { limit: '5', maker: row.maker } },
766
+ TestApp.auth,
767
+ )
768
+ const body = await TestApp.json(response, Exchanges.schema.getOrders.Response)
769
+ for (const order of body.data) expect(order.maker).toBe(row.maker)
770
+ }, 60000)
771
+
772
+ test('embeds pair token metadata when requested via `include=tokens`', async () => {
773
+ const client = TestApp.client()
774
+ const response = await client.v1.exchange.orders.$get(
775
+ { query: { include: 'tokens', limit: '10' } },
776
+ TestApp.auth,
777
+ )
778
+ expect(response.status).toBe(200)
779
+ const body = await TestApp.json(response, Exchanges.schema.getOrders.Response)
780
+ if (body.data.length === 0) return
781
+
782
+ const enriched = body.data.find((order) => typeof order.pair.base.symbol === 'string')
783
+ if (enriched) {
784
+ expect(typeof enriched.pair.base.symbol).toBe('string')
785
+ expect(typeof enriched.pair.base.decimals).toBe('number')
786
+ }
787
+ }, 60000)
788
+ })
789
+
790
+ describe('GET /exchange/orders/:orderId', () => {
791
+ test('rejects a non-decimal order id with 400', async () => {
792
+ const client = TestApp.client()
793
+ const response = await client.v1.exchange.orders[':orderId'].$get(
794
+ { param: { orderId: 'not-a-number' }, query: {} },
795
+ TestApp.auth,
796
+ )
797
+ expect(response.status).toBe(400)
798
+ }, 30000)
799
+
800
+ test('returns 404 for an unknown order id', async () => {
801
+ const client = TestApp.client()
802
+ // A very large id is overwhelmingly likely to not exist on testnet. The
803
+ // on-chain `dex.getOrder` view reverts with `OrderDoesNotExist()` for ids
804
+ // it does not hold, which the handler maps to a 404 (not a 502).
805
+ const unknownOrderId = '99999999999999999999'
806
+ const response = await client.v1.exchange.orders[':orderId'].$get(
807
+ { param: { orderId: unknownOrderId }, query: {} },
808
+ TestApp.auth,
809
+ )
810
+ // Surface a genuine RPC transport failure as a transient skip; the unknown
811
+ // id itself must resolve to a clean 404.
812
+ if (response.status === 502) return
813
+ expect(response.status).toBe(404)
814
+ }, 30000)
815
+
816
+ test('returns live order state and pair for a real id', async () => {
817
+ const client = TestApp.client()
818
+ const sampleOrderId = await sampleOrderIdFromOrders(client)
819
+ if (!sampleOrderId) return
820
+
821
+ const response = await client.v1.exchange.orders[':orderId'].$get(
822
+ { param: { orderId: sampleOrderId }, query: {} },
823
+ TestApp.auth,
824
+ )
825
+ // The on-chain `dex.getOrder` view is a single RPC; surface 502 as a
826
+ // transient-skip so the test stays green when the route's contract is
827
+ // intact.
828
+ if (response.status === 502) return
829
+ expect(response.status).toBe(200)
830
+ const order = await TestApp.json(response, Exchanges.schema.getOrder.Response)
831
+
832
+ expect(order.orderId).toBe(sampleOrderId)
833
+ expect(order.maker).toMatch(/^0x[0-9a-f]{40}$/)
834
+ expect(order.amount).toMatch(/^\d+$/)
835
+ expect(order.remaining).toMatch(/^\d+$/)
836
+ expect(['exactSource', 'exactDestination']).toContain(order.mode)
837
+ expect(order.rate).toMatch(/^\d+(\.\d+)?$/)
838
+ expect(order.price).toMatch(/^\d+(\.\d+)?$/)
839
+ expect(order.pair.key).toMatch(/^0x[0-9a-f]{64}$/)
840
+ expect(order.pair.base.address).toMatch(/^0x[0-9a-f]{40}$/)
841
+ expect(order.pair.quote.address).toMatch(/^0x[0-9a-f]{40}$/)
842
+ }, 60000)
843
+
844
+ test('embeds pair token metadata when requested via `include=tokens`', async () => {
845
+ const client = TestApp.client()
846
+ const sampleOrderId = await sampleOrderIdFromOrders(client)
847
+ if (!sampleOrderId) return
848
+
849
+ const response = await client.v1.exchange.orders[':orderId'].$get(
850
+ { param: { orderId: sampleOrderId }, query: { include: 'tokens' } },
851
+ TestApp.auth,
852
+ )
853
+ if (response.status === 502) return
854
+ expect(response.status).toBe(200)
855
+ const order = await TestApp.json(response, Exchanges.schema.getOrder.Response)
856
+
857
+ // Both pair tokens share the same metadata path: when one resolves a
858
+ // symbol, both sides of the trimmed token reference are present.
859
+ if (typeof order.pair.base.symbol === 'string') {
860
+ expect(typeof order.pair.base.decimals).toBe('number')
861
+ expect(typeof order.pair.quote.symbol).toBe('string')
862
+ }
863
+ }, 60000)
864
+ })
865
+
866
+ describe('GET /exchange/orders/:orderId/fills', () => {
867
+ test('rejects a non-decimal order id with 400', async () => {
868
+ const client = TestApp.client()
869
+ const response = await client.v1.exchange.orders[':orderId'].fills.$get(
870
+ { param: { orderId: 'not-a-number' }, query: {} },
871
+ TestApp.auth,
872
+ )
873
+ expect(response.status).toBe(400)
874
+ }, 30000)
875
+
876
+ test('returns a paginated newest-first fill page for a real order id', async () => {
877
+ const client = TestApp.client()
878
+ const sampleOrderId = await sampleOrderIdFromSwaps(client)
879
+ if (!sampleOrderId) return
880
+
881
+ const response = await client.v1.exchange.orders[':orderId'].fills.$get(
882
+ { param: { orderId: sampleOrderId }, query: { limit: '5' } },
883
+ TestApp.auth,
884
+ )
885
+ // Treat transient indexer 502s as a skip — see order-detail test.
886
+ if (response.status === 502) return
887
+ expect(response.status).toBe(200)
888
+ const page = await TestApp.json(response, Exchanges.schema.getOrderFills.Response)
889
+
890
+ // The sample order was discovered via the swap feed, so it has at least
891
+ // one fill.
892
+ expect(page.data.length).toBeGreaterThan(0)
893
+ for (const fill of page.data) {
894
+ expect(fill.amountFilled).toMatch(/^\d+$/)
895
+ expect(fill.taker).toMatch(/^0x[0-9a-f]{40}$/)
896
+ expect(fill.transactionHash).toMatch(/^0x[0-9a-f]{64}$/)
897
+ expect(fill.filledAt).toMatch(/^\d{4}-\d{2}-\d{2}T/)
898
+ }
899
+ // Newest-first ordering.
900
+ for (let i = 1; i < page.data.length; i++) {
901
+ const prev = page.data[i - 1]!
902
+ const curr = page.data[i]!
903
+ expect(
904
+ prev.blockNumber > curr.blockNumber ||
905
+ (prev.blockNumber === curr.blockNumber && prev.logIndex >= curr.logIndex),
906
+ ).toBe(true)
907
+ }
908
+ // The base page does not run or embed the count.
909
+ expect(page.meta).toBeUndefined()
910
+ }, 60000)
911
+
912
+ test('embeds an exact fill total on demand via include=totalCount', async () => {
913
+ const client = TestApp.client()
914
+ const sampleOrderId = await sampleOrderIdFromSwaps(client)
915
+ if (!sampleOrderId) return
916
+
917
+ const response = await client.v1.exchange.orders[':orderId'].fills.$get(
918
+ { param: { orderId: sampleOrderId }, query: { include: 'totalCount', limit: '5' } },
919
+ TestApp.auth,
920
+ )
921
+ if (response.status === 502) return
922
+ expect(response.status).toBe(200)
923
+ const page = await TestApp.json(response, Exchanges.schema.getOrderFills.Response)
924
+
925
+ expect(response.headers.get('server-timing')?.includes('order_fills_count;dur=')).toBe(true)
926
+ expect(Number.isInteger(page.meta?.totalCount)).toBe(true)
927
+ expect(page.meta!.totalCount).toBeGreaterThanOrEqual(page.data.length)
928
+ // The count is sort-key-pruned on ClickHouse, so it is exact (never capped).
929
+ expect(page.meta!.totalCountCapped).toBe(false)
930
+ }, 60000)
931
+ })
932
+
933
+ /**
934
+ * Discovers a real order id from the recent global swap feed so the
935
+ * order-fills test can run against live testnet data without hard-coding
936
+ * ids (a swap-feed id is guaranteed to have at least one fill). Returns
937
+ * `null` when the feed is empty.
938
+ */
939
+ async function sampleOrderIdFromSwaps(client: ReturnType<typeof TestApp.client>) {
940
+ const response = await client.v1.exchange.swaps.$get({ query: { limit: '5' } }, TestApp.auth)
941
+ if (response.status !== 200) return null
942
+ const body = await TestApp.json(response, Exchanges.schema.getSwaps.Response)
943
+ return body.data[0]?.fills[0]?.orderId ?? null
944
+ }
945
+
946
+ /**
947
+ * Discovers a live resting-order id from the global orders feed for the
948
+ * order-detail tests. Unlike the swap feed — which (since the point-in-time
949
+ * state rework) also surfaces fills on flip-only orders and on books missing
950
+ * from `dex_pairs` — the orders feed only lists ids the order-detail
951
+ * endpoint can resolve. Returns `null` when the book is empty.
952
+ */
953
+ async function sampleOrderIdFromOrders(client: ReturnType<typeof TestApp.client>) {
954
+ const response = await client.v1.exchange.orders.$get({ query: { limit: '5' } }, TestApp.auth)
955
+ if (response.status !== 200) return null
956
+ const body = await TestApp.json(response, Exchanges.schema.getOrders.Response)
957
+ return body.data[0]?.orderId ?? null
958
+ }