Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fee-payer-policy-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'mppx': patch
---

Make Tempo charge fee-sponsorship policy resolve per chain and allow overriding it with `feePayerPolicy`.
6 changes: 4 additions & 2 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -117,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
Expand Down
74 changes: 74 additions & 0 deletions src/tempo/internal/fee-payer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,80 @@ 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('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,
Expand Down
38 changes: 36 additions & 2 deletions src/tempo/internal/fee-payer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -26,17 +27,47 @@ 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<defaults.ChainId, Policy>

function getPolicy(chainId: number, overrides: Partial<Policy> | undefined): Policy {
const base = policyByChainId[chainId as defaults.ChainId] ?? defaultPolicy
if (!overrides) return base

return {
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,
}
}

/** Validates that a set of transaction calls matches an allowed fee-payer pattern. */
export function validateCalls(
Expand Down Expand Up @@ -89,6 +120,7 @@ export function prepareSponsoredTransaction(parameters: {
details: Record<string, string>
expectedFeeToken?: TempoAddress.Address | undefined
now?: Date | undefined
policy?: Partial<Policy> | undefined
transaction: ReturnType<(typeof Transaction)['deserialize']>
}) {
const {
Expand All @@ -98,8 +130,10 @@ export function prepareSponsoredTransaction(parameters: {
details,
expectedFeeToken,
now = new Date(),
policy: policyOverrides,
transaction,
} = parameters
const policy = getPolicy(chainId, policyOverrides)

const {
accessList,
Expand Down
13 changes: 13 additions & 0 deletions src/tempo/server/Charge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export function charge<const parameters extends charge.Parameters>(
decimals = defaults.decimals,
description,
externalId,
feePayerPolicy,
html,
memo,
waitForConfirmation = true,
Expand Down Expand Up @@ -313,6 +314,7 @@ export function charge<const parameters extends charge.Parameters>(
chainId: chainId ?? client.chain!.id,
details: { amount, currency, recipient },
expectedFeeToken,
policy: feePayerPolicy,
transaction: {
...transaction,
...(resolvedFeeToken ? { feeToken: resolvedFeeToken } : {}),
Expand Down Expand Up @@ -397,6 +399,15 @@ 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.
*
* 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. */
testnet?: boolean | undefined
/**
Expand Down Expand Up @@ -436,6 +447,8 @@ export declare namespace charge {
> & {
decimals: number
}

type FeePayerPolicy = Partial<FeePayer.Policy>
}

type ExpectedTransfer = {
Expand Down
Loading