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,1121 @@
1
+ import { Hono, type Context } from 'hono'
2
+ import { AbiEvent, AbiParameters, Hash, type Hex, Value as core_Value } from 'ox'
3
+ import { Abis, Addresses } from 'viem/tempo'
4
+ import * as z from 'zod/mini'
5
+
6
+ import type * as App from '../App.js'
7
+ import * as Auth from '../internal/Auth.js'
8
+ import * as Cache from '../internal/Cache.js'
9
+ import * as Cursor from '../internal/Cursor.js'
10
+ import * as OpenApi from '../internal/OpenApi.js'
11
+ import * as Response from '../internal/Response.js'
12
+ import * as Schema from '../internal/Schema.js'
13
+ import * as Store from '../internal/Store.js'
14
+ import * as Timing from '../internal/Timing.js'
15
+ import * as Ttl from '../internal/Ttl.js'
16
+ import * as Value from '../internal/Value.js'
17
+ import * as VerifiedTokens from '../internal/VerifiedTokens.js'
18
+ import * as Tokens from './tokens.js'
19
+
20
+ // The protocol Fee AMM lives at the reserved FeeManager address. Pools are
21
+ // `(userToken, validatorToken)` pairs discovered from the `Mint` events the
22
+ // FeeManager emits; there is no on-chain pool registry to enumerate.
23
+ const feeManager = Addresses.feeManager.toLowerCase()
24
+
25
+ // pathUSD is the protocol's default fee token: a transaction whose fee token
26
+ // matches the validator's token (or falls back to pathUSD) pays fees without
27
+ // needing a Fee AMM pool, so pathUSD is always fee-usable even when it has no
28
+ // user-side pool. Lowercased to match the indexer's stored address casing.
29
+ const pathUsd = Addresses.pathUsd.toLowerCase()
30
+
31
+ // Topic0 hashes of the FeeManager `Mint` event, current and legacy. Both
32
+ // signatures index `userToken` as topic2 and `validatorToken` as topic3, but
33
+ // their other fields differ, so rows are decoded per selector: the current
34
+ // event carries data words `[sender, amountValidatorToken, liquidity]` with
35
+ // topic1 = `to`; the legacy event carries `[amountUserToken,
36
+ // amountValidatorToken, liquidity]` with topic1 = `sender`. Queries hit the
37
+ // raw `logs` table rather than a signature CTE: the event-CTE planner rejects
38
+ // filters on event params, and the two signatures need different decoders
39
+ // anyway.
40
+ const mintTopic = AbiEvent.getSelector(
41
+ 'event Mint(address sender, address indexed to, address indexed userToken, address indexed validatorToken, uint256 amountValidatorToken, uint256 liquidity)',
42
+ )
43
+ const legacyMintTopic = AbiEvent.getSelector(
44
+ 'event Mint(address indexed sender, address indexed userToken, address indexed validatorToken, uint256 amountUserToken, uint256 amountValidatorToken, uint256 liquidity)',
45
+ )
46
+
47
+ /** Zod schemas owned by the fee AMM handlers. */
48
+ export namespace schema {
49
+ /**
50
+ * A token referenced by a fee pool: its contract address plus best-effort
51
+ * metadata. Mirrors the transfer feed's `sourceToken` technique: `address`
52
+ * is always present; the metadata fields spread in only when resolution
53
+ * succeeds, so a token whose metadata is unavailable keeps just the bare
54
+ * reference. The pools listing extends this shape with the side's reserve
55
+ * (`getFeeAmmPools.Token`).
56
+ */
57
+ export const PoolToken = Schema.describe(
58
+ z.object({
59
+ address: Schema.TokenAddress.check(
60
+ z.describe('TIP-20 token contract address used by this fee pool.'),
61
+ ),
62
+ currency: z
63
+ .optional(z.string())
64
+ .check(
65
+ z.describe('Display currency for the token, when available.'),
66
+ z.meta({ examples: ['USD'] }),
67
+ ),
68
+ decimals: z
69
+ .optional(z.number().check(z.int(), z.nonnegative()))
70
+ .check(z.describe('Number of decimal places the token uses.'), z.meta({ examples: [6] })),
71
+ logoUri: z.optional(z.string()).check(
72
+ z.describe('URL for the token logo SVG, when available.'),
73
+ z.meta({
74
+ examples: [
75
+ 'https://api.tempo.xyz/data/4217/icons/0x20c0000000000000000000008f5425160ebe5525.svg',
76
+ ],
77
+ }),
78
+ ),
79
+ name: z
80
+ .optional(z.string())
81
+ .check(z.describe('Human-readable token name.'), z.meta({ examples: ['USD Coin'] })),
82
+ symbol: z
83
+ .optional(z.string())
84
+ .check(z.describe('Short token ticker symbol.'), z.meta({ examples: ['USDC'] })),
85
+ verified: z
86
+ .optional(z.boolean())
87
+ .check(
88
+ z.describe('Whether Tempo has verified this token’s metadata.'),
89
+ z.meta({ examples: [true] }),
90
+ ),
91
+ }),
92
+ 'A TIP-20 token used by a Fee AMM pool, with metadata when available.',
93
+ )
94
+
95
+ /** Schemas for the getFeeAmmPools operation. */
96
+ export namespace getFeeAmmPools {
97
+ /** Optional related resources that callers opt into via `include`. */
98
+ export const Include = z
99
+ .enum(['token'])
100
+ .check(z.describe('Optional related data you can request with `include`.'))
101
+
102
+ /**
103
+ * Parses a comma-separated `include` query value into a list of optional
104
+ * resources to embed. Token metadata resolution is one extra (batched)
105
+ * `token.getMetadata` round trip, so it runs only when `include=token` is
106
+ * requested; the base page returns each side's bare `address` plus its
107
+ * reserve and skips the lookup entirely.
108
+ */
109
+ export const includeQuery = Schema.includeQuery(
110
+ Include,
111
+ 'Comma-separated related resources to include, such as `token` metadata.',
112
+ )
113
+
114
+ /** Query parameters for the fee pool list request. */
115
+ export const Query = z
116
+ .strictObject({
117
+ chainId: Schema.ChainIdQuery,
118
+ cursor: Schema.Cursor,
119
+ include: includeQuery,
120
+ limit: Schema.Limit,
121
+ page: Schema.Page,
122
+ })
123
+ .check(...Schema.pageChecks(), z.describe('Query parameters for listing Fee AMM pools.'))
124
+
125
+ /**
126
+ * One side of a fee pool: the token reference carrying the side's current
127
+ * on-chain reserve. `amount`/`formatted` come from `FeeManager.getPool`
128
+ * and are absent when the read fails (best-effort, like the metadata
129
+ * fields).
130
+ */
131
+ export const Token = Schema.describe(
132
+ z.extend(PoolToken, {
133
+ amount: z
134
+ .optional(Schema.DecimalString)
135
+ .check(
136
+ z.describe(
137
+ 'Current onchain reserve for this token, in base units, from `FeeManager.getPool`. Omitted if the read fails.',
138
+ ),
139
+ ),
140
+ formatted: z
141
+ .optional(z.string())
142
+ .check(
143
+ z.describe(
144
+ 'Reserve as a human-readable decimal string using the token’s `decimals`. Omitted if the read fails.',
145
+ ),
146
+ z.meta({ examples: ['1.5'] }),
147
+ ),
148
+ }),
149
+ 'One token side of a Fee AMM pool, including its current onchain reserve when available.',
150
+ )
151
+
152
+ /** A fee AMM pool, aggregated from its `Mint` events. */
153
+ export const Pool = z
154
+ .object({
155
+ createdAt: z.iso
156
+ .datetime()
157
+ .check(
158
+ z.describe('Timestamp when liquidity was first minted into this pool.'),
159
+ z.meta({ examples: ['2024-01-01T00:00:00Z'] }),
160
+ ),
161
+ id: Schema.Hash.check(
162
+ z.describe('Stable resource id for the pool; this is the same value as `poolId`.'),
163
+ ),
164
+ lastMintAt: z.iso
165
+ .datetime()
166
+ .check(
167
+ z.describe('Timestamp when liquidity was most recently minted into this pool.'),
168
+ z.meta({ examples: ['2024-01-01T00:00:00Z'] }),
169
+ ),
170
+ mintCount: z
171
+ .number()
172
+ .check(
173
+ z.int(),
174
+ z.nonnegative(),
175
+ z.describe('Number of liquidity mints into this pool.'),
176
+ z.meta({ examples: [25] }),
177
+ ),
178
+ poolId: Schema.Hash.check(
179
+ z.describe(
180
+ 'Pool id computed as `keccak256(abi.encode(userToken, validatorToken))`, matching `FeeManager.getPoolId`.',
181
+ ),
182
+ ),
183
+ userToken: Token.check(
184
+ z.describe(
185
+ 'Token users pay fees with in this pool, including its current reserve when available.',
186
+ ),
187
+ ),
188
+ validatorToken: Token.check(
189
+ z.describe(
190
+ 'Token validators receive from this pool, including its current reserve when available.',
191
+ ),
192
+ ),
193
+ })
194
+ .check(z.describe('A Fee AMM pool discovered from its onchain `Mint` events.'))
195
+
196
+ /** Page of fee pools, ordered by mint count (most active first). */
197
+ export const Response = z
198
+ .object({
199
+ data: z
200
+ .array(Pool)
201
+ .check(z.describe('Fee AMM pools ordered by mint count, most active first.')),
202
+ nextCursor: Schema.NextCursor,
203
+ })
204
+ .check(z.describe('A page of Fee AMM pools ordered by mint count, most active first.'))
205
+ }
206
+
207
+ /** Schemas for the getFeeAmmMints operation. */
208
+ export namespace getFeeAmmMints {
209
+ /** Query parameters for the pool mint list request. */
210
+ export const Query = z
211
+ .strictObject({
212
+ chainId: Schema.ChainIdQuery,
213
+ cursor: Schema.Cursor,
214
+ include: Schema.totalCountInclude,
215
+ limit: Schema.Limit,
216
+ order: Schema.Order,
217
+ page: Schema.Page,
218
+ userToken: z
219
+ .optional(Schema.TokenAddress)
220
+ .check(z.describe('Only include mints for pools where users paid fees with this token.')),
221
+ validatorToken: z
222
+ .optional(Schema.TokenAddress)
223
+ .check(z.describe('Only include mints for pools where validators receive this token.')),
224
+ })
225
+ .check(
226
+ ...Schema.pageChecks(),
227
+ z.describe('Query parameters for listing Fee AMM liquidity mints.'),
228
+ )
229
+
230
+ /** A single liquidity mint into a fee pool. */
231
+ export const Mint = z
232
+ .object({
233
+ amountUserToken: z
234
+ .optional(Schema.DecimalString)
235
+ .check(
236
+ z.describe(
237
+ 'Amount of the user-side token deposited, in base units. Only present on legacy-signature mints.',
238
+ ),
239
+ ),
240
+ amountValidatorToken: Schema.DecimalString.check(
241
+ z.describe('Amount of the validator-side token deposited, in base units.'),
242
+ ),
243
+ blockNumber: z
244
+ .number()
245
+ .check(
246
+ z.int(),
247
+ z.nonnegative(),
248
+ z.describe('Block number where the mint occurred.'),
249
+ z.meta({ examples: [23456789] }),
250
+ ),
251
+ id: z.string().check(
252
+ z.describe(
253
+ 'Stable mint id built from the transaction hash and log index (`${transactionHash}-${logIndex}`).',
254
+ ),
255
+ z.meta({
256
+ examples: ['0x515801d7f9a5ac705e793e85904c9c69b3f1694b465cc8fb6ba3f0298dc82665-0'],
257
+ }),
258
+ ),
259
+ liquidity: Schema.DecimalString.check(
260
+ z.describe('Amount of pool liquidity minted, as an integer string.'),
261
+ ),
262
+ logIndex: z
263
+ .number()
264
+ .check(
265
+ z.int(),
266
+ z.nonnegative(),
267
+ z.describe('Log index of this mint within the block.'),
268
+ z.meta({ examples: [0] }),
269
+ ),
270
+ minter: Schema.Address.check(z.describe('Address that deposited tokens into the pool.')),
271
+ recipient: z
272
+ .optional(Schema.Address)
273
+ .check(
274
+ z.describe(
275
+ 'Address that received the minted liquidity. Only present on current-signature mints.',
276
+ ),
277
+ ),
278
+ timestamp: z.iso
279
+ .datetime()
280
+ .check(
281
+ z.describe('Block timestamp when the mint occurred.'),
282
+ z.meta({ examples: ['2024-01-01T00:00:00Z'] }),
283
+ ),
284
+ transactionHash: Schema.Hash.check(
285
+ z.describe('Hash of the transaction that emitted this mint.'),
286
+ ),
287
+ userToken: PoolToken.check(z.describe('Token users pay fees with in this pool.')),
288
+ validatorToken: PoolToken.check(z.describe('Token validators receive from this pool.')),
289
+ })
290
+ .check(z.describe('One liquidity mint into a Fee AMM pool.'))
291
+
292
+ /** Page of fee pool mints, ordered by block then log index. */
293
+ export const Response = z
294
+ .object({
295
+ data: z.array(Mint).check(z.describe('Liquidity mints returned on this page.')),
296
+ meta: z
297
+ .optional(Schema.CountMeta)
298
+ .check(
299
+ z.describe('Response-wide metadata requested with `include`, such as `totalCount`.'),
300
+ ),
301
+ nextCursor: Schema.NextCursor,
302
+ })
303
+ .check(z.describe('A page of Fee AMM liquidity mints.'))
304
+ }
305
+ }
306
+
307
+ /** Creates fee AMM handlers. */
308
+ export function feeAmm() {
309
+ return new Hono<App.Environment>()
310
+ .get(
311
+ '/pools',
312
+ Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }),
313
+ OpenApi.validate('query', schema.getFeeAmmPools.Query, {
314
+ code: 'query_invalid',
315
+ message: 'Invalid query parameters',
316
+ }),
317
+ OpenApi.describeRoute({
318
+ description:
319
+ 'Lists Fee AMM pools, most active first, so you can see which stablecoin fee conversions have the most liquidity activity.',
320
+ operationId: 'getFeeAmmPools',
321
+ responses: OpenApi.responses({
322
+ errors: { 502: 'The upstream Tempo indexer could not complete the request.' },
323
+ success: {
324
+ description: 'List of Fee AMM pools.',
325
+ schema: schema.getFeeAmmPools.Response,
326
+ },
327
+ }),
328
+ summary: 'List pools',
329
+ tags: ['Fee AMM'],
330
+ }),
331
+ Cache.response<App.Environment>({
332
+ cacheControl: Cache.policies.state,
333
+ name: 'tempo-api:fee-amm:v1',
334
+ key: (c) => Cache.urlKey(c, schema.getFeeAmmPools.Query),
335
+ }),
336
+ async (c) => {
337
+ if (Auth.narrowAccess) return Auth.paidAccessError(c)
338
+ if (OpenApi.narrowValidation)
339
+ return OpenApi.validationError(c, {
340
+ code: 'query_invalid',
341
+ message: 'Invalid query parameters',
342
+ })
343
+
344
+ const query = c.req.valid('query')
345
+ const chainId = query.chainId ?? c.get('chainId')
346
+
347
+ try {
348
+ const page = await listPools(c, {
349
+ chainId,
350
+ cursor: query.cursor,
351
+ limit: query.limit,
352
+ page: query.page,
353
+ })
354
+ const pools = page.data
355
+
356
+ // Reserves are one `getPool` eth_call per pool; viem auto-batches
357
+ // the concurrent calls into a single deployless multicall round
358
+ // trip (see `internal/Viem.ts`).
359
+ const reserves = await Timing.time(c, 'fee_amm_reserves', () =>
360
+ Promise.all(
361
+ pools.map((pool) =>
362
+ getPoolReserves(c, {
363
+ chainId,
364
+ userToken: pool.userToken,
365
+ validatorToken: pool.validatorToken,
366
+ }),
367
+ ),
368
+ ),
369
+ )
370
+
371
+ // Token metadata is one extra (batched) `token.getMetadata` round
372
+ // trip, opt-in via `include=token`. Without it each side carries
373
+ // just its `address` and reserve; reserves still format via the
374
+ // `reserve` helper's TIP-20 (6-decimal) fallback.
375
+ const tokensByAddress = query.include.includes('token')
376
+ ? await resolveTokenMap(c, {
377
+ addresses: Array.from(
378
+ new Set(pools.flatMap((pool) => [pool.userToken, pool.validatorToken])),
379
+ ),
380
+ chainId,
381
+ })
382
+ : undefined
383
+
384
+ const data = pools.map((pool, index) => {
385
+ const id = poolId(pool.userToken, pool.validatorToken)
386
+ const userToken = { address: pool.userToken, ...tokensByAddress?.get(pool.userToken) }
387
+ const validatorToken = {
388
+ address: pool.validatorToken,
389
+ ...tokensByAddress?.get(pool.validatorToken),
390
+ }
391
+ // Each side's reserve rides on its token reference (like the
392
+ // metadata fields): absent when the `getPool` read failed.
393
+ return {
394
+ createdAt: pool.createdAt,
395
+ id,
396
+ lastMintAt: pool.lastMintAt,
397
+ mintCount: pool.mintCount,
398
+ poolId: id,
399
+ userToken: {
400
+ ...userToken,
401
+ ...reserve(reserves[index]?.userToken, userToken.decimals),
402
+ },
403
+ validatorToken: {
404
+ ...validatorToken,
405
+ ...reserve(reserves[index]?.validatorToken, validatorToken.decimals),
406
+ },
407
+ }
408
+ })
409
+
410
+ return c.json(
411
+ Response.validated(schema.getFeeAmmPools.Response, {
412
+ data,
413
+ nextCursor: page.nextCursor,
414
+ }),
415
+ 200,
416
+ )
417
+ } catch (cause) {
418
+ return Response.upstream(c, cause)
419
+ }
420
+ },
421
+ )
422
+ .get(
423
+ '/mints',
424
+ Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }),
425
+ OpenApi.validate('query', schema.getFeeAmmMints.Query, {
426
+ code: 'query_invalid',
427
+ message: 'Invalid query parameters',
428
+ }),
429
+ OpenApi.describeRoute({
430
+ description: 'Lists liquidity mints into Fee AMM pools.',
431
+ operationId: 'getFeeAmmMints',
432
+ responses: OpenApi.responses({
433
+ errors: { 502: 'The upstream Tempo indexer could not complete the request.' },
434
+ success: {
435
+ description: 'Page of Fee AMM liquidity mints.',
436
+ schema: schema.getFeeAmmMints.Response,
437
+ },
438
+ }),
439
+ summary: 'List mints',
440
+ tags: ['Fee AMM'],
441
+ }),
442
+ Cache.response<App.Environment>({
443
+ cacheControl: Cache.policies.feed,
444
+ name: 'tempo-api:fee-amm:v1',
445
+ key: (c) => Cache.urlKey(c, schema.getFeeAmmMints.Query),
446
+ }),
447
+ async (c) => {
448
+ if (Auth.narrowAccess) return Auth.paidAccessError(c)
449
+ if (OpenApi.narrowValidation)
450
+ return OpenApi.validationError(c, {
451
+ code: 'query_invalid',
452
+ message: 'Invalid query parameters',
453
+ })
454
+
455
+ const query = c.req.valid('query')
456
+ const chainId = query.chainId ?? c.get('chainId')
457
+
458
+ // A cursor page is anchored below the head, so its contents are
459
+ // append-only and effectively immutable; cache it aggressively. The
460
+ // head page (no cursor) keeps the route's short `feed` default.
461
+ if (query.cursor !== undefined) Cache.setPolicy(c, Cache.policies.immutable)
462
+
463
+ try {
464
+ // `totalCount` is opt-in and shares the page's filters; run it
465
+ // concurrently with the page. Best-effort: a count failure omits
466
+ // `meta` rather than failing the page.
467
+ const countPromise = query.include.includes('totalCount')
468
+ ? countMints(c, {
469
+ chainId,
470
+ userToken: query.userToken,
471
+ validatorToken: query.validatorToken,
472
+ }).catch(() => undefined)
473
+ : undefined
474
+
475
+ const page = await listMints(c, {
476
+ chainId,
477
+ cursor: query.cursor,
478
+ limit: query.limit,
479
+ order: query.order,
480
+ page: query.page,
481
+ userToken: query.userToken,
482
+ validatorToken: query.validatorToken,
483
+ })
484
+
485
+ // Token references take the same enriched shape as the pools
486
+ // listing. The page's unique tokens are few (often one pool), and
487
+ // each resolution is memoized, so this stays one bounded fan-out.
488
+ const tokensByAddress = await resolveTokenMap(c, {
489
+ addresses: Array.from(
490
+ new Set(page.data.flatMap((mint) => [mint.userToken, mint.validatorToken])),
491
+ ),
492
+ chainId,
493
+ })
494
+ const data = page.data.map((mint) => ({
495
+ ...mint,
496
+ userToken: { address: mint.userToken, ...tokensByAddress.get(mint.userToken) },
497
+ validatorToken: {
498
+ address: mint.validatorToken,
499
+ ...tokensByAddress.get(mint.validatorToken),
500
+ },
501
+ }))
502
+
503
+ const meta = countPromise ? await countPromise : undefined
504
+ return c.json(
505
+ Response.validated(schema.getFeeAmmMints.Response, {
506
+ data,
507
+ ...(meta ? { meta } : {}),
508
+ nextCursor: page.nextCursor,
509
+ }),
510
+ 200,
511
+ )
512
+ } catch (cause) {
513
+ return Response.upstream(c, cause)
514
+ }
515
+ },
516
+ )
517
+ }
518
+
519
+ /**
520
+ * Discovers fee pools by aggregating the FeeManager's `Mint` logs into one
521
+ * row per `(userToken, validatorToken)` pair, paginated and ordered by mint
522
+ * count (most active first), so the real fee pools (thousands to millions of
523
+ * mints) lead the head pages and the one-shot spam pairs (mint count 1) sink
524
+ * to the tail.
525
+ *
526
+ * Keyset pagination on `(mint_count, topic2, topic3)`: `mint_count` ranks the
527
+ * page (mutable — it grows as pools mint — so this is best-effort across mint
528
+ * activity, the inherent limit of a ranking feed), and the `(topic2, topic3)`
529
+ * group key is a stable, unique tiebreaker. A malformed cursor falls back to
530
+ * the head page.
531
+ */
532
+ function listPools(c: Context<App.Environment>, options: listPools.Options) {
533
+ const { chainId, limit } = options
534
+ const store = c.get('store')
535
+ const tidx = c.get('getTidx')(chainId)
536
+ const cursor = options.cursor
537
+ ? Cursor.decode(options.cursor, ['int', 'address', 'address'])
538
+ : undefined
539
+ // Bounded positional lane (exclusive with `cursor` at the schema): page 1 is
540
+ // the head page, so it shares the head's SQL and cache entries; deeper pages
541
+ // translate to a SQL OFFSET of `(page - 1) * limit` rows.
542
+ const offset =
543
+ options.page !== undefined && options.page > 1 ? (options.page - 1) * limit : undefined
544
+
545
+ return Timing.time(c, 'fee_amm_pools', () =>
546
+ Store.memoize(
547
+ async () => {
548
+ // The keyset compares the aggregate `mint_count`, so it lives in the
549
+ // outer WHERE over the GROUP BY subquery. Topics are padded 32-byte
550
+ // log topics, so render their literals via `padTopic` (lowercased to
551
+ // match ClickHouse's stored topic casing) rather than `Cursor.literal`.
552
+ const keyset =
553
+ cursor !== undefined
554
+ ? ` WHERE ${Cursor.keyset([
555
+ { literal: Cursor.literal(cursor[0]!, 'int'), name: 'mint_count', order: 'desc' },
556
+ {
557
+ literal: `'${padTopic(cursor[1] as z.output<typeof Schema.Address>).toLowerCase()}'`,
558
+ name: 'topic2',
559
+ order: 'desc',
560
+ },
561
+ {
562
+ literal: `'${padTopic(cursor[2] as z.output<typeof Schema.Address>).toLowerCase()}'`,
563
+ name: 'topic3',
564
+ order: 'desc',
565
+ },
566
+ ])}`
567
+ : ''
568
+
569
+ // Raw-`logs` ClickHouse aggregate (dynamic query, hence the cast):
570
+ // ordering is by mint count so the head pages surface the real fee
571
+ // pools and the one-shot spam pairs sink to the tail. Fetch one extra
572
+ // row to detect `hasMore` without a separate count query.
573
+ const result = await tidx.fetch({
574
+ chainId,
575
+ engine: 'clickhouse',
576
+ query: `
577
+ SELECT topic2, topic3, mint_count, first_at, last_at
578
+ FROM (
579
+ SELECT topic2, topic3, count() AS mint_count, min(block_timestamp) AS first_at, max(block_timestamp) AS last_at
580
+ FROM logs
581
+ WHERE address = '${feeManager}' AND selector IN ('${mintTopic}', '${legacyMintTopic}')
582
+ GROUP BY topic2, topic3
583
+ )${keyset}
584
+ ORDER BY mint_count DESC, topic2 DESC, topic3 DESC
585
+ LIMIT ${limit + 1}${offset !== undefined ? ` OFFSET ${offset}` : ''}
586
+ ` as string,
587
+ })
588
+
589
+ // The next page anchors below the last row's `(mint_count, topic2,
590
+ // topic3)`; topics use the lowercase unpadded address so they round
591
+ // trip through the keyset literals above.
592
+ const page = Cursor.paginate({
593
+ rows: result.rows,
594
+ limit,
595
+ key: (row) => {
596
+ const mintCount = Value.toNumber(row['mint_count'])
597
+ const userToken = unpadTopic(Value.toText(row['topic2']))?.toLowerCase()
598
+ const validatorToken = unpadTopic(Value.toText(row['topic3']))?.toLowerCase()
599
+ return mintCount !== undefined &&
600
+ userToken !== undefined &&
601
+ validatorToken !== undefined
602
+ ? [mintCount, userToken, validatorToken]
603
+ : undefined
604
+ },
605
+ })
606
+
607
+ const data: listPools.Pool[] = []
608
+ for (const row of page.rows) {
609
+ const userToken = Schema.Address.safeParse(unpadTopic(Value.toText(row['topic2'])))
610
+ const validatorToken = Schema.Address.safeParse(unpadTopic(Value.toText(row['topic3'])))
611
+ const mintCount = Value.toNumber(row['mint_count'])
612
+ const createdAt = Value.toIsoDateTime(row['first_at'])
613
+ const lastMintAt = Value.toIsoDateTime(row['last_at'])
614
+ if (
615
+ !userToken.success ||
616
+ !validatorToken.success ||
617
+ mintCount === undefined ||
618
+ createdAt === undefined ||
619
+ lastMintAt === undefined
620
+ )
621
+ continue
622
+ data.push({
623
+ createdAt,
624
+ lastMintAt,
625
+ mintCount,
626
+ userToken: userToken.data,
627
+ validatorToken: validatorToken.data,
628
+ })
629
+ }
630
+
631
+ return { data, nextCursor: page.nextCursor }
632
+ },
633
+ {
634
+ key: `feeamm:v1:${chainId}:pools:${cursor ? `cursor:${cursor[0]}:${cursor[1]}:${cursor[2]}` : offset !== undefined ? `page:${options.page}` : 'head'}:${limit}`,
635
+ store,
636
+ ttl: Ttl.minutes(1),
637
+ },
638
+ ),
639
+ )
640
+ }
641
+
642
+ declare namespace listPools {
643
+ /** Options for {@link listPools}. */
644
+ type Options = {
645
+ chainId: z.output<typeof Schema.ChainId>
646
+ /** Opaque keyset cursor anchoring the page; omit for the head page. */
647
+ cursor?: string | undefined
648
+ limit: number
649
+ /** 1-indexed page number (positional pagination; exclusive with `cursor`). */
650
+ page?: number | undefined
651
+ }
652
+ /** A discovered pool, aggregated from its `Mint` logs. */
653
+ type Pool = {
654
+ /** ISO timestamp of the pool's first mint. */
655
+ createdAt: string
656
+ /** ISO timestamp of the pool's most recent mint. */
657
+ lastMintAt: string
658
+ mintCount: number
659
+ userToken: z.output<typeof Schema.Address>
660
+ validatorToken: z.output<typeof Schema.Address>
661
+ }
662
+ }
663
+
664
+ /**
665
+ * The set of token addresses usable to pay transaction fees on a chain: the
666
+ * curated verified tokens that have at least one Fee AMM pool (i.e. appear as a
667
+ * `userToken` / topic2 in the FeeManager's `Mint` logs), plus the protocol
668
+ * default fee token (`pathUSD`), which pays fees without needing a pool.
669
+ * Addresses are lowercased to match the indexer's stored casing.
670
+ *
671
+ * The set is intentionally gated to the verified list rather than every pooled
672
+ * token: anyone can mint a one-shot spam pool, so the raw set of pool
673
+ * `userToken`s is unbounded (tens of thousands on busy chains) and those
674
+ * liquidity-less pools can't actually settle fees. Restricting to verified
675
+ * tokens keeps the set small (a handful of USD stablecoins), bounds the scan
676
+ * (the `topic2 IN (…)` candidate list is the verified list), and matches the
677
+ * tokens fees are realistically paid in.
678
+ *
679
+ * Memoized per chain and verified-snapshot version (the only inputs), so
680
+ * callers — e.g. the address balances endpoint — can flag which holdings are
681
+ * fee-usable without a second `/fee-amm/pools` query.
682
+ */
683
+ export async function feeTokenSet(
684
+ c: Context<App.Environment>,
685
+ chainId: z.output<typeof Schema.ChainId>,
686
+ ): Promise<Set<string>> {
687
+ const store = c.get('store')
688
+ const tidx = c.get('getTidx')(chainId)
689
+ const snapshot = await VerifiedTokens.snapshot(c, chainId)
690
+ // pathUSD is always fee-usable; the verified tokens are the only other
691
+ // candidates we probe for a pool. With no verified candidates there is
692
+ // nothing to query — pathUSD stands alone.
693
+ const candidates = snapshot.list.map((token) => token.address.toLowerCase())
694
+ if (candidates.length === 0) return new Set([pathUsd])
695
+
696
+ // `memoize` JSON-encodes its value, so cache a plain `string[]` and rebuild
697
+ // the `Set` per call (cheap; the list is a handful of stablecoins).
698
+ const addresses = await Timing.time(c, 'fee_token_set', () =>
699
+ Store.memoize(
700
+ async () => {
701
+ // Dynamic raw-`logs` query (hence the cast), mirroring the pools
702
+ // listing's FeeManager `Mint` filter but collapsing to the distinct
703
+ // user-side tokens — and scoped to the verified candidates via
704
+ // `topic2 IN (…)` so the result is bounded by the verified list.
705
+ const topics = candidates.map((address) => `'${padTopic(address as `0x${string}`)}'`)
706
+ const result = await tidx.fetch({
707
+ chainId,
708
+ engine: 'clickhouse',
709
+ query: `
710
+ SELECT DISTINCT topic2
711
+ FROM logs
712
+ WHERE address = '${feeManager}' AND selector IN ('${mintTopic}', '${legacyMintTopic}')
713
+ AND topic2 IN (${topics.join(', ')})
714
+ ` as string,
715
+ })
716
+ const tokens = new Set<string>([pathUsd])
717
+ for (const row of result.rows) {
718
+ const token = unpadTopic(Value.toText(row['topic2']))?.toLowerCase()
719
+ if (token !== undefined && Schema.Address.safeParse(token).success) tokens.add(token)
720
+ }
721
+ return Array.from(tokens)
722
+ },
723
+ {
724
+ key: `feeamm:v2:${chainId}:fee-token-set:v:${snapshot.version}`,
725
+ store,
726
+ ttl: Ttl.minutes(5),
727
+ },
728
+ ),
729
+ )
730
+ return new Set(addresses)
731
+ }
732
+
733
+ /**
734
+ * Reads a pool's current on-chain reserves via `FeeManager.getPool`, memoized
735
+ * briefly per pool. Reserves are best-effort decoration on the pool listing:
736
+ * a failing RPC read resolves to `null` (degrading that pool's reserve
737
+ * fields) rather than failing the whole response. Failures are not memoized,
738
+ * so the next request retries.
739
+ */
740
+ function getPoolReserves(
741
+ c: Context<App.Environment>,
742
+ options: getPoolReserves.Options,
743
+ ): Promise<getPoolReserves.Reserves | null> {
744
+ const { chainId, userToken, validatorToken } = options
745
+ const client = c.get('getClient')(chainId)
746
+ const store = c.get('store')
747
+
748
+ return Store.memoize(
749
+ async () => {
750
+ const pool = await client.readContract({
751
+ abi: Abis.feeAmm,
752
+ address: Addresses.feeManager,
753
+ args: [userToken, validatorToken],
754
+ functionName: 'getPool',
755
+ })
756
+ return {
757
+ userToken: pool.reserveUserToken.toString(),
758
+ validatorToken: pool.reserveValidatorToken.toString(),
759
+ }
760
+ },
761
+ {
762
+ key: `feeamm:v1:${chainId}:pool:${userToken}:${validatorToken}:reserves`,
763
+ store,
764
+ ttl: Ttl.seconds(30),
765
+ },
766
+ ).catch(() => null)
767
+ }
768
+
769
+ declare namespace getPoolReserves {
770
+ /** Options for {@link getPoolReserves}. */
771
+ type Options = {
772
+ chainId: z.output<typeof Schema.ChainId>
773
+ userToken: z.output<typeof Schema.Address>
774
+ validatorToken: z.output<typeof Schema.Address>
775
+ }
776
+ /** Raw base-unit reserves of both pool sides. */
777
+ type Reserves = {
778
+ /** User-side token reserve in its base unit. */
779
+ userToken: string
780
+ /** Validator-side token reserve in its base unit. */
781
+ validatorToken: string
782
+ }
783
+ }
784
+
785
+ /**
786
+ * Lists `Mint` logs as a keyset-paginated page on `(block_num, log_idx)`,
787
+ * optionally filtered by the pool's tokens. Runs on ClickHouse: the postgres
788
+ * `logs` table has no topic index, so topic-filtered pages exceed its
789
+ * statement timeout, while ClickHouse serves every filter shape.
790
+ */
791
+ function listMints(c: Context<App.Environment>, options: listMints.Options) {
792
+ const { chainId, limit, order, userToken, validatorToken } = options
793
+ const store = c.get('store')
794
+ const tidx = c.get('getTidx')(chainId)
795
+ const direction = order === 'asc' ? 'ASC' : 'DESC'
796
+ // Keyset pagination: anchor the page below the previous row's
797
+ // `(block, log_idx)` position instead of a numeric offset, so rows arriving
798
+ // at the head can't shift items across pages. A malformed cursor falls back
799
+ // to the head page.
800
+ const cursor = options.cursor ? Cursor.decode(options.cursor, ['int', 'int']) : undefined
801
+ // Bounded positional lane (exclusive with `cursor` at the schema): page 1 is
802
+ // the head page, so it shares the head's SQL and cache entries; deeper pages
803
+ // translate to a SQL OFFSET of `(page - 1) * limit` rows.
804
+ const offset =
805
+ options.page !== undefined && options.page > 1 ? (options.page - 1) * limit : undefined
806
+
807
+ return Timing.time(c, 'fee_amm_mints', () =>
808
+ Store.memoize(
809
+ async () => {
810
+ const filters = [
811
+ `address = '${feeManager}'`,
812
+ `selector IN ('${mintTopic}', '${legacyMintTopic}')`,
813
+ ]
814
+ if (userToken !== undefined) filters.push(`topic2 = '${padTopic(userToken)}'`)
815
+ if (validatorToken !== undefined) filters.push(`topic3 = '${padTopic(validatorToken)}'`)
816
+ if (cursor !== undefined)
817
+ filters.push(
818
+ Cursor.keyset([
819
+ { literal: Cursor.literal(cursor[0]!, 'int'), name: 'block_num', order },
820
+ { literal: Cursor.literal(cursor[1]!, 'int'), name: 'log_idx', order },
821
+ ]),
822
+ )
823
+
824
+ // Fetch one extra row to detect `hasMore` without a separate count
825
+ // query. Raw-`logs` dynamic query, hence the cast. `block_timestamp`
826
+ // is aliased to a non-standard name on purpose: the TIDX client
827
+ // coerces standard `logs` columns, and its coercion parses
828
+ // ClickHouse's timezone-less `DateTime` text as *local* time. The
829
+ // alias passes the raw text through so `Value.toIsoDateTime` can
830
+ // apply its UTC normalization instead.
831
+ const result = await tidx.fetch({
832
+ chainId,
833
+ engine: 'clickhouse',
834
+ query: `
835
+ SELECT tx_hash, block_num, log_idx, block_timestamp AS block_time, selector, topic1, topic2, topic3, data
836
+ FROM logs
837
+ WHERE ${filters.join(' AND ')}
838
+ ORDER BY block_num ${direction}, log_idx ${direction}
839
+ LIMIT ${limit + 1}${offset !== undefined ? ` OFFSET ${offset}` : ''}
840
+ ` as string,
841
+ })
842
+
843
+ // The next page anchors below the last fetched row's `(block, log_idx)`.
844
+ const page = Cursor.paginate({
845
+ rows: result.rows,
846
+ limit,
847
+ key: (row) => {
848
+ const block = Value.toNumber(row['block_num'])
849
+ const index = Value.toNumber(row['log_idx'])
850
+ return block !== undefined && index !== undefined ? [block, index] : undefined
851
+ },
852
+ })
853
+
854
+ const data: decodeMint.Mint[] = []
855
+ for (const row of page.rows) {
856
+ const mint = decodeMint(row)
857
+ if (mint) data.push(mint)
858
+ }
859
+ return { data, nextCursor: page.nextCursor }
860
+ },
861
+ {
862
+ key: `feeamm:v1:${chainId}:mints:${order}:${userToken ?? ''}:${validatorToken ?? ''}:${cursor ? `cursor:${cursor[0]}:${cursor[1]}` : offset !== undefined ? `page:${options.page}` : 'head'}:${limit}`,
863
+ store,
864
+ ttl: Ttl.seconds(15),
865
+ },
866
+ ),
867
+ )
868
+ }
869
+
870
+ declare namespace listMints {
871
+ /** Options for {@link listMints}. */
872
+ type Options = {
873
+ chainId: z.output<typeof Schema.ChainId>
874
+ /** Opaque keyset cursor anchoring the page; omit for the head page. */
875
+ cursor?: string | undefined
876
+ limit: number
877
+ /** 1-indexed page number (positional pagination; exclusive with `cursor`). */
878
+ page?: number | undefined
879
+ order: 'asc' | 'desc'
880
+ /** Restrict to pools with this user-side token. */
881
+ userToken?: z.output<typeof Schema.Address> | undefined
882
+ /** Restrict to pools with this validator-side token. */
883
+ validatorToken?: z.output<typeof Schema.Address> | undefined
884
+ }
885
+ }
886
+
887
+ /**
888
+ * Exact total count of pool mints matching the same filters as the page. Runs
889
+ * on ClickHouse over `logs`, pruning on the FeeManager `address` (the page's
890
+ * own engine/key). Because `address` aligns with the `logs` sort key,
891
+ * ClickHouse counts via the sparse primary index without a row scan, so the
892
+ * count is exact and cheap with no cap (`totalCountCapped` is always `false`) —
893
+ * verified at ~20M matching mints in ~1s.
894
+ */
895
+ function countMints(
896
+ c: Context<App.Environment>,
897
+ options: countMints.Options,
898
+ ): Promise<z.output<typeof Schema.CountMeta>> {
899
+ const { chainId, userToken, validatorToken } = options
900
+ const store = c.get('store')
901
+ const tidx = c.get('getTidx')(chainId)
902
+ return Timing.time(c, 'fee_amm_mints_count', () =>
903
+ Store.memoize(
904
+ async () => {
905
+ const filters = [
906
+ `address = '${feeManager}'`,
907
+ `selector IN ('${mintTopic}', '${legacyMintTopic}')`,
908
+ ]
909
+ if (userToken !== undefined) filters.push(`topic2 = '${padTopic(userToken)}'`)
910
+ if (validatorToken !== undefined) filters.push(`topic3 = '${padTopic(validatorToken)}'`)
911
+ const result = await tidx.fetch({
912
+ chainId,
913
+ engine: 'clickhouse',
914
+ query: `SELECT count(*) AS total FROM logs WHERE ${filters.join(' AND ')}` as string,
915
+ })
916
+ const totalCount = Value.toNumber(result.rows[0]?.['total']) ?? 0
917
+ return { totalCountCapped: false, totalCount }
918
+ },
919
+ {
920
+ key: `feeamm:v1:${chainId}:mints-count:${userToken ?? ''}:${validatorToken ?? ''}`,
921
+ store,
922
+ ttl: Ttl.seconds(15),
923
+ },
924
+ ),
925
+ )
926
+ }
927
+
928
+ declare namespace countMints {
929
+ /** Filters for {@link countMints}: the page filters minus pagination. */
930
+ type Options = Pick<listMints.Options, 'chainId' | 'userToken' | 'validatorToken'>
931
+ }
932
+
933
+ /**
934
+ * Decodes one raw `Mint` log row, dispatching on its selector (current vs
935
+ * legacy signature — see the topic constants for the field layouts). Returns
936
+ * undefined for rows that fail to decode so callers can skip them.
937
+ */
938
+ function decodeMint(row: Record<string, unknown>): decodeMint.Mint | undefined {
939
+ const selector = Value.toText(row['selector'])
940
+ const transactionHash = Schema.Hash.safeParse(row['tx_hash'])
941
+ const blockNumber = Value.toNumber(row['block_num'])
942
+ const logIndex = Value.toNumber(row['log_idx'])
943
+ const timestamp = Value.toIsoDateTime(row['block_time'])
944
+ const userToken = Schema.Address.safeParse(unpadTopic(Value.toText(row['topic2'])))
945
+ const validatorToken = Schema.Address.safeParse(unpadTopic(Value.toText(row['topic3'])))
946
+ const data = Value.toText(row['data'])
947
+ if (
948
+ !transactionHash.success ||
949
+ blockNumber === undefined ||
950
+ logIndex === undefined ||
951
+ timestamp === undefined ||
952
+ !userToken.success ||
953
+ !validatorToken.success ||
954
+ data === undefined
955
+ )
956
+ return undefined
957
+
958
+ const word = (index: number) => data.slice(2 + 64 * index, 2 + 64 * (index + 1))
959
+ const uint = (value: string) =>
960
+ /^[0-9a-fA-F]{64}$/.test(value) ? BigInt(`0x${value}`).toString() : undefined
961
+
962
+ if (selector === mintTopic) {
963
+ const minter = Schema.Address.safeParse(`0x${word(0).slice(24)}`)
964
+ const recipient = Schema.Address.safeParse(unpadTopic(Value.toText(row['topic1'])))
965
+ const amountValidatorToken = uint(word(1))
966
+ const liquidity = uint(word(2))
967
+ if (
968
+ !minter.success ||
969
+ !recipient.success ||
970
+ amountValidatorToken === undefined ||
971
+ liquidity === undefined
972
+ )
973
+ return undefined
974
+ return {
975
+ amountValidatorToken,
976
+ blockNumber,
977
+ id: `${transactionHash.data}-${logIndex}`,
978
+ liquidity,
979
+ logIndex,
980
+ minter: minter.data,
981
+ recipient: recipient.data,
982
+ timestamp,
983
+ transactionHash: transactionHash.data,
984
+ userToken: userToken.data,
985
+ validatorToken: validatorToken.data,
986
+ }
987
+ }
988
+
989
+ if (selector === legacyMintTopic) {
990
+ const minter = Schema.Address.safeParse(unpadTopic(Value.toText(row['topic1'])))
991
+ const amountUserToken = uint(word(0))
992
+ const amountValidatorToken = uint(word(1))
993
+ const liquidity = uint(word(2))
994
+ if (
995
+ !minter.success ||
996
+ amountUserToken === undefined ||
997
+ amountValidatorToken === undefined ||
998
+ liquidity === undefined
999
+ )
1000
+ return undefined
1001
+ return {
1002
+ amountUserToken,
1003
+ amountValidatorToken,
1004
+ blockNumber,
1005
+ id: `${transactionHash.data}-${logIndex}`,
1006
+ liquidity,
1007
+ logIndex,
1008
+ minter: minter.data,
1009
+ timestamp,
1010
+ transactionHash: transactionHash.data,
1011
+ userToken: userToken.data,
1012
+ validatorToken: validatorToken.data,
1013
+ }
1014
+ }
1015
+
1016
+ return undefined
1017
+ }
1018
+
1019
+ declare namespace decodeMint {
1020
+ /**
1021
+ * A decoded `Mint` row, carrying bare token addresses. The route shapes
1022
+ * them into the enriched `PoolToken` references before responding.
1023
+ */
1024
+ type Mint = Omit<z.output<typeof schema.getFeeAmmMints.Mint>, 'userToken' | 'validatorToken'> & {
1025
+ /** The pool's user-side (fee) token address. */
1026
+ userToken: z.output<typeof Schema.Address>
1027
+ /** The pool's validator-side token address. */
1028
+ validatorToken: z.output<typeof Schema.Address>
1029
+ }
1030
+ }
1031
+
1032
+ /**
1033
+ * Resolves best-effort metadata for the given token addresses, sharing one
1034
+ * verified-token snapshot across the lookups. Mirrors the transfer feed's
1035
+ * `sourceToken` enrichment: a token whose metadata is unavailable maps to
1036
+ * undefined, so call sites spreading the result keep just the bare `address`
1037
+ * (the `PoolToken` shape).
1038
+ *
1039
+ * Unlike the full token resource, this resolves only the on-chain metadata
1040
+ * (one `token.getMetadata` read per token, auto-batched into a single
1041
+ * multicall) plus the in-memory verified snapshot. It deliberately skips the
1042
+ * curated R2 logo lookup (`getTokenLogo`), which is an un-batched storage read
1043
+ * per token and the dominant cost of the pools/mints listings; `logoUri`
1044
+ * therefore falls back to the verified entry's curated URL or the precompile's
1045
+ * on-chain `logoURI`, both already in hand.
1046
+ */
1047
+ async function resolveTokenMap(c: Context<App.Environment>, options: resolveTokenMap.Options) {
1048
+ const { addresses, chainId } = options
1049
+ const snapshot = await VerifiedTokens.snapshot(c, chainId)
1050
+ const resolved = await Timing.time(c, 'tokens', () =>
1051
+ Promise.all(
1052
+ addresses.map((token) =>
1053
+ Tokens.getTokenMetadata(c, { address: token, chainId })
1054
+ .then((metadata) => {
1055
+ const verified = snapshot.byAddress.get(token)
1056
+ return [
1057
+ token,
1058
+ {
1059
+ currency: metadata.currency,
1060
+ decimals: metadata.decimals,
1061
+ logoUri: verified?.logoUri ?? metadata.logoUri,
1062
+ name: metadata.name,
1063
+ symbol: metadata.symbol,
1064
+ verified: snapshot.byAddress.has(token),
1065
+ },
1066
+ ] as const
1067
+ })
1068
+ .catch(() => [token, undefined] as const),
1069
+ ),
1070
+ ),
1071
+ )
1072
+ return new Map(resolved)
1073
+ }
1074
+
1075
+ declare namespace resolveTokenMap {
1076
+ /** Options for {@link resolveTokenMap}. */
1077
+ type Options = {
1078
+ /** Unique token addresses to resolve. */
1079
+ addresses: readonly z.output<typeof Schema.Address>[]
1080
+ /** Tempo chain id. */
1081
+ chainId: z.output<typeof Schema.ChainId>
1082
+ }
1083
+ }
1084
+
1085
+ /**
1086
+ * Computes a pool id locally: `keccak256(abi.encode(userToken,
1087
+ * validatorToken))`, matching the on-chain `FeeManager.getPoolId`.
1088
+ */
1089
+ function poolId(
1090
+ userToken: z.output<typeof Schema.Address>,
1091
+ validatorToken: z.output<typeof Schema.Address>,
1092
+ ): Hex.Hex {
1093
+ return Hash.keccak256(
1094
+ AbiParameters.encode(AbiParameters.from(['address', 'address']), [userToken, validatorToken]),
1095
+ )
1096
+ }
1097
+
1098
+ /**
1099
+ * Shapes one side's on-chain reserve for spreading into its token reference:
1100
+ * the base-unit `amount` plus its `formatted` decimal form. `decimals` comes
1101
+ * from the side's resolved token metadata; an unresolvable token falls back
1102
+ * to 6, the fixed TIP-20 precision, so the reserve still formats. A missing
1103
+ * amount (failed `getPool` read) yields undefined, leaving both fields absent.
1104
+ */
1105
+ function reserve(
1106
+ amount: string | undefined,
1107
+ decimals: number | undefined,
1108
+ ): { amount: string; formatted: string } | undefined {
1109
+ if (amount === undefined) return undefined
1110
+ return { amount, formatted: core_Value.format(BigInt(amount), decimals ?? 6) }
1111
+ }
1112
+
1113
+ /** Left-pads a 20-byte address to its 32-byte log-topic form. */
1114
+ function padTopic(address: z.output<typeof Schema.Address>): string {
1115
+ return `0x${'0'.repeat(24)}${address.slice(2)}`
1116
+ }
1117
+
1118
+ /** Unpads a 32-byte log topic to its trailing 20-byte address. */
1119
+ function unpadTopic(topic: string | undefined): string | undefined {
1120
+ return topic === undefined ? undefined : `0x${topic.slice(26)}`
1121
+ }