import { Hash } from 'ox' import { TxEnvelopeTempo } from 'ox/tempo' import type { LocalAccount } from 'viem/accounts' import { TokenId, Transaction } from 'viem/tempo' import type * as Auth from '../../../internal/Auth.js' import * as BillingSettings from '../../../db/tables/billingSettings.js' import type * as Db from '../../../db/Db.js' import * as SponsoredTransactions from '../../../db/tables/sponsoredTransactions.js' import * as Fees from '../../../internal/Fees.js' import * as VerifiedTokens from '../../../internal/VerifiedTokens.js' import * as Viem from '../../../internal/Viem.js' import * as Billing from '../../management/Billing.js' import * as Policy from './Policy.js' /** Wraps a managed fee payer to record its signed transaction before broadcast. */ export function decorate(options: decorate.Options): LocalAccount { const { account, onSigned } = options const signTransaction = account.signTransaction.bind(account) return new Proxy(account, { get(target, property, receiver) { if (property !== 'signTransaction') return Reflect.get(target, property, receiver) return async (...parameters: Parameters) => { const serializedTransaction = await signTransaction(...parameters) await onSigned({ serializedTransaction }) return serializedTransaction } }, }) as LocalAccount } export declare namespace decorate { /** Dependencies used to record one MPP-sponsored transaction. */ type Options = { /** Account that MPPX uses to add the fee-payer signature. */ account: LocalAccount /** Records the signed transaction before it can reach the network. */ onSigned: (options: { serializedTransaction: `0x${string}` }) => Promise } } type SponsoredTransaction = { chainId?: unknown feeToken?: unknown from?: `0x${string}` | undefined gas?: unknown maxFeePerGas?: unknown } /** Records a sponsorship and atomically enforces its billing limits. */ export async function record(options: record.Options): Promise { const { db, dbCached, principal, serializedTransaction, txFeeLimit } = options const transaction = Transaction.deserialize(serializedTransaction) as SponsoredTransaction if (!transaction.from) throw new Policy.PolicyDeniedError('Fee-sponsored transaction must declare a sender.') if (typeof transaction.chainId !== 'number') throw new Policy.PolicyDeniedError('Fee-sponsored transaction must declare a chain id.') const chainId = transaction.chainId const feeMax = Fees.maxOf(transaction) if (feeMax === undefined) throw new Policy.PolicyDeniedError('Fee-sponsored transaction must declare a fee limit.') const feeToken = (() => { try { return transaction.feeToken === undefined ? undefined : TokenId.toAddress(transaction.feeToken as never) } catch { throw new Policy.PolicyDeniedError('Fee-sponsored transaction uses an invalid fee token.') } })() const mainnet = Viem.isMainnet(chainId) const settings = mainnet ? await BillingSettings.get(db, principal.orgId) : undefined let currency: string | undefined if (mainnet) { const verified = feeToken ? (await VerifiedTokens.read(dbCached, chainId))?.byAddress.get(feeToken.toLowerCase()) : undefined if ( !verified || verified.currency.toLowerCase() !== 'usd' || verified.decimals !== Fees.tokenDecimals ) throw new Policy.PolicyDeniedError('Fee-sponsored transaction uses an unsupported fee token.') currency = verified.currency.toLowerCase() const cap = (() => { if (settings?.txFeeLimit == null) return txFeeLimit const org = Billing.toBaseUnits(settings.txFeeLimit) return org < txFeeLimit ? org : txFeeLimit })() if (feeMax > cap) throw new Policy.PolicyDeniedError('Transaction fee limit exceeded.') } const envelope = TxEnvelopeTempo.deserialize(serializedTransaction as TxEnvelopeTempo.Serialized) const signPayload = TxEnvelopeTempo.getFeePayerSignPayload(envelope, { sender: transaction.from }) const input = { apiKeyId: principal.id, billable: mainnet && principal.environment === 'production', chainId, ...(currency ? { currency } : {}), environment: principal.environment, feeMax: feeMax.toString(), ...(feeToken ? { feeToken } : {}), orgId: principal.orgId, ...(principal.projectId === undefined ? {} : { projectId: principal.projectId }), signPayload, transaction: serializedTransaction, transactionHash: Hash.keccak256(serializedTransaction), } if (settings?.spendLimit == null) { await SponsoredTransactions.upsert(db, input) return } try { await SponsoredTransactions.reserve(db, input, { chainIds: [Viem.chainId.mainnet], max: Billing.toBaseUnits(settings.spendLimit), since: Billing.periodStart(settings.period), }) } catch (cause) { if (cause instanceof SponsoredTransactions.PeriodSpendLimitError) throw new Policy.PolicyDeniedError('Spend limit exceeded.') throw cause } } export declare namespace record { /** Dependencies used to persist one MPP sponsorship. */ type Options = { /** Primary database for billing enforcement and sponsorship state. */ db: Db.Db /** Cached database used for verified-token reads. */ dbCached: Db.Db /** API key that requested sponsorship. */ principal: Extract /** Final fee-payer-signed transaction, before broadcast. */ serializedTransaction: `0x${string}` /** Platform per-transaction cap in fee-token base units. */ txFeeLimit: bigint } }