diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index dae634f91df..1181b72ee6f 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Strip redundant `gasPrice` from fee-market transactions (type `0x2`/`0x4`) instead of rejecting them, fixing compatibility with RPCs that return both `gasPrice` and EIP-1559 fields (e.g. Arbitrum) ([#7877](https://github.com/MetaMask/core/issues/7877)) + ### Added - Add optional `submissionMethod` property and `TransactionSubmissionMethod` enum to `TransactionMeta` ([#8375](https://github.com/MetaMask/core/pull/8375)) diff --git a/packages/transaction-controller/src/utils/validation.test.ts b/packages/transaction-controller/src/utils/validation.test.ts index 05e1b802160..1184399daae 100644 --- a/packages/transaction-controller/src/utils/validation.test.ts +++ b/packages/transaction-controller/src/utils/validation.test.ts @@ -315,6 +315,56 @@ describe('validation', () => { ); }); + it('strips gasPrice instead of throwing when type is fee-market and EIP-1559 fields are present', () => { + const txParams = { + from: FROM_MOCK, + to: TO_MOCK, + gasPrice: '0x01', + maxFeePerGas: '0x02', + maxPriorityFeePerGas: '0x01', + type: TransactionEnvelopeType.feeMarket, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; + + expect(() => validateTxParams(txParams)).not.toThrow(); + expect(txParams.gasPrice).toBeUndefined(); + expect(txParams.maxFeePerGas).toBe('0x02'); + }); + + it('strips gasPrice instead of throwing when type is setCode and EIP-1559 fields are present', () => { + const txParams = { + from: FROM_MOCK, + to: TO_MOCK, + gasPrice: '0x01', + maxFeePerGas: '0x02', + maxPriorityFeePerGas: '0x01', + type: TransactionEnvelopeType.setCode, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; + + expect(() => validateTxParams(txParams)).not.toThrow(); + expect(txParams.gasPrice).toBeUndefined(); + }); + + it('still throws if gasPrice and EIP-1559 fields are present but no explicit type', () => { + expect(() => + validateTxParams({ + from: FROM_MOCK, + to: TO_MOCK, + gasPrice: '0x01', + maxFeePerGas: '0x01', + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any), + ).toThrow( + rpcErrors.invalidParams( + 'Invalid transaction params: specified gasPrice but also included maxFeePerGas, these cannot be mixed', + ), + ); + }); + it('throws if gasPrice, maxPriorityFeePerGas or maxFeePerGas is not a valid hexadecimal string', () => { expect(() => validateTxParams({ diff --git a/packages/transaction-controller/src/utils/validation.ts b/packages/transaction-controller/src/utils/validation.ts index 203aaf075a0..1be7e3a6768 100644 --- a/packages/transaction-controller/src/utils/validation.ts +++ b/packages/transaction-controller/src/utils/validation.ts @@ -353,9 +353,28 @@ function validateParamChainId( /** * Validates gas values. * + * Some RPC providers (e.g. Arbitrum's eth_fillTransaction) return both + * gasPrice and EIP-1559 fee fields. When the envelope type is explicitly + * fee-market (0x2/0x4), strip the redundant gasPrice before validating + * mutual exclusivity, matching the normalization already done elsewhere + * in the codebase (GasFeePoller, updateGasFees, retry utils). + * * @param txParams - The transaction parameters to validate. */ function validateGasFeeParams(txParams: TransactionParams): void { + const type = txParams.type as TransactionEnvelopeType | undefined; + + // When the envelope type explicitly indicates a fee-market transaction, + // strip gasPrice rather than rejecting — some RPCs return both fields. + if ( + txParams.gasPrice && + (txParams.maxFeePerGas || txParams.maxPriorityFeePerGas) && + type && + TRANSACTION_ENVELOPE_TYPES_FEE_MARKET.includes(type) + ) { + delete txParams.gasPrice; + } + if (txParams.gasPrice) { ensureProperTransactionEnvelopeTypeProvided(txParams, 'gasPrice'); ensureMutuallyExclusiveFieldsNotProvided(