diff --git a/packages/gas-fee-controller/src/GasFeeController.test.ts b/packages/gas-fee-controller/src/GasFeeController.test.ts index 866d8e85864..7d52f8981f8 100644 --- a/packages/gas-fee-controller/src/GasFeeController.test.ts +++ b/packages/gas-fee-controller/src/GasFeeController.test.ts @@ -1,8 +1,10 @@ import { ControllerMessenger } from '@metamask/base-controller'; import { NetworkType, toHex } from '@metamask/controller-utils'; import EthQuery from '@metamask/eth-query'; -import { NetworkController } from '@metamask/network-controller'; +import { NetworkController, NetworkStatus } from '@metamask/network-controller'; import type { + NetworkControllerGetEIP1559CompatibilityAction, + NetworkControllerGetNetworkClientByIdAction, NetworkControllerGetStateAction, NetworkControllerNetworkDidChangeEvent, NetworkControllerStateChangeEvent, @@ -40,7 +42,10 @@ const mockedDetermineGasFeeCalculations = const name = 'GasFeeController'; type MainControllerMessenger = ControllerMessenger< - GetGasFeeState | NetworkControllerGetStateAction, + | GetGasFeeState + | NetworkControllerGetStateAction + | NetworkControllerGetNetworkClientByIdAction + | NetworkControllerGetEIP1559CompatibilityAction, | GasFeeStateChange | NetworkControllerStateChangeEvent | NetworkControllerNetworkDidChangeEvent @@ -61,7 +66,11 @@ const setupNetworkController = async ({ }) => { const restrictedMessenger = unrestrictedMessenger.getRestricted({ name: 'NetworkController', - allowedActions: ['NetworkController:getState'], + allowedActions: [ + 'NetworkController:getState', + 'NetworkController:getNetworkClientById', + 'NetworkController:getEIP1559Compatibility', + ], allowedEvents: [ 'NetworkController:stateChange', 'NetworkController:networkDidChange', @@ -89,7 +98,11 @@ const getRestrictedMessenger = ( ) => { const messenger = controllerMessenger.getRestricted({ name, - allowedActions: ['NetworkController:getState'], + allowedActions: [ + 'NetworkController:getState', + 'NetworkController:getNetworkClientById', + 'NetworkController:getEIP1559Compatibility', + ], allowedEvents: ['NetworkController:stateChange'], }); @@ -216,6 +229,7 @@ describe('GasFeeController', () => { * @param options.networkControllerState - State object to initialize * NetworkController with. * @param options.interval - The polling interval. + * @param options.state - The initial GasFeeController state */ async function setupGasFeeController({ getIsEIP1559Compatible = jest.fn().mockResolvedValue(true), @@ -227,6 +241,7 @@ describe('GasFeeController', () => { clientId, getChainId, networkControllerState = {}, + state, interval, }: { getChainId?: jest.Mock; @@ -236,6 +251,7 @@ describe('GasFeeController', () => { EIP1559APIEndpoint?: string; clientId?: string; networkControllerState?: Partial; + state?: GasFeeState; interval?: number; } = {}) { const controllerMessenger = getControllerMessenger(); @@ -253,6 +269,7 @@ describe('GasFeeController', () => { getCurrentNetworkEIP1559Compatibility: getIsEIP1559Compatible, // change this for networkDetails.state.networkDetails.isEIP1559Compatible ??? legacyAPIEndpoint, EIP1559APIEndpoint, + state, clientId, interval, }); @@ -851,4 +868,91 @@ describe('GasFeeController', () => { }); }); }); + describe('executePoll', () => { + it('should call determineGasFeeCalculations with a URL that contains the chain ID', async () => { + await setupGasFeeController({ + getIsEIP1559Compatible: jest.fn().mockResolvedValue(false), + getCurrentNetworkLegacyGasAPICompatibility: jest + .fn() + .mockReturnValue(true), + legacyAPIEndpoint: 'https://some-legacy-endpoint/', + EIP1559APIEndpoint: 'https://some-eip-1559-endpoint/', + networkControllerState: { + networksMetadata: { + mainnet: { + EIPS: { + 1559: true, + }, + status: NetworkStatus.Available, + }, + sepolia: { + EIPS: { + 1559: true, + }, + status: NetworkStatus.Available, + }, + }, + }, + clientId: '99999', + }); + + await gasFeeController.executePoll('mainnet'); + await gasFeeController.executePoll('sepolia'); + + expect(mockedDetermineGasFeeCalculations).toHaveBeenCalledWith( + expect.objectContaining({ + fetchGasEstimatesUrl: 'https://some-eip-1559-endpoint/1', + }), + ); + expect(mockedDetermineGasFeeCalculations).toHaveBeenCalledWith( + expect.objectContaining({ + fetchGasEstimatesUrl: 'https://some-eip-1559-endpoint/11155111', + }), + ); + expect(mockedDetermineGasFeeCalculations).not.toHaveBeenCalledWith( + expect.objectContaining({ + fetchGasEstimatesUrl: 'https://some-eip-1559-endpoint/5', + }), + ); + }); + }); + + describe('polling (by networkClientId)', () => { + it('should call determineGasFeeCalculations (via executePoll) with a URL that contains the chain ID after the interval passed via the constructor', async () => { + const pollingInterval = 10000; + await setupGasFeeController({ + getIsEIP1559Compatible: jest.fn().mockResolvedValue(false), + getCurrentNetworkLegacyGasAPICompatibility: jest + .fn() + .mockReturnValue(true), + legacyAPIEndpoint: 'https://some-legacy-endpoint/', + EIP1559APIEndpoint: 'https://some-eip-1559-endpoint/', + networkControllerState: { + networksMetadata: { + goerli: { + EIPS: { + 1559: true, + }, + status: NetworkStatus.Available, + }, + }, + }, + clientId: '99999', + interval: pollingInterval, + }); + + gasFeeController.startPollingByNetworkClientId('goerli'); + await clock.tickAsync(pollingInterval / 2); + expect(mockedDetermineGasFeeCalculations).not.toHaveBeenCalled(); + await clock.tickAsync(pollingInterval / 2); + expect(mockedDetermineGasFeeCalculations).toHaveBeenCalledWith( + expect.objectContaining({ + fetchGasEstimatesUrl: 'https://some-eip-1559-endpoint/5', + }), + ); + expect( + gasFeeController.state.gasFeeEstimatesByChainId?.['0x5'], + ).toStrictEqual(buildMockGasFeeStateFeeMarket()); + }); + }); }); diff --git a/packages/gas-fee-controller/src/GasFeeController.ts b/packages/gas-fee-controller/src/GasFeeController.ts index 2f799376528..a81f77dff32 100644 --- a/packages/gas-fee-controller/src/GasFeeController.ts +++ b/packages/gas-fee-controller/src/GasFeeController.ts @@ -1,13 +1,15 @@ import type { RestrictedControllerMessenger } from '@metamask/base-controller'; -import { BaseControllerV2 } from '@metamask/base-controller'; import { convertHexToDecimal, safelyExecute } from '@metamask/controller-utils'; import EthQuery from '@metamask/eth-query'; import type { + NetworkControllerGetEIP1559CompatibilityAction, + NetworkControllerGetNetworkClientByIdAction, NetworkControllerGetStateAction, NetworkControllerStateChangeEvent, NetworkState, ProviderProxy, } from '@metamask/network-controller'; +import { PollingController } from '@metamask/polling-controller'; import type { Hex } from '@metamask/utils'; import type { Patch } from 'immer'; import { v1 as random } from 'uuid'; @@ -150,6 +152,10 @@ type FallbackGasFeeEstimates = { }; const metadata = { + gasFeeEstimatesByChainId: { + persist: true, + anonymous: false, + }, gasFeeEstimates: { persist: true, anonymous: false }, estimatedGasFeeTimeBounds: { persist: true, anonymous: false }, gasEstimateType: { persist: true, anonymous: false }, @@ -190,12 +196,18 @@ export type FetchGasFeeEstimateOptions = { * @property gasFeeEstimates - Gas fee estimate data based on new EIP-1559 properties * @property estimatedGasFeeTimeBounds - Estimates representing the minimum and maximum */ -export type GasFeeState = +export type SingleChainGasFeeState = | GasFeeStateEthGasPrice | GasFeeStateFeeMarket | GasFeeStateLegacy | GasFeeStateNoEstimates; +export type GasFeeEstimatesByChainId = { + gasFeeEstimatesByChainId?: Record; +}; + +export type GasFeeState = GasFeeEstimatesByChainId & SingleChainGasFeeState; + const name = 'GasFeeController'; export type GasFeeStateChange = { @@ -210,13 +222,19 @@ export type GetGasFeeState = { type GasFeeMessenger = RestrictedControllerMessenger< typeof name, - GetGasFeeState | NetworkControllerGetStateAction, + | GetGasFeeState + | NetworkControllerGetStateAction + | NetworkControllerGetNetworkClientByIdAction + | NetworkControllerGetEIP1559CompatibilityAction, GasFeeStateChange | NetworkControllerStateChangeEvent, - NetworkControllerGetStateAction['type'], + | NetworkControllerGetStateAction['type'] + | NetworkControllerGetNetworkClientByIdAction['type'] + | NetworkControllerGetEIP1559CompatibilityAction['type'], NetworkControllerStateChangeEvent['type'] >; const defaultState: GasFeeState = { + gasFeeEstimatesByChainId: {}, gasFeeEstimates: {}, estimatedGasFeeTimeBounds: {}, gasEstimateType: GAS_ESTIMATE_TYPES.NONE, @@ -225,7 +243,7 @@ const defaultState: GasFeeState = { /** * Controller that retrieves gas fee estimate data and polls for updated data on a set interval */ -export class GasFeeController extends BaseControllerV2< +export class GasFeeController extends PollingController< typeof name, GasFeeState, GasFeeMessenger @@ -311,6 +329,7 @@ export class GasFeeController extends BaseControllerV2< state: { ...defaultState, ...state }, }); this.intervalDelay = interval; + this.setIntervalLength(interval); this.pollTokens = new Set(); this.getCurrentNetworkEIP1559Compatibility = getCurrentNetworkEIP1559Compatibility; @@ -373,6 +392,63 @@ export class GasFeeController extends BaseControllerV2< return _pollToken; } + async #fetchGasFeeEstimateForNetworkClientId(networkClientId: string) { + let isEIP1559Compatible = false; + + const networkClient = this.messagingSystem.call( + 'NetworkController:getNetworkClientById', + networkClientId, + ); + + const isLegacyGasAPICompatible = + networkClient.configuration.chainId === '0x38'; + + const decimalChainId = convertHexToDecimal( + networkClient.configuration.chainId, + ); + + try { + const result = await this.messagingSystem.call( + 'NetworkController:getEIP1559Compatibility', + networkClientId, + ); + isEIP1559Compatible = result || false; + } catch { + isEIP1559Compatible = false; + } + + const ethQuery = new EthQuery(networkClient.provider); + + const gasFeeCalculations = await determineGasFeeCalculations({ + isEIP1559Compatible, + isLegacyGasAPICompatible, + fetchGasEstimates, + fetchGasEstimatesUrl: this.EIP1559APIEndpoint.replace( + '', + `${decimalChainId}`, + ), + fetchGasEstimatesViaEthFeeHistory, + fetchLegacyGasPriceEstimates, + fetchLegacyGasPriceEstimatesUrl: this.legacyAPIEndpoint.replace( + '', + `${decimalChainId}`, + ), + fetchEthGasPriceEstimate, + calculateTimeEstimate, + clientId: this.clientId, + ethQuery, + }); + + this.update((state) => { + state.gasFeeEstimatesByChainId = state.gasFeeEstimatesByChainId || {}; + state.gasFeeEstimatesByChainId[networkClient.configuration.chainId] = { + gasFeeEstimates: gasFeeCalculations.gasFeeEstimates, + estimatedGasFeeTimeBounds: gasFeeCalculations.estimatedGasFeeTimeBounds, + gasEstimateType: gasFeeCalculations.gasEstimateType, + } as any; + }); + } + /** * Gets and sets gasFeeEstimates in state. * @@ -470,6 +546,10 @@ export class GasFeeController extends BaseControllerV2< }, this.intervalDelay); } + async executePoll(networkClientId: string): Promise { + await this.#fetchGasFeeEstimateForNetworkClientId(networkClientId); + } + private resetState() { this.update(() => { return defaultState; diff --git a/packages/gas-fee-controller/tsconfig.build.json b/packages/gas-fee-controller/tsconfig.build.json index ac0df4920c6..0e520317bbc 100644 --- a/packages/gas-fee-controller/tsconfig.build.json +++ b/packages/gas-fee-controller/tsconfig.build.json @@ -8,7 +8,8 @@ "references": [ { "path": "../base-controller/tsconfig.build.json" }, { "path": "../controller-utils/tsconfig.build.json" }, - { "path": "../network-controller/tsconfig.build.json" } + { "path": "../network-controller/tsconfig.build.json" }, + { "path": "../polling-controller/tsconfig.build.json" } ], "include": ["../../types", "./src"] } diff --git a/packages/gas-fee-controller/tsconfig.json b/packages/gas-fee-controller/tsconfig.json index 4bbb0be81b1..ce385aec37b 100644 --- a/packages/gas-fee-controller/tsconfig.json +++ b/packages/gas-fee-controller/tsconfig.json @@ -6,7 +6,8 @@ "references": [ { "path": "../base-controller" }, { "path": "../controller-utils" }, - { "path": "../network-controller" } + { "path": "../network-controller" }, + { "path": "../polling-controller" } ], "include": ["../../types", "./src"] } diff --git a/packages/network-controller/src/NetworkController.ts b/packages/network-controller/src/NetworkController.ts index c41ab8840e9..67cfcce8390 100644 --- a/packages/network-controller/src/NetworkController.ts +++ b/packages/network-controller/src/NetworkController.ts @@ -430,11 +430,17 @@ export type NetworkControllerGetNetworkClientByIdAction = { handler: NetworkController['getNetworkClientById']; }; +export type NetworkControllerGetEIP1559CompatibilityAction = { + type: `NetworkController:getEIP1559Compatibility`; + handler: NetworkController['getEIP1559Compatibility']; +}; + export type NetworkControllerActions = | NetworkControllerGetStateAction | NetworkControllerGetProviderConfigAction | NetworkControllerGetEthQueryAction - | NetworkControllerGetNetworkClientByIdAction; + | NetworkControllerGetNetworkClientByIdAction + | NetworkControllerGetEIP1559CompatibilityAction; export type NetworkControllerMessenger = RestrictedControllerMessenger< typeof name, @@ -579,6 +585,11 @@ export class NetworkController extends BaseControllerV2< this.getNetworkClientById.bind(this), ); + this.messagingSystem.registerActionHandler( + `${this.name}:getEIP1559Compatibility`, + this.getEIP1559Compatibility.bind(this), + ); + this.#previousProviderConfig = this.state.providerConfig; } @@ -997,7 +1008,7 @@ export class NetworkController extends BaseControllerV2< */ async getEIP1559Compatibility(networkClientId?: NetworkClientId) { if (networkClientId) { - return this.get1555CompatibilityWithNetworkClientId(networkClientId); + return this.get1559CompatibilityWithNetworkClientId(networkClientId); } if (!this.#ethQuery) { return false; @@ -1022,7 +1033,7 @@ export class NetworkController extends BaseControllerV2< return isEIP1559Compatible; } - async get1555CompatibilityWithNetworkClientId( + async get1559CompatibilityWithNetworkClientId( networkClientId: NetworkClientId, ) { let metadata = this.state.networksMetadata[networkClientId]; diff --git a/yarn.lock b/yarn.lock index 5c536d5af33..b3ed1d1f7fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10191,11 +10191,11 @@ __metadata: linkType: hard "uuid@npm:^9.0.0": - version: 9.0.0 - resolution: "uuid@npm:9.0.0" + version: 9.0.1 + resolution: "uuid@npm:9.0.1" bin: uuid: dist/bin/uuid - checksum: 8dd2c83c43ddc7e1c71e36b60aea40030a6505139af6bee0f382ebcd1a56f6cd3028f7f06ffb07f8cf6ced320b76aea275284b224b002b289f89fe89c389b028 + checksum: 39931f6da74e307f51c0fb463dc2462807531dc80760a9bff1e35af4316131b4fc3203d16da60ae33f07fdca5b56f3f1dd662da0c99fea9aaeab2004780cc5f4 languageName: node linkType: hard