From 2a223d91a6a7d54cbd59a39d32194ed8b0862db4 Mon Sep 17 00:00:00 2001 From: Zachary Belford Date: Mon, 5 Dec 2022 13:52:15 -0800 Subject: [PATCH] Testability --- packages/network-controller/package.json | 2 + .../src/NetworkController.test.ts | 141 +++++++++++++----- .../src/NetworkController.ts | 44 ++++-- .../src/clients/createInfuraClient.ts | 50 +++++-- .../src/clients/createJsonRpcClient.ts | 28 ++-- .../network-controller/src/clients/types.ts | 7 + 6 files changed, 204 insertions(+), 68 deletions(-) create mode 100644 packages/network-controller/src/clients/types.ts diff --git a/packages/network-controller/package.json b/packages/network-controller/package.json index 132d47f1aa4..6f23a3b0d24 100644 --- a/packages/network-controller/package.json +++ b/packages/network-controller/package.json @@ -34,10 +34,12 @@ "@metamask/eth-json-rpc-infura": "^7.0.0", "async-mutex": "^0.2.6", "babel-runtime": "^6.26.0", + "eth-block-tracker": "^6.0.0", "eth-json-rpc-infura": "^5.1.0", "eth-json-rpc-middleware": "^9.0.1", "eth-query": "^2.1.2", "immer": "^9.0.6", + "json-rpc-engine": "^6.1.0", "web3-provider-engine": "^16.0.3" }, "devDependencies": { diff --git a/packages/network-controller/src/NetworkController.test.ts b/packages/network-controller/src/NetworkController.test.ts index f4cfc3137fd..de956240ce8 100644 --- a/packages/network-controller/src/NetworkController.test.ts +++ b/packages/network-controller/src/NetworkController.test.ts @@ -1,41 +1,60 @@ import * as sinon from 'sinon'; import { ControllerMessenger } from '@metamask/base-controller'; import { NetworkType, NetworksChainId } from '@metamask/controller-utils'; +import SafeEventEmitter from '@metamask/safe-event-emitter'; +import nock from 'nock'; import { NetworkController, NetworkControllerMessenger, NetworkControllerOptions, - ProviderConfig, } from './NetworkController'; -import SafeEventEmitter from '@metamask/safe-event-emitter'; -import nock from 'nock'; const RPC_TARGET = 'http://foo'; type WithMockedBlockTrackerOptions = { - nextBlockNumber?: () => string + nextBlockNumber?: () => string; }; -const withMockedBlockTracker = (fn?: () => void, options: WithMockedBlockTrackerOptions = {}) => { - const nextBlockNumber = options.nextBlockNumber ? options.nextBlockNumber : () => '0x42'; - - nock(new RegExp('https://.*')) - .post( - new RegExp('.*'), - { - jsonrpc: '2.0', - id: new RegExp('.*'), - method: new RegExp('.*'), - params: [] - } - ) - .reply((uri, reqBody: any) => { +const withMockedBlockTracker = async ( + options: WithMockedBlockTrackerOptions = {}, +) => { + const nextBlockNumber = options.nextBlockNumber + ? options.nextBlockNumber + : () => '0x42'; + + const urlRegex = /https:\/\/.*/u; + const anyRegex = /.*/u; + nock(urlRegex) + .post(anyRegex, { + jsonrpc: '2.0', + id: anyRegex, + method: "eth_blockNumber", + params: [], + }) + .reply((_, reqBody: any) => { + console.log(reqBody); + return [ + 200, + { jsonrpc: '2.0', id: reqBody.id, result: nextBlockNumber() }, + ]; + }) + .persist(); + + nock(urlRegex) + .post(anyRegex, { + jsonrpc: '2.0', + id: anyRegex, + method: "eth_getBlockByNumber", + params: ["0x42", false], + }) + .reply((_, reqBody: any) => { console.log(reqBody); return [ 200, - { jsonrpc: '2.0', id: reqBody.id, result: nextBlockNumber() } + { jsonrpc: '2.0', id: reqBody.id, result: {} }, ]; - }).persist(); + }) + .persist(); }; const setupController = ( @@ -55,7 +74,6 @@ const setupController = ( messenger, }; const controller = new NetworkController(networkControllerOpts); - controller.setProviderType(controller.state.provider.type); return controller; }; @@ -72,12 +90,11 @@ describe('NetworkController', () => { afterEach(() => { sinon.restore(); - // nock.restore(); - // nock.cleanAll(); + nock.restore(); + nock.cleanAll(); }); - it.only('should set default state', () => { - withMockedBlockTracker(); + it('should set default state', () => { const controller = new NetworkController({ messenger, infuraProjectId: 'potate', @@ -86,7 +103,9 @@ describe('NetworkController', () => { expect(controller.state).toStrictEqual({ network: 'loading', isCustomNetwork: false, - properties: {}, + properties: { + isEIP1559Compatible: false + }, provider: { type: 'mainnet', chainId: '1', @@ -100,21 +119,42 @@ describe('NetworkController', () => { messenger, }; const controller = new NetworkController(networkControllerOpts); + const setupInfuraProvider = jest.spyOn(NetworkController.prototype as any, 'setupInfuraProvider'); + setupInfuraProvider.mockImplementationOnce(() => { }); + controller.setProviderType(controller.state.provider.type); - expect(controller.provider).toBeInstanceOf(SafeEventEmitter); + expect(setupInfuraProvider).toHaveBeenCalled(); }); ( - ['kovan', 'rinkeby', 'ropsten', 'mainnet', 'localhost'] as NetworkType[] + ['kovan', 'rinkeby', 'ropsten', 'mainnet'] as NetworkType[] ).forEach((n) => { it(`should create a provider instance for ${n} infura network`, () => { const networkController = setupController(n, messenger); - expect(networkController.provider).toBeInstanceOf(SafeEventEmitter); + + const setupInfuraProvider = jest.spyOn(NetworkController.prototype as any, 'setupInfuraProvider'); + setupInfuraProvider.mockImplementationOnce(() => { }); expect(networkController.state.isCustomNetwork).toBe(false); + networkController.setProviderType(n); + expect(setupInfuraProvider).toHaveBeenCalled(); }); }); - it('should create a provider instance for optimism network', () => { + it(`should create a provider instance for localhost network`, () => { + const networkController = setupController('localhost', messenger); + + const setupStandardProvider = jest.spyOn( + NetworkController.prototype as any, + 'setupStandardProvider' + ); + setupStandardProvider.mockImplementationOnce(() => { }); + + expect(networkController.state.isCustomNetwork).toBe(false); + networkController.setProviderType('localhost'); + expect(setupStandardProvider).toHaveBeenCalled(); + }); + + it.only('should create a provider instance for optimism network', () => { const networkControllerOpts: NetworkControllerOptions = { infuraProjectId: 'foo', state: { @@ -128,12 +168,21 @@ describe('NetworkController', () => { }, messenger, }; + const controller = new NetworkController(networkControllerOpts); - expect(controller.provider).toBeInstanceOf(SafeEventEmitter); + + const setupStandardProvider = jest.spyOn( + NetworkController.prototype as any, + 'setupStandardProvider' + ); + setupStandardProvider.mockImplementationOnce(() => { }); + + controller.setProviderType(controller.state.provider.type); expect(controller.state.isCustomNetwork).toBe(true); + expect(setupStandardProvider).toHaveBeenCalled(); }); - it('should create a provider instance for rpc network', () => { + it.only('should create a provider instance for rpc network', () => { const networkControllerOpts: NetworkControllerOptions = { infuraProjectId: 'foo', state: { @@ -147,20 +196,33 @@ describe('NetworkController', () => { messenger, }; const controller = new NetworkController(networkControllerOpts); + + const setupStandardProvider = jest.spyOn( + NetworkController.prototype as any, + 'setupStandardProvider' + ); + setupStandardProvider.mockImplementationOnce(() => { }); + controller.setProviderType(controller.state.provider.type); - expect(controller.provider).toBeInstanceOf(SafeEventEmitter); expect(controller.state.isCustomNetwork).toBe(false); + expect(setupStandardProvider).toHaveBeenCalled(); }); it('should set new RPC target', () => { - const controller = new NetworkController({ messenger, infuraProjectId: 'potate' }); + const controller = new NetworkController({ + messenger, + infuraProjectId: 'potate', + }); controller.setRpcTarget(RPC_TARGET, NetworksChainId.rpc); expect(controller.state.provider.rpcTarget).toBe(RPC_TARGET); expect(controller.state.isCustomNetwork).toBe(false); }); it('should set new provider type', () => { - const controller = new NetworkController({ messenger, infuraProjectId: 'potate' }); + const controller = new NetworkController({ + messenger, + infuraProjectId: 'potate', + }); controller.setProviderType('localhost'); expect(controller.state.provider.type).toBe('localhost'); expect(controller.state.isCustomNetwork).toBe(false); @@ -207,7 +269,10 @@ describe('NetworkController', () => { }); it('should throw when setting an unrecognized provider type', () => { - const controller = new NetworkController({ messenger, infuraProjectId: 'potate' }); + const controller = new NetworkController({ + messenger, + infuraProjectId: 'potate', + }); expect(() => controller.setProviderType('junk' as NetworkType)).toThrow( "Unrecognized network type: 'junk'", ); @@ -223,7 +288,9 @@ describe('NetworkController', () => { }); controller.setProviderType(controller.state.provider.type); controller.lookupNetwork = sinon.stub(); - if (controller.provider === undefined) { throw new Error('provider is undefined'); } + if (controller.provider === undefined) { + throw new Error('provider is undefined'); + } controller.provider.emit('error', {}); expect((controller.lookupNetwork as any).called).toBe(true); }); diff --git a/packages/network-controller/src/NetworkController.ts b/packages/network-controller/src/NetworkController.ts index 96bbf030677..04ce892fe98 100644 --- a/packages/network-controller/src/NetworkController.ts +++ b/packages/network-controller/src/NetworkController.ts @@ -12,9 +12,15 @@ import { NetworksChainId, NetworkType, } from '@metamask/controller-utils'; -import { JsonRpcEngine } from 'json-rpc-engine'; -import createInfuraClient, { InfuraNetworkType } from './clients/createInfuraClient'; -import { providerFromEngine, SafeEventEmitterProvider } from 'eth-json-rpc-middleware'; +import { JsonRpcEngine, JsonRpcMiddleware } from 'json-rpc-engine'; +import { + providerFromEngine, + SafeEventEmitterProvider, +} from 'eth-json-rpc-middleware'; +import { PollingBlockTracker } from 'eth-block-tracker'; +import createInfuraClient, { + InfuraNetworkType, +} from './clients/createInfuraClient'; import createJsonRpcClient from './clients/createJsonRpcClient'; /** @@ -162,12 +168,14 @@ export class NetworkController extends BaseControllerV2< private initializeProvider( type: NetworkType, rpcTarget?: string, - chainId?: string + chainId?: string, ) { this.update((state) => { state.isCustomNetwork = this.getIsCustomNetwork(chainId); }); - + console.log("initialize provider:"); + console.log(type); + console.log(chainId); switch (type) { case 'kovan': case MAINNET: @@ -180,8 +188,7 @@ export class NetworkController extends BaseControllerV2< this.setupStandardProvider(LOCALHOST_RPC_URL); break; case RPC: - rpcTarget && - this.setupStandardProvider(rpcTarget, chainId as string); + rpcTarget && this.setupStandardProvider(rpcTarget, chainId as string); break; default: throw new Error(`Unrecognized network type: '${type}'`); @@ -201,7 +208,9 @@ export class NetworkController extends BaseControllerV2< private registerProvider() { if (this.provider === undefined) { - throw new Error('dont call registerProvider when networkController.provider is unset'); + throw new Error( + 'dont call registerProvider when networkController.provider is unset', + ); } this.provider.on('error', this.verifyNetwork.bind(this)); @@ -216,10 +225,7 @@ export class NetworkController extends BaseControllerV2< this.updateProvider(networkMiddleware, blockTracker); } - private setupStandardProvider( - rpcTarget: string, - chainId?: string, - ) { + private setupStandardProvider(rpcTarget: string, chainId?: string) { const { networkMiddleware, blockTracker } = createJsonRpcClient( rpcTarget, chainId, @@ -241,7 +247,10 @@ export class NetworkController extends BaseControllerV2< // extensions network controller saves a copy of the blockTracker. // need to figure out if we migrate this functionality or not. - private updateProvider(networkMiddleware: any, blockTracker: any) { + private updateProvider( + networkMiddleware: JsonRpcMiddleware, + blockTracker: PollingBlockTracker, + ) { this.safelyStopProvider(this.provider); const engine = new JsonRpcEngine(); @@ -249,6 +258,13 @@ export class NetworkController extends BaseControllerV2< const provider = providerFromEngine(engine); this.provider = provider; + this.blockTracker = blockTracker; + + this.messagingSystem.publish( + `NetworkController:providerChange`, + this.state.provider, + ); + this.registerProvider(); } @@ -262,6 +278,8 @@ export class NetworkController extends BaseControllerV2< this.state.network === 'loading' && this.lookupNetwork(); } + blockTracker: PollingBlockTracker | undefined; + /** * Ethereum provider object for the current network * todo: should never be undefined (definitely assigned in constructor) diff --git a/packages/network-controller/src/clients/createInfuraClient.ts b/packages/network-controller/src/clients/createInfuraClient.ts index 412adf262f9..11b88bf94e4 100644 --- a/packages/network-controller/src/clients/createInfuraClient.ts +++ b/packages/network-controller/src/clients/createInfuraClient.ts @@ -1,4 +1,8 @@ -import { createScaffoldMiddleware, mergeMiddleware, JsonRpcMiddleware } from 'json-rpc-engine'; +import { + createScaffoldMiddleware, + JsonRpcMiddleware, + mergeMiddleware, +} from 'json-rpc-engine'; import { createBlockRefMiddleware, createRetryOnEmptyMiddleware, @@ -9,10 +13,10 @@ import { } from 'eth-json-rpc-middleware'; import { createInfuraMiddleware } from '@metamask/eth-json-rpc-infura'; -import { InfuraJsonRpcSupportedNetwork } from '@metamask/eth-json-rpc-infura/dist/types'; import { PollingBlockTracker } from 'eth-block-tracker'; import { NetworksChainId, NetworkType } from '@metamask/controller-utils'; +import { CreateClientResult } from './types'; export type InfuraNetworkType = | 'kovan' @@ -21,7 +25,17 @@ export type InfuraNetworkType = | 'goerli' | 'ropsten'; -export default function createInfuraClient(network: InfuraNetworkType, projectId: string) { +/** + * Create client middleware for infura. + * + * @param network - the network name. + * @param projectId - infura project id. + * @returns The network middleware and the block tracker. + */ +export default function createInfuraClient( + network: InfuraNetworkType, + projectId: string, +): CreateClientResult { const infuraMiddleware = createInfuraMiddleware({ network, projectId, @@ -30,21 +44,39 @@ export default function createInfuraClient(network: InfuraNetworkType, projectId }); const infuraProvider = providerFromMiddleware(infuraMiddleware); // there is a type mismatch for Provider & SafeEventEmitter. - const blockTracker = new PollingBlockTracker({ provider: infuraProvider as any }); + const blockTracker = new PollingBlockTracker({ + provider: infuraProvider as any, + }); const networkMiddleware = mergeMiddleware([ createNetworkAndChainIdMiddleware(network), - createBlockCacheMiddleware({ blockTracker }) as any, // something wrong with typing + createBlockCacheMiddleware({ blockTracker: blockTracker as any }) as any, // something wrong with typing createInflightCacheMiddleware(), - createBlockRefMiddleware({ blockTracker, provider: infuraProvider }), - createRetryOnEmptyMiddleware({ blockTracker, provider: infuraProvider }), - createBlockTrackerInspectorMiddleware({ blockTracker }), + createBlockRefMiddleware({ + blockTracker: blockTracker as any, + provider: infuraProvider, + }), + createRetryOnEmptyMiddleware({ + blockTracker: blockTracker as any, + provider: infuraProvider, + }), + createBlockTrackerInspectorMiddleware({ + blockTracker: blockTracker as any, + }), infuraMiddleware, ]); return { networkMiddleware, blockTracker }; } -function createNetworkAndChainIdMiddleware(network: NetworkType) { +/** + * Create middleware that will trap calls to get network or chain id. + * + * @param network - network type that we are connecting to. + * @returns json-rpc-engine middleware + */ +function createNetworkAndChainIdMiddleware( + network: NetworkType, +): JsonRpcMiddleware { const chainId = NetworksChainId[network] === undefined; if (typeof chainId === undefined) { diff --git a/packages/network-controller/src/clients/createJsonRpcClient.ts b/packages/network-controller/src/clients/createJsonRpcClient.ts index ac1664225e4..67caf1370ac 100644 --- a/packages/network-controller/src/clients/createJsonRpcClient.ts +++ b/packages/network-controller/src/clients/createJsonRpcClient.ts @@ -8,16 +8,22 @@ import { providerFromMiddleware, } from 'eth-json-rpc-middleware'; import { PollingBlockTracker } from 'eth-block-tracker'; +import { CreateClientResult } from './types'; -const SECOND = 1000; -const inTest = process.env.IN_TEST; -const blockTrackerOpts = inTest ? { pollingInterval: SECOND } : {}; - -export default function createJsonRpcClient(rpcUrl: string, chainId?: string) { +/** + * Create client middleware for a custom rpc endpoint. + * + * @param rpcUrl - url of the rpc endpoint. + * @param chainId - the chain id for the rpc endpoint. This value will always be returned by eth_chainId. + * @returns The network middleware and the block tracker. + */ +export default function createJsonRpcClient( + rpcUrl: string, + chainId?: string, +): CreateClientResult { const fetchMiddleware = createFetchMiddleware({ rpcUrl }); const blockProvider = providerFromMiddleware(fetchMiddleware); const blockTracker = new PollingBlockTracker({ - ...blockTrackerOpts, provider: blockProvider as any, }); @@ -29,10 +35,14 @@ export default function createJsonRpcClient(rpcUrl: string, chainId?: string) { const networkMiddleware = mergeMiddleware([ ...scaffolded, - createBlockRefRewriteMiddleware({ blockTracker }) as any, - createBlockCacheMiddleware({ blockTracker }), + createBlockRefRewriteMiddleware({ + blockTracker: blockTracker as any, + }) as any, + createBlockCacheMiddleware({ blockTracker: blockTracker as any }), createInflightCacheMiddleware(), - createBlockTrackerInspectorMiddleware({ blockTracker }), + createBlockTrackerInspectorMiddleware({ + blockTracker: blockTracker as any, + }), fetchMiddleware, ]); diff --git a/packages/network-controller/src/clients/types.ts b/packages/network-controller/src/clients/types.ts new file mode 100644 index 00000000000..c330b5fdeb4 --- /dev/null +++ b/packages/network-controller/src/clients/types.ts @@ -0,0 +1,7 @@ +import { PollingBlockTracker } from 'eth-block-tracker'; +import { JsonRpcMiddleware } from 'json-rpc-engine'; + +export type CreateClientResult = { + networkMiddleware: JsonRpcMiddleware; + blockTracker: PollingBlockTracker; +};