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.
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/dist/ApiKey.d.ts +65 -0
- package/dist/ApiKey.d.ts.map +1 -0
- package/dist/ApiKey.js +105 -0
- package/dist/ApiKey.js.map +1 -0
- package/dist/ApiKeySource.d.ts +166 -0
- package/dist/ApiKeySource.d.ts.map +1 -0
- package/dist/ApiKeySource.js +234 -0
- package/dist/ApiKeySource.js.map +1 -0
- package/dist/App.d.ts +19317 -0
- package/dist/App.d.ts.map +1 -0
- package/dist/App.js +612 -0
- package/dist/App.js.map +1 -0
- package/dist/Assets.d.ts +44 -0
- package/dist/Assets.d.ts.map +1 -0
- package/dist/Assets.js +23 -0
- package/dist/Assets.js.map +1 -0
- package/dist/Auth.d.ts +2 -0
- package/dist/Auth.d.ts.map +1 -0
- package/dist/Auth.js +2 -0
- package/dist/Auth.js.map +1 -0
- package/dist/Cli.d.ts +21 -0
- package/dist/Cli.d.ts.map +1 -0
- package/dist/Cli.js +13 -0
- package/dist/Cli.js.map +1 -0
- package/dist/Client.d.ts +20 -0
- package/dist/Client.d.ts.map +1 -0
- package/dist/Client.js +24 -0
- package/dist/Client.js.map +1 -0
- package/dist/Metrics.d.ts +70 -0
- package/dist/Metrics.d.ts.map +1 -0
- package/dist/Metrics.js +91 -0
- package/dist/Metrics.js.map +1 -0
- package/dist/Scope.d.ts +35 -0
- package/dist/Scope.d.ts.map +1 -0
- package/dist/Scope.js +38 -0
- package/dist/Scope.js.map +1 -0
- package/dist/Webhooks.d.ts +79 -0
- package/dist/Webhooks.d.ts.map +1 -0
- package/dist/Webhooks.js +107 -0
- package/dist/Webhooks.js.map +1 -0
- package/dist/admin/Access.d.ts +41 -0
- package/dist/admin/Access.d.ts.map +1 -0
- package/dist/admin/Access.js +75 -0
- package/dist/admin/Access.js.map +1 -0
- package/dist/admin/App.d.ts +1221 -0
- package/dist/admin/App.d.ts.map +1 -0
- package/dist/admin/App.js +113 -0
- package/dist/admin/App.js.map +1 -0
- package/dist/admin/apps/api-keys.d.ts +209 -0
- package/dist/admin/apps/api-keys.d.ts.map +1 -0
- package/dist/admin/apps/api-keys.js +129 -0
- package/dist/admin/apps/api-keys.js.map +1 -0
- package/dist/admin/apps/scopes.d.ts +46 -0
- package/dist/admin/apps/scopes.d.ts.map +1 -0
- package/dist/admin/apps/scopes.js +41 -0
- package/dist/admin/apps/scopes.js.map +1 -0
- package/dist/admin/apps/verified-tokens.d.ts +1206 -0
- package/dist/admin/apps/verified-tokens.d.ts.map +1 -0
- package/dist/admin/apps/verified-tokens.js +485 -0
- package/dist/admin/apps/verified-tokens.js.map +1 -0
- package/dist/admin/index.d.ts +3 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +3 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/ui.generated.d.ts +11 -0
- package/dist/admin/ui.generated.d.ts.map +1 -0
- package/dist/admin/ui.generated.js +43 -0
- package/dist/admin/ui.generated.js.map +1 -0
- package/dist/apps/activities.d.ts +18725 -0
- package/dist/apps/activities.d.ts.map +1 -0
- package/dist/apps/activities.js +3138 -0
- package/dist/apps/activities.js.map +1 -0
- package/dist/apps/addresses.d.ts +3662 -0
- package/dist/apps/addresses.d.ts.map +1 -0
- package/dist/apps/addresses.js +22 -0
- package/dist/apps/addresses.js.map +1 -0
- package/dist/apps/balances.d.ts +342 -0
- package/dist/apps/balances.d.ts.map +1 -0
- package/dist/apps/balances.js +393 -0
- package/dist/apps/balances.js.map +1 -0
- package/dist/apps/blocks.d.ts +931 -0
- package/dist/apps/blocks.d.ts.map +1 -0
- package/dist/apps/blocks.js +689 -0
- package/dist/apps/blocks.js.map +1 -0
- package/dist/apps/coingecko.d.ts +1053 -0
- package/dist/apps/coingecko.d.ts.map +1 -0
- package/dist/apps/coingecko.js +1015 -0
- package/dist/apps/coingecko.js.map +1 -0
- package/dist/apps/exchanges.d.ts +3000 -0
- package/dist/apps/exchanges.d.ts.map +1 -0
- package/dist/apps/exchanges.js +3570 -0
- package/dist/apps/exchanges.js.map +1 -0
- package/dist/apps/fee-amm.d.ts +640 -0
- package/dist/apps/fee-amm.d.ts.map +1 -0
- package/dist/apps/fee-amm.js +815 -0
- package/dist/apps/fee-amm.js.map +1 -0
- package/dist/apps/indexer.d.ts +116 -0
- package/dist/apps/indexer.d.ts.map +1 -0
- package/dist/apps/indexer.js +270 -0
- package/dist/apps/indexer.js.map +1 -0
- package/dist/apps/receipts.d.ts +1438 -0
- package/dist/apps/receipts.d.ts.map +1 -0
- package/dist/apps/receipts.js +874 -0
- package/dist/apps/receipts.js.map +1 -0
- package/dist/apps/rpc.d.ts +173 -0
- package/dist/apps/rpc.d.ts.map +1 -0
- package/dist/apps/rpc.js +191 -0
- package/dist/apps/rpc.js.map +1 -0
- package/dist/apps/tokenlist.d.ts +192 -0
- package/dist/apps/tokenlist.d.ts.map +1 -0
- package/dist/apps/tokenlist.js +153 -0
- package/dist/apps/tokenlist.js.map +1 -0
- package/dist/apps/tokens.d.ts +2246 -0
- package/dist/apps/tokens.d.ts.map +1 -0
- package/dist/apps/tokens.js +1789 -0
- package/dist/apps/tokens.js.map +1 -0
- package/dist/apps/transactions.d.ts +2327 -0
- package/dist/apps/transactions.d.ts.map +1 -0
- package/dist/apps/transactions.js +975 -0
- package/dist/apps/transactions.js.map +1 -0
- package/dist/apps/transfers.d.ts +589 -0
- package/dist/apps/transfers.d.ts.map +1 -0
- package/dist/apps/transfers.js +769 -0
- package/dist/apps/transfers.js.map +1 -0
- package/dist/apps/verified-tokens.d.ts +544 -0
- package/dist/apps/verified-tokens.d.ts.map +1 -0
- package/dist/apps/verified-tokens.js +234 -0
- package/dist/apps/verified-tokens.js.map +1 -0
- package/dist/apps/webhooks.d.ts +7684 -0
- package/dist/apps/webhooks.d.ts.map +1 -0
- package/dist/apps/webhooks.js +2112 -0
- package/dist/apps/webhooks.js.map +1 -0
- package/dist/cloudflare.d.ts +32 -0
- package/dist/cloudflare.d.ts.map +1 -0
- package/dist/cloudflare.js +45 -0
- package/dist/cloudflare.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/Auth.d.ts +298 -0
- package/dist/internal/Auth.d.ts.map +1 -0
- package/dist/internal/Auth.js +618 -0
- package/dist/internal/Auth.js.map +1 -0
- package/dist/internal/Cache.d.ts +74 -0
- package/dist/internal/Cache.d.ts.map +1 -0
- package/dist/internal/Cache.js +215 -0
- package/dist/internal/Cache.js.map +1 -0
- package/dist/internal/Cursor.d.ts +79 -0
- package/dist/internal/Cursor.d.ts.map +1 -0
- package/dist/internal/Cursor.js +116 -0
- package/dist/internal/Cursor.js.map +1 -0
- package/dist/internal/EdgeCache.d.ts +59 -0
- package/dist/internal/EdgeCache.d.ts.map +1 -0
- package/dist/internal/EdgeCache.js +174 -0
- package/dist/internal/EdgeCache.js.map +1 -0
- package/dist/internal/Log.d.ts +104 -0
- package/dist/internal/Log.d.ts.map +1 -0
- package/dist/internal/Log.js +118 -0
- package/dist/internal/Log.js.map +1 -0
- package/dist/internal/MetricSink.d.ts +65 -0
- package/dist/internal/MetricSink.d.ts.map +1 -0
- package/dist/internal/MetricSink.js +146 -0
- package/dist/internal/MetricSink.js.map +1 -0
- package/dist/internal/Mpp.d.ts +71 -0
- package/dist/internal/Mpp.d.ts.map +1 -0
- package/dist/internal/Mpp.js +105 -0
- package/dist/internal/Mpp.js.map +1 -0
- package/dist/internal/OpenApi.d.ts +313 -0
- package/dist/internal/OpenApi.d.ts.map +1 -0
- package/dist/internal/OpenApi.fonts.d.ts +2 -0
- package/dist/internal/OpenApi.fonts.d.ts.map +1 -0
- package/dist/internal/OpenApi.fonts.js +39 -0
- package/dist/internal/OpenApi.fonts.js.map +1 -0
- package/dist/internal/OpenApi.js +335 -0
- package/dist/internal/OpenApi.js.map +1 -0
- package/dist/internal/OpenApi.logo.d.ts +6 -0
- package/dist/internal/OpenApi.logo.d.ts.map +1 -0
- package/dist/internal/OpenApi.logo.js +19 -0
- package/dist/internal/OpenApi.logo.js.map +1 -0
- package/dist/internal/Path.d.ts +5 -0
- package/dist/internal/Path.d.ts.map +1 -0
- package/dist/internal/Path.js +16 -0
- package/dist/internal/Path.js.map +1 -0
- package/dist/internal/RateLimit.d.ts +60 -0
- package/dist/internal/RateLimit.d.ts.map +1 -0
- package/dist/internal/RateLimit.js +48 -0
- package/dist/internal/RateLimit.js.map +1 -0
- package/dist/internal/Response.d.ts +132 -0
- package/dist/internal/Response.d.ts.map +1 -0
- package/dist/internal/Response.js +72 -0
- package/dist/internal/Response.js.map +1 -0
- package/dist/internal/Schema.d.ts +257 -0
- package/dist/internal/Schema.d.ts.map +1 -0
- package/dist/internal/Schema.js +360 -0
- package/dist/internal/Schema.js.map +1 -0
- package/dist/internal/Store.d.ts +204 -0
- package/dist/internal/Store.d.ts.map +1 -0
- package/dist/internal/Store.js +291 -0
- package/dist/internal/Store.js.map +1 -0
- package/dist/internal/Tidx.d.ts +120 -0
- package/dist/internal/Tidx.d.ts.map +1 -0
- package/dist/internal/Tidx.js +156 -0
- package/dist/internal/Tidx.js.map +1 -0
- package/dist/internal/Timing.d.ts +22 -0
- package/dist/internal/Timing.d.ts.map +1 -0
- package/dist/internal/Timing.js +32 -0
- package/dist/internal/Timing.js.map +1 -0
- package/dist/internal/Ttl.d.ts +25 -0
- package/dist/internal/Ttl.d.ts.map +1 -0
- package/dist/internal/Ttl.js +49 -0
- package/dist/internal/Ttl.js.map +1 -0
- package/dist/internal/Value.d.ts +47 -0
- package/dist/internal/Value.d.ts.map +1 -0
- package/dist/internal/Value.js +122 -0
- package/dist/internal/Value.js.map +1 -0
- package/dist/internal/VerifiedTokens.d.ts +228 -0
- package/dist/internal/VerifiedTokens.d.ts.map +1 -0
- package/dist/internal/VerifiedTokens.js +450 -0
- package/dist/internal/VerifiedTokens.js.map +1 -0
- package/dist/internal/Viem.d.ts +2082 -0
- package/dist/internal/Viem.d.ts.map +1 -0
- package/dist/internal/Viem.js +63 -0
- package/dist/internal/Viem.js.map +1 -0
- package/dist/internal/WebhookDestination.d.ts +198 -0
- package/dist/internal/WebhookDestination.d.ts.map +1 -0
- package/dist/internal/WebhookDestination.js +170 -0
- package/dist/internal/WebhookDestination.js.map +1 -0
- package/dist/internal/Webhooks.d.ts +433 -0
- package/dist/internal/Webhooks.d.ts.map +1 -0
- package/dist/internal/Webhooks.js +489 -0
- package/dist/internal/Webhooks.js.map +1 -0
- package/dist/internal/webhookDestinations/betterStack.d.ts +26 -0
- package/dist/internal/webhookDestinations/betterStack.d.ts.map +1 -0
- package/dist/internal/webhookDestinations/betterStack.js +76 -0
- package/dist/internal/webhookDestinations/betterStack.js.map +1 -0
- package/dist/internal/webhookDestinations/slack.d.ts +59 -0
- package/dist/internal/webhookDestinations/slack.d.ts.map +1 -0
- package/dist/internal/webhookDestinations/slack.js +422 -0
- package/dist/internal/webhookDestinations/slack.js.map +1 -0
- package/dist/internal/webhookDestinations/url.d.ts +17 -0
- package/dist/internal/webhookDestinations/url.d.ts.map +1 -0
- package/dist/internal/webhookDestinations/url.js +130 -0
- package/dist/internal/webhookDestinations/url.js.map +1 -0
- package/dist/openrpc.json +5302 -0
- package/package.json +46 -4
- package/seeds/31318/icons/0x20c0000000000000000000000000000000000000.svg +1 -0
- package/seeds/31318/icons/0x20c0000000000000000000000000000000000001.svg +1 -0
- package/seeds/31318/icons/0x20c0000000000000000000000000000000000002.svg +1 -0
- package/seeds/31318/icons/0x20c0000000000000000000000000000000000003.svg +1 -0
- package/seeds/31318/verified-tokens.json +30 -0
- package/seeds/4217/icons/0x20c0000000000000000000000000000000000000.svg +25 -0
- package/seeds/4217/icons/0x20c000000000000000000000048c8f36df1c9a4a.svg +1 -0
- package/seeds/4217/icons/0x20c0000000000000000000000520792dcccccccc.svg +1 -0
- package/seeds/4217/icons/0x20c0000000000000000000000a6da882d075a4c3.svg +4 -0
- package/seeds/4217/icons/0x20c000000000000000000000111111111e910f0f.svg +30 -0
- package/seeds/4217/icons/0x20c00000000000000000000014f22ca97301eb73.svg +1 -0
- package/seeds/4217/icons/0x20c0000000000000000000001621e21f71cf12fb.svg +1 -0
- package/seeds/4217/icons/0x20c0000000000000000000002f52d5cc21a3207b.svg +1 -0
- package/seeds/4217/icons/0x20c0000000000000000000003158081efd85bfc2.svg +11 -0
- package/seeds/4217/icons/0x20c0000000000000000000003554d28269e0f3c2.svg +1 -0
- package/seeds/4217/icons/0x20c000000000000000000000383a23bacb546ab9.svg +1 -0
- package/seeds/4217/icons/0x20c00000000000000000000042109aef2f8b28e1.png +0 -0
- package/seeds/4217/icons/0x20c0000000000000000000005c0bac7cef389a11.svg +1 -0
- package/seeds/4217/icons/0x20c0000000000000000000006fd9a167923ba194.png +0 -0
- package/seeds/4217/icons/0x20c0000000000000000000007f7ba549dd0251b9.svg +1 -0
- package/seeds/4217/icons/0x20c0000000000000000000008191667423f70e67.svg +5 -0
- package/seeds/4217/icons/0x20c0000000000000000000008ee4fcff88888888.svg +1 -0
- package/seeds/4217/icons/0x20c0000000000000000000009a4a4b17e0dc6651.svg +1 -0
- package/seeds/4217/icons/0x20c000000000000000000000ab02d39df30bd17e.svg +1 -0
- package/seeds/4217/icons/0x20c000000000000000000000ae247a1130450f09.svg +1 -0
- package/seeds/4217/icons/0x20c000000000000000000000aeed2ec36a54d0e5.svg +1 -0
- package/seeds/4217/icons/0x20c000000000000000000000b9537d11c60e8b50.svg +1 -0
- package/seeds/4217/icons/0x20c000000000000000000000bd95bfb69fbe6ce3.svg +1 -0
- package/seeds/4217/icons/0x20c000000000000000000000c412ec89d0c08be5.svg +1 -0
- package/seeds/4217/icons/0x20c000000000000000000000f047dd7018e50367.png +0 -0
- package/seeds/4217/verified-tokens.json +177 -0
- package/seeds/42431/icons/0x20c0000000000000000000000000000000000000.svg +25 -0
- package/seeds/42431/icons/0x20c0000000000000000000000000000000000001.svg +1 -0
- package/seeds/42431/icons/0x20c0000000000000000000000000000000000002.svg +1 -0
- package/seeds/42431/icons/0x20c0000000000000000000000000000000000003.svg +1 -0
- package/seeds/42431/icons/0x20c0000000000000000000009e8d7eb59b783726.svg +1 -0
- package/seeds/42431/icons/0x20c000000000000000000000d72572838bbee59c.svg +1 -0
- package/seeds/42431/verified-tokens.json +44 -0
- package/seeds/openrpc/README.md +27 -0
- package/seeds/openrpc/execution-apis.json +8989 -0
- package/src/ApiKey.test.ts +91 -0
- package/src/ApiKey.ts +122 -0
- package/src/ApiKeySource.test-d.ts +11 -0
- package/src/ApiKeySource.test.ts +295 -0
- package/src/ApiKeySource.ts +303 -0
- package/src/App.test-d.ts +38 -0
- package/src/App.test.ts +65927 -0
- package/src/App.ts +855 -0
- package/src/Assets.ts +58 -0
- package/src/Auth.ts +1 -0
- package/src/Cli.test.ts +81 -0
- package/src/Cli.ts +38 -0
- package/src/Client.test-d.ts +1438 -0
- package/src/Client.test.ts +134 -0
- package/src/Client.ts +45 -0
- package/src/Metrics.ts +154 -0
- package/src/Scope.ts +50 -0
- package/src/Webhooks.test.ts +49 -0
- package/src/Webhooks.ts +172 -0
- package/src/admin/Access.test.ts +130 -0
- package/src/admin/Access.ts +87 -0
- package/src/admin/App.test.ts +225 -0
- package/src/admin/App.ts +216 -0
- package/src/admin/apps/api-keys.test.ts +156 -0
- package/src/admin/apps/api-keys.ts +157 -0
- package/src/admin/apps/scopes.test.ts +52 -0
- package/src/admin/apps/scopes.ts +47 -0
- package/src/admin/apps/verified-tokens.test.ts +415 -0
- package/src/admin/apps/verified-tokens.ts +566 -0
- package/src/admin/index.ts +2 -0
- package/src/admin/ui/plugin.ts +135 -0
- package/src/admin/ui/src/components/Checkbox.tsx +22 -0
- package/src/admin/ui/src/components/Dialog.tsx +32 -0
- package/src/admin/ui/src/components/MobileProfile.tsx +31 -0
- package/src/admin/ui/src/components/Page.tsx +53 -0
- package/src/admin/ui/src/components/TempoLogo.tsx +37 -0
- package/src/admin/ui/src/fonts/Pilat-Demi.woff2 +0 -0
- package/src/admin/ui/src/fonts/Pilat-Regular.woff2 +0 -0
- package/src/admin/ui/src/lib/api-keys.ts +63 -0
- package/src/admin/ui/src/lib/client.ts +15 -0
- package/src/admin/ui/src/lib/config.ts +57 -0
- package/src/admin/ui/src/lib/query-client.ts +26 -0
- package/src/admin/ui/src/lib/scopes.ts +25 -0
- package/src/admin/ui/src/lib/verified-tokens.ts +89 -0
- package/src/admin/ui/src/lib/wagmi.ts +18 -0
- package/src/admin/ui/src/routeTree.gen.ts +104 -0
- package/src/admin/ui/src/router.tsx +25 -0
- package/src/admin/ui/src/routes/__root.tsx +219 -0
- package/src/admin/ui/src/routes/api-keys.tsx +329 -0
- package/src/admin/ui/src/routes/index.tsx +7 -0
- package/src/admin/ui/src/routes/verified-tokens.tsx +331 -0
- package/src/admin/ui/src/styles.css +20 -0
- package/src/admin/ui/tsconfig.json +16 -0
- package/src/admin/ui/vite.config.ts +29 -0
- package/src/admin/ui.generated.ts +60 -0
- package/src/apps/activities.test.ts +1020 -0
- package/src/apps/activities.ts +3934 -0
- package/src/apps/addresses.ts +24 -0
- package/src/apps/balances.test.ts +274 -0
- package/src/apps/balances.ts +533 -0
- package/src/apps/blocks.test.ts +385 -0
- package/src/apps/blocks.ts +896 -0
- package/src/apps/coingecko.test.ts +218 -0
- package/src/apps/coingecko.ts +1383 -0
- package/src/apps/exchanges.test.ts +958 -0
- package/src/apps/exchanges.ts +4773 -0
- package/src/apps/fee-amm.test.ts +321 -0
- package/src/apps/fee-amm.ts +1121 -0
- package/src/apps/indexer.test.ts +153 -0
- package/src/apps/indexer.ts +330 -0
- package/src/apps/receipts.test.ts +285 -0
- package/src/apps/receipts.ts +1193 -0
- package/src/apps/rpc.test.ts +302 -0
- package/src/apps/rpc.ts +222 -0
- package/src/apps/tokenlist.test.ts +80 -0
- package/src/apps/tokenlist.ts +207 -0
- package/src/apps/tokens.test.ts +1062 -0
- package/src/apps/tokens.ts +2292 -0
- package/src/apps/transactions.test.ts +408 -0
- package/src/apps/transactions.ts +1363 -0
- package/src/apps/transfers.test.ts +371 -0
- package/src/apps/transfers.ts +1116 -0
- package/src/apps/verified-tokens.test.ts +114 -0
- package/src/apps/verified-tokens.ts +295 -0
- package/src/apps/webhooks.test.ts +1937 -0
- package/src/apps/webhooks.ts +2926 -0
- package/src/cloudflare.ts +55 -0
- package/src/index.ts +15 -0
- package/src/internal/Auth.test.ts +980 -0
- package/src/internal/Auth.ts +1012 -0
- package/src/internal/Cache.test.ts +240 -0
- package/src/internal/Cache.ts +262 -0
- package/src/internal/Cursor.test.ts +100 -0
- package/src/internal/Cursor.ts +149 -0
- package/src/internal/EdgeCache.test.ts +234 -0
- package/src/internal/EdgeCache.ts +224 -0
- package/src/internal/Errors.ts +14 -0
- package/src/internal/Log.test.ts +244 -0
- package/src/internal/Log.ts +216 -0
- package/src/internal/MetricSink.ts +220 -0
- package/src/internal/Mpp.test.ts +64 -0
- package/src/internal/Mpp.ts +141 -0
- package/src/internal/OpenApi.fonts.ts +38 -0
- package/src/internal/OpenApi.logo.ts +18 -0
- package/src/internal/OpenApi.ts +488 -0
- package/src/internal/Path.test.ts +14 -0
- package/src/internal/Path.ts +15 -0
- package/src/internal/RateLimit.test.ts +93 -0
- package/src/internal/RateLimit.ts +93 -0
- package/src/internal/Response.ts +131 -0
- package/src/internal/Schema.test.ts +34 -0
- package/src/internal/Schema.ts +523 -0
- package/src/internal/Store.test.ts +545 -0
- package/src/internal/Store.ts +526 -0
- package/src/internal/Tidx.local.test.ts +33 -0
- package/src/internal/Tidx.test.ts +294 -0
- package/src/internal/Tidx.ts +238 -0
- package/src/internal/Timing.test.ts +22 -0
- package/src/internal/Timing.ts +62 -0
- package/src/internal/Ttl.test.ts +18 -0
- package/src/internal/Ttl.ts +59 -0
- package/src/internal/Value.ts +114 -0
- package/src/internal/VerifiedTokens.test.ts +333 -0
- package/src/internal/VerifiedTokens.ts +645 -0
- package/src/internal/Viem.test.ts +115 -0
- package/src/internal/Viem.ts +165 -0
- package/src/internal/WebhookDestination.test.ts +648 -0
- package/src/internal/WebhookDestination.ts +331 -0
- package/src/internal/Webhooks.test.ts +668 -0
- package/src/internal/Webhooks.ts +940 -0
- package/src/internal/webhookDestinations/betterStack.ts +91 -0
- package/src/internal/webhookDestinations/slack.ts +496 -0
- package/src/internal/webhookDestinations/url.ts +128 -0
- 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
|
+
}
|