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,1015 @@
1
+ import { Hono } from 'hono';
2
+ import { createMiddleware } from 'hono/factory';
3
+ import { Address, Hash } from 'ox';
4
+ import { formatUnits } from 'viem';
5
+ import { Addresses, Tick } from 'viem/tempo';
6
+ import * as z from 'zod/mini';
7
+ import * as Auth from '../internal/Auth.js';
8
+ import * as Cache from '../internal/Cache.js';
9
+ import * as OpenApi from '../internal/OpenApi.js';
10
+ import * as Response from '../internal/Response.js';
11
+ import * as Schema from '../internal/Schema.js';
12
+ import * as Timing from '../internal/Timing.js';
13
+ import * as Value from '../internal/Value.js';
14
+ import { resolveBookQuotes, resolveBookReserves, resolveOrderStates } from './exchanges.js';
15
+ import { getTokenMetadata } from './tokens.js';
16
+ // Stablecoin DEX precompile. Pairs are announced by `PairCreated` (pre-decoded
17
+ // into `dex_pairs`); fills land in `dex_fills`. The lowercase form is for inline
18
+ // SQL; `dexAccount` keeps the checksummed address for on-chain reads.
19
+ const stablecoinDex = Addresses.stablecoinDex.toLowerCase();
20
+ const dexAccount = Addresses.stablecoinDex;
21
+ // `OrderFilled` event signature, used to read the latest decoded DEX fill block
22
+ // from the indexer's signature-backed event stream (mirrors the legacy
23
+ // `tempoxyz/api` adapter's `/latest-block` query).
24
+ const orderFilledSignature = 'event OrderFilled(uint128 indexed orderId, address indexed maker, address indexed taker, uint128 amountFilled, bool partialFill)';
25
+ // Default and maximum page size backing the `/pairs` list endpoint's `?limit`
26
+ // search param. GeckoTerminal itself never enumerates pairs — it discovers them
27
+ // from `/events` and fetches `/pairs/:pairId` — so listing the full set (820k+
28
+ // pairs on Moderato) is neither required nor feasible. Callers tune the page
29
+ // size via `?limit`; the max stays a server-side cap so the list can't be made
30
+ // unbounded. Returns the newest pairs, mirroring `/v1/exchange/pairs`' index.
31
+ const defaultPairsLimit = 50;
32
+ const maxPairsLimit = 500;
33
+ // Total `OrderPlaced` budget for the `/pairs` list's per-book reserve scan. The
34
+ // per-book cap is this budget divided by the page size (floored at a small
35
+ // minimum), so the aggregate scan — and the downstream fill/cancel `IN`-joins —
36
+ // stays bounded regardless of `?limit`, and one very active book can't starve
37
+ // the others under a shared cap. `/pairs/:pairId` instead uses the full per-book
38
+ // default cap for an accurate single-pair reserve.
39
+ const listReserveOrderBudget = 10_000;
40
+ const minListReserveScanCap = 20;
41
+ // Stable identifier for the single venue this adapter exposes to GeckoTerminal.
42
+ const dexKey = 'tempo-stablecoin-dex';
43
+ /**
44
+ * Builds a fully-qualified cache-key URL for `Cache.response`. The Workers Cache
45
+ * API (used in production) rejects bare-string keys — "Cache API keys must be
46
+ * fully-qualified, valid URLs" — so every coingecko cache key is namespaced
47
+ * under a synthetic origin whose path/query identify the entry. (The in-memory
48
+ * test store accepts any string, which is why this only surfaces in workerd.)
49
+ */
50
+ function cacheKey(path) {
51
+ return new URL(path, 'https://cache.tempo-api.internal').toString();
52
+ }
53
+ // Largest `toBlock - fromBlock` span the `/events` endpoint will scan in one
54
+ // request. GeckoTerminal polls in small windows; a hard cap keeps a single
55
+ // request from sweeping the entire (10⁹-row) fill history and timing out
56
+ // upstream. Mirrors the swap feed's `swapWindowBlocks`.
57
+ const maxEventBlockRange = 100_000;
58
+ // Default `/events` window when `fromBlock`/`toBlock` are omitted: `toBlock`
59
+ // defaults to the latest indexed block and `fromBlock` to `toBlock` minus this
60
+ // span. Kept small so the convenience default stays fast (the per-fill state
61
+ // scan grows with the window); callers wanting more pass an explicit range.
62
+ const defaultEventBlockRange = 1_000;
63
+ // 36-dp fixed-point precision for `priceNative`, matching the GeckoTerminal DEX
64
+ // integration contract the legacy `/gecko` adapter served.
65
+ const pricePrecision = 36n;
66
+ // `tidx.fetch` transmits queries as GET, so an `IN (...)` list of every id in a
67
+ // wide `/events` window overflows the request URI (the indexer drops the
68
+ // request near ~60 KB of SQL). These cap how many ids/addresses go into a single
69
+ // query; the rest are split across queries that run sequentially and merge. 256
70
+ // padded order-id literals produce ~37 KB of SQL — comfortably under the limit
71
+ // (verified: 384 ids/~55 KB still OK, 448 ids/~64 KB dropped).
72
+ const orderIdChunkSize = 256;
73
+ const addressChunkSize = 200;
74
+ // How many token reads (RPC `getMetadata`/`getBalance`) to fan out per batch.
75
+ // The client auto-batches concurrent `eth_call`s in the same macrotask into one
76
+ // deployless Multicall3 call; a whole page of pairs (~2N tokens) overflows the
77
+ // Tempo RPC's per-call limit ("Request exceeds defined limit"). Reading in small
78
+ // chunks — with an `await` between groups so each chunk is its own batch — keeps
79
+ // every multicall comfortably under the limit while still aggregating per chunk.
80
+ const tokenReadChunkSize = 20;
81
+ /** Zod schemas owned by the CoinGecko/GeckoTerminal compatibility adapter. */
82
+ export var schema;
83
+ (function (schema) {
84
+ /**
85
+ * A 20-byte hex address that preserves case. Unlike `Schema.Address` (which
86
+ * lowercases on output), the GeckoTerminal contract returns EIP-55
87
+ * checksummed addresses, so response bodies keep the checksum produced by
88
+ * `Address.checksum`.
89
+ */
90
+ schema.ChecksummedAddress = z
91
+ .string()
92
+ .check(z.regex(/^0x[0-9a-fA-F]{40}$/), z.describe('Checksummed 0x-prefixed 20-byte account or contract address.'));
93
+ /**
94
+ * Path `chainId`. Unlike the rest of Tempo API (where `chainId` is an optional
95
+ * query parameter defaulting from app composition), GeckoTerminal templates a
96
+ * per-network base URL, so the chain is addressed positionally here.
97
+ */
98
+ schema.ChainIdParam = z
99
+ .object({
100
+ chainId: Schema.ChainId.check(z.meta({ examples: [4217] })),
101
+ })
102
+ .check(z.describe('Path values that select the Tempo network for this GeckoTerminal-compatible request.'));
103
+ /** Indexed-block tip exposed to GeckoTerminal. */
104
+ schema.Block = Schema.describe(z.object({
105
+ blockNumber: z
106
+ .number()
107
+ .check(z.int(), z.nonnegative(), z.describe('Block number on Tempo.'), z.meta({ examples: [23456789] })),
108
+ blockTimestamp: z
109
+ .number()
110
+ .check(z.int(), z.nonnegative(), z.describe('Block time as Unix seconds.'), z.meta({ examples: [1718668800] })),
111
+ }), 'A Tempo block reference with its number and timestamp.');
112
+ /** `GET /:chainId/latest-block` response. */
113
+ schema.LatestBlockResponse = Schema.describe(z.object({ block: schema.Block }), 'The newest Tempo block that has been indexed for CoinGecko-compatible data.');
114
+ /** Path parameters addressing a single asset. */
115
+ schema.AssetParams = z
116
+ .object({
117
+ chainId: Schema.ChainId.check(z.meta({ examples: [4217] })),
118
+ address: Schema.TokenAddress.check(z.describe('TIP-20 token contract address for the asset.')),
119
+ })
120
+ .check(z.describe('Path values that select a TIP-20 asset on a Tempo network.'));
121
+ /** A TIP-20 asset in GeckoTerminal shape. */
122
+ schema.Asset = Schema.describe(z.object({
123
+ id: schema.ChecksummedAddress.check(z.describe('Checksummed TIP-20 token contract address.'), z.meta({ examples: ['0x20c0000000000000000000008f5425160ebe5525'] })),
124
+ name: z
125
+ .string()
126
+ .check(z.describe('Human-readable token name.'), z.meta({ examples: ['Tempo USD'] })),
127
+ symbol: z
128
+ .string()
129
+ .check(z.describe('Short token ticker symbol.'), z.meta({ examples: ['USDT'] })),
130
+ decimals: z
131
+ .number()
132
+ .check(z.int(), z.gte(0), z.lte(255), z.describe('Number of decimal places the token uses.'), z.meta({ examples: [6] })),
133
+ totalSupply: z
134
+ .string()
135
+ .check(z.describe('Total token supply as a decimal string, already adjusted for token decimals.'), z.meta({ examples: ['1000000'] })),
136
+ }), 'A TIP-20 token in CoinGecko’s GeckoTerminal asset format.');
137
+ /** `GET /:chainId/assets/:address` response. */
138
+ schema.AssetResponse = Schema.describe(z.object({ asset: schema.Asset }), 'One TIP-20 token in CoinGecko’s GeckoTerminal asset format.');
139
+ /** A pair-side token (asset plus checksummed address). */
140
+ schema.PairToken = Schema.describe(z.object({
141
+ address: schema.ChecksummedAddress.check(z.describe('Checksummed TIP-20 token contract address.'), z.meta({ examples: ['0x20c0000000000000000000008f5425160ebe5525'] })),
142
+ name: z
143
+ .string()
144
+ .check(z.describe('Human-readable token name.'), z.meta({ examples: ['Tempo USD'] })),
145
+ symbol: z
146
+ .string()
147
+ .check(z.describe('Short token ticker symbol.'), z.meta({ examples: ['USDT'] })),
148
+ decimals: z
149
+ .number()
150
+ .check(z.int(), z.gte(0), z.lte(255), z.describe('Number of decimal places the token uses.'), z.meta({ examples: [6] })),
151
+ totalSupply: z
152
+ .string()
153
+ .check(z.describe('Total token supply as a decimal string, already adjusted for token decimals.'), z.meta({ examples: ['1000000'] })),
154
+ }), 'One token side of a trading pair.');
155
+ /** A GeckoTerminal trading pair. */
156
+ schema.Pair = Schema.describe(z.object({
157
+ id: Schema.Hash.check(z.describe('32-byte onchain order-book key for this trading pair.')),
158
+ dexKey: z
159
+ .string()
160
+ .check(z.describe('Stable identifier for Tempo’s DEX venue.'), z.meta({ examples: [dexKey] })),
161
+ asset0Id: schema.ChecksummedAddress.check(z.describe('Checksummed address of the base token (`asset0`).'), z.meta({ examples: ['0x20c0000000000000000000008f5425160ebe5525'] })),
162
+ asset1Id: schema.ChecksummedAddress.check(z.describe('Checksummed address of the quote token (`asset1`).'), z.meta({ examples: ['0x20C0000000000000000000000000000000000000'] })),
163
+ token0: schema.PairToken,
164
+ token1: schema.PairToken,
165
+ reserve0: z
166
+ .string()
167
+ .check(z.describe('Best-effort current base-side reserve as a decimal string.'), z.meta({ examples: ['1000000'] })),
168
+ reserve1: z
169
+ .string()
170
+ .check(z.describe('Best-effort current quote-side reserve as a decimal string.'), z.meta({ examples: ['1500000'] })),
171
+ createdAtBlockNumber: z
172
+ .optional(z
173
+ .number()
174
+ .check(z.int(), z.nonnegative(), z.describe('Block number where the pair was created, when indexed.')))
175
+ .check(z.meta({ examples: [23456789] })),
176
+ createdAtBlockTimestamp: z
177
+ .optional(z
178
+ .number()
179
+ .check(z.int(), z.nonnegative(), z.describe('Unix timestamp for when the pair was created, when indexed.')))
180
+ .check(z.meta({ examples: [1718668800] })),
181
+ createdAtTxnId: z
182
+ .optional(z.string().check(z.describe('Transaction hash that created the pair, when indexed.')))
183
+ .check(z.meta({
184
+ examples: ['0x515801d7f9a5ac705e793e85904c9c69b3f1694b465cc8fb6ba3f0298dc82665'],
185
+ })),
186
+ }), 'A Tempo stablecoin DEX trading pair in GeckoTerminal format.');
187
+ /** `GET /:chainId/pairs` response. */
188
+ schema.PairsResponse = Schema.describe(z.object({
189
+ pairs: z.array(schema.Pair).check(z.describe('Trading pairs ordered by creation block.')),
190
+ }), 'A page of Tempo stablecoin DEX trading pairs.');
191
+ /** `GET /:chainId/pairs/:pairId` response. */
192
+ schema.PairResponse = Schema.describe(z.object({ pair: schema.Pair }), 'One Tempo stablecoin DEX trading pair.');
193
+ /** Path parameters addressing a single pair. */
194
+ schema.PairParams = z
195
+ .object({
196
+ chainId: Schema.ChainId.check(z.meta({ examples: [4217] })),
197
+ pairId: z.string().check(z.describe('32-byte onchain order-book key for this trading pair.'), z.meta({
198
+ examples: ['0x44f7b8011db3e3647a530b4ff635726de5fafc8fa8ad10f0f31c0eb9dd52fc65'],
199
+ })),
200
+ })
201
+ .check(z.describe('Path values that select one Tempo stablecoin DEX trading pair.'));
202
+ /** `GET /:chainId/pairs` query parameters. */
203
+ schema.PairsQuery = z
204
+ .object({
205
+ limit: z
206
+ ._default(z.coerce.number().check(z.int(), z.gte(1), z.lte(maxPairsLimit)), defaultPairsLimit)
207
+ .check(z.describe(`Maximum number of pairs to return. Defaults to ${defaultPairsLimit} and is capped at ${maxPairsLimit}.`), z.meta({ examples: [50] })),
208
+ })
209
+ .check(z.describe('Pagination controls for the trading-pair list.'));
210
+ /** `GET /:chainId/events` query parameters. */
211
+ schema.EventsQuery = z
212
+ .object({
213
+ fromBlock: z
214
+ .optional(z.coerce.number().check(z.int(), z.nonnegative()))
215
+ .check(z.describe('First block to include. Defaults to `toBlock` minus 1000.'), z.meta({ examples: [23456789] })),
216
+ toBlock: z
217
+ .optional(z.coerce.number().check(z.int(), z.nonnegative()))
218
+ .check(z.describe('Last block to include. Defaults to the latest indexed block.'), z.meta({ examples: [23456999] })),
219
+ })
220
+ .check(z.describe('Block range to scan for swap events, including both endpoints.'));
221
+ /** A GeckoTerminal swap event (one per `OrderFilled`). */
222
+ schema.SwapEvent = Schema.describe(z.object({
223
+ block: schema.Block,
224
+ eventType: z
225
+ .literal('swap')
226
+ .check(z.describe('Event type; always `swap` for this feed.'), z.meta({ examples: ['swap'] })),
227
+ txnId: z.string().check(z.describe('Hash of the transaction that emitted the swap event.'), z.meta({
228
+ examples: ['0x515801d7f9a5ac705e793e85904c9c69b3f1694b465cc8fb6ba3f0298dc82665'],
229
+ })),
230
+ txnIndex: z
231
+ .number()
232
+ .check(z.int(), z.nonnegative(), z.describe('Zero-based position of the transaction within its block.'), z.meta({ examples: [1] })),
233
+ eventIndex: z
234
+ .number()
235
+ .check(z.int(), z.nonnegative(), z.describe('Zero-based position of this swap within its transaction.'), z.meta({ examples: [0] })),
236
+ maker: schema.ChecksummedAddress.check(z.describe('Checksummed address of the taker, usually the transaction sender.'), z.meta({ examples: ['0xbe058e1c4df8a4366a387bf595b284246a93039e'] })),
237
+ pairId: Schema.Hash.check(z.describe('On-chain order-book key for the trading pair.')),
238
+ asset0In: z
239
+ .optional(z.string())
240
+ .check(z.describe('Base token (`asset0`) amount sent into the swap, when applicable.'), z.meta({ examples: ['1000'] })),
241
+ asset1In: z
242
+ .optional(z.string())
243
+ .check(z.describe('Quote token (`asset1`) amount sent into the swap, when applicable.'), z.meta({ examples: ['1500'] })),
244
+ asset0Out: z
245
+ .optional(z.string())
246
+ .check(z.describe('Base token (`asset0`) amount received from the swap, when applicable.'), z.meta({ examples: ['1000'] })),
247
+ asset1Out: z
248
+ .optional(z.string())
249
+ .check(z.describe('Quote token (`asset1`) amount received from the swap, when applicable.'), z.meta({ examples: ['1500'] })),
250
+ priceNative: z
251
+ .string()
252
+ .check(z.describe('Quote-per-base price as a 36-decimal-place string.'), z.meta({ examples: ['1.5'] })),
253
+ reserves: z
254
+ .object({
255
+ asset0: z
256
+ .string()
257
+ .check(z.describe('Best-effort current base-token reserve.'), z.meta({ examples: ['1000000'] })),
258
+ asset1: z
259
+ .string()
260
+ .check(z.describe('Best-effort current quote-token reserve.'), z.meta({ examples: ['1500000'] })),
261
+ })
262
+ .check(z.describe('Best-effort current reserves for both sides of the pair.')),
263
+ }), 'A Tempo DEX swap event in GeckoTerminal format.');
264
+ /** `GET /:chainId/events` response. */
265
+ schema.EventsResponse = Schema.describe(z.object({
266
+ events: z
267
+ .array(schema.SwapEvent)
268
+ .check(z.describe('Swap events in the requested block range, ordered by block and log index.')),
269
+ }), 'Tempo DEX swap events for the requested block range.');
270
+ })(schema || (schema = {}));
271
+ /**
272
+ * Composes the GeckoTerminal/CoinGecko DEX-integration compatibility adapter.
273
+ */
274
+ /**
275
+ * Rejects a path `chainId` this deployment does not serve. GeckoTerminal
276
+ * templates `chainId` into the path, so the App-level query-param guard never
277
+ * sees it; reject a well-formed-but-unsupported chain here before it reaches an
278
+ * upstream (which would otherwise surface as an opaque `upstream_error`).
279
+ * Malformed values fall through to each route's own `param` validation.
280
+ */
281
+ const guardChainId = createMiddleware(async (c, next) => {
282
+ const parsed = Schema.ChainId.safeParse(c.req.param('chainId'));
283
+ const supported = c.get('supportedChainIds');
284
+ if (!parsed.success || supported.has(parsed.data))
285
+ return next();
286
+ return Response.unsupportedChainId(c, parsed.data, supported);
287
+ });
288
+ export function coingecko() {
289
+ return new Hono()
290
+ .use('/:chainId/*', guardChainId)
291
+ .get('/:chainId/latest-block', Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }), OpenApi.validate('param', schema.ChainIdParam, {
292
+ code: 'chain_id_invalid',
293
+ message: 'Invalid chain id',
294
+ }), OpenApi.describeRoute({
295
+ description: 'Returns the latest indexed Tempo block in GeckoTerminal format so DEX data tools know where to start polling.',
296
+ operationId: 'coingeckoLatestBlock',
297
+ responses: OpenApi.responses({
298
+ success: {
299
+ description: 'Latest indexed Tempo block.',
300
+ schema: schema.LatestBlockResponse,
301
+ },
302
+ }),
303
+ summary: 'Get latest indexed block',
304
+ tags: ['CoinGecko'],
305
+ }),
306
+ // The latest block is a point-in-time read of the indexed tip (capped to
307
+ // the latest decoded DEX fill), so it is never cached and carries no
308
+ // `Cache-Control` header (DEX tools poll this to learn where to start).
309
+ async (c) => {
310
+ if (Auth.narrowAccess)
311
+ return Auth.paidAccessError(c);
312
+ if (OpenApi.narrowValidation)
313
+ return OpenApi.validationError(c, {
314
+ code: 'chain_id_invalid',
315
+ message: 'Invalid chain id',
316
+ });
317
+ const { chainId } = c.req.valid('param');
318
+ try {
319
+ const block = await getLatestBlock(c, chainId);
320
+ if (!block)
321
+ return notFound(c, 'No indexed block found');
322
+ return c.json(Response.validated(schema.LatestBlockResponse, { block }), 200);
323
+ }
324
+ catch (cause) {
325
+ return Response.upstream(c, cause);
326
+ }
327
+ })
328
+ .get('/:chainId/assets/:address', Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }), OpenApi.validate('param', schema.AssetParams, {
329
+ code: 'address_invalid',
330
+ message: 'Invalid chain id or token address',
331
+ }), OpenApi.describeRoute({
332
+ description: 'Returns one TIP-20 asset by contract address in GeckoTerminal format.',
333
+ operationId: 'coingeckoAsset',
334
+ responses: OpenApi.responses({
335
+ errors: {
336
+ 404: {
337
+ description: 'No TIP-20 asset was found for that address.',
338
+ codes: ['not_found'],
339
+ },
340
+ },
341
+ success: {
342
+ description: 'A TIP-20 token in CoinGecko’s GeckoTerminal asset format.',
343
+ schema: schema.AssetResponse,
344
+ },
345
+ }),
346
+ summary: 'Get asset',
347
+ tags: ['CoinGecko'],
348
+ }), Cache.response({
349
+ cacheControl: Cache.policies.metadata,
350
+ name: 'tempo-api:coingecko:v1',
351
+ key: (c) => cacheKey(`/gecko/${c.req.param('chainId')}/assets/${c.req.param('address')?.toLowerCase()}`),
352
+ }), async (c) => {
353
+ if (Auth.narrowAccess)
354
+ return Auth.paidAccessError(c);
355
+ if (OpenApi.narrowValidation)
356
+ return OpenApi.validationError(c, {
357
+ code: 'address_invalid',
358
+ message: 'Invalid chain id or token address',
359
+ });
360
+ const { address, chainId } = c.req.valid('param');
361
+ try {
362
+ const asset = await resolveAsset(c, chainId, address);
363
+ if (!asset)
364
+ return notFound(c, 'Asset not found');
365
+ return c.json(Response.validated(schema.AssetResponse, { asset }), 200);
366
+ }
367
+ catch (cause) {
368
+ return Response.upstream(c, cause);
369
+ }
370
+ })
371
+ .get('/:chainId/pairs', Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }), OpenApi.validate('param', schema.ChainIdParam, {
372
+ code: 'chain_id_invalid',
373
+ message: 'Invalid chain id',
374
+ }), OpenApi.validate('query', schema.PairsQuery, {
375
+ code: 'query_invalid',
376
+ message: 'Invalid limit',
377
+ }), OpenApi.describeRoute({
378
+ description: 'Lists Tempo stablecoin DEX trading pairs in GeckoTerminal format.',
379
+ operationId: 'coingeckoPairs',
380
+ responses: OpenApi.responses({
381
+ errors: { 400: 'The `limit` query parameter is invalid.' },
382
+ success: {
383
+ description: 'Trading pairs in GeckoTerminal format.',
384
+ schema: schema.PairsResponse,
385
+ },
386
+ }),
387
+ summary: 'List trading pairs',
388
+ tags: ['CoinGecko'],
389
+ }), Cache.response({
390
+ cacheControl: Cache.policies.state,
391
+ name: 'tempo-api:coingecko:v1',
392
+ key: (c) => {
393
+ const limit = new URL(c.req.url).searchParams.get('limit') ?? String(defaultPairsLimit);
394
+ return cacheKey(`/gecko/${c.req.param('chainId')}/pairs?limit=${limit}`);
395
+ },
396
+ }), async (c) => {
397
+ if (Auth.narrowAccess)
398
+ return Auth.paidAccessError(c);
399
+ if (OpenApi.narrowValidation)
400
+ return OpenApi.validationError(c, {
401
+ code: 'query_invalid',
402
+ message: 'Invalid limit',
403
+ });
404
+ const { chainId } = c.req.valid('param');
405
+ const { limit } = c.req.valid('query');
406
+ try {
407
+ const rows = await listPairRows(c, chainId, limit);
408
+ const tokens = rows.flatMap((row) => [row.base, row.quote]);
409
+ const metadata = await loadTokenMetadata(c, chainId, tokens);
410
+ // Per-book resting-order reserves, with a fair per-book cap bounded by
411
+ // the page size. Best-effort: a failed scan degrades to zero reserves
412
+ // (uncached) rather than failing the whole page.
413
+ const scanCap = Math.max(minListReserveScanCap, Math.floor(listReserveOrderBudget / Math.max(rows.length, 1)));
414
+ const reserves = await loadBookReserves(c, chainId, rows.map((row) => row.base), scanCap);
415
+ // A degraded best-effort page (some reads failed) must not be cached as
416
+ // if authoritative; serve it once and let the next request retry.
417
+ if (metadata.failed || reserves.failed)
418
+ Cache.setPolicy(c, Cache.policies.noStore);
419
+ const pairs = rows.map((row) => resolvePairDetails(row, reserves.reserves, metadata.metadata));
420
+ return c.json(Response.validated(schema.PairsResponse, { pairs }), 200);
421
+ }
422
+ catch (cause) {
423
+ return Response.upstream(c, cause);
424
+ }
425
+ })
426
+ .get('/:chainId/pairs/:pairId', Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }), OpenApi.validate('param', schema.PairParams, {
427
+ code: 'pair_id_invalid',
428
+ message: 'Invalid pair id',
429
+ }), OpenApi.describeRoute({
430
+ description: 'Returns one Tempo stablecoin DEX trading pair in GeckoTerminal format.',
431
+ operationId: 'coingeckoPair',
432
+ responses: OpenApi.responses({
433
+ errors: {
434
+ 404: { description: 'No trading pair was found for that id.', codes: ['not_found'] },
435
+ },
436
+ success: {
437
+ description: 'One trading pair in GeckoTerminal format.',
438
+ schema: schema.PairResponse,
439
+ },
440
+ }),
441
+ summary: 'Get trading pair',
442
+ tags: ['CoinGecko'],
443
+ }), Cache.response({
444
+ cacheControl: Cache.policies.state,
445
+ name: 'tempo-api:coingecko:v1',
446
+ key: (c) => cacheKey(`/gecko/${c.req.param('chainId')}/pairs/${c.req.param('pairId')?.toLowerCase()}`),
447
+ }), async (c) => {
448
+ if (Auth.narrowAccess)
449
+ return Auth.paidAccessError(c);
450
+ if (OpenApi.narrowValidation)
451
+ return OpenApi.validationError(c, {
452
+ code: 'pair_id_invalid',
453
+ message: 'Invalid pair id',
454
+ });
455
+ const { chainId, pairId } = c.req.valid('param');
456
+ if (!Hash.validate(pairId))
457
+ return Response.error(c, {
458
+ code: 'pair_id_invalid',
459
+ message: 'Invalid pair id',
460
+ status: 400,
461
+ });
462
+ try {
463
+ const row = await getPairRow(c, chainId, pairId);
464
+ if (!row)
465
+ return notFound(c, 'Pair not found');
466
+ const tokens = [row.base, row.quote];
467
+ const metadata = await loadTokenMetadata(c, chainId, tokens);
468
+ // Full per-book scan cap for the single book — an accurate reserve for
469
+ // the pair GeckoTerminal fetches on demand.
470
+ const reserves = await loadBookReserves(c, chainId, [row.base]);
471
+ if (metadata.failed || reserves.failed)
472
+ Cache.setPolicy(c, Cache.policies.noStore);
473
+ const pair = resolvePairDetails(row, reserves.reserves, metadata.metadata);
474
+ return c.json(Response.validated(schema.PairResponse, { pair }), 200);
475
+ }
476
+ catch (cause) {
477
+ return Response.upstream(c, cause);
478
+ }
479
+ })
480
+ .get('/:chainId/events', Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }), OpenApi.validate('param', schema.ChainIdParam, {
481
+ code: 'chain_id_invalid',
482
+ message: 'Invalid chain id',
483
+ }), OpenApi.validate('query', schema.EventsQuery, {
484
+ code: 'query_invalid',
485
+ message: 'Invalid block range',
486
+ }), OpenApi.describeRoute({
487
+ description: 'Returns Tempo DEX swap events over a block range in GeckoTerminal format.',
488
+ operationId: 'coingeckoEvents',
489
+ responses: OpenApi.responses({
490
+ errors: { 400: 'The requested block range is invalid.' },
491
+ success: {
492
+ description: 'Swap events in the requested block range.',
493
+ schema: schema.EventsResponse,
494
+ },
495
+ }),
496
+ summary: 'List swap events',
497
+ tags: ['CoinGecko'],
498
+ }), Cache.response({
499
+ cacheControl: Cache.policies.state,
500
+ name: 'tempo-api:coingecko:v1',
501
+ key: (c) => {
502
+ const url = new URL(c.req.url);
503
+ const fromBlock = url.searchParams.get('fromBlock') ?? '';
504
+ const toBlock = url.searchParams.get('toBlock') ?? '';
505
+ return cacheKey(`/gecko/${c.req.param('chainId')}/events?fromBlock=${fromBlock}&toBlock=${toBlock}`);
506
+ },
507
+ }), async (c) => {
508
+ if (Auth.narrowAccess)
509
+ return Auth.paidAccessError(c);
510
+ if (OpenApi.narrowValidation)
511
+ return OpenApi.validationError(c, {
512
+ code: 'query_invalid',
513
+ message: 'Invalid block range',
514
+ });
515
+ const { chainId } = c.req.valid('param');
516
+ const query = c.req.valid('query');
517
+ // `toBlock` defaults to the latest indexed block; `fromBlock` to
518
+ // `toBlock` minus the default window. Both may still be passed
519
+ // explicitly (GeckoTerminal always does).
520
+ let toBlock = query.toBlock;
521
+ if (toBlock === undefined) {
522
+ try {
523
+ const block = await getLatestBlock(c, chainId);
524
+ if (!block)
525
+ return notFound(c, 'No indexed block found');
526
+ toBlock = block.blockNumber;
527
+ }
528
+ catch (cause) {
529
+ return Response.upstream(c, cause);
530
+ }
531
+ }
532
+ const fromBlock = query.fromBlock ?? Math.max(0, toBlock - defaultEventBlockRange);
533
+ if (toBlock < fromBlock)
534
+ return Response.error(c, {
535
+ code: 'query_invalid',
536
+ message: '`toBlock` must be greater than or equal to `fromBlock`',
537
+ status: 400,
538
+ });
539
+ if (toBlock - fromBlock > maxEventBlockRange)
540
+ return Response.error(c, {
541
+ code: 'query_invalid',
542
+ message: `Block range exceeds the maximum of ${maxEventBlockRange} blocks`,
543
+ status: 400,
544
+ });
545
+ try {
546
+ const events = await listEvents(c, chainId, fromBlock, toBlock);
547
+ return c.json(Response.validated(schema.EventsResponse, { events }), 200);
548
+ }
549
+ catch (cause) {
550
+ return Response.upstream(c, cause);
551
+ }
552
+ });
553
+ }
554
+ /** Standard 404 envelope for the adapter. */
555
+ function notFound(c, message) {
556
+ return Response.error(c, { code: 'not_found', message, status: 404 });
557
+ }
558
+ /**
559
+ * Resolves the block tip GeckoTerminal should treat as fully indexed: the lower
560
+ * of the latest indexed block and the latest indexed DEX `OrderFilled` event.
561
+ * Capping to the fill tip prevents GeckoTerminal from polling `/events` past the
562
+ * point swaps are decoded (it never re-polls a range), at the cost of a slightly
563
+ * stale tip during swap-quiet periods. Mirrors the legacy `tempoxyz/api`
564
+ * adapter's query and `min(blockTip, fillTip)` logic. It is never cached, so DEX
565
+ * tools polling this endpoint always see the current tip.
566
+ */
567
+ async function getLatestBlock(c, chainId) {
568
+ const tidx = c.get('getTidx')(chainId);
569
+ const [blocksTip, fillsTip] = await Promise.all([
570
+ tidx.fetch({
571
+ chainId,
572
+ query: 'SELECT num, timestamp FROM blocks ORDER BY num DESC LIMIT 1',
573
+ }),
574
+ tidx.fetch({
575
+ chainId,
576
+ query: `SELECT block_num AS num, block_timestamp AS ts FROM OrderFilled WHERE address = '${stablecoinDex}' ORDER BY block_num DESC LIMIT 1`,
577
+ signatures: [orderFilledSignature],
578
+ }),
579
+ ]);
580
+ const blocksRow = blocksTip.rows[0];
581
+ const blocksBlock = blocksRow ? Value.toNumber(blocksRow['num']) : undefined;
582
+ if (blocksBlock === undefined)
583
+ return undefined;
584
+ const fillsRow = fillsTip.rows[0];
585
+ const fillsBlock = (fillsRow ? Value.toNumber(fillsRow['num']) : undefined) ?? 0;
586
+ const blockNumber = fillsBlock > 0 ? Math.min(blocksBlock, fillsBlock) : blocksBlock;
587
+ const rawTimestamp = blockNumber === fillsBlock && fillsRow ? fillsRow['ts'] : blocksRow['timestamp'];
588
+ return { blockNumber, blockTimestamp: toUnixSeconds(rawTimestamp) };
589
+ }
590
+ /** Reads TIP-20 metadata and shapes it into the GeckoTerminal asset object. */
591
+ async function resolveAsset(c, chainId, address) {
592
+ const metadata = await getTokenMetadata(c, { address, chainId });
593
+ if (!metadata)
594
+ return undefined;
595
+ return {
596
+ id: Address.checksum(address),
597
+ name: metadata.name,
598
+ symbol: metadata.symbol,
599
+ decimals: metadata.decimals,
600
+ totalSupply: formatUnits(BigInt(metadata.totalSupply), metadata.decimals),
601
+ };
602
+ }
603
+ /**
604
+ * Lists the newest stablecoin-DEX pairs from the pre-decoded `dex_pairs` table,
605
+ * bounded by `limit`. Ordered newest-first (not the legacy oldest-first): the
606
+ * full set is far too large to enumerate at Tempo API's scale (820k+ pairs), and
607
+ * GeckoTerminal discovers pairs from `/events` rather than this list.
608
+ */
609
+ async function listPairRows(c, chainId, limit) {
610
+ const tidx = c.get('getTidx')(chainId);
611
+ const result = await tidx.fetch({
612
+ chainId,
613
+ engine: 'clickhouse',
614
+ query: `SELECT key, base, quote, tx_hash, block_num, block_timestamp FROM dex_pairs WHERE address = '${stablecoinDex}' ORDER BY block_num DESC LIMIT ${limit}`,
615
+ });
616
+ const rows = [];
617
+ for (const row of result.rows) {
618
+ const parsed = parsePairRow(row);
619
+ if (parsed)
620
+ rows.push(parsed);
621
+ }
622
+ return rows;
623
+ }
624
+ /** Resolves a single pair's `dex_pairs` row by its order-book key. */
625
+ async function getPairRow(c, chainId, pairId) {
626
+ const tidx = c.get('getTidx')(chainId);
627
+ const result = await tidx.fetch({
628
+ chainId,
629
+ engine: 'clickhouse',
630
+ query: `SELECT key, base, quote, tx_hash, block_num, block_timestamp FROM dex_pairs WHERE address = '${stablecoinDex}' AND key = '${pairId.toLowerCase()}' ORDER BY block_num ASC LIMIT 1`,
631
+ });
632
+ const row = result.rows[0];
633
+ return row ? parsePairRow(row) : undefined;
634
+ }
635
+ /** Validates and shapes one `dex_pairs` row. */
636
+ function parsePairRow(row) {
637
+ const key = Schema.Hash.safeParse(row['key']);
638
+ const base = Schema.TokenAddress.safeParse(row['base']);
639
+ const quote = Schema.TokenAddress.safeParse(row['quote']);
640
+ if (!key.success || !base.success || !quote.success)
641
+ return undefined;
642
+ const transactionHash = Schema.Hash.safeParse(row['tx_hash']);
643
+ const blockNumber = Value.toNumber(row['block_num']);
644
+ const timestamp = Value.toIsoDateTime(row['block_timestamp']);
645
+ return {
646
+ base: base.data,
647
+ blockNumber,
648
+ key: key.data,
649
+ quote: quote.data,
650
+ timestamp: timestamp ? Math.floor(new Date(timestamp).getTime() / 1_000) : undefined,
651
+ transactionHash: transactionHash.success ? transactionHash.data : undefined,
652
+ };
653
+ }
654
+ /**
655
+ * Builds the GeckoTerminal pair object from pre-loaded token metadata and
656
+ * per-book resting-order reserves. Pure (no I/O): callers load the `metadata`
657
+ * map via {@link loadTokenMetadata} and the base-keyed `reserves` map via
658
+ * {@link resolveBookReserves}. When a token's metadata is missing (a degraded
659
+ * best-effort read), its reserve is reported as `'0'` rather than formatting a
660
+ * raw amount with an unknown (defaulted-to-zero) decimals, which would be off by
661
+ * orders of magnitude.
662
+ */
663
+ function resolvePairDetails(row, reserves, metadata) {
664
+ const base = metadata.get(row.base);
665
+ const quote = metadata.get(row.quote);
666
+ const baseDecimals = base?.decimals ?? 0;
667
+ const quoteDecimals = quote?.decimals ?? 0;
668
+ // Reserves are keyed by lowercase base token (a book's identity); `row.base`
669
+ // is already lowercase from the indexer.
670
+ const reserve = reserves.get(row.base);
671
+ return {
672
+ id: row.key,
673
+ dexKey,
674
+ asset0Id: Address.checksum(row.base),
675
+ asset1Id: Address.checksum(row.quote),
676
+ token0: {
677
+ address: Address.checksum(row.base),
678
+ name: base?.name ?? '',
679
+ symbol: base?.symbol ?? '',
680
+ decimals: baseDecimals,
681
+ totalSupply: base ? formatUnits(BigInt(base.totalSupply), baseDecimals) : '0',
682
+ },
683
+ token1: {
684
+ address: Address.checksum(row.quote),
685
+ name: quote?.name ?? '',
686
+ symbol: quote?.symbol ?? '',
687
+ decimals: quoteDecimals,
688
+ totalSupply: quote ? formatUnits(BigInt(quote.totalSupply), quoteDecimals) : '0',
689
+ },
690
+ reserve0: base ? formatUnits(reserve?.base ?? 0n, baseDecimals) : '0',
691
+ reserve1: quote ? formatUnits(reserve?.quote ?? 0n, quoteDecimals) : '0',
692
+ ...(row.blockNumber !== undefined && { createdAtBlockNumber: row.blockNumber }),
693
+ ...(row.timestamp !== undefined && { createdAtBlockTimestamp: row.timestamp }),
694
+ ...(row.transactionHash !== undefined && { createdAtTxnId: row.transactionHash }),
695
+ };
696
+ }
697
+ /**
698
+ * Reads RPC metadata for a set of tokens, returning an `address -> metadata`
699
+ * map. Deduplicates so a quote shared across many books (e.g. pathUSD) is read
700
+ * once per request, and reads in {@link tokenReadChunkSize} chunks so the
701
+ * client's deployless multicall never overflows the RPC's per-call limit. A read
702
+ * failure maps the token to `undefined` and flips `failed` rather than failing
703
+ * the whole response (metadata is best-effort); callers must not cache a
704
+ * degraded page.
705
+ */
706
+ async function loadTokenMetadata(c, chainId, tokens) {
707
+ const metadata = new Map();
708
+ const unique = [...new Set(tokens)];
709
+ let failed = false;
710
+ await mapSeries(chunk(unique, tokenReadChunkSize), (group) => Promise.all(group.map(async (address) => {
711
+ try {
712
+ metadata.set(address, await getTokenMetadata(c, {
713
+ address: address,
714
+ chainId,
715
+ }));
716
+ }
717
+ catch {
718
+ failed = true;
719
+ metadata.set(address, undefined);
720
+ }
721
+ })));
722
+ return { failed, metadata };
723
+ }
724
+ /**
725
+ * Reads the DEX's current on-chain `balanceOf` for a set of tokens, returning a
726
+ * `token -> balance` map. Deduplicates so a quote shared across many books (e.g.
727
+ * pathUSD) is read once per request, and reads in {@link tokenReadChunkSize}
728
+ * chunks so the client's deployless multicall never overflows the RPC's per-call
729
+ * limit. A read failure maps the token to `0n` and flips `failed` rather than
730
+ * failing the whole response (reserves are best-effort); callers must not cache
731
+ * a degraded page. See the module docstring for the `reserve1` lower-bound
732
+ * caveat.
733
+ */
734
+ async function loadDexBalances(c, chainId, tokens) {
735
+ const balances = new Map();
736
+ const unique = [...new Set(tokens)];
737
+ let failed = false;
738
+ if (unique.length === 0)
739
+ return { balances, failed };
740
+ const client = c.get('getClient')(chainId);
741
+ await Timing.time(c, 'coingecko_reserves', () => mapSeries(chunk(unique, tokenReadChunkSize), (group) => Promise.all(group.map(async (token) => {
742
+ try {
743
+ const balance = await client.token.getBalance({
744
+ account: dexAccount,
745
+ token: token,
746
+ });
747
+ balances.set(token, balance.amount);
748
+ }
749
+ catch {
750
+ failed = true;
751
+ balances.set(token, 0n);
752
+ }
753
+ }))));
754
+ return { balances, failed };
755
+ }
756
+ /**
757
+ * Loads per-book resting-order reserves for a set of base tokens via
758
+ * {@link resolveBookReserves}, wrapping it as a best-effort read: a scan failure
759
+ * degrades to an empty map (zero reserves) and flips `failed` so callers don't
760
+ * cache a degraded page, rather than failing the whole response. `scanCap`
761
+ * bounds the newest-placement window per book; omit for the per-pair default.
762
+ */
763
+ async function loadBookReserves(c, chainId, bases, scanCap) {
764
+ if (bases.length === 0)
765
+ return { failed: false, reserves: new Map() };
766
+ try {
767
+ const { reserves } = await Timing.time(c, 'coingecko_reserves', () => resolveBookReserves(c, { bases, chainId, scanCap }));
768
+ return { failed: false, reserves };
769
+ }
770
+ catch {
771
+ return { failed: true, reserves: new Map() };
772
+ }
773
+ }
774
+ /**
775
+ * Builds the GeckoTerminal swap-event feed for a bounded block range.
776
+ *
777
+ * One event per `OrderFilled`. Each fill is oriented and priced from its
778
+ * point-in-time `(token, isBid, tick)` state ({@link resolveOrderStates}, which
779
+ * — unlike the legacy adapter's `OrderPlaced`-only lookup — also tracks flip
780
+ * orders), with amounts base-denominated and the quote leg reconstructed from
781
+ * the tick.
782
+ */
783
+ async function listEvents(c, chainId, fromBlock, toBlock) {
784
+ const tidx = c.get('getTidx')(chainId);
785
+ const priceScale = BigInt(Tick.priceScale);
786
+ const fills = await Timing.time(c, 'coingecko_fills', async () => {
787
+ const result = await tidx.fetch({
788
+ chainId,
789
+ engine: 'clickhouse',
790
+ query: `
791
+ SELECT toString("orderId") AS orderId, taker, toString("amountFilled") AS amountFilled,
792
+ block_num, block_timestamp, tx_hash, tx_idx, log_idx
793
+ FROM dex_fills
794
+ WHERE address = '${stablecoinDex}' AND block_num >= ${fromBlock} AND block_num <= ${toBlock}
795
+ ORDER BY block_num ASC, log_idx ASC
796
+ `,
797
+ });
798
+ const rows = [];
799
+ for (const row of result.rows) {
800
+ const parsed = parseFillRow(row);
801
+ if (parsed)
802
+ rows.push(parsed);
803
+ }
804
+ return rows;
805
+ });
806
+ if (fills.length === 0)
807
+ return [];
808
+ // Point-in-time order state for every filled order id. The order ids are
809
+ // chunked: `tidx.fetch` sends queries as GET, so a single `topic1 IN (...)`
810
+ // of every id in a wide window overflows the request URI (HTTP 414). Each
811
+ // chunk resolves independently and the (disjoint) maps merge.
812
+ const orderIds = [...new Set(fills.map((fill) => fill.orderId))];
813
+ const states = new Map();
814
+ await Timing.time(c, 'coingecko_states', async () => {
815
+ const partials = await mapSeries(chunk(orderIds, orderIdChunkSize), (ids) => resolveOrderStates(c, {
816
+ chainId,
817
+ maxBlock: toBlock,
818
+ minBlock: fromBlock,
819
+ orderIds: ids,
820
+ }));
821
+ for (const partial of partials)
822
+ for (const [id, events] of partial)
823
+ states.set(id, events);
824
+ });
825
+ // Resolve each book's quote token, plus its pair id (the `dex_pairs` key) so
826
+ // event `pairId`s line up with `/pairs`. Genesis books absent from
827
+ // `dex_pairs` fall back to the on-chain `quoteToken()` and a derived key.
828
+ const bases = [
829
+ ...new Set([...states.values()].flatMap((events) => events.map((event) => event.token))),
830
+ ];
831
+ const { keyByBase, quoteByBase } = await resolvePairIndex(c, chainId, bases);
832
+ // Token decimals (RPC metadata) and best-effort current reserves (RPC
833
+ // `balanceOf`) for every base/quote involved. Loaded in bounded chunks and
834
+ // sequentially (not `Promise.all`) so the two read kinds don't merge into the
835
+ // same multicall window and overflow the RPC's per-call limit. A degraded read
836
+ // drops the affected events below and must not be cached as authoritative.
837
+ const tokens = [...new Set([...bases, ...quoteByBase.values()])];
838
+ const metadata = await loadTokenMetadata(c, chainId, tokens);
839
+ const reserves = await loadDexBalances(c, chainId, tokens);
840
+ if (metadata.failed || reserves.failed)
841
+ Cache.setPolicy(c, Cache.policies.noStore);
842
+ const balances = reserves.balances;
843
+ const decimals = new Map();
844
+ for (const [token, info] of metadata.metadata)
845
+ if (info)
846
+ decimals.set(token, info.decimals);
847
+ const events = [];
848
+ const eventIndexByTx = new Map();
849
+ for (const fill of fills) {
850
+ const state = latestStateBefore(states.get(fill.orderId), fill.blockNumber, fill.logIndex);
851
+ if (!state)
852
+ continue;
853
+ const quote = quoteByBase.get(state.token);
854
+ if (!quote)
855
+ continue;
856
+ const baseDecimals = decimals.get(state.token);
857
+ const quoteDecimals = decimals.get(quote);
858
+ if (baseDecimals === undefined || quoteDecimals === undefined)
859
+ continue;
860
+ const pairId = keyByBase.get(state.token) ?? deriveBookKey(state.token, quote);
861
+ const tickPrice = priceScale + BigInt(state.tick);
862
+ const baseAmount = fill.amountFilled;
863
+ const quoteAmount = (baseAmount * tickPrice) / priceScale;
864
+ const baseDec = formatUnits(baseAmount, baseDecimals);
865
+ const quoteDec = formatUnits(quoteAmount, quoteDecimals);
866
+ // Maker bid: taker sells base into the book (base in, quote out).
867
+ // Maker ask: taker buys base out of the book (quote in, base out).
868
+ const sides = state.isBid
869
+ ? { asset0In: baseDec, asset1Out: quoteDec }
870
+ : { asset0Out: baseDec, asset1In: quoteDec };
871
+ const baseReserve = balances.get(state.token) ?? 0n;
872
+ const quoteReserve = balances.get(quote) ?? 0n;
873
+ const eventIndex = eventIndexByTx.get(fill.transactionHash) ?? 0;
874
+ eventIndexByTx.set(fill.transactionHash, eventIndex + 1);
875
+ events.push({
876
+ block: { blockNumber: fill.blockNumber, blockTimestamp: fill.timestamp },
877
+ eventType: 'swap',
878
+ txnId: fill.transactionHash,
879
+ txnIndex: fill.txnIndex,
880
+ eventIndex,
881
+ maker: Address.checksum(fill.taker),
882
+ pairId,
883
+ ...sides,
884
+ priceNative: computePriceNative(tickPrice, baseDecimals, quoteDecimals, priceScale),
885
+ reserves: {
886
+ asset0: formatUnits(baseReserve, baseDecimals),
887
+ asset1: formatUnits(quoteReserve, quoteDecimals),
888
+ },
889
+ });
890
+ }
891
+ return events;
892
+ }
893
+ /** Parses one `dex_fills` row, dropping malformed rows. */
894
+ function parseFillRow(row) {
895
+ const orderId = Value.toIntegerString(row['orderId']);
896
+ const taker = Schema.Address.safeParse(row['taker']);
897
+ const amountFilled = Value.toIntegerString(row['amountFilled']);
898
+ const blockNumber = Value.toNumber(row['block_num']);
899
+ const logIndex = Value.toNumber(row['log_idx']);
900
+ const txnIndex = Value.toNumber(row['tx_idx']);
901
+ const transactionHash = Schema.Hash.safeParse(row['tx_hash']);
902
+ const timestamp = Value.toIsoDateTime(row['block_timestamp']);
903
+ if (orderId === undefined ||
904
+ !taker.success ||
905
+ amountFilled === undefined ||
906
+ blockNumber === undefined ||
907
+ logIndex === undefined ||
908
+ txnIndex === undefined ||
909
+ !transactionHash.success ||
910
+ timestamp === undefined)
911
+ return undefined;
912
+ return {
913
+ amountFilled: BigInt(amountFilled),
914
+ blockNumber,
915
+ logIndex,
916
+ orderId,
917
+ taker: taker.data,
918
+ timestamp: Math.floor(new Date(timestamp).getTime() / 1_000),
919
+ transactionHash: transactionHash.data,
920
+ txnIndex,
921
+ };
922
+ }
923
+ /**
924
+ * Resolves, for a set of base tokens, each book's quote token and `dex_pairs`
925
+ * key. Indexed pairs come from `dex_pairs`; the rest fall back to the on-chain
926
+ * `quoteToken()` ({@link resolveBookQuotes}) with a key derived from
927
+ * `keccak256(base ++ quote)`.
928
+ */
929
+ async function resolvePairIndex(c, chainId, bases) {
930
+ const keyByBase = new Map();
931
+ const quoteByBase = new Map();
932
+ if (bases.length === 0)
933
+ return { keyByBase, quoteByBase };
934
+ // Chunk the `base IN (...)` lookup for the same GET-URI reason as the order
935
+ // states above; chunks resolve serially and merge.
936
+ const tidx = c.get('getTidx')(chainId);
937
+ const results = await mapSeries(chunk(bases, addressChunkSize), (group) => {
938
+ const list = group.map((base) => `'${base}'`).join(', ');
939
+ return tidx.fetch({
940
+ chainId,
941
+ engine: 'clickhouse',
942
+ query: `SELECT key, base, quote FROM dex_pairs WHERE address = '${stablecoinDex}' AND base IN (${list})`,
943
+ });
944
+ });
945
+ for (const result of results)
946
+ for (const row of result.rows) {
947
+ const parsed = parsePairRow(row);
948
+ if (!parsed)
949
+ continue;
950
+ keyByBase.set(parsed.base, parsed.key);
951
+ quoteByBase.set(parsed.base, parsed.quote);
952
+ }
953
+ const missing = bases.filter((base) => !quoteByBase.has(base));
954
+ const fallback = await resolveBookQuotes(c, { bases: missing, chainId });
955
+ for (const [base, quote] of fallback)
956
+ quoteByBase.set(base, quote);
957
+ return { keyByBase, quoteByBase };
958
+ }
959
+ /** Latest order state strictly before a fill's `(blockNumber, logIndex)`. */
960
+ function latestStateBefore(events, blockNumber, logIndex) {
961
+ if (!events)
962
+ return undefined;
963
+ for (let i = events.length - 1; i >= 0; i--) {
964
+ const event = events[i];
965
+ if (event.blockNumber < blockNumber ||
966
+ (event.blockNumber === blockNumber && event.logIndex < logIndex))
967
+ return event;
968
+ }
969
+ return undefined;
970
+ }
971
+ /** Splits `items` into consecutive groups of at most `size`. */
972
+ function chunk(items, size) {
973
+ const groups = [];
974
+ for (let i = 0; i < items.length; i += size)
975
+ groups.push(items.slice(i, i + size));
976
+ return groups;
977
+ }
978
+ /**
979
+ * Maps `fn` over `items` strictly one at a time. ClickHouse chunk queries are
980
+ * issued serially on purpose: the managed indexer rejects bursts of concurrent
981
+ * queries with a fast HTTP 422 (verified: heavy baseline scans 422 at 2-3 in
982
+ * flight), and each chunk is individually fast (~2 s), so serial execution
983
+ * trades a little latency for reliability. Real GeckoTerminal polling windows
984
+ * are small and cached, so the latency is rarely on the hot path.
985
+ */
986
+ async function mapSeries(items, fn) {
987
+ const results = [];
988
+ for (const item of items)
989
+ results.push(await fn(item));
990
+ return results;
991
+ }
992
+ /** `keccak256(base ++ quote)` — the on-chain order-book key. */
993
+ function deriveBookKey(base, quote) {
994
+ return Hash.keccak256(`0x${base.slice(2)}${quote.slice(2)}`);
995
+ }
996
+ /**
997
+ * Formats `priceNative` as quote-per-base at 36-dp precision, matching the
998
+ * legacy adapter: `tickPrice * 10^(baseDecimals + 36) / (scale * 10^quoteDecimals)`.
999
+ */
1000
+ function computePriceNative(tickPrice, baseDecimals, quoteDecimals, scale) {
1001
+ const priceBig = (tickPrice * 10n ** (BigInt(baseDecimals) + pricePrecision)) /
1002
+ (scale * 10n ** BigInt(quoteDecimals));
1003
+ return formatUnits(priceBig, Number(pricePrecision));
1004
+ }
1005
+ /** Coerces a TIDX timestamp (Unix seconds/ms or ISO/CH text) to Unix seconds. */
1006
+ function toUnixSeconds(value) {
1007
+ const iso = Value.toIsoDateTime(value);
1008
+ if (iso)
1009
+ return Math.floor(new Date(iso).getTime() / 1_000);
1010
+ const num = Value.toNumber(value);
1011
+ if (num === undefined)
1012
+ return 0;
1013
+ return num > 1e12 ? Math.floor(num / 1_000) : num;
1014
+ }
1015
+ //# sourceMappingURL=coingecko.js.map