From cfa35231dba321015563a23d5704804d1b0064cb Mon Sep 17 00:00:00 2001 From: mikesposito Date: Thu, 23 Jul 2026 19:00:48 +0200 Subject: [PATCH] feat!: wire gas strategy to `ConfigRegistryController` --- .../src/config-registry-api-service/types.ts | 15 ++++++++- .../config-registry-controller/src/index.ts | 8 ++--- .../transaction-controller/jest.config.js | 6 ++-- packages/transaction-controller/package.json | 1 + .../src/TransactionController.ts | 12 +++++-- .../TransactionControllerIntegration.test.ts | 6 ++++ .../OptimismLayer1GasFeeFlow.test.ts | 19 +++++++++++ .../src/gas-flows/OptimismLayer1GasFeeFlow.ts | 8 +++++ .../gas-flows/OracleLayer1GasFeeFlow.test.ts | 31 +++++++++++++++++- .../src/gas-flows/OracleLayer1GasFeeFlow.ts | 32 ++++++++++++++++--- .../tsconfig.build.json | 3 ++ packages/transaction-controller/tsconfig.json | 3 ++ yarn.lock | 1 + 13 files changed, 129 insertions(+), 16 deletions(-) diff --git a/packages/config-registry-controller/src/config-registry-api-service/types.ts b/packages/config-registry-controller/src/config-registry-api-service/types.ts index f7eb1628ea7..fb1d483a2b9 100644 --- a/packages/config-registry-controller/src/config-registry-api-service/types.ts +++ b/packages/config-registry-controller/src/config-registry-api-service/types.ts @@ -7,8 +7,11 @@ import { optional, string, type, + union, + object, + literal, } from '@metamask/superstruct'; -import { CaipChainIdStruct } from '@metamask/utils'; +import { CaipChainIdStruct, StrictHexStruct } from '@metamask/utils'; const AssetSchema = type({ assetId: string(), @@ -41,6 +44,13 @@ const BlockExplorerUrlsSchema = type({ fallbacks: array(string()), }); +const GasEstimationStrategySchema = union([ + object({ + type: literal('l1Oracle'), + l1OracleAddress: optional(StrictHexStruct), + }), +]); + const ChainConfigSchema = type({ isActive: boolean(), isTestnet: boolean(), @@ -49,6 +59,7 @@ const ChainConfigSchema = type({ isDeprecated: boolean(), isDeletable: boolean(), priority: number(), + gasEstimationStrategy: optional(GasEstimationStrategySchema), }); /** @@ -78,6 +89,8 @@ export const RegistryConfigApiResponseSchema = type({ }), }); +export type GasEstimationStrategy = Infer; + export type RegistryNetworkConfig = Infer; export type RegistryConfigApiResponse = Infer< diff --git a/packages/config-registry-controller/src/index.ts b/packages/config-registry-controller/src/index.ts index 2d1e87d3662..86fede9ef3d 100644 --- a/packages/config-registry-controller/src/index.ts +++ b/packages/config-registry-controller/src/index.ts @@ -9,7 +9,7 @@ export type { export type { ConfigRegistryControllerStartPollingAction, ConfigRegistryControllerStopPollingAction, - ConfigRegistryControllerMethodActions, + ConfigRegistryControllerGetNetworkConfigByCaip2ChainIdAction, } from './ConfigRegistryController-method-action-types.js'; export { ConfigRegistryController, @@ -21,6 +21,7 @@ export type { FetchConfigResult, RegistryNetworkConfig, RegistryConfigApiResponse, + GasEstimationStrategy, } from './config-registry-api-service/types.js'; export type { ConfigRegistryApiServiceOptions, @@ -28,10 +29,7 @@ export type { ConfigRegistryApiServiceEvents, ConfigRegistryApiServiceMessenger, } from './config-registry-api-service/config-registry-api-service.js'; -export type { - ConfigRegistryApiServiceFetchConfigAction, - ConfigRegistryApiServiceMethodActions, -} from './config-registry-api-service/config-registry-api-service-method-action-types.js'; +export type { ConfigRegistryApiServiceFetchConfigAction } from './config-registry-api-service/config-registry-api-service-method-action-types.js'; export type { NetworkFilterOptions } from './config-registry-api-service/filters.js'; export { ConfigRegistryApiService } from './config-registry-api-service/config-registry-api-service.js'; export { filterNetworks } from './config-registry-api-service/filters.js'; diff --git a/packages/transaction-controller/jest.config.js b/packages/transaction-controller/jest.config.js index 0a7110162b2..fba296e6f94 100644 --- a/packages/transaction-controller/jest.config.js +++ b/packages/transaction-controller/jest.config.js @@ -17,9 +17,9 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 93.49, - functions: 93.77, - lines: 97.15, + branches: 93.51, + functions: 93.79, + lines: 97.16, statements: 97.12, }, }, diff --git a/packages/transaction-controller/package.json b/packages/transaction-controller/package.json index a91099c8ae2..e769bf2f764 100644 --- a/packages/transaction-controller/package.json +++ b/packages/transaction-controller/package.json @@ -64,6 +64,7 @@ "@metamask/accounts-controller": "^39.0.5", "@metamask/approval-controller": "^9.0.2", "@metamask/base-controller": "^9.1.0", + "@metamask/config-registry-controller": "^0.4.1", "@metamask/controller-utils": "^12.3.0", "@metamask/core-backend": "^7.0.0", "@metamask/gas-fee-controller": "^26.3.0", diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index ea765290515..0b41ffeb28d 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -16,6 +16,7 @@ import type { StateMetadata, } from '@metamask/base-controller'; import { BaseController } from '@metamask/base-controller'; +import { ConfigRegistryControllerGetNetworkConfigByCaip2ChainIdAction } from '@metamask/config-registry-controller'; import { ApprovalType, ORIGIN_METAMASK, @@ -64,7 +65,7 @@ import { JsonRpcError, } from '@metamask/rpc-errors'; import type { Hex, Json } from '@metamask/utils'; -import { add0x } from '@metamask/utils'; +import { add0x, hexToNumber } from '@metamask/utils'; // This package purposefully relies on Node's EventEmitter module. // eslint-disable-next-line import-x/no-nodejs-modules import { EventEmitter } from 'events'; @@ -435,6 +436,7 @@ export type AllowedActions = | AccountsControllerGetSelectedAccountAction | AccountsControllerGetStateAction | ApprovalControllerAddRequestAction + | ConfigRegistryControllerGetNetworkConfigByCaip2ChainIdAction | GasFeeControllerFetchGasFeeEstimatesAction | KeyringControllerGetStateAction | KeyringControllerSignEip7702AuthorizationAction @@ -3959,7 +3961,13 @@ export class TransactionController extends BaseController< #getLayer1GasFeeFlows(): Layer1GasFeeFlow[] { return [ new MantleLayer1GasFeeFlow(), - new OptimismLayer1GasFeeFlow(), + new OptimismLayer1GasFeeFlow({ + getRegistryGasStrategyForChain: (chainId: Hex) => + this.messenger.call( + 'ConfigRegistryController:getNetworkConfigByCaip2ChainId', + `eip155:${hexToNumber(chainId)}`, + )?.config.gasEstimationStrategy, + }), new ScrollLayer1GasFeeFlow(), ]; } diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index c815c116a6e..43d1f750a4a 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -274,6 +274,7 @@ const setupController = async ( 'AccountsController:getSelectedAccount', 'AccountsController:getState', 'ApprovalController:addRequest', + 'ConfigRegistryController:getNetworkConfigByCaip2ChainId', 'GasFeeController:fetchGasFeeEstimates', 'KeyringController:signTransaction', 'NetworkController:findNetworkClientIdByChainId', @@ -319,6 +320,11 @@ const setupController = async ( transaction.toJSON() as TypedTxData, ); + rootMessenger.registerActionHandler( + 'ConfigRegistryController:getNetworkConfigByCaip2ChainId', + () => undefined, + ); + const options: TransactionControllerOptions = { disableSwaps: false, isAutomaticGasFeeUpdateEnabled: () => true, diff --git a/packages/transaction-controller/src/gas-flows/OptimismLayer1GasFeeFlow.test.ts b/packages/transaction-controller/src/gas-flows/OptimismLayer1GasFeeFlow.test.ts index 2f8c0d84459..2d55a1f6022 100644 --- a/packages/transaction-controller/src/gas-flows/OptimismLayer1GasFeeFlow.test.ts +++ b/packages/transaction-controller/src/gas-flows/OptimismLayer1GasFeeFlow.test.ts @@ -1,3 +1,4 @@ +import { GasEstimationStrategy } from '@metamask/config-registry-controller'; import * as ControllerUtils from '@metamask/controller-utils'; import { hexToNumber } from '@metamask/utils'; import type { Hex } from '@metamask/utils'; @@ -91,6 +92,24 @@ describe('OptimismLayer1GasFeeFlow', () => { ).toBe(true); }); + it('returns true when the config registry lists the gas strategy for the chain as `l1Oracle`', async () => { + const flow = new OptimismLayer1GasFeeFlow({ + getRegistryGasStrategyForChain: (): GasEstimationStrategy => ({ + type: 'l1Oracle', + l1OracleAddress: '0x123', + }), + }); + const transactionMeta = createTransaction(CHAIN_IDS.OPTIMISM); + + expect( + await flow.matchesTransaction({ + transactionMeta, + messenger, + }), + ).toBe(true); + expect(handleFetchMock).not.toHaveBeenCalled(); + }); + it('falls back to static list when remote list omits the chain', async () => { handleFetchMock.mockResolvedValue({ fullSupport: [], diff --git a/packages/transaction-controller/src/gas-flows/OptimismLayer1GasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/OptimismLayer1GasFeeFlow.ts index 8378bfcdf4b..4d75ec90893 100644 --- a/packages/transaction-controller/src/gas-flows/OptimismLayer1GasFeeFlow.ts +++ b/packages/transaction-controller/src/gas-flows/OptimismLayer1GasFeeFlow.ts @@ -39,6 +39,14 @@ export class OptimismLayer1GasFeeFlow extends OracleLayer1GasFeeFlow { transactionMeta: TransactionMeta; messenger: TransactionControllerMessenger; }): Promise { + const registryGasStrategy = this.getRegistryGasStrategyForChain?.( + transactionMeta.chainId, + ); + + if (registryGasStrategy?.type === 'l1Oracle') { + return true; + } + const chainIdAsNumber = hexToNumber(transactionMeta.chainId); const supportedChains = await this.#fetchOptimismSupportedChains(); diff --git a/packages/transaction-controller/src/gas-flows/OracleLayer1GasFeeFlow.test.ts b/packages/transaction-controller/src/gas-flows/OracleLayer1GasFeeFlow.test.ts index fd73bf17630..d6d2c55084b 100644 --- a/packages/transaction-controller/src/gas-flows/OracleLayer1GasFeeFlow.test.ts +++ b/packages/transaction-controller/src/gas-flows/OracleLayer1GasFeeFlow.test.ts @@ -13,6 +13,7 @@ import type { Layer1GasFeeFlowRequest, TransactionMeta } from '../types.js'; import { rpcRequest } from '../utils/provider.js'; import { bnFromHex, padHexToEvenLength } from '../utils/utils.js'; import { OracleLayer1GasFeeFlow } from './OracleLayer1GasFeeFlow.js'; +import { GasEstimationStrategy } from '../../../config-registry-controller/src/config-registry-api-service/types.js'; jest.mock('../utils/provider'); @@ -258,7 +259,35 @@ describe('OracleLayer1GasFeeFlow', () => { }); }); - it('uses default oracle configuration when subclasses do not override helpers', async () => { + it('uses the registry network configuration oracle address when subclasses do not override helpers', async () => { + const serializedTransactionMock = Buffer.from( + SERIALIZED_TRANSACTION_MOCK, + 'hex', + ); + + const typedTransactionMock = createMockTypedTransaction( + serializedTransactionMock, + ); + + jest + .spyOn(TransactionFactory, 'fromTxData') + .mockReturnValueOnce(typedTransactionMock); + + const flow = new DefaultOracleLayer1GasFeeFlow({ + getRegistryGasStrategyForChain: (): GasEstimationStrategy => ({ + type: 'l1Oracle', + l1OracleAddress: '0x123', + }), + }); + await flow.getLayer1Fee(request); + + expect(rpcRequestMock).toHaveBeenCalledTimes(1); + const { to } = getEthCallObject(rpcRequestMock.mock.calls[0]); + expect(to).toBe('0x123'); + expect(typedTransactionMock.sign).not.toHaveBeenCalled(); + }); + + it('uses default oracle configuration when subclasses do not override helpers and registry config is undefined', async () => { const serializedTransactionMock = Buffer.from( SERIALIZED_TRANSACTION_MOCK, 'hex', diff --git a/packages/transaction-controller/src/gas-flows/OracleLayer1GasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/OracleLayer1GasFeeFlow.ts index 034fb69473d..5a49accbf54 100644 --- a/packages/transaction-controller/src/gas-flows/OracleLayer1GasFeeFlow.ts +++ b/packages/transaction-controller/src/gas-flows/OracleLayer1GasFeeFlow.ts @@ -1,4 +1,5 @@ import { Interface } from '@ethersproject/abi'; +import type { GasEstimationStrategy } from '@metamask/config-registry-controller'; import type { Hex } from '@metamask/utils'; import { add0x, createModuleLogger } from '@metamask/utils'; import BN from 'bn.js'; @@ -70,20 +71,43 @@ const DEFAULT_GAS_PRICE_ORACLE_ADDRESS = const GAS_PRICE_ORACLE_INTERFACE = new Interface(GAS_PRICE_ORACLE_ABI); +type GetRegistryGasStrategyForChain = ( + chainId: Hex, +) => GasEstimationStrategy | undefined; + /** * Layer 1 gas fee flow that obtains gas fee estimate using an oracle smart contract. */ export abstract class OracleLayer1GasFeeFlow implements Layer1GasFeeFlow { + /** + * Optional lookup of the Config Registry network configuration, + * used to source the layer 1 oracle address from the registry before + * falling back to bundled constants. + */ + protected readonly getRegistryGasStrategyForChain?: GetRegistryGasStrategyForChain; + + constructor({ + getRegistryGasStrategyForChain, + }: { + getRegistryGasStrategyForChain?: GetRegistryGasStrategyForChain; + } = {}) { + this.getRegistryGasStrategyForChain = getRegistryGasStrategyForChain; + } + /** * Resolves the oracle address for the given chain. Subclasses can override * this method to provide chain-specific oracle addresses. By default, this - * returns the standard OP Stack gas price oracle address. + * uses the registry-provided oracle address when present, otherwise the + * standard OP Stack gas price oracle address. * - * @param _chainId - The chain ID to resolve the oracle address for. + * @param chainId - The chain ID to resolve the oracle address for. * @returns The oracle address for the given chain. */ - protected getOracleAddressForChain(_chainId: Hex): Hex { - return DEFAULT_GAS_PRICE_ORACLE_ADDRESS; + protected getOracleAddressForChain(chainId: Hex): Hex { + return ( + this.getRegistryGasStrategyForChain?.(chainId)?.l1OracleAddress ?? + DEFAULT_GAS_PRICE_ORACLE_ADDRESS + ); } /** diff --git a/packages/transaction-controller/tsconfig.build.json b/packages/transaction-controller/tsconfig.build.json index 598ddf7d483..64966b4de49 100644 --- a/packages/transaction-controller/tsconfig.build.json +++ b/packages/transaction-controller/tsconfig.build.json @@ -15,6 +15,9 @@ { "path": "../base-controller/tsconfig.build.json" }, + { + "path": "../config-registry-controller/tsconfig.build.json" + }, { "path": "../controller-utils/tsconfig.build.json" }, diff --git a/packages/transaction-controller/tsconfig.json b/packages/transaction-controller/tsconfig.json index fd2cf57a53d..a5735fd752d 100644 --- a/packages/transaction-controller/tsconfig.json +++ b/packages/transaction-controller/tsconfig.json @@ -14,6 +14,9 @@ { "path": "../base-controller" }, + { + "path": "../config-registry-controller" + }, { "path": "../controller-utils" }, diff --git a/yarn.lock b/yarn.lock index 13788425946..59fb17d08f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9035,6 +9035,7 @@ __metadata: "@metamask/approval-controller": "npm:^9.0.2" "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" + "@metamask/config-registry-controller": "npm:^0.4.1" "@metamask/connectivity-controller": "npm:^0.3.0" "@metamask/controller-utils": "npm:^12.3.0" "@metamask/core-backend": "npm:^7.0.0"