From a71c93ea88a50058c2a8623193ab2cd4f90e7101 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 14 Jun 2021 12:32:53 -0230 Subject: [PATCH 01/46] First draft of gas fee controller --- src/gas/GasFee.controller.ts | 259 +++++++++++++++++++++++++++++++ src/network/NetworkController.ts | 23 +++ 2 files changed, 282 insertions(+) create mode 100644 src/gas/GasFee.controller.ts diff --git a/src/gas/GasFee.controller.ts b/src/gas/GasFee.controller.ts new file mode 100644 index 00000000000..e5bd9088132 --- /dev/null +++ b/src/gas/GasFee.controller.ts @@ -0,0 +1,259 @@ +import { Mutex } from 'async-mutex'; +import type { Patch } from 'immer'; + +import { BaseController } from '../BaseControllerV2'; +import { safelyExecute } from '../util'; +import { fetchGasEstimates as defaultFetchGasEstimates } from './gas-util'; + +import type { RestrictedControllerMessenger } from '../ControllerMessenger'; + +/** + * @type LegacyGasFee + * + * Data necessary to provide an estimated legacy gas price + * + * @property gasPrice - A representation of a single `gasPrice`, for legacy transactions. A GWEI hex number + */ + +interface LegacyGasFee { + gasPrice: string, // a GWEI hex number +} + +/** + * @type Eip1559GasFee + * + * Data necessary to provide an estimate of a gas fee with a specific tip + * + * @property minWaitTimeEstimate - The fastest the transaction will take, in milliseconds + * @property maxWaitTimeEstimate - The slowest the transaction will take, in milliseconds + * @property suggestedMaxPriorityFeePerGas - A suggested "tip", a GWEI hex number + * @property suggestedMaxFeePerGas - A suggested max fee, the most a user will pay. a GWEI hex number + * @property calculatedTotalMinFee - suggestedMaxPriorityFeePerGas + estimatedNextBlockBaseFee + */ + + /** + * @type LegacyGasPriceEstimates + * + * Data necessary to provide multiple GasFee estimates, and supporting information, to the user + * + * @property low - A LegacyGasFee for a minimum necessary gas price + * @property medium - A LegacyGasFee for a recommended gas price + * @property high - A GasLegacyGasFeeFee for a high gas price + */ + +interface LegacyGasPriceEstimates { + low: LegacyGasFee, + medium: LegacyGasFee, + high: LegacyGasFee, +} + +interface Eip1559GasFee { + minWaitTimeEstimate: number, // a time duration in milliseconds + maxWaitTimeEstimate: number, // a time duration in milliseconds + suggestedMaxPriorityFeePerGas: string, // a GWEI hex number + suggestedMaxFeePerGas: string, // a GWEI hex number + calculatedTotalMinFee: string, // a GWEI hex number +} + +/** + * @type GasFeeEstimates + * + * Data necessary to provide multiple GasFee estimates, and supporting information, to the user + * + * @property low - A GasFee for a minimum necessary combination of tip and maxFee + * @property medium - A GasFee for a recommended combination of tip and maxFee + * @property high - A GasFee for a high combination of tip and maxFee + * @property estimatedNextBlockBaseFee - An estimate of what the base fee will be for the pending/next block. A GWEI hex number + * @property lastBlockBaseFee - The base fee for the most recent block. A GWEI hex number + * @property lastBlockMinPriorityFee - The lowest tip that succeeded in the most recent block. A GWEI hex number + * @property lastBlockMaxPriorityFee - The highest tip that succeeded in the most recent block. A GWEI hex number + */ + + interface GasFeeEstimates { + low: Eip1559GasFee, + medium: Eip1559GasFee, + high: Eip1559GasFee, + estimatedNextBlockBaseFee: string, + lastBlockBaseFee: string, + lastBlockMinPriorityFee: string, + lastBlockMaxPriorityFee: string, + } + + +/** + * @type GasFeeState + * + * Gas Fee controller state + * + * @property legacyGasPriceEstimates - Gas fee estimate data using the legacy `gasPrice` property + * @property gasFeeEstimates - Gas fee estimate data based on new EIP-1559 properties + */ +export interface GasFeeState extends BaseState { + legacyGasPriceEstimates: LegacyGasPriceEstimates | undefined; + gasFeeEstimates: GasFeeEstimates | undefined; +} + +const name = 'GasFeeController'; + +export type GasFeeStateChange = { + type: `${typeof name}:stateChange`; + payload: [GasFeeState, Patch[]]; +}; + +export type GetGasFeeState = { + type: `${typeof name}:getState`; + handler: () => GasFeeState; +}; + +const defaultState = { + legacyGasPriceEstimates: undefined, + gasFeeEstimates: undefined, +}; + +/** + * Controller that retrieves gas fee estimate data and polls for updated data on a set interval + */ +export class GasFeeController extends BaseController< + typeof name, + GasFeeState +> { + private mutex = new Mutex(); + + private intervalId?: NodeJS.Timeout; + + private intervalDelay; + + private getChainEIP1559Compatibility; + + private getAccountEIP1559Compatibility; + + private pollCount: number = 0; + + /** + * Creates a GasFeeController instance + * + */ + constructor({ + interval = 15000, + messenger, + state, + fetchGasEstimates = defaultFetchGasEstimates, + }: { + interval?: number; + messenger: RestrictedControllerMessenger< + typeof name, + GetGasFeeState, + GasFeeStateChange, + never, + never + >; + state?: Partial; + fetchGasEstimates?: typeof defaultFetchGasEstimates; + getChainEIP1559Compatibility?: Function; + getAccountEIP1559Compatibility?: Function; + }) { + super({ + name, + messenger, + state: { ...defaultState, ...state }, + }); + this.intervalDelay = interval; + } + + async getGasFeeEstimatesAndStartPolling(): Promise { + let gasEstimates + if (this.pollCount > 0) { + gasEstimates = this.state + } else { + gasEstimates = await this._fetchGasFeeEstimateData() + } + + this._startPolling() + + return gasEstimates + } + + /** + * Gets and sets gasFeeEstimates in state + * + * @returns GasFeeEstimates + */ + async _fetchGasFeeEstimateData(): Promise { + let newEstimates: GasFeeState; + try { + const estimates = await this.fetchGasEstimates(); + newEstimates = { + legacyGasPriceEstimates: { + low: { + gasPrice: estimates.low.suggestedMaxFeePerGas + }, + medium: { + gasPrice: estimates.medium.suggestedMaxFeePerGas + }, + high: { + gasPrice: estimates.high.suggestedMaxFeePerGas + } + }, + gasFeeEstimates: estimates, + }; + } catch (error) { + console.error(error); + } finally { + try { + this.update(() => { + return newEstimates; + }); + } finally { + return newEstimates; + } + } + } + + async _startPolling() { + if (this.pollCount === 0) { + this._poll(); + } + this.pollCount += 1; + } + + async _poll() { + this.intervalId = setInterval(async () => { + await safelyExecute(() => this._fetchGasFeeEstimateData()); + }, this.intervalDelay); + } + + /** + * Reduce the count of opened transactions for which polling is needed, and stop polling if polling is no longer needed + */ + disconnectPoller() { + this.pollCount -= 1; + if (this.pollCount === 0) { + this.stopPolling(); + } + } + + private stopPolling() { + if (this.intervalId) { + clearInterval(this.intervalId); + } + this.pollCount = 0; + this.resetState(); + } + + private resetState() { + this.state = defaultState; + } + + /** + * Prepare to discard this controller. + * + * This stops any active polling. + */ + private destroy() { + super.destroy(); + this.stopPolling(); + } + +} + +export default GasFeeController; diff --git a/src/network/NetworkController.ts b/src/network/NetworkController.ts index 734d18aab20..ceb07613e9a 100644 --- a/src/network/NetworkController.ts +++ b/src/network/NetworkController.ts @@ -47,6 +47,14 @@ export interface ProviderConfig { nickname?: string; } +export interface Block { + baseFee?: string; +} + +export interface NetworkProperties { + isEIP1559Compatible?: boolean; +} + /** * @type NetworkConfig * @@ -71,6 +79,7 @@ export interface NetworkConfig extends BaseConfig { export interface NetworkState extends BaseState { network: string; provider: ProviderConfig; + properties: NetworkProperties; } const LOCALHOST_RPC_URL = 'http://localhost:8545'; @@ -204,6 +213,7 @@ export class NetworkController extends BaseController< provider: { type: MAINNET, chainId: NetworksChainId.mainnet }, }; this.initialize(); + this.getNetworkProperties(); } /** @@ -288,6 +298,19 @@ export class NetworkController extends BaseController< }); this.refreshNetwork(); } + + getNetworkProperties() { + this.ethQuery.sendAsync( + { method: 'eth_getBlockByNumber', params: ['latest', false] }, + (error: Error, block: Block) => { + this.update({ + properties: { + isEIP1559Compatible: typeof block.baseFee !== undefined, + } + }); + }, + ); + } } export default NetworkController; From c4a26061050e10218c817eab689b02513c5643d3 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 16 Jun 2021 12:09:33 -0230 Subject: [PATCH 02/46] Second draft of gas fee controller --- src/gas/GasFee.controller.ts | 153 +++++++++++++------------- src/gas/gas-util.ts | 180 +++++++++++++++++++++++++++++++ src/network/NetworkController.ts | 15 ++- 3 files changed, 264 insertions(+), 84 deletions(-) create mode 100644 src/gas/gas-util.ts diff --git a/src/gas/GasFee.controller.ts b/src/gas/GasFee.controller.ts index e5bd9088132..5ca6b8dd290 100644 --- a/src/gas/GasFee.controller.ts +++ b/src/gas/GasFee.controller.ts @@ -1,11 +1,9 @@ -import { Mutex } from 'async-mutex'; import type { Patch } from 'immer'; import { BaseController } from '../BaseControllerV2'; import { safelyExecute } from '../util'; -import { fetchGasEstimates as defaultFetchGasEstimates } from './gas-util'; - import type { RestrictedControllerMessenger } from '../ControllerMessenger'; +import { fetchGasEstimates as defaultFetchGasEstimates } from './gas-util'; /** * @type LegacyGasFee @@ -16,7 +14,7 @@ import type { RestrictedControllerMessenger } from '../ControllerMessenger'; */ interface LegacyGasFee { - gasPrice: string, // a GWEI hex number + gasPrice: string; // a GWEI hex number } /** @@ -31,28 +29,28 @@ interface LegacyGasFee { * @property calculatedTotalMinFee - suggestedMaxPriorityFeePerGas + estimatedNextBlockBaseFee */ - /** - * @type LegacyGasPriceEstimates - * - * Data necessary to provide multiple GasFee estimates, and supporting information, to the user - * - * @property low - A LegacyGasFee for a minimum necessary gas price - * @property medium - A LegacyGasFee for a recommended gas price - * @property high - A GasLegacyGasFeeFee for a high gas price - */ +/** + * @type LegacyGasPriceEstimates + * + * Data necessary to provide multiple GasFee estimates, and supporting information, to the user + * + * @property low - A LegacyGasFee for a minimum necessary gas price + * @property medium - A LegacyGasFee for a recommended gas price + * @property high - A GasLegacyGasFeeFee for a high gas price + */ interface LegacyGasPriceEstimates { - low: LegacyGasFee, - medium: LegacyGasFee, - high: LegacyGasFee, + low: LegacyGasFee; + medium: LegacyGasFee; + high: LegacyGasFee; } interface Eip1559GasFee { - minWaitTimeEstimate: number, // a time duration in milliseconds - maxWaitTimeEstimate: number, // a time duration in milliseconds - suggestedMaxPriorityFeePerGas: string, // a GWEI hex number - suggestedMaxFeePerGas: string, // a GWEI hex number - calculatedTotalMinFee: string, // a GWEI hex number + minWaitTimeEstimate: number; // a time duration in milliseconds + maxWaitTimeEstimate: number; // a time duration in milliseconds + suggestedMaxPriorityFeePerGas: string; // a GWEI hex number + suggestedMaxFeePerGas: string; // a GWEI hex number + calculatedTotalMinFee: string; // a GWEI hex number } /** @@ -69,16 +67,20 @@ interface Eip1559GasFee { * @property lastBlockMaxPriorityFee - The highest tip that succeeded in the most recent block. A GWEI hex number */ - interface GasFeeEstimates { - low: Eip1559GasFee, - medium: Eip1559GasFee, - high: Eip1559GasFee, - estimatedNextBlockBaseFee: string, - lastBlockBaseFee: string, - lastBlockMinPriorityFee: string, - lastBlockMaxPriorityFee: string, - } +export interface GasFeeEstimates { + low: Eip1559GasFee; + medium: Eip1559GasFee; + high: Eip1559GasFee; + estimatedNextBlockBaseFee: string; + lastBlockBaseFee: string; + lastBlockMinPriorityFee: string; + lastBlockMaxPriorityFee: string; +} +const metadata = { + legacyGasPriceEstimates: { persist: true, anonymous: false }, + gasFeeEstimates: { persist: true, anonymous: false }, +}; /** * @type GasFeeState @@ -88,10 +90,10 @@ interface Eip1559GasFee { * @property legacyGasPriceEstimates - Gas fee estimate data using the legacy `gasPrice` property * @property gasFeeEstimates - Gas fee estimate data based on new EIP-1559 properties */ -export interface GasFeeState extends BaseState { - legacyGasPriceEstimates: LegacyGasPriceEstimates | undefined; - gasFeeEstimates: GasFeeEstimates | undefined; -} +export type GasFeeState = { + legacyGasPriceEstimates: LegacyGasPriceEstimates | Record; + gasFeeEstimates: GasFeeEstimates | Record; +}; const name = 'GasFeeController'; @@ -106,28 +108,21 @@ export type GetGasFeeState = { }; const defaultState = { - legacyGasPriceEstimates: undefined, - gasFeeEstimates: undefined, + legacyGasPriceEstimates: {}, + gasFeeEstimates: {}, }; /** * Controller that retrieves gas fee estimate data and polls for updated data on a set interval */ -export class GasFeeController extends BaseController< - typeof name, - GasFeeState -> { - private mutex = new Mutex(); - +export class GasFeeController extends BaseController { private intervalId?: NodeJS.Timeout; private intervalDelay; - private getChainEIP1559Compatibility; - - private getAccountEIP1559Compatibility; + private pollCount = 0; - private pollCount: number = 0; + private fetchGasEstimates; /** * Creates a GasFeeController instance @@ -149,28 +144,28 @@ export class GasFeeController extends BaseController< >; state?: Partial; fetchGasEstimates?: typeof defaultFetchGasEstimates; - getChainEIP1559Compatibility?: Function; - getAccountEIP1559Compatibility?: Function; }) { super({ name, + metadata, messenger, state: { ...defaultState, ...state }, }); this.intervalDelay = interval; + this.fetchGasEstimates = fetchGasEstimates; } async getGasFeeEstimatesAndStartPolling(): Promise { - let gasEstimates + let gasEstimates; if (this.pollCount > 0) { - gasEstimates = this.state + gasEstimates = this.state; } else { - gasEstimates = await this._fetchGasFeeEstimateData() + gasEstimates = await this._fetchGasFeeEstimateData(); } - this._startPolling() + this._startPolling(); - return gasEstimates + return gasEstimates; } /** @@ -179,20 +174,20 @@ export class GasFeeController extends BaseController< * @returns GasFeeEstimates */ async _fetchGasFeeEstimateData(): Promise { - let newEstimates: GasFeeState; + let newEstimates = this.state; try { const estimates = await this.fetchGasEstimates(); newEstimates = { legacyGasPriceEstimates: { low: { - gasPrice: estimates.low.suggestedMaxFeePerGas + gasPrice: estimates.low.suggestedMaxFeePerGas, }, medium: { - gasPrice: estimates.medium.suggestedMaxFeePerGas + gasPrice: estimates.medium.suggestedMaxFeePerGas, }, high: { - gasPrice: estimates.high.suggestedMaxFeePerGas - } + gasPrice: estimates.high.suggestedMaxFeePerGas, + }, }, gasFeeEstimates: estimates, }; @@ -203,23 +198,11 @@ export class GasFeeController extends BaseController< this.update(() => { return newEstimates; }); - } finally { - return newEstimates; + } catch (error) { + console.error(error); } } - } - - async _startPolling() { - if (this.pollCount === 0) { - this._poll(); - } - this.pollCount += 1; - } - - async _poll() { - this.intervalId = setInterval(async () => { - await safelyExecute(() => this._fetchGasFeeEstimateData()); - }, this.intervalDelay); + return newEstimates; } /** @@ -232,7 +215,7 @@ export class GasFeeController extends BaseController< } } - private stopPolling() { + stopPolling() { if (this.intervalId) { clearInterval(this.intervalId); } @@ -240,20 +223,32 @@ export class GasFeeController extends BaseController< this.resetState(); } - private resetState() { - this.state = defaultState; - } - /** * Prepare to discard this controller. * * This stops any active polling. */ - private destroy() { + destroy() { super.destroy(); this.stopPolling(); } + private async _startPolling() { + if (this.pollCount === 0) { + this._poll(); + } + this.pollCount += 1; + } + + private async _poll() { + this.intervalId = setInterval(async () => { + await safelyExecute(() => this._fetchGasFeeEstimateData()); + }, this.intervalDelay); + } + + private resetState() { + this.state = defaultState; + } } export default GasFeeController; diff --git a/src/gas/gas-util.ts b/src/gas/gas-util.ts new file mode 100644 index 00000000000..80b0735d64a --- /dev/null +++ b/src/gas/gas-util.ts @@ -0,0 +1,180 @@ +import { GasFeeEstimates } from './GasFee.controller'; + +// import { handleFetch } from '../util'; + +// const GAS_FEE_API = 'https://gas-fee-api-goes-here'; + +const mockApiResponses = [ + { + low: { + minWaitTimeEstimate: 120000, + maxWaitTimeEstimate: 300000, + suggestedMaxPriorityFeePerGas: '1', + suggestedMaxFeePerGas: '35', + calculatedTotalMinFee: '31', + }, + medium: { + minWaitTimeEstimate: 0, + maxWaitTimeEstimate: 30000, + suggestedMaxPriorityFeePerGas: '2', + suggestedMaxFeePerGas: '40', + calculatedTotalMinFee: '32', + }, + high: { + minWaitTimeEstimate: 0, + maxWaitTimeEstimate: 150000, + suggestedMaxPriorityFeePerGas: '3', + suggestedMaxFeePerGas: '60', + calculatedTotalMinFee: '33', + }, + estimatedNextBlockBaseFee: '30', + lastBlockBaseFee: '28', + lastBlockMinPriorityFee: '1', + lastBlockMaxPriorityFee: '9', + }, + { + low: { + minWaitTimeEstimate: 180000, + maxWaitTimeEstimate: 360000, + suggestedMaxPriorityFeePerGas: '1', + suggestedMaxFeePerGas: '40', + calculatedTotalMinFee: '33', + }, + medium: { + minWaitTimeEstimate: 15000, + maxWaitTimeEstimate: 60000, + suggestedMaxPriorityFeePerGas: '2', + suggestedMaxFeePerGas: '45', + calculatedTotalMinFee: '34', + }, + high: { + minWaitTimeEstimate: 0, + maxWaitTimeEstimate: 150000, + suggestedMaxPriorityFeePerGas: '3', + suggestedMaxFeePerGas: '65', + calculatedTotalMinFee: '35', + }, + estimatedNextBlockBaseFee: '32', + lastBlockBaseFee: '30', + lastBlockMinPriorityFee: '1', + lastBlockMaxPriorityFee: '10', + }, + { + low: { + minWaitTimeEstimate: 60000, + maxWaitTimeEstimate: 240000, + suggestedMaxPriorityFeePerGas: '1', + suggestedMaxFeePerGas: '42', + calculatedTotalMinFee: '36', + }, + medium: { + minWaitTimeEstimate: 0, + maxWaitTimeEstimate: 30000, + suggestedMaxPriorityFeePerGas: '3', + suggestedMaxFeePerGas: '47', + calculatedTotalMinFee: '38', + }, + high: { + minWaitTimeEstimate: 0, + maxWaitTimeEstimate: 150000, + suggestedMaxPriorityFeePerGas: '4', + suggestedMaxFeePerGas: '67', + calculatedTotalMinFee: '39', + }, + estimatedNextBlockBaseFee: '35', + lastBlockBaseFee: '32', + lastBlockMinPriorityFee: '1', + lastBlockMaxPriorityFee: '10', + }, + { + low: { + minWaitTimeEstimate: 180000, + maxWaitTimeEstimate: 300000, + suggestedMaxPriorityFeePerGas: '3', + suggestedMaxFeePerGas: '53', + calculatedTotalMinFee: '53', + }, + medium: { + minWaitTimeEstimate: 15000, + maxWaitTimeEstimate: 60000, + suggestedMaxPriorityFeePerGas: '7', + suggestedMaxFeePerGas: '70', + calculatedTotalMinFee: '57', + }, + high: { + minWaitTimeEstimate: 0, + maxWaitTimeEstimate: 150000, + suggestedMaxPriorityFeePerGas: '10', + suggestedMaxFeePerGas: '100', + calculatedTotalMinFee: '60', + }, + estimatedNextBlockBaseFee: '50', + lastBlockBaseFee: '35', + lastBlockMinPriorityFee: '2', + lastBlockMaxPriorityFee: '15', + }, + { + low: { + minWaitTimeEstimate: 120000, + maxWaitTimeEstimate: 360000, + suggestedMaxPriorityFeePerGas: '1', + suggestedMaxFeePerGas: '35', + calculatedTotalMinFee: '31', + }, + medium: { + minWaitTimeEstimate: 15000, + maxWaitTimeEstimate: 60000, + suggestedMaxPriorityFeePerGas: '3', + suggestedMaxFeePerGas: '40', + calculatedTotalMinFee: '33', + }, + high: { + minWaitTimeEstimate: 0, + maxWaitTimeEstimate: 150000, + suggestedMaxPriorityFeePerGas: '4', + suggestedMaxFeePerGas: '60', + calculatedTotalMinFee: '34', + }, + estimatedNextBlockBaseFee: '30', + lastBlockBaseFee: '50', + lastBlockMinPriorityFee: '4', + lastBlockMaxPriorityFee: '25', + }, + { + low: { + minWaitTimeEstimate: 60000, + maxWaitTimeEstimate: 600000, + suggestedMaxPriorityFeePerGas: '1', + suggestedMaxFeePerGas: '35', + calculatedTotalMinFee: '31', + }, + medium: { + minWaitTimeEstimate: 15000, + maxWaitTimeEstimate: 60000, + suggestedMaxPriorityFeePerGas: '1.8', + suggestedMaxFeePerGas: '38', + calculatedTotalMinFee: '29.8', + }, + high: { + minWaitTimeEstimate: 0, + maxWaitTimeEstimate: 150000, + suggestedMaxPriorityFeePerGas: '2', + suggestedMaxFeePerGas: '50', + calculatedTotalMinFee: '30', + }, + estimatedNextBlockBaseFee: '28', + lastBlockBaseFee: '28', + lastBlockMinPriorityFee: '1', + lastBlockMaxPriorityFee: '7', + }, +]; + +const getMockApiResponse = (): GasFeeEstimates => + mockApiResponses[Math.floor(Math.random() * 6)]; + +export function fetchGasEstimates(): Promise { + // return handleFetch(GAS_FEE_API) + return new Promise((resolve) => { + resolve(getMockApiResponse()); + }); +} diff --git a/src/network/NetworkController.ts b/src/network/NetworkController.ts index ceb07613e9a..8417c14d7f7 100644 --- a/src/network/NetworkController.ts +++ b/src/network/NetworkController.ts @@ -211,6 +211,7 @@ export class NetworkController extends BaseController< this.defaultState = { network: 'loading', provider: { type: MAINNET, chainId: NetworksChainId.mainnet }, + properties: { isEIP1559Compatible: false }, }; this.initialize(); this.getNetworkProperties(); @@ -303,11 +304,15 @@ export class NetworkController extends BaseController< this.ethQuery.sendAsync( { method: 'eth_getBlockByNumber', params: ['latest', false] }, (error: Error, block: Block) => { - this.update({ - properties: { - isEIP1559Compatible: typeof block.baseFee !== undefined, - } - }); + if (error) { + console.error(error); + } else { + this.update({ + properties: { + isEIP1559Compatible: typeof block.baseFee !== undefined, + }, + }); + } }, ); } From 788618077cc14332fa8b539547bbc3bfacfa933c Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 17 Jun 2021 13:12:12 -0230 Subject: [PATCH 03/46] Third draft of gas fee controller --- src/gas/GasFee.controller.ts | 32 ++++++++------------- src/gas/gas-util.ts | 55 +++++++----------------------------- 2 files changed, 22 insertions(+), 65 deletions(-) diff --git a/src/gas/GasFee.controller.ts b/src/gas/GasFee.controller.ts index 5ca6b8dd290..143adb0b6f4 100644 --- a/src/gas/GasFee.controller.ts +++ b/src/gas/GasFee.controller.ts @@ -17,18 +17,6 @@ interface LegacyGasFee { gasPrice: string; // a GWEI hex number } -/** - * @type Eip1559GasFee - * - * Data necessary to provide an estimate of a gas fee with a specific tip - * - * @property minWaitTimeEstimate - The fastest the transaction will take, in milliseconds - * @property maxWaitTimeEstimate - The slowest the transaction will take, in milliseconds - * @property suggestedMaxPriorityFeePerGas - A suggested "tip", a GWEI hex number - * @property suggestedMaxFeePerGas - A suggested max fee, the most a user will pay. a GWEI hex number - * @property calculatedTotalMinFee - suggestedMaxPriorityFeePerGas + estimatedNextBlockBaseFee - */ - /** * @type LegacyGasPriceEstimates * @@ -45,12 +33,22 @@ interface LegacyGasPriceEstimates { high: LegacyGasFee; } +/** + * @type Eip1559GasFee + * + * Data necessary to provide an estimate of a gas fee with a specific tip + * + * @property minWaitTimeEstimate - The fastest the transaction will take, in milliseconds + * @property maxWaitTimeEstimate - The slowest the transaction will take, in milliseconds + * @property suggestedMaxPriorityFeePerGas - A suggested "tip", a GWEI hex number + * @property suggestedMaxFeePerGas - A suggested max fee, the most a user will pay. a GWEI hex number + */ + interface Eip1559GasFee { minWaitTimeEstimate: number; // a time duration in milliseconds maxWaitTimeEstimate: number; // a time duration in milliseconds suggestedMaxPriorityFeePerGas: string; // a GWEI hex number suggestedMaxFeePerGas: string; // a GWEI hex number - calculatedTotalMinFee: string; // a GWEI hex number } /** @@ -62,19 +60,13 @@ interface Eip1559GasFee { * @property medium - A GasFee for a recommended combination of tip and maxFee * @property high - A GasFee for a high combination of tip and maxFee * @property estimatedNextBlockBaseFee - An estimate of what the base fee will be for the pending/next block. A GWEI hex number - * @property lastBlockBaseFee - The base fee for the most recent block. A GWEI hex number - * @property lastBlockMinPriorityFee - The lowest tip that succeeded in the most recent block. A GWEI hex number - * @property lastBlockMaxPriorityFee - The highest tip that succeeded in the most recent block. A GWEI hex number */ export interface GasFeeEstimates { low: Eip1559GasFee; medium: Eip1559GasFee; high: Eip1559GasFee; - estimatedNextBlockBaseFee: string; - lastBlockBaseFee: string; - lastBlockMinPriorityFee: string; - lastBlockMaxPriorityFee: string; + estimatedBaseFee: string; } const metadata = { diff --git a/src/gas/gas-util.ts b/src/gas/gas-util.ts index 80b0735d64a..4709de609a3 100644 --- a/src/gas/gas-util.ts +++ b/src/gas/gas-util.ts @@ -4,33 +4,27 @@ import { GasFeeEstimates } from './GasFee.controller'; // const GAS_FEE_API = 'https://gas-fee-api-goes-here'; -const mockApiResponses = [ +const mockEIP1559ApiResponses = [ { low: { minWaitTimeEstimate: 120000, maxWaitTimeEstimate: 300000, suggestedMaxPriorityFeePerGas: '1', suggestedMaxFeePerGas: '35', - calculatedTotalMinFee: '31', }, medium: { minWaitTimeEstimate: 0, maxWaitTimeEstimate: 30000, suggestedMaxPriorityFeePerGas: '2', suggestedMaxFeePerGas: '40', - calculatedTotalMinFee: '32', }, high: { minWaitTimeEstimate: 0, maxWaitTimeEstimate: 150000, suggestedMaxPriorityFeePerGas: '3', suggestedMaxFeePerGas: '60', - calculatedTotalMinFee: '33', }, - estimatedNextBlockBaseFee: '30', - lastBlockBaseFee: '28', - lastBlockMinPriorityFee: '1', - lastBlockMaxPriorityFee: '9', + estimatedBaseFee: '30', }, { low: { @@ -38,26 +32,20 @@ const mockApiResponses = [ maxWaitTimeEstimate: 360000, suggestedMaxPriorityFeePerGas: '1', suggestedMaxFeePerGas: '40', - calculatedTotalMinFee: '33', }, medium: { minWaitTimeEstimate: 15000, maxWaitTimeEstimate: 60000, suggestedMaxPriorityFeePerGas: '2', suggestedMaxFeePerGas: '45', - calculatedTotalMinFee: '34', }, high: { minWaitTimeEstimate: 0, maxWaitTimeEstimate: 150000, suggestedMaxPriorityFeePerGas: '3', suggestedMaxFeePerGas: '65', - calculatedTotalMinFee: '35', }, - estimatedNextBlockBaseFee: '32', - lastBlockBaseFee: '30', - lastBlockMinPriorityFee: '1', - lastBlockMaxPriorityFee: '10', + estimatedBaseFee: '32', }, { low: { @@ -65,26 +53,20 @@ const mockApiResponses = [ maxWaitTimeEstimate: 240000, suggestedMaxPriorityFeePerGas: '1', suggestedMaxFeePerGas: '42', - calculatedTotalMinFee: '36', }, medium: { minWaitTimeEstimate: 0, maxWaitTimeEstimate: 30000, suggestedMaxPriorityFeePerGas: '3', suggestedMaxFeePerGas: '47', - calculatedTotalMinFee: '38', }, high: { minWaitTimeEstimate: 0, maxWaitTimeEstimate: 150000, suggestedMaxPriorityFeePerGas: '4', suggestedMaxFeePerGas: '67', - calculatedTotalMinFee: '39', }, - estimatedNextBlockBaseFee: '35', - lastBlockBaseFee: '32', - lastBlockMinPriorityFee: '1', - lastBlockMaxPriorityFee: '10', + estimatedBaseFee: '35', }, { low: { @@ -92,26 +74,20 @@ const mockApiResponses = [ maxWaitTimeEstimate: 300000, suggestedMaxPriorityFeePerGas: '3', suggestedMaxFeePerGas: '53', - calculatedTotalMinFee: '53', }, medium: { minWaitTimeEstimate: 15000, maxWaitTimeEstimate: 60000, suggestedMaxPriorityFeePerGas: '7', suggestedMaxFeePerGas: '70', - calculatedTotalMinFee: '57', }, high: { minWaitTimeEstimate: 0, maxWaitTimeEstimate: 150000, suggestedMaxPriorityFeePerGas: '10', suggestedMaxFeePerGas: '100', - calculatedTotalMinFee: '60', }, - estimatedNextBlockBaseFee: '50', - lastBlockBaseFee: '35', - lastBlockMinPriorityFee: '2', - lastBlockMaxPriorityFee: '15', + estimatedBaseFee: '50', }, { low: { @@ -119,26 +95,20 @@ const mockApiResponses = [ maxWaitTimeEstimate: 360000, suggestedMaxPriorityFeePerGas: '1', suggestedMaxFeePerGas: '35', - calculatedTotalMinFee: '31', }, medium: { minWaitTimeEstimate: 15000, maxWaitTimeEstimate: 60000, suggestedMaxPriorityFeePerGas: '3', suggestedMaxFeePerGas: '40', - calculatedTotalMinFee: '33', }, high: { minWaitTimeEstimate: 0, maxWaitTimeEstimate: 150000, suggestedMaxPriorityFeePerGas: '4', suggestedMaxFeePerGas: '60', - calculatedTotalMinFee: '34', }, - estimatedNextBlockBaseFee: '30', - lastBlockBaseFee: '50', - lastBlockMinPriorityFee: '4', - lastBlockMaxPriorityFee: '25', + estimatedBaseFee: '30', }, { low: { @@ -146,31 +116,26 @@ const mockApiResponses = [ maxWaitTimeEstimate: 600000, suggestedMaxPriorityFeePerGas: '1', suggestedMaxFeePerGas: '35', - calculatedTotalMinFee: '31', }, medium: { minWaitTimeEstimate: 15000, maxWaitTimeEstimate: 60000, suggestedMaxPriorityFeePerGas: '1.8', suggestedMaxFeePerGas: '38', - calculatedTotalMinFee: '29.8', }, high: { minWaitTimeEstimate: 0, maxWaitTimeEstimate: 150000, suggestedMaxPriorityFeePerGas: '2', suggestedMaxFeePerGas: '50', - calculatedTotalMinFee: '30', }, - estimatedNextBlockBaseFee: '28', - lastBlockBaseFee: '28', - lastBlockMinPriorityFee: '1', - lastBlockMaxPriorityFee: '7', + estimatedBaseFee: '28', }, ]; -const getMockApiResponse = (): GasFeeEstimates => - mockApiResponses[Math.floor(Math.random() * 6)]; +const getMockApiResponse = (): GasFeeEstimates => { + return mockEIP1559ApiResponses[Math.floor(Math.random() * 6)]; +}; export function fetchGasEstimates(): Promise { // return handleFetch(GAS_FEE_API) From 2be94cb1e3e24772cf7da1576c303850226ca14d Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 18 Jun 2021 09:10:42 -0230 Subject: [PATCH 04/46] Delete use of legacy gas fee --- src/gas/GasFee.controller.ts | 42 +----------------------------------- 1 file changed, 1 insertion(+), 41 deletions(-) diff --git a/src/gas/GasFee.controller.ts b/src/gas/GasFee.controller.ts index 143adb0b6f4..c2742d27672 100644 --- a/src/gas/GasFee.controller.ts +++ b/src/gas/GasFee.controller.ts @@ -5,34 +5,6 @@ import { safelyExecute } from '../util'; import type { RestrictedControllerMessenger } from '../ControllerMessenger'; import { fetchGasEstimates as defaultFetchGasEstimates } from './gas-util'; -/** - * @type LegacyGasFee - * - * Data necessary to provide an estimated legacy gas price - * - * @property gasPrice - A representation of a single `gasPrice`, for legacy transactions. A GWEI hex number - */ - -interface LegacyGasFee { - gasPrice: string; // a GWEI hex number -} - -/** - * @type LegacyGasPriceEstimates - * - * Data necessary to provide multiple GasFee estimates, and supporting information, to the user - * - * @property low - A LegacyGasFee for a minimum necessary gas price - * @property medium - A LegacyGasFee for a recommended gas price - * @property high - A GasLegacyGasFeeFee for a high gas price - */ - -interface LegacyGasPriceEstimates { - low: LegacyGasFee; - medium: LegacyGasFee; - high: LegacyGasFee; -} - /** * @type Eip1559GasFee * @@ -83,7 +55,6 @@ const metadata = { * @property gasFeeEstimates - Gas fee estimate data based on new EIP-1559 properties */ export type GasFeeState = { - legacyGasPriceEstimates: LegacyGasPriceEstimates | Record; gasFeeEstimates: GasFeeEstimates | Record; }; @@ -100,7 +71,6 @@ export type GetGasFeeState = { }; const defaultState = { - legacyGasPriceEstimates: {}, gasFeeEstimates: {}, }; @@ -170,17 +140,6 @@ export class GasFeeController extends BaseController { try { const estimates = await this.fetchGasEstimates(); newEstimates = { - legacyGasPriceEstimates: { - low: { - gasPrice: estimates.low.suggestedMaxFeePerGas, - }, - medium: { - gasPrice: estimates.medium.suggestedMaxFeePerGas, - }, - high: { - gasPrice: estimates.high.suggestedMaxFeePerGas, - }, - }, gasFeeEstimates: estimates, }; } catch (error) { @@ -225,6 +184,7 @@ export class GasFeeController extends BaseController { this.stopPolling(); } + // should take a token, so we know that we are only counting once for each open transaction private async _startPolling() { if (this.pollCount === 0) { this._poll(); From ba8e2757f6b3b8976bc81610f932244be8c8833a Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 18 Jun 2021 09:29:33 -0230 Subject: [PATCH 05/46] Track whether polling should be instantiated or stopped with tokens --- src/gas/GasFee.controller.ts | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/gas/GasFee.controller.ts b/src/gas/GasFee.controller.ts index c2742d27672..ea75c38cfe7 100644 --- a/src/gas/GasFee.controller.ts +++ b/src/gas/GasFee.controller.ts @@ -82,7 +82,7 @@ export class GasFeeController extends BaseController { private intervalDelay; - private pollCount = 0; + private pollTokens: Set; private fetchGasEstimates; @@ -115,17 +115,20 @@ export class GasFeeController extends BaseController { }); this.intervalDelay = interval; this.fetchGasEstimates = fetchGasEstimates; + this.pollTokens = new Set(); } - async getGasFeeEstimatesAndStartPolling(): Promise { + async getGasFeeEstimatesAndStartPolling( + pollToken: string, + ): Promise { let gasEstimates; - if (this.pollCount > 0) { + if (this.pollTokens.size > 0) { gasEstimates = this.state; } else { gasEstimates = await this._fetchGasFeeEstimateData(); } - this._startPolling(); + this._startPolling(pollToken); return gasEstimates; } @@ -157,11 +160,11 @@ export class GasFeeController extends BaseController { } /** - * Reduce the count of opened transactions for which polling is needed, and stop polling if polling is no longer needed + * Remove the poll token, and stop polling if the set of poll tokens is empty */ - disconnectPoller() { - this.pollCount -= 1; - if (this.pollCount === 0) { + disconnectPoller(pollToken: string) { + this.pollTokens.delete(pollToken); + if (this.pollTokens.size === 0) { this.stopPolling(); } } @@ -170,7 +173,7 @@ export class GasFeeController extends BaseController { if (this.intervalId) { clearInterval(this.intervalId); } - this.pollCount = 0; + this.pollTokens.clear(); this.resetState(); } @@ -185,14 +188,17 @@ export class GasFeeController extends BaseController { } // should take a token, so we know that we are only counting once for each open transaction - private async _startPolling() { - if (this.pollCount === 0) { + private async _startPolling(pollToken: string) { + if (this.pollTokens.size === 0) { this._poll(); } - this.pollCount += 1; + this.pollTokens.add(pollToken); } private async _poll() { + if (this.intervalId) { + clearInterval(this.intervalId); + } this.intervalId = setInterval(async () => { await safelyExecute(() => this._fetchGasFeeEstimateData()); }, this.intervalDelay); From 179dee6cb3872c29964ef46ae5e9c5de3372a2e6 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 18 Jun 2021 10:30:06 -0230 Subject: [PATCH 06/46] Network controller getEIP1559Compatibility can be called whenever a new transaction is created --- src/network/NetworkController.ts | 41 +++++++++++++++++++------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/src/network/NetworkController.ts b/src/network/NetworkController.ts index 8417c14d7f7..7bc71d9fa97 100644 --- a/src/network/NetworkController.ts +++ b/src/network/NetworkController.ts @@ -214,7 +214,7 @@ export class NetworkController extends BaseController< properties: { isEIP1559Compatible: false }, }; this.initialize(); - this.getNetworkProperties(); + this.getEIP1559Compatibility(); } /** @@ -300,21 +300,30 @@ export class NetworkController extends BaseController< this.refreshNetwork(); } - getNetworkProperties() { - this.ethQuery.sendAsync( - { method: 'eth_getBlockByNumber', params: ['latest', false] }, - (error: Error, block: Block) => { - if (error) { - console.error(error); - } else { - this.update({ - properties: { - isEIP1559Compatible: typeof block.baseFee !== undefined, - }, - }); - } - }, - ); + getEIP1559Compatibility() { + const { properties = {} } = this.state; + + if (!properties.isEIP1559Compatible) { + return new Promise((resolve, reject) => { + this.ethQuery.sendAsync( + { method: 'eth_getBlockByNumber', params: ['latest', false] }, + (error: Error, block: Block) => { + if (error) { + reject(error); + } else { + const isEIP1559Compatible = typeof block.baseFee !== undefined; + this.update({ + properties: { + isEIP1559Compatible, + }, + }); + resolve(isEIP1559Compatible); + } + }, + ); + }); + } + return Promise.resolve(true); } } From 55d3607c0c042ec6273d117c6dd4c4283c4926cd Mon Sep 17 00:00:00 2001 From: ricky Date: Fri, 18 Jun 2021 14:47:46 -0400 Subject: [PATCH 07/46] update tests (#495) --- src/ComposableController.test.ts | 2 ++ src/network/NetworkController.test.ts | 1 + src/network/NetworkController.ts | 3 +++ src/transaction/TransactionController.test.ts | 3 +++ 4 files changed, 9 insertions(+) diff --git a/src/ComposableController.test.ts b/src/ComposableController.test.ts index 189c6521f94..f33eb874f7b 100644 --- a/src/ComposableController.test.ts +++ b/src/ComposableController.test.ts @@ -132,6 +132,7 @@ describe('ComposableController', () => { }, NetworkController: { network: 'loading', + properties: { isEIP1559Compatible: false }, provider: { type: 'mainnet', chainId: NetworksChainId.mainnet }, }, PreferencesController: { @@ -188,6 +189,7 @@ describe('ComposableController', () => { ipfsGateway: 'https://ipfs.io/ipfs/', lostIdentities: {}, network: 'loading', + properties: { isEIP1559Compatible: false }, provider: { type: 'mainnet', chainId: NetworksChainId.mainnet }, selectedAddress: '', suggestedAssets: [], diff --git a/src/network/NetworkController.test.ts b/src/network/NetworkController.test.ts index b6a2bbf8a3c..faaf24aac42 100644 --- a/src/network/NetworkController.test.ts +++ b/src/network/NetworkController.test.ts @@ -13,6 +13,7 @@ describe('NetworkController', () => { const controller = new NetworkController(); expect(controller.state).toStrictEqual({ network: 'loading', + properties: { isEIP1559Compatible: false }, provider: { type: 'mainnet', chainId: '1', diff --git a/src/network/NetworkController.ts b/src/network/NetworkController.ts index 7bc71d9fa97..09a230ce5f2 100644 --- a/src/network/NetworkController.ts +++ b/src/network/NetworkController.ts @@ -304,6 +304,9 @@ export class NetworkController extends BaseController< const { properties = {} } = this.state; if (!properties.isEIP1559Compatible) { + if (!this.ethQuery || !this.ethQuery.sendAsync) { + return Promise.resolve(true); + } return new Promise((resolve, reject) => { this.ethQuery.sendAsync( { method: 'eth_getBlockByNumber', params: ['latest', false] }, diff --git a/src/transaction/TransactionController.test.ts b/src/transaction/TransactionController.test.ts index 7623a8b6a16..738f4baaf30 100644 --- a/src/transaction/TransactionController.test.ts +++ b/src/transaction/TransactionController.test.ts @@ -82,6 +82,7 @@ const MOCK_NETWORK = { getProvider: () => PROVIDER, state: { network: '3', + properties: { isEIP1559Compatible: false }, provider: { type: 'ropsten' as NetworkType, chainId: NetworksChainId.ropsten, @@ -98,6 +99,7 @@ const MOCK_MAINNET_NETWORK = { getProvider: () => MAINNET_PROVIDER, state: { network: '1', + properties: { isEIP1559Compatible: false }, provider: { type: 'mainnet' as NetworkType, chainId: NetworksChainId.mainnet, @@ -109,6 +111,7 @@ const MOCK_CUSTOM_NETWORK = { getProvider: () => MAINNET_PROVIDER, state: { network: '80001', + properties: { isEIP1559Compatible: false }, provider: { type: 'rpc' as NetworkType, chainId: '80001', From 16075b0afe91fd81d74dbdf21d4d5ad19ee6085b Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 21 Jun 2021 05:15:24 -0230 Subject: [PATCH 08/46] Fetch estimate using eth_gasPrice if on a custom network --- src/gas/GasFee.controller.ts | 77 ++++++++++++++++++++++++++++++++++-- src/gas/gas-util.ts | 9 ++++- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/src/gas/GasFee.controller.ts b/src/gas/GasFee.controller.ts index ea75c38cfe7..fa70b029561 100644 --- a/src/gas/GasFee.controller.ts +++ b/src/gas/GasFee.controller.ts @@ -1,9 +1,29 @@ import type { Patch } from 'immer'; +import EthQuery from 'eth-query'; import { BaseController } from '../BaseControllerV2'; import { safelyExecute } from '../util'; import type { RestrictedControllerMessenger } from '../ControllerMessenger'; -import { fetchGasEstimates as defaultFetchGasEstimates } from './gas-util'; +import type { + NetworkController, + NetworkState, +} from '../network/NetworkController'; +import { + fetchGasEstimates as defaultFetchGasEstimates, + fetchLegacyGasPriceEstimate as defaultFetchLegacyGasPriceEstimate, +} from './gas-util'; + +/** + * @type LegacyGasPriceEstimate + * + * A single gas price estimate for networks and accounts that don't support EIP-1559 + * + * @property gasPrice - A GWEI hex number, the result of a call to eth_gasPrice + */ + +export interface LegacyGasPriceEstimate { + gasPrice: string; +} /** * @type Eip1559GasFee @@ -42,7 +62,6 @@ export interface GasFeeEstimates { } const metadata = { - legacyGasPriceEstimates: { persist: true, anonymous: false }, gasFeeEstimates: { persist: true, anonymous: false }, }; @@ -55,7 +74,10 @@ const metadata = { * @property gasFeeEstimates - Gas fee estimate data based on new EIP-1559 properties */ export type GasFeeState = { - gasFeeEstimates: GasFeeEstimates | Record; + gasFeeEstimates: + | GasFeeEstimates + | LegacyGasPriceEstimate + | Record; }; const name = 'GasFeeController'; @@ -86,6 +108,14 @@ export class GasFeeController extends BaseController { private fetchGasEstimates; + private fetchLegacyGasPriceEstimate; + + private getCurrentNetworkEIP1559Compatibility; + + private getCurrentAccountEIP1559Compatibility; + + private ethQuery: any; + /** * Creates a GasFeeController instance * @@ -95,6 +125,11 @@ export class GasFeeController extends BaseController { messenger, state, fetchGasEstimates = defaultFetchGasEstimates, + fetchLegacyGasPriceEstimate = defaultFetchLegacyGasPriceEstimate, + getCurrentNetworkEIP1559Compatibility, + getCurrentAccountEIP1559Compatibility, + getProvider, + onNetworkStateChange, }: { interval?: number; messenger: RestrictedControllerMessenger< @@ -106,6 +141,11 @@ export class GasFeeController extends BaseController { >; state?: Partial; fetchGasEstimates?: typeof defaultFetchGasEstimates; + fetchLegacyGasPriceEstimate?: typeof defaultFetchLegacyGasPriceEstimate; + getCurrentNetworkEIP1559Compatibility: () => Promise; + getCurrentAccountEIP1559Compatibility?: () => boolean; + getProvider: () => NetworkController['provider']; + onNetworkStateChange: (listener: (state: NetworkState) => void) => void; }) { super({ name, @@ -115,7 +155,17 @@ export class GasFeeController extends BaseController { }); this.intervalDelay = interval; this.fetchGasEstimates = fetchGasEstimates; + this.fetchLegacyGasPriceEstimate = fetchLegacyGasPriceEstimate; this.pollTokens = new Set(); + this.getCurrentNetworkEIP1559Compatibility = getCurrentNetworkEIP1559Compatibility; + this.getCurrentAccountEIP1559Compatibility = getCurrentAccountEIP1559Compatibility; + + const provider = getProvider(); + this.ethQuery = new EthQuery(provider); + onNetworkStateChange(() => { + const newProvider = getProvider(); + this.ethQuery = new EthQuery(newProvider); + }); } async getGasFeeEstimatesAndStartPolling( @@ -140,8 +190,17 @@ export class GasFeeController extends BaseController { */ async _fetchGasFeeEstimateData(): Promise { let newEstimates = this.state; + let isEIP1559Compatible; try { - const estimates = await this.fetchGasEstimates(); + isEIP1559Compatible = await this.getEIP1559Compatibility(); + } catch (e) { + console.error(e); + isEIP1559Compatible = false; + } + try { + const estimates = isEIP1559Compatible + ? await this.fetchGasEstimates() + : await this.fetchLegacyGasPriceEstimate(this.ethQuery); newEstimates = { gasFeeEstimates: estimates, }; @@ -207,6 +266,16 @@ export class GasFeeController extends BaseController { private resetState() { this.state = defaultState; } + + private async getEIP1559Compatibility() { + const currentNetworkIsEIP1559Compatible = await this.getCurrentNetworkEIP1559Compatibility(); + const currentAccountIsEIP1559Compatible = + this.getCurrentAccountEIP1559Compatibility?.() ?? true; + + return ( + currentNetworkIsEIP1559Compatible && currentAccountIsEIP1559Compatible + ); + } } export default GasFeeController; diff --git a/src/gas/gas-util.ts b/src/gas/gas-util.ts index 4709de609a3..674f42118a2 100644 --- a/src/gas/gas-util.ts +++ b/src/gas/gas-util.ts @@ -1,4 +1,5 @@ -import { GasFeeEstimates } from './GasFee.controller'; +import { query } from '../util'; +import { GasFeeEstimates, LegacyGasPriceEstimate } from './GasFee.controller'; // import { handleFetch } from '../util'; @@ -143,3 +144,9 @@ export function fetchGasEstimates(): Promise { resolve(getMockApiResponse()); }); } + +export async function fetchLegacyGasPriceEstimate( + ethQuery: any, +): Promise { + return await query(ethQuery, 'gasPrice'); +} From b70d323f86d22a3684d1969d1d155a84238b017b Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 21 Jun 2021 05:51:18 -0230 Subject: [PATCH 09/46] getGasFeeEstimatesAndStartPolling returns poll token, and a new one if none is passed --- src/gas/GasFee.controller.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/gas/GasFee.controller.ts b/src/gas/GasFee.controller.ts index fa70b029561..9acf83dda7d 100644 --- a/src/gas/GasFee.controller.ts +++ b/src/gas/GasFee.controller.ts @@ -1,6 +1,7 @@ import type { Patch } from 'immer'; import EthQuery from 'eth-query'; +import { v1 as random } from 'uuid'; import { BaseController } from '../BaseControllerV2'; import { safelyExecute } from '../util'; import type { RestrictedControllerMessenger } from '../ControllerMessenger'; @@ -169,18 +170,17 @@ export class GasFeeController extends BaseController { } async getGasFeeEstimatesAndStartPolling( - pollToken: string, - ): Promise { - let gasEstimates; - if (this.pollTokens.size > 0) { - gasEstimates = this.state; - } else { - gasEstimates = await this._fetchGasFeeEstimateData(); + pollToken: string | undefined, + ): Promise { + if (this.pollTokens.size === 0) { + await this._fetchGasFeeEstimateData(); } - this._startPolling(pollToken); + const _pollToken = pollToken || random(); + + this._startPolling(_pollToken); - return gasEstimates; + return _pollToken; } /** From 0a8a2484795a92f35b257704c45e15b45c9d9d3a Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 21 Jun 2021 07:18:33 -0230 Subject: [PATCH 10/46] Add getTimeEstimate to GasFee controller --- src/gas/GasFee.controller.ts | 47 ++++++++++++++++++++++++ src/gas/gas-util.ts | 71 +++++++++++++++++++++++++++++++++++- 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/src/gas/GasFee.controller.ts b/src/gas/GasFee.controller.ts index 9acf83dda7d..25b5790947c 100644 --- a/src/gas/GasFee.controller.ts +++ b/src/gas/GasFee.controller.ts @@ -12,8 +12,16 @@ import type { import { fetchGasEstimates as defaultFetchGasEstimates, fetchLegacyGasPriceEstimate as defaultFetchLegacyGasPriceEstimate, + calculateTimeEstimate, } from './gas-util'; +export type unknownString = 'unknown'; + +export interface EstimatedGasFeeTimeBounds { + lowerTimeBound: number | null; + upperTimeBound: number | unknownString; +} + /** * @type LegacyGasPriceEstimate * @@ -44,6 +52,16 @@ interface Eip1559GasFee { suggestedMaxFeePerGas: string; // a GWEI hex number } +function isEIP1559GasFeee(object: any): object is Eip1559GasFee { + return ( + 'minWaitTimeEstimate' in object && + 'maxWaitTimeEstimate' in object && + 'suggestedMaxPriorityFeePerGas' in object && + 'suggestedMaxFeePerGas' in object && + Object.keys(object).length === 4 + ); +} + /** * @type GasFeeEstimates * @@ -62,6 +80,18 @@ export interface GasFeeEstimates { estimatedBaseFee: string; } +function isEIP1559Estimate(object: any): object is GasFeeEstimates { + return ( + 'low' in object && + isEIP1559GasFeee(object.low) && + 'medium' in object && + isEIP1559GasFeee(object.medium) && + 'high' in object && + isEIP1559GasFeee(object.high) && + 'estimatedBaseFee' in object + ); +} + const metadata = { gasFeeEstimates: { persist: true, anonymous: false }, }; @@ -276,6 +306,23 @@ export class GasFeeController extends BaseController { currentNetworkIsEIP1559Compatible && currentAccountIsEIP1559Compatible ); } + + getTimeEstimate( + maxPriorityFeePerGas: string, + maxFeePerGas: string, + ): EstimatedGasFeeTimeBounds | undefined { + if ( + !this.state.gasFeeEstimates || + !isEIP1559Estimate(this.state.gasFeeEstimates) + ) { + return undefined; + } + return calculateTimeEstimate( + maxPriorityFeePerGas, + maxFeePerGas, + this.state.gasFeeEstimates, + ); + } } export default GasFeeController; diff --git a/src/gas/gas-util.ts b/src/gas/gas-util.ts index 674f42118a2..828d6755c57 100644 --- a/src/gas/gas-util.ts +++ b/src/gas/gas-util.ts @@ -1,5 +1,11 @@ +import { BN } from 'ethereumjs-util'; import { query } from '../util'; -import { GasFeeEstimates, LegacyGasPriceEstimate } from './GasFee.controller'; +import { + GasFeeEstimates, + LegacyGasPriceEstimate, + EstimatedGasFeeTimeBounds, + unknownString, +} from './GasFee.controller'; // import { handleFetch } from '../util'; @@ -150,3 +156,66 @@ export async function fetchLegacyGasPriceEstimate( ): Promise { return await query(ethQuery, 'gasPrice'); } + +function gweiHexToWEIBN(n: any) { + const BN_1000 = new BN(1000, 10); + return new BN(n, 16).mul(BN_1000); +} + +export function calculateTimeEstimate( + maxPriorityFeePerGas: string, + maxFeePerGas: string, + gasFeeEstimates: GasFeeEstimates, +): EstimatedGasFeeTimeBounds { + const { low, medium, high, estimatedBaseFee } = gasFeeEstimates; + + const maxPriorityFeePerGasInWEI = gweiHexToWEIBN(maxPriorityFeePerGas); + const maxFeePerGasInWEI = gweiHexToWEIBN(maxFeePerGas); + const estimatedBaseFeeInWEI = gweiHexToWEIBN(estimatedBaseFee); + + const effectiveMaxPriorityFee = BN.min( + maxPriorityFeePerGasInWEI, + maxFeePerGasInWEI.sub(estimatedBaseFeeInWEI), + ); + + const lowMaxPriorityFeeInWEI = gweiHexToWEIBN( + low.suggestedMaxPriorityFeePerGas, + ); + const mediumMaxPriorityFeeInWEI = gweiHexToWEIBN( + medium.suggestedMaxPriorityFeePerGas, + ); + const highMaxPriorityFeeInWEI = gweiHexToWEIBN( + high.suggestedMaxPriorityFeePerGas, + ); + + let lowerTimeBound; + let upperTimeBound; + + if (effectiveMaxPriorityFee.lt(lowMaxPriorityFeeInWEI)) { + lowerTimeBound = null; + upperTimeBound = 'unknown' as unknownString; + } else if ( + effectiveMaxPriorityFee.gte(lowMaxPriorityFeeInWEI) && + effectiveMaxPriorityFee.lt(mediumMaxPriorityFeeInWEI) + ) { + lowerTimeBound = low.minWaitTimeEstimate; + upperTimeBound = low.maxWaitTimeEstimate; + } else if ( + effectiveMaxPriorityFee.gte(mediumMaxPriorityFeeInWEI) && + effectiveMaxPriorityFee.lt(highMaxPriorityFeeInWEI) + ) { + lowerTimeBound = medium.minWaitTimeEstimate; + upperTimeBound = medium.maxWaitTimeEstimate; + } else if (effectiveMaxPriorityFee.eq(highMaxPriorityFeeInWEI)) { + lowerTimeBound = high.minWaitTimeEstimate; + upperTimeBound = high.maxWaitTimeEstimate; + } else { + lowerTimeBound = 0; + upperTimeBound = high.maxWaitTimeEstimate; + } + + return { + lowerTimeBound, + upperTimeBound, + }; +} From cb884780eaa69ec1c3ce13bdc17fd82f26cd410e Mon Sep 17 00:00:00 2001 From: Ricky Miller Date: Mon, 21 Jun 2021 16:56:42 -0400 Subject: [PATCH 11/46] export GasFeeController --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index 587918200f7..ac49bf10335 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,4 +33,5 @@ export * from './message-manager/PersonalMessageManager'; export * from './message-manager/TypedMessageManager'; export * from './notification/NotificationController'; export * from './assets/TokenListController'; +export * from './gas/GasFee.controller'; export { util }; From 1ccd62ae78f2a88b8e4e581d8e06fd4c93ff6bcf Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 23 Jun 2021 10:13:45 -0230 Subject: [PATCH 12/46] Add public method for calling and returning gas estimates without polling --- src/gas/GasFee.controller.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/gas/GasFee.controller.ts b/src/gas/GasFee.controller.ts index 25b5790947c..d72a9834c5a 100644 --- a/src/gas/GasFee.controller.ts +++ b/src/gas/GasFee.controller.ts @@ -199,6 +199,10 @@ export class GasFeeController extends BaseController { }); } + async fetchGasFeeEstimates () { + return await this._fetchGasFeeEstimateData(); + } + async getGasFeeEstimatesAndStartPolling( pollToken: string | undefined, ): Promise { From dbf9f6cb9b928224708bed76a9670b630bb887c2 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 23 Jun 2021 10:59:42 -0230 Subject: [PATCH 13/46] Fix return of fetchLegacyGasPriceEstimate --- src/gas/gas-util.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/gas/gas-util.ts b/src/gas/gas-util.ts index 828d6755c57..73e6e1d55cd 100644 --- a/src/gas/gas-util.ts +++ b/src/gas/gas-util.ts @@ -154,7 +154,10 @@ export function fetchGasEstimates(): Promise { export async function fetchLegacyGasPriceEstimate( ethQuery: any, ): Promise { - return await query(ethQuery, 'gasPrice'); + const gasPrice = await query(ethQuery, 'gasPrice'); + return { + gasPrice, + }; } function gweiHexToWEIBN(n: any) { From 16b313bc85b2c60f37c2694d25aa422a60a4e7aa Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 23 Jun 2021 11:00:38 -0230 Subject: [PATCH 14/46] Proper error handling and fallback for _fetchGasFeeEstimateData --- src/gas/GasFee.controller.ts | 47 +++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/src/gas/GasFee.controller.ts b/src/gas/GasFee.controller.ts index d72a9834c5a..c334c5da296 100644 --- a/src/gas/GasFee.controller.ts +++ b/src/gas/GasFee.controller.ts @@ -199,7 +199,7 @@ export class GasFeeController extends BaseController { }); } - async fetchGasFeeEstimates () { + async fetchGasFeeEstimates() { return await this._fetchGasFeeEstimateData(); } @@ -223,7 +223,7 @@ export class GasFeeController extends BaseController { * @returns GasFeeEstimates */ async _fetchGasFeeEstimateData(): Promise { - let newEstimates = this.state; + let estimates; let isEIP1559Compatible; try { isEIP1559Compatible = await this.getEIP1559Compatibility(); @@ -231,25 +231,38 @@ export class GasFeeController extends BaseController { console.error(e); isEIP1559Compatible = false; } - try { - const estimates = isEIP1559Compatible - ? await this.fetchGasEstimates() - : await this.fetchLegacyGasPriceEstimate(this.ethQuery); - newEstimates = { - gasFeeEstimates: estimates, - }; - } catch (error) { - console.error(error); - } finally { + + if (isEIP1559Compatible) { try { - this.update(() => { - return newEstimates; - }); + estimates = await this.fetchGasEstimates(); } catch (error) { - console.error(error); + try { + estimates = await this.fetchLegacyGasPriceEstimate(this.ethQuery); + } catch (error2) { + throw new Error( + `Gas fee/price estimation failed. Message: ${error2.message}`, + ); + } + } + } else { + try { + estimates = await this.fetchLegacyGasPriceEstimate(this.ethQuery); + } catch (error2) { + throw new Error( + `Gas fee/price estimation failed. Message: ${error2.message}`, + ); } } - return newEstimates; + + const newState: GasFeeState = { + gasFeeEstimates: estimates, + }; + + this.update(() => { + return newState; + }); + + return newState; } /** From a782c9d5aa2f862150cd49a48b551dc731266ed6 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 23 Jun 2021 11:27:08 -0230 Subject: [PATCH 15/46] Include estimated time bounds in gas fee state --- src/gas/GasFee.controller.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/gas/GasFee.controller.ts b/src/gas/GasFee.controller.ts index c334c5da296..717bba2d122 100644 --- a/src/gas/GasFee.controller.ts +++ b/src/gas/GasFee.controller.ts @@ -94,6 +94,7 @@ function isEIP1559Estimate(object: any): object is GasFeeEstimates { const metadata = { gasFeeEstimates: { persist: true, anonymous: false }, + estimatedGasFeeTimeBounds: { persist: true, anonymous: false }, }; /** @@ -101,14 +102,15 @@ const metadata = { * * Gas Fee controller state * - * @property legacyGasPriceEstimates - Gas fee estimate data using the legacy `gasPrice` property * @property gasFeeEstimates - Gas fee estimate data based on new EIP-1559 properties + * @property estimatedGasFeeTimeBounds - Estimates representing the minimum and maximum */ export type GasFeeState = { gasFeeEstimates: | GasFeeEstimates | LegacyGasPriceEstimate | Record; + estimatedGasFeeTimeBounds: EstimatedGasFeeTimeBounds | Record; }; const name = 'GasFeeController'; @@ -125,6 +127,7 @@ export type GetGasFeeState = { const defaultState = { gasFeeEstimates: {}, + estimatedGasFeeTimeBounds: {}, }; /** @@ -224,6 +227,7 @@ export class GasFeeController extends BaseController { */ async _fetchGasFeeEstimateData(): Promise { let estimates; + let estimatedGasFeeTimeBounds = {}; let isEIP1559Compatible; try { isEIP1559Compatible = await this.getEIP1559Compatibility(); @@ -235,6 +239,14 @@ export class GasFeeController extends BaseController { if (isEIP1559Compatible) { try { estimates = await this.fetchGasEstimates(); + const { + suggestedMaxPriorityFeePerGas, + suggestedMaxFeePerGas, + } = estimates.medium; + estimatedGasFeeTimeBounds = this.getTimeEstimate( + suggestedMaxPriorityFeePerGas, + suggestedMaxFeePerGas, + ); } catch (error) { try { estimates = await this.fetchLegacyGasPriceEstimate(this.ethQuery); @@ -256,6 +268,7 @@ export class GasFeeController extends BaseController { const newState: GasFeeState = { gasFeeEstimates: estimates, + estimatedGasFeeTimeBounds, }; this.update(() => { @@ -327,12 +340,12 @@ export class GasFeeController extends BaseController { getTimeEstimate( maxPriorityFeePerGas: string, maxFeePerGas: string, - ): EstimatedGasFeeTimeBounds | undefined { + ): EstimatedGasFeeTimeBounds | Record { if ( !this.state.gasFeeEstimates || !isEIP1559Estimate(this.state.gasFeeEstimates) ) { - return undefined; + return {}; } return calculateTimeEstimate( maxPriorityFeePerGas, From 32b293033edb29e3734c0de85bcc94a42e104f53 Mon Sep 17 00:00:00 2001 From: Ricky Miller Date: Wed, 23 Jun 2021 14:03:07 -0400 Subject: [PATCH 16/46] rename --- src/gas/{GasFee.controller.ts => GasFeeController.ts} | 0 src/gas/gas-util.ts | 2 +- src/index.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/gas/{GasFee.controller.ts => GasFeeController.ts} (100%) diff --git a/src/gas/GasFee.controller.ts b/src/gas/GasFeeController.ts similarity index 100% rename from src/gas/GasFee.controller.ts rename to src/gas/GasFeeController.ts diff --git a/src/gas/gas-util.ts b/src/gas/gas-util.ts index 73e6e1d55cd..add62b32647 100644 --- a/src/gas/gas-util.ts +++ b/src/gas/gas-util.ts @@ -5,7 +5,7 @@ import { LegacyGasPriceEstimate, EstimatedGasFeeTimeBounds, unknownString, -} from './GasFee.controller'; +} from './GasFeeController'; // import { handleFetch } from '../util'; diff --git a/src/index.ts b/src/index.ts index ac49bf10335..2a5e203ebdd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,5 +33,5 @@ export * from './message-manager/PersonalMessageManager'; export * from './message-manager/TypedMessageManager'; export * from './notification/NotificationController'; export * from './assets/TokenListController'; -export * from './gas/GasFee.controller'; +export * from './gas/GasFeeController'; export { util }; From 97e9e8f8ffe4b14c50b492111cb50af24a43f6a8 Mon Sep 17 00:00:00 2001 From: Ricky Miller Date: Wed, 23 Jun 2021 15:28:55 -0400 Subject: [PATCH 17/46] Add GasFeeController.test.ts --- src/gas/GasFeeController.test.ts | 38 ++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/gas/GasFeeController.test.ts diff --git a/src/gas/GasFeeController.test.ts b/src/gas/GasFeeController.test.ts new file mode 100644 index 00000000000..ecfddafe979 --- /dev/null +++ b/src/gas/GasFeeController.test.ts @@ -0,0 +1,38 @@ +import { stub } from 'sinon'; +import { + ControllerMessenger, + RestrictedControllerMessenger, +} from '../ControllerMessenger'; +import { + GasFeeController, + GetGasFeeState, + GasFeeStateChange, +} from './GasFeeController'; + +const name = 'GasFeeController'; + +const controllerMessenger = new RestrictedControllerMessenger< + typeof name, + GetGasFeeState, + GasFeeStateChange, + never, + never +>({ + name, + controllerMessenger: new ControllerMessenger(), +}); + +describe('GasFeeController', () => { + it('should initialize', () => { + const controller = new GasFeeController({ + interval: 10000, + // TODO: fix this + messenger: controllerMessenger, + getProvider: () => stub(), + onNetworkStateChange: () => stub(), + getCurrentNetworkEIP1559Compatibility: () => Promise.resolve(true), // change this for networkController.state.properties.isEIP1559Compatible ??? + }); + + expect(controller.name).toBe(name); + }); +}); From 47f3659697e7d372188e07225e110075be752531 Mon Sep 17 00:00:00 2001 From: Ricky Miller Date: Wed, 23 Jun 2021 22:08:20 -0400 Subject: [PATCH 18/46] remove TODO --- src/gas/GasFeeController.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/gas/GasFeeController.test.ts b/src/gas/GasFeeController.test.ts index ecfddafe979..148f3555c33 100644 --- a/src/gas/GasFeeController.test.ts +++ b/src/gas/GasFeeController.test.ts @@ -26,7 +26,6 @@ describe('GasFeeController', () => { it('should initialize', () => { const controller = new GasFeeController({ interval: 10000, - // TODO: fix this messenger: controllerMessenger, getProvider: () => stub(), onNetworkStateChange: () => stub(), From 9971e68c6055d617e01146e313baa834ad64c872 Mon Sep 17 00:00:00 2001 From: Ricky Miller Date: Wed, 23 Jun 2021 22:22:44 -0400 Subject: [PATCH 19/46] Add result token length test --- src/gas/GasFeeController.test.ts | 7 +++++-- src/gas/GasFeeController.ts | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/gas/GasFeeController.test.ts b/src/gas/GasFeeController.test.ts index 148f3555c33..0012305c1ed 100644 --- a/src/gas/GasFeeController.test.ts +++ b/src/gas/GasFeeController.test.ts @@ -23,7 +23,7 @@ const controllerMessenger = new RestrictedControllerMessenger< }); describe('GasFeeController', () => { - it('should initialize', () => { + it('should getGasFeeEstimatesAndStartPolling', async () => { const controller = new GasFeeController({ interval: 10000, messenger: controllerMessenger, @@ -31,7 +31,10 @@ describe('GasFeeController', () => { onNetworkStateChange: () => stub(), getCurrentNetworkEIP1559Compatibility: () => Promise.resolve(true), // change this for networkController.state.properties.isEIP1559Compatible ??? }); - expect(controller.name).toBe(name); + const result = await controller.getGasFeeEstimatesAndStartPolling( + undefined, + ); + expect(result).toHaveLength(36); }); }); diff --git a/src/gas/GasFeeController.ts b/src/gas/GasFeeController.ts index 717bba2d122..9c72915f717 100644 --- a/src/gas/GasFeeController.ts +++ b/src/gas/GasFeeController.ts @@ -209,6 +209,7 @@ export class GasFeeController extends BaseController { async getGasFeeEstimatesAndStartPolling( pollToken: string | undefined, ): Promise { + console.log('pollTokens', this.pollTokens); if (this.pollTokens.size === 0) { await this._fetchGasFeeEstimateData(); } From 91a2cd273cb8d03d731c0bc60eb915ad7be5c06b Mon Sep 17 00:00:00 2001 From: Ricky Miller Date: Wed, 23 Jun 2021 22:36:38 -0400 Subject: [PATCH 20/46] Add estimates property test --- src/gas/GasFeeController.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/gas/GasFeeController.test.ts b/src/gas/GasFeeController.test.ts index 0012305c1ed..dd6be0138ec 100644 --- a/src/gas/GasFeeController.test.ts +++ b/src/gas/GasFeeController.test.ts @@ -36,5 +36,8 @@ describe('GasFeeController', () => { undefined, ); expect(result).toHaveLength(36); + + const estimates = await controller._fetchGasFeeEstimateData(); + expect(estimates).toHaveProperty('gasFeeEstimates'); }); }); From 0dec6e72d85c042b0026413ee9790a96b119bdde Mon Sep 17 00:00:00 2001 From: Ricky Miller Date: Thu, 24 Jun 2021 10:12:36 -0400 Subject: [PATCH 21/46] Add should fail to re-initialize test --- src/gas/GasFeeController.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/gas/GasFeeController.test.ts b/src/gas/GasFeeController.test.ts index dd6be0138ec..57a65d6cacc 100644 --- a/src/gas/GasFeeController.test.ts +++ b/src/gas/GasFeeController.test.ts @@ -40,4 +40,19 @@ describe('GasFeeController', () => { const estimates = await controller._fetchGasFeeEstimateData(); expect(estimates).toHaveProperty('gasFeeEstimates'); }); + + it('should fail to re-initialize', () => { + expect(() => { + const controller = new GasFeeController({ + interval: 10000, + messenger: controllerMessenger, + getProvider: () => stub(), + onNetworkStateChange: () => stub(), + getCurrentNetworkEIP1559Compatibility: () => Promise.resolve(true), // change this for networkController.state.properties.isEIP1559Compatible ??? + }); + console.log({ controller }); + }).toThrow( + 'A handler for GasFeeController:getState has already been registered', + ); + }); }); From e4e962e9aa700f5f155539316d92e27b5258f087 Mon Sep 17 00:00:00 2001 From: Ricky Miller Date: Thu, 24 Jun 2021 10:30:39 -0400 Subject: [PATCH 22/46] remove console.log --- src/gas/GasFeeController.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/gas/GasFeeController.ts b/src/gas/GasFeeController.ts index 9c72915f717..717bba2d122 100644 --- a/src/gas/GasFeeController.ts +++ b/src/gas/GasFeeController.ts @@ -209,7 +209,6 @@ export class GasFeeController extends BaseController { async getGasFeeEstimatesAndStartPolling( pollToken: string | undefined, ): Promise { - console.log('pollTokens', this.pollTokens); if (this.pollTokens.size === 0) { await this._fetchGasFeeEstimateData(); } From afdc385ca8daf2b37539d4923415742378ffa3fb Mon Sep 17 00:00:00 2001 From: Ricky Miller Date: Thu, 24 Jun 2021 12:03:37 -0400 Subject: [PATCH 23/46] do not modify state directly --- src/gas/GasFeeController.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/gas/GasFeeController.ts b/src/gas/GasFeeController.ts index 717bba2d122..1db4d0418a6 100644 --- a/src/gas/GasFeeController.ts +++ b/src/gas/GasFeeController.ts @@ -324,7 +324,9 @@ export class GasFeeController extends BaseController { } private resetState() { - this.state = defaultState; + this.update(() => { + return defaultState; + }); } private async getEIP1559Compatibility() { From fd4bd64562d861e7de855589732434ec0f3c02f0 Mon Sep 17 00:00:00 2001 From: Ricky Miller Date: Thu, 24 Jun 2021 12:35:17 -0400 Subject: [PATCH 24/46] Use before/afterEach and fix messenger --- src/gas/GasFeeController.test.ts | 70 ++++++++++++++++---------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/src/gas/GasFeeController.test.ts b/src/gas/GasFeeController.test.ts index 57a65d6cacc..b92a73bba23 100644 --- a/src/gas/GasFeeController.test.ts +++ b/src/gas/GasFeeController.test.ts @@ -1,8 +1,5 @@ import { stub } from 'sinon'; -import { - ControllerMessenger, - RestrictedControllerMessenger, -} from '../ControllerMessenger'; +import { ControllerMessenger } from '../ControllerMessenger'; import { GasFeeController, GetGasFeeState, @@ -11,48 +8,51 @@ import { const name = 'GasFeeController'; -const controllerMessenger = new RestrictedControllerMessenger< - typeof name, - GetGasFeeState, - GasFeeStateChange, - never, - never ->({ - name, - controllerMessenger: new ControllerMessenger(), -}); +function getRestrictedMessenger() { + const controllerMessenger = new ControllerMessenger< + GetGasFeeState, + GasFeeStateChange + >(); + const messenger = controllerMessenger.getRestricted< + typeof name, + never, + never + >({ + name, + }); + return messenger; +} describe('GasFeeController', () => { - it('should getGasFeeEstimatesAndStartPolling', async () => { - const controller = new GasFeeController({ + let gasFeeController: GasFeeController; + + beforeEach(() => { + gasFeeController = new GasFeeController({ interval: 10000, - messenger: controllerMessenger, + messenger: getRestrictedMessenger(), getProvider: () => stub(), onNetworkStateChange: () => stub(), getCurrentNetworkEIP1559Compatibility: () => Promise.resolve(true), // change this for networkController.state.properties.isEIP1559Compatible ??? }); - expect(controller.name).toBe(name); - const result = await controller.getGasFeeEstimatesAndStartPolling( + }); + + afterEach(() => { + gasFeeController.destroy(); + }); + + it('should initialize', async () => { + expect(gasFeeController.name).toBe(name); + }); + + it('should getGasFeeEstimatesAndStartPolling', async () => { + const result = await gasFeeController.getGasFeeEstimatesAndStartPolling( undefined, ); expect(result).toHaveLength(36); - - const estimates = await controller._fetchGasFeeEstimateData(); - expect(estimates).toHaveProperty('gasFeeEstimates'); }); - it('should fail to re-initialize', () => { - expect(() => { - const controller = new GasFeeController({ - interval: 10000, - messenger: controllerMessenger, - getProvider: () => stub(), - onNetworkStateChange: () => stub(), - getCurrentNetworkEIP1559Compatibility: () => Promise.resolve(true), // change this for networkController.state.properties.isEIP1559Compatible ??? - }); - console.log({ controller }); - }).toThrow( - 'A handler for GasFeeController:getState has already been registered', - ); + it('should _fetchGasFeeEstimateData', async () => { + const estimates = await gasFeeController._fetchGasFeeEstimateData(); + expect(estimates).toHaveProperty('gasFeeEstimates'); }); }); From b1c5631aab2f3e3d1721e8c7f87f2ef7876a9554 Mon Sep 17 00:00:00 2001 From: Ricky Miller Date: Thu, 24 Jun 2021 12:44:58 -0400 Subject: [PATCH 25/46] check gasFeeEstimates properties --- src/gas/GasFeeController.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/gas/GasFeeController.test.ts b/src/gas/GasFeeController.test.ts index b92a73bba23..c4ce0f6b2f7 100644 --- a/src/gas/GasFeeController.test.ts +++ b/src/gas/GasFeeController.test.ts @@ -49,6 +49,13 @@ describe('GasFeeController', () => { undefined, ); expect(result).toHaveLength(36); + + const { gasFeeEstimates } = gasFeeController.state; + + expect(gasFeeEstimates).toHaveProperty('low'); + expect(gasFeeEstimates).toHaveProperty('medium'); + expect(gasFeeEstimates).toHaveProperty('high'); + expect(gasFeeEstimates).toHaveProperty('estimatedBaseFee'); }); it('should _fetchGasFeeEstimateData', async () => { From 3646d62b779467b0529289b817cbc3dc4711f8cf Mon Sep 17 00:00:00 2001 From: Ricky Miller Date: Thu, 24 Jun 2021 12:58:27 -0400 Subject: [PATCH 26/46] check that state is empty to start --- src/gas/GasFeeController.test.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/gas/GasFeeController.test.ts b/src/gas/GasFeeController.test.ts index c4ce0f6b2f7..e17b8668994 100644 --- a/src/gas/GasFeeController.test.ts +++ b/src/gas/GasFeeController.test.ts @@ -45,20 +45,21 @@ describe('GasFeeController', () => { }); it('should getGasFeeEstimatesAndStartPolling', async () => { + expect(gasFeeController.state.gasFeeEstimates).toStrictEqual({}); const result = await gasFeeController.getGasFeeEstimatesAndStartPolling( undefined, ); expect(result).toHaveLength(36); - - const { gasFeeEstimates } = gasFeeController.state; - - expect(gasFeeEstimates).toHaveProperty('low'); - expect(gasFeeEstimates).toHaveProperty('medium'); - expect(gasFeeEstimates).toHaveProperty('high'); - expect(gasFeeEstimates).toHaveProperty('estimatedBaseFee'); + expect(gasFeeController.state.gasFeeEstimates).toHaveProperty('low'); + expect(gasFeeController.state.gasFeeEstimates).toHaveProperty('medium'); + expect(gasFeeController.state.gasFeeEstimates).toHaveProperty('high'); + expect(gasFeeController.state.gasFeeEstimates).toHaveProperty( + 'estimatedBaseFee', + ); }); it('should _fetchGasFeeEstimateData', async () => { + expect(gasFeeController.state.gasFeeEstimates).toStrictEqual({}); const estimates = await gasFeeController._fetchGasFeeEstimateData(); expect(estimates).toHaveProperty('gasFeeEstimates'); }); From 51dad0d1511bfe15f82bfc7b3b9bc516d4bd7fc4 Mon Sep 17 00:00:00 2001 From: Ricky Miller Date: Thu, 24 Jun 2021 13:15:51 -0400 Subject: [PATCH 27/46] Add one additional property check --- src/gas/GasFeeController.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/gas/GasFeeController.test.ts b/src/gas/GasFeeController.test.ts index e17b8668994..4cde4766f3e 100644 --- a/src/gas/GasFeeController.test.ts +++ b/src/gas/GasFeeController.test.ts @@ -62,5 +62,8 @@ describe('GasFeeController', () => { expect(gasFeeController.state.gasFeeEstimates).toStrictEqual({}); const estimates = await gasFeeController._fetchGasFeeEstimateData(); expect(estimates).toHaveProperty('gasFeeEstimates'); + expect(gasFeeController.state.gasFeeEstimates).toHaveProperty( + 'estimatedBaseFee', + ); }); }); From 4ac4ffedb635d6c25f4f7d5da80f0a85e54baa30 Mon Sep 17 00:00:00 2001 From: Niranjana Binoy <43930900+NiranjanaBinoy@users.noreply.github.com> Date: Mon, 28 Jun 2021 13:53:02 -0400 Subject: [PATCH 28/46] Adding TokenListController to fetch the token list from token services API (#478) --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index 2a5e203ebdd..9670251543d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,4 +34,5 @@ export * from './message-manager/TypedMessageManager'; export * from './notification/NotificationController'; export * from './assets/TokenListController'; export * from './gas/GasFeeController'; +export * from './assets/TokenListController'; export { util }; From c1ca5134e96eeb38f7c097e9284f42e79cd4c755 Mon Sep 17 00:00:00 2001 From: Ricky Miller Date: Tue, 29 Jun 2021 22:30:24 -0400 Subject: [PATCH 29/46] address feedback --- src/gas/GasFeeController.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/gas/GasFeeController.ts b/src/gas/GasFeeController.ts index 1db4d0418a6..4c4139bfcc8 100644 --- a/src/gas/GasFeeController.ts +++ b/src/gas/GasFeeController.ts @@ -45,14 +45,14 @@ export interface LegacyGasPriceEstimate { * @property suggestedMaxFeePerGas - A suggested max fee, the most a user will pay. a GWEI hex number */ -interface Eip1559GasFee { +export interface Eip1559GasFee { minWaitTimeEstimate: number; // a time duration in milliseconds maxWaitTimeEstimate: number; // a time duration in milliseconds suggestedMaxPriorityFeePerGas: string; // a GWEI hex number suggestedMaxFeePerGas: string; // a GWEI hex number } -function isEIP1559GasFeee(object: any): object is Eip1559GasFee { +function isEIP1559GasFee(object: any): object is Eip1559GasFee { return ( 'minWaitTimeEstimate' in object && 'maxWaitTimeEstimate' in object && @@ -83,11 +83,11 @@ export interface GasFeeEstimates { function isEIP1559Estimate(object: any): object is GasFeeEstimates { return ( 'low' in object && - isEIP1559GasFeee(object.low) && + isEIP1559GasFee(object.low) && 'medium' in object && - isEIP1559GasFeee(object.medium) && + isEIP1559GasFee(object.medium) && 'high' in object && - isEIP1559GasFeee(object.high) && + isEIP1559GasFee(object.high) && 'estimatedBaseFee' in object ); } From 04586793e0ef83d788d43b3fe1b265d21dcf8fb3 Mon Sep 17 00:00:00 2001 From: Ricky Miller Date: Tue, 15 Jun 2021 16:33:51 -0400 Subject: [PATCH 30/46] add mock server for eip1559 --- package.json | 3 +- src/apis/eip-1559-mock.js | 161 ++++++++++++++++++++++++++++++++++++++ src/gas/gas-util.ts | 146 +--------------------------------- 3 files changed, 167 insertions(+), 143 deletions(-) create mode 100644 src/apis/eip-1559-mock.js diff --git a/package.json b/package.json index c911f11f7c4..af92839e405 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "build": "rimraf dist && tsc --project .", "build:watch": "yarn build --watch", "build:link": "yarn build && cd dist && yarn link && rm -rf node_modules && cd ..", - "doc": "typedoc && touch docs/.nojekyll" + "doc": "typedoc && touch docs/.nojekyll", + "test:eip1559-server": "node ./src/apis/eip-1559-mock.js" }, "homepage": "https://github.com/MetaMask/controllers#readme", "repository": { diff --git a/src/apis/eip-1559-mock.js b/src/apis/eip-1559-mock.js new file mode 100644 index 00000000000..c3a8457ea75 --- /dev/null +++ b/src/apis/eip-1559-mock.js @@ -0,0 +1,161 @@ +const http = require('http'); + +// eslint-disable-next-line +const { PORT, HOSTNAME } = process.env; +const hostname = HOSTNAME || '127.0.0.1'; +const port = PORT || 3000; + +const end = (res, json) => { + res.statusCode = 200; + res.setHeader('Content-Type', 'application/json'); + console.log({ json }); + res.end(json); +}; + +const mockEIP1559ApiResponses = [ + { + low: { + minWaitTimeEstimate: 120000, + maxWaitTimeEstimate: 300000, + suggestedMaxPriorityFeePerGas: '1', + suggestedMaxFeePerGas: '35', + }, + medium: { + minWaitTimeEstimate: 0, + maxWaitTimeEstimate: 30000, + suggestedMaxPriorityFeePerGas: '2', + suggestedMaxFeePerGas: '40', + }, + high: { + minWaitTimeEstimate: 0, + maxWaitTimeEstimate: 150000, + suggestedMaxPriorityFeePerGas: '3', + suggestedMaxFeePerGas: '60', + }, + estimatedBaseFee: '30', + }, + { + low: { + minWaitTimeEstimate: 180000, + maxWaitTimeEstimate: 360000, + suggestedMaxPriorityFeePerGas: '1', + suggestedMaxFeePerGas: '40', + }, + medium: { + minWaitTimeEstimate: 15000, + maxWaitTimeEstimate: 60000, + suggestedMaxPriorityFeePerGas: '2', + suggestedMaxFeePerGas: '45', + }, + high: { + minWaitTimeEstimate: 0, + maxWaitTimeEstimate: 150000, + suggestedMaxPriorityFeePerGas: '3', + suggestedMaxFeePerGas: '65', + }, + estimatedBaseFee: '32', + }, + { + low: { + minWaitTimeEstimate: 60000, + maxWaitTimeEstimate: 240000, + suggestedMaxPriorityFeePerGas: '1', + suggestedMaxFeePerGas: '42', + }, + medium: { + minWaitTimeEstimate: 0, + maxWaitTimeEstimate: 30000, + suggestedMaxPriorityFeePerGas: '3', + suggestedMaxFeePerGas: '47', + }, + high: { + minWaitTimeEstimate: 0, + maxWaitTimeEstimate: 150000, + suggestedMaxPriorityFeePerGas: '4', + suggestedMaxFeePerGas: '67', + }, + estimatedBaseFee: '35', + }, + { + low: { + minWaitTimeEstimate: 180000, + maxWaitTimeEstimate: 300000, + suggestedMaxPriorityFeePerGas: '3', + suggestedMaxFeePerGas: '53', + }, + medium: { + minWaitTimeEstimate: 15000, + maxWaitTimeEstimate: 60000, + suggestedMaxPriorityFeePerGas: '7', + suggestedMaxFeePerGas: '70', + }, + high: { + minWaitTimeEstimate: 0, + maxWaitTimeEstimate: 150000, + suggestedMaxPriorityFeePerGas: '10', + suggestedMaxFeePerGas: '100', + }, + estimatedBaseFee: '50', + }, + { + low: { + minWaitTimeEstimate: 120000, + maxWaitTimeEstimate: 360000, + suggestedMaxPriorityFeePerGas: '1', + suggestedMaxFeePerGas: '35', + }, + medium: { + minWaitTimeEstimate: 15000, + maxWaitTimeEstimate: 60000, + suggestedMaxPriorityFeePerGas: '3', + suggestedMaxFeePerGas: '40', + }, + high: { + minWaitTimeEstimate: 0, + maxWaitTimeEstimate: 150000, + suggestedMaxPriorityFeePerGas: '4', + suggestedMaxFeePerGas: '60', + }, + estimatedBaseFee: '30', + }, + { + low: { + minWaitTimeEstimate: 60000, + maxWaitTimeEstimate: 600000, + suggestedMaxPriorityFeePerGas: '1', + suggestedMaxFeePerGas: '35', + }, + medium: { + minWaitTimeEstimate: 15000, + maxWaitTimeEstimate: 60000, + suggestedMaxPriorityFeePerGas: '1.8', + suggestedMaxFeePerGas: '38', + }, + high: { + minWaitTimeEstimate: 0, + maxWaitTimeEstimate: 150000, + suggestedMaxPriorityFeePerGas: '2', + suggestedMaxFeePerGas: '50', + }, + estimatedBaseFee: '28', + }, +]; + +const getMockApiResponse = () => { + return mockEIP1559ApiResponses[Math.floor(Math.random() * 6)]; +}; + +const get_payload = () => { + return getMockApiResponse(); +}; + +const server = http.createServer((_, res) => { + const json = JSON.stringify(get_payload()); + end(res, json); +}); + +server.listen(port, hostname, () => { + const url = `http://${hostname}:${port}/`; + console.log(`Mock server running at: ${url}`); + console.log(`You can now: \`curl ${url}\``); +}); diff --git a/src/gas/gas-util.ts b/src/gas/gas-util.ts index add62b32647..607377ad59f 100644 --- a/src/gas/gas-util.ts +++ b/src/gas/gas-util.ts @@ -1,5 +1,5 @@ import { BN } from 'ethereumjs-util'; -import { query } from '../util'; +import { query, handleFetch } from '../util'; import { GasFeeEstimates, LegacyGasPriceEstimate, @@ -7,148 +7,10 @@ import { unknownString, } from './GasFeeController'; -// import { handleFetch } from '../util'; +const GAS_FEE_API = 'http://127.0.0.1:3000'; -// const GAS_FEE_API = 'https://gas-fee-api-goes-here'; - -const mockEIP1559ApiResponses = [ - { - low: { - minWaitTimeEstimate: 120000, - maxWaitTimeEstimate: 300000, - suggestedMaxPriorityFeePerGas: '1', - suggestedMaxFeePerGas: '35', - }, - medium: { - minWaitTimeEstimate: 0, - maxWaitTimeEstimate: 30000, - suggestedMaxPriorityFeePerGas: '2', - suggestedMaxFeePerGas: '40', - }, - high: { - minWaitTimeEstimate: 0, - maxWaitTimeEstimate: 150000, - suggestedMaxPriorityFeePerGas: '3', - suggestedMaxFeePerGas: '60', - }, - estimatedBaseFee: '30', - }, - { - low: { - minWaitTimeEstimate: 180000, - maxWaitTimeEstimate: 360000, - suggestedMaxPriorityFeePerGas: '1', - suggestedMaxFeePerGas: '40', - }, - medium: { - minWaitTimeEstimate: 15000, - maxWaitTimeEstimate: 60000, - suggestedMaxPriorityFeePerGas: '2', - suggestedMaxFeePerGas: '45', - }, - high: { - minWaitTimeEstimate: 0, - maxWaitTimeEstimate: 150000, - suggestedMaxPriorityFeePerGas: '3', - suggestedMaxFeePerGas: '65', - }, - estimatedBaseFee: '32', - }, - { - low: { - minWaitTimeEstimate: 60000, - maxWaitTimeEstimate: 240000, - suggestedMaxPriorityFeePerGas: '1', - suggestedMaxFeePerGas: '42', - }, - medium: { - minWaitTimeEstimate: 0, - maxWaitTimeEstimate: 30000, - suggestedMaxPriorityFeePerGas: '3', - suggestedMaxFeePerGas: '47', - }, - high: { - minWaitTimeEstimate: 0, - maxWaitTimeEstimate: 150000, - suggestedMaxPriorityFeePerGas: '4', - suggestedMaxFeePerGas: '67', - }, - estimatedBaseFee: '35', - }, - { - low: { - minWaitTimeEstimate: 180000, - maxWaitTimeEstimate: 300000, - suggestedMaxPriorityFeePerGas: '3', - suggestedMaxFeePerGas: '53', - }, - medium: { - minWaitTimeEstimate: 15000, - maxWaitTimeEstimate: 60000, - suggestedMaxPriorityFeePerGas: '7', - suggestedMaxFeePerGas: '70', - }, - high: { - minWaitTimeEstimate: 0, - maxWaitTimeEstimate: 150000, - suggestedMaxPriorityFeePerGas: '10', - suggestedMaxFeePerGas: '100', - }, - estimatedBaseFee: '50', - }, - { - low: { - minWaitTimeEstimate: 120000, - maxWaitTimeEstimate: 360000, - suggestedMaxPriorityFeePerGas: '1', - suggestedMaxFeePerGas: '35', - }, - medium: { - minWaitTimeEstimate: 15000, - maxWaitTimeEstimate: 60000, - suggestedMaxPriorityFeePerGas: '3', - suggestedMaxFeePerGas: '40', - }, - high: { - minWaitTimeEstimate: 0, - maxWaitTimeEstimate: 150000, - suggestedMaxPriorityFeePerGas: '4', - suggestedMaxFeePerGas: '60', - }, - estimatedBaseFee: '30', - }, - { - low: { - minWaitTimeEstimate: 60000, - maxWaitTimeEstimate: 600000, - suggestedMaxPriorityFeePerGas: '1', - suggestedMaxFeePerGas: '35', - }, - medium: { - minWaitTimeEstimate: 15000, - maxWaitTimeEstimate: 60000, - suggestedMaxPriorityFeePerGas: '1.8', - suggestedMaxFeePerGas: '38', - }, - high: { - minWaitTimeEstimate: 0, - maxWaitTimeEstimate: 150000, - suggestedMaxPriorityFeePerGas: '2', - suggestedMaxFeePerGas: '50', - }, - estimatedBaseFee: '28', - }, -]; - -const getMockApiResponse = (): GasFeeEstimates => { - return mockEIP1559ApiResponses[Math.floor(Math.random() * 6)]; -}; - -export function fetchGasEstimates(): Promise { - // return handleFetch(GAS_FEE_API) - return new Promise((resolve) => { - resolve(getMockApiResponse()); - }); +export async function fetchGasEstimates(): Promise { + return await handleFetch(GAS_FEE_API); } export async function fetchLegacyGasPriceEstimate( From 728c0397ff241b85cdc25f5f0dc163042a06d824 Mon Sep 17 00:00:00 2001 From: Ricky Miller Date: Tue, 29 Jun 2021 23:23:32 -0400 Subject: [PATCH 31/46] get tests working again --- .github/workflows/build-lint-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-lint-test.yml b/.github/workflows/build-lint-test.yml index 2b1407cc454..973db7caf53 100644 --- a/.github/workflows/build-lint-test.yml +++ b/.github/workflows/build-lint-test.yml @@ -34,7 +34,7 @@ jobs: - run: yarn allow-scripts - run: yarn build - run: yarn lint - - run: yarn test --maxWorkers=1 + - run: yarn test:eip1559-server & yarn test --maxWorkers=1 all-jobs-pass: name: All jobs pass runs-on: ubuntu-20.04 From cd90799fe16b0698e7f4d10ca3b3d1d01a902c4c Mon Sep 17 00:00:00 2001 From: Ricky Miller Date: Wed, 30 Jun 2021 10:12:30 -0400 Subject: [PATCH 32/46] Use heroku endpoint --- .github/workflows/build-lint-test.yml | 2 +- package.json | 3 +- src/apis/eip-1559-mock.js | 161 -------------------------- src/gas/gas-util.ts | 2 +- 4 files changed, 3 insertions(+), 165 deletions(-) delete mode 100644 src/apis/eip-1559-mock.js diff --git a/.github/workflows/build-lint-test.yml b/.github/workflows/build-lint-test.yml index 973db7caf53..2b1407cc454 100644 --- a/.github/workflows/build-lint-test.yml +++ b/.github/workflows/build-lint-test.yml @@ -34,7 +34,7 @@ jobs: - run: yarn allow-scripts - run: yarn build - run: yarn lint - - run: yarn test:eip1559-server & yarn test --maxWorkers=1 + - run: yarn test --maxWorkers=1 all-jobs-pass: name: All jobs pass runs-on: ubuntu-20.04 diff --git a/package.json b/package.json index af92839e405..c911f11f7c4 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,7 @@ "build": "rimraf dist && tsc --project .", "build:watch": "yarn build --watch", "build:link": "yarn build && cd dist && yarn link && rm -rf node_modules && cd ..", - "doc": "typedoc && touch docs/.nojekyll", - "test:eip1559-server": "node ./src/apis/eip-1559-mock.js" + "doc": "typedoc && touch docs/.nojekyll" }, "homepage": "https://github.com/MetaMask/controllers#readme", "repository": { diff --git a/src/apis/eip-1559-mock.js b/src/apis/eip-1559-mock.js deleted file mode 100644 index c3a8457ea75..00000000000 --- a/src/apis/eip-1559-mock.js +++ /dev/null @@ -1,161 +0,0 @@ -const http = require('http'); - -// eslint-disable-next-line -const { PORT, HOSTNAME } = process.env; -const hostname = HOSTNAME || '127.0.0.1'; -const port = PORT || 3000; - -const end = (res, json) => { - res.statusCode = 200; - res.setHeader('Content-Type', 'application/json'); - console.log({ json }); - res.end(json); -}; - -const mockEIP1559ApiResponses = [ - { - low: { - minWaitTimeEstimate: 120000, - maxWaitTimeEstimate: 300000, - suggestedMaxPriorityFeePerGas: '1', - suggestedMaxFeePerGas: '35', - }, - medium: { - minWaitTimeEstimate: 0, - maxWaitTimeEstimate: 30000, - suggestedMaxPriorityFeePerGas: '2', - suggestedMaxFeePerGas: '40', - }, - high: { - minWaitTimeEstimate: 0, - maxWaitTimeEstimate: 150000, - suggestedMaxPriorityFeePerGas: '3', - suggestedMaxFeePerGas: '60', - }, - estimatedBaseFee: '30', - }, - { - low: { - minWaitTimeEstimate: 180000, - maxWaitTimeEstimate: 360000, - suggestedMaxPriorityFeePerGas: '1', - suggestedMaxFeePerGas: '40', - }, - medium: { - minWaitTimeEstimate: 15000, - maxWaitTimeEstimate: 60000, - suggestedMaxPriorityFeePerGas: '2', - suggestedMaxFeePerGas: '45', - }, - high: { - minWaitTimeEstimate: 0, - maxWaitTimeEstimate: 150000, - suggestedMaxPriorityFeePerGas: '3', - suggestedMaxFeePerGas: '65', - }, - estimatedBaseFee: '32', - }, - { - low: { - minWaitTimeEstimate: 60000, - maxWaitTimeEstimate: 240000, - suggestedMaxPriorityFeePerGas: '1', - suggestedMaxFeePerGas: '42', - }, - medium: { - minWaitTimeEstimate: 0, - maxWaitTimeEstimate: 30000, - suggestedMaxPriorityFeePerGas: '3', - suggestedMaxFeePerGas: '47', - }, - high: { - minWaitTimeEstimate: 0, - maxWaitTimeEstimate: 150000, - suggestedMaxPriorityFeePerGas: '4', - suggestedMaxFeePerGas: '67', - }, - estimatedBaseFee: '35', - }, - { - low: { - minWaitTimeEstimate: 180000, - maxWaitTimeEstimate: 300000, - suggestedMaxPriorityFeePerGas: '3', - suggestedMaxFeePerGas: '53', - }, - medium: { - minWaitTimeEstimate: 15000, - maxWaitTimeEstimate: 60000, - suggestedMaxPriorityFeePerGas: '7', - suggestedMaxFeePerGas: '70', - }, - high: { - minWaitTimeEstimate: 0, - maxWaitTimeEstimate: 150000, - suggestedMaxPriorityFeePerGas: '10', - suggestedMaxFeePerGas: '100', - }, - estimatedBaseFee: '50', - }, - { - low: { - minWaitTimeEstimate: 120000, - maxWaitTimeEstimate: 360000, - suggestedMaxPriorityFeePerGas: '1', - suggestedMaxFeePerGas: '35', - }, - medium: { - minWaitTimeEstimate: 15000, - maxWaitTimeEstimate: 60000, - suggestedMaxPriorityFeePerGas: '3', - suggestedMaxFeePerGas: '40', - }, - high: { - minWaitTimeEstimate: 0, - maxWaitTimeEstimate: 150000, - suggestedMaxPriorityFeePerGas: '4', - suggestedMaxFeePerGas: '60', - }, - estimatedBaseFee: '30', - }, - { - low: { - minWaitTimeEstimate: 60000, - maxWaitTimeEstimate: 600000, - suggestedMaxPriorityFeePerGas: '1', - suggestedMaxFeePerGas: '35', - }, - medium: { - minWaitTimeEstimate: 15000, - maxWaitTimeEstimate: 60000, - suggestedMaxPriorityFeePerGas: '1.8', - suggestedMaxFeePerGas: '38', - }, - high: { - minWaitTimeEstimate: 0, - maxWaitTimeEstimate: 150000, - suggestedMaxPriorityFeePerGas: '2', - suggestedMaxFeePerGas: '50', - }, - estimatedBaseFee: '28', - }, -]; - -const getMockApiResponse = () => { - return mockEIP1559ApiResponses[Math.floor(Math.random() * 6)]; -}; - -const get_payload = () => { - return getMockApiResponse(); -}; - -const server = http.createServer((_, res) => { - const json = JSON.stringify(get_payload()); - end(res, json); -}); - -server.listen(port, hostname, () => { - const url = `http://${hostname}:${port}/`; - console.log(`Mock server running at: ${url}`); - console.log(`You can now: \`curl ${url}\``); -}); diff --git a/src/gas/gas-util.ts b/src/gas/gas-util.ts index 607377ad59f..8d6c8256b9e 100644 --- a/src/gas/gas-util.ts +++ b/src/gas/gas-util.ts @@ -7,7 +7,7 @@ import { unknownString, } from './GasFeeController'; -const GAS_FEE_API = 'http://127.0.0.1:3000'; +const GAS_FEE_API = 'https://mock-gas-server.herokuapp.com/'; export async function fetchGasEstimates(): Promise { return await handleFetch(GAS_FEE_API); From 8d570bb7cb0f4b8eaa4f2fe3db6f6c5f105f518b Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 30 Jun 2021 11:44:50 -0230 Subject: [PATCH 33/46] Handle fetch correctly in gasfeecontroller unit tests, using nock --- src/gas/GasFeeController.test.ts | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/gas/GasFeeController.test.ts b/src/gas/GasFeeController.test.ts index 4cde4766f3e..fd0101d90ae 100644 --- a/src/gas/GasFeeController.test.ts +++ b/src/gas/GasFeeController.test.ts @@ -1,4 +1,5 @@ import { stub } from 'sinon'; +import nock from 'nock'; import { ControllerMessenger } from '../ControllerMessenger'; import { GasFeeController, @@ -6,6 +7,8 @@ import { GasFeeStateChange, } from './GasFeeController'; +const GAS_FEE_API = 'https://mock-gas-server.herokuapp.com/'; + const name = 'GasFeeController'; function getRestrictedMessenger() { @@ -26,7 +29,44 @@ function getRestrictedMessenger() { describe('GasFeeController', () => { let gasFeeController: GasFeeController; + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.enableNetConnect(); + }); + + afterEach(() => { + nock.cleanAll(); + }); + beforeEach(() => { + nock(GAS_FEE_API) + .get('/') + .reply(200, { + low: { + minWaitTimeEstimate: 60000, + maxWaitTimeEstimate: 600000, + suggestedMaxPriorityFeePerGas: '1', + suggestedMaxFeePerGas: '35', + }, + medium: { + minWaitTimeEstimate: 15000, + maxWaitTimeEstimate: 60000, + suggestedMaxPriorityFeePerGas: '1.8', + suggestedMaxFeePerGas: '38', + }, + high: { + minWaitTimeEstimate: 0, + maxWaitTimeEstimate: 15000, + suggestedMaxPriorityFeePerGas: '2', + suggestedMaxFeePerGas: '50', + }, + estimatedBaseFee: '28', + }, + ) + gasFeeController = new GasFeeController({ interval: 10000, messenger: getRestrictedMessenger(), From 2e2b1a72952333c87f9bc01d5496b3e9765dbd20 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 30 Jun 2021 10:58:10 -0230 Subject: [PATCH 34/46] gasFee controller calculateTimeEstimate handles decimals, by way of updated gweiDecToWEIBN util --- src/gas/gas-util.ts | 19 +++++++------------ src/util.test.ts | 34 ++++++++++++++++++++++++++++++++++ src/util.ts | 21 +++++++++++++++++++++ 3 files changed, 62 insertions(+), 12 deletions(-) diff --git a/src/gas/gas-util.ts b/src/gas/gas-util.ts index 8d6c8256b9e..0f98eeed08e 100644 --- a/src/gas/gas-util.ts +++ b/src/gas/gas-util.ts @@ -1,5 +1,5 @@ import { BN } from 'ethereumjs-util'; -import { query, handleFetch } from '../util'; +import { query, handleFetch, gweiDecToWEIBN } from '../util'; import { GasFeeEstimates, LegacyGasPriceEstimate, @@ -22,11 +22,6 @@ export async function fetchLegacyGasPriceEstimate( }; } -function gweiHexToWEIBN(n: any) { - const BN_1000 = new BN(1000, 10); - return new BN(n, 16).mul(BN_1000); -} - export function calculateTimeEstimate( maxPriorityFeePerGas: string, maxFeePerGas: string, @@ -34,22 +29,22 @@ export function calculateTimeEstimate( ): EstimatedGasFeeTimeBounds { const { low, medium, high, estimatedBaseFee } = gasFeeEstimates; - const maxPriorityFeePerGasInWEI = gweiHexToWEIBN(maxPriorityFeePerGas); - const maxFeePerGasInWEI = gweiHexToWEIBN(maxFeePerGas); - const estimatedBaseFeeInWEI = gweiHexToWEIBN(estimatedBaseFee); + const maxPriorityFeePerGasInWEI = gweiDecToWEIBN(maxPriorityFeePerGas); + const maxFeePerGasInWEI = gweiDecToWEIBN(maxFeePerGas); + const estimatedBaseFeeInWEI = gweiDecToWEIBN(estimatedBaseFee); const effectiveMaxPriorityFee = BN.min( maxPriorityFeePerGasInWEI, maxFeePerGasInWEI.sub(estimatedBaseFeeInWEI), ); - const lowMaxPriorityFeeInWEI = gweiHexToWEIBN( + const lowMaxPriorityFeeInWEI = gweiDecToWEIBN( low.suggestedMaxPriorityFeePerGas, ); - const mediumMaxPriorityFeeInWEI = gweiHexToWEIBN( + const mediumMaxPriorityFeeInWEI = gweiDecToWEIBN( medium.suggestedMaxPriorityFeePerGas, ); - const highMaxPriorityFeeInWEI = gweiHexToWEIBN( + const highMaxPriorityFeeInWEI = gweiDecToWEIBN( high.suggestedMaxPriorityFeePerGas, ); diff --git a/src/util.test.ts b/src/util.test.ts index c615a6b9642..3a9128fe44f 100644 --- a/src/util.test.ts +++ b/src/util.test.ts @@ -107,6 +107,40 @@ describe('util', () => { }); }); + describe('gweiDecToWEIBN', () => { + it('should convert a whole number to WEI', () => { + expect(util.gweiDecToWEIBN(1).toNumber()).toBe(1000); + expect(util.gweiDecToWEIBN(123).toNumber()).toBe(123000); + expect(util.gweiDecToWEIBN(101).toNumber()).toBe(101000); + expect(util.gweiDecToWEIBN(1234).toNumber()).toBe(1234000); + }); + + it('should convert a number with a decimal part to WEI', () => { + expect(util.gweiDecToWEIBN(1.1).toNumber()).toBe(1100); + expect(util.gweiDecToWEIBN(123.01).toNumber()).toBe(123010); + expect(util.gweiDecToWEIBN(101.001).toNumber()).toBe(101001); + expect(util.gweiDecToWEIBN(1234.567).toNumber()).toBe(1234567); + }); + + it('should convert a number < 1 to WEI', () => { + expect(util.gweiDecToWEIBN(0.1).toNumber()).toBe(100); + expect(util.gweiDecToWEIBN(0.01).toNumber()).toBe(10); + expect(util.gweiDecToWEIBN(0.001).toNumber()).toBe(1); + expect(util.gweiDecToWEIBN(0.567).toNumber()).toBe(567); + }); + + it('should round to whole WEI numbers', () => { + expect(util.gweiDecToWEIBN(0.1001).toNumber()).toBe(100); + expect(util.gweiDecToWEIBN(0.0109).toNumber()).toBe(11); + expect(util.gweiDecToWEIBN(0.0014).toNumber()).toBe(1); + expect(util.gweiDecToWEIBN(0.5676).toNumber()).toBe(568); + }); + + it('should handle NaN', () => { + expect(util.gweiDecToWEIBN(NaN).toNumber()).toBe(0); + }); + }); + describe('safelyExecute', () => { it('should swallow errors', async () => { expect( diff --git a/src/util.ts b/src/util.ts index ef0e0eb703d..7f7149ab03a 100644 --- a/src/util.ts +++ b/src/util.ts @@ -62,6 +62,27 @@ export function fractionBN( return targetBN.mul(numBN).div(denomBN); } +const BN_1000 = new BN(1000, 10); + +/** + * Used to convert a base-10 number from GWEI to WEI. Can handle numbers with decimal parts + * + * @param n - The base 10 number to convert to WEI + * @returns - The number in WEI, as a BN + */ +export function gweiDecToWEIBN(n: number | string) { + const wholePart = Math.floor(Number(n)); + const decimalPartMatch = Number(n) + .toFixed(3) + .match(/\.(\d+)/u); + const decimalPart = decimalPartMatch ? decimalPartMatch[1] : '0'; + + const wholePartAsWEI = new BN(wholePart, 10).mul(BN_1000); + const decimalPartAsWEI = new BN(decimalPart, 10); + + return wholePartAsWEI.add(decimalPartAsWEI); +} + /** * Return a URL that can be used to obtain ETH for a given network * From 1516d87b685990ea33f3bcaa33a43c2021acf26e Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 30 Jun 2021 11:47:41 -0230 Subject: [PATCH 35/46] Lint fix --- src/gas/GasFeeController.test.ts | 46 +++++++++++++++----------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/src/gas/GasFeeController.test.ts b/src/gas/GasFeeController.test.ts index fd0101d90ae..c88997a4cba 100644 --- a/src/gas/GasFeeController.test.ts +++ b/src/gas/GasFeeController.test.ts @@ -37,35 +37,30 @@ describe('GasFeeController', () => { nock.enableNetConnect(); }); - afterEach(() => { - nock.cleanAll(); - }); - beforeEach(() => { nock(GAS_FEE_API) .get('/') - .reply(200, { - low: { - minWaitTimeEstimate: 60000, - maxWaitTimeEstimate: 600000, - suggestedMaxPriorityFeePerGas: '1', - suggestedMaxFeePerGas: '35', - }, - medium: { - minWaitTimeEstimate: 15000, - maxWaitTimeEstimate: 60000, - suggestedMaxPriorityFeePerGas: '1.8', - suggestedMaxFeePerGas: '38', - }, - high: { - minWaitTimeEstimate: 0, - maxWaitTimeEstimate: 15000, - suggestedMaxPriorityFeePerGas: '2', - suggestedMaxFeePerGas: '50', - }, - estimatedBaseFee: '28', + .reply(200, { + low: { + minWaitTimeEstimate: 60000, + maxWaitTimeEstimate: 600000, + suggestedMaxPriorityFeePerGas: '1', + suggestedMaxFeePerGas: '35', }, - ) + medium: { + minWaitTimeEstimate: 15000, + maxWaitTimeEstimate: 60000, + suggestedMaxPriorityFeePerGas: '1.8', + suggestedMaxFeePerGas: '38', + }, + high: { + minWaitTimeEstimate: 0, + maxWaitTimeEstimate: 15000, + suggestedMaxPriorityFeePerGas: '2', + suggestedMaxFeePerGas: '50', + }, + estimatedBaseFee: '28', + }); gasFeeController = new GasFeeController({ interval: 10000, @@ -77,6 +72,7 @@ describe('GasFeeController', () => { }); afterEach(() => { + nock.cleanAll(); gasFeeController.destroy(); }); From 6eac11c679bd98b857760ff0f2fec87f9decd975 Mon Sep 17 00:00:00 2001 From: Brad Decker Date: Thu, 1 Jul 2021 10:02:24 -0500 Subject: [PATCH 36/46] Fix dec to gwi (#504) --- src/util.test.ts | 32 ++++++++++++++++---------------- src/util.ts | 6 +++--- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/util.test.ts b/src/util.test.ts index 3a9128fe44f..b0c35e827b4 100644 --- a/src/util.test.ts +++ b/src/util.test.ts @@ -109,31 +109,31 @@ describe('util', () => { describe('gweiDecToWEIBN', () => { it('should convert a whole number to WEI', () => { - expect(util.gweiDecToWEIBN(1).toNumber()).toBe(1000); - expect(util.gweiDecToWEIBN(123).toNumber()).toBe(123000); - expect(util.gweiDecToWEIBN(101).toNumber()).toBe(101000); - expect(util.gweiDecToWEIBN(1234).toNumber()).toBe(1234000); + expect(util.gweiDecToWEIBN(1).toNumber()).toBe(1000000000); + expect(util.gweiDecToWEIBN(123).toNumber()).toBe(123000000000); + expect(util.gweiDecToWEIBN(101).toNumber()).toBe(101000000000); + expect(util.gweiDecToWEIBN(1234).toNumber()).toBe(1234000000000); }); it('should convert a number with a decimal part to WEI', () => { - expect(util.gweiDecToWEIBN(1.1).toNumber()).toBe(1100); - expect(util.gweiDecToWEIBN(123.01).toNumber()).toBe(123010); - expect(util.gweiDecToWEIBN(101.001).toNumber()).toBe(101001); - expect(util.gweiDecToWEIBN(1234.567).toNumber()).toBe(1234567); + expect(util.gweiDecToWEIBN(1.1).toNumber()).toBe(1100000000); + expect(util.gweiDecToWEIBN(123.01).toNumber()).toBe(123010000000); + expect(util.gweiDecToWEIBN(101.001).toNumber()).toBe(101001000000); + expect(util.gweiDecToWEIBN(1234.567).toNumber()).toBe(1234567000000); }); it('should convert a number < 1 to WEI', () => { - expect(util.gweiDecToWEIBN(0.1).toNumber()).toBe(100); - expect(util.gweiDecToWEIBN(0.01).toNumber()).toBe(10); - expect(util.gweiDecToWEIBN(0.001).toNumber()).toBe(1); - expect(util.gweiDecToWEIBN(0.567).toNumber()).toBe(567); + expect(util.gweiDecToWEIBN(0.1).toNumber()).toBe(100000000); + expect(util.gweiDecToWEIBN(0.01).toNumber()).toBe(10000000); + expect(util.gweiDecToWEIBN(0.001).toNumber()).toBe(1000000); + expect(util.gweiDecToWEIBN(0.567).toNumber()).toBe(567000000); }); it('should round to whole WEI numbers', () => { - expect(util.gweiDecToWEIBN(0.1001).toNumber()).toBe(100); - expect(util.gweiDecToWEIBN(0.0109).toNumber()).toBe(11); - expect(util.gweiDecToWEIBN(0.0014).toNumber()).toBe(1); - expect(util.gweiDecToWEIBN(0.5676).toNumber()).toBe(568); + expect(util.gweiDecToWEIBN(0.1001).toNumber()).toBe(100100000); + expect(util.gweiDecToWEIBN(0.0109).toNumber()).toBe(10900000); + expect(util.gweiDecToWEIBN(0.0014).toNumber()).toBe(1400000); + expect(util.gweiDecToWEIBN(0.5676).toNumber()).toBe(567600000); }); it('should handle NaN', () => { diff --git a/src/util.ts b/src/util.ts index 7f7149ab03a..fd7fc3f84a5 100644 --- a/src/util.ts +++ b/src/util.ts @@ -62,7 +62,7 @@ export function fractionBN( return targetBN.mul(numBN).div(denomBN); } -const BN_1000 = new BN(1000, 10); +const BN_1GWEI_IN_WEI = new BN(1000000000, 10); /** * Used to convert a base-10 number from GWEI to WEI. Can handle numbers with decimal parts @@ -73,11 +73,11 @@ const BN_1000 = new BN(1000, 10); export function gweiDecToWEIBN(n: number | string) { const wholePart = Math.floor(Number(n)); const decimalPartMatch = Number(n) - .toFixed(3) + .toFixed(9) .match(/\.(\d+)/u); const decimalPart = decimalPartMatch ? decimalPartMatch[1] : '0'; - const wholePartAsWEI = new BN(wholePart, 10).mul(BN_1000); + const wholePartAsWEI = new BN(wholePart, 10).mul(BN_1GWEI_IN_WEI); const decimalPartAsWEI = new BN(decimalPart, 10); return wholePartAsWEI.add(decimalPartAsWEI); From 3df6baf364f472451beed8a540df5617ba9bf389 Mon Sep 17 00:00:00 2001 From: Brad Decker Date: Thu, 1 Jul 2021 14:33:07 -0500 Subject: [PATCH 37/46] use ethjs-unit for unit conversions (#506) --- package.json | 1 + src/dependencies.d.ts | 2 ++ src/util.test.ts | 29 +++++++++++++++++++++++++++++ src/util.ts | 25 ++++++++++++++----------- yarn.lock | 2 +- 5 files changed, 47 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index c911f11f7c4..07e8dd6c00c 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "eth-sig-util": "^3.0.0", "ethereumjs-util": "^7.0.10", "ethereumjs-wallet": "^1.0.1", + "ethjs-unit": "^0.1.6", "ethjs-util": "^0.1.6", "human-standard-collectible-abi": "^1.0.2", "human-standard-token-abi": "^2.0.0", diff --git a/src/dependencies.d.ts b/src/dependencies.d.ts index 775b4b27f17..f564e5ea410 100644 --- a/src/dependencies.d.ts +++ b/src/dependencies.d.ts @@ -18,6 +18,8 @@ declare module 'ethjs-provider-http'; declare module 'ethjs-util'; +declare module 'ethjs-unit'; + declare module 'human-standard-collectible-abi'; declare module 'human-standard-token-abi'; diff --git a/src/util.test.ts b/src/util.test.ts index b0c35e827b4..67d936df9b8 100644 --- a/src/util.test.ts +++ b/src/util.test.ts @@ -141,6 +141,35 @@ describe('util', () => { }); }); + describe('hexWei', () => { + it('should convert a whole number to WEI', () => { + const numbersInGwei = [1, 123, 101, 1234]; + numbersInGwei.forEach((gweiDec) => { + expect( + util.weiHexToGweiDec(util.gweiDecToWEIBN(gweiDec).toString(16)), + ).toBe(gweiDec.toString()); + }); + }); + + it('should convert a number with a decimal part to WEI', () => { + const numbersInGwei = [1.1, 123.01, 101.001, 1234.567]; + numbersInGwei.forEach((gweiDec) => { + expect( + util.weiHexToGweiDec(util.gweiDecToWEIBN(gweiDec).toString(16)), + ).toBe(gweiDec.toString()); + }); + }); + + it('should convert a number < 1 to WEI', () => { + const numbersInGwei = [0.1, 0.01, 0.001, 0.567]; + numbersInGwei.forEach((gweiDec) => { + expect( + util.weiHexToGweiDec(util.gweiDecToWEIBN(gweiDec).toString(16)), + ).toBe(gweiDec.toString()); + }); + }); + }); + describe('safelyExecute', () => { it('should swallow errors', async () => { expect( diff --git a/src/util.ts b/src/util.ts index fd7fc3f84a5..0a05137b52f 100644 --- a/src/util.ts +++ b/src/util.ts @@ -7,6 +7,7 @@ import { toChecksumAddress, } from 'ethereumjs-util'; import { stripHexPrefix } from 'ethjs-util'; +import { fromWei, toWei } from 'ethjs-unit'; import { ethErrors } from 'eth-rpc-errors'; import ensNamehash from 'eth-ens-namehash'; import { TYPED_MESSAGE_SCHEMA, typedSignatureHash } from 'eth-sig-util'; @@ -62,8 +63,6 @@ export function fractionBN( return targetBN.mul(numBN).div(denomBN); } -const BN_1GWEI_IN_WEI = new BN(1000000000, 10); - /** * Used to convert a base-10 number from GWEI to WEI. Can handle numbers with decimal parts * @@ -71,16 +70,20 @@ const BN_1GWEI_IN_WEI = new BN(1000000000, 10); * @returns - The number in WEI, as a BN */ export function gweiDecToWEIBN(n: number | string) { - const wholePart = Math.floor(Number(n)); - const decimalPartMatch = Number(n) - .toFixed(9) - .match(/\.(\d+)/u); - const decimalPart = decimalPartMatch ? decimalPartMatch[1] : '0'; - - const wholePartAsWEI = new BN(wholePart, 10).mul(BN_1GWEI_IN_WEI); - const decimalPartAsWEI = new BN(decimalPart, 10); + if (Number.isNaN(n)) { + return new BN(0); + } + return toWei(n.toString(), 'gwei'); +} - return wholePartAsWEI.add(decimalPartAsWEI); +/** + * Used to convert values from wei hex format to dec gwei format + * @param hex - value in hex wei + * @returns - value in dec gwei as string + */ +export function weiHexToGweiDec(hex: string) { + const hexWei = new BN(hex, 16); + return fromWei(hexWei, 'gwei').toString(10); } /** diff --git a/yarn.lock b/yarn.lock index 38368776c62..2c47fe7d330 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3036,7 +3036,7 @@ ethjs-schema@0.2.1: resolved "https://registry.yarnpkg.com/ethjs-schema/-/ethjs-schema-0.2.1.tgz#47e138920421453617069034684642e26bb310f4" integrity sha512-DXd8lwNrhT9sjsh/Vd2Z+4pfyGxhc0POVnLBUfwk5udtdoBzADyq+sK39dcb48+ZU+2VgtwHxtGWnLnCfmfW5g== -ethjs-unit@0.1.6: +ethjs-unit@0.1.6, ethjs-unit@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/ethjs-unit/-/ethjs-unit-0.1.6.tgz#c665921e476e87bce2a9d588a6fe0405b2c41699" integrity sha1-xmWSHkduh7ziqdWIpv4EBbLEFpk= From 117d89825bfabb521056404cf910e59f48324e84 Mon Sep 17 00:00:00 2001 From: Brad Decker Date: Thu, 1 Jul 2021 14:54:46 -0500 Subject: [PATCH 38/46] Add metaswaps API and normalize all gas fee units to dec gwei (#507) --- src/gas/GasFeeController.test.ts | 55 ++++++++++++++--- src/gas/GasFeeController.ts | 103 +++++++++++++++++++++++-------- src/gas/gas-util.test.ts | 28 +++++++++ src/gas/gas-util.ts | 36 +++++++++-- 4 files changed, 181 insertions(+), 41 deletions(-) create mode 100644 src/gas/gas-util.test.ts diff --git a/src/gas/GasFeeController.test.ts b/src/gas/GasFeeController.test.ts index c88997a4cba..9946f49d7ee 100644 --- a/src/gas/GasFeeController.test.ts +++ b/src/gas/GasFeeController.test.ts @@ -5,7 +5,9 @@ import { GasFeeController, GetGasFeeState, GasFeeStateChange, + LegacyGasPriceEstimate, } from './GasFeeController'; +import { EXTERNAL_GAS_PRICES_API_URL } from './gas-util'; const GAS_FEE_API = 'https://mock-gas-server.herokuapp.com/'; @@ -28,6 +30,8 @@ function getRestrictedMessenger() { describe('GasFeeController', () => { let gasFeeController: GasFeeController; + let getIsMainnet: jest.Mock; + let getIsEIP1559Compatible: jest.Mock>; beforeAll(() => { nock.disableNetConnect(); @@ -38,8 +42,12 @@ describe('GasFeeController', () => { }); beforeEach(() => { + getIsMainnet = jest.fn().mockImplementation(() => false); + getIsEIP1559Compatible = jest + .fn() + .mockImplementation(() => Promise.resolve(true)); nock(GAS_FEE_API) - .get('/') + .get(/.+/u) .reply(200, { low: { minWaitTimeEstimate: 60000, @@ -60,14 +68,25 @@ describe('GasFeeController', () => { suggestedMaxFeePerGas: '50', }, estimatedBaseFee: '28', - }); + }) + .persist(); + + nock(EXTERNAL_GAS_PRICES_API_URL) + .get(/.+/u) + .reply(200, { + SafeGasPrice: '22', + ProposeGasPrice: '25', + FastGasPrice: '30', + }) + .persist(); gasFeeController = new GasFeeController({ interval: 10000, messenger: getRestrictedMessenger(), getProvider: () => stub(), onNetworkStateChange: () => stub(), - getCurrentNetworkEIP1559Compatibility: () => Promise.resolve(true), // change this for networkController.state.properties.isEIP1559Compatible ??? + getIsMainnet, + getCurrentNetworkEIP1559Compatibility: getIsEIP1559Compatible, // change this for networkController.state.properties.isEIP1559Compatible ??? }); }); @@ -94,12 +113,28 @@ describe('GasFeeController', () => { ); }); - it('should _fetchGasFeeEstimateData', async () => { - expect(gasFeeController.state.gasFeeEstimates).toStrictEqual({}); - const estimates = await gasFeeController._fetchGasFeeEstimateData(); - expect(estimates).toHaveProperty('gasFeeEstimates'); - expect(gasFeeController.state.gasFeeEstimates).toHaveProperty( - 'estimatedBaseFee', - ); + describe('when on mainnet before london', () => { + it('should _fetchGasFeeEstimateData', async () => { + getIsMainnet.mockImplementation(() => true); + getIsEIP1559Compatible.mockImplementation(() => Promise.resolve(false)); + expect(gasFeeController.state.gasFeeEstimates).toStrictEqual({}); + const estimates = await gasFeeController._fetchGasFeeEstimateData(); + expect(estimates).toHaveProperty('gasFeeEstimates'); + expect( + (gasFeeController.state.gasFeeEstimates as LegacyGasPriceEstimate).high, + ).toBe('30'); + }); + }); + + describe('when on any network supporting EIP-1559', () => { + it('should _fetchGasFeeEstimateData', async () => { + getIsMainnet.mockImplementation(() => true); + expect(gasFeeController.state.gasFeeEstimates).toStrictEqual({}); + const estimates = await gasFeeController._fetchGasFeeEstimateData(); + expect(estimates).toHaveProperty('gasFeeEstimates'); + expect(gasFeeController.state.gasFeeEstimates).toHaveProperty( + 'estimatedBaseFee', + ); + }); }); }); diff --git a/src/gas/GasFeeController.ts b/src/gas/GasFeeController.ts index 4c4139bfcc8..47384441273 100644 --- a/src/gas/GasFeeController.ts +++ b/src/gas/GasFeeController.ts @@ -11,29 +11,64 @@ import type { } from '../network/NetworkController'; import { fetchGasEstimates as defaultFetchGasEstimates, - fetchLegacyGasPriceEstimate as defaultFetchLegacyGasPriceEstimate, + fetchEthGasPriceEstimate as defaultFetchEthGasPriceEstimate, + fetchLegacyGasPriceEstimates as defaultFetchLegacyGasPriceEstimates, calculateTimeEstimate, } from './gas-util'; export type unknownString = 'unknown'; +/** + * Indicates which type of gasEstimate the controller is currently returning. + * This is useful as a way of asserting that the shape of gasEstimates matches + * expectations. NONE is a special case indicating that no previous gasEstimate + * has been fetched. + */ +export const GAS_ESTIMATE_TYPES = { + FEE_MARKET: 'fee-market' as const, + LEGACY: 'legacy' as const, + ETH_GASPRICE: 'eth_gasPrice' as const, + NONE: 'none' as const, +}; + +export type GasEstimateType = typeof GAS_ESTIMATE_TYPES[keyof typeof GAS_ESTIMATE_TYPES]; + export interface EstimatedGasFeeTimeBounds { lowerTimeBound: number | null; upperTimeBound: number | unknownString; } /** - * @type LegacyGasPriceEstimate + * @type EthGasPriceEstimate * * A single gas price estimate for networks and accounts that don't support EIP-1559 + * This estimate comes from eth_gasPrice but is converted to dec gwei to match other + * return values * - * @property gasPrice - A GWEI hex number, the result of a call to eth_gasPrice + * @property gasPrice - A GWEI dec string */ -export interface LegacyGasPriceEstimate { +export interface EthGasPriceEstimate { gasPrice: string; } +/** + * @type LegacyGasPriceEstimate + * + * A set of gas price estimates for networks and accounts that don't support EIP-1559 + * These estimates include low, medium and high all as strings representing gwei in + * decimal format. + * + * @property high - gasPrice, in decimal gwei string format, suggested for fast inclusion + * @property medium - gasPrice, in decimal gwei string format, suggested for avg inclusion + * @property low - gasPrice, in decimal gwei string format, suggested for slow inclusion + */ +export interface LegacyGasPriceEstimate { + high: string; + medium: string; + low: string; +} + /** * @type Eip1559GasFee * @@ -48,8 +83,8 @@ export interface LegacyGasPriceEstimate { export interface Eip1559GasFee { minWaitTimeEstimate: number; // a time duration in milliseconds maxWaitTimeEstimate: number; // a time duration in milliseconds - suggestedMaxPriorityFeePerGas: string; // a GWEI hex number - suggestedMaxFeePerGas: string; // a GWEI hex number + suggestedMaxPriorityFeePerGas: string; // a GWEI decimal number + suggestedMaxFeePerGas: string; // a GWEI decimal number } function isEIP1559GasFee(object: any): object is Eip1559GasFee { @@ -70,7 +105,7 @@ function isEIP1559GasFee(object: any): object is Eip1559GasFee { * @property low - A GasFee for a minimum necessary combination of tip and maxFee * @property medium - A GasFee for a recommended combination of tip and maxFee * @property high - A GasFee for a high combination of tip and maxFee - * @property estimatedNextBlockBaseFee - An estimate of what the base fee will be for the pending/next block. A GWEI hex number + * @property estimatedBaseFee - An estimate of what the base fee will be for the pending/next block. A GWEI dec number */ export interface GasFeeEstimates { @@ -95,6 +130,7 @@ function isEIP1559Estimate(object: any): object is GasFeeEstimates { const metadata = { gasFeeEstimates: { persist: true, anonymous: false }, estimatedGasFeeTimeBounds: { persist: true, anonymous: false }, + gasEstimateType: { persist: true, anonymous: false }, }; /** @@ -108,9 +144,11 @@ const metadata = { export type GasFeeState = { gasFeeEstimates: | GasFeeEstimates + | EthGasPriceEstimate | LegacyGasPriceEstimate | Record; estimatedGasFeeTimeBounds: EstimatedGasFeeTimeBounds | Record; + gasEstimateType: GasEstimateType; }; const name = 'GasFeeController'; @@ -128,6 +166,7 @@ export type GetGasFeeState = { const defaultState = { gasFeeEstimates: {}, estimatedGasFeeTimeBounds: {}, + gasEstimateType: GAS_ESTIMATE_TYPES.NONE, }; /** @@ -142,12 +181,16 @@ export class GasFeeController extends BaseController { private fetchGasEstimates; - private fetchLegacyGasPriceEstimate; + private fetchEthGasPriceEstimate; + + private fetchLegacyGasPriceEstimates; private getCurrentNetworkEIP1559Compatibility; private getCurrentAccountEIP1559Compatibility; + private getIsMainnet; + private ethQuery: any; /** @@ -159,9 +202,11 @@ export class GasFeeController extends BaseController { messenger, state, fetchGasEstimates = defaultFetchGasEstimates, - fetchLegacyGasPriceEstimate = defaultFetchLegacyGasPriceEstimate, + fetchEthGasPriceEstimate = defaultFetchEthGasPriceEstimate, + fetchLegacyGasPriceEstimates = defaultFetchLegacyGasPriceEstimates, getCurrentNetworkEIP1559Compatibility, getCurrentAccountEIP1559Compatibility, + getIsMainnet, getProvider, onNetworkStateChange, }: { @@ -175,9 +220,11 @@ export class GasFeeController extends BaseController { >; state?: Partial; fetchGasEstimates?: typeof defaultFetchGasEstimates; - fetchLegacyGasPriceEstimate?: typeof defaultFetchLegacyGasPriceEstimate; + fetchEthGasPriceEstimate?: typeof defaultFetchEthGasPriceEstimate; + fetchLegacyGasPriceEstimates?: typeof defaultFetchLegacyGasPriceEstimates; getCurrentNetworkEIP1559Compatibility: () => Promise; getCurrentAccountEIP1559Compatibility?: () => boolean; + getIsMainnet: () => boolean; getProvider: () => NetworkController['provider']; onNetworkStateChange: (listener: (state: NetworkState) => void) => void; }) { @@ -189,10 +236,12 @@ export class GasFeeController extends BaseController { }); this.intervalDelay = interval; this.fetchGasEstimates = fetchGasEstimates; - this.fetchLegacyGasPriceEstimate = fetchLegacyGasPriceEstimate; + this.fetchEthGasPriceEstimate = fetchEthGasPriceEstimate; + this.fetchLegacyGasPriceEstimates = fetchLegacyGasPriceEstimates; this.pollTokens = new Set(); this.getCurrentNetworkEIP1559Compatibility = getCurrentNetworkEIP1559Compatibility; this.getCurrentAccountEIP1559Compatibility = getCurrentAccountEIP1559Compatibility; + this.getIsMainnet = getIsMainnet; const provider = getProvider(); this.ethQuery = new EthQuery(provider); @@ -226,9 +275,11 @@ export class GasFeeController extends BaseController { * @returns GasFeeEstimates */ async _fetchGasFeeEstimateData(): Promise { - let estimates; + let estimates: GasFeeState['gasFeeEstimates']; let estimatedGasFeeTimeBounds = {}; let isEIP1559Compatible; + let gasEstimateType: GasEstimateType = GAS_ESTIMATE_TYPES.NONE; + const isMainnet = this.getIsMainnet(); try { isEIP1559Compatible = await this.getEIP1559Compatibility(); } catch (e) { @@ -236,8 +287,8 @@ export class GasFeeController extends BaseController { isEIP1559Compatible = false; } - if (isEIP1559Compatible) { - try { + try { + if (isEIP1559Compatible) { estimates = await this.fetchGasEstimates(); const { suggestedMaxPriorityFeePerGas, @@ -247,21 +298,20 @@ export class GasFeeController extends BaseController { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas, ); - } catch (error) { - try { - estimates = await this.fetchLegacyGasPriceEstimate(this.ethQuery); - } catch (error2) { - throw new Error( - `Gas fee/price estimation failed. Message: ${error2.message}`, - ); - } + gasEstimateType = GAS_ESTIMATE_TYPES.FEE_MARKET; + } else if (isMainnet) { + estimates = await this.fetchLegacyGasPriceEstimates(); + gasEstimateType = GAS_ESTIMATE_TYPES.LEGACY; + } else { + throw new Error('Main gas fee/price estimation failed. Use fallback'); } - } else { + } catch { try { - estimates = await this.fetchLegacyGasPriceEstimate(this.ethQuery); - } catch (error2) { + estimates = await this.fetchEthGasPriceEstimate(this.ethQuery); + gasEstimateType = GAS_ESTIMATE_TYPES.ETH_GASPRICE; + } catch (error) { throw new Error( - `Gas fee/price estimation failed. Message: ${error2.message}`, + `Gas fee/price estimation failed. Message: ${error.message}`, ); } } @@ -269,6 +319,7 @@ export class GasFeeController extends BaseController { const newState: GasFeeState = { gasFeeEstimates: estimates, estimatedGasFeeTimeBounds, + gasEstimateType, }; this.update(() => { diff --git a/src/gas/gas-util.test.ts b/src/gas/gas-util.test.ts new file mode 100644 index 00000000000..15323f1f20d --- /dev/null +++ b/src/gas/gas-util.test.ts @@ -0,0 +1,28 @@ +import nock from 'nock'; +import { + EXTERNAL_GAS_PRICES_API_URL, + fetchLegacyGasPriceEstimates, +} from './gas-util'; + +describe('gas utils', () => { + describe('fetchLegacyGasPriceEstimates', () => { + it('should fetch external gasPrices and return high/medium/low', async () => { + const scope = nock(EXTERNAL_GAS_PRICES_API_URL) + .get(/.+/u) + .reply(200, { + SafeGasPrice: '22', + ProposeGasPrice: '25', + FastGasPrice: '30', + }) + .persist(); + const result = await fetchLegacyGasPriceEstimates(); + expect(result).toMatchObject({ + high: '30', + medium: '25', + low: '22', + }); + scope.done(); + nock.cleanAll(); + }); + }); +}); diff --git a/src/gas/gas-util.ts b/src/gas/gas-util.ts index 0f98eeed08e..0b55adbacbb 100644 --- a/src/gas/gas-util.ts +++ b/src/gas/gas-util.ts @@ -1,24 +1,50 @@ import { BN } from 'ethereumjs-util'; -import { query, handleFetch, gweiDecToWEIBN } from '../util'; +import { query, handleFetch, gweiDecToWEIBN, weiHexToGweiDec } from '../util'; import { GasFeeEstimates, - LegacyGasPriceEstimate, + EthGasPriceEstimate, EstimatedGasFeeTimeBounds, unknownString, } from './GasFeeController'; const GAS_FEE_API = 'https://mock-gas-server.herokuapp.com/'; +export const EXTERNAL_GAS_PRICES_API_URL = `https://api.metaswap.codefi.network/gasPrices`; export async function fetchGasEstimates(): Promise { return await handleFetch(GAS_FEE_API); } -export async function fetchLegacyGasPriceEstimate( +/** + * Hit the legacy MetaSwaps gasPrices estimate api and return the low, medium + * high values from that API. + */ +export async function fetchLegacyGasPriceEstimates(): Promise<{ + low: string; + medium: string; + high: string; +}> { + const result = await handleFetch(EXTERNAL_GAS_PRICES_API_URL, { + referrer: EXTERNAL_GAS_PRICES_API_URL, + referrerPolicy: 'no-referrer-when-downgrade', + method: 'GET', + mode: 'cors', + headers: { + 'Content-Type': 'application/json', + }, + }); + return { + low: result.SafeGasPrice, + medium: result.ProposeGasPrice, + high: result.FastGasPrice, + }; +} + +export async function fetchEthGasPriceEstimate( ethQuery: any, -): Promise { +): Promise { const gasPrice = await query(ethQuery, 'gasPrice'); return { - gasPrice, + gasPrice: weiHexToGweiDec(gasPrice).toString(), }; } From b4eebacae1c88349965e620e62b167b3be57bd10 Mon Sep 17 00:00:00 2001 From: Brad Decker Date: Fri, 2 Jul 2021 10:28:27 -0500 Subject: [PATCH 39/46] update types to be more identifiable (#508) --- src/gas/GasFeeController.ts | 137 +++++++++++++++++++++--------------- 1 file changed, 82 insertions(+), 55 deletions(-) diff --git a/src/gas/GasFeeController.ts b/src/gas/GasFeeController.ts index 47384441273..f6d226ab7b5 100644 --- a/src/gas/GasFeeController.ts +++ b/src/gas/GasFeeController.ts @@ -18,6 +18,22 @@ import { export type unknownString = 'unknown'; +// Fee Market describes the way gas is set after the london hardfork, and was +// defined by EIP-1559. +export type FeeMarketEstimateType = 'fee-market'; +// Legacy describes gasPrice estimates from before london hardfork, when the +// user is connected to mainnet and are presented with fast/average/slow +// estimate levels to choose from. +export type LegacyEstimateType = 'legacy'; +// EthGasPrice describes a gasPrice estimate received from eth_gasPrice. Post +// london this value should only be used for legacy type transactions when on +// networks that support EIP-1559. This type of estimate is the most accurate +// to display on custom networks that don't support EIP-1559. +export type EthGasPriceEstimateType = 'eth_gasPrice'; +// NoEstimate describes the state of the controller before receiving its first +// estimate. +export type NoEstimateType = 'none'; + /** * Indicates which type of gasEstimate the controller is currently returning. * This is useful as a way of asserting that the shape of gasEstimates matches @@ -25,13 +41,17 @@ export type unknownString = 'unknown'; * has been fetched. */ export const GAS_ESTIMATE_TYPES = { - FEE_MARKET: 'fee-market' as const, - LEGACY: 'legacy' as const, - ETH_GASPRICE: 'eth_gasPrice' as const, - NONE: 'none' as const, + FEE_MARKET: 'fee-market' as FeeMarketEstimateType, + LEGACY: 'legacy' as LegacyEstimateType, + ETH_GASPRICE: 'eth_gasPrice' as EthGasPriceEstimateType, + NONE: 'none' as NoEstimateType, }; -export type GasEstimateType = typeof GAS_ESTIMATE_TYPES[keyof typeof GAS_ESTIMATE_TYPES]; +export type GasEstimateType = + | FeeMarketEstimateType + | EthGasPriceEstimateType + | LegacyEstimateType + | NoEstimateType; export interface EstimatedGasFeeTimeBounds { lowerTimeBound: number | null; @@ -87,16 +107,6 @@ export interface Eip1559GasFee { suggestedMaxFeePerGas: string; // a GWEI decimal number } -function isEIP1559GasFee(object: any): object is Eip1559GasFee { - return ( - 'minWaitTimeEstimate' in object && - 'maxWaitTimeEstimate' in object && - 'suggestedMaxPriorityFeePerGas' in object && - 'suggestedMaxFeePerGas' in object && - Object.keys(object).length === 4 - ); -} - /** * @type GasFeeEstimates * @@ -115,24 +125,36 @@ export interface GasFeeEstimates { estimatedBaseFee: string; } -function isEIP1559Estimate(object: any): object is GasFeeEstimates { - return ( - 'low' in object && - isEIP1559GasFee(object.low) && - 'medium' in object && - isEIP1559GasFee(object.medium) && - 'high' in object && - isEIP1559GasFee(object.high) && - 'estimatedBaseFee' in object - ); -} - const metadata = { gasFeeEstimates: { persist: true, anonymous: false }, estimatedGasFeeTimeBounds: { persist: true, anonymous: false }, gasEstimateType: { persist: true, anonymous: false }, }; +export type GasFeeStateEthGasPrice = { + gasFeeEstimates: EthGasPriceEstimate; + estimatedGasFeeTimeBounds: Record; + gasEstimateType: EthGasPriceEstimateType; +}; + +export type GasFeeStateFeeMarket = { + gasFeeEstimates: GasFeeEstimates; + estimatedGasFeeTimeBounds: EstimatedGasFeeTimeBounds | Record; + gasEstimateType: FeeMarketEstimateType; +}; + +export type GasFeeStateLegacy = { + gasFeeEstimates: LegacyGasPriceEstimate; + estimatedGasFeeTimeBounds: Record; + gasEstimateType: LegacyEstimateType; +}; + +export type GasFeeStateNoEstimates = { + gasFeeEstimates: Record; + estimatedGasFeeTimeBounds: Record; + gasEstimateType: NoEstimateType; +}; + /** * @type GasFeeState * @@ -141,15 +163,11 @@ const metadata = { * @property gasFeeEstimates - Gas fee estimate data based on new EIP-1559 properties * @property estimatedGasFeeTimeBounds - Estimates representing the minimum and maximum */ -export type GasFeeState = { - gasFeeEstimates: - | GasFeeEstimates - | EthGasPriceEstimate - | LegacyGasPriceEstimate - | Record; - estimatedGasFeeTimeBounds: EstimatedGasFeeTimeBounds | Record; - gasEstimateType: GasEstimateType; -}; +export type GasFeeState = + | GasFeeStateEthGasPrice + | GasFeeStateFeeMarket + | GasFeeStateLegacy + | GasFeeStateNoEstimates; const name = 'GasFeeController'; @@ -163,7 +181,7 @@ export type GetGasFeeState = { handler: () => GasFeeState; }; -const defaultState = { +const defaultState: GasFeeState = { gasFeeEstimates: {}, estimatedGasFeeTimeBounds: {}, gasEstimateType: GAS_ESTIMATE_TYPES.NONE, @@ -218,7 +236,7 @@ export class GasFeeController extends BaseController { never, never >; - state?: Partial; + state?: GasFeeState; fetchGasEstimates?: typeof defaultFetchGasEstimates; fetchEthGasPriceEstimate?: typeof defaultFetchEthGasPriceEstimate; fetchLegacyGasPriceEstimates?: typeof defaultFetchLegacyGasPriceEstimates; @@ -275,10 +293,7 @@ export class GasFeeController extends BaseController { * @returns GasFeeEstimates */ async _fetchGasFeeEstimateData(): Promise { - let estimates: GasFeeState['gasFeeEstimates']; - let estimatedGasFeeTimeBounds = {}; let isEIP1559Compatible; - let gasEstimateType: GasEstimateType = GAS_ESTIMATE_TYPES.NONE; const isMainnet = this.getIsMainnet(); try { isEIP1559Compatible = await this.getEIP1559Compatibility(); @@ -287,28 +302,46 @@ export class GasFeeController extends BaseController { isEIP1559Compatible = false; } + let newState: GasFeeState = { + gasFeeEstimates: {}, + estimatedGasFeeTimeBounds: {}, + gasEstimateType: GAS_ESTIMATE_TYPES.NONE, + }; + try { if (isEIP1559Compatible) { - estimates = await this.fetchGasEstimates(); + const estimates = await this.fetchGasEstimates(); const { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas, } = estimates.medium; - estimatedGasFeeTimeBounds = this.getTimeEstimate( + const estimatedGasFeeTimeBounds = this.getTimeEstimate( suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas, ); - gasEstimateType = GAS_ESTIMATE_TYPES.FEE_MARKET; + newState = { + gasFeeEstimates: estimates, + estimatedGasFeeTimeBounds, + gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, + }; } else if (isMainnet) { - estimates = await this.fetchLegacyGasPriceEstimates(); - gasEstimateType = GAS_ESTIMATE_TYPES.LEGACY; + const estimates = await this.fetchLegacyGasPriceEstimates(); + newState = { + gasFeeEstimates: estimates, + estimatedGasFeeTimeBounds: {}, + gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY, + }; } else { throw new Error('Main gas fee/price estimation failed. Use fallback'); } } catch { try { - estimates = await this.fetchEthGasPriceEstimate(this.ethQuery); - gasEstimateType = GAS_ESTIMATE_TYPES.ETH_GASPRICE; + const estimates = await this.fetchEthGasPriceEstimate(this.ethQuery); + newState = { + gasFeeEstimates: estimates, + estimatedGasFeeTimeBounds: {}, + gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE, + }; } catch (error) { throw new Error( `Gas fee/price estimation failed. Message: ${error.message}`, @@ -316,12 +349,6 @@ export class GasFeeController extends BaseController { } } - const newState: GasFeeState = { - gasFeeEstimates: estimates, - estimatedGasFeeTimeBounds, - gasEstimateType, - }; - this.update(() => { return newState; }); @@ -396,7 +423,7 @@ export class GasFeeController extends BaseController { ): EstimatedGasFeeTimeBounds | Record { if ( !this.state.gasFeeEstimates || - !isEIP1559Estimate(this.state.gasFeeEstimates) + this.state.gasEstimateType !== GAS_ESTIMATE_TYPES.FEE_MARKET ) { return {}; } From 6fbf02421fc467ea26110893f3c4c2ea42efe646 Mon Sep 17 00:00:00 2001 From: Brad Decker Date: Fri, 2 Jul 2021 12:05:43 -0500 Subject: [PATCH 40/46] use LegacyGasFeeEstimate type --- src/gas/gas-util.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/gas/gas-util.ts b/src/gas/gas-util.ts index 0b55adbacbb..6667e3a735f 100644 --- a/src/gas/gas-util.ts +++ b/src/gas/gas-util.ts @@ -5,6 +5,7 @@ import { EthGasPriceEstimate, EstimatedGasFeeTimeBounds, unknownString, + LegacyGasPriceEstimate, } from './GasFeeController'; const GAS_FEE_API = 'https://mock-gas-server.herokuapp.com/'; @@ -18,11 +19,7 @@ export async function fetchGasEstimates(): Promise { * Hit the legacy MetaSwaps gasPrices estimate api and return the low, medium * high values from that API. */ -export async function fetchLegacyGasPriceEstimates(): Promise<{ - low: string; - medium: string; - high: string; -}> { +export async function fetchLegacyGasPriceEstimates(): Promise { const result = await handleFetch(EXTERNAL_GAS_PRICES_API_URL, { referrer: EXTERNAL_GAS_PRICES_API_URL, referrerPolicy: 'no-referrer-when-downgrade', From 5d2908e04b020e9d00c1e8efffd08061dbb6c98b Mon Sep 17 00:00:00 2001 From: Brad Decker Date: Mon, 5 Jul 2021 16:53:27 -0500 Subject: [PATCH 41/46] handle hex prefixed --- src/util.test.ts | 6 +++++- src/util.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/util.test.ts b/src/util.test.ts index 67d936df9b8..3e18554b6b3 100644 --- a/src/util.test.ts +++ b/src/util.test.ts @@ -141,7 +141,7 @@ describe('util', () => { }); }); - describe('hexWei', () => { + describe('weiHexToGweiDec', () => { it('should convert a whole number to WEI', () => { const numbersInGwei = [1, 123, 101, 1234]; numbersInGwei.forEach((gweiDec) => { @@ -168,6 +168,10 @@ describe('util', () => { ).toBe(gweiDec.toString()); }); }); + + it('should work with 0x prefixed values', () => { + expect(util.weiHexToGweiDec('0x5f48b0f7')).toBe('1.598599415'); + }); }); describe('safelyExecute', () => { diff --git a/src/util.ts b/src/util.ts index 0a05137b52f..1379ee5765d 100644 --- a/src/util.ts +++ b/src/util.ts @@ -82,7 +82,7 @@ export function gweiDecToWEIBN(n: number | string) { * @returns - value in dec gwei as string */ export function weiHexToGweiDec(hex: string) { - const hexWei = new BN(hex, 16); + const hexWei = new BN(stripHexPrefix(hex), 16); return fromWei(hexWei, 'gwei').toString(10); } From 283fc881fbe8ee2515c29166708a1f5eaea07811 Mon Sep 17 00:00:00 2001 From: Brad Decker Date: Mon, 5 Jul 2021 16:55:18 -0500 Subject: [PATCH 42/46] baseFee -> baseFeePerGas --- src/network/NetworkController.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/network/NetworkController.ts b/src/network/NetworkController.ts index 09a230ce5f2..9b4605b8f02 100644 --- a/src/network/NetworkController.ts +++ b/src/network/NetworkController.ts @@ -48,7 +48,7 @@ export interface ProviderConfig { } export interface Block { - baseFee?: string; + baseFeePerGas?: string; } export interface NetworkProperties { @@ -314,7 +314,8 @@ export class NetworkController extends BaseController< if (error) { reject(error); } else { - const isEIP1559Compatible = typeof block.baseFee !== undefined; + const isEIP1559Compatible = + typeof block.baseFeePerGas !== 'undefined'; this.update({ properties: { isEIP1559Compatible, From cf82e705f090e247ffc454bfd424effb456a844f Mon Sep 17 00:00:00 2001 From: Brad Decker Date: Tue, 6 Jul 2021 15:25:15 -0500 Subject: [PATCH 43/46] make more configurable (#513) --- src/gas/GasFeeController.test.ts | 65 +++++++++++++++++++++++++++----- src/gas/GasFeeController.ts | 44 +++++++++++++++++---- src/gas/gas-util.test.ts | 11 +++--- src/gas/gas-util.ts | 15 ++++---- 4 files changed, 103 insertions(+), 32 deletions(-) diff --git a/src/gas/GasFeeController.test.ts b/src/gas/GasFeeController.test.ts index 9946f49d7ee..51f13d26fe1 100644 --- a/src/gas/GasFeeController.test.ts +++ b/src/gas/GasFeeController.test.ts @@ -7,9 +7,9 @@ import { GasFeeStateChange, LegacyGasPriceEstimate, } from './GasFeeController'; -import { EXTERNAL_GAS_PRICES_API_URL } from './gas-util'; -const GAS_FEE_API = 'https://mock-gas-server.herokuapp.com/'; +const TEST_GAS_FEE_API = 'https://mock-gas-server.herokuapp.com/'; +const TEST_LEGACY_FEE_API = 'https://test/'; const name = 'GasFeeController'; @@ -30,8 +30,9 @@ function getRestrictedMessenger() { describe('GasFeeController', () => { let gasFeeController: GasFeeController; - let getIsMainnet: jest.Mock; + let getCurrentNetworkLegacyGasAPICompatibility: jest.Mock; let getIsEIP1559Compatible: jest.Mock>; + let getChainId: jest.Mock<`0x${string}` | `${number}` | number>; beforeAll(() => { nock.disableNetConnect(); @@ -42,11 +43,14 @@ describe('GasFeeController', () => { }); beforeEach(() => { - getIsMainnet = jest.fn().mockImplementation(() => false); + getChainId = jest.fn().mockImplementation(() => '0x1'); + getCurrentNetworkLegacyGasAPICompatibility = jest + .fn() + .mockImplementation(() => false); getIsEIP1559Compatible = jest .fn() .mockImplementation(() => Promise.resolve(true)); - nock(GAS_FEE_API) + nock(TEST_GAS_FEE_API.replace('', '1')) .get(/.+/u) .reply(200, { low: { @@ -71,7 +75,7 @@ describe('GasFeeController', () => { }) .persist(); - nock(EXTERNAL_GAS_PRICES_API_URL) + nock(TEST_LEGACY_FEE_API.replace('', '0x1')) .get(/.+/u) .reply(200, { SafeGasPrice: '22', @@ -84,8 +88,11 @@ describe('GasFeeController', () => { interval: 10000, messenger: getRestrictedMessenger(), getProvider: () => stub(), + getChainId, + legacyAPIEndpoint: TEST_LEGACY_FEE_API, + EIP1559APIEndpoint: TEST_GAS_FEE_API, onNetworkStateChange: () => stub(), - getIsMainnet, + getCurrentNetworkLegacyGasAPICompatibility, getCurrentNetworkEIP1559Compatibility: getIsEIP1559Compatible, // change this for networkController.state.properties.isEIP1559Compatible ??? }); }); @@ -113,9 +120,47 @@ describe('GasFeeController', () => { ); }); - describe('when on mainnet before london', () => { + describe('when on any network supporting legacy gas estimation api', () => { it('should _fetchGasFeeEstimateData', async () => { - getIsMainnet.mockImplementation(() => true); + getCurrentNetworkLegacyGasAPICompatibility.mockImplementation(() => true); + getIsEIP1559Compatible.mockImplementation(() => Promise.resolve(false)); + expect(gasFeeController.state.gasFeeEstimates).toStrictEqual({}); + const estimates = await gasFeeController._fetchGasFeeEstimateData(); + expect(estimates).toHaveProperty('gasFeeEstimates'); + expect( + (gasFeeController.state.gasFeeEstimates as LegacyGasPriceEstimate).high, + ).toBe('30'); + }); + }); + + describe('getChainId', () => { + it('should work with a number input', async () => { + getChainId.mockImplementation(() => 1); + getCurrentNetworkLegacyGasAPICompatibility.mockImplementation(() => true); + getIsEIP1559Compatible.mockImplementation(() => Promise.resolve(false)); + expect(gasFeeController.state.gasFeeEstimates).toStrictEqual({}); + const estimates = await gasFeeController._fetchGasFeeEstimateData(); + expect(estimates).toHaveProperty('gasFeeEstimates'); + expect( + (gasFeeController.state.gasFeeEstimates as LegacyGasPriceEstimate).high, + ).toBe('30'); + }); + + it('should work with a hexstring input', async () => { + getChainId.mockImplementation(() => '0x1'); + getCurrentNetworkLegacyGasAPICompatibility.mockImplementation(() => true); + getIsEIP1559Compatible.mockImplementation(() => Promise.resolve(false)); + expect(gasFeeController.state.gasFeeEstimates).toStrictEqual({}); + const estimates = await gasFeeController._fetchGasFeeEstimateData(); + expect(estimates).toHaveProperty('gasFeeEstimates'); + expect( + (gasFeeController.state.gasFeeEstimates as LegacyGasPriceEstimate).high, + ).toBe('30'); + }); + + it('should work with a numeric string input', async () => { + getChainId.mockImplementation(() => '1'); + getCurrentNetworkLegacyGasAPICompatibility.mockImplementation(() => true); getIsEIP1559Compatible.mockImplementation(() => Promise.resolve(false)); expect(gasFeeController.state.gasFeeEstimates).toStrictEqual({}); const estimates = await gasFeeController._fetchGasFeeEstimateData(); @@ -128,7 +173,7 @@ describe('GasFeeController', () => { describe('when on any network supporting EIP-1559', () => { it('should _fetchGasFeeEstimateData', async () => { - getIsMainnet.mockImplementation(() => true); + getCurrentNetworkLegacyGasAPICompatibility.mockImplementation(() => true); expect(gasFeeController.state.gasFeeEstimates).toStrictEqual({}); const estimates = await gasFeeController._fetchGasFeeEstimateData(); expect(estimates).toHaveProperty('gasFeeEstimates'); diff --git a/src/gas/GasFeeController.ts b/src/gas/GasFeeController.ts index f6d226ab7b5..f1e6d1e4033 100644 --- a/src/gas/GasFeeController.ts +++ b/src/gas/GasFeeController.ts @@ -2,6 +2,7 @@ import type { Patch } from 'immer'; import EthQuery from 'eth-query'; import { v1 as random } from 'uuid'; +import { isHexString } from 'ethereumjs-util'; import { BaseController } from '../BaseControllerV2'; import { safelyExecute } from '../util'; import type { RestrictedControllerMessenger } from '../ControllerMessenger'; @@ -16,6 +17,9 @@ import { calculateTimeEstimate, } from './gas-util'; +const GAS_FEE_API = 'https://mock-gas-server.herokuapp.com/'; +export const LEGACY_GAS_PRICES_API_URL = `https://api.metaswap.codefi.network/gasPrices`; + export type unknownString = 'unknown'; // Fee Market describes the way gas is set after the london hardfork, and was @@ -197,6 +201,10 @@ export class GasFeeController extends BaseController { private pollTokens: Set; + private legacyAPIEndpoint: string; + + private EIP1559APIEndpoint: string; + private fetchGasEstimates; private fetchEthGasPriceEstimate; @@ -205,9 +213,11 @@ export class GasFeeController extends BaseController { private getCurrentNetworkEIP1559Compatibility; + private getCurrentNetworkLegacyGasAPICompatibility; + private getCurrentAccountEIP1559Compatibility; - private getIsMainnet; + private getChainId; private ethQuery: any; @@ -224,9 +234,12 @@ export class GasFeeController extends BaseController { fetchLegacyGasPriceEstimates = defaultFetchLegacyGasPriceEstimates, getCurrentNetworkEIP1559Compatibility, getCurrentAccountEIP1559Compatibility, - getIsMainnet, + getChainId, + getCurrentNetworkLegacyGasAPICompatibility, getProvider, onNetworkStateChange, + legacyAPIEndpoint = LEGACY_GAS_PRICES_API_URL, + EIP1559APIEndpoint = GAS_FEE_API, }: { interval?: number; messenger: RestrictedControllerMessenger< @@ -241,10 +254,13 @@ export class GasFeeController extends BaseController { fetchEthGasPriceEstimate?: typeof defaultFetchEthGasPriceEstimate; fetchLegacyGasPriceEstimates?: typeof defaultFetchLegacyGasPriceEstimates; getCurrentNetworkEIP1559Compatibility: () => Promise; + getCurrentNetworkLegacyGasAPICompatibility: () => boolean; getCurrentAccountEIP1559Compatibility?: () => boolean; - getIsMainnet: () => boolean; + getChainId: () => `0x${string}` | `${number}` | number; getProvider: () => NetworkController['provider']; onNetworkStateChange: (listener: (state: NetworkState) => void) => void; + legacyAPIEndpoint?: string; + EIP1559APIEndpoint?: string; }) { super({ name, @@ -258,8 +274,11 @@ export class GasFeeController extends BaseController { this.fetchLegacyGasPriceEstimates = fetchLegacyGasPriceEstimates; this.pollTokens = new Set(); this.getCurrentNetworkEIP1559Compatibility = getCurrentNetworkEIP1559Compatibility; + this.getCurrentNetworkLegacyGasAPICompatibility = getCurrentNetworkLegacyGasAPICompatibility; this.getCurrentAccountEIP1559Compatibility = getCurrentAccountEIP1559Compatibility; - this.getIsMainnet = getIsMainnet; + this.EIP1559APIEndpoint = EIP1559APIEndpoint; + this.legacyAPIEndpoint = legacyAPIEndpoint; + this.getChainId = getChainId; const provider = getProvider(); this.ethQuery = new EthQuery(provider); @@ -294,7 +313,12 @@ export class GasFeeController extends BaseController { */ async _fetchGasFeeEstimateData(): Promise { let isEIP1559Compatible; - const isMainnet = this.getIsMainnet(); + const isLegacyGasAPICompatible = this.getCurrentNetworkLegacyGasAPICompatibility(); + + let chainId = this.getChainId(); + if (typeof chainId === 'string' && isHexString(chainId)) { + chainId = parseInt(chainId, 16); + } try { isEIP1559Compatible = await this.getEIP1559Compatibility(); } catch (e) { @@ -310,7 +334,9 @@ export class GasFeeController extends BaseController { try { if (isEIP1559Compatible) { - const estimates = await this.fetchGasEstimates(); + const estimates = await this.fetchGasEstimates( + this.EIP1559APIEndpoint.replace('', `${chainId}`), + ); const { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas, @@ -324,8 +350,10 @@ export class GasFeeController extends BaseController { estimatedGasFeeTimeBounds, gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, }; - } else if (isMainnet) { - const estimates = await this.fetchLegacyGasPriceEstimates(); + } else if (isLegacyGasAPICompatible) { + const estimates = await this.fetchLegacyGasPriceEstimates( + this.legacyAPIEndpoint.replace('', `${chainId}`), + ); newState = { gasFeeEstimates: estimates, estimatedGasFeeTimeBounds: {}, diff --git a/src/gas/gas-util.test.ts b/src/gas/gas-util.test.ts index 15323f1f20d..cff089d42bb 100644 --- a/src/gas/gas-util.test.ts +++ b/src/gas/gas-util.test.ts @@ -1,13 +1,10 @@ import nock from 'nock'; -import { - EXTERNAL_GAS_PRICES_API_URL, - fetchLegacyGasPriceEstimates, -} from './gas-util'; +import { fetchLegacyGasPriceEstimates } from './gas-util'; describe('gas utils', () => { describe('fetchLegacyGasPriceEstimates', () => { it('should fetch external gasPrices and return high/medium/low', async () => { - const scope = nock(EXTERNAL_GAS_PRICES_API_URL) + const scope = nock('https://not-a-real-url/') .get(/.+/u) .reply(200, { SafeGasPrice: '22', @@ -15,7 +12,9 @@ describe('gas utils', () => { FastGasPrice: '30', }) .persist(); - const result = await fetchLegacyGasPriceEstimates(); + const result = await fetchLegacyGasPriceEstimates( + 'https://not-a-real-url/', + ); expect(result).toMatchObject({ high: '30', medium: '25', diff --git a/src/gas/gas-util.ts b/src/gas/gas-util.ts index 6667e3a735f..e2ca7346c36 100644 --- a/src/gas/gas-util.ts +++ b/src/gas/gas-util.ts @@ -8,20 +8,19 @@ import { LegacyGasPriceEstimate, } from './GasFeeController'; -const GAS_FEE_API = 'https://mock-gas-server.herokuapp.com/'; -export const EXTERNAL_GAS_PRICES_API_URL = `https://api.metaswap.codefi.network/gasPrices`; - -export async function fetchGasEstimates(): Promise { - return await handleFetch(GAS_FEE_API); +export async function fetchGasEstimates(url: string): Promise { + return await handleFetch(url); } /** * Hit the legacy MetaSwaps gasPrices estimate api and return the low, medium * high values from that API. */ -export async function fetchLegacyGasPriceEstimates(): Promise { - const result = await handleFetch(EXTERNAL_GAS_PRICES_API_URL, { - referrer: EXTERNAL_GAS_PRICES_API_URL, +export async function fetchLegacyGasPriceEstimates( + url: string, +): Promise { + const result = await handleFetch(url, { + referrer: url, referrerPolicy: 'no-referrer-when-downgrade', method: 'GET', mode: 'cors', From 8d04cc5cfffa6659c77d096cb35ff620de469481 Mon Sep 17 00:00:00 2001 From: Brad Decker Date: Tue, 6 Jul 2021 15:36:50 -0500 Subject: [PATCH 44/46] use optional chaining --- src/network/NetworkController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/network/NetworkController.ts b/src/network/NetworkController.ts index 9b4605b8f02..5a68d1bb521 100644 --- a/src/network/NetworkController.ts +++ b/src/network/NetworkController.ts @@ -304,7 +304,7 @@ export class NetworkController extends BaseController< const { properties = {} } = this.state; if (!properties.isEIP1559Compatible) { - if (!this.ethQuery || !this.ethQuery.sendAsync) { + if (typeof this.ethQuery?.sendAsync !== 'function') { return Promise.resolve(true); } return new Promise((resolve, reject) => { From 41cdace735728de3790b0475a2a1aff3354b765b Mon Sep 17 00:00:00 2001 From: Brad Decker Date: Wed, 7 Jul 2021 12:04:23 -0500 Subject: [PATCH 45/46] update test case wording --- src/util.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/util.test.ts b/src/util.test.ts index 3e18554b6b3..5fca899287a 100644 --- a/src/util.test.ts +++ b/src/util.test.ts @@ -142,7 +142,7 @@ describe('util', () => { }); describe('weiHexToGweiDec', () => { - it('should convert a whole number to WEI', () => { + it('should convert a WEI hex representing whole amounts of GWEI to whole number', () => { const numbersInGwei = [1, 123, 101, 1234]; numbersInGwei.forEach((gweiDec) => { expect( @@ -151,7 +151,7 @@ describe('util', () => { }); }); - it('should convert a number with a decimal part to WEI', () => { + it('should convert a WEI hex representing fractional GWEI into a decimal number', () => { const numbersInGwei = [1.1, 123.01, 101.001, 1234.567]; numbersInGwei.forEach((gweiDec) => { expect( @@ -160,7 +160,7 @@ describe('util', () => { }); }); - it('should convert a number < 1 to WEI', () => { + it('should convert a WEI hex representing values less than 1 GWEI into decimal numbers', () => { const numbersInGwei = [0.1, 0.01, 0.001, 0.567]; numbersInGwei.forEach((gweiDec) => { expect( From 3a42d9800e4721784389f2b71ff23df3efa5ce3d Mon Sep 17 00:00:00 2001 From: Brad Decker Date: Wed, 7 Jul 2021 12:37:50 -0500 Subject: [PATCH 46/46] use testdata instead of programmatic testing --- src/util.test.ts | 83 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 65 insertions(+), 18 deletions(-) diff --git a/src/util.test.ts b/src/util.test.ts index 5fca899287a..c6b06bf90c1 100644 --- a/src/util.test.ts +++ b/src/util.test.ts @@ -142,30 +142,77 @@ describe('util', () => { }); describe('weiHexToGweiDec', () => { - it('should convert a WEI hex representing whole amounts of GWEI to whole number', () => { - const numbersInGwei = [1, 123, 101, 1234]; - numbersInGwei.forEach((gweiDec) => { - expect( - util.weiHexToGweiDec(util.gweiDecToWEIBN(gweiDec).toString(16)), - ).toBe(gweiDec.toString()); + it('should convert a whole number to WEI', () => { + const testData = [ + { + input: '3b9aca00', + expectedResult: '1', + }, + { + input: '1ca35f0e00', + expectedResult: '123', + }, + { + input: '178411b200', + expectedResult: '101', + }, + { + input: '11f5021b400', + expectedResult: '1234', + }, + ]; + testData.forEach(({ input, expectedResult }) => { + expect(util.weiHexToGweiDec(input)).toBe(expectedResult); }); }); - it('should convert a WEI hex representing fractional GWEI into a decimal number', () => { - const numbersInGwei = [1.1, 123.01, 101.001, 1234.567]; - numbersInGwei.forEach((gweiDec) => { - expect( - util.weiHexToGweiDec(util.gweiDecToWEIBN(gweiDec).toString(16)), - ).toBe(gweiDec.toString()); + it('should convert a number with a decimal part to WEI', () => { + const testData = [ + { + input: '4190ab00', + expectedResult: '1.1', + }, + { + input: '1ca3f7a480', + expectedResult: '123.01', + }, + { + input: '178420f440', + expectedResult: '101.001', + }, + { + input: '11f71ed6fc0', + expectedResult: '1234.567', + }, + ]; + + testData.forEach(({ input, expectedResult }) => { + expect(util.weiHexToGweiDec(input)).toBe(expectedResult); }); }); - it('should convert a WEI hex representing values less than 1 GWEI into decimal numbers', () => { - const numbersInGwei = [0.1, 0.01, 0.001, 0.567]; - numbersInGwei.forEach((gweiDec) => { - expect( - util.weiHexToGweiDec(util.gweiDecToWEIBN(gweiDec).toString(16)), - ).toBe(gweiDec.toString()); + it('should convert a number < 1 to WEI', () => { + const testData = [ + { + input: '5f5e100', + expectedResult: '0.1', + }, + { + input: '989680', + expectedResult: '0.01', + }, + { + input: 'f4240', + expectedResult: '0.001', + }, + { + input: '21cbbbc0', + expectedResult: '0.567', + }, + ]; + + testData.forEach(({ input, expectedResult }) => { + expect(util.weiHexToGweiDec(input)).toBe(expectedResult); }); });