import { Address } from 'ox' import { SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo' import * as TestApp from '../../../../test/App.js' import * as Tempo from '../../../../test/Tempo.js' import * as BillingSettings from '../../../db/tables/billingSettings.js' import * as SponsoredTransactions from '../../../db/tables/sponsoredTransactions.js' import type * as Auth from '../../../internal/Auth.js' import * as VerifiedTokens from '../../../internal/VerifiedTokens.js' import * as Zones from '../../Zones.js' import * as Sponsorship from './Sponsorship.js' const zones = new Map(Zones.chains.map((zone) => [zone.id, zone])) const mainnetChainIds = [4217, Zones.zoneMainnetInternal.id, Zones.zoneDeel.id] const feeToken = Address.from(Tempo.currency) const principal = { apiKey: { allowedIps: [], environment: 'production', id: 'key_mpp', orgId: 'org_mpp', scopes: ['mpp:write', '*'], }, environment: 'production', id: 'key_mpp', orgId: 'org_mpp', type: 'api_key', } satisfies Auth.Principal const token = { address: feeToken, currency: 'USD', decimals: 6, name: 'Path USD', symbol: 'pathUSD', } describe('record', () => { test.each(mainnetChainIds)( 'records chain %s as billable and eligible for metering', async (chainId) => { const db = TestApp.database() await VerifiedTokens.replace(db, chainId, [token]) await Sponsorship.record({ db, dbCached: db, principal, serializedTransaction: await transaction({ chainId }), txFeeLimit: 1_000_000n, zones, }) const [record] = await SponsoredTransactions.listPending(db) expect(record).toMatchObject({ billable: true, chainId, currency: 'usd', feeMax: '600000', feeToken: Tempo.currency, orgId: principal.orgId, }) await SponsoredTransactions.finalize(db, record!.id, { feeAmount: '500000', finalizedAt: new Date().toISOString(), }) expect( await SponsoredTransactions.listUnreported(db, { chainIds: mainnetChainIds }), ).toMatchObject([{ billable: true, chainId, feeAmount: '500000' }]) }, ) test.each([undefined, { ...token, currency: 'EUR' }, { ...token, decimals: 18 }])( 'rejects an unsupported Zone fee token %j', async (verified) => { const db = TestApp.database() const chainId = Zones.zoneDeel.id await VerifiedTokens.replace(db, chainId, verified ? [verified] : []) await expect( Sponsorship.record({ db, dbCached: db, principal, serializedTransaction: await transaction({ chainId }), txFeeLimit: 1_000_000n, zones, }), ).rejects.toThrowErrorMatchingInlineSnapshot( `[Mpp.Policy.PolicyDeniedError: Fee-sponsored transaction uses an unsupported fee token.]`, ) expect(await SponsoredTransactions.listPending(db)).toMatchInlineSnapshot(`[]`) }, ) test.each([ { orgLimit: '1.00', platformLimit: 500_000n }, { orgLimit: '0.50', platformLimit: 1_000_000n }, ])('enforces Zone caps $orgLimit and $platformLimit', async ({ orgLimit, platformLimit }) => { const db = TestApp.database() const chainId = Zones.zoneDeel.id await VerifiedTokens.replace(db, chainId, [token]) await BillingSettings.upsert(db, { orgId: principal.orgId, txFeeLimit: orgLimit }) await expect( Sponsorship.record({ db, dbCached: db, principal, serializedTransaction: await transaction({ chainId }), txFeeLimit: platformLimit, zones, }), ).rejects.toThrowErrorMatchingInlineSnapshot( `[Mpp.Policy.PolicyDeniedError: Transaction fee limit exceeded.]`, ) expect(await SponsoredTransactions.listPending(db)).toMatchInlineSnapshot(`[]`) }) test.each([ [4217, Zones.zoneDeel.id], [Zones.zoneDeel.id, 4217], [Zones.zoneMainnetInternal.id, Zones.zoneDeel.id], [Zones.zoneDeel.id, Zones.zoneDeel.id], ])('shares the organization spend limit between chains %s and %s', async (first, second) => { const db = TestApp.database() for (const chainId of new Set([first, second])) await VerifiedTokens.replace(db, chainId, [token]) await BillingSettings.upsert(db, { orgId: principal.orgId, spendLimit: '1.00' }) const options = { db, dbCached: db, principal, txFeeLimit: 1_000_000n, zones } await Sponsorship.record({ ...options, serializedTransaction: await transaction({ chainId: first }), }) await expect( Sponsorship.record({ ...options, serializedTransaction: await transaction({ chainId: second, nonce: 1 }), }), ).rejects.toThrowErrorMatchingInlineSnapshot( `[Mpp.Policy.PolicyDeniedError: Spend limit exceeded.]`, ) expect(await SponsoredTransactions.listPending(db)).toHaveLength(1) }) test('rejects a sandbox key at mainnet Zone recording', async () => { const db = TestApp.database() await expect( Sponsorship.record({ db, dbCached: db, principal: { ...principal, environment: 'sandbox' }, serializedTransaction: await transaction({ chainId: Zones.zoneDeel.id }), txFeeLimit: 1_000_000n, zones, }), ).rejects.toThrowErrorMatchingInlineSnapshot( `[Mpp.Policy.PolicyDeniedError: Mainnet fee sponsorship requires a production API key.]`, ) expect(await SponsoredTransactions.listPending(db)).toMatchInlineSnapshot(`[]`) }) test('keeps sandbox testnet Zone sponsorship non-billable', async () => { const db = TestApp.database() await BillingSettings.upsert(db, { orgId: principal.orgId, spendLimit: '0.00', txFeeLimit: '0.00', }) await Sponsorship.record({ db, dbCached: db, principal: { ...principal, environment: 'sandbox' }, serializedTransaction: await transaction({ chainId: Zones.zoneModeratoInternal.id }), txFeeLimit: 1n, zones, }) expect(await SponsoredTransactions.listPending(db)).toMatchObject([ { billable: false, chainId: Zones.zoneModeratoInternal.id, environment: 'sandbox' }, ]) }) }) async function transaction(options: transaction.Options) { const serialized = await Tempo.accounts[0].signTransaction({ calls: [{ data: '0x', to: Tempo.accounts[1].address }], chainId: options.chainId, feePayer: true, gas: 100_000n, maxFeePerGas: 6_000_000_000_000n, maxPriorityFeePerGas: 0n, nonce: options.nonce ?? 0, }) const envelope = { ...TxEnvelopeTempo.deserialize(serialized as TxEnvelopeTempo.Serialized), feeToken, } const signature = SignatureEnvelope.from( await Tempo.accounts[2].sign({ hash: TxEnvelopeTempo.getFeePayerSignPayload(envelope, { sender: Tempo.accounts[0].address }), }), ) if (signature.type !== 'secp256k1') throw new Error('Expected a secp256k1 fee-payer signature.') return TxEnvelopeTempo.serialize(envelope, { feePayerSignature: signature.signature }) } declare namespace transaction { type Options = { chainId: number nonce?: number | undefined } }