Skip to content

Commit fd03dc6

Browse files
authored
Integrate PollingController mixin with GasFeeController (#1673)
Integrates recently introduced [`PollingController` mixin](#1736) with `GasFeeController`. Leaves old polling pattern in pace for now so as to not force a ton of breaking changes for mobile, but with intention to activate new pattern in both clients ASAP. Addresses: MetaMask/MetaMask-planning#1314
1 parent c9b2cc7 commit fd03dc6

File tree

6 files changed

+214
-17
lines changed

6 files changed

+214
-17
lines changed

packages/gas-fee-controller/src/GasFeeController.test.ts

+108-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { ControllerMessenger } from '@metamask/base-controller';
22
import { NetworkType, toHex } from '@metamask/controller-utils';
33
import EthQuery from '@metamask/eth-query';
4-
import { NetworkController } from '@metamask/network-controller';
4+
import { NetworkController, NetworkStatus } from '@metamask/network-controller';
55
import type {
6+
NetworkControllerGetEIP1559CompatibilityAction,
7+
NetworkControllerGetNetworkClientByIdAction,
68
NetworkControllerGetStateAction,
79
NetworkControllerNetworkDidChangeEvent,
810
NetworkControllerStateChangeEvent,
@@ -40,7 +42,10 @@ const mockedDetermineGasFeeCalculations =
4042
const name = 'GasFeeController';
4143

4244
type MainControllerMessenger = ControllerMessenger<
43-
GetGasFeeState | NetworkControllerGetStateAction,
45+
| GetGasFeeState
46+
| NetworkControllerGetStateAction
47+
| NetworkControllerGetNetworkClientByIdAction
48+
| NetworkControllerGetEIP1559CompatibilityAction,
4449
| GasFeeStateChange
4550
| NetworkControllerStateChangeEvent
4651
| NetworkControllerNetworkDidChangeEvent
@@ -61,7 +66,11 @@ const setupNetworkController = async ({
6166
}) => {
6267
const restrictedMessenger = unrestrictedMessenger.getRestricted({
6368
name: 'NetworkController',
64-
allowedActions: ['NetworkController:getState'],
69+
allowedActions: [
70+
'NetworkController:getState',
71+
'NetworkController:getNetworkClientById',
72+
'NetworkController:getEIP1559Compatibility',
73+
],
6574
allowedEvents: [
6675
'NetworkController:stateChange',
6776
'NetworkController:networkDidChange',
@@ -89,7 +98,11 @@ const getRestrictedMessenger = (
8998
) => {
9099
const messenger = controllerMessenger.getRestricted({
91100
name,
92-
allowedActions: ['NetworkController:getState'],
101+
allowedActions: [
102+
'NetworkController:getState',
103+
'NetworkController:getNetworkClientById',
104+
'NetworkController:getEIP1559Compatibility',
105+
],
93106
allowedEvents: ['NetworkController:stateChange'],
94107
});
95108

@@ -216,6 +229,7 @@ describe('GasFeeController', () => {
216229
* @param options.networkControllerState - State object to initialize
217230
* NetworkController with.
218231
* @param options.interval - The polling interval.
232+
* @param options.state - The initial GasFeeController state
219233
*/
220234
async function setupGasFeeController({
221235
getIsEIP1559Compatible = jest.fn().mockResolvedValue(true),
@@ -227,6 +241,7 @@ describe('GasFeeController', () => {
227241
clientId,
228242
getChainId,
229243
networkControllerState = {},
244+
state,
230245
interval,
231246
}: {
232247
getChainId?: jest.Mock<Hex>;
@@ -236,6 +251,7 @@ describe('GasFeeController', () => {
236251
EIP1559APIEndpoint?: string;
237252
clientId?: string;
238253
networkControllerState?: Partial<NetworkState>;
254+
state?: GasFeeState;
239255
interval?: number;
240256
} = {}) {
241257
const controllerMessenger = getControllerMessenger();
@@ -253,6 +269,7 @@ describe('GasFeeController', () => {
253269
getCurrentNetworkEIP1559Compatibility: getIsEIP1559Compatible, // change this for networkDetails.state.networkDetails.isEIP1559Compatible ???
254270
legacyAPIEndpoint,
255271
EIP1559APIEndpoint,
272+
state,
256273
clientId,
257274
interval,
258275
});
@@ -851,4 +868,91 @@ describe('GasFeeController', () => {
851868
});
852869
});
853870
});
871+
describe('executePoll', () => {
872+
it('should call determineGasFeeCalculations with a URL that contains the chain ID', async () => {
873+
await setupGasFeeController({
874+
getIsEIP1559Compatible: jest.fn().mockResolvedValue(false),
875+
getCurrentNetworkLegacyGasAPICompatibility: jest
876+
.fn()
877+
.mockReturnValue(true),
878+
legacyAPIEndpoint: 'https://some-legacy-endpoint/<chain_id>',
879+
EIP1559APIEndpoint: 'https://some-eip-1559-endpoint/<chain_id>',
880+
networkControllerState: {
881+
networksMetadata: {
882+
mainnet: {
883+
EIPS: {
884+
1559: true,
885+
},
886+
status: NetworkStatus.Available,
887+
},
888+
sepolia: {
889+
EIPS: {
890+
1559: true,
891+
},
892+
status: NetworkStatus.Available,
893+
},
894+
},
895+
},
896+
clientId: '99999',
897+
});
898+
899+
await gasFeeController.executePoll('mainnet');
900+
await gasFeeController.executePoll('sepolia');
901+
902+
expect(mockedDetermineGasFeeCalculations).toHaveBeenCalledWith(
903+
expect.objectContaining({
904+
fetchGasEstimatesUrl: 'https://some-eip-1559-endpoint/1',
905+
}),
906+
);
907+
expect(mockedDetermineGasFeeCalculations).toHaveBeenCalledWith(
908+
expect.objectContaining({
909+
fetchGasEstimatesUrl: 'https://some-eip-1559-endpoint/11155111',
910+
}),
911+
);
912+
expect(mockedDetermineGasFeeCalculations).not.toHaveBeenCalledWith(
913+
expect.objectContaining({
914+
fetchGasEstimatesUrl: 'https://some-eip-1559-endpoint/5',
915+
}),
916+
);
917+
});
918+
});
919+
920+
describe('polling (by networkClientId)', () => {
921+
it('should call determineGasFeeCalculations (via executePoll) with a URL that contains the chain ID after the interval passed via the constructor', async () => {
922+
const pollingInterval = 10000;
923+
await setupGasFeeController({
924+
getIsEIP1559Compatible: jest.fn().mockResolvedValue(false),
925+
getCurrentNetworkLegacyGasAPICompatibility: jest
926+
.fn()
927+
.mockReturnValue(true),
928+
legacyAPIEndpoint: 'https://some-legacy-endpoint/<chain_id>',
929+
EIP1559APIEndpoint: 'https://some-eip-1559-endpoint/<chain_id>',
930+
networkControllerState: {
931+
networksMetadata: {
932+
goerli: {
933+
EIPS: {
934+
1559: true,
935+
},
936+
status: NetworkStatus.Available,
937+
},
938+
},
939+
},
940+
clientId: '99999',
941+
interval: pollingInterval,
942+
});
943+
944+
gasFeeController.startPollingByNetworkClientId('goerli');
945+
await clock.tickAsync(pollingInterval / 2);
946+
expect(mockedDetermineGasFeeCalculations).not.toHaveBeenCalled();
947+
await clock.tickAsync(pollingInterval / 2);
948+
expect(mockedDetermineGasFeeCalculations).toHaveBeenCalledWith(
949+
expect.objectContaining({
950+
fetchGasEstimatesUrl: 'https://some-eip-1559-endpoint/5',
951+
}),
952+
);
953+
expect(
954+
gasFeeController.state.gasFeeEstimatesByChainId?.['0x5'],
955+
).toStrictEqual(buildMockGasFeeStateFeeMarket());
956+
});
957+
});
854958
});

packages/gas-fee-controller/src/GasFeeController.ts

+85-5
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import type { RestrictedControllerMessenger } from '@metamask/base-controller';
2-
import { BaseControllerV2 } from '@metamask/base-controller';
32
import { convertHexToDecimal, safelyExecute } from '@metamask/controller-utils';
43
import EthQuery from '@metamask/eth-query';
54
import type {
5+
NetworkControllerGetEIP1559CompatibilityAction,
6+
NetworkControllerGetNetworkClientByIdAction,
67
NetworkControllerGetStateAction,
78
NetworkControllerStateChangeEvent,
89
NetworkState,
910
ProviderProxy,
1011
} from '@metamask/network-controller';
12+
import { PollingController } from '@metamask/polling-controller';
1113
import type { Hex } from '@metamask/utils';
1214
import type { Patch } from 'immer';
1315
import { v1 as random } from 'uuid';
@@ -150,6 +152,10 @@ type FallbackGasFeeEstimates = {
150152
};
151153

152154
const metadata = {
155+
gasFeeEstimatesByChainId: {
156+
persist: true,
157+
anonymous: false,
158+
},
153159
gasFeeEstimates: { persist: true, anonymous: false },
154160
estimatedGasFeeTimeBounds: { persist: true, anonymous: false },
155161
gasEstimateType: { persist: true, anonymous: false },
@@ -190,12 +196,18 @@ export type FetchGasFeeEstimateOptions = {
190196
* @property gasFeeEstimates - Gas fee estimate data based on new EIP-1559 properties
191197
* @property estimatedGasFeeTimeBounds - Estimates representing the minimum and maximum
192198
*/
193-
export type GasFeeState =
199+
export type SingleChainGasFeeState =
194200
| GasFeeStateEthGasPrice
195201
| GasFeeStateFeeMarket
196202
| GasFeeStateLegacy
197203
| GasFeeStateNoEstimates;
198204

205+
export type GasFeeEstimatesByChainId = {
206+
gasFeeEstimatesByChainId?: Record<string, SingleChainGasFeeState>;
207+
};
208+
209+
export type GasFeeState = GasFeeEstimatesByChainId & SingleChainGasFeeState;
210+
199211
const name = 'GasFeeController';
200212

201213
export type GasFeeStateChange = {
@@ -210,13 +222,19 @@ export type GetGasFeeState = {
210222

211223
type GasFeeMessenger = RestrictedControllerMessenger<
212224
typeof name,
213-
GetGasFeeState | NetworkControllerGetStateAction,
225+
| GetGasFeeState
226+
| NetworkControllerGetStateAction
227+
| NetworkControllerGetNetworkClientByIdAction
228+
| NetworkControllerGetEIP1559CompatibilityAction,
214229
GasFeeStateChange | NetworkControllerStateChangeEvent,
215-
NetworkControllerGetStateAction['type'],
230+
| NetworkControllerGetStateAction['type']
231+
| NetworkControllerGetNetworkClientByIdAction['type']
232+
| NetworkControllerGetEIP1559CompatibilityAction['type'],
216233
NetworkControllerStateChangeEvent['type']
217234
>;
218235

219236
const defaultState: GasFeeState = {
237+
gasFeeEstimatesByChainId: {},
220238
gasFeeEstimates: {},
221239
estimatedGasFeeTimeBounds: {},
222240
gasEstimateType: GAS_ESTIMATE_TYPES.NONE,
@@ -225,7 +243,7 @@ const defaultState: GasFeeState = {
225243
/**
226244
* Controller that retrieves gas fee estimate data and polls for updated data on a set interval
227245
*/
228-
export class GasFeeController extends BaseControllerV2<
246+
export class GasFeeController extends PollingController<
229247
typeof name,
230248
GasFeeState,
231249
GasFeeMessenger
@@ -311,6 +329,7 @@ export class GasFeeController extends BaseControllerV2<
311329
state: { ...defaultState, ...state },
312330
});
313331
this.intervalDelay = interval;
332+
this.setIntervalLength(interval);
314333
this.pollTokens = new Set();
315334
this.getCurrentNetworkEIP1559Compatibility =
316335
getCurrentNetworkEIP1559Compatibility;
@@ -373,6 +392,63 @@ export class GasFeeController extends BaseControllerV2<
373392
return _pollToken;
374393
}
375394

395+
async #fetchGasFeeEstimateForNetworkClientId(networkClientId: string) {
396+
let isEIP1559Compatible = false;
397+
398+
const networkClient = this.messagingSystem.call(
399+
'NetworkController:getNetworkClientById',
400+
networkClientId,
401+
);
402+
403+
const isLegacyGasAPICompatible =
404+
networkClient.configuration.chainId === '0x38';
405+
406+
const decimalChainId = convertHexToDecimal(
407+
networkClient.configuration.chainId,
408+
);
409+
410+
try {
411+
const result = await this.messagingSystem.call(
412+
'NetworkController:getEIP1559Compatibility',
413+
networkClientId,
414+
);
415+
isEIP1559Compatible = result || false;
416+
} catch {
417+
isEIP1559Compatible = false;
418+
}
419+
420+
const ethQuery = new EthQuery(networkClient.provider);
421+
422+
const gasFeeCalculations = await determineGasFeeCalculations({
423+
isEIP1559Compatible,
424+
isLegacyGasAPICompatible,
425+
fetchGasEstimates,
426+
fetchGasEstimatesUrl: this.EIP1559APIEndpoint.replace(
427+
'<chain_id>',
428+
`${decimalChainId}`,
429+
),
430+
fetchGasEstimatesViaEthFeeHistory,
431+
fetchLegacyGasPriceEstimates,
432+
fetchLegacyGasPriceEstimatesUrl: this.legacyAPIEndpoint.replace(
433+
'<chain_id>',
434+
`${decimalChainId}`,
435+
),
436+
fetchEthGasPriceEstimate,
437+
calculateTimeEstimate,
438+
clientId: this.clientId,
439+
ethQuery,
440+
});
441+
442+
this.update((state) => {
443+
state.gasFeeEstimatesByChainId = state.gasFeeEstimatesByChainId || {};
444+
state.gasFeeEstimatesByChainId[networkClient.configuration.chainId] = {
445+
gasFeeEstimates: gasFeeCalculations.gasFeeEstimates,
446+
estimatedGasFeeTimeBounds: gasFeeCalculations.estimatedGasFeeTimeBounds,
447+
gasEstimateType: gasFeeCalculations.gasEstimateType,
448+
} as any;
449+
});
450+
}
451+
376452
/**
377453
* Gets and sets gasFeeEstimates in state.
378454
*
@@ -470,6 +546,10 @@ export class GasFeeController extends BaseControllerV2<
470546
}, this.intervalDelay);
471547
}
472548

549+
async executePoll(networkClientId: string): Promise<void> {
550+
await this.#fetchGasFeeEstimateForNetworkClientId(networkClientId);
551+
}
552+
473553
private resetState() {
474554
this.update(() => {
475555
return defaultState;

packages/gas-fee-controller/tsconfig.build.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"references": [
99
{ "path": "../base-controller/tsconfig.build.json" },
1010
{ "path": "../controller-utils/tsconfig.build.json" },
11-
{ "path": "../network-controller/tsconfig.build.json" }
11+
{ "path": "../network-controller/tsconfig.build.json" },
12+
{ "path": "../polling-controller/tsconfig.build.json" }
1213
],
1314
"include": ["../../types", "./src"]
1415
}

packages/gas-fee-controller/tsconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"references": [
77
{ "path": "../base-controller" },
88
{ "path": "../controller-utils" },
9-
{ "path": "../network-controller" }
9+
{ "path": "../network-controller" },
10+
{ "path": "../polling-controller" }
1011
],
1112
"include": ["../../types", "./src"]
1213
}

0 commit comments

Comments
 (0)