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,533 @@
1
+ import { Hono, type Context } from 'hono'
2
+ import { Hex, Value as core_Value } from 'ox'
3
+ import * as z from 'zod/mini'
4
+
5
+ import type * as App from '../App.js'
6
+ import * as Auth from '../internal/Auth.js'
7
+ import * as Cache from '../internal/Cache.js'
8
+ import * as Cursor from '../internal/Cursor.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 * as VerifiedTokens from '../internal/VerifiedTokens.js'
15
+ import * as FeeAmm from './fee-amm.js'
16
+ import * as Tokens from './tokens.js'
17
+
18
+ // TIP-20 tokens are minted by the TIP-20 factory and live at deterministic
19
+ // `0x20c0…`-prefixed addresses (e.g. pathUSD `0x20c0…0000`). The indexer's
20
+ // `token_balances_snapshot` view, by contrast, records balances for every
21
+ // Transfer-emitting contract, so balance queries filter on this prefix to keep
22
+ // non-TIP-20 tokens (which have no resolvable TIP-20 metadata) out of results.
23
+ const tip20AddressPrefix = '0x20c0'
24
+
25
+ /** Zod schemas owned by the balance handlers. */
26
+ export namespace schema {
27
+ /** Schemas for the getAddressBalances operation. */
28
+ export namespace getAddressBalances {
29
+ /** Path parameters for address balance requests. */
30
+ export const Params = z
31
+ .object({
32
+ address: Schema.Address.check(
33
+ z.describe('The account address whose token balances you want to list.'),
34
+ ),
35
+ })
36
+ .check(z.describe('Path parameters for listing an account’s token balances.'))
37
+
38
+ /** Query parameters for address balance requests. */
39
+ export const Query = z
40
+ .strictObject({
41
+ chainId: Schema.ChainIdQuery,
42
+ currency: z
43
+ .optional(z.string())
44
+ .check(
45
+ z.meta({ examples: VerifiedTokens.currencies }),
46
+ z.describe(
47
+ 'Only include verified tokens denominated in this currency (e.g. `USD`). ' +
48
+ 'Case-insensitive. Implies `verified=true` because the balances ' +
49
+ 'snapshot does not store currency for unverified tokens. ' +
50
+ 'Unknown currency strings are accepted but match no rows.',
51
+ ),
52
+ ),
53
+ cursor: Schema.Cursor,
54
+ feeEligible: z
55
+ .optional(Schema.booleanQuery())
56
+ .check(
57
+ z.describe(
58
+ 'When `true`, include only tokens eligible to pay transaction fees on Tempo: ' +
59
+ 'verified tokens that have a Fee AMM pool, plus the default fee token ' +
60
+ '(`pathUSD`). Use this to avoid a second `/fee-amm/pools` query when you ' +
61
+ 'only want fee-payable holdings.',
62
+ ),
63
+ z.meta({ examples: [true] }),
64
+ ),
65
+ include: Schema.totalCountInclude,
66
+ limit: Schema.Limit,
67
+ page: Schema.Page,
68
+ verified: z
69
+ .optional(Schema.booleanQuery())
70
+ .check(
71
+ z.describe(
72
+ 'When `true`, include only tokens from Tempo’s curated verified token list.',
73
+ ),
74
+ z.meta({ examples: [true] }),
75
+ ),
76
+ })
77
+ .check(
78
+ ...Schema.pageChecks(),
79
+ z.describe('Query parameters for listing an account’s token balances.'),
80
+ )
81
+
82
+ /**
83
+ * A TIP-20 token balance held by an address: the held `amount` in the
84
+ * token's base unit, its `formatted` value, and the trimmed `token`
85
+ * reference inlined per row. Rows whose token metadata cannot be resolved
86
+ * are omitted.
87
+ */
88
+ export const Balance = Schema.describe(
89
+ z.object({
90
+ amount: z
91
+ .string()
92
+ .check(
93
+ z.regex(/^\d+$/),
94
+ z.describe(
95
+ 'A non-negative whole number, given as a decimal string so very large token amounts keep full precision. Expressed in the smallest unit of the token.',
96
+ ),
97
+ z.meta({ examples: ['1500000'] }),
98
+ ),
99
+ formatted: z
100
+ .string()
101
+ .check(
102
+ z.describe(
103
+ 'The same balance in human-readable decimal form, using the token’s `decimals`.',
104
+ ),
105
+ z.meta({ examples: ['1.5'] }),
106
+ ),
107
+ id: z
108
+ .string()
109
+ .check(
110
+ z.describe(
111
+ 'A stable resource ID for this balance, equal to the token contract address.',
112
+ ),
113
+ z.meta({ examples: ['0x20c0000000000000000000008f5425160ebe5525'] }),
114
+ ),
115
+ feeEligible: z
116
+ .optional(z.boolean())
117
+ .check(
118
+ z.describe(
119
+ 'Whether this token can be used to pay transaction fees on Tempo: `true` when ' +
120
+ 'the token is verified and has a Fee AMM pool, or is the default fee token ' +
121
+ '(`pathUSD`). Lets you identify fee-payable holdings without a separate ' +
122
+ '`/fee-amm/pools` query. Omitted (best-effort) when the fee-token lookup ' +
123
+ 'is unavailable.',
124
+ ),
125
+ z.meta({ examples: [true] }),
126
+ ),
127
+ token: Tokens.schema.Token,
128
+ }),
129
+ 'One TIP-20 token balance held by this account, with both raw and human-readable amounts.',
130
+ )
131
+
132
+ /** Page of TIP-20 token balances held by an address, ordered by amount descending. */
133
+ export const Response = z
134
+ .object({
135
+ data: z
136
+ .array(Balance)
137
+ .check(z.describe('The balances in this page, ordered from largest to smallest amount.')),
138
+ meta: z
139
+ .optional(Schema.CountMeta)
140
+ .check(z.describe('Page-level resources you requested with `include`.')),
141
+ nextCursor: Schema.NextCursor,
142
+ })
143
+ .check(z.describe('A page of TIP-20 token balances held by this account.'))
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Creates address-scoped balance handlers, mounted under the `/addresses`
149
+ * composer. Exposes `GET /:address/balances`, the held-balance page.
150
+ */
151
+ export function addresses() {
152
+ return new Hono<App.Environment>().get(
153
+ '/:address/balances',
154
+ Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }),
155
+ OpenApi.validate('param', schema.getAddressBalances.Params, {
156
+ code: 'address_invalid',
157
+ message: 'Invalid account address',
158
+ }),
159
+ OpenApi.validate('query', schema.getAddressBalances.Query, {
160
+ code: 'query_invalid',
161
+ message: 'Invalid query parameters',
162
+ }),
163
+ OpenApi.describeRoute({
164
+ description:
165
+ 'Lists how much of each TIP-20 token an account holds, ordered from largest to smallest balance. Amounts are returned in raw base units and human-readable form.',
166
+ operationId: 'getAddressBalances',
167
+ responses: OpenApi.responses({
168
+ errors: { 502: 'The upstream indexer could not serve this request.' },
169
+ success: {
170
+ description: 'A page of token balances for this account.',
171
+ schema: schema.getAddressBalances.Response,
172
+ },
173
+ }),
174
+ summary: 'List address balances',
175
+ tags: ['Balances'],
176
+ }),
177
+ Cache.response<App.Environment>({
178
+ cacheControl: Cache.policies.noStore,
179
+ name: 'tempo-api:addresses:v1',
180
+ key: (c) => Cache.urlKey(c, schema.getAddressBalances.Query),
181
+ }),
182
+ async (c) => {
183
+ if (Auth.narrowAccess) return Auth.paidAccessError(c)
184
+ if (OpenApi.narrowValidation)
185
+ return OpenApi.validationError(c, {
186
+ code: 'address_invalid',
187
+ message: 'Invalid request parameters',
188
+ })
189
+ if (OpenApi.narrowValidation)
190
+ return OpenApi.validationError(c, {
191
+ code: 'query_invalid',
192
+ message: 'Invalid request parameters',
193
+ })
194
+
195
+ const { address } = c.req.valid('param')
196
+ const query = c.req.valid('query')
197
+
198
+ try {
199
+ const chainId = query.chainId ?? c.get('chainId')
200
+ // `currency` only makes sense against verified tokens, so it implies
201
+ // `verified=true` from the caller's perspective.
202
+ const verified = query.verified || query.currency !== undefined
203
+ const feeEligible = query.feeEligible ?? false
204
+
205
+ // The fee-token set powers both the per-row `feeEligible` flag (always)
206
+ // and the optional `feeEligible=true` page restriction. It is memoized
207
+ // per chain (a near-constant), so one read feeds the page, the count,
208
+ // and the row enrichment. It is a best-effort enrichment: a lookup
209
+ // failure omits `feeEligible` rather than failing the page — unless the
210
+ // caller explicitly asked to filter by `feeEligible`, where the filter
211
+ // can't be honored, so the failure surfaces as the declared `502`.
212
+ const feeTokens = await FeeAmm.feeTokenSet(c, chainId).catch((cause) => {
213
+ if (feeEligible) throw cause
214
+ return undefined
215
+ })
216
+
217
+ // `totalCount` (the number of matching holdings) is opt-in and shares
218
+ // the page's filters; run it concurrently with the page. Best-effort: a
219
+ // count failure omits `meta` rather than failing the page.
220
+ const countPromise = query.include.includes('totalCount')
221
+ ? countAddressBalances(c, {
222
+ address,
223
+ chainId,
224
+ currency: query.currency,
225
+ feeEligible,
226
+ feeTokens,
227
+ verified,
228
+ }).catch(() => undefined)
229
+ : undefined
230
+
231
+ const balances = await getAddressBalances(c, {
232
+ address,
233
+ chainId,
234
+ currency: query.currency,
235
+ cursor: query.cursor,
236
+ feeEligible,
237
+ feeTokens,
238
+ limit: query.limit,
239
+ page: query.page,
240
+ verified,
241
+ })
242
+
243
+ const meta = countPromise ? await countPromise : undefined
244
+
245
+ return c.json(
246
+ Response.validated(schema.getAddressBalances.Response, {
247
+ ...balances,
248
+ ...(meta ? { meta } : {}),
249
+ }),
250
+ 200,
251
+ )
252
+ } catch (cause) {
253
+ return Response.upstream(c, cause)
254
+ }
255
+ },
256
+ )
257
+ }
258
+
259
+ async function getAddressBalances(
260
+ c: Context<App.Environment>,
261
+ options: getAddressBalances.Options,
262
+ ) {
263
+ const { address, chainId, currency, feeEligible, feeTokens, limit, verified } = options
264
+ const tidx = c.get('getTidx')(chainId)
265
+ // Keyset pagination on `(balance, token)`: balance is descending and `token`
266
+ // is a stable tiebreaker for the many holdings sharing a balance. A malformed
267
+ // cursor falls back to the head page. Note: because `balance` is mutable, this
268
+ // is best-effort across balance changes (the inherent limit of ranking feeds).
269
+ const cursor = options.cursor ? Cursor.decode(options.cursor, ['uint', 'address']) : undefined
270
+ // Bounded positional lane (exclusive with `cursor` at the schema): page 1 is
271
+ // the head page, so it shares the head's SQL and cache entries; deeper pages
272
+ // translate to a SQL OFFSET of `(page - 1) * limit` rows.
273
+ const offset =
274
+ options.page !== undefined && options.page > 1 ? (options.page - 1) * limit : undefined
275
+
276
+ // Build the optional `token IN (…)` restriction before hitting TIDX. The
277
+ // verified/`currency` filter and the `feeEligible` filter each contribute a set
278
+ // of allowed addresses (`currency` matched case-insensitively so callers can
279
+ // type `usd`/`USD`/`Usd`); when both are present the page is their
280
+ // intersection. If any active filter rules every token out, return an empty
281
+ // page without a round trip. The snapshot is reused for the page enrichment
282
+ // below.
283
+ const snapshot = await VerifiedTokens.snapshot(c, chainId)
284
+ const allowed = allowedTokens({ currency, feeEligible, feeTokens, snapshot, verified })
285
+ const tokenFilter = allowed?.map((address) => `'${address}'`)
286
+ if (tokenFilter?.length === 0) return { data: [], nextCursor: null }
287
+
288
+ const holdings = await Timing.time(c, 'address_balances', async () => {
289
+ // A held token is one this address holds a positive amount of. Read the
290
+ // live `address_balances` view (`holder`-keyed: one row per
291
+ // `(holder, token, balance)`), which aggregates `address_holder_deltas`
292
+ // directly so balances reflect the indexer's chain head (no periodic
293
+ // snapshot refresh — see the indexer's `address_holder_deltas` MV). The
294
+ // `holder` equality prunes the view by its leading sort key, so this stays
295
+ // a bounded read per request. Fetch one extra row to detect `hasMore`
296
+ // without a separate count query. `startsWith` restricts results to TIP-20
297
+ // tokens (see `tip20AddressPrefix`) before pagination, so `hasMore`/page
298
+ // sizes stay correct and every returned token has resolvable metadata. The
299
+ // inline query is cast to `string` so TIDX treats it as a dynamic
300
+ // ClickHouse query that needs no event signature for the
301
+ // `address_balances` view.
302
+ //
303
+ // Let upstream failures propagate: this is a dedicated endpoint, so a
304
+ // failure should surface as the declared `502` rather than masquerade as
305
+ // an address holding zero tokens.
306
+ const keyset =
307
+ cursor !== undefined
308
+ ? ` AND ${Cursor.keyset([
309
+ { literal: Cursor.literal(cursor[0]!, 'uint'), name: 'balance', order: 'desc' },
310
+ { literal: Cursor.literal(cursor[1]!, 'address'), name: 'token', order: 'asc' },
311
+ ])}`
312
+ : ''
313
+ const tokenIn = tokenFilter ? ` AND token IN (${tokenFilter.join(', ')})` : ''
314
+
315
+ const result = await tidx.fetch({
316
+ chainId,
317
+ engine: 'clickhouse',
318
+ // `balance` is UInt256; SELECT it as a string so values > 2^53
319
+ // survive JSON without precision loss. The keyset cursor encodes the
320
+ // row's exact balance, so any rounding would re-include the cursor
321
+ // row on the next page. Alias is `balance_str` (not `balance`)
322
+ // because TIDX rejects expression aliases that shadow the source
323
+ // column (422).
324
+ query: `
325
+ SELECT token, toString(balance) AS balance_str
326
+ FROM address_balances
327
+ WHERE holder = '${address}' AND startsWith(token, '${tip20AddressPrefix}') AND balance > 0${tokenIn}${keyset}
328
+ ORDER BY balance DESC, token ASC
329
+ LIMIT ${limit + 1}${offset !== undefined ? ` OFFSET ${offset}` : ''}
330
+ ` as string,
331
+ })
332
+
333
+ // The next page anchors below the last fetched row's `(balance, token)`.
334
+ const page = Cursor.paginate({
335
+ rows: result.rows,
336
+ limit,
337
+ key: (row) => {
338
+ const balance = Value.toIntegerString(row['balance_str'])
339
+ const token = Schema.Address.safeParse(row['token'])
340
+ return balance !== undefined && token.success ? [balance, token.data] : undefined
341
+ },
342
+ })
343
+
344
+ const data: { address: Hex.Hex; amount: string }[] = []
345
+ for (const row of page.rows) {
346
+ const token = Schema.Address.safeParse(row['token'])
347
+ const amount = Value.toIntegerString(row['balance_str'])
348
+ if (token.success && amount !== undefined) data.push({ address: token.data, amount })
349
+ }
350
+
351
+ return { data, nextCursor: page.nextCursor }
352
+ })
353
+
354
+ if (holdings.data.length === 0) return { data: [], nextCursor: holdings.nextCursor }
355
+
356
+ // Resolve token metadata for each balance. The endpoint's contract is a
357
+ // balance with its `token` reference inlined per row (and `formatted` needs
358
+ // the token `decimals`), so a row whose metadata cannot be resolved is
359
+ // dropped rather than returned with placeholders. Because the TIDX filter
360
+ // already restricts to TIP-20 (`0x20c0…`) tokens, resolution should succeed
361
+ // for nearly every row in practice.
362
+ //
363
+ // The verified `snapshot` fetched above is reused across the page instead of
364
+ // re-resolved per row. `createdAt` is not part of this endpoint's response,
365
+ // so it is never fetched here.
366
+ const resolved = await Timing.time(c, 'tokens', () =>
367
+ Promise.all(
368
+ holdings.data.map((holding) =>
369
+ Tokens.resolveToken(c, {
370
+ address: holding.address,
371
+ chainId,
372
+ snapshot,
373
+ })
374
+ .then((metadata) => ({ amount: holding.amount, metadata }) as const)
375
+ .catch(() => ({ amount: holding.amount, metadata: undefined }) as const),
376
+ ),
377
+ ),
378
+ )
379
+
380
+ const data: z.output<typeof schema.getAddressBalances.Balance>[] = []
381
+ for (const entry of resolved) {
382
+ if (!entry.metadata) continue
383
+ data.push({
384
+ amount: entry.amount,
385
+ formatted: core_Value.format(BigInt(entry.amount), entry.metadata.decimals),
386
+ id: entry.metadata.address,
387
+ // Best-effort: omit `feeEligible` entirely when the fee-token set was
388
+ // unavailable, rather than reporting a misleading `false`.
389
+ ...(feeTokens ? { feeEligible: feeTokens.has(entry.metadata.address.toLowerCase()) } : {}),
390
+ token: {
391
+ address: entry.metadata.address,
392
+ currency: entry.metadata.currency,
393
+ decimals: entry.metadata.decimals,
394
+ id: entry.metadata.id,
395
+ logoUri: entry.metadata.logoUri,
396
+ name: entry.metadata.name,
397
+ symbol: entry.metadata.symbol,
398
+ verified: entry.metadata.verified,
399
+ },
400
+ })
401
+ }
402
+
403
+ return { data, nextCursor: holdings.nextCursor }
404
+ }
405
+
406
+ declare namespace getAddressBalances {
407
+ type Options = {
408
+ address: z.output<typeof Schema.Address>
409
+ chainId: z.output<typeof Schema.ChainId>
410
+ /** Only include verified tokens denominated in this currency (implies `verified`). */
411
+ currency?: string | undefined
412
+ /** Opaque keyset cursor anchoring the page; omit for the head page. */
413
+ cursor?: string | undefined
414
+ /** When true, restrict the page to fee-usable tokens (members of `feeTokens`). */
415
+ feeEligible?: boolean | undefined
416
+ /**
417
+ * The chain's fee-token set, if resolved (best-effort): flags each row's
418
+ * `feeEligible` and backs the `feeEligible` filter. Undefined when the lookup
419
+ * failed, in which case `feeEligible` is omitted from rows.
420
+ */
421
+ feeTokens?: ReadonlySet<string> | undefined
422
+ limit: number
423
+ /** 1-indexed page number (positional pagination; exclusive with `cursor`). */
424
+ page?: number | undefined
425
+ /** When true, restrict the page to addresses in the curated verified list. */
426
+ verified?: boolean | undefined
427
+ }
428
+ }
429
+
430
+ /**
431
+ * Computes the optional set of allowed token addresses for the page's
432
+ * `token IN (…)` restriction, combining the verified/`currency` filter with the
433
+ * `feeEligible` filter. Each active filter contributes a candidate address list;
434
+ * when both are active the result is their intersection. Returns `undefined`
435
+ * when no filter is active (no restriction), or an empty array when an active
436
+ * filter excludes every token (caller short-circuits to an empty page).
437
+ * Addresses are lowercased to match the indexer's stored casing.
438
+ */
439
+ function allowedTokens(options: allowedTokens.Options): string[] | undefined {
440
+ const { currency, feeEligible, feeTokens, snapshot, verified } = options
441
+ let allowed: string[] | undefined
442
+ if (verified) {
443
+ const matching = currency
444
+ ? (snapshot.byCurrency.get(currency.toLowerCase()) ?? [])
445
+ : snapshot.list
446
+ allowed = matching.map((token) => token.address.toLowerCase())
447
+ }
448
+ // The handler guarantees `feeTokens` is present whenever `feeEligible` filtering
449
+ // is requested (a lookup failure surfaces as a `502` instead); the `?? []`
450
+ // is a defensive fall-through that yields an empty page rather than silently
451
+ // dropping the filter.
452
+ if (feeEligible) {
453
+ if (!feeTokens) allowed = []
454
+ else if (allowed) allowed = allowed.filter((address) => feeTokens.has(address))
455
+ else allowed = Array.from(feeTokens)
456
+ }
457
+ return allowed
458
+ }
459
+
460
+ declare namespace allowedTokens {
461
+ type Options = {
462
+ /** Only include verified tokens denominated in this currency (implies `verified`). */
463
+ currency?: string | undefined
464
+ /** When true, restrict to fee-usable tokens (members of `feeTokens`). */
465
+ feeEligible?: boolean | undefined
466
+ /** The chain's fee-token set, if resolved (best-effort). */
467
+ feeTokens?: ReadonlySet<string> | undefined
468
+ /** Compiled verified-token snapshot for the chain. */
469
+ snapshot: VerifiedTokens.Snapshot
470
+ /** When true, restrict to addresses in the curated verified list. */
471
+ verified?: boolean | undefined
472
+ }
473
+ }
474
+
475
+ /**
476
+ * Exact count of an address's matching TIP-20 holdings, sharing
477
+ * {@link getAddressBalances}'s `holder`/`verified`/`currency` filters (minus
478
+ * pagination) so the count matches the page it annotates. The `holder` equality
479
+ * prunes the `token_balances_snapshot` view by its sort key, so ClickHouse
480
+ * counts via the sparse primary index without a row scan — the count is exact
481
+ * and cheap with no cap (`totalCountCapped` is always `false`), verified at
482
+ * ~370k holdings in <1s. Feeds the opt-in `meta.totalCount`.
483
+ */
484
+ async function countAddressBalances(
485
+ c: Context<App.Environment>,
486
+ options: countAddressBalances.Options,
487
+ ): Promise<{ totalCountCapped: boolean; totalCount: number }> {
488
+ const { address, chainId, currency, feeEligible, feeTokens, verified } = options
489
+ const tidx = c.get('getTidx')(chainId)
490
+
491
+ // Same verified/currency/feeEligible `token IN (…)` restriction as the page; an
492
+ // empty filter means nothing matches, so the count is zero without a round
493
+ // trip.
494
+ const snapshot = await VerifiedTokens.snapshot(c, chainId)
495
+ const allowed = allowedTokens({ currency, feeEligible, feeTokens, snapshot, verified })
496
+ const tokenFilter = allowed?.map((address) => `'${address}'`)
497
+ if (tokenFilter?.length === 0) return { totalCountCapped: false, totalCount: 0 }
498
+ const tokenIn = tokenFilter ? ` AND token IN (${tokenFilter.join(', ')})` : ''
499
+
500
+ return Timing.time(c, 'address_balances_count', async () => {
501
+ // Exact count over the holder-pruned `address_balances` rows (mirrors the
502
+ // page's WHERE, minus the keyset cursor). The `holder` sort-key prune lets
503
+ // ClickHouse count via the sparse index, so no cap is needed. The inline
504
+ // query is cast to `string` so TIDX treats it as a dynamic ClickHouse
505
+ // query needing no signature.
506
+ const result = await tidx.fetch({
507
+ chainId,
508
+ engine: 'clickhouse',
509
+ query: `
510
+ SELECT count(*) AS total
511
+ FROM address_balances
512
+ WHERE holder = '${address}' AND startsWith(token, '${tip20AddressPrefix}') AND balance > 0${tokenIn}
513
+ ` as string,
514
+ })
515
+ const totalCount = Value.toNumber(result.rows[0]?.['total']) ?? 0
516
+ return { totalCountCapped: false, totalCount }
517
+ })
518
+ }
519
+
520
+ declare namespace countAddressBalances {
521
+ type Options = {
522
+ address: z.output<typeof Schema.Address>
523
+ chainId: z.output<typeof Schema.ChainId>
524
+ /** Only include verified tokens denominated in this currency (implies `verified`). */
525
+ currency?: string | undefined
526
+ /** When true, restrict the count to fee-usable tokens (members of `feeTokens`). */
527
+ feeEligible?: boolean | undefined
528
+ /** The chain's fee-token set (best-effort), backing the `feeEligible` restriction. */
529
+ feeTokens?: ReadonlySet<string> | undefined
530
+ /** When true, restrict the count to addresses in the curated verified list. */
531
+ verified?: boolean | undefined
532
+ }
533
+ }