tapimo 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/App.d.ts +6064 -4829
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +1 -0
- package/dist/App.js.map +1 -1
- package/dist/Webhooks.js +2 -2
- package/dist/Webhooks.js.map +1 -1
- package/dist/admin/App.d.ts +1008 -917
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/earn-vaults.d.ts +48 -0
- package/dist/admin/apps/earn-vaults.d.ts.map +1 -1
- package/dist/admin/apps/earn-vaults.js +2 -1
- package/dist/admin/apps/earn-vaults.js.map +1 -1
- package/dist/admin/apps/organizations.d.ts +92 -3
- package/dist/admin/apps/organizations.d.ts.map +1 -1
- package/dist/admin/apps/organizations.js +69 -7
- package/dist/admin/apps/organizations.js.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +74 -74
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.js +69 -69
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/apps/Zones.d.ts +12 -12
- package/dist/apps/Zones.js +3 -3
- package/dist/apps/data/App.d.ts +2909 -2523
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +11 -6
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/routes/addresses.d.ts +22 -22
- package/dist/apps/data/routes/earn.d.ts +183 -7
- package/dist/apps/data/routes/earn.d.ts.map +1 -1
- package/dist/apps/data/routes/earn.js +434 -75
- package/dist/apps/data/routes/earn.js.map +1 -1
- package/dist/apps/data/routes/exchanges.d.ts +15 -0
- package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
- package/dist/apps/data/routes/exchanges.js +180 -114
- package/dist/apps/data/routes/exchanges.js.map +1 -1
- package/dist/apps/data/routes/fee-amm.d.ts.map +1 -1
- package/dist/apps/data/routes/fee-amm.js +18 -19
- package/dist/apps/data/routes/fee-amm.js.map +1 -1
- package/dist/apps/data/routes/tokens.d.ts +93 -93
- package/dist/apps/data/routes/transfers.d.ts +12 -12
- package/dist/apps/data/routes/valuation.d.ts +30 -30
- package/dist/apps/data/routes/verified-tokens.d.ts +5 -5
- package/dist/apps/data/routes/webhooks.d.ts +805 -102
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +47 -5
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/funding/App.d.ts +456 -45
- package/dist/apps/funding/App.d.ts.map +1 -1
- package/dist/apps/funding/App.js +1 -1
- package/dist/apps/funding/App.js.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.d.ts +17 -17
- package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.js +1 -1
- package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
- package/dist/apps/funding/routes/transfers.d.ts +654 -73
- package/dist/apps/funding/routes/transfers.d.ts.map +1 -1
- package/dist/apps/funding/routes/transfers.js +211 -5
- package/dist/apps/funding/routes/transfers.js.map +1 -1
- package/dist/apps/management/App.d.ts +831 -387
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/Billing.d.ts +1 -1
- package/dist/apps/management/Billing.d.ts.map +1 -1
- package/dist/apps/management/Billing.js +17 -1
- package/dist/apps/management/Billing.js.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +76 -76
- package/dist/apps/management/routes/billing.d.ts +76 -76
- package/dist/apps/management/routes/faucet.d.ts +12 -12
- package/dist/apps/management/routes/invitations.d.ts +24 -24
- package/dist/apps/management/routes/invite-links.d.ts +46 -46
- package/dist/apps/management/routes/members.d.ts +46 -46
- package/dist/apps/management/routes/orgs.d.ts +20 -20
- package/dist/apps/management/routes/projects.d.ts +44 -44
- package/dist/apps/management/routes/usage.d.ts +32 -32
- package/dist/apps/management/routes/verified-token-requests.d.ts +16 -16
- package/dist/apps/management/routes/webhooks.d.ts +707 -57
- package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/management/routes/webhooks.js +21 -21
- package/dist/apps/management/routes/webhooks.js.map +1 -1
- package/dist/apps/relay/App.d.ts.map +1 -1
- package/dist/apps/relay/App.js +115 -23
- 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 +27 -1
- package/dist/apps/relay/Sponsorships.js.map +1 -1
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +22 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +8 -3
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +16 -1
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/fundingTransferTransactions.d.ts +38 -0
- package/dist/db/tables/fundingTransferTransactions.d.ts.map +1 -1
- package/dist/db/tables/fundingTransferTransactions.js +44 -3
- package/dist/db/tables/fundingTransferTransactions.js.map +1 -1
- package/dist/db/tables/fundingTransfers.d.ts +17 -0
- package/dist/db/tables/fundingTransfers.d.ts.map +1 -1
- package/dist/db/tables/fundingTransfers.js +21 -0
- package/dist/db/tables/fundingTransfers.js.map +1 -1
- package/dist/db/tables/organizations.d.ts +24 -0
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +27 -0
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/db/tables/projects.d.ts +25 -1
- package/dist/db/tables/projects.d.ts.map +1 -1
- package/dist/db/tables/projects.js +26 -0
- package/dist/db/tables/projects.js.map +1 -1
- package/dist/db/tables/sponsoredTransactions.d.ts +44 -0
- package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
- package/dist/db/tables/sponsoredTransactions.js +146 -13
- package/dist/db/tables/sponsoredTransactions.js.map +1 -1
- package/dist/db/tables/webhookSubscriptions.d.ts +10 -6
- package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
- package/dist/db/tables/webhookSubscriptions.js +9 -6
- package/dist/db/tables/webhookSubscriptions.js.map +1 -1
- package/dist/internal/EarnVaults.js +5 -5
- package/dist/internal/EarnVaults.js.map +1 -1
- package/dist/internal/Log.d.ts +1 -1
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/Viem.d.ts +16 -1
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Viem.js +12 -2
- package/dist/internal/Viem.js.map +1 -1
- package/dist/internal/funding/DepositWebhook.d.ts.map +1 -1
- package/dist/internal/funding/DepositWebhook.js +2 -1
- package/dist/internal/funding/DepositWebhook.js.map +1 -1
- package/dist/internal/funding/Evidence.d.ts.map +1 -1
- package/dist/internal/funding/Evidence.js +2 -10
- package/dist/internal/funding/Evidence.js.map +1 -1
- package/dist/internal/funding/Provider.d.ts +40 -0
- package/dist/internal/funding/Provider.d.ts.map +1 -1
- package/dist/internal/funding/Provider.js +5 -0
- package/dist/internal/funding/Provider.js.map +1 -1
- package/dist/internal/funding/SourceObservation.d.ts.map +1 -1
- package/dist/internal/funding/SourceObservation.js +5 -7
- package/dist/internal/funding/SourceObservation.js.map +1 -1
- package/dist/internal/funding/Transfer.d.ts +101 -6
- package/dist/internal/funding/Transfer.d.ts.map +1 -1
- package/dist/internal/funding/Transfer.js +162 -34
- package/dist/internal/funding/Transfer.js.map +1 -1
- package/dist/internal/funding/TransferReconciliation.d.ts +52 -0
- package/dist/internal/funding/TransferReconciliation.d.ts.map +1 -0
- package/dist/internal/funding/TransferReconciliation.js +192 -0
- package/dist/internal/funding/TransferReconciliation.js.map +1 -0
- package/dist/internal/funding/TransferWebhook.d.ts +21 -0
- package/dist/internal/funding/TransferWebhook.d.ts.map +1 -0
- package/dist/internal/funding/TransferWebhook.js +43 -0
- package/dist/internal/funding/TransferWebhook.js.map +1 -0
- package/dist/internal/funding/index.d.ts +1 -0
- package/dist/internal/funding/index.d.ts.map +1 -1
- package/dist/internal/funding/index.js +1 -0
- package/dist/internal/funding/index.js.map +1 -1
- package/dist/internal/funding/providers/relay.d.ts.map +1 -1
- package/dist/internal/funding/providers/relay.js +3 -12
- package/dist/internal/funding/providers/relay.js.map +1 -1
- package/dist/internal/funding/providers/stargate.d.ts +99 -0
- package/dist/internal/funding/providers/stargate.d.ts.map +1 -1
- package/dist/internal/funding/providers/stargate.js +337 -35
- package/dist/internal/funding/providers/stargate.js.map +1 -1
- package/dist/internal/webhookDestinations/slack.d.ts.map +1 -1
- package/dist/internal/webhookDestinations/slack.js +16 -0
- package/dist/internal/webhookDestinations/slack.js.map +1 -1
- package/package.json +1 -1
- package/src/App.ts +1 -0
- package/src/Client.test-d.ts +83 -2
- package/src/Webhooks.ts +2 -2
- package/src/admin/apps/earn-vaults.ts +2 -1
- package/src/admin/apps/organizations.test.ts +53 -1
- package/src/admin/apps/organizations.ts +90 -5
- package/src/admin/ui/src/lib/organizations.test.ts +112 -1
- package/src/admin/ui/src/lib/organizations.ts +55 -0
- package/src/admin/ui/src/routes/organizations_.$orgId.tsx +116 -0
- package/src/admin/ui.generated.ts +69 -69
- package/src/apps/Zones.test.ts +3 -3
- package/src/apps/Zones.ts +3 -3
- package/src/apps/composition.test.ts +1 -0
- package/src/apps/data/App.ts +15 -6
- package/src/apps/data/routes/earn.test.ts +369 -14
- package/src/apps/data/routes/earn.ts +548 -79
- package/src/apps/data/routes/exchanges.test.ts +740 -15
- package/src/apps/data/routes/exchanges.ts +954 -753
- package/src/apps/data/routes/fee-amm.ts +17 -18
- package/src/apps/data/routes/webhooks.test.ts +21 -5
- package/src/apps/data/routes/webhooks.ts +63 -8
- package/src/apps/funding/App.ts +12 -2
- package/src/apps/funding/routes/deposit-addresses.test.ts +4 -4
- package/src/apps/funding/routes/deposit-addresses.ts +2 -2
- package/src/apps/funding/routes/transfers.test.ts +510 -19
- package/src/apps/funding/routes/transfers.ts +271 -5
- package/src/apps/management/Billing.ts +18 -5
- package/src/apps/management/routes/webhooks.test.ts +3 -3
- package/src/apps/management/routes/webhooks.ts +24 -25
- package/src/apps/relay/App.test.ts +212 -1
- package/src/apps/relay/App.ts +158 -29
- package/src/apps/relay/Sponsorships.ts +24 -1
- package/src/db/Db.ts +35 -0
- package/src/db/Schema.ts +17 -2
- package/src/db/tables/fundingTransferTransactions.test.ts +43 -0
- package/src/db/tables/fundingTransferTransactions.ts +79 -3
- package/src/db/tables/fundingTransfers.test.ts +26 -0
- package/src/db/tables/fundingTransfers.ts +36 -0
- package/src/db/tables/organizations.test.ts +41 -0
- package/src/db/tables/organizations.ts +49 -0
- package/src/db/tables/projects.test.ts +55 -0
- package/src/db/tables/projects.ts +46 -1
- package/src/db/tables/sponsoredTransactions.test.ts +338 -0
- package/src/db/tables/sponsoredTransactions.ts +197 -13
- package/src/db/tables/webhookSubscriptions.ts +18 -11
- package/src/internal/EarnVaults.ts +5 -5
- package/src/internal/Log.test-d.ts +2 -0
- package/src/internal/Log.ts +2 -0
- package/src/internal/Viem.ts +39 -3
- package/src/internal/Webhooks.test.ts +45 -0
- package/src/internal/funding/DepositWebhook.ts +2 -1
- package/src/internal/funding/Evidence.ts +2 -13
- package/src/internal/funding/Provider.ts +43 -0
- package/src/internal/funding/SourceObservation.ts +5 -9
- package/src/internal/funding/Transfer.test.ts +155 -0
- package/src/internal/funding/Transfer.ts +254 -37
- package/src/internal/funding/TransferReconciliation.test.ts +147 -0
- package/src/internal/funding/TransferReconciliation.ts +264 -0
- package/src/internal/funding/TransferWebhook.test.ts +128 -0
- package/src/internal/funding/TransferWebhook.ts +67 -0
- package/src/internal/funding/index.test.ts +1 -0
- package/src/internal/funding/index.ts +1 -0
- package/src/internal/funding/providers/relay.ts +3 -15
- package/src/internal/funding/providers/stargate.test.ts +169 -4
- package/src/internal/funding/providers/stargate.ts +498 -51
- package/src/internal/webhookDestinations/slack.ts +15 -0
|
@@ -18,6 +18,7 @@ import type * as Db from '../../db/Db.js'
|
|
|
18
18
|
import * as Fees from '../../internal/Fees.js'
|
|
19
19
|
import type * as Log from '../../internal/Log.js'
|
|
20
20
|
import * as Organizations from '../../db/tables/organizations.js'
|
|
21
|
+
import * as Projects from '../../db/tables/projects.js'
|
|
21
22
|
import * as SponsoredTransactions from '../../db/tables/sponsoredTransactions.js'
|
|
22
23
|
import * as StripeCustomers from '../../db/tables/stripeCustomers.js'
|
|
23
24
|
import * as core_Billing from '../management/Billing.js'
|
|
@@ -104,6 +105,13 @@ const keys = [
|
|
|
104
105
|
scopes: ['rpc-relay:sponsor'],
|
|
105
106
|
token: 'secret_unattributed',
|
|
106
107
|
},
|
|
108
|
+
{
|
|
109
|
+
id: 'key_unattributed_prod',
|
|
110
|
+
orgId: 'org_1',
|
|
111
|
+
environment: 'production',
|
|
112
|
+
scopes: ['rpc-relay:sponsor'],
|
|
113
|
+
token: 'secret_unattributed_prod',
|
|
114
|
+
},
|
|
107
115
|
] as const satisfies readonly TestApp.kvStore.Key[]
|
|
108
116
|
|
|
109
117
|
describe('behavior: Zone chain routing', () => {
|
|
@@ -525,7 +533,7 @@ describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship', () => {
|
|
|
525
533
|
const body = await relayRequest(app, {
|
|
526
534
|
method: 'eth_signRawTransaction',
|
|
527
535
|
params: [await signSponsorable()],
|
|
528
|
-
token: '
|
|
536
|
+
token: 'secret_unattributed_prod',
|
|
529
537
|
})
|
|
530
538
|
expect(body.error).toBeUndefined()
|
|
531
539
|
expect(Transaction.deserialize(body.result as `0x76${string}`).feePayerSignature).toBeDefined()
|
|
@@ -535,6 +543,28 @@ describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship', () => {
|
|
|
535
543
|
expect(row?.projectId).toBeNull()
|
|
536
544
|
})
|
|
537
545
|
|
|
546
|
+
test('behavior: organization-attributed keys use a validated request project', async () => {
|
|
547
|
+
const db = TestApp.database()
|
|
548
|
+
await Organizations.create(db, { id: 'org_1', name: 'One' })
|
|
549
|
+
const project = await Projects.create(db, { name: 'Privy app', orgId: 'org_1' })
|
|
550
|
+
const app = TestApp.create({
|
|
551
|
+
auth: { keys },
|
|
552
|
+
db,
|
|
553
|
+
relay: { feePayer: { account: feePayerAccount } },
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
const body = await relayRequest(app, {
|
|
557
|
+
method: 'eth_signRawTransaction',
|
|
558
|
+
params: [await signSponsorable()],
|
|
559
|
+
projectId: project.id,
|
|
560
|
+
token: 'secret_unattributed_prod',
|
|
561
|
+
})
|
|
562
|
+
expect(body.error).toBeUndefined()
|
|
563
|
+
|
|
564
|
+
const [row] = await SponsoredTransactions.listPending(db)
|
|
565
|
+
expect(row?.projectId).toBe(project.id)
|
|
566
|
+
})
|
|
567
|
+
|
|
538
568
|
test('behavior: batches aggregate sponsorship rejections without retaining payloads', async () => {
|
|
539
569
|
const db = TestApp.database()
|
|
540
570
|
const entries: Log.Entry[] = []
|
|
@@ -948,6 +978,18 @@ describe('behavior: mainnet billing gate', () => {
|
|
|
948
978
|
await TestApp.verifiedSeed(db, Viem.chainId.mainnet)
|
|
949
979
|
}
|
|
950
980
|
|
|
981
|
+
/** Configures one attributed project for Tempo-funded sponsorship. */
|
|
982
|
+
async function configureSubsidy(db: ReturnType<typeof TestApp.database>) {
|
|
983
|
+
await Organizations.create(db, { id: 'org_1', name: 'Privy' })
|
|
984
|
+
await Organizations.setSponsorshipSubsidy(db, 'org_1', {
|
|
985
|
+
durationDays: 90,
|
|
986
|
+
projectSpendLimit: '100.00',
|
|
987
|
+
})
|
|
988
|
+
const project = await Projects.create(db, { name: 'Privy app', orgId: 'org_1' })
|
|
989
|
+
await activate(db)
|
|
990
|
+
return project
|
|
991
|
+
}
|
|
992
|
+
|
|
951
993
|
// Gas prices are attodollars (1e-18 USD) per gas; the signed cap in
|
|
952
994
|
// fee-token units is ceil(gas × maxFeePerGas / 1e12). The helper's default
|
|
953
995
|
// envelope (100k gas × 1e6 atto) caps at 1 microdollar — negligible.
|
|
@@ -956,6 +998,172 @@ describe('behavior: mainnet billing gate', () => {
|
|
|
956
998
|
/** A $2 signed cap, over the `'1.00'` platform default. */
|
|
957
999
|
const overCapFees = { gas: 1_000_000n, maxFeePerGas: 2_000_000_000_000n } as const
|
|
958
1000
|
|
|
1001
|
+
test('behavior: active project promotions waive charges with active billing', async () => {
|
|
1002
|
+
const db = TestApp.database()
|
|
1003
|
+
const project = await configureSubsidy(db)
|
|
1004
|
+
const body = await relayRequest(createMainnetApp({ db }), {
|
|
1005
|
+
method: 'eth_signRawTransaction',
|
|
1006
|
+
params: [await signSponsorable({ chainId: Viem.chainId.mainnet })],
|
|
1007
|
+
projectId: project.id,
|
|
1008
|
+
token: 'secret_unattributed_prod',
|
|
1009
|
+
})
|
|
1010
|
+
expect(body.error).toBeUndefined()
|
|
1011
|
+
|
|
1012
|
+
const [row] = await SponsoredTransactions.listPending(db)
|
|
1013
|
+
expect(row).toMatchObject({
|
|
1014
|
+
billable: false,
|
|
1015
|
+
orgId: 'org_1',
|
|
1016
|
+
projectId: project.id,
|
|
1017
|
+
})
|
|
1018
|
+
const updated = await Projects.get(db, project.id)
|
|
1019
|
+
const startsAt = updated?.sponsorshipSubsidyStartsAt
|
|
1020
|
+
const endsAt = updated?.sponsorshipSubsidyEndsAt
|
|
1021
|
+
expect({ endsAt, startsAt }).toMatchObject({ endsAt: expect.any(String), startsAt: expect.any(String) }) // prettier-ignore
|
|
1022
|
+
if (!endsAt || !startsAt) throw new Error('Expected an activated promotion window.')
|
|
1023
|
+
expect(Date.parse(endsAt) - Date.parse(startsAt)).toBe(90 * 86_400_000)
|
|
1024
|
+
})
|
|
1025
|
+
|
|
1026
|
+
test('behavior: exhausted project promotions return to customer billing', async () => {
|
|
1027
|
+
const db = TestApp.database()
|
|
1028
|
+
await Organizations.create(db, { id: 'org_1', name: 'Privy' })
|
|
1029
|
+
await Organizations.setSponsorshipSubsidy(db, 'org_1', {
|
|
1030
|
+
durationDays: 90,
|
|
1031
|
+
projectSpendLimit: '0.000001',
|
|
1032
|
+
})
|
|
1033
|
+
const project = await Projects.create(db, { name: 'Privy app', orgId: 'org_1' })
|
|
1034
|
+
await activate(db)
|
|
1035
|
+
const app = createMainnetApp({ db })
|
|
1036
|
+
|
|
1037
|
+
const subsidized = await relayRequest(app, {
|
|
1038
|
+
method: 'eth_signRawTransaction',
|
|
1039
|
+
params: [await signSponsorable({ chainId: Viem.chainId.mainnet })],
|
|
1040
|
+
projectId: project.id,
|
|
1041
|
+
token: 'secret_unattributed_prod',
|
|
1042
|
+
})
|
|
1043
|
+
const billed = await relayRequest(app, {
|
|
1044
|
+
method: 'eth_signRawTransaction',
|
|
1045
|
+
params: [await signSponsorable({ chainId: Viem.chainId.mainnet, nonce: 1 })],
|
|
1046
|
+
projectId: project.id,
|
|
1047
|
+
token: 'secret_unattributed_prod',
|
|
1048
|
+
})
|
|
1049
|
+
|
|
1050
|
+
expect({ billed: billed.error, subsidized: subsidized.error }).toStrictEqual({
|
|
1051
|
+
billed: undefined,
|
|
1052
|
+
subsidized: undefined,
|
|
1053
|
+
})
|
|
1054
|
+
expect(await SponsoredTransactions.listPending(db)).toMatchObject([
|
|
1055
|
+
{ billable: false, projectId: project.id },
|
|
1056
|
+
{ billable: true, projectId: project.id },
|
|
1057
|
+
])
|
|
1058
|
+
})
|
|
1059
|
+
|
|
1060
|
+
test('behavior: uncapped project promotions waive charges with active billing', async () => {
|
|
1061
|
+
const db = TestApp.database()
|
|
1062
|
+
await Organizations.create(db, { id: 'org_1', name: 'Privy' })
|
|
1063
|
+
await Organizations.setSponsorshipSubsidy(db, 'org_1', {
|
|
1064
|
+
durationDays: 90,
|
|
1065
|
+
projectSpendLimit: null,
|
|
1066
|
+
})
|
|
1067
|
+
const project = await Projects.create(db, { name: 'Privy app', orgId: 'org_1' })
|
|
1068
|
+
await activate(db)
|
|
1069
|
+
const body = await relayRequest(createMainnetApp({ db }), {
|
|
1070
|
+
method: 'eth_signRawTransaction',
|
|
1071
|
+
params: [await signSponsorable({ chainId: Viem.chainId.mainnet })],
|
|
1072
|
+
projectId: project.id,
|
|
1073
|
+
token: 'secret_unattributed_prod',
|
|
1074
|
+
})
|
|
1075
|
+
expect(body.error).toBeUndefined()
|
|
1076
|
+
expect(await SponsoredTransactions.listPending(db)).toMatchObject([
|
|
1077
|
+
{ billable: false, projectId: project.id },
|
|
1078
|
+
])
|
|
1079
|
+
})
|
|
1080
|
+
|
|
1081
|
+
test('behavior: project promotions still require active billing', async () => {
|
|
1082
|
+
const db = TestApp.database()
|
|
1083
|
+
await Organizations.create(db, { id: 'org_1', name: 'Privy' })
|
|
1084
|
+
await Organizations.setSponsorshipSubsidy(db, 'org_1', {
|
|
1085
|
+
durationDays: 90,
|
|
1086
|
+
projectSpendLimit: null,
|
|
1087
|
+
})
|
|
1088
|
+
const project = await Projects.create(db, { name: 'Privy app', orgId: 'org_1' })
|
|
1089
|
+
await TestApp.verifiedSeed(db, Viem.chainId.mainnet)
|
|
1090
|
+
const body = await relayRequest(createMainnetApp({ db }), {
|
|
1091
|
+
method: 'eth_signRawTransaction',
|
|
1092
|
+
params: [await signSponsorable({ chainId: Viem.chainId.mainnet })],
|
|
1093
|
+
projectId: project.id,
|
|
1094
|
+
token: 'secret_unattributed_prod',
|
|
1095
|
+
})
|
|
1096
|
+
expect(body.error?.data).toStrictEqual({ code: 'billing_required' })
|
|
1097
|
+
expect(await SponsoredTransactions.listPending(db)).toHaveLength(0)
|
|
1098
|
+
expect(await Projects.get(db, project.id)).toMatchObject({
|
|
1099
|
+
sponsorshipSubsidyEndsAt: null,
|
|
1100
|
+
sponsorshipSubsidyStartsAt: null,
|
|
1101
|
+
})
|
|
1102
|
+
})
|
|
1103
|
+
|
|
1104
|
+
test('behavior: subsidized organization keys without project attribution use customer billing', async () => {
|
|
1105
|
+
const db = TestApp.database()
|
|
1106
|
+
await configureSubsidy(db)
|
|
1107
|
+
const body = await relayRequest(createMainnetApp({ db }), {
|
|
1108
|
+
method: 'eth_signRawTransaction',
|
|
1109
|
+
params: [await signSponsorable({ chainId: Viem.chainId.mainnet })],
|
|
1110
|
+
token: 'secret_unattributed_prod',
|
|
1111
|
+
})
|
|
1112
|
+
expect(body.error).toBeUndefined()
|
|
1113
|
+
expect(await SponsoredTransactions.listPending(db)).toMatchObject([
|
|
1114
|
+
{ billable: true, orgId: 'org_1', projectId: null },
|
|
1115
|
+
])
|
|
1116
|
+
})
|
|
1117
|
+
|
|
1118
|
+
test('behavior: expired project promotions return to customer billing', async () => {
|
|
1119
|
+
const db = TestApp.database()
|
|
1120
|
+
const project = await configureSubsidy(db)
|
|
1121
|
+
await Projects.activateSponsorship(db, {
|
|
1122
|
+
at: new Date(Date.now() - 91 * 86_400_000).toISOString(),
|
|
1123
|
+
endsAt: new Date(Date.now() - 86_400_000).toISOString(),
|
|
1124
|
+
id: project.id,
|
|
1125
|
+
orgId: 'org_1',
|
|
1126
|
+
spendLimit: '100000000',
|
|
1127
|
+
})
|
|
1128
|
+
const body = await relayRequest(createMainnetApp({ db }), {
|
|
1129
|
+
method: 'eth_signRawTransaction',
|
|
1130
|
+
params: [await signSponsorable({ chainId: Viem.chainId.mainnet })],
|
|
1131
|
+
projectId: project.id,
|
|
1132
|
+
token: 'secret_unattributed_prod',
|
|
1133
|
+
})
|
|
1134
|
+
expect(body.error).toBeUndefined()
|
|
1135
|
+
expect(await SponsoredTransactions.listPending(db)).toMatchObject([
|
|
1136
|
+
{ billable: true, projectId: project.id },
|
|
1137
|
+
])
|
|
1138
|
+
})
|
|
1139
|
+
|
|
1140
|
+
test('behavior: rejects request attribution from project-scoped keys', async () => {
|
|
1141
|
+
const body = await relayRequest(createMainnetApp(), {
|
|
1142
|
+
method: 'eth_signRawTransaction',
|
|
1143
|
+
params: [await signSponsorable({ chainId: Viem.chainId.mainnet })],
|
|
1144
|
+
projectId: 'prj_1',
|
|
1145
|
+
token: 'secret_sponsor_prod',
|
|
1146
|
+
})
|
|
1147
|
+
expect(body.error).toStrictEqual({
|
|
1148
|
+
code: -32602,
|
|
1149
|
+
data: { code: 'project_id_invalid' },
|
|
1150
|
+
message: 'Project attribution rejected.',
|
|
1151
|
+
})
|
|
1152
|
+
})
|
|
1153
|
+
|
|
1154
|
+
test('behavior: rejects request attribution outside the key organization', async () => {
|
|
1155
|
+
const db = TestApp.database()
|
|
1156
|
+
await Organizations.create(db, { id: 'org_2', name: 'Two' })
|
|
1157
|
+
const project = await Projects.create(db, { name: 'Other app', orgId: 'org_2' })
|
|
1158
|
+
const body = await relayRequest(createMainnetApp({ db }), {
|
|
1159
|
+
method: 'eth_signRawTransaction',
|
|
1160
|
+
params: [await signSponsorable({ chainId: Viem.chainId.mainnet })],
|
|
1161
|
+
projectId: project.id,
|
|
1162
|
+
token: 'secret_unattributed_prod',
|
|
1163
|
+
})
|
|
1164
|
+
expect(body.error?.data).toStrictEqual({ code: 'project_id_invalid' })
|
|
1165
|
+
})
|
|
1166
|
+
|
|
959
1167
|
test('behavior: refuses mainnet sponsorship without a billing source', async () => {
|
|
960
1168
|
const entries: Log.Entry[] = []
|
|
961
1169
|
const body = await relayRequest(
|
|
@@ -1584,6 +1792,7 @@ async function relayRequest(app: ReturnType<typeof TestApp.create>, options: rel
|
|
|
1584
1792
|
body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: options.method, params: options.params }), // prettier-ignore
|
|
1585
1793
|
headers: {
|
|
1586
1794
|
'content-type': 'application/json',
|
|
1795
|
+
...(options.projectId ? { 'tempo-project-id': options.projectId } : {}),
|
|
1587
1796
|
...(options.token ? { 'tempo-api-key': options.token } : {}),
|
|
1588
1797
|
},
|
|
1589
1798
|
method: 'POST',
|
|
@@ -1604,6 +1813,8 @@ declare namespace relayRequest {
|
|
|
1604
1813
|
mount?: 'relay' | 'sponsor' | undefined
|
|
1605
1814
|
/** JSON-RPC parameters. */
|
|
1606
1815
|
params: readonly unknown[]
|
|
1816
|
+
/** Request-level project attribution. */
|
|
1817
|
+
projectId?: string | undefined
|
|
1607
1818
|
/** API-key credential. */
|
|
1608
1819
|
token?: string | undefined
|
|
1609
1820
|
}
|
package/src/apps/relay/App.ts
CHANGED
|
@@ -7,9 +7,11 @@ import { Addresses, TokenId, Transaction } from 'viem/tempo'
|
|
|
7
7
|
import type * as App from '../../App.js'
|
|
8
8
|
import * as BillingSettings from '../../db/tables/billingSettings.js'
|
|
9
9
|
import * as Db from '../../db/Db.js'
|
|
10
|
+
import * as Organizations from '../../db/tables/organizations.js'
|
|
11
|
+
import * as Projects from '../../db/tables/projects.js'
|
|
12
|
+
import * as SponsoredTransactions from '../../db/tables/sponsoredTransactions.js'
|
|
10
13
|
import * as Fees from '../../internal/Fees.js'
|
|
11
14
|
import * as Log from '../../internal/Log.js'
|
|
12
|
-
import * as SponsoredTransactions from '../../db/tables/sponsoredTransactions.js'
|
|
13
15
|
import * as Billing from '../management/Billing.js'
|
|
14
16
|
import * as Handler from '../../Handler.js'
|
|
15
17
|
import * as Auth from '../../internal/Auth.js'
|
|
@@ -85,6 +87,7 @@ function middleware(options: middleware.Options) {
|
|
|
85
87
|
: 0n
|
|
86
88
|
return createMiddleware<App.Environment>(async (c) => {
|
|
87
89
|
const principal = Auth.getPrincipal(c)
|
|
90
|
+
const requestedAt = new Date().toISOString()
|
|
88
91
|
const verifiedTokens = c.get('verifiedTokens')
|
|
89
92
|
const zoneToken_header = c.req.header(ZoneRpcAuthentication.headerName)
|
|
90
93
|
const zoneToken = zoneToken_header?.trim() ? zoneToken_header : undefined
|
|
@@ -97,7 +100,11 @@ function middleware(options: middleware.Options) {
|
|
|
97
100
|
|
|
98
101
|
const recordRejection = (options: middleware.Rejection) => {
|
|
99
102
|
const id =
|
|
100
|
-
|
|
103
|
+
options.chainId !== undefined &&
|
|
104
|
+
Number.isSafeInteger(options.chainId) &&
|
|
105
|
+
options.chainId >= 0
|
|
106
|
+
? options.chainId
|
|
107
|
+
: null
|
|
101
108
|
if (chainId !== null) chainId = chainId === undefined || chainId === id ? id : null
|
|
102
109
|
if (options.reason === 'internal_error') internalErrors = Math.min(internalErrors + 1, 65_535)
|
|
103
110
|
if (reason !== null)
|
|
@@ -121,22 +128,49 @@ function middleware(options: middleware.Options) {
|
|
|
121
128
|
throw new Error('Internal error', { cause: new Error(options.message) })
|
|
122
129
|
}
|
|
123
130
|
|
|
124
|
-
const feePayer = (() => {
|
|
131
|
+
const feePayer = await (async () => {
|
|
125
132
|
if (!options.feePayer) return undefined
|
|
126
|
-
const
|
|
127
|
-
principal
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
...(principal.projectId === undefined ? {} : { projectId: principal.projectId }),
|
|
133
|
-
}
|
|
134
|
-
: undefined
|
|
133
|
+
const resolved = await resolveAttribution(Db.get(c.get('db')), {
|
|
134
|
+
principal,
|
|
135
|
+
projectId: c.req.header('tempo-project-id')?.trim(),
|
|
136
|
+
})
|
|
137
|
+
const attribution = resolved?.attribution
|
|
138
|
+
const attributionError = resolved?.error
|
|
135
139
|
const sponsorable =
|
|
136
140
|
attribution !== undefined &&
|
|
137
141
|
principal?.type === 'api_key' &&
|
|
138
142
|
(principal.apiKey.scopes.includes('rpc-relay:sponsor') ||
|
|
139
143
|
principal.apiKey.scopes.includes(Scope.wildcard))
|
|
144
|
+
let promotionState_promise: Promise<middleware.PromotionState>
|
|
145
|
+
const promotionState = () =>
|
|
146
|
+
(promotionState_promise ??= (async () => {
|
|
147
|
+
if (!attribution) return { status: 'inactive' }
|
|
148
|
+
const db = Db.get(c.get('db'))
|
|
149
|
+
const organization = await Organizations.get(db, attribution.orgId)
|
|
150
|
+
if (!organization || organization.sponsorshipSubsidyDurationDays === null)
|
|
151
|
+
return { status: 'inactive' }
|
|
152
|
+
// Missing project attribution opts out of the subsidy but must not interrupt sponsorship.
|
|
153
|
+
if (!attribution.projectId) return { status: 'inactive' }
|
|
154
|
+
const project = await Projects.get(db, attribution.projectId)
|
|
155
|
+
if (!project) return { message: 'Attributed project disappeared.', status: 'error' }
|
|
156
|
+
if (project.orgId !== attribution.orgId)
|
|
157
|
+
return { message: 'Attributed project changed ownership.', status: 'error' }
|
|
158
|
+
if (
|
|
159
|
+
(project.sponsorshipSubsidyStartsAt === null) !==
|
|
160
|
+
(project.sponsorshipSubsidyEndsAt === null)
|
|
161
|
+
)
|
|
162
|
+
return { message: 'Project promotion window is inconsistent.', status: 'error' }
|
|
163
|
+
if (
|
|
164
|
+
project.sponsorshipSubsidyEndsAt !== null &&
|
|
165
|
+
Date.parse(requestedAt) >= Date.parse(project.sponsorshipSubsidyEndsAt)
|
|
166
|
+
)
|
|
167
|
+
return { status: 'inactive' }
|
|
168
|
+
return { status: 'active' }
|
|
169
|
+
})())
|
|
170
|
+
const validatePromotion = async (chainId: number) => {
|
|
171
|
+
const state = await promotionState()
|
|
172
|
+
if (state.status === 'error') failSponsorship({ chainId, message: state.message })
|
|
173
|
+
}
|
|
140
174
|
// Lifted so validation and the sponsored-token default read one value.
|
|
141
175
|
const feeToken_sponsor = options.feePayer.feeToken ?? Addresses.pathUsd
|
|
142
176
|
return {
|
|
@@ -147,6 +181,12 @@ function middleware(options: middleware.Options) {
|
|
|
147
181
|
// only mainnet: the relay resolves chainId from the request body, so
|
|
148
182
|
// the query-string chain guard never sees it.
|
|
149
183
|
const id = Number(transaction.chainId)
|
|
184
|
+
if (attributionError)
|
|
185
|
+
rejectSponsorship({
|
|
186
|
+
chainId: id,
|
|
187
|
+
message: 'Project attribution rejected.',
|
|
188
|
+
reason: attributionError,
|
|
189
|
+
})
|
|
150
190
|
if (!attribution) {
|
|
151
191
|
if (sponsorshipRequired)
|
|
152
192
|
rejectSponsorship({
|
|
@@ -186,15 +226,9 @@ function middleware(options: middleware.Options) {
|
|
|
186
226
|
})
|
|
187
227
|
return false
|
|
188
228
|
}
|
|
189
|
-
|
|
190
|
-
// Enforcement reads hit the primary, never the cached replica: a
|
|
191
|
-
// stale `past_due`, a lowered limit, or another isolate's pending
|
|
192
|
-
// spend must not keep the gate open.
|
|
193
229
|
const db = Db.get(c.get('db'))
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
// `past_due` gets its own reason: the caller must settle an invoice,
|
|
197
|
-
// not attach a card.
|
|
230
|
+
// Enforcement reads hit the primary, never the cached replica: a
|
|
231
|
+
// stale `past_due` or another isolate's state must not keep the gate open.
|
|
198
232
|
const billing = await Billing.status(db, attribution.orgId)
|
|
199
233
|
if (billing === 'past_due') {
|
|
200
234
|
if (sponsorshipRequired)
|
|
@@ -214,6 +248,7 @@ function middleware(options: middleware.Options) {
|
|
|
214
248
|
})
|
|
215
249
|
return 'billing_required'
|
|
216
250
|
}
|
|
251
|
+
await validatePromotion(id)
|
|
217
252
|
|
|
218
253
|
const feeToken = (() => {
|
|
219
254
|
try {
|
|
@@ -286,7 +321,6 @@ function middleware(options: middleware.Options) {
|
|
|
286
321
|
// reconciliation convention; pending rows charge it to spend limits.
|
|
287
322
|
const feeMax = Fees.maxOf(Transaction.deserialize(event.transaction as `0x76${string}`))
|
|
288
323
|
const db = Db.get(c.get('db'))
|
|
289
|
-
|
|
290
324
|
const feeToken = (() => {
|
|
291
325
|
try {
|
|
292
326
|
return event.feeToken !== undefined ? TokenId.toAddress(event.feeToken) : undefined
|
|
@@ -318,13 +352,54 @@ function middleware(options: middleware.Options) {
|
|
|
318
352
|
// Re-read settings on the authoritative path: the limit may have
|
|
319
353
|
// changed since `validate`, and only mainnet spend is limited.
|
|
320
354
|
const settings = mainnet ? await BillingSettings.get(db, attribution.orgId) : undefined
|
|
321
|
-
|
|
355
|
+
const billingLimit =
|
|
356
|
+
settings?.spendLimit == null
|
|
357
|
+
? undefined
|
|
358
|
+
: {
|
|
359
|
+
chainIds: [Viem.chainId.mainnet],
|
|
360
|
+
max: Billing.toBaseUnits(settings.spendLimit),
|
|
361
|
+
since: Billing.periodStart(settings.period),
|
|
362
|
+
}
|
|
363
|
+
if (
|
|
364
|
+
mainnet &&
|
|
365
|
+
attribution.environment === 'production' &&
|
|
366
|
+
attribution.projectId !== undefined
|
|
367
|
+
)
|
|
368
|
+
try {
|
|
369
|
+
const promotion = await SponsoredTransactions.reservePromotion(
|
|
370
|
+
db,
|
|
371
|
+
{ ...input, billable: false },
|
|
372
|
+
{
|
|
373
|
+
at: new Date().toISOString(),
|
|
374
|
+
...(billingLimit ? { billingLimit } : {}),
|
|
375
|
+
chainIds: [Viem.chainId.mainnet],
|
|
376
|
+
projectId: attribution.projectId,
|
|
377
|
+
},
|
|
378
|
+
)
|
|
379
|
+
if (promotion.status !== 'ineligible') return
|
|
380
|
+
} catch (error) {
|
|
381
|
+
if (error instanceof SponsoredTransactions.AttributionConflictError)
|
|
382
|
+
rejectSponsorship({
|
|
383
|
+
chainId: event.chainId,
|
|
384
|
+
message: 'Project attribution rejected.',
|
|
385
|
+
reason: 'project_id_invalid',
|
|
386
|
+
})
|
|
387
|
+
if (error instanceof SponsoredTransactions.PromotionActivationError)
|
|
388
|
+
failSponsorship({
|
|
389
|
+
chainId: event.chainId,
|
|
390
|
+
message: 'Project promotion activation failed.',
|
|
391
|
+
})
|
|
392
|
+
if (error instanceof SponsoredTransactions.PeriodSpendLimitError)
|
|
393
|
+
rejectSponsorship({
|
|
394
|
+
chainId: event.chainId,
|
|
395
|
+
message: 'Spend limit exceeded.',
|
|
396
|
+
reason: 'spend_limit_exceeded',
|
|
397
|
+
})
|
|
398
|
+
throw error
|
|
399
|
+
}
|
|
400
|
+
if (!billingLimit) return void (await SponsoredTransactions.upsert(db, input))
|
|
322
401
|
try {
|
|
323
|
-
await SponsoredTransactions.reserve(db, input,
|
|
324
|
-
chainIds: [Viem.chainId.mainnet],
|
|
325
|
-
max: Billing.toBaseUnits(settings.spendLimit),
|
|
326
|
-
since: Billing.periodStart(settings.period),
|
|
327
|
-
})
|
|
402
|
+
await SponsoredTransactions.reserve(db, input, billingLimit)
|
|
328
403
|
} catch (error) {
|
|
329
404
|
if (error instanceof SponsoredTransactions.PeriodSpendLimitError)
|
|
330
405
|
rejectSponsorship({
|
|
@@ -518,6 +593,57 @@ function sponsorshipMethod(value: unknown): Log.SponsorshipMethod | undefined {
|
|
|
518
593
|
}
|
|
519
594
|
}
|
|
520
595
|
|
|
596
|
+
/** Resolves optional request-level project attribution within the API key's organization. */
|
|
597
|
+
async function resolveAttribution(
|
|
598
|
+
db: Db.Db,
|
|
599
|
+
options: resolveAttribution.Options,
|
|
600
|
+
): Promise<resolveAttribution.Result | undefined> {
|
|
601
|
+
if (options.principal?.type !== 'api_key') return undefined
|
|
602
|
+
const { principal } = options
|
|
603
|
+
const attribution = {
|
|
604
|
+
apiKeyId: principal.id,
|
|
605
|
+
environment: principal.environment,
|
|
606
|
+
orgId: principal.orgId,
|
|
607
|
+
...(principal.projectId === undefined ? {} : { projectId: principal.projectId }),
|
|
608
|
+
}
|
|
609
|
+
if (!options.projectId) return { attribution }
|
|
610
|
+
if (principal.projectId !== undefined) return { attribution, error: 'project_id_invalid' }
|
|
611
|
+
const project = await Projects.get(db, options.projectId)
|
|
612
|
+
if (!project || project.orgId !== principal.orgId)
|
|
613
|
+
return { attribution, error: 'project_id_invalid' }
|
|
614
|
+
return { attribution: { ...attribution, projectId: project.id } }
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
declare namespace resolveAttribution {
|
|
618
|
+
/** Inputs for resolving API-key project attribution. */
|
|
619
|
+
type Options = {
|
|
620
|
+
/** Authenticated request principal. */
|
|
621
|
+
principal: Auth.Principal | null
|
|
622
|
+
/** Optional project id supplied by the caller. */
|
|
623
|
+
projectId?: string | undefined
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** Resolved API-key attribution. */
|
|
627
|
+
type Attribution = {
|
|
628
|
+
/** API key id. */
|
|
629
|
+
apiKeyId: string
|
|
630
|
+
/** API key environment. */
|
|
631
|
+
environment: 'production' | 'sandbox'
|
|
632
|
+
/** API key organization id. */
|
|
633
|
+
orgId: string
|
|
634
|
+
/** Attributed project id. */
|
|
635
|
+
projectId?: string | undefined
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/** Attribution result with an optional policy error. */
|
|
639
|
+
type Result = {
|
|
640
|
+
/** Attribution retained for request diagnostics. */
|
|
641
|
+
attribution: Attribution
|
|
642
|
+
/** Project attribution error detected before transaction validation. */
|
|
643
|
+
error?: 'project_id_invalid' | undefined
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
521
647
|
declare namespace middleware {
|
|
522
648
|
/** One invariant sponsorship failure. */
|
|
523
649
|
type Failure = {
|
|
@@ -535,10 +661,13 @@ declare namespace middleware {
|
|
|
535
661
|
path: 'rpc/relay' | 'rpc/sponsor'
|
|
536
662
|
}
|
|
537
663
|
|
|
664
|
+
/** Cached promotion validation state; request accounting is applied per batch item. */
|
|
665
|
+
type PromotionState = { status: 'active' | 'inactive' } | { message: string; status: 'error' }
|
|
666
|
+
|
|
538
667
|
/** One sponsorship rejection recorded for request logging. */
|
|
539
668
|
type Rejection = {
|
|
540
669
|
/** Candidate transaction chain, validated before it enters the log entry. */
|
|
541
|
-
chainId
|
|
670
|
+
chainId?: number | undefined
|
|
542
671
|
/** Stable client error code. */
|
|
543
672
|
reason: Log.SponsorshipReason
|
|
544
673
|
}
|
|
@@ -546,7 +675,7 @@ declare namespace middleware {
|
|
|
546
675
|
/** One client-visible sponsorship refusal. */
|
|
547
676
|
type Refusal = {
|
|
548
677
|
/** Candidate transaction chain. */
|
|
549
|
-
chainId
|
|
678
|
+
chainId?: number | undefined
|
|
550
679
|
/** Client-safe error message. */
|
|
551
680
|
message: string
|
|
552
681
|
/** Stable client error code. */
|
|
@@ -11,6 +11,7 @@ import { getTransactionReceipt } from 'viem/actions'
|
|
|
11
11
|
|
|
12
12
|
import * as Db from '../../db/Db.js'
|
|
13
13
|
import * as BillingSettings from '../../db/tables/billingSettings.js'
|
|
14
|
+
import * as Projects from '../../db/tables/projects.js'
|
|
14
15
|
import * as Fees from '../../internal/Fees.js'
|
|
15
16
|
import * as SponsoredTransactions from '../../db/tables/sponsoredTransactions.js'
|
|
16
17
|
import * as Tidx from '../../internal/Tidx.js'
|
|
@@ -292,7 +293,29 @@ export function createFinalizer(options: createFinalizer.Options): createFinaliz
|
|
|
292
293
|
getClient,
|
|
293
294
|
getTidx: (chainId) => getTidx(chainId as Viem.ChainId),
|
|
294
295
|
limitFor: async (intent) => {
|
|
295
|
-
if (!
|
|
296
|
+
if (!Viem.isMainnet(intent.chainId)) return undefined
|
|
297
|
+
if (!intent.billable) {
|
|
298
|
+
if (!intent.projectId) return undefined
|
|
299
|
+
const project = await Projects.get(db, intent.projectId)
|
|
300
|
+
// Missing promotion state cannot prove the intent remains within Tempo's budget.
|
|
301
|
+
if (!project || project.sponsorshipSubsidyStartsAt === null)
|
|
302
|
+
return {
|
|
303
|
+
billable: false,
|
|
304
|
+
chainIds: [Viem.chainId.mainnet],
|
|
305
|
+
max: -1n,
|
|
306
|
+
projectId: intent.projectId,
|
|
307
|
+
since: intent.createdAt,
|
|
308
|
+
}
|
|
309
|
+
if (project.sponsorshipSpendLimit === null) return undefined
|
|
310
|
+
return {
|
|
311
|
+
billable: false,
|
|
312
|
+
chainIds: [Viem.chainId.mainnet],
|
|
313
|
+
max: BigInt(project.sponsorshipSpendLimit),
|
|
314
|
+
projectId: intent.projectId,
|
|
315
|
+
since: project.sponsorshipSubsidyStartsAt,
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (!intent.billable) return undefined
|
|
296
319
|
const settings = await BillingSettings.get(db, intent.orgId)
|
|
297
320
|
if (settings?.spendLimit == null) return undefined
|
|
298
321
|
return {
|
package/src/db/Db.ts
CHANGED
|
@@ -1488,6 +1488,41 @@ export const migrations: readonly Migration[] = [
|
|
|
1488
1488
|
up: (db) =>
|
|
1489
1489
|
raw(db, sql`ALTER TABLE funding_routes ADD COLUMN IF NOT EXISTS configuration jsonb`),
|
|
1490
1490
|
},
|
|
1491
|
+
{
|
|
1492
|
+
name: '0138_organizations_sponsorship_subsidy',
|
|
1493
|
+
up: (db) =>
|
|
1494
|
+
raw(
|
|
1495
|
+
db,
|
|
1496
|
+
sql`ALTER TABLE organizations
|
|
1497
|
+
ADD COLUMN IF NOT EXISTS sponsorship_subsidy_duration_days integer,
|
|
1498
|
+
ADD COLUMN IF NOT EXISTS sponsorship_subsidy_project_spend_limit text`,
|
|
1499
|
+
),
|
|
1500
|
+
},
|
|
1501
|
+
{
|
|
1502
|
+
name: '0139_projects_sponsorship_activation',
|
|
1503
|
+
up: (db) =>
|
|
1504
|
+
raw(
|
|
1505
|
+
db,
|
|
1506
|
+
sql`ALTER TABLE projects
|
|
1507
|
+
ADD COLUMN IF NOT EXISTS sponsorship_subsidy_starts_at text,
|
|
1508
|
+
ADD COLUMN IF NOT EXISTS sponsorship_subsidy_ends_at text,
|
|
1509
|
+
DROP CONSTRAINT IF EXISTS projects_sponsorship_subsidy_window_consistent,
|
|
1510
|
+
ADD CONSTRAINT projects_sponsorship_subsidy_window_consistent CHECK ((sponsorship_subsidy_starts_at IS NULL) = (sponsorship_subsidy_ends_at IS NULL))`,
|
|
1511
|
+
),
|
|
1512
|
+
},
|
|
1513
|
+
{
|
|
1514
|
+
name: '0140_projects_sponsorship_spend_limit',
|
|
1515
|
+
up: (db) =>
|
|
1516
|
+
raw(db, sql`ALTER TABLE projects ADD COLUMN IF NOT EXISTS sponsorship_spend_limit text`),
|
|
1517
|
+
},
|
|
1518
|
+
{
|
|
1519
|
+
name: '0141_sponsored_transactions_promotion_spend_index',
|
|
1520
|
+
up: (db) =>
|
|
1521
|
+
raw(
|
|
1522
|
+
db,
|
|
1523
|
+
sql`CREATE INDEX IF NOT EXISTS sponsored_transactions_promotion_spend_idx ON sponsored_transactions (org_id, project_id, status, created_at) WHERE NOT billable`,
|
|
1524
|
+
),
|
|
1525
|
+
},
|
|
1491
1526
|
]
|
|
1492
1527
|
|
|
1493
1528
|
/**
|
package/src/db/Schema.ts
CHANGED
|
@@ -219,6 +219,12 @@ export const Organization = z.object({
|
|
|
219
219
|
name: z
|
|
220
220
|
.string()
|
|
221
221
|
.check(z.describe('Human-readable organization name.'), z.meta({ examples: ['Acme, Inc.'] })),
|
|
222
|
+
sponsorshipSubsidyDurationDays: z
|
|
223
|
+
.nullable(z.number().check(z.int(), z.positive()))
|
|
224
|
+
.check(z.describe('Promotional sponsorship duration in days, or null when disabled.')),
|
|
225
|
+
sponsorshipSubsidyProjectSpendLimit: z
|
|
226
|
+
.nullable(z.string())
|
|
227
|
+
.check(z.describe('Per-project promotional spend cap in decimal USD, or null when disabled.')),
|
|
222
228
|
updatedAt: z.string().check(z.describe('When the organization was last updated (ISO 8601).')),
|
|
223
229
|
userId: z
|
|
224
230
|
.nullable(z.string())
|
|
@@ -243,6 +249,15 @@ export const Project = z.object({
|
|
|
243
249
|
.string()
|
|
244
250
|
.check(z.describe('Human-readable project name.'), z.meta({ examples: ['Checkout'] })),
|
|
245
251
|
orgId: z.string().check(z.describe('Owning organization id (`org_…`).')),
|
|
252
|
+
sponsorshipSubsidyStartsAt: z
|
|
253
|
+
.nullable(z.string())
|
|
254
|
+
.check(z.describe('When the project first activated promotional sponsorship, or null.')),
|
|
255
|
+
sponsorshipSubsidyEndsAt: z
|
|
256
|
+
.nullable(z.string())
|
|
257
|
+
.check(z.describe('When the project promotional sponsorship ends, or null.')),
|
|
258
|
+
sponsorshipSpendLimit: z
|
|
259
|
+
.nullable(z.string())
|
|
260
|
+
.check(z.describe('Promotional spend cap in fee-token base units, or null.')),
|
|
246
261
|
updatedAt: z.string().check(z.describe('When the project was last updated (ISO 8601).')),
|
|
247
262
|
})
|
|
248
263
|
|
|
@@ -640,8 +655,8 @@ export const FundingTransfer = z.object({
|
|
|
640
655
|
.check(z.describe('Attributed project id (`prj_…`), or null for organization-attributed keys.')), // prettier-ignore
|
|
641
656
|
providerId: z.string().check(z.describe('Selected funding provider id.')),
|
|
642
657
|
providerState: z
|
|
643
|
-
.nullable(z.custom<
|
|
644
|
-
.check(z.describe('Private provider
|
|
658
|
+
.nullable(z.custom<funding_Transfer.ProviderState>())
|
|
659
|
+
.check(z.describe('Private provider state (`jsonb`), or null. Never serialized publicly.')), // prettier-ignore
|
|
645
660
|
quoteExpiresAt: z
|
|
646
661
|
.string()
|
|
647
662
|
.check(z.describe('When the quoted terms stop being executable (ISO 8601).')),
|