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
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
import { type Db,
|
|
2
|
-
import * as z from 'zod/mini'
|
|
1
|
+
import { type Db, Webhooks as Core } from 'tapimo'
|
|
3
2
|
|
|
4
3
|
import * as Cursor from '../../../internal/Cursor.js'
|
|
5
|
-
import * as Viem from '../../../internal/Viem.js'
|
|
6
4
|
import * as TestApp from '../../../../test/App.js'
|
|
7
5
|
import * as Mppx from '../../../../test/Mppx.js'
|
|
8
|
-
import * as Runtime from '../../../../test/runtime.js'
|
|
9
6
|
import * as Tempo from '../../../../test/Tempo.js'
|
|
10
7
|
import * as Webhooks from './webhooks.js'
|
|
11
8
|
|
|
12
|
-
/** Humanized transaction payload delivered by transaction webhooks. */
|
|
13
|
-
type TransactionEvent = z.output<typeof Webhooks.schema.TransactionEvent>
|
|
14
|
-
|
|
15
9
|
/** API key with both webhook scopes, owned by `org_test`. */
|
|
16
10
|
const writer = {
|
|
17
11
|
id: 'key_writer',
|
|
@@ -115,6 +109,29 @@ describe('feature gate', () => {
|
|
|
115
109
|
const body = (await response.json()) as { error?: { code?: string } }
|
|
116
110
|
expect(body.error?.code).toMatchInlineSnapshot(`"webhooks_not_enabled"`)
|
|
117
111
|
})
|
|
112
|
+
|
|
113
|
+
test('does not register funding events without a host producer', async () => {
|
|
114
|
+
const app = TestApp.client({
|
|
115
|
+
auth: { keys: [writer] },
|
|
116
|
+
webhook: { supportedChainIds: [4217, 42431] },
|
|
117
|
+
})
|
|
118
|
+
const catalog = await app.v1.webhooks['event-types'].$get({})
|
|
119
|
+
const catalogBody = await TestApp.json(catalog, Webhooks.schema.getWebhookEventTypes.Response)
|
|
120
|
+
const created = await app.v1.webhooks.$post(
|
|
121
|
+
{
|
|
122
|
+
json: {
|
|
123
|
+
destination: { type: 'url', url: 'https://example.com/funding' },
|
|
124
|
+
eventType: 'funding:deposit.updated',
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
as(writer),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
expect(catalogBody.data.map((eventType) => eventType.type)).not.toContain(
|
|
131
|
+
'funding:deposit.updated',
|
|
132
|
+
)
|
|
133
|
+
expect(created.status).toBe(400)
|
|
134
|
+
})
|
|
118
135
|
})
|
|
119
136
|
|
|
120
137
|
describe('access matrix', () => {
|
|
@@ -191,6 +208,7 @@ describe('access matrix', () => {
|
|
|
191
208
|
expect(body.data.map((entry) => entry.type)).toMatchInlineSnapshot(`
|
|
192
209
|
[
|
|
193
210
|
"block:created",
|
|
211
|
+
"funding:deposit.updated",
|
|
194
212
|
"log:emitted",
|
|
195
213
|
"token:transfer",
|
|
196
214
|
"transaction:included",
|
|
@@ -220,6 +238,151 @@ describe('access matrix', () => {
|
|
|
220
238
|
})
|
|
221
239
|
|
|
222
240
|
describe('CRUD lifecycle', () => {
|
|
241
|
+
test('requires funding read access to create a funding deposit subscription', async () => {
|
|
242
|
+
const webhookWriter = {
|
|
243
|
+
id: 'key_funding_webhook_only',
|
|
244
|
+
orgId: 'org_funding_webhook_only',
|
|
245
|
+
scopes: ['webhooks:write'],
|
|
246
|
+
token: 'secret_funding_webhook_only',
|
|
247
|
+
} satisfies TestApp.kvStore.Key
|
|
248
|
+
const app = TestApp.client({ auth: { keys: [webhookWriter] } })
|
|
249
|
+
const response = await app.v1.webhooks.$post(
|
|
250
|
+
{
|
|
251
|
+
json: {
|
|
252
|
+
destination: { type: 'url', url: 'https://example.com/funding' },
|
|
253
|
+
eventType: 'funding:deposit.updated',
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
as(webhookWriter),
|
|
257
|
+
)
|
|
258
|
+
const body = (await response.json()) as { error?: { code?: string } }
|
|
259
|
+
|
|
260
|
+
expect(response.status).toBe(403)
|
|
261
|
+
expect(body.error?.code).toBe('api_key_forbidden')
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
test('captures funding deposit ownership scope without reading the RPC head', async () => {
|
|
265
|
+
const db = TestApp.database()
|
|
266
|
+
const projectWriter = {
|
|
267
|
+
id: 'key_funding_webhook_project',
|
|
268
|
+
orgId: 'org_funding_webhook',
|
|
269
|
+
projectId: 'prj_funding_webhook',
|
|
270
|
+
scopes: ['funding:read', 'webhooks:write'],
|
|
271
|
+
token: 'secret_funding_webhook_project',
|
|
272
|
+
} satisfies TestApp.kvStore.Key
|
|
273
|
+
const app = TestApp.client({ auth: { keys: [projectWriter] }, db })
|
|
274
|
+
const response = await app.v1.webhooks.$post(
|
|
275
|
+
{
|
|
276
|
+
json: {
|
|
277
|
+
destination: { type: 'url', url: 'https://example.com/funding' },
|
|
278
|
+
eventType: 'funding:deposit.updated',
|
|
279
|
+
filters: { status: 'completed' },
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
as(projectWriter),
|
|
283
|
+
)
|
|
284
|
+
const body = await TestApp.json(response, Webhooks.schema.createWebhook.Response)
|
|
285
|
+
const stored = await Core.getSubscription(
|
|
286
|
+
db,
|
|
287
|
+
{ orgId: projectWriter.orgId, type: 'api_key' },
|
|
288
|
+
body.id,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
expect(response.status).toBe(200)
|
|
292
|
+
expect(stored).toMatchObject({
|
|
293
|
+
environment: 'production',
|
|
294
|
+
projectId: projectWriter.projectId,
|
|
295
|
+
})
|
|
296
|
+
expect(await Core.getCursor(db, body.id)).toBeNull()
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
test('isolates funding subscriptions by API-key environment and project', async () => {
|
|
300
|
+
const db = TestApp.database()
|
|
301
|
+
const shared = {
|
|
302
|
+
orgId: 'org_funding_access',
|
|
303
|
+
scopes: ['funding:read', 'webhooks:read', 'webhooks:write'],
|
|
304
|
+
} as const
|
|
305
|
+
const projectA = {
|
|
306
|
+
...shared,
|
|
307
|
+
id: 'key_funding_project_a',
|
|
308
|
+
projectId: 'prj_funding_a',
|
|
309
|
+
token: 'secret_funding_project_a',
|
|
310
|
+
} satisfies TestApp.kvStore.Key
|
|
311
|
+
const projectB = {
|
|
312
|
+
...shared,
|
|
313
|
+
id: 'key_funding_project_b',
|
|
314
|
+
projectId: 'prj_funding_b',
|
|
315
|
+
token: 'secret_funding_project_b',
|
|
316
|
+
} satisfies TestApp.kvStore.Key
|
|
317
|
+
const sandboxProjectB = {
|
|
318
|
+
...projectB,
|
|
319
|
+
environment: 'sandbox',
|
|
320
|
+
id: 'key_funding_project_b_sandbox',
|
|
321
|
+
token: 'secret_funding_project_b_sandbox',
|
|
322
|
+
} satisfies TestApp.kvStore.Key
|
|
323
|
+
const projectBWebhookReader = {
|
|
324
|
+
...projectB,
|
|
325
|
+
id: 'key_funding_project_b_webhook_reader',
|
|
326
|
+
scopes: ['webhooks:read'],
|
|
327
|
+
token: 'secret_funding_project_b_webhook_reader',
|
|
328
|
+
} satisfies TestApp.kvStore.Key
|
|
329
|
+
const organization = {
|
|
330
|
+
...shared,
|
|
331
|
+
id: 'key_funding_organization',
|
|
332
|
+
token: 'secret_funding_organization',
|
|
333
|
+
} satisfies TestApp.kvStore.Key
|
|
334
|
+
const app = TestApp.client({
|
|
335
|
+
auth: {
|
|
336
|
+
keys: [organization, projectA, projectB, projectBWebhookReader, sandboxProjectB],
|
|
337
|
+
},
|
|
338
|
+
db,
|
|
339
|
+
})
|
|
340
|
+
const subscription = await Core.createSubscription(db, {
|
|
341
|
+
chainId: 4217,
|
|
342
|
+
destination: { type: 'url', url: 'https://example.com/project-b' },
|
|
343
|
+
environment: 'production',
|
|
344
|
+
eventType: 'funding:deposit.updated',
|
|
345
|
+
owner: { orgId: shared.orgId, type: 'api_key' },
|
|
346
|
+
projectId: projectB.projectId,
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
const listed = await app.v1.webhooks.$get({ query: { include: 'totalCount' } }, as(projectA))
|
|
350
|
+
const listedBody = await TestApp.json(listed, Webhooks.schema.listWebhooks.Response)
|
|
351
|
+
const hidden = await app.v1.webhooks[':id{wh_[A-Za-z0-9_-]+}'].$get(
|
|
352
|
+
{ param: { id: subscription.id } },
|
|
353
|
+
as(projectA),
|
|
354
|
+
)
|
|
355
|
+
const sandboxHidden = await app.v1.webhooks[':id{wh_[A-Za-z0-9_-]+}'].$get(
|
|
356
|
+
{ param: { id: subscription.id } },
|
|
357
|
+
as(sandboxProjectB),
|
|
358
|
+
)
|
|
359
|
+
const scopeHidden = await app.v1.webhooks[':id{wh_[A-Za-z0-9_-]+}'].$get(
|
|
360
|
+
{ param: { id: subscription.id } },
|
|
361
|
+
as(projectBWebhookReader),
|
|
362
|
+
)
|
|
363
|
+
const patchHidden = await app.v1.webhooks[':id{wh_[A-Za-z0-9_-]+}'].$patch(
|
|
364
|
+
{ json: { status: 'paused' }, param: { id: subscription.id } },
|
|
365
|
+
as(projectA),
|
|
366
|
+
)
|
|
367
|
+
const deleteHidden = await app.v1.webhooks[':id{wh_[A-Za-z0-9_-]+}'].$delete(
|
|
368
|
+
{ param: { id: subscription.id } },
|
|
369
|
+
as(projectA),
|
|
370
|
+
)
|
|
371
|
+
const organizationRead = await app.v1.webhooks[':id{wh_[A-Za-z0-9_-]+}'].$get(
|
|
372
|
+
{ param: { id: subscription.id } },
|
|
373
|
+
as(organization),
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
expect(listed.status).toBe(200)
|
|
377
|
+
expect(listedBody).toMatchObject({ data: [], meta: { totalCount: 0 } })
|
|
378
|
+
expect(hidden.status).toBe(404)
|
|
379
|
+
expect(sandboxHidden.status).toBe(404)
|
|
380
|
+
expect(scopeHidden.status).toBe(404)
|
|
381
|
+
expect(patchHidden.status).toBe(404)
|
|
382
|
+
expect(deleteHidden.status).toBe(404)
|
|
383
|
+
expect(organizationRead.status).toBe(200)
|
|
384
|
+
})
|
|
385
|
+
|
|
223
386
|
test('creates, reads, lists, updates, and deletes a subscription', async () => {
|
|
224
387
|
const db = TestApp.database()
|
|
225
388
|
const app = TestApp.client({
|
|
@@ -1152,6 +1315,7 @@ describe('OpenAPI visibility', () => {
|
|
|
1152
1315
|
"Included transaction event",
|
|
1153
1316
|
"Log event",
|
|
1154
1317
|
"Block event",
|
|
1318
|
+
"Funding deposit updated event",
|
|
1155
1319
|
"Ping (test delivery)",
|
|
1156
1320
|
]
|
|
1157
1321
|
`)
|
|
@@ -1250,6 +1414,14 @@ describe('OpenAPI visibility', () => {
|
|
|
1250
1414
|
],
|
|
1251
1415
|
"title": "Block subscription",
|
|
1252
1416
|
},
|
|
1417
|
+
{
|
|
1418
|
+
"filters": [
|
|
1419
|
+
"depositAddressId",
|
|
1420
|
+
"recipient",
|
|
1421
|
+
"status",
|
|
1422
|
+
],
|
|
1423
|
+
"title": "Funding deposit subscription",
|
|
1424
|
+
},
|
|
1253
1425
|
]
|
|
1254
1426
|
`)
|
|
1255
1427
|
})
|
|
@@ -1351,1496 +1523,6 @@ describe('MPP access (disabled for now — API-key-only)', () => {
|
|
|
1351
1523
|
})
|
|
1352
1524
|
})
|
|
1353
1525
|
|
|
1354
|
-
// The poll tests pin a fixed immutable historical window on testnet chain 42431.
|
|
1355
|
-
// Under `localnet`, the test RPC client is redirected to the disposable local
|
|
1356
|
-
// chain, which lacks that history, so the whole block is testnet-only.
|
|
1357
|
-
describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
|
|
1358
|
-
/** Decodes an opaque creation checkpoint or `(block, index)` poller cursor. */
|
|
1359
|
-
function decodeCursor(token: string | null): number[] | null {
|
|
1360
|
-
if (!token) return null
|
|
1361
|
-
const json = atob(token.replaceAll('-', '+').replaceAll('_', '/'))
|
|
1362
|
-
return JSON.parse(json) as number[]
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
/** Encodes a `(block, index)` keyset cursor in the poller's opaque format. */
|
|
1366
|
-
function encodeCursor(values: [number, number]): string {
|
|
1367
|
-
return btoa(JSON.stringify(values)).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '')
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
/** End-of-block index sentinel (matches the poller's own cursor sentinel). */
|
|
1371
|
-
const endOfBlockIndex = 2_147_483_647
|
|
1372
|
-
|
|
1373
|
-
// Every scanner reads this immutable window over real RPC (chain 42431 to
|
|
1374
|
-
// `rpc.testnet.tempo.xyz`), so the results remain snapshot-stable.
|
|
1375
|
-
const client = Viem.getClient({ chainId: 42431, rpc: TestApp.rpc })
|
|
1376
|
-
// Confirmed scan window `[windowFrom, windowTo]`: anchor the cursor just below
|
|
1377
|
-
// `windowFrom`, and pass `headBlockNumber = windowTo + confirmations` so the
|
|
1378
|
-
// poller's `safeBlock` lands exactly on `windowTo`.
|
|
1379
|
-
const windowFrom = 1_000_000
|
|
1380
|
-
const windowTo = 1_000_002
|
|
1381
|
-
const windowCursor = encodeCursor([windowFrom - 1, endOfBlockIndex])
|
|
1382
|
-
const windowHead = windowTo + 1
|
|
1383
|
-
// A sender whose only transfers in `[windowFrom, windowTo]` are exactly two
|
|
1384
|
-
// rows, both in block 1,000,002 (`log_idx` 0 then 1): a TIP-20 transfer
|
|
1385
|
-
// followed by its fee payment. Filtering on it makes the scan deterministic.
|
|
1386
|
-
const windowRecipient = '0x9e39034aae71fb89f66061a2602eb6efec271754'
|
|
1387
|
-
const windowSender = '0xe7687128b0a808c2831ff94d4f7b2fb35c65af38'
|
|
1388
|
-
// This token has 226 transfers across blocks 1,000,000 through 1,000,003.
|
|
1389
|
-
const volumeHead = 1_000_005
|
|
1390
|
-
const volumeToken = '0x20c0000000000000000000000000000000000001'
|
|
1391
|
-
|
|
1392
|
-
async function createTransferSubscription(db: Db.Db) {
|
|
1393
|
-
return Core.createSubscription(db, {
|
|
1394
|
-
chainId: 42431,
|
|
1395
|
-
eventType: 'token:transfer',
|
|
1396
|
-
filters: { sender: windowSender },
|
|
1397
|
-
owner: { orgId: 'org_test', type: 'api_key' },
|
|
1398
|
-
destination: { type: 'url', url: 'https://hooks.example.com/x' },
|
|
1399
|
-
})
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
async function createVolumeSubscription(db: Db.Db, ownerId = 'org_test') {
|
|
1403
|
-
return Core.createSubscription(db, {
|
|
1404
|
-
chainId: 42431,
|
|
1405
|
-
destination: { type: 'url', url: `https://hooks.example.com/${ownerId}` },
|
|
1406
|
-
eventType: 'token:transfer',
|
|
1407
|
-
filters: { token: volumeToken },
|
|
1408
|
-
owner: { orgId: ownerId, type: 'api_key' },
|
|
1409
|
-
})
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
test('dispatches a signed envelope per row and advances the cursor', async () => {
|
|
1413
|
-
const db = TestApp.database()
|
|
1414
|
-
const subscription = await createTransferSubscription(db)
|
|
1415
|
-
// Anchor the subscription just below the pinned window so the poller scans
|
|
1416
|
-
// it this tick (a brand-new subscription anchors at head and skips its first
|
|
1417
|
-
// tick).
|
|
1418
|
-
await Core.setCursor(db, subscription.id, windowCursor)
|
|
1419
|
-
|
|
1420
|
-
const dispatched: Webhooks.poll.Dispatchable[] = []
|
|
1421
|
-
const scanCheckpoints: Webhooks.poll.ScanCheckpoint[] = []
|
|
1422
|
-
const stages: Webhooks.poll.Stage[] = []
|
|
1423
|
-
let cursorLagBlocks: number | undefined
|
|
1424
|
-
const result = await Webhooks.poll({
|
|
1425
|
-
chainId: 42431,
|
|
1426
|
-
dispatch: async (input) => {
|
|
1427
|
-
dispatched.push(input)
|
|
1428
|
-
return { ok: true }
|
|
1429
|
-
},
|
|
1430
|
-
eventTypes: ['token:transfer', 'token:transfer'],
|
|
1431
|
-
headBlockNumber: windowHead,
|
|
1432
|
-
client,
|
|
1433
|
-
db,
|
|
1434
|
-
onCursorLag(blocks) {
|
|
1435
|
-
cursorLagBlocks = blocks
|
|
1436
|
-
},
|
|
1437
|
-
onScanCheckpoint(checkpoint) {
|
|
1438
|
-
scanCheckpoints.push(checkpoint)
|
|
1439
|
-
},
|
|
1440
|
-
onStage(stage) {
|
|
1441
|
-
stages.push(stage)
|
|
1442
|
-
},
|
|
1443
|
-
})
|
|
1444
|
-
|
|
1445
|
-
expect(result).toMatchInlineSnapshot(`
|
|
1446
|
-
{
|
|
1447
|
-
"chainId": 42431,
|
|
1448
|
-
"dispatched": 2,
|
|
1449
|
-
"failed": 0,
|
|
1450
|
-
"safeBlock": 1000002,
|
|
1451
|
-
"scanned": 2,
|
|
1452
|
-
"subscriptions": 1,
|
|
1453
|
-
}
|
|
1454
|
-
`)
|
|
1455
|
-
expect(cursorLagBlocks).toBe(3)
|
|
1456
|
-
expect(scanCheckpoints.map(({ durationMs: _, ...checkpoint }) => checkpoint))
|
|
1457
|
-
.toMatchInlineSnapshot(`
|
|
1458
|
-
[
|
|
1459
|
-
{
|
|
1460
|
-
"concurrency": 8,
|
|
1461
|
-
"eventType": "token:transfer",
|
|
1462
|
-
"maxWindowBlocks": 3,
|
|
1463
|
-
"phase": "start",
|
|
1464
|
-
"rpcMethod": "eth_getLogs",
|
|
1465
|
-
"subscriptionCount": 1,
|
|
1466
|
-
"subscriptionLimit": 256,
|
|
1467
|
-
"totalWindowBlocks": 3,
|
|
1468
|
-
},
|
|
1469
|
-
{
|
|
1470
|
-
"concurrency": 8,
|
|
1471
|
-
"eventType": "token:transfer",
|
|
1472
|
-
"failedSubscriptions": 0,
|
|
1473
|
-
"matchedItems": 2,
|
|
1474
|
-
"maxWindowBlocks": 3,
|
|
1475
|
-
"outcome": "ok",
|
|
1476
|
-
"phase": "complete",
|
|
1477
|
-
"rpcMethod": "eth_getLogs",
|
|
1478
|
-
"subscriptionCount": 1,
|
|
1479
|
-
"subscriptionLimit": 256,
|
|
1480
|
-
"totalWindowBlocks": 3,
|
|
1481
|
-
},
|
|
1482
|
-
]
|
|
1483
|
-
`)
|
|
1484
|
-
expect(stages.map(({ eventType, outcome, stage }) => ({ eventType, outcome, stage })))
|
|
1485
|
-
.toMatchInlineSnapshot(`
|
|
1486
|
-
[
|
|
1487
|
-
{
|
|
1488
|
-
"eventType": undefined,
|
|
1489
|
-
"outcome": "ok",
|
|
1490
|
-
"stage": "rpc_head",
|
|
1491
|
-
},
|
|
1492
|
-
{
|
|
1493
|
-
"eventType": undefined,
|
|
1494
|
-
"outcome": "ok",
|
|
1495
|
-
"stage": "subscription_read",
|
|
1496
|
-
},
|
|
1497
|
-
{
|
|
1498
|
-
"eventType": "token:transfer",
|
|
1499
|
-
"outcome": "ok",
|
|
1500
|
-
"stage": "scan",
|
|
1501
|
-
},
|
|
1502
|
-
{
|
|
1503
|
-
"eventType": "token:transfer",
|
|
1504
|
-
"outcome": "ok",
|
|
1505
|
-
"stage": "enrich",
|
|
1506
|
-
},
|
|
1507
|
-
{
|
|
1508
|
-
"eventType": "token:transfer",
|
|
1509
|
-
"outcome": "ok",
|
|
1510
|
-
"stage": "cursor_write",
|
|
1511
|
-
},
|
|
1512
|
-
]
|
|
1513
|
-
`)
|
|
1514
|
-
expect(dispatched).toHaveLength(2)
|
|
1515
|
-
// The envelope carries the decoded RPC transfer row, here the first of the
|
|
1516
|
-
// two pinned-window transfers from `windowSender`.
|
|
1517
|
-
expect(dispatched[0]?.envelope.data).toMatchInlineSnapshot(`
|
|
1518
|
-
{
|
|
1519
|
-
"address": "0x20c00000000000000000000071de0cd31ab0d105",
|
|
1520
|
-
"amount": "10000",
|
|
1521
|
-
"blockNumber": 1000002,
|
|
1522
|
-
"logIndex": 0,
|
|
1523
|
-
"recipient": "0x9e39034aae71fb89f66061a2602eb6efec271754",
|
|
1524
|
-
"sender": "0xe7687128b0a808c2831ff94d4f7b2fb35c65af38",
|
|
1525
|
-
"timestamp": "2026-01-14T18:38:03.685Z",
|
|
1526
|
-
"token": {
|
|
1527
|
-
"decimals": 6,
|
|
1528
|
-
"symbol": "QKSUSD",
|
|
1529
|
-
},
|
|
1530
|
-
"transactionHash": "0x3d24a706cc2f6f4c96620bef1f61ddb23040ff77c22c8db42918c7c424bbf9d3",
|
|
1531
|
-
}
|
|
1532
|
-
`)
|
|
1533
|
-
|
|
1534
|
-
// Cursor advanced to the last dispatched row `(1000002, 1)`.
|
|
1535
|
-
expect(decodeCursor(await Core.getCursor(db, subscription.id))).toMatchInlineSnapshot(`
|
|
1536
|
-
[
|
|
1537
|
-
1000002,
|
|
1538
|
-
1,
|
|
1539
|
-
]
|
|
1540
|
-
`)
|
|
1541
|
-
})
|
|
1542
|
-
|
|
1543
|
-
test('delivers collision-shaped token metadata from a cache hit', async () => {
|
|
1544
|
-
const db = TestApp.database()
|
|
1545
|
-
const subscription = await createTransferSubscription(db)
|
|
1546
|
-
await Core.setCursor(db, subscription.id, windowCursor)
|
|
1547
|
-
const cache = Store.memory()
|
|
1548
|
-
const address = '0x20c00000000000000000000071de0cd31ab0d105'
|
|
1549
|
-
await Store.memoize(
|
|
1550
|
-
async () => ({
|
|
1551
|
-
currency: 'USD',
|
|
1552
|
-
decimals: 6,
|
|
1553
|
-
name: 'Collision token',
|
|
1554
|
-
symbol: '123#__bigint',
|
|
1555
|
-
}),
|
|
1556
|
-
{
|
|
1557
|
-
key: `webhook:token-meta:v2:42431:${address}`,
|
|
1558
|
-
store: cache,
|
|
1559
|
-
ttl: 60_000,
|
|
1560
|
-
},
|
|
1561
|
-
)
|
|
1562
|
-
const bodies: string[] = []
|
|
1563
|
-
const fetch = (async (_input, init) => {
|
|
1564
|
-
bodies.push(typeof init?.body === 'string' ? init.body : '')
|
|
1565
|
-
return new Response(null, { status: 200 })
|
|
1566
|
-
}) satisfies typeof globalThis.fetch
|
|
1567
|
-
|
|
1568
|
-
const result = await Webhooks.poll({
|
|
1569
|
-
cache,
|
|
1570
|
-
chainId: 42431,
|
|
1571
|
-
client,
|
|
1572
|
-
db,
|
|
1573
|
-
dispatch: ({ envelope, subscription }) =>
|
|
1574
|
-
Core.deliverAndRecord(db, subscription, envelope, { fetch }),
|
|
1575
|
-
eventTypes: ['token:transfer'],
|
|
1576
|
-
headBlockNumber: windowHead,
|
|
1577
|
-
})
|
|
1578
|
-
|
|
1579
|
-
const payloads = bodies.map((body) => Webhooks.schema.Envelope.parse(JSON.parse(body)))
|
|
1580
|
-
const delivered = payloads.find(
|
|
1581
|
-
(payload) => payload.type === 'token:transfer' && payload.data.address === address,
|
|
1582
|
-
)
|
|
1583
|
-
expect(result.dispatched).toBe(2)
|
|
1584
|
-
expect(delivered?.type === 'token:transfer' ? delivered.data.token?.symbol : undefined).toBe(
|
|
1585
|
-
'123#__bigint',
|
|
1586
|
-
)
|
|
1587
|
-
})
|
|
1588
|
-
|
|
1589
|
-
test('scans event types concurrently without increasing scan bounds', async () => {
|
|
1590
|
-
const db = TestApp.database()
|
|
1591
|
-
const transfer = await createTransferSubscription(db)
|
|
1592
|
-
const block = await Core.createSubscription(db, {
|
|
1593
|
-
chainId: 42431,
|
|
1594
|
-
destination: { type: 'url', url: 'https://hooks.example.com/block' },
|
|
1595
|
-
eventType: 'block:created',
|
|
1596
|
-
filters: {},
|
|
1597
|
-
owner: { orgId: 'org_block', type: 'api_key' },
|
|
1598
|
-
})
|
|
1599
|
-
await Promise.all([
|
|
1600
|
-
Core.setCursor(db, transfer.id, windowCursor),
|
|
1601
|
-
Core.setCursor(db, block.id, windowCursor),
|
|
1602
|
-
])
|
|
1603
|
-
|
|
1604
|
-
const active = new Map<
|
|
1605
|
-
Webhooks.EventType,
|
|
1606
|
-
Pick<Webhooks.poll.ScanCheckpoint, 'concurrency' | 'subscriptionLimit'>
|
|
1607
|
-
>()
|
|
1608
|
-
let maxConcurrency = 0
|
|
1609
|
-
let maxEventTypes = 0
|
|
1610
|
-
let maxSubscriptionLimit = 0
|
|
1611
|
-
await Webhooks.poll({
|
|
1612
|
-
chainId: 42431,
|
|
1613
|
-
client,
|
|
1614
|
-
db,
|
|
1615
|
-
dispatch: async () => ({ ok: true }),
|
|
1616
|
-
eventTypes: ['token:transfer', 'block:created'],
|
|
1617
|
-
headBlockNumber: windowHead,
|
|
1618
|
-
onScanCheckpoint(checkpoint) {
|
|
1619
|
-
expect(checkpoint.subscriptionCount).toBeLessThanOrEqual(checkpoint.subscriptionLimit)
|
|
1620
|
-
if (checkpoint.phase === 'start')
|
|
1621
|
-
active.set(checkpoint.eventType, {
|
|
1622
|
-
concurrency: checkpoint.concurrency,
|
|
1623
|
-
subscriptionLimit: checkpoint.subscriptionLimit,
|
|
1624
|
-
})
|
|
1625
|
-
else active.delete(checkpoint.eventType)
|
|
1626
|
-
maxConcurrency = Math.max(
|
|
1627
|
-
maxConcurrency,
|
|
1628
|
-
...[...active.values()].map((value) => value.concurrency),
|
|
1629
|
-
)
|
|
1630
|
-
maxEventTypes = Math.max(maxEventTypes, active.size)
|
|
1631
|
-
maxSubscriptionLimit = Math.max(
|
|
1632
|
-
maxSubscriptionLimit,
|
|
1633
|
-
[...active.values()].reduce((total, value) => total + value.subscriptionLimit, 0),
|
|
1634
|
-
)
|
|
1635
|
-
},
|
|
1636
|
-
})
|
|
1637
|
-
|
|
1638
|
-
expect(active.size).toBe(0)
|
|
1639
|
-
expect(maxConcurrency).toBe(8)
|
|
1640
|
-
expect(maxEventTypes).toBe(2)
|
|
1641
|
-
expect(maxSubscriptionLimit).toBe(256)
|
|
1642
|
-
})
|
|
1643
|
-
|
|
1644
|
-
test('dispatches a page as one batch and advances the cursor', async () => {
|
|
1645
|
-
const db = TestApp.database()
|
|
1646
|
-
const subscription = await createTransferSubscription(db)
|
|
1647
|
-
await Core.setCursor(db, subscription.id, windowCursor)
|
|
1648
|
-
|
|
1649
|
-
const batches: Webhooks.poll.Dispatchable[][] = []
|
|
1650
|
-
const result = await Webhooks.poll({
|
|
1651
|
-
chainId: 42431,
|
|
1652
|
-
client,
|
|
1653
|
-
db,
|
|
1654
|
-
dispatchBatch: async (dispatchables, context) => {
|
|
1655
|
-
expect(context.db).toBe(db)
|
|
1656
|
-
batches.push([...dispatchables])
|
|
1657
|
-
return { ok: true }
|
|
1658
|
-
},
|
|
1659
|
-
eventTypes: ['token:transfer'],
|
|
1660
|
-
headBlockNumber: windowHead,
|
|
1661
|
-
})
|
|
1662
|
-
|
|
1663
|
-
expect(result.dispatched).toBe(2)
|
|
1664
|
-
expect(batches.map((batch) => batch.length)).toEqual([2])
|
|
1665
|
-
expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([windowTo, 1])
|
|
1666
|
-
})
|
|
1667
|
-
|
|
1668
|
-
test('keeps the cursor unchanged when a batch dispatch fails', async () => {
|
|
1669
|
-
const db = TestApp.database()
|
|
1670
|
-
const subscription = await createTransferSubscription(db)
|
|
1671
|
-
await Core.setCursor(db, subscription.id, windowCursor)
|
|
1672
|
-
|
|
1673
|
-
const result = await Webhooks.poll({
|
|
1674
|
-
chainId: 42431,
|
|
1675
|
-
client,
|
|
1676
|
-
db,
|
|
1677
|
-
dispatchBatch: async () => ({ error: 'unreachable', ok: false }),
|
|
1678
|
-
eventTypes: ['token:transfer'],
|
|
1679
|
-
headBlockNumber: windowHead,
|
|
1680
|
-
})
|
|
1681
|
-
|
|
1682
|
-
expect(result.dispatched).toBe(0)
|
|
1683
|
-
expect(result.failed).toBe(2)
|
|
1684
|
-
expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([
|
|
1685
|
-
windowFrom - 1,
|
|
1686
|
-
endOfBlockIndex,
|
|
1687
|
-
])
|
|
1688
|
-
})
|
|
1689
|
-
|
|
1690
|
-
test('matches the either-side address filter through Viem log filters', async () => {
|
|
1691
|
-
const db = TestApp.database()
|
|
1692
|
-
const subscription = await Core.createSubscription(db, {
|
|
1693
|
-
chainId: 42431,
|
|
1694
|
-
destination: { type: 'url', url: 'https://hooks.example.com/address' },
|
|
1695
|
-
eventType: 'token:transfer',
|
|
1696
|
-
filters: { address: windowSender },
|
|
1697
|
-
owner: { orgId: 'org_address', type: 'api_key' },
|
|
1698
|
-
})
|
|
1699
|
-
await Core.setCursor(db, subscription.id, windowCursor)
|
|
1700
|
-
|
|
1701
|
-
const positions: string[] = []
|
|
1702
|
-
const result = await Webhooks.poll({
|
|
1703
|
-
chainId: 42431,
|
|
1704
|
-
client,
|
|
1705
|
-
db,
|
|
1706
|
-
dispatch: async ({ envelope }) => {
|
|
1707
|
-
const data = envelope.data as { blockNumber: number; logIndex: number }
|
|
1708
|
-
positions.push(`${data.blockNumber}:${data.logIndex}`)
|
|
1709
|
-
return { ok: true }
|
|
1710
|
-
},
|
|
1711
|
-
eventTypes: ['token:transfer'],
|
|
1712
|
-
headBlockNumber: windowHead,
|
|
1713
|
-
})
|
|
1714
|
-
|
|
1715
|
-
expect(result.dispatched).toBe(2)
|
|
1716
|
-
expect(positions).toEqual(['1000002:0', '1000002:1'])
|
|
1717
|
-
})
|
|
1718
|
-
|
|
1719
|
-
test('keeps distinct transfer pushdowns isolated', async () => {
|
|
1720
|
-
const db = TestApp.database()
|
|
1721
|
-
const recipient = await Core.createSubscription(db, {
|
|
1722
|
-
chainId: 42431,
|
|
1723
|
-
destination: { type: 'url', url: 'https://hooks.example.com/recipient' },
|
|
1724
|
-
eventType: 'token:transfer',
|
|
1725
|
-
filters: { recipient: windowRecipient },
|
|
1726
|
-
owner: { orgId: 'org_recipient', type: 'api_key' },
|
|
1727
|
-
})
|
|
1728
|
-
const sender = await createTransferSubscription(db)
|
|
1729
|
-
await Promise.all([
|
|
1730
|
-
Core.setCursor(db, recipient.id, windowCursor),
|
|
1731
|
-
Core.setCursor(db, sender.id, windowCursor),
|
|
1732
|
-
])
|
|
1733
|
-
|
|
1734
|
-
const dispatched: Webhooks.poll.Dispatchable[] = []
|
|
1735
|
-
const result = await Webhooks.poll({
|
|
1736
|
-
chainId: 42431,
|
|
1737
|
-
client,
|
|
1738
|
-
db,
|
|
1739
|
-
dispatch: async (input) => {
|
|
1740
|
-
dispatched.push(input)
|
|
1741
|
-
return { ok: true }
|
|
1742
|
-
},
|
|
1743
|
-
eventTypes: ['token:transfer'],
|
|
1744
|
-
headBlockNumber: windowHead,
|
|
1745
|
-
})
|
|
1746
|
-
|
|
1747
|
-
expect(result.dispatched).toBe(3)
|
|
1748
|
-
expect(
|
|
1749
|
-
Object.fromEntries(
|
|
1750
|
-
[...Map.groupBy(dispatched, ({ subscription }) => subscription.id)].map(([id, items]) => [
|
|
1751
|
-
id,
|
|
1752
|
-
items.length,
|
|
1753
|
-
]),
|
|
1754
|
-
),
|
|
1755
|
-
).toEqual({ [recipient.id]: 1, [sender.id]: 2 })
|
|
1756
|
-
})
|
|
1757
|
-
|
|
1758
|
-
test('pauses the cursor at the last success when dispatch fails', async () => {
|
|
1759
|
-
const db = TestApp.database()
|
|
1760
|
-
const subscription = await createTransferSubscription(db)
|
|
1761
|
-
await Core.setCursor(db, subscription.id, windowCursor)
|
|
1762
|
-
|
|
1763
|
-
let calls = 0
|
|
1764
|
-
const result = await Webhooks.poll({
|
|
1765
|
-
chainId: 42431,
|
|
1766
|
-
// Succeed on the first row, fail on the second.
|
|
1767
|
-
dispatch: async () => (++calls === 1 ? { ok: true } : { error: 'boom', ok: false }),
|
|
1768
|
-
eventTypes: ['token:transfer'],
|
|
1769
|
-
headBlockNumber: windowHead,
|
|
1770
|
-
client,
|
|
1771
|
-
db,
|
|
1772
|
-
})
|
|
1773
|
-
|
|
1774
|
-
expect(result.dispatched).toMatchInlineSnapshot(`1`)
|
|
1775
|
-
expect(result.failed).toMatchInlineSnapshot(`1`)
|
|
1776
|
-
|
|
1777
|
-
// Cursor stops at the first (successful) row `(1000002, 0)`, so the failed
|
|
1778
|
-
// one re-runs on the next tick (at-least-once).
|
|
1779
|
-
expect(decodeCursor(await Core.getCursor(db, subscription.id))).toMatchInlineSnapshot(`
|
|
1780
|
-
[
|
|
1781
|
-
1000002,
|
|
1782
|
-
0,
|
|
1783
|
-
]
|
|
1784
|
-
`)
|
|
1785
|
-
})
|
|
1786
|
-
|
|
1787
|
-
test('does not advance the cursor when the first row fails', async () => {
|
|
1788
|
-
const db = TestApp.database()
|
|
1789
|
-
const subscription = await createTransferSubscription(db)
|
|
1790
|
-
await Core.setCursor(db, subscription.id, windowCursor)
|
|
1791
|
-
|
|
1792
|
-
const result = await Webhooks.poll({
|
|
1793
|
-
chainId: 42431,
|
|
1794
|
-
dispatch: async () => ({ error: 'unreachable', ok: false }),
|
|
1795
|
-
eventTypes: ['token:transfer'],
|
|
1796
|
-
headBlockNumber: windowHead,
|
|
1797
|
-
client,
|
|
1798
|
-
db,
|
|
1799
|
-
})
|
|
1800
|
-
|
|
1801
|
-
expect(result.dispatched).toMatchInlineSnapshot(`0`)
|
|
1802
|
-
expect(result.failed).toMatchInlineSnapshot(`1`)
|
|
1803
|
-
// Nothing accepted → cursor untouched at the window anchor, so the row
|
|
1804
|
-
// retries next tick.
|
|
1805
|
-
expect(decodeCursor(await Core.getCursor(db, subscription.id))).toMatchInlineSnapshot(`
|
|
1806
|
-
[
|
|
1807
|
-
999999,
|
|
1808
|
-
2147483647,
|
|
1809
|
-
]
|
|
1810
|
-
`)
|
|
1811
|
-
})
|
|
1812
|
-
|
|
1813
|
-
test('stops dispatching when the poll signal is aborted', async () => {
|
|
1814
|
-
const db = TestApp.database()
|
|
1815
|
-
const subscription = await createTransferSubscription(db)
|
|
1816
|
-
await Core.setCursor(db, subscription.id, windowCursor)
|
|
1817
|
-
|
|
1818
|
-
const abort = new AbortController()
|
|
1819
|
-
let calls = 0
|
|
1820
|
-
await expect(
|
|
1821
|
-
Webhooks.poll({
|
|
1822
|
-
chainId: 42431,
|
|
1823
|
-
client,
|
|
1824
|
-
db,
|
|
1825
|
-
dispatch: async () => {
|
|
1826
|
-
calls++
|
|
1827
|
-
abort.abort(new Error('lease lost'))
|
|
1828
|
-
return { ok: true }
|
|
1829
|
-
},
|
|
1830
|
-
eventTypes: ['token:transfer'],
|
|
1831
|
-
headBlockNumber: windowHead,
|
|
1832
|
-
signal: abort.signal,
|
|
1833
|
-
}),
|
|
1834
|
-
).rejects.toThrow('lease lost')
|
|
1835
|
-
expect(calls).toBe(1)
|
|
1836
|
-
})
|
|
1837
|
-
|
|
1838
|
-
test('drains two bounded transfer pages per tick without gaps', async () => {
|
|
1839
|
-
const db = TestApp.database()
|
|
1840
|
-
const subscription = await createVolumeSubscription(db)
|
|
1841
|
-
await Core.setCursor(db, subscription.id, windowCursor)
|
|
1842
|
-
|
|
1843
|
-
const positions: string[] = []
|
|
1844
|
-
const tick = () =>
|
|
1845
|
-
Webhooks.poll({
|
|
1846
|
-
chainId: 42431,
|
|
1847
|
-
client,
|
|
1848
|
-
confirmations: 2,
|
|
1849
|
-
db,
|
|
1850
|
-
dispatch: async ({ envelope }) => {
|
|
1851
|
-
const data = envelope.data as { blockNumber: number; logIndex: number }
|
|
1852
|
-
positions.push(`${data.blockNumber}:${data.logIndex}`)
|
|
1853
|
-
return { ok: true }
|
|
1854
|
-
},
|
|
1855
|
-
eventTypes: ['token:transfer'],
|
|
1856
|
-
headBlockNumber: volumeHead,
|
|
1857
|
-
})
|
|
1858
|
-
|
|
1859
|
-
expect((await tick()).dispatched).toBe(200)
|
|
1860
|
-
expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([1_000_003, 144])
|
|
1861
|
-
expect((await tick()).dispatched).toBe(26)
|
|
1862
|
-
expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([1_000_003, 223])
|
|
1863
|
-
expect(positions).toHaveLength(226)
|
|
1864
|
-
expect(new Set(positions).size).toBe(226)
|
|
1865
|
-
expect(positions[99]).toBe('1000002:9')
|
|
1866
|
-
expect(positions[100]).toBe('1000002:10')
|
|
1867
|
-
})
|
|
1868
|
-
|
|
1869
|
-
test('commits the first page before a continuation delivery fails', async () => {
|
|
1870
|
-
const db = TestApp.database()
|
|
1871
|
-
const subscription = await createVolumeSubscription(db)
|
|
1872
|
-
await Core.setCursor(db, subscription.id, windowCursor)
|
|
1873
|
-
|
|
1874
|
-
let calls = 0
|
|
1875
|
-
const first = await Webhooks.poll({
|
|
1876
|
-
chainId: 42431,
|
|
1877
|
-
client,
|
|
1878
|
-
confirmations: 2,
|
|
1879
|
-
db,
|
|
1880
|
-
dispatch: async () => (++calls === 101 ? { error: 'unreachable', ok: false } : { ok: true }),
|
|
1881
|
-
eventTypes: ['token:transfer'],
|
|
1882
|
-
headBlockNumber: volumeHead,
|
|
1883
|
-
})
|
|
1884
|
-
|
|
1885
|
-
expect(first.dispatched).toBe(100)
|
|
1886
|
-
expect(first.failed).toBe(1)
|
|
1887
|
-
expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([1_000_002, 9])
|
|
1888
|
-
|
|
1889
|
-
let resumed: readonly [number, number] | undefined
|
|
1890
|
-
await Webhooks.poll({
|
|
1891
|
-
batchSize: 1,
|
|
1892
|
-
chainId: 42431,
|
|
1893
|
-
client,
|
|
1894
|
-
confirmations: 2,
|
|
1895
|
-
db,
|
|
1896
|
-
dispatch: async ({ envelope }) => {
|
|
1897
|
-
const data = envelope.data as { blockNumber: number; logIndex: number }
|
|
1898
|
-
resumed = [data.blockNumber, data.logIndex]
|
|
1899
|
-
return { ok: true }
|
|
1900
|
-
},
|
|
1901
|
-
eventTypes: ['token:transfer'],
|
|
1902
|
-
headBlockNumber: volumeHead,
|
|
1903
|
-
})
|
|
1904
|
-
expect(resumed).toEqual([1_000_002, 10])
|
|
1905
|
-
})
|
|
1906
|
-
|
|
1907
|
-
test('round-robins continuation pages across subscriptions', async () => {
|
|
1908
|
-
const db = TestApp.database()
|
|
1909
|
-
const a = await createVolumeSubscription(db, 'org_a')
|
|
1910
|
-
const b = await createVolumeSubscription(db, 'org_b')
|
|
1911
|
-
await Core.setCursor(db, a.id, windowCursor)
|
|
1912
|
-
await Core.setCursor(db, b.id, windowCursor)
|
|
1913
|
-
const [first, second] = [a.id, b.id].sort()
|
|
1914
|
-
|
|
1915
|
-
const order: string[] = []
|
|
1916
|
-
await Webhooks.poll({
|
|
1917
|
-
chainId: 42431,
|
|
1918
|
-
client,
|
|
1919
|
-
confirmations: 2,
|
|
1920
|
-
db,
|
|
1921
|
-
dispatch: async ({ subscription }) => {
|
|
1922
|
-
order.push(subscription.id)
|
|
1923
|
-
return { ok: true }
|
|
1924
|
-
},
|
|
1925
|
-
eventTypes: ['token:transfer'],
|
|
1926
|
-
headBlockNumber: volumeHead,
|
|
1927
|
-
})
|
|
1928
|
-
|
|
1929
|
-
expect(order).toHaveLength(400)
|
|
1930
|
-
expect(new Set(order.slice(0, 100))).toEqual(new Set([first]))
|
|
1931
|
-
expect(new Set(order.slice(100, 200))).toEqual(new Set([second]))
|
|
1932
|
-
expect(new Set(order.slice(200, 300))).toEqual(new Set([first]))
|
|
1933
|
-
expect(new Set(order.slice(300, 400))).toEqual(new Set([second]))
|
|
1934
|
-
})
|
|
1935
|
-
|
|
1936
|
-
test('anchors a first-seen subscription at the confirmed head without backfilling', async () => {
|
|
1937
|
-
const db = TestApp.database()
|
|
1938
|
-
const subscription = await createTransferSubscription(db)
|
|
1939
|
-
|
|
1940
|
-
const dispatched: Webhooks.poll.Dispatchable[] = []
|
|
1941
|
-
const result = await Webhooks.poll({
|
|
1942
|
-
chainId: 42431,
|
|
1943
|
-
dispatch: async (input) => {
|
|
1944
|
-
dispatched.push(input)
|
|
1945
|
-
return { ok: true }
|
|
1946
|
-
},
|
|
1947
|
-
eventTypes: ['token:transfer'],
|
|
1948
|
-
headBlockNumber: windowHead,
|
|
1949
|
-
client,
|
|
1950
|
-
db,
|
|
1951
|
-
// Rows exist in history, but a brand-new subscription must not replay them.
|
|
1952
|
-
})
|
|
1953
|
-
|
|
1954
|
-
// Nothing scanned or dispatched on the first tick; the cursor is anchored at
|
|
1955
|
-
// the head of the confirmed window (`windowHead - confirmations` = 1000002).
|
|
1956
|
-
expect(result.scanned).toMatchInlineSnapshot(`0`)
|
|
1957
|
-
expect(dispatched).toHaveLength(0)
|
|
1958
|
-
expect(decodeCursor(await Core.getCursor(db, subscription.id))).toMatchInlineSnapshot(`
|
|
1959
|
-
[
|
|
1960
|
-
1000002,
|
|
1961
|
-
2147483647,
|
|
1962
|
-
]
|
|
1963
|
-
`)
|
|
1964
|
-
})
|
|
1965
|
-
|
|
1966
|
-
test('dispatches creation-head replacements once confirmed', async () => {
|
|
1967
|
-
const db = TestApp.database()
|
|
1968
|
-
const creationHead = windowTo
|
|
1969
|
-
const subscription = await Core.createSubscription(
|
|
1970
|
-
db,
|
|
1971
|
-
{
|
|
1972
|
-
chainId: 42431,
|
|
1973
|
-
destination: { type: 'url', url: 'https://hooks.example.com/x' },
|
|
1974
|
-
eventType: 'token:transfer',
|
|
1975
|
-
filters: { sender: windowSender },
|
|
1976
|
-
owner: { orgId: 'org_test', type: 'api_key' },
|
|
1977
|
-
},
|
|
1978
|
-
{ startBlockNumber: creationHead },
|
|
1979
|
-
)
|
|
1980
|
-
|
|
1981
|
-
const result = await Webhooks.poll({
|
|
1982
|
-
chainId: 42431,
|
|
1983
|
-
confirmations: 3,
|
|
1984
|
-
dispatch: async () => ({ ok: true }),
|
|
1985
|
-
eventTypes: ['token:transfer'],
|
|
1986
|
-
headBlockNumber: creationHead + 3,
|
|
1987
|
-
client,
|
|
1988
|
-
db,
|
|
1989
|
-
})
|
|
1990
|
-
|
|
1991
|
-
expect(result.dispatched).toBe(2)
|
|
1992
|
-
expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([windowTo, 1])
|
|
1993
|
-
})
|
|
1994
|
-
|
|
1995
|
-
test('keeps confirmed creation anchors while sources catch up', async () => {
|
|
1996
|
-
const db = TestApp.database()
|
|
1997
|
-
const startBlockNumber = windowTo + 100
|
|
1998
|
-
const transfer = await Core.createSubscription(
|
|
1999
|
-
db,
|
|
2000
|
-
{
|
|
2001
|
-
chainId: 42431,
|
|
2002
|
-
destination: { type: 'url', url: 'https://hooks.example.com/transfer' },
|
|
2003
|
-
eventType: 'token:transfer',
|
|
2004
|
-
filters: { sender: windowSender },
|
|
2005
|
-
owner: { orgId: 'org_transfer', type: 'api_key' },
|
|
2006
|
-
},
|
|
2007
|
-
{ startBlockNumber },
|
|
2008
|
-
)
|
|
2009
|
-
const block = await Core.createSubscription(
|
|
2010
|
-
db,
|
|
2011
|
-
{
|
|
2012
|
-
chainId: 42431,
|
|
2013
|
-
destination: { type: 'url', url: 'https://hooks.example.com/block' },
|
|
2014
|
-
eventType: 'block:created',
|
|
2015
|
-
filters: {},
|
|
2016
|
-
owner: { orgId: 'org_block', type: 'api_key' },
|
|
2017
|
-
},
|
|
2018
|
-
{ startBlockNumber },
|
|
2019
|
-
)
|
|
2020
|
-
|
|
2021
|
-
await Webhooks.poll({
|
|
2022
|
-
chainId: 42431,
|
|
2023
|
-
dispatch: async () => ({ ok: true }),
|
|
2024
|
-
eventTypes: ['token:transfer', 'block:created'],
|
|
2025
|
-
headBlockNumber: windowHead,
|
|
2026
|
-
client,
|
|
2027
|
-
db,
|
|
2028
|
-
})
|
|
2029
|
-
|
|
2030
|
-
expect(decodeCursor(await Core.getCursor(db, transfer.id))).toEqual([
|
|
2031
|
-
startBlockNumber - 1,
|
|
2032
|
-
endOfBlockIndex,
|
|
2033
|
-
])
|
|
2034
|
-
expect(decodeCursor(await Core.getCursor(db, block.id))).toEqual([
|
|
2035
|
-
startBlockNumber - 1,
|
|
2036
|
-
endOfBlockIndex,
|
|
2037
|
-
])
|
|
2038
|
-
})
|
|
2039
|
-
|
|
2040
|
-
test('advances to the end of the confirmed window on an empty scan', async () => {
|
|
2041
|
-
const db = TestApp.database()
|
|
2042
|
-
const subscription = await createTransferSubscription(db)
|
|
2043
|
-
// Anchor just past the window's only matching block (1000002) and confirm up
|
|
2044
|
-
// to block 1000004 — a range `windowSender` has no transfers in, so the scan
|
|
2045
|
-
// is genuinely empty.
|
|
2046
|
-
await Core.setCursor(db, subscription.id, encodeCursor([windowTo, endOfBlockIndex]))
|
|
2047
|
-
|
|
2048
|
-
const result = await Webhooks.poll({
|
|
2049
|
-
chainId: 42431,
|
|
2050
|
-
dispatch: async () => ({ ok: true }),
|
|
2051
|
-
eventTypes: ['token:transfer'],
|
|
2052
|
-
headBlockNumber: windowTo + 3,
|
|
2053
|
-
client,
|
|
2054
|
-
db,
|
|
2055
|
-
})
|
|
2056
|
-
|
|
2057
|
-
expect(result.scanned).toMatchInlineSnapshot(`0`)
|
|
2058
|
-
|
|
2059
|
-
// No rows, clean scan: cursor jumps past the end of the confirmed window
|
|
2060
|
-
// (`headBlockNumber - confirmations` = block 1000004) so it is not rescanned.
|
|
2061
|
-
expect(decodeCursor(await Core.getCursor(db, subscription.id))).toMatchInlineSnapshot(`
|
|
2062
|
-
[
|
|
2063
|
-
1000004,
|
|
2064
|
-
2147483647,
|
|
2065
|
-
]
|
|
2066
|
-
`)
|
|
2067
|
-
})
|
|
2068
|
-
|
|
2069
|
-
// The two pinned-window rows are `Transfer(from, to, value)` logs. Their raw
|
|
2070
|
-
// topic0 (the `Transfer` selector) and the `from` address padded to a bytes32
|
|
2071
|
-
// topic let the raw-log path be filtered as deterministically as the decoded
|
|
2072
|
-
// path.
|
|
2073
|
-
const transferSignature =
|
|
2074
|
-
'event Transfer(address indexed from, address indexed to, uint256 value)'
|
|
2075
|
-
const transferSelector = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
|
|
2076
|
-
const windowToken = '0x20c00000000000000000000071de0cd31ab0d105'
|
|
2077
|
-
const windowSenderTopic = `0x${'0'.repeat(24)}${windowSender.slice(2)}`
|
|
2078
|
-
|
|
2079
|
-
test('log:emitted decodes args from a signature-matched event view', async () => {
|
|
2080
|
-
const db = TestApp.database()
|
|
2081
|
-
const subscription = await Core.createSubscription(db, {
|
|
2082
|
-
chainId: 42431,
|
|
2083
|
-
eventType: 'log:emitted',
|
|
2084
|
-
filters: { signature: transferSignature, args: { from: windowSender } },
|
|
2085
|
-
owner: { orgId: 'org_test', type: 'api_key' },
|
|
2086
|
-
destination: { type: 'url', url: 'https://hooks.example.com/x' },
|
|
2087
|
-
})
|
|
2088
|
-
await Core.setCursor(db, subscription.id, windowCursor)
|
|
2089
|
-
|
|
2090
|
-
const dispatched: Webhooks.poll.Dispatchable[] = []
|
|
2091
|
-
const result = await Webhooks.poll({
|
|
2092
|
-
chainId: 42431,
|
|
2093
|
-
dispatch: async (input) => {
|
|
2094
|
-
dispatched.push(input)
|
|
2095
|
-
return { ok: true }
|
|
2096
|
-
},
|
|
2097
|
-
eventTypes: ['log:emitted'],
|
|
2098
|
-
headBlockNumber: windowHead,
|
|
2099
|
-
client,
|
|
2100
|
-
db,
|
|
2101
|
-
})
|
|
2102
|
-
|
|
2103
|
-
expect(result.scanned).toMatchInlineSnapshot(`2`)
|
|
2104
|
-
expect(dispatched[0]?.envelope.data).toMatchInlineSnapshot(`
|
|
2105
|
-
{
|
|
2106
|
-
"address": "0x20c00000000000000000000071de0cd31ab0d105",
|
|
2107
|
-
"args": {
|
|
2108
|
-
"from": "0xE7687128b0A808C2831ff94d4F7B2fb35C65aF38",
|
|
2109
|
-
"to": "0x9E39034aAE71fb89f66061a2602eb6eFeC271754",
|
|
2110
|
-
"value": "10000",
|
|
2111
|
-
},
|
|
2112
|
-
"blockNumber": 1000002,
|
|
2113
|
-
"data": "0x0000000000000000000000000000000000000000000000000000000000002710",
|
|
2114
|
-
"event": {
|
|
2115
|
-
"name": "Transfer",
|
|
2116
|
-
"signature": "event Transfer(address indexed from, address indexed to, uint256 value)",
|
|
2117
|
-
"topic0": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
|
2118
|
-
},
|
|
2119
|
-
"logIndex": 0,
|
|
2120
|
-
"timestamp": "2026-01-14T18:38:03.685Z",
|
|
2121
|
-
"topics": [
|
|
2122
|
-
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
|
2123
|
-
"0x000000000000000000000000e7687128b0a808c2831ff94d4f7b2fb35c65af38",
|
|
2124
|
-
"0x0000000000000000000000009e39034aae71fb89f66061a2602eb6efec271754",
|
|
2125
|
-
],
|
|
2126
|
-
"transactionHash": "0x3d24a706cc2f6f4c96620bef1f61ddb23040ff77c22c8db42918c7c424bbf9d3",
|
|
2127
|
-
"transactionIndex": 0,
|
|
2128
|
-
}
|
|
2129
|
-
`)
|
|
2130
|
-
})
|
|
2131
|
-
|
|
2132
|
-
test('log:emitted scans raw logs by topic when no signature is given', async () => {
|
|
2133
|
-
const db = TestApp.database()
|
|
2134
|
-
const subscription = await Core.createSubscription(db, {
|
|
2135
|
-
chainId: 42431,
|
|
2136
|
-
eventType: 'log:emitted',
|
|
2137
|
-
filters: { topic0: transferSelector, topic1: windowSenderTopic },
|
|
2138
|
-
owner: { orgId: 'org_test', type: 'api_key' },
|
|
2139
|
-
destination: { type: 'url', url: 'https://hooks.example.com/x' },
|
|
2140
|
-
})
|
|
2141
|
-
await Core.setCursor(db, subscription.id, windowCursor)
|
|
2142
|
-
|
|
2143
|
-
const dispatched: Webhooks.poll.Dispatchable[] = []
|
|
2144
|
-
const result = await Webhooks.poll({
|
|
2145
|
-
chainId: 42431,
|
|
2146
|
-
dispatch: async (input) => {
|
|
2147
|
-
dispatched.push(input)
|
|
2148
|
-
return { ok: true }
|
|
2149
|
-
},
|
|
2150
|
-
eventTypes: ['log:emitted'],
|
|
2151
|
-
headBlockNumber: windowHead,
|
|
2152
|
-
client,
|
|
2153
|
-
db,
|
|
2154
|
-
})
|
|
2155
|
-
|
|
2156
|
-
expect(result.scanned).toMatchInlineSnapshot(`2`)
|
|
2157
|
-
expect(dispatched[0]?.envelope.data).toMatchInlineSnapshot(`
|
|
2158
|
-
{
|
|
2159
|
-
"address": "0x20c00000000000000000000071de0cd31ab0d105",
|
|
2160
|
-
"blockNumber": 1000002,
|
|
2161
|
-
"data": "0x0000000000000000000000000000000000000000000000000000000000002710",
|
|
2162
|
-
"logIndex": 0,
|
|
2163
|
-
"timestamp": "2026-01-14T18:38:03.685Z",
|
|
2164
|
-
"topics": [
|
|
2165
|
-
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
|
2166
|
-
"0x000000000000000000000000e7687128b0a808c2831ff94d4f7b2fb35c65af38",
|
|
2167
|
-
"0x0000000000000000000000009e39034aae71fb89f66061a2602eb6efec271754",
|
|
2168
|
-
],
|
|
2169
|
-
"transactionHash": "0x3d24a706cc2f6f4c96620bef1f61ddb23040ff77c22c8db42918c7c424bbf9d3",
|
|
2170
|
-
"transactionIndex": 0,
|
|
2171
|
-
}
|
|
2172
|
-
`)
|
|
2173
|
-
})
|
|
2174
|
-
|
|
2175
|
-
test('log:emitted scans an address-only filter', async () => {
|
|
2176
|
-
const db = TestApp.database()
|
|
2177
|
-
const subscription = await Core.createSubscription(db, {
|
|
2178
|
-
chainId: 42431,
|
|
2179
|
-
destination: { type: 'url', url: 'https://hooks.example.com/address-only' },
|
|
2180
|
-
eventType: 'log:emitted',
|
|
2181
|
-
filters: { address: '0xffffffffffffffffffffffffffffffffffffffff' },
|
|
2182
|
-
owner: { orgId: 'org_address_only', type: 'api_key' },
|
|
2183
|
-
})
|
|
2184
|
-
await Core.setCursor(db, subscription.id, windowCursor)
|
|
2185
|
-
|
|
2186
|
-
const result = await Webhooks.poll({
|
|
2187
|
-
chainId: 42431,
|
|
2188
|
-
client,
|
|
2189
|
-
db,
|
|
2190
|
-
eventTypes: ['log:emitted'],
|
|
2191
|
-
headBlockNumber: windowHead,
|
|
2192
|
-
})
|
|
2193
|
-
|
|
2194
|
-
expect(result.scanned).toBe(0)
|
|
2195
|
-
expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([
|
|
2196
|
-
windowTo,
|
|
2197
|
-
endOfBlockIndex,
|
|
2198
|
-
])
|
|
2199
|
-
})
|
|
2200
|
-
|
|
2201
|
-
test('log:emitted keeps distinct address and topic pushdowns isolated', async () => {
|
|
2202
|
-
const db = TestApp.database()
|
|
2203
|
-
const address = await Core.createSubscription(db, {
|
|
2204
|
-
chainId: 42431,
|
|
2205
|
-
destination: { type: 'url', url: 'https://hooks.example.com/address' },
|
|
2206
|
-
eventType: 'log:emitted',
|
|
2207
|
-
filters: {
|
|
2208
|
-
address: windowToken,
|
|
2209
|
-
args: { from: windowSender },
|
|
2210
|
-
signature: transferSignature,
|
|
2211
|
-
},
|
|
2212
|
-
owner: { orgId: 'org_address', type: 'api_key' },
|
|
2213
|
-
})
|
|
2214
|
-
const topic = await Core.createSubscription(db, {
|
|
2215
|
-
chainId: 42431,
|
|
2216
|
-
destination: { type: 'url', url: 'https://hooks.example.com/topic' },
|
|
2217
|
-
eventType: 'log:emitted',
|
|
2218
|
-
filters: { topic0: transferSelector, topic1: windowSenderTopic },
|
|
2219
|
-
owner: { orgId: 'org_topic', type: 'api_key' },
|
|
2220
|
-
})
|
|
2221
|
-
await Promise.all([
|
|
2222
|
-
Core.setCursor(db, address.id, windowCursor),
|
|
2223
|
-
Core.setCursor(db, topic.id, windowCursor),
|
|
2224
|
-
])
|
|
2225
|
-
|
|
2226
|
-
const dispatched: Webhooks.poll.Dispatchable[] = []
|
|
2227
|
-
const result = await Webhooks.poll({
|
|
2228
|
-
chainId: 42431,
|
|
2229
|
-
client,
|
|
2230
|
-
db,
|
|
2231
|
-
dispatch: async (input) => {
|
|
2232
|
-
dispatched.push(input)
|
|
2233
|
-
return { ok: true }
|
|
2234
|
-
},
|
|
2235
|
-
eventTypes: ['log:emitted'],
|
|
2236
|
-
headBlockNumber: windowHead,
|
|
2237
|
-
})
|
|
2238
|
-
|
|
2239
|
-
expect(result.dispatched).toBe(3)
|
|
2240
|
-
expect(
|
|
2241
|
-
Object.fromEntries(
|
|
2242
|
-
[...Map.groupBy(dispatched, ({ subscription }) => subscription.id)].map(([id, items]) => [
|
|
2243
|
-
id,
|
|
2244
|
-
items.length,
|
|
2245
|
-
]),
|
|
2246
|
-
),
|
|
2247
|
-
).toEqual({ [address.id]: 1, [topic.id]: 2 })
|
|
2248
|
-
expect(
|
|
2249
|
-
await Promise.all([Core.getCursor(db, address.id), Core.getCursor(db, topic.id)]),
|
|
2250
|
-
).toEqual([
|
|
2251
|
-
encodeCursor([windowTo, endOfBlockIndex]),
|
|
2252
|
-
encodeCursor([windowTo, endOfBlockIndex]),
|
|
2253
|
-
])
|
|
2254
|
-
})
|
|
2255
|
-
|
|
2256
|
-
test('log:emitted limits a tick and resumes within the block', async () => {
|
|
2257
|
-
const db = TestApp.database()
|
|
2258
|
-
const subscription = await Core.createSubscription(db, {
|
|
2259
|
-
chainId: 42431,
|
|
2260
|
-
destination: { type: 'url', url: 'https://hooks.example.com/x' },
|
|
2261
|
-
eventType: 'log:emitted',
|
|
2262
|
-
filters: { args: { from: windowSender }, signature: transferSignature },
|
|
2263
|
-
owner: { orgId: 'org_test', type: 'api_key' },
|
|
2264
|
-
})
|
|
2265
|
-
await Core.setCursor(db, subscription.id, windowCursor)
|
|
2266
|
-
|
|
2267
|
-
const dispatched: Webhooks.poll.Dispatchable[] = []
|
|
2268
|
-
const tick = () =>
|
|
2269
|
-
Webhooks.poll({
|
|
2270
|
-
batchSize: 1,
|
|
2271
|
-
chainId: 42431,
|
|
2272
|
-
client,
|
|
2273
|
-
db,
|
|
2274
|
-
dispatch: async (input) => {
|
|
2275
|
-
dispatched.push(input)
|
|
2276
|
-
return { ok: true }
|
|
2277
|
-
},
|
|
2278
|
-
eventTypes: ['log:emitted'],
|
|
2279
|
-
headBlockNumber: windowHead,
|
|
2280
|
-
})
|
|
2281
|
-
|
|
2282
|
-
const first = await tick()
|
|
2283
|
-
expect(first.dispatched).toBe(1)
|
|
2284
|
-
expect(first.hasMore).toBe(true)
|
|
2285
|
-
expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([windowTo, 0])
|
|
2286
|
-
const second = await tick()
|
|
2287
|
-
expect(second.dispatched).toBe(1)
|
|
2288
|
-
expect(second.hasMore).toBe(true)
|
|
2289
|
-
expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([windowTo, 1])
|
|
2290
|
-
const third = await tick()
|
|
2291
|
-
expect(third.dispatched).toBe(0)
|
|
2292
|
-
expect(third.hasMore).toBeUndefined()
|
|
2293
|
-
expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([
|
|
2294
|
-
windowTo,
|
|
2295
|
-
endOfBlockIndex,
|
|
2296
|
-
])
|
|
2297
|
-
expect(
|
|
2298
|
-
dispatched.map(({ envelope }) => (envelope.data as { logIndex: number }).logIndex),
|
|
2299
|
-
).toEqual([0, 1])
|
|
2300
|
-
})
|
|
2301
|
-
|
|
2302
|
-
test('block:created scans new blocks and resolves transaction counts', async () => {
|
|
2303
|
-
const db = TestApp.database()
|
|
2304
|
-
const subscription = await Core.createSubscription(
|
|
2305
|
-
db,
|
|
2306
|
-
{
|
|
2307
|
-
chainId: 42431,
|
|
2308
|
-
eventType: 'block:created',
|
|
2309
|
-
// Pin to the creation-head height, which may be replaced before confirmation.
|
|
2310
|
-
filters: { number: { eq: `0x${windowTo.toString(16)}` } },
|
|
2311
|
-
owner: { orgId: 'org_test', type: 'api_key' },
|
|
2312
|
-
destination: { type: 'url', url: 'https://hooks.example.com/x' },
|
|
2313
|
-
},
|
|
2314
|
-
{ startBlockNumber: windowTo },
|
|
2315
|
-
)
|
|
2316
|
-
|
|
2317
|
-
const dispatched: Webhooks.poll.Dispatchable[] = []
|
|
2318
|
-
const result = await Webhooks.poll({
|
|
2319
|
-
chainId: 42431,
|
|
2320
|
-
confirmations: 3,
|
|
2321
|
-
dispatch: async (input) => {
|
|
2322
|
-
dispatched.push(input)
|
|
2323
|
-
return { ok: true }
|
|
2324
|
-
},
|
|
2325
|
-
eventTypes: ['block:created'],
|
|
2326
|
-
headBlockNumber: windowTo + 3,
|
|
2327
|
-
client,
|
|
2328
|
-
db,
|
|
2329
|
-
})
|
|
2330
|
-
|
|
2331
|
-
expect(result.scanned).toMatchInlineSnapshot(`1`)
|
|
2332
|
-
expect(dispatched[0]?.envelope.data).toMatchInlineSnapshot(`
|
|
2333
|
-
{
|
|
2334
|
-
"gasLimit": 500000000,
|
|
2335
|
-
"gasUsed": 4340281,
|
|
2336
|
-
"hash": "0x3fe7d9e595f3d8215ec840c6ef55ac0bead58933e96fc2ead0f2aaf79715f45d",
|
|
2337
|
-
"miner": "0x0000000000000000000000000000000000000000",
|
|
2338
|
-
"number": 1000002,
|
|
2339
|
-
"parentHash": "0xa406dff9c51cc2abbef3a811f0499e99a568bf781019f129ab45dca1df0e43e5",
|
|
2340
|
-
"timestamp": "2026-01-14T18:38:03.685Z",
|
|
2341
|
-
"transactionCount": 52,
|
|
2342
|
-
}
|
|
2343
|
-
`)
|
|
2344
|
-
// The window scanner advances to the end of the fully-scanned block so the
|
|
2345
|
-
// next tick resumes at the following block.
|
|
2346
|
-
expect(decodeCursor(await Core.getCursor(db, subscription.id))).toMatchInlineSnapshot(`
|
|
2347
|
-
[
|
|
2348
|
-
1000002,
|
|
2349
|
-
2147483647,
|
|
2350
|
-
]
|
|
2351
|
-
`)
|
|
2352
|
-
})
|
|
2353
|
-
|
|
2354
|
-
test('block:created limits a tick and resumes at the next block', async () => {
|
|
2355
|
-
const db = TestApp.database()
|
|
2356
|
-
const subscription = await Core.createSubscription(db, {
|
|
2357
|
-
chainId: 42431,
|
|
2358
|
-
destination: { type: 'url', url: 'https://hooks.example.com/x' },
|
|
2359
|
-
eventType: 'block:created',
|
|
2360
|
-
filters: {},
|
|
2361
|
-
owner: { orgId: 'org_test', type: 'api_key' },
|
|
2362
|
-
})
|
|
2363
|
-
await Core.setCursor(db, subscription.id, windowCursor)
|
|
2364
|
-
|
|
2365
|
-
const dispatched: Webhooks.poll.Dispatchable[] = []
|
|
2366
|
-
const tick = () =>
|
|
2367
|
-
Webhooks.poll({
|
|
2368
|
-
batchSize: 1,
|
|
2369
|
-
chainId: 42431,
|
|
2370
|
-
client,
|
|
2371
|
-
db,
|
|
2372
|
-
dispatch: async (input) => {
|
|
2373
|
-
dispatched.push(input)
|
|
2374
|
-
return { ok: true }
|
|
2375
|
-
},
|
|
2376
|
-
eventTypes: ['block:created'],
|
|
2377
|
-
headBlockNumber: windowHead,
|
|
2378
|
-
})
|
|
2379
|
-
|
|
2380
|
-
expect((await tick()).dispatched).toBe(1)
|
|
2381
|
-
expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([windowFrom, 0])
|
|
2382
|
-
expect((await tick()).dispatched).toBe(1)
|
|
2383
|
-
expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([windowFrom + 1, 0])
|
|
2384
|
-
expect(dispatched.map(({ envelope }) => (envelope.data as { number: number }).number)).toEqual([
|
|
2385
|
-
windowFrom,
|
|
2386
|
-
windowFrom + 1,
|
|
2387
|
-
])
|
|
2388
|
-
})
|
|
2389
|
-
|
|
2390
|
-
test('block:created continues other subscriptions when a block header cannot be read', async () => {
|
|
2391
|
-
const db = TestApp.database()
|
|
2392
|
-
const missingBlock = 4_000_000_000
|
|
2393
|
-
const cursor = encodeCursor([missingBlock - 1, endOfBlockIndex])
|
|
2394
|
-
const unavailable = await Core.createSubscription(
|
|
2395
|
-
db,
|
|
2396
|
-
{
|
|
2397
|
-
chainId: 42431,
|
|
2398
|
-
destination: { type: 'url', url: 'https://hooks.example.com/unavailable' },
|
|
2399
|
-
eventType: 'block:created',
|
|
2400
|
-
filters: { number: { eq: `0x${missingBlock.toString(16)}` } },
|
|
2401
|
-
owner: { orgId: 'org_test', type: 'api_key' },
|
|
2402
|
-
},
|
|
2403
|
-
{ now: () => new Date('2026-01-01T00:00:00.000Z') },
|
|
2404
|
-
)
|
|
2405
|
-
const healthy = await Core.createSubscription(
|
|
2406
|
-
db,
|
|
2407
|
-
{
|
|
2408
|
-
chainId: 42431,
|
|
2409
|
-
destination: { type: 'url', url: 'https://hooks.example.com/healthy' },
|
|
2410
|
-
eventType: 'block:created',
|
|
2411
|
-
filters: { number: { eq: `0x${windowTo.toString(16)}` } },
|
|
2412
|
-
owner: { orgId: 'org_test', type: 'api_key' },
|
|
2413
|
-
},
|
|
2414
|
-
{ now: () => new Date('2026-01-01T00:00:01.000Z') },
|
|
2415
|
-
)
|
|
2416
|
-
await Core.setCursor(db, unavailable.id, cursor)
|
|
2417
|
-
await Core.setCursor(db, healthy.id, windowCursor)
|
|
2418
|
-
|
|
2419
|
-
const dispatched: Webhooks.poll.Dispatchable[] = []
|
|
2420
|
-
const subscriptions: number[] = []
|
|
2421
|
-
|
|
2422
|
-
await expect(
|
|
2423
|
-
Webhooks.poll({
|
|
2424
|
-
chainId: 42431,
|
|
2425
|
-
dispatch: async (input) => {
|
|
2426
|
-
dispatched.push(input)
|
|
2427
|
-
return { ok: true }
|
|
2428
|
-
},
|
|
2429
|
-
eventTypes: ['block:created'],
|
|
2430
|
-
headBlockNumber: missingBlock + 2,
|
|
2431
|
-
client,
|
|
2432
|
-
db,
|
|
2433
|
-
maxBlocks: 3,
|
|
2434
|
-
onSubscriptions(count) {
|
|
2435
|
-
subscriptions.push(count)
|
|
2436
|
-
},
|
|
2437
|
-
}),
|
|
2438
|
-
).rejects.toThrow()
|
|
2439
|
-
|
|
2440
|
-
expect(await Core.getCursor(db, unavailable.id)).toBe(cursor)
|
|
2441
|
-
expect(subscriptions).toEqual([2])
|
|
2442
|
-
expect(decodeCursor(await Core.getCursor(db, healthy.id))).toMatchInlineSnapshot(`
|
|
2443
|
-
[
|
|
2444
|
-
1000002,
|
|
2445
|
-
2147483647,
|
|
2446
|
-
]
|
|
2447
|
-
`)
|
|
2448
|
-
expect(dispatched.map(({ subscription }) => subscription.id)).toEqual([healthy.id])
|
|
2449
|
-
})
|
|
2450
|
-
|
|
2451
|
-
test('transaction:included filters on txs columns and inner calls', async () => {
|
|
2452
|
-
const db = TestApp.database()
|
|
2453
|
-
const subscription = await Core.createSubscription(
|
|
2454
|
-
db,
|
|
2455
|
-
{
|
|
2456
|
-
chainId: 42431,
|
|
2457
|
-
eventType: 'transaction:included',
|
|
2458
|
-
// `from` + an `input` selector (ERC20 `transfer` = 0xa9059cbb) pins the
|
|
2459
|
-
// scan to the deterministic creation-head transaction.
|
|
2460
|
-
filters: { from: windowSender, input: { selector: '0xa9059cbb' } },
|
|
2461
|
-
owner: { orgId: 'org_test', type: 'api_key' },
|
|
2462
|
-
destination: { type: 'url', url: 'https://hooks.example.com/x' },
|
|
2463
|
-
},
|
|
2464
|
-
{ startBlockNumber: windowTo },
|
|
2465
|
-
)
|
|
2466
|
-
|
|
2467
|
-
const dispatched: Webhooks.poll.Dispatchable[] = []
|
|
2468
|
-
const result = await Webhooks.poll({
|
|
2469
|
-
chainId: 42431,
|
|
2470
|
-
confirmations: 3,
|
|
2471
|
-
dispatch: async (input) => {
|
|
2472
|
-
dispatched.push(input)
|
|
2473
|
-
return { ok: true }
|
|
2474
|
-
},
|
|
2475
|
-
eventTypes: ['transaction:included'],
|
|
2476
|
-
headBlockNumber: windowTo + 3,
|
|
2477
|
-
client,
|
|
2478
|
-
db,
|
|
2479
|
-
})
|
|
2480
|
-
|
|
2481
|
-
expect(result.scanned).toMatchInlineSnapshot(`1`)
|
|
2482
|
-
const tx = dispatched[0]?.envelope.data as TransactionEvent | undefined
|
|
2483
|
-
expect(tx?.hash).toMatchInlineSnapshot(
|
|
2484
|
-
`"0x3d24a706cc2f6f4c96620bef1f61ddb23040ff77c22c8db42918c7c424bbf9d3"`,
|
|
2485
|
-
)
|
|
2486
|
-
expect(tx?.sender).toBe(windowSender)
|
|
2487
|
-
if (tx?.meta.rpc.feeToken) {
|
|
2488
|
-
expect(tx.feeToken?.address).toBe(tx.meta.rpc.feeToken)
|
|
2489
|
-
expect(typeof tx.feeToken?.currency).toBe('string')
|
|
2490
|
-
expect(typeof tx.feeToken?.decimals).toBe('number')
|
|
2491
|
-
expect(typeof tx.feeToken?.name).toBe('string')
|
|
2492
|
-
expect(typeof tx.feeToken?.symbol).toBe('string')
|
|
2493
|
-
} else expect(tx?.feeToken).toBeUndefined()
|
|
2494
|
-
expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([
|
|
2495
|
-
windowTo,
|
|
2496
|
-
endOfBlockIndex,
|
|
2497
|
-
])
|
|
2498
|
-
})
|
|
2499
|
-
|
|
2500
|
-
test('disables stored subscriptions with removed receipt filters', async () => {
|
|
2501
|
-
const db = TestApp.database()
|
|
2502
|
-
const subscription = await Core.createSubscription(db, {
|
|
2503
|
-
chainId: 42431,
|
|
2504
|
-
destination: { type: 'url', url: 'https://hooks.example.com/legacy' },
|
|
2505
|
-
eventType: 'transaction:included',
|
|
2506
|
-
filters: { includeReceipt: true },
|
|
2507
|
-
owner: { orgId: 'org_legacy', type: 'api_key' },
|
|
2508
|
-
})
|
|
2509
|
-
|
|
2510
|
-
await Webhooks.poll({
|
|
2511
|
-
chainId: 42431,
|
|
2512
|
-
client,
|
|
2513
|
-
db,
|
|
2514
|
-
eventTypes: ['transaction:included'],
|
|
2515
|
-
headBlockNumber: windowHead,
|
|
2516
|
-
})
|
|
2517
|
-
|
|
2518
|
-
const disabled = await Core.getSubscription(db, subscription.owner, subscription.id)
|
|
2519
|
-
expect(disabled?.status).toBe('disabled')
|
|
2520
|
-
})
|
|
2521
|
-
|
|
2522
|
-
test('transaction:included limits a tick and resumes within the block', async () => {
|
|
2523
|
-
const db = TestApp.database()
|
|
2524
|
-
const subscription = await Core.createSubscription(db, {
|
|
2525
|
-
chainId: 42431,
|
|
2526
|
-
destination: { type: 'url', url: 'https://hooks.example.com/x' },
|
|
2527
|
-
eventType: 'transaction:included',
|
|
2528
|
-
filters: {},
|
|
2529
|
-
owner: { orgId: 'org_test', type: 'api_key' },
|
|
2530
|
-
})
|
|
2531
|
-
await Core.setCursor(db, subscription.id, encodeCursor([windowTo - 1, endOfBlockIndex]))
|
|
2532
|
-
|
|
2533
|
-
const dispatched: Webhooks.poll.Dispatchable[] = []
|
|
2534
|
-
const tick = () =>
|
|
2535
|
-
Webhooks.poll({
|
|
2536
|
-
batchSize: 1,
|
|
2537
|
-
chainId: 42431,
|
|
2538
|
-
client,
|
|
2539
|
-
db,
|
|
2540
|
-
dispatch: async (input) => {
|
|
2541
|
-
dispatched.push(input)
|
|
2542
|
-
return { ok: true }
|
|
2543
|
-
},
|
|
2544
|
-
eventTypes: ['transaction:included'],
|
|
2545
|
-
headBlockNumber: windowHead,
|
|
2546
|
-
})
|
|
2547
|
-
|
|
2548
|
-
expect((await tick()).dispatched).toBe(1)
|
|
2549
|
-
expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([windowTo, 0])
|
|
2550
|
-
expect((await tick()).dispatched).toBe(1)
|
|
2551
|
-
expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([windowTo, 1])
|
|
2552
|
-
expect(
|
|
2553
|
-
new Set(dispatched.map(({ envelope }) => (envelope.data as { hash: string }).hash)).size,
|
|
2554
|
-
).toBe(2)
|
|
2555
|
-
})
|
|
2556
|
-
|
|
2557
|
-
test('transaction:included preserves the cursor when a confirmed block is unavailable', async () => {
|
|
2558
|
-
const db = TestApp.database()
|
|
2559
|
-
const missingBlock = 4_000_000_000
|
|
2560
|
-
const cursor = encodeCursor([missingBlock - 1, endOfBlockIndex])
|
|
2561
|
-
const subscription = await Core.createSubscription(db, {
|
|
2562
|
-
chainId: 42431,
|
|
2563
|
-
destination: { type: 'url', url: 'https://hooks.example.com/x' },
|
|
2564
|
-
eventType: 'transaction:included',
|
|
2565
|
-
filters: { blockNumber: { eq: `0x${missingBlock.toString(16)}` } },
|
|
2566
|
-
owner: { orgId: 'org_test', type: 'api_key' },
|
|
2567
|
-
})
|
|
2568
|
-
await Core.setCursor(db, subscription.id, cursor)
|
|
2569
|
-
|
|
2570
|
-
await expect(
|
|
2571
|
-
Webhooks.poll({
|
|
2572
|
-
chainId: 42431,
|
|
2573
|
-
eventTypes: ['transaction:included'],
|
|
2574
|
-
headBlockNumber: missingBlock + 2,
|
|
2575
|
-
client,
|
|
2576
|
-
db,
|
|
2577
|
-
}),
|
|
2578
|
-
).rejects.toThrowErrorMatchingInlineSnapshot(`
|
|
2579
|
-
[Webhooks.BlockUnavailableError: Block 4000000000 is unavailable.]
|
|
2580
|
-
`)
|
|
2581
|
-
|
|
2582
|
-
expect(await Core.getCursor(db, subscription.id)).toBe(cursor)
|
|
2583
|
-
})
|
|
2584
|
-
|
|
2585
|
-
test('transaction:included matches exact empty calldata through the poller', async () => {
|
|
2586
|
-
const db = TestApp.database()
|
|
2587
|
-
const subscription = await Core.createSubscription(db, {
|
|
2588
|
-
chainId: 42431,
|
|
2589
|
-
eventType: 'transaction:included',
|
|
2590
|
-
filters: { input: { eq: '0x' } },
|
|
2591
|
-
owner: { orgId: 'org_test', type: 'api_key' },
|
|
2592
|
-
destination: { type: 'url', url: 'https://hooks.example.com/x' },
|
|
2593
|
-
})
|
|
2594
|
-
await Core.setCursor(db, subscription.id, windowCursor)
|
|
2595
|
-
|
|
2596
|
-
const dispatched: Webhooks.poll.Dispatchable[] = []
|
|
2597
|
-
const result = await Webhooks.poll({
|
|
2598
|
-
chainId: 42431,
|
|
2599
|
-
dispatch: async (input) => {
|
|
2600
|
-
dispatched.push(input)
|
|
2601
|
-
return { ok: true }
|
|
2602
|
-
},
|
|
2603
|
-
eventTypes: ['transaction:included'],
|
|
2604
|
-
headBlockNumber: windowHead,
|
|
2605
|
-
client,
|
|
2606
|
-
db,
|
|
2607
|
-
})
|
|
2608
|
-
|
|
2609
|
-
expect(result.scanned).toMatchInlineSnapshot(`1`)
|
|
2610
|
-
expect(dispatched[0]?.envelope.data).toMatchObject({
|
|
2611
|
-
hash: '0x359a47d5a7a582ae412e8ba8bb729b326464f2e97d00bcabf28694e919327f33',
|
|
2612
|
-
input: '0x',
|
|
2613
|
-
})
|
|
2614
|
-
})
|
|
2615
|
-
|
|
2616
|
-
// End-to-end through the *default* transport: `poll` with no injected
|
|
2617
|
-
// `dispatch` falls back to `deliverAndRecord`, which signs and POSTs via
|
|
2618
|
-
// `globalThis.fetch` and records the outcome. These intercept the delivery
|
|
2619
|
-
// request while real RPC reads pass through.
|
|
2620
|
-
describe('default transport (deliverAndRecord)', () => {
|
|
2621
|
-
afterEach(() => {
|
|
2622
|
-
vi.restoreAllMocks()
|
|
2623
|
-
})
|
|
2624
|
-
|
|
2625
|
-
test('delivers a verifiable signed envelope and records success', async () => {
|
|
2626
|
-
const db = TestApp.database()
|
|
2627
|
-
const subscription = await createTransferSubscription(db)
|
|
2628
|
-
await Core.setCursor(db, subscription.id, windowCursor)
|
|
2629
|
-
|
|
2630
|
-
// RPC queries also go through `globalThis.fetch`, so the interceptor passes
|
|
2631
|
-
// non-webhook requests through and captures only the delivery POST.
|
|
2632
|
-
const realFetch = globalThis.fetch.bind(globalThis)
|
|
2633
|
-
const calls: { body: string; headers: Headers; url: string }[] = []
|
|
2634
|
-
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
|
|
2635
|
-
const url =
|
|
2636
|
-
typeof input === 'string' ? input : input instanceof Request ? input.url : input.href
|
|
2637
|
-
if (!url.startsWith(subscription.destination.url)) return realFetch(input, init)
|
|
2638
|
-
calls.push({
|
|
2639
|
-
body: typeof init?.body === 'string' ? init.body : '',
|
|
2640
|
-
headers: new Headers(init?.headers),
|
|
2641
|
-
url,
|
|
2642
|
-
})
|
|
2643
|
-
return new Response(null, { status: 200 })
|
|
2644
|
-
})
|
|
2645
|
-
|
|
2646
|
-
const result = await Webhooks.poll({
|
|
2647
|
-
// One row per tick keeps the delivery assertions focused on a single
|
|
2648
|
-
// signed envelope.
|
|
2649
|
-
batchSize: 1,
|
|
2650
|
-
chainId: 42431,
|
|
2651
|
-
eventTypes: ['token:transfer'],
|
|
2652
|
-
headBlockNumber: windowHead,
|
|
2653
|
-
client,
|
|
2654
|
-
db,
|
|
2655
|
-
})
|
|
2656
|
-
|
|
2657
|
-
expect(result.dispatched).toBe(1)
|
|
2658
|
-
expect(calls).toHaveLength(1)
|
|
2659
|
-
expect(calls[0]?.url).toBe(subscription.destination.url)
|
|
2660
|
-
|
|
2661
|
-
// The captured body verifies against the subscription's signing secret.
|
|
2662
|
-
expect(
|
|
2663
|
-
Core.verify({
|
|
2664
|
-
body: calls[0]?.body ?? '',
|
|
2665
|
-
secret: subscription.secret,
|
|
2666
|
-
signature: calls[0]?.headers.get('tempo-signature') ?? '',
|
|
2667
|
-
}),
|
|
2668
|
-
).toBe(true)
|
|
2669
|
-
|
|
2670
|
-
// The delivered envelope carries the matched transfer row and its type.
|
|
2671
|
-
const envelope = JSON.parse(calls[0]?.body ?? '{}') as Core.Envelope
|
|
2672
|
-
expect(envelope.type).toBe('token:transfer')
|
|
2673
|
-
expect(calls[0]?.headers.get('tempo-event-type')).toBe('token:transfer')
|
|
2674
|
-
expect((envelope.data as { blockNumber: number }).blockNumber).toBe(windowTo)
|
|
2675
|
-
|
|
2676
|
-
// Success resets the failure counter, stamps lastDeliveryAt, stays active.
|
|
2677
|
-
const after = await Core.getSubscription(db, subscription.owner, subscription.id)
|
|
2678
|
-
expect(after?.status).toBe('active')
|
|
2679
|
-
expect(after?.failureCount).toBe(0)
|
|
2680
|
-
expect(after?.lastDeliveryAt).toBeDefined()
|
|
2681
|
-
})
|
|
2682
|
-
|
|
2683
|
-
test('pauses on a failed delivery and re-delivers on the next tick', async () => {
|
|
2684
|
-
const db = TestApp.database()
|
|
2685
|
-
const subscription = await createTransferSubscription(db)
|
|
2686
|
-
await Core.setCursor(db, subscription.id, windowCursor)
|
|
2687
|
-
|
|
2688
|
-
const realFetch = globalThis.fetch.bind(globalThis)
|
|
2689
|
-
let attempts = 0
|
|
2690
|
-
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
|
|
2691
|
-
const url =
|
|
2692
|
-
typeof input === 'string' ? input : input instanceof Request ? input.url : input.href
|
|
2693
|
-
if (!url.startsWith(subscription.destination.url)) return realFetch(input, init)
|
|
2694
|
-
attempts++
|
|
2695
|
-
return new Response(null, { status: attempts === 1 ? 500 : 200 })
|
|
2696
|
-
})
|
|
2697
|
-
|
|
2698
|
-
const tick = () =>
|
|
2699
|
-
Webhooks.poll({
|
|
2700
|
-
batchSize: 1,
|
|
2701
|
-
chainId: 42431,
|
|
2702
|
-
eventTypes: ['token:transfer'],
|
|
2703
|
-
headBlockNumber: windowHead,
|
|
2704
|
-
client,
|
|
2705
|
-
db,
|
|
2706
|
-
})
|
|
2707
|
-
|
|
2708
|
-
const first = await tick()
|
|
2709
|
-
expect(first.failed).toBe(1)
|
|
2710
|
-
expect(first.dispatched).toBe(0)
|
|
2711
|
-
// Cursor paused at the prior position so the row re-delivers next tick.
|
|
2712
|
-
expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([
|
|
2713
|
-
windowFrom - 1,
|
|
2714
|
-
endOfBlockIndex,
|
|
2715
|
-
])
|
|
2716
|
-
|
|
2717
|
-
const second = await tick()
|
|
2718
|
-
expect(second.dispatched).toBe(1)
|
|
2719
|
-
expect(attempts).toBe(2)
|
|
2720
|
-
// Now advanced past the redelivered row (the first row in block 1000002).
|
|
2721
|
-
expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([windowTo, 0])
|
|
2722
|
-
})
|
|
2723
|
-
|
|
2724
|
-
test('auto-disables a subscription after sustained failures', async () => {
|
|
2725
|
-
const db = TestApp.database()
|
|
2726
|
-
const subscription = await createTransferSubscription(db)
|
|
2727
|
-
await Core.setCursor(db, subscription.id, windowCursor)
|
|
2728
|
-
|
|
2729
|
-
// Real RPC queries pass through; only the webhook delivery fails (500).
|
|
2730
|
-
const realFetch = globalThis.fetch.bind(globalThis)
|
|
2731
|
-
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
|
|
2732
|
-
const url =
|
|
2733
|
-
typeof input === 'string' ? input : input instanceof Request ? input.url : input.href
|
|
2734
|
-
if (!url.startsWith(subscription.destination.url)) return realFetch(input, init)
|
|
2735
|
-
return new Response(null, { status: 500 })
|
|
2736
|
-
})
|
|
2737
|
-
|
|
2738
|
-
const tick = () =>
|
|
2739
|
-
Webhooks.poll({
|
|
2740
|
-
batchSize: 1,
|
|
2741
|
-
chainId: 42431,
|
|
2742
|
-
eventTypes: ['token:transfer'],
|
|
2743
|
-
headBlockNumber: windowHead,
|
|
2744
|
-
client,
|
|
2745
|
-
db,
|
|
2746
|
-
})
|
|
2747
|
-
|
|
2748
|
-
// Each tick delivers once and fails; after `maxFailures` ticks the
|
|
2749
|
-
// subscription auto-disables and drops out of the poller's active index.
|
|
2750
|
-
for (let i = 0; i < Core.maxFailures; i++) await tick()
|
|
2751
|
-
|
|
2752
|
-
const disabled = await Core.getSubscription(db, subscription.owner, subscription.id)
|
|
2753
|
-
expect(disabled?.status).toBe('disabled')
|
|
2754
|
-
expect(disabled?.failureCount).toBe(Core.maxFailures)
|
|
2755
|
-
|
|
2756
|
-
// A subsequent tick visits no active subscriptions.
|
|
2757
|
-
const after = await tick()
|
|
2758
|
-
expect(after.subscriptions).toBe(0)
|
|
2759
|
-
})
|
|
2760
|
-
})
|
|
2761
|
-
|
|
2762
|
-
// Full loop through the public HTTP surface: create over the route, let the
|
|
2763
|
-
// poller detect an RPC transfer log and deliver via the default transport, then
|
|
2764
|
-
// read the recorded outcome back through the deliveries route. Unlike the
|
|
2765
|
-
// `poll` tests above (which seed subscriptions via the core helper), this
|
|
2766
|
-
// proves the route-issued secret and id line up with what is delivered/logged.
|
|
2767
|
-
describe('end-to-end (routes + poller)', () => {
|
|
2768
|
-
test('create → poll → signed delivery → deliveries route records success', async () => {
|
|
2769
|
-
const db = TestApp.database()
|
|
2770
|
-
const app = TestApp.client({
|
|
2771
|
-
auth: { keys: [writer] },
|
|
2772
|
-
db,
|
|
2773
|
-
})
|
|
2774
|
-
|
|
2775
|
-
// 1. Create through the route; capture the one-time signing secret + id.
|
|
2776
|
-
// The `windowSender` filter pins the scan to the deterministic window rows.
|
|
2777
|
-
const created = await app.v1.webhooks.$post(
|
|
2778
|
-
{ json: { ...validBody, chainId: 42431, filters: { sender: windowSender } } },
|
|
2779
|
-
as(writer),
|
|
2780
|
-
)
|
|
2781
|
-
expect(created.status).toBe(200)
|
|
2782
|
-
const { id, secret } = await TestApp.json(created, Webhooks.schema.createWebhook.Response)
|
|
2783
|
-
|
|
2784
|
-
// Anchor the cursor just below the window so the poller scans this tick (a
|
|
2785
|
-
// brand-new subscription anchors at the confirmed head and skips its first
|
|
2786
|
-
// tick).
|
|
2787
|
-
await Core.setCursor(db, id, windowCursor)
|
|
2788
|
-
|
|
2789
|
-
// 2. A local delivery sink that captures the request and acks 200, injected
|
|
2790
|
-
// via `deliverAndRecord`'s `fetch` option (the SSRF guard blocks loopback,
|
|
2791
|
-
// so a real local server can't be a subscriber URL) — no global mock.
|
|
2792
|
-
const calls: { body: string; headers: Headers; url: string }[] = []
|
|
2793
|
-
const fetch = (async (input, init) => {
|
|
2794
|
-
calls.push({
|
|
2795
|
-
body: typeof init?.body === 'string' ? init.body : '',
|
|
2796
|
-
headers: new Headers(init?.headers),
|
|
2797
|
-
url: typeof input === 'string' ? input : '',
|
|
2798
|
-
})
|
|
2799
|
-
return new Response(null, { status: 200 })
|
|
2800
|
-
}) satisfies typeof globalThis.fetch
|
|
2801
|
-
|
|
2802
|
-
// 3. Poll a real RPC transfer log; dispatch through the real
|
|
2803
|
-
// sign+deliver+record core with the injected transport. `batchSize: 1`
|
|
2804
|
-
// delivers exactly one envelope from the window.
|
|
2805
|
-
const result = await Webhooks.poll({
|
|
2806
|
-
batchSize: 1,
|
|
2807
|
-
chainId: 42431,
|
|
2808
|
-
dispatch: ({ envelope, subscription }) =>
|
|
2809
|
-
Core.deliverAndRecord(db, subscription, envelope, { fetch }),
|
|
2810
|
-
eventTypes: ['token:transfer'],
|
|
2811
|
-
headBlockNumber: windowHead,
|
|
2812
|
-
client,
|
|
2813
|
-
db,
|
|
2814
|
-
})
|
|
2815
|
-
expect(result.dispatched).toBe(1)
|
|
2816
|
-
|
|
2817
|
-
// The subscriber received exactly one POST to its URL, signed verifiably
|
|
2818
|
-
// with the secret the route returned.
|
|
2819
|
-
expect(calls).toHaveLength(1)
|
|
2820
|
-
expect(calls[0]?.url).toBe(validBody.destination.url)
|
|
2821
|
-
expect(
|
|
2822
|
-
Core.verify({
|
|
2823
|
-
body: calls[0]?.body ?? '',
|
|
2824
|
-
secret,
|
|
2825
|
-
signature: calls[0]?.headers.get('tempo-signature') ?? '',
|
|
2826
|
-
}),
|
|
2827
|
-
).toBe(true)
|
|
2828
|
-
const envelope = JSON.parse(calls[0]?.body ?? '{}') as Core.Envelope
|
|
2829
|
-
expect(envelope.type).toBe('token:transfer')
|
|
2830
|
-
|
|
2831
|
-
// 4. The deliveries route shows the recorded success for this event.
|
|
2832
|
-
const deliveries = await app.v1.webhooks[':id{wh_[A-Za-z0-9_-]+}'].deliveries.$get(
|
|
2833
|
-
{ param: { id }, query: {} },
|
|
2834
|
-
as(writer),
|
|
2835
|
-
)
|
|
2836
|
-
const log = await TestApp.json(deliveries, Webhooks.schema.listWebhookDeliveries.Response)
|
|
2837
|
-
expect(log.data).toHaveLength(1)
|
|
2838
|
-
expect(log.data[0]?.status).toBe('succeeded')
|
|
2839
|
-
expect(log.data[0]?.eventId).toBe(envelope.id)
|
|
2840
|
-
})
|
|
2841
|
-
})
|
|
2842
|
-
})
|
|
2843
|
-
|
|
2844
1526
|
describe('matchesOperator', () => {
|
|
2845
1527
|
const addr = '0x20c0000000000000000000008f5425160ebe5525'
|
|
2846
1528
|
const addr2 = '0x1111111111111111111111111111111111111111'
|