Skip to content

Commit

Permalink
Integrate PollingController mixin with GasFeeController (#1673)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
adonesky1 authored and MajorLift committed Oct 11, 2023
1 parent febcd8c commit e88538f
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 17 deletions.
112 changes: 108 additions & 4 deletions packages/gas-fee-controller/src/GasFeeController.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -40,7 +42,10 @@ const mockedDetermineGasFeeCalculations =
const name = 'GasFeeController';

type MainControllerMessenger = ControllerMessenger<
GetGasFeeState | NetworkControllerGetStateAction,
| GetGasFeeState
| NetworkControllerGetStateAction
| NetworkControllerGetNetworkClientByIdAction
| NetworkControllerGetEIP1559CompatibilityAction,
| GasFeeStateChange
| NetworkControllerStateChangeEvent
| NetworkControllerNetworkDidChangeEvent
Expand All @@ -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',
Expand Down Expand Up @@ -89,7 +98,11 @@ const getRestrictedMessenger = (
) => {
const messenger = controllerMessenger.getRestricted({
name,
allowedActions: ['NetworkController:getState'],
allowedActions: [
'NetworkController:getState',
'NetworkController:getNetworkClientById',
'NetworkController:getEIP1559Compatibility',
],
allowedEvents: ['NetworkController:stateChange'],
});

Expand Down Expand Up @@ -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),
Expand All @@ -227,6 +241,7 @@ describe('GasFeeController', () => {
clientId,
getChainId,
networkControllerState = {},
state,
interval,
}: {
getChainId?: jest.Mock<Hex>;
Expand All @@ -236,6 +251,7 @@ describe('GasFeeController', () => {
EIP1559APIEndpoint?: string;
clientId?: string;
networkControllerState?: Partial<NetworkState>;
state?: GasFeeState;
interval?: number;
} = {}) {
const controllerMessenger = getControllerMessenger();
Expand All @@ -253,6 +269,7 @@ describe('GasFeeController', () => {
getCurrentNetworkEIP1559Compatibility: getIsEIP1559Compatible, // change this for networkDetails.state.networkDetails.isEIP1559Compatible ???
legacyAPIEndpoint,
EIP1559APIEndpoint,
state,
clientId,
interval,
});
Expand Down Expand Up @@ -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/<chain_id>',
EIP1559APIEndpoint: 'https://some-eip-1559-endpoint/<chain_id>',
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/<chain_id>',
EIP1559APIEndpoint: 'https://some-eip-1559-endpoint/<chain_id>',
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());
});
});
});
90 changes: 85 additions & 5 deletions packages/gas-fee-controller/src/GasFeeController.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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<string, SingleChainGasFeeState>;
};

export type GasFeeState = GasFeeEstimatesByChainId & SingleChainGasFeeState;

const name = 'GasFeeController';

export type GasFeeStateChange = {
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(
'<chain_id>',
`${decimalChainId}`,
),
fetchGasEstimatesViaEthFeeHistory,
fetchLegacyGasPriceEstimates,
fetchLegacyGasPriceEstimatesUrl: this.legacyAPIEndpoint.replace(
'<chain_id>',
`${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.
*
Expand Down Expand Up @@ -470,6 +546,10 @@ export class GasFeeController extends BaseControllerV2<
}, this.intervalDelay);
}

async executePoll(networkClientId: string): Promise<void> {
await this.#fetchGasFeeEstimateForNetworkClientId(networkClientId);
}

private resetState() {
this.update(() => {
return defaultState;
Expand Down
3 changes: 2 additions & 1 deletion packages/gas-fee-controller/tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
3 changes: 2 additions & 1 deletion packages/gas-fee-controller/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"references": [
{ "path": "../base-controller" },
{ "path": "../controller-utils" },
{ "path": "../network-controller" }
{ "path": "../network-controller" },
{ "path": "../polling-controller" }
],
"include": ["../../types", "./src"]
}
Loading

0 comments on commit e88538f

Please sign in to comment.