From 11b6c4f6d171ffd5ccbdad8ea98a0ce299210893 Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Wed, 15 Apr 2026 10:33:12 -0700 Subject: [PATCH 1/4] fix: make tempo fee-payer policy chain-aware --- .changeset/fee-payer-policy-config.md | 5 ++++ src/tempo/internal/fee-payer.test.ts | 36 +++++++++++++++++++++++++++ src/tempo/internal/fee-payer.ts | 32 ++++++++++++++++++++++-- src/tempo/server/Charge.ts | 16 ++++++++++++ 4 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 .changeset/fee-payer-policy-config.md diff --git a/.changeset/fee-payer-policy-config.md b/.changeset/fee-payer-policy-config.md new file mode 100644 index 00000000..542c47dc --- /dev/null +++ b/.changeset/fee-payer-policy-config.md @@ -0,0 +1,5 @@ +--- +'mppx': patch +--- + +Make Tempo charge fee-sponsorship policy resolve per chain and allow overriding it with `feePayerPolicy`. diff --git a/src/tempo/internal/fee-payer.test.ts b/src/tempo/internal/fee-payer.test.ts index 0570b75f..879682af 100644 --- a/src/tempo/internal/fee-payer.test.ts +++ b/src/tempo/internal/fee-payer.test.ts @@ -285,6 +285,42 @@ describe('prepareSponsoredTransaction', () => { ).not.toThrow() }) + test('accepts higher Moderato priority fees by default', () => { + expect(() => + prepareSponsoredTransaction({ + account: sponsor, + chainId: 42431, + details, + expectedFeeToken: bogus, + transaction: { + ...baseTransaction, + gas: 626_497n, + maxFeePerGas: 24_000_000_000n, + maxPriorityFeePerGas: 24_000_000_000n, + } as any, + }), + ).not.toThrow() + }) + + test('accepts fee-payer policy overrides', () => { + expect(() => + prepareSponsoredTransaction({ + account: sponsor, + chainId: 4217, + details, + expectedFeeToken: bogus, + policy: { maxPriorityFeePerGas: 50_000_000_000n }, + transaction: { + ...baseTransaction, + chainId: 4217, + gas: 626_497n, + maxFeePerGas: 24_000_000_000n, + maxPriorityFeePerGas: 24_000_000_000n, + } as any, + }), + ).not.toThrow() + }) + test('drops unknown top-level fields from the sponsored transaction', () => { const sponsored = prepareSponsoredTransaction({ account: sponsor, diff --git a/src/tempo/internal/fee-payer.ts b/src/tempo/internal/fee-payer.ts index 636d5e5a..461662f6 100644 --- a/src/tempo/internal/fee-payer.ts +++ b/src/tempo/internal/fee-payer.ts @@ -5,6 +5,7 @@ import { decodeFunctionData } from 'viem' import { Abis, Addresses, Transaction } from 'viem/tempo' import * as TempoAddress_internal from './address.js' +import * as defaults from './defaults.js' import * as Selectors from './selectors.js' /** Returns true if the serialized transaction has a Tempo envelope prefix. */ @@ -26,17 +27,41 @@ export const callScopes = [ [Selectors.approve, Selectors.swapExactAmountOut, Selectors.transferWithMemo], ] +export type Policy = { + maxGas: bigint + maxFeePerGas: bigint + maxPriorityFeePerGas: bigint + maxTotalFee: bigint + maxValidityWindowSeconds: number +} + /** * maxTotalFee must be high enough to cover `transferWithMemo` and * swap transactions at peak gas prices. Bumped from 0.01 ETH in #327. */ -const policy = { +const defaultPolicy: Policy = { maxGas: 2_000_000n, maxFeePerGas: 100_000_000_000n, maxPriorityFeePerGas: 10_000_000_000n, maxTotalFee: 50_000_000_000_000_000n, maxValidityWindowSeconds: 15 * 60, -} as const +} + +const policyByChainId = { + [defaults.chainId.mainnet]: defaultPolicy, + // Moderato regularly needs a higher priority fee than mainnet. + [defaults.chainId.testnet]: { + ...defaultPolicy, + maxPriorityFeePerGas: 50_000_000_000n, + }, +} as const satisfies Record + +function getPolicy(chainId: number, overrides: Partial | undefined): Policy { + return { + ...(policyByChainId[chainId as defaults.ChainId] ?? defaultPolicy), + ...overrides, + } +} /** Validates that a set of transaction calls matches an allowed fee-payer pattern. */ export function validateCalls( @@ -89,6 +114,7 @@ export function prepareSponsoredTransaction(parameters: { details: Record expectedFeeToken?: TempoAddress.Address | undefined now?: Date | undefined + policy?: Partial | undefined transaction: ReturnType<(typeof Transaction)['deserialize']> }) { const { @@ -98,8 +124,10 @@ export function prepareSponsoredTransaction(parameters: { details, expectedFeeToken, now = new Date(), + policy: policyOverrides, transaction, } = parameters + const policy = getPolicy(chainId, policyOverrides) const { accessList, diff --git a/src/tempo/server/Charge.ts b/src/tempo/server/Charge.ts index b13a5161..3faafb42 100644 --- a/src/tempo/server/Charge.ts +++ b/src/tempo/server/Charge.ts @@ -60,6 +60,7 @@ export function charge( decimals = defaults.decimals, description, externalId, + feePayerPolicy, html, memo, waitForConfirmation = true, @@ -313,6 +314,7 @@ export function charge( chainId: chainId ?? client.chain!.id, details: { amount, currency, recipient }, expectedFeeToken, + policy: feePayerPolicy, transaction: { ...transaction, ...(resolvedFeeToken ? { feeToken: resolvedFeeToken } : {}), @@ -397,6 +399,12 @@ export declare namespace charge { type Parameters = { /** Render payment page when Accept header is text/html (e.g. in browsers) */ html?: boolean | Html.Config | undefined + /** + * Override the fee-sponsor policy used when co-signing Tempo charge + * transactions. Defaults resolve per chain, including a higher + * priority-fee ceiling on Moderato. + */ + feePayerPolicy?: FeePayerPolicy | undefined /** Testnet mode. */ testnet?: boolean | undefined /** @@ -436,6 +444,14 @@ export declare namespace charge { > & { decimals: number } + + type FeePayerPolicy = { + maxGas?: bigint + maxFeePerGas?: bigint + maxPriorityFeePerGas?: bigint + maxTotalFee?: bigint + maxValidityWindowSeconds?: number + } } type ExpectedTransfer = { From feefad3f570124c0c9673520fd7d501adc4ae63a Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Wed, 15 Apr 2026 10:45:05 -0700 Subject: [PATCH 2/4] fix: harden fee payer policy overrides --- src/tempo/internal/fee-payer.test.ts | 38 ++++++++++++++++++++++++++++ src/tempo/internal/fee-payer.ts | 10 ++++++-- src/tempo/server/Charge.ts | 11 +++----- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/src/tempo/internal/fee-payer.test.ts b/src/tempo/internal/fee-payer.test.ts index 879682af..e6eb4c11 100644 --- a/src/tempo/internal/fee-payer.test.ts +++ b/src/tempo/internal/fee-payer.test.ts @@ -321,6 +321,44 @@ describe('prepareSponsoredTransaction', () => { ).not.toThrow() }) + test('error: rejects excessive priority fee under a custom policy override', () => { + expect(() => + prepareSponsoredTransaction({ + account: sponsor, + chainId: 4217, + details, + expectedFeeToken: bogus, + policy: { maxPriorityFeePerGas: 20_000_000_000n }, + transaction: { + ...baseTransaction, + chainId: 4217, + gas: 626_497n, + maxFeePerGas: 24_000_000_000n, + maxPriorityFeePerGas: 24_000_000_000n, + } as any, + }), + ).toThrow('maxPriorityFeePerGas exceeds sponsor policy') + }) + + test('ignores undefined policy override values', () => { + expect(() => + prepareSponsoredTransaction({ + account: sponsor, + chainId: 4217, + details, + expectedFeeToken: bogus, + policy: { maxPriorityFeePerGas: undefined } as any, + transaction: { + ...baseTransaction, + chainId: 4217, + gas: 626_497n, + maxFeePerGas: 24_000_000_000n, + maxPriorityFeePerGas: 24_000_000_000n, + } as any, + }), + ).toThrow('maxPriorityFeePerGas exceeds sponsor policy') + }) + test('drops unknown top-level fields from the sponsored transaction', () => { const sponsored = prepareSponsoredTransaction({ account: sponsor, diff --git a/src/tempo/internal/fee-payer.ts b/src/tempo/internal/fee-payer.ts index 461662f6..eeef5fc8 100644 --- a/src/tempo/internal/fee-payer.ts +++ b/src/tempo/internal/fee-payer.ts @@ -57,9 +57,15 @@ const policyByChainId = { } as const satisfies Record function getPolicy(chainId: number, overrides: Partial | undefined): Policy { + const base = policyByChainId[chainId as defaults.ChainId] ?? defaultPolicy + if (!overrides) return base + return { - ...(policyByChainId[chainId as defaults.ChainId] ?? defaultPolicy), - ...overrides, + maxGas: overrides.maxGas ?? base.maxGas, + maxFeePerGas: overrides.maxFeePerGas ?? base.maxFeePerGas, + maxPriorityFeePerGas: overrides.maxPriorityFeePerGas ?? base.maxPriorityFeePerGas, + maxTotalFee: overrides.maxTotalFee ?? base.maxTotalFee, + maxValidityWindowSeconds: overrides.maxValidityWindowSeconds ?? base.maxValidityWindowSeconds, } } diff --git a/src/tempo/server/Charge.ts b/src/tempo/server/Charge.ts index 3faafb42..fa78bdcd 100644 --- a/src/tempo/server/Charge.ts +++ b/src/tempo/server/Charge.ts @@ -403,6 +403,9 @@ export declare namespace charge { * Override the fee-sponsor policy used when co-signing Tempo charge * transactions. Defaults resolve per chain, including a higher * priority-fee ceiling on Moderato. + * + * If you increase `maxGas` or `maxFeePerGas`, you may also need to raise + * `maxTotalFee` so the combined fee budget remains valid. */ feePayerPolicy?: FeePayerPolicy | undefined /** Testnet mode. */ @@ -445,13 +448,7 @@ export declare namespace charge { decimals: number } - type FeePayerPolicy = { - maxGas?: bigint - maxFeePerGas?: bigint - maxPriorityFeePerGas?: bigint - maxTotalFee?: bigint - maxValidityWindowSeconds?: number - } + type FeePayerPolicy = Partial } type ExpectedTransfer = { From 84f01ad3ee2f9ac069ff0b4abe20911a3446232f Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Wed, 15 Apr 2026 10:53:26 -0700 Subject: [PATCH 3/4] fix: tolerate npm audit registry outages in CI --- .github/workflows/verify.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index b310b464..faa188fa 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -44,7 +44,9 @@ jobs: fi - name: Audit dependencies - run: pnpm audit + # npm's legacy audit endpoint is returning 410 Gone. Keep the audit + # check enabled, but don't fail CI on registry-level audit outages. + run: pnpm audit --ignore-registry-errors - name: Lint & format run: pnpm check:ci From ce97ba487f2c7e709033a6b824e3f315d396537f Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Wed, 15 Apr 2026 11:05:32 -0700 Subject: [PATCH 4/4] fix: give html verify job more time in CI --- .github/workflows/verify.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index faa188fa..26401452 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -119,7 +119,7 @@ jobs: test-html: name: Test HTML runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 15 steps: - name: Clone repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2