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