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,645 @@
1
+ import type { Context } from 'hono'
2
+ import { Hex } from 'ox'
3
+ import * as z from 'zod/mini'
4
+
5
+ import type * as App from '../App.js'
6
+ import * as Schema from './Schema.js'
7
+ import * as Store from './Store.js'
8
+ import * as Timing from './Timing.js'
9
+
10
+ /** Chain id covered by the curated verified-token lists. */
11
+ export type ChainId = z.output<typeof Schema.ChainId>
12
+
13
+ /** Schema for a chain's verified token list (the value stored in KV). */
14
+ export const Data = z
15
+ .array(
16
+ z
17
+ .object({
18
+ address: Schema.Address.check(z.describe('Verified TIP-20 token contract address.')),
19
+ currency: z.string().check(z.describe('Verified token display currency.')),
20
+ decimals: z
21
+ .number()
22
+ .check(z.int(), z.nonnegative(), z.describe('Verified token decimal precision.')),
23
+ id: z.string().check(z.describe('Stable resource id (the token address).')),
24
+ logoUri: z
25
+ .optional(z.url({ protocol: /^https$/ }))
26
+ .check(z.describe('Curated HTTPS URL for the token logo image, when set.')),
27
+ name: z.string().check(z.describe('Verified token display name.')),
28
+ symbol: z.string().check(z.describe('Verified token ticker symbol.')),
29
+ })
30
+ .check(z.describe('Static verified token entry.')),
31
+ )
32
+ .check(z.describe("A chain's verified token list."))
33
+
34
+ type SnapshotToken = z.output<typeof Data>[number]
35
+
36
+ /** A single curated verified TIP-20 token accepted by write/compile APIs. */
37
+ export type Token = Omit<SnapshotToken, 'id'> & { id?: string | undefined }
38
+
39
+ /** Verified-token input accepted before the stable resource id is derived. */
40
+ export type TokenInput = Token
41
+
42
+ /**
43
+ * A compiled, indexed snapshot of one chain's curated verified-token list.
44
+ * Fetched once per request and threaded into per-row enrichment so lookups are
45
+ * plain in-memory map reads rather than repeated array scans.
46
+ */
47
+ export type Snapshot = {
48
+ /** Tokens indexed by lowercased contract address (membership + lookup). */
49
+ byAddress: ReadonlyMap<string, SnapshotToken>
50
+ /** Tokens grouped by lowercased display currency, preserving list order. */
51
+ byCurrency: ReadonlyMap<string, readonly SnapshotToken[]>
52
+ /** Tokens indexed by lowercased ticker symbol. */
53
+ bySymbol: ReadonlyMap<string, SnapshotToken>
54
+ /** Chain the snapshot belongs to. */
55
+ chainId: number
56
+ /** Sorted, distinct display currencies present in the list. */
57
+ currencies: readonly string[]
58
+ /** The curated tokens in their canonical order. */
59
+ list: readonly SnapshotToken[]
60
+ /** ISO timestamp of the snapshot's last update. */
61
+ updatedAt: string
62
+ /** Opaque snapshot version; changes whenever the list changes. */
63
+ version: string
64
+ }
65
+
66
+ /** Compiles a raw token list into an indexed {@link Snapshot}. */
67
+ export function compile(options: compile.Options): Snapshot {
68
+ const { chainId, tokens, updatedAt, version } = options
69
+ // Backfill the derived `id` (lowercased address) without re-parsing through
70
+ // `Data`/`Schema.Address`, which would lowercase the stored `address` and
71
+ // drop its curated checksum casing. Callers (readBlob/normalize) own validation.
72
+ const list: SnapshotToken[] = tokens.map((token) => ({
73
+ ...token,
74
+ id: token.id ?? (token.address.toLowerCase() as Hex.Hex),
75
+ }))
76
+
77
+ const byAddress = new Map<string, SnapshotToken>()
78
+ const byCurrency = new Map<string, SnapshotToken[]>()
79
+ const bySymbol = new Map<string, SnapshotToken>()
80
+ for (const token of list) {
81
+ byAddress.set(token.address.toLowerCase(), token)
82
+ bySymbol.set(token.symbol.toLowerCase(), token)
83
+ const currency = token.currency.toLowerCase()
84
+ const group = byCurrency.get(currency)
85
+ if (group) group.push(token)
86
+ else byCurrency.set(currency, [token])
87
+ }
88
+
89
+ return {
90
+ byAddress,
91
+ byCurrency,
92
+ bySymbol,
93
+ chainId,
94
+ currencies: Array.from(new Set(list.map((token) => token.currency))).sort(),
95
+ list,
96
+ updatedAt,
97
+ version,
98
+ }
99
+ }
100
+
101
+ export declare namespace compile {
102
+ /** Options for {@link compile}. */
103
+ type Options = {
104
+ /** Chain the tokens belong to. */
105
+ chainId: number
106
+ /** Curated tokens in canonical order. */
107
+ tokens: readonly TokenInput[]
108
+ /** ISO timestamp of the snapshot's last update. */
109
+ updatedAt: string
110
+ /** Opaque snapshot version. */
111
+ version: string
112
+ }
113
+ }
114
+
115
+ // Storage layout: one head pointer + one whole-chain blob per version, so
116
+ // reads are a single `get` by known key (no `list()` scans) and a publish is an
117
+ // atomic flip of `head` to a new, already-written `snapshot:{version}`.
118
+ const keyPrefix = 'verified-token:v1'
119
+
120
+ // Storage schema version stamped on every head/blob, so a future on-disk format
121
+ // change can be detected and migrated.
122
+ const storeSchema = 1
123
+
124
+ /** Store key for a chain's head pointer (current version + metadata). */
125
+ function headKey(chainId: ChainId): string {
126
+ return `${keyPrefix}:${chainId}:head`
127
+ }
128
+
129
+ /** Store key for a chain's immutable snapshot blob at a given version. */
130
+ function snapshotKey(chainId: ChainId, version: string): string {
131
+ return `${keyPrefix}:${chainId}:snapshot:${version}`
132
+ }
133
+
134
+ // Head pointer: tiny, read once per soft-TTL window to detect version changes
135
+ // without fetching the (much larger) token blob.
136
+ const Head = z
137
+ .object({
138
+ chainId: z.number().check(z.int(), z.describe('Chain the head belongs to.')),
139
+ count: z.number().check(z.int(), z.nonnegative(), z.describe('Token count at this version.')),
140
+ schema: z.number().check(z.int(), z.describe('Storage schema version.')),
141
+ updatedAt: z.string().check(z.describe('ISO timestamp of the last publish.')),
142
+ version: z.string().check(z.describe('Current snapshot version.')),
143
+ })
144
+ .check(z.describe('Verified-token head pointer stored under the head key.'))
145
+
146
+ // Immutable per-version blob holding the validated token list.
147
+ const Blob = z
148
+ .object({
149
+ chainId: z.number().check(z.int(), z.describe('Chain the blob belongs to.')),
150
+ schema: z.number().check(z.int(), z.describe('Storage schema version.')),
151
+ tokens: Data,
152
+ updatedAt: z.string().check(z.describe('ISO timestamp of the last publish.')),
153
+ version: z.string().check(z.describe('Snapshot version this blob holds.')),
154
+ })
155
+ .check(z.describe('Verified-token snapshot blob stored under a versioned key.'))
156
+
157
+ /** Parsed head pointer for a chain. */
158
+ export type Head = z.output<typeof Head>
159
+
160
+ /** Reads a chain's head pointer, or `null` when the chain has no head yet. */
161
+ async function readHead(store: Store.State, chainId: ChainId): Promise<Head | null> {
162
+ const raw = await store.get(headKey(chainId))
163
+ if (!raw) return null
164
+ return Head.parse(JSON.parse(raw))
165
+ }
166
+
167
+ /**
168
+ * Reads and compiles a chain's snapshot blob at a specific version. Throws
169
+ * {@link TornStateError} when the blob is missing (a torn write that should
170
+ * never happen given the write-blob-before-flip-head ordering).
171
+ */
172
+ async function readBlob(store: Store.State, chainId: ChainId, version: string): Promise<Snapshot> {
173
+ const raw = await store.get(snapshotKey(chainId, version))
174
+ if (!raw) throw new TornStateError({ chainId, version })
175
+ const parsed = JSON.parse(raw)
176
+ const blob = Blob.parse({
177
+ ...parsed,
178
+ tokens: Array.isArray(parsed.tokens) ? withIds(parsed.tokens) : parsed.tokens,
179
+ })
180
+ return compile({
181
+ chainId,
182
+ tokens: blob.tokens,
183
+ updatedAt: blob.updatedAt,
184
+ version: blob.version,
185
+ })
186
+ }
187
+
188
+ /**
189
+ * Reads and compiles the snapshot a chain's head points at. Returns `null` when
190
+ * the chain has no head. Throws {@link TornStateError} when `head` references a
191
+ * missing snapshot blob.
192
+ */
193
+ export async function read(store: Store.State, chainId: ChainId): Promise<Snapshot | null> {
194
+ const head = await readHead(store, chainId)
195
+ if (!head) return null
196
+ return readBlob(store, chainId, head.version)
197
+ }
198
+
199
+ // Tracks the last epoch-ms used by `nextVersion` so versions stay strictly
200
+ // monotonic even when multiple writes land in the same millisecond.
201
+ let lastVersionMs = 0
202
+
203
+ // Generates a monotonic, collision-resistant snapshot version. Zero-padded
204
+ // epoch-ms sorts chronologically by string compare (matching webhook ids) and
205
+ // is bumped forward when two writes share a millisecond so the prefix alone
206
+ // strictly increases per process; the random suffix keeps versions opaque.
207
+ function nextVersion(): string {
208
+ lastVersionMs = Math.max(Date.now(), lastVersionMs + 1)
209
+ return `${lastVersionMs.toString().padStart(15, '0')}_${Hex.random(4).slice(2)}`
210
+ }
211
+
212
+ /**
213
+ * Validates a whole token list and normalizes it for storage: addresses are
214
+ * lowercased, and address + case-insensitive symbol must be unique across the
215
+ * entire list. Throws {@link DuplicateAddressError} /
216
+ * {@link DuplicateSymbolError} on a collision.
217
+ */
218
+ function normalize(chainId: ChainId, tokens: readonly TokenInput[]): Token[] {
219
+ const list = Data.parse(withIds(tokens)).map((token) => ({
220
+ ...token,
221
+ address: token.address.toLowerCase() as Hex.Hex,
222
+ id: token.address.toLowerCase() as Hex.Hex,
223
+ }))
224
+ const addresses = new Set<string>()
225
+ const symbols = new Set<string>()
226
+ for (const token of list) {
227
+ if (addresses.has(token.address)) throw new DuplicateAddressError({ address: token.address, chainId }) // prettier-ignore
228
+ addresses.add(token.address)
229
+ const symbol = token.symbol.toLowerCase()
230
+ if (symbols.has(symbol)) throw new DuplicateSymbolError({ chainId, symbol: token.symbol })
231
+ symbols.add(symbol)
232
+ }
233
+ return list
234
+ }
235
+
236
+ // Backfills the derived `id` (lowercased address) onto raw token records
237
+ // without touching the stored `address` casing, so compiled snapshots preserve
238
+ // the curated checksum form while still exposing a stable lowercase id.
239
+ function withIds(tokens: readonly unknown[]): unknown[] {
240
+ return tokens.map((token) => {
241
+ if (!token || typeof token !== 'object' || !('address' in token)) return token
242
+ return { ...(token as Record<string, unknown>), id: String(token.address).toLowerCase() }
243
+ })
244
+ }
245
+
246
+ // Returns the current authoritative list for a chain to read-modify-write
247
+ // against: the durable snapshot, or an empty list when the chain has no head yet
248
+ // (the first write starts the chain's list from scratch).
249
+ async function currentList(
250
+ store: Store.State,
251
+ chainId: ChainId,
252
+ ): Promise<readonly SnapshotToken[]> {
253
+ return (await read(store, chainId))?.list ?? []
254
+ }
255
+
256
+ // Best-effort optimistic concurrency: when `ifMatch` is set, the caller's
257
+ // expected version must equal the current head version. On KV the head
258
+ // read can itself be stale, so this narrows — but does not eliminate — the
259
+ // lost-update window under concurrent writers.
260
+ async function assertVersion(
261
+ store: Store.State,
262
+ chainId: ChainId,
263
+ ifMatch: string | undefined,
264
+ ): Promise<void> {
265
+ if (ifMatch === undefined) return
266
+ const head = await readHead(store, chainId)
267
+ const actual = head?.version ?? emptyVersion
268
+ if (actual !== ifMatch) throw new VersionMismatchError({ actual, chainId, expected: ifMatch })
269
+ }
270
+
271
+ // Write-modify-flip-prime: validate the whole list, write the immutable
272
+ // blob, flip `head` to it, then prime this isolate for read-your-write. Writing
273
+ // the blob before flipping head means a crash in between leaves the old head
274
+ // valid (no torn state).
275
+ async function publish(
276
+ store: Store.State,
277
+ chainId: ChainId,
278
+ tokens: readonly TokenInput[],
279
+ ): Promise<Snapshot> {
280
+ const list = normalize(chainId, tokens)
281
+ const version = nextVersion()
282
+ const updatedAt = new Date().toISOString()
283
+ await store.put(
284
+ snapshotKey(chainId, version),
285
+ JSON.stringify({ chainId, schema: storeSchema, tokens: list, updatedAt, version }),
286
+ )
287
+ await store.put(
288
+ headKey(chainId),
289
+ JSON.stringify({ chainId, count: list.length, schema: storeSchema, updatedAt, version }),
290
+ )
291
+ const snapshot = compile({ chainId, tokens: list, updatedAt, version })
292
+ prime(chainId, snapshot)
293
+ return snapshot
294
+ }
295
+
296
+ /**
297
+ * Appends a verified token to a chain's list and publishes a new snapshot.
298
+ * Throws on a duplicate address or (case-insensitive) symbol.
299
+ */
300
+ export async function create(store: Store.State, input: create.Input): Promise<create.Result> {
301
+ const { chainId, currency, decimals, logoUri, name, symbol } = input
302
+ const address = input.address.toLowerCase() as Hex.Hex
303
+ const list = await currentList(store, chainId)
304
+ const token: Token = {
305
+ address,
306
+ currency,
307
+ decimals,
308
+ id: address,
309
+ name,
310
+ symbol,
311
+ ...(logoUri === undefined ? {} : { logoUri }),
312
+ }
313
+ const snapshot = await publish(store, chainId, [...list, token])
314
+ return { snapshot, token: snapshot.byAddress.get(address)! }
315
+ }
316
+
317
+ export declare namespace create {
318
+ /** Input for {@link create}. */
319
+ type Input = {
320
+ /** TIP-20 token contract address (normalized to lowercase). */
321
+ address: string
322
+ /** Chain the token belongs to. */
323
+ chainId: ChainId
324
+ /** Display currency, e.g. `USD`. */
325
+ currency: string
326
+ /** Decimal precision. */
327
+ decimals: number
328
+ /** Curated HTTPS logo URL. Omit when no curated logo is set. */
329
+ logoUri?: string | undefined
330
+ /** Display name. */
331
+ name: string
332
+ /** Ticker symbol. */
333
+ symbol: string
334
+ }
335
+ /** Result of {@link create}. */
336
+ type Result = {
337
+ /** The published snapshot reflecting the new token. */
338
+ snapshot: Snapshot
339
+ /** The created token. */
340
+ token: Token
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Partially updates a verified token in place (preserving list order) and
346
+ * publishes a new snapshot. Throws {@link NotFoundError} when the address is
347
+ * absent, and the uniqueness errors when an edit collides with another entry.
348
+ */
349
+ export async function patch(
350
+ store: Store.State,
351
+ chainId: ChainId,
352
+ address: string,
353
+ input: patch.Input,
354
+ options: patch.Options = {},
355
+ ): Promise<create.Result> {
356
+ await assertVersion(store, chainId, options.ifMatch)
357
+ const target = address.toLowerCase() as Hex.Hex
358
+ const list = await currentList(store, chainId)
359
+ const index = list.findIndex((token) => token.address.toLowerCase() === target)
360
+ if (index === -1) throw new NotFoundError({ address: target, chainId })
361
+ // Edit in place, overriding only the fields the patch supplies.
362
+ const next = list.map((token, i) => {
363
+ if (i !== index) return token
364
+ const logoUri = input.logoUri ?? token.logoUri
365
+ return {
366
+ address: target,
367
+ currency: input.currency ?? token.currency,
368
+ decimals: input.decimals ?? token.decimals,
369
+ id: target,
370
+ name: input.name ?? token.name,
371
+ symbol: input.symbol ?? token.symbol,
372
+ ...(logoUri === undefined ? {} : { logoUri }),
373
+ }
374
+ })
375
+ const snapshot = await publish(store, chainId, next)
376
+ return { snapshot, token: snapshot.byAddress.get(target)! }
377
+ }
378
+
379
+ export declare namespace patch {
380
+ /** Patchable fields for {@link patch} (address and chain are immutable). */
381
+ type Input = {
382
+ /** Display currency, e.g. `USD`. */
383
+ currency?: string | undefined
384
+ /** Decimal precision. */
385
+ decimals?: number | undefined
386
+ /** Curated HTTPS logo URL. Omitting keeps the current value (use `replace` to clear). */
387
+ logoUri?: string | undefined
388
+ /** Display name. */
389
+ name?: string | undefined
390
+ /** Ticker symbol. */
391
+ symbol?: string | undefined
392
+ }
393
+ /** Options for {@link patch}. */
394
+ type Options = {
395
+ /** Expected current version for optimistic concurrency (best-effort). */
396
+ ifMatch?: string | undefined
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Removes a verified token from a chain's list and publishes a new snapshot.
402
+ * Throws {@link NotFoundError} when the address is absent.
403
+ */
404
+ export async function remove(
405
+ store: Store.State,
406
+ chainId: ChainId,
407
+ address: string,
408
+ options: patch.Options = {},
409
+ ): Promise<remove.Result> {
410
+ await assertVersion(store, chainId, options.ifMatch)
411
+ const target = address.toLowerCase()
412
+ const list = await currentList(store, chainId)
413
+ const next = list.filter((token) => token.address.toLowerCase() !== target)
414
+ if (next.length === list.length) throw new NotFoundError({ address: target, chainId })
415
+ return { snapshot: await publish(store, chainId, next) }
416
+ }
417
+
418
+ export declare namespace remove {
419
+ /** Result of {@link remove}. */
420
+ type Result = {
421
+ /** The published snapshot reflecting the removal. */
422
+ snapshot: Snapshot
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Replaces a chain's entire verified list and publishes a new snapshot. Used to
428
+ * bulk-load the initial list and to roll back to a previous one.
429
+ */
430
+ export async function replace(
431
+ store: Store.State,
432
+ chainId: ChainId,
433
+ tokens: readonly TokenInput[],
434
+ options: patch.Options = {},
435
+ ): Promise<remove.Result> {
436
+ await assertVersion(store, chainId, options.ifMatch)
437
+ return { snapshot: await publish(store, chainId, tokens) }
438
+ }
439
+
440
+ // Sentinel version/time for the empty snapshot served before a chain has any
441
+ // durable head (KV is the single source of truth; nothing is verified until it
442
+ // is written through the API). A real publish replaces these with a monotonic
443
+ // version + actual `updatedAt`.
444
+ const emptyVersion = 'empty'
445
+ const emptyUpdatedAt = new Date(0).toISOString()
446
+
447
+ const emptySnapshots = new Map<ChainId, Snapshot>()
448
+
449
+ /** Returns the memoized empty {@link Snapshot} for a chain (no verified tokens). */
450
+ function emptySnapshot(chainId: ChainId): Snapshot {
451
+ const existing = emptySnapshots.get(chainId)
452
+ if (existing) return existing
453
+ const snapshot = compile({
454
+ chainId,
455
+ tokens: [],
456
+ updatedAt: emptyUpdatedAt,
457
+ version: emptyVersion,
458
+ })
459
+ emptySnapshots.set(chainId, snapshot)
460
+ return snapshot
461
+ }
462
+
463
+ // Default per-isolate soft-refresh window: lookups within this window of the
464
+ // last head check do zero store I/O.
465
+ const defaultRefreshMs = 10_000
466
+
467
+ // Per-isolate compiled snapshot + the wall-clock time it was last verified
468
+ // against the store's head.
469
+ type Cell = {
470
+ /** When the snapshot was last checked against the store head. */
471
+ checkedAt: number
472
+ /** The compiled snapshot served to readers. */
473
+ snapshot: Snapshot
474
+ }
475
+ const cells = new Map<ChainId, Cell>()
476
+
477
+ // Coalesce concurrent refreshes per chain within one isolate so a soft-stale
478
+ // burst triggers a single head check, not one per in-flight request.
479
+ const refreshes = new Map<ChainId, Promise<Snapshot>>()
480
+
481
+ /**
482
+ * Primes this isolate's in-memory snapshot for a chain. The writer isolate calls
483
+ * this after a successful publish so the next read reflects the write without
484
+ * waiting on store propagation (read-your-write).
485
+ */
486
+ export function prime(chainId: ChainId, snapshot: Snapshot): void {
487
+ cells.set(chainId, { checkedAt: Date.now(), snapshot })
488
+ }
489
+
490
+ // Single-flight wrapper around `refreshCell`: concurrent stale/cold reads for a
491
+ // chain share one in-flight refresh.
492
+ function refresh(
493
+ c: Context<App.Environment>,
494
+ store: Store.State,
495
+ chainId: ChainId,
496
+ cell: Cell | undefined,
497
+ ): Promise<Snapshot> {
498
+ const existing = refreshes.get(chainId)
499
+ if (existing) return existing
500
+ const flight = (async () => {
501
+ try {
502
+ return await refreshCell(c, store, chainId, cell)
503
+ } finally {
504
+ refreshes.delete(chainId)
505
+ }
506
+ })()
507
+ refreshes.set(chainId, flight)
508
+ return flight
509
+ }
510
+
511
+ // Refresh state machine. Soft-stale reads only the head and recompiles
512
+ // solely on a version change; cold loads the full snapshot (an empty snapshot
513
+ // when the chain has no head yet).
514
+ async function refreshCell(
515
+ c: Context<App.Environment>,
516
+ store: Store.State,
517
+ chainId: ChainId,
518
+ cell: Cell | undefined,
519
+ ): Promise<Snapshot> {
520
+ // Cold: no prior snapshot. Load from store, or an empty snapshot when the
521
+ // chain has no head. A hard failure here propagates (we have nothing good to
522
+ // serve, and must not resurrect deleted tokens).
523
+ if (!cell) {
524
+ const loaded = await Timing.time(c, 'verified_snapshot', () => read(store, chainId))
525
+ const snapshot = loaded ?? emptySnapshot(chainId)
526
+ cells.set(chainId, { checkedAt: Date.now(), snapshot })
527
+ return snapshot
528
+ }
529
+
530
+ // Soft-stale: serve the last good in-memory snapshot and log loudly on
531
+ // failure rather than dropping to empty (no resurrection of deleted tokens).
532
+ try {
533
+ const head = await Timing.time(c, 'verified_head', () => readHead(store, chainId))
534
+ // Unchanged version (or head not yet present): keep snapshot, bump checkedAt.
535
+ if (!head || head.version === cell.snapshot.version) {
536
+ cells.set(chainId, { checkedAt: Date.now(), snapshot: cell.snapshot })
537
+ return cell.snapshot
538
+ }
539
+ // Version changed: load the new blob directly (head already in hand).
540
+ const snapshot = await Timing.time(c, 'verified_snapshot', () =>
541
+ readBlob(store, chainId, head.version),
542
+ )
543
+ cells.set(chainId, { checkedAt: Date.now(), snapshot })
544
+ return snapshot
545
+ } catch (error) {
546
+ console.error('[verified-tokens] refresh failed; serving last good snapshot', error)
547
+ cells.set(chainId, { checkedAt: Date.now(), snapshot: cell.snapshot })
548
+ return cell.snapshot
549
+ }
550
+ }
551
+
552
+ /**
553
+ * Returns the compiled verified-token {@link Snapshot} for a chain.
554
+ *
555
+ * When the verified-tokens feature is unconfigured, serves an empty snapshot.
556
+ * When configured with a {@link Store.State}, serves a per-isolate snapshot
557
+ * refreshed on a soft TTL: fresh lookups do zero store I/O; soft-stale
558
+ * lookups return the current snapshot immediately and refresh in the background
559
+ * (single-flight, head-only unless the version changed); a cold isolate loads
560
+ * from the store (an empty snapshot when no head exists).
561
+ */
562
+ export async function snapshot(c: Context<App.Environment>, chainId: ChainId): Promise<Snapshot> {
563
+ return Timing.time(c, 'verified_tokens', () => {
564
+ const config = c.get('verifiedTokens')
565
+ // Unconfigured: nothing is verified. No store, no cell.
566
+ if (!config) return emptySnapshot(chainId)
567
+
568
+ const refreshMs = config.refreshMs ?? defaultRefreshMs
569
+ const cell = cells.get(chainId)
570
+
571
+ // Fresh: serve the in-memory snapshot with zero store I/O.
572
+ if (cell && Date.now() - cell.checkedAt < refreshMs) return cell.snapshot
573
+
574
+ // Soft-stale: serve immediately, refresh in the background. Never blocks.
575
+ if (cell) {
576
+ void refresh(c, config.store, chainId, cell)
577
+ return cell.snapshot
578
+ }
579
+
580
+ // Cold: must load before the first response.
581
+ return refresh(c, config.store, chainId, undefined)
582
+ })
583
+ }
584
+
585
+ /**
586
+ * Well-known display currencies surfaced as an OpenAPI `examples` hint on
587
+ * filters that accept any currency string but want to populate the well-known
588
+ * ones in Swagger/Stoplight/Scalar dropdowns. Static hint only — the
589
+ * authoritative set is `Snapshot.currencies` from the live store.
590
+ */
591
+ export const currencies = ['EUR', 'USD']
592
+
593
+ /**
594
+ * Thrown when a chain's `head` points at a snapshot version whose blob is
595
+ * missing from the store. The write path always writes the blob before flipping
596
+ * the head, so this indicates a torn write or external tampering.
597
+ */
598
+ export class TornStateError extends Error {
599
+ override name = 'VerifiedTokens.TornStateError'
600
+ constructor(options: { chainId: number; version: string }) {
601
+ super(
602
+ `verified-token head for chain ${options.chainId} points at missing snapshot version "${options.version}".`,
603
+ )
604
+ }
605
+ }
606
+
607
+ /** Thrown when a write would introduce a duplicate address on a chain. */
608
+ export class DuplicateAddressError extends Error {
609
+ override name = 'VerifiedTokens.DuplicateAddressError'
610
+ constructor(options: { address: string; chainId: number }) {
611
+ super(`verified token ${options.address} already exists on chain ${options.chainId}.`)
612
+ }
613
+ }
614
+
615
+ /**
616
+ * Thrown when a write would introduce a duplicate (case-insensitive) symbol on a
617
+ * chain — which would make `GET /tokens/:symbol` ambiguous.
618
+ */
619
+ export class DuplicateSymbolError extends Error {
620
+ override name = 'VerifiedTokens.DuplicateSymbolError'
621
+ constructor(options: { chainId: number; symbol: string }) {
622
+ super(`verified symbol "${options.symbol}" already exists on chain ${options.chainId}.`)
623
+ }
624
+ }
625
+
626
+ /** Thrown when a patch/delete targets an address not in a chain's verified list. */
627
+ export class NotFoundError extends Error {
628
+ override name = 'VerifiedTokens.NotFoundError'
629
+ constructor(options: { address: string; chainId: number }) {
630
+ super(`verified token ${options.address} not found on chain ${options.chainId}.`)
631
+ }
632
+ }
633
+
634
+ /**
635
+ * Thrown when an `If-Match` precondition does not equal the current head version
636
+ * (best-effort optimistic concurrency).
637
+ */
638
+ export class VersionMismatchError extends Error {
639
+ override name = 'VerifiedTokens.VersionMismatchError'
640
+ constructor(options: { actual: string; chainId: number; expected: string }) {
641
+ super(
642
+ `verified-token version mismatch on chain ${options.chainId}: expected "${options.expected}", found "${options.actual}".`,
643
+ )
644
+ }
645
+ }