diff --git a/apps/randomness/src/index.ts b/apps/randomness/src/index.ts index 1209079f55..8976cdc1bd 100644 --- a/apps/randomness/src/index.ts +++ b/apps/randomness/src/index.ts @@ -30,7 +30,9 @@ class RandomnessService { allowDebug: true, pollingInterval: 250, }, - maxPriorityFeePerGas: 10n, + gas: { + minPriorityFeePerGas: 10n, + }, }) this.transactionFactory = new TransactionFactory(this.txm, env.RANDOM_CONTRACT_ADDRESS, env.PRECOMMIT_DELAY) this.drandService = new DrandService(this.drandRepository, this.transactionFactory) diff --git a/packages/txm/lib/GasPriceOracle.ts b/packages/txm/lib/GasPriceOracle.ts index f3a9625def..0df8be7994 100644 --- a/packages/txm/lib/GasPriceOracle.ts +++ b/packages/txm/lib/GasPriceOracle.ts @@ -1,4 +1,5 @@ import { bigIntMax } from "@happy.tech/common" +import { type Result, err, ok } from "neverthrow" import type { LatestBlock } from "./BlockMonitor.js" import { Topics, eventBus } from "./EventBus.js" import type { TransactionManager } from "./TransactionManager.js" @@ -24,6 +25,7 @@ import type { TransactionManager } from "./TransactionManager.js" export class GasPriceOracle { private txmgr: TransactionManager private expectedNextBaseFeePerGas!: bigint + private targetPriorityFee!: bigint constructor(_transactionManager: TransactionManager) { this.txmgr = _transactionManager @@ -34,10 +36,10 @@ export class GasPriceOracle { const block = await this.txmgr.viemClient.getBlock({ blockTag: "latest", }) - this.onNewBlock(block) + await this.onNewBlock(block) } - private onNewBlock(block: LatestBlock) { + private async onNewBlock(block: LatestBlock) { const baseFeePerGas = block.baseFeePerGas const gasUsed = block.gasUsed const gasLimit = block.gasLimit @@ -47,6 +49,44 @@ export class GasPriceOracle { gasUsed, gasLimit, ) + const targetPriorityFeeResult = await this.calculateTargetPriorityFee() + if (targetPriorityFeeResult.isErr()) { + if (this.targetPriorityFee === undefined) { + this.targetPriorityFee = this.txmgr.maxPriorityFeePerGas ?? 0n + } + return + } + this.targetPriorityFee = targetPriorityFeeResult.value + } + + private async calculateTargetPriorityFee(): Promise> { + const feeHistory = await this.txmgr.viemClient.safeGetFeeHistory({ + blockCount: this.txmgr.priorityFeeAnalysisBlocks, + blockTag: "latest", + rewardPercentiles: [this.txmgr.priorityFeeTargetPercentile], + }) + + if (feeHistory.isErr()) { + return err(feeHistory.error) + } + + if (!feeHistory.value.reward) { + return err(new Error("No fee history found")) + } + + const priorityFee = + feeHistory.value.reward.flat().reduce((acc, curr) => acc + BigInt(curr), 0n) / + BigInt(feeHistory.value.reward.flat().length) + + if (this.txmgr.minPriorityFeePerGas && priorityFee < this.txmgr.minPriorityFeePerGas) { + return ok(this.txmgr.minPriorityFeePerGas) + } + + if (this.txmgr.maxPriorityFeePerGas && priorityFee > this.txmgr.maxPriorityFeePerGas) { + return ok(this.txmgr.maxPriorityFeePerGas) + } + + return ok(priorityFee) } private calculateExpectedNextBaseFeePerGas(baseFeePerGas: bigint, gasUsed: bigint, gasLimit: bigint): bigint { @@ -67,7 +107,7 @@ export class GasPriceOracle { public suggestGasForNextBlock(): { maxFeePerGas: bigint; maxPriorityFeePerGas: bigint } { const maxBaseFeePerGas = (this.expectedNextBaseFeePerGas * (100n + this.txmgr.baseFeeMargin)) / 100n - const maxFeePerGas = maxBaseFeePerGas + this.txmgr.maxPriorityFeePerGas - return { maxFeePerGas, maxPriorityFeePerGas: this.txmgr.maxPriorityFeePerGas } + const maxFeePerGas = maxBaseFeePerGas + this.targetPriorityFee + return { maxFeePerGas, maxPriorityFeePerGas: this.targetPriorityFee } } } diff --git a/packages/txm/lib/TransactionManager.ts b/packages/txm/lib/TransactionManager.ts index f01b003a50..d32b73e11e 100644 --- a/packages/txm/lib/TransactionManager.ts +++ b/packages/txm/lib/TransactionManager.ts @@ -120,25 +120,58 @@ export type TransactionManagerConfig = { /** The private key of the account used for signing transactions. */ privateKey: Hex - /** Optional EIP-1559 parameters. If not provided, defaults to the OP stack's stock parameters. */ - eip1559?: EIP1559Parameters - /** - * Safety margin for transaction base fees, expressed as a percentage. - * For example, a 20% increase should be represented as 20n. - * - * This is used to calculate the maximum fee per gas and safeguard from unanticipated or - * unpredictable gas price increases, in particular when the transaction cannot be included in - * the very next block. - * - * If not provided, defaults to 20n (20% increase). - */ - baseFeePercentageMargin?: bigint - /** - * Optional maximum priority fee per gas. - * This is the maximum amount of wei per gas the transaction is willing to pay as a tip to miners. - * If not provided, defaults to 0n. - */ - maxPriorityFeePerGas?: bigint + gas: { + /** Optional EIP-1559 parameters. If not provided, defaults to the OP stack's stock parameters. */ + eip1559?: EIP1559Parameters + /** + * Safety margin for transaction base fees, expressed as a percentage. + * For example, a 20% increase should be represented as 20n. + * + * This is used to calculate the maximum fee per gas and safeguard from unanticipated or + * unpredictable gas price increases, in particular when the transaction cannot be included in + * the very next block. + * + * @default 20n (20% increase) + */ + baseFeePercentageMargin?: bigint + /** + * Maximum allowed priority fee per gas unit (in wei). + * Acts as a safety cap to prevent overpaying for transaction priority. + * Even if the calculated priority fee based on percentile is higher, + * it will be capped at this value. + * @default undefined (no maximum cap) + * @example 2_000_000_000n // 2 Gwei max priority fee + */ + maxPriorityFeePerGas?: bigint + + /** + * Minimum required priority fee per gas unit (in wei). + * Ensures transactions don't get stuck in the mempool due to too low priority fees. + * If the calculated priority fee based on percentile is lower, + * it will be raised to this minimum value. + * @default undefined (no minimum) + * @example 500_000_000n // 0.5 Gwei minimum priority fee + */ + minPriorityFeePerGas?: bigint + + /** + * Target percentile for priority fee calculation (0-100). + * The system analyzes priority fees from transactions in the analyzed blocks + * and sets the fee at this percentile level. + * Higher percentiles mean higher priority but more expensive transactions. + * @default 50 (50th percentile - median priority fee) + */ + priorityFeeTargetPercentile?: number + + /** + * Number of blocks to analyze when calculating the priority fee percentile. + * The system will look at transactions from this many blocks back to determine + * the appropriate priority fee level. + * Higher values provide more stable fee estimates but may be less responsive to recent changes. + * @default 2 (analyze the last 2 blocks) + */ + priorityFeeAnalysisBlocks?: number + } /** The ABIs used by the TransactionManager. * This is a record of aliases to ABIs. The aliases are used to reference the ABIs in the @@ -233,7 +266,6 @@ export class TransactionManager { public readonly chainId: number public readonly eip1559: EIP1559Parameters public readonly baseFeeMargin: bigint - public readonly maxPriorityFeePerGas: bigint public readonly rpcAllowDebug: boolean public readonly blockTime: bigint public readonly finalizedTransactionPurgeTime: number @@ -245,6 +277,10 @@ export class TransactionManager { public readonly livenessSuccessCount: number public readonly livenessDownDelay: number public readonly livenessCheckInterval: number + public readonly priorityFeeTargetPercentile: number + public readonly priorityFeeAnalysisBlocks: number + public readonly minPriorityFeePerGas: bigint | undefined + public readonly maxPriorityFeePerGas: bigint | undefined constructor(_config: TransactionManagerConfig) { initializeTelemetry({ @@ -343,11 +379,14 @@ export class TransactionManager { this.rpcLivenessMonitor = new RpcLivenessMonitor(this) this.chainId = _config.chainId - this.eip1559 = _config.eip1559 ?? opStackDefaultEIP1559Parameters + this.eip1559 = _config.gas.eip1559 ?? opStackDefaultEIP1559Parameters this.abiManager = new ABIManager(_config.abis) - this.baseFeeMargin = _config.baseFeePercentageMargin ?? 20n - this.maxPriorityFeePerGas = _config.maxPriorityFeePerGas ?? 0n + this.baseFeeMargin = _config.gas.baseFeePercentageMargin ?? 20n + this.maxPriorityFeePerGas = _config.gas.maxPriorityFeePerGas + this.minPriorityFeePerGas = _config.gas.minPriorityFeePerGas + this.priorityFeeTargetPercentile = _config.gas.priorityFeeTargetPercentile ?? 50 + this.priorityFeeAnalysisBlocks = _config.gas.priorityFeeAnalysisBlocks ?? 2 this.rpcAllowDebug = _config.rpc.allowDebug ?? false this.blockTime = _config.blockTime ?? 2n diff --git a/packages/txm/lib/utils/safeViemClients.ts b/packages/txm/lib/utils/safeViemClients.ts index e75e6876aa..9c1ba6735a 100644 --- a/packages/txm/lib/utils/safeViemClients.ts +++ b/packages/txm/lib/utils/safeViemClients.ts @@ -6,6 +6,7 @@ import type { Chain, EstimateGasErrorType, GetChainIdErrorType, + GetFeeHistoryErrorType, GetTransactionCountErrorType, GetTransactionReceiptErrorType, Hash, @@ -102,6 +103,9 @@ export interface SafeViemPublicClient extends ViemPublicClient { safeGetTransactionCount: ( ...args: Parameters ) => ResultAsync>, GetTransactionCountErrorType> + safeGetFeeHistory: ( + ...args: Parameters + ) => ResultAsync>, GetFeeHistoryErrorType> } export interface MetricsHandlers { @@ -221,6 +225,25 @@ export function convertToSafeViemPublicClient( }) } + safeClient.safeGetFeeHistory = (...args: Parameters) => { + if (safeClient.rpcCounter) safeClient.rpcCounter.add(1, { method: "getFeeHistory" }) + const startTime = Date.now() + + return ResultAsync.fromPromise(client.getFeeHistory(...args), unknownToError) + .map((result) => { + const duration = Date.now() - startTime + if (safeClient.rpcResponseTimeHistogram) + safeClient.rpcResponseTimeHistogram.record(duration, { method: "getFeeHistory" }) + return result + }) + .mapErr((error) => { + if (safeClient.rpcErrorCounter) { + safeClient.rpcErrorCounter.add(1, { method: "getFeeHistory" }) + } + return error as GetFeeHistoryErrorType + }) + } + return safeClient } diff --git a/packages/txm/test/txm.test.ts b/packages/txm/test/txm.test.ts index d3913881b1..f7bff6c9fc 100644 --- a/packages/txm/test/txm.test.ts +++ b/packages/txm/test/txm.test.ts @@ -44,8 +44,10 @@ const txm = new TransactionManager({ abis: abis, gasEstimator: new TestGasEstimator(), retryPolicyManager: retryManager, - baseFeePercentageMargin: BASE_FEE_PERCENTAGE_MARGIN, - eip1559: ethereumDefaultEIP1559Parameters, + gas: { + baseFeePercentageMargin: BASE_FEE_PERCENTAGE_MARGIN, + eip1559: ethereumDefaultEIP1559Parameters, + }, metrics: { active: false, },