tapimo 0.1.0 → 0.3.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/README.md +57 -16
- package/dist/ApiKey.d.ts +4 -1
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKey.js +6 -2
- package/dist/ApiKey.js.map +1 -1
- package/dist/ApiKeys.d.ts +98 -3
- package/dist/ApiKeys.d.ts.map +1 -1
- package/dist/ApiKeys.js +150 -1
- package/dist/ApiKeys.js.map +1 -1
- package/dist/App.d.ts +7917 -2315
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +141 -329
- package/dist/App.js.map +1 -1
- package/dist/Scope.d.ts +20 -4
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +23 -4
- package/dist/Scope.js.map +1 -1
- package/dist/Webhooks.d.ts +2 -2
- package/dist/Webhooks.d.ts.map +1 -1
- package/dist/Webhooks.js +2 -2
- package/dist/Webhooks.js.map +1 -1
- package/dist/admin/App.d.ts +203 -120
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +111 -6
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +67 -0
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/scopes.d.ts +12 -1
- package/dist/admin/apps/scopes.d.ts.map +1 -1
- package/dist/admin/apps/scopes.js +3 -0
- package/dist/admin/apps/scopes.js.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +116 -116
- package/dist/admin/apps/verified-tokens.js +1 -1
- package/dist/admin/apps/verified-tokens.js.map +1 -1
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.js +13 -13
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts +151 -0
- package/dist/analytics/Analytics.d.ts.map +1 -0
- package/dist/analytics/Analytics.js +212 -0
- package/dist/analytics/Analytics.js.map +1 -0
- package/dist/analytics/tables/requestEvents.d.ts +215 -0
- package/dist/analytics/tables/requestEvents.d.ts.map +1 -0
- package/dist/analytics/tables/requestEvents.js +264 -0
- package/dist/analytics/tables/requestEvents.js.map +1 -0
- package/dist/apps/data/App.d.ts +19576 -0
- package/dist/apps/data/App.d.ts.map +1 -0
- package/dist/apps/data/App.js +272 -0
- package/dist/apps/data/App.js.map +1 -0
- package/dist/apps/data/index.d.ts +3 -0
- package/dist/apps/data/index.d.ts.map +1 -0
- package/dist/apps/data/index.js +3 -0
- package/dist/apps/data/index.js.map +1 -0
- package/dist/apps/{activities.d.ts → data/routes/activities.d.ts} +32 -32
- package/dist/apps/{activities.d.ts.map → data/routes/activities.d.ts.map} +1 -1
- package/dist/apps/{activities.js → data/routes/activities.js} +13 -13
- package/dist/apps/data/routes/activities.js.map +1 -0
- package/dist/apps/{addresses.d.ts → data/routes/addresses.d.ts} +22 -22
- package/dist/apps/{addresses.d.ts.map → data/routes/addresses.d.ts.map} +1 -1
- package/dist/apps/{addresses.js → data/routes/addresses.js} +5 -5
- package/dist/apps/data/routes/addresses.js.map +1 -0
- package/dist/apps/{balances.d.ts → data/routes/balances.d.ts} +4 -4
- package/dist/apps/data/routes/balances.d.ts.map +1 -0
- package/dist/apps/{balances.js → data/routes/balances.js} +42 -11
- package/dist/apps/data/routes/balances.js.map +1 -0
- package/dist/apps/{blocks.d.ts → data/routes/blocks.d.ts} +47 -47
- package/dist/apps/data/routes/blocks.d.ts.map +1 -0
- package/dist/apps/{blocks.js → data/routes/blocks.js} +13 -13
- package/dist/apps/data/routes/blocks.js.map +1 -0
- package/dist/apps/{coingecko.d.ts → data/routes/coingecko.d.ts} +8 -8
- package/dist/apps/data/routes/coingecko.d.ts.map +1 -0
- package/dist/apps/{coingecko.js → data/routes/coingecko.js} +13 -13
- package/dist/apps/data/routes/coingecko.js.map +1 -0
- package/dist/apps/{exchanges.d.ts → data/routes/exchanges.d.ts} +108 -108
- package/dist/apps/data/routes/exchanges.d.ts.map +1 -0
- package/dist/apps/{exchanges.js → data/routes/exchanges.js} +19 -19
- package/dist/apps/data/routes/exchanges.js.map +1 -0
- package/dist/apps/{fee-amm.d.ts → data/routes/fee-amm.d.ts} +39 -39
- package/dist/apps/data/routes/fee-amm.d.ts.map +1 -0
- package/dist/apps/{fee-amm.js → data/routes/fee-amm.js} +17 -17
- package/dist/apps/data/routes/fee-amm.js.map +1 -0
- package/dist/apps/data/routes/funding.d.ts +765 -0
- package/dist/apps/data/routes/funding.d.ts.map +1 -0
- package/dist/apps/data/routes/funding.js +383 -0
- package/dist/apps/data/routes/funding.js.map +1 -0
- package/dist/apps/{indexer.d.ts → data/routes/indexer.d.ts} +4 -4
- package/dist/apps/data/routes/indexer.d.ts.map +1 -0
- package/dist/apps/{indexer.js → data/routes/indexer.js} +13 -9
- package/dist/apps/data/routes/indexer.js.map +1 -0
- package/dist/apps/{receipts.d.ts → data/routes/receipts.d.ts} +49 -49
- package/dist/apps/data/routes/receipts.d.ts.map +1 -0
- package/dist/apps/{receipts.js → data/routes/receipts.js} +14 -20
- package/dist/apps/data/routes/receipts.js.map +1 -0
- package/dist/apps/{rpc.d.ts → data/routes/rpc.d.ts} +33 -40
- package/dist/apps/data/routes/rpc.d.ts.map +1 -0
- package/dist/apps/{rpc.js → data/routes/rpc.js} +35 -7
- package/dist/apps/data/routes/rpc.js.map +1 -0
- package/dist/apps/{tokenlist.d.ts → data/routes/tokenlist.d.ts} +40 -40
- package/dist/apps/data/routes/tokenlist.d.ts.map +1 -0
- package/dist/apps/{tokenlist.js → data/routes/tokenlist.js} +8 -8
- package/dist/apps/data/routes/tokenlist.js.map +1 -0
- package/dist/apps/{tokens.d.ts → data/routes/tokens.d.ts} +468 -468
- package/dist/apps/data/routes/tokens.d.ts.map +1 -0
- package/dist/apps/{tokens.js → data/routes/tokens.js} +19 -19
- package/dist/apps/data/routes/tokens.js.map +1 -0
- package/dist/apps/{transactions.d.ts → data/routes/transactions.d.ts} +304 -304
- package/dist/apps/data/routes/transactions.d.ts.map +1 -0
- package/dist/apps/{transactions.js → data/routes/transactions.js} +13 -13
- package/dist/apps/data/routes/transactions.js.map +1 -0
- package/dist/apps/{transfers.d.ts → data/routes/transfers.d.ts} +62 -62
- package/dist/apps/data/routes/transfers.d.ts.map +1 -0
- package/dist/apps/{transfers.js → data/routes/transfers.js} +13 -13
- package/dist/apps/data/routes/transfers.js.map +1 -0
- package/dist/apps/{verified-tokens.d.ts → data/routes/verified-tokens.d.ts} +6 -6
- package/dist/apps/data/routes/verified-tokens.d.ts.map +1 -0
- package/dist/apps/{verified-tokens.js → data/routes/verified-tokens.js} +9 -9
- package/dist/apps/data/routes/verified-tokens.js.map +1 -0
- package/dist/apps/{webhooks.d.ts → data/routes/webhooks.d.ts} +17 -17
- package/dist/apps/{webhooks.d.ts.map → data/routes/webhooks.d.ts.map} +1 -1
- package/dist/apps/{webhooks.js → data/routes/webhooks.js} +38 -34
- package/dist/apps/data/routes/webhooks.js.map +1 -0
- package/dist/apps/docs/App.d.ts +29 -0
- package/dist/apps/docs/App.d.ts.map +1 -0
- package/dist/apps/docs/App.js +54 -0
- package/dist/apps/docs/App.js.map +1 -0
- package/dist/{internal/OpenApi.fonts.d.ts → apps/docs/fonts.d.ts} +1 -1
- package/dist/apps/docs/fonts.d.ts.map +1 -0
- package/dist/{internal/OpenApi.fonts.js → apps/docs/fonts.js} +1 -1
- package/dist/apps/docs/fonts.js.map +1 -0
- package/dist/apps/docs/index.d.ts +2 -0
- package/dist/apps/docs/index.d.ts.map +1 -0
- package/dist/apps/docs/index.js +2 -0
- package/dist/apps/docs/index.js.map +1 -0
- package/dist/{internal/OpenApi.logo.d.ts → apps/docs/logo.d.ts} +1 -1
- package/dist/apps/docs/logo.d.ts.map +1 -0
- package/dist/{internal/OpenApi.logo.js → apps/docs/logo.js} +1 -1
- package/dist/apps/docs/logo.js.map +1 -0
- package/dist/apps/docs/reference.d.ts +21 -0
- package/dist/apps/docs/reference.d.ts.map +1 -0
- package/dist/apps/docs/reference.js +112 -0
- package/dist/apps/docs/reference.js.map +1 -0
- package/dist/apps/index.d.ts +5 -0
- package/dist/apps/index.d.ts.map +1 -0
- package/dist/apps/index.js +5 -0
- package/dist/apps/index.js.map +1 -0
- package/dist/apps/mcp.d.ts +76 -0
- package/dist/apps/mcp.d.ts.map +1 -0
- package/dist/apps/mcp.js +188 -0
- package/dist/apps/mcp.js.map +1 -0
- package/dist/apps/metadata.d.ts +7 -0
- package/dist/apps/metadata.d.ts.map +1 -0
- package/dist/apps/metadata.js +15 -0
- package/dist/apps/metadata.js.map +1 -0
- package/dist/apps/platform/App.d.ts +5262 -0
- package/dist/apps/platform/App.d.ts.map +1 -0
- package/dist/apps/platform/App.js +84 -0
- package/dist/apps/platform/App.js.map +1 -0
- package/dist/apps/platform/Billing.d.ts +279 -0
- package/dist/apps/platform/Billing.d.ts.map +1 -0
- package/dist/apps/platform/Billing.js +477 -0
- package/dist/apps/platform/Billing.js.map +1 -0
- package/dist/apps/platform/index.d.ts +3 -0
- package/dist/apps/platform/index.d.ts.map +1 -0
- package/dist/apps/platform/index.js +3 -0
- package/dist/apps/platform/index.js.map +1 -0
- package/dist/apps/platform/routes/api-keys.d.ts +973 -0
- package/dist/apps/platform/routes/api-keys.d.ts.map +1 -0
- package/dist/apps/platform/routes/api-keys.js +430 -0
- package/dist/apps/platform/routes/api-keys.js.map +1 -0
- package/dist/apps/platform/routes/billing.d.ts +1520 -0
- package/dist/apps/platform/routes/billing.d.ts.map +1 -0
- package/dist/apps/platform/routes/billing.js +795 -0
- package/dist/apps/platform/routes/billing.js.map +1 -0
- package/dist/apps/platform/routes/invitations.d.ts +851 -0
- package/dist/apps/platform/routes/invitations.d.ts.map +1 -0
- package/dist/apps/platform/routes/invitations.js +471 -0
- package/dist/apps/platform/routes/invitations.js.map +1 -0
- package/dist/apps/platform/routes/me.d.ts +95 -0
- package/dist/apps/platform/routes/me.d.ts.map +1 -0
- package/dist/apps/platform/routes/me.js +81 -0
- package/dist/apps/platform/routes/me.js.map +1 -0
- package/dist/apps/platform/routes/members.d.ts +554 -0
- package/dist/apps/platform/routes/members.d.ts.map +1 -0
- package/dist/apps/platform/routes/members.js +251 -0
- package/dist/apps/platform/routes/members.js.map +1 -0
- package/dist/apps/platform/routes/orgs.d.ts +596 -0
- package/dist/apps/platform/routes/orgs.d.ts.map +1 -0
- package/dist/apps/platform/routes/orgs.js +276 -0
- package/dist/apps/platform/routes/orgs.js.map +1 -0
- package/dist/apps/platform/routes/projects.d.ts +740 -0
- package/dist/apps/platform/routes/projects.d.ts.map +1 -0
- package/dist/apps/platform/routes/projects.js +286 -0
- package/dist/apps/platform/routes/projects.js.map +1 -0
- package/dist/apps/platform/routes/scopes.d.ts +107 -0
- package/dist/apps/platform/routes/scopes.d.ts.map +1 -0
- package/dist/apps/platform/routes/scopes.js +66 -0
- package/dist/apps/platform/routes/scopes.js.map +1 -0
- package/dist/apps/platform/routes/usage.d.ts +588 -0
- package/dist/apps/platform/routes/usage.d.ts.map +1 -0
- package/dist/apps/platform/routes/usage.js +391 -0
- package/dist/apps/platform/routes/usage.js.map +1 -0
- package/dist/apps/relay/App.d.ts +44 -0
- package/dist/apps/relay/App.d.ts.map +1 -0
- package/dist/apps/relay/App.js +211 -0
- package/dist/apps/relay/App.js.map +1 -0
- package/dist/apps/relay/Sponsorships.d.ts +107 -0
- package/dist/apps/relay/Sponsorships.d.ts.map +1 -0
- package/dist/apps/relay/Sponsorships.js +194 -0
- package/dist/apps/relay/Sponsorships.js.map +1 -0
- package/dist/apps/relay/index.d.ts +3 -0
- package/dist/apps/relay/index.d.ts.map +1 -0
- package/dist/apps/relay/index.js +3 -0
- package/dist/apps/relay/index.js.map +1 -0
- package/dist/cli/Cli.d.ts +9 -2
- package/dist/cli/Cli.d.ts.map +1 -1
- package/dist/cli/Cli.js +12 -4
- package/dist/cli/Cli.js.map +1 -1
- package/dist/cli/commands/admin.d.ts +5 -1
- package/dist/cli/commands/admin.d.ts.map +1 -1
- package/dist/cli/commands/admin.js +82 -3
- package/dist/cli/commands/admin.js.map +1 -1
- package/dist/db/Db.d.ts +17 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +254 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +159 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +187 -2
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/billingSettings.d.ts +55 -0
- package/dist/db/tables/billingSettings.d.ts.map +1 -0
- package/dist/db/tables/billingSettings.js +70 -0
- package/dist/db/tables/billingSettings.js.map +1 -0
- package/dist/db/tables/invitations.d.ts +84 -0
- package/dist/db/tables/invitations.d.ts.map +1 -0
- package/dist/db/tables/invitations.js +165 -0
- package/dist/db/tables/invitations.js.map +1 -0
- package/dist/db/tables/memberships.d.ts +108 -0
- package/dist/db/tables/memberships.d.ts.map +1 -0
- package/dist/db/tables/memberships.js +153 -0
- package/dist/db/tables/memberships.js.map +1 -0
- package/dist/db/tables/organizations.d.ts +48 -0
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +97 -2
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/db/tables/projects.d.ts +65 -0
- package/dist/db/tables/projects.d.ts.map +1 -0
- package/dist/db/tables/projects.js +75 -0
- package/dist/db/tables/projects.js.map +1 -0
- package/dist/db/tables/requestUsage.d.ts +121 -0
- package/dist/db/tables/requestUsage.d.ts.map +1 -0
- package/dist/db/tables/requestUsage.js +150 -0
- package/dist/db/tables/requestUsage.js.map +1 -0
- package/dist/db/tables/sponsoredTransactions.d.ts +239 -0
- package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -0
- package/dist/db/tables/sponsoredTransactions.js +314 -0
- package/dist/db/tables/sponsoredTransactions.js.map +1 -0
- package/dist/db/tables/stripeCustomers.d.ts +81 -0
- package/dist/db/tables/stripeCustomers.d.ts.map +1 -0
- package/dist/db/tables/stripeCustomers.js +120 -0
- package/dist/db/tables/stripeCustomers.js.map +1 -0
- package/dist/db/tables/users.d.ts +48 -0
- package/dist/db/tables/users.d.ts.map +1 -0
- package/dist/db/tables/users.js +65 -0
- package/dist/db/tables/users.js.map +1 -0
- package/dist/handlers/internal/executionError.d.ts.map +1 -1
- package/dist/handlers/internal/executionError.js +2 -0
- package/dist/handlers/internal/executionError.js.map +1 -1
- package/dist/handlers/internal/multisig.d.ts +2 -6
- package/dist/handlers/internal/multisig.d.ts.map +1 -1
- package/dist/handlers/internal/multisig.js +11 -36
- package/dist/handlers/internal/multisig.js.map +1 -1
- package/dist/handlers/internal/sponsorship.d.ts +35 -2
- package/dist/handlers/internal/sponsorship.d.ts.map +1 -1
- package/dist/handlers/internal/sponsorship.js +53 -14
- package/dist/handlers/internal/sponsorship.js.map +1 -1
- package/dist/handlers/internal/utils.d.ts +10 -3
- package/dist/handlers/internal/utils.d.ts.map +1 -1
- package/dist/handlers/internal/utils.js +14 -6
- package/dist/handlers/internal/utils.js.map +1 -1
- package/dist/handlers/relay.d.ts +6 -2
- package/dist/handlers/relay.d.ts.map +1 -1
- package/dist/handlers/relay.js +16 -2
- package/dist/handlers/relay.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/Auth.d.ts +222 -6
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +625 -74
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Cache.d.ts.map +1 -1
- package/dist/internal/Cache.js +4 -1
- package/dist/internal/Cache.js.map +1 -1
- package/dist/internal/EdgeCache.d.ts +10 -0
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +39 -8
- package/dist/internal/EdgeCache.js.map +1 -1
- package/dist/internal/Fees.d.ts +27 -0
- package/dist/internal/Fees.d.ts.map +1 -0
- package/dist/internal/Fees.js +48 -0
- package/dist/internal/Fees.js.map +1 -0
- package/dist/internal/FundingRoutes.d.ts +183 -0
- package/dist/internal/FundingRoutes.d.ts.map +1 -0
- package/dist/internal/FundingRoutes.js +490 -0
- package/dist/internal/FundingRoutes.js.map +1 -0
- package/dist/internal/Id.d.ts +15 -0
- package/dist/internal/Id.d.ts.map +1 -0
- package/dist/internal/Id.js +22 -0
- package/dist/internal/Id.js.map +1 -0
- package/dist/internal/Log.d.ts +9 -3
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +7 -0
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/OpenApi.d.ts +10 -20
- package/dist/internal/OpenApi.d.ts.map +1 -1
- package/dist/internal/OpenApi.js +8 -58
- package/dist/internal/OpenApi.js.map +1 -1
- package/dist/internal/RateLimit.d.ts +25 -0
- package/dist/internal/RateLimit.d.ts.map +1 -1
- package/dist/internal/RateLimit.js +51 -0
- package/dist/internal/RateLimit.js.map +1 -1
- package/dist/internal/Response.d.ts +1 -1
- package/dist/internal/Response.d.ts.map +1 -1
- package/dist/internal/Schema.d.ts +8 -0
- package/dist/internal/Schema.d.ts.map +1 -1
- package/dist/internal/Schema.js +7 -0
- package/dist/internal/Schema.js.map +1 -1
- package/dist/internal/Store.d.ts +7 -1
- package/dist/internal/Store.d.ts.map +1 -1
- package/dist/internal/Store.js +16 -2
- package/dist/internal/Store.js.map +1 -1
- package/dist/internal/Viem.d.ts +84 -0
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Webhooks.d.ts +1 -1
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +3 -3
- package/dist/internal/Webhooks.js.map +1 -1
- package/package.json +33 -6
- package/src/ApiKey.ts +10 -2
- package/src/ApiKeys.test.ts +155 -1
- package/src/ApiKeys.ts +203 -1
- package/src/App.test.ts +64 -75741
- package/src/App.ts +218 -420
- package/src/Client.test-d.ts +657 -15
- package/src/Scope.ts +26 -4
- package/src/Webhooks.ts +2 -2
- package/src/admin/apps/api-keys.test.ts +51 -0
- package/src/admin/apps/api-keys.ts +84 -0
- package/src/admin/apps/scopes.test.ts +11 -1
- package/src/admin/apps/scopes.ts +3 -0
- package/src/admin/apps/verified-tokens.ts +1 -1
- package/src/admin/ui/src/components/Page.tsx +2 -3
- package/src/admin/ui/src/routes/__root.tsx +2 -2
- package/src/admin/ui/src/routes/api-keys.tsx +2 -2
- package/src/admin/ui.generated.ts +13 -13
- package/src/analytics/Analytics.test.ts +527 -0
- package/src/analytics/Analytics.ts +318 -0
- package/src/analytics/tables/requestEvents.ts +523 -0
- package/src/apps/composition.test.ts +66 -0
- package/src/apps/data/App.test.ts +72 -0
- package/src/apps/data/App.ts +330 -0
- package/src/apps/data/index.ts +2 -0
- package/src/apps/{activities.test.ts → data/routes/activities.test.ts} +3 -3
- package/src/apps/{activities.ts → data/routes/activities.ts} +15 -15
- package/src/apps/{addresses.ts → data/routes/addresses.ts} +6 -6
- package/src/apps/{balances.test.ts → data/routes/balances.test.ts} +2 -2
- package/src/apps/{balances.ts → data/routes/balances.ts} +46 -12
- package/src/apps/{blocks.test.ts → data/routes/blocks.test.ts} +3 -3
- package/src/apps/{blocks.ts → data/routes/blocks.ts} +14 -14
- package/src/apps/{coingecko.test.ts → data/routes/coingecko.test.ts} +2 -2
- package/src/apps/{coingecko.ts → data/routes/coingecko.ts} +14 -14
- package/src/apps/{exchanges.test.ts → data/routes/exchanges.test.ts} +1 -1
- package/src/apps/{exchanges.ts → data/routes/exchanges.ts} +20 -20
- package/src/apps/{fee-amm.test.ts → data/routes/fee-amm.test.ts} +3 -3
- package/src/apps/{fee-amm.ts → data/routes/fee-amm.ts} +18 -18
- package/src/apps/data/routes/funding.test.ts +214 -0
- package/src/apps/data/routes/funding.ts +584 -0
- package/src/apps/{indexer.test.ts → data/routes/indexer.test.ts} +26 -5
- package/src/apps/{indexer.ts → data/routes/indexer.ts} +13 -10
- package/src/apps/{receipts.test.ts → data/routes/receipts.test.ts} +3 -3
- package/src/apps/{receipts.ts → data/routes/receipts.ts} +15 -22
- package/src/apps/{rpc.test.ts → data/routes/rpc.test.ts} +122 -7
- package/src/apps/{rpc.ts → data/routes/rpc.ts} +38 -8
- package/src/apps/{tokenlist.test.ts → data/routes/tokenlist.test.ts} +3 -3
- package/src/apps/{tokenlist.ts → data/routes/tokenlist.ts} +9 -9
- package/src/apps/{tokens.test.ts → data/routes/tokens.test.ts} +2 -2
- package/src/apps/{tokens.ts → data/routes/tokens.ts} +20 -20
- package/src/apps/{transactions.test.ts → data/routes/transactions.test.ts} +3 -3
- package/src/apps/{transactions.ts → data/routes/transactions.ts} +15 -15
- package/src/apps/{transfers.test.ts → data/routes/transfers.test.ts} +2 -2
- package/src/apps/{transfers.ts → data/routes/transfers.ts} +15 -15
- package/src/apps/{verified-tokens.test.ts → data/routes/verified-tokens.test.ts} +3 -3
- package/src/apps/{verified-tokens.ts → data/routes/verified-tokens.ts} +10 -10
- package/src/apps/{webhooks.test.ts → data/routes/webhooks.test.ts} +7 -7
- package/src/apps/{webhooks.ts → data/routes/webhooks.ts} +50 -43
- package/src/apps/docs/App.ts +65 -0
- package/src/apps/docs/index.ts +1 -0
- package/src/apps/docs/reference.ts +132 -0
- package/src/apps/index.ts +4 -0
- package/src/apps/mcp.test.ts +206 -0
- package/src/apps/mcp.ts +250 -0
- package/src/apps/metadata.ts +23 -0
- package/src/apps/platform/App.ts +169 -0
- package/src/apps/platform/Billing.test.ts +530 -0
- package/src/apps/platform/Billing.ts +682 -0
- package/src/apps/platform/index.ts +2 -0
- package/src/apps/platform/routes/api-keys.test.ts +493 -0
- package/src/apps/platform/routes/api-keys.ts +543 -0
- package/src/apps/platform/routes/billing.test.ts +1100 -0
- package/src/apps/platform/routes/billing.ts +966 -0
- package/src/apps/platform/routes/invitations.test.ts +409 -0
- package/src/apps/platform/routes/invitations.ts +587 -0
- package/src/apps/platform/routes/me.test.ts +351 -0
- package/src/apps/platform/routes/me.ts +106 -0
- package/src/apps/platform/routes/members.test.ts +195 -0
- package/src/apps/platform/routes/members.ts +313 -0
- package/src/apps/platform/routes/orgs.test.ts +313 -0
- package/src/apps/platform/routes/orgs.ts +365 -0
- package/src/apps/platform/routes/projects.test.ts +181 -0
- package/src/apps/platform/routes/projects.ts +367 -0
- package/src/apps/platform/routes/scopes.test.ts +29 -0
- package/src/apps/platform/routes/scopes.ts +85 -0
- package/src/apps/platform/routes/usage.test.ts +422 -0
- package/src/apps/platform/routes/usage.ts +527 -0
- package/src/apps/relay/App.test.ts +933 -0
- package/src/apps/relay/App.ts +236 -0
- package/src/apps/relay/Sponsorships.test.ts +375 -0
- package/src/apps/relay/Sponsorships.ts +306 -0
- package/src/apps/relay/index.ts +2 -0
- package/src/cli/Cli.test.ts +25 -2
- package/src/cli/Cli.ts +18 -3
- package/src/cli/commands/admin.test.ts +83 -0
- package/src/cli/commands/admin.ts +91 -3
- package/src/db/Db.ts +296 -0
- package/src/db/Schema.ts +241 -2
- package/src/db/tables/billingSettings.test.ts +87 -0
- package/src/db/tables/billingSettings.ts +108 -0
- package/src/db/tables/invitations.ts +198 -0
- package/src/db/tables/memberships.ts +208 -0
- package/src/db/tables/organizations.test.ts +38 -2
- package/src/db/tables/organizations.ts +117 -2
- package/src/db/tables/projects.ts +107 -0
- package/src/db/tables/requestUsage.ts +231 -0
- package/src/db/tables/sponsoredTransactions.test.ts +325 -0
- package/src/db/tables/sponsoredTransactions.ts +464 -0
- package/src/db/tables/stripeCustomers.test.ts +109 -0
- package/src/db/tables/stripeCustomers.ts +162 -0
- package/src/db/tables/users.ts +85 -0
- package/src/handlers/internal/executionError.ts +2 -0
- package/src/handlers/internal/multisig.test.ts +84 -10
- package/src/handlers/internal/multisig.ts +15 -50
- package/src/handlers/internal/sponsorship.ts +106 -20
- package/src/handlers/internal/utils.ts +19 -7
- package/src/handlers/{internal/relay.localnet.test.ts → relay.test.ts} +2 -2
- package/src/handlers/relay.ts +23 -5
- package/src/index.ts +1 -0
- package/src/internal/Auth.test.ts +491 -25
- package/src/internal/Auth.ts +839 -84
- package/src/internal/Cache.ts +5 -1
- package/src/internal/Cursor.test.ts +3 -2
- package/src/internal/EdgeCache.test.ts +27 -4
- package/src/internal/EdgeCache.ts +47 -9
- package/src/internal/Fees.test.ts +32 -0
- package/src/internal/Fees.ts +51 -0
- package/src/internal/FundingRoutes.ts +583 -0
- package/src/internal/Id.test.ts +17 -0
- package/src/internal/Id.ts +24 -0
- package/src/internal/Log.test.ts +2 -1
- package/src/internal/Log.ts +16 -3
- package/src/internal/MetricSink.ts +1 -1
- package/src/internal/OpenApi.ts +12 -60
- package/src/internal/RateLimit.test.ts +84 -0
- package/src/internal/RateLimit.ts +70 -0
- package/src/internal/Response.ts +1 -1
- package/src/internal/Schema.ts +8 -0
- package/src/internal/Store.test.ts +52 -0
- package/src/internal/Store.ts +24 -5
- package/src/internal/Tidx.local.test.ts +1 -1
- package/src/internal/VerifiedTokens.test.ts +1 -1
- package/src/internal/WebhookDestination.test.ts +1 -1
- package/src/internal/Webhooks.test.ts +7 -7
- package/src/internal/Webhooks.ts +4 -4
- package/dist/apps/activities.js.map +0 -1
- package/dist/apps/addresses.js.map +0 -1
- package/dist/apps/balances.d.ts.map +0 -1
- package/dist/apps/balances.js.map +0 -1
- package/dist/apps/blocks.d.ts.map +0 -1
- package/dist/apps/blocks.js.map +0 -1
- package/dist/apps/coingecko.d.ts.map +0 -1
- package/dist/apps/coingecko.js.map +0 -1
- package/dist/apps/exchanges.d.ts.map +0 -1
- package/dist/apps/exchanges.js.map +0 -1
- package/dist/apps/fee-amm.d.ts.map +0 -1
- package/dist/apps/fee-amm.js.map +0 -1
- package/dist/apps/indexer.d.ts.map +0 -1
- package/dist/apps/indexer.js.map +0 -1
- package/dist/apps/receipts.d.ts.map +0 -1
- package/dist/apps/receipts.js.map +0 -1
- package/dist/apps/rpc.d.ts.map +0 -1
- package/dist/apps/rpc.js.map +0 -1
- package/dist/apps/tokenlist.d.ts.map +0 -1
- package/dist/apps/tokenlist.js.map +0 -1
- package/dist/apps/tokens.d.ts.map +0 -1
- package/dist/apps/tokens.js.map +0 -1
- package/dist/apps/transactions.d.ts.map +0 -1
- package/dist/apps/transactions.js.map +0 -1
- package/dist/apps/transfers.d.ts.map +0 -1
- package/dist/apps/transfers.js.map +0 -1
- package/dist/apps/verified-tokens.d.ts.map +0 -1
- package/dist/apps/verified-tokens.js.map +0 -1
- package/dist/apps/webhooks.js.map +0 -1
- package/dist/internal/OpenApi.fonts.d.ts.map +0 -1
- package/dist/internal/OpenApi.fonts.js.map +0 -1
- package/dist/internal/OpenApi.logo.d.ts.map +0 -1
- package/dist/internal/OpenApi.logo.js.map +0 -1
- package/src/handlers/internal/multisig.localnet.test.ts +0 -146
- /package/src/{internal/OpenApi.fonts.ts → apps/docs/fonts.ts} +0 -0
- /package/src/{internal/OpenApi.logo.ts → apps/docs/logo.ts} +0 -0
|
@@ -0,0 +1,933 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid'
|
|
2
|
+
import { Hash, Secp256k1 } from 'ox'
|
|
3
|
+
import { createClient, http } from 'viem'
|
|
4
|
+
import { prepareTransactionRequest, sendRawTransactionSync, signTransaction } from 'viem/actions'
|
|
5
|
+
import { Account as TempoAccount, Actions, Transaction, withRelay } from 'viem/tempo'
|
|
6
|
+
|
|
7
|
+
import * as TestApp from '../../../test/App.js'
|
|
8
|
+
import { startTempo } from '../../../test/containers.js'
|
|
9
|
+
import * as Relay from '../../../test/Relay.js'
|
|
10
|
+
import * as Runtime from '../../../test/runtime.js'
|
|
11
|
+
import * as Tempo from '../../../test/Tempo.js'
|
|
12
|
+
import * as TestStripe from '../../../test/Stripe.js'
|
|
13
|
+
import * as BillingSettings from '../../db/tables/billingSettings.js'
|
|
14
|
+
import type * as Db from '../../db/Db.js'
|
|
15
|
+
import * as Fees from '../../internal/Fees.js'
|
|
16
|
+
import * as Organizations from '../../db/tables/organizations.js'
|
|
17
|
+
import * as SponsoredTransactions from '../../db/tables/sponsoredTransactions.js'
|
|
18
|
+
import * as StripeCustomers from '../../db/tables/stripeCustomers.js'
|
|
19
|
+
import * as core_Billing from '../platform/Billing.js'
|
|
20
|
+
import * as RequestListener from '../../handlers/internal/requestListener.js'
|
|
21
|
+
import * as Viem from '../../internal/Viem.js'
|
|
22
|
+
import * as Sponsorships from './Sponsorships.js'
|
|
23
|
+
|
|
24
|
+
const runtime = Runtime.get()
|
|
25
|
+
|
|
26
|
+
const feePayerAccount = Tempo.accounts[10]!
|
|
27
|
+
const senderAccount = Tempo.accounts[9]!
|
|
28
|
+
|
|
29
|
+
/** Super admin secret configured on the end-to-end billing app. */
|
|
30
|
+
const superAdminSecret = 'tempo:sk:a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1'
|
|
31
|
+
|
|
32
|
+
/** Keys covering the sponsorship policy matrix; all hold the base relay scope. */
|
|
33
|
+
const keys = [
|
|
34
|
+
{
|
|
35
|
+
id: 'key_sponsor',
|
|
36
|
+
orgId: 'org_1',
|
|
37
|
+
projectId: 'prj_1',
|
|
38
|
+
environment: 'sandbox',
|
|
39
|
+
scopes: ['rpc-relay:read', 'rpc-relay:sponsor'],
|
|
40
|
+
token: 'secret_sponsor',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'key_sponsor_prod',
|
|
44
|
+
orgId: 'org_1',
|
|
45
|
+
projectId: 'prj_1',
|
|
46
|
+
environment: 'production',
|
|
47
|
+
scopes: ['rpc-relay:read', 'rpc-relay:sponsor'],
|
|
48
|
+
token: 'secret_sponsor_prod',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'key_wildcard',
|
|
52
|
+
orgId: 'org_2',
|
|
53
|
+
projectId: 'prj_2',
|
|
54
|
+
environment: 'sandbox',
|
|
55
|
+
scopes: ['*'],
|
|
56
|
+
token: 'secret_wildcard',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'key_wildcard_prod',
|
|
60
|
+
orgId: 'org_2',
|
|
61
|
+
projectId: 'prj_2',
|
|
62
|
+
environment: 'production',
|
|
63
|
+
scopes: ['*'],
|
|
64
|
+
token: 'secret_wildcard_prod',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'key_unscoped',
|
|
68
|
+
orgId: 'org_1',
|
|
69
|
+
projectId: 'prj_1',
|
|
70
|
+
environment: 'sandbox',
|
|
71
|
+
scopes: ['rpc-relay:read'],
|
|
72
|
+
token: 'secret_unscoped',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'key_unattributed',
|
|
76
|
+
orgId: 'org_1',
|
|
77
|
+
environment: 'sandbox',
|
|
78
|
+
scopes: ['rpc-relay:read', 'rpc-relay:sponsor'],
|
|
79
|
+
token: 'secret_unattributed',
|
|
80
|
+
},
|
|
81
|
+
] as const satisfies readonly TestApp.kvStore.Key[]
|
|
82
|
+
|
|
83
|
+
describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship', () => {
|
|
84
|
+
test('behavior: scoped keys sponsor, recorded with attribution', async () => {
|
|
85
|
+
const db = TestApp.database()
|
|
86
|
+
const app = TestApp.create({
|
|
87
|
+
auth: { keys },
|
|
88
|
+
db,
|
|
89
|
+
relay: { feePayer: { account: feePayerAccount } },
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const body = await relayRequest(app, {
|
|
93
|
+
method: 'eth_signRawTransaction',
|
|
94
|
+
params: [await signSponsorable()],
|
|
95
|
+
token: 'secret_sponsor',
|
|
96
|
+
})
|
|
97
|
+
expect(body.error).toBeUndefined()
|
|
98
|
+
// The relay returned the envelope counter-signed by the fee payer.
|
|
99
|
+
const signed = Transaction.deserialize(body.result as `0x76${string}`)
|
|
100
|
+
expect(signed.feePayerSignature).toBeDefined()
|
|
101
|
+
|
|
102
|
+
const [row] = await SponsoredTransactions.listPending(db)
|
|
103
|
+
expect(row?.transactionHash).toBe(Hash.keccak256(body.result!))
|
|
104
|
+
expect(row?.signPayload).toMatch(/^0x/)
|
|
105
|
+
expect(row?.chainId).toBe(runtime.chainId)
|
|
106
|
+
// The stored envelope is exactly the counter-signed transaction returned.
|
|
107
|
+
expect(row?.transaction).toBe(body.result)
|
|
108
|
+
expect({ ...row, chainId: '<chainId>', createdAt: '<timestamp>', id: '<id>', signPayload: '<hash>', transaction: '<hex>', transactionHash: '<hash>' }) // prettier-ignore
|
|
109
|
+
.toMatchInlineSnapshot(`
|
|
110
|
+
{
|
|
111
|
+
"apiKeyId": "key_sponsor",
|
|
112
|
+
"billable": false,
|
|
113
|
+
"chainId": "<chainId>",
|
|
114
|
+
"createdAt": "<timestamp>",
|
|
115
|
+
"currency": null,
|
|
116
|
+
"environment": "sandbox",
|
|
117
|
+
"feeAmount": null,
|
|
118
|
+
"feeMax": "1",
|
|
119
|
+
"feeToken": "0x20c0000000000000000000000000000000000000",
|
|
120
|
+
"finalizedAt": null,
|
|
121
|
+
"id": "<id>",
|
|
122
|
+
"meterReportedAt": null,
|
|
123
|
+
"orgId": "org_1",
|
|
124
|
+
"projectId": "prj_1",
|
|
125
|
+
"signPayload": "<hash>",
|
|
126
|
+
"status": "pending",
|
|
127
|
+
"transaction": "<hex>",
|
|
128
|
+
"transactionHash": "<hash>",
|
|
129
|
+
}
|
|
130
|
+
`)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('behavior: production keys record billable rows', async () => {
|
|
134
|
+
const db = TestApp.database()
|
|
135
|
+
const app = TestApp.create({
|
|
136
|
+
auth: { keys },
|
|
137
|
+
db,
|
|
138
|
+
relay: { feePayer: { account: feePayerAccount } },
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const body = await relayRequest(app, {
|
|
142
|
+
method: 'eth_signRawTransaction',
|
|
143
|
+
params: [await signSponsorable()],
|
|
144
|
+
token: 'secret_sponsor_prod',
|
|
145
|
+
})
|
|
146
|
+
expect(body.error).toBeUndefined()
|
|
147
|
+
|
|
148
|
+
const [row] = await SponsoredTransactions.listPending(db)
|
|
149
|
+
expect(row?.billable).toBe(true)
|
|
150
|
+
expect(row?.environment).toMatchInlineSnapshot(`"production"`)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('behavior: the wildcard scope grants sponsorship', async () => {
|
|
154
|
+
const db = TestApp.database()
|
|
155
|
+
const app = TestApp.create({
|
|
156
|
+
auth: { keys },
|
|
157
|
+
db,
|
|
158
|
+
relay: { feePayer: { account: feePayerAccount } },
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const body = await relayRequest(app, {
|
|
162
|
+
method: 'eth_signRawTransaction',
|
|
163
|
+
params: [await signSponsorable()],
|
|
164
|
+
token: 'secret_wildcard',
|
|
165
|
+
})
|
|
166
|
+
expect(body.error).toBeUndefined()
|
|
167
|
+
|
|
168
|
+
const [row] = await SponsoredTransactions.listPending(db)
|
|
169
|
+
expect(row?.orgId).toMatchInlineSnapshot(`"org_2"`)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('behavior: keys without the sponsor scope are refused', async () => {
|
|
173
|
+
const db = TestApp.database()
|
|
174
|
+
const app = TestApp.create({
|
|
175
|
+
auth: { keys },
|
|
176
|
+
db,
|
|
177
|
+
relay: { feePayer: { account: feePayerAccount } },
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
const body = await relayRequest(app, {
|
|
181
|
+
method: 'eth_signRawTransaction',
|
|
182
|
+
params: [await signSponsorable()],
|
|
183
|
+
token: 'secret_unscoped',
|
|
184
|
+
})
|
|
185
|
+
// "Sponsorship rejected." surfaces as invalid params (-32602).
|
|
186
|
+
expect(body.result).toBeUndefined()
|
|
187
|
+
expect(body.error?.code).toBe(-32602)
|
|
188
|
+
expect(await SponsoredTransactions.listPending(db)).toHaveLength(0)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test('behavior: keys without project attribution are refused', async () => {
|
|
192
|
+
const db = TestApp.database()
|
|
193
|
+
const app = TestApp.create({
|
|
194
|
+
auth: { keys },
|
|
195
|
+
db,
|
|
196
|
+
relay: { feePayer: { account: feePayerAccount } },
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
const body = await relayRequest(app, {
|
|
200
|
+
method: 'eth_signRawTransaction',
|
|
201
|
+
params: [await signSponsorable()],
|
|
202
|
+
token: 'secret_unattributed',
|
|
203
|
+
})
|
|
204
|
+
expect(body.result).toBeUndefined()
|
|
205
|
+
expect(body.error?.code).toBe(-32602)
|
|
206
|
+
expect(await SponsoredTransactions.listPending(db)).toHaveLength(0)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test('behavior: super admin principals are refused', async () => {
|
|
210
|
+
const db = TestApp.database()
|
|
211
|
+
const app = TestApp.create({
|
|
212
|
+
auth: { keys, superAdmin: { secret: 'super-admin-secret' } },
|
|
213
|
+
db,
|
|
214
|
+
relay: { feePayer: { account: feePayerAccount } },
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
const body = await relayRequest(app, {
|
|
218
|
+
method: 'eth_signRawTransaction',
|
|
219
|
+
params: [await signSponsorable()],
|
|
220
|
+
token: 'super-admin-secret',
|
|
221
|
+
})
|
|
222
|
+
expect(body.result).toBeUndefined()
|
|
223
|
+
expect(body.error?.code).toBe(-32602)
|
|
224
|
+
expect(await SponsoredTransactions.listPending(db)).toHaveLength(0)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test('behavior: sponsorship stays closed without a fee payer', async () => {
|
|
228
|
+
const db = TestApp.database()
|
|
229
|
+
const app = TestApp.create({ auth: { keys }, db })
|
|
230
|
+
|
|
231
|
+
const body = await relayRequest(app, {
|
|
232
|
+
method: 'eth_signRawTransaction',
|
|
233
|
+
params: [await signSponsorable()],
|
|
234
|
+
token: 'secret_sponsor',
|
|
235
|
+
})
|
|
236
|
+
expect(body.error?.code).toBe(-32601)
|
|
237
|
+
expect(await SponsoredTransactions.listPending(db)).toHaveLength(0)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
test('behavior: mainnet sponsorship is refused until billing ships', async () => {
|
|
241
|
+
const db = TestApp.database()
|
|
242
|
+
const app = TestApp.create({
|
|
243
|
+
auth: { keys },
|
|
244
|
+
db,
|
|
245
|
+
relay: { feePayer: { account: feePayerAccount } },
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
const body = await relayRequest(app, {
|
|
249
|
+
method: 'eth_signRawTransaction',
|
|
250
|
+
params: [
|
|
251
|
+
await senderAccount.signTransaction({
|
|
252
|
+
chainId: 4217,
|
|
253
|
+
feePayer: true,
|
|
254
|
+
gas: 100_000n,
|
|
255
|
+
maxFeePerGas: 1_000_000n,
|
|
256
|
+
maxPriorityFeePerGas: 0n,
|
|
257
|
+
nonce: 0,
|
|
258
|
+
to: Tempo.accounts[8]!.address,
|
|
259
|
+
value: 0n,
|
|
260
|
+
}),
|
|
261
|
+
],
|
|
262
|
+
token: 'secret_sponsor',
|
|
263
|
+
})
|
|
264
|
+
expect(body.result).toBeUndefined()
|
|
265
|
+
expect(body.error?.code).toBe(-32602)
|
|
266
|
+
expect(await SponsoredTransactions.listPending(db)).toHaveLength(0)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
test('behavior: an app-provided fee payer URL cannot trigger the managed fee payer', async () => {
|
|
270
|
+
const db = TestApp.database()
|
|
271
|
+
const app = TestApp.create({
|
|
272
|
+
auth: { keys },
|
|
273
|
+
db,
|
|
274
|
+
relay: { feePayer: { account: feePayerAccount } },
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
const body = await relayRequest(app, {
|
|
278
|
+
method: 'eth_fillTransaction',
|
|
279
|
+
params: [
|
|
280
|
+
{
|
|
281
|
+
calls: [{ to: Tempo.accounts[8]!.address, value: '0x0' }],
|
|
282
|
+
chainId: `0x${runtime.chainId.toString(16)}`,
|
|
283
|
+
feePayer: 'https://example.com',
|
|
284
|
+
from: senderAccount.address,
|
|
285
|
+
gas: '0x186a0',
|
|
286
|
+
maxFeePerGas: '0xf4240',
|
|
287
|
+
maxPriorityFeePerGas: '0x0',
|
|
288
|
+
nonce: '0x0',
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
token: 'secret_unscoped',
|
|
292
|
+
})
|
|
293
|
+
const result = body.result as unknown as
|
|
294
|
+
| { capabilities?: { sponsored?: boolean }; tx?: { feePayerSignature?: string | null } }
|
|
295
|
+
| undefined
|
|
296
|
+
expect(result?.tx?.feePayerSignature ?? null).toBeNull()
|
|
297
|
+
expect(result?.capabilities?.sponsored).toBe(false)
|
|
298
|
+
expect(await SponsoredTransactions.listPending(db)).toHaveLength(0)
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
test('behavior: sponsorship is refused for chains this deployment does not serve', async () => {
|
|
302
|
+
const db = TestApp.database()
|
|
303
|
+
const app = TestApp.create({
|
|
304
|
+
auth: { keys },
|
|
305
|
+
db,
|
|
306
|
+
relay: { feePayer: { account: feePayerAccount } },
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
const body = await relayRequest(app, {
|
|
310
|
+
method: 'eth_signRawTransaction',
|
|
311
|
+
params: [
|
|
312
|
+
await senderAccount.signTransaction({
|
|
313
|
+
chainId: 999_999,
|
|
314
|
+
feePayer: true,
|
|
315
|
+
gas: 100_000n,
|
|
316
|
+
maxFeePerGas: 1_000_000n,
|
|
317
|
+
maxPriorityFeePerGas: 0n,
|
|
318
|
+
nonce: 0,
|
|
319
|
+
to: Tempo.accounts[8]!.address,
|
|
320
|
+
value: 0n,
|
|
321
|
+
}),
|
|
322
|
+
],
|
|
323
|
+
token: 'secret_sponsor',
|
|
324
|
+
})
|
|
325
|
+
expect(body.result).toBeUndefined()
|
|
326
|
+
expect(body.error?.code).toBe(-32602)
|
|
327
|
+
expect(await SponsoredTransactions.listPending(db)).toHaveLength(0)
|
|
328
|
+
})
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
// Chain-free (`eth_signRawTransaction` signs locally; the sponsor fee token
|
|
332
|
+
// short-circuits resolution), so the gate matrix runs in every mode.
|
|
333
|
+
describe('behavior: mainnet billing gate', () => {
|
|
334
|
+
function createApp(db: Db.Source = TestApp.database()) {
|
|
335
|
+
return TestApp.create({
|
|
336
|
+
auth: { keys },
|
|
337
|
+
db,
|
|
338
|
+
relay: { feePayer: { account: feePayerAccount } },
|
|
339
|
+
supportedChainIds: [Viem.chainId.mainnet],
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Activates billing for the org so the gate reaches the limit checks. */
|
|
344
|
+
async function activate(db: ReturnType<typeof TestApp.database>, orgId = 'org_1') {
|
|
345
|
+
await StripeCustomers.create(db, { orgId, stripeCustomerId: `cus_gate_${orgId}` })
|
|
346
|
+
await StripeCustomers.setStatus(db, orgId, 'active')
|
|
347
|
+
// The fee-token gate reads the verified list; seed mainnet's curated tokens.
|
|
348
|
+
await TestApp.verifiedSeed(db, Viem.chainId.mainnet)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Gas prices are attodollars (1e-18 USD) per gas; the signed cap in
|
|
352
|
+
// fee-token units is ceil(gas × maxFeePerGas / 1e12). The helper's default
|
|
353
|
+
// envelope (100k gas × 1e6 atto) caps at 1 microdollar — negligible.
|
|
354
|
+
/** A $0.50 signed cap: 1M gas × 5e11 attodollars. */
|
|
355
|
+
const halfDollarFees = { gas: 1_000_000n, maxFeePerGas: 500_000_000_000n } as const
|
|
356
|
+
/** A $2 signed cap, over the `'1.00'` platform default. */
|
|
357
|
+
const overCapFees = { gas: 1_000_000n, maxFeePerGas: 2_000_000_000_000n } as const
|
|
358
|
+
|
|
359
|
+
test('behavior: refuses mainnet sponsorship without a billing source', async () => {
|
|
360
|
+
const body = await relayRequest(createApp(), {
|
|
361
|
+
method: 'eth_signRawTransaction',
|
|
362
|
+
params: [await signSponsorable({ chainId: Viem.chainId.mainnet })],
|
|
363
|
+
token: 'secret_sponsor_prod',
|
|
364
|
+
})
|
|
365
|
+
expect(body.error?.code).toBe(-32602)
|
|
366
|
+
expect(body.error?.message).toBe('Billing required.')
|
|
367
|
+
expect(body.error?.data).toStrictEqual({ code: 'billing_required' })
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
test('behavior: refuses mainnet sponsorship while billing is inactive', async () => {
|
|
371
|
+
const db = TestApp.database()
|
|
372
|
+
await StripeCustomers.create(db, { orgId: 'org_1', stripeCustomerId: 'cus_gate' })
|
|
373
|
+
const body = await relayRequest(createApp(db), {
|
|
374
|
+
method: 'eth_signRawTransaction',
|
|
375
|
+
params: [await signSponsorable({ chainId: Viem.chainId.mainnet })],
|
|
376
|
+
token: 'secret_sponsor_prod',
|
|
377
|
+
})
|
|
378
|
+
expect(body.error?.code).toBe(-32602)
|
|
379
|
+
expect(body.error?.message).toBe('Billing required.')
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
test('behavior: past-due billing refuses with its own reason', async () => {
|
|
383
|
+
const db = TestApp.database()
|
|
384
|
+
await activate(db)
|
|
385
|
+
// An unpaid invoice parked the org; the caller must settle it, not attach a card.
|
|
386
|
+
await StripeCustomers.setStatus(db, 'org_1', 'past_due')
|
|
387
|
+
const body = await relayRequest(createApp(db), {
|
|
388
|
+
method: 'eth_signRawTransaction',
|
|
389
|
+
params: [await signSponsorable({ chainId: Viem.chainId.mainnet, ...halfDollarFees })],
|
|
390
|
+
token: 'secret_sponsor_prod',
|
|
391
|
+
})
|
|
392
|
+
expect(body.error?.code).toBe(-32602)
|
|
393
|
+
expect(body.error?.message).toBe('Billing past due.')
|
|
394
|
+
expect(body.error?.data).toStrictEqual({ code: 'billing_past_due' })
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
test('behavior: active billing opens mainnet sponsorship and records it', async () => {
|
|
398
|
+
const db = TestApp.database()
|
|
399
|
+
await activate(db)
|
|
400
|
+
|
|
401
|
+
const body = await relayRequest(createApp(db), {
|
|
402
|
+
method: 'eth_signRawTransaction',
|
|
403
|
+
params: [await signSponsorable({ chainId: Viem.chainId.mainnet, ...halfDollarFees })],
|
|
404
|
+
token: 'secret_sponsor_prod',
|
|
405
|
+
})
|
|
406
|
+
expect(body.error).toBeUndefined()
|
|
407
|
+
const signed = Transaction.deserialize(body.result as `0x76${string}`)
|
|
408
|
+
expect(signed.feePayerSignature).toBeDefined()
|
|
409
|
+
|
|
410
|
+
const [row] = await SponsoredTransactions.listPending(db)
|
|
411
|
+
expect(row?.chainId).toBe(Viem.chainId.mainnet)
|
|
412
|
+
expect(row?.billable).toBe(true)
|
|
413
|
+
expect(row?.orgId).toBe('org_1')
|
|
414
|
+
// The signed liability cap in fee-token units, for spend-limit accounting.
|
|
415
|
+
expect(row?.feeMax).toBe('500000')
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
test('behavior: a sandbox key never sponsors mainnet, even with active billing', async () => {
|
|
419
|
+
const db = TestApp.database()
|
|
420
|
+
await activate(db) // org_1 is active…
|
|
421
|
+
const body = await relayRequest(createApp(db), {
|
|
422
|
+
method: 'eth_signRawTransaction',
|
|
423
|
+
params: [await signSponsorable({ chainId: Viem.chainId.mainnet, ...halfDollarFees })],
|
|
424
|
+
token: 'secret_sponsor', // …but this key is sandbox.
|
|
425
|
+
})
|
|
426
|
+
// Refused for the environment, before the billing check reveals a reason.
|
|
427
|
+
expect(body.error?.code).toBe(-32602)
|
|
428
|
+
expect(body.error?.message).toBe('Sponsorship rejected.')
|
|
429
|
+
expect(await SponsoredTransactions.listPending(db)).toHaveLength(0)
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
test('behavior: other orgs stay gated by their own billing', async () => {
|
|
433
|
+
const db = TestApp.database()
|
|
434
|
+
// org_1 is active; the production wildcard key belongs to org_2, which has nothing.
|
|
435
|
+
await activate(db)
|
|
436
|
+
const body = await relayRequest(createApp(db), {
|
|
437
|
+
method: 'eth_signRawTransaction',
|
|
438
|
+
params: [await signSponsorable({ chainId: Viem.chainId.mainnet, ...halfDollarFees })],
|
|
439
|
+
token: 'secret_wildcard_prod',
|
|
440
|
+
})
|
|
441
|
+
expect(body.error?.code).toBe(-32602)
|
|
442
|
+
expect(body.error?.message).toBe('Billing required.')
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
test('behavior: refuses a transaction over the platform fee cap', async () => {
|
|
446
|
+
const db = TestApp.database()
|
|
447
|
+
await activate(db)
|
|
448
|
+
const body = await relayRequest(createApp(db), {
|
|
449
|
+
method: 'eth_signRawTransaction',
|
|
450
|
+
params: [await signSponsorable({ chainId: Viem.chainId.mainnet, ...overCapFees })],
|
|
451
|
+
token: 'secret_sponsor_prod',
|
|
452
|
+
})
|
|
453
|
+
expect(body.error?.code).toBe(-32602)
|
|
454
|
+
expect(body.error?.message).toBe('Transaction fee limit exceeded.')
|
|
455
|
+
expect(body.error?.data).toStrictEqual({ code: 'tx_fee_limit_exceeded' })
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
test('behavior: the org fee limit tightens the platform cap but never loosens it', async () => {
|
|
459
|
+
const db = TestApp.database()
|
|
460
|
+
await activate(db)
|
|
461
|
+
// Tighter than the platform default: $0.50 envelopes now refuse.
|
|
462
|
+
await BillingSettings.upsert(db, { orgId: 'org_1', txFeeLimit: '0.10' })
|
|
463
|
+
const app = createApp(db)
|
|
464
|
+
const tightened = await relayRequest(app, {
|
|
465
|
+
method: 'eth_signRawTransaction',
|
|
466
|
+
params: [await signSponsorable({ chainId: Viem.chainId.mainnet, ...halfDollarFees })],
|
|
467
|
+
token: 'secret_sponsor_prod',
|
|
468
|
+
})
|
|
469
|
+
expect(tightened.error?.data).toStrictEqual({ code: 'tx_fee_limit_exceeded' })
|
|
470
|
+
|
|
471
|
+
// Looser than the platform default: the $2 envelope still refuses.
|
|
472
|
+
await BillingSettings.upsert(db, { orgId: 'org_1', txFeeLimit: '999999' })
|
|
473
|
+
const loosened = await relayRequest(app, {
|
|
474
|
+
method: 'eth_signRawTransaction',
|
|
475
|
+
params: [await signSponsorable({ chainId: Viem.chainId.mainnet, ...overCapFees })],
|
|
476
|
+
token: 'secret_sponsor_prod',
|
|
477
|
+
})
|
|
478
|
+
expect(loosened.error?.data).toStrictEqual({ code: 'tx_fee_limit_exceeded' })
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
test('behavior: refuses fee tokens the verified list does not resolve to USD', async () => {
|
|
482
|
+
const db = TestApp.database()
|
|
483
|
+
await activate(db)
|
|
484
|
+
// EURC is verified yet non-USD; fee math reads micro-USD, so it refuses.
|
|
485
|
+
const eurc = `0x${'e0'.repeat(20)}` as const
|
|
486
|
+
await TestApp.verifiedSeed(db, Viem.chainId.mainnet, {
|
|
487
|
+
tokens: [{ address: eurc, currency: 'EUR', decimals: 6, name: 'Euro Coin', symbol: 'EURC' }],
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
const app = createApp(db)
|
|
491
|
+
const request = async (feeToken: `0x${string}`) =>
|
|
492
|
+
await relayRequest(app, {
|
|
493
|
+
method: 'eth_signRawTransaction',
|
|
494
|
+
params: [await signSponsorable({ chainId: Viem.chainId.mainnet, feeToken })],
|
|
495
|
+
token: 'secret_sponsor_prod',
|
|
496
|
+
})
|
|
497
|
+
const nonUsd = await request(eurc)
|
|
498
|
+
expect(nonUsd.error?.message).toBe('Fee token unsupported.')
|
|
499
|
+
expect(nonUsd.error?.data).toStrictEqual({ code: 'fee_token_unsupported' })
|
|
500
|
+
// Tokens absent from the verified list refuse the same way.
|
|
501
|
+
const unverified = await request(`0x${'ab'.repeat(20)}`)
|
|
502
|
+
expect(unverified.error?.data).toStrictEqual({ code: 'fee_token_unsupported' })
|
|
503
|
+
expect(await SponsoredTransactions.listPending(db)).toHaveLength(0)
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
test('behavior: sponsored rows snapshot the fee currency', async () => {
|
|
507
|
+
const db = TestApp.database()
|
|
508
|
+
await activate(db)
|
|
509
|
+
|
|
510
|
+
const body = await relayRequest(createApp(db), {
|
|
511
|
+
method: 'eth_signRawTransaction',
|
|
512
|
+
params: [await signSponsorable({ chainId: Viem.chainId.mainnet, ...halfDollarFees })],
|
|
513
|
+
token: 'secret_sponsor_prod',
|
|
514
|
+
})
|
|
515
|
+
expect(body.error).toBeUndefined()
|
|
516
|
+
const [row] = await SponsoredTransactions.listPending(db)
|
|
517
|
+
expect(row?.currency).toBe('usd')
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
test('behavior: refuses sponsorship over the period spend limit; raising restores', async () => {
|
|
521
|
+
const db = TestApp.database()
|
|
522
|
+
await activate(db)
|
|
523
|
+
await BillingSettings.upsert(db, { orgId: 'org_1', spendLimit: '1' })
|
|
524
|
+
// $0.90 already finalized this window; a $0.50 cap would overshoot $1.
|
|
525
|
+
const seeded = await SponsoredTransactions.upsert(db, {
|
|
526
|
+
apiKeyId: 'key_sponsor_prod',
|
|
527
|
+
billable: true,
|
|
528
|
+
chainId: Viem.chainId.mainnet,
|
|
529
|
+
environment: 'production',
|
|
530
|
+
orgId: 'org_1',
|
|
531
|
+
projectId: 'prj_1',
|
|
532
|
+
signPayload: `0x${'31'.repeat(32)}`,
|
|
533
|
+
transaction: `0x76${'cc'.repeat(16)}`,
|
|
534
|
+
transactionHash: `0x${'32'.repeat(32)}`,
|
|
535
|
+
})
|
|
536
|
+
await SponsoredTransactions.finalize(db, seeded.id, {
|
|
537
|
+
feeAmount: '900000',
|
|
538
|
+
finalizedAt: new Date().toISOString(),
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
const app = createApp(db)
|
|
542
|
+
const request = async () =>
|
|
543
|
+
await relayRequest(app, {
|
|
544
|
+
method: 'eth_signRawTransaction',
|
|
545
|
+
params: [await signSponsorable({ chainId: Viem.chainId.mainnet, ...halfDollarFees })],
|
|
546
|
+
token: 'secret_sponsor_prod',
|
|
547
|
+
})
|
|
548
|
+
const refused = await request()
|
|
549
|
+
expect(refused.error?.message).toBe('Spend limit exceeded.')
|
|
550
|
+
expect(refused.error?.data).toStrictEqual({ code: 'spend_limit_exceeded' })
|
|
551
|
+
|
|
552
|
+
await BillingSettings.upsert(db, { orgId: 'org_1', spendLimit: '10' })
|
|
553
|
+
const restored = await request()
|
|
554
|
+
expect(restored.error).toBeUndefined()
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
test('behavior: pending signed caps consume spend-limit budget', async () => {
|
|
558
|
+
const db = TestApp.database()
|
|
559
|
+
await activate(db)
|
|
560
|
+
await BillingSettings.upsert(db, { orgId: 'org_1', spendLimit: '1' })
|
|
561
|
+
// A pending $0.90 cap, fee unknown until its receipt lands.
|
|
562
|
+
await SponsoredTransactions.upsert(db, {
|
|
563
|
+
apiKeyId: 'key_sponsor_prod',
|
|
564
|
+
billable: true,
|
|
565
|
+
chainId: Viem.chainId.mainnet,
|
|
566
|
+
environment: 'production',
|
|
567
|
+
feeMax: '900000',
|
|
568
|
+
orgId: 'org_1',
|
|
569
|
+
projectId: 'prj_1',
|
|
570
|
+
signPayload: `0x${'33'.repeat(32)}`,
|
|
571
|
+
transaction: `0x76${'cc'.repeat(16)}`,
|
|
572
|
+
transactionHash: `0x${'34'.repeat(32)}`,
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
const body = await relayRequest(createApp(db), {
|
|
576
|
+
method: 'eth_signRawTransaction',
|
|
577
|
+
params: [await signSponsorable({ chainId: Viem.chainId.mainnet, ...halfDollarFees })],
|
|
578
|
+
token: 'secret_sponsor_prod',
|
|
579
|
+
})
|
|
580
|
+
expect(body.error?.data).toStrictEqual({ code: 'spend_limit_exceeded' })
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
test('behavior: concurrent bursts cannot overshoot the spend limit', async () => {
|
|
584
|
+
// A per-request factory (like production) over a multi-connection pool, so
|
|
585
|
+
// the reserve transactions run concurrently and the advisory lock — not the
|
|
586
|
+
// pool — is what serializes them.
|
|
587
|
+
const db = TestApp.databaseFactory()
|
|
588
|
+
const seed = db()
|
|
589
|
+
await activate(seed)
|
|
590
|
+
// Limit fits exactly two $0.50 sponsorships.
|
|
591
|
+
await BillingSettings.upsert(seed, { orgId: 'org_1', spendLimit: '1' })
|
|
592
|
+
const app = createApp(db)
|
|
593
|
+
// Distinct nonces → distinct envelopes → distinct rows, fired together.
|
|
594
|
+
const results = await Promise.all(
|
|
595
|
+
Array.from({ length: 6 }, async (_, nonce) =>
|
|
596
|
+
relayRequest(app, {
|
|
597
|
+
method: 'eth_signRawTransaction',
|
|
598
|
+
params: [await signSponsorable({ chainId: Viem.chainId.mainnet, nonce, ...halfDollarFees })], // prettier-ignore
|
|
599
|
+
token: 'secret_sponsor_prod',
|
|
600
|
+
}),
|
|
601
|
+
),
|
|
602
|
+
)
|
|
603
|
+
const sponsored = results.filter((r) => !r.error)
|
|
604
|
+
const refused = results.filter((r) => r.error?.data?.code === 'spend_limit_exceeded')
|
|
605
|
+
expect(sponsored).toHaveLength(2)
|
|
606
|
+
expect(refused).toHaveLength(4)
|
|
607
|
+
// Recorded spend never exceeds the limit ($1 = 1e6 base units).
|
|
608
|
+
const spend = await SponsoredTransactions.spend(seed, {
|
|
609
|
+
chainIds: [Viem.chainId.mainnet],
|
|
610
|
+
orgId: 'org_1',
|
|
611
|
+
since: '2000-01-01T00:00:00.000Z',
|
|
612
|
+
})
|
|
613
|
+
expect(spend).toBe(1_000_000n)
|
|
614
|
+
})
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship (viem)', () => {
|
|
618
|
+
const alphaUsd = '0x20c0000000000000000000000000000000000001'
|
|
619
|
+
const feePayer = TempoAccount.fromSecp256k1(Secp256k1.randomPrivateKey())
|
|
620
|
+
const sender = TempoAccount.fromSecp256k1(Secp256k1.randomPrivateKey())
|
|
621
|
+
const relayClient = Relay.getClient()
|
|
622
|
+
|
|
623
|
+
let db: ReturnType<typeof TestApp.database>
|
|
624
|
+
let server: Relay.Server
|
|
625
|
+
|
|
626
|
+
function getClient(policy?: 'sign-and-broadcast' | 'sign-only') {
|
|
627
|
+
return createClient({
|
|
628
|
+
account: sender,
|
|
629
|
+
chain: Tempo.chain,
|
|
630
|
+
transport: withRelay(
|
|
631
|
+
Relay.http(),
|
|
632
|
+
http(`${server.url}/rpc/relay`, {
|
|
633
|
+
fetchOptions: { headers: { 'tempo-api-key': 'secret_sponsor' } },
|
|
634
|
+
}),
|
|
635
|
+
policy ? { policy } : {},
|
|
636
|
+
),
|
|
637
|
+
})
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
beforeAll(async () => {
|
|
641
|
+
await Promise.all(
|
|
642
|
+
[feePayer, sender].map((account) =>
|
|
643
|
+
Actions.faucet.fundSync(relayClient, { account, timeout: 60_000 }),
|
|
644
|
+
),
|
|
645
|
+
)
|
|
646
|
+
db = TestApp.database()
|
|
647
|
+
const app = TestApp.create({
|
|
648
|
+
auth: { keys },
|
|
649
|
+
db,
|
|
650
|
+
// Explicit fee token: exercises the consumer override of the pathUSD default.
|
|
651
|
+
relay: { feePayer: { account: feePayer, feeToken: alphaUsd } },
|
|
652
|
+
})
|
|
653
|
+
server = await Relay.createServer(RequestListener.fromFetchHandler((req) => app.fetch(req)))
|
|
654
|
+
}, 120_000)
|
|
655
|
+
|
|
656
|
+
afterAll(async () => {
|
|
657
|
+
await server.closeAsync()
|
|
658
|
+
})
|
|
659
|
+
|
|
660
|
+
test('behavior: transferSync sponsors end-to-end, recording a fill intent', async () => {
|
|
661
|
+
const { receipt } = await Actions.token.transferSync(getClient(), {
|
|
662
|
+
amount: 1n,
|
|
663
|
+
feePayer: true,
|
|
664
|
+
to: Tempo.accounts[8]!.address,
|
|
665
|
+
token: alphaUsd,
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
// Sponsored on-chain: the fee payer paid, the sender's transfer landed.
|
|
669
|
+
expect(receipt.status).toBe('success')
|
|
670
|
+
expect(receipt.feePayer?.toLowerCase()).toBe(feePayer.address.toLowerCase())
|
|
671
|
+
|
|
672
|
+
const intent = (await SponsoredTransactions.listPending(db)).find(
|
|
673
|
+
(row) => row.transactionHash === null,
|
|
674
|
+
)
|
|
675
|
+
expect(intent?.apiKeyId).toBe('key_sponsor')
|
|
676
|
+
expect(intent?.orgId).toBe('org_1')
|
|
677
|
+
expect(intent?.billable).toBe(false)
|
|
678
|
+
expect(intent?.feeToken?.toLowerCase()).toBe(alphaUsd)
|
|
679
|
+
|
|
680
|
+
const finalizer = Sponsorships.createFinalizer({
|
|
681
|
+
db,
|
|
682
|
+
defaultChainId: runtime.chainId,
|
|
683
|
+
feePayer: feePayer.address,
|
|
684
|
+
rpc: TestApp.rpc,
|
|
685
|
+
tidx: { baseUrl: runtime.tidxUrl! },
|
|
686
|
+
})
|
|
687
|
+
let finalized: Awaited<ReturnType<typeof SponsoredTransactions.get>>
|
|
688
|
+
for (let attempt = 0; attempt < 30; attempt++) {
|
|
689
|
+
await finalizer.tick()
|
|
690
|
+
finalized = await SponsoredTransactions.get(db, intent!.id)
|
|
691
|
+
if (finalized?.status === 'finalized') break
|
|
692
|
+
await new Promise((resolve) => setTimeout(resolve, 1_000))
|
|
693
|
+
}
|
|
694
|
+
expect(finalized?.status).toBe('finalized')
|
|
695
|
+
expect(finalized?.transactionHash).toBe(receipt.transactionHash)
|
|
696
|
+
expect(finalized?.feeAmount).toBe(
|
|
697
|
+
Fees.fromGas(receipt.gasUsed, receipt.effectiveGasPrice).toString(),
|
|
698
|
+
)
|
|
699
|
+
}, 60_000)
|
|
700
|
+
|
|
701
|
+
test('behavior: sign-only sends record and finalize from the real receipt', async () => {
|
|
702
|
+
const request = await prepareTransactionRequest(relayClient, {
|
|
703
|
+
account: sender,
|
|
704
|
+
calls: [
|
|
705
|
+
Actions.token.transfer.call(relayClient, {
|
|
706
|
+
amount: 1n,
|
|
707
|
+
to: Tempo.accounts[8]!.address,
|
|
708
|
+
token: alphaUsd,
|
|
709
|
+
}),
|
|
710
|
+
],
|
|
711
|
+
feePayer: true,
|
|
712
|
+
})
|
|
713
|
+
const signed = await signTransaction(relayClient, request)
|
|
714
|
+
|
|
715
|
+
const receipt = await sendRawTransactionSync(getClient(), {
|
|
716
|
+
serializedTransaction: signed,
|
|
717
|
+
})
|
|
718
|
+
|
|
719
|
+
const row = (await SponsoredTransactions.listPending(db)).find(
|
|
720
|
+
(row) => row.transactionHash === receipt.transactionHash,
|
|
721
|
+
)
|
|
722
|
+
expect(row?.apiKeyId).toBe('key_sponsor')
|
|
723
|
+
expect(row?.orgId).toBe('org_1')
|
|
724
|
+
expect(row?.transaction.startsWith('0x76')).toBe(true)
|
|
725
|
+
|
|
726
|
+
// The finalizer resolves the row against the real localnet receipt.
|
|
727
|
+
const result = await Sponsorships.finalize(db, { getClient: () => relayClient })
|
|
728
|
+
expect(result.failed).toBe(0)
|
|
729
|
+
expect(result.finalized).toBeGreaterThanOrEqual(1)
|
|
730
|
+
const finalized = await SponsoredTransactions.get(db, row!.id)
|
|
731
|
+
expect(finalized?.status).toBe('finalized')
|
|
732
|
+
expect(finalized?.feeAmount).toBe(
|
|
733
|
+
Fees.fromGas(receipt.gasUsed, receipt.effectiveGasPrice).toString(),
|
|
734
|
+
)
|
|
735
|
+
}, 30_000)
|
|
736
|
+
|
|
737
|
+
test('behavior: sign-and-broadcast sends record through the relay', async () => {
|
|
738
|
+
const request = await prepareTransactionRequest(relayClient, {
|
|
739
|
+
account: sender,
|
|
740
|
+
calls: [
|
|
741
|
+
Actions.token.transfer.call(relayClient, {
|
|
742
|
+
amount: 2n,
|
|
743
|
+
to: Tempo.accounts[8]!.address,
|
|
744
|
+
token: alphaUsd,
|
|
745
|
+
}),
|
|
746
|
+
],
|
|
747
|
+
feePayer: true,
|
|
748
|
+
})
|
|
749
|
+
const signed = await signTransaction(relayClient, request)
|
|
750
|
+
|
|
751
|
+
const receipt = await sendRawTransactionSync(getClient('sign-and-broadcast'), {
|
|
752
|
+
serializedTransaction: signed,
|
|
753
|
+
})
|
|
754
|
+
|
|
755
|
+
const rows = await SponsoredTransactions.listPending(db)
|
|
756
|
+
expect(rows.find((row) => row.transactionHash === receipt.transactionHash)).toBeDefined()
|
|
757
|
+
}, 30_000)
|
|
758
|
+
})
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Signs a sponsorship-requesting Tempo transaction (fee-payer slot present but
|
|
762
|
+
* empty; the serializer strips `feeToken`, the sponsor picks it) entirely
|
|
763
|
+
* offline: `eth_signRawTransaction` never touches the chain, so the suite
|
|
764
|
+
* runs without a node.
|
|
765
|
+
*/
|
|
766
|
+
async function signSponsorable(
|
|
767
|
+
overrides: {
|
|
768
|
+
chainId?: number
|
|
769
|
+
feeToken?: `0x${string}`
|
|
770
|
+
gas?: bigint
|
|
771
|
+
maxFeePerGas?: bigint
|
|
772
|
+
nonce?: number
|
|
773
|
+
} = {},
|
|
774
|
+
) {
|
|
775
|
+
return await senderAccount.signTransaction({
|
|
776
|
+
chainId: overrides.chainId ?? runtime.chainId,
|
|
777
|
+
feePayer: true,
|
|
778
|
+
gas: overrides.gas ?? 100_000n,
|
|
779
|
+
maxFeePerGas: overrides.maxFeePerGas ?? 1_000_000n,
|
|
780
|
+
maxPriorityFeePerGas: 0n,
|
|
781
|
+
nonce: overrides.nonce ?? 0,
|
|
782
|
+
to: Tempo.accounts[8]!.address,
|
|
783
|
+
value: 0n,
|
|
784
|
+
...(overrides.feeToken ? { feeToken: overrides.feeToken } : {}),
|
|
785
|
+
})
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/** Posts one JSON-RPC request to the composed app's relay mount. */
|
|
789
|
+
async function relayRequest(
|
|
790
|
+
app: ReturnType<typeof TestApp.create>,
|
|
791
|
+
options: { method: string; params: readonly unknown[]; token?: string | undefined },
|
|
792
|
+
) {
|
|
793
|
+
const response = await app.fetch(
|
|
794
|
+
new Request('http://tempo-api.test/rpc/relay', {
|
|
795
|
+
body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: options.method, params: options.params }), // prettier-ignore
|
|
796
|
+
headers: {
|
|
797
|
+
'content-type': 'application/json',
|
|
798
|
+
...(options.token ? { 'tempo-api-key': options.token } : {}),
|
|
799
|
+
},
|
|
800
|
+
method: 'POST',
|
|
801
|
+
}),
|
|
802
|
+
)
|
|
803
|
+
return (await response.json()) as {
|
|
804
|
+
error?: { code: number; data?: { code: string } | undefined; message: string } | undefined
|
|
805
|
+
result?: `0x${string}` | undefined
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// The full billing-to-invoice flow with nothing synthetic: a disposable node
|
|
810
|
+
// runs under mainnet's chain id (dev consensus, funded dev alloc), so a real
|
|
811
|
+
// card attach opens the real gate, a real sponsored transaction mines with a
|
|
812
|
+
// real receipt, the real finalizer prices it, and real meter events land on a
|
|
813
|
+
// real Stripe invoice.
|
|
814
|
+
describe.runIf(TestStripe.secretKey)(
|
|
815
|
+
'behavior: sponsored spend reaches the invoice (stripe sandbox)',
|
|
816
|
+
() => {
|
|
817
|
+
// prettier-ignore
|
|
818
|
+
const webhookSecret = `whsec_${'c'.repeat(32)}`
|
|
819
|
+
let node: Awaited<ReturnType<typeof startTempo>> | undefined
|
|
820
|
+
|
|
821
|
+
beforeAll(async () => {
|
|
822
|
+
node = await startTempo({ chainId: Viem.chainId.mainnet })
|
|
823
|
+
}, 240_000)
|
|
824
|
+
|
|
825
|
+
afterAll(async () => {
|
|
826
|
+
await TestStripe.sweep()
|
|
827
|
+
await node?.container.stop()
|
|
828
|
+
})
|
|
829
|
+
|
|
830
|
+
test(
|
|
831
|
+
'a sponsored transaction meters real usage onto the invoice',
|
|
832
|
+
{ retry: 0, timeout: 300_000 },
|
|
833
|
+
async () => {
|
|
834
|
+
// prettier-ignore
|
|
835
|
+
const { url } = node!
|
|
836
|
+
const stripe = TestStripe.client()
|
|
837
|
+
const db = TestApp.database()
|
|
838
|
+
// The key attributes to org_1; the row must exist for the usage read.
|
|
839
|
+
await Organizations.create(db, { id: 'org_1', name: 'Sponsored Spend Org' })
|
|
840
|
+
// Mainnet sponsorship refuses fee tokens the verified list cannot
|
|
841
|
+
// resolve to USD; seed it like the gate tests do.
|
|
842
|
+
await TestApp.verifiedSeed(db, Viem.chainId.mainnet)
|
|
843
|
+
const app = TestApp.create({
|
|
844
|
+
auth: { keys, superAdmin: { secret: superAdminSecret } },
|
|
845
|
+
billing: { stripe: { client: stripe, webhookSecret } },
|
|
846
|
+
db,
|
|
847
|
+
relay: { feePayer: { account: feePayerAccount } },
|
|
848
|
+
rpc: { url: () => url },
|
|
849
|
+
supportedChainIds: [Viem.chainId.mainnet],
|
|
850
|
+
})
|
|
851
|
+
const chainClient = Viem.getClient({
|
|
852
|
+
chainId: Viem.chainId.mainnet,
|
|
853
|
+
rpc: { url: () => url },
|
|
854
|
+
})
|
|
855
|
+
// Both signers pay real fee-token balances on the node.
|
|
856
|
+
await Actions.faucet.fundSync(chainClient, { account: senderAccount.address, timeout: 60_000 }) // prettier-ignore
|
|
857
|
+
await Actions.faucet.fundSync(chainClient, { account: feePayerAccount.address, timeout: 60_000 }) // prettier-ignore
|
|
858
|
+
|
|
859
|
+
// Closed until a real card attach flips the derived status.
|
|
860
|
+
const refused = await relayRequest(app, {
|
|
861
|
+
method: 'eth_signRawTransaction',
|
|
862
|
+
params: [await signSponsorable({ chainId: Viem.chainId.mainnet })],
|
|
863
|
+
token: 'secret_sponsor_prod',
|
|
864
|
+
})
|
|
865
|
+
expect(refused.error?.data).toStrictEqual({ code: 'billing_required' })
|
|
866
|
+
|
|
867
|
+
const customer = await stripe.customers.create({
|
|
868
|
+
metadata: { orgId: 'org_1' },
|
|
869
|
+
name: `Test Org ${nanoid(10)}`,
|
|
870
|
+
})
|
|
871
|
+
TestStripe.track(customer.id)
|
|
872
|
+
await StripeCustomers.create(db, { orgId: 'org_1', stripeCustomerId: customer.id })
|
|
873
|
+
await TestStripe.completeCheckout(app, { customer: customer.id, webhookSecret })
|
|
874
|
+
expect((await StripeCustomers.get(db, 'org_1'))?.status).toBe('active')
|
|
875
|
+
|
|
876
|
+
// Broadcast a sponsored transaction through the relay; it mines locally.
|
|
877
|
+
const sent = await relayRequest(app, {
|
|
878
|
+
method: 'eth_sendRawTransactionSync',
|
|
879
|
+
params: [
|
|
880
|
+
await signSponsorable({
|
|
881
|
+
chainId: Viem.chainId.mainnet,
|
|
882
|
+
gas: 1_000_000n,
|
|
883
|
+
maxFeePerGas: 500_000_000_000n,
|
|
884
|
+
}),
|
|
885
|
+
],
|
|
886
|
+
token: 'secret_sponsor_prod',
|
|
887
|
+
})
|
|
888
|
+
expect(sent.error).toBeUndefined()
|
|
889
|
+
|
|
890
|
+
// The real receipt finalizes the row with the actual charged fee.
|
|
891
|
+
const finalizer = Sponsorships.createFinalizer({ db, rpc: { url: () => url } })
|
|
892
|
+
await finalizer.tick()
|
|
893
|
+
const [row] = await SponsoredTransactions.listUnreported(db, { chainIds: [Viem.chainId.mainnet] }) // prettier-ignore
|
|
894
|
+
expect(row?.status).toBe('finalized')
|
|
895
|
+
expect(row?.feeMax).toBe('500000')
|
|
896
|
+
const feeAmount = BigInt(row!.feeAmount!)
|
|
897
|
+
expect(feeAmount).toBeGreaterThan(0n)
|
|
898
|
+
expect(feeAmount).toBeLessThanOrEqual(500_000n)
|
|
899
|
+
|
|
900
|
+
// Usage surfaces through the API exactly as enforced.
|
|
901
|
+
const usage = await app.request('/v1/orgs/org_1/billing', {
|
|
902
|
+
headers: { 'tempo-api-key': superAdminSecret },
|
|
903
|
+
})
|
|
904
|
+
expect(usage.status).toBe(200)
|
|
905
|
+
const body = (await usage.json()) as { spend: { amount: string } }
|
|
906
|
+
expect(body.spend.amount).toBe(core_Billing.fromBaseUnits(feeAmount))
|
|
907
|
+
|
|
908
|
+
// The reporter meters the spend; Stripe aggregates it onto the upcoming
|
|
909
|
+
// invoice (asynchronously, so poll).
|
|
910
|
+
expect(await core_Billing.createReporter({ db, stripe }).tick()).toStrictEqual({
|
|
911
|
+
failed: 0,
|
|
912
|
+
reported: 1,
|
|
913
|
+
skipped: 0,
|
|
914
|
+
})
|
|
915
|
+
const { priceId } = await core_Billing.ensureFixtures(stripe)
|
|
916
|
+
const [subscription] = (await stripe.subscriptions.list({ customer: customer.id, limit: 1, price: priceId })).data // prettier-ignore
|
|
917
|
+
expect(subscription).toBeDefined()
|
|
918
|
+
const deadline = Date.now() + 180_000
|
|
919
|
+
let quantity: number | null | undefined
|
|
920
|
+
while (Date.now() < deadline) {
|
|
921
|
+
const preview = await stripe.invoices.createPreview({
|
|
922
|
+
customer: customer.id,
|
|
923
|
+
subscription: subscription!.id,
|
|
924
|
+
})
|
|
925
|
+
quantity = preview.lines.data.find((l) => l.pricing?.price_details?.price === priceId)?.quantity // prettier-ignore
|
|
926
|
+
if (quantity === Number(feeAmount)) break
|
|
927
|
+
await new Promise((resolve) => setTimeout(resolve, 5_000))
|
|
928
|
+
}
|
|
929
|
+
expect(quantity).toBe(Number(feeAmount))
|
|
930
|
+
},
|
|
931
|
+
)
|
|
932
|
+
},
|
|
933
|
+
)
|