diff --git a/packages/assets-controllers/src/AccountTrackerController.ts b/packages/assets-controllers/src/AccountTrackerController.ts index a18e8b6b82..682514f851 100644 --- a/packages/assets-controllers/src/AccountTrackerController.ts +++ b/packages/assets-controllers/src/AccountTrackerController.ts @@ -1,4 +1,5 @@ import EthQuery from 'eth-query'; +import type { Provider } from 'eth-query'; import { Mutex } from 'async-mutex'; import { BaseConfig, @@ -30,7 +31,7 @@ export interface AccountInformation { */ export interface AccountTrackerConfig extends BaseConfig { interval: number; - provider?: any; + provider?: Provider; } /** @@ -50,7 +51,7 @@ export class AccountTrackerController extends BaseController< AccountTrackerConfig, AccountTrackerState > { - private ethQuery: any; + private ethQuery?: EthQuery; private mutex = new Mutex(); @@ -125,7 +126,7 @@ export class AccountTrackerController extends BaseController< * * @param provider - Provider used to create a new underlying EthQuery instance. */ - set provider(provider: any) { + set provider(provider: Provider) { this.ethQuery = new EthQuery(provider); } @@ -157,6 +158,9 @@ export class AccountTrackerController extends BaseController< const accounts = { ...this.state.accounts }; for (const address in accounts) { await safelyExecuteWithTimeout(async () => { + if (!this.ethQuery) { + throw new Error('Provider not set'); + } const balance = await query(this.ethQuery, 'getBalance', [address]); accounts[address] = { balance: BNToHex(balance) }; }); @@ -176,6 +180,9 @@ export class AccountTrackerController extends BaseController< return await Promise.all( addresses.map((address): Promise<[string, string] | undefined> => { return safelyExecuteWithTimeout(async () => { + if (!this.ethQuery) { + throw new Error('Provider not set'); + } const balance = await query(this.ethQuery, 'getBalance', [address]); return [address, balance]; }); diff --git a/packages/controller-utils/package.json b/packages/controller-utils/package.json index 2fe2bda931..76b1e4cf58 100644 --- a/packages/controller-utils/package.json +++ b/packages/controller-utils/package.json @@ -31,7 +31,9 @@ "dependencies": { "@metamask/utils": "^5.0.2", "@spruceid/siwe-parser": "1.1.3", + "babel-runtime": "^6.26.0", "eth-ens-namehash": "^2.0.8", + "eth-query": "^2.1.2", "eth-rpc-errors": "^4.0.2", "ethereumjs-util": "^7.0.10", "ethjs-unit": "^0.1.6", diff --git a/packages/controller-utils/src/util.test.ts b/packages/controller-utils/src/util.test.ts index 8c8828c02b..d09f80bfac 100644 --- a/packages/controller-utils/src/util.test.ts +++ b/packages/controller-utils/src/util.test.ts @@ -441,6 +441,8 @@ describe('util', () => { const ethQuery = { getBlockByHash: (blockId: any, cb: any) => cb(null, { id: blockId }), }; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore const result = await util.query(ethQuery, 'getBlockByHash', ['0x1234']); expect(result).toStrictEqual({ id: '0x1234' }); }); @@ -451,6 +453,8 @@ describe('util', () => { cb(new Error('uh oh'), null), }; await expect( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore util.query(ethQuery, 'getBlockByHash', ['0x1234']), ).rejects.toThrow('uh oh'); }); diff --git a/packages/controller-utils/src/util.ts b/packages/controller-utils/src/util.ts index 6534ee8abb..a796b85c34 100644 --- a/packages/controller-utils/src/util.ts +++ b/packages/controller-utils/src/util.ts @@ -6,11 +6,12 @@ import { toChecksumAddress, stripHexPrefix, } from 'ethereumjs-util'; +import type EthQuery from 'eth-query'; import { fromWei, toWei } from 'ethjs-unit'; import ensNamehash from 'eth-ens-namehash'; import deepEqual from 'fast-deep-equal'; import type { Hex } from '@metamask/utils'; -import { isStrictHexString } from '@metamask/utils'; +import { hasProperty, isStrictHexString } from '@metamask/utils'; import type { Json } from './types'; import { MAX_SAFE_CHAIN_ID } from './constants'; @@ -426,12 +427,12 @@ export function normalizeEnsName(ensName: string): string | null { * @returns Promise resolving the request. */ export function query( - ethQuery: any, + ethQuery: EthQuery, method: string, args: any[] = [], ): Promise { return new Promise((resolve, reject) => { - const cb = (error: Error, result: any) => { + const cb = (error: unknown, result: any) => { if (error) { reject(error); return; @@ -439,7 +440,13 @@ export function query( resolve(result); }; - if (typeof ethQuery[method] === 'function') { + if ( + hasProperty(ethQuery, method) && + typeof ethQuery[method] === 'function' + ) { + // All of the generated method types have this signature, but our types don't support these yet + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore ethQuery[method](...args, cb); } else { ethQuery.sendAsync({ method, params: args }, cb); diff --git a/packages/gas-fee-controller/src/GasFeeController.ts b/packages/gas-fee-controller/src/GasFeeController.ts index ced9c28abe..d7c203aac8 100644 --- a/packages/gas-fee-controller/src/GasFeeController.ts +++ b/packages/gas-fee-controller/src/GasFeeController.ts @@ -255,7 +255,7 @@ export class GasFeeController extends BaseControllerV2< private currentChainId; - private ethQuery: any; + private ethQuery?: EthQuery; private clientId?: string; @@ -346,9 +346,6 @@ export class GasFeeController extends BaseControllerV2< 'NetworkController:getProviderConfig', ); this.currentChainId = providerConfig.chainId; - this.ethQuery = this.messagingSystem.call( - 'NetworkController:getEthQuery', - ); this.messagingSystem.subscribe( 'NetworkController:providerConfigChange', diff --git a/packages/gas-fee-controller/src/fetchBlockFeeHistory.test.ts b/packages/gas-fee-controller/src/fetchBlockFeeHistory.test.ts index 5fb8da5083..3a7145937c 100644 --- a/packages/gas-fee-controller/src/fetchBlockFeeHistory.test.ts +++ b/packages/gas-fee-controller/src/fetchBlockFeeHistory.test.ts @@ -40,12 +40,16 @@ describe('fetchBlockFeeHistory', () => { beforeEach(() => { when(mockedQuery) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore .calledWith(ethQuery, 'blockNumber') .mockResolvedValue(new BN(latestBlockNumber)); }); it('should return a representation of fee history from the Ethereum network, organized by block rather than type of data', async () => { when(mockedQuery) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore .calledWith(ethQuery, 'eth_feeHistory', [ toHex(numberOfRequestedBlocks), toHex(latestBlockNumber), @@ -95,6 +99,8 @@ describe('fetchBlockFeeHistory', () => { it('should be able to handle an "empty" response from eth_feeHistory', async () => { when(mockedQuery) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore .calledWith(ethQuery, 'eth_feeHistory', [ toHex(numberOfRequestedBlocks), toHex(latestBlockNumber), @@ -116,6 +122,8 @@ describe('fetchBlockFeeHistory', () => { it('should be able to handle an response with undefined baseFeePerGas from eth_feeHistory', async () => { when(mockedQuery) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore .calledWith(ethQuery, 'eth_feeHistory', [ toHex(numberOfRequestedBlocks), toHex(latestBlockNumber), @@ -153,6 +161,8 @@ describe('fetchBlockFeeHistory', () => { }); when(mockedQuery) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore .calledWith(ethQuery, 'blockNumber') .mockResolvedValue(new BN(latestBlockNumber)); @@ -165,6 +175,8 @@ describe('fetchBlockFeeHistory', () => { .map((block) => block.gasUsedRatio); when(mockedQuery) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore .calledWith(ethQuery, 'eth_feeHistory', [ toHex(endBlockNumber - startBlockNumber + 1), toHex(endBlockNumber), @@ -201,6 +213,8 @@ describe('fetchBlockFeeHistory', () => { const numberOfRequestedBlocks = 3; const endBlock = new BN(latestBlockNumber); when(mockedQuery) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore .calledWith(ethQuery, 'eth_feeHistory', [ toHex(numberOfRequestedBlocks), toHex(endBlock), @@ -228,12 +242,16 @@ describe('fetchBlockFeeHistory', () => { beforeEach(() => { when(mockedQuery) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore .calledWith(ethQuery, 'blockNumber') .mockResolvedValue(new BN(latestBlockNumber)); }); it('should match each item in the "reward" key from the response to its percentile', async () => { when(mockedQuery) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore .calledWith(ethQuery, 'eth_feeHistory', [ toHex(numberOfRequestedBlocks), toHex(latestBlockNumber), @@ -309,6 +327,8 @@ describe('fetchBlockFeeHistory', () => { it('should be able to handle an "empty" response from eth_feeHistory including an empty "reward" array', async () => { when(mockedQuery) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore .calledWith(ethQuery, 'eth_feeHistory', [ toHex(numberOfRequestedBlocks), toHex(latestBlockNumber), @@ -337,6 +357,8 @@ describe('fetchBlockFeeHistory', () => { it('includes an extra block with an estimated baseFeePerGas', async () => { when(mockedQuery) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore .calledWith(ethQuery, 'eth_feeHistory', [ toHex(numberOfRequestedBlocks), toHex(latestBlockNumber), @@ -403,6 +425,8 @@ describe('fetchBlockFeeHistory', () => { const endBlock = new BN(latestBlockNumber); when(mockedQuery) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore .calledWith(ethQuery, 'eth_feeHistory', [ toHex(latestBlockNumber), toHex(latestBlockNumber), diff --git a/packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory.test.ts b/packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory.test.ts index c9086ab4fc..9d350fb229 100644 --- a/packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory.test.ts +++ b/packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory.test.ts @@ -83,6 +83,8 @@ describe('fetchGasEstimatesViaEthFeeHistory', () => { .calledWith(blocks) .mockReturnValue(levelSpecificEstimates); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore const gasFeeEstimates = await fetchGasEstimatesViaEthFeeHistory(ethQuery); expect(gasFeeEstimates).toStrictEqual({ diff --git a/packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory.ts b/packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory.ts index 13baabf398..bd326f543e 100644 --- a/packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory.ts +++ b/packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory.ts @@ -1,7 +1,7 @@ import { fromWei } from 'ethjs-unit'; +import type EthQuery from 'eth-query'; import { GWEI } from '@metamask/controller-utils'; import { GasFeeEstimates } from './GasFeeController'; -import { EthQuery } from './fetchGasEstimatesViaEthFeeHistory/types'; import fetchBlockFeeHistory from './fetchBlockFeeHistory'; import fetchLatestBlock from './fetchGasEstimatesViaEthFeeHistory/fetchLatestBlock'; import calculateGasFeeEstimatesForPriorityLevels from './fetchGasEstimatesViaEthFeeHistory/calculateGasFeeEstimatesForPriorityLevels'; diff --git a/packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory/fetchLatestBlock.ts b/packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory/fetchLatestBlock.ts index a3d686f284..034423eda3 100644 --- a/packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory/fetchLatestBlock.ts +++ b/packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory/fetchLatestBlock.ts @@ -1,5 +1,6 @@ import { query, fromHex } from '@metamask/controller-utils'; -import { EthBlock, EthQuery } from './types'; +import type EthQuery from 'eth-query'; +import { EthBlock } from './types'; /** * Returns information about the latest completed block. diff --git a/packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory/types.ts b/packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory/types.ts index f58908206f..02002cff62 100644 --- a/packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory/types.ts +++ b/packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory/types.ts @@ -5,10 +5,4 @@ export type EthBlock = { baseFeePerGas: BN; }; -export type EthQuery = { - getBlockByNumber: ( - blockNumber: BN | 'latest' | 'earliest' | 'pending', - ) => Promise; -}; - export type FeeRange = [string, string]; diff --git a/packages/network-controller/src/NetworkController.ts b/packages/network-controller/src/NetworkController.ts index b3c6226303..82ebbc5d51 100644 --- a/packages/network-controller/src/NetworkController.ts +++ b/packages/network-controller/src/NetworkController.ts @@ -1,4 +1,6 @@ +import type EventEmitter from 'events'; import EthQuery from 'eth-query'; +import type { Provider as EthQueryProvider } from 'eth-query'; import Subprovider from 'web3-provider-engine/subproviders/provider'; import createInfuraProvider from 'eth-json-rpc-infura/src/createProvider'; import createMetamaskProvider from 'web3-provider-engine/zero'; @@ -20,7 +22,7 @@ import { isNetworkType, BUILT_IN_NETWORKS, } from '@metamask/controller-utils'; -import { assertIsStrictHexString } from '@metamask/utils'; +import { assertIsStrictHexString, hasProperty } from '@metamask/utils'; import { NetworkStatus } from './constants'; @@ -123,16 +125,14 @@ const LOCALHOST_RPC_URL = 'http://localhost:8545'; const name = 'NetworkController'; -export type EthQuery = any; - -type Provider = any; - -export type ProviderProxy = SwappableProxy; - type BlockTracker = any; export type BlockTrackerProxy = SwappableProxy; +export type Provider = EventEmitter & EthQueryProvider & { stop: () => void }; + +export type ProviderProxy = SwappableProxy; + export type NetworkControllerStateChangeEvent = { type: `NetworkController:stateChange`; payload: [NetworkState, Patch[]]; @@ -211,7 +211,7 @@ export class NetworkController extends BaseControllerV2< NetworkState, NetworkControllerMessenger > { - #ethQuery: EthQuery; + #ethQuery?: EthQuery; #infuraProjectId: string | undefined; @@ -272,6 +272,9 @@ export class NetworkController extends BaseControllerV2< this.messagingSystem.registerActionHandler( `${this.name}:getEthQuery`, () => { + if (!this.#ethQuery) { + throw new Error('Provider has not been initialized'); + } return this.#ethQuery; }, ); @@ -347,7 +350,11 @@ export class NetworkController extends BaseControllerV2< pollingInterval: 12000, }, }; - this.#updateProvider(createMetamaskProvider(config)); + + // Cast needed because the `web3-provider-engine` type for `sendAsync` + // incorrectly suggests that an array is accepted as the first parameter + // of `sendAsync`. + this.#updateProvider(createMetamaskProvider(config) as Provider); } #setupStandardProvider( @@ -363,11 +370,18 @@ export class NetworkController extends BaseControllerV2< rpcUrl: rpcTarget, ticker, }; - this.#updateProvider(createMetamaskProvider(config)); + + // Cast needed because the `web3-provider-engine` type for `sendAsync` + // incorrectly suggests that an array is accepted as the first parameter + // of `sendAsync`. + this.#updateProvider(createMetamaskProvider(config) as Provider); } #updateProvider(provider: Provider) { this.#safelyStopProvider(this.#provider); + if (!hasProperty(provider, '_blockTracker')) { + throw new Error('Provider is missing block tracker'); + } this.#setProviderAndBlockTracker({ provider, blockTracker: provider._blockTracker, @@ -377,7 +391,9 @@ export class NetworkController extends BaseControllerV2< #safelyStopProvider(provider: Provider | undefined) { setTimeout(() => { - provider?.stop(); + if (provider) { + provider?.stop(); + } }, 500); } @@ -403,13 +419,17 @@ export class NetworkController extends BaseControllerV2< async #getNetworkId(): Promise { const possibleNetworkId = await new Promise((resolve, reject) => { + if (!this.#ethQuery) { + throw new Error('Provider has not been initialized'); + } this.#ethQuery.sendAsync( { method: 'net_version' }, - (error: Error, result: string) => { + (error: unknown, result?: unknown) => { if (error) { reject(error); } else { - resolve(result); + // TODO: Validate this type + resolve(result as string); } }, ); @@ -534,13 +554,17 @@ export class NetworkController extends BaseControllerV2< #getLatestBlock(): Promise { return new Promise((resolve, reject) => { + if (!this.#ethQuery) { + throw new Error('Provider has not been initialized'); + } this.#ethQuery.sendAsync( { method: 'eth_getBlockByNumber', params: ['latest', false] }, - (error: Error, block: Block) => { + (error: unknown, block?: unknown) => { if (error) { reject(error); } else { - resolve(block); + // TODO: Validate this type + resolve(block as Block); } }, ); diff --git a/packages/network-controller/tests/NetworkController.test.ts b/packages/network-controller/tests/NetworkController.test.ts index 9b1fe873f2..93575fcfff 100644 --- a/packages/network-controller/tests/NetworkController.test.ts +++ b/packages/network-controller/tests/NetworkController.test.ts @@ -6,7 +6,6 @@ import { ControllerMessenger } from '@metamask/base-controller'; import * as ethQueryModule from 'eth-query'; import Subprovider from 'web3-provider-engine/subproviders/provider'; import createInfuraProvider from 'eth-json-rpc-infura/src/createProvider'; -import type { ProviderEngine } from 'web3-provider-engine'; import createMetamaskProvider from 'web3-provider-engine/zero'; import { Patch } from 'immer'; import { v4 } from 'uuid'; @@ -27,6 +26,7 @@ import { NetworkState, ProviderConfig, } from '../src/NetworkController'; +import type { Provider } from '../src/NetworkController'; import { NetworkStatus } from '../src/constants'; import { BUILT_IN_NETWORKS } from '../../controller-utils/src/constants'; import { FakeProviderEngine, FakeProviderStub } from './fake-provider-engine'; @@ -221,6 +221,7 @@ describe('NetworkController', () => { id: 1, jsonrpc: '2.0', method: 'eth_chainId', + params: [], }); expect(chainIdResult.result).toBe('0x1337'); }, @@ -331,7 +332,7 @@ describe('NetworkController', () => { produceStateChanges: () => { controller .getProviderAndBlockTracker() - .provider.emit('error', { some: 'error' }); + .provider?.emit('error', { some: 'error' }); }, }); expect(controller.state.networkId).toBe('2'); @@ -401,7 +402,7 @@ describe('NetworkController', () => { produceStateChanges: () => { controller .getProviderAndBlockTracker() - .provider.emit('error', { some: 'error' }); + .provider?.emit('error', { some: 'error' }); }, }); expect(controller.state.networkId).toBe('1'); @@ -451,11 +452,15 @@ describe('NetworkController', () => { ticker: undefined, }); const { provider } = controller.getProviderAndBlockTracker(); + assert(provider); const promisifiedSendAsync = promisify(provider.sendAsync).bind( provider, ); const chainIdResult = await promisifiedSendAsync({ + id: 1, + jsonrpc: '2.0', method: 'eth_chainId', + params: [], }); expect(chainIdResult.result).toBe('0x1337'); }, @@ -552,7 +557,7 @@ describe('NetworkController', () => { produceStateChanges: () => { controller .getProviderAndBlockTracker() - .provider.emit('error', { some: 'error' }); + .provider?.emit('error', { some: 'error' }); }, }); expect(controller.state.networkId).toBe('2'); @@ -613,7 +618,7 @@ describe('NetworkController', () => { produceStateChanges: () => { controller .getProviderAndBlockTracker() - .provider.emit('error', { some: 'error' }); + .provider?.emit('error', { some: 'error' }); }, }); expect(controller.state.networkId).toBe('1'); @@ -662,11 +667,15 @@ describe('NetworkController', () => { ticker: 'ABC', }); const { provider } = controller.getProviderAndBlockTracker(); + assert(provider); const promisifiedSendAsync = promisify(provider.sendAsync).bind( provider, ); const chainIdResult = await promisifiedSendAsync({ + id: 1, + jsonrpc: '2.0', method: 'eth_chainId', + params: [], }); expect(chainIdResult.result).toBe('0x1337'); }, @@ -767,7 +776,7 @@ describe('NetworkController', () => { produceStateChanges: () => { controller .getProviderAndBlockTracker() - .provider.emit('error', { some: 'error' }); + .provider?.emit('error', { some: 'error' }); }, }); expect(controller.state.networkId).toBe('2'); @@ -831,7 +840,7 @@ describe('NetworkController', () => { produceStateChanges: () => { controller .getProviderAndBlockTracker() - .provider.emit('error', { some: 'error' }); + .provider?.emit('error', { some: 'error' }); }, }); expect(controller.state.networkId).toBe('1'); @@ -1559,11 +1568,15 @@ describe('NetworkController', () => { }, }); const { provider } = controller.getProviderAndBlockTracker(); + assert(provider); const promisifiedSendAsync = promisify(provider.sendAsync).bind( provider, ); const chainIdResult = await promisifiedSendAsync({ + id: 1, + jsonrpc: '2.0', method: 'eth_chainId', + params: [], }); expect(chainIdResult.result).toBe('0x1337'); }, @@ -1695,7 +1708,7 @@ describe('NetworkController', () => { produceStateChanges: () => { controller .getProviderAndBlockTracker() - .provider.emit('error', { some: 'error' }); + .provider?.emit('error', { some: 'error' }); }, }); expect(controller.state.networkId).toBe('42'); @@ -1741,7 +1754,7 @@ describe('NetworkController', () => { produceStateChanges: () => { controller .getProviderAndBlockTracker() - .provider.emit('error', { some: 'error' }); + .provider?.emit('error', { some: 'error' }); }, }); expect(controller.state.networkId).toBe('1'); @@ -1893,11 +1906,15 @@ describe('NetworkController', () => { ticker: undefined, }); const { provider } = controller.getProviderAndBlockTracker(); + assert(provider); const promisifiedSendAsync = promisify(provider.sendAsync).bind( provider, ); const chainIdResult = await promisifiedSendAsync({ + id: 1, + jsonrpc: '2.0', method: 'eth_chainId', + params: [], }); expect(chainIdResult.result).toBe('0x1337'); }); @@ -2003,7 +2020,7 @@ describe('NetworkController', () => { produceStateChanges: () => { controller .getProviderAndBlockTracker() - .provider.emit('error', { some: 'error' }); + .provider?.emit('error', { some: 'error' }); }, }); expect(controller.state.networkId).toBe('42'); @@ -2043,7 +2060,7 @@ describe('NetworkController', () => { produceStateChanges: () => { controller .getProviderAndBlockTracker() - .provider.emit('error', { some: 'error' }); + .provider?.emit('error', { some: 'error' }); }, }); expect(controller.state.networkId).toBe('1'); @@ -2148,11 +2165,15 @@ describe('NetworkController', () => { engineParams: { pollingInterval: 12000 }, }); const { provider } = controller.getProviderAndBlockTracker(); + assert(provider); const promisifiedSendAsync = promisify(provider.sendAsync).bind( provider, ); const chainIdResult = await promisifiedSendAsync({ + id: 1, + jsonrpc: '2.0', method: 'eth_chainId', + params: [], }); expect(chainIdResult.result).toBe('0x1337'); }, @@ -2327,7 +2348,7 @@ describe('NetworkController', () => { produceStateChanges: () => { controller .getProviderAndBlockTracker() - .provider.emit('error', { some: 'error' }); + .provider?.emit('error', { some: 'error' }); }, }); expect(controller.state.networkId).toBe('42'); @@ -2384,7 +2405,7 @@ describe('NetworkController', () => { produceStateChanges: () => { controller .getProviderAndBlockTracker() - .provider.emit('error', { some: 'error' }); + .provider?.emit('error', { some: 'error' }); }, }); expect(controller.state.networkId).toBe('1'); @@ -3349,11 +3370,15 @@ describe('NetworkController', () => { await controller.resetConnection(); const { provider } = controller.getProviderAndBlockTracker(); + assert(provider); const promisifiedSendAsync = promisify(provider.sendAsync).bind( provider, ); const { result: chainIdResult } = await promisifiedSendAsync({ + id: 1, + jsonrpc: '2.0', method: 'eth_chainId', + params: [], }); expect(chainIdResult).toBe('0x1337'); }, @@ -3547,7 +3572,7 @@ describe('NetworkController', () => { produceStateChanges: () => { controller .getProviderAndBlockTracker() - .provider.emit('error', { some: 'error' }); + .provider?.emit('error', { some: 'error' }); }, }); await resetPromise; @@ -3721,11 +3746,15 @@ describe('NetworkController', () => { await controller.resetConnection(); const { provider } = controller.getProviderAndBlockTracker(); + assert(provider); const promisifiedSendAsync = promisify(provider.sendAsync).bind( provider, ); const { result: chainIdResult } = await promisifiedSendAsync({ + id: 1, + jsonrpc: '2.0', method: 'eth_chainId', + params: [], }); expect(chainIdResult).toBe('0x1337'); }, @@ -3910,7 +3939,7 @@ describe('NetworkController', () => { produceStateChanges: () => { controller .getProviderAndBlockTracker() - .provider.emit('error', { some: 'error' }); + .provider?.emit('error', { some: 'error' }); }, }); await resetPromise; @@ -3965,6 +3994,20 @@ describe('NetworkController', () => { expect(ethQuery).toBe(fakeEthQuery); }); }); + + it('throws if the provider is not set', async () => { + const messenger = buildMessenger(); + await withController({ messenger }, async () => { + const fakeEthQuery = { + sendAsync: jest.fn(), + }; + jest.spyOn(ethQueryModule, 'default').mockReturnValue(fakeEthQuery); + + await expect( + async () => await messenger.call('NetworkController:getEthQuery'), + ).rejects.toThrow('Provider has not been initialized'); + }); + }); }); describe('upsertNetworkConfiguration', () => { @@ -4958,11 +5001,15 @@ describe('NetworkController', () => { await controller.rollbackToPreviousProvider(); const { provider } = controller.getProviderAndBlockTracker(); + assert(provider); const promisifiedSendAsync = promisify(provider.sendAsync).bind( provider, ); const { result: chainIdResult } = await promisifiedSendAsync({ + id: 1, + jsonrpc: '2.0', method: 'eth_chainId', + params: [], }); expect(chainIdResult).toBe(chainId); }, @@ -5400,11 +5447,15 @@ describe('NetworkController', () => { await controller.rollbackToPreviousProvider(); const { provider } = controller.getProviderAndBlockTracker(); + assert(provider); const promisifiedSendAsync = promisify(provider.sendAsync).bind( provider, ); const { result: chainIdResult } = await promisifiedSendAsync({ + id: 1, + jsonrpc: '2.0', method: 'eth_chainId', + params: [], }); expect(chainIdResult).toBe( initialProviderConfigNetworkConfiguration.chainId, @@ -5793,7 +5844,7 @@ async function setFakeProvider( stubLookupNetworkWhileSetting?: boolean; stubGetEIP1559CompatibilityWhileSetting?: boolean; } = {}, -): Promise { +): Promise { const fakeMetamaskProvider = buildFakeMetamaskProvider(stubs); createMetamaskProviderMock.mockReturnValue(fakeMetamaskProvider); const lookupNetworkMock = jest.spyOn(controller, 'lookupNetwork'); @@ -5823,7 +5874,9 @@ async function setFakeProvider( lookupGetEIP1559CompatibilityMock.mockRestore(); } - return controller.getProviderAndBlockTracker().provider; + const { provider } = controller.getProviderAndBlockTracker(); + assert(provider); + return provider; } /** diff --git a/packages/network-controller/tests/provider-api-tests/helpers.ts b/packages/network-controller/tests/provider-api-tests/helpers.ts index 133c99b759..c3e8510737 100644 --- a/packages/network-controller/tests/provider-api-tests/helpers.ts +++ b/packages/network-controller/tests/provider-api-tests/helpers.ts @@ -1,6 +1,7 @@ /* eslint-disable node/no-process-env */ import nock, { Scope as NockScope } from 'nock'; import sinon from 'sinon'; +import type EthQuery from 'eth-query'; import { JSONRPCResponse, JSONRPCResponseResult, @@ -9,7 +10,6 @@ import { ControllerMessenger } from '@metamask/base-controller'; import { NetworkType } from '@metamask/controller-utils'; import { NetworkController, - EthQuery, NetworkControllerMessenger, } from '../../src/NetworkController'; @@ -437,7 +437,9 @@ export const withNetworkClient = async ( getEIP1559CompatibilityMock.mockRestore(); lookupNetworkMock.mockRestore(); blockTracker.removeAllListeners(); - provider.stop(); + if (provider) { + provider.stop(); + } clock.restore(); } }; diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 349a58ebde..f8d7252eb1 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -271,7 +271,7 @@ export class TransactionController extends BaseController< TransactionConfig, TransactionState > { - private ethQuery: any; + private ethQuery: EthQuery; private nonceTracker: NonceTracker; diff --git a/types/eth-query.d.ts b/types/eth-query.d.ts index e857105f28..2661d54ce5 100644 --- a/types/eth-query.d.ts +++ b/types/eth-query.d.ts @@ -1 +1,51 @@ -declare module 'eth-query'; +declare module 'eth-query' { + // What it says on the tin. We omit `null` because confusingly, this is used + // for a successful response to indicate a lack of an error. + type EverythingButNull = + | string + | number + | boolean + // eslint-disable-next-line @typescript-eslint/ban-types + | object + | symbol + | undefined; + + type ProviderSendAsyncResponse = { + error?: { message: string }; + result?: Result; + }; + + type ProviderSendAsyncCallback = ( + error: unknown, + response: ProviderSendAsyncResponse, + ) => void; + + type Provider = { + sendAsync( + payload: SendAsyncPayload, + callback: ProviderSendAsyncCallback, + ): void; + }; + + type SendAsyncPayload = { + id: number; + jsonrpc: '2.0'; + method: string; + params: Params; + }; + + type SendAsyncCallback = ( + ...args: + | [error: EverythingButNull, result: undefined] + | [error: null, result: Result] + ) => void; + + export default class EthQuery { + constructor(provider: Provider); + + sendAsync( + opts: Partial>, + callback: SendAsyncCallback, + ): void; + } +} diff --git a/yarn.lock b/yarn.lock index b50b408403..3580f23a3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1457,8 +1457,10 @@ __metadata: "@spruceid/siwe-parser": 1.1.3 "@types/jest": ^27.4.1 abort-controller: ^3.0.0 + babel-runtime: ^6.26.0 deepmerge: ^4.2.2 eth-ens-namehash: ^2.0.8 + eth-query: ^2.1.2 eth-rpc-errors: ^4.0.2 ethereumjs-util: ^7.0.10 ethjs-unit: ^0.1.6