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,2112 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { parseAbiItem } from 'viem';
|
|
3
|
+
import * as z from 'zod/mini';
|
|
4
|
+
import * as Auth from '../internal/Auth.js';
|
|
5
|
+
import * as Cache from '../internal/Cache.js';
|
|
6
|
+
import * as Cursor from '../internal/Cursor.js';
|
|
7
|
+
import * as OpenApi from '../internal/OpenApi.js';
|
|
8
|
+
import * as Response from '../internal/Response.js';
|
|
9
|
+
import * as Schema from '../internal/Schema.js';
|
|
10
|
+
import * as Store from '../internal/Store.js';
|
|
11
|
+
import * as Ttl from '../internal/Ttl.js';
|
|
12
|
+
import * as Value from '../internal/Value.js';
|
|
13
|
+
import * as Webhooks from '../internal/Webhooks.js';
|
|
14
|
+
import * as Receipts from './receipts.js';
|
|
15
|
+
import * as Transactions from './transactions.js';
|
|
16
|
+
import * as Transfers from './transfers.js';
|
|
17
|
+
/** Default cap on active subscriptions per owner, overridable via `webhook.maxPerOwner`. */
|
|
18
|
+
const defaultMaxPerOwner = 100;
|
|
19
|
+
/**
|
|
20
|
+
* Lifetime of an MPP-payer-owned subscription. A payer pays once, so the
|
|
21
|
+
* subscription is TTL-bounded (renewable by another paid create) to bound the
|
|
22
|
+
* paid-once / deliver-forever mismatch. API-key-owned subscriptions have no TTL.
|
|
23
|
+
*/
|
|
24
|
+
const mppTtlMs = 30 * 24 * 60 * 60 * 1_000;
|
|
25
|
+
/**
|
|
26
|
+
* The subscribable webhook event types, in alphabetical order. The source of
|
|
27
|
+
* truth for the event-type *set*: the {@link EventType} union and the route zod
|
|
28
|
+
* enum ({@link schema.EventType}) both derive from this.
|
|
29
|
+
*/
|
|
30
|
+
export const eventTypes = [
|
|
31
|
+
'block:created',
|
|
32
|
+
'log:emitted',
|
|
33
|
+
'token:transfer',
|
|
34
|
+
'transaction:included',
|
|
35
|
+
];
|
|
36
|
+
/** Subscribable event types surfaced by `GET /webhooks/event-types`. */
|
|
37
|
+
const eventTypeDescriptions = [
|
|
38
|
+
{
|
|
39
|
+
description: 'A new block was added to the chain. Filter by producer, gas usage, or block number, or omit filters for a per-block heartbeat.',
|
|
40
|
+
type: 'block:created',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
description: 'Any contract event log, filtered by emitting address, event signature/topics, or decoded arguments.',
|
|
44
|
+
type: 'log:emitted',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
description: 'TIP-20 token transfer event, such as a stablecoin payment sent or received.',
|
|
48
|
+
type: 'token:transfer',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
description: 'A transaction included in a block, filterable on every transaction field (sender, recipient, value, calldata, inner account-abstraction calls, fees, nonce) and its execution status.',
|
|
52
|
+
type: 'transaction:included',
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
/** Zod schemas owned by the webhooks resource. */
|
|
56
|
+
export var schema;
|
|
57
|
+
(function (schema) {
|
|
58
|
+
/** Subscribable event type. */
|
|
59
|
+
schema.EventType = z.enum(eventTypes).check(z.describe('Event type you can subscribe to.'));
|
|
60
|
+
/** Subscription lifecycle status. */
|
|
61
|
+
schema.Status = z
|
|
62
|
+
.enum(['active', 'disabled', 'paused'])
|
|
63
|
+
.check(z.describe('Current state of the webhook subscription.'), z.meta({ examples: ['active'] }));
|
|
64
|
+
const Filters = z.record(z.string(), z.unknown());
|
|
65
|
+
const chainIdResponse = z
|
|
66
|
+
.number()
|
|
67
|
+
.check(z.int(), z.positive(), z.describe('Tempo chain ID for this subscription.'), z.meta({ examples: [4217] }));
|
|
68
|
+
const Url = z
|
|
69
|
+
.url()
|
|
70
|
+
.check(z.describe('Your HTTPS endpoint where Tempo sends signed event POSTs.'), z.meta({ examples: ['https://example.com/webhooks'] }));
|
|
71
|
+
const SlackUrl = z
|
|
72
|
+
.url()
|
|
73
|
+
.check(z.describe('Your Slack incoming-webhook URL (`https://hooks.slack.com/services/…`).'), z.meta({ examples: ['https://hooks.slack.com/services/T000/B000/XXXXXXXX'] }));
|
|
74
|
+
const BetterstackUrl = z
|
|
75
|
+
.url()
|
|
76
|
+
.check(z.describe('Your Better Stack source ingest host URL (`https://<host>.betterstackdata.com`).'), z.meta({ examples: ['https://s1234567.eu-nbg-2.betterstackdata.com'] }));
|
|
77
|
+
const BetterstackToken = z
|
|
78
|
+
.string()
|
|
79
|
+
.check(z.minLength(1), z.describe('Your Better Stack source token (sent as `Authorization: Bearer …`).'), z.meta({ examples: ['FczKcxEhjEDE58dBX7XaeX1q'] }));
|
|
80
|
+
/**
|
|
81
|
+
* Where matched events are delivered: a `url` destination (a signed event POST
|
|
82
|
+
* to your HTTPS endpoint), a `slack` destination (a formatted message posted to
|
|
83
|
+
* your Slack incoming-webhook URL), or a `betterstack` destination (a structured
|
|
84
|
+
* log event POSTed to a Better Stack source with a bearer source token).
|
|
85
|
+
*/
|
|
86
|
+
const Destination = z
|
|
87
|
+
.discriminatedUnion('type', [
|
|
88
|
+
z.object({
|
|
89
|
+
type: z.literal('url'),
|
|
90
|
+
url: Url,
|
|
91
|
+
}),
|
|
92
|
+
z.object({
|
|
93
|
+
type: z.literal('slack'),
|
|
94
|
+
url: SlackUrl,
|
|
95
|
+
}),
|
|
96
|
+
z.object({
|
|
97
|
+
token: BetterstackToken,
|
|
98
|
+
type: z.literal('betterstack'),
|
|
99
|
+
url: BetterstackUrl,
|
|
100
|
+
}),
|
|
101
|
+
])
|
|
102
|
+
.check(z.describe('Delivery destination: an HTTPS URL, a Slack incoming-webhook URL, or a Better Stack source.'));
|
|
103
|
+
/**
|
|
104
|
+
* Equality / membership operator for a filterable field. A consumer can pass a
|
|
105
|
+
* bare value (shorthand for exact equality, the common case) or an operator
|
|
106
|
+
* object: `{ eq }` exact, `{ in: [...] }` OR-list, `{ not }` negation. Used for
|
|
107
|
+
* addresses, hashes, byte strings, enums, and raw topics. `in` lists are capped
|
|
108
|
+
* at 64 members so a single filter can't fan out into an unbounded SQL `IN`.
|
|
109
|
+
*
|
|
110
|
+
* @see {@link matchesOperator} for how it is evaluated.
|
|
111
|
+
*/
|
|
112
|
+
schema.eq = (item) => z.union([
|
|
113
|
+
item,
|
|
114
|
+
z.strictObject({ eq: item }),
|
|
115
|
+
z.strictObject({ in: z.array(item).check(z.minLength(1), z.maxLength(64)) }),
|
|
116
|
+
z.strictObject({ not: item }),
|
|
117
|
+
]);
|
|
118
|
+
/**
|
|
119
|
+
* Ordered-comparison operator for a numeric field (`value`, gas/fee fields,
|
|
120
|
+
* `nonce`, `blockNumber`, timestamps). A bare value is exact equality; the
|
|
121
|
+
* object form AND-combines any of `eq`/`gt`/`gte`/`lt`/`lte` to express ranges.
|
|
122
|
+
* Values are `0x`-hex quantities (the wire format), compiled to decimal in SQL.
|
|
123
|
+
*/
|
|
124
|
+
schema.compare = z.union([
|
|
125
|
+
Schema.Quantity,
|
|
126
|
+
z
|
|
127
|
+
.strictObject({
|
|
128
|
+
eq: z.optional(Schema.Quantity),
|
|
129
|
+
gt: z.optional(Schema.Quantity),
|
|
130
|
+
gte: z.optional(Schema.Quantity),
|
|
131
|
+
lt: z.optional(Schema.Quantity),
|
|
132
|
+
lte: z.optional(Schema.Quantity),
|
|
133
|
+
})
|
|
134
|
+
.check(z.refine((c) => Object.keys(c).length > 0, { error: 'Comparison cannot be empty.' })),
|
|
135
|
+
]);
|
|
136
|
+
/**
|
|
137
|
+
* Byte/calldata-matching operator for hex fields (tx `input`, `calls[].input`).
|
|
138
|
+
* `{ selector }` matches the first 4 bytes (the function selector, the cheap
|
|
139
|
+
* common case), `{ startsWith }` a hex prefix, `{ eq }` exact calldata.
|
|
140
|
+
*/
|
|
141
|
+
schema.bytes = z.union([
|
|
142
|
+
z.strictObject({ selector: Schema.Hex }),
|
|
143
|
+
z.strictObject({ startsWith: Schema.Hex }),
|
|
144
|
+
z.strictObject({ eq: Schema.Hex }),
|
|
145
|
+
]);
|
|
146
|
+
/**
|
|
147
|
+
* `transfer` subscription filters. The predicate fields are shared verbatim
|
|
148
|
+
* with `GET /transfers` via `Transfers.schema.Predicates`, so the two can't
|
|
149
|
+
* drift. All are optional and AND-combined; omit every field to subscribe to
|
|
150
|
+
* the full stream.
|
|
151
|
+
*/
|
|
152
|
+
schema.TransferFilters = Transfers.schema.Predicates.check(z.describe('Filters that narrow which TIP-20 transfer events trigger this webhook.'), z.meta({ title: 'Transfer filters' }));
|
|
153
|
+
/**
|
|
154
|
+
* `transaction:included` subscription filters: operators over every column the
|
|
155
|
+
* `txs` envelope exposes, plus inner account-abstraction `calls`, the
|
|
156
|
+
* top-level `input` calldata, and the receipt-sourced execution `status`. All
|
|
157
|
+
* optional and AND-combined; omit every field for the full stream.
|
|
158
|
+
*
|
|
159
|
+
* `address` matches either side (`from` OR `to`); when `includeCalls` is set
|
|
160
|
+
* (default), `to`/`address` also match an inner call target (`calls[].to`).
|
|
161
|
+
* Set `includeReceipt` to embed the receipt in the delivered payload.
|
|
162
|
+
*/
|
|
163
|
+
schema.TxFilters = z
|
|
164
|
+
.strictObject({
|
|
165
|
+
hash: z.optional(schema.eq(Schema.Hash).check(z.describe('Match this exact transaction hash (or any of several).'))),
|
|
166
|
+
address: z.optional(schema.eq(Schema.Address).check(z.describe('Match transactions where this address is the sender or recipient.'))),
|
|
167
|
+
from: z.optional(schema.eq(Schema.Address).check(z.describe('Match transactions whose sender (`from`) is this.'))),
|
|
168
|
+
to: z.optional(schema.eq(Schema.Address).check(z.describe('Match transactions sent to this address (root `to`; see `includeCalls`).'))),
|
|
169
|
+
includeCalls: z.optional(z
|
|
170
|
+
.boolean()
|
|
171
|
+
.check(z.describe('When true (default), `to`/`address` also match an inner account-abstraction call target (`calls[].to`).'), z.meta({ examples: [true] }))),
|
|
172
|
+
includeReceipt: z.optional(z
|
|
173
|
+
.boolean()
|
|
174
|
+
.check(z.describe('When true, embed the transaction receipt (execution `status`, `gasUsed`, `logs`, …) in the delivered payload under `meta.receipt`. Default false (the receipt is an extra fetch). Mirrors the read endpoint\u2019s `include=receipt`.'), z.meta({ examples: [true] }))),
|
|
175
|
+
value: z.optional(schema.compare.check(z.describe('Match the transaction value (wei) bounds.'))),
|
|
176
|
+
input: z.optional(schema.bytes.check(z.describe('Match top-level calldata by 4-byte `selector`, hex `startsWith`, or exact.'))),
|
|
177
|
+
calls: z.optional(z
|
|
178
|
+
.strictObject({
|
|
179
|
+
to: schema.eq(Schema.Address),
|
|
180
|
+
})
|
|
181
|
+
.check(z.describe('Match if ANY inner account-abstraction call targets this address (`calls[].to`).'))),
|
|
182
|
+
callCount: z.optional(schema.compare.check(z.describe('Match the number of inner calls (`call_count`).'))),
|
|
183
|
+
txType: z.optional(schema.eq(Schema.Hex).check(z.describe('Match the transaction type (e.g. `0x76` for Tempo AA).'))),
|
|
184
|
+
feeToken: z.optional(schema.eq(Schema.TokenAddress).check(z.describe('Match the fee token address.'))),
|
|
185
|
+
feePayer: z.optional(schema.eq(Schema.Address).check(z.describe('Match the fee payer address.'))),
|
|
186
|
+
gasLimit: z.optional(schema.compare.check(z.describe('Match the gas limit bounds.'))),
|
|
187
|
+
gasUsed: z.optional(schema.compare.check(z.describe('Match the gas used bounds.'))),
|
|
188
|
+
maxFeePerGas: z.optional(schema.compare.check(z.describe('Match the max fee per gas bounds.'))),
|
|
189
|
+
maxPriorityFeePerGas: z.optional(schema.compare.check(z.describe('Match the max priority fee per gas bounds.'))),
|
|
190
|
+
nonce: z.optional(schema.compare.check(z.describe('Match the nonce bounds.'))),
|
|
191
|
+
nonceKey: z.optional(schema.eq(Schema.Hex).check(z.describe('Match the nonce key (Tempo AA).'))),
|
|
192
|
+
validBefore: z.optional(schema.compare.check(z.describe('Match the `validBefore` bounds (unix s).'))),
|
|
193
|
+
validAfter: z.optional(schema.compare.check(z.describe('Match the `validAfter` bounds (unix s).'))),
|
|
194
|
+
blockNumber: z.optional(schema.compare.check(z.describe('Match the block-number bounds.'))),
|
|
195
|
+
timestamp: z.optional(schema.compare.check(z.describe('Match the block timestamp bounds (unix s).'))),
|
|
196
|
+
status: z.optional(z
|
|
197
|
+
.enum(['success', 'reverted'])
|
|
198
|
+
.check(z.describe('Match the receipt execution status (routes through `receipts`).'), z.meta({ examples: ['success'] }))),
|
|
199
|
+
})
|
|
200
|
+
.check(z.describe('Filters that narrow which included transactions trigger this webhook.'), z.meta({ title: 'Included-transaction filters' }));
|
|
201
|
+
/** A 32-byte log topic (lowercased hex). */
|
|
202
|
+
const topic = Schema.Hash;
|
|
203
|
+
/**
|
|
204
|
+
* `log:emitted` subscription filters. Raw `topic0..3` map straight to the
|
|
205
|
+
* `logs` table; an optional human-readable ABI event `signature` is sugar that
|
|
206
|
+
* pins `topic0` and lets `args` match decoded (indexed **or** non-indexed)
|
|
207
|
+
* parameters by name. At least one predicate is required (no chain-wide
|
|
208
|
+
* firehose), and an addressless filter must still be selective — an event
|
|
209
|
+
* anchor (`signature`/`topic0`) plus at least one indexed topic or argument.
|
|
210
|
+
*/
|
|
211
|
+
schema.LogFilters = z
|
|
212
|
+
.strictObject({
|
|
213
|
+
address: z.optional(schema.eq(Schema.Address).check(z.describe('Only match logs emitted by this contract address (or any of several).'), z.meta({ examples: ['0x20c0000000000000000000008f5425160ebe5525'] }))),
|
|
214
|
+
signature: z.optional(z.string().check(z.describe('Human-readable ABI event signature used to derive `topic0` and decode `args`, e.g. `event Transfer(address indexed from, address indexed to, uint256 value)`.'), z.meta({
|
|
215
|
+
examples: ['event Transfer(address indexed from, address indexed to, uint256 value)'],
|
|
216
|
+
}))),
|
|
217
|
+
topic0: z.optional(schema.eq(topic).check(z.describe('Event signature hash (topic 0). Mutually exclusive with `signature`.'), z.meta({
|
|
218
|
+
examples: ['0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'],
|
|
219
|
+
}))),
|
|
220
|
+
topic1: z.optional(schema.eq(topic).check(z.describe('Raw indexed argument 1 (topic 1).'), z.meta({
|
|
221
|
+
examples: ['0x000000000000000000000000e7687128b0a808c2831ff94d4f7b2fb35c65af38'],
|
|
222
|
+
}))),
|
|
223
|
+
topic2: z.optional(schema.eq(topic).check(z.describe('Raw indexed argument 2 (topic 2).'), z.meta({
|
|
224
|
+
examples: ['0x0000000000000000000000009e39034aae71fb89f66061a2602eb6efec271754'],
|
|
225
|
+
}))),
|
|
226
|
+
topic3: z.optional(schema.eq(topic).check(z.describe('Raw indexed argument 3 (topic 3).'), z.meta({
|
|
227
|
+
examples: ['0x0000000000000000000000000000000000000000000000000000000000000000'],
|
|
228
|
+
}))),
|
|
229
|
+
args: z.optional(z
|
|
230
|
+
.record(z.string(), z.unknown())
|
|
231
|
+
.check(z.describe('Match decoded event arguments by name (indexed or non-indexed). Requires `signature`. Each value is an exact value or an operator object.'), z.meta({ examples: [{ to: '0x9e39034aae71fb89f66061a2602eb6efec271754' }] }))),
|
|
232
|
+
blockNumber: z.optional(schema.compare.check(z.describe('Only match logs within these block-number bounds.'))),
|
|
233
|
+
})
|
|
234
|
+
.check(z.describe('Filters that narrow which contract event logs trigger this webhook.'), z.meta({ title: 'Log filters' }), z.refine((f) => Object.keys(f).length > 0, {
|
|
235
|
+
error: 'At least one log filter is required.',
|
|
236
|
+
}), z.refine((f) => !(f.signature && f.topic0), {
|
|
237
|
+
error: 'Pass `signature` or `topic0`, not both.',
|
|
238
|
+
}), z.refine((f) => !f.args || f.signature, {
|
|
239
|
+
error: '`args` requires `signature` to decode the event.',
|
|
240
|
+
}), z.refine((f) => Boolean(f.address) ||
|
|
241
|
+
(Boolean(f.signature || f.topic0) && Boolean(f.topic1 || f.topic2 || f.topic3 || f.args)), {
|
|
242
|
+
error: 'Addressless log filters must include an event signature or topic0 plus at least one indexed topic or argument.',
|
|
243
|
+
}));
|
|
244
|
+
/**
|
|
245
|
+
* `block:created` subscription filters. All optional and AND-combined; unlike
|
|
246
|
+
* `log:emitted`, an empty filter is allowed — one row per block is a bounded,
|
|
247
|
+
* non-abusive heartbeat feed. Block-level metadata only (no transaction
|
|
248
|
+
* predicates; use `transaction:included` for those).
|
|
249
|
+
*/
|
|
250
|
+
schema.BlockFilters = z
|
|
251
|
+
.strictObject({
|
|
252
|
+
number: z.optional(schema.compare.check(z.describe('Only match blocks within these block-number bounds.'))),
|
|
253
|
+
miner: z.optional(schema.eq(Schema.Address).check(z.describe('Only match blocks produced by this address (the miner/producer).'), z.meta({ examples: ['0x0000000000000000000000000000000000000000'] }))),
|
|
254
|
+
proposer: z.optional(schema.eq(Schema.Address).check(z.describe('Only match blocks proposed by this consensus proposer (when available).'))),
|
|
255
|
+
gasUsed: z.optional(schema.compare.check(z.describe('Only match blocks whose total gas used is within these bounds.'))),
|
|
256
|
+
gasLimit: z.optional(schema.compare.check(z.describe('Only match blocks whose gas limit is within these bounds.'))),
|
|
257
|
+
timestamp: z.optional(schema.compare.check(z.describe('Only match blocks whose unix timestamp (seconds) is within these bounds.'))),
|
|
258
|
+
})
|
|
259
|
+
.check(z.describe('Filters that narrow which new blocks trigger this webhook.'), z.meta({ title: 'Block filters' }));
|
|
260
|
+
/**
|
|
261
|
+
* Optional human context describing what a subscription is for. Surfaced by
|
|
262
|
+
* destinations (Slack header + description, Better Stack log fields) and echoed
|
|
263
|
+
* on every delivered event envelope.
|
|
264
|
+
*/
|
|
265
|
+
schema.Context = z
|
|
266
|
+
.object({
|
|
267
|
+
description: z.optional(z
|
|
268
|
+
.string()
|
|
269
|
+
.check(z.maxLength(500), z.describe('Longer description of what this subscription is for.'), z.meta({ examples: ['Notify #ops when a large USDC transfer settles on mainnet.'] }))),
|
|
270
|
+
metadata: z.optional(z
|
|
271
|
+
.record(z.string().check(z.minLength(1), z.maxLength(64)), z.string().check(z.maxLength(500)))
|
|
272
|
+
.check(z.refine((m) => Object.keys(m).length <= 20, {
|
|
273
|
+
error: 'At most 20 metadata entries are allowed.',
|
|
274
|
+
}), z.describe('Arbitrary key/value labels echoed on every delivered event and rendered by destinations (Slack fields, Better Stack log fields). Keys are 1–64 chars, values ≤500 chars, ≤20 entries.'), z.meta({ examples: [{ env: 'prod', team: 'payments' }] }))),
|
|
275
|
+
title: z.optional(z
|
|
276
|
+
.string()
|
|
277
|
+
.check(z.maxLength(120), z.describe('Short label for this subscription.'), z.meta({ examples: ['Prod USDC large transfers'] }))),
|
|
278
|
+
})
|
|
279
|
+
.check(z.describe('Human context describing what this webhook subscription is for.'));
|
|
280
|
+
/**
|
|
281
|
+
* Public subscription representation. Never includes the signing `secret`
|
|
282
|
+
* (returned once by `POST /webhooks`) or the internal owner.
|
|
283
|
+
*/
|
|
284
|
+
schema.Subscription = z
|
|
285
|
+
.object({
|
|
286
|
+
chainId: chainIdResponse,
|
|
287
|
+
context: z.optional(schema.Context),
|
|
288
|
+
createdAt: z.iso
|
|
289
|
+
.datetime()
|
|
290
|
+
.check(z.describe('When the subscription was created, as an ISO 8601 timestamp.'), z.meta({ examples: ['2024-01-01T00:00:00Z'] })),
|
|
291
|
+
destination: Destination,
|
|
292
|
+
eventType: schema.EventType.check(z.meta({ examples: ['token:transfer'] })),
|
|
293
|
+
expiresAt: z
|
|
294
|
+
.optional(z.iso.datetime())
|
|
295
|
+
.check(z.describe('When the subscription expires, as an ISO 8601 timestamp. Only MPP-paid subscriptions expire.'), z.meta({ examples: ['2024-01-01T00:00:00Z'] })),
|
|
296
|
+
failureCount: z
|
|
297
|
+
.number()
|
|
298
|
+
.check(z.int(), z.nonnegative(), z.describe('Number of delivery failures in a row for this subscription.'), z.meta({ examples: [0] })),
|
|
299
|
+
filters: Filters.check(z.describe('Filters applied to this event type.'), z.meta({ examples: [{ address: '0x20c0000000000000000000008f5425160ebe5525' }] })),
|
|
300
|
+
id: z
|
|
301
|
+
.string()
|
|
302
|
+
.check(z.describe('Webhook subscription ID (`wh_…`).'), z.meta({ examples: ['wh_abc123'] })),
|
|
303
|
+
lastDeliveryAt: z
|
|
304
|
+
.optional(z.iso.datetime())
|
|
305
|
+
.check(z.describe('When Tempo last delivered an event successfully, as an ISO 8601 timestamp.'), z.meta({ examples: ['2024-01-01T00:00:00Z'] })),
|
|
306
|
+
status: schema.Status.check(z.meta({ examples: ['active'] })),
|
|
307
|
+
updatedAt: z.iso
|
|
308
|
+
.datetime()
|
|
309
|
+
.check(z.describe('When the subscription was last changed, as an ISO 8601 timestamp.'), z.meta({ examples: ['2024-01-01T00:00:00Z'] })),
|
|
310
|
+
})
|
|
311
|
+
.check(z.describe('A webhook subscription that tells Tempo which onchain events to send to your destination.'));
|
|
312
|
+
/** Outcome of a single delivery attempt. */
|
|
313
|
+
schema.DeliveryStatus = z
|
|
314
|
+
.enum(['failed', 'pending', 'succeeded'])
|
|
315
|
+
.check(z.describe('Outcome of a webhook delivery attempt.'), z.meta({ examples: ['succeeded'] }));
|
|
316
|
+
/** A logged delivery attempt for a subscription. */
|
|
317
|
+
schema.Delivery = z
|
|
318
|
+
.object({
|
|
319
|
+
attempt: z
|
|
320
|
+
.number()
|
|
321
|
+
.check(z.int(), z.positive(), z.describe('Retry attempt number for this delivery, starting at 1.'), z.meta({ examples: [1] })),
|
|
322
|
+
createdAt: z.iso
|
|
323
|
+
.datetime()
|
|
324
|
+
.check(z.describe('When this delivery attempt was created, as an ISO 8601 timestamp.'), z.meta({ examples: ['2024-01-01T00:00:00Z'] })),
|
|
325
|
+
error: z
|
|
326
|
+
.optional(z.string())
|
|
327
|
+
.check(z.describe('Why delivery failed, present when `status` is `failed`.'), z.meta({ examples: ['Connection timed out'] })),
|
|
328
|
+
eventId: z
|
|
329
|
+
.string()
|
|
330
|
+
.check(z.describe('Stable event ID (`evt_…`) you can use to dedupe webhook deliveries.'), z.meta({ examples: ['evt_abc123'] })),
|
|
331
|
+
id: z
|
|
332
|
+
.string()
|
|
333
|
+
.check(z.describe('Webhook delivery ID (`whd_…`).'), z.meta({ examples: ['whd_abc123'] })),
|
|
334
|
+
requestUrl: z
|
|
335
|
+
.string()
|
|
336
|
+
.check(z.describe('Your callback URL that Tempo attempted to deliver to.'), z.meta({ examples: ['https://example.com/webhooks'] })),
|
|
337
|
+
responseMs: z
|
|
338
|
+
.optional(z.number().check(z.int(), z.nonnegative()))
|
|
339
|
+
.check(z.describe('How long the delivery attempt took in milliseconds, when Tempo made a request.'), z.meta({ examples: [12] })),
|
|
340
|
+
responseStatus: z
|
|
341
|
+
.optional(z.number().check(z.int()))
|
|
342
|
+
.check(z.describe('HTTP status your endpoint returned, when Tempo received a response.'), z.meta({ examples: [200] })),
|
|
343
|
+
status: schema.DeliveryStatus.check(z.meta({ examples: ['succeeded'] })),
|
|
344
|
+
subscriptionId: z
|
|
345
|
+
.string()
|
|
346
|
+
.check(z.describe('Subscription ID (`wh_…`) this delivery belongs to.'), z.meta({ examples: ['wh_abc123'] })),
|
|
347
|
+
})
|
|
348
|
+
.check(z.describe('One attempt by Tempo to deliver an event to your webhook URL.'));
|
|
349
|
+
/**
|
|
350
|
+
* A `transfer` event payload: the decoded TIP-20 `Transfer` row. Mirrors a
|
|
351
|
+
* `GET /transfers` row but without the `sourceToken` metadata enrichment the
|
|
352
|
+
* read endpoint adds (webhook delivery does not perform per-event token
|
|
353
|
+
* lookups).
|
|
354
|
+
*/
|
|
355
|
+
schema.TransferEvent = z
|
|
356
|
+
.object({
|
|
357
|
+
address: Schema.TokenAddress.check(z.describe('TIP-20 token contract address.'), z.meta({ examples: ['0x20c0000000000000000000008f5425160ebe5525'] })),
|
|
358
|
+
amount: z
|
|
359
|
+
.string()
|
|
360
|
+
.check(z.describe('Amount transferred, as a decimal integer string in the token base unit.'), z.meta({ examples: ['10000'] })),
|
|
361
|
+
blockNumber: z
|
|
362
|
+
.number()
|
|
363
|
+
.check(z.int(), z.nonnegative(), z.describe('Block number where this transfer was included.'), z.meta({ examples: [1000002] })),
|
|
364
|
+
recipient: Schema.Address.check(z.describe('Address that received the transfer.'), z.meta({ examples: ['0x9e39034aae71fb89f66061a2602eb6efec271754'] })),
|
|
365
|
+
sender: Schema.Address.check(z.describe('Address that sent the transfer.'), z.meta({ examples: ['0xe7687128b0a808c2831ff94d4f7b2fb35c65af38'] })),
|
|
366
|
+
timestamp: z.iso
|
|
367
|
+
.datetime()
|
|
368
|
+
.check(z.describe('Block timestamp as an ISO 8601 string.'), z.meta({ examples: ['2026-01-14T18:38:03.000Z'] })),
|
|
369
|
+
token: z.optional(z
|
|
370
|
+
.object({
|
|
371
|
+
decimals: z
|
|
372
|
+
.number()
|
|
373
|
+
.check(z.int(), z.nonnegative(), z.describe('Number of decimals the token uses.'), z.meta({ examples: [6] })),
|
|
374
|
+
symbol: z
|
|
375
|
+
.string()
|
|
376
|
+
.check(z.describe('Token symbol (e.g. `USDC`).'), z.meta({ examples: ['USDC'] })),
|
|
377
|
+
})
|
|
378
|
+
.check(z.describe('Best-effort token metadata (symbol/decimals) resolved by the poller so `amount` can render as a human amount. Omitted when the lookup is unavailable.'))),
|
|
379
|
+
transactionHash: Schema.Hash.check(z.describe('Transaction hash that contains this transfer.'), z.meta({
|
|
380
|
+
examples: ['0x3d24a706cc2f6f4c96620bef1f61ddb23040ff77c22c8db42918c7c424bbf9d3'],
|
|
381
|
+
})),
|
|
382
|
+
})
|
|
383
|
+
.check(z.describe('Payload for a `token:transfer` event, decoded from a TIP-20 `Transfer` log.'));
|
|
384
|
+
/**
|
|
385
|
+
* A `transaction` event payload: identical to the `GET /transactions/:hash`
|
|
386
|
+
* response (the poller reuses the same humanized shape).
|
|
387
|
+
*/
|
|
388
|
+
schema.TransactionEvent = Transactions.schema.getTransaction.Response;
|
|
389
|
+
/**
|
|
390
|
+
* A `log:emitted` event payload: the raw contract event log. When a
|
|
391
|
+
* `signature` filter was supplied and decoding succeeds, the best-effort
|
|
392
|
+
* `event`/`args` fields carry the decoded event name and arguments (large
|
|
393
|
+
* integers as decimal strings).
|
|
394
|
+
*/
|
|
395
|
+
schema.LogEvent = z
|
|
396
|
+
.object({
|
|
397
|
+
address: Schema.Address.check(z.describe('Contract that emitted the log.'), z.meta({ examples: ['0x20c00000000000000000000071de0cd31ab0d105'] })),
|
|
398
|
+
args: z.optional(z
|
|
399
|
+
.record(z.string(), z.unknown())
|
|
400
|
+
.check(z.describe('Decoded event arguments, present only when a `signature` filter was supplied and decoding succeeded.'), z.meta({ examples: [{ from: '0xe768…', to: '0x9e39…', value: '10000' }] }))),
|
|
401
|
+
blockNumber: z
|
|
402
|
+
.number()
|
|
403
|
+
.check(z.int(), z.nonnegative(), z.describe('Block number the log was included in.'), z.meta({ examples: [1000002] })),
|
|
404
|
+
data: Schema.Hex.check(z.describe('Unindexed log data (ABI-encoded).'), z.meta({
|
|
405
|
+
examples: ['0x0000000000000000000000000000000000000000000000000000000000002710'],
|
|
406
|
+
})),
|
|
407
|
+
event: z.optional(z
|
|
408
|
+
.object({
|
|
409
|
+
name: z
|
|
410
|
+
.string()
|
|
411
|
+
.check(z.describe('Decoded event name.'), z.meta({ examples: ['Transfer'] })),
|
|
412
|
+
signature: z.string().check(z.describe('Human-readable event signature used to decode the log.'), z.meta({
|
|
413
|
+
examples: [
|
|
414
|
+
'event Transfer(address indexed from, address indexed to, uint256 value)',
|
|
415
|
+
],
|
|
416
|
+
})),
|
|
417
|
+
topic0: Schema.Hash.check(z.describe('Event signature hash (topic 0).'), z.meta({
|
|
418
|
+
examples: ['0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'],
|
|
419
|
+
})),
|
|
420
|
+
})
|
|
421
|
+
.check(z.describe('Decoded event identity, present only when a `signature` filter was supplied.'))),
|
|
422
|
+
logIndex: z
|
|
423
|
+
.number()
|
|
424
|
+
.check(z.int(), z.nonnegative(), z.describe('Index of the log within its block.'), z.meta({ examples: [0] })),
|
|
425
|
+
timestamp: z.iso
|
|
426
|
+
.datetime()
|
|
427
|
+
.check(z.describe('When the block was produced, as an ISO 8601 timestamp.'), z.meta({ examples: ['2026-01-14T18:38:03.000Z'] })),
|
|
428
|
+
topics: z.array(Schema.Hash).check(z.describe('Log topics, starting with the event signature hash (topic 0).'), z.meta({
|
|
429
|
+
examples: [
|
|
430
|
+
[
|
|
431
|
+
'0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
|
|
432
|
+
'0x000000000000000000000000e7687128b0a808c2831ff94d4f7b2fb35c65af38',
|
|
433
|
+
],
|
|
434
|
+
],
|
|
435
|
+
})),
|
|
436
|
+
transactionHash: Schema.Hash.check(z.describe('Hash of the transaction that emitted the log.'), z.meta({
|
|
437
|
+
examples: ['0x3d24a706cc2f6f4c96620bef1f61ddb23040ff77c22c8db42918c7c424bbf9d3'],
|
|
438
|
+
})),
|
|
439
|
+
transactionIndex: z
|
|
440
|
+
.number()
|
|
441
|
+
.check(z.int(), z.nonnegative(), z.describe('Index of the transaction within its block.'), z.meta({ examples: [0] })),
|
|
442
|
+
})
|
|
443
|
+
.check(z.describe('Payload for a `log:emitted` event (a raw contract event log).'));
|
|
444
|
+
/** Payload for a `block:created` event (a TIDX-derived block header). */
|
|
445
|
+
schema.BlockEvent = z
|
|
446
|
+
.object({
|
|
447
|
+
number: z
|
|
448
|
+
.number()
|
|
449
|
+
.check(z.int(), z.nonnegative(), z.describe('Block height, starting from genesis block 0.'), z.meta({ examples: [1000002] })),
|
|
450
|
+
hash: Schema.Hash.check(z.describe('Hash that identifies this block.'), z.meta({
|
|
451
|
+
examples: ['0x3fe7d9e595f3d8215ec840c6ef55ac0bead58933e96fc2ead0f2aaf79715f45d'],
|
|
452
|
+
})),
|
|
453
|
+
parentHash: Schema.Hash.check(z.describe('Hash of the previous block in the chain.'), z.meta({
|
|
454
|
+
examples: ['0xa406dff9c51cc2abbef3a811f0499e99a568bf781019f129ab45dca1df0e43e5'],
|
|
455
|
+
})),
|
|
456
|
+
miner: Schema.Address.check(z.describe('Address of the block producer (miner/proposer).'), z.meta({ examples: ['0x0000000000000000000000000000000000000000'] })),
|
|
457
|
+
proposer: z.optional(Schema.Address.check(z.describe('Consensus proposer of the block, when the chain records one.'), z.meta({ examples: ['0x0000000000000000000000000000000000000000'] }))),
|
|
458
|
+
gasUsed: z
|
|
459
|
+
.number()
|
|
460
|
+
.check(z.int(), z.nonnegative(), z.describe('Total gas used by every transaction in this block.'), z.meta({ examples: [4340281] })),
|
|
461
|
+
gasLimit: z
|
|
462
|
+
.number()
|
|
463
|
+
.check(z.int(), z.nonnegative(), z.describe('Maximum gas available for all transactions in this block.'), z.meta({ examples: [500000000] })),
|
|
464
|
+
transactionCount: z
|
|
465
|
+
.number()
|
|
466
|
+
.check(z.int(), z.nonnegative(), z.describe('Number of transactions included in this block.'), z.meta({ examples: [3] })),
|
|
467
|
+
timestamp: z.iso
|
|
468
|
+
.datetime()
|
|
469
|
+
.check(z.describe('When the block was produced, as an ISO 8601 timestamp.'), z.meta({ examples: ['2026-01-14T18:38:03.000Z'] })),
|
|
470
|
+
})
|
|
471
|
+
.check(z.describe('Payload for a `block:created` event (a new block header).'));
|
|
472
|
+
/** A `ping` (synthetic test-delivery) payload, sent by `POST /webhooks/:id/ping`. */
|
|
473
|
+
schema.PingEvent = z
|
|
474
|
+
.object({
|
|
475
|
+
ping: z.literal(true).check(z.describe('Marks this payload as a synthetic ping test.')),
|
|
476
|
+
})
|
|
477
|
+
.check(z.describe('Payload for a webhook ping test delivery.'));
|
|
478
|
+
/** Fields common to every delivery envelope. */
|
|
479
|
+
const envelopeBase = {
|
|
480
|
+
chainId: Schema.ChainId,
|
|
481
|
+
context: z.optional(schema.Context),
|
|
482
|
+
createdAt: z.iso
|
|
483
|
+
.datetime()
|
|
484
|
+
.check(z.describe('When Tempo built this delivery envelope, as an ISO 8601 timestamp.'), z.meta({ examples: ['2026-01-14T18:38:03.000Z'] })),
|
|
485
|
+
id: z
|
|
486
|
+
.string()
|
|
487
|
+
.check(z.describe('Stable event ID (`evt_…`) for this delivery. Store it and ignore duplicates.'), z.meta({ examples: ['evt_abc123'] })),
|
|
488
|
+
subscriptionId: z
|
|
489
|
+
.string()
|
|
490
|
+
.check(z.describe('Subscription ID (`wh_…`) that produced this delivery.'), z.meta({ examples: ['wh_abc123'] })),
|
|
491
|
+
};
|
|
492
|
+
/**
|
|
493
|
+
* The signed JSON body POSTed to subscriber URLs, discriminated by `type`. The
|
|
494
|
+
* `tempo-event-type` header mirrors `type`. Event variants carry the matched
|
|
495
|
+
* event row; `ping` is a synthetic test delivery.
|
|
496
|
+
*
|
|
497
|
+
* Each variant carries a `title` so doc renderers (Scalar) show a labelled
|
|
498
|
+
* variant selector — without per-variant titles, inline `oneOf` members render
|
|
499
|
+
* as a single (first) collapsed schema.
|
|
500
|
+
*/
|
|
501
|
+
schema.Envelope = z
|
|
502
|
+
.discriminatedUnion('type', [
|
|
503
|
+
z
|
|
504
|
+
.object({ ...envelopeBase, data: schema.TransferEvent, type: z.literal('token:transfer') })
|
|
505
|
+
.check(z.meta({ title: 'Transfer event' })),
|
|
506
|
+
z
|
|
507
|
+
.object({
|
|
508
|
+
...envelopeBase,
|
|
509
|
+
data: schema.TransactionEvent,
|
|
510
|
+
type: z.literal('transaction:included'),
|
|
511
|
+
})
|
|
512
|
+
.check(z.meta({ title: 'Included transaction event' })),
|
|
513
|
+
z
|
|
514
|
+
.object({ ...envelopeBase, data: schema.LogEvent, type: z.literal('log:emitted') })
|
|
515
|
+
.check(z.meta({ title: 'Log event' })),
|
|
516
|
+
z
|
|
517
|
+
.object({ ...envelopeBase, data: schema.BlockEvent, type: z.literal('block:created') })
|
|
518
|
+
.check(z.meta({ title: 'Block event' })),
|
|
519
|
+
z
|
|
520
|
+
.object({ ...envelopeBase, data: schema.PingEvent, type: z.literal('ping') })
|
|
521
|
+
.check(z.meta({ title: 'Ping (test delivery)' })),
|
|
522
|
+
])
|
|
523
|
+
.check(z.describe('The signed JSON payload Tempo POSTs to your webhook URL.'));
|
|
524
|
+
/** Schemas for the getWebhookEventTypes operation. */
|
|
525
|
+
let getWebhookEventTypes;
|
|
526
|
+
(function (getWebhookEventTypes) {
|
|
527
|
+
getWebhookEventTypes.Response = z
|
|
528
|
+
.object({
|
|
529
|
+
data: z
|
|
530
|
+
.array(z.object({
|
|
531
|
+
description: z
|
|
532
|
+
.string()
|
|
533
|
+
.check(z.describe('Plain-English description of the event type.'), z.meta({ examples: ['TIP-20 token transfer event.'] })),
|
|
534
|
+
type: schema.EventType.check(z.meta({ examples: ['token:transfer'] })),
|
|
535
|
+
}))
|
|
536
|
+
.check(z.describe('Webhook event types available for subscription.')),
|
|
537
|
+
})
|
|
538
|
+
.check(z.describe('List of webhook event types Tempo can send.'));
|
|
539
|
+
})(getWebhookEventTypes = schema.getWebhookEventTypes || (schema.getWebhookEventTypes = {}));
|
|
540
|
+
/** Schemas for the createWebhook operation. */
|
|
541
|
+
let createWebhook;
|
|
542
|
+
(function (createWebhook) {
|
|
543
|
+
/**
|
|
544
|
+
* Discriminated on `eventType` so each variant carries its own typed
|
|
545
|
+
* `filters` object — this is what lets the docs render the valid filter
|
|
546
|
+
* fields per event type instead of an opaque map. Variant `title`s drive the
|
|
547
|
+
* doc renderer's selector (see {@link schema.Envelope}).
|
|
548
|
+
*/
|
|
549
|
+
createWebhook.Body = z
|
|
550
|
+
.discriminatedUnion('eventType', [
|
|
551
|
+
z
|
|
552
|
+
.object({
|
|
553
|
+
chainId: z.optional(Schema.ChainId).check(z.meta({ examples: [4217] })),
|
|
554
|
+
context: z.optional(schema.Context),
|
|
555
|
+
destination: Destination,
|
|
556
|
+
eventType: z
|
|
557
|
+
.literal('token:transfer')
|
|
558
|
+
.check(z.describe('Event type you want Tempo to send to your webhook URL.'), z.meta({ examples: ['token:transfer'] })),
|
|
559
|
+
filters: z.optional(schema.TransferFilters),
|
|
560
|
+
})
|
|
561
|
+
.check(z.meta({ title: 'Transfer subscription' })),
|
|
562
|
+
z
|
|
563
|
+
.object({
|
|
564
|
+
chainId: z.optional(Schema.ChainId).check(z.meta({ examples: [4217] })),
|
|
565
|
+
context: z.optional(schema.Context),
|
|
566
|
+
destination: Destination,
|
|
567
|
+
eventType: z
|
|
568
|
+
.literal('transaction:included')
|
|
569
|
+
.check(z.describe('Event type you want Tempo to send to your webhook URL.'), z.meta({ examples: ['transaction:included'] })),
|
|
570
|
+
filters: z.optional(schema.TxFilters),
|
|
571
|
+
})
|
|
572
|
+
.check(z.meta({ title: 'Included-transaction subscription' })),
|
|
573
|
+
z
|
|
574
|
+
.object({
|
|
575
|
+
chainId: z.optional(Schema.ChainId).check(z.meta({ examples: [4217] })),
|
|
576
|
+
context: z.optional(schema.Context),
|
|
577
|
+
destination: Destination,
|
|
578
|
+
eventType: z
|
|
579
|
+
.literal('log:emitted')
|
|
580
|
+
.check(z.describe('Event type you want Tempo to send to your webhook URL.'), z.meta({ examples: ['log:emitted'] })),
|
|
581
|
+
filters: schema.LogFilters,
|
|
582
|
+
})
|
|
583
|
+
.check(z.meta({ title: 'Log subscription' })),
|
|
584
|
+
z
|
|
585
|
+
.object({
|
|
586
|
+
chainId: z.optional(Schema.ChainId).check(z.meta({ examples: [4217] })),
|
|
587
|
+
context: z.optional(schema.Context),
|
|
588
|
+
destination: Destination,
|
|
589
|
+
eventType: z
|
|
590
|
+
.literal('block:created')
|
|
591
|
+
.check(z.describe('Event type you want Tempo to send to your webhook URL.'), z.meta({ examples: ['block:created'] })),
|
|
592
|
+
filters: z.optional(schema.BlockFilters),
|
|
593
|
+
})
|
|
594
|
+
.check(z.meta({ title: 'Block subscription' })),
|
|
595
|
+
])
|
|
596
|
+
.check(z.describe('Details for creating a webhook subscription.'));
|
|
597
|
+
createWebhook.Response = z
|
|
598
|
+
.extend(schema.Subscription, {
|
|
599
|
+
secret: z
|
|
600
|
+
.string()
|
|
601
|
+
.check(z.describe('HMAC signing secret (`whsec_…`) used to verify Tempo webhook signatures. It is shown once when you create the webhook, so store it now.'), z.meta({ examples: ['whsec_abc123'] })),
|
|
602
|
+
})
|
|
603
|
+
.check(z.describe('The new webhook subscription, including the one-time signing secret.'));
|
|
604
|
+
})(createWebhook = schema.createWebhook || (schema.createWebhook = {}));
|
|
605
|
+
/** Schemas for the listWebhooks operation. */
|
|
606
|
+
let listWebhooks;
|
|
607
|
+
(function (listWebhooks) {
|
|
608
|
+
listWebhooks.Query = z
|
|
609
|
+
.object({
|
|
610
|
+
cursor: Schema.Cursor,
|
|
611
|
+
include: Schema.totalCountInclude,
|
|
612
|
+
limit: Schema.Limit,
|
|
613
|
+
page: Schema.Page,
|
|
614
|
+
})
|
|
615
|
+
.check(...Schema.pageChecks(), z.describe('Query parameters for listing your webhook subscriptions.'));
|
|
616
|
+
listWebhooks.Response = z
|
|
617
|
+
.object({
|
|
618
|
+
data: z.array(schema.Subscription).check(z.describe('Webhook subscriptions on this page.')),
|
|
619
|
+
meta: z
|
|
620
|
+
.optional(Schema.CountMeta)
|
|
621
|
+
.check(z.describe('Extra response metadata requested with `include`.')),
|
|
622
|
+
nextCursor: Schema.NextCursor,
|
|
623
|
+
})
|
|
624
|
+
.check(z.describe('A page of your webhook subscriptions.'));
|
|
625
|
+
})(listWebhooks = schema.listWebhooks || (schema.listWebhooks = {}));
|
|
626
|
+
/** Schemas for the getWebhook operation. */
|
|
627
|
+
let getWebhook;
|
|
628
|
+
(function (getWebhook) {
|
|
629
|
+
getWebhook.Params = z
|
|
630
|
+
.object({
|
|
631
|
+
id: z
|
|
632
|
+
.string()
|
|
633
|
+
.check(z.describe('Webhook subscription ID (`wh_…`).'), z.meta({ examples: ['wh_001718668800000_V1StGXR8Z5jdHi6B2myTq'] })),
|
|
634
|
+
})
|
|
635
|
+
.check(z.describe('Path parameters for a webhook subscription request.'));
|
|
636
|
+
getWebhook.Response = schema.Subscription;
|
|
637
|
+
})(getWebhook = schema.getWebhook || (schema.getWebhook = {}));
|
|
638
|
+
/** Schemas for the listWebhookDeliveries operation. */
|
|
639
|
+
let listWebhookDeliveries;
|
|
640
|
+
(function (listWebhookDeliveries) {
|
|
641
|
+
listWebhookDeliveries.Params = getWebhook.Params;
|
|
642
|
+
listWebhookDeliveries.Query = z
|
|
643
|
+
.object({
|
|
644
|
+
cursor: Schema.Cursor,
|
|
645
|
+
include: Schema.totalCountInclude,
|
|
646
|
+
limit: Schema.Limit,
|
|
647
|
+
page: Schema.Page,
|
|
648
|
+
})
|
|
649
|
+
.check(...Schema.pageChecks(), z.describe('Query parameters for listing webhook delivery attempts.'));
|
|
650
|
+
listWebhookDeliveries.Response = z
|
|
651
|
+
.object({
|
|
652
|
+
data: z.array(schema.Delivery).check(z.describe('Webhook delivery attempts on this page.')),
|
|
653
|
+
meta: z
|
|
654
|
+
.optional(Schema.CountMeta)
|
|
655
|
+
.check(z.describe('Extra response metadata requested with `include`.')),
|
|
656
|
+
nextCursor: Schema.NextCursor,
|
|
657
|
+
})
|
|
658
|
+
.check(z.describe('A page of webhook delivery attempts.'));
|
|
659
|
+
})(listWebhookDeliveries = schema.listWebhookDeliveries || (schema.listWebhookDeliveries = {}));
|
|
660
|
+
/** Schemas for the pingWebhook operation. */
|
|
661
|
+
let pingWebhook;
|
|
662
|
+
(function (pingWebhook) {
|
|
663
|
+
pingWebhook.Params = getWebhook.Params;
|
|
664
|
+
pingWebhook.Response = z
|
|
665
|
+
.object({
|
|
666
|
+
delivered: z
|
|
667
|
+
.boolean()
|
|
668
|
+
.check(z.describe('Whether your endpoint returned a 2xx response.'), z.meta({ examples: [true] })),
|
|
669
|
+
error: z
|
|
670
|
+
.optional(z.string())
|
|
671
|
+
.check(z.describe('Why the delivery failed, present when `delivered` is `false`.'), z.meta({ examples: ['Connection timed out'] })),
|
|
672
|
+
eventId: z
|
|
673
|
+
.string()
|
|
674
|
+
.check(z.describe('Synthetic event ID (`evt_…`) Tempo sent for this ping.'), z.meta({ examples: ['evt_abc123'] })),
|
|
675
|
+
responseMs: z
|
|
676
|
+
.optional(z.number().check(z.int(), z.nonnegative()))
|
|
677
|
+
.check(z.describe('Round-trip delivery time in milliseconds, when the request completed.'), z.meta({ examples: [12] })),
|
|
678
|
+
responseStatus: z
|
|
679
|
+
.optional(z.number().check(z.int()))
|
|
680
|
+
.check(z.describe('HTTP status your endpoint returned, when Tempo received a response.'), z.meta({ examples: [200] })),
|
|
681
|
+
})
|
|
682
|
+
.check(z.describe('Result of sending a synthetic ping event to your webhook URL.'));
|
|
683
|
+
})(pingWebhook = schema.pingWebhook || (schema.pingWebhook = {}));
|
|
684
|
+
/** Schemas for the retryWebhookDelivery operation. */
|
|
685
|
+
let retryWebhookDelivery;
|
|
686
|
+
(function (retryWebhookDelivery) {
|
|
687
|
+
retryWebhookDelivery.Params = z
|
|
688
|
+
.object({
|
|
689
|
+
deliveryId: z
|
|
690
|
+
.string()
|
|
691
|
+
.check(z.describe('Delivery ID (`whd_…`) you want Tempo to replay.'), z.meta({ examples: ['whd_001718668800000_8oQ2pVmZ4kKxWnLcRfTbN'] })),
|
|
692
|
+
id: z
|
|
693
|
+
.string()
|
|
694
|
+
.check(z.describe('Webhook subscription ID (`wh_…`).'), z.meta({ examples: ['wh_001718668800000_V1StGXR8Z5jdHi6B2myTq'] })),
|
|
695
|
+
})
|
|
696
|
+
.check(z.describe('Path parameters for retrying a webhook delivery.'));
|
|
697
|
+
retryWebhookDelivery.Response = pingWebhook.Response;
|
|
698
|
+
})(retryWebhookDelivery = schema.retryWebhookDelivery || (schema.retryWebhookDelivery = {}));
|
|
699
|
+
/** Schemas for the updateWebhook operation. */
|
|
700
|
+
let updateWebhook;
|
|
701
|
+
(function (updateWebhook) {
|
|
702
|
+
updateWebhook.Params = getWebhook.Params;
|
|
703
|
+
updateWebhook.Body = z
|
|
704
|
+
.object({
|
|
705
|
+
context: z
|
|
706
|
+
.optional(z.nullable(schema.Context))
|
|
707
|
+
.check(z.describe('New human context, or `null` to clear it.')),
|
|
708
|
+
destination: z
|
|
709
|
+
.optional(Destination)
|
|
710
|
+
.check(z.describe('New delivery destination for future events (URL or Slack channel).')),
|
|
711
|
+
filters: z
|
|
712
|
+
.optional(Filters)
|
|
713
|
+
.check(z.describe('New filters to apply to future webhook events.'), z.meta({ examples: [{ address: '0x20c0000000000000000000008f5425160ebe5525' }] })),
|
|
714
|
+
status: z
|
|
715
|
+
.optional(schema.Status)
|
|
716
|
+
.check(z.describe('New lifecycle status, such as pausing or resuming delivery.')),
|
|
717
|
+
})
|
|
718
|
+
.check(z.describe('Fields you want to update on the webhook subscription.'));
|
|
719
|
+
updateWebhook.Response = schema.Subscription;
|
|
720
|
+
})(updateWebhook = schema.updateWebhook || (schema.updateWebhook = {}));
|
|
721
|
+
/** Schemas for the deleteWebhook operation. */
|
|
722
|
+
let deleteWebhook;
|
|
723
|
+
(function (deleteWebhook) {
|
|
724
|
+
deleteWebhook.Params = getWebhook.Params;
|
|
725
|
+
deleteWebhook.Response = z
|
|
726
|
+
.object({
|
|
727
|
+
id: z
|
|
728
|
+
.string()
|
|
729
|
+
.check(z.describe('ID of the webhook subscription that was deleted.'), z.meta({ examples: ['wh_abc123'] })),
|
|
730
|
+
})
|
|
731
|
+
.check(z.describe('Confirmation that the webhook subscription was deleted.'));
|
|
732
|
+
})(deleteWebhook = schema.deleteWebhook || (schema.deleteWebhook = {}));
|
|
733
|
+
})(schema || (schema = {}));
|
|
734
|
+
/**
|
|
735
|
+
* Creates the webhooks resource. The subscription surface is non-public and
|
|
736
|
+
* API-key-only for now (MPP paid access is disabled) and never edge-cached; only
|
|
737
|
+
* `GET /webhooks/event-types` is public catalog data. Routes are always mounted
|
|
738
|
+
* but return `404` when no state `webhook.store` is configured, and are hidden
|
|
739
|
+
* from the OpenAPI document in that case.
|
|
740
|
+
*/
|
|
741
|
+
export function webhooks(options = {}) {
|
|
742
|
+
const hidden = !options.enabled;
|
|
743
|
+
return (new Hono()
|
|
744
|
+
.use('*', async (c, next) => {
|
|
745
|
+
// Gate the whole resource on a configured state store. Without it the
|
|
746
|
+
// outbound-HTTP surface is disabled (read-only deployment).
|
|
747
|
+
if (!c.get('webhook'))
|
|
748
|
+
return notEnabled(c);
|
|
749
|
+
await next();
|
|
750
|
+
// Subscriptions are principal-scoped, mutating, and stateful: never cache.
|
|
751
|
+
if (!c.res.headers.has('Cache-Control'))
|
|
752
|
+
c.res.headers.set('Cache-Control', 'no-store');
|
|
753
|
+
return;
|
|
754
|
+
})
|
|
755
|
+
.get('/event-types', Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }), OpenApi.describeRoute({
|
|
756
|
+
description: 'See which onchain event types Tempo can POST to your webhook URL.',
|
|
757
|
+
hide: hidden,
|
|
758
|
+
operationId: 'getWebhookEventTypes',
|
|
759
|
+
responses: OpenApi.responses({
|
|
760
|
+
success: {
|
|
761
|
+
description: 'Webhook event types available for subscription.',
|
|
762
|
+
schema: schema.getWebhookEventTypes.Response,
|
|
763
|
+
},
|
|
764
|
+
}),
|
|
765
|
+
summary: 'List webhook event types',
|
|
766
|
+
tags: ['Webhooks'],
|
|
767
|
+
}), Cache.response({
|
|
768
|
+
cacheControl: Cache.policies.stable,
|
|
769
|
+
name: 'tempo-api:webhooks:v1',
|
|
770
|
+
key: (c) => {
|
|
771
|
+
const url = new URL(c.req.url);
|
|
772
|
+
url.search = '';
|
|
773
|
+
return url.toString();
|
|
774
|
+
},
|
|
775
|
+
}), (c) => {
|
|
776
|
+
if (Auth.narrowAccess)
|
|
777
|
+
return Auth.paidAccessError(c);
|
|
778
|
+
if (!c.get('webhook'))
|
|
779
|
+
return notEnabled(c);
|
|
780
|
+
return c.json(Response.validated(schema.getWebhookEventTypes.Response, {
|
|
781
|
+
data: eventTypeDescriptions.map((eventType) => ({ ...eventType })),
|
|
782
|
+
}), 200);
|
|
783
|
+
})
|
|
784
|
+
.post('/', Auth.policy({ apiKey: { scopes: ['webhooks:write'] } }), OpenApi.validate('json', schema.createWebhook.Body, {
|
|
785
|
+
code: 'body_invalid',
|
|
786
|
+
message: 'Check the request body and try again.',
|
|
787
|
+
}), OpenApi.describeRoute({
|
|
788
|
+
description: 'Create a webhook subscription so Tempo can POST signed onchain events to your URL. The signing secret is shown only once, and each call creates a separate subscription.',
|
|
789
|
+
hide: hidden,
|
|
790
|
+
operationId: 'createWebhook',
|
|
791
|
+
responses: OpenApi.responses({
|
|
792
|
+
errors: { 403: 'You have reached the webhook subscription limit.' },
|
|
793
|
+
success: {
|
|
794
|
+
description: 'The created webhook subscription, including the one-time signing secret.',
|
|
795
|
+
schema: schema.createWebhook.Response,
|
|
796
|
+
},
|
|
797
|
+
}),
|
|
798
|
+
summary: 'Create webhook',
|
|
799
|
+
tags: ['Webhooks'],
|
|
800
|
+
}), async (c) => {
|
|
801
|
+
if (Auth.narrowAccess)
|
|
802
|
+
return Auth.accessError(c);
|
|
803
|
+
if (!c.get('webhook'))
|
|
804
|
+
return notEnabled(c);
|
|
805
|
+
if (OpenApi.narrowValidation)
|
|
806
|
+
return OpenApi.validationError(c, {
|
|
807
|
+
code: 'body_invalid',
|
|
808
|
+
message: 'Check the request body and try again.',
|
|
809
|
+
});
|
|
810
|
+
const webhook = c.get('webhook');
|
|
811
|
+
const store = webhook.store;
|
|
812
|
+
const owner = ownerFor(c);
|
|
813
|
+
if (!owner)
|
|
814
|
+
return unauthorized(c);
|
|
815
|
+
const body = c.req.valid('json');
|
|
816
|
+
const chainId = body.chainId ?? c.get('chainId');
|
|
817
|
+
try {
|
|
818
|
+
const subscription = await Webhooks.createSubscription(store, {
|
|
819
|
+
chainId,
|
|
820
|
+
...(body.context === undefined ? {} : { context: body.context }),
|
|
821
|
+
destination: body.destination,
|
|
822
|
+
eventType: body.eventType,
|
|
823
|
+
filters: body.filters,
|
|
824
|
+
owner,
|
|
825
|
+
...(owner.type === 'mpp' ? { ttl: mppTtlMs } : {}),
|
|
826
|
+
}, { maxPerOwner: webhook.maxPerOwner ?? defaultMaxPerOwner });
|
|
827
|
+
return c.json(Response.validated(schema.createWebhook.Response, toCreated(subscription)), 200);
|
|
828
|
+
}
|
|
829
|
+
catch (cause) {
|
|
830
|
+
return mutationError(c, cause);
|
|
831
|
+
}
|
|
832
|
+
})
|
|
833
|
+
.get('/', Auth.policy({ apiKey: { scopes: ['webhooks:read'] } }), OpenApi.validate('query', schema.listWebhooks.Query, {
|
|
834
|
+
code: 'query_invalid',
|
|
835
|
+
message: 'Check the query parameters and try again.',
|
|
836
|
+
}), OpenApi.describeRoute({
|
|
837
|
+
description: 'List your webhook subscriptions.',
|
|
838
|
+
hide: hidden,
|
|
839
|
+
operationId: 'listWebhooks',
|
|
840
|
+
responses: OpenApi.responses({
|
|
841
|
+
success: {
|
|
842
|
+
description: 'A page of webhook subscriptions.',
|
|
843
|
+
schema: schema.listWebhooks.Response,
|
|
844
|
+
},
|
|
845
|
+
}),
|
|
846
|
+
summary: 'List webhooks',
|
|
847
|
+
tags: ['Webhooks'],
|
|
848
|
+
}), async (c) => {
|
|
849
|
+
if (Auth.narrowAccess)
|
|
850
|
+
return Auth.accessError(c);
|
|
851
|
+
if (!c.get('webhook'))
|
|
852
|
+
return notEnabled(c);
|
|
853
|
+
if (OpenApi.narrowValidation)
|
|
854
|
+
return OpenApi.validationError(c, {
|
|
855
|
+
code: 'query_invalid',
|
|
856
|
+
message: 'Check the query parameters and try again.',
|
|
857
|
+
});
|
|
858
|
+
const store = c.get('webhook').store;
|
|
859
|
+
const owner = ownerFor(c);
|
|
860
|
+
if (!owner)
|
|
861
|
+
return unauthorized(c);
|
|
862
|
+
const { cursor, include, limit, page } = c.req.valid('query');
|
|
863
|
+
try {
|
|
864
|
+
// `totalCount` is opt-in and exact (the per-owner set is bounded);
|
|
865
|
+
// run it concurrently with the page list. Best-effort: a failure
|
|
866
|
+
// omits `meta` rather than failing the page.
|
|
867
|
+
const countPromise = include.includes('totalCount')
|
|
868
|
+
? Webhooks.countSubscriptions(store, owner).catch(() => undefined)
|
|
869
|
+
: undefined;
|
|
870
|
+
// Fetch one extra to detect a further page without a second round-trip.
|
|
871
|
+
// `page` translates to a positional slice of `(page - 1) * limit` rows.
|
|
872
|
+
const rows = await Webhooks.listSubscriptions(store, owner, {
|
|
873
|
+
cursor,
|
|
874
|
+
limit: limit + 1,
|
|
875
|
+
offset: page !== undefined && page > 1 ? (page - 1) * limit : undefined,
|
|
876
|
+
});
|
|
877
|
+
const hasMore = rows.length > limit;
|
|
878
|
+
const data = (hasMore ? rows.slice(0, limit) : rows).map(toPublic);
|
|
879
|
+
const nextCursor = hasMore ? (data[data.length - 1]?.id ?? null) : null;
|
|
880
|
+
const totalCount = countPromise ? await countPromise : undefined;
|
|
881
|
+
return c.json(Response.validated(schema.listWebhooks.Response, {
|
|
882
|
+
data,
|
|
883
|
+
...(totalCount !== undefined
|
|
884
|
+
? { meta: { totalCountCapped: false, totalCount } }
|
|
885
|
+
: {}),
|
|
886
|
+
nextCursor,
|
|
887
|
+
}), 200);
|
|
888
|
+
}
|
|
889
|
+
catch (cause) {
|
|
890
|
+
return Response.upstream(c, cause);
|
|
891
|
+
}
|
|
892
|
+
})
|
|
893
|
+
// The `:id` param is constrained to the `wh_…` id shape so it can't also
|
|
894
|
+
// match the sibling static route `GET /webhooks/event-types`. Without the
|
|
895
|
+
// constraint, this API-key-only route's policy would be collected for the
|
|
896
|
+
// event-types request and override its (intentionally public) access.
|
|
897
|
+
.get('/:id{wh_[A-Za-z0-9_-]+}', Auth.policy({ apiKey: { scopes: ['webhooks:read'] } }), OpenApi.validate('param', schema.getWebhook.Params, {
|
|
898
|
+
code: 'param_invalid',
|
|
899
|
+
message: 'Check the path parameters and try again.',
|
|
900
|
+
}), OpenApi.describeRoute({
|
|
901
|
+
description: 'Get one webhook subscription by its ID.',
|
|
902
|
+
hide: hidden,
|
|
903
|
+
operationId: 'getWebhook',
|
|
904
|
+
responses: OpenApi.responses({
|
|
905
|
+
errors: {
|
|
906
|
+
404: {
|
|
907
|
+
description: 'No webhook subscription was found for that ID.',
|
|
908
|
+
codes: ['webhook_not_found'],
|
|
909
|
+
},
|
|
910
|
+
},
|
|
911
|
+
success: {
|
|
912
|
+
description: 'The requested webhook subscription.',
|
|
913
|
+
schema: schema.getWebhook.Response,
|
|
914
|
+
},
|
|
915
|
+
}),
|
|
916
|
+
summary: 'Get webhook',
|
|
917
|
+
tags: ['Webhooks'],
|
|
918
|
+
}), async (c) => {
|
|
919
|
+
if (Auth.narrowAccess)
|
|
920
|
+
return Auth.accessError(c);
|
|
921
|
+
if (!c.get('webhook'))
|
|
922
|
+
return notEnabled(c);
|
|
923
|
+
if (OpenApi.narrowValidation)
|
|
924
|
+
return OpenApi.validationError(c, {
|
|
925
|
+
code: 'param_invalid',
|
|
926
|
+
message: 'Check the path parameters and try again.',
|
|
927
|
+
});
|
|
928
|
+
const store = c.get('webhook').store;
|
|
929
|
+
const owner = ownerFor(c);
|
|
930
|
+
if (!owner)
|
|
931
|
+
return unauthorized(c);
|
|
932
|
+
const { id } = c.req.valid('param');
|
|
933
|
+
try {
|
|
934
|
+
const subscription = await Webhooks.getSubscription(store, owner, id);
|
|
935
|
+
if (!subscription)
|
|
936
|
+
return notFound(c);
|
|
937
|
+
return c.json(Response.validated(schema.getWebhook.Response, toPublic(subscription)), 200);
|
|
938
|
+
}
|
|
939
|
+
catch (cause) {
|
|
940
|
+
return Response.upstream(c, cause);
|
|
941
|
+
}
|
|
942
|
+
})
|
|
943
|
+
.get('/:id{wh_[A-Za-z0-9_-]+}/deliveries', Auth.policy({ apiKey: { scopes: ['webhooks:read'] } }), OpenApi.validate('param', schema.listWebhookDeliveries.Params, {
|
|
944
|
+
code: 'param_invalid',
|
|
945
|
+
message: 'Check the path parameters and try again.',
|
|
946
|
+
}), OpenApi.validate('query', schema.listWebhookDeliveries.Query, {
|
|
947
|
+
code: 'query_invalid',
|
|
948
|
+
message: 'Check the query parameters and try again.',
|
|
949
|
+
}), OpenApi.describeRoute({
|
|
950
|
+
description: 'List delivery attempts for a webhook subscription.',
|
|
951
|
+
hide: hidden,
|
|
952
|
+
operationId: 'listWebhookDeliveries',
|
|
953
|
+
responses: OpenApi.responses({
|
|
954
|
+
errors: {
|
|
955
|
+
404: {
|
|
956
|
+
description: 'No webhook subscription was found for that ID.',
|
|
957
|
+
codes: ['webhook_not_found'],
|
|
958
|
+
},
|
|
959
|
+
},
|
|
960
|
+
success: {
|
|
961
|
+
description: 'A newest-first page of webhook delivery attempts.',
|
|
962
|
+
schema: schema.listWebhookDeliveries.Response,
|
|
963
|
+
},
|
|
964
|
+
}),
|
|
965
|
+
summary: 'List webhook deliveries',
|
|
966
|
+
tags: ['Webhooks'],
|
|
967
|
+
}), async (c) => {
|
|
968
|
+
if (Auth.narrowAccess)
|
|
969
|
+
return Auth.accessError(c);
|
|
970
|
+
if (!c.get('webhook'))
|
|
971
|
+
return notEnabled(c);
|
|
972
|
+
if (OpenApi.narrowValidation)
|
|
973
|
+
return OpenApi.validationError(c, {
|
|
974
|
+
code: 'param_invalid',
|
|
975
|
+
message: 'Check the path parameters and try again.',
|
|
976
|
+
});
|
|
977
|
+
if (OpenApi.narrowValidation)
|
|
978
|
+
return OpenApi.validationError(c, {
|
|
979
|
+
code: 'query_invalid',
|
|
980
|
+
message: 'Check the query parameters and try again.',
|
|
981
|
+
});
|
|
982
|
+
const store = c.get('webhook').store;
|
|
983
|
+
const owner = ownerFor(c);
|
|
984
|
+
if (!owner)
|
|
985
|
+
return unauthorized(c);
|
|
986
|
+
const { id } = c.req.valid('param');
|
|
987
|
+
const { cursor, include, limit, page } = c.req.valid('query');
|
|
988
|
+
try {
|
|
989
|
+
// Owner-scope first: resolving the subscription under the caller's owner
|
|
990
|
+
// key 404s ids they don't own (indistinguishable from "doesn't exist").
|
|
991
|
+
const subscription = await Webhooks.getSubscription(store, owner, id);
|
|
992
|
+
if (!subscription)
|
|
993
|
+
return notFound(c);
|
|
994
|
+
// `totalCount` is opt-in and exact (the delivery log is TTL-bounded);
|
|
995
|
+
// run it concurrently with the page list. Best-effort: a failure
|
|
996
|
+
// omits `meta` rather than failing the page.
|
|
997
|
+
const countPromise = include.includes('totalCount')
|
|
998
|
+
? Webhooks.countDeliveries(store, id).catch(() => undefined)
|
|
999
|
+
: undefined;
|
|
1000
|
+
// Fetch one extra to detect a further page without a second round-trip.
|
|
1001
|
+
// `page` translates to a positional slice of `(page - 1) * limit` rows.
|
|
1002
|
+
const rows = await Webhooks.listDeliveries(store, id, {
|
|
1003
|
+
cursor,
|
|
1004
|
+
limit: limit + 1,
|
|
1005
|
+
offset: page !== undefined && page > 1 ? (page - 1) * limit : undefined,
|
|
1006
|
+
});
|
|
1007
|
+
const hasMore = rows.length > limit;
|
|
1008
|
+
const data = hasMore ? rows.slice(0, limit) : rows;
|
|
1009
|
+
const nextCursor = hasMore ? (data[data.length - 1]?.id ?? null) : null;
|
|
1010
|
+
const totalCount = countPromise ? await countPromise : undefined;
|
|
1011
|
+
return c.json(Response.validated(schema.listWebhookDeliveries.Response, {
|
|
1012
|
+
data,
|
|
1013
|
+
...(totalCount !== undefined
|
|
1014
|
+
? { meta: { totalCountCapped: false, totalCount } }
|
|
1015
|
+
: {}),
|
|
1016
|
+
nextCursor,
|
|
1017
|
+
}), 200);
|
|
1018
|
+
}
|
|
1019
|
+
catch (cause) {
|
|
1020
|
+
return Response.upstream(c, cause);
|
|
1021
|
+
}
|
|
1022
|
+
})
|
|
1023
|
+
.post('/:id{wh_[A-Za-z0-9_-]+}/ping', Auth.policy({ apiKey: { scopes: ['webhooks:write'] } }), OpenApi.validate('param', schema.pingWebhook.Params, {
|
|
1024
|
+
code: 'param_invalid',
|
|
1025
|
+
message: 'Check the path parameters and try again.',
|
|
1026
|
+
}), OpenApi.describeRoute({
|
|
1027
|
+
description: 'Send a signed `ping` event to your webhook URL to test delivery and signature verification.',
|
|
1028
|
+
hide: hidden,
|
|
1029
|
+
operationId: 'pingWebhook',
|
|
1030
|
+
responses: OpenApi.responses({
|
|
1031
|
+
errors: {
|
|
1032
|
+
404: {
|
|
1033
|
+
description: 'No webhook subscription was found for that ID.',
|
|
1034
|
+
codes: ['webhook_not_found'],
|
|
1035
|
+
},
|
|
1036
|
+
},
|
|
1037
|
+
success: {
|
|
1038
|
+
description: 'Result of the synthetic ping delivery.',
|
|
1039
|
+
schema: schema.pingWebhook.Response,
|
|
1040
|
+
},
|
|
1041
|
+
}),
|
|
1042
|
+
summary: 'Ping webhook',
|
|
1043
|
+
tags: ['Webhooks'],
|
|
1044
|
+
}), async (c) => {
|
|
1045
|
+
if (Auth.narrowAccess)
|
|
1046
|
+
return Auth.accessError(c);
|
|
1047
|
+
if (!c.get('webhook'))
|
|
1048
|
+
return notEnabled(c);
|
|
1049
|
+
if (OpenApi.narrowValidation)
|
|
1050
|
+
return OpenApi.validationError(c, {
|
|
1051
|
+
code: 'param_invalid',
|
|
1052
|
+
message: 'Check the path parameters and try again.',
|
|
1053
|
+
});
|
|
1054
|
+
const store = c.get('webhook').store;
|
|
1055
|
+
const owner = ownerFor(c);
|
|
1056
|
+
if (!owner)
|
|
1057
|
+
return unauthorized(c);
|
|
1058
|
+
const { id } = c.req.valid('param');
|
|
1059
|
+
try {
|
|
1060
|
+
// Owner-scope first so unowned/unknown ids are indistinguishable (404).
|
|
1061
|
+
const subscription = await Webhooks.getSubscription(store, owner, id);
|
|
1062
|
+
if (!subscription)
|
|
1063
|
+
return notFound(c);
|
|
1064
|
+
// Inline delivery (not the queue) so the caller gets an immediate
|
|
1065
|
+
// result. `ping` never mutates the subscription's failure/lifecycle
|
|
1066
|
+
// state — a test must not disable a healthy endpoint.
|
|
1067
|
+
const { envelope, result } = await Webhooks.ping(store, subscription);
|
|
1068
|
+
return c.json(Response.validated(schema.pingWebhook.Response, {
|
|
1069
|
+
delivered: result.ok,
|
|
1070
|
+
eventId: envelope.id,
|
|
1071
|
+
...(result.error === undefined ? {} : { error: result.error }),
|
|
1072
|
+
...(result.durationMs === undefined ? {} : { responseMs: result.durationMs }),
|
|
1073
|
+
...(result.status === undefined ? {} : { responseStatus: result.status }),
|
|
1074
|
+
}), 200);
|
|
1075
|
+
}
|
|
1076
|
+
catch (cause) {
|
|
1077
|
+
return Response.upstream(c, cause);
|
|
1078
|
+
}
|
|
1079
|
+
})
|
|
1080
|
+
.post('/:id{wh_[A-Za-z0-9_-]+}/deliveries/:deliveryId/retry', Auth.policy({ apiKey: { scopes: ['webhooks:write'] } }), OpenApi.validate('param', schema.retryWebhookDelivery.Params, {
|
|
1081
|
+
code: 'param_invalid',
|
|
1082
|
+
message: 'Check the path parameters and try again.',
|
|
1083
|
+
}), OpenApi.describeRoute({
|
|
1084
|
+
description: 'Replay a previous webhook delivery and update the delivery result.',
|
|
1085
|
+
hide: hidden,
|
|
1086
|
+
operationId: 'retryWebhookDelivery',
|
|
1087
|
+
responses: OpenApi.responses({
|
|
1088
|
+
errors: {
|
|
1089
|
+
404: {
|
|
1090
|
+
description: 'No webhook subscription or delivery was found for those IDs.',
|
|
1091
|
+
codes: ['webhook_not_found', 'delivery_not_found'],
|
|
1092
|
+
},
|
|
1093
|
+
},
|
|
1094
|
+
success: {
|
|
1095
|
+
description: 'Result of replaying the webhook delivery.',
|
|
1096
|
+
schema: schema.retryWebhookDelivery.Response,
|
|
1097
|
+
},
|
|
1098
|
+
}),
|
|
1099
|
+
summary: 'Retry webhook delivery',
|
|
1100
|
+
tags: ['Webhooks'],
|
|
1101
|
+
}), async (c) => {
|
|
1102
|
+
if (Auth.narrowAccess)
|
|
1103
|
+
return Auth.accessError(c);
|
|
1104
|
+
if (!c.get('webhook'))
|
|
1105
|
+
return notEnabled(c);
|
|
1106
|
+
if (OpenApi.narrowValidation)
|
|
1107
|
+
return OpenApi.validationError(c, {
|
|
1108
|
+
code: 'param_invalid',
|
|
1109
|
+
message: 'Check the path parameters and try again.',
|
|
1110
|
+
});
|
|
1111
|
+
const store = c.get('webhook').store;
|
|
1112
|
+
const owner = ownerFor(c);
|
|
1113
|
+
if (!owner)
|
|
1114
|
+
return unauthorized(c);
|
|
1115
|
+
const { deliveryId, id } = c.req.valid('param');
|
|
1116
|
+
try {
|
|
1117
|
+
// Owner-scope the subscription first so unowned ids 404 like elsewhere.
|
|
1118
|
+
const subscription = await Webhooks.getSubscription(store, owner, id);
|
|
1119
|
+
if (!subscription)
|
|
1120
|
+
return notFound(c);
|
|
1121
|
+
const delivery = await Webhooks.getDelivery(store, id, deliveryId);
|
|
1122
|
+
if (!delivery)
|
|
1123
|
+
return Response.error(c, {
|
|
1124
|
+
code: 'delivery_not_found',
|
|
1125
|
+
message: 'Delivery not found',
|
|
1126
|
+
status: 404,
|
|
1127
|
+
});
|
|
1128
|
+
// Replay the exact persisted envelope inline. Unlike `ping`, this is a
|
|
1129
|
+
// real event, so the outcome updates the subscription's failure state.
|
|
1130
|
+
const result = await Webhooks.deliverAndRecord(store, subscription, delivery.envelope);
|
|
1131
|
+
return c.json(Response.validated(schema.retryWebhookDelivery.Response, {
|
|
1132
|
+
delivered: result.ok,
|
|
1133
|
+
eventId: delivery.envelope.id,
|
|
1134
|
+
...(result.error === undefined ? {} : { error: result.error }),
|
|
1135
|
+
...(result.durationMs === undefined ? {} : { responseMs: result.durationMs }),
|
|
1136
|
+
...(result.status === undefined ? {} : { responseStatus: result.status }),
|
|
1137
|
+
}), 200);
|
|
1138
|
+
}
|
|
1139
|
+
catch (cause) {
|
|
1140
|
+
return Response.upstream(c, cause);
|
|
1141
|
+
}
|
|
1142
|
+
})
|
|
1143
|
+
.patch('/:id{wh_[A-Za-z0-9_-]+}', Auth.policy({ apiKey: { scopes: ['webhooks:write'] } }), OpenApi.validate('param', schema.updateWebhook.Params, {
|
|
1144
|
+
code: 'param_invalid',
|
|
1145
|
+
message: 'Check the path parameters and try again.',
|
|
1146
|
+
}), OpenApi.validate('json', schema.updateWebhook.Body, {
|
|
1147
|
+
code: 'body_invalid',
|
|
1148
|
+
message: 'Check the request body and try again.',
|
|
1149
|
+
}), OpenApi.describeRoute({
|
|
1150
|
+
description: 'Update a webhook subscription URL, filters, or delivery status.',
|
|
1151
|
+
hide: hidden,
|
|
1152
|
+
operationId: 'updateWebhook',
|
|
1153
|
+
responses: OpenApi.responses({
|
|
1154
|
+
errors: {
|
|
1155
|
+
404: {
|
|
1156
|
+
description: 'No webhook subscription was found for that ID.',
|
|
1157
|
+
codes: ['webhook_not_found'],
|
|
1158
|
+
},
|
|
1159
|
+
},
|
|
1160
|
+
success: {
|
|
1161
|
+
description: 'The updated webhook subscription.',
|
|
1162
|
+
schema: schema.updateWebhook.Response,
|
|
1163
|
+
},
|
|
1164
|
+
}),
|
|
1165
|
+
summary: 'Update webhook',
|
|
1166
|
+
tags: ['Webhooks'],
|
|
1167
|
+
}), async (c) => {
|
|
1168
|
+
if (Auth.narrowAccess)
|
|
1169
|
+
return Auth.accessError(c);
|
|
1170
|
+
if (!c.get('webhook'))
|
|
1171
|
+
return notEnabled(c);
|
|
1172
|
+
if (OpenApi.narrowValidation)
|
|
1173
|
+
return OpenApi.validationError(c, {
|
|
1174
|
+
code: 'param_invalid',
|
|
1175
|
+
message: 'Check the path parameters and try again.',
|
|
1176
|
+
});
|
|
1177
|
+
if (OpenApi.narrowValidation)
|
|
1178
|
+
return OpenApi.validationError(c, {
|
|
1179
|
+
code: 'body_invalid',
|
|
1180
|
+
message: 'Check the request body and try again.',
|
|
1181
|
+
});
|
|
1182
|
+
const store = c.get('webhook').store;
|
|
1183
|
+
const owner = ownerFor(c);
|
|
1184
|
+
if (!owner)
|
|
1185
|
+
return unauthorized(c);
|
|
1186
|
+
const { id } = c.req.valid('param');
|
|
1187
|
+
const patch = c.req.valid('json');
|
|
1188
|
+
try {
|
|
1189
|
+
// The PATCH body accepts an opaque `filters` map (it can't know the
|
|
1190
|
+
// subscription's event type up front), so revalidate the incoming
|
|
1191
|
+
// filters against the stored `eventType` and fail closed before
|
|
1192
|
+
// persisting — otherwise a bad filter would silently widen the match
|
|
1193
|
+
// set at scan time.
|
|
1194
|
+
if (patch.filters !== undefined) {
|
|
1195
|
+
const existing = await Webhooks.getSubscription(store, owner, id);
|
|
1196
|
+
if (!existing)
|
|
1197
|
+
return notFound(c);
|
|
1198
|
+
parseFilters(existing.eventType, patch.filters);
|
|
1199
|
+
}
|
|
1200
|
+
const subscription = await Webhooks.updateSubscription(store, owner, id, patch);
|
|
1201
|
+
if (!subscription)
|
|
1202
|
+
return notFound(c);
|
|
1203
|
+
return c.json(Response.validated(schema.updateWebhook.Response, toPublic(subscription)), 200);
|
|
1204
|
+
}
|
|
1205
|
+
catch (cause) {
|
|
1206
|
+
return mutationError(c, cause);
|
|
1207
|
+
}
|
|
1208
|
+
})
|
|
1209
|
+
.delete('/:id{wh_[A-Za-z0-9_-]+}', Auth.policy({ apiKey: { scopes: ['webhooks:write'] } }), OpenApi.validate('param', schema.deleteWebhook.Params, {
|
|
1210
|
+
code: 'param_invalid',
|
|
1211
|
+
message: 'Check the path parameters and try again.',
|
|
1212
|
+
}), OpenApi.describeRoute({
|
|
1213
|
+
description: 'Delete a webhook subscription and stop future deliveries immediately.',
|
|
1214
|
+
hide: hidden,
|
|
1215
|
+
operationId: 'deleteWebhook',
|
|
1216
|
+
responses: OpenApi.responses({
|
|
1217
|
+
errors: {
|
|
1218
|
+
404: {
|
|
1219
|
+
description: 'No webhook subscription was found for that ID.',
|
|
1220
|
+
codes: ['webhook_not_found'],
|
|
1221
|
+
},
|
|
1222
|
+
},
|
|
1223
|
+
success: {
|
|
1224
|
+
description: 'Confirmation that the webhook subscription was deleted.',
|
|
1225
|
+
schema: schema.deleteWebhook.Response,
|
|
1226
|
+
},
|
|
1227
|
+
}),
|
|
1228
|
+
summary: 'Delete webhook',
|
|
1229
|
+
tags: ['Webhooks'],
|
|
1230
|
+
}), async (c) => {
|
|
1231
|
+
if (Auth.narrowAccess)
|
|
1232
|
+
return Auth.accessError(c);
|
|
1233
|
+
if (!c.get('webhook'))
|
|
1234
|
+
return notEnabled(c);
|
|
1235
|
+
if (OpenApi.narrowValidation)
|
|
1236
|
+
return OpenApi.validationError(c, {
|
|
1237
|
+
code: 'param_invalid',
|
|
1238
|
+
message: 'Check the path parameters and try again.',
|
|
1239
|
+
});
|
|
1240
|
+
const store = c.get('webhook').store;
|
|
1241
|
+
const owner = ownerFor(c);
|
|
1242
|
+
if (!owner)
|
|
1243
|
+
return unauthorized(c);
|
|
1244
|
+
const { id } = c.req.valid('param');
|
|
1245
|
+
try {
|
|
1246
|
+
const deleted = await Webhooks.deleteSubscription(store, owner, id);
|
|
1247
|
+
if (!deleted)
|
|
1248
|
+
return notFound(c);
|
|
1249
|
+
return c.json(Response.validated(schema.deleteWebhook.Response, { id }), 200);
|
|
1250
|
+
}
|
|
1251
|
+
catch (cause) {
|
|
1252
|
+
return Response.upstream(c, cause);
|
|
1253
|
+
}
|
|
1254
|
+
}));
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Largest representable in-block index, used as an end-of-block cursor sentinel
|
|
1258
|
+
* so a fully-scanned block advances the cursor strictly past it (no row index
|
|
1259
|
+
* can equal it).
|
|
1260
|
+
*/
|
|
1261
|
+
const endOfBlockIndex = 2_147_483_647;
|
|
1262
|
+
/**
|
|
1263
|
+
* Polls the chain over RPC for new events and dispatches a signed envelope per
|
|
1264
|
+
* matching row, advancing each active subscription's `(block, index)` keyset
|
|
1265
|
+
* cursor.
|
|
1266
|
+
*
|
|
1267
|
+
* The new monitoring event types (`log:emitted`, `block:created`,
|
|
1268
|
+
* `transaction:included`) scan a **narrow confirmed window** `[cursor, head -
|
|
1269
|
+
* confirmations]` directly over JSON-RPC (`eth_getLogs`, `eth_getBlockByNumber`,
|
|
1270
|
+
* `eth_getBlockReceipts`) — the same data sources the indexer itself consumes —
|
|
1271
|
+
* so they need no indexer and stay fresh. The pre-existing `token:transfer`
|
|
1272
|
+
* type continues to read its humanized shape from TIDX, clamped to the indexed
|
|
1273
|
+
* head so it never runs ahead of the indexer.
|
|
1274
|
+
*
|
|
1275
|
+
* The chain head is injectable (tests pin a fixed historical window); only
|
|
1276
|
+
* blocks at least `confirmations` behind head are emitted, giving at-least-once,
|
|
1277
|
+
* finalized-ish semantics (re-delivery overlap is harmless because the receiver
|
|
1278
|
+
* dedupes on the stable `event.id`).
|
|
1279
|
+
*
|
|
1280
|
+
* Cursor advances only past successfully dispatched rows, so a dispatch failure
|
|
1281
|
+
* pauses that subscription at the last good position and the next tick resumes.
|
|
1282
|
+
*/
|
|
1283
|
+
export async function poll(options) {
|
|
1284
|
+
const { batchSize = 100, chainId, client, confirmations = 2, eventTypes = ['token:transfer', 'transaction:included', 'log:emitted', 'block:created'], maxBlocks = 1_000, store, tidx, } = options;
|
|
1285
|
+
const now = options.now ?? (() => new Date());
|
|
1286
|
+
// Default (self-host) transport: deliver inline and record the outcome so a
|
|
1287
|
+
// sustained-failure subscription auto-disables. On Cloudflare, callers pass a
|
|
1288
|
+
// Queue producer instead and the consumer runs `deliverAndRecord`.
|
|
1289
|
+
const dispatch = options.dispatch ??
|
|
1290
|
+
(({ envelope, subscription }) => Webhooks.deliverAndRecord(store, subscription, envelope));
|
|
1291
|
+
// Head resolution is lazy and per-source. RPC types work back from the live
|
|
1292
|
+
// RPC head; the legacy TIDX types from the indexed head (never ahead of the
|
|
1293
|
+
// indexer). A pinned `headBlockNumber` (tests) overrides both.
|
|
1294
|
+
const override = options.headBlockNumber;
|
|
1295
|
+
let rpcHead = override === undefined ? undefined : Number(override);
|
|
1296
|
+
const resolveRpcHead = async () => (rpcHead ??= await getRpcHead(client));
|
|
1297
|
+
let tidxHead = override === undefined ? undefined : Number(override);
|
|
1298
|
+
const resolveTidxHead = async () => (tidxHead ??= tidx ? await getIndexedHead(tidx, chainId) : await resolveRpcHead());
|
|
1299
|
+
const rpcSafeBlock = Math.max(0, (await resolveRpcHead()) - confirmations);
|
|
1300
|
+
// Per-tick lazy cache so multiple subscriptions of the same/related types
|
|
1301
|
+
// share fetched blocks and receipts instead of refetching the same window.
|
|
1302
|
+
const cache = createChainCache(client);
|
|
1303
|
+
const result = {
|
|
1304
|
+
chainId,
|
|
1305
|
+
dispatched: 0,
|
|
1306
|
+
failed: 0,
|
|
1307
|
+
safeBlock: rpcSafeBlock,
|
|
1308
|
+
scanned: 0,
|
|
1309
|
+
subscriptions: 0,
|
|
1310
|
+
};
|
|
1311
|
+
for (const eventType of eventTypes) {
|
|
1312
|
+
const legacy = eventType === 'token:transfer';
|
|
1313
|
+
const safeBlock = legacy ? Math.max(0, (await resolveTidxHead()) - confirmations) : rpcSafeBlock;
|
|
1314
|
+
const subscriptions = await Webhooks.listActive(store, chainId, eventType);
|
|
1315
|
+
for (const subscription of subscriptions) {
|
|
1316
|
+
result.subscriptions++;
|
|
1317
|
+
const cursor = await Webhooks.getCursor(store, subscription.id);
|
|
1318
|
+
if (cursor === null) {
|
|
1319
|
+
// First time we've seen this subscription: anchor it at the head of the
|
|
1320
|
+
// confirmed window so we never backfill from genesis. Events start
|
|
1321
|
+
// flowing from the next tick onward.
|
|
1322
|
+
await Webhooks.setCursor(store, subscription.id, Cursor.encode([safeBlock, endOfBlockIndex]));
|
|
1323
|
+
continue;
|
|
1324
|
+
}
|
|
1325
|
+
const page = await scanSubscription({
|
|
1326
|
+
batchSize,
|
|
1327
|
+
cache,
|
|
1328
|
+
client,
|
|
1329
|
+
cursor,
|
|
1330
|
+
maxBlocks,
|
|
1331
|
+
safeBlock,
|
|
1332
|
+
subscription,
|
|
1333
|
+
tidx,
|
|
1334
|
+
});
|
|
1335
|
+
result.scanned += page.items.length;
|
|
1336
|
+
let accepted;
|
|
1337
|
+
let failed = false;
|
|
1338
|
+
for (const item of page.items) {
|
|
1339
|
+
// Enrich `token:transfer` rows with best-effort token metadata so
|
|
1340
|
+
// downstream destinations (Slack human amounts, richer `url`/`betterstack`
|
|
1341
|
+
// payloads) can render `amount` in token units. Non-fatal on failure.
|
|
1342
|
+
const data = eventType === 'token:transfer'
|
|
1343
|
+
? await withTokenMetadata(item.data, { chainId, client, store })
|
|
1344
|
+
: item.data;
|
|
1345
|
+
const envelope = Webhooks.buildEnvelope({
|
|
1346
|
+
blockNumber: item.cursor[0],
|
|
1347
|
+
createdAt: now(),
|
|
1348
|
+
data,
|
|
1349
|
+
logIndex: item.cursor[1],
|
|
1350
|
+
subscription,
|
|
1351
|
+
});
|
|
1352
|
+
const delivered = await dispatch({ envelope, subscription });
|
|
1353
|
+
if (!delivered.ok) {
|
|
1354
|
+
result.failed++;
|
|
1355
|
+
failed = true;
|
|
1356
|
+
break;
|
|
1357
|
+
}
|
|
1358
|
+
accepted = item.cursor;
|
|
1359
|
+
result.dispatched++;
|
|
1360
|
+
}
|
|
1361
|
+
// Advance the high-water mark. On a dispatch failure, stop at the last
|
|
1362
|
+
// accepted row so the failed one retries next tick. Otherwise: window
|
|
1363
|
+
// scanners jump to the end of the fully-scanned range (even with zero
|
|
1364
|
+
// matches); keyset scanners advance past the last dispatched row, or — on
|
|
1365
|
+
// a complete empty page — to the end of the confirmed window.
|
|
1366
|
+
if (failed) {
|
|
1367
|
+
if (accepted)
|
|
1368
|
+
await Webhooks.setCursor(store, subscription.id, Cursor.encode([...accepted]));
|
|
1369
|
+
}
|
|
1370
|
+
else if (page.scannedThrough !== undefined)
|
|
1371
|
+
await Webhooks.setCursor(store, subscription.id, Cursor.encode([page.scannedThrough, endOfBlockIndex]));
|
|
1372
|
+
else if (accepted)
|
|
1373
|
+
await Webhooks.setCursor(store, subscription.id, Cursor.encode([...accepted]));
|
|
1374
|
+
else if (!page.hasMore)
|
|
1375
|
+
await Webhooks.setCursor(store, subscription.id, Cursor.encode([safeBlock, endOfBlockIndex]));
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
return result;
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Attaches best-effort `{ symbol, decimals }` token metadata to a
|
|
1382
|
+
* `token:transfer` row so destinations can render human amounts. Reads the row's
|
|
1383
|
+
* `address` (the TIP-20 contract), resolves metadata via a memoized RPC lookup,
|
|
1384
|
+
* and returns the row unchanged when the address is missing or the lookup fails.
|
|
1385
|
+
*/
|
|
1386
|
+
async function withTokenMetadata(data, deps) {
|
|
1387
|
+
if (typeof data !== 'object' || data === null)
|
|
1388
|
+
return data;
|
|
1389
|
+
const address = data['address'];
|
|
1390
|
+
if (typeof address !== 'string')
|
|
1391
|
+
return data;
|
|
1392
|
+
const token = await resolveTokenMetadata(address, deps);
|
|
1393
|
+
return token ? { ...data, token } : data;
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* Resolves a token's `{ symbol, decimals }` via `token.getMetadata`, memoized per
|
|
1397
|
+
* `(chainId, address)` with a short TTL so a busy transfer stream shares one RPC
|
|
1398
|
+
* lookup per token. Returns `undefined` on any failure — metadata is a rendering
|
|
1399
|
+
* nicety, never a delivery blocker.
|
|
1400
|
+
*/
|
|
1401
|
+
async function resolveTokenMetadata(address, deps) {
|
|
1402
|
+
try {
|
|
1403
|
+
return await Store.memoize(async () => {
|
|
1404
|
+
const meta = await deps.client.token.getMetadata({ token: address });
|
|
1405
|
+
return { decimals: meta.decimals, symbol: meta.symbol };
|
|
1406
|
+
}, {
|
|
1407
|
+
key: `webhook:token-meta:v1:${deps.chainId}:${address}`,
|
|
1408
|
+
store: deps.store,
|
|
1409
|
+
ttl: Ttl.minutes(5),
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
catch {
|
|
1413
|
+
// Metadata is best-effort; fall back to base-unit rendering.
|
|
1414
|
+
return undefined;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
/** Returns the live RPC head block number for the chain. */
|
|
1418
|
+
export async function getRpcHead(client) {
|
|
1419
|
+
return Number(await client.getBlockNumber());
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Returns the highest block number TIDX has indexed for a chain (0 when none).
|
|
1423
|
+
* The legacy `token:transfer` type works back from this so it never scans past
|
|
1424
|
+
* the indexer.
|
|
1425
|
+
*/
|
|
1426
|
+
export async function getIndexedHead(tidx, chainId) {
|
|
1427
|
+
const result = await tidx.fetch({
|
|
1428
|
+
chainId,
|
|
1429
|
+
query: 'SELECT num FROM blocks ORDER BY num DESC LIMIT 1',
|
|
1430
|
+
});
|
|
1431
|
+
return Value.toNumber(result.rows[0]?.['num']) ?? 0;
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* Scans new events for one subscription. The RPC monitoring types
|
|
1435
|
+
* (`log:emitted` / `block:created` / `transaction:included`) read a confirmed
|
|
1436
|
+
* window directly over JSON-RPC; the legacy `token:transfer` type dispatches to
|
|
1437
|
+
* its TIDX read core (`Transfers.scan`).
|
|
1438
|
+
*/
|
|
1439
|
+
async function scanSubscription(options) {
|
|
1440
|
+
const { batchSize, cache, client, cursor, maxBlocks, safeBlock, subscription, tidx } = options;
|
|
1441
|
+
// Stored `chainId` was validated against `Schema.ChainId` at create time.
|
|
1442
|
+
const chainId = subscription.chainId;
|
|
1443
|
+
if (subscription.eventType === 'log:emitted')
|
|
1444
|
+
return scanLogs({
|
|
1445
|
+
cache,
|
|
1446
|
+
client,
|
|
1447
|
+
cursor,
|
|
1448
|
+
filters: parseFilters('log:emitted', subscription.filters),
|
|
1449
|
+
maxBlocks,
|
|
1450
|
+
safeBlock,
|
|
1451
|
+
});
|
|
1452
|
+
if (subscription.eventType === 'block:created')
|
|
1453
|
+
return scanBlocks({
|
|
1454
|
+
cache,
|
|
1455
|
+
cursor,
|
|
1456
|
+
filters: parseFilters('block:created', subscription.filters),
|
|
1457
|
+
maxBlocks,
|
|
1458
|
+
safeBlock,
|
|
1459
|
+
});
|
|
1460
|
+
if (subscription.eventType === 'transaction:included')
|
|
1461
|
+
return scanTxs({
|
|
1462
|
+
cache,
|
|
1463
|
+
cursor,
|
|
1464
|
+
filters: parseFilters('transaction:included', subscription.filters),
|
|
1465
|
+
maxBlocks,
|
|
1466
|
+
safeBlock,
|
|
1467
|
+
});
|
|
1468
|
+
// Legacy TIDX-backed type.
|
|
1469
|
+
if (!tidx)
|
|
1470
|
+
return { hasMore: false, items: [] };
|
|
1471
|
+
const filters = transferFilters(subscription.filters);
|
|
1472
|
+
const page = await Transfers.scan({ tidx }, { chainId, cursor, limit: batchSize, order: 'asc', toBlock: safeBlock, ...filters });
|
|
1473
|
+
return { hasMore: page.hasMore, items: page.items };
|
|
1474
|
+
}
|
|
1475
|
+
const filterSchemas = {
|
|
1476
|
+
'block:created': schema.BlockFilters,
|
|
1477
|
+
'log:emitted': schema.LogFilters,
|
|
1478
|
+
'token:transfer': schema.TransferFilters,
|
|
1479
|
+
'transaction:included': schema.TxFilters,
|
|
1480
|
+
};
|
|
1481
|
+
/**
|
|
1482
|
+
* Validates `filters` against the schema for `eventType`, **failing closed**: an
|
|
1483
|
+
* invalid filter throws {@link Webhooks.InvalidFilterError} instead of degrading
|
|
1484
|
+
* to `{}` (a firehose). Used at create/patch time and by the scanners so a stored
|
|
1485
|
+
* bad filter can never silently widen the match set.
|
|
1486
|
+
*/
|
|
1487
|
+
export function parseFilters(eventType, filters) {
|
|
1488
|
+
const result = filterSchemas[eventType].safeParse(filters ?? {});
|
|
1489
|
+
if (!result.success)
|
|
1490
|
+
throw new Webhooks.InvalidFilterError(eventType, Response.validationDetails(result.error.issues));
|
|
1491
|
+
return result.data;
|
|
1492
|
+
}
|
|
1493
|
+
/** Narrows stored `transfer` filters into validated `Transfers.scan` predicates. */
|
|
1494
|
+
function transferFilters(filters) {
|
|
1495
|
+
const parsed = schema.TransferFilters.safeParse(filters);
|
|
1496
|
+
const { address, recipient, sender, token } = parsed.success ? parsed.data : {};
|
|
1497
|
+
return {
|
|
1498
|
+
...(address === undefined ? {} : { eitherSide: address }),
|
|
1499
|
+
...(recipient === undefined ? {} : { recipient }),
|
|
1500
|
+
...(sender === undefined ? {} : { sender }),
|
|
1501
|
+
...(token === undefined ? {} : { token }),
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
/** Recursively lowercases string leaves of a filter operator (for address args). */
|
|
1505
|
+
function lowercaseLeaves(op) {
|
|
1506
|
+
if (typeof op === 'string')
|
|
1507
|
+
return op.toLowerCase();
|
|
1508
|
+
if (Array.isArray(op))
|
|
1509
|
+
return op.map(lowercaseLeaves);
|
|
1510
|
+
if (op && typeof op === 'object') {
|
|
1511
|
+
const out = {};
|
|
1512
|
+
for (const [key, value] of Object.entries(op))
|
|
1513
|
+
out[key] = lowercaseLeaves(value);
|
|
1514
|
+
return out;
|
|
1515
|
+
}
|
|
1516
|
+
return op;
|
|
1517
|
+
}
|
|
1518
|
+
/** Recursively renders decoded event args JSON-safe (bigint → decimal string). */
|
|
1519
|
+
function serializeArgs(value) {
|
|
1520
|
+
if (typeof value === 'bigint')
|
|
1521
|
+
return value.toString();
|
|
1522
|
+
if (Array.isArray(value))
|
|
1523
|
+
return value.map(serializeArgs);
|
|
1524
|
+
if (value && typeof value === 'object') {
|
|
1525
|
+
const out = {};
|
|
1526
|
+
for (const [key, inner] of Object.entries(value))
|
|
1527
|
+
out[key] = serializeArgs(inner);
|
|
1528
|
+
return out;
|
|
1529
|
+
}
|
|
1530
|
+
return value;
|
|
1531
|
+
}
|
|
1532
|
+
// Shared filter matching (JS predicates over RPC objects).
|
|
1533
|
+
/** Parses a `0x`-hex (or decimal) quantity into a bigint for numeric comparison. */
|
|
1534
|
+
const toBig = (value) => BigInt(String(value));
|
|
1535
|
+
/** Case-insensitive equality for string fields; bigint equality for numeric. */
|
|
1536
|
+
function operandEquals(value, target, kind) {
|
|
1537
|
+
return kind === 'number'
|
|
1538
|
+
? toBig(value) === toBig(target)
|
|
1539
|
+
: value.toLowerCase() === String(target).toLowerCase();
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* The JS counterpart of the old SQL `applyOperator`: evaluates one parsed filter
|
|
1543
|
+
* operator (from {@link schema.eq}/{@link schema.compare}/{@link schema.bytes})
|
|
1544
|
+
* against a single `0x`-hex field value pulled from an RPC object. The one place
|
|
1545
|
+
* operator semantics live, shared by every RPC scanner. Returns `false` for a
|
|
1546
|
+
* missing field so an absent value never matches (fail-closed).
|
|
1547
|
+
*
|
|
1548
|
+
* - bare value / `{ eq }` → equality
|
|
1549
|
+
* - `{ in: [...] }` → membership
|
|
1550
|
+
* - `{ not }` → inequality
|
|
1551
|
+
* - `{ gt|gte|lt|lte }` → numeric range (AND-combined)
|
|
1552
|
+
* - `{ selector }` → first 4 bytes (10 hex chars) equal
|
|
1553
|
+
* - `{ startsWith }` → hex prefix
|
|
1554
|
+
*/
|
|
1555
|
+
export function matchesOperator(value, op, kind = 'string') {
|
|
1556
|
+
if (value === undefined)
|
|
1557
|
+
return false;
|
|
1558
|
+
if (typeof op !== 'object' || op === null)
|
|
1559
|
+
return operandEquals(value, op, kind);
|
|
1560
|
+
const o = op;
|
|
1561
|
+
if ('selector' in o)
|
|
1562
|
+
return value.slice(0, 10).toLowerCase() === String(o['selector']).toLowerCase();
|
|
1563
|
+
if ('startsWith' in o)
|
|
1564
|
+
return value.toLowerCase().startsWith(String(o['startsWith']).toLowerCase());
|
|
1565
|
+
if ('in' in o)
|
|
1566
|
+
return o['in'].some((v) => operandEquals(value, v, kind));
|
|
1567
|
+
if ('not' in o)
|
|
1568
|
+
return !operandEquals(value, o['not'], kind);
|
|
1569
|
+
// Comparison object: AND-combine whichever of eq/gt/gte/lt/lte are present.
|
|
1570
|
+
const n = toBig(value);
|
|
1571
|
+
let ok = true;
|
|
1572
|
+
if ('eq' in o)
|
|
1573
|
+
ok = ok && n === toBig(o['eq']);
|
|
1574
|
+
if ('gt' in o)
|
|
1575
|
+
ok = ok && n > toBig(o['gt']);
|
|
1576
|
+
if ('gte' in o)
|
|
1577
|
+
ok = ok && n >= toBig(o['gte']);
|
|
1578
|
+
if ('lt' in o)
|
|
1579
|
+
ok = ok && n < toBig(o['lt']);
|
|
1580
|
+
if ('lte' in o)
|
|
1581
|
+
ok = ok && n <= toBig(o['lte']);
|
|
1582
|
+
return ok;
|
|
1583
|
+
}
|
|
1584
|
+
/** Matches an `eq`-operator against any inner account-abstraction call target. */
|
|
1585
|
+
function matchesCalls(calls, op) {
|
|
1586
|
+
const targets = (Array.isArray(calls) ? calls : [])
|
|
1587
|
+
.map((c) => (c && typeof c === 'object' ? c['to'] : undefined))
|
|
1588
|
+
.filter((to) => typeof to === 'string')
|
|
1589
|
+
.map((to) => to.toLowerCase());
|
|
1590
|
+
const has = (addr) => targets.includes(String(addr).toLowerCase());
|
|
1591
|
+
if (typeof op === 'object' && op !== null) {
|
|
1592
|
+
const o = op;
|
|
1593
|
+
if ('in' in o)
|
|
1594
|
+
return o['in'].some(has);
|
|
1595
|
+
if ('not' in o)
|
|
1596
|
+
return !has(o['not']);
|
|
1597
|
+
if ('eq' in o)
|
|
1598
|
+
return has(o['eq']);
|
|
1599
|
+
}
|
|
1600
|
+
return has(op);
|
|
1601
|
+
}
|
|
1602
|
+
// RPC window helpers + per-tick lazy cache.
|
|
1603
|
+
/** Renders a block number as a `0x`-hex tag for JSON-RPC params. */
|
|
1604
|
+
const blockTag = (n) => `0x${n.toString(16)}`;
|
|
1605
|
+
/** Issues a JSON-RPC request through the viem client for a method viem can't type. */
|
|
1606
|
+
function rpc(client, method, params) {
|
|
1607
|
+
return client.request({
|
|
1608
|
+
method,
|
|
1609
|
+
params: [...params],
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
/**
|
|
1613
|
+
* The confirmed scan window for a cursor. A cursor `(cb, ci)` means rows through
|
|
1614
|
+
* `(cb, ci)` were dispatched: when `ci` is the end-of-block sentinel the whole
|
|
1615
|
+
* block `cb` is done and scanning resumes at `cb + 1`; otherwise block `cb` is
|
|
1616
|
+
* re-scanned (skipping indices `<= ci`). `to` is capped by both `safeBlock` and
|
|
1617
|
+
* the per-tick backfill cap. Returns `undefined` when the subscription is
|
|
1618
|
+
* already caught up (nothing to scan, cursor unchanged).
|
|
1619
|
+
*/
|
|
1620
|
+
function scanWindow(cursor, safeBlock, maxBlocks) {
|
|
1621
|
+
const decoded = cursor ? Cursor.decode(cursor, ['int', 'int']) : undefined;
|
|
1622
|
+
const cb = decoded ? decoded[0] : 0;
|
|
1623
|
+
const ci = decoded ? decoded[1] : endOfBlockIndex;
|
|
1624
|
+
const from = ci >= endOfBlockIndex ? cb + 1 : cb;
|
|
1625
|
+
if (from > safeBlock)
|
|
1626
|
+
return undefined;
|
|
1627
|
+
const to = Math.min(safeBlock, from + maxBlocks - 1);
|
|
1628
|
+
return { from, skip: [cb, ci], to };
|
|
1629
|
+
}
|
|
1630
|
+
/** True when `(block, index)` is at or before the already-dispatched cursor. */
|
|
1631
|
+
function atOrBefore(block, index, skip) {
|
|
1632
|
+
return block < skip[0] || (block === skip[0] && index <= skip[1]);
|
|
1633
|
+
}
|
|
1634
|
+
/** Builds a per-tick {@link ChainCache} bound to a viem client. */
|
|
1635
|
+
function createChainCache(client) {
|
|
1636
|
+
const headers = new Map();
|
|
1637
|
+
const blocks = new Map();
|
|
1638
|
+
const receipts = new Map();
|
|
1639
|
+
return {
|
|
1640
|
+
block(n) {
|
|
1641
|
+
let p = blocks.get(n);
|
|
1642
|
+
if (!p) {
|
|
1643
|
+
// The transaction payload reuses `humanizeTransaction`, which consumes
|
|
1644
|
+
// the raw RPC tx shape, so the full block stays on the untyped path.
|
|
1645
|
+
p = rpc(client, 'eth_getBlockByNumber', [
|
|
1646
|
+
blockTag(n),
|
|
1647
|
+
true,
|
|
1648
|
+
]).then((b) => b ?? undefined);
|
|
1649
|
+
blocks.set(n, p);
|
|
1650
|
+
}
|
|
1651
|
+
return p;
|
|
1652
|
+
},
|
|
1653
|
+
header(n) {
|
|
1654
|
+
let p = headers.get(n);
|
|
1655
|
+
if (!p) {
|
|
1656
|
+
p = client
|
|
1657
|
+
.getBlock({ blockNumber: BigInt(n), includeTransactions: false })
|
|
1658
|
+
.catch(() => undefined);
|
|
1659
|
+
headers.set(n, p);
|
|
1660
|
+
}
|
|
1661
|
+
return p;
|
|
1662
|
+
},
|
|
1663
|
+
receipts(n) {
|
|
1664
|
+
let p = receipts.get(n);
|
|
1665
|
+
if (!p) {
|
|
1666
|
+
p = rpc(client, 'eth_getBlockReceipts', [blockTag(n)]).then((list) => {
|
|
1667
|
+
const map = new Map();
|
|
1668
|
+
for (const receipt of list ?? []) {
|
|
1669
|
+
const hash = receipt['transactionHash'];
|
|
1670
|
+
if (typeof hash === 'string')
|
|
1671
|
+
map.set(hash.toLowerCase(), receipt);
|
|
1672
|
+
}
|
|
1673
|
+
return map;
|
|
1674
|
+
});
|
|
1675
|
+
receipts.set(n, p);
|
|
1676
|
+
}
|
|
1677
|
+
return p;
|
|
1678
|
+
},
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* Builds the delivered `log:emitted` payload from a viem-formatted log plus the
|
|
1683
|
+
* block timestamp. Returns `undefined` when a required field is missing/invalid
|
|
1684
|
+
* (the row is skipped). When a `signature`/`event` is supplied, `getLogs` has
|
|
1685
|
+
* already decoded `args`/`eventName`, so this just serializes them.
|
|
1686
|
+
*/
|
|
1687
|
+
function toLogEvent(log, timestamp, signature, event) {
|
|
1688
|
+
const address = Schema.Address.safeParse(log.address);
|
|
1689
|
+
const transactionHash = Schema.Hash.safeParse(log.transactionHash);
|
|
1690
|
+
const blockNumber = log.blockNumber === null ? undefined : Number(log.blockNumber);
|
|
1691
|
+
const logIndex = log.logIndex ?? undefined;
|
|
1692
|
+
const transactionIndex = log.transactionIndex ?? undefined;
|
|
1693
|
+
const topics = log.topics.filter((t) => typeof t === 'string');
|
|
1694
|
+
const topic0 = Schema.Hash.safeParse(topics[0]);
|
|
1695
|
+
const data = typeof log.data === 'string' ? log.data : '0x';
|
|
1696
|
+
if (!address.success ||
|
|
1697
|
+
!transactionHash.success ||
|
|
1698
|
+
!topic0.success ||
|
|
1699
|
+
blockNumber === undefined ||
|
|
1700
|
+
logIndex === undefined ||
|
|
1701
|
+
transactionIndex === undefined ||
|
|
1702
|
+
timestamp === undefined)
|
|
1703
|
+
return undefined;
|
|
1704
|
+
const base = {
|
|
1705
|
+
address: address.data,
|
|
1706
|
+
blockNumber,
|
|
1707
|
+
data,
|
|
1708
|
+
logIndex,
|
|
1709
|
+
timestamp,
|
|
1710
|
+
topics,
|
|
1711
|
+
transactionHash: transactionHash.data,
|
|
1712
|
+
transactionIndex,
|
|
1713
|
+
};
|
|
1714
|
+
if (!signature || !event || log.args === undefined)
|
|
1715
|
+
return base;
|
|
1716
|
+
return {
|
|
1717
|
+
...base,
|
|
1718
|
+
args: serializeArgs(log.args),
|
|
1719
|
+
event: { name: event.name, signature, topic0: topic0.data },
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
/** Narrows an `eq`/`in` address operator into the `getLogs` address pushdown. */
|
|
1723
|
+
function pushAddress(op) {
|
|
1724
|
+
if (op === undefined)
|
|
1725
|
+
return undefined;
|
|
1726
|
+
if (typeof op === 'string')
|
|
1727
|
+
return op;
|
|
1728
|
+
if (typeof op === 'object' && op !== null) {
|
|
1729
|
+
const o = op;
|
|
1730
|
+
if (typeof o['eq'] === 'string')
|
|
1731
|
+
return o['eq'];
|
|
1732
|
+
if (Array.isArray(o['in']))
|
|
1733
|
+
return o['in'].filter((v) => typeof v === 'string');
|
|
1734
|
+
}
|
|
1735
|
+
// `not`/range forms can't be pushed; widen here, re-check in JS.
|
|
1736
|
+
return undefined;
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* Scans contract event logs over the confirmed window via viem's `getLogs`,
|
|
1740
|
+
* then re-checks **every** filter in JS (so operators the RPC can't express —
|
|
1741
|
+
* `not`, ranges, decoded `args` — still apply). Two modes:
|
|
1742
|
+
* - **raw** (no `signature`): `topic0..3` are matched directly.
|
|
1743
|
+
* - **decoded** (`signature` present): `getLogs` is given the `event`, so it
|
|
1744
|
+
* pushes the event selector down, filters to matching logs, and best-effort
|
|
1745
|
+
* decodes `args` — which the JS pass then matches by name.
|
|
1746
|
+
*
|
|
1747
|
+
* `address` (`eq`/`in`) is pushed down to the node; all other operators are
|
|
1748
|
+
* re-checked in JS over the narrow window.
|
|
1749
|
+
*/
|
|
1750
|
+
async function scanLogs(options) {
|
|
1751
|
+
const { cache, client, cursor, filters, maxBlocks, safeBlock } = options;
|
|
1752
|
+
const f = filters;
|
|
1753
|
+
const window = scanWindow(cursor, safeBlock, maxBlocks);
|
|
1754
|
+
if (!window)
|
|
1755
|
+
return { items: [], scannedThrough: undefined };
|
|
1756
|
+
const event = f.signature ? parseAbiItem(f.signature) : undefined;
|
|
1757
|
+
if (event && !/^[A-Za-z_]\w*$/.test(event.name))
|
|
1758
|
+
throw new Webhooks.InvalidFilterError('log:emitted', [
|
|
1759
|
+
{ message: 'Unsupported event signature.', path: ['signature'] },
|
|
1760
|
+
]);
|
|
1761
|
+
const address = pushAddress(f.address);
|
|
1762
|
+
const fromBlock = BigInt(window.from);
|
|
1763
|
+
const toBlock = BigInt(window.to);
|
|
1764
|
+
const logs = (event
|
|
1765
|
+
? await client.getLogs({
|
|
1766
|
+
...(address ? { address } : {}),
|
|
1767
|
+
event,
|
|
1768
|
+
fromBlock,
|
|
1769
|
+
strict: false,
|
|
1770
|
+
toBlock,
|
|
1771
|
+
})
|
|
1772
|
+
: await client.getLogs({
|
|
1773
|
+
...(address ? { address } : {}),
|
|
1774
|
+
fromBlock,
|
|
1775
|
+
toBlock,
|
|
1776
|
+
}));
|
|
1777
|
+
logs.sort((a, b) => {
|
|
1778
|
+
const ab = Number(a.blockNumber ?? 0n);
|
|
1779
|
+
const bb = Number(b.blockNumber ?? 0n);
|
|
1780
|
+
if (ab !== bb)
|
|
1781
|
+
return ab - bb;
|
|
1782
|
+
return (a.logIndex ?? 0) - (b.logIndex ?? 0);
|
|
1783
|
+
});
|
|
1784
|
+
const items = [];
|
|
1785
|
+
for (const log of logs) {
|
|
1786
|
+
const blockNumber = log.blockNumber === null ? undefined : Number(log.blockNumber);
|
|
1787
|
+
const logIndex = log.logIndex ?? undefined;
|
|
1788
|
+
if (blockNumber === undefined || logIndex === undefined)
|
|
1789
|
+
continue;
|
|
1790
|
+
if (atOrBefore(blockNumber, logIndex, window.skip))
|
|
1791
|
+
continue;
|
|
1792
|
+
if (!matchesLog(log, f, event))
|
|
1793
|
+
continue;
|
|
1794
|
+
const header = await cache.header(blockNumber);
|
|
1795
|
+
const timestamp = blockIso(header);
|
|
1796
|
+
const data = toLogEvent(log, timestamp, f.signature, event);
|
|
1797
|
+
if (data)
|
|
1798
|
+
items.push({ cursor: [data.blockNumber, data.logIndex], data });
|
|
1799
|
+
}
|
|
1800
|
+
return { items, scannedThrough: window.to };
|
|
1801
|
+
}
|
|
1802
|
+
/** Re-checks all log filters (including non-pushable operators) in JS. */
|
|
1803
|
+
function matchesLog(log, f, event) {
|
|
1804
|
+
const topics = log.topics;
|
|
1805
|
+
if (f.address !== undefined && !matchesOperator(log.address, f.address))
|
|
1806
|
+
return false;
|
|
1807
|
+
if (f.blockNumber !== undefined &&
|
|
1808
|
+
!matchesOperator(log.blockNumber?.toString(), f.blockNumber, 'number'))
|
|
1809
|
+
return false;
|
|
1810
|
+
// `getLogs(event)` already anchors topic0 on the event selector, so the raw
|
|
1811
|
+
// `topic0` operator only applies in raw (no-signature) mode.
|
|
1812
|
+
if (!event && f.topic0 !== undefined && !matchesOperator(topics[0], f.topic0))
|
|
1813
|
+
return false;
|
|
1814
|
+
if (f.topic1 !== undefined && !matchesOperator(topics[1], f.topic1))
|
|
1815
|
+
return false;
|
|
1816
|
+
if (f.topic2 !== undefined && !matchesOperator(topics[2], f.topic2))
|
|
1817
|
+
return false;
|
|
1818
|
+
if (f.topic3 !== undefined && !matchesOperator(topics[3], f.topic3))
|
|
1819
|
+
return false;
|
|
1820
|
+
if (event && f.args && Object.keys(f.args).length > 0) {
|
|
1821
|
+
// `getLogs(event, strict:false)` keeps non-decodable matches with no `args`.
|
|
1822
|
+
if (log.args === undefined || Array.isArray(log.args))
|
|
1823
|
+
return false;
|
|
1824
|
+
const args = log.args;
|
|
1825
|
+
for (const [name, op] of Object.entries(f.args)) {
|
|
1826
|
+
const input = event.inputs.find((i) => i.name === name);
|
|
1827
|
+
if (!input)
|
|
1828
|
+
throw new Webhooks.InvalidFilterError('log:emitted', [
|
|
1829
|
+
{ message: `Unknown event argument: ${name}.`, path: ['args', name] },
|
|
1830
|
+
]);
|
|
1831
|
+
const numeric = /^u?int/.test(input.type);
|
|
1832
|
+
const value = args[name];
|
|
1833
|
+
const normalized = input.type === 'address' ? lowercaseLeaves(op) : op;
|
|
1834
|
+
if (!matchesOperator(asString(value), normalized, numeric ? 'number' : 'string'))
|
|
1835
|
+
return false;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
return true;
|
|
1839
|
+
}
|
|
1840
|
+
// block:created (getBlock)
|
|
1841
|
+
/** Converts a viem block's `timestamp` (seconds) to an ISO 8601 string. */
|
|
1842
|
+
function blockIso(block) {
|
|
1843
|
+
if (!block || typeof block.timestamp !== 'bigint')
|
|
1844
|
+
return undefined;
|
|
1845
|
+
return new Date(Number(block.timestamp) * 1000).toISOString();
|
|
1846
|
+
}
|
|
1847
|
+
/** Builds the delivered `block:created` payload from a viem block header. */
|
|
1848
|
+
function toBlockEvent(block) {
|
|
1849
|
+
const number = block.number === null ? undefined : Number(block.number);
|
|
1850
|
+
const hash = Schema.Hash.safeParse(block.hash);
|
|
1851
|
+
const parentHash = Schema.Hash.safeParse(block.parentHash);
|
|
1852
|
+
const miner = Schema.Address.safeParse(block.miner);
|
|
1853
|
+
const gasUsed = typeof block.gasUsed === 'bigint' ? Number(block.gasUsed) : undefined;
|
|
1854
|
+
const gasLimit = typeof block.gasLimit === 'bigint' ? Number(block.gasLimit) : undefined;
|
|
1855
|
+
const timestamp = blockIso(block);
|
|
1856
|
+
const transactionCount = Array.isArray(block.transactions) ? block.transactions.length : 0;
|
|
1857
|
+
if (number === undefined ||
|
|
1858
|
+
!hash.success ||
|
|
1859
|
+
!parentHash.success ||
|
|
1860
|
+
!miner.success ||
|
|
1861
|
+
gasUsed === undefined ||
|
|
1862
|
+
gasLimit === undefined ||
|
|
1863
|
+
timestamp === undefined)
|
|
1864
|
+
return undefined;
|
|
1865
|
+
return {
|
|
1866
|
+
gasLimit,
|
|
1867
|
+
gasUsed,
|
|
1868
|
+
hash: hash.data,
|
|
1869
|
+
miner: miner.data,
|
|
1870
|
+
number,
|
|
1871
|
+
parentHash: parentHash.data,
|
|
1872
|
+
timestamp,
|
|
1873
|
+
transactionCount,
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
/**
|
|
1877
|
+
* Scans new blocks over the confirmed window via viem's `getBlock`. One row per
|
|
1878
|
+
* block (the keyset index is always 0); filters match block-level metadata. An
|
|
1879
|
+
* empty filter is a per-block heartbeat.
|
|
1880
|
+
*/
|
|
1881
|
+
async function scanBlocks(options) {
|
|
1882
|
+
const { cache, cursor, filters, maxBlocks, safeBlock } = options;
|
|
1883
|
+
const f = filters;
|
|
1884
|
+
const window = scanWindow(cursor, safeBlock, maxBlocks);
|
|
1885
|
+
if (!window)
|
|
1886
|
+
return { items: [], scannedThrough: undefined };
|
|
1887
|
+
const items = [];
|
|
1888
|
+
for (let n = window.from; n <= window.to; n++) {
|
|
1889
|
+
if (atOrBefore(n, 0, window.skip))
|
|
1890
|
+
continue;
|
|
1891
|
+
const block = await cache.header(n);
|
|
1892
|
+
if (!block)
|
|
1893
|
+
continue;
|
|
1894
|
+
if (!matchesBlock(block, f))
|
|
1895
|
+
continue;
|
|
1896
|
+
const data = toBlockEvent(block);
|
|
1897
|
+
if (data)
|
|
1898
|
+
items.push({ cursor: [data.number, 0], data });
|
|
1899
|
+
}
|
|
1900
|
+
return { items, scannedThrough: window.to };
|
|
1901
|
+
}
|
|
1902
|
+
/** Re-checks all block filters in JS. The RPC has no consensus `proposer`. */
|
|
1903
|
+
function matchesBlock(block, f) {
|
|
1904
|
+
if (f.number !== undefined && !matchesOperator(block.number?.toString(), f.number, 'number'))
|
|
1905
|
+
return false;
|
|
1906
|
+
if (f.miner !== undefined && !matchesOperator(block.miner, f.miner))
|
|
1907
|
+
return false;
|
|
1908
|
+
if (f.gasUsed !== undefined && !matchesOperator(block.gasUsed?.toString(), f.gasUsed, 'number'))
|
|
1909
|
+
return false;
|
|
1910
|
+
if (f.gasLimit !== undefined &&
|
|
1911
|
+
!matchesOperator(block.gasLimit?.toString(), f.gasLimit, 'number'))
|
|
1912
|
+
return false;
|
|
1913
|
+
if (f.timestamp !== undefined &&
|
|
1914
|
+
!matchesOperator(block.timestamp?.toString(), f.timestamp, 'number'))
|
|
1915
|
+
return false;
|
|
1916
|
+
// `proposer` is not exposed over standard RPC, so it can never match.
|
|
1917
|
+
if (f.proposer !== undefined)
|
|
1918
|
+
return false;
|
|
1919
|
+
return true;
|
|
1920
|
+
}
|
|
1921
|
+
// transaction:included (eth_getBlockByNumber + eth_getBlockReceipts)
|
|
1922
|
+
/** Maps a `transaction:included` scalar filter field to its raw-tx field + kind. */
|
|
1923
|
+
const txFields = {
|
|
1924
|
+
blockNumber: { field: 'blockNumber', kind: 'number' },
|
|
1925
|
+
feeToken: { field: 'feeToken', kind: 'string' },
|
|
1926
|
+
gasLimit: { field: 'gas', kind: 'number' },
|
|
1927
|
+
hash: { field: 'hash', kind: 'string' },
|
|
1928
|
+
maxFeePerGas: { field: 'maxFeePerGas', kind: 'number' },
|
|
1929
|
+
maxPriorityFeePerGas: { field: 'maxPriorityFeePerGas', kind: 'number' },
|
|
1930
|
+
nonce: { field: 'nonce', kind: 'number' },
|
|
1931
|
+
nonceKey: { field: 'nonceKey', kind: 'string' },
|
|
1932
|
+
txType: { field: 'type', kind: 'string' },
|
|
1933
|
+
validAfter: { field: 'validAfter', kind: 'number' },
|
|
1934
|
+
validBefore: { field: 'validBefore', kind: 'number' },
|
|
1935
|
+
value: { field: 'value', kind: 'number' },
|
|
1936
|
+
};
|
|
1937
|
+
/**
|
|
1938
|
+
* Scans included transactions over the confirmed window. Each block is fetched
|
|
1939
|
+
* with full transactions via `eth_getBlockByNumber(n, true)`; receipts are
|
|
1940
|
+
* fetched lazily (only when a `status`/`gasUsed`/`feePayer` filter needs them).
|
|
1941
|
+
* The delivered payload reuses `Transactions.humanizeTransaction`, so it is
|
|
1942
|
+
* byte-identical to the read endpoint.
|
|
1943
|
+
*/
|
|
1944
|
+
async function scanTxs(options) {
|
|
1945
|
+
const { cache, cursor, filters, maxBlocks, safeBlock } = options;
|
|
1946
|
+
const f = filters;
|
|
1947
|
+
const window = scanWindow(cursor, safeBlock, maxBlocks);
|
|
1948
|
+
if (!window)
|
|
1949
|
+
return { items: [], scannedThrough: undefined };
|
|
1950
|
+
const includeCalls = f.includeCalls ?? true;
|
|
1951
|
+
const includeReceipt = f.includeReceipt ?? false;
|
|
1952
|
+
// Filter-driven receipt need gates matching; `includeReceipt` only enriches the
|
|
1953
|
+
// payload, so it never drops a row when the receipt is missing.
|
|
1954
|
+
const needsReceipt = f.status !== undefined || f.gasUsed !== undefined || f.feePayer !== undefined;
|
|
1955
|
+
const items = [];
|
|
1956
|
+
for (let n = window.from; n <= window.to; n++) {
|
|
1957
|
+
const block = await cache.block(n);
|
|
1958
|
+
const txs = Array.isArray(block?.['transactions']) ? block['transactions'] : [];
|
|
1959
|
+
if (txs.length === 0)
|
|
1960
|
+
continue;
|
|
1961
|
+
const receipts = needsReceipt || includeReceipt ? await cache.receipts(n) : undefined;
|
|
1962
|
+
const timestamp = asString(block?.['timestamp']);
|
|
1963
|
+
for (let i = 0; i < txs.length; i++) {
|
|
1964
|
+
const tx = txs[i];
|
|
1965
|
+
const index = Value.hexToNumber(tx['transactionIndex']) ?? i;
|
|
1966
|
+
if (atOrBefore(n, index, window.skip))
|
|
1967
|
+
continue;
|
|
1968
|
+
const receipt = receipts?.get(String(tx['hash']).toLowerCase());
|
|
1969
|
+
if (needsReceipt && !receipt)
|
|
1970
|
+
continue;
|
|
1971
|
+
if (!matchesTx(tx, f, includeCalls, receipt, timestamp, n))
|
|
1972
|
+
continue;
|
|
1973
|
+
const parsed = Transactions.schema.Rpc.Transaction.safeParse(tx);
|
|
1974
|
+
if (!parsed.success)
|
|
1975
|
+
continue;
|
|
1976
|
+
let data = Transactions.humanizeTransaction(parsed.data);
|
|
1977
|
+
// Embed the humanized receipt under `meta.receipt`, matching the read
|
|
1978
|
+
// endpoint's `include=receipt` shape so `data` stays a valid envelope.
|
|
1979
|
+
if (includeReceipt && receipt) {
|
|
1980
|
+
const r = Receipts.schema.Rpc.Receipt.safeParse(receipt);
|
|
1981
|
+
if (r.success)
|
|
1982
|
+
data = { ...data, meta: { ...data.meta, receipt: Receipts.humanizeReceipt(r.data) } };
|
|
1983
|
+
}
|
|
1984
|
+
items.push({ cursor: [n, index], data });
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
return { items, scannedThrough: window.to };
|
|
1988
|
+
}
|
|
1989
|
+
/** Re-checks all transaction filters in JS against the raw RPC tx (+ receipt). */
|
|
1990
|
+
function matchesTx(tx, f, includeCalls, receipt, timestamp, blockNumber) {
|
|
1991
|
+
const from = tx['from'];
|
|
1992
|
+
const to = tx['to'];
|
|
1993
|
+
const calls = tx['calls'];
|
|
1994
|
+
if (f.from !== undefined && !matchesOperator(asString(from), f.from))
|
|
1995
|
+
return false;
|
|
1996
|
+
if (f.to !== undefined) {
|
|
1997
|
+
const direct = matchesOperator(asString(to), f.to);
|
|
1998
|
+
if (!direct && !(includeCalls && matchesCalls(calls, f.to)))
|
|
1999
|
+
return false;
|
|
2000
|
+
}
|
|
2001
|
+
if (f.address !== undefined) {
|
|
2002
|
+
const direct = matchesOperator(asString(from), f.address) || matchesOperator(asString(to), f.address);
|
|
2003
|
+
if (!direct && !(includeCalls && matchesCalls(calls, f.address)))
|
|
2004
|
+
return false;
|
|
2005
|
+
}
|
|
2006
|
+
if (f.calls?.to !== undefined && !matchesCalls(calls, f.calls.to))
|
|
2007
|
+
return false;
|
|
2008
|
+
if (f.input !== undefined && !matchesOperator(asString(tx['input']), f.input))
|
|
2009
|
+
return false;
|
|
2010
|
+
if (f.callCount !== undefined) {
|
|
2011
|
+
const count = Array.isArray(calls) ? calls.length : 0;
|
|
2012
|
+
if (!matchesOperator(String(count), f.callCount, 'number'))
|
|
2013
|
+
return false;
|
|
2014
|
+
}
|
|
2015
|
+
for (const [name, spec] of Object.entries(txFields)) {
|
|
2016
|
+
const op = f[name];
|
|
2017
|
+
if (op === undefined)
|
|
2018
|
+
continue;
|
|
2019
|
+
const value = name === 'blockNumber'
|
|
2020
|
+
? (asString(tx['blockNumber']) ?? blockNumber.toString())
|
|
2021
|
+
: asString(tx[spec.field]);
|
|
2022
|
+
if (!matchesOperator(value, op, spec.kind))
|
|
2023
|
+
return false;
|
|
2024
|
+
}
|
|
2025
|
+
if (f.timestamp !== undefined && !matchesOperator(timestamp, f.timestamp, 'number'))
|
|
2026
|
+
return false;
|
|
2027
|
+
if (receipt) {
|
|
2028
|
+
if (f.feePayer !== undefined && !matchesOperator(asString(receipt['feePayer']), f.feePayer))
|
|
2029
|
+
return false;
|
|
2030
|
+
if (f.gasUsed !== undefined &&
|
|
2031
|
+
!matchesOperator(asString(receipt['gasUsed']), f.gasUsed, 'number'))
|
|
2032
|
+
return false;
|
|
2033
|
+
if (f.status !== undefined) {
|
|
2034
|
+
const ok = Value.hexToNumber(receipt['status']) === 1;
|
|
2035
|
+
if ((f.status === 'success') !== ok)
|
|
2036
|
+
return false;
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
return true;
|
|
2040
|
+
}
|
|
2041
|
+
/** Coerces an RPC field to a string for matching, or `undefined` when absent. */
|
|
2042
|
+
function asString(value) {
|
|
2043
|
+
return typeof value === 'string' ? value : undefined;
|
|
2044
|
+
}
|
|
2045
|
+
function ownerFor(c) {
|
|
2046
|
+
const principal = Auth.getPrincipal(c);
|
|
2047
|
+
if (!principal)
|
|
2048
|
+
return null;
|
|
2049
|
+
if (principal.type === 'api_key')
|
|
2050
|
+
return { orgId: principal.orgId, type: 'api_key' };
|
|
2051
|
+
const payer = principal.payment?.payer;
|
|
2052
|
+
return payer ? { payer, type: 'mpp' } : null;
|
|
2053
|
+
}
|
|
2054
|
+
/** Strips internal fields (`owner`, `secret`) from a subscription for reads. */
|
|
2055
|
+
function toPublic(subscription) {
|
|
2056
|
+
return {
|
|
2057
|
+
chainId: subscription.chainId,
|
|
2058
|
+
...(subscription.context === undefined ? {} : { context: subscription.context }),
|
|
2059
|
+
createdAt: subscription.createdAt,
|
|
2060
|
+
destination: subscription.destination,
|
|
2061
|
+
eventType: subscription.eventType,
|
|
2062
|
+
...(subscription.expiresAt === undefined ? {} : { expiresAt: subscription.expiresAt }),
|
|
2063
|
+
failureCount: subscription.failureCount,
|
|
2064
|
+
filters: subscription.filters,
|
|
2065
|
+
id: subscription.id,
|
|
2066
|
+
...(subscription.lastDeliveryAt === undefined
|
|
2067
|
+
? {}
|
|
2068
|
+
: { lastDeliveryAt: subscription.lastDeliveryAt }),
|
|
2069
|
+
status: subscription.status,
|
|
2070
|
+
updatedAt: subscription.updatedAt,
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
/** Public read shape plus the one-time signing secret (creation only). */
|
|
2074
|
+
function toCreated(subscription) {
|
|
2075
|
+
return { ...toPublic(subscription), secret: subscription.secret };
|
|
2076
|
+
}
|
|
2077
|
+
function mutationError(c, cause) {
|
|
2078
|
+
if (cause instanceof Webhooks.InvalidUrlError)
|
|
2079
|
+
return Response.error(c, { code: 'url_invalid', message: cause.message, status: 400 });
|
|
2080
|
+
if (cause instanceof Webhooks.InvalidFilterError)
|
|
2081
|
+
return Response.error(c, {
|
|
2082
|
+
code: 'filters_invalid',
|
|
2083
|
+
details: cause.details,
|
|
2084
|
+
message: cause.message,
|
|
2085
|
+
status: 400,
|
|
2086
|
+
});
|
|
2087
|
+
if (cause instanceof Webhooks.LimitExceededError)
|
|
2088
|
+
return Response.error(c, { code: 'limit_exceeded', message: cause.message, status: 403 });
|
|
2089
|
+
return Response.upstream(c, cause);
|
|
2090
|
+
}
|
|
2091
|
+
function notFound(c) {
|
|
2092
|
+
return Response.error(c, {
|
|
2093
|
+
code: 'webhook_not_found',
|
|
2094
|
+
message: 'Webhook not found',
|
|
2095
|
+
status: 404,
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
function notEnabled(c) {
|
|
2099
|
+
return Response.error(c, {
|
|
2100
|
+
code: 'webhooks_not_enabled',
|
|
2101
|
+
message: 'Webhooks are not enabled for this Tempo API deployment.',
|
|
2102
|
+
status: 404,
|
|
2103
|
+
});
|
|
2104
|
+
}
|
|
2105
|
+
function unauthorized(c) {
|
|
2106
|
+
return Response.error(c, {
|
|
2107
|
+
code: 'unauthorized',
|
|
2108
|
+
message: 'Tempo could not determine which authenticated account owns this webhook.',
|
|
2109
|
+
status: 401,
|
|
2110
|
+
});
|
|
2111
|
+
}
|
|
2112
|
+
//# sourceMappingURL=webhooks.js.map
|