tapimo 0.2.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 +31 -3
- package/dist/ApiKey.d.ts +1 -0
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKey.js +5 -2
- package/dist/ApiKey.js.map +1 -1
- package/dist/ApiKeys.d.ts +28 -1
- package/dist/ApiKeys.d.ts.map +1 -1
- package/dist/ApiKeys.js +34 -1
- package/dist/ApiKeys.js.map +1 -1
- package/dist/App.d.ts +7167 -4507
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +8 -0
- package/dist/App.js.map +1 -1
- package/dist/Scope.d.ts +11 -2
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +14 -3
- package/dist/Scope.js.map +1 -1
- package/dist/admin/App.d.ts +128 -118
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +7 -0
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/scopes.d.ts +8 -0
- 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 +57 -57
- 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 +1024 -624
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +36 -0
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +45 -45
- package/dist/apps/data/routes/coingecko.d.ts +176 -176
- package/dist/apps/data/routes/exchanges.d.ts +455 -455
- package/dist/apps/data/routes/fee-amm.d.ts +101 -101
- 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/data/routes/receipts.d.ts.map +1 -1
- package/dist/apps/data/routes/receipts.js +2 -8
- package/dist/apps/data/routes/receipts.js.map +1 -1
- package/dist/apps/data/routes/rpc.d.ts +29 -36
- package/dist/apps/data/routes/rpc.d.ts.map +1 -1
- package/dist/apps/data/routes/rpc.js +29 -1
- package/dist/apps/data/routes/rpc.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts +36 -36
- package/dist/apps/data/routes/transfers.d.ts +56 -56
- package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
- package/dist/apps/data/routes/webhooks.d.ts +45 -45
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +8 -8
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/docs/App.d.ts +4 -2
- package/dist/apps/docs/App.d.ts.map +1 -1
- package/dist/apps/docs/App.js +27 -95
- package/dist/apps/docs/App.js.map +1 -1
- 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 +1 -0
- package/dist/apps/index.d.ts.map +1 -1
- package/dist/apps/index.js +1 -0
- package/dist/apps/index.js.map +1 -1
- 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/platform/App.d.ts +3005 -728
- package/dist/apps/platform/App.d.ts.map +1 -1
- package/dist/apps/platform/App.js +13 -3
- package/dist/apps/platform/App.js.map +1 -1
- 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 +1 -0
- package/dist/apps/platform/index.d.ts.map +1 -1
- package/dist/apps/platform/index.js +1 -0
- package/dist/apps/platform/index.js.map +1 -1
- package/dist/apps/platform/routes/api-keys.d.ts +341 -49
- package/dist/apps/platform/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/platform/routes/api-keys.js +115 -13
- package/dist/apps/platform/routes/api-keys.js.map +1 -1
- 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 +201 -83
- package/dist/apps/platform/routes/invitations.d.ts.map +1 -1
- package/dist/apps/platform/routes/invitations.js +58 -0
- package/dist/apps/platform/routes/invitations.js.map +1 -1
- package/dist/apps/platform/routes/me.d.ts +17 -17
- package/dist/apps/platform/routes/orgs.d.ts +50 -36
- package/dist/apps/platform/routes/orgs.d.ts.map +1 -1
- package/dist/apps/platform/routes/orgs.js +14 -3
- package/dist/apps/platform/routes/orgs.js.map +1 -1
- package/dist/apps/platform/routes/projects.d.ts +97 -97
- 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 +4 -1
- package/dist/apps/relay/App.d.ts.map +1 -1
- package/dist/apps/relay/App.js +118 -10
- package/dist/apps/relay/App.js.map +1 -1
- package/dist/apps/relay/Sponsorships.d.ts.map +1 -1
- package/dist/apps/relay/Sponsorships.js +2 -1
- package/dist/apps/relay/Sponsorships.js.map +1 -1
- package/dist/cli/Cli.d.ts +8 -1
- package/dist/cli/Cli.d.ts.map +1 -1
- package/dist/cli/Cli.js +11 -3
- package/dist/cli/Cli.js.map +1 -1
- package/dist/cli/commands/admin.d.ts +4 -0
- package/dist/cli/commands/admin.d.ts.map +1 -1
- package/dist/cli/commands/admin.js +80 -1
- package/dist/cli/commands/admin.js.map +1 -1
- package/dist/db/Db.d.ts +7 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +126 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +79 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +88 -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.js +2 -2
- package/dist/db/tables/invitations.js.map +1 -1
- package/dist/db/tables/organizations.d.ts +6 -2
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +10 -4
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/db/tables/projects.js +2 -2
- package/dist/db/tables/projects.js.map +1 -1
- 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 +119 -0
- package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
- package/dist/db/tables/sponsoredTransactions.js +155 -2
- package/dist/db/tables/sponsoredTransactions.js.map +1 -1
- 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.js +2 -2
- package/dist/db/tables/users.js.map +1 -1
- package/dist/handlers/internal/sponsorship.d.ts +5 -2
- package/dist/handlers/internal/sponsorship.d.ts.map +1 -1
- package/dist/handlers/internal/sponsorship.js +19 -5
- 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/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 +18 -4
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +148 -76
- 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 +2 -0
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +3 -0
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/OpenApi.d.ts +10 -1
- package/dist/internal/OpenApi.d.ts.map +1 -1
- package/dist/internal/OpenApi.js +8 -1
- 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/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +3 -3
- package/dist/internal/Webhooks.js.map +1 -1
- package/package.json +4 -3
- package/src/ApiKey.ts +9 -2
- package/src/ApiKeys.test.ts +51 -0
- package/src/ApiKeys.ts +50 -1
- package/src/App.test.ts +33 -86718
- package/src/App.ts +18 -0
- package/src/Client.test-d.ts +351 -0
- package/src/Scope.ts +16 -3
- package/src/admin/apps/scopes.test.ts +6 -0
- package/src/admin/apps/scopes.ts +3 -0
- 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/data/App.test.ts +72 -0
- package/src/apps/data/App.ts +35 -0
- package/src/apps/data/routes/funding.test.ts +214 -0
- package/src/apps/data/routes/funding.ts +584 -0
- package/src/apps/data/routes/receipts.ts +2 -9
- package/src/apps/data/routes/rpc.test.ts +78 -2
- package/src/apps/data/routes/rpc.ts +31 -1
- package/src/apps/data/routes/webhooks.test.ts +1 -1
- package/src/apps/data/routes/webhooks.ts +11 -8
- package/src/apps/docs/App.ts +29 -100
- package/src/apps/docs/reference.ts +132 -0
- package/src/apps/index.ts +1 -0
- package/src/apps/mcp.test.ts +206 -0
- package/src/apps/mcp.ts +250 -0
- package/src/apps/platform/App.ts +43 -4
- package/src/apps/platform/Billing.test.ts +530 -0
- package/src/apps/platform/Billing.ts +682 -0
- package/src/apps/platform/index.ts +1 -0
- package/src/apps/platform/routes/api-keys.test.ts +172 -9
- package/src/apps/platform/routes/api-keys.ts +136 -20
- 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 +55 -7
- package/src/apps/platform/routes/invitations.ts +68 -0
- package/src/apps/platform/routes/me.test.ts +230 -0
- package/src/apps/platform/routes/orgs.ts +26 -4
- 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 +457 -8
- package/src/apps/relay/App.ts +120 -12
- package/src/apps/relay/Sponsorships.test.ts +1 -1
- package/src/apps/relay/Sponsorships.ts +2 -1
- package/src/cli/Cli.test.ts +25 -2
- package/src/cli/Cli.ts +17 -2
- package/src/cli/commands/admin.test.ts +83 -0
- package/src/cli/commands/admin.ts +89 -1
- package/src/db/Db.ts +137 -0
- package/src/db/Schema.ts +104 -2
- package/src/db/tables/billingSettings.test.ts +87 -0
- package/src/db/tables/billingSettings.ts +108 -0
- package/src/db/tables/invitations.ts +2 -2
- package/src/db/tables/organizations.test.ts +1 -1
- package/src/db/tables/organizations.ts +15 -5
- package/src/db/tables/projects.ts +2 -2
- package/src/db/tables/requestUsage.ts +231 -0
- package/src/db/tables/sponsoredTransactions.test.ts +108 -1
- package/src/db/tables/sponsoredTransactions.ts +230 -2
- package/src/db/tables/stripeCustomers.test.ts +109 -0
- package/src/db/tables/stripeCustomers.ts +162 -0
- package/src/db/tables/users.ts +2 -2
- package/src/handlers/internal/sponsorship.ts +34 -7
- package/src/handlers/internal/utils.ts +19 -7
- package/src/index.ts +1 -0
- package/src/internal/Auth.test.ts +175 -25
- package/src/internal/Auth.ts +175 -86
- package/src/internal/Cache.ts +5 -1
- 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 +1 -0
- package/src/internal/Log.ts +5 -0
- package/src/internal/OpenApi.ts +12 -1
- 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/Webhooks.test.ts +3 -3
- package/src/internal/Webhooks.ts +3 -3
|
@@ -1,14 +1,24 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid'
|
|
1
2
|
import { Hash, Secp256k1 } from 'ox'
|
|
2
3
|
import { createClient, http } from 'viem'
|
|
3
4
|
import { prepareTransactionRequest, sendRawTransactionSync, signTransaction } from 'viem/actions'
|
|
4
5
|
import { Account as TempoAccount, Actions, Transaction, withRelay } from 'viem/tempo'
|
|
5
6
|
|
|
6
7
|
import * as TestApp from '../../../test/App.js'
|
|
8
|
+
import { startTempo } from '../../../test/containers.js'
|
|
7
9
|
import * as Relay from '../../../test/Relay.js'
|
|
8
10
|
import * as Runtime from '../../../test/runtime.js'
|
|
9
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'
|
|
10
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'
|
|
11
20
|
import * as RequestListener from '../../handlers/internal/requestListener.js'
|
|
21
|
+
import * as Viem from '../../internal/Viem.js'
|
|
12
22
|
import * as Sponsorships from './Sponsorships.js'
|
|
13
23
|
|
|
14
24
|
const runtime = Runtime.get()
|
|
@@ -16,6 +26,9 @@ const runtime = Runtime.get()
|
|
|
16
26
|
const feePayerAccount = Tempo.accounts[10]!
|
|
17
27
|
const senderAccount = Tempo.accounts[9]!
|
|
18
28
|
|
|
29
|
+
/** Super admin secret configured on the end-to-end billing app. */
|
|
30
|
+
const superAdminSecret = 'tempo:sk:a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1'
|
|
31
|
+
|
|
19
32
|
/** Keys covering the sponsorship policy matrix; all hold the base relay scope. */
|
|
20
33
|
const keys = [
|
|
21
34
|
{
|
|
@@ -42,6 +55,14 @@ const keys = [
|
|
|
42
55
|
scopes: ['*'],
|
|
43
56
|
token: 'secret_wildcard',
|
|
44
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
|
+
},
|
|
45
66
|
{
|
|
46
67
|
id: 'key_unscoped',
|
|
47
68
|
orgId: 'org_1',
|
|
@@ -91,11 +112,14 @@ describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship', () => {
|
|
|
91
112
|
"billable": false,
|
|
92
113
|
"chainId": "<chainId>",
|
|
93
114
|
"createdAt": "<timestamp>",
|
|
115
|
+
"currency": null,
|
|
94
116
|
"environment": "sandbox",
|
|
95
117
|
"feeAmount": null,
|
|
118
|
+
"feeMax": "1",
|
|
96
119
|
"feeToken": "0x20c0000000000000000000000000000000000000",
|
|
97
120
|
"finalizedAt": null,
|
|
98
121
|
"id": "<id>",
|
|
122
|
+
"meterReportedAt": null,
|
|
99
123
|
"orgId": "org_1",
|
|
100
124
|
"projectId": "prj_1",
|
|
101
125
|
"signPayload": "<hash>",
|
|
@@ -304,6 +328,292 @@ describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship', () => {
|
|
|
304
328
|
})
|
|
305
329
|
})
|
|
306
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
|
+
|
|
307
617
|
describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship (viem)', () => {
|
|
308
618
|
const alphaUsd = '0x20c0000000000000000000000000000000000001'
|
|
309
619
|
const feePayer = TempoAccount.fromSecp256k1(Secp256k1.randomPrivateKey())
|
|
@@ -383,7 +693,9 @@ describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship (viem)', ()
|
|
|
383
693
|
}
|
|
384
694
|
expect(finalized?.status).toBe('finalized')
|
|
385
695
|
expect(finalized?.transactionHash).toBe(receipt.transactionHash)
|
|
386
|
-
expect(finalized?.feeAmount).toBe(
|
|
696
|
+
expect(finalized?.feeAmount).toBe(
|
|
697
|
+
Fees.fromGas(receipt.gasUsed, receipt.effectiveGasPrice).toString(),
|
|
698
|
+
)
|
|
387
699
|
}, 60_000)
|
|
388
700
|
|
|
389
701
|
test('behavior: sign-only sends record and finalize from the real receipt', async () => {
|
|
@@ -417,7 +729,9 @@ describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship (viem)', ()
|
|
|
417
729
|
expect(result.finalized).toBeGreaterThanOrEqual(1)
|
|
418
730
|
const finalized = await SponsoredTransactions.get(db, row!.id)
|
|
419
731
|
expect(finalized?.status).toBe('finalized')
|
|
420
|
-
expect(finalized?.feeAmount).toBe(
|
|
732
|
+
expect(finalized?.feeAmount).toBe(
|
|
733
|
+
Fees.fromGas(receipt.gasUsed, receipt.effectiveGasPrice).toString(),
|
|
734
|
+
)
|
|
421
735
|
}, 30_000)
|
|
422
736
|
|
|
423
737
|
test('behavior: sign-and-broadcast sends record through the relay', async () => {
|
|
@@ -449,16 +763,25 @@ describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship (viem)', ()
|
|
|
449
763
|
* offline: `eth_signRawTransaction` never touches the chain, so the suite
|
|
450
764
|
* runs without a node.
|
|
451
765
|
*/
|
|
452
|
-
async function signSponsorable(
|
|
766
|
+
async function signSponsorable(
|
|
767
|
+
overrides: {
|
|
768
|
+
chainId?: number
|
|
769
|
+
feeToken?: `0x${string}`
|
|
770
|
+
gas?: bigint
|
|
771
|
+
maxFeePerGas?: bigint
|
|
772
|
+
nonce?: number
|
|
773
|
+
} = {},
|
|
774
|
+
) {
|
|
453
775
|
return await senderAccount.signTransaction({
|
|
454
|
-
chainId: runtime.chainId,
|
|
776
|
+
chainId: overrides.chainId ?? runtime.chainId,
|
|
455
777
|
feePayer: true,
|
|
456
|
-
gas: 100_000n,
|
|
457
|
-
maxFeePerGas: 1_000_000n,
|
|
778
|
+
gas: overrides.gas ?? 100_000n,
|
|
779
|
+
maxFeePerGas: overrides.maxFeePerGas ?? 1_000_000n,
|
|
458
780
|
maxPriorityFeePerGas: 0n,
|
|
459
|
-
nonce: 0,
|
|
781
|
+
nonce: overrides.nonce ?? 0,
|
|
460
782
|
to: Tempo.accounts[8]!.address,
|
|
461
783
|
value: 0n,
|
|
784
|
+
...(overrides.feeToken ? { feeToken: overrides.feeToken } : {}),
|
|
462
785
|
})
|
|
463
786
|
}
|
|
464
787
|
|
|
@@ -478,7 +801,133 @@ async function relayRequest(
|
|
|
478
801
|
}),
|
|
479
802
|
)
|
|
480
803
|
return (await response.json()) as {
|
|
481
|
-
error?: { code: number; message: string } | undefined
|
|
804
|
+
error?: { code: number; data?: { code: string } | undefined; message: string } | undefined
|
|
482
805
|
result?: `0x${string}` | undefined
|
|
483
806
|
}
|
|
484
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
|
+
)
|