diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index eb181b8f82..c5b5392900 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Include Mantle operator fee in the displayed L1 gas estimate when simulated `gasUsed` is unavailable, by falling back to the transaction's gas limit ([#8837](https://github.com/MetaMask/core/pull/8837)) + - Adds a `protected getOperatorFeeGas` hook on `OracleLayer1GasFeeFlow` that subclasses can override to supply a fallback value. The default behaviour is unchanged (returns `transactionMeta.gasUsed`). + - `MantleLayer1GasFeeFlow` overrides the hook with `gasUsed ?? txParams.gas ?? txParams.gasLimit`, so the operator-fee oracle is called with the gas limit when `gasUsed` is missing. Gas limit is an upper bound on actual gas used, so the operator fee is over-estimated rather than under-reported. + ## [66.0.0] ### Changed diff --git a/packages/transaction-controller/src/gas-flows/MantleLayer1GasFeeFlow.test.ts b/packages/transaction-controller/src/gas-flows/MantleLayer1GasFeeFlow.test.ts index 5980118a4e..e6528702e9 100644 --- a/packages/transaction-controller/src/gas-flows/MantleLayer1GasFeeFlow.test.ts +++ b/packages/transaction-controller/src/gas-flows/MantleLayer1GasFeeFlow.test.ts @@ -180,7 +180,85 @@ describe('MantleLayer1GasFeeFlow', () => { }); }); - it('returns converted L1 fee when no gasUsed (no operator fee)', async () => { + it('falls back to txParams.gas for operator fee when gasUsed is missing', async () => { + contractGetOperatorFeeMock.mockResolvedValueOnce( + bnFromHex(OPERATOR_FEE_MOCK), + ); + + jest + .spyOn(TransactionFactory, 'fromTxData') + .mockReturnValueOnce( + createMockTypedTransaction( + Buffer.from(SERIALIZED_TRANSACTION_MOCK, 'hex'), + ), + ); + + const flow = new MantleLayer1GasFeeFlow(); + const response = await flow.getLayer1Fee(request); + + const expectedL1FeeInMnt = bnFromHex(L1_FEE_MOCK).mul(TOKEN_RATIO_MOCK); + const expectedTotal = expectedL1FeeInMnt.add( + bnFromHex(OPERATOR_FEE_MOCK), + ); + + expect(contractGetOperatorFeeMock).toHaveBeenCalledTimes(1); + expect(contractGetOperatorFeeMock).toHaveBeenCalledWith( + TRANSACTION_PARAMS_MOCK.gas, + ); + expect(response).toStrictEqual({ + layer1Fee: add0x(padHexToEvenLength(expectedTotal.toString(16))), + }); + }); + + it('falls back to txParams.gasLimit for operator fee when gasUsed and txParams.gas are missing', async () => { + const gasLimit = '0x9999'; + request = { + ...request, + transactionMeta: { + ...request.transactionMeta, + txParams: { + from: TRANSACTION_PARAMS_MOCK.from, + gasLimit, + }, + }, + }; + + contractGetOperatorFeeMock.mockResolvedValueOnce( + bnFromHex(OPERATOR_FEE_MOCK), + ); + + jest + .spyOn(TransactionFactory, 'fromTxData') + .mockReturnValueOnce( + createMockTypedTransaction( + Buffer.from(SERIALIZED_TRANSACTION_MOCK, 'hex'), + ), + ); + + const flow = new MantleLayer1GasFeeFlow(); + const response = await flow.getLayer1Fee(request); + + const expectedL1FeeInMnt = bnFromHex(L1_FEE_MOCK).mul(TOKEN_RATIO_MOCK); + const expectedTotal = expectedL1FeeInMnt.add( + bnFromHex(OPERATOR_FEE_MOCK), + ); + + expect(contractGetOperatorFeeMock).toHaveBeenCalledTimes(1); + expect(contractGetOperatorFeeMock).toHaveBeenCalledWith(gasLimit); + expect(response).toStrictEqual({ + layer1Fee: add0x(padHexToEvenLength(expectedTotal.toString(16))), + }); + }); + + it('skips operator fee when no gasUsed, txParams.gas, or txParams.gasLimit is available', async () => { + request = { + ...request, + transactionMeta: { + ...request.transactionMeta, + txParams: { from: TRANSACTION_PARAMS_MOCK.from }, + }, + }; + jest .spyOn(TransactionFactory, 'fromTxData') .mockReturnValueOnce( diff --git a/packages/transaction-controller/src/gas-flows/MantleLayer1GasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/MantleLayer1GasFeeFlow.ts index d353659c83..faae3cf509 100644 --- a/packages/transaction-controller/src/gas-flows/MantleLayer1GasFeeFlow.ts +++ b/packages/transaction-controller/src/gas-flows/MantleLayer1GasFeeFlow.ts @@ -46,6 +46,16 @@ export class MantleLayer1GasFeeFlow extends OracleLayer1GasFeeFlow { return MANTLE_CHAIN_IDS.includes(transactionMeta.chainId); } + protected override getOperatorFeeGas( + transactionMeta: TransactionMeta, + ): string | undefined { + return ( + transactionMeta.gasUsed ?? + transactionMeta.txParams.gas ?? + transactionMeta.txParams.gasLimit + ); + } + protected override async transformOracleFee( oracleFee: BN, request: Layer1GasFeeFlowRequest, diff --git a/packages/transaction-controller/src/gas-flows/OracleLayer1GasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/OracleLayer1GasFeeFlow.ts index 21bcb2c14f..e76586b2a7 100644 --- a/packages/transaction-controller/src/gas-flows/OracleLayer1GasFeeFlow.ts +++ b/packages/transaction-controller/src/gas-flows/OracleLayer1GasFeeFlow.ts @@ -174,18 +174,33 @@ export abstract class OracleLayer1GasFeeFlow implements Layer1GasFeeFlow { return toBN(result); } + /** + * Returns the gas value to pass to the operator-fee oracle, or undefined + * to skip the operator-fee call entirely. Defaults to the simulated + * `gasUsed` on the transaction. Subclasses can override to supply a + * fallback (e.g. estimated gas limit) when `gasUsed` is unavailable. + * + * @param transactionMeta - The transaction metadata. + * @returns The gas value, or undefined to skip the operator-fee call. + */ + protected getOperatorFeeGas( + transactionMeta: TransactionMeta, + ): string | undefined { + return transactionMeta.gasUsed; + } + async #getOperatorLayer1GasFee( contract: Contract, transactionMeta: TransactionMeta, ): Promise { - const { gasUsed } = transactionMeta; + const gas = this.getOperatorFeeGas(transactionMeta); - if (!gasUsed) { + if (!gas) { return ZERO; } try { - const result = await contract.getOperatorFee(gasUsed); + const result = await contract.getOperatorFee(gas); if (result === undefined) { return ZERO;