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,1062 @@
1
+ import { Schema } from 'cadent'
2
+
3
+ import * as TestApp from '../../test/App.js'
4
+ import * as Runtime from '../../test/runtime.js'
5
+ import * as Tokens from './tokens.js'
6
+
7
+ // Verified enrichment is store-backed now that the build-time bundle is gone, so
8
+ // seed a shared in-memory verified store from the curated seeds file. Tests use
9
+ // this wrapper instead of `TestApp.client` so the default app exposes the same
10
+ // verified tokens the bundle used to provide; a test can still override
11
+ // `verifiedTokens`.
12
+ const runtime = Runtime.get()
13
+ const verified = TestApp.verifiedStore(runtime.chainId)
14
+ function app(options: TestApp.create.Options = {}) {
15
+ return TestApp.client({ verifiedTokens: { store: verified }, ...options })
16
+ }
17
+
18
+ const verifiedTokenCount = 6
19
+
20
+ describe('GET /tokens', () => {
21
+ test.runIf(runtime.mode === 'testnet')(
22
+ 'serves a bounded positional page via `page` (indexed mode)',
23
+ async () => {
24
+ const client = app()
25
+ // Ascending creation order anchors the indexed feed at the first
26
+ // `TokenCreated`, so positional pages are deterministic against the head.
27
+ const headResponse = await client.v1.tokens.$get(
28
+ { query: { limit: '10', order: 'asc' } },
29
+ TestApp.auth,
30
+ )
31
+ const head = await TestApp.json(headResponse, Tokens.schema.getTokens.Response)
32
+ expect(headResponse.status).toBe(200)
33
+ expect(head.data.length).toBe(10)
34
+
35
+ const offsetResponse = await client.v1.tokens.$get(
36
+ { query: { limit: '5', order: 'asc', page: '2' } },
37
+ TestApp.auth,
38
+ )
39
+ const page = await TestApp.json(offsetResponse, Tokens.schema.getTokens.Response)
40
+ expect(offsetResponse.status).toBe(200)
41
+ expect(page.data.map((token) => token.address)).toEqual(
42
+ head.data.slice(5, 10).map((token) => token.address),
43
+ )
44
+ // Offset pages still return a keyset cursor for deep traversal.
45
+ expect(typeof page.nextCursor).toBe('string')
46
+ },
47
+ )
48
+
49
+ test('returns an indexer-backed token page', async () => {
50
+ const client = app()
51
+ const response = await client.v1.tokens.$get({ query: { limit: '5' } }, TestApp.auth)
52
+ const body = await TestApp.json(response, Tokens.schema.getTokens.Response)
53
+ const timing = response.headers.get('server-timing')
54
+
55
+ expect(response.status).toMatchInlineSnapshot(`200`)
56
+ expect(timing?.includes('verified_tokens;dur=')).toMatchInlineSnapshot(`true`)
57
+ expect(timing?.includes('tokens_indexed;dur=')).toMatchInlineSnapshot(`true`)
58
+ expect(timing?.includes('request;dur=')).toMatchInlineSnapshot(`true`)
59
+ // Holder counts are opt-in: the base page must not run the indexer query.
60
+ expect(timing?.includes('tokens_holders;dur=')).toMatchInlineSnapshot(`false`)
61
+ expect(body.data.every((token) => token.holderCount === undefined)).toMatchInlineSnapshot(
62
+ `true`,
63
+ )
64
+ expect(body.data.length).toBeGreaterThan(0)
65
+ expect(body.data.length).toBeLessThanOrEqual(5)
66
+ // Cursor (keyset) pagination: the head page has no `page`, but carries an
67
+ // opaque `nextCursor` (a string when more rows follow, else null).
68
+ expect('page' in body).toBe(false)
69
+ expect(body.nextCursor === null || typeof body.nextCursor === 'string').toBe(true)
70
+ expect(
71
+ body.data.every((token) => /^0x[0-9a-f]{40}$/.test(token.address)),
72
+ ).toMatchInlineSnapshot(`true`)
73
+ expect(body.data.every((token) => token.decimals === 6)).toMatchInlineSnapshot(`true`)
74
+ expect(body.data.every((token) => typeof token.currency === 'string')).toMatchInlineSnapshot(
75
+ `true`,
76
+ )
77
+ // `createdAt` is opt-in: absent on the base page.
78
+ expect(body.data.every((token) => token.createdAt === undefined)).toMatchInlineSnapshot(`true`)
79
+
80
+ // With `include=createdAt`, every row carries a valid ISO timestamp.
81
+ const withCreatedAt = await client.v1.tokens.$get(
82
+ { query: { include: 'createdAt', limit: '5' } },
83
+ TestApp.auth,
84
+ )
85
+ const withCreatedAtBody = await TestApp.json(withCreatedAt, Tokens.schema.getTokens.Response)
86
+ expect(
87
+ withCreatedAtBody.data.every(
88
+ (token) =>
89
+ typeof token.createdAt === 'string' && !Number.isNaN(Date.parse(token.createdAt)),
90
+ ),
91
+ ).toMatchInlineSnapshot(`true`)
92
+ })
93
+
94
+ test('includes batched holder counts for a token page when requested', async () => {
95
+ const client = app()
96
+ const response = await client.v1.tokens.$get(
97
+ { query: { include: 'holderCount', limit: '10' } },
98
+ TestApp.auth,
99
+ )
100
+ const body = await TestApp.json(response, Tokens.schema.getTokens.Response)
101
+ const timing = response.headers.get('server-timing')
102
+
103
+ expect(response.status).toBe(200)
104
+ expect(timing?.includes('tokens_holders;dur=')).toBe(true)
105
+ // Tokens with no holders are omitted from the batched result, so each item
106
+ // either carries a non-negative integer count or no count at all.
107
+ expect(
108
+ body.data.every(
109
+ (token) => token.holderCount === undefined || Number.isInteger(token.holderCount),
110
+ ),
111
+ ).toBe(true)
112
+ })
113
+
114
+ test.runIf(runtime.mode === 'testnet')(
115
+ 'walks the cursor to a disjoint next token page',
116
+ async () => {
117
+ const client = app()
118
+ const first = await client.v1.tokens.$get({ query: { limit: '5' } }, TestApp.auth)
119
+ const firstBody = await TestApp.json(first, Tokens.schema.getTokens.Response)
120
+ expect(first.status).toMatchInlineSnapshot(`200`)
121
+ expect(typeof firstBody.nextCursor).toMatchInlineSnapshot(`"string"`)
122
+
123
+ const second = await client.v1.tokens.$get(
124
+ { query: { limit: '5', cursor: firstBody.nextCursor! } },
125
+ TestApp.auth,
126
+ )
127
+ const secondBody = await TestApp.json(second, Tokens.schema.getTokens.Response)
128
+
129
+ expect(second.status).toMatchInlineSnapshot(`200`)
130
+ expect(secondBody.data.length).toMatchInlineSnapshot(`5`)
131
+ expect(
132
+ secondBody.data.every((token) => /^0x[0-9a-f]{40}$/.test(token.address)),
133
+ ).toMatchInlineSnapshot(`true`)
134
+ // Walking the cursor must yield a disjoint next page (no offset-style overlap).
135
+ const firstAddresses = new Set(firstBody.data.map((token) => token.address))
136
+ expect(
137
+ secondBody.data.every((token) => !firstAddresses.has(token.address)),
138
+ ).toMatchInlineSnapshot(`true`)
139
+ },
140
+ )
141
+
142
+ test('narrows tokens by currency (case-insensitive)', async () => {
143
+ const client = app()
144
+ // Lowercase input must match the on-chain `USD` casing.
145
+ const response = await client.v1.tokens.$get(
146
+ { query: { currency: 'usd', limit: '20' } },
147
+ TestApp.auth,
148
+ )
149
+ const body = await TestApp.json(response, Tokens.schema.getTokens.Response)
150
+
151
+ expect(response.status).toBe(200)
152
+ expect(body.data.length).toBeGreaterThan(0)
153
+ for (const token of body.data) expect(token.currency.toLowerCase()).toBe('usd')
154
+ })
155
+
156
+ test('falls back to the head page for a malformed cursor', async () => {
157
+ const client = app()
158
+ // An opaque cursor is decode-validated; a forged/garbage value degrades to
159
+ // the head page rather than erroring (or reaching SQL).
160
+ const response = await client.v1.tokens.$get(
161
+ { query: { limit: '5', cursor: 'not-a-real-cursor' } },
162
+ TestApp.auth,
163
+ )
164
+ const body = await TestApp.json(response, Tokens.schema.getTokens.Response)
165
+
166
+ expect(response.status).toMatchInlineSnapshot(`200`)
167
+ expect(body.data.length).toBeGreaterThan(0)
168
+ expect(body.data.length).toBeLessThanOrEqual(5)
169
+ })
170
+ })
171
+
172
+ describe('GET /tokens?verified=true', () => {
173
+ test('returns the curated verified token list in the same shape as a normal page', async () => {
174
+ const client = app()
175
+ const response = await client.v1.tokens.$get({ query: { verified: 'true' } }, TestApp.auth)
176
+ const body = await TestApp.json(response, Tokens.schema.getTokens.Response)
177
+ const timing = response.headers.get('server-timing')
178
+
179
+ expect(response.status).toMatchInlineSnapshot(`200`)
180
+ // Verified mode is served from the static list, so the indexer token
181
+ // listing query does not fire (`createdAt` is opt-in via `include` and so
182
+ // is not fetched on this base page).
183
+ expect(timing?.includes('verified_tokens;dur=')).toMatchInlineSnapshot(`true`)
184
+ expect(timing?.includes('tokens_indexed;dur=')).toMatchInlineSnapshot(`false`)
185
+ expect(body.data.length).toBeGreaterThan(0)
186
+ expect(body.data.every((token) => token.verified === true)).toBe(true)
187
+ // Logos ride along on the verified page (resolved from the icon asset
188
+ // store), so at least one curated token exposes a `logoUri`.
189
+ if (runtime.mode === 'testnet')
190
+ expect(body.data.some((token) => typeof token.logoUri === 'string')).toBe(true)
191
+ // Same pagination envelope as the unfiltered page so clients don't need a
192
+ // separate response shape.
193
+ expect(body.nextCursor).toBeNull()
194
+ })
195
+
196
+ test('narrows verified tokens by currency (case-insensitive)', async () => {
197
+ const client = app()
198
+ const response = await client.v1.tokens.$get(
199
+ { query: { verified: 'true', currency: 'usd' } },
200
+ TestApp.auth,
201
+ )
202
+ const body = await TestApp.json(response, Tokens.schema.getTokens.Response)
203
+
204
+ expect(response.status).toBe(200)
205
+ expect(body.data.length).toBeGreaterThan(0)
206
+ for (const token of body.data) {
207
+ expect(token.currency.toLowerCase()).toBe('usd')
208
+ expect(token.verified).toBe(true)
209
+ }
210
+ })
211
+
212
+ test('returns verified tokens even when no asset loader is configured', async () => {
213
+ const client = app({ assetsPath: false })
214
+ const response = await client.v1.tokens.$get({ query: { verified: 'true' } }, TestApp.auth)
215
+ const body = await TestApp.json(response, Tokens.schema.getTokens.Response)
216
+
217
+ expect(response.status).toMatchInlineSnapshot(`200`)
218
+ expect(body.data.length).toBe(verifiedTokenCount)
219
+ expect(body.data.every((token) => token.verified === true)).toMatchInlineSnapshot(`true`)
220
+ })
221
+
222
+ test('paginates the verified list via limit + cursor', async () => {
223
+ const client = app({ assetsPath: false })
224
+ // Full list for comparison (6 verified tokens on the test chain).
225
+ const full = await TestApp.json(
226
+ await client.v1.tokens.$get({ query: { verified: 'true' } }, TestApp.auth),
227
+ Tokens.schema.getTokens.Response,
228
+ )
229
+
230
+ // First page: a bounded slice with a cursor to the next page (`minLimit` is
231
+ // 5, and the test chain has 6 verified tokens, so a page of 5 leaves 1).
232
+ const first = await TestApp.json(
233
+ await client.v1.tokens.$get({ query: { verified: 'true', limit: '5' } }, TestApp.auth),
234
+ Tokens.schema.getTokens.Response,
235
+ )
236
+ expect(first.data.length).toBe(5)
237
+ expect(first.nextCursor).toBeTypeOf('string')
238
+
239
+ // Following the cursor returns the remainder with no further pages.
240
+ const second = await TestApp.json(
241
+ await client.v1.tokens.$get(
242
+ { query: { verified: 'true', limit: '5', cursor: first.nextCursor! } },
243
+ TestApp.auth,
244
+ ),
245
+ Tokens.schema.getTokens.Response,
246
+ )
247
+ expect(second.data.length).toBe(1)
248
+ expect(second.nextCursor).toBeNull()
249
+
250
+ // The two pages reconstruct the full list in order, with no overlap.
251
+ expect([...first.data, ...second.data].map((token) => token.address)).toEqual(
252
+ full.data.map((token) => token.address),
253
+ )
254
+ })
255
+
256
+ test('reverses the verified list with order=asc', async () => {
257
+ const client = app({ assetsPath: false })
258
+ const desc = await TestApp.json(
259
+ await client.v1.tokens.$get({ query: { verified: 'true' } }, TestApp.auth),
260
+ Tokens.schema.getTokens.Response,
261
+ )
262
+ const asc = await TestApp.json(
263
+ await client.v1.tokens.$get({ query: { verified: 'true', order: 'asc' } }, TestApp.auth),
264
+ Tokens.schema.getTokens.Response,
265
+ )
266
+
267
+ expect(asc.data.map((token) => token.address)).toEqual(
268
+ [...desc.data].reverse().map((token) => token.address),
269
+ )
270
+ })
271
+ })
272
+
273
+ describe('GET /tokens?addresses=', () => {
274
+ test('returns the requested tokens as a single page in input order', async () => {
275
+ const client = app()
276
+ const response = await client.v1.tokens.$get(
277
+ { query: { addresses: `${TestApp.token},${TestApp.tokenWithHolders}` } },
278
+ TestApp.auth,
279
+ )
280
+ const body = await TestApp.json(response, Tokens.schema.getTokens.Response)
281
+
282
+ expect(response.status).toMatchInlineSnapshot(`200`)
283
+ expect(body.data.map((token) => token.address)).toEqual([
284
+ TestApp.token,
285
+ TestApp.tokenWithHolders,
286
+ ])
287
+ // The addressed page is degenerate: a single page with no cursor.
288
+ expect(body.nextCursor).toBeNull()
289
+ // Rows resolve through `resolveToken` (RPC metadata), so each carries the
290
+ // detail shape, including a live `totalSupply`.
291
+ expect(body.data.every((token) => /^\d+$/.test(token.totalSupply!))).toBe(true)
292
+ })
293
+
294
+ test('carries batched holder counts and drops unresolvable addresses', async () => {
295
+ const client = app()
296
+ // Well-formed but nonexistent TIP-20 address: resolution fails upstream
297
+ // and the row is dropped rather than failing the page.
298
+ const missing = '0x20c0000000000000000000000000000000099999'
299
+ const response = await client.v1.tokens.$get(
300
+ {
301
+ query: {
302
+ addresses: `${TestApp.token},${missing},${TestApp.tokenWithHolders}`,
303
+ include: 'holderCount',
304
+ },
305
+ },
306
+ TestApp.auth,
307
+ )
308
+ const body = await TestApp.json(response, Tokens.schema.getTokens.Response)
309
+ const timing = response.headers.get('server-timing')
310
+
311
+ expect(response.status).toBe(200)
312
+ // One batched holder-count query serves the whole page.
313
+ expect(timing?.includes('tokens_holders;dur=')).toBe(true)
314
+ expect(body.data.map((token) => token.address)).toEqual([
315
+ TestApp.token,
316
+ TestApp.tokenWithHolders,
317
+ ])
318
+ // pathUSD is the busiest token on testnet, so its count is always
319
+ // positive. (The fixture token currently has no positive-balance holders,
320
+ // so its count is legitimately absent.)
321
+ const pathUsd = body.data.find((token) => token.address === TestApp.tokenWithHolders)
322
+ if (runtime.mode === 'testnet') expect(pathUsd?.holderCount).toBeGreaterThan(0)
323
+ })
324
+
325
+ test('rejects more than 50 addresses', async () => {
326
+ const client = app()
327
+ const addresses = Array.from(
328
+ { length: 51 },
329
+ (_, index) => `0x20c${'0'.repeat(32)}${10000 + index}`,
330
+ ).join(',')
331
+ const response = await client.v1.tokens.$get({ query: { addresses } }, TestApp.auth)
332
+ const body = await TestApp.json(response, Schema.ErrorResponse)
333
+ const { requestId, ...stable } = body
334
+
335
+ expect(response.status).toMatchInlineSnapshot(`400`)
336
+ expect(typeof requestId).toMatchInlineSnapshot(`"string"`)
337
+ expect(stable).toMatchInlineSnapshot(`
338
+ {
339
+ "error": {
340
+ "code": "query_invalid",
341
+ "details": [
342
+ {
343
+ "message": "Too big: expected array to have <=50 items",
344
+ "path": [
345
+ "addresses",
346
+ ],
347
+ },
348
+ ],
349
+ "message": "Invalid query parameters",
350
+ },
351
+ }
352
+ `)
353
+ })
354
+ })
355
+
356
+ describe('GET /tokens/:token', () => {
357
+ test('returns token metadata for a small token', async () => {
358
+ const client = app()
359
+ const response = await client.v1.tokens[':token'].$get(
360
+ { param: { token: TestApp.token }, query: {} },
361
+ TestApp.auth,
362
+ )
363
+ const body = await TestApp.json(response, Tokens.schema.getToken.Response)
364
+ const timing = response.headers.get('server-timing')
365
+ const { totalSupply, ...stable } = body
366
+
367
+ expect(response.status).toMatchInlineSnapshot(`200`)
368
+ expect(timing?.includes('token_metadata;dur=')).toMatchInlineSnapshot(`true`)
369
+ expect(timing?.includes('verified_tokens;dur=')).toMatchInlineSnapshot(`true`)
370
+ expect(timing?.includes('token_logo;dur=')).toMatchInlineSnapshot(`true`)
371
+ expect(timing?.includes('token;dur=')).toMatchInlineSnapshot(`true`)
372
+ expect(timing?.includes('request;dur=')).toMatchInlineSnapshot(`true`)
373
+ // Holder counts are opt-in: the base request must not run the indexer query.
374
+ expect(timing?.includes('token_holders;dur=')).toMatchInlineSnapshot(`false`)
375
+ expect(body.holderCount).toMatchInlineSnapshot(`undefined`)
376
+ expect(/^\d+$/.test(totalSupply!)).toMatchInlineSnapshot(`true`)
377
+ if (runtime.mode === 'testnet')
378
+ expect(stable).toMatchInlineSnapshot(`
379
+ {
380
+ "address": "0x20c0000000000000000000008f5425160ebe5525",
381
+ "currency": "USD",
382
+ "decimals": 6,
383
+ "id": "0x20c0000000000000000000008f5425160ebe5525",
384
+ "name": "Alpha Land",
385
+ "symbol": "ALD",
386
+ "verified": false,
387
+ }
388
+ `)
389
+ else {
390
+ expect(stable.address).toBe(TestApp.token)
391
+ expect(stable.currency).toBe('USD')
392
+ expect(stable.decimals).toBe(6)
393
+ expect(stable.verified).toBe(true)
394
+ }
395
+ })
396
+
397
+ test('rejects an unsupported chain id', async () => {
398
+ // `chainId` now accepts any positive integer syntactically, so an unknown
399
+ // chain (`1`) is rejected by the App-level supported-chain guard rather than
400
+ // schema validation; exercise it via `app.request`.
401
+ const response = await TestApp.create().request(`/v1/tokens/${TestApp.token}?chainId=1`, {
402
+ headers: { authorization: `Bearer ${TestApp.key.token}` },
403
+ })
404
+ const body = await TestApp.json(response, Schema.ErrorResponse)
405
+ const { requestId, ...stable } = body
406
+
407
+ expect(response.status).toMatchInlineSnapshot(`400`)
408
+ expect(response.headers.get('tempo-request-id') === requestId).toMatchInlineSnapshot(`true`)
409
+ expect(typeof requestId).toMatchInlineSnapshot(`"string"`)
410
+ expect(stable.error.code).toBe('chain_id_unsupported')
411
+ expect(stable.error.message).toContain('Unsupported chain id: 1.')
412
+ })
413
+
414
+ test.runIf(runtime.mode === 'testnet')(
415
+ 'returns static logo URI when token icon exists',
416
+ async () => {
417
+ const client = app()
418
+ const response = await client.v1.tokens[':token'].$get(
419
+ { param: { token: TestApp.tokenWithLogo }, query: {} },
420
+ TestApp.auth,
421
+ )
422
+ const body = await TestApp.json(response, Tokens.schema.getToken.Response)
423
+ const { totalSupply, ...stable } = body
424
+
425
+ expect(response.status).toMatchInlineSnapshot(`200`)
426
+ expect(/^\d+$/.test(totalSupply!)).toMatchInlineSnapshot(`true`)
427
+ expect(stable).toMatchInlineSnapshot(`
428
+ {
429
+ "address": "0x20c0000000000000000000009e8d7eb59b783726",
430
+ "currency": "USDC",
431
+ "decimals": 6,
432
+ "id": "0x20c0000000000000000000009e8d7eb59b783726",
433
+ "logoUri": "http://localhost/assets/42431/icons/0x20c0000000000000000000009e8d7eb59b783726",
434
+ "name": "Bridged USDC (Stargate)",
435
+ "symbol": "USDC.e",
436
+ "verified": true,
437
+ }
438
+ `)
439
+ },
440
+ )
441
+
442
+ test('includes the holder count when requested via include', async () => {
443
+ const client = app()
444
+ const response = await client.v1.tokens[':token'].$get(
445
+ { param: { token: TestApp.tokenWithHolders }, query: { include: 'holderCount' } },
446
+ TestApp.auth,
447
+ )
448
+ const body = await TestApp.json(response, Tokens.schema.getToken.Response)
449
+ const timing = response.headers.get('server-timing')
450
+
451
+ expect(response.status).toBe(200)
452
+ expect(timing?.includes('token_holders;dur=')).toBe(true)
453
+ if (runtime.mode === 'testnet') {
454
+ expect(Number.isInteger(body.holderCount)).toBe(true)
455
+ expect(body.holderCount).toBeGreaterThan(0)
456
+ }
457
+ })
458
+
459
+ test.runIf(runtime.mode === 'testnet')(
460
+ 'includes TokenCreated admin and quote token when requested via include',
461
+ async () => {
462
+ const client = app()
463
+ const response = await client.v1.tokens[':token'].$get(
464
+ { param: { token: TestApp.token }, query: { include: 'admin,quoteToken' } },
465
+ TestApp.auth,
466
+ )
467
+ const body = await TestApp.json(response, Tokens.schema.getToken.Response)
468
+
469
+ expect(response.status).toMatchInlineSnapshot(`200`)
470
+ expect(body.admin).toMatchInlineSnapshot(`"0xb2f15e10adea883417de870aa99912cc98b358c7"`)
471
+ expect(body.quoteToken).toMatchInlineSnapshot(`"0x20c0000000000000000000000000000000000000"`)
472
+ },
473
+ )
474
+
475
+ test.runIf(runtime.mode === 'testnet')(
476
+ 'accepts repeated include parameters alongside the comma form',
477
+ async () => {
478
+ const client = app()
479
+ // `?include=admin&include=quoteToken` — each repeat arrives at the
480
+ // validator as one array entry; equivalent to `include=admin,quoteToken`.
481
+ const response = await client.v1.tokens[':token'].$get(
482
+ { param: { token: TestApp.token }, query: { include: ['admin', 'quoteToken'] } },
483
+ TestApp.auth,
484
+ )
485
+ const body = await TestApp.json(response, Tokens.schema.getToken.Response)
486
+
487
+ expect(response.status).toMatchInlineSnapshot(`200`)
488
+ expect(body.admin).toMatchInlineSnapshot(`"0xb2f15e10adea883417de870aa99912cc98b358c7"`)
489
+ expect(body.quoteToken).toMatchInlineSnapshot(`"0x20c0000000000000000000000000000000000000"`)
490
+ },
491
+ )
492
+
493
+ test.runIf(runtime.mode === 'testnet')(
494
+ 'omits admin when only quoteToken is requested',
495
+ async () => {
496
+ const client = app()
497
+ const response = await client.v1.tokens[':token'].$get(
498
+ { param: { token: TestApp.token }, query: { include: 'quoteToken' } },
499
+ TestApp.auth,
500
+ )
501
+ const body = await TestApp.json(response, Tokens.schema.getToken.Response)
502
+
503
+ expect(response.status).toBe(200)
504
+ // `admin` and `quoteToken` ride one `TokenCreated` fetch but surface
505
+ // independently: asking for one must not leak the other.
506
+ expect(body.quoteToken).toMatchInlineSnapshot(`"0x20c0000000000000000000000000000000000000"`)
507
+ expect('admin' in body).toBe(false)
508
+ },
509
+ )
510
+
511
+ test('includes lifetime transfer statistics when requested via include', async () => {
512
+ const client = app()
513
+ const response = await client.v1.tokens[':token'].$get(
514
+ { param: { token: TestApp.token }, query: { include: 'transferStats' } },
515
+ TestApp.auth,
516
+ )
517
+ const body = await TestApp.json(response, Tokens.schema.getToken.Response)
518
+
519
+ expect(response.status).toBe(200)
520
+ // The transfer count grows with chain activity; assert shape, not value.
521
+ expect(body.transferStats?.count).toBeGreaterThan(0)
522
+ // Timezone-correctness guard: ClickHouse returns naive-UTC timestamps
523
+ // (`2026-05-25 22:35:11.000`); a local-time misparse would shift this
524
+ // immutable first-transfer instant by the host's UTC offset.
525
+ if (runtime.mode === 'testnet')
526
+ expect(body.transferStats?.firstAt).toMatchInlineSnapshot(`"2026-05-25T22:35:11.000Z"`)
527
+ else expect(typeof body.transferStats?.firstAt).toBe('string')
528
+ expect(Date.parse(body.transferStats!.firstAt!)).toBeLessThanOrEqual(
529
+ Date.parse(body.transferStats!.lastAt!),
530
+ )
531
+ })
532
+
533
+ test('omits TokenCreated extras and transfer stats without include', async () => {
534
+ const client = app()
535
+ const response = await client.v1.tokens[':token'].$get(
536
+ { param: { token: TestApp.token }, query: {} },
537
+ TestApp.auth,
538
+ )
539
+ const body = await TestApp.json(response, Tokens.schema.getToken.Response)
540
+
541
+ expect(response.status).toBe(200)
542
+ expect('admin' in body).toBe(false)
543
+ expect('quoteToken' in body).toBe(false)
544
+ expect('transferStats' in body).toBe(false)
545
+ })
546
+
547
+ test('returns 200 without TokenCreated fields for a genesis token', async () => {
548
+ const client = app()
549
+ // pathUSD is a genesis token: it has no `TokenCreated` log, so the
550
+ // included fields resolve as absent rather than erroring.
551
+ const response = await client.v1.tokens[':token'].$get(
552
+ {
553
+ param: { token: TestApp.tokenWithHolders },
554
+ query: { include: 'admin,quoteToken,createdAt' },
555
+ },
556
+ TestApp.auth,
557
+ )
558
+ const body = await TestApp.json(response, Tokens.schema.getToken.Response)
559
+
560
+ expect(response.status).toMatchInlineSnapshot(`200`)
561
+ expect('admin' in body).toBe(false)
562
+ expect('quoteToken' in body).toBe(false)
563
+ expect('createdAt' in body).toBe(false)
564
+ })
565
+ })
566
+
567
+ describe('GET /tokens/:token/logo', () => {
568
+ test.runIf(runtime.mode === 'testnet')(
569
+ 'serves the curated R2 icon bytes when present',
570
+ async () => {
571
+ const client = app()
572
+ const response = await client.v1.tokens[':token'].logo.$get(
573
+ { param: { token: TestApp.tokenWithLogo }, query: {} },
574
+ TestApp.auth,
575
+ )
576
+
577
+ expect(response.status).toBe(200)
578
+ expect(response.headers.get('content-type')).toBe('image/svg+xml')
579
+ expect(response.headers.get('cache-control')).toBe(
580
+ 'public, max-age=86400, stale-while-revalidate=604800',
581
+ )
582
+ expect(response.headers.get('vary')).toBe('Accept-Encoding')
583
+ const body = await response.text()
584
+ expect(body.includes('<svg')).toBe(true)
585
+ },
586
+ )
587
+
588
+ test('returns 404 when no curated icon or on-chain logo exists', async () => {
589
+ const client = app({ assetsPath: false })
590
+ const response = await client.v1.tokens[':token'].logo.$get(
591
+ { param: { token: TestApp.token }, query: {} },
592
+ TestApp.auth,
593
+ )
594
+ const body = await TestApp.json(response, Schema.ErrorResponse)
595
+
596
+ expect(response.status).toBe(404)
597
+ expect(body.error.code).toBe('token_logo_not_found')
598
+ })
599
+
600
+ test('returns 404 when the on-chain metadata read reverts for an unregistered token', async () => {
601
+ const client = app()
602
+ const response = await client.v1.tokens[':token'].logo.$get(
603
+ // A syntactically valid address that is not a registered TIP-20: the
604
+ // `getMetadata` read reverts, which must surface as a `404`, not a `502`.
605
+ { param: { token: '0xc97613eb1c39b0b57267739b5400ab08f8c4c285' }, query: {} },
606
+ TestApp.auth,
607
+ )
608
+ const body = await TestApp.json(response, Schema.ErrorResponse)
609
+
610
+ expect(response.status).toBe(404)
611
+ expect(body.error.code).toBe('token_logo_not_found')
612
+ })
613
+
614
+ test('rejects an invalid token address', async () => {
615
+ const response = await TestApp.create().request('/v1/tokens/not-an-address/logo', {
616
+ headers: { authorization: `Bearer ${TestApp.key.token}` },
617
+ })
618
+ const body = await TestApp.json(response, Schema.ErrorResponse)
619
+
620
+ expect(response.status).toBe(400)
621
+ expect(body.error.code).toBe('token_invalid')
622
+ })
623
+ })
624
+
625
+ describe('GET /tokens/:token/holders', () => {
626
+ test('returns a balance-ranked holder page', async () => {
627
+ const client = app()
628
+ const response = await client.v1.tokens[':token'].holders.$get(
629
+ { param: { token: TestApp.tokenWithHolders }, query: { limit: '5' } },
630
+ TestApp.auth,
631
+ )
632
+ const body = await TestApp.json(response, Tokens.schema.getTokenHolders.Response)
633
+ const timing = response.headers.get('server-timing')
634
+
635
+ expect(response.status).toBe(200)
636
+ expect(timing?.includes('token_holders;dur=')).toBe(true)
637
+ expect(Array.isArray(body.data)).toBe(true)
638
+ expect(body.data.length).toBeLessThanOrEqual(5)
639
+ // Pagination fields stay at the response root; cursor pagination has no
640
+ // `page`, just an opaque `nextCursor`.
641
+ expect('page' in body).toBe(false)
642
+ expect(body.nextCursor === null || typeof body.nextCursor === 'string').toBe(true)
643
+ expect(
644
+ body.data.every(
645
+ (holder) => /^0x[0-9a-f]{40}$/.test(holder.address) && /^\d+$/.test(holder.balance),
646
+ ),
647
+ ).toBe(true)
648
+ // Holders are returned largest-first.
649
+ const balances = body.data.map((holder) => BigInt(holder.balance))
650
+ expect(balances.every((balance, index) => index === 0 || balances[index - 1]! >= balance)).toBe(
651
+ true,
652
+ )
653
+ // Token metadata is opt-in: the base page must not embed `meta.token`.
654
+ expect(body.meta).toBeUndefined()
655
+ })
656
+
657
+ test('embeds the exact holder total on demand via include=totalCount', async () => {
658
+ const client = app()
659
+ const response = await client.v1.tokens[':token'].holders.$get(
660
+ { param: { token: TestApp.tokenWithHolders }, query: { include: 'totalCount', limit: '5' } },
661
+ TestApp.auth,
662
+ )
663
+ const body = await TestApp.json(response, Tokens.schema.getTokenHolders.Response)
664
+
665
+ expect(response.status).toBe(200)
666
+ expect(response.headers.get('server-timing')?.includes('token_holders_count;dur=')).toBe(true)
667
+ if (runtime.mode === 'testnet') {
668
+ expect(body.meta?.totalCount).toBeDefined()
669
+ expect(Number.isInteger(body.meta?.totalCount)).toBe(true)
670
+ expect(body.meta!.totalCount!).toBeGreaterThanOrEqual(body.data.length)
671
+ // The holder total comes from the pre-aggregated view, so it is exact.
672
+ expect(body.meta?.totalCountCapped).toBe(false)
673
+ }
674
+
675
+ // Without the include, `meta` is omitted and no count query runs.
676
+ const bareResponse = await client.v1.tokens[':token'].holders.$get(
677
+ { param: { token: TestApp.tokenWithHolders }, query: { limit: '5' } },
678
+ TestApp.auth,
679
+ )
680
+ const bare = await TestApp.json(bareResponse, Tokens.schema.getTokenHolders.Response)
681
+ expect(bareResponse.status).toBe(200)
682
+ expect(bare.meta).toBeUndefined()
683
+ expect(bareResponse.headers.get('server-timing')?.includes('token_holders_count;dur=')).toBe(
684
+ false,
685
+ )
686
+ })
687
+
688
+ test('serves a bounded positional holder page via `page`', async () => {
689
+ const client = app()
690
+ const headResponse = await client.v1.tokens[':token'].holders.$get(
691
+ { param: { token: TestApp.tokenWithHolders }, query: { limit: '10' } },
692
+ TestApp.auth,
693
+ )
694
+ const head = await TestApp.json(headResponse, Tokens.schema.getTokenHolders.Response)
695
+ expect(headResponse.status).toBe(200)
696
+ if (head.data.length < 10) return
697
+
698
+ const offsetResponse = await client.v1.tokens[':token'].holders.$get(
699
+ { param: { token: TestApp.tokenWithHolders }, query: { limit: '5', page: '2' } },
700
+ TestApp.auth,
701
+ )
702
+ const page = await TestApp.json(offsetResponse, Tokens.schema.getTokenHolders.Response)
703
+ expect(offsetResponse.status).toBe(200)
704
+ // The balance ranking is stable within the test window, so the offset page
705
+ // matches rows 5..9 of the head page (best-effort, as with cursor pages on
706
+ // a mutable ranking).
707
+ expect(page.data.map((holder) => holder.address)).toEqual(
708
+ head.data.slice(5, 10).map((holder) => holder.address),
709
+ )
710
+ })
711
+
712
+ test('walks the cursor to a disjoint next holder page', async () => {
713
+ const client = app()
714
+ const first = await client.v1.tokens[':token'].holders.$get(
715
+ { param: { token: TestApp.tokenWithHolders }, query: { limit: '5' } },
716
+ TestApp.auth,
717
+ )
718
+ const firstBody = await TestApp.json(first, Tokens.schema.getTokenHolders.Response)
719
+ expect(first.status).toBe(200)
720
+ if (!firstBody.nextCursor) return
721
+
722
+ const second = await client.v1.tokens[':token'].holders.$get(
723
+ {
724
+ param: { token: TestApp.tokenWithHolders },
725
+ query: { limit: '5', cursor: firstBody.nextCursor },
726
+ },
727
+ TestApp.auth,
728
+ )
729
+ const secondBody = await TestApp.json(second, Tokens.schema.getTokenHolders.Response)
730
+
731
+ expect(second.status).toBe(200)
732
+ expect(secondBody.data.length).toBeGreaterThan(0)
733
+ // Walking the cursor must yield a disjoint next page (the mixed-direction
734
+ // ranking keyset, exercised end-to-end against ClickHouse).
735
+ const firstAddresses = new Set(firstBody.data.map((holder) => holder.address))
736
+ expect(secondBody.data.every((holder) => !firstAddresses.has(holder.address))).toBe(true)
737
+ })
738
+
739
+ test('embeds token metadata when requested via include=token', async () => {
740
+ const client = app()
741
+ const response = await client.v1.tokens[':token'].holders.$get(
742
+ { param: { token: TestApp.tokenWithHolders }, query: { limit: '5', include: 'token' } },
743
+ TestApp.auth,
744
+ )
745
+ const body = await TestApp.json(response, Tokens.schema.getTokenHolders.Response)
746
+ const timing = response.headers.get('server-timing')
747
+
748
+ expect(response.status).toBe(200)
749
+ expect(Array.isArray(body.data)).toBe(true)
750
+ // The embedded token resolves alongside the holder page.
751
+ expect(timing?.includes('token;dur=')).toBe(true)
752
+ expect(body.meta?.token?.address).toBe(TestApp.tokenWithHolders)
753
+ expect(typeof body.meta?.token?.symbol).toBe('string')
754
+ expect(Number.isInteger(body.meta?.token?.decimals)).toBe(true)
755
+ expect(typeof body.meta?.token?.verified).toBe('boolean')
756
+ // The embed is a trimmed reference: supply/holder/created fields are omitted.
757
+ expect(body.meta?.token).not.toHaveProperty('totalSupply')
758
+ expect(body.meta?.token).not.toHaveProperty('holderCount')
759
+ expect(body.meta?.token).not.toHaveProperty('createdAt')
760
+ })
761
+
762
+ test('surfaces a 502 when holder data is unavailable', async () => {
763
+ // Point at an unreachable indexer so the upstream query fails deterministically.
764
+ const client = app({ tidx: { baseUrl: 'http://127.0.0.1:1' } })
765
+ const response = await client.v1.tokens[':token'].holders.$get(
766
+ { param: { token: TestApp.token }, query: {} },
767
+ TestApp.auth,
768
+ )
769
+ const body = await TestApp.json(response, Schema.ErrorResponse)
770
+ // The upstream message varies by failure; only the envelope is stable.
771
+ const { requestId, error } = body
772
+ const { message, ...stableError } = error
773
+
774
+ // Unlike the opt-in holder count (optional enrichment that degrades to
775
+ // null), this dedicated endpoint reports upstream failure honestly rather
776
+ // than masquerading as a token with zero holders.
777
+ expect(response.status).toMatchInlineSnapshot(`502`)
778
+ expect(typeof requestId).toMatchInlineSnapshot(`"string"`)
779
+ expect(typeof message).toMatchInlineSnapshot(`"string"`)
780
+ expect(stableError).toMatchInlineSnapshot(`
781
+ {
782
+ "code": "upstream_error",
783
+ }
784
+ `)
785
+ })
786
+
787
+ test('rejects invalid token addresses', async () => {
788
+ // The typed client requires `token: `Hex.Hex`` so an invalid literal
789
+ // cannot pass through it; exercise the validator via `app.request`.
790
+ const response = await TestApp.create().request('/v1/tokens/not-an-address/holders', {
791
+ headers: { authorization: `Bearer ${TestApp.key.token}` },
792
+ })
793
+ const body = await TestApp.json(response, Schema.ErrorResponse)
794
+ const { requestId, ...stable } = body
795
+
796
+ expect(response.status).toMatchInlineSnapshot(`400`)
797
+ expect(typeof requestId).toMatchInlineSnapshot(`"string"`)
798
+ expect(stable).toMatchInlineSnapshot(`
799
+ {
800
+ "error": {
801
+ "code": "token_invalid",
802
+ "details": [
803
+ {
804
+ "message": "Invalid input",
805
+ "path": [
806
+ "token",
807
+ ],
808
+ },
809
+ ],
810
+ "message": "Invalid token address",
811
+ },
812
+ }
813
+ `)
814
+ })
815
+
816
+ test('rejects invalid pagination', async () => {
817
+ const client = app()
818
+ const response = await client.v1.tokens[':token'].holders.$get(
819
+ { param: { token: TestApp.token }, query: { limit: '0' } },
820
+ TestApp.auth,
821
+ )
822
+ const body = await TestApp.json(response, Schema.ErrorResponse)
823
+ const { requestId, ...stable } = body
824
+
825
+ expect(response.status).toMatchInlineSnapshot(`400`)
826
+ expect(typeof requestId).toMatchInlineSnapshot(`"string"`)
827
+ expect(stable).toMatchInlineSnapshot(`
828
+ {
829
+ "error": {
830
+ "code": "query_invalid",
831
+ "details": [
832
+ {
833
+ "message": "Too small: expected number to be >=5",
834
+ "path": [
835
+ "limit",
836
+ ],
837
+ },
838
+ ],
839
+ "message": "Invalid query parameters",
840
+ },
841
+ }
842
+ `)
843
+ })
844
+ })
845
+
846
+ describe('GET /tokens/:symbol', () => {
847
+ // Hono encodes the regex-constrained `/:symbol{<pattern>}` route into the
848
+ // typed client under a literal path key that includes the full pattern.
849
+ test.runIf(runtime.mode === 'testnet')(
850
+ 'returns verified token metadata with live supply by symbol',
851
+ async () => {
852
+ const client = app()
853
+ const response = await client.v1.tokens[':symbol{[A-Za-z][A-Za-z0-9._]{0,63}}'].$get(
854
+ { param: { symbol: 'USDC.e' }, query: {} },
855
+ TestApp.auth,
856
+ )
857
+ const body = await TestApp.json(response, Tokens.schema.getToken.Response)
858
+ const { totalSupply, ...stable } = body
859
+
860
+ expect(response.status).toMatchInlineSnapshot(`200`)
861
+ expect(/^\d+$/.test(totalSupply!)).toMatchInlineSnapshot(`true`)
862
+ expect(stable).toMatchInlineSnapshot(`
863
+ {
864
+ "address": "0x20c0000000000000000000009e8d7eb59b783726",
865
+ "currency": "USDC",
866
+ "decimals": 6,
867
+ "id": "0x20c0000000000000000000009e8d7eb59b783726",
868
+ "logoUri": "http://localhost/assets/42431/icons/0x20c0000000000000000000009e8d7eb59b783726",
869
+ "name": "Bridged USDC (Stargate)",
870
+ "symbol": "USDC.e",
871
+ "verified": true,
872
+ }
873
+ `)
874
+ },
875
+ )
876
+
877
+ test('does not resolve unverified token symbols', async () => {
878
+ const client = app()
879
+ const response = await client.v1.tokens[':symbol{[A-Za-z][A-Za-z0-9._]{0,63}}'].$get(
880
+ { param: { symbol: 'ALD' }, query: {} },
881
+ TestApp.auth,
882
+ )
883
+ const body = await TestApp.json(response, Schema.ErrorResponse)
884
+ const { requestId, ...stable } = body
885
+
886
+ expect(response.status).toMatchInlineSnapshot(`404`)
887
+ expect(response.headers.get('tempo-request-id') === requestId).toMatchInlineSnapshot(`true`)
888
+ expect(typeof requestId).toMatchInlineSnapshot(`"string"`)
889
+ expect(stable).toMatchInlineSnapshot(`
890
+ {
891
+ "error": {
892
+ "code": "token_not_found",
893
+ "message": "Token not found",
894
+ },
895
+ }
896
+ `)
897
+ })
898
+
899
+ test.runIf(runtime.mode === 'testnet')(
900
+ 'resolves verified symbols without a logo when no asset loader is configured',
901
+ async () => {
902
+ const client = app({ assetsPath: false })
903
+ const response = await client.v1.tokens[':symbol{[A-Za-z][A-Za-z0-9._]{0,63}}'].$get(
904
+ { param: { symbol: 'USDC.e' }, query: {} },
905
+ TestApp.auth,
906
+ )
907
+ const body = await TestApp.json(response, Tokens.schema.getToken.Response)
908
+ const { totalSupply, ...stable } = body
909
+
910
+ expect(response.status).toMatchInlineSnapshot(`200`)
911
+ expect(/^\d+$/.test(totalSupply!)).toMatchInlineSnapshot(`true`)
912
+ expect(stable).toMatchInlineSnapshot(`
913
+ {
914
+ "address": "0x20c0000000000000000000009e8d7eb59b783726",
915
+ "currency": "USDC",
916
+ "decimals": 6,
917
+ "id": "0x20c0000000000000000000009e8d7eb59b783726",
918
+ "name": "Bridged USDC (Stargate)",
919
+ "symbol": "USDC.e",
920
+ "verified": true,
921
+ }
922
+ `)
923
+ },
924
+ )
925
+ })
926
+
927
+ describe('GET /tokens/:token/transactions', () => {
928
+ test('returns a transaction page touching the token contract', async () => {
929
+ const client = app()
930
+ const response = await client.v1.tokens[':token'].transactions.$get(
931
+ { param: { token: TestApp.token }, query: { limit: '5' } },
932
+ TestApp.auth,
933
+ )
934
+ const body = await TestApp.json(response, Tokens.schema.getTokenTransactions.Response)
935
+
936
+ expect(response.status).toBe(200)
937
+ expect(Array.isArray(body.data)).toBe(true)
938
+ expect(body.data.length).toBeLessThanOrEqual(5)
939
+ expect(body.nextCursor === null || typeof body.nextCursor === 'string').toBe(true)
940
+ // Every transaction either originated from or was sent to the token contract.
941
+ for (const tx of body.data) {
942
+ const involves = tx.sender === TestApp.token || tx.recipient === TestApp.token
943
+ expect(involves).toBe(true)
944
+ }
945
+ // Default ordering is newest-first (descending block then in-block index).
946
+ const blocks = body.data
947
+ .map((entry) => entry.blockNumber)
948
+ .filter((b): b is number => b !== null)
949
+ expect(blocks.every((block, index) => index === 0 || blocks[index - 1]! >= block)).toBe(true)
950
+ // Token metadata is opt-in: the base page must not embed it.
951
+ expect(body.meta).toBeUndefined()
952
+ })
953
+
954
+ test('embeds a capped total count on demand via include=totalCount', async () => {
955
+ const client = app()
956
+ const response = await client.v1.tokens[':token'].transactions.$get(
957
+ { param: { token: TestApp.token }, query: { limit: '5', include: 'totalCount' } },
958
+ TestApp.auth,
959
+ )
960
+ const body = await TestApp.json(response, Tokens.schema.getTokenTransactions.Response)
961
+
962
+ expect(response.status).toBe(200)
963
+ expect(body.meta?.totalCount).toBeDefined()
964
+ expect(Number.isInteger(body.meta?.totalCount)).toBe(true)
965
+ expect(body.meta!.totalCount!).toBeGreaterThanOrEqual(body.data.length)
966
+ expect(body.meta!.totalCount!).toBeLessThanOrEqual(Schema.countCap)
967
+ expect(body.meta!.totalCountCapped).toBe(body.meta!.totalCount! >= Schema.countCap)
968
+ // `token` was not requested, so only the count rides in `meta`.
969
+ expect(body.meta?.token).toBeUndefined()
970
+ })
971
+
972
+ test('embeds both the scoped token and the total count together', async () => {
973
+ const client = app()
974
+ const response = await client.v1.tokens[':token'].transactions.$get(
975
+ { param: { token: TestApp.token }, query: { limit: '5', include: 'token,totalCount' } },
976
+ TestApp.auth,
977
+ )
978
+ const body = await TestApp.json(response, Tokens.schema.getTokenTransactions.Response)
979
+
980
+ expect(response.status).toBe(200)
981
+ expect(body.meta?.token?.address).toBe(TestApp.token)
982
+ expect(Number.isInteger(body.meta?.totalCount)).toBe(true)
983
+ })
984
+
985
+ test('embeds the scoped token when requested via include=token', async () => {
986
+ const client = app()
987
+ const response = await client.v1.tokens[':token'].transactions.$get(
988
+ { param: { token: TestApp.token }, query: { limit: '5', include: 'token' } },
989
+ TestApp.auth,
990
+ )
991
+ const body = await TestApp.json(response, Tokens.schema.getTokenTransactions.Response)
992
+
993
+ expect(response.status).toBe(200)
994
+ expect(body.meta?.token?.address).toBe(TestApp.token)
995
+ expect(typeof body.meta?.token?.symbol).toBe('string')
996
+ // The embed is a trimmed reference: supply/holder/created fields are omitted.
997
+ expect(body.meta?.token).not.toHaveProperty('totalSupply')
998
+ expect(body.meta?.token).not.toHaveProperty('holderCount')
999
+ expect(body.meta?.token).not.toHaveProperty('createdAt')
1000
+ })
1001
+
1002
+ test('walks the cursor to a disjoint next transaction page', async () => {
1003
+ const client = app()
1004
+ const first = await client.v1.tokens[':token'].transactions.$get(
1005
+ { param: { token: TestApp.token }, query: { limit: '5' } },
1006
+ TestApp.auth,
1007
+ )
1008
+ const firstBody = await TestApp.json(first, Tokens.schema.getTokenTransactions.Response)
1009
+ expect(first.status).toBe(200)
1010
+ if (!firstBody.nextCursor) return
1011
+
1012
+ const second = await client.v1.tokens[':token'].transactions.$get(
1013
+ {
1014
+ param: { token: TestApp.token },
1015
+ query: { limit: '5', cursor: firstBody.nextCursor },
1016
+ },
1017
+ TestApp.auth,
1018
+ )
1019
+ const secondBody = await TestApp.json(second, Tokens.schema.getTokenTransactions.Response)
1020
+
1021
+ expect(second.status).toBe(200)
1022
+ expect(secondBody.data.length).toBeGreaterThan(0)
1023
+ // Walking the cursor must yield a disjoint next page.
1024
+ const firstHashes = new Set(firstBody.data.map((entry) => entry.hash))
1025
+ expect(secondBody.data.every((entry) => !firstHashes.has(entry.hash))).toBe(true)
1026
+ })
1027
+
1028
+ test('returns 400 for an invalid token address', async () => {
1029
+ // The typed client requires `token: `Hex.Hex`` so an invalid literal
1030
+ // cannot pass through it; exercise the validator via `app.request`.
1031
+ const response = await TestApp.create().request('/v1/tokens/not-an-address/transactions', {
1032
+ headers: { authorization: `Bearer ${TestApp.key.token}` },
1033
+ })
1034
+ const body = await TestApp.json(response, Schema.ErrorResponse)
1035
+ expect(response.status).toBe(400)
1036
+ expect(body.error.code).toBe('token_invalid')
1037
+ })
1038
+
1039
+ // Regression: high-traffic token contracts (verified `pathUSD` is the
1040
+ // busiest token on testnet) used to crash TIDX with `422 db error` because
1041
+ // the planner can't handle a UNION-of-sides whose `ORDER BY` lives in the
1042
+ // outer query. Pushing the per-side `ORDER BY block_num DESC, idx DESC
1043
+ // LIMIT N+1` *inside* each branch lets the planner use each side's index
1044
+ // and keeps the response fast.
1045
+ test('returns a transaction page for a high-traffic token contract', async () => {
1046
+ const client = app()
1047
+ const pathUsd = '0x20c0000000000000000000000000000000000000'
1048
+ const response = await client.v1.tokens[':token'].transactions.$get(
1049
+ { param: { token: pathUsd }, query: { limit: '5' } },
1050
+ TestApp.auth,
1051
+ )
1052
+ const body = await TestApp.json(response, Tokens.schema.getTokenTransactions.Response)
1053
+
1054
+ expect(response.status).toBe(200)
1055
+ expect(Array.isArray(body.data)).toBe(true)
1056
+ expect(body.data.length).toBeGreaterThan(0)
1057
+ for (const tx of body.data) {
1058
+ const involves = tx.sender === pathUsd || tx.recipient === pathUsd
1059
+ expect(involves).toBe(true)
1060
+ }
1061
+ })
1062
+ })