tapimo 0.10.2 → 0.12.0
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/dist/App.d.ts +4896 -2166
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +2 -2
- package/dist/App.js.map +1 -1
- package/dist/Assets.d.ts +15 -0
- package/dist/Assets.d.ts.map +1 -1
- package/dist/Assets.js +7 -0
- package/dist/Assets.js.map +1 -1
- package/dist/Webhooks.d.ts +0 -75
- package/dist/Webhooks.d.ts.map +1 -1
- package/dist/Webhooks.js +1 -143
- package/dist/Webhooks.js.map +1 -1
- package/dist/admin/App.d.ts +127 -78
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/earn-vaults.d.ts +115 -1
- package/dist/admin/apps/earn-vaults.d.ts.map +1 -1
- package/dist/admin/apps/earn-vaults.js +59 -6
- package/dist/admin/apps/earn-vaults.js.map +1 -1
- package/dist/admin/apps/organizations.d.ts +3 -3
- package/dist/admin/apps/organizations.d.ts.map +1 -1
- package/dist/admin/apps/organizations.js +4 -2
- package/dist/admin/apps/organizations.js.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +83 -83
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.d.ts.map +1 -1
- package/dist/admin/ui.generated.js +151 -151
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/apps/data/App.d.ts +5924 -4310
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +61 -9
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/ZoneSelection.d.ts +62 -0
- package/dist/apps/data/ZoneSelection.d.ts.map +1 -0
- package/dist/apps/data/ZoneSelection.js +44 -0
- package/dist/apps/data/ZoneSelection.js.map +1 -0
- package/dist/apps/data/routes/activities.d.ts +2368 -5
- package/dist/apps/data/routes/activities.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.js +423 -114
- package/dist/apps/data/routes/activities.js.map +1 -1
- package/dist/apps/data/routes/addresses.d.ts +131 -0
- package/dist/apps/data/routes/addresses.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +25 -25
- package/dist/apps/data/routes/balances.js +1 -1
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/blocks.d.ts +1 -1
- package/dist/apps/data/routes/blocks.js +1 -1
- package/dist/apps/data/routes/blocks.js.map +1 -1
- package/dist/apps/data/routes/earn.d.ts +135 -10
- package/dist/apps/data/routes/earn.d.ts.map +1 -1
- package/dist/apps/data/routes/earn.js +157 -25
- package/dist/apps/data/routes/earn.js.map +1 -1
- package/dist/apps/data/routes/exchanges.d.ts +829 -123
- package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
- package/dist/apps/data/routes/exchanges.js +16 -13
- package/dist/apps/data/routes/exchanges.js.map +1 -1
- package/dist/apps/data/routes/fee-amm.d.ts +26 -26
- package/dist/apps/data/routes/fee-amm.d.ts.map +1 -1
- package/dist/apps/data/routes/fee-amm.js +9 -11
- package/dist/apps/data/routes/fee-amm.js.map +1 -1
- package/dist/apps/data/routes/receipts.d.ts +54 -54
- package/dist/apps/data/routes/receipts.js +1 -1
- package/dist/apps/data/routes/receipts.js.map +1 -1
- package/dist/apps/data/routes/rpc.js +24 -10
- package/dist/apps/data/routes/rpc.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts +12 -12
- package/dist/apps/data/routes/tokenlist.d.ts.map +1 -1
- package/dist/apps/data/routes/tokenlist.js +16 -15
- package/dist/apps/data/routes/tokenlist.js.map +1 -1
- package/dist/apps/data/routes/tokens.d.ts +17 -3
- package/dist/apps/data/routes/tokens.d.ts.map +1 -1
- package/dist/apps/data/routes/tokens.js +34 -15
- package/dist/apps/data/routes/tokens.js.map +1 -1
- package/dist/apps/data/routes/transactions.d.ts +1 -1
- package/dist/apps/data/routes/transactions.js +1 -1
- package/dist/apps/data/routes/transactions.js.map +1 -1
- package/dist/apps/data/routes/transfers.d.ts +13 -13
- package/dist/apps/data/routes/transfers.d.ts.map +1 -1
- package/dist/apps/data/routes/transfers.js +7 -9
- package/dist/apps/data/routes/transfers.js.map +1 -1
- package/dist/apps/data/routes/valuation.d.ts +21 -21
- package/dist/apps/data/routes/verified-tokens.d.ts +5 -5
- package/dist/apps/data/routes/webhooks.d.ts +1011 -151
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +126 -739
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/data/routes/zones.d.ts +15 -15
- package/dist/apps/funding/App.d.ts +27 -27
- package/dist/apps/funding/routes/chains.d.ts.map +1 -1
- package/dist/apps/funding/routes/chains.js +5 -4
- package/dist/apps/funding/routes/chains.js.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.d.ts +18 -18
- package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.js +8 -8
- package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
- package/dist/apps/funding/routes/providers.d.ts.map +1 -1
- package/dist/apps/funding/routes/providers.js +3 -2
- package/dist/apps/funding/routes/providers.js.map +1 -1
- package/dist/apps/funding/routes/quotes.d.ts.map +1 -1
- package/dist/apps/funding/routes/quotes.js +2 -2
- package/dist/apps/funding/routes/quotes.js.map +1 -1
- package/dist/apps/funding/routes/transfers.d.ts +33 -33
- package/dist/apps/management/App.d.ts +1433 -322
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/Billing.d.ts.map +1 -1
- package/dist/apps/management/Billing.js +19 -6
- package/dist/apps/management/Billing.js.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +154 -94
- package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.js +76 -30
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/billing.d.ts +103 -30
- package/dist/apps/management/routes/billing.d.ts.map +1 -1
- package/dist/apps/management/routes/billing.js +91 -32
- package/dist/apps/management/routes/billing.js.map +1 -1
- package/dist/apps/management/routes/faucet.d.ts +12 -12
- package/dist/apps/management/routes/faucet.d.ts.map +1 -1
- package/dist/apps/management/routes/faucet.js +23 -12
- package/dist/apps/management/routes/faucet.js.map +1 -1
- package/dist/apps/management/routes/invitations.d.ts +46 -13
- package/dist/apps/management/routes/invitations.d.ts.map +1 -1
- package/dist/apps/management/routes/invitations.js +79 -29
- package/dist/apps/management/routes/invitations.js.map +1 -1
- package/dist/apps/management/routes/invite-links.d.ts +128 -83
- package/dist/apps/management/routes/invite-links.d.ts.map +1 -1
- package/dist/apps/management/routes/invite-links.js +100 -41
- package/dist/apps/management/routes/invite-links.js.map +1 -1
- package/dist/apps/management/routes/me.d.ts +5 -5
- package/dist/apps/management/routes/me.d.ts.map +1 -1
- package/dist/apps/management/routes/me.js +14 -10
- package/dist/apps/management/routes/me.js.map +1 -1
- package/dist/apps/management/routes/members.d.ts +68 -47
- package/dist/apps/management/routes/members.d.ts.map +1 -1
- package/dist/apps/management/routes/members.js +37 -18
- package/dist/apps/management/routes/members.js.map +1 -1
- package/dist/apps/management/routes/orgs.d.ts +48 -8
- package/dist/apps/management/routes/orgs.d.ts.map +1 -1
- package/dist/apps/management/routes/orgs.js +48 -19
- package/dist/apps/management/routes/orgs.js.map +1 -1
- package/dist/apps/management/routes/projects.d.ts +130 -68
- package/dist/apps/management/routes/projects.d.ts.map +1 -1
- package/dist/apps/management/routes/projects.js +72 -21
- package/dist/apps/management/routes/projects.js.map +1 -1
- package/dist/apps/management/routes/scopes.d.ts.map +1 -1
- package/dist/apps/management/routes/scopes.js +8 -5
- package/dist/apps/management/routes/scopes.js.map +1 -1
- package/dist/apps/management/routes/usage.d.ts +38 -38
- package/dist/apps/management/routes/usage.d.ts.map +1 -1
- package/dist/apps/management/routes/usage.js +41 -31
- package/dist/apps/management/routes/usage.js.map +1 -1
- package/dist/apps/management/routes/verified-token-requests.d.ts +34 -6
- package/dist/apps/management/routes/verified-token-requests.d.ts.map +1 -1
- package/dist/apps/management/routes/verified-token-requests.js +36 -17
- package/dist/apps/management/routes/verified-token-requests.js.map +1 -1
- package/dist/apps/management/routes/webhooks.d.ts +1186 -197
- package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/management/routes/webhooks.js +106 -38
- package/dist/apps/management/routes/webhooks.js.map +1 -1
- package/dist/apps/mpp/internal/Adapter.d.ts +1 -1
- package/dist/apps/mpp/internal/Adapter.d.ts.map +1 -1
- package/dist/apps/mpp/internal/Adapter.js +12 -0
- package/dist/apps/mpp/internal/Adapter.js.map +1 -1
- package/dist/db/Db.d.ts +2 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +61 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +30 -2
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +28 -3
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/earnVaults.d.ts +5 -1
- package/dist/db/tables/earnVaults.d.ts.map +1 -1
- package/dist/db/tables/earnVaults.js +5 -0
- package/dist/db/tables/earnVaults.js.map +1 -1
- package/dist/db/tables/webhookDeliveries.d.ts.map +1 -1
- package/dist/db/tables/webhookDeliveries.js +10 -0
- package/dist/db/tables/webhookDeliveries.js.map +1 -1
- package/dist/db/tables/webhookQueueCompletions.d.ts +15 -0
- package/dist/db/tables/webhookQueueCompletions.d.ts.map +1 -0
- package/dist/db/tables/webhookQueueCompletions.js +32 -0
- package/dist/db/tables/webhookQueueCompletions.js.map +1 -0
- package/dist/db/tables/webhookQueueEvents.d.ts +16 -20
- package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -1
- package/dist/db/tables/webhookQueueEvents.js +68 -43
- package/dist/db/tables/webhookQueueEvents.js.map +1 -1
- package/dist/db/tables/webhookSubscriptions.d.ts +47 -72
- package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
- package/dist/db/tables/webhookSubscriptions.js +92 -105
- package/dist/db/tables/webhookSubscriptions.js.map +1 -1
- package/dist/internal/Auth.d.ts +8 -1
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +13 -3
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Cache.d.ts.map +1 -1
- package/dist/internal/Cache.js +11 -7
- package/dist/internal/Cache.js.map +1 -1
- package/dist/internal/EarnVaults.d.ts +41 -2
- package/dist/internal/EarnVaults.d.ts.map +1 -1
- package/dist/internal/EarnVaults.js +167 -0
- package/dist/internal/EarnVaults.js.map +1 -1
- package/dist/internal/MetricSink.d.ts +35 -125
- package/dist/internal/MetricSink.d.ts.map +1 -1
- package/dist/internal/MetricSink.js +50 -121
- package/dist/internal/MetricSink.js.map +1 -1
- package/dist/internal/OpenApi.d.ts +26 -22
- package/dist/internal/OpenApi.d.ts.map +1 -1
- package/dist/internal/OpenApi.js +128 -39
- package/dist/internal/OpenApi.js.map +1 -1
- package/dist/internal/Response.d.ts +13 -8
- package/dist/internal/Response.d.ts.map +1 -1
- package/dist/internal/Response.js +16 -5
- package/dist/internal/Response.js.map +1 -1
- package/dist/internal/Schema.d.ts +8 -8
- package/dist/internal/Schema.d.ts.map +1 -1
- package/dist/internal/Schema.js +10 -9
- package/dist/internal/Schema.js.map +1 -1
- package/dist/internal/Store.d.ts +15 -6
- package/dist/internal/Store.d.ts.map +1 -1
- package/dist/internal/Store.js +45 -15
- package/dist/internal/Store.js.map +1 -1
- package/dist/internal/Tidx.d.ts +2 -0
- package/dist/internal/Tidx.d.ts.map +1 -1
- package/dist/internal/Tidx.js +16 -4
- package/dist/internal/Tidx.js.map +1 -1
- package/dist/internal/WebhookTransfer.d.ts +1 -55
- package/dist/internal/WebhookTransfer.d.ts.map +1 -1
- package/dist/internal/WebhookTransfer.js +1 -239
- package/dist/internal/WebhookTransfer.js.map +1 -1
- package/dist/internal/Webhooks.d.ts +112 -56
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +124 -93
- package/dist/internal/Webhooks.js.map +1 -1
- package/dist/internal/funding/Deposit.d.ts +2 -2
- package/dist/internal/funding/Deposit.d.ts.map +1 -1
- package/dist/internal/funding/Deposit.js +9 -8
- package/dist/internal/funding/Deposit.js.map +1 -1
- package/dist/internal/funding/DepositAddress.d.ts +4 -4
- package/dist/internal/funding/DepositAddress.d.ts.map +1 -1
- package/dist/internal/funding/DepositAddress.js +9 -8
- package/dist/internal/funding/DepositAddress.js.map +1 -1
- package/dist/internal/funding/DepositWebhook.d.ts +25 -0
- package/dist/internal/funding/DepositWebhook.d.ts.map +1 -0
- package/dist/internal/funding/DepositWebhook.js +74 -0
- package/dist/internal/funding/DepositWebhook.js.map +1 -0
- package/dist/internal/funding/Provider.d.ts.map +1 -1
- package/dist/internal/funding/Provider.js +13 -12
- package/dist/internal/funding/Provider.js.map +1 -1
- package/dist/internal/funding/Reconciliation.d.ts +6 -0
- package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
- package/dist/internal/funding/Reconciliation.js +42 -10
- package/dist/internal/funding/Reconciliation.js.map +1 -1
- package/dist/internal/funding/index.d.ts +1 -0
- package/dist/internal/funding/index.d.ts.map +1 -1
- package/dist/internal/funding/index.js +1 -0
- package/dist/internal/funding/index.js.map +1 -1
- package/dist/internal/funding/providers/stargate.d.ts +19 -0
- package/dist/internal/funding/providers/stargate.d.ts.map +1 -0
- package/dist/internal/funding/providers/stargate.js +143 -0
- package/dist/internal/funding/providers/stargate.js.map +1 -0
- package/dist/internal/webhookDestinations/slack.d.ts.map +1 -1
- package/dist/internal/webhookDestinations/slack.js +16 -0
- package/dist/internal/webhookDestinations/slack.js.map +1 -1
- package/dist/providers/index.d.ts +7 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +3 -0
- package/dist/providers/index.js.map +1 -1
- package/package.json +2 -2
- package/src/App.test.ts +48 -0
- package/src/App.ts +7 -2
- package/src/Assets.test.ts +18 -0
- package/src/Assets.ts +22 -0
- package/src/Client.test-d.ts +287 -17
- package/src/Webhooks.ts +1 -220
- package/src/admin/apps/earn-vaults.test.ts +143 -0
- package/src/admin/apps/earn-vaults.ts +83 -6
- package/src/admin/apps/organizations.ts +6 -2
- package/src/admin/ui/src/routes/api-keys.tsx +36 -0
- package/src/admin/ui.generated.ts +151 -189
- package/src/apps/data/App.test.ts +31 -0
- package/src/apps/data/App.ts +64 -10
- package/src/apps/data/ZoneSelection.test.ts +196 -0
- package/src/apps/data/ZoneSelection.ts +108 -0
- package/src/apps/data/routes/activities.test.ts +435 -12
- package/src/apps/data/routes/activities.ts +562 -125
- package/src/apps/data/routes/balances.ts +1 -1
- package/src/apps/data/routes/blocks.ts +1 -1
- package/src/apps/data/routes/earn.test.ts +183 -23
- package/src/apps/data/routes/earn.ts +225 -29
- package/src/apps/data/routes/earn.zone-access.test.ts +46 -0
- package/src/apps/data/routes/exchanges.test.ts +31 -1
- package/src/apps/data/routes/exchanges.ts +19 -15
- package/src/apps/data/routes/fee-amm.ts +18 -11
- package/src/apps/data/routes/receipts.ts +1 -1
- package/src/apps/data/routes/rpc.test.ts +75 -0
- package/src/apps/data/routes/rpc.ts +23 -9
- package/src/apps/data/routes/tokenlist.ts +21 -18
- package/src/apps/data/routes/tokens.test.ts +29 -0
- package/src/apps/data/routes/tokens.ts +38 -15
- package/src/apps/data/routes/transactions.ts +1 -1
- package/src/apps/data/routes/transfers.test.ts +3 -3
- package/src/apps/data/routes/transfers.ts +12 -9
- package/src/apps/data/routes/webhooks.test.ts +179 -1497
- package/src/apps/data/routes/webhooks.ts +156 -976
- package/src/apps/data/routes/zones.test.ts +19 -13
- package/src/apps/funding/routes/chains.test.ts +211 -7
- package/src/apps/funding/routes/chains.ts +19 -12
- package/src/apps/funding/routes/deposit-addresses.test.ts +394 -0
- package/src/apps/funding/routes/deposit-addresses.ts +84 -72
- package/src/apps/funding/routes/providers.ts +9 -5
- package/src/apps/funding/routes/quotes.test.ts +46 -0
- package/src/apps/funding/routes/quotes.ts +8 -5
- package/src/apps/funding/routes/transfers.test.ts +35 -32
- package/src/apps/management/Billing.test.ts +26 -0
- package/src/apps/management/Billing.ts +24 -7
- package/src/apps/management/routes/api-keys.test.ts +178 -0
- package/src/apps/management/routes/api-keys.ts +203 -144
- package/src/apps/management/routes/billing.test.ts +209 -0
- package/src/apps/management/routes/billing.ts +254 -177
- package/src/apps/management/routes/faucet.test.ts +39 -0
- package/src/apps/management/routes/faucet.ts +75 -41
- package/src/apps/management/routes/invitations.test.ts +216 -0
- package/src/apps/management/routes/invitations.ts +186 -109
- package/src/apps/management/routes/invite-links.test.ts +263 -1
- package/src/apps/management/routes/invite-links.ts +295 -214
- package/src/apps/management/routes/me.test.ts +49 -0
- package/src/apps/management/routes/me.ts +42 -32
- package/src/apps/management/routes/members.test.ts +124 -1
- package/src/apps/management/routes/members.ts +81 -51
- package/src/apps/management/routes/orgs.test.ts +176 -0
- package/src/apps/management/routes/orgs.ts +101 -59
- package/src/apps/management/routes/projects.test.ts +183 -0
- package/src/apps/management/routes/projects.ts +124 -60
- package/src/apps/management/routes/scopes.test.ts +44 -0
- package/src/apps/management/routes/scopes.ts +37 -28
- package/src/apps/management/routes/usage.test.ts +92 -0
- package/src/apps/management/routes/usage.ts +186 -151
- package/src/apps/management/routes/verified-token-requests.test.ts +114 -0
- package/src/apps/management/routes/verified-token-requests.ts +166 -133
- package/src/apps/management/routes/webhooks.test.ts +313 -1
- package/src/apps/management/routes/webhooks.ts +220 -71
- package/src/apps/mpp/internal/Adapter.ts +13 -1
- package/src/db/Db.test.ts +42 -5
- package/src/db/Db.ts +85 -0
- package/src/db/Schema.ts +41 -3
- package/src/db/tables/earnVaults.test.ts +22 -0
- package/src/db/tables/earnVaults.ts +11 -1
- package/src/db/tables/organizations.test.ts +5 -2
- package/src/db/tables/webhookDeliveries.ts +13 -0
- package/src/db/tables/webhookQueueCompletions.ts +55 -0
- package/src/db/tables/webhookQueueEvents.ts +97 -57
- package/src/db/tables/webhookSubscriptions.ts +134 -144
- package/src/internal/Auth.ts +23 -5
- package/src/internal/Cache.ts +14 -7
- package/src/internal/EarnVaults.capabilities.test.ts +33 -0
- package/src/internal/EarnVaults.test.ts +79 -0
- package/src/internal/EarnVaults.ts +277 -0
- package/src/internal/MetricSink.test.ts +41 -373
- package/src/internal/MetricSink.ts +92 -273
- package/src/internal/OpenApi.ts +165 -49
- package/src/internal/Response.test.ts +95 -0
- package/src/internal/Response.ts +40 -2
- package/src/internal/Schema.test.ts +34 -0
- package/src/internal/Schema.ts +20 -9
- package/src/internal/Store.test.ts +186 -0
- package/src/internal/Store.ts +57 -17
- package/src/internal/Tidx.test.ts +19 -0
- package/src/internal/Tidx.ts +16 -4
- package/src/internal/WebhookTransfer.ts +1 -351
- package/src/internal/Webhooks.test.ts +222 -145
- package/src/internal/Webhooks.ts +234 -135
- package/src/internal/funding/Deposit.ts +71 -64
- package/src/internal/funding/DepositAddress.ts +50 -43
- package/src/internal/funding/DepositWebhook.test.ts +131 -0
- package/src/internal/funding/DepositWebhook.ts +101 -0
- package/src/internal/funding/Provider.ts +164 -142
- package/src/internal/funding/Reconciliation.test.ts +14 -0
- package/src/internal/funding/Reconciliation.ts +161 -86
- package/src/internal/funding/index.test.ts +1 -0
- package/src/internal/funding/index.ts +1 -0
- package/src/internal/funding/providers/index.test.ts +185 -0
- package/src/internal/funding/providers/stargate.ts +184 -0
- package/src/internal/webhookDestinations/slack.ts +15 -0
- package/src/providers/index.test.ts +1 -0
- package/src/providers/index.ts +9 -0
- package/src/Webhooks.bench.ts +0 -714
- package/src/Webhooks.memory.bench.ts +0 -250
- package/src/Webhooks.test.ts +0 -146
|
@@ -3,11 +3,10 @@ import { type AbiEvent, decodeEventLog, encodeEventTopics, parseAbiItem, type Rp
|
|
|
3
3
|
import * as z from 'zod/mini'
|
|
4
4
|
|
|
5
5
|
import type * as App from '../../../App.js'
|
|
6
|
+
import * as Scope from '../../../Scope.js'
|
|
6
7
|
import * as Auth from '../../../internal/Auth.js'
|
|
7
8
|
import * as Cache from '../../../internal/Cache.js'
|
|
8
|
-
import * as Cursor from '../../../internal/Cursor.js'
|
|
9
9
|
import * as Db from '../../../db/Db.js'
|
|
10
|
-
import type * as MetricSink from '../../../internal/MetricSink.js'
|
|
11
10
|
import * as OpenApi from '../../../internal/OpenApi.js'
|
|
12
11
|
import * as Response from '../../../internal/Response.js'
|
|
13
12
|
import * as Schema from '../../../internal/Schema.js'
|
|
@@ -18,6 +17,7 @@ import type * as Viem from '../../../internal/Viem.js'
|
|
|
18
17
|
import * as WebhookDestination from '../../../internal/WebhookDestination.js'
|
|
19
18
|
import * as WebhookTransfer from '../../../internal/WebhookTransfer.js'
|
|
20
19
|
import * as Webhooks from '../../../internal/Webhooks.js'
|
|
20
|
+
import * as FundingDeposit from '../../../internal/funding/Deposit.js'
|
|
21
21
|
import * as Transactions from './transactions.js'
|
|
22
22
|
import * as Transfers from './transfers.js'
|
|
23
23
|
|
|
@@ -41,6 +41,7 @@ const redactedSlackUrl = 'https://hooks.slack.com/…'
|
|
|
41
41
|
*/
|
|
42
42
|
export const eventTypes = [
|
|
43
43
|
'block:created',
|
|
44
|
+
'funding:deposit.updated',
|
|
44
45
|
'log:emitted',
|
|
45
46
|
'token:transfer',
|
|
46
47
|
'transaction:included',
|
|
@@ -56,6 +57,11 @@ const eventTypeDescriptions = [
|
|
|
56
57
|
'A new block was added to the chain. Filter by producer, gas usage, or block number, or omit filters for a per-block heartbeat.',
|
|
57
58
|
type: 'block:created',
|
|
58
59
|
},
|
|
60
|
+
{
|
|
61
|
+
description:
|
|
62
|
+
'A funding deposit materially changed after verified reconciliation, filterable by deposit address, recipient, or status.',
|
|
63
|
+
type: 'funding:deposit.updated',
|
|
64
|
+
},
|
|
59
65
|
{
|
|
60
66
|
description:
|
|
61
67
|
'Any contract event log, filtered by emitting address, event signature/topics, or decoded arguments.',
|
|
@@ -447,6 +453,18 @@ export namespace schema {
|
|
|
447
453
|
z.meta({ title: 'Block filters' }),
|
|
448
454
|
)
|
|
449
455
|
|
|
456
|
+
/** `funding:deposit.updated` subscription filters. All are optional and AND-combined. */
|
|
457
|
+
export const FundingDepositFilters = z
|
|
458
|
+
.strictObject({
|
|
459
|
+
depositAddressId: z.optional(FundingDeposit.schema.Snapshot.shape.depositAddressId),
|
|
460
|
+
recipient: z.optional(Schema.Address),
|
|
461
|
+
status: z.optional(FundingDeposit.schema.Status),
|
|
462
|
+
})
|
|
463
|
+
.check(
|
|
464
|
+
z.describe('Filters that narrow which funding deposit changes trigger this webhook.'),
|
|
465
|
+
z.meta({ title: 'Funding deposit filters' }),
|
|
466
|
+
)
|
|
467
|
+
|
|
450
468
|
/**
|
|
451
469
|
* Optional human context describing what a subscription is for. Surfaced by
|
|
452
470
|
* destinations (Slack header + description, Better Stack log fields) and echoed
|
|
@@ -506,6 +524,12 @@ export namespace schema {
|
|
|
506
524
|
z.meta({ examples: ['2024-01-01T00:00:00Z'] }),
|
|
507
525
|
),
|
|
508
526
|
destination: PublicDestination,
|
|
527
|
+
environment: z
|
|
528
|
+
.optional(z.enum(['production', 'sandbox']))
|
|
529
|
+
.check(
|
|
530
|
+
z.describe('API-key environment for private resource events, when applicable.'),
|
|
531
|
+
z.meta({ examples: ['production'] }),
|
|
532
|
+
),
|
|
509
533
|
eventType: EventType.check(z.meta({ examples: ['token:transfer'] })),
|
|
510
534
|
expiresAt: z
|
|
511
535
|
.optional(z.iso.datetime())
|
|
@@ -520,7 +544,9 @@ export namespace schema {
|
|
|
520
544
|
.check(
|
|
521
545
|
z.int(),
|
|
522
546
|
z.nonnegative(),
|
|
523
|
-
z.describe(
|
|
547
|
+
z.describe(
|
|
548
|
+
'Number of delivery failures in a row for this subscription, capped at the auto-disable threshold.',
|
|
549
|
+
),
|
|
524
550
|
z.meta({ examples: [0] }),
|
|
525
551
|
),
|
|
526
552
|
filters: Filters.check(
|
|
@@ -536,7 +562,9 @@ export namespace schema {
|
|
|
536
562
|
lastDeliveryAt: z
|
|
537
563
|
.optional(z.iso.datetime())
|
|
538
564
|
.check(
|
|
539
|
-
z.describe(
|
|
565
|
+
z.describe(
|
|
566
|
+
'When Tempo last delivered an event successfully, as an ISO 8601 timestamp. Refreshed at most once per minute during sustained delivery.',
|
|
567
|
+
),
|
|
540
568
|
z.meta({ examples: ['2024-01-01T00:00:00Z'] }),
|
|
541
569
|
),
|
|
542
570
|
status: Status.check(z.meta({ examples: ['active'] })),
|
|
@@ -683,7 +711,7 @@ export namespace schema {
|
|
|
683
711
|
})
|
|
684
712
|
.check(
|
|
685
713
|
z.describe(
|
|
686
|
-
'Best-effort token metadata (symbol/decimals) resolved
|
|
714
|
+
'Best-effort token metadata (symbol/decimals) resolved before delivery so `amount` can render as a human amount. Omitted when the lookup is unavailable.',
|
|
687
715
|
),
|
|
688
716
|
),
|
|
689
717
|
),
|
|
@@ -886,6 +914,9 @@ export namespace schema {
|
|
|
886
914
|
})
|
|
887
915
|
.check(z.describe('Payload for a `block:created` event (a new block header).'))
|
|
888
916
|
|
|
917
|
+
/** Public funding deposit after a committed material change. */
|
|
918
|
+
export const FundingDepositEvent = FundingDeposit.schema.FundingDeposit
|
|
919
|
+
|
|
889
920
|
/** A `ping` (synthetic test-delivery) payload, sent by `POST /webhooks/:id/ping`. */
|
|
890
921
|
export const PingEvent = z
|
|
891
922
|
.object({
|
|
@@ -944,6 +975,13 @@ export namespace schema {
|
|
|
944
975
|
z
|
|
945
976
|
.object({ ...envelopeBase, data: BlockEvent, type: z.literal('block:created') })
|
|
946
977
|
.check(z.meta({ title: 'Block event' })),
|
|
978
|
+
z
|
|
979
|
+
.object({
|
|
980
|
+
...envelopeBase,
|
|
981
|
+
data: FundingDepositEvent,
|
|
982
|
+
type: z.literal('funding:deposit.updated'),
|
|
983
|
+
})
|
|
984
|
+
.check(z.meta({ title: 'Funding deposit updated event' })),
|
|
947
985
|
z
|
|
948
986
|
.object({ ...envelopeBase, data: PingEvent, type: z.literal('ping') })
|
|
949
987
|
.check(z.meta({ title: 'Ping (test delivery)' })),
|
|
@@ -1037,6 +1075,20 @@ export namespace schema {
|
|
|
1037
1075
|
filters: z.optional(BlockFilters),
|
|
1038
1076
|
})
|
|
1039
1077
|
.check(z.meta({ title: 'Block subscription' })),
|
|
1078
|
+
z
|
|
1079
|
+
.object({
|
|
1080
|
+
chainId: z.optional(Schema.ChainId).check(z.meta({ examples: [4217] })),
|
|
1081
|
+
context: z.optional(Context),
|
|
1082
|
+
destination: Destination,
|
|
1083
|
+
eventType: z
|
|
1084
|
+
.literal('funding:deposit.updated')
|
|
1085
|
+
.check(
|
|
1086
|
+
z.describe('Event type for committed funding deposit changes.'),
|
|
1087
|
+
z.meta({ examples: ['funding:deposit.updated'] }),
|
|
1088
|
+
),
|
|
1089
|
+
filters: z.optional(FundingDepositFilters),
|
|
1090
|
+
})
|
|
1091
|
+
.check(z.meta({ title: 'Funding deposit subscription' })),
|
|
1040
1092
|
])
|
|
1041
1093
|
.check(z.describe('Details for creating a webhook subscription.'))
|
|
1042
1094
|
|
|
@@ -1239,6 +1291,12 @@ export namespace schema {
|
|
|
1239
1291
|
*/
|
|
1240
1292
|
export function webhooks(options: webhooks.Options = {}) {
|
|
1241
1293
|
const hidden = !options.enabled
|
|
1294
|
+
const availableEventTypes = eventTypeDescriptions.filter(
|
|
1295
|
+
(eventType) =>
|
|
1296
|
+
eventType.type !== 'funding:deposit.updated' ||
|
|
1297
|
+
options.applicationEventTypes?.includes(eventType.type),
|
|
1298
|
+
)
|
|
1299
|
+
const availableEventTypeSet = new Set(availableEventTypes.map((eventType) => eventType.type))
|
|
1242
1300
|
// Gate the whole resource on a configured state store. Without it the
|
|
1243
1301
|
// outbound-HTTP surface is disabled (read-only deployment).
|
|
1244
1302
|
const gate = async (c: Context<App.Environment>, next: () => Promise<void>) => {
|
|
@@ -1286,7 +1344,7 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1286
1344
|
if (!c.get('webhook')) return notEnabled(c)
|
|
1287
1345
|
return c.json(
|
|
1288
1346
|
Response.validated(schema.getWebhookEventTypes.Response, {
|
|
1289
|
-
data:
|
|
1347
|
+
data: availableEventTypes.map((eventType) => ({ ...eventType })),
|
|
1290
1348
|
}),
|
|
1291
1349
|
200,
|
|
1292
1350
|
)
|
|
@@ -1307,12 +1365,18 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1307
1365
|
responses: OpenApi.responses({
|
|
1308
1366
|
errors: {
|
|
1309
1367
|
400: {
|
|
1310
|
-
codes: [
|
|
1368
|
+
codes: [
|
|
1369
|
+
'body_invalid',
|
|
1370
|
+
'chain_id_unsupported',
|
|
1371
|
+
'event_type_unsupported',
|
|
1372
|
+
'filters_invalid',
|
|
1373
|
+
'url_invalid',
|
|
1374
|
+
],
|
|
1311
1375
|
description: 'The request, destination, filters, or chain are invalid.',
|
|
1312
1376
|
},
|
|
1313
1377
|
403: {
|
|
1314
|
-
codes: ['limit_exceeded'],
|
|
1315
|
-
description: '
|
|
1378
|
+
codes: ['api_key_forbidden', 'limit_exceeded'],
|
|
1379
|
+
description: 'The API key lacks access or the subscription limit was reached.',
|
|
1316
1380
|
},
|
|
1317
1381
|
},
|
|
1318
1382
|
success: {
|
|
@@ -1335,10 +1399,30 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1335
1399
|
|
|
1336
1400
|
const webhook = c.get('webhook')!
|
|
1337
1401
|
const db = Db.get(c.get('db'))
|
|
1402
|
+
const principal = Auth.getPrincipal(c)
|
|
1338
1403
|
const owner = ownerFor(c)
|
|
1339
1404
|
if (!owner) return unauthorized(c)
|
|
1340
1405
|
|
|
1341
1406
|
const body = c.req.valid('json')
|
|
1407
|
+
if (!availableEventTypeSet.has(body.eventType))
|
|
1408
|
+
return Response.error(c, {
|
|
1409
|
+
code: 'event_type_unsupported',
|
|
1410
|
+
message: 'Webhook event type is not available in this deployment',
|
|
1411
|
+
status: 400,
|
|
1412
|
+
})
|
|
1413
|
+
if (body.eventType === 'funding:deposit.updated') {
|
|
1414
|
+
if (principal?.type !== 'api_key') return unauthorized(c)
|
|
1415
|
+
if (
|
|
1416
|
+
!principal.apiKey.scopes.some(
|
|
1417
|
+
(scope) => scope === Scope.wildcard || scope === 'funding:read',
|
|
1418
|
+
)
|
|
1419
|
+
)
|
|
1420
|
+
return Response.error(c, {
|
|
1421
|
+
code: 'api_key_forbidden',
|
|
1422
|
+
message: 'API key missing required scope',
|
|
1423
|
+
status: 403,
|
|
1424
|
+
})
|
|
1425
|
+
}
|
|
1342
1426
|
const chainId = body.chainId ?? c.get('chainId')
|
|
1343
1427
|
const supportedChainIds = new Set(webhook.supportedChainIds)
|
|
1344
1428
|
if (!supportedChainIds.has(chainId))
|
|
@@ -1350,13 +1434,24 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1350
1434
|
const maxPerOwner = webhook.maxPerOwner ?? defaultMaxPerOwner
|
|
1351
1435
|
if ((await Webhooks.countSubscriptions(db, owner)) >= maxPerOwner)
|
|
1352
1436
|
throw new Webhooks.LimitExceededError(maxPerOwner)
|
|
1353
|
-
const startBlockNumber =
|
|
1437
|
+
const startBlockNumber =
|
|
1438
|
+
body.eventType === 'funding:deposit.updated'
|
|
1439
|
+
? undefined
|
|
1440
|
+
: await getRpcHead(c.get('getClient')(chainId))
|
|
1354
1441
|
const subscription = await Webhooks.createSubscription(
|
|
1355
1442
|
db,
|
|
1356
1443
|
{
|
|
1357
1444
|
chainId,
|
|
1358
1445
|
...(body.context === undefined ? {} : { context: body.context }),
|
|
1359
1446
|
destination: body.destination,
|
|
1447
|
+
...(principal?.type === 'api_key'
|
|
1448
|
+
? {
|
|
1449
|
+
environment: principal.environment,
|
|
1450
|
+
...(principal.projectId === undefined
|
|
1451
|
+
? {}
|
|
1452
|
+
: { projectId: principal.projectId }),
|
|
1453
|
+
}
|
|
1454
|
+
: {}),
|
|
1360
1455
|
eventType: body.eventType,
|
|
1361
1456
|
filters: body.filters,
|
|
1362
1457
|
owner,
|
|
@@ -1364,7 +1459,7 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1364
1459
|
},
|
|
1365
1460
|
{
|
|
1366
1461
|
maxPerOwner,
|
|
1367
|
-
startBlockNumber,
|
|
1462
|
+
...(startBlockNumber === undefined ? {} : { startBlockNumber }),
|
|
1368
1463
|
},
|
|
1369
1464
|
)
|
|
1370
1465
|
return c.json(
|
|
@@ -1408,6 +1503,7 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1408
1503
|
const db = Db.get(c.get('db'))
|
|
1409
1504
|
const owner = ownerFor(c)
|
|
1410
1505
|
if (!owner) return unauthorized(c)
|
|
1506
|
+
const access = accessFor(c)
|
|
1411
1507
|
|
|
1412
1508
|
const { cursor, include, limit, page } = c.req.valid('query')
|
|
1413
1509
|
try {
|
|
@@ -1415,12 +1511,13 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1415
1511
|
// run it concurrently with the page list. Best-effort: a failure
|
|
1416
1512
|
// omits `meta` rather than failing the page.
|
|
1417
1513
|
const countPromise = include.includes('totalCount')
|
|
1418
|
-
? Webhooks.countSubscriptions(db, owner).catch(() => undefined)
|
|
1514
|
+
? Webhooks.countSubscriptions(db, owner, { access }).catch(() => undefined)
|
|
1419
1515
|
: undefined
|
|
1420
1516
|
|
|
1421
1517
|
// Fetch one extra to detect a further page without a second round-trip.
|
|
1422
1518
|
// `page` translates to a positional slice of `(page - 1) * limit` rows.
|
|
1423
1519
|
const rows = await Webhooks.listSubscriptions(db, owner, {
|
|
1520
|
+
access,
|
|
1424
1521
|
cursor,
|
|
1425
1522
|
limit: limit + 1,
|
|
1426
1523
|
offset: page !== undefined && page > 1 ? (page - 1) * limit : undefined,
|
|
@@ -1488,10 +1585,11 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1488
1585
|
const db = Db.get(c.get('db'))
|
|
1489
1586
|
const owner = ownerFor(c)
|
|
1490
1587
|
if (!owner) return unauthorized(c)
|
|
1588
|
+
const access = accessFor(c)
|
|
1491
1589
|
|
|
1492
1590
|
const { id } = c.req.valid('param')
|
|
1493
1591
|
try {
|
|
1494
|
-
const subscription = await Webhooks.getSubscription(db, owner, id)
|
|
1592
|
+
const subscription = await Webhooks.getSubscription(db, owner, id, { access })
|
|
1495
1593
|
if (!subscription) return notFound(c)
|
|
1496
1594
|
return c.json(
|
|
1497
1595
|
Response.validated(schema.getWebhook.Response, toPublic(subscription)),
|
|
@@ -1549,13 +1647,14 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1549
1647
|
const db = Db.get(c.get('db'))
|
|
1550
1648
|
const owner = ownerFor(c)
|
|
1551
1649
|
if (!owner) return unauthorized(c)
|
|
1650
|
+
const access = accessFor(c)
|
|
1552
1651
|
|
|
1553
1652
|
const { id } = c.req.valid('param')
|
|
1554
1653
|
const { cursor, include, limit, page } = c.req.valid('query')
|
|
1555
1654
|
try {
|
|
1556
1655
|
// Owner-scope first: resolving the subscription under the caller's owner
|
|
1557
1656
|
// key 404s ids they don't own (indistinguishable from "doesn't exist").
|
|
1558
|
-
const subscription = await Webhooks.getSubscription(db, owner, id)
|
|
1657
|
+
const subscription = await Webhooks.getSubscription(db, owner, id, { access })
|
|
1559
1658
|
if (!subscription) return notFound(c)
|
|
1560
1659
|
|
|
1561
1660
|
// `totalCount` is opt-in and exact (the delivery log is TTL-bounded);
|
|
@@ -1632,11 +1731,12 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1632
1731
|
const db = Db.get(c.get('db'))
|
|
1633
1732
|
const owner = ownerFor(c)
|
|
1634
1733
|
if (!owner) return unauthorized(c)
|
|
1734
|
+
const access = accessFor(c)
|
|
1635
1735
|
|
|
1636
1736
|
const { id } = c.req.valid('param')
|
|
1637
1737
|
try {
|
|
1638
1738
|
// Owner-scope first so unowned/unknown ids are indistinguishable (404).
|
|
1639
|
-
const subscription = await Webhooks.getSubscription(db, owner, id)
|
|
1739
|
+
const subscription = await Webhooks.getSubscription(db, owner, id, { access })
|
|
1640
1740
|
if (!subscription) return notFound(c)
|
|
1641
1741
|
|
|
1642
1742
|
// Inline delivery (not the queue) so the caller gets an immediate
|
|
@@ -1696,11 +1796,12 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1696
1796
|
const db = Db.get(c.get('db'))
|
|
1697
1797
|
const owner = ownerFor(c)
|
|
1698
1798
|
if (!owner) return unauthorized(c)
|
|
1799
|
+
const access = accessFor(c)
|
|
1699
1800
|
|
|
1700
1801
|
const { deliveryId, id } = c.req.valid('param')
|
|
1701
1802
|
try {
|
|
1702
1803
|
// Owner-scope the subscription first so unowned ids 404 like elsewhere.
|
|
1703
|
-
const subscription = await Webhooks.getSubscription(db, owner, id)
|
|
1804
|
+
const subscription = await Webhooks.getSubscription(db, owner, id, { access })
|
|
1704
1805
|
if (!subscription) return notFound(c)
|
|
1705
1806
|
|
|
1706
1807
|
const delivery = await Webhooks.getDelivery(db, id, deliveryId)
|
|
@@ -1786,6 +1887,7 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1786
1887
|
const db = Db.get(c.get('db'))
|
|
1787
1888
|
const owner = ownerFor(c)
|
|
1788
1889
|
if (!owner) return unauthorized(c)
|
|
1890
|
+
const access = accessFor(c)
|
|
1789
1891
|
|
|
1790
1892
|
const { id } = c.req.valid('param')
|
|
1791
1893
|
const patch = c.req.valid('json')
|
|
@@ -1796,11 +1898,11 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1796
1898
|
// persisting — otherwise a bad filter would silently widen the match
|
|
1797
1899
|
// set at scan time.
|
|
1798
1900
|
if (patch.filters !== undefined) {
|
|
1799
|
-
const existing = await Webhooks.getSubscription(db, owner, id)
|
|
1901
|
+
const existing = await Webhooks.getSubscription(db, owner, id, { access })
|
|
1800
1902
|
if (!existing) return notFound(c)
|
|
1801
1903
|
parseFilters(existing.eventType, patch.filters)
|
|
1802
1904
|
}
|
|
1803
|
-
const subscription = await Webhooks.updateSubscription(db, owner, id, patch)
|
|
1905
|
+
const subscription = await Webhooks.updateSubscription(db, owner, id, patch, { access })
|
|
1804
1906
|
if (!subscription) return notFound(c)
|
|
1805
1907
|
return c.json(
|
|
1806
1908
|
Response.validated(schema.updateWebhook.Response, toPublic(subscription)),
|
|
@@ -1849,10 +1951,11 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1849
1951
|
const db = Db.get(c.get('db'))
|
|
1850
1952
|
const owner = ownerFor(c)
|
|
1851
1953
|
if (!owner) return unauthorized(c)
|
|
1954
|
+
const access = accessFor(c)
|
|
1852
1955
|
|
|
1853
1956
|
const { id } = c.req.valid('param')
|
|
1854
1957
|
try {
|
|
1855
|
-
const deleted = await Webhooks.deleteSubscription(db, owner, id)
|
|
1958
|
+
const deleted = await Webhooks.deleteSubscription(db, owner, id, { access })
|
|
1856
1959
|
if (!deleted) return notFound(c)
|
|
1857
1960
|
return c.json(Response.validated(schema.deleteWebhook.Response, { id }), 200)
|
|
1858
1961
|
} catch (cause) {
|
|
@@ -1866,626 +1969,13 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1866
1969
|
export declare namespace webhooks {
|
|
1867
1970
|
/** Options for creating webhook routes. */
|
|
1868
1971
|
type Options = {
|
|
1972
|
+
/** Application events produced by this deployment outside the per-block chain scan. */
|
|
1973
|
+
applicationEventTypes?: readonly Webhooks.EventType[] | undefined
|
|
1869
1974
|
/** Whether webhook state is configured and routes should appear in OpenAPI. */
|
|
1870
1975
|
enabled?: boolean | undefined
|
|
1871
1976
|
}
|
|
1872
1977
|
}
|
|
1873
1978
|
|
|
1874
|
-
/**
|
|
1875
|
-
* Largest representable in-block index, used as an end-of-block cursor sentinel
|
|
1876
|
-
* so a fully-scanned block advances the cursor strictly past it (no row index
|
|
1877
|
-
* can equal it).
|
|
1878
|
-
*/
|
|
1879
|
-
const endOfBlockIndex = 2_147_483_647
|
|
1880
|
-
const scanPageSize = 100
|
|
1881
|
-
|
|
1882
|
-
/** A scanned row paired with its `(block, index)` keyset position. */
|
|
1883
|
-
type ScanItem = { cursor: readonly [number, number]; data: unknown }
|
|
1884
|
-
|
|
1885
|
-
/**
|
|
1886
|
-
* The result of scanning one subscription page. `scannedThrough` is the
|
|
1887
|
-
* highest block fully scanned. A batch-limited page omits it, so the poller
|
|
1888
|
-
* resumes after the last dispatched row instead of skipping later matches.
|
|
1889
|
-
*/
|
|
1890
|
-
type ScanPage = {
|
|
1891
|
-
hasMore?: boolean | undefined
|
|
1892
|
-
items: readonly ScanItem[]
|
|
1893
|
-
scannedThrough?: number | undefined
|
|
1894
|
-
}
|
|
1895
|
-
|
|
1896
|
-
/** Per-subscription state retained between fair scan passes. */
|
|
1897
|
-
type PollState = {
|
|
1898
|
-
cursor: string
|
|
1899
|
-
filters: Record<string, unknown>
|
|
1900
|
-
remaining: number
|
|
1901
|
-
safeBlock: number
|
|
1902
|
-
subscription: Webhooks.Subscription
|
|
1903
|
-
}
|
|
1904
|
-
|
|
1905
|
-
/** Concurrent per-subscription scans and cursor writes per poll round; caps RPC and DB fan-out. */
|
|
1906
|
-
const pollConcurrency = 8
|
|
1907
|
-
|
|
1908
|
-
/** Event types scanned together when their combined state fits one existing scan round. */
|
|
1909
|
-
const pollEventTypeConcurrency = 2
|
|
1910
|
-
|
|
1911
|
-
/** Maximum subscription states retained by concurrent event-type scan batches. */
|
|
1912
|
-
const pollStateLimit = WebhookTransfer.scanCohortSize * pollConcurrency
|
|
1913
|
-
|
|
1914
|
-
type Schedule = <value>(fn: () => Promise<value>) => Promise<value>
|
|
1915
|
-
|
|
1916
|
-
/** Schedules asynchronous work through one shared concurrency bound. */
|
|
1917
|
-
function createLimiter(limit: number): Schedule {
|
|
1918
|
-
let active = 0
|
|
1919
|
-
const waiting: (() => void)[] = []
|
|
1920
|
-
return async (fn) => {
|
|
1921
|
-
if (active >= limit) await new Promise<void>((resolve) => waiting.push(resolve))
|
|
1922
|
-
else active++
|
|
1923
|
-
try {
|
|
1924
|
-
return await fn()
|
|
1925
|
-
} finally {
|
|
1926
|
-
const next = waiting.shift()
|
|
1927
|
-
if (next) next()
|
|
1928
|
-
else active--
|
|
1929
|
-
}
|
|
1930
|
-
}
|
|
1931
|
-
}
|
|
1932
|
-
|
|
1933
|
-
/** Runs `fn` over `items` with at most `limit` in flight, preserving order. */
|
|
1934
|
-
async function mapWithLimit<item, value>(
|
|
1935
|
-
items: readonly item[],
|
|
1936
|
-
limit: number,
|
|
1937
|
-
fn: (item: item) => Promise<value>,
|
|
1938
|
-
): Promise<PromiseSettledResult<value>[]> {
|
|
1939
|
-
const results: PromiseSettledResult<value>[] = []
|
|
1940
|
-
let next = 0
|
|
1941
|
-
await Promise.all(
|
|
1942
|
-
Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
1943
|
-
while (next < items.length) {
|
|
1944
|
-
const index = next++
|
|
1945
|
-
try {
|
|
1946
|
-
results[index] = { status: 'fulfilled', value: await fn(items[index]!) }
|
|
1947
|
-
} catch (reason) {
|
|
1948
|
-
results[index] = { reason, status: 'rejected' }
|
|
1949
|
-
}
|
|
1950
|
-
}
|
|
1951
|
-
}),
|
|
1952
|
-
)
|
|
1953
|
-
return results
|
|
1954
|
-
}
|
|
1955
|
-
|
|
1956
|
-
/** Returns `candidate` only when it is lexicographically after `current`. */
|
|
1957
|
-
function laterCursor(current: string, candidate: string): string {
|
|
1958
|
-
const before = Cursor.decode(current, ['int', 'int'])
|
|
1959
|
-
const after = Cursor.decode(candidate, ['int', 'int'])
|
|
1960
|
-
if (!after) return current
|
|
1961
|
-
if (!before) return candidate
|
|
1962
|
-
const [beforeBlock, beforeIndex] = before as [number, number]
|
|
1963
|
-
const [afterBlock, afterIndex] = after as [number, number]
|
|
1964
|
-
return afterBlock > beforeBlock || (afterBlock === beforeBlock && afterIndex > beforeIndex)
|
|
1965
|
-
? candidate
|
|
1966
|
-
: current
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
|
-
/**
|
|
1970
|
-
* Polls the chain over RPC for new events and dispatches a signed envelope per
|
|
1971
|
-
* matching row, advancing each active subscription's `(block, index)` keyset
|
|
1972
|
-
* cursor.
|
|
1973
|
-
*
|
|
1974
|
-
* Every event type scans a **narrow confirmed window** `[cursor, head -
|
|
1975
|
-
* confirmations]` directly over JSON-RPC (`eth_getLogs`,
|
|
1976
|
-
* `eth_getBlockByNumber`) so webhook delivery does not wait for an indexer.
|
|
1977
|
-
*
|
|
1978
|
-
* The chain head is injectable (tests pin a fixed historical window); only
|
|
1979
|
-
* blocks at least `confirmations` behind head are emitted, giving at-least-once,
|
|
1980
|
-
* finalized-ish semantics (re-delivery overlap is harmless because the receiver
|
|
1981
|
-
* dedupes on the stable `event.id`).
|
|
1982
|
-
*
|
|
1983
|
-
* Cursor advances only past successfully dispatched rows, so a dispatch failure
|
|
1984
|
-
* pauses that subscription at the last good position and the next tick resumes.
|
|
1985
|
-
* Scan failures leave that subscription unchanged; the first error is rethrown
|
|
1986
|
-
* after the remaining subscriptions run.
|
|
1987
|
-
*/
|
|
1988
|
-
export async function poll(options: poll.Options): Promise<poll.Result> {
|
|
1989
|
-
const {
|
|
1990
|
-
batchSize = 200,
|
|
1991
|
-
cache: cache_metadata = Store.memory(),
|
|
1992
|
-
chainId,
|
|
1993
|
-
client,
|
|
1994
|
-
confirmations = 1,
|
|
1995
|
-
db,
|
|
1996
|
-
eventTypes: eventTypes_input = [
|
|
1997
|
-
'token:transfer',
|
|
1998
|
-
'transaction:included',
|
|
1999
|
-
'log:emitted',
|
|
2000
|
-
'block:created',
|
|
2001
|
-
],
|
|
2002
|
-
maxBlocks = 1_000,
|
|
2003
|
-
} = options
|
|
2004
|
-
const now = options.now ?? (() => new Date())
|
|
2005
|
-
options.signal?.throwIfAborted()
|
|
2006
|
-
// Default (self-host) transport: deliver inline and record the outcome so a
|
|
2007
|
-
// sustained-failure subscription auto-disables. On Cloudflare, callers pass a
|
|
2008
|
-
// Queue producer instead and the consumer runs `deliverAndRecord`.
|
|
2009
|
-
const dispatch =
|
|
2010
|
-
options.dispatch ??
|
|
2011
|
-
(({ envelope, subscription }) => Webhooks.deliverAndRecord(db, subscription, envelope))
|
|
2012
|
-
const dispatchBatch = options.dispatchBatch
|
|
2013
|
-
|
|
2014
|
-
// A pinned `headBlockNumber` lets a head subscription supply the exact live
|
|
2015
|
-
// boundary and keeps historical tests deterministic.
|
|
2016
|
-
const override = options.headBlockNumber
|
|
2017
|
-
let rpcHead: number | undefined = override === undefined ? undefined : Number(override)
|
|
2018
|
-
const resolveRpcHead = async () => (rpcHead ??= await getRpcHead(client))
|
|
2019
|
-
|
|
2020
|
-
const headStartedAt = Date.now()
|
|
2021
|
-
const rpcSafeBlock = await (async () => {
|
|
2022
|
-
try {
|
|
2023
|
-
const safeBlock = Math.max(0, (await resolveRpcHead()) - confirmations)
|
|
2024
|
-
options.onStage?.({
|
|
2025
|
-
durationMs: Date.now() - headStartedAt,
|
|
2026
|
-
outcome: 'ok',
|
|
2027
|
-
stage: 'rpc_head',
|
|
2028
|
-
})
|
|
2029
|
-
return safeBlock
|
|
2030
|
-
} catch (error) {
|
|
2031
|
-
options.onStage?.({
|
|
2032
|
-
durationMs: Date.now() - headStartedAt,
|
|
2033
|
-
outcome: 'error',
|
|
2034
|
-
stage: 'rpc_head',
|
|
2035
|
-
})
|
|
2036
|
-
throw error
|
|
2037
|
-
}
|
|
2038
|
-
})()
|
|
2039
|
-
const result: poll.Result = {
|
|
2040
|
-
chainId,
|
|
2041
|
-
dispatched: 0,
|
|
2042
|
-
failed: 0,
|
|
2043
|
-
safeBlock: rpcSafeBlock,
|
|
2044
|
-
scanned: 0,
|
|
2045
|
-
subscriptions: 0,
|
|
2046
|
-
}
|
|
2047
|
-
let cursorLagBlocks = 0
|
|
2048
|
-
const pollErrors: unknown[] = []
|
|
2049
|
-
|
|
2050
|
-
const eventTypes = [...new Set(eventTypes_input)]
|
|
2051
|
-
const subscriptionsStartedAt = Date.now()
|
|
2052
|
-
const pairs = await Webhooks.listActiveForChain(db, { chainId, eventTypes })
|
|
2053
|
-
options.signal?.throwIfAborted()
|
|
2054
|
-
options.onSubscriptions?.(pairs.length)
|
|
2055
|
-
options.onStage?.({
|
|
2056
|
-
durationMs: Date.now() - subscriptionsStartedAt,
|
|
2057
|
-
outcome: 'ok',
|
|
2058
|
-
stage: 'subscription_read',
|
|
2059
|
-
})
|
|
2060
|
-
const pairsByEventType = Map.groupBy(pairs, (pair) => pair.subscription.eventType)
|
|
2061
|
-
const eventTypes_active = eventTypes.filter(
|
|
2062
|
-
(eventType) => (pairsByEventType.get(eventType)?.length ?? 0) > 0,
|
|
2063
|
-
)
|
|
2064
|
-
const eventTypeConcurrency =
|
|
2065
|
-
pairs.length <= pollStateLimit
|
|
2066
|
-
? Math.min(pollEventTypeConcurrency, eventTypes_active.length)
|
|
2067
|
-
: Math.min(1, eventTypes_active.length)
|
|
2068
|
-
const scanRoundSize = Math.floor(pollStateLimit / Math.max(1, eventTypeConcurrency))
|
|
2069
|
-
const scheduleScan = createLimiter(pollConcurrency)
|
|
2070
|
-
|
|
2071
|
-
const eventTypeResults = await mapWithLimit(
|
|
2072
|
-
eventTypes_active,
|
|
2073
|
-
eventTypeConcurrency,
|
|
2074
|
-
async (eventType) => {
|
|
2075
|
-
options.signal?.throwIfAborted()
|
|
2076
|
-
const safeBlock = rpcSafeBlock
|
|
2077
|
-
const pairs = pairsByEventType.get(eventType) ?? []
|
|
2078
|
-
const durations = {
|
|
2079
|
-
cursor_write: 0,
|
|
2080
|
-
enrich: 0,
|
|
2081
|
-
scan: 0,
|
|
2082
|
-
} satisfies Record<Exclude<MetricSink.PollStage, 'rpc_head' | 'subscription_read'>, number>
|
|
2083
|
-
const observed = new Set<keyof typeof durations>()
|
|
2084
|
-
let cursorWriteOutcome: 'error' | 'ok' = 'ok'
|
|
2085
|
-
let scanOutcome: 'error' | 'ok' = 'ok'
|
|
2086
|
-
const writeCursor = async (parameters: Webhooks.compareAndSetCursor.Options) => {
|
|
2087
|
-
const startedAt = Date.now()
|
|
2088
|
-
observed.add('cursor_write')
|
|
2089
|
-
try {
|
|
2090
|
-
return await Webhooks.compareAndSetCursor(db, parameters)
|
|
2091
|
-
} catch (error) {
|
|
2092
|
-
cursorWriteOutcome = 'error'
|
|
2093
|
-
pollErrors.push(error)
|
|
2094
|
-
return false
|
|
2095
|
-
} finally {
|
|
2096
|
-
durations.cursor_write += Date.now() - startedAt
|
|
2097
|
-
}
|
|
2098
|
-
}
|
|
2099
|
-
let states: PollState[] = []
|
|
2100
|
-
for (const pair of pairs) {
|
|
2101
|
-
const { subscription } = pair
|
|
2102
|
-
result.subscriptions++
|
|
2103
|
-
let filters: Record<string, unknown>
|
|
2104
|
-
try {
|
|
2105
|
-
filters = parseFilters(subscription.eventType, subscription.filters)
|
|
2106
|
-
} catch (error) {
|
|
2107
|
-
if (!(error instanceof Webhooks.InvalidFilterError)) throw error
|
|
2108
|
-
options.signal?.throwIfAborted()
|
|
2109
|
-
// A breaking filter-schema removal must not poison every chain poll.
|
|
2110
|
-
// Disable the legacy subscription without widening its match set.
|
|
2111
|
-
await Webhooks.updateSubscription(db, subscription.owner, subscription.id, {
|
|
2112
|
-
status: 'disabled',
|
|
2113
|
-
})
|
|
2114
|
-
continue
|
|
2115
|
-
}
|
|
2116
|
-
let cursor = pair.cursor
|
|
2117
|
-
if (cursor === null) {
|
|
2118
|
-
// Legacy subscriptions without a creation checkpoint anchor at the
|
|
2119
|
-
// current confirmed head, avoiding a genesis backfill.
|
|
2120
|
-
await writeCursor({
|
|
2121
|
-
cursor: Cursor.encode([safeBlock, endOfBlockIndex]),
|
|
2122
|
-
expectedCursor: null,
|
|
2123
|
-
subscriptionId: subscription.id,
|
|
2124
|
-
})
|
|
2125
|
-
continue
|
|
2126
|
-
}
|
|
2127
|
-
const checkpoint = Cursor.decode(cursor, ['int'])
|
|
2128
|
-
if (checkpoint) {
|
|
2129
|
-
// Creation checkpoints record the live head without finalizing it. Rebase
|
|
2130
|
-
// to this poller's confirmed boundary so replacement blocks stay eligible.
|
|
2131
|
-
const confirmedCursor = Cursor.encode([
|
|
2132
|
-
Math.max(0, (checkpoint[0] as number) - confirmations),
|
|
2133
|
-
endOfBlockIndex,
|
|
2134
|
-
])
|
|
2135
|
-
const committed = await writeCursor({
|
|
2136
|
-
cursor: confirmedCursor,
|
|
2137
|
-
expectedCursor: cursor,
|
|
2138
|
-
subscriptionId: subscription.id,
|
|
2139
|
-
})
|
|
2140
|
-
if (!committed) continue
|
|
2141
|
-
cursor = confirmedCursor
|
|
2142
|
-
}
|
|
2143
|
-
const position = Cursor.decode(cursor, ['int', 'int'])
|
|
2144
|
-
if (position)
|
|
2145
|
-
cursorLagBlocks = Math.max(cursorLagBlocks, safeBlock - (position[0] as number))
|
|
2146
|
-
const window = scanWindow(cursor, safeBlock, maxBlocks)
|
|
2147
|
-
if (window && window.to < safeBlock) result.hasMore = true
|
|
2148
|
-
states.push({
|
|
2149
|
-
cursor,
|
|
2150
|
-
filters,
|
|
2151
|
-
remaining: batchSize,
|
|
2152
|
-
safeBlock: window?.to ?? safeBlock,
|
|
2153
|
-
subscription,
|
|
2154
|
-
})
|
|
2155
|
-
}
|
|
2156
|
-
|
|
2157
|
-
// Every subscription gets one bounded page before any subscription gets a
|
|
2158
|
-
// continuation page, preserving fairness while allowing catch-up.
|
|
2159
|
-
while (states.length > 0) {
|
|
2160
|
-
options.signal?.throwIfAborted()
|
|
2161
|
-
const continuations: PollState[] = []
|
|
2162
|
-
const round = states
|
|
2163
|
-
for (let offset = 0; offset < round.length; offset += scanRoundSize) {
|
|
2164
|
-
const batch = round.slice(offset, offset + scanRoundSize)
|
|
2165
|
-
const windows = batch.flatMap((state) => {
|
|
2166
|
-
const window = scanWindow(state.cursor, state.safeBlock, maxBlocks)
|
|
2167
|
-
return window ? [window] : []
|
|
2168
|
-
})
|
|
2169
|
-
const maxWindowBlocks = Math.max(
|
|
2170
|
-
0,
|
|
2171
|
-
...windows.map((window) => window.to - window.from + 1),
|
|
2172
|
-
)
|
|
2173
|
-
const totalWindowBlocks = windows.reduce(
|
|
2174
|
-
(total, window) => total + window.to - window.from + 1,
|
|
2175
|
-
0,
|
|
2176
|
-
)
|
|
2177
|
-
options.onScanCheckpoint?.({
|
|
2178
|
-
concurrency: pollConcurrency,
|
|
2179
|
-
eventType,
|
|
2180
|
-
maxWindowBlocks,
|
|
2181
|
-
phase: 'start',
|
|
2182
|
-
rpcMethod: scanRpcMethod(eventType),
|
|
2183
|
-
subscriptionLimit: scanRoundSize,
|
|
2184
|
-
subscriptionCount: batch.length,
|
|
2185
|
-
totalWindowBlocks,
|
|
2186
|
-
})
|
|
2187
|
-
const scanStartedAt = Date.now()
|
|
2188
|
-
const pages = await scanSubscriptions({
|
|
2189
|
-
client,
|
|
2190
|
-
eventType,
|
|
2191
|
-
maxBlocks,
|
|
2192
|
-
schedule: scheduleScan,
|
|
2193
|
-
states: batch,
|
|
2194
|
-
})
|
|
2195
|
-
options.onScanCheckpoint?.({
|
|
2196
|
-
concurrency: pollConcurrency,
|
|
2197
|
-
durationMs: Date.now() - scanStartedAt,
|
|
2198
|
-
eventType,
|
|
2199
|
-
failedSubscriptions: pages.filter((page) => page.status === 'rejected').length,
|
|
2200
|
-
matchedItems: pages.reduce(
|
|
2201
|
-
(total, page) => total + (page.status === 'fulfilled' ? page.value.items.length : 0),
|
|
2202
|
-
0,
|
|
2203
|
-
),
|
|
2204
|
-
maxWindowBlocks,
|
|
2205
|
-
outcome: scanResultOutcome(pages),
|
|
2206
|
-
phase: 'complete',
|
|
2207
|
-
rpcMethod: scanRpcMethod(eventType),
|
|
2208
|
-
subscriptionLimit: scanRoundSize,
|
|
2209
|
-
subscriptionCount: batch.length,
|
|
2210
|
-
totalWindowBlocks,
|
|
2211
|
-
})
|
|
2212
|
-
options.signal?.throwIfAborted()
|
|
2213
|
-
durations.scan += Date.now() - scanStartedAt
|
|
2214
|
-
observed.add('scan')
|
|
2215
|
-
if (pages.some((page) => page.status === 'rejected')) scanOutcome = 'error'
|
|
2216
|
-
|
|
2217
|
-
type Scanned = { page: ScanPage; state: PollState }
|
|
2218
|
-
const busy: Scanned[] = []
|
|
2219
|
-
const idle: Scanned[] = []
|
|
2220
|
-
for (let i = 0; i < batch.length; i++) {
|
|
2221
|
-
const state = batch[i]!
|
|
2222
|
-
const settled = pages[i]!
|
|
2223
|
-
if (settled.status === 'rejected') {
|
|
2224
|
-
pollErrors.push(settled.reason)
|
|
2225
|
-
continue
|
|
2226
|
-
}
|
|
2227
|
-
result.scanned += settled.value.items.length
|
|
2228
|
-
state.remaining -= settled.value.items.length
|
|
2229
|
-
;(settled.value.items.length > 0 ? busy : idle).push({ page: settled.value, state })
|
|
2230
|
-
}
|
|
2231
|
-
|
|
2232
|
-
// Empty pages only advance their cursor: no dispatch order to preserve,
|
|
2233
|
-
// so one statement commits the cohort while retaining per-row CAS semantics.
|
|
2234
|
-
const cursorWritesStartedAt = Date.now()
|
|
2235
|
-
const candidates = idle.flatMap(({ page, state }) => {
|
|
2236
|
-
options.signal?.throwIfAborted()
|
|
2237
|
-
const candidate =
|
|
2238
|
-
page.scannedThrough !== undefined
|
|
2239
|
-
? Cursor.encode([page.scannedThrough, endOfBlockIndex])
|
|
2240
|
-
: page.hasMore
|
|
2241
|
-
? undefined
|
|
2242
|
-
: Cursor.encode([state.safeBlock, endOfBlockIndex])
|
|
2243
|
-
const cursor = candidate ? laterCursor(state.cursor, candidate) : state.cursor
|
|
2244
|
-
return cursor === state.cursor ? [] : [{ cursor, state }]
|
|
2245
|
-
})
|
|
2246
|
-
if (candidates.length > 0) {
|
|
2247
|
-
const committed = await (async () => {
|
|
2248
|
-
try {
|
|
2249
|
-
return await Webhooks.compareAndSetCursors(db, {
|
|
2250
|
-
cursors: candidates.map(({ cursor, state }) => ({
|
|
2251
|
-
cursor,
|
|
2252
|
-
expectedCursor: state.cursor,
|
|
2253
|
-
subscriptionId: state.subscription.id,
|
|
2254
|
-
})),
|
|
2255
|
-
})
|
|
2256
|
-
} catch (error) {
|
|
2257
|
-
cursorWriteOutcome = 'error'
|
|
2258
|
-
pollErrors.push(error)
|
|
2259
|
-
return new Set<string>()
|
|
2260
|
-
}
|
|
2261
|
-
})()
|
|
2262
|
-
for (const { cursor, state } of candidates)
|
|
2263
|
-
if (committed.has(state.subscription.id)) state.cursor = cursor
|
|
2264
|
-
durations.cursor_write += Date.now() - cursorWritesStartedAt
|
|
2265
|
-
observed.add('cursor_write')
|
|
2266
|
-
}
|
|
2267
|
-
|
|
2268
|
-
for (const { page, state } of busy) {
|
|
2269
|
-
options.signal?.throwIfAborted()
|
|
2270
|
-
const { subscription } = state
|
|
2271
|
-
|
|
2272
|
-
let accepted: readonly [number, number] | undefined
|
|
2273
|
-
let failed = false
|
|
2274
|
-
const buildDispatchable = async (item: ScanItem): Promise<poll.Dispatchable> => {
|
|
2275
|
-
options.signal?.throwIfAborted()
|
|
2276
|
-
// Token metadata is best-effort for webhooks: transfers use it for display,
|
|
2277
|
-
// while transactions expose fee-token references without blocking delivery.
|
|
2278
|
-
const enrichStartedAt = Date.now()
|
|
2279
|
-
const data = await enrichEventData(eventType, item.data, {
|
|
2280
|
-
cache: cache_metadata,
|
|
2281
|
-
chainId,
|
|
2282
|
-
client,
|
|
2283
|
-
})
|
|
2284
|
-
durations.enrich += Date.now() - enrichStartedAt
|
|
2285
|
-
observed.add('enrich')
|
|
2286
|
-
options.signal?.throwIfAborted()
|
|
2287
|
-
const envelope = Webhooks.buildEnvelope({
|
|
2288
|
-
blockNumber: item.cursor[0],
|
|
2289
|
-
createdAt: now(),
|
|
2290
|
-
data,
|
|
2291
|
-
logIndex: item.cursor[1],
|
|
2292
|
-
subscription,
|
|
2293
|
-
})
|
|
2294
|
-
return { envelope, subscription }
|
|
2295
|
-
}
|
|
2296
|
-
|
|
2297
|
-
if (dispatchBatch) {
|
|
2298
|
-
const dispatchables: poll.Dispatchable[] = []
|
|
2299
|
-
for (const item of page.items) dispatchables.push(await buildDispatchable(item))
|
|
2300
|
-
options.signal?.throwIfAborted()
|
|
2301
|
-
const delivered = await dispatchBatch(dispatchables, { db })
|
|
2302
|
-
if (delivered.ok) {
|
|
2303
|
-
accepted = page.items.at(-1)?.cursor
|
|
2304
|
-
result.dispatched += dispatchables.length
|
|
2305
|
-
} else {
|
|
2306
|
-
result.failed += dispatchables.length
|
|
2307
|
-
failed = true
|
|
2308
|
-
}
|
|
2309
|
-
} else {
|
|
2310
|
-
for (const item of page.items) {
|
|
2311
|
-
const dispatchable = await buildDispatchable(item)
|
|
2312
|
-
const delivered = await dispatch(dispatchable)
|
|
2313
|
-
if (!delivered.ok) {
|
|
2314
|
-
result.failed++
|
|
2315
|
-
failed = true
|
|
2316
|
-
break
|
|
2317
|
-
}
|
|
2318
|
-
accepted = item.cursor
|
|
2319
|
-
result.dispatched++
|
|
2320
|
-
}
|
|
2321
|
-
}
|
|
2322
|
-
|
|
2323
|
-
// Persist every accepted page before another subscription begins its
|
|
2324
|
-
// continuation, so a later scan failure retains completed work.
|
|
2325
|
-
const candidate = (() => {
|
|
2326
|
-
if (failed) return accepted ? Cursor.encode([...accepted]) : undefined
|
|
2327
|
-
if (page.scannedThrough !== undefined)
|
|
2328
|
-
return Cursor.encode([page.scannedThrough, endOfBlockIndex])
|
|
2329
|
-
if (accepted) return Cursor.encode([...accepted])
|
|
2330
|
-
if (!page.hasMore) return Cursor.encode([state.safeBlock, endOfBlockIndex])
|
|
2331
|
-
return undefined
|
|
2332
|
-
})()
|
|
2333
|
-
const cursor = candidate ? laterCursor(state.cursor, candidate) : state.cursor
|
|
2334
|
-
const progressed = cursor !== state.cursor
|
|
2335
|
-
let committed = false
|
|
2336
|
-
if (progressed) {
|
|
2337
|
-
committed = await writeCursor({
|
|
2338
|
-
cursor,
|
|
2339
|
-
expectedCursor: state.cursor,
|
|
2340
|
-
subscriptionId: subscription.id,
|
|
2341
|
-
})
|
|
2342
|
-
if (committed) state.cursor = cursor
|
|
2343
|
-
}
|
|
2344
|
-
|
|
2345
|
-
if (!failed && page.hasMore === true) {
|
|
2346
|
-
if (committed && state.remaining > 0) continuations.push(state)
|
|
2347
|
-
else result.hasMore = true
|
|
2348
|
-
}
|
|
2349
|
-
}
|
|
2350
|
-
}
|
|
2351
|
-
states = continuations
|
|
2352
|
-
}
|
|
2353
|
-
for (const stage of observed) {
|
|
2354
|
-
const outcome = (() => {
|
|
2355
|
-
if (stage === 'cursor_write') return cursorWriteOutcome
|
|
2356
|
-
if (stage === 'scan') return scanOutcome
|
|
2357
|
-
return 'ok'
|
|
2358
|
-
})()
|
|
2359
|
-
options.onStage?.({
|
|
2360
|
-
durationMs: durations[stage],
|
|
2361
|
-
eventType,
|
|
2362
|
-
outcome,
|
|
2363
|
-
stage,
|
|
2364
|
-
})
|
|
2365
|
-
}
|
|
2366
|
-
},
|
|
2367
|
-
)
|
|
2368
|
-
for (const result of eventTypeResults)
|
|
2369
|
-
if (result.status === 'rejected') pollErrors.push(result.reason)
|
|
2370
|
-
|
|
2371
|
-
options.onCursorLag?.(cursorLagBlocks)
|
|
2372
|
-
if (pollErrors.length > 0) throw pollErrors[0]
|
|
2373
|
-
return result
|
|
2374
|
-
}
|
|
2375
|
-
|
|
2376
|
-
export declare namespace poll {
|
|
2377
|
-
/** A row to dispatch, paired with its owning subscription. */
|
|
2378
|
-
type Dispatchable = {
|
|
2379
|
-
/** The signed-on-dispatch delivery envelope. */
|
|
2380
|
-
envelope: Webhooks.Envelope
|
|
2381
|
-
/** The subscription the event matched. */
|
|
2382
|
-
subscription: Webhooks.Subscription
|
|
2383
|
-
}
|
|
2384
|
-
/** Options for {@link poll}. */
|
|
2385
|
-
type Options = {
|
|
2386
|
-
/** Max matching rows scanned per subscription per tick (default 200). */
|
|
2387
|
-
batchSize?: number | undefined
|
|
2388
|
-
/** Cache store for the token-metadata memoize (defaults to a fresh in-memory store; pass a persistent one to share lookups across ticks). */
|
|
2389
|
-
cache?: Store.Store | undefined
|
|
2390
|
-
/** Chain to poll. Callers loop over chains. */
|
|
2391
|
-
chainId: number
|
|
2392
|
-
/** viem RPC client for the chain. */
|
|
2393
|
-
client: Viem.getClient.ReturnType
|
|
2394
|
-
/** Reorg-safety window: only emit blocks this far behind head (default 1). */
|
|
2395
|
-
confirmations?: number | undefined
|
|
2396
|
-
/** Database holding subscriptions + cursors. */
|
|
2397
|
-
db: Db.Db
|
|
2398
|
-
/**
|
|
2399
|
-
* Delivery transport. Defaults to inline {@link Webhooks.deliverAndRecord} (self-host);
|
|
2400
|
-
* on Cloudflare pass a Queue producer so retry/backoff/DLQ are native.
|
|
2401
|
-
*/
|
|
2402
|
-
dispatch?: ((input: Dispatchable) => Promise<Webhooks.Result>) | undefined
|
|
2403
|
-
/** Batch delivery transport. A successful result confirms every row in the input. */
|
|
2404
|
-
dispatchBatch?:
|
|
2405
|
-
| ((input: readonly Dispatchable[], context: DispatchContext) => Promise<Webhooks.Result>)
|
|
2406
|
-
| undefined
|
|
2407
|
-
/** Event types to poll (default all). */
|
|
2408
|
-
eventTypes?: readonly EventType[] | undefined
|
|
2409
|
-
/** Current chain head; the poller emits up to `head - confirmations`. */
|
|
2410
|
-
headBlockNumber?: bigint | number | undefined
|
|
2411
|
-
/** Backfill cap: most blocks an RPC scanner inspects per tick (default 1000). */
|
|
2412
|
-
maxBlocks?: number | undefined
|
|
2413
|
-
/** Records the worst confirmed-head distance without changing the poll result. */
|
|
2414
|
-
onCursorLag?: ((blocks: number) => void) | undefined
|
|
2415
|
-
/** Records bounded scan inputs before allocation and outcomes after completion. */
|
|
2416
|
-
onScanCheckpoint?: ((checkpoint: ScanCheckpoint) => void) | undefined
|
|
2417
|
-
/** Records bounded, per-tick poll-stage timings without subscription identities. */
|
|
2418
|
-
onStage?: ((stage: Stage) => void) | undefined
|
|
2419
|
-
/** Records subscriptions eligible for this poll before any scan runs. */
|
|
2420
|
-
onSubscriptions?: ((count: number) => void) | undefined
|
|
2421
|
-
/** Clock for envelope timestamps; injectable for tests. */
|
|
2422
|
-
now?: (() => Date) | undefined
|
|
2423
|
-
/** Stops scanning and dispatch when the caller loses its execution lease. */
|
|
2424
|
-
signal?: AbortSignal | undefined
|
|
2425
|
-
}
|
|
2426
|
-
|
|
2427
|
-
/** Runtime context shared with a batch delivery transport. */
|
|
2428
|
-
type DispatchContext = {
|
|
2429
|
-
/** Database instance already resolved for this poll. */
|
|
2430
|
-
db: Db.Db
|
|
2431
|
-
}
|
|
2432
|
-
/** Summary counters for one poll tick. */
|
|
2433
|
-
type Result = {
|
|
2434
|
-
/** Chain that was polled. */
|
|
2435
|
-
chainId: number
|
|
2436
|
-
/** Rows successfully dispatched. */
|
|
2437
|
-
dispatched: number
|
|
2438
|
-
/** Rows whose dispatch failed (pausing that subscription). */
|
|
2439
|
-
failed: number
|
|
2440
|
-
/** True when a bounded scan left committed work behind the same head. */
|
|
2441
|
-
hasMore?: boolean | undefined
|
|
2442
|
-
/** Highest confirmed block emitted this tick (`head - confirmations`). */
|
|
2443
|
-
safeBlock: number
|
|
2444
|
-
/** Total rows scanned across all subscriptions. */
|
|
2445
|
-
scanned: number
|
|
2446
|
-
/** Active subscriptions visited. */
|
|
2447
|
-
subscriptions: number
|
|
2448
|
-
}
|
|
2449
|
-
/** One scan boundary used to correlate production memory failures with workload shape. */
|
|
2450
|
-
type ScanCheckpoint = {
|
|
2451
|
-
/** Shared upper bound for concurrent upstream scans across event types. */
|
|
2452
|
-
concurrency: number
|
|
2453
|
-
/** Scan duration, present only after completion. */
|
|
2454
|
-
durationMs?: number | undefined
|
|
2455
|
-
/** Subscription event type scanned by this batch. */
|
|
2456
|
-
eventType: EventType
|
|
2457
|
-
/** Subscriptions whose scan rejected, present only after completion. */
|
|
2458
|
-
failedSubscriptions?: number | undefined
|
|
2459
|
-
/** Matching rows retained by completed pages, present only after completion. */
|
|
2460
|
-
matchedItems?: number | undefined
|
|
2461
|
-
/** Largest block window requested by one subscription. */
|
|
2462
|
-
maxWindowBlocks: number
|
|
2463
|
-
/** Whether the checkpoint precedes allocation or follows scan completion. */
|
|
2464
|
-
phase: 'complete' | 'start'
|
|
2465
|
-
/** Outcome of the completed RPC-backed scan. */
|
|
2466
|
-
outcome?: 'empty' | 'error' | 'missing' | 'ok' | undefined
|
|
2467
|
-
/** JSON-RPC method used by this event type's scanner. */
|
|
2468
|
-
rpcMethod: 'eth_getBlockByNumber' | 'eth_getLogs'
|
|
2469
|
-
/** Subscriptions retained in this scan batch. */
|
|
2470
|
-
subscriptionCount: number
|
|
2471
|
-
/** Maximum subscriptions retained by this event-type batch. */
|
|
2472
|
-
subscriptionLimit: number
|
|
2473
|
-
/** Sum of subscription block windows in this scan batch. */
|
|
2474
|
-
totalWindowBlocks: number
|
|
2475
|
-
}
|
|
2476
|
-
/** One bounded poll-stage timing observation. */
|
|
2477
|
-
type Stage = {
|
|
2478
|
-
/** Stage duration in milliseconds. */
|
|
2479
|
-
durationMs: number
|
|
2480
|
-
/** Event type being processed, omitted for the shared RPC-head stage. */
|
|
2481
|
-
eventType?: EventType | undefined
|
|
2482
|
-
/** Whether the stage completed. */
|
|
2483
|
-
outcome: 'error' | 'ok'
|
|
2484
|
-
/** Bounded stage name. */
|
|
2485
|
-
stage: MetricSink.PollStage
|
|
2486
|
-
}
|
|
2487
|
-
}
|
|
2488
|
-
|
|
2489
1979
|
/**
|
|
2490
1980
|
* Attaches best-effort `{ symbol, decimals }` token metadata to a
|
|
2491
1981
|
* `token:transfer` row so destinations can render human amounts. Reads the row's
|
|
@@ -2596,78 +2086,9 @@ export async function getRpcHead(client: Viem.getClient.ReturnType): Promise<num
|
|
|
2596
2086
|
return Number(await client.getBlockNumber())
|
|
2597
2087
|
}
|
|
2598
2088
|
|
|
2599
|
-
/** Groups aligned cursors only when their bounded upstream scans are equivalent. */
|
|
2600
|
-
async function scanSubscriptions(options: {
|
|
2601
|
-
client: Viem.getClient.ReturnType
|
|
2602
|
-
eventType: EventType
|
|
2603
|
-
maxBlocks: number
|
|
2604
|
-
schedule: Schedule
|
|
2605
|
-
states: readonly PollState[]
|
|
2606
|
-
}): Promise<PromiseSettledResult<ScanPage>[]> {
|
|
2607
|
-
const { client, eventType, maxBlocks, schedule, states } = options
|
|
2608
|
-
if (eventType !== 'token:transfer')
|
|
2609
|
-
return scanIndependent({ client, eventType, maxBlocks, schedule, states })
|
|
2610
|
-
return WebhookTransfer.scan({
|
|
2611
|
-
client,
|
|
2612
|
-
concurrency: pollConcurrency,
|
|
2613
|
-
maxBlocks,
|
|
2614
|
-
schedule,
|
|
2615
|
-
states: states.map((state) => ({
|
|
2616
|
-
filters: state.filters,
|
|
2617
|
-
limit: Math.min(scanPageSize, state.remaining),
|
|
2618
|
-
window: scanWindow(state.cursor, state.safeBlock, maxBlocks),
|
|
2619
|
-
})),
|
|
2620
|
-
})
|
|
2621
|
-
}
|
|
2622
|
-
|
|
2623
|
-
/** Returns the bounded JSON-RPC method used by an event scanner. */
|
|
2624
|
-
function scanRpcMethod(eventType: EventType): poll.ScanCheckpoint['rpcMethod'] {
|
|
2625
|
-
return eventType === 'block:created' || eventType === 'transaction:included'
|
|
2626
|
-
? 'eth_getBlockByNumber'
|
|
2627
|
-
: 'eth_getLogs'
|
|
2628
|
-
}
|
|
2629
|
-
|
|
2630
|
-
/** Classifies a completed batch without exposing an unbounded upstream error. */
|
|
2631
|
-
function scanResultOutcome(
|
|
2632
|
-
pages: readonly PromiseSettledResult<ScanPage>[],
|
|
2633
|
-
): NonNullable<poll.ScanCheckpoint['outcome']> {
|
|
2634
|
-
const rejected = pages.filter((page) => page.status === 'rejected')
|
|
2635
|
-
if (rejected.some((page) => page.reason instanceof BlockUnavailableError)) return 'missing'
|
|
2636
|
-
if (rejected.length > 0) return 'error'
|
|
2637
|
-
return pages.every((page) => page.status === 'fulfilled' && page.value.items.length === 0)
|
|
2638
|
-
? 'empty'
|
|
2639
|
-
: 'ok'
|
|
2640
|
-
}
|
|
2641
|
-
|
|
2642
|
-
/** Scans CPU-matched streams per subscription while sharing concurrent block reads. */
|
|
2643
|
-
function scanIndependent(options: {
|
|
2644
|
-
client: Viem.getClient.ReturnType
|
|
2645
|
-
eventType: Exclude<EventType, 'token:transfer'>
|
|
2646
|
-
maxBlocks: number
|
|
2647
|
-
schedule: Schedule
|
|
2648
|
-
states: readonly PollState[]
|
|
2649
|
-
}): Promise<PromiseSettledResult<ScanPage>[]> {
|
|
2650
|
-
const { client, eventType, maxBlocks, schedule, states } = options
|
|
2651
|
-
const cache = createChainCache(client)
|
|
2652
|
-
return mapWithLimit(states, pollConcurrency, (state) =>
|
|
2653
|
-
schedule(() => {
|
|
2654
|
-
const shared = {
|
|
2655
|
-
batchSize: Math.min(scanPageSize, state.remaining),
|
|
2656
|
-
cache,
|
|
2657
|
-
cursor: state.cursor,
|
|
2658
|
-
filters: state.filters,
|
|
2659
|
-
maxBlocks,
|
|
2660
|
-
safeBlock: state.safeBlock,
|
|
2661
|
-
}
|
|
2662
|
-
if (eventType === 'block:created') return scanBlocks(shared)
|
|
2663
|
-
if (eventType === 'log:emitted') return scanLogs({ ...shared, client })
|
|
2664
|
-
return scanTxs(shared)
|
|
2665
|
-
}),
|
|
2666
|
-
)
|
|
2667
|
-
}
|
|
2668
|
-
|
|
2669
2089
|
const filterSchemas = {
|
|
2670
2090
|
'block:created': schema.BlockFilters,
|
|
2091
|
+
'funding:deposit.updated': schema.FundingDepositFilters,
|
|
2671
2092
|
'log:emitted': schema.LogFilters,
|
|
2672
2093
|
'token:transfer': schema.TransferFilters,
|
|
2673
2094
|
'transaction:included': schema.TxFilters,
|
|
@@ -2676,7 +2097,7 @@ const filterSchemas = {
|
|
|
2676
2097
|
/**
|
|
2677
2098
|
* Validates `filters` against the schema for `eventType`, **failing closed**: an
|
|
2678
2099
|
* invalid filter throws {@link Webhooks.InvalidFilterError} instead of degrading
|
|
2679
|
-
* to `{}` (a firehose). Used at create/patch time and by
|
|
2100
|
+
* to `{}` (a firehose). Used at create/patch time and by {@link scanBlock} so a stored
|
|
2680
2101
|
* bad filter can never silently widen the match set.
|
|
2681
2102
|
*/
|
|
2682
2103
|
export function parseFilters(
|
|
@@ -2732,7 +2153,7 @@ function operandEquals(value: string, target: unknown, kind: 'number' | 'string'
|
|
|
2732
2153
|
* The JS counterpart of the old SQL `applyOperator`: evaluates one parsed filter
|
|
2733
2154
|
* operator (from {@link schema.eq}/{@link schema.compare}/{@link schema.bytes})
|
|
2734
2155
|
* against a single `0x`-hex field value pulled from an RPC object. The one place
|
|
2735
|
-
* operator semantics live, shared by every
|
|
2156
|
+
* operator semantics live, shared by every event matcher. Returns `false` for a
|
|
2736
2157
|
* missing field so an absent value never matches (fail-closed).
|
|
2737
2158
|
*
|
|
2738
2159
|
* - bare value / `{ eq }` → equality
|
|
@@ -2784,107 +2205,15 @@ function matchesCalls(calls: unknown, op: unknown): boolean {
|
|
|
2784
2205
|
return has(op)
|
|
2785
2206
|
}
|
|
2786
2207
|
|
|
2787
|
-
// RPC window helpers + per-tick lazy cache.
|
|
2788
|
-
|
|
2789
|
-
/** Renders a block number as a `0x`-hex tag for JSON-RPC params. */
|
|
2790
|
-
const blockTag = (n: number) => `0x${n.toString(16)}` as const
|
|
2791
|
-
|
|
2792
|
-
/** Issues a JSON-RPC request through the viem client for a method viem can't type. */
|
|
2793
|
-
function rpc<result>(
|
|
2794
|
-
client: Viem.getClient.ReturnType,
|
|
2795
|
-
method: string,
|
|
2796
|
-
params: readonly unknown[],
|
|
2797
|
-
): Promise<result> {
|
|
2798
|
-
return (client.request as (args: { method: string; params: unknown[] }) => Promise<result>)({
|
|
2799
|
-
method,
|
|
2800
|
-
params: [...params],
|
|
2801
|
-
})
|
|
2802
|
-
}
|
|
2803
|
-
|
|
2804
|
-
/**
|
|
2805
|
-
* The confirmed scan window for a cursor. A cursor `(cb, ci)` means rows through
|
|
2806
|
-
* `(cb, ci)` were dispatched: when `ci` is the end-of-block sentinel the whole
|
|
2807
|
-
* block `cb` is done and scanning resumes at `cb + 1`; otherwise block `cb` is
|
|
2808
|
-
* re-scanned (skipping indices `<= ci`). `to` is capped by both `safeBlock` and
|
|
2809
|
-
* the per-tick backfill cap. Returns `undefined` when the subscription is
|
|
2810
|
-
* already caught up (nothing to scan, cursor unchanged).
|
|
2811
|
-
*/
|
|
2812
|
-
function scanWindow(
|
|
2813
|
-
cursor: string | undefined,
|
|
2814
|
-
safeBlock: number,
|
|
2815
|
-
maxBlocks: number,
|
|
2816
|
-
): { from: number; skip: readonly [number, number]; to: number } | undefined {
|
|
2817
|
-
const decoded = cursor ? Cursor.decode(cursor, ['int', 'int']) : undefined
|
|
2818
|
-
const cb = decoded ? (decoded[0] as number) : 0
|
|
2819
|
-
const ci = decoded ? (decoded[1] as number) : endOfBlockIndex
|
|
2820
|
-
const from = ci >= endOfBlockIndex ? cb + 1 : cb
|
|
2821
|
-
if (from > safeBlock) return undefined
|
|
2822
|
-
const to = Math.min(safeBlock, from + maxBlocks - 1)
|
|
2823
|
-
return { from, skip: [cb, ci], to }
|
|
2824
|
-
}
|
|
2825
|
-
|
|
2826
|
-
/** True when `(block, index)` is at or before the already-dispatched cursor. */
|
|
2827
|
-
function atOrBefore(block: number, index: number, skip: readonly [number, number]): boolean {
|
|
2828
|
-
return block < skip[0] || (block === skip[0] && index <= skip[1])
|
|
2829
|
-
}
|
|
2830
|
-
|
|
2831
2208
|
/** A viem-formatted block header (no transactions), as returned by `getBlock`. */
|
|
2832
2209
|
type RpcBlock = Awaited<ReturnType<Viem.getClient.ReturnType['getBlock']>>
|
|
2833
2210
|
|
|
2834
|
-
|
|
2835
|
-
type ChainCache = {
|
|
2836
|
-
/** Full raw block (with transactions) via `eth_getBlockByNumber(n, true)`. */
|
|
2837
|
-
block(n: number): Promise<Record<string, unknown>>
|
|
2838
|
-
/** Viem-formatted block header (transaction hashes only) via `getBlock`. */
|
|
2839
|
-
header(n: number): Promise<RpcBlock | undefined>
|
|
2840
|
-
}
|
|
2841
|
-
|
|
2842
|
-
/** Builds a per-batch {@link ChainCache} bound to a viem client. */
|
|
2843
|
-
function createChainCache(client: Viem.getClient.ReturnType): ChainCache {
|
|
2844
|
-
const headers = new Map<number, Promise<RpcBlock | undefined>>()
|
|
2845
|
-
const blocks = new Map<number, Promise<Record<string, unknown>>>()
|
|
2846
|
-
|
|
2847
|
-
const singleFlight = <key, value>(
|
|
2848
|
-
pending: Map<key, Promise<value>>,
|
|
2849
|
-
key: key,
|
|
2850
|
-
load: () => Promise<value>,
|
|
2851
|
-
) => {
|
|
2852
|
-
const existing = pending.get(key)
|
|
2853
|
-
if (existing) return existing
|
|
2854
|
-
const promise = load().finally(() => pending.delete(key))
|
|
2855
|
-
pending.set(key, promise)
|
|
2856
|
-
return promise
|
|
2857
|
-
}
|
|
2858
|
-
|
|
2859
|
-
return {
|
|
2860
|
-
block(n) {
|
|
2861
|
-
return singleFlight(blocks, n, () =>
|
|
2862
|
-
// The transaction payload reuses `humanizeTransaction`, which consumes
|
|
2863
|
-
// the raw RPC tx shape, so the full block stays on the untyped path.
|
|
2864
|
-
rpc<Record<string, unknown> | null>(client, 'eth_getBlockByNumber', [
|
|
2865
|
-
blockTag(n),
|
|
2866
|
-
true,
|
|
2867
|
-
]).then((block) => {
|
|
2868
|
-
if (!block) throw new BlockUnavailableError(n)
|
|
2869
|
-
return block
|
|
2870
|
-
}),
|
|
2871
|
-
)
|
|
2872
|
-
},
|
|
2873
|
-
header(n) {
|
|
2874
|
-
return singleFlight(headers, n, () =>
|
|
2875
|
-
client.getBlock({ blockNumber: BigInt(n), includeTransactions: false }),
|
|
2876
|
-
)
|
|
2877
|
-
},
|
|
2878
|
-
}
|
|
2879
|
-
}
|
|
2880
|
-
|
|
2881
|
-
// log:emitted (eth_getLogs)
|
|
2211
|
+
// log:emitted
|
|
2882
2212
|
|
|
2883
2213
|
/**
|
|
2884
|
-
* A viem-
|
|
2885
|
-
*
|
|
2886
|
-
*
|
|
2887
|
-
* `decodeEventLog`/selector-anchoring the scanner used to do is no longer needed.
|
|
2214
|
+
* A viem-shaped log consumed by the log matchers. `args`/`eventName` are present
|
|
2215
|
+
* once {@link decodeScanLog} best-effort decodes the log against a subscription
|
|
2216
|
+
* event signature.
|
|
2888
2217
|
*/
|
|
2889
2218
|
type ScanLog = {
|
|
2890
2219
|
address: string
|
|
@@ -2913,10 +2242,9 @@ type LogEventData = {
|
|
|
2913
2242
|
}
|
|
2914
2243
|
|
|
2915
2244
|
/**
|
|
2916
|
-
* Builds the delivered `log:emitted` payload from a viem-
|
|
2245
|
+
* Builds the delivered `log:emitted` payload from a viem-shaped log plus the
|
|
2917
2246
|
* block timestamp. Returns `undefined` when a required field is missing/invalid
|
|
2918
|
-
* (the row is skipped).
|
|
2919
|
-
* already decoded `args`/`eventName`, so this just serializes them.
|
|
2247
|
+
* (the row is skipped). Decoded `args`/`eventName` are serialized when present.
|
|
2920
2248
|
*/
|
|
2921
2249
|
function toLogEvent(
|
|
2922
2250
|
log: ScanLog,
|
|
@@ -2985,7 +2313,7 @@ function pushHex(op: unknown): `0x${string}` | `0x${string}`[] | undefined {
|
|
|
2985
2313
|
return undefined
|
|
2986
2314
|
}
|
|
2987
2315
|
|
|
2988
|
-
/** Narrows an `eq`/`in` address operator into
|
|
2316
|
+
/** Narrows an `eq`/`in` address operator into a pushdown-capable address value. */
|
|
2989
2317
|
function pushAddress(op: unknown) {
|
|
2990
2318
|
return pushHex(op)
|
|
2991
2319
|
}
|
|
@@ -3001,7 +2329,7 @@ function parseEvent(signature: string | undefined): AbiEvent | undefined {
|
|
|
3001
2329
|
}
|
|
3002
2330
|
}
|
|
3003
2331
|
|
|
3004
|
-
/** Returns exact indexed arguments
|
|
2332
|
+
/** Returns exact indexed arguments usable as a selective log anchor. */
|
|
3005
2333
|
function pushEventArgs(args: Record<string, unknown> | undefined, event: AbiEvent) {
|
|
3006
2334
|
const pushed: Record<string, unknown> = {}
|
|
3007
2335
|
for (const [name, op] of Object.entries(args ?? {})) {
|
|
@@ -3012,7 +2340,7 @@ function pushEventArgs(args: Record<string, unknown> | undefined, event: AbiEven
|
|
|
3012
2340
|
return pushed
|
|
3013
2341
|
}
|
|
3014
2342
|
|
|
3015
|
-
/** Checks that the accepted filter
|
|
2343
|
+
/** Checks that the accepted filter carries a selective anchor rather than matching every log. */
|
|
3016
2344
|
function hasLogPushdown(f: z.output<typeof schema.LogFilters>): boolean {
|
|
3017
2345
|
if (pushAddress(f.address) !== undefined) return true
|
|
3018
2346
|
|
|
@@ -3025,19 +2353,7 @@ function hasLogPushdown(f: z.output<typeof schema.LogFilters>): boolean {
|
|
|
3025
2353
|
return pushHex(f.topic0) !== undefined && secondaryTopic
|
|
3026
2354
|
}
|
|
3027
2355
|
|
|
3028
|
-
/**
|
|
3029
|
-
function pushTopics(f: z.output<typeof schema.LogFilters>, event: AbiEvent | undefined) {
|
|
3030
|
-
const topics: (`0x${string}` | `0x${string}`[] | null | undefined)[] = [
|
|
3031
|
-
event ? encodeEventTopics({ abi: [event] })[0] : pushHex(f.topic0),
|
|
3032
|
-
pushHex(f.topic1),
|
|
3033
|
-
pushHex(f.topic2),
|
|
3034
|
-
pushHex(f.topic3),
|
|
3035
|
-
]
|
|
3036
|
-
while (topics.length > 0 && topics.at(-1) === undefined) topics.pop()
|
|
3037
|
-
return topics.map((topic) => topic ?? null)
|
|
3038
|
-
}
|
|
3039
|
-
|
|
3040
|
-
/** Converts one raw JSON-RPC log into the viem-shaped scanner input. */
|
|
2356
|
+
/** Converts one raw JSON-RPC log into the viem-shaped matcher input. */
|
|
3041
2357
|
function fromRpcLog(log: RpcLog): ScanLog {
|
|
3042
2358
|
return {
|
|
3043
2359
|
address: log.address,
|
|
@@ -3095,78 +2411,6 @@ export declare namespace enrichEventData {
|
|
|
3095
2411
|
}
|
|
3096
2412
|
}
|
|
3097
2413
|
|
|
3098
|
-
/** Scans contract logs for one subscription, then re-checks every filter in memory. */
|
|
3099
|
-
async function scanLogs(options: {
|
|
3100
|
-
batchSize: number
|
|
3101
|
-
cache: ChainCache
|
|
3102
|
-
client: Viem.getClient.ReturnType
|
|
3103
|
-
cursor: string | undefined
|
|
3104
|
-
filters: Record<string, unknown>
|
|
3105
|
-
maxBlocks: number
|
|
3106
|
-
safeBlock: number
|
|
3107
|
-
}): Promise<ScanPage> {
|
|
3108
|
-
const { batchSize, cache, client, cursor, filters, maxBlocks, safeBlock } = options
|
|
3109
|
-
const f = filters as z.output<typeof schema.LogFilters>
|
|
3110
|
-
const window = scanWindow(cursor, safeBlock, maxBlocks)
|
|
3111
|
-
if (!window) return { items: [], scannedThrough: undefined }
|
|
3112
|
-
|
|
3113
|
-
const event = parseEvent(f.signature)
|
|
3114
|
-
if (f.signature && (!event || !/^[A-Za-z_]\w*$/.test(event.name)))
|
|
3115
|
-
throw new Webhooks.InvalidFilterError('log:emitted', [
|
|
3116
|
-
{ message: 'Unsupported event signature.', path: ['signature'] },
|
|
3117
|
-
])
|
|
3118
|
-
|
|
3119
|
-
const address = pushAddress(f.address)
|
|
3120
|
-
const topics = pushTopics(f, event)
|
|
3121
|
-
const hasRawTopics = topics.slice(1).some((topic) => topic !== null)
|
|
3122
|
-
const args = event ? pushEventArgs(f.args, event) : {}
|
|
3123
|
-
const logs = (!event || hasRawTopics
|
|
3124
|
-
? (
|
|
3125
|
-
await rpc<RpcLog[]>(client, 'eth_getLogs', [
|
|
3126
|
-
{
|
|
3127
|
-
...(address ? { address } : {}),
|
|
3128
|
-
fromBlock: blockTag(window.from),
|
|
3129
|
-
...(topics.length > 0 ? { topics } : {}),
|
|
3130
|
-
toBlock: blockTag(window.to),
|
|
3131
|
-
},
|
|
3132
|
-
])
|
|
3133
|
-
)
|
|
3134
|
-
.map(fromRpcLog)
|
|
3135
|
-
.map((log) => (event ? decodeScanLog(log, event) : log))
|
|
3136
|
-
: await client.getLogs({
|
|
3137
|
-
...(address ? { address } : {}),
|
|
3138
|
-
...(Object.keys(args).length > 0 ? { args: args as never } : {}),
|
|
3139
|
-
event,
|
|
3140
|
-
fromBlock: BigInt(window.from),
|
|
3141
|
-
strict: false,
|
|
3142
|
-
toBlock: BigInt(window.to),
|
|
3143
|
-
})) as unknown as ScanLog[]
|
|
3144
|
-
logs.sort((a, b) => {
|
|
3145
|
-
const block = Number(a.blockNumber ?? 0n) - Number(b.blockNumber ?? 0n)
|
|
3146
|
-
return block || (a.logIndex ?? 0) - (b.logIndex ?? 0)
|
|
3147
|
-
})
|
|
3148
|
-
|
|
3149
|
-
const items: ScanItem[] = []
|
|
3150
|
-
for (const log of logs) {
|
|
3151
|
-
const blockNumber = log.blockNumber === null ? undefined : Number(log.blockNumber)
|
|
3152
|
-
const logIndex = log.logIndex ?? undefined
|
|
3153
|
-
if (blockNumber === undefined || logIndex === undefined) continue
|
|
3154
|
-
if (atOrBefore(blockNumber, logIndex, window.skip)) continue
|
|
3155
|
-
if (!matchesLog(log, f, event)) continue
|
|
3156
|
-
const data = toLogEvent(
|
|
3157
|
-
log,
|
|
3158
|
-
Value.blockToIso(await cache.header(blockNumber)),
|
|
3159
|
-
f.signature,
|
|
3160
|
-
event,
|
|
3161
|
-
)
|
|
3162
|
-
if (data) {
|
|
3163
|
-
items.push({ cursor: [data.blockNumber, data.logIndex], data })
|
|
3164
|
-
if (items.length >= batchSize) return { hasMore: true, items }
|
|
3165
|
-
}
|
|
3166
|
-
}
|
|
3167
|
-
return { items, scannedThrough: window.to }
|
|
3168
|
-
}
|
|
3169
|
-
|
|
3170
2414
|
/** Re-checks all log filters (including non-pushable operators) in JS. */
|
|
3171
2415
|
function matchesLog(
|
|
3172
2416
|
log: ScanLog,
|
|
@@ -3184,15 +2428,15 @@ function matchesLog(
|
|
|
3184
2428
|
!matchesOperator(log.blockNumber?.toString(), f.blockNumber, 'number')
|
|
3185
2429
|
)
|
|
3186
2430
|
return false
|
|
3187
|
-
//
|
|
3188
|
-
// `topic0` operator only applies in raw (no-signature) mode.
|
|
2431
|
+
// With a signature, topic0 is anchored on the event selector above, so the
|
|
2432
|
+
// raw `topic0` operator only applies in raw (no-signature) mode.
|
|
3189
2433
|
if (!event && f.topic0 !== undefined && !matchesOperator(topics[0], f.topic0)) return false
|
|
3190
2434
|
if (f.topic1 !== undefined && !matchesOperator(topics[1], f.topic1)) return false
|
|
3191
2435
|
if (f.topic2 !== undefined && !matchesOperator(topics[2], f.topic2)) return false
|
|
3192
2436
|
if (f.topic3 !== undefined && !matchesOperator(topics[3], f.topic3)) return false
|
|
3193
2437
|
|
|
3194
2438
|
if (event && f.args && Object.keys(f.args).length > 0) {
|
|
3195
|
-
//
|
|
2439
|
+
// Best-effort decoding leaves a non-decodable log without `args`.
|
|
3196
2440
|
if (log.args === undefined || Array.isArray(log.args)) return false
|
|
3197
2441
|
const args = log.args as Record<string, unknown>
|
|
3198
2442
|
for (const [name, op] of Object.entries(f.args)) {
|
|
@@ -3210,7 +2454,7 @@ function matchesLog(
|
|
|
3210
2454
|
return true
|
|
3211
2455
|
}
|
|
3212
2456
|
|
|
3213
|
-
// block:created
|
|
2457
|
+
// block:created
|
|
3214
2458
|
|
|
3215
2459
|
type BlockEventData = {
|
|
3216
2460
|
number: number
|
|
@@ -3255,35 +2499,6 @@ function toBlockEvent(block: RpcBlock): BlockEventData | undefined {
|
|
|
3255
2499
|
}
|
|
3256
2500
|
}
|
|
3257
2501
|
|
|
3258
|
-
/** Scans new blocks over one subscription's confirmed window. */
|
|
3259
|
-
async function scanBlocks(options: {
|
|
3260
|
-
batchSize: number
|
|
3261
|
-
cache: ChainCache
|
|
3262
|
-
cursor: string | undefined
|
|
3263
|
-
filters: Record<string, unknown>
|
|
3264
|
-
maxBlocks: number
|
|
3265
|
-
safeBlock: number
|
|
3266
|
-
}): Promise<ScanPage> {
|
|
3267
|
-
const { batchSize, cache, cursor, filters, maxBlocks, safeBlock } = options
|
|
3268
|
-
const f = filters as z.output<typeof schema.BlockFilters>
|
|
3269
|
-
const window = scanWindow(cursor, safeBlock, maxBlocks)
|
|
3270
|
-
if (!window) return { items: [], scannedThrough: undefined }
|
|
3271
|
-
|
|
3272
|
-
const items: ScanItem[] = []
|
|
3273
|
-
for (let blockNumber = window.from; blockNumber <= window.to; blockNumber++) {
|
|
3274
|
-
if (atOrBefore(blockNumber, 0, window.skip)) continue
|
|
3275
|
-
const block = await cache.header(blockNumber)
|
|
3276
|
-
if (!block) continue
|
|
3277
|
-
if (!matchesBlock(block, f)) continue
|
|
3278
|
-
const data = toBlockEvent(block)
|
|
3279
|
-
if (data) {
|
|
3280
|
-
items.push({ cursor: [data.number, 0], data })
|
|
3281
|
-
if (items.length >= batchSize) return { hasMore: true, items }
|
|
3282
|
-
}
|
|
3283
|
-
}
|
|
3284
|
-
return { items, scannedThrough: window.to }
|
|
3285
|
-
}
|
|
3286
|
-
|
|
3287
2502
|
/** Re-checks all block filters in JS. The RPC has no consensus `proposer`. */
|
|
3288
2503
|
function matchesBlock(block: RpcBlock, f: z.output<typeof schema.BlockFilters>): boolean {
|
|
3289
2504
|
if (f.number !== undefined && !matchesOperator(block.number?.toString(), f.number, 'number'))
|
|
@@ -3306,7 +2521,7 @@ function matchesBlock(block: RpcBlock, f: z.output<typeof schema.BlockFilters>):
|
|
|
3306
2521
|
return true
|
|
3307
2522
|
}
|
|
3308
2523
|
|
|
3309
|
-
// transaction:included
|
|
2524
|
+
// transaction:included
|
|
3310
2525
|
|
|
3311
2526
|
/** Maps a `transaction:included` scalar filter field to its raw-tx field + kind. */
|
|
3312
2527
|
const txFields = {
|
|
@@ -3324,47 +2539,6 @@ const txFields = {
|
|
|
3324
2539
|
value: { field: 'value', kind: 'number' },
|
|
3325
2540
|
} as const satisfies Record<string, { field: string; kind: 'number' | 'string' }>
|
|
3326
2541
|
|
|
3327
|
-
/** Scans included transactions over one subscription's confirmed window. */
|
|
3328
|
-
async function scanTxs(options: {
|
|
3329
|
-
batchSize: number
|
|
3330
|
-
cache: ChainCache
|
|
3331
|
-
cursor: string | undefined
|
|
3332
|
-
filters: Record<string, unknown>
|
|
3333
|
-
maxBlocks: number
|
|
3334
|
-
safeBlock: number
|
|
3335
|
-
}): Promise<ScanPage> {
|
|
3336
|
-
const { batchSize, cache, cursor, filters, maxBlocks, safeBlock } = options
|
|
3337
|
-
const f = filters as z.output<typeof schema.TxFilters>
|
|
3338
|
-
const window = scanWindow(cursor, safeBlock, maxBlocks)
|
|
3339
|
-
if (!window) return { items: [], scannedThrough: undefined }
|
|
3340
|
-
|
|
3341
|
-
const includeCalls = f.includeCalls ?? true
|
|
3342
|
-
const items: ScanItem[] = []
|
|
3343
|
-
for (let blockNumber = window.from; blockNumber <= window.to; blockNumber++) {
|
|
3344
|
-
const block = await cache.block(blockNumber)
|
|
3345
|
-
const transactions = Array.isArray(block['transactions'])
|
|
3346
|
-
? (block['transactions'] as unknown[])
|
|
3347
|
-
: []
|
|
3348
|
-
const timestamp = asString(block['timestamp'])
|
|
3349
|
-
// A transaction's own `blockTimestamp` is seconds-only, so the delivered
|
|
3350
|
-
// timestamp comes from the enclosing block, which carries milliseconds.
|
|
3351
|
-
const blockTime = Value.blockToIso(block)
|
|
3352
|
-
for (let offset = 0; offset < transactions.length; offset++) {
|
|
3353
|
-
const transaction = transactions[offset] as Record<string, unknown>
|
|
3354
|
-
const transactionIndex = Value.hexToNumber(transaction['transactionIndex']) ?? offset
|
|
3355
|
-
if (atOrBefore(blockNumber, transactionIndex, window.skip)) continue
|
|
3356
|
-
if (!matchesTx(transaction, f, includeCalls, timestamp, blockNumber)) continue
|
|
3357
|
-
const parsed = Transactions.schema.Rpc.Transaction.safeParse(transaction)
|
|
3358
|
-
if (!parsed.success) continue
|
|
3359
|
-
const humanized = Transactions.humanizeTransaction(parsed.data)
|
|
3360
|
-
const data = blockTime === undefined ? humanized : { ...humanized, timestamp: blockTime }
|
|
3361
|
-
items.push({ cursor: [blockNumber, transactionIndex], data })
|
|
3362
|
-
if (items.length >= batchSize) return { hasMore: true, items }
|
|
3363
|
-
}
|
|
3364
|
-
}
|
|
3365
|
-
return { items, scannedThrough: window.to }
|
|
3366
|
-
}
|
|
3367
|
-
|
|
3368
2542
|
/** Re-checks transaction filters against the raw RPC transaction. */
|
|
3369
2543
|
function matchesTx(
|
|
3370
2544
|
tx: Record<string, unknown>,
|
|
@@ -3420,6 +2594,16 @@ function ownerFor(c: Context<App.Environment>): Webhooks.Owner | null {
|
|
|
3420
2594
|
return payer ? { payer, type: 'mpp' } : null
|
|
3421
2595
|
}
|
|
3422
2596
|
|
|
2597
|
+
function accessFor(c: Context<App.Environment>): Webhooks.Access {
|
|
2598
|
+
const principal = Auth.getPrincipal(c)
|
|
2599
|
+
if (principal?.type !== 'api_key') throw new Error('Webhook routes require an API key.')
|
|
2600
|
+
return {
|
|
2601
|
+
environment: principal.environment,
|
|
2602
|
+
...(principal.projectId === undefined ? {} : { projectId: principal.projectId }),
|
|
2603
|
+
scopes: principal.apiKey.scopes,
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
|
|
3423
2607
|
/** Strips internal fields (`owner`, `secret`) from a subscription for reads. */
|
|
3424
2608
|
function toPublic(subscription: Webhooks.Subscription) {
|
|
3425
2609
|
return {
|
|
@@ -3427,6 +2611,10 @@ function toPublic(subscription: Webhooks.Subscription) {
|
|
|
3427
2611
|
...(subscription.context === undefined ? {} : { context: subscription.context }),
|
|
3428
2612
|
createdAt: subscription.createdAt,
|
|
3429
2613
|
destination: redactDestination(subscription.destination),
|
|
2614
|
+
...(subscription.eventType === 'funding:deposit.updated' &&
|
|
2615
|
+
subscription.environment !== undefined
|
|
2616
|
+
? { environment: subscription.environment }
|
|
2617
|
+
: {}),
|
|
3430
2618
|
eventType: subscription.eventType,
|
|
3431
2619
|
...(subscription.expiresAt === undefined ? {} : { expiresAt: subscription.expiresAt }),
|
|
3432
2620
|
failureCount: subscription.failureCount,
|
|
@@ -3709,11 +2897,3 @@ export declare namespace scanBlock {
|
|
|
3709
2897
|
matches: readonly Match<subscription>[]
|
|
3710
2898
|
}
|
|
3711
2899
|
}
|
|
3712
|
-
|
|
3713
|
-
class BlockUnavailableError extends Error {
|
|
3714
|
-
override name = 'Webhooks.BlockUnavailableError'
|
|
3715
|
-
|
|
3716
|
-
constructor(blockNumber: number) {
|
|
3717
|
-
super(`Block ${blockNumber} is unavailable.`)
|
|
3718
|
-
}
|
|
3719
|
-
}
|