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,294 @@
|
|
|
1
|
+
import { Tidx as TidxClient } from 'tidx.ts'
|
|
2
|
+
|
|
3
|
+
import type * as Auth from './Auth.js'
|
|
4
|
+
import * as Tidx from './Tidx.js'
|
|
5
|
+
import type * as Viem from './Viem.js'
|
|
6
|
+
|
|
7
|
+
/** Anonymous (public) request principal. */
|
|
8
|
+
const publicPrincipal: Auth.Principal = { id: 'pub', type: 'public' }
|
|
9
|
+
/** Authenticated API-key request principal. */
|
|
10
|
+
const apiKeyPrincipal: Auth.Principal = {
|
|
11
|
+
apiKey: {} as never,
|
|
12
|
+
environment: 'production',
|
|
13
|
+
id: 'key_test',
|
|
14
|
+
orgId: 'org_test',
|
|
15
|
+
type: 'api_key',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Builds a `FetchRequestError` as the indexer surfaces it at a given status. */
|
|
19
|
+
function appError(message: string, status = 200) {
|
|
20
|
+
return new TidxClient.FetchRequestError(message, new Response(null, { status }))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Minimal client whose `fetch` is a controllable mock. */
|
|
24
|
+
function fakeClient(fetch: Tidx.Client['fetch']): Tidx.Client {
|
|
25
|
+
return { fetch } as never
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('escape', () => {
|
|
29
|
+
test('doubles quotes for ANSI string literals, leaving backslashes alone', () => {
|
|
30
|
+
expect(Tidx.escape('USD')).toMatchInlineSnapshot(`"USD"`)
|
|
31
|
+
expect(Tidx.escape("US' OR '1'='1")).toMatchInlineSnapshot(`"US'' OR ''1''=''1"`)
|
|
32
|
+
expect(Tidx.escape('back\\slash')).toMatchInlineSnapshot(`"back\\slash"`)
|
|
33
|
+
expect(Tidx.escape("\\'")).toMatchInlineSnapshot(`"\\''"`)
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('withTransientRetry', () => {
|
|
38
|
+
test('retries a transient `db error` and resolves once it succeeds', async () => {
|
|
39
|
+
vi.useFakeTimers()
|
|
40
|
+
try {
|
|
41
|
+
const result = { hasMore: false, rows: [] } as never
|
|
42
|
+
const fetch = vi
|
|
43
|
+
.fn()
|
|
44
|
+
.mockRejectedValueOnce(appError('db error'))
|
|
45
|
+
.mockRejectedValueOnce(appError('db error'))
|
|
46
|
+
.mockResolvedValueOnce(result)
|
|
47
|
+
const client = Tidx.withTransientRetry(fakeClient(fetch))
|
|
48
|
+
|
|
49
|
+
const promise = client.fetch({ chainId: 42431, query: 'SELECT 1' })
|
|
50
|
+
await vi.runAllTimersAsync()
|
|
51
|
+
await expect(promise).resolves.toBe(result)
|
|
52
|
+
expect(fetch).toHaveBeenCalledTimes(3)
|
|
53
|
+
} finally {
|
|
54
|
+
vi.useRealTimers()
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('retries a dropped ClickHouse Cloud connection', async () => {
|
|
59
|
+
vi.useFakeTimers()
|
|
60
|
+
try {
|
|
61
|
+
const result = { hasMore: false, rows: [] } as never
|
|
62
|
+
const fetch = vi
|
|
63
|
+
.fn()
|
|
64
|
+
.mockRejectedValueOnce(
|
|
65
|
+
appError('ClickHouse HTTP request failed: error sending request for url (…)'),
|
|
66
|
+
)
|
|
67
|
+
.mockResolvedValueOnce(result)
|
|
68
|
+
const client = Tidx.withTransientRetry(fakeClient(fetch))
|
|
69
|
+
|
|
70
|
+
const promise = client.fetch({ chainId: 42431, query: 'SELECT 1' })
|
|
71
|
+
await vi.runAllTimersAsync()
|
|
72
|
+
await expect(promise).resolves.toBe(result)
|
|
73
|
+
expect(fetch).toHaveBeenCalledTimes(2)
|
|
74
|
+
} finally {
|
|
75
|
+
vi.useRealTimers()
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('retries HTTP-level transient failures under the shared budget', async () => {
|
|
80
|
+
vi.useFakeTimers()
|
|
81
|
+
try {
|
|
82
|
+
const result = { hasMore: false, rows: [] } as never
|
|
83
|
+
const fetch = vi
|
|
84
|
+
.fn()
|
|
85
|
+
.mockRejectedValueOnce(appError('bad gateway', 502))
|
|
86
|
+
.mockRejectedValueOnce(appError('too many requests', 429))
|
|
87
|
+
.mockResolvedValueOnce(result)
|
|
88
|
+
const client = Tidx.withTransientRetry(fakeClient(fetch))
|
|
89
|
+
|
|
90
|
+
const promise = client.fetch({ chainId: 42431, query: 'SELECT 1' })
|
|
91
|
+
await vi.runAllTimersAsync()
|
|
92
|
+
await expect(promise).resolves.toBe(result)
|
|
93
|
+
expect(fetch).toHaveBeenCalledTimes(3)
|
|
94
|
+
} finally {
|
|
95
|
+
vi.useRealTimers()
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('disables the inner client retries so the budget is not multiplied', async () => {
|
|
100
|
+
const fetch = vi.fn().mockResolvedValue({ hasMore: false, rows: [] } as never)
|
|
101
|
+
const client = Tidx.withTransientRetry(fakeClient(fetch))
|
|
102
|
+
|
|
103
|
+
await client.fetch({ chainId: 42431, query: 'SELECT 1' })
|
|
104
|
+
|
|
105
|
+
expect(fetch.mock.calls[0]![0]).toMatchInlineSnapshot(`
|
|
106
|
+
{
|
|
107
|
+
"chainId": 42431,
|
|
108
|
+
"query": "SELECT 1",
|
|
109
|
+
"retryCount": 1,
|
|
110
|
+
}
|
|
111
|
+
`)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('does not retry a deterministic application error', async () => {
|
|
115
|
+
const fetch = vi.fn().mockRejectedValue(appError('syntax error near "SELCT"'))
|
|
116
|
+
const client = Tidx.withTransientRetry(fakeClient(fetch))
|
|
117
|
+
|
|
118
|
+
await expect(client.fetch({ chainId: 42431, query: 'SELCT 1' })).rejects.toThrow('syntax error')
|
|
119
|
+
expect(fetch).toHaveBeenCalledTimes(1)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('does not retry an HTTP 422 `db error` (deterministic query-shape failure)', async () => {
|
|
123
|
+
const fetch = vi.fn().mockRejectedValue(appError('db error', 422))
|
|
124
|
+
const client = Tidx.withTransientRetry(fakeClient(fetch))
|
|
125
|
+
|
|
126
|
+
await expect(client.fetch({ chainId: 42431, query: 'SELECT 1' })).rejects.toThrow('db error')
|
|
127
|
+
expect(fetch).toHaveBeenCalledTimes(1)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('gives up after exhausting retries on persistent `db error`', async () => {
|
|
131
|
+
vi.useFakeTimers()
|
|
132
|
+
try {
|
|
133
|
+
const fetch = vi.fn().mockRejectedValue(appError('db error'))
|
|
134
|
+
const client = Tidx.withTransientRetry(fakeClient(fetch))
|
|
135
|
+
|
|
136
|
+
const promise = client.fetch({ chainId: 42431, query: 'SELECT 1' })
|
|
137
|
+
const assertion = expect(promise).rejects.toThrow('db error')
|
|
138
|
+
await vi.runAllTimersAsync()
|
|
139
|
+
await assertion
|
|
140
|
+
// First attempt plus four retries.
|
|
141
|
+
expect(fetch).toHaveBeenCalledTimes(5)
|
|
142
|
+
} finally {
|
|
143
|
+
vi.useRealTimers()
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
describe('resolve', () => {
|
|
149
|
+
test('defaults to the per-chain indexer URL', () => {
|
|
150
|
+
expect(Tidx.resolve(undefined, { chainId: 4217, principal: null })).toMatchInlineSnapshot(`
|
|
151
|
+
{
|
|
152
|
+
"baseUrl": "https://indexer.tempo.xyz",
|
|
153
|
+
"basicAuth": undefined,
|
|
154
|
+
}
|
|
155
|
+
`)
|
|
156
|
+
expect(Tidx.resolve(undefined, { chainId: 42431, principal: null })).toMatchInlineSnapshot(`
|
|
157
|
+
{
|
|
158
|
+
"baseUrl": "https://indexer.testnet.tempo.xyz",
|
|
159
|
+
"basicAuth": undefined,
|
|
160
|
+
}
|
|
161
|
+
`)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('uses a static base URL override', () => {
|
|
165
|
+
expect(Tidx.resolve({ baseUrl: 'https://indexer.test' }, { chainId: 4217, principal: null }))
|
|
166
|
+
.toMatchInlineSnapshot(`
|
|
167
|
+
{
|
|
168
|
+
"baseUrl": "https://indexer.test",
|
|
169
|
+
"basicAuth": undefined,
|
|
170
|
+
}
|
|
171
|
+
`)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
test('uses a base URL resolver override', () => {
|
|
175
|
+
expect(
|
|
176
|
+
Tidx.resolve(
|
|
177
|
+
{ baseUrl: (chainId) => `https://${chainId}.indexer.test` },
|
|
178
|
+
{ chainId: 42431, principal: null },
|
|
179
|
+
),
|
|
180
|
+
).toMatchInlineSnapshot(`
|
|
181
|
+
{
|
|
182
|
+
"baseUrl": "https://42431.indexer.test",
|
|
183
|
+
"basicAuth": undefined,
|
|
184
|
+
}
|
|
185
|
+
`)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('the static form ignores the principal', () => {
|
|
189
|
+
const tidx = { baseUrl: 'https://internal.indexer', basicAuth: 'user:pass' }
|
|
190
|
+
expect(Tidx.resolve(tidx, { chainId: 4217, principal: publicPrincipal }))
|
|
191
|
+
.toMatchInlineSnapshot(`
|
|
192
|
+
{
|
|
193
|
+
"baseUrl": "https://internal.indexer",
|
|
194
|
+
"basicAuth": "user:pass",
|
|
195
|
+
}
|
|
196
|
+
`)
|
|
197
|
+
expect(Tidx.resolve(tidx, { chainId: 4217, principal: apiKeyPrincipal }))
|
|
198
|
+
.toMatchInlineSnapshot(`
|
|
199
|
+
{
|
|
200
|
+
"baseUrl": "https://internal.indexer",
|
|
201
|
+
"basicAuth": "user:pass",
|
|
202
|
+
}
|
|
203
|
+
`)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
test('the resolver form routes anonymous callers to a separate upstream', () => {
|
|
207
|
+
const tidx = ({ principal }: Viem.resolve.Context) =>
|
|
208
|
+
principal?.type === 'public'
|
|
209
|
+
? { baseUrl: 'https://indexer.tempo.xyz' }
|
|
210
|
+
: { baseUrl: 'https://internal.indexer', basicAuth: 'user:pass' }
|
|
211
|
+
expect(Tidx.resolve(tidx, { chainId: 4217, principal: publicPrincipal }))
|
|
212
|
+
.toMatchInlineSnapshot(`
|
|
213
|
+
{
|
|
214
|
+
"baseUrl": "https://indexer.tempo.xyz",
|
|
215
|
+
"basicAuth": undefined,
|
|
216
|
+
}
|
|
217
|
+
`)
|
|
218
|
+
expect(Tidx.resolve(tidx, { chainId: 4217, principal: apiKeyPrincipal }))
|
|
219
|
+
.toMatchInlineSnapshot(`
|
|
220
|
+
{
|
|
221
|
+
"baseUrl": "https://internal.indexer",
|
|
222
|
+
"basicAuth": "user:pass",
|
|
223
|
+
}
|
|
224
|
+
`)
|
|
225
|
+
// A null principal (trusted poller) takes the non-public branch.
|
|
226
|
+
expect(Tidx.resolve(tidx, { chainId: 4217, principal: null })).toMatchInlineSnapshot(`
|
|
227
|
+
{
|
|
228
|
+
"baseUrl": "https://internal.indexer",
|
|
229
|
+
"basicAuth": "user:pass",
|
|
230
|
+
}
|
|
231
|
+
`)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
test('a resolver that omits baseUrl falls back to the built-in public host', () => {
|
|
235
|
+
const tidx = () => ({})
|
|
236
|
+
expect(Tidx.resolve(tidx, { chainId: 42431, principal: publicPrincipal }))
|
|
237
|
+
.toMatchInlineSnapshot(`
|
|
238
|
+
{
|
|
239
|
+
"baseUrl": "https://indexer.testnet.tempo.xyz",
|
|
240
|
+
"basicAuth": undefined,
|
|
241
|
+
}
|
|
242
|
+
`)
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
describe('createGetClient', () => {
|
|
247
|
+
test('memoizes one client per resolved upstream', () => {
|
|
248
|
+
const getTidx = Tidx.createGetClient({
|
|
249
|
+
tidx: ({ principal }) =>
|
|
250
|
+
principal?.type === 'public'
|
|
251
|
+
? { baseUrl: 'https://indexer.tempo.xyz' }
|
|
252
|
+
: { baseUrl: 'https://internal.indexer', basicAuth: 'user:pass' },
|
|
253
|
+
})
|
|
254
|
+
// Callers resolving to the same upstream share a client; distinct upstreams
|
|
255
|
+
// (and chain ids) get distinct clients.
|
|
256
|
+
expect(getTidx(4217, apiKeyPrincipal)).toBe(getTidx(4217, apiKeyPrincipal))
|
|
257
|
+
expect(getTidx(4217, publicPrincipal)).toBe(getTidx(4217, publicPrincipal))
|
|
258
|
+
expect(getTidx(4217, publicPrincipal)).not.toBe(getTidx(4217, apiKeyPrincipal))
|
|
259
|
+
expect(getTidx(4217, apiKeyPrincipal)).not.toBe(getTidx(42431, apiKeyPrincipal))
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
test('shares a client across principals when the resolver ignores them', () => {
|
|
263
|
+
const getTidx = Tidx.createGetClient()
|
|
264
|
+
// With no resolver the upstream is identical for every caller, so the key
|
|
265
|
+
// (resolved output) collapses and all principals share one client.
|
|
266
|
+
expect(getTidx(4217, publicPrincipal)).toBe(getTidx(4217, apiKeyPrincipal))
|
|
267
|
+
expect(getTidx(4217, null)).toBe(getTidx(4217, publicPrincipal))
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
test('consults the resolver on every call but builds one client per upstream', () => {
|
|
271
|
+
const seen: (string | undefined)[] = []
|
|
272
|
+
const getTidx = Tidx.createGetClient({
|
|
273
|
+
tidx: ({ principal }) => {
|
|
274
|
+
const config =
|
|
275
|
+
principal?.type === 'public'
|
|
276
|
+
? { baseUrl: 'https://indexer.tempo.xyz' }
|
|
277
|
+
: { baseUrl: 'https://internal.indexer', basicAuth: 'user:pass' }
|
|
278
|
+
seen.push(principal?.type)
|
|
279
|
+
return config
|
|
280
|
+
},
|
|
281
|
+
})
|
|
282
|
+
const first = getTidx(4217, apiKeyPrincipal)
|
|
283
|
+
const second = getTidx(4217, apiKeyPrincipal)
|
|
284
|
+
getTidx(4217, publicPrincipal)
|
|
285
|
+
// The resolver is consulted on every call (it derives the cache key from its
|
|
286
|
+
// resolved output) — including the repeat that hits the cache — so a
|
|
287
|
+
// per-request principal is always honored.
|
|
288
|
+
expect(seen).toContain('api_key')
|
|
289
|
+
expect(seen).toContain('public')
|
|
290
|
+
expect(seen.length).toBeGreaterThanOrEqual(3)
|
|
291
|
+
// …but the client itself (the costly construction) is built once per upstream.
|
|
292
|
+
expect(first).toBe(second)
|
|
293
|
+
})
|
|
294
|
+
})
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { Tidx } from 'tidx.ts'
|
|
2
|
+
|
|
3
|
+
import type * as Auth from './Auth.js'
|
|
4
|
+
import * as Viem from './Viem.js'
|
|
5
|
+
|
|
6
|
+
/** Default Tempo indexer (TIDX) base URLs. */
|
|
7
|
+
export const url: Record<number, string> = {
|
|
8
|
+
4217: 'https://indexer.tempo.xyz',
|
|
9
|
+
42431: 'https://indexer.testnet.tempo.xyz',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** TIDX query client. */
|
|
13
|
+
export type Client = Tidx.Tidx
|
|
14
|
+
|
|
15
|
+
/** Gets a TIDX query client for a Tempo chain id and request principal. */
|
|
16
|
+
export type GetClient = (chainId?: Viem.ChainId, principal?: Auth.Principal | null) => Client
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolves a TIDX base URL for a Tempo chain id. May return `undefined` to fall
|
|
20
|
+
* back to the built-in {@link url} map (e.g. for a chain the resolver doesn't
|
|
21
|
+
* serve).
|
|
22
|
+
*/
|
|
23
|
+
export type UrlResolver = (chainId: Viem.ChainId) => string | undefined
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolves the effective TIDX config for a single request.
|
|
27
|
+
*
|
|
28
|
+
* Accepts either a static config object or a per-request resolver function that
|
|
29
|
+
* receives a {@link Viem.resolve.Context} (`chainId` plus the request
|
|
30
|
+
* `principal`), so callers can route traffic per caller — e.g. send anonymous
|
|
31
|
+
* (`principal.type === 'public'`) traffic to a separate, no-auth indexer. A
|
|
32
|
+
* missing `baseUrl` falls back to the built-in public host map ({@link url}); a
|
|
33
|
+
* missing `basicAuth` means no Basic auth is sent.
|
|
34
|
+
*/
|
|
35
|
+
export function resolve(
|
|
36
|
+
tidx: getClient.Tidx | undefined,
|
|
37
|
+
context: Viem.resolve.Context,
|
|
38
|
+
): resolve.Resolved {
|
|
39
|
+
const config = typeof tidx === 'function' ? tidx(context) : tidx
|
|
40
|
+
return {
|
|
41
|
+
baseUrl:
|
|
42
|
+
(typeof config?.baseUrl === 'function' ? config.baseUrl(context.chainId) : config?.baseUrl) ??
|
|
43
|
+
url[context.chainId],
|
|
44
|
+
basicAuth: config?.basicAuth,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export declare namespace resolve {
|
|
49
|
+
/** Resolved TIDX config for a single request. */
|
|
50
|
+
type Resolved = {
|
|
51
|
+
/**
|
|
52
|
+
* Resolved TIDX base URL, or `undefined` when neither a configured `tidx`
|
|
53
|
+
* nor the built-in {@link url} map covers the chain (e.g. an unconfigured
|
|
54
|
+
* localnet). A `undefined` base URL falls through to the TIDX client default.
|
|
55
|
+
*/
|
|
56
|
+
baseUrl: string | undefined
|
|
57
|
+
/** Basic auth credentials, or undefined to send no Basic auth. */
|
|
58
|
+
basicAuth?: string | undefined
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getClient(options: getClient.Options): Client {
|
|
63
|
+
const { chainId = Viem.defaultChainId, principal = null, tidx } = options
|
|
64
|
+
const { baseUrl, basicAuth } = resolve(tidx, { chainId, principal })
|
|
65
|
+
return withTransientRetry(Tidx.create({ baseUrl, basicAuth, chainId }))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Builds a memoized {@link GetClient} that constructs one TIDX client per
|
|
70
|
+
* resolved upstream and reuses it. Clients are stateless HTTP transports, so
|
|
71
|
+
* reusing them avoids per-call construction cost. The cache key is the resolved
|
|
72
|
+
* output (`baseUrl` + `basicAuth` + `chainId`), not the principal, so a resolver
|
|
73
|
+
* `tidx` may branch on any principal field while client cardinality stays
|
|
74
|
+
* bounded by the number of distinct upstreams — callers that resolve to the same
|
|
75
|
+
* upstream share a client. Shared by `App.create` (per-request reads) and
|
|
76
|
+
* `Webhooks.createPoller` (the scheduled poller) so the resolver logic lives in
|
|
77
|
+
* one place.
|
|
78
|
+
*/
|
|
79
|
+
export function createGetClient(options: createGetClient.Options = {}): GetClient {
|
|
80
|
+
const { defaultChainId = Viem.defaultChainId, tidx } = options
|
|
81
|
+
const clients = new Map<string, Client>()
|
|
82
|
+
|
|
83
|
+
return (chainId = defaultChainId, principal = null) => {
|
|
84
|
+
// Resolve to derive the cache key; `getClient` re-resolves to build the
|
|
85
|
+
// client on a miss. `resolve` is pure and cheap, so the extra call is free.
|
|
86
|
+
const { baseUrl, basicAuth } = resolve(tidx, { chainId, principal })
|
|
87
|
+
const cacheKey = `${baseUrl}|${basicAuth ?? ''}|${chainId}`
|
|
88
|
+
const existing = clients.get(cacheKey)
|
|
89
|
+
if (existing) return existing
|
|
90
|
+
|
|
91
|
+
const client = getClient({ chainId, principal, tidx })
|
|
92
|
+
clients.set(cacheKey, client)
|
|
93
|
+
return client
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Number of additional `fetch` attempts for transient application-level
|
|
99
|
+
* indexer errors. The first attempt plus this many retries.
|
|
100
|
+
*
|
|
101
|
+
* Combined with {@link transientRetryDelay} this gives a ~3.75s backoff window
|
|
102
|
+
* (250 + 500 + 1000 + 2000ms over five attempts). The heaviest reads — e.g.
|
|
103
|
+
* the unfiltered global swap scan — can momentarily trip the indexer's `db
|
|
104
|
+
* error` under concurrent load, and a ~1s window was too short to ride those
|
|
105
|
+
* bursts out, surfacing them to callers as a 502.
|
|
106
|
+
*/
|
|
107
|
+
const transientRetryCount = 4
|
|
108
|
+
|
|
109
|
+
/** Base backoff (ms) between transient-error retries; doubles each attempt. */
|
|
110
|
+
const transientRetryDelay = 250
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Application-level indexer failures that are transient and safe to retry on
|
|
114
|
+
* an idempotent read. The indexer surfaces transient ClickHouse failures (a
|
|
115
|
+
* busy/contended planner, a query-execution hiccup) as the literal message
|
|
116
|
+
* `db error`; timeouts and connection resets are likewise non-deterministic.
|
|
117
|
+
* `error sending request` is how TIDX reports a ClickHouse Cloud connection
|
|
118
|
+
* dropped at the server's execution cap — typically a cold object-storage
|
|
119
|
+
* cache read that warms across attempts, so retrying genuinely helps.
|
|
120
|
+
*/
|
|
121
|
+
const transientErrorPattern =
|
|
122
|
+
/db error|timeout|timed out|connection reset|connection refused|error sending request/i
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Escapes a free-form string for safe embedding inside a single-quoted SQL
|
|
126
|
+
* string literal sent to the indexer. TIDX parses every query with an ANSI
|
|
127
|
+
* SQL parser before re-rendering it for the engine, and ANSI strings have
|
|
128
|
+
* exactly one special character inside `'…'`: the quote itself, escaped by
|
|
129
|
+
* doubling (`''`). Backslash escaping (`\'`) is **rejected** with a parse
|
|
130
|
+
* error, and a backslash is an ordinary character on both engines (verified
|
|
131
|
+
* against the deployed indexer on `postgres` and `clickhouse`).
|
|
132
|
+
*
|
|
133
|
+
* Most interpolated values (addresses, hashes, ISO timestamps) are already
|
|
134
|
+
* schema-constrained to charsets that cannot contain a quote; reach for this
|
|
135
|
+
* whenever a query must interpolate genuinely free-form user input (e.g. the
|
|
136
|
+
* token `currency` filter).
|
|
137
|
+
*/
|
|
138
|
+
export function escape(value: string) {
|
|
139
|
+
return value.replaceAll("'", "''")
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Wraps a TIDX client so `fetch` retries transient errors under **one**
|
|
144
|
+
* shared budget ({@link transientRetryCount} retries with backoff).
|
|
145
|
+
*
|
|
146
|
+
* Two transient failure modes are covered: HTTP-level failures (408/429/5xx)
|
|
147
|
+
* and the indexer's transient ClickHouse errors, which arrive as **HTTP 200**
|
|
148
|
+
* with an `{ ok: false, error: 'db error' }` body (a `FetchRequestError`
|
|
149
|
+
* whose `status` is 200). The raw `tidx.ts` client would retry the HTTP-level
|
|
150
|
+
* mode itself (5 attempts), which stacked multiplicatively under this wrapper
|
|
151
|
+
* — one flapping query shape could cost ~25 upstream executions — so the
|
|
152
|
+
* wrapper disables the client-level retries (`retryCount: 1`) and owns the
|
|
153
|
+
* whole budget: at most {@link transientRetryCount} + 1 upstream executions.
|
|
154
|
+
* Deterministic errors (e.g. a malformed query) fall through immediately.
|
|
155
|
+
*/
|
|
156
|
+
export function withTransientRetry(client: Client): Client {
|
|
157
|
+
const fetch = client.fetch.bind(client)
|
|
158
|
+
return {
|
|
159
|
+
...client,
|
|
160
|
+
fetch: async (options) => {
|
|
161
|
+
let lastError: unknown
|
|
162
|
+
for (let attempt = 0; attempt <= transientRetryCount; attempt++) {
|
|
163
|
+
try {
|
|
164
|
+
// `retryCount: 1` caps the inner client at a single HTTP attempt so
|
|
165
|
+
// the combined budget stays this wrapper's alone.
|
|
166
|
+
return await fetch({ ...options, retryCount: 1 })
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (!isTransientError(error)) throw error
|
|
169
|
+
lastError = error
|
|
170
|
+
if (attempt < transientRetryCount)
|
|
171
|
+
await new Promise((resolve) => setTimeout(resolve, transientRetryDelay * 2 ** attempt))
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
throw lastError
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* True for transient indexer errors: an HTTP-level transient failure
|
|
181
|
+
* (408/429/5xx), or a `FetchRequestError` that came back on a **2xx** HTTP
|
|
182
|
+
* response whose message matches a known transient failure.
|
|
183
|
+
*
|
|
184
|
+
* The 2xx gate on the message pattern matters. The indexer reports two
|
|
185
|
+
* different `db error`s: a transient ClickHouse hiccup arrives on an HTTP 200
|
|
186
|
+
* with `{ ok: false, error: 'db error' }` (worth retrying), whereas an HTTP
|
|
187
|
+
* 422 `db error` means the planner cannot execute that query shape at all — a
|
|
188
|
+
* deterministic failure that will never succeed on retry. Retrying the latter
|
|
189
|
+
* would just burn the backoff budget before failing anyway, so 4xx statuses
|
|
190
|
+
* other than 408/429 are excluded.
|
|
191
|
+
*/
|
|
192
|
+
function isTransientError(error: unknown): boolean {
|
|
193
|
+
if (!(error instanceof Tidx.FetchRequestError)) return false
|
|
194
|
+
if (error.status === 408 || error.status === 429 || error.status >= 500) return true
|
|
195
|
+
return error.status >= 200 && error.status < 300 && transientErrorPattern.test(error.message)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export declare namespace getClient {
|
|
199
|
+
/** Options for getting a TIDX query client. */
|
|
200
|
+
type Options = {
|
|
201
|
+
/** Tempo chain id. */
|
|
202
|
+
chainId?: Viem.ChainId | undefined
|
|
203
|
+
/** Request principal, or `null`/omitted for a trusted non-request caller. */
|
|
204
|
+
principal?: Auth.Principal | null | undefined
|
|
205
|
+
/** TIDX query client options. */
|
|
206
|
+
tidx?: Tidx | undefined
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* TIDX query client options: either a static config, or a per-request
|
|
211
|
+
* resolver function that receives a {@link Viem.resolve.Context} and returns
|
|
212
|
+
* the config to use (e.g. routing anonymous callers to a public, no-auth
|
|
213
|
+
* indexer).
|
|
214
|
+
*/
|
|
215
|
+
type Tidx =
|
|
216
|
+
| {
|
|
217
|
+
/** Resolves the TIDX base URL for a Tempo chain id, or a static base URL. */
|
|
218
|
+
baseUrl?: UrlResolver | string | undefined
|
|
219
|
+
/** Basic auth credentials for TIDX requests. */
|
|
220
|
+
basicAuth?: string | undefined
|
|
221
|
+
}
|
|
222
|
+
| ((context: Viem.resolve.Context) => {
|
|
223
|
+
/** TIDX base URL to use; falls back to the built-in public host when omitted. */
|
|
224
|
+
baseUrl?: string | undefined
|
|
225
|
+
/** Basic auth credentials for TIDX requests, or undefined for none. */
|
|
226
|
+
basicAuth?: string | undefined
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export declare namespace createGetClient {
|
|
231
|
+
/** Options for building a memoized TIDX client resolver. */
|
|
232
|
+
type Options = {
|
|
233
|
+
/** Tempo chain id used when a caller omits one. */
|
|
234
|
+
defaultChainId?: Viem.ChainId | undefined
|
|
235
|
+
/** TIDX query client options applied to every constructed client. */
|
|
236
|
+
tidx?: getClient.Tidx | undefined
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Hono } from 'hono'
|
|
2
|
+
import * as Timing from './Timing.js'
|
|
3
|
+
|
|
4
|
+
describe('middleware', () => {
|
|
5
|
+
test('emits request and operation server timings', async () => {
|
|
6
|
+
const app = new Hono<{ Variables: Timing.Variables }>()
|
|
7
|
+
|
|
8
|
+
app.use('*', Timing.middleware())
|
|
9
|
+
app.get('/', async (c) => {
|
|
10
|
+
await Timing.time(c, 'example.work', () => Promise.resolve())
|
|
11
|
+
return c.text('ok')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const response = await app.request('/')
|
|
15
|
+
const timing = response.headers.get('server-timing')
|
|
16
|
+
|
|
17
|
+
expect(response.status).toMatchInlineSnapshot(`200`)
|
|
18
|
+
expect(response.headers.get('content-type')).toMatchInlineSnapshot(`"text/plain;charset=UTF-8"`)
|
|
19
|
+
expect(timing?.includes('example.work;dur=')).toMatchInlineSnapshot(`true`)
|
|
20
|
+
expect(timing?.includes('request;dur=')).toMatchInlineSnapshot(`true`)
|
|
21
|
+
})
|
|
22
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Context, MiddlewareHandler } from 'hono'
|
|
2
|
+
|
|
3
|
+
/** Hono variables used to collect Server-Timing metrics. */
|
|
4
|
+
export type Variables = {
|
|
5
|
+
/** Metrics emitted in the `Server-Timing` response header. */
|
|
6
|
+
serverTiming?: Metric[] | undefined
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Hono environment shape used by timing helpers. */
|
|
10
|
+
export type Environment = {
|
|
11
|
+
Variables: Variables
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** One Server-Timing metric. */
|
|
15
|
+
export type Metric = {
|
|
16
|
+
/** Metric duration in milliseconds. */
|
|
17
|
+
duration: number
|
|
18
|
+
/** Metric name. */
|
|
19
|
+
name: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Adds a `Server-Timing` header with total request duration. */
|
|
23
|
+
export function middleware<
|
|
24
|
+
environment extends Environment = Environment,
|
|
25
|
+
>(): MiddlewareHandler<environment> {
|
|
26
|
+
return async (c, next) => {
|
|
27
|
+
const metrics: Metric[] = []
|
|
28
|
+
c.set('serverTiming', metrics)
|
|
29
|
+
|
|
30
|
+
const start = performance.now()
|
|
31
|
+
await next()
|
|
32
|
+
metrics.push({ duration: performance.now() - start, name: 'request' })
|
|
33
|
+
|
|
34
|
+
const value = format(metrics)
|
|
35
|
+
const existing = c.res.headers.get('Server-Timing')
|
|
36
|
+
c.res.headers.set('Server-Timing', existing ? `${existing}, ${value}` : value)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Measures one async operation and adds it to the current request timings. */
|
|
41
|
+
export async function time<environment extends Environment, value>(
|
|
42
|
+
c: Context<environment>,
|
|
43
|
+
name: string,
|
|
44
|
+
fn: () => Promise<value> | value,
|
|
45
|
+
) {
|
|
46
|
+
const start = performance.now()
|
|
47
|
+
try {
|
|
48
|
+
return await fn()
|
|
49
|
+
} finally {
|
|
50
|
+
c.get('serverTiming')?.push({ duration: performance.now() - start, name })
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function format(metrics: readonly Metric[]) {
|
|
55
|
+
return metrics
|
|
56
|
+
.map((metric) => `${name(metric.name)};dur=${metric.duration.toFixed(1)}`)
|
|
57
|
+
.join(', ')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function name(value: string) {
|
|
61
|
+
return value.replace(/[^A-Za-z0-9_.*-]/g, '_')
|
|
62
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as Ttl from './Ttl.js'
|
|
2
|
+
|
|
3
|
+
describe('Ttl', () => {
|
|
4
|
+
test('converts common durations to milliseconds', () => {
|
|
5
|
+
expect(Ttl.millisecond(1)).toMatchInlineSnapshot(`1`)
|
|
6
|
+
expect(Ttl.milliseconds(2)).toMatchInlineSnapshot(`2`)
|
|
7
|
+
expect(Ttl.second(1)).toMatchInlineSnapshot(`1000`)
|
|
8
|
+
expect(Ttl.seconds(2)).toMatchInlineSnapshot(`2000`)
|
|
9
|
+
expect(Ttl.minute(1)).toMatchInlineSnapshot(`60000`)
|
|
10
|
+
expect(Ttl.minutes(2)).toMatchInlineSnapshot(`120000`)
|
|
11
|
+
expect(Ttl.hour(1)).toMatchInlineSnapshot(`3600000`)
|
|
12
|
+
expect(Ttl.hours(2)).toMatchInlineSnapshot(`7200000`)
|
|
13
|
+
expect(Ttl.day(1)).toMatchInlineSnapshot(`86400000`)
|
|
14
|
+
expect(Ttl.days(2)).toMatchInlineSnapshot(`172800000`)
|
|
15
|
+
expect(Ttl.week(1)).toMatchInlineSnapshot(`604800000`)
|
|
16
|
+
expect(Ttl.weeks(2)).toMatchInlineSnapshot(`1209600000`)
|
|
17
|
+
})
|
|
18
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Returns milliseconds from milliseconds. */
|
|
2
|
+
export function milliseconds(value: number) {
|
|
3
|
+
return value
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** Returns milliseconds from seconds. */
|
|
7
|
+
export function seconds(value: number) {
|
|
8
|
+
return value * 1_000
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Returns milliseconds from minutes. */
|
|
12
|
+
export function minutes(value: number) {
|
|
13
|
+
return seconds(value) * 60
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Returns milliseconds from hours. */
|
|
17
|
+
export function hours(value: number) {
|
|
18
|
+
return minutes(value) * 60
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Returns milliseconds from days. */
|
|
22
|
+
export function days(value: number) {
|
|
23
|
+
return hours(value) * 24
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Returns milliseconds from weeks. */
|
|
27
|
+
export function weeks(value: number) {
|
|
28
|
+
return days(value) * 7
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Returns milliseconds from milliseconds. */
|
|
32
|
+
export function millisecond(value: number) {
|
|
33
|
+
return milliseconds(value)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Returns milliseconds from seconds. */
|
|
37
|
+
export function second(value: number) {
|
|
38
|
+
return seconds(value)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Returns milliseconds from minutes. */
|
|
42
|
+
export function minute(value: number) {
|
|
43
|
+
return minutes(value)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Returns milliseconds from hours. */
|
|
47
|
+
export function hour(value: number) {
|
|
48
|
+
return hours(value)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Returns milliseconds from days. */
|
|
52
|
+
export function day(value: number) {
|
|
53
|
+
return days(value)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Returns milliseconds from weeks. */
|
|
57
|
+
export function week(value: number) {
|
|
58
|
+
return weeks(value)
|
|
59
|
+
}
|