From dacd89b2069b5123546919fe336081f732d495cd Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Mon, 25 Sep 2023 13:33:48 +0100 Subject: [PATCH 1/3] Remove dormant proposed names (#1688) Make updateProposeNames remove any proposed names for any source IDs that are no longer used by any of the providers for the specified type. Update the SetNameRequest type and associated validation to support removing a saved name using a null value. --- .../src/NameController.test.ts | 119 +++++++++++++++++- .../name-controller/src/NameController.ts | 51 ++++++-- 2 files changed, 159 insertions(+), 11 deletions(-) diff --git a/packages/name-controller/src/NameController.test.ts b/packages/name-controller/src/NameController.test.ts index e5ac9dc1ef..de557e1449 100644 --- a/packages/name-controller/src/NameController.test.ts +++ b/packages/name-controller/src/NameController.test.ts @@ -227,6 +227,51 @@ describe('NameController', () => { }); }); + it('can clear saved name', () => { + const provider1 = createMockProvider(1); + + const controller = new NameController({ + ...CONTROLLER_ARGS_MOCK, + providers: [provider1], + }); + + controller.state.names = { + [NameType.ETHEREUM_ADDRESS]: { + [VALUE_MOCK]: { + [CHAIN_ID_MOCK]: { + name: NAME_MOCK, + sourceId: SOURCE_ID_MOCK, + proposedNamesLastUpdated: null, + proposedNames: { + [SOURCE_ID_MOCK]: [PROPOSED_NAME_MOCK, PROPOSED_NAME_2_MOCK], + }, + }, + }, + }, + }; + + controller.setName({ + value: VALUE_MOCK, + type: NameType.ETHEREUM_ADDRESS, + name: null, + }); + + expect(controller.state.names).toStrictEqual({ + [NameType.ETHEREUM_ADDRESS]: { + [VALUE_MOCK]: { + [CHAIN_ID_MOCK]: { + name: null, + sourceId: null, + proposedNamesLastUpdated: null, + proposedNames: { + [SOURCE_ID_MOCK]: [PROPOSED_NAME_MOCK, PROPOSED_NAME_2_MOCK], + }, + }, + }, + }, + }); + }); + describe('throws if', () => { it.each([ ['missing', undefined], @@ -278,7 +323,7 @@ describe('NameController', () => { type: NameType.ETHEREUM_ADDRESS, name, } as any), - ).toThrow('Must specify a non-empty string for name.'); + ).toThrow('Must specify a non-empty string or null for name.'); }); it.each([ @@ -311,6 +356,21 @@ describe('NameController', () => { `Unknown source ID for type '${NameType.ETHEREUM_ADDRESS}': ${SOURCE_ID_MOCK}`, ); }); + + it('source ID is set but name is being cleared', () => { + const controller = new NameController(CONTROLLER_ARGS_MOCK); + + expect(() => + controller.setName({ + value: VALUE_MOCK, + type: NameType.ETHEREUM_ADDRESS, + name: null, + sourceId: SOURCE_ID_MOCK, + } as any), + ).toThrow( + `Cannot specify a source ID when clearing the saved name: ${SOURCE_ID_MOCK}`, + ); + }); }); }); @@ -398,7 +458,6 @@ describe('NameController', () => { proposedNames: { [`${SOURCE_ID_MOCK}1`]: ['ShouldBeDeleted1'], [`${SOURCE_ID_MOCK}2`]: ['ShouldBeDeleted2'], - [`${SOURCE_ID_MOCK}3`]: ['ShouldNotBeDeleted3'], }, }, }, @@ -426,7 +485,6 @@ describe('NameController', () => { `${PROPOSED_NAME_MOCK}2`, `${PROPOSED_NAME_MOCK}2_2`, ], - [`${SOURCE_ID_MOCK}3`]: ['ShouldNotBeDeleted3'], }, }, }, @@ -453,6 +511,58 @@ describe('NameController', () => { }); }); + it('removes proposed names if source ID not used by any provider', async () => { + const provider1 = createMockProvider(1); + const provider2 = createMockProvider(2); + + const controller = new NameController({ + ...CONTROLLER_ARGS_MOCK, + providers: [provider1, provider2], + }); + + controller.state.names = { + [NameType.ETHEREUM_ADDRESS]: { + [VALUE_MOCK]: { + [CHAIN_ID_MOCK]: { + name: null, + sourceId: null, + proposedNamesLastUpdated: 12, + proposedNames: { + [`${SOURCE_ID_MOCK}3`]: ['ShouldBeDeleted3'], + }, + }, + }, + }, + }; + + await controller.updateProposedNames({ + value: VALUE_MOCK, + type: NameType.ETHEREUM_ADDRESS, + }); + + expect(controller.state.names).toStrictEqual({ + [NameType.ETHEREUM_ADDRESS]: { + [VALUE_MOCK]: { + [CHAIN_ID_MOCK]: { + name: null, + sourceId: null, + proposedNamesLastUpdated: TIME_MOCK, + proposedNames: { + [`${SOURCE_ID_MOCK}1`]: [ + `${PROPOSED_NAME_MOCK}1`, + `${PROPOSED_NAME_MOCK}1_2`, + ], + [`${SOURCE_ID_MOCK}2`]: [ + `${PROPOSED_NAME_MOCK}2`, + `${PROPOSED_NAME_MOCK}2_2`, + ], + }, + }, + }, + }, + }); + }); + it.each([ ['undefined', undefined], ['empty string', ''], @@ -848,10 +958,11 @@ describe('NameController', () => { it('updates entry using matching providers only', async () => { const provider1 = createMockProvider(1); const provider2 = createMockProvider(2); + const provider3 = createMockProvider(3); const controller = new NameController({ ...CONTROLLER_ARGS_MOCK, - providers: [provider1, provider2], + providers: [provider1, provider2, provider3], }); controller.state.names = { diff --git a/packages/name-controller/src/NameController.ts b/packages/name-controller/src/NameController.ts index 598a762958..f4475a178b 100644 --- a/packages/name-controller/src/NameController.ts +++ b/packages/name-controller/src/NameController.ts @@ -82,7 +82,7 @@ export type UpdateProposedNamesResult = { export type SetNameRequest = { value: string; type: NameType; - name: string; + name: string | null; sourceId?: string; }; @@ -136,9 +136,10 @@ export class NameController extends BaseControllerV2< setName(request: SetNameRequest) { this.#validateSetNameRequest(request); - const { value, type, name, sourceId } = request; + const { value, type, name, sourceId: requestSourceId } = request; + const sourceId = requestSourceId ?? null; - this.#updateEntry(value, type, { name, sourceId: sourceId ?? null }); + this.#updateEntry(value, type, { name, sourceId }); } /** @@ -205,8 +206,11 @@ export class NameController extends BaseControllerV2< const existingProposedNames = this.state.names[type]?.[value]?.[variationKey]?.proposedNames; + const existingProposedNamesWithoutDormant = + this.#removeDormantProposedNames(existingProposedNames, type); + const proposedNames = { - ...existingProposedNames, + ...existingProposedNamesWithoutDormant, ...newProposedNames, }; @@ -370,7 +374,7 @@ export class NameController extends BaseControllerV2< this.#validateValue(value, errorMessages); this.#validateType(type, errorMessages); this.#validateName(name, errorMessages); - this.#validateSourceId(sourceId, type, errorMessages); + this.#validateSourceId(sourceId, type, name, errorMessages); if (errorMessages.length) { throw new Error(errorMessages.join(' ')); @@ -407,9 +411,13 @@ export class NameController extends BaseControllerV2< } } - #validateName(name: string, errorMessages: string[]) { + #validateName(name: string | null, errorMessages: string[]) { + if (name === null) { + return; + } + if (!name?.length || typeof name !== 'string') { - errorMessages.push('Must specify a non-empty string for name.'); + errorMessages.push('Must specify a non-empty string or null for name.'); } } @@ -442,12 +450,20 @@ export class NameController extends BaseControllerV2< #validateSourceId( sourceId: string | undefined, type: NameType, + name: string | null, errorMessages: string[], ) { if (sourceId === null || sourceId === undefined) { return; } + if (name === null) { + errorMessages.push( + `Cannot specify a source ID when clearing the saved name: ${sourceId}`, + ); + return; + } + const allSourceIds = this.#getAllSourceIds(type); if (!sourceId.length || typeof sourceId !== 'string') { @@ -488,4 +504,25 @@ export class NameController extends BaseControllerV2< #getSourceIds(provider: NameProvider, type: NameType): string[] { return provider.getMetadata().sourceIds[type]; } + + #removeDormantProposedNames( + proposedNames: Record, + type: NameType, + ): Record { + if (!proposedNames || Object.keys(proposedNames).length === 0) { + return proposedNames; + } + + const typeSourceIds = this.#getAllSourceIds(type); + + return Object.keys(proposedNames) + .filter((sourceId) => typeSourceIds.includes(sourceId)) + .reduce( + (acc, sourceId) => ({ + ...acc, + [sourceId]: proposedNames[sourceId], + }), + {}, + ); + } } From 1d71da301031c9b81f441f6b76b9d31109df106b Mon Sep 17 00:00:00 2001 From: Michele Esposito <34438276+mikesposito@users.noreply.github.com> Date: Mon, 25 Sep 2023 17:23:34 +0200 Subject: [PATCH 2/3] Subscribe to `QRKeyring` store and publish updates (#1702) ## Explanation Currently, the extension is directly subscribing to the `QRKeyring` store, to determine whether to open the modal for syncing and signing. This raises concerns when the instance of the QRKeyring will be held by `KeyringController` instead of the client because it will be tricky to handle `subscribe` and `unsubscribe` in the keyring's lifecycle, risking to have multiple subscriptions to the same QRKeyring instance or even no subscriptions at all! This PR gives this responsibility to `KeyringController`. `KeyringController` will subscribe to the QRKeyring instance in these methods: - `submitPassword` - because here we unlock the vault and potentially find a QRKeyring (even in case of re-construction from backup) - `submitEncryptionKey` - same as above - `getOrAddQRKeyring` - because here we explicitly create a new QRKeyring if it doesn't exist - `addNewKeyring(type)` - when `type` is of a QRKeyring then it will return `getOrAddQRKeyring`, to avoid creating multiple keyring instances (and multiple subscriptions) `KeyringController` will unsubscribe from the `QRKeyring` instance (if there's one ) when calling `setLocked()`. Moreover, `KeyringController:qrKeyringStateChange` event is now available on the messenger ## References * Related to https://github.com/MetaMask/metamask-extension/issues/18776 * Related to https://github.com/MetaMask/metamask-extension/pull/20502 ## Changelog ### `@metamask/keyring-controller` - **BREAKING**: `addNewKeyring(type)` return type changed from `Promise>` to `Promise` - When calling with QRKeyring `type` the keyring instance is retrieved or created (no multiple QRKeyring instances possible) - **ADDED**: Add `getQRKeyring(): QRKeyring | undefined` method - **ADDED**: Add `KeyringController:qrKeyringStateChange` messenger event The event emits updates from the internal `QRKeyring` instance, if there's one ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- .../src/KeyringController.test.ts | 142 +++++++++++++++++- .../src/KeyringController.ts | 95 ++++++++++-- 2 files changed, 219 insertions(+), 18 deletions(-) diff --git a/packages/keyring-controller/src/KeyringController.test.ts b/packages/keyring-controller/src/KeyringController.test.ts index b34613c1bc..66a121abc2 100644 --- a/packages/keyring-controller/src/KeyringController.test.ts +++ b/packages/keyring-controller/src/KeyringController.test.ts @@ -188,9 +188,9 @@ describe('KeyringController', () => { ], }, async ({ controller, initialState, preferences }) => { - const mockKeyring = await controller.addNewKeyring( + const mockKeyring = (await controller.addNewKeyring( MockShallowGetAccountsKeyring.type, - ); + )) as Keyring; const addedAccountAddress = await controller.addNewAccountForKeyring(mockKeyring); @@ -1490,6 +1490,7 @@ describe('KeyringController', () => { }; let signProcessKeyringController: KeyringController; + let signProcessKeyringControllerMessenger: KeyringControllerMessenger; let requestSignatureStub: sinon.SinonStub; let readAccountSub: sinon.SinonStub; @@ -1510,11 +1511,18 @@ describe('KeyringController', () => { }; beforeEach(async () => { - signProcessKeyringController = await withController( - // @ts-expect-error QRKeyring is not yet compatible with Keyring type. - { keyringBuilders: [keyringBuilderFactory(QRKeyring)] }, - ({ controller }) => controller, + const { controller, messenger } = await withController( + { + // @ts-expect-error QRKeyring is not yet compatible with Keyring type. + keyringBuilders: [keyringBuilderFactory(QRKeyring)], + cacheEncryptionKey: true, + }, + (args) => args, ); + + signProcessKeyringController = controller; + signProcessKeyringControllerMessenger = messenger; + const qrkeyring = await signProcessKeyringController.getOrAddQRKeyring(); qrkeyring.forgetDevice(); @@ -1529,6 +1537,21 @@ describe('KeyringController', () => { ); }); + describe('getQRKeyring', () => { + it('should return QR keyring', async () => { + const qrKeyring = signProcessKeyringController.getQRKeyring(); + expect(qrKeyring).toBeDefined(); + expect(qrKeyring).toBeInstanceOf(QRKeyring); + }); + + it('should return undefined if QR keyring is not present', async () => { + await withController(async ({ controller }) => { + const qrKeyring = controller.getQRKeyring(); + expect(qrKeyring).toBeUndefined(); + }); + }); + }); + describe('connectQRHardware', () => { it('should setup QR keyring with crypto-hdkey', async () => { readAccountSub.resolves( @@ -1935,6 +1958,112 @@ describe('KeyringController', () => { expect(cancelSyncRequestStub.called).toBe(true); }); }); + + describe('QRKeyring store events', () => { + describe('KeyringController:qrKeyringStateChange', () => { + it('should emit KeyringController:qrKeyringStateChange event after `getOrAddQRKeyring()`', async () => { + const listener = jest.fn(); + signProcessKeyringControllerMessenger.subscribe( + 'KeyringController:qrKeyringStateChange', + listener, + ); + const qrKeyring = + await signProcessKeyringController.getOrAddQRKeyring(); + + qrKeyring.getMemStore().updateState({ + sync: { + reading: true, + }, + }); + + expect(listener).toHaveBeenCalledTimes(1); + }); + + it('should emit KeyringController:qrKeyringStateChange after `submitPassword()`', async () => { + const listener = jest.fn(); + signProcessKeyringControllerMessenger.subscribe( + 'KeyringController:qrKeyringStateChange', + listener, + ); + // We ensure there is a QRKeyring before locking + await signProcessKeyringController.getOrAddQRKeyring(); + // Locking the keyring will dereference the QRKeyring + await signProcessKeyringController.setLocked(); + // ..and unlocking it should add a new instance of QRKeyring + await signProcessKeyringController.submitPassword(password); + // We call `getQRKeyring` instead of `getOrAddQRKeyring` so that + // we are able to test if the subscription to the internal QR keyring + // was made while unlocking the keyring. + const qrKeyring = signProcessKeyringController.getQRKeyring(); + + // As we added a QR keyring before lock/unlock, this must be defined + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + qrKeyring!.getMemStore().updateState({ + sync: { + reading: true, + }, + }); + + // Only one call ensures that the first subscription made by + // QR keyring before locking was removed + expect(listener).toHaveBeenCalledTimes(1); + }); + + it('should emit KeyringController:qrKeyringStateChange after `submitEncryptionKey()`', async () => { + const listener = jest.fn(); + signProcessKeyringControllerMessenger.subscribe( + 'KeyringController:qrKeyringStateChange', + listener, + ); + const salt = signProcessKeyringController.state + .encryptionSalt as string; + // We ensure there is a QRKeyring before locking + await signProcessKeyringController.getOrAddQRKeyring(); + // Locking the keyring will dereference the QRKeyring + await signProcessKeyringController.setLocked(); + // ..and unlocking it should add a new instance of QRKeyring + await signProcessKeyringController.submitEncryptionKey( + mockKey.toString('hex'), + salt, + ); + // We call `getQRKeyring` instead of `getOrAddQRKeyring` so that + // we are able to test if the subscription to the internal QR keyring + // was made while unlocking the keyring. + const qrKeyring = signProcessKeyringController.getQRKeyring(); + + // As we added a QR keyring before lock/unlock, this must be defined + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + qrKeyring!.getMemStore().updateState({ + sync: { + reading: true, + }, + }); + + // Only one call ensures that the first subscription made by + // QR keyring before locking was removed + expect(listener).toHaveBeenCalledTimes(1); + }); + + it('should emit KeyringController:qrKeyringStateChange after `addNewKeyring()`', async () => { + const listener = jest.fn(); + signProcessKeyringControllerMessenger.subscribe( + 'KeyringController:qrKeyringStateChange', + listener, + ); + const qrKeyring = (await signProcessKeyringController.addNewKeyring( + KeyringTypes.qr, + )) as QRKeyring; + + qrKeyring.getMemStore().updateState({ + sync: { + reading: true, + }, + }); + + expect(listener).toHaveBeenCalledTimes(1); + }); + }); + }); }); describe('actions', () => { @@ -2181,6 +2310,7 @@ function buildKeyringControllerMessenger(messenger = buildMessenger()) { 'KeyringController:lock', 'KeyringController:unlock', 'KeyringController:accountRemoved', + 'KeyringController:qrKeyringStateChange', ], }); } diff --git a/packages/keyring-controller/src/KeyringController.ts b/packages/keyring-controller/src/KeyringController.ts index e285b111b0..7ce48bc0ba 100644 --- a/packages/keyring-controller/src/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -1,7 +1,7 @@ import type { TxData, TypedTransaction } from '@ethereumjs/tx'; -import { - type MetaMaskKeyring as QRKeyring, - type IKeyringState as IQRKeyringState, +import type { + MetaMaskKeyring as QRKeyring, + IKeyringState as IQRKeyringState, } from '@keystonehq/metamask-airgapped-keyring'; import type { RestrictedControllerMessenger } from '@metamask/base-controller'; import { BaseControllerV2 } from '@metamask/base-controller'; @@ -125,6 +125,11 @@ export type KeyringControllerUnlockEvent = { payload: []; }; +export type KeyringControllerQRKeyringStateChangeEvent = { + type: `${typeof name}:qrKeyringStateChange`; + payload: [ReturnType]; +}; + export type KeyringControllerActions = | KeyringControllerGetStateAction | KeyringControllerSignMessageAction @@ -140,7 +145,8 @@ export type KeyringControllerEvents = | KeyringControllerStateChangeEvent | KeyringControllerLockEvent | KeyringControllerUnlockEvent - | KeyringControllerAccountRemovedEvent; + | KeyringControllerAccountRemovedEvent + | KeyringControllerQRKeyringStateChangeEvent; export type KeyringControllerMessenger = RestrictedControllerMessenger< typeof name, @@ -246,6 +252,10 @@ export class KeyringController extends BaseControllerV2< #keyring: EthKeyringController; + #qrKeyringStateListener?: ( + state: ReturnType, + ) => void; + /** * Creates a KeyringController instance. * @@ -467,7 +477,11 @@ export class KeyringController extends BaseControllerV2< async addNewKeyring( type: KeyringTypes | string, opts?: unknown, - ): Promise> { + ): Promise { + if (type === KeyringTypes.qr) { + return this.getOrAddQRKeyring(); + } + return this.#keyring.addNewKeyring(type, opts); } @@ -678,6 +692,7 @@ export class KeyringController extends BaseControllerV2< * @returns Promise resolving to current state. */ async setLocked(): Promise { + this.#unsubscribeFromQRKeyringsEvents(); await this.#keyring.setLocked(); return this.#getMemState(); } @@ -771,6 +786,14 @@ export class KeyringController extends BaseControllerV2< encryptionSalt: string, ): Promise { await this.#keyring.submitEncryptionKey(encryptionKey, encryptionSalt); + + const qrKeyring = this.getQRKeyring(); + if (qrKeyring) { + // if there is a QR keyring, we need to subscribe + // to its events after unlocking the vault + this.#subscribeToQRKeyringEvents(qrKeyring); + } + return this.#getMemState(); } @@ -784,6 +807,14 @@ export class KeyringController extends BaseControllerV2< async submitPassword(password: string): Promise { await this.#keyring.submitPassword(password); const accounts = await this.#keyring.getAccounts(); + + const qrKeyring = this.getQRKeyring(); + if (qrKeyring) { + // if there is a QR keyring, we need to subscribe + // to its events after unlocking the vault + this.#subscribeToQRKeyringEvents(qrKeyring); + } + await this.syncIdentities(accounts); return this.#getMemState(); } @@ -841,16 +872,24 @@ export class KeyringController extends BaseControllerV2< // QR Hardware related methods /** - * Get qr hardware keyring. + * Get QR Hardware keyring. + * + * @returns The QR Keyring if defined, otherwise undefined + */ + getQRKeyring(): QRKeyring | undefined { + // QRKeyring is not yet compatible with Keyring type from @metamask/utils + return this.#keyring.getKeyringsByType( + KeyringTypes.qr, + )[0] as unknown as QRKeyring; + } + + /** + * Get QR hardware keyring. If it doesn't exist, add it. * * @returns The added keyring */ async getOrAddQRKeyring(): Promise { - const keyring = - (this.#keyring.getKeyringsByType( - KeyringTypes.qr, - )[0] as unknown as QRKeyring) || (await this.#addQRKeyring()); - return keyring; + return this.getQRKeyring() || (await this.#addQRKeyring()); } async restoreQRKeyring(serialized: any): Promise { @@ -1015,7 +1054,39 @@ export class KeyringController extends BaseControllerV2< */ async #addQRKeyring(): Promise { // QRKeyring is not yet compatible with Keyring type from @metamask/utils - return this.#keyring.addNewKeyring(KeyringTypes.qr) as unknown as QRKeyring; + const qrKeyring = (await this.#keyring.addNewKeyring( + KeyringTypes.qr, + )) as unknown as QRKeyring; + + this.#subscribeToQRKeyringEvents(qrKeyring); + + return qrKeyring; + } + + /** + * Subscribe to a QRKeyring state change events and + * forward them through the messaging system. + * + * @param qrKeyring - The QRKeyring instance to subscribe to + */ + #subscribeToQRKeyringEvents(qrKeyring: QRKeyring) { + this.#qrKeyringStateListener = (state) => { + this.messagingSystem.publish(`${name}:qrKeyringStateChange`, state); + }; + + qrKeyring.getMemStore().subscribe(this.#qrKeyringStateListener); + } + + #unsubscribeFromQRKeyringsEvents() { + const qrKeyrings = this.#keyring.getKeyringsByType( + KeyringTypes.qr, + ) as unknown as QRKeyring[]; + + qrKeyrings.forEach((qrKeyring) => { + if (this.#qrKeyringStateListener) { + qrKeyring.getMemStore().unsubscribe(this.#qrKeyringStateListener); + } + }); } /** From c06ea78b91c70181184ddb0c36b0d3b42bc0cb3a Mon Sep 17 00:00:00 2001 From: jiexi Date: Mon, 25 Sep 2023 11:07:40 -0700 Subject: [PATCH 3/3] Remove `networkId` from NetworkController (#1633) ## Explanation Wallets shouldn't be directly concerned about the network ID as this is more of a node concept for gossip. What wallets really care about is chain ID as that is the correct value to use to identify a chain, build transactions, etc. Although these two values usually match (ignoring hex/dec formatting), there are [exceptions](https://medium.com/@pedrouid/chainid-vs-networkid-how-do-they-differ-on-ethereum-eec2ed41635b). We want to remove `networkId` from the NetworkController state to encourage the replacement of networkId with chainId in any usage downstream (extension, mobile, etc). It will still be possible to get networkId using the rpc client to make a call to the `net_version` method for cases that truly still rely on it. ## References * Fixes [mmp-1068](https://github.com/MetaMask/MetaMask-planning/issues/1068) ## Changelog ### `@metamask/controller-utils` - **BREAKING**: `NETWORK_ID_TO_ETHERS_NETWORK_NAME_MAP` renamed to `CHAIN_ID_TO_ETHERS_NETWORK_NAME_MAP ` and is now a mapping of `Hex` chain ID to `BuiltInNetworkName` - **REMOVED**: `NetworkId` constant and type ### `@metamask/ens-controller` - **CHANGED**: From Network state, uses `providerConfig.chainId` instead of `networkId` to determine ENS compatability ### `@metamask/network-controller` - **REMOVED**: `NetworkId` type - **REMOVED**: From `NetworkState` removed `networkId` field - **CHANGED**: No longer calls `net_version` to determine network status (only relies on `eth_getBlockByNumber` now) - **CHANGED**: For Built-in Infura Networks, `net_version` is no longer locally resolved by the scaffold middleware ### `@metamask/transaction-controller` - **BREAKING**: Uses `chainId` and `txParams.chainId` to determine if a `TransactionMeta` object belongs to the current chain. No longer considers `networkID` - **BREAKING**: `TransactionMeta.chainId` is now a required field - **REMOVED**: From `RemoteTransactionSourceRequest` removed `currentNetworkId` field - **CHANGED**: From `RemoteTransactionSource` updated `isSupportedNetwork()` params to exclude networkId - **DEPRECATED**: `TransactionMeta.networkID` is deprecated and marked readonly --- .../src/NftDetectionController.ts | 3 +- .../src/TokenListController.test.ts | 1 - packages/controller-utils/src/constants.ts | 24 +- packages/controller-utils/src/types.ts | 12 - .../ens-controller/src/EnsController.test.ts | 12 +- packages/ens-controller/src/EnsController.ts | 49 +- .../src/GasFeeController.test.ts | 4 +- .../src/NetworkController.ts | 81 +- .../src/create-network-client.ts | 5 +- .../tests/NetworkController.test.ts | 8982 +++++++---------- .../tests/provider-api-tests/shared-tests.ts | 47 +- .../tests/SelectedNetworkMiddleware.test.ts | 8 +- .../transaction-controller/jest.config.js | 6 +- .../EtherscanRemoteTransactionSource.test.ts | 4 +- .../src/EtherscanRemoteTransactionSource.ts | 27 +- .../src/IncomingTransactionHelper.test.ts | 2 - .../src/IncomingTransactionHelper.ts | 13 +- .../src/TransactionController.test.ts | 69 +- .../src/TransactionController.ts | 60 +- .../transaction-controller/src/constants.ts | 19 - packages/transaction-controller/src/types.ts | 16 +- .../transaction-controller/src/utils.test.ts | 84 +- packages/transaction-controller/src/utils.ts | 25 - 23 files changed, 3501 insertions(+), 6052 deletions(-) diff --git a/packages/assets-controllers/src/NftDetectionController.ts b/packages/assets-controllers/src/NftDetectionController.ts index ac9b333100..561446e401 100644 --- a/packages/assets-controllers/src/NftDetectionController.ts +++ b/packages/assets-controllers/src/NftDetectionController.ts @@ -115,9 +115,8 @@ export interface ApiNftCreator { * * NftDetection configuration * @property interval - Polling interval used to fetch new token rates - * @property networkType - Network type ID as per net_version + * @property chainId - Current chain ID * @property selectedAddress - Vault selected address - * @property tokens - List of tokens associated with the active vault */ export interface NftDetectionConfig extends BaseConfig { interval: number; diff --git a/packages/assets-controllers/src/TokenListController.test.ts b/packages/assets-controllers/src/TokenListController.test.ts index a28fc8bcb6..4b44f1961d 100644 --- a/packages/assets-controllers/src/TokenListController.test.ts +++ b/packages/assets-controllers/src/TokenListController.test.ts @@ -519,7 +519,6 @@ function buildNetworkControllerStateWithProviderConfig( return { selectedNetworkClientId, providerConfig, - networkId: '1', networksMetadata: { [selectedNetworkClientId]: { EIPS: {}, diff --git a/packages/controller-utils/src/constants.ts b/packages/controller-utils/src/constants.ts index ca19716d9a..b6a1e9ae7f 100644 --- a/packages/controller-utils/src/constants.ts +++ b/packages/controller-utils/src/constants.ts @@ -1,4 +1,9 @@ -import { NetworkType, NetworksTicker, ChainId, NetworkId } from './types'; +import { + NetworkType, + NetworksTicker, + ChainId, + BuiltInNetworkName, +} from './types'; export const RPC = 'rpc'; export const FALL_BACK_VS_CURRENCY = 'ETH'; @@ -125,13 +130,14 @@ export enum ApprovalType { WatchAsset = 'wallet_watchAsset', } -export const NETWORK_ID_TO_ETHERS_NETWORK_NAME_MAP: Record< - NetworkId, - NetworkType +export const CHAIN_ID_TO_ETHERS_NETWORK_NAME_MAP: Record< + ChainId, + BuiltInNetworkName > = { - [NetworkId.goerli]: NetworkType.goerli, - [NetworkId.sepolia]: NetworkType.sepolia, - [NetworkId.mainnet]: NetworkType.mainnet, - [NetworkId['linea-goerli']]: NetworkType['linea-goerli'], - [NetworkId['linea-mainnet']]: NetworkType['linea-mainnet'], + [ChainId.goerli]: BuiltInNetworkName.Goerli, + [ChainId.sepolia]: BuiltInNetworkName.Sepolia, + [ChainId.mainnet]: BuiltInNetworkName.Mainnet, + [ChainId['linea-goerli']]: BuiltInNetworkName.LineaGoerli, + [ChainId['linea-mainnet']]: BuiltInNetworkName.LineaMainnet, + [ChainId.aurora]: BuiltInNetworkName.Aurora, }; diff --git a/packages/controller-utils/src/types.ts b/packages/controller-utils/src/types.ts index 9567a36204..3c66003118 100644 --- a/packages/controller-utils/src/types.ts +++ b/packages/controller-utils/src/types.ts @@ -61,18 +61,6 @@ export const ChainId = { } as const; export type ChainId = (typeof ChainId)[keyof typeof ChainId]; -/** - * Decimal string network IDs of built-in Infura networks, by name. - */ -export const NetworkId = { - [InfuraNetworkType.mainnet]: '1', - [InfuraNetworkType.goerli]: '5', - [InfuraNetworkType.sepolia]: '11155111', - [InfuraNetworkType['linea-goerli']]: '59140', - [InfuraNetworkType['linea-mainnet']]: '59144', -} as const; -export type NetworkId = (typeof NetworkId)[keyof typeof NetworkId]; - export enum NetworksTicker { mainnet = 'ETH', goerli = 'GoerliETH', diff --git a/packages/ens-controller/src/EnsController.test.ts b/packages/ens-controller/src/EnsController.test.ts index cc74a3a5a9..8cf7332919 100644 --- a/packages/ens-controller/src/EnsController.test.ts +++ b/packages/ens-controller/src/EnsController.test.ts @@ -119,7 +119,6 @@ describe('EnsController', () => { provider: getProvider(), onNetworkStateChange: (listener) => { listener({ - networkId: '1', providerConfig: { chainId: toHex(1), type: NetworkType.mainnet, @@ -445,7 +444,6 @@ describe('EnsController', () => { provider: getProvider(), onNetworkStateChange: (listener) => { listener({ - networkId: null, providerConfig: { chainId: toHex(1), type: NetworkType.mainnet, @@ -464,9 +462,8 @@ describe('EnsController', () => { provider: getProvider(), onNetworkStateChange: (listener) => { listener({ - networkId: '1544', providerConfig: { - chainId: toHex(1), + chainId: toHex(0), type: NetworkType.mainnet, ticker: NetworksTicker.mainnet, }, @@ -490,7 +487,6 @@ describe('EnsController', () => { provider: getProvider(), onNetworkStateChange: (listener) => { listener({ - networkId: '1', providerConfig: { chainId: toHex(1), type: NetworkType.mainnet, @@ -514,7 +510,6 @@ describe('EnsController', () => { provider: getProvider(), onNetworkStateChange: (listener) => { listener({ - networkId: '1', providerConfig: { chainId: toHex(1), type: NetworkType.mainnet, @@ -537,7 +532,6 @@ describe('EnsController', () => { provider: getProvider(), onNetworkStateChange: (listener) => { listener({ - networkId: '1', providerConfig: { chainId: toHex(1), type: NetworkType.mainnet, @@ -563,7 +557,6 @@ describe('EnsController', () => { provider: getProvider(), onNetworkStateChange: (listener) => { listener({ - networkId: '1', providerConfig: { chainId: toHex(1), type: NetworkType.mainnet, @@ -589,7 +582,6 @@ describe('EnsController', () => { provider: getProvider(), onNetworkStateChange: (listener) => { listener({ - networkId: '1', providerConfig: { chainId: toHex(1), type: NetworkType.mainnet, @@ -617,7 +609,6 @@ describe('EnsController', () => { provider: getProvider(), onNetworkStateChange: (listener) => { listener({ - networkId: '1', providerConfig: { chainId: toHex(1), type: NetworkType.mainnet, @@ -644,7 +635,6 @@ describe('EnsController', () => { provider: getProvider(), onNetworkStateChange: (listener) => { listener({ - networkId: '1', providerConfig: { chainId: toHex(1), type: NetworkType.mainnet, diff --git a/packages/ens-controller/src/EnsController.ts b/packages/ens-controller/src/EnsController.ts index 16ec25260e..e65ebcd20f 100644 --- a/packages/ens-controller/src/EnsController.ts +++ b/packages/ens-controller/src/EnsController.ts @@ -5,36 +5,22 @@ import type { import { Web3Provider } from '@ethersproject/providers'; import type { RestrictedControllerMessenger } from '@metamask/base-controller'; import { BaseControllerV2 } from '@metamask/base-controller'; +import type { ChainId } from '@metamask/controller-utils'; import { normalizeEnsName, isValidHexAddress, toChecksumHexAddress, - NETWORK_ID_TO_ETHERS_NETWORK_NAME_MAP, + CHAIN_ID_TO_ETHERS_NETWORK_NAME_MAP, convertHexToDecimal, } from '@metamask/controller-utils'; import type { NetworkState } from '@metamask/network-controller'; import type { Hex } from '@metamask/utils'; -import { createProjectLogger, hasProperty } from '@metamask/utils'; +import { createProjectLogger } from '@metamask/utils'; import ensNetworkMap from 'ethereum-ens-network-map'; import { toASCII } from 'punycode/'; const log = createProjectLogger('ens-controller'); -/** - * Checks whether the given string is a known network ID. - * - * @param networkId - Network id. - * @returns Boolean indicating if the network ID is recognized. - */ -function isKnownNetworkId( - networkId: string | null, -): networkId is keyof typeof NETWORK_ID_TO_ETHERS_NETWORK_NAME_MAP { - return ( - networkId !== null && - hasProperty(NETWORK_ID_TO_ETHERS_NETWORK_NAME_MAP, networkId) - ); -} - const name = 'EnsController'; /** @@ -118,9 +104,7 @@ export class EnsController extends BaseControllerV2< state?: Partial; provider?: ExternalProvider | JsonRpcFetchFunc; onNetworkStateChange?: ( - listener: ( - networkState: Pick, - ) => void, + listener: (networkState: Pick) => void, ) => void; }) { super({ @@ -136,15 +120,14 @@ export class EnsController extends BaseControllerV2< if (provider && onNetworkStateChange) { onNetworkStateChange((networkState) => { this.resetState(); - const currentNetwork = networkState.networkId; - if ( - isKnownNetworkId(currentNetwork) && - this.#getNetworkEnsSupport(currentNetwork) - ) { + const currentChainId = networkState.providerConfig.chainId; + if (this.#getChainEnsSupport(currentChainId)) { this.#ethProvider = new Web3Provider(provider, { - chainId: convertHexToDecimal(networkState.providerConfig.chainId), - name: NETWORK_ID_TO_ETHERS_NETWORK_NAME_MAP[currentNetwork], - ensAddress: ensNetworkMap[currentNetwork], + chainId: convertHexToDecimal(currentChainId), + name: CHAIN_ID_TO_ETHERS_NETWORK_NAME_MAP[ + currentChainId as ChainId + ], + ensAddress: ensNetworkMap[parseInt(currentChainId, 16)], }); } else { this.#ethProvider = null; @@ -269,13 +252,13 @@ export class EnsController extends BaseControllerV2< } /** - * Check if network supports ENS. + * Check if the chain supports ENS. * - * @param networkId - Network id. - * @returns Boolean indicating if the network supports ENS. + * @param chainId - chain id. + * @returns Boolean indicating if the chain supports ENS. */ - #getNetworkEnsSupport(networkId: string) { - return Boolean(ensNetworkMap[networkId]); + #getChainEnsSupport(chainId: string) { + return Boolean(ensNetworkMap[parseInt(chainId, 16)]); } /** diff --git a/packages/gas-fee-controller/src/GasFeeController.test.ts b/packages/gas-fee-controller/src/GasFeeController.test.ts index 77d23fcbee..866d8e8586 100644 --- a/packages/gas-fee-controller/src/GasFeeController.test.ts +++ b/packages/gas-fee-controller/src/GasFeeController.test.ts @@ -77,8 +77,8 @@ const setupNetworkController = async ({ // Call this without awaiting to simulate what the extension or mobile app // might do networkController.initializeProvider(); - // Ensure that the request for net_version that the network controller makes - // goes through + // Ensure that the request for eth_getBlockByNumber made by the PollingBlockTracker + // inside the NetworkController goes through await clock.nextAsync(); return networkController; diff --git a/packages/network-controller/src/NetworkController.ts b/packages/network-controller/src/NetworkController.ts index b835602a5a..c41ab8840e 100644 --- a/packages/network-controller/src/NetworkController.ts +++ b/packages/network-controller/src/NetworkController.ts @@ -2,7 +2,6 @@ import type { RestrictedControllerMessenger } from '@metamask/base-controller'; import { BaseControllerV2 } from '@metamask/base-controller'; import { BUILT_IN_NETWORKS, - convertHexToDecimal, NetworksTicker, ChainId, InfuraNetworkType, @@ -17,7 +16,6 @@ import { assertIsStrictHexString, hasProperty, isPlainObject, - isStrictHexString, } from '@metamask/utils'; import { strict as assert } from 'assert'; import { errorCodes } from 'eth-rpc-errors'; @@ -174,25 +172,6 @@ function pick, Keys extends keyof Obj>( return pickedObject; } -/** - * Convert the given value into a valid network ID. The ID is accepted - * as either a number, a decimal string, or a 0x-prefixed hex string. - * - * @param value - The network ID to convert, in an unknown format. - * @returns A valid network ID (as a decimal string) - * @throws If the given value cannot be safely parsed. - */ -function convertNetworkId(value: unknown): NetworkId { - if (typeof value === 'number' && !Number.isNaN(value)) { - return `${value}`; - } else if (isStrictHexString(value)) { - return `${convertHexToDecimal(value)}`; - } else if (typeof value === 'string' && /^\d+$/u.test(value)) { - return value as NetworkId; - } - throw new Error(`Cannot parse as a valid network ID: '${value}'`); -} - /** * Type guard for determining whether the given value is an error object with a * `code` property, such as an instance of Error. @@ -343,23 +322,16 @@ export type NetworksMetadata = { [networkClientId: NetworkClientId]: NetworkMetadata; }; -/** - * The network ID of a network. - */ -export type NetworkId = `${number}`; - /** * @type NetworkState * * Network controller state - * @property network - Network ID as per net_version of the currently connected network * @property providerConfig - RPC URL and network name provider settings of the currently connected network * @property properties - an additional set of network properties for the currently connected network * @property networkConfigurations - the full list of configured networks either preloaded or added by the user. */ export type NetworkState = { selectedNetworkClientId: NetworkClientId; - networkId: NetworkId | null; providerConfig: ProviderConfig; networkConfigurations: NetworkConfigurations; networksMetadata: NetworksMetadata; @@ -481,7 +453,6 @@ export type NetworkControllerOptions = { export const defaultState: NetworkState = { selectedNetworkClientId: NetworkType.mainnet, - networkId: null, providerConfig: { type: NetworkType.mainnet, chainId: ChainId.mainnet, @@ -568,10 +539,6 @@ export class NetworkController extends BaseControllerV2< persist: true, anonymous: false, }, - networkId: { - persist: true, - anonymous: false, - }, networksMetadata: { persist: true, anonymous: false, @@ -719,9 +686,6 @@ export class NetworkController extends BaseControllerV2< */ async #refreshNetwork() { this.messagingSystem.publish('NetworkController:networkWillChange'); - this.update((state) => { - state.networkId = null; - }); this.#applyNetworkSelection(); this.messagingSystem.publish('NetworkController:networkDidChange'); await this.lookupNetwork(); @@ -738,41 +702,6 @@ export class NetworkController extends BaseControllerV2< await this.lookupNetwork(); } - /** - * Fetches the network ID for the network, ensuring that it is a hex string. - * - * @param networkClientId - The ID of the network client to fetch the network - * @returns A promise that either resolves to the network ID, or rejects with - * an error. - * @throws If the network ID of the network is not a valid hex string. - */ - async #getNetworkId(networkClientId: NetworkClientId): Promise { - const possibleNetworkId = await new Promise((resolve, reject) => { - let ethQuery = this.#ethQuery; - if (networkClientId) { - const networkClient = this.getNetworkClientById(networkClientId); - ethQuery = new EthQuery(networkClient.provider); - } - if (!ethQuery) { - throw new Error('Provider has not been initialized'); - } - - ethQuery.sendAsync( - { method: 'net_version' }, - (error: unknown, result?: unknown) => { - if (error) { - reject(error); - } else { - // TODO: Validate this type - resolve(result as string); - } - }, - ); - }); - - return convertNetworkId(possibleNetworkId); - } - /** * Refreshes the network meta with EIP-1559 support and the network status * based on the given network client ID. @@ -884,16 +813,13 @@ export class NetworkController extends BaseControllerV2< ); let updatedNetworkStatus: NetworkStatus; - let updatedNetworkId: NetworkId | null = null; let updatedIsEIP1559Compatible: boolean | undefined; try { - const [networkId, isEIP1559Compatible] = await Promise.all([ - this.#getNetworkId(this.state.selectedNetworkClientId), - this.#determineEIP1559Compatibility(this.state.selectedNetworkClientId), - ]); + const isEIP1559Compatible = await this.#determineEIP1559Compatibility( + this.state.selectedNetworkClientId, + ); updatedNetworkStatus = NetworkStatus.Available; - updatedNetworkId = networkId; updatedIsEIP1559Compatible = isEIP1559Compatible; } catch (error) { if (isErrorWithCode(error)) { @@ -937,7 +863,6 @@ export class NetworkController extends BaseControllerV2< ); this.update((state) => { - state.networkId = updatedNetworkId; const meta = state.networksMetadata[state.selectedNetworkClientId]; meta.status = updatedNetworkStatus; if (updatedIsEIP1559Compatible === undefined) { diff --git a/packages/network-controller/src/create-network-client.ts b/packages/network-controller/src/create-network-client.ts index d0ed381636..0f8ddbaf37 100644 --- a/packages/network-controller/src/create-network-client.ts +++ b/packages/network-controller/src/create-network-client.ts @@ -1,5 +1,5 @@ import type { InfuraNetworkType } from '@metamask/controller-utils'; -import { ChainId, NetworkId } from '@metamask/controller-utils'; +import { ChainId } from '@metamask/controller-utils'; import { createInfuraMiddleware } from '@metamask/eth-json-rpc-infura'; import { createBlockCacheMiddleware, @@ -144,7 +144,7 @@ function createInfuraNetworkMiddleware({ * * @param args - The Arguments. * @param args.network - The Infura network to use. - * @returns The middleware that implements eth_chainId & net_version methods. + * @returns The middleware that implements the eth_chainId method. */ function createNetworkAndChainIdMiddleware({ network, @@ -153,7 +153,6 @@ function createNetworkAndChainIdMiddleware({ }) { return createScaffoldMiddleware({ eth_chainId: ChainId[network], - net_version: NetworkId[network], }); } diff --git a/packages/network-controller/tests/NetworkController.test.ts b/packages/network-controller/tests/NetworkController.test.ts index 31ea778d79..97b53f4c28 100644 --- a/packages/network-controller/tests/NetworkController.test.ts +++ b/packages/network-controller/tests/NetworkController.test.ts @@ -125,14 +125,6 @@ const SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE = { result: BLOCK, }; -/** - * A response object for a successful request to `net_version`. It is assumed - * that the network ID here is insignificant to the test. - */ -const SUCCESSFUL_NET_VERSION_RESPONSE = { - result: '42', -}; - /** * A response object for a request that has been geoblocked by Infura. */ @@ -181,7 +173,6 @@ describe('NetworkController', () => { expect(controller.state).toMatchInlineSnapshot(` Object { "networkConfigurations": Object {}, - "networkId": null, "networksMetadata": Object {}, "providerConfig": Object { "chainId": "0x1", @@ -217,7 +208,6 @@ describe('NetworkController', () => { expect(controller.state).toMatchInlineSnapshot(` Object { "networkConfigurations": Object {}, - "networkId": null, "networksMetadata": Object { "mainnet": Object { "EIPS": Object { @@ -2001,25 +1991,10 @@ describe('NetworkController', () => { }); }); - describe('if a provider has not been set', () => { - it('does not change network in state', async () => { - await withController(async ({ controller, messenger }) => { - const promiseForNetworkChanges = waitForStateChanges({ - messenger, - propertyPath: ['networkId'], - }); - - await controller.lookupNetwork(); - - await expect(promiseForNetworkChanges).toNeverResolve(); - }); - }); - }); - [NetworkType.mainnet, NetworkType.goerli, NetworkType.sepolia].forEach( (networkType) => { describe(`when the provider config in state contains a network type of "${networkType}"`, () => { - describe('if the network was switched after the net_version request started but before it completed', () => { + describe('if the network was switched after the eth_getBlockByNumber request started but before it completed', () => { it('stores the network status of the second network, not the first', async () => { await withController( { @@ -2040,12 +2015,6 @@ describe('NetworkController', () => { const fakeProviders = [ buildFakeProvider([ // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, { request: { method: 'eth_getBlockByNumber', @@ -2055,29 +2024,21 @@ describe('NetworkController', () => { // Called via `lookupNetwork` directly { request: { - method: 'net_version', + method: 'eth_getBlockByNumber', }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, + response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, beforeCompleting: () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request controller.setActiveNetwork( 'testNetworkConfigurationId', ); }, }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, ]), buildFakeProvider([ // Called when switching networks { request: { - method: 'net_version', + method: 'eth_getBlockByNumber', }, error: GENERIC_JSON_RPC_ERROR, }, @@ -2127,115 +2088,6 @@ describe('NetworkController', () => { ); }); - it('stores the ID of the second network, not the first', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ type: networkType }), - networkConfigurations: { - testNetworkConfigurationId: { - id: 'testNetworkConfigurationId', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'ABC', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller, messenger }) => { - const fakeProviders = [ - buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - beforeCompleting: async () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request - controller.setActiveNetwork( - 'testNetworkConfigurationId', - ); - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - ]), - buildFakeProvider([ - // Called when switching networks - { - request: { - method: 'net_version', - }, - response: { - result: '2', - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - chainId: BUILT_IN_NETWORKS[networkType].chainId, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[1]); - await waitForStateChanges({ - messenger, - propertyPath: ['networkId'], - operation: async () => { - await controller.initializeProvider(); - }, - }); - expect(controller.state.networkId).toBe('1'); - - await waitForStateChanges({ - messenger, - propertyPath: ['networkId'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - expect(controller.state.networkId).toBe('2'); - }, - ); - }); - it('stores the EIP-1559 support of the second network, not the first', async () => { await withController( { @@ -2256,14 +2108,6 @@ describe('NetworkController', () => { const fakeProviders = [ buildFakeProvider([ // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - }, { request: { method: 'eth_getBlockByNumber', @@ -2275,38 +2119,20 @@ describe('NetworkController', () => { // Called via `lookupNetwork` directly { request: { - method: 'net_version', + method: 'eth_getBlockByNumber', }, response: { - result: '1', + result: POST_1559_BLOCK, }, beforeCompleting: () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request controller.setActiveNetwork( 'testNetworkConfigurationId', ); }, }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: POST_1559_BLOCK, - }, - }, ]), buildFakeProvider([ // Called when switching networks - { - request: { - method: 'net_version', - }, - response: { - result: '2', - }, - }, { request: { method: 'eth_getBlockByNumber', @@ -2380,12 +2206,6 @@ describe('NetworkController', () => { const fakeProviders = [ buildFakeProvider([ // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, { request: { method: 'eth_getBlockByNumber', @@ -2395,32 +2215,18 @@ describe('NetworkController', () => { // Called via `lookupNetwork` directly { request: { - method: 'net_version', + method: 'eth_getBlockByNumber', }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, + error: BLOCKED_INFURA_JSON_RPC_ERROR, beforeCompleting: () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request controller.setActiveNetwork( 'testNetworkConfigurationId', ); }, }, - { - request: { - method: 'eth_getBlockByNumber', - }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, - }, ]), buildFakeProvider([ // Called when switching networks - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, { request: { method: 'eth_getBlockByNumber', @@ -2437,8 +2243,8 @@ describe('NetworkController', () => { .calledWith({ network: networkType, infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, chainId: BUILT_IN_NETWORKS[networkType].chainId, + type: NetworkClientType.Infura, }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ @@ -2483,486 +2289,201 @@ describe('NetworkController', () => { }); }); - describe('if the network was switched after the eth_getBlockByNumber request started but before it completed', () => { - it('stores the network status of the second network, not the first', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ type: networkType }), - networkConfigurations: { - testNetworkConfigurationId: { - id: 'testNetworkConfigurationId', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'ABC', - }, + lookupNetworkTests({ + expectedProviderConfig: buildProviderConfig({ type: networkType }), + initialState: { + providerConfig: buildProviderConfig({ type: networkType }), + }, + operation: async (controller) => { + await controller.lookupNetwork(); + }, + }); + }); + }, + ); + + describe(`when the provider config in state contains a network type of "rpc"`, () => { + describe('if the network was switched after the eth_getBlockByNumber request started but before it completed', () => { + it('stores the network status of the second network, not the first', async () => { + await withController( + { + state: { + providerConfig: buildProviderConfig({ + type: NetworkType.rpc, + chainId: toHex(1337), + rpcUrl: 'https://mock-rpc-url', + }), + }, + infuraProjectId: 'some-infura-project-id', + }, + async ({ controller, messenger }) => { + const fakeProviders = [ + buildFakeProvider([ + // Called during provider initialization + { + request: { + method: 'eth_getBlockByNumber', + }, + response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, + }, + // Called via `lookupNetwork` directly + { + request: { + method: 'eth_getBlockByNumber', + }, + response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, + beforeCompleting: () => { + controller.setProviderType(NetworkType.goerli); + }, + }, + ]), + buildFakeProvider([ + // Called when switching networks + { + request: { + method: 'eth_getBlockByNumber', }, + error: GENERIC_JSON_RPC_ERROR, }, + ]), + ]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ + chainId: toHex(1337), + rpcUrl: 'https://mock-rpc-url', + type: NetworkClientType.Custom, + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + network: NetworkType.goerli, infuraProjectId: 'some-infura-project-id', - }, - async ({ controller, messenger }) => { - const fakeProviders = [ - buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - beforeCompleting: () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request - controller.setActiveNetwork( - 'testNetworkConfigurationId', - ); - }, - }, - ]), - buildFakeProvider([ - // Called when switching networks - { - request: { - method: 'net_version', - }, - error: GENERIC_JSON_RPC_ERROR, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - chainId: BUILT_IN_NETWORKS[networkType].chainId, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.initializeProvider(); - expect( - controller.state.networksMetadata[networkType].status, - ).toBe('available'); - - await waitForStateChanges({ - messenger, - propertyPath: [ - 'networksMetadata', - 'testNetworkConfigurationId', - 'status', - ], - operation: async () => { - await controller.lookupNetwork(); - }, - }); + chainId: BUILT_IN_NETWORKS[NetworkType.goerli].chainId, + type: NetworkClientType.Infura, + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.initializeProvider(); + expect( + controller.state.networksMetadata['https://mock-rpc-url'] + .status, + ).toBe('available'); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].status, - ).toBe('unknown'); + await waitForStateChanges({ + messenger, + propertyPath: [ + 'networksMetadata', + NetworkType.goerli, + 'status', + ], + operation: async () => { + await controller.lookupNetwork(); }, - ); - }); + }); - it('stores the ID of the second network, not the first', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ type: networkType }), - networkConfigurations: { - testNetworkConfigurationId: { - id: 'testNetworkConfigurationId', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'ABC', - }, + expect( + controller.state.networksMetadata[NetworkType.goerli].status, + ).toBe('unknown'); + }, + ); + }); + + it('stores the EIP-1559 support of the second network, not the first', async () => { + await withController( + { + state: { + providerConfig: buildProviderConfig({ + type: NetworkType.rpc, + chainId: toHex(1337), + rpcUrl: 'https://mock-rpc-url', + }), + }, + infuraProjectId: 'some-infura-project-id', + }, + async ({ controller, messenger }) => { + const fakeProviders = [ + buildFakeProvider([ + // Called during provider initialization + { + request: { + method: 'eth_getBlockByNumber', + }, + response: { + result: POST_1559_BLOCK, }, }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller, messenger }) => { - const fakeProviders = [ - buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - beforeCompleting: async () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request - controller.setActiveNetwork( - 'testNetworkConfigurationId', - ); - }, - }, - ]), - buildFakeProvider([ - // Called when switching networks - { - request: { - method: 'net_version', - }, - response: { - result: '2', - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - chainId: BUILT_IN_NETWORKS[networkType].chainId, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[1]); - await waitForStateChanges({ - messenger, - propertyPath: ['networkId'], - operation: async () => { - await controller.initializeProvider(); + // Called via `lookupNetwork` directly + { + request: { + method: 'eth_getBlockByNumber', }, - }); - expect(controller.state.networkId).toBe('1'); - - await waitForStateChanges({ - messenger, - propertyPath: ['networkId'], - operation: async () => { - await controller.lookupNetwork(); + response: { + result: POST_1559_BLOCK, }, - }); - - expect(controller.state.networkId).toBe('2'); - }, - ); - }); - - it('stores the EIP-1559 support of the second network, not the first', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ type: networkType }), - networkConfigurations: { - testNetworkConfigurationId: { - id: 'testNetworkConfigurationId', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'ABC', - }, + beforeCompleting: () => { + controller.setProviderType(NetworkType.goerli); + }, + }, + ]), + buildFakeProvider([ + // Called when switching networks + { + request: { + method: 'eth_getBlockByNumber', + }, + response: { + result: PRE_1559_BLOCK, }, }, + ]), + ]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ + chainId: toHex(1337), + rpcUrl: 'https://mock-rpc-url', + type: NetworkClientType.Custom, + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + network: NetworkType.goerli, infuraProjectId: 'some-infura-project-id', - }, - async ({ controller, messenger }) => { - const fakeProviders = [ - buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: POST_1559_BLOCK, - }, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: POST_1559_BLOCK, - }, - beforeCompleting: () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request - controller.setActiveNetwork( - 'testNetworkConfigurationId', - ); - }, - }, - ]), - buildFakeProvider([ - // Called when switching networks - { - request: { - method: 'net_version', - }, - response: { - result: '2', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: PRE_1559_BLOCK, - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - chainId: BUILT_IN_NETWORKS[networkType].chainId, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.initializeProvider(); - expect( - controller.state.networksMetadata[networkType].EIPS[1559], - ).toBe(true); - - await waitForStateChanges({ - messenger, - propertyPath: [ - 'networksMetadata', - 'testNetworkConfigurationId', - 'EIPS', - ], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - expect( - controller.state.networksMetadata.testNetworkConfigurationId - .EIPS[1559], - ).toBe(false); - }, - ); - }); - - it('emits infuraIsUnblocked, not infuraIsBlocked, assuming that the first network was blocked', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ type: networkType }), - networkConfigurations: { - testNetworkConfigurationId: { - id: 'testNetworkConfigurationId', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'ABC', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller, messenger }) => { - const fakeProviders = [ - buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, - beforeCompleting: () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request - controller.setActiveNetwork( - 'testNetworkConfigurationId', - ); - }, - }, - ]), - buildFakeProvider([ - // Called when switching networks - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[networkType].chainId, - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.initializeProvider(); - const promiseForInfuraIsUnblockedEvents = - waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - }); - const promiseForNoInfuraIsBlockedEvents = - waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', - count: 0, - }); - - await waitForStateChanges({ - messenger, - propertyPath: [ - 'networksMetadata', - 'testNetworkConfigurationId', - 'status', - ], - operation: async () => { - await controller.lookupNetwork(); - }, - }); + chainId: BUILT_IN_NETWORKS[NetworkType.goerli].chainId, + type: NetworkClientType.Infura, + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.initializeProvider(); + expect( + controller.state.networksMetadata['https://mock-rpc-url'] + .EIPS[1559], + ).toBe(true); - await expect( - promiseForInfuraIsUnblockedEvents, - ).toBeFulfilled(); - await expect( - promiseForNoInfuraIsBlockedEvents, - ).toBeFulfilled(); + await waitForStateChanges({ + messenger, + propertyPath: ['networksMetadata', NetworkType.goerli, 'EIPS'], + operation: async () => { + await controller.lookupNetwork(); }, - ); - }); - }); + }); - lookupNetworkTests({ - expectedProviderConfig: buildProviderConfig({ type: networkType }), - initialState: { - providerConfig: buildProviderConfig({ type: networkType }), - }, - operation: async (controller) => { - await controller.lookupNetwork(); + expect( + controller.state.networksMetadata[NetworkType.goerli] + .EIPS[1559], + ).toBe(false); + expect( + controller.state.networksMetadata['https://mock-rpc-url'] + .EIPS[1559], + ).toBe(true); }, - }); + ); }); - }, - ); - describe(`when the provider config in state contains a network type of "rpc"`, () => { - describe('if the network was switched after the net_version request started but before it completed', () => { - it('stores the network status of the second network, not the first', async () => { + it('emits infuraIsBlocked, not infuraIsUnblocked, if the second network was blocked and the first network was not', async () => { await withController( { state: { @@ -2978,12 +2499,6 @@ describe('NetworkController', () => { const fakeProviders = [ buildFakeProvider([ // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, { request: { method: 'eth_getBlockByNumber', @@ -2993,29 +2508,21 @@ describe('NetworkController', () => { // Called via `lookupNetwork` directly { request: { - method: 'net_version', + method: 'eth_getBlockByNumber', }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, + response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, beforeCompleting: () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request controller.setProviderType(NetworkType.goerli); }, }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, ]), buildFakeProvider([ // Called when switching networks { request: { - method: 'net_version', + method: 'eth_getBlockByNumber', }, - error: GENERIC_JSON_RPC_ERROR, + error: BLOCKED_INFURA_JSON_RPC_ERROR, }, ]), ]; @@ -3033,15 +2540,21 @@ describe('NetworkController', () => { .calledWith({ network: NetworkType.goerli, infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, chainId: BUILT_IN_NETWORKS[NetworkType.goerli].chainId, + type: NetworkClientType.Infura, }) .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); - expect( - controller.state.networksMetadata['https://mock-rpc-url'] - .status, - ).toBe('available'); + const promiseForNoInfuraIsUnblockedEvents = + waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + count: 0, + }); + const promiseForInfuraIsBlockedEvents = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsBlocked', + }); await waitForStateChanges({ messenger, @@ -3055,5341 +2568,3649 @@ describe('NetworkController', () => { }, }); - expect( - controller.state.networksMetadata[NetworkType.goerli].status, - ).toBe('unknown'); + await expect(promiseForNoInfuraIsUnblockedEvents).toBeFulfilled(); + await expect(promiseForInfuraIsBlockedEvents).toBeFulfilled(); }, ); }); + }); - it('stores the ID of the second network, not the first', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - }), + lookupNetworkTests({ + expectedProviderConfig: buildProviderConfig({ type: NetworkType.rpc }), + initialState: { + providerConfig: buildProviderConfig({ type: NetworkType.rpc }), + }, + operation: async (controller) => { + await controller.lookupNetwork(); + }, + }); + }); + }); + + describe('setProviderType', () => { + for (const { + networkType, + chainId, + ticker, + blockExplorerUrl, + } of INFURA_NETWORKS) { + describe(`given a network type of "${networkType}"`, () => { + refreshNetworkTests({ + expectedProviderConfig: buildProviderConfig({ + type: networkType, + }), + operation: async (controller) => { + await controller.setProviderType(networkType); + }, + }); + }); + + it(`overwrites the provider configuration using a predetermined chainId, ticker, and blockExplorerUrl for "${networkType}", clearing id, rpcUrl, and nickname`, async () => { + await withController( + { + state: { + providerConfig: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + nickname: 'test-chain', + ticker: 'TEST', + rpcPrefs: { + blockExplorerUrl: 'https://test-block-explorer.com', + }, }, - infuraProjectId: 'some-infura-project-id', }, - async ({ controller, messenger }) => { - const fakeProviders = [ - buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - beforeCompleting: async () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request - controller.setProviderType(NetworkType.goerli); - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - ]), - buildFakeProvider([ - // Called when switching networks - { - request: { - method: 'net_version', - }, - response: { - result: '2', - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: NetworkType.goerli, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[NetworkType.goerli].chainId, - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await waitForStateChanges({ - messenger, - propertyPath: ['networkId'], - operation: async () => { - await controller.initializeProvider(); - }, - }); - expect(controller.state.networkId).toBe('1'); + }, + async ({ controller }) => { + const fakeProvider = buildFakeProvider(); + const fakeNetworkClient = buildFakeClient(fakeProvider); + mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - await waitForStateChanges({ - messenger, - propertyPath: ['networkId'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); + await controller.setProviderType(networkType); - expect(controller.state.networkId).toBe('2'); - }, + expect(controller.state.providerConfig).toStrictEqual({ + type: networkType, + rpcUrl: undefined, + chainId, + ticker, + nickname: undefined, + rpcPrefs: { blockExplorerUrl }, + id: undefined, + }); + }, + ); + }); + + it(`updates state.selectedNetworkClientId, setting it to ${networkType}`, async () => { + await withController({}, async ({ controller }) => { + const fakeProvider = buildFakeProvider(); + const fakeNetworkClient = buildFakeClient(fakeProvider); + mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); + + await controller.setProviderType(networkType); + + expect(controller.state.selectedNetworkClientId).toStrictEqual( + networkType, ); }); + }); + } - it('stores the EIP-1559 support of the second network, not the first', async () => { - await withController( + describe('given a network type of "rpc"', () => { + it('throws because there is no way to switch to a custom RPC endpoint using this method', async () => { + await withController( + { + state: { + providerConfig: { + type: NetworkType.rpc, + rpcUrl: 'http://somethingexisting.com', + chainId: toHex(99999), + ticker: 'something existing', + nickname: 'something existing', + }, + }, + }, + async ({ controller }) => { + await expect(() => + // @ts-expect-error Intentionally passing invalid type + controller.setProviderType(NetworkType.rpc), + ).rejects.toThrow( + 'NetworkController - cannot call "setProviderType" with type "rpc". Use "setActiveNetwork"', + ); + }, + ); + }); + + it("doesn't set a provider", async () => { + await withController(async ({ controller }) => { + const fakeProvider = buildFakeProvider(); + const fakeNetworkClient = buildFakeClient(fakeProvider); + createNetworkClientMock.mockReturnValue(fakeNetworkClient); + + try { + // @ts-expect-error Intentionally passing invalid type + await controller.setProviderType(NetworkType.rpc); + } catch { + // catch the rejection (it is tested above) + } + + expect(createNetworkClientMock).not.toHaveBeenCalled(); + expect( + controller.getProviderAndBlockTracker().provider, + ).toBeUndefined(); + }); + }); + + it('does not update networksMetadata[...].EIPS in state', async () => { + await withController(async ({ controller }) => { + const fakeProvider = buildFakeProvider([ { - state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - }), + request: { + method: 'eth_getBlockByNumber', + params: ['latest', false], + }, + response: { + result: { + baseFeePerGas: '0x1', + }, }, - infuraProjectId: 'some-infura-project-id', }, - async ({ controller, messenger }) => { - const fakeProviders = [ - buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: POST_1559_BLOCK, - }, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - beforeCompleting: () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request - controller.setProviderType(NetworkType.goerli); - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: POST_1559_BLOCK, - }, - }, - ]), - buildFakeProvider([ - // Called when switching networks - { - request: { - method: 'net_version', - }, - response: { - result: '2', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: PRE_1559_BLOCK, - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: NetworkType.goerli, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[NetworkType.goerli].chainId, - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.initializeProvider(); - expect( - controller.state.networksMetadata['https://mock-rpc-url'] - .EIPS[1559], - ).toBe(true); + ]); + const fakeNetworkClient = buildFakeClient(fakeProvider); + createNetworkClientMock.mockReturnValue(fakeNetworkClient); - await waitForStateChanges({ - messenger, - propertyPath: ['networksMetadata', NetworkType.goerli, 'EIPS'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); + const detailsPre = + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ]; - expect( - controller.state.networksMetadata[NetworkType.goerli] - .EIPS[1559], - ).toBe(false); - }, - ); - }); + try { + // @ts-expect-error Intentionally passing invalid type + await controller.setProviderType(NetworkType.rpc); + } catch { + // catch the rejection (it is tested above) + } - it('emits infuraIsBlocked, not infuraIsUnblocked, if the second network was blocked and the first network was not', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - }), - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller, messenger }) => { - const fakeProviders = [ - buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - beforeCompleting: () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request - controller.setProviderType(NetworkType.goerli); - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - ]), - buildFakeProvider([ - // Called when switching networks - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: NetworkType.goerli, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[NetworkType.goerli].chainId, - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.initializeProvider(); - const promiseForNoInfuraIsUnblockedEvents = - waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - count: 0, - }); - const promiseForInfuraIsBlockedEvents = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', - }); + const detailsPost = + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ]; - await waitForStateChanges({ - messenger, - propertyPath: ['networksMetadata', 'goerli', 'status'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); + expect(detailsPost).toBe(detailsPre); + }); + }); + }); - await expect(promiseForNoInfuraIsUnblockedEvents).toBeFulfilled(); - await expect(promiseForInfuraIsBlockedEvents).toBeFulfilled(); - }, + describe('given an invalid Infura network name', () => { + it('throws', async () => { + await withController(async ({ controller }) => { + await expect(() => + // @ts-expect-error Intentionally passing invalid type + controller.setProviderType('invalid-infura-network'), + ).rejects.toThrow( + new Error('Unknown Infura provider type "invalid-infura-network".'), ); }); }); + }); + }); - describe('if the network was switched after the eth_getBlockByNumber request started but before it completed', () => { - it('stores the network status of the second network, not the first', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - chainId: toHex(1337), + describe('setActiveNetwork', () => { + refreshNetworkTests({ + expectedProviderConfig: { + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(111), + ticker: 'TEST', + nickname: 'something existing', + id: 'testNetworkConfigurationId', + rpcPrefs: undefined, + type: NetworkType.rpc, + }, + initialState: { + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(111), + ticker: 'TEST', + nickname: 'something existing', + id: 'testNetworkConfigurationId', + rpcPrefs: undefined, + }, + }, + }, + operation: async (controller) => { + await controller.setActiveNetwork('testNetworkConfigurationId'); + }, + }); + + describe('if the given ID does not match a network configuration in networkConfigurations', () => { + it('throws', async () => { + await withController( + { + state: { + networkConfigurations: { + testNetworkConfigurationId: { rpcUrl: 'https://mock-rpc-url', - }), - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller, messenger }) => { - const fakeProviders = [ - buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - beforeCompleting: () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request - controller.setProviderType(NetworkType.goerli); - }, - }, - ]), - buildFakeProvider([ - // Called when switching networks - { - request: { - method: 'net_version', - }, - error: GENERIC_JSON_RPC_ERROR, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: NetworkType.goerli, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[NetworkType.goerli].chainId, - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.initializeProvider(); - expect( - controller.state.networksMetadata['https://mock-rpc-url'] - .status, - ).toBe('available'); - - await waitForStateChanges({ - messenger, - propertyPath: [ - 'networksMetadata', - NetworkType.goerli, - 'status', - ], - operation: async () => { - await controller.lookupNetwork(); + chainId: toHex(111), + ticker: 'TEST', + id: 'testNetworkConfigurationId', }, - }); - - expect( - controller.state.networksMetadata[NetworkType.goerli].status, - ).toBe('unknown'); + }, }, - ); - }); + }, + async ({ controller }) => { + const fakeProvider = buildFakeProvider(); + const fakeNetworkClient = buildFakeClient(fakeProvider); + mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - it('stores the ID of the second network, not the first', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - }), + await expect(() => + controller.setActiveNetwork('invalidNetworkConfigurationId'), + ).rejects.toThrow( + new Error( + 'networkConfigurationId invalidNetworkConfigurationId does not match a configured networkConfiguration', + ), + ); + }, + ); + }); + }); + + describe('if the network config does not contain an RPC URL', () => { + it('throws', async () => { + await withController( + // @ts-expect-error RPC URL intentionally omitted + { + state: { + providerConfig: { + type: NetworkType.rpc, + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(111), + ticker: 'TEST', + nickname: 'something existing', + rpcPrefs: undefined, }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller, messenger }) => { - const fakeProviders = [ - buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - beforeCompleting: async () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request - controller.setProviderType(NetworkType.goerli); - }, - }, - ]), - buildFakeProvider([ - // Called when switching networks - { - request: { - method: 'net_version', - }, - response: { - result: '2', - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - chainId: toHex(1337), + networkConfigurations: { + testNetworkConfigurationId1: { rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: NetworkType.goerli, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[NetworkType.goerli].chainId, - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await waitForStateChanges({ - messenger, - propertyPath: ['networkId'], - operation: async () => { - await controller.initializeProvider(); + chainId: toHex(111), + ticker: 'TEST', + nickname: 'something existing', + id: 'testNetworkConfigurationId1', + rpcPrefs: undefined, }, - }); - expect(controller.state.networkId).toBe('1'); - - await waitForStateChanges({ - messenger, - propertyPath: ['networkId'], - operation: async () => { - await controller.lookupNetwork(); + testNetworkConfigurationId2: { + rpcUrl: undefined, + chainId: toHex(222), + ticker: 'something existing', + nickname: 'something existing', + id: 'testNetworkConfigurationId2', + rpcPrefs: undefined, }, - }); - - expect(controller.state.networkId).toBe('2'); + }, }, - ); - }); + }, + async ({ controller }) => { + const fakeProvider = buildFakeProvider(); + const fakeNetworkClient = buildFakeClient(fakeProvider); + createNetworkClientMock.mockReturnValue(fakeNetworkClient); - it('stores the EIP-1559 support of the second network, not the first', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - }), + await expect(() => + controller.setActiveNetwork('testNetworkConfigurationId2'), + ).rejects.toThrow( + 'rpcUrl must be provided for custom RPC endpoints', + ); + + expect(createNetworkClientMock).not.toHaveBeenCalled(); + const { provider, blockTracker } = + controller.getProviderAndBlockTracker(); + expect(provider).toBeUndefined(); + expect(blockTracker).toBeUndefined(); + }, + ); + }); + }); + + describe('if the network config does not contain a chain ID', () => { + it('throws', async () => { + await withController( + // @ts-expect-error chain ID intentionally omitted + { + state: { + providerConfig: { + type: NetworkType.rpc, + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(111), + ticker: 'TEST', + nickname: 'something existing', + rpcPrefs: undefined, }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller, messenger }) => { - const fakeProviders = [ - buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: POST_1559_BLOCK, - }, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: POST_1559_BLOCK, - }, - beforeCompleting: () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request - controller.setProviderType(NetworkType.goerli); - }, - }, - ]), - buildFakeProvider([ - // Called when switching networks - { - request: { - method: 'net_version', - }, - response: { - result: '2', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: PRE_1559_BLOCK, - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: NetworkType.goerli, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[NetworkType.goerli].chainId, - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.initializeProvider(); - expect( - controller.state.networksMetadata['https://mock-rpc-url'] - .EIPS[1559], - ).toBe(true); - - await waitForStateChanges({ - messenger, - propertyPath: ['networksMetadata', NetworkType.goerli, 'EIPS'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - expect( - controller.state.networksMetadata[NetworkType.goerli] - .EIPS[1559], - ).toBe(false); - expect( - controller.state.networksMetadata['https://mock-rpc-url'] - .EIPS[1559], - ).toBe(true); - }, - ); - }); - - it('emits infuraIsBlocked, not infuraIsUnblocked, if the second network was blocked and the first network was not', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - }), - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller, messenger }) => { - const fakeProviders = [ - buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - beforeCompleting: () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request - controller.setProviderType(NetworkType.goerli); - }, - }, - ]), - buildFakeProvider([ - // Called when switching networks - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: NetworkType.goerli, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[NetworkType.goerli].chainId, - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.initializeProvider(); - const promiseForNoInfuraIsUnblockedEvents = - waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - count: 0, - }); - const promiseForInfuraIsBlockedEvents = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', - }); - - await waitForStateChanges({ - messenger, - propertyPath: [ - 'networksMetadata', - NetworkType.goerli, - 'status', - ], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - await expect(promiseForNoInfuraIsUnblockedEvents).toBeFulfilled(); - await expect(promiseForInfuraIsBlockedEvents).toBeFulfilled(); - }, - ); - }); - }); - - lookupNetworkTests({ - expectedProviderConfig: buildProviderConfig({ type: NetworkType.rpc }), - initialState: { - providerConfig: buildProviderConfig({ type: NetworkType.rpc }), - }, - operation: async (controller) => { - await controller.lookupNetwork(); - }, - }); - }); - }); - - describe('setProviderType', () => { - for (const { - networkType, - chainId, - ticker, - blockExplorerUrl, - } of INFURA_NETWORKS) { - describe(`given a network type of "${networkType}"`, () => { - refreshNetworkTests({ - expectedProviderConfig: buildProviderConfig({ - type: networkType, - }), - operation: async (controller) => { - await controller.setProviderType(networkType); - }, - }); - }); - - it(`overwrites the provider configuration using a predetermined chainId, ticker, and blockExplorerUrl for "${networkType}", clearing id, rpcUrl, and nickname`, async () => { - await withController( - { - state: { - providerConfig: { - type: 'rpc', - rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', - nickname: 'test-chain', - ticker: 'TEST', - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer.com', - }, - }, - }, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - - await controller.setProviderType(networkType); - - expect(controller.state.providerConfig).toStrictEqual({ - type: networkType, - rpcUrl: undefined, - chainId, - ticker, - nickname: undefined, - rpcPrefs: { blockExplorerUrl }, - id: undefined, - }); - }, - ); - }); - - it(`updates state.selectedNetworkId, setting it to ${networkType}`, async () => { - await withController({}, async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - - await controller.setProviderType(networkType); - - expect(controller.state.selectedNetworkClientId).toStrictEqual( - networkType, - ); - }); - }); - } - - describe('given a network type of "rpc"', () => { - it('throws because there is no way to switch to a custom RPC endpoint using this method', async () => { - await withController( - { - state: { - providerConfig: { - type: NetworkType.rpc, - rpcUrl: 'http://somethingexisting.com', - chainId: toHex(99999), - ticker: 'something existing', - nickname: 'something existing', - }, - }, - }, - async ({ controller }) => { - await expect(() => - // @ts-expect-error Intentionally passing invalid type - controller.setProviderType(NetworkType.rpc), - ).rejects.toThrow( - 'NetworkController - cannot call "setProviderType" with type "rpc". Use "setActiveNetwork"', - ); - }, - ); - }); - - it("doesn't set a provider", async () => { - await withController(async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - - try { - // @ts-expect-error Intentionally passing invalid type - await controller.setProviderType(NetworkType.rpc); - } catch { - // catch the rejection (it is tested above) - } - - expect(createNetworkClientMock).not.toHaveBeenCalled(); - expect( - controller.getProviderAndBlockTracker().provider, - ).toBeUndefined(); - }); - }); - - it('does not update networksMetadata[...].EIPS in state', async () => { - await withController(async ({ controller }) => { - const fakeProvider = buildFakeProvider([ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: { - baseFeePerGas: '0x1', - }, - }, - }, - ]); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - - const detailsPre = - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ]; - - try { - // @ts-expect-error Intentionally passing invalid type - await controller.setProviderType(NetworkType.rpc); - } catch { - // catch the rejection (it is tested above) - } - - const detailsPost = - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ]; - - expect(detailsPost).toBe(detailsPre); - }); - }); - }); - - describe('given an invalid Infura network name', () => { - it('throws', async () => { - await withController(async ({ controller }) => { - await expect(() => - // @ts-expect-error Intentionally passing invalid type - controller.setProviderType('invalid-infura-network'), - ).rejects.toThrow( - new Error('Unknown Infura provider type "invalid-infura-network".'), - ); - }); - }); - }); - }); - - describe('setActiveNetwork', () => { - refreshNetworkTests({ - expectedProviderConfig: { - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - nickname: 'something existing', - id: 'testNetworkConfigurationId', - rpcPrefs: undefined, - type: NetworkType.rpc, - }, - initialState: { - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - nickname: 'something existing', - id: 'testNetworkConfigurationId', - rpcPrefs: undefined, - }, - }, - }, - operation: async (controller) => { - await controller.setActiveNetwork('testNetworkConfigurationId'); - }, - }); - - describe('if the given ID does not match a network configuration in networkConfigurations', () => { - it('throws', async () => { - await withController( - { - state: { - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }, - }, - }, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - - await expect(() => - controller.setActiveNetwork('invalidNetworkConfigurationId'), - ).rejects.toThrow( - new Error( - 'networkConfigurationId invalidNetworkConfigurationId does not match a configured networkConfiguration', - ), - ); - }, - ); - }); - }); - - describe('if the network config does not contain an RPC URL', () => { - it('throws', async () => { - await withController( - // @ts-expect-error RPC URL intentionally omitted - { - state: { - providerConfig: { - type: NetworkType.rpc, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - nickname: 'something existing', - rpcPrefs: undefined, - }, - networkConfigurations: { - testNetworkConfigurationId1: { - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - nickname: 'something existing', - id: 'testNetworkConfigurationId1', - rpcPrefs: undefined, - }, - testNetworkConfigurationId2: { - rpcUrl: undefined, - chainId: toHex(222), - ticker: 'something existing', - nickname: 'something existing', - id: 'testNetworkConfigurationId2', - rpcPrefs: undefined, - }, - }, - }, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - - await expect(() => - controller.setActiveNetwork('testNetworkConfigurationId2'), - ).rejects.toThrow( - 'rpcUrl must be provided for custom RPC endpoints', - ); - - expect(createNetworkClientMock).not.toHaveBeenCalled(); - const { provider, blockTracker } = - controller.getProviderAndBlockTracker(); - expect(provider).toBeUndefined(); - expect(blockTracker).toBeUndefined(); - }, - ); - }); - }); - - describe('if the network config does not contain a chain ID', () => { - it('throws', async () => { - await withController( - // @ts-expect-error chain ID intentionally omitted - { - state: { - providerConfig: { - type: NetworkType.rpc, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - nickname: 'something existing', - rpcPrefs: undefined, - }, - networkConfigurations: { - testNetworkConfigurationId1: { - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - nickname: 'something existing', - id: 'testNetworkConfigurationId1', - rpcPrefs: undefined, - }, - testNetworkConfigurationId2: { - rpcUrl: 'http://somethingexisting.com', - chainId: undefined, - ticker: 'something existing', - nickname: 'something existing', - id: 'testNetworkConfigurationId2', - rpcPrefs: undefined, - }, - }, - }, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - - await expect(() => - controller.setActiveNetwork('testNetworkConfigurationId2'), - ).rejects.toThrow( - 'chainId must be provided for custom RPC endpoints', - ); - - expect(createNetworkClientMock).not.toHaveBeenCalled(); - const { provider, blockTracker } = - controller.getProviderAndBlockTracker(); - expect(provider).toBeUndefined(); - expect(blockTracker).toBeUndefined(); - }, - ); - }); - }); - - it('overwrites the provider configuration given a networkConfigurationId that matches a configured networkConfiguration', async () => { - await withController( - { - state: { - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - nickname: 'something existing', - id: 'testNetworkConfigurationId', - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer-2.com', - }, - }, - }, - }, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient() - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClient); - - await controller.setActiveNetwork('testNetworkConfigurationId'); - - expect(controller.state.providerConfig).toStrictEqual({ - type: 'rpc', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - nickname: 'something existing', - id: 'testNetworkConfigurationId', - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer-2.com', - }, - }); - }, - ); - }); - - it('updates state.selectedNetworkClientId setting it to the networkConfiguration.id', async () => { - const testNetworkClientId = 'testNetworkConfigurationId'; - await withController( - { - state: { - networkConfigurations: { - [testNetworkClientId]: { - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - nickname: 'something existing', - id: testNetworkClientId, - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer-2.com', - }, - }, - }, - }, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient() - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClient); - - await controller.setActiveNetwork(testNetworkClientId); - - expect(controller.state.selectedNetworkClientId).toStrictEqual( - testNetworkClientId, - ); - }, - ); - }); - }); - - describe('getEIP1559Compatibility', () => { - describe('if no provider has been set yet', () => { - it('does not make any state changes', async () => { - await withController(async ({ controller, messenger }) => { - const promiseForNoStateChanges = waitForStateChanges({ - messenger, - count: 0, - operation: async () => { - await controller.getEIP1559Compatibility(); - }, - }); - - expect(Boolean(promiseForNoStateChanges)).toBe(true); - }); - }); - - it('returns false', async () => { - await withController(async ({ controller }) => { - const isEIP1559Compatible = - await controller.getEIP1559Compatibility(); - - expect(isEIP1559Compatible).toBe(false); - }); - }); - }); - - describe('if a networkClientId is passed in', () => { - it('uses the built in state for networksMetadata', async () => { - await withController( - { - state: { - networksMetadata: { - 'linea-mainnet': { - EIPS: { - 1559: true, - }, - status: NetworkStatus.Unknown, - }, - }, - }, - }, - async ({ controller }) => { - const isEIP1559Compatible = - await controller.getEIP1559Compatibility('linea-mainnet'); - - expect(isEIP1559Compatible).toBe(true); - }, - ); - }); - it('uses the built in false state for networksMetadata', async () => { - await withController( - { - state: { - networksMetadata: { - 'linea-mainnet': { - EIPS: { - 1559: false, - }, - status: NetworkStatus.Unknown, - }, - }, - }, - }, - async ({ controller }) => { - const isEIP1559Compatible = - await controller.getEIP1559Compatibility('linea-mainnet'); - - expect(isEIP1559Compatible).toBe(false); - }, - ); - }); - it('calls provider of the networkClientId and returns true', async () => { - await withController( - { - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: POST_1559_BLOCK, - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: POST_1559_BLOCK, - }, - }, - ], - }); - const isEIP1559Compatible = - await controller.getEIP1559Compatibility('linea-mainnet'); - expect(isEIP1559Compatible).toBe(true); - }, - ); - }); - }); - - describe('if a provider has been set but networksMetadata[selectedNetworkId].EIPS in state already has a "1559" property', () => { - it('does not make any state changes', async () => { - await withController( - { - state: { - networksMetadata: { - mainnet: { - EIPS: { - 1559: true, - }, - status: NetworkStatus.Unknown, - }, - }, - }, - }, - async ({ controller, messenger }) => { - setFakeProvider(controller, { - stubLookupNetworkWhileSetting: true, - }); - const promiseForNoStateChanges = waitForStateChanges({ - messenger, - count: 0, - operation: async () => { - await controller.getEIP1559Compatibility(); - }, - }); - - expect(Boolean(promiseForNoStateChanges)).toBe(true); - }, - ); - }); - - it('returns the value of the "1559" property', async () => { - await withController( - { - state: { - networksMetadata: { - mainnet: { - EIPS: { - 1559: true, - }, - status: NetworkStatus.Unknown, - }, - }, - }, - }, - async ({ controller }) => { - setFakeProvider(controller, { - stubLookupNetworkWhileSetting: true, - }); - - const isEIP1559Compatible = - await controller.getEIP1559Compatibility(); - - expect(isEIP1559Compatible).toBe(true); - }, - ); - }); - }); - - describe('if a provider has been set and networksMetadata[selectedNetworkId].EIPS in state does not already have a "1559" property', () => { - describe('if the request for the latest block is successful', () => { - describe('if the latest block has a "baseFeePerGas" property', () => { - it('sets the "1559" property to true', async () => { - await withController(async ({ controller }) => { - setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: POST_1559_BLOCK, - }, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - await controller.getEIP1559Compatibility(); - - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].EIPS[1559], - ).toBe(true); - }); - }); - - it('returns true', async () => { - await withController(async ({ controller }) => { - setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: POST_1559_BLOCK, - }, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const isEIP1559Compatible = - await controller.getEIP1559Compatibility(); - - expect(isEIP1559Compatible).toBe(true); - }); - }); - }); - - describe('if the latest block does not have a "baseFeePerGas" property', () => { - it('sets the "1559" property to false', async () => { - await withController(async ({ controller }) => { - setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: PRE_1559_BLOCK, - }, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - await controller.getEIP1559Compatibility(); - - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].EIPS[1559], - ).toBe(false); - }); - }); - - it('returns false', async () => { - await withController(async ({ controller }) => { - setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: PRE_1559_BLOCK, - }, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const isEIP1559Compatible = - await controller.getEIP1559Compatibility(); - - expect(isEIP1559Compatible).toBe(false); - }); - }); - }); - - describe('if the request for the latest block responds with null', () => { - const latestBlockRespondsNull = { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: null, - }, - }; - it('keeps the "1559" property as undefined', async () => { - await withController(async ({ controller }) => { - setFakeProvider(controller, { - stubs: [latestBlockRespondsNull], - stubLookupNetworkWhileSetting: true, - }); - - await controller.getEIP1559Compatibility(); - - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].EIPS[1559], - ).toBeUndefined(); - }); - }); - - it('returns undefined', async () => { - await withController(async ({ controller }) => { - setFakeProvider(controller, { - stubs: [latestBlockRespondsNull], - stubLookupNetworkWhileSetting: true, - }); - - const isEIP1559Compatible = - await controller.getEIP1559Compatibility(); - - expect(isEIP1559Compatible).toBeUndefined(); - }); - }); - }); - }); - - describe('if the request for the latest block is unsuccessful', () => { - it('does not make any state changes', async () => { - await withController(async ({ controller, messenger }) => { - setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - error: GENERIC_JSON_RPC_ERROR, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const promiseForNoStateChanges = waitForStateChanges({ - messenger, - count: 0, - operation: async () => { - try { - await controller.getEIP1559Compatibility(); - } catch (error) { - // ignore error - } - }, - }); - - expect(Boolean(promiseForNoStateChanges)).toBe(true); - }); - }); - }); - }); - }); - - describe('resetConnection', () => { - [NetworkType.mainnet, NetworkType.goerli, NetworkType.sepolia].forEach( - (networkType) => { - describe(`when the type in the provider configuration is "${networkType}"`, () => { - refreshNetworkTests({ - expectedProviderConfig: buildProviderConfig({ type: networkType }), - initialState: { - providerConfig: buildProviderConfig({ type: networkType }), - }, - operation: async (controller) => { - await controller.resetConnection(); - }, - }); - }); - }, - ); - - describe(`when the type in the provider configuration is "rpc"`, () => { - refreshNetworkTests({ - expectedProviderConfig: buildProviderConfig({ type: NetworkType.rpc }), - initialState: { - providerConfig: buildProviderConfig({ type: NetworkType.rpc }), - }, - operation: async (controller) => { - await controller.resetConnection(); - }, - }); - }); - }); - - describe('NetworkController:getProviderConfig action', () => { - it('returns the provider config in state', async () => { - await withController( - { - state: { - providerConfig: { - type: NetworkType.mainnet, - ...BUILT_IN_NETWORKS.mainnet, - }, - }, - }, - async ({ messenger }) => { - const providerConfig = await messenger.call( - 'NetworkController:getProviderConfig', - ); - - expect(providerConfig).toStrictEqual({ - type: NetworkType.mainnet, - ...BUILT_IN_NETWORKS.mainnet, - }); - }, - ); - }); - }); - - describe('NetworkController:getEthQuery action', () => { - it('returns a EthQuery object that can be used to make requests to the currently selected network', async () => { - await withController(async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'test_method', - params: [], - }, - response: { - result: 'test response', - }, - }, - ], - }); - - const ethQuery = messenger.call('NetworkController:getEthQuery'); - assert(ethQuery, 'ethQuery is not set'); - - const promisifiedSendAsync = promisify(ethQuery.sendAsync).bind( - ethQuery, - ); - const result = await promisifiedSendAsync({ - id: 1, - jsonrpc: '2.0', - method: 'test_method', - params: [], - }); - expect(result).toBe('test response'); - }); - }); - - it('returns undefined if the provider has not been set yet', async () => { - await withController(({ messenger }) => { - const ethQuery = messenger.call('NetworkController:getEthQuery'); - - expect(ethQuery).toBeUndefined(); - }); - }); - }); - - describe('upsertNetworkConfiguration', () => { - describe('when the rpcUrl of the given network configuration does not match an existing network configuration', () => { - it('adds the network configuration to state without updating or removing any existing network configurations', async () => { - await withController( - { - state: { - networkConfigurations: { - 'AAAA-AAAA-AAAA-AAAA': { - rpcUrl: 'https://test.network.1', - chainId: toHex(111), - ticker: 'TICKER1', - id: 'AAAA-AAAA-AAAA-AAAA', - }, - }, - }, - }, - async ({ controller }) => { - uuidV4Mock.mockReturnValue('BBBB-BBBB-BBBB-BBBB'); - - await controller.upsertNetworkConfiguration( - { - rpcUrl: 'https://test.network.2', - chainId: toHex(222), - ticker: 'TICKER2', - nickname: 'test network 2', - rpcPrefs: { - blockExplorerUrl: 'https://testchainscan.io', - }, - }, - { - referrer: 'https://test-dapp.com', - source: 'dapp', - }, - ); - - expect(controller.state.networkConfigurations).toStrictEqual({ - 'AAAA-AAAA-AAAA-AAAA': { - rpcUrl: 'https://test.network.1', - chainId: toHex(111), - ticker: 'TICKER1', - id: 'AAAA-AAAA-AAAA-AAAA', - }, - 'BBBB-BBBB-BBBB-BBBB': { - rpcUrl: 'https://test.network.2', - chainId: toHex(222), - ticker: 'TICKER2', - nickname: 'test network 2', - rpcPrefs: { - blockExplorerUrl: 'https://testchainscan.io', - }, - id: 'BBBB-BBBB-BBBB-BBBB', - }, - }); - }, - ); - }); - - it('removes properties not specific to the NetworkConfiguration interface before persisting it to state', async function () { - await withController(async ({ controller }) => { - uuidV4Mock.mockReturnValue('AAAA-AAAA-AAAA-AAAA'); - - await controller.upsertNetworkConfiguration( - { - rpcUrl: 'https://test.network', - chainId: toHex(111), - ticker: 'TICKER', - nickname: 'test network', - rpcPrefs: { - blockExplorerUrl: 'https://testchainscan.io', - }, - // @ts-expect-error We are intentionally passing bad input. - invalidKey: 'some value', - }, - { - referrer: 'https://test-dapp.com', - source: 'dapp', - }, - ); - - expect(controller.state.networkConfigurations).toStrictEqual({ - 'AAAA-AAAA-AAAA-AAAA': { - rpcUrl: 'https://test.network', - chainId: toHex(111), - ticker: 'TICKER', - nickname: 'test network', - rpcPrefs: { - blockExplorerUrl: 'https://testchainscan.io', - }, - id: 'AAAA-AAAA-AAAA-AAAA', - }, - }); - }); - }); - - it('creates a new network client for the network configuration and adds it to the registry', async () => { - await withController( - { infuraProjectId: 'some-infura-project-id' }, - async ({ controller }) => { - uuidV4Mock.mockReturnValue('AAAA-AAAA-AAAA-AAAA'); - const newCustomNetworkClient = buildFakeClient(); - mockCreateNetworkClientWithDefaultsForBuiltInNetworkClients({ - infuraProjectId: 'some-infura-project-id', - }) - .calledWith({ - chainId: toHex(111), - rpcUrl: 'https://test.network', - type: NetworkClientType.Custom, - }) - .mockReturnValue(newCustomNetworkClient); - - await controller.upsertNetworkConfiguration( - { - rpcUrl: 'https://test.network', - chainId: toHex(111), - ticker: 'TICKER', - }, - { - referrer: 'https://test-dapp.com', - source: 'dapp', - }, - ); - - const networkClients = controller.getNetworkClientRegistry(); - expect(Object.keys(networkClients)).toHaveLength(6); - expect(networkClients).toMatchObject({ - 'AAAA-AAAA-AAAA-AAAA': expect.objectContaining({ - configuration: { - chainId: toHex(111), - rpcUrl: 'https://test.network', - type: NetworkClientType.Custom, - }, - }), - }); - }, - ); - }); - - describe('if the setActive option is not given', () => { - it('does not update the provider config to the new network configuration by default', async () => { - const originalProvider = { - type: NetworkType.rpc, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TICKER', - id: 'testNetworkConfigurationId', - }; - - await withController( - { - state: { - providerConfig: originalProvider, - }, - }, - async ({ controller }) => { - uuidV4Mock.mockReturnValue('AAAA-AAAA-AAAA-AAAA'); - - await controller.upsertNetworkConfiguration( - { - rpcUrl: 'https://test.network', - chainId: toHex(111), - ticker: 'TICKER', - }, - { - referrer: 'https://test-dapp.com', - source: 'dapp', - }, - ); - - expect(controller.state.providerConfig).toStrictEqual( - originalProvider, - ); - }, - ); - }); - - it('does not set the new network to active by default', async () => { - await withController( - { infuraProjectId: 'some-infura-project-id' }, - async ({ controller }) => { - uuidV4Mock.mockReturnValue('AAAA-AAAA-AAAA-AAAA'); - const builtInNetworkProvider = buildFakeProvider([ - { - request: { - method: 'test_method', - params: [], - }, - response: { - result: 'test response from built-in network', - }, - }, - ]); - const builtInNetworkClient = buildFakeClient( - builtInNetworkProvider, - ); - const newCustomNetworkProvider = buildFakeProvider([ - { - request: { - method: 'test_method', - params: [], - }, - response: { - result: 'test response from custom network', - }, - }, - ]); - const newCustomNetworkClient = buildFakeClient( - newCustomNetworkProvider, - ); - mockCreateNetworkClientWithDefaultsForBuiltInNetworkClients({ - builtInNetworkClient, - infuraProjectId: 'some-infura-project-id', - }) - .calledWith({ - chainId: toHex(111), - rpcUrl: 'https://test.network', - type: NetworkClientType.Custom, - }) - .mockReturnValue(newCustomNetworkClient); - // Will use mainnet by default - await controller.initializeProvider(); - - await controller.upsertNetworkConfiguration( - { - rpcUrl: 'https://test.network', - chainId: toHex(111), - ticker: 'TICKER', - }, - { - referrer: 'https://test-dapp.com', - source: 'dapp', - }, - ); - - const { provider } = controller.getProviderAndBlockTracker(); - assert(provider, 'Provider is not set'); - const { result } = await promisify(provider.sendAsync).call( - provider, - { - id: 1, - jsonrpc: '2.0', - method: 'test_method', - params: [], - }, - ); - expect(result).toBe('test response from built-in network'); - }, - ); - }); - }); - - describe('if the setActive option is false', () => { - it('does not update the provider config to the new network configuration by default', async () => { - const originalProvider = { - type: NetworkType.rpc, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TICKER', - id: 'testNetworkConfigurationId', - }; - - await withController( - { - state: { - providerConfig: originalProvider, - }, - }, - async ({ controller }) => { - uuidV4Mock.mockReturnValue('AAAA-AAAA-AAAA-AAAA'); - - await controller.upsertNetworkConfiguration( - { - rpcUrl: 'https://test.network', - chainId: toHex(111), - ticker: 'TICKER', - }, - { - setActive: false, - referrer: 'https://test-dapp.com', - source: 'dapp', - }, - ); - - expect(controller.state.providerConfig).toStrictEqual( - originalProvider, - ); - }, - ); - }); - - it('does not set the new network to active by default', async () => { - await withController( - { infuraProjectId: 'some-infura-project-id' }, - async ({ controller }) => { - uuidV4Mock.mockReturnValue('AAAA-AAAA-AAAA-AAAA'); - const builtInNetworkProvider = buildFakeProvider([ - { - request: { - method: 'test_method', - params: [], - }, - response: { - result: 'test response from built-in network', - }, - }, - ]); - const builtInNetworkClient = buildFakeClient( - builtInNetworkProvider, - ); - const newCustomNetworkProvider = buildFakeProvider([ - { - request: { - method: 'test_method', - params: [], - }, - response: { - result: 'test response from custom network', - }, - }, - ]); - const newCustomNetworkClient = buildFakeClient( - newCustomNetworkProvider, - ); - mockCreateNetworkClientWithDefaultsForBuiltInNetworkClients({ - builtInNetworkClient, - infuraProjectId: 'some-infura-project-id', - }) - .calledWith({ - chainId: toHex(111), - rpcUrl: 'https://test.network', - type: NetworkClientType.Custom, - }) - .mockReturnValue(newCustomNetworkClient); - // Will use mainnet by default - await controller.initializeProvider(); - - await controller.upsertNetworkConfiguration( - { - rpcUrl: 'https://test.network', - chainId: toHex(111), - ticker: 'TICKER', - }, - { - setActive: false, - referrer: 'https://test-dapp.com', - source: 'dapp', - }, - ); - - const { provider } = controller.getProviderAndBlockTracker(); - assert(provider, 'Provider is not set'); - const { result } = await promisify(provider.sendAsync).call( - provider, - { - id: 1, - jsonrpc: '2.0', - method: 'test_method', - params: [], - }, - ); - expect(result).toBe('test response from built-in network'); - }, - ); - }); - }); - - describe('if the setActive option is true', () => { - it('updates the provider config to the new network configuration', async () => { - await withController(async ({ controller }) => { - uuidV4Mock.mockReturnValue('AAAA-AAAA-AAAA-AAAA'); - const newCustomNetworkClient = buildFakeClient(); - mockCreateNetworkClientWithDefaultsForBuiltInNetworkClients() - .calledWith({ - chainId: toHex(111), - rpcUrl: 'https://test.network', - type: NetworkClientType.Custom, - }) - .mockReturnValue(newCustomNetworkClient); - - await controller.upsertNetworkConfiguration( - { - rpcUrl: 'https://test.network', - chainId: toHex(111), - ticker: 'TICKER', - nickname: 'test network', - rpcPrefs: { - blockExplorerUrl: 'https://some.chainscan.io', - }, - }, - { - setActive: true, - referrer: 'https://test-dapp.com', - source: 'dapp', - }, - ); - - expect(controller.state.providerConfig).toStrictEqual({ - type: NetworkType.rpc, - rpcUrl: 'https://test.network', - chainId: toHex(111), - ticker: 'TICKER', - nickname: 'test network', - rpcPrefs: { - blockExplorerUrl: 'https://some.chainscan.io', - }, - id: 'AAAA-AAAA-AAAA-AAAA', - }); - }); - }); - - refreshNetworkTests({ - expectedProviderConfig: { - type: NetworkType.rpc, - rpcUrl: 'https://some.other.network', - chainId: toHex(222), - ticker: 'TICKER2', - id: 'BBBB-BBBB-BBBB-BBBB', - nickname: undefined, - rpcPrefs: undefined, - }, - initialState: { - networkConfigurations: { - 'AAAA-AAAA-AAAA-AAAA': { - rpcUrl: 'https://test.network', - chainId: toHex(111), - ticker: 'TICKER1', - id: 'AAAA-AAAA-AAAA-AAAA', - }, - }, - }, - operation: async (controller) => { - uuidV4Mock.mockReturnValue('BBBB-BBBB-BBBB-BBBB'); - - await controller.upsertNetworkConfiguration( - { - rpcUrl: 'https://some.other.network', - chainId: toHex(222), - ticker: 'TICKER2', - }, - { - setActive: true, - referrer: 'https://test-dapp.com', - source: 'dapp', - }, - ); - }, - }); - }); - - it('calls trackMetaMetricsEvent with details about the new network', async () => { - const trackMetaMetricsEventSpy = jest.fn(); - - await withController( - { - trackMetaMetricsEvent: trackMetaMetricsEventSpy, - }, - async ({ controller }) => { - uuidV4Mock.mockReturnValue('AAAA-AAAA-AAAA-AAAA'); - - await controller.upsertNetworkConfiguration( - { - rpcUrl: 'https://test.network', - chainId: toHex(111), - ticker: 'TICKER', - }, - { - referrer: 'https://test-dapp.com', - source: 'dapp', - }, - ); - - expect(trackMetaMetricsEventSpy).toHaveBeenCalledWith({ - event: 'Custom Network Added', - category: 'Network', - referrer: { - url: 'https://test-dapp.com', - }, - properties: { - chain_id: toHex(111), - symbol: 'TICKER', - source: 'dapp', - }, - }); - }, - ); - }); - }); - - describe.each([ - ['case-sensitively', 'https://test.network', 'https://test.network'], - ['case-insensitively', 'https://test.network', 'https://TEST.NETWORK'], - ])( - 'when the rpcUrl of the given network configuration matches an existing network configuration in state (%s)', - (_qualifier, oldRpcUrl, newRpcUrl) => { - it('completely overwrites the existing network configuration in state, but does not update or remove any other network configurations', async () => { - await withController( - { - state: { - networkConfigurations: { - 'AAAA-AAAA-AAAA-AAAA': { - rpcUrl: 'https://test.network.1', - chainId: toHex(111), - ticker: 'TICKER1', - id: 'AAAA-AAAA-AAAA-AAAA', - }, - 'BBBB-BBBB-BBBB-BBBB': { - rpcUrl: oldRpcUrl, - chainId: toHex(222), - ticker: 'TICKER2', - id: 'BBBB-BBBB-BBBB-BBBB', - }, - }, - }, - }, - async ({ controller }) => { - await controller.upsertNetworkConfiguration( - { - rpcUrl: newRpcUrl, - chainId: toHex(999), - ticker: 'NEW_TICKER', - nickname: 'test network 2', - rpcPrefs: { - blockExplorerUrl: 'https://testchainscan.io', - }, - }, - { - referrer: 'https://test-dapp.com', - source: 'dapp', - }, - ); - - expect(controller.state.networkConfigurations).toStrictEqual({ - 'AAAA-AAAA-AAAA-AAAA': { - rpcUrl: 'https://test.network.1', - chainId: toHex(111), - ticker: 'TICKER1', - id: 'AAAA-AAAA-AAAA-AAAA', - }, - 'BBBB-BBBB-BBBB-BBBB': { - rpcUrl: newRpcUrl, - chainId: toHex(999), - ticker: 'NEW_TICKER', - nickname: 'test network 2', - rpcPrefs: { - blockExplorerUrl: 'https://testchainscan.io', - }, - id: 'BBBB-BBBB-BBBB-BBBB', - }, - }); - }, - ); - }); - - it('removes properties not specific to the NetworkConfiguration interface before persisting it to state', async function () { - await withController( - { - state: { - networkConfigurations: { - 'AAAA-AAAA-AAAA-AAAA': { - rpcUrl: oldRpcUrl, - chainId: toHex(111), - ticker: 'TICKER', - id: 'AAAA-AAAA-AAAA-AAAA', - }, - }, - }, - }, - async ({ controller }) => { - await controller.upsertNetworkConfiguration( - { - rpcUrl: newRpcUrl, - chainId: toHex(999), - ticker: 'NEW_TICKER', - nickname: 'test network', - rpcPrefs: { - blockExplorerUrl: 'https://testchainscan.io', - }, - // @ts-expect-error We are intentionally passing bad input. - invalidKey: 'some value', - }, - { - referrer: 'https://test-dapp.com', - source: 'dapp', - }, - ); - - expect(controller.state.networkConfigurations).toStrictEqual({ - 'AAAA-AAAA-AAAA-AAAA': { - rpcUrl: newRpcUrl, - chainId: toHex(999), - ticker: 'NEW_TICKER', - nickname: 'test network', - rpcPrefs: { - blockExplorerUrl: 'https://testchainscan.io', - }, - id: 'AAAA-AAAA-AAAA-AAAA', - }, - }); - }, - ); - }); - - describe('if at least the chain ID is being updated', () => { - it('destroys and removes the existing network client for the old network configuration', async () => { - await withController( - { - state: { - networkConfigurations: { - 'AAAA-AAAA-AAAA-AAAA': { - rpcUrl: oldRpcUrl, - chainId: toHex(111), - ticker: 'TICKER', - id: 'AAAA-AAAA-AAAA-AAAA', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const newCustomNetworkClient = buildFakeClient(); - mockCreateNetworkClientWithDefaultsForBuiltInNetworkClients({ - infuraProjectId: 'some-infura-project-id', - }) - .calledWith({ - chainId: toHex(111), - rpcUrl: 'https://test.network', - type: NetworkClientType.Custom, - }) - .mockReturnValue(newCustomNetworkClient); - const networkClientToDestroy = Object.values( - controller.getNetworkClientRegistry(), - ).find(({ configuration }) => { - return ( - configuration.type === NetworkClientType.Custom && - configuration.chainId === toHex(111) && - configuration.rpcUrl === 'https://test.network' - ); - }); - assert(networkClientToDestroy); - jest.spyOn(networkClientToDestroy, 'destroy'); - - await controller.upsertNetworkConfiguration( - { - rpcUrl: newRpcUrl, - chainId: toHex(999), - ticker: 'TICKER', - }, - { - referrer: 'https://test-dapp.com', - source: 'dapp', - }, - ); - - const networkClients = controller.getNetworkClientRegistry(); - expect(networkClientToDestroy.destroy).toHaveBeenCalled(); - expect(Object.keys(networkClients)).toHaveLength(6); - expect(networkClients).not.toMatchObject({ - [oldRpcUrl]: expect.objectContaining({ - configuration: { - chainId: toHex(111), - rpcUrl: oldRpcUrl, - type: NetworkClientType.Custom, - }, - }), - }); - }, - ); - }); - - it('creates a new network client for the network configuration and adds it to the registry', async () => { - await withController( - { - state: { - networkConfigurations: { - 'AAAA-AAAA-AAAA-AAAA': { - rpcUrl: oldRpcUrl, - chainId: toHex(111), - ticker: 'TICKER', - id: 'AAAA-AAAA-AAAA-AAAA', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const newCustomNetworkClient = buildFakeClient(); - mockCreateNetworkClientWithDefaultsForBuiltInNetworkClients({ - infuraProjectId: 'some-infura-project-id', - }) - .calledWith({ - chainId: toHex(999), - rpcUrl: newRpcUrl, - type: NetworkClientType.Custom, - }) - .mockReturnValue(newCustomNetworkClient); - - await controller.upsertNetworkConfiguration( - { - rpcUrl: newRpcUrl, - chainId: toHex(999), - ticker: 'TICKER', - }, - { - referrer: 'https://test-dapp.com', - source: 'dapp', - }, - ); - - const networkClients = controller.getNetworkClientRegistry(); - expect(Object.keys(networkClients)).toHaveLength(6); - expect(networkClients).toMatchObject({ - 'AAAA-AAAA-AAAA-AAAA': expect.objectContaining({ - configuration: { - chainId: toHex(999), - rpcUrl: newRpcUrl, - type: NetworkClientType.Custom, - }, - }), - }); - }, - ); - }); - }); - - describe('if the chain ID is not being updated', () => { - it('does not update the network client registry', async () => { - await withController( - { - state: { - networkConfigurations: { - 'AAAA-AAAA-AAAA-AAAA': { - rpcUrl: oldRpcUrl, - chainId: toHex(111), - ticker: 'TICKER', - id: 'AAAA-AAAA-AAAA-AAAA', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const newCustomNetworkClient = buildFakeClient(); - mockCreateNetworkClientWithDefaultsForBuiltInNetworkClients({ - infuraProjectId: 'some-infura-project-id', - }) - .calledWith({ - chainId: toHex(111), - rpcUrl: 'https://test.network', - type: NetworkClientType.Custom, - }) - .mockReturnValue(newCustomNetworkClient); - const networkClientsBefore = - controller.getNetworkClientRegistry(); - - await controller.upsertNetworkConfiguration( - { - rpcUrl: newRpcUrl, - chainId: toHex(111), - ticker: 'NEW_TICKER', - }, - { - referrer: 'https://test-dapp.com', - source: 'dapp', - }, - ); - - const networkClientsAfter = - controller.getNetworkClientRegistry(); - expect(networkClientsBefore).toStrictEqual(networkClientsAfter); - }, - ); - }); - }); - - it('does not call trackMetaMetricsEvent', async () => { - const trackMetaMetricsEventSpy = jest.fn(); - - await withController( - { - state: { - networkConfigurations: { - 'AAAA-AAAA-AAAA-AAAA': { - rpcUrl: oldRpcUrl, - chainId: toHex(111), - ticker: 'TICKER', - id: 'AAAA-AAAA-AAAA-AAAA', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - trackMetaMetricsEvent: trackMetaMetricsEventSpy, - }, - async ({ controller }) => { - await controller.upsertNetworkConfiguration( - { - rpcUrl: newRpcUrl, + networkConfigurations: { + testNetworkConfigurationId1: { + rpcUrl: 'https://mock-rpc-url', chainId: toHex(111), - ticker: 'NEW_TICKER', + ticker: 'TEST', + nickname: 'something existing', + id: 'testNetworkConfigurationId1', + rpcPrefs: undefined, }, - { - referrer: 'https://test-dapp.com', - source: 'dapp', + testNetworkConfigurationId2: { + rpcUrl: 'http://somethingexisting.com', + chainId: undefined, + ticker: 'something existing', + nickname: 'something existing', + id: 'testNetworkConfigurationId2', + rpcPrefs: undefined, }, - ); - - expect(trackMetaMetricsEventSpy).not.toHaveBeenCalled(); - }, - ); - }); - }, - ); - - it('throws if the given chain ID is not a 0x-prefixed hex number', async () => { - await withController(async ({ controller }) => { - await expect( - controller.upsertNetworkConfiguration( - { - rpcUrl: 'https://test.network', - // @ts-expect-error We are intentionally passing bad input. - chainId: '1', - ticker: 'TICKER', - }, - { - referrer: 'https://test-dapp.com', - source: 'dapp', + }, }, - ), - ).rejects.toThrow( - new Error('Value must be a hexadecimal string, starting with "0x".'), - ); - }); - }); + }, + async ({ controller }) => { + const fakeProvider = buildFakeProvider(); + const fakeNetworkClient = buildFakeClient(fakeProvider); + createNetworkClientMock.mockReturnValue(fakeNetworkClient); - it('throws if the given chain ID is greater than the maximum allowed ID', async () => { - await withController(async ({ controller }) => { - await expect( - controller.upsertNetworkConfiguration( - { - rpcUrl: 'https://test.network', - chainId: toHex(MAX_SAFE_CHAIN_ID + 1), - ticker: 'TICKER', - }, - { - referrer: 'https://test-dapp.com', - source: 'dapp', - }, - ), - ).rejects.toThrow( - new Error( - 'Invalid chain ID "0xfffffffffffed": numerical value greater than max safe value.', - ), - ); - }); - }); + await expect(() => + controller.setActiveNetwork('testNetworkConfigurationId2'), + ).rejects.toThrow( + 'chainId must be provided for custom RPC endpoints', + ); - it('throws if a falsy rpcUrl is given', async () => { - await withController(async ({ controller }) => { - await expect(() => - controller.upsertNetworkConfiguration( - { - // @ts-expect-error We are intentionally passing bad input. - rpcUrl: false, - chainId: toHex(111), - ticker: 'TICKER', - }, - { - referrer: 'https://test-dapp.com', - source: 'dapp', - }, - ), - ).rejects.toThrow( - new Error( - 'An rpcUrl is required to add or update network configuration', - ), + expect(createNetworkClientMock).not.toHaveBeenCalled(); + const { provider, blockTracker } = + controller.getProviderAndBlockTracker(); + expect(provider).toBeUndefined(); + expect(blockTracker).toBeUndefined(); + }, ); }); }); - it('throws if no rpcUrl is given', async () => { - await withController(async ({ controller }) => { - await expect( - controller.upsertNetworkConfiguration( - // @ts-expect-error We are intentionally passing bad input. - { - chainId: toHex(111), - ticker: 'TICKER', - }, - { - referrer: 'https://test-dapp.com', - source: 'dapp', + it('overwrites the provider configuration given a networkConfigurationId that matches a configured networkConfiguration', async () => { + await withController( + { + state: { + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(111), + ticker: 'TEST', + nickname: 'something existing', + id: 'testNetworkConfigurationId', + rpcPrefs: { + blockExplorerUrl: 'https://test-block-explorer-2.com', + }, + }, }, - ), - ).rejects.toThrow( - new Error( - 'An rpcUrl is required to add or update network configuration', - ), - ); - }); - }); - - it('throws if the rpcUrl given is not a valid URL', async () => { - await withController(async ({ controller }) => { - await expect( - controller.upsertNetworkConfiguration( - { - rpcUrl: 'test', + }, + }, + async ({ controller }) => { + const fakeProvider = buildFakeProvider(); + const fakeNetworkClient = buildFakeClient(fakeProvider); + mockCreateNetworkClient() + .calledWith({ + rpcUrl: 'https://mock-rpc-url', chainId: toHex(111), - ticker: 'TICKER', - }, - { - referrer: 'https://test-dapp.com', - source: 'dapp', + type: NetworkClientType.Custom, + }) + .mockReturnValue(fakeNetworkClient); + + await controller.setActiveNetwork('testNetworkConfigurationId'); + + expect(controller.state.providerConfig).toStrictEqual({ + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(111), + ticker: 'TEST', + nickname: 'something existing', + id: 'testNetworkConfigurationId', + rpcPrefs: { + blockExplorerUrl: 'https://test-block-explorer-2.com', }, - ), - ).rejects.toThrow(new Error('rpcUrl must be a valid URL')); - }); + }); + }, + ); }); - it('throws if a falsy referrer is given', async () => { - await withController(async ({ controller }) => { - await expect( - controller.upsertNetworkConfiguration( - { - rpcUrl: 'https://test.network', - chainId: toHex(111), - ticker: 'TICKER', - }, - { - // @ts-expect-error We are intentionally passing bad input. - referrer: false, - source: 'dapp', + it('updates state.selectedNetworkClientId setting it to the networkConfiguration.id', async () => { + const testNetworkClientId = 'testNetworkConfigurationId'; + await withController( + { + state: { + networkConfigurations: { + [testNetworkClientId]: { + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(111), + ticker: 'TEST', + nickname: 'something existing', + id: testNetworkClientId, + rpcPrefs: { + blockExplorerUrl: 'https://test-block-explorer-2.com', + }, + }, }, - ), - ).rejects.toThrow( - new Error( - 'referrer and source are required arguments for adding or updating a network configuration', - ), - ); - }); + }, + }, + async ({ controller }) => { + const fakeProvider = buildFakeProvider(); + const fakeNetworkClient = buildFakeClient(fakeProvider); + mockCreateNetworkClient() + .calledWith({ + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(111), + type: NetworkClientType.Custom, + }) + .mockReturnValue(fakeNetworkClient); + + await controller.setActiveNetwork(testNetworkClientId); + + expect(controller.state.selectedNetworkClientId).toStrictEqual( + testNetworkClientId, + ); + }, + ); }); + }); - it('throws if no referrer is given', async () => { - await withController(async ({ controller }) => { - await expect( - controller.upsertNetworkConfiguration( - { - rpcUrl: 'https://test.network', - chainId: toHex(111), - ticker: 'TICKER', - }, - // @ts-expect-error We are intentionally passing bad input. - { - source: 'dapp', + describe('getEIP1559Compatibility', () => { + describe('if no provider has been set yet', () => { + it('does not make any state changes', async () => { + await withController(async ({ controller, messenger }) => { + const promiseForNoStateChanges = waitForStateChanges({ + messenger, + count: 0, + operation: async () => { + await controller.getEIP1559Compatibility(); }, - ), - ).rejects.toThrow( - new Error( - 'referrer and source are required arguments for adding or updating a network configuration', - ), - ); + }); + + expect(Boolean(promiseForNoStateChanges)).toBe(true); + }); }); - }); - it('throws if a falsy source is given', async () => { - await withController(async ({ controller }) => { - await expect( - controller.upsertNetworkConfiguration( - { - rpcUrl: 'https://test.network', - chainId: toHex(111), - ticker: 'TICKER', - }, - { - referrer: 'https://test-dapp.com', - // @ts-expect-error We are intentionally passing bad input. - source: false, - }, - ), - ).rejects.toThrow( - new Error( - 'referrer and source are required arguments for adding or updating a network configuration', - ), - ); + it('returns false', async () => { + await withController(async ({ controller }) => { + const isEIP1559Compatible = + await controller.getEIP1559Compatibility(); + + expect(isEIP1559Compatible).toBe(false); + }); }); }); - it('throws if no source is given', async () => { - await withController(async ({ controller }) => { - await expect( - controller.upsertNetworkConfiguration( - { - rpcUrl: 'https://test.network', - chainId: toHex(111), - ticker: 'TICKER', - }, - // @ts-expect-error We are intentionally passing bad input. - { - referrer: 'https://test-dapp.com', + describe('if a networkClientId is passed in', () => { + it('uses the built in state for networksMetadata', async () => { + await withController( + { + state: { + networksMetadata: { + 'linea-mainnet': { + EIPS: { + 1559: true, + }, + status: NetworkStatus.Unknown, + }, + }, }, - ), - ).rejects.toThrow( - new Error( - 'referrer and source are required arguments for adding or updating a network configuration', - ), + }, + async ({ controller }) => { + const isEIP1559Compatible = + await controller.getEIP1559Compatibility('linea-mainnet'); + + expect(isEIP1559Compatible).toBe(true); + }, ); }); - }); - - it('throws if a falsy ticker is given', async () => { - await withController(async ({ controller }) => { - await expect( - controller.upsertNetworkConfiguration( - { - rpcUrl: 'https://test.network', - chainId: toHex(111), - // @ts-expect-error We are intentionally passing bad input. - ticker: false, - }, - { - referrer: 'https://test-dapp.com', - source: 'dapp', + it('uses the built in false state for networksMetadata', async () => { + await withController( + { + state: { + networksMetadata: { + 'linea-mainnet': { + EIPS: { + 1559: false, + }, + status: NetworkStatus.Unknown, + }, + }, }, - ), - ).rejects.toThrow( - new Error( - 'A ticker is required to add or update networkConfiguration', - ), + }, + async ({ controller }) => { + const isEIP1559Compatible = + await controller.getEIP1559Compatibility('linea-mainnet'); + + expect(isEIP1559Compatible).toBe(false); + }, ); }); - }); - - it('throws if no ticker is given', async () => { - await withController(async ({ controller }) => { - await expect( - controller.upsertNetworkConfiguration( - // @ts-expect-error We are intentionally passing bad input. - { - rpcUrl: 'https://test.network', - chainId: toHex(111), - }, - { - referrer: 'https://test-dapp.com', - source: 'dapp', - }, - ), - ).rejects.toThrow( - new Error( - 'A ticker is required to add or update networkConfiguration', - ), + it('calls provider of the networkClientId and returns true', async () => { + await withController( + { + infuraProjectId: 'some-infura-project-id', + }, + async ({ controller }) => { + await setFakeProvider(controller, { + stubs: [ + { + request: { + method: 'eth_getBlockByNumber', + params: ['latest', false], + }, + response: { + result: POST_1559_BLOCK, + }, + }, + { + request: { + method: 'eth_getBlockByNumber', + params: ['latest', false], + }, + response: { + result: POST_1559_BLOCK, + }, + }, + ], + }); + const isEIP1559Compatible = + await controller.getEIP1559Compatibility('linea-mainnet'); + expect(isEIP1559Compatible).toBe(true); + }, ); }); }); - }); - describe('removeNetworkConfiguration', () => { - describe('given an ID that identifies a network configuration in state', () => { - it('removes the network configuration from state', async () => { + describe('if a provider has been set but networksMetadata[selectedNetworkClientId].EIPS in state already has a "1559" property', () => { + it('does not make any state changes', async () => { await withController( { state: { - networkConfigurations: { - 'AAAA-AAAA-AAAA-AAAA': { - rpcUrl: 'https://test.network', - ticker: 'TICKER', - chainId: toHex(111), - id: 'AAAA-AAAA-AAAA-AAAA', + networksMetadata: { + mainnet: { + EIPS: { + 1559: true, + }, + status: NetworkStatus.Unknown, }, }, }, }, - async ({ controller }) => { - controller.removeNetworkConfiguration('AAAA-AAAA-AAAA-AAAA'); + async ({ controller, messenger }) => { + setFakeProvider(controller, { + stubLookupNetworkWhileSetting: true, + }); + const promiseForNoStateChanges = waitForStateChanges({ + messenger, + count: 0, + operation: async () => { + await controller.getEIP1559Compatibility(); + }, + }); - expect(controller.state.networkConfigurations).toStrictEqual({}); + expect(Boolean(promiseForNoStateChanges)).toBe(true); }, ); }); - it('destroys and removes the network client in the network client registry that corresponds to the given ID', async () => { + it('returns the value of the "1559" property', async () => { await withController( { state: { - networkConfigurations: { - 'AAAA-AAAA-AAAA-AAAA': { - rpcUrl: 'https://test.network', - ticker: 'TICKER', - chainId: toHex(111), - id: 'AAAA-AAAA-AAAA-AAAA', + networksMetadata: { + mainnet: { + EIPS: { + 1559: true, + }, + status: NetworkStatus.Unknown, }, }, }, }, async ({ controller }) => { - mockCreateNetworkClientWithDefaultsForBuiltInNetworkClients() - .calledWith({ - chainId: toHex(111), - rpcUrl: 'https://test.network', - type: NetworkClientType.Custom, - }) - .mockReturnValue(buildFakeClient()); - const networkClientToDestroy = Object.values( - controller.getNetworkClientRegistry(), - ).find(({ configuration }) => { - return ( - configuration.type === NetworkClientType.Custom && - configuration.chainId === toHex(111) && - configuration.rpcUrl === 'https://test.network' - ); + setFakeProvider(controller, { + stubLookupNetworkWhileSetting: true, }); - assert(networkClientToDestroy); - jest.spyOn(networkClientToDestroy, 'destroy'); - controller.removeNetworkConfiguration('AAAA-AAAA-AAAA-AAAA'); + const isEIP1559Compatible = + await controller.getEIP1559Compatibility(); - expect(networkClientToDestroy.destroy).toHaveBeenCalled(); - expect(controller.getNetworkClientRegistry()).not.toMatchObject({ - 'https://test.network': expect.objectContaining({ - configuration: { - chainId: toHex(111), - rpcUrl: 'https://test.network', - type: NetworkClientType.Custom, - }, - }), - }); + expect(isEIP1559Compatible).toBe(true); }, ); }); }); - describe('given an ID that does not identify a network configuration in state', () => { - it('throws', async () => { - await withController(async ({ controller }) => { - expect(() => - controller.removeNetworkConfiguration('NONEXISTENT'), - ).toThrow( - `networkConfigurationId NONEXISTENT does not match a configured networkConfiguration`, - ); + describe('if a provider has been set and networksMetadata[selectedNetworkClientId].EIPS in state does not already have a "1559" property', () => { + describe('if the request for the latest block is successful', () => { + describe('if the latest block has a "baseFeePerGas" property', () => { + it('sets the "1559" property to true', async () => { + await withController(async ({ controller }) => { + setFakeProvider(controller, { + stubs: [ + { + request: { + method: 'eth_getBlockByNumber', + params: ['latest', false], + }, + response: { + result: POST_1559_BLOCK, + }, + }, + ], + stubLookupNetworkWhileSetting: true, + }); + + await controller.getEIP1559Compatibility(); + + expect( + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ].EIPS[1559], + ).toBe(true); + }); + }); + + it('returns true', async () => { + await withController(async ({ controller }) => { + setFakeProvider(controller, { + stubs: [ + { + request: { + method: 'eth_getBlockByNumber', + params: ['latest', false], + }, + response: { + result: POST_1559_BLOCK, + }, + }, + ], + stubLookupNetworkWhileSetting: true, + }); + + const isEIP1559Compatible = + await controller.getEIP1559Compatibility(); + + expect(isEIP1559Compatible).toBe(true); + }); + }); + }); + + describe('if the latest block does not have a "baseFeePerGas" property', () => { + it('sets the "1559" property to false', async () => { + await withController(async ({ controller }) => { + setFakeProvider(controller, { + stubs: [ + { + request: { + method: 'eth_getBlockByNumber', + params: ['latest', false], + }, + response: { + result: PRE_1559_BLOCK, + }, + }, + ], + stubLookupNetworkWhileSetting: true, + }); + + await controller.getEIP1559Compatibility(); + + expect( + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ].EIPS[1559], + ).toBe(false); + }); + }); + + it('returns false', async () => { + await withController(async ({ controller }) => { + setFakeProvider(controller, { + stubs: [ + { + request: { + method: 'eth_getBlockByNumber', + params: ['latest', false], + }, + response: { + result: PRE_1559_BLOCK, + }, + }, + ], + stubLookupNetworkWhileSetting: true, + }); + + const isEIP1559Compatible = + await controller.getEIP1559Compatibility(); + + expect(isEIP1559Compatible).toBe(false); + }); + }); + }); + + describe('if the request for the latest block responds with null', () => { + const latestBlockRespondsNull = { + request: { + method: 'eth_getBlockByNumber', + params: ['latest', false], + }, + response: { + result: null, + }, + }; + it('keeps the "1559" property as undefined', async () => { + await withController(async ({ controller }) => { + setFakeProvider(controller, { + stubs: [latestBlockRespondsNull], + stubLookupNetworkWhileSetting: true, + }); + + await controller.getEIP1559Compatibility(); + + expect( + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ].EIPS[1559], + ).toBeUndefined(); + }); + }); + + it('returns undefined', async () => { + await withController(async ({ controller }) => { + setFakeProvider(controller, { + stubs: [latestBlockRespondsNull], + stubLookupNetworkWhileSetting: true, + }); + + const isEIP1559Compatible = + await controller.getEIP1559Compatibility(); + + expect(isEIP1559Compatible).toBeUndefined(); + }); + }); }); }); - it('does not update the network client registry', async () => { - await withController(async ({ controller }) => { - mockCreateNetworkClientWithDefaultsForBuiltInNetworkClients(); - const networkClients = controller.getNetworkClientRegistry(); + describe('if the request for the latest block is unsuccessful', () => { + it('does not make any state changes', async () => { + await withController(async ({ controller, messenger }) => { + setFakeProvider(controller, { + stubs: [ + { + request: { + method: 'eth_getBlockByNumber', + params: ['latest', false], + }, + error: GENERIC_JSON_RPC_ERROR, + }, + ], + stubLookupNetworkWhileSetting: true, + }); - try { - controller.removeNetworkConfiguration('NONEXISTENT'); - } catch { - // ignore error (it is tested elsewhere) - } + const promiseForNoStateChanges = waitForStateChanges({ + messenger, + count: 0, + operation: async () => { + try { + await controller.getEIP1559Compatibility(); + } catch (error) { + // ignore error + } + }, + }); - expect(controller.getNetworkClientRegistry()).toStrictEqual( - networkClients, - ); + expect(Boolean(promiseForNoStateChanges)).toBe(true); + }); }); }); }); }); - describe('rollbackToPreviousProvider', () => { - describe('if a provider has not been set', () => { - [NetworkType.mainnet, NetworkType.goerli, NetworkType.sepolia].forEach( - (networkType) => { - describe(`when the type in the provider configuration is "${networkType}"`, () => { - refreshNetworkTests({ - expectedProviderConfig: buildProviderConfig({ - type: networkType, - }), - initialState: { - providerConfig: buildProviderConfig({ type: networkType }), - }, - operation: async (controller) => { - await controller.rollbackToPreviousProvider(); - }, - }); + describe('resetConnection', () => { + [NetworkType.mainnet, NetworkType.goerli, NetworkType.sepolia].forEach( + (networkType) => { + describe(`when the type in the provider configuration is "${networkType}"`, () => { + refreshNetworkTests({ + expectedProviderConfig: buildProviderConfig({ type: networkType }), + initialState: { + providerConfig: buildProviderConfig({ type: networkType }), + }, + operation: async (controller) => { + await controller.resetConnection(); + }, }); - }, - ); - - describe(`when the type in the provider configuration is "rpc"`, () => { - refreshNetworkTests({ - expectedProviderConfig: buildProviderConfig({ - type: NetworkType.rpc, - }), - initialState: { - providerConfig: buildProviderConfig({ type: NetworkType.rpc }), - }, - operation: async (controller) => { - await controller.rollbackToPreviousProvider(); - }, }); + }, + ); + + describe(`when the type in the provider configuration is "rpc"`, () => { + refreshNetworkTests({ + expectedProviderConfig: buildProviderConfig({ type: NetworkType.rpc }), + initialState: { + providerConfig: buildProviderConfig({ type: NetworkType.rpc }), + }, + operation: async (controller) => { + await controller.resetConnection(); + }, }); }); + }); - describe('if a provider has been set', () => { - for (const { networkType } of INFURA_NETWORKS) { - describe(`if the previous provider configuration had a type of "${networkType}"`, () => { - it('emits networkWillChange', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - networkConfigurations: { - testNetworkConfiguration: { - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - nickname: 'test network', - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer.com', - }, - }, - }, - }, - }, - async ({ controller, messenger }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - await controller.setActiveNetwork('testNetworkConfiguration'); - - const networkWillChange = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:networkWillChange', - operation: () => { - // Intentionally not awaited because we're capturing an event - // emitted partway through the operation - controller.rollbackToPreviousProvider(); - }, - }); + describe('NetworkController:getProviderConfig action', () => { + it('returns the provider config in state', async () => { + await withController( + { + state: { + providerConfig: { + type: NetworkType.mainnet, + ...BUILT_IN_NETWORKS.mainnet, + }, + }, + }, + async ({ messenger }) => { + const providerConfig = await messenger.call( + 'NetworkController:getProviderConfig', + ); - await expect(networkWillChange).toBeFulfilled(); - }, - ); + expect(providerConfig).toStrictEqual({ + type: NetworkType.mainnet, + ...BUILT_IN_NETWORKS.mainnet, }); + }, + ); + }); + }); - it('emits networkDidChange', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - networkConfigurations: { - testNetworkConfiguration: { - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - nickname: 'test network', - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer.com', - }, - }, - }, - }, + describe('NetworkController:getEthQuery action', () => { + it('returns a EthQuery object that can be used to make requests to the currently selected network', async () => { + await withController(async ({ controller, messenger }) => { + await setFakeProvider(controller, { + stubs: [ + { + request: { + method: 'test_method', + params: [], }, - async ({ controller, messenger }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - await controller.setActiveNetwork('testNetworkConfiguration'); - - const networkDidChange = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:networkDidChange', - operation: () => { - // Intentionally not awaited because we're capturing an event - // emitted partway through the operation - controller.rollbackToPreviousProvider(); - }, - }); - - await expect(networkDidChange).toBeFulfilled(); + response: { + result: 'test response', }, - ); - }); + }, + ], + }); - it('overwrites the the current provider configuration with the previous provider configuration', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - networkConfigurations: { - testNetworkConfiguration: { - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - nickname: 'test network', - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer.com', - }, - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider(), - buildFakeProvider(), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[networkType].chainId, - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setActiveNetwork('testNetworkConfiguration'); - expect(controller.state.providerConfig).toStrictEqual({ - type: 'rpc', - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - nickname: 'test network', - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer.com', - }, - }); + const ethQuery = messenger.call('NetworkController:getEthQuery'); + assert(ethQuery, 'ethQuery is not set'); + + const promisifiedSendAsync = promisify(ethQuery.sendAsync).bind( + ethQuery, + ); + const result = await promisifiedSendAsync({ + id: 1, + jsonrpc: '2.0', + method: 'test_method', + params: [], + }); + expect(result).toBe('test response'); + }); + }); - await controller.rollbackToPreviousProvider(); + it('returns undefined if the provider has not been set yet', async () => { + await withController(({ messenger }) => { + const ethQuery = messenger.call('NetworkController:getEthQuery'); - expect(controller.state.providerConfig).toStrictEqual( - buildProviderConfig({ - type: networkType, - }), - ); + expect(ethQuery).toBeUndefined(); + }); + }); + }); + + describe('upsertNetworkConfiguration', () => { + describe('when the rpcUrl of the given network configuration does not match an existing network configuration', () => { + it('adds the network configuration to state without updating or removing any existing network configurations', async () => { + await withController( + { + state: { + networkConfigurations: { + 'AAAA-AAAA-AAAA-AAAA': { + rpcUrl: 'https://test.network.1', + chainId: toHex(111), + ticker: 'TICKER1', + id: 'AAAA-AAAA-AAAA-AAAA', + }, }, - ); - }); + }, + }, + async ({ controller }) => { + uuidV4Mock.mockReturnValue('BBBB-BBBB-BBBB-BBBB'); - it('resets the network status to "unknown" before updating the provider', async () => { - await withController( + await controller.upsertNetworkConfiguration( { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - networkConfigurations: { - testNetworkConfiguration: { - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - }, - }, + rpcUrl: 'https://test.network.2', + chainId: toHex(222), + ticker: 'TICKER2', + nickname: 'test network 2', + rpcPrefs: { + blockExplorerUrl: 'https://testchainscan.io', }, - infuraProjectId: 'some-infura-project-id', }, - async ({ controller, messenger }) => { - const fakeProviders = [ - buildFakeProvider([ - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - ]), - buildFakeProvider(), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: networkType, - chainId: BUILT_IN_NETWORKS[networkType].chainId, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setActiveNetwork('testNetworkConfiguration'); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].status, - ).toBe('available'); - - await waitForStateChanges({ - messenger, - propertyPath: ['networksMetadata', networkType, 'status'], - // We only care about the first state change, because it - // happens before networkDidChange - count: 1, - operation: () => { - // Intentionally not awaited because we want to check state - // while this operation is in-progress - controller.rollbackToPreviousProvider(); - }, - beforeResolving: () => { - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].status, - ).toBe('unknown'); - }, - }); + { + referrer: 'https://test-dapp.com', + source: 'dapp', }, ); - }); - it(`initializes a provider pointed to the "${networkType}" Infura network`, async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - networkConfigurations: { - testNetworkConfiguration: { - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - }, - }, + expect(controller.state.networkConfigurations).toStrictEqual({ + 'AAAA-AAAA-AAAA-AAAA': { + rpcUrl: 'https://test.network.1', + chainId: toHex(111), + ticker: 'TICKER1', + id: 'AAAA-AAAA-AAAA-AAAA', + }, + 'BBBB-BBBB-BBBB-BBBB': { + rpcUrl: 'https://test.network.2', + chainId: toHex(222), + ticker: 'TICKER2', + nickname: 'test network 2', + rpcPrefs: { + blockExplorerUrl: 'https://testchainscan.io', }, - infuraProjectId: 'some-infura-project-id', + id: 'BBBB-BBBB-BBBB-BBBB', }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider(), - buildFakeProvider([ - { - request: { - method: 'test', - }, - response: { - result: 'test response', - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[networkType].chainId, - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setActiveNetwork('testNetworkConfiguration'); + }); + }, + ); + }); - await controller.rollbackToPreviousProvider(); + it('removes properties not specific to the NetworkConfiguration interface before persisting it to state', async function () { + await withController(async ({ controller }) => { + uuidV4Mock.mockReturnValue('AAAA-AAAA-AAAA-AAAA'); - const { provider } = controller.getProviderAndBlockTracker(); - assert(provider, 'Provider is somehow unset'); - const promisifiedSendAsync = promisify(provider.sendAsync).bind( - provider, - ); - const response = await promisifiedSendAsync({ - id: '1', - jsonrpc: '2.0', - method: 'test', - }); - expect(response.result).toBe('test response'); + await controller.upsertNetworkConfiguration( + { + rpcUrl: 'https://test.network', + chainId: toHex(111), + ticker: 'TICKER', + nickname: 'test network', + rpcPrefs: { + blockExplorerUrl: 'https://testchainscan.io', + }, + // @ts-expect-error We are intentionally passing bad input. + invalidKey: 'some value', + }, + { + referrer: 'https://test-dapp.com', + source: 'dapp', + }, + ); + + expect(controller.state.networkConfigurations).toStrictEqual({ + 'AAAA-AAAA-AAAA-AAAA': { + rpcUrl: 'https://test.network', + chainId: toHex(111), + ticker: 'TICKER', + nickname: 'test network', + rpcPrefs: { + blockExplorerUrl: 'https://testchainscan.io', + }, + id: 'AAAA-AAAA-AAAA-AAAA', + }, + }); + }); + }); + + it('creates a new network client for the network configuration and adds it to the registry', async () => { + await withController( + { infuraProjectId: 'some-infura-project-id' }, + async ({ controller }) => { + uuidV4Mock.mockReturnValue('AAAA-AAAA-AAAA-AAAA'); + const newCustomNetworkClient = buildFakeClient(); + mockCreateNetworkClientWithDefaultsForBuiltInNetworkClients({ + infuraProjectId: 'some-infura-project-id', + }) + .calledWith({ + chainId: toHex(111), + rpcUrl: 'https://test.network', + type: NetworkClientType.Custom, + }) + .mockReturnValue(newCustomNetworkClient); + + await controller.upsertNetworkConfiguration( + { + rpcUrl: 'https://test.network', + chainId: toHex(111), + ticker: 'TICKER', + }, + { + referrer: 'https://test-dapp.com', + source: 'dapp', }, ); - }); - it('replaces the provider object underlying the provider proxy without creating a new instance of the proxy itself', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - networkConfigurations: { - testNetworkConfiguration: { - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - }, - }, + const networkClients = controller.getNetworkClientRegistry(); + expect(Object.keys(networkClients)).toHaveLength(6); + expect(networkClients).toMatchObject({ + 'AAAA-AAAA-AAAA-AAAA': expect.objectContaining({ + configuration: { + chainId: toHex(111), + rpcUrl: 'https://test.network', + type: NetworkClientType.Custom, }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider(), - buildFakeProvider(), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[networkType].chainId, - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setActiveNetwork('testNetworkConfiguration'); - const { provider: providerBefore } = - controller.getProviderAndBlockTracker(); + }), + }); + }, + ); + }); - await controller.rollbackToPreviousProvider(); + describe('if the setActive option is not given', () => { + it('does not update the provider config to the new network configuration by default', async () => { + const originalProvider = { + type: NetworkType.rpc, + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(111), + ticker: 'TICKER', + id: 'testNetworkConfigurationId', + }; - const { provider: providerAfter } = - controller.getProviderAndBlockTracker(); - expect(providerBefore).toBe(providerAfter); + await withController( + { + state: { + providerConfig: originalProvider, }, - ); - }); + }, + async ({ controller }) => { + uuidV4Mock.mockReturnValue('AAAA-AAAA-AAAA-AAAA'); - it('emits infuraIsBlocked or infuraIsUnblocked, depending on whether Infura is blocking requests for the previous network', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - networkConfigurations: { - testNetworkConfiguration: { - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - }, + await controller.upsertNetworkConfiguration( + { + rpcUrl: 'https://test.network', + chainId: toHex(111), + ticker: 'TICKER', + }, + { + referrer: 'https://test-dapp.com', + source: 'dapp', + }, + ); + + expect(controller.state.providerConfig).toStrictEqual( + originalProvider, + ); + }, + ); + }); + + it('does not set the new network to active by default', async () => { + await withController( + { infuraProjectId: 'some-infura-project-id' }, + async ({ controller }) => { + uuidV4Mock.mockReturnValue('AAAA-AAAA-AAAA-AAAA'); + const builtInNetworkProvider = buildFakeProvider([ + { + request: { + method: 'test_method', + params: [], + }, + response: { + result: 'test response from built-in network', + }, + }, + ]); + const builtInNetworkClient = buildFakeClient( + builtInNetworkProvider, + ); + const newCustomNetworkProvider = buildFakeProvider([ + { + request: { + method: 'test_method', + params: [], + }, + response: { + result: 'test response from custom network', }, }, + ]); + const newCustomNetworkClient = buildFakeClient( + newCustomNetworkProvider, + ); + mockCreateNetworkClientWithDefaultsForBuiltInNetworkClients({ + builtInNetworkClient, infuraProjectId: 'some-infura-project-id', + }) + .calledWith({ + chainId: toHex(111), + rpcUrl: 'https://test.network', + type: NetworkClientType.Custom, + }) + .mockReturnValue(newCustomNetworkClient); + // Will use mainnet by default + await controller.initializeProvider(); + + await controller.upsertNetworkConfiguration( + { + rpcUrl: 'https://test.network', + chainId: toHex(111), + ticker: 'TICKER', + }, + { + referrer: 'https://test-dapp.com', + source: 'dapp', + }, + ); + + const { provider } = controller.getProviderAndBlockTracker(); + assert(provider, 'Provider is not set'); + const { result } = await promisify(provider.sendAsync).call( + provider, + { + id: 1, + jsonrpc: '2.0', + method: 'test_method', + params: [], + }, + ); + expect(result).toBe('test response from built-in network'); + }, + ); + }); + }); + + describe('if the setActive option is false', () => { + it('does not update the provider config to the new network configuration by default', async () => { + const originalProvider = { + type: NetworkType.rpc, + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(111), + ticker: 'TICKER', + id: 'testNetworkConfigurationId', + }; + + await withController( + { + state: { + providerConfig: originalProvider, }, - async ({ controller, messenger }) => { - const fakeProviders = [ - buildFakeProvider(), - buildFakeProvider([ - { - request: { - method: 'eth_getBlockByNumber', - }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[networkType].chainId, - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setActiveNetwork('testNetworkConfiguration'); - const promiseForNoInfuraIsUnblockedEvents = - waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - count: 0, - }); - const promiseForInfuraIsBlocked = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', - }); + }, + async ({ controller }) => { + uuidV4Mock.mockReturnValue('AAAA-AAAA-AAAA-AAAA'); - await controller.rollbackToPreviousProvider(); + await controller.upsertNetworkConfiguration( + { + rpcUrl: 'https://test.network', + chainId: toHex(111), + ticker: 'TICKER', + }, + { + setActive: false, + referrer: 'https://test-dapp.com', + source: 'dapp', + }, + ); - await expect( - promiseForNoInfuraIsUnblockedEvents, - ).toBeFulfilled(); - await expect(promiseForInfuraIsBlocked).toBeFulfilled(); - }, - ); - }); + expect(controller.state.providerConfig).toStrictEqual( + originalProvider, + ); + }, + ); + }); - it('checks the status of the previous network again and updates state accordingly', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - networkConfigurations: { - testNetworkConfiguration: { - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - }, + it('does not set the new network to active by default', async () => { + await withController( + { infuraProjectId: 'some-infura-project-id' }, + async ({ controller }) => { + uuidV4Mock.mockReturnValue('AAAA-AAAA-AAAA-AAAA'); + const builtInNetworkProvider = buildFakeProvider([ + { + request: { + method: 'test_method', + params: [], + }, + response: { + result: 'test response from built-in network', }, }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller, messenger }) => { - const fakeProviders = [ - buildFakeProvider([ - { - request: { - method: 'net_version', - }, - error: ethErrors.rpc.methodNotFound(), - }, - ]), - buildFakeProvider([ - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[networkType].chainId, - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setActiveNetwork('testNetworkConfiguration'); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].status, - ).toBe('unavailable'); - - await waitForStateChanges({ - messenger, - propertyPath: ['networksMetadata', networkType, 'status'], - operation: async () => { - await controller.rollbackToPreviousProvider(); + ]); + const builtInNetworkClient = buildFakeClient( + builtInNetworkProvider, + ); + const newCustomNetworkProvider = buildFakeProvider([ + { + request: { + method: 'test_method', + params: [], }, - }); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].status, - ).toBe('available'); - }, - ); - }); - - it('checks whether the previous network supports EIP-1559 again and updates state accordingly', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - networkConfigurations: { - testNetworkConfiguration: { - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - }, + response: { + result: 'test response from custom network', }, }, + ]); + const newCustomNetworkClient = buildFakeClient( + newCustomNetworkProvider, + ); + mockCreateNetworkClientWithDefaultsForBuiltInNetworkClients({ + builtInNetworkClient, infuraProjectId: 'some-infura-project-id', - }, - async ({ controller, messenger }) => { - const fakeProviders = [ - buildFakeProvider([ - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: PRE_1559_BLOCK, - }, - }, - ]), - buildFakeProvider([ - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: POST_1559_BLOCK, - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[networkType].chainId, - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setActiveNetwork('testNetworkConfiguration'); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].EIPS[1559], - ).toBe(false); + }) + .calledWith({ + chainId: toHex(111), + rpcUrl: 'https://test.network', + type: NetworkClientType.Custom, + }) + .mockReturnValue(newCustomNetworkClient); + // Will use mainnet by default + await controller.initializeProvider(); - await waitForStateChanges({ - messenger, - propertyPath: ['networksMetadata', networkType, 'EIPS'], - count: 2, - operation: async () => { - await controller.rollbackToPreviousProvider(); - }, - }); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].EIPS[1559], - ).toBe(true); + await controller.upsertNetworkConfiguration( + { + rpcUrl: 'https://test.network', + chainId: toHex(111), + ticker: 'TICKER', + }, + { + setActive: false, + referrer: 'https://test-dapp.com', + source: 'dapp', + }, + ); + + const { provider } = controller.getProviderAndBlockTracker(); + assert(provider, 'Provider is not set'); + const { result } = await promisify(provider.sendAsync).call( + provider, + { + id: 1, + jsonrpc: '2.0', + method: 'test_method', + params: [], + }, + ); + expect(result).toBe('test response from built-in network'); + }, + ); + }); + }); + + describe('if the setActive option is true', () => { + it('updates the provider config to the new network configuration', async () => { + await withController(async ({ controller }) => { + uuidV4Mock.mockReturnValue('AAAA-AAAA-AAAA-AAAA'); + const newCustomNetworkClient = buildFakeClient(); + mockCreateNetworkClientWithDefaultsForBuiltInNetworkClients() + .calledWith({ + chainId: toHex(111), + rpcUrl: 'https://test.network', + type: NetworkClientType.Custom, + }) + .mockReturnValue(newCustomNetworkClient); + + await controller.upsertNetworkConfiguration( + { + rpcUrl: 'https://test.network', + chainId: toHex(111), + ticker: 'TICKER', + nickname: 'test network', + rpcPrefs: { + blockExplorerUrl: 'https://some.chainscan.io', + }, + }, + { + setActive: true, + referrer: 'https://test-dapp.com', + source: 'dapp', }, ); + + expect(controller.state.providerConfig).toStrictEqual({ + type: NetworkType.rpc, + rpcUrl: 'https://test.network', + chainId: toHex(111), + ticker: 'TICKER', + nickname: 'test network', + rpcPrefs: { + blockExplorerUrl: 'https://some.chainscan.io', + }, + id: 'AAAA-AAAA-AAAA-AAAA', + }); }); }); - } - describe(`if the previous provider configuration had a type of "rpc"`, () => { - it('emits networkWillChange', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - }), + refreshNetworkTests({ + expectedProviderConfig: { + type: NetworkType.rpc, + rpcUrl: 'https://some.other.network', + chainId: toHex(222), + ticker: 'TICKER2', + id: 'BBBB-BBBB-BBBB-BBBB', + nickname: undefined, + rpcPrefs: undefined, + }, + initialState: { + networkConfigurations: { + 'AAAA-AAAA-AAAA-AAAA': { + rpcUrl: 'https://test.network', + chainId: toHex(111), + ticker: 'TICKER1', + id: 'AAAA-AAAA-AAAA-AAAA', }, }, - async ({ controller, messenger }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - await controller.setProviderType(InfuraNetworkType.goerli); - - const networkWillChange = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:networkWillChange', - operation: () => { - // Intentionally not awaited because we're capturing an event - // emitted partway through the operation - controller.rollbackToPreviousProvider(); - }, - }); + }, + operation: async (controller) => { + uuidV4Mock.mockReturnValue('BBBB-BBBB-BBBB-BBBB'); - await expect(networkWillChange).toBeFulfilled(); - }, - ); + await controller.upsertNetworkConfiguration( + { + rpcUrl: 'https://some.other.network', + chainId: toHex(222), + ticker: 'TICKER2', + }, + { + setActive: true, + referrer: 'https://test-dapp.com', + source: 'dapp', + }, + ); + }, }); + }); - it('emits networkDidChange', async () => { + it('calls trackMetaMetricsEvent with details about the new network', async () => { + const trackMetaMetricsEventSpy = jest.fn(); + + await withController( + { + trackMetaMetricsEvent: trackMetaMetricsEventSpy, + }, + async ({ controller }) => { + uuidV4Mock.mockReturnValue('AAAA-AAAA-AAAA-AAAA'); + + await controller.upsertNetworkConfiguration( + { + rpcUrl: 'https://test.network', + chainId: toHex(111), + ticker: 'TICKER', + }, + { + referrer: 'https://test-dapp.com', + source: 'dapp', + }, + ); + + expect(trackMetaMetricsEventSpy).toHaveBeenCalledWith({ + event: 'Custom Network Added', + category: 'Network', + referrer: { + url: 'https://test-dapp.com', + }, + properties: { + chain_id: toHex(111), + symbol: 'TICKER', + source: 'dapp', + }, + }); + }, + ); + }); + }); + + describe.each([ + ['case-sensitively', 'https://test.network', 'https://test.network'], + ['case-insensitively', 'https://test.network', 'https://TEST.NETWORK'], + ])( + 'when the rpcUrl of the given network configuration matches an existing network configuration in state (%s)', + (_qualifier, oldRpcUrl, newRpcUrl) => { + it('completely overwrites the existing network configuration in state, but does not update or remove any other network configurations', async () => { await withController( { state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - }), + networkConfigurations: { + 'AAAA-AAAA-AAAA-AAAA': { + rpcUrl: 'https://test.network.1', + chainId: toHex(111), + ticker: 'TICKER1', + id: 'AAAA-AAAA-AAAA-AAAA', + }, + 'BBBB-BBBB-BBBB-BBBB': { + rpcUrl: oldRpcUrl, + chainId: toHex(222), + ticker: 'TICKER2', + id: 'BBBB-BBBB-BBBB-BBBB', + }, + }, }, }, - async ({ controller, messenger }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - await controller.setProviderType(InfuraNetworkType.goerli); + async ({ controller }) => { + await controller.upsertNetworkConfiguration( + { + rpcUrl: newRpcUrl, + chainId: toHex(999), + ticker: 'NEW_TICKER', + nickname: 'test network 2', + rpcPrefs: { + blockExplorerUrl: 'https://testchainscan.io', + }, + }, + { + referrer: 'https://test-dapp.com', + source: 'dapp', + }, + ); - const networkDidChange = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:networkDidChange', - operation: () => { - // Intentionally not awaited because we're capturing an event - // emitted partway through the operation - controller.rollbackToPreviousProvider(); + expect(controller.state.networkConfigurations).toStrictEqual({ + 'AAAA-AAAA-AAAA-AAAA': { + rpcUrl: 'https://test.network.1', + chainId: toHex(111), + ticker: 'TICKER1', + id: 'AAAA-AAAA-AAAA-AAAA', + }, + 'BBBB-BBBB-BBBB-BBBB': { + rpcUrl: newRpcUrl, + chainId: toHex(999), + ticker: 'NEW_TICKER', + nickname: 'test network 2', + rpcPrefs: { + blockExplorerUrl: 'https://testchainscan.io', + }, + id: 'BBBB-BBBB-BBBB-BBBB', }, }); - - await expect(networkDidChange).toBeFulfilled(); }, ); }); - it('overwrites the the current provider configuration with the previous provider configuration', async () => { + it('removes properties not specific to the NetworkConfiguration interface before persisting it to state', async function () { await withController( { state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - nickname: 'network', - ticker: 'TEST', - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer.com', + networkConfigurations: { + 'AAAA-AAAA-AAAA-AAAA': { + rpcUrl: oldRpcUrl, + chainId: toHex(111), + ticker: 'TICKER', + id: 'AAAA-AAAA-AAAA-AAAA', }, - }), + }, }, - infuraProjectId: 'some-infura-project-id', }, async ({ controller }) => { - const fakeProviders = [buildFakeProvider(), buildFakeProvider()]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: InfuraNetworkType.goerli, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setProviderType('goerli'); - expect(controller.state.providerConfig).toStrictEqual({ - type: 'goerli', - rpcUrl: undefined, - chainId: toHex(5), - ticker: 'GoerliETH', - nickname: undefined, - rpcPrefs: { - blockExplorerUrl: 'https://goerli.etherscan.io', + await controller.upsertNetworkConfiguration( + { + rpcUrl: newRpcUrl, + chainId: toHex(999), + ticker: 'NEW_TICKER', + nickname: 'test network', + rpcPrefs: { + blockExplorerUrl: 'https://testchainscan.io', + }, + // @ts-expect-error We are intentionally passing bad input. + invalidKey: 'some value', }, - id: undefined, - }); + { + referrer: 'https://test-dapp.com', + source: 'dapp', + }, + ); - await controller.rollbackToPreviousProvider(); - expect(controller.state.providerConfig).toStrictEqual( - buildProviderConfig({ - type: 'rpc', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - nickname: 'network', - ticker: 'TEST', + expect(controller.state.networkConfigurations).toStrictEqual({ + 'AAAA-AAAA-AAAA-AAAA': { + rpcUrl: newRpcUrl, + chainId: toHex(999), + ticker: 'NEW_TICKER', + nickname: 'test network', rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer.com', + blockExplorerUrl: 'https://testchainscan.io', }, - }), - ); + id: 'AAAA-AAAA-AAAA-AAAA', + }, + }); }, ); }); - it('resets the network state to "unknown" before updating the provider', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - }), + describe('if at least the chain ID is being updated', () => { + it('destroys and removes the existing network client for the old network configuration', async () => { + await withController( + { + state: { + networkConfigurations: { + 'AAAA-AAAA-AAAA-AAAA': { + rpcUrl: oldRpcUrl, + chainId: toHex(111), + ticker: 'TICKER', + id: 'AAAA-AAAA-AAAA-AAAA', + }, + }, + }, + infuraProjectId: 'some-infura-project-id', }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller, messenger }) => { - const fakeProviders = [ - buildFakeProvider([ + async ({ controller }) => { + const newCustomNetworkClient = buildFakeClient(); + mockCreateNetworkClientWithDefaultsForBuiltInNetworkClients({ + infuraProjectId: 'some-infura-project-id', + }) + .calledWith({ + chainId: toHex(111), + rpcUrl: 'https://test.network', + type: NetworkClientType.Custom, + }) + .mockReturnValue(newCustomNetworkClient); + const networkClientToDestroy = Object.values( + controller.getNetworkClientRegistry(), + ).find(({ configuration }) => { + return ( + configuration.type === NetworkClientType.Custom && + configuration.chainId === toHex(111) && + configuration.rpcUrl === 'https://test.network' + ); + }); + assert(networkClientToDestroy); + jest.spyOn(networkClientToDestroy, 'destroy'); + + await controller.upsertNetworkConfiguration( { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, + rpcUrl: newRpcUrl, + chainId: toHex(999), + ticker: 'TICKER', }, { - request: { - method: 'eth_getBlockByNumber', + referrer: 'https://test-dapp.com', + source: 'dapp', + }, + ); + + const networkClients = controller.getNetworkClientRegistry(); + expect(networkClientToDestroy.destroy).toHaveBeenCalled(); + expect(Object.keys(networkClients)).toHaveLength(6); + expect(networkClients).not.toMatchObject({ + [oldRpcUrl]: expect.objectContaining({ + configuration: { + chainId: toHex(111), + rpcUrl: oldRpcUrl, + type: NetworkClientType.Custom, + }, + }), + }); + }, + ); + }); + + it('creates a new network client for the network configuration and adds it to the registry', async () => { + await withController( + { + state: { + networkConfigurations: { + 'AAAA-AAAA-AAAA-AAAA': { + rpcUrl: oldRpcUrl, + chainId: toHex(111), + ticker: 'TICKER', + id: 'AAAA-AAAA-AAAA-AAAA', }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, }, - ]), - buildFakeProvider(), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: InfuraNetworkType.goerli, + }, + infuraProjectId: 'some-infura-project-id', + }, + async ({ controller }) => { + const newCustomNetworkClient = buildFakeClient(); + mockCreateNetworkClientWithDefaultsForBuiltInNetworkClients({ infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setProviderType('goerli'); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].status, - ).toBe('available'); + .calledWith({ + chainId: toHex(999), + rpcUrl: newRpcUrl, + type: NetworkClientType.Custom, + }) + .mockReturnValue(newCustomNetworkClient); - await waitForStateChanges({ - messenger, - propertyPath: [ - 'networksMetadata', - 'https://mock-rpc-url', - 'status', - ], - // We only care about the first state change, because it - // happens before networkDidChange - count: 1, - operation: () => { - // Intentionally not awaited because we want to check state - // while this operation is in-progress - controller.rollbackToPreviousProvider(); - }, - beforeResolving: () => { - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].status, - ).toBe('unknown'); + await controller.upsertNetworkConfiguration( + { + rpcUrl: newRpcUrl, + chainId: toHex(999), + ticker: 'TICKER', + }, + { + referrer: 'https://test-dapp.com', + source: 'dapp', + }, + ); + + const networkClients = controller.getNetworkClientRegistry(); + expect(Object.keys(networkClients)).toHaveLength(6); + expect(networkClients).toMatchObject({ + 'AAAA-AAAA-AAAA-AAAA': expect.objectContaining({ + configuration: { + chainId: toHex(999), + rpcUrl: newRpcUrl, + type: NetworkClientType.Custom, + }, + }), + }); + }, + ); + }); + }); + + describe('if the chain ID is not being updated', () => { + it('does not update the network client registry', async () => { + await withController( + { + state: { + networkConfigurations: { + 'AAAA-AAAA-AAAA-AAAA': { + rpcUrl: oldRpcUrl, + chainId: toHex(111), + ticker: 'TICKER', + id: 'AAAA-AAAA-AAAA-AAAA', + }, + }, }, - }); - }, - ); + infuraProjectId: 'some-infura-project-id', + }, + async ({ controller }) => { + const newCustomNetworkClient = buildFakeClient(); + mockCreateNetworkClientWithDefaultsForBuiltInNetworkClients({ + infuraProjectId: 'some-infura-project-id', + }) + .calledWith({ + chainId: toHex(111), + rpcUrl: 'https://test.network', + type: NetworkClientType.Custom, + }) + .mockReturnValue(newCustomNetworkClient); + const networkClientsBefore = + controller.getNetworkClientRegistry(); + + await controller.upsertNetworkConfiguration( + { + rpcUrl: newRpcUrl, + chainId: toHex(111), + ticker: 'NEW_TICKER', + }, + { + referrer: 'https://test-dapp.com', + source: 'dapp', + }, + ); + + const networkClientsAfter = + controller.getNetworkClientRegistry(); + expect(networkClientsBefore).toStrictEqual(networkClientsAfter); + }, + ); + }); }); - it('initializes a provider pointed to the given RPC URL', async () => { + it('does not call trackMetaMetricsEvent', async () => { + const trackMetaMetricsEventSpy = jest.fn(); + await withController( { state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - }), + networkConfigurations: { + 'AAAA-AAAA-AAAA-AAAA': { + rpcUrl: oldRpcUrl, + chainId: toHex(111), + ticker: 'TICKER', + id: 'AAAA-AAAA-AAAA-AAAA', + }, + }, }, infuraProjectId: 'some-infura-project-id', + trackMetaMetricsEvent: trackMetaMetricsEventSpy, }, async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider(), - buildFakeProvider([ - { - request: { - method: 'test', - }, - response: { - result: 'test response', - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: InfuraNetworkType.goerli, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setProviderType('goerli'); - - await controller.rollbackToPreviousProvider(); - - const { provider } = controller.getProviderAndBlockTracker(); - assert(provider, 'Provider is somehow unset'); - const promisifiedSendAsync = promisify(provider.sendAsync).bind( - provider, + await controller.upsertNetworkConfiguration( + { + rpcUrl: newRpcUrl, + chainId: toHex(111), + ticker: 'NEW_TICKER', + }, + { + referrer: 'https://test-dapp.com', + source: 'dapp', + }, ); - const response = await promisifiedSendAsync({ - id: '1', - jsonrpc: '2.0', - method: 'test', - }); - expect(response.result).toBe('test response'); + + expect(trackMetaMetricsEventSpy).not.toHaveBeenCalled(); }, ); }); + }, + ); - it('replaces the provider object underlying the provider proxy without creating a new instance of the proxy itself', async () => { - await withController( + it('throws if the given chain ID is not a 0x-prefixed hex number', async () => { + await withController(async ({ controller }) => { + await expect( + controller.upsertNetworkConfiguration( { - state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - }), - }, - infuraProjectId: 'some-infura-project-id', + rpcUrl: 'https://test.network', + // @ts-expect-error We are intentionally passing bad input. + chainId: '1', + ticker: 'TICKER', }, - async ({ controller }) => { - const fakeProviders = [buildFakeProvider(), buildFakeProvider()]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: InfuraNetworkType.goerli, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setProviderType('goerli'); - const { provider: providerBefore } = - controller.getProviderAndBlockTracker(); + { + referrer: 'https://test-dapp.com', + source: 'dapp', + }, + ), + ).rejects.toThrow( + new Error('Value must be a hexadecimal string, starting with "0x".'), + ); + }); + }); - await controller.rollbackToPreviousProvider(); + it('throws if the given chain ID is greater than the maximum allowed ID', async () => { + await withController(async ({ controller }) => { + await expect( + controller.upsertNetworkConfiguration( + { + rpcUrl: 'https://test.network', + chainId: toHex(MAX_SAFE_CHAIN_ID + 1), + ticker: 'TICKER', + }, + { + referrer: 'https://test-dapp.com', + source: 'dapp', + }, + ), + ).rejects.toThrow( + new Error( + 'Invalid chain ID "0xfffffffffffed": numerical value greater than max safe value.', + ), + ); + }); + }); - const { provider: providerAfter } = - controller.getProviderAndBlockTracker(); - expect(providerBefore).toBe(providerAfter); + it('throws if a falsy rpcUrl is given', async () => { + await withController(async ({ controller }) => { + await expect(() => + controller.upsertNetworkConfiguration( + { + // @ts-expect-error We are intentionally passing bad input. + rpcUrl: false, + chainId: toHex(111), + ticker: 'TICKER', }, - ); - }); + { + referrer: 'https://test-dapp.com', + source: 'dapp', + }, + ), + ).rejects.toThrow( + new Error( + 'An rpcUrl is required to add or update network configuration', + ), + ); + }); + }); - it('emits infuraIsUnblocked', async () => { - await withController( + it('throws if no rpcUrl is given', async () => { + await withController(async ({ controller }) => { + await expect( + controller.upsertNetworkConfiguration( + // @ts-expect-error We are intentionally passing bad input. { - state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - }), - }, - infuraProjectId: 'some-infura-project-id', + chainId: toHex(111), + ticker: 'TICKER', }, - async ({ controller, messenger }) => { - const fakeProviders = [buildFakeProvider(), buildFakeProvider()]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: InfuraNetworkType.goerli, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setProviderType('goerli'); + { + referrer: 'https://test-dapp.com', + source: 'dapp', + }, + ), + ).rejects.toThrow( + new Error( + 'An rpcUrl is required to add or update network configuration', + ), + ); + }); + }); - const promiseForInfuraIsUnblocked = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - operation: async () => { - await controller.rollbackToPreviousProvider(); - }, - }); + it('throws if the rpcUrl given is not a valid URL', async () => { + await withController(async ({ controller }) => { + await expect( + controller.upsertNetworkConfiguration( + { + rpcUrl: 'test', + chainId: toHex(111), + ticker: 'TICKER', + }, + { + referrer: 'https://test-dapp.com', + source: 'dapp', + }, + ), + ).rejects.toThrow(new Error('rpcUrl must be a valid URL')); + }); + }); - await expect(promiseForInfuraIsUnblocked).toBeFulfilled(); + it('throws if a falsy referrer is given', async () => { + await withController(async ({ controller }) => { + await expect( + controller.upsertNetworkConfiguration( + { + rpcUrl: 'https://test.network', + chainId: toHex(111), + ticker: 'TICKER', }, - ); - }); - - it('checks the status of the previous network again and updates state accordingly', async () => { - await withController( { - state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - }), - }, - infuraProjectId: 'some-infura-project-id', + // @ts-expect-error We are intentionally passing bad input. + referrer: false, + source: 'dapp', }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider([ - { - request: { - method: 'net_version', - }, - error: ethErrors.rpc.methodNotFound(), - }, - ]), - buildFakeProvider([ - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: InfuraNetworkType.goerli, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setProviderType('goerli'); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].status, - ).toBe('unavailable'); + ), + ).rejects.toThrow( + new Error( + 'referrer and source are required arguments for adding or updating a network configuration', + ), + ); + }); + }); - await controller.rollbackToPreviousProvider(); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].status, - ).toBe('available'); + it('throws if no referrer is given', async () => { + await withController(async ({ controller }) => { + await expect( + controller.upsertNetworkConfiguration( + { + rpcUrl: 'https://test.network', + chainId: toHex(111), + ticker: 'TICKER', }, - ); - }); + // @ts-expect-error We are intentionally passing bad input. + { + source: 'dapp', + }, + ), + ).rejects.toThrow( + new Error( + 'referrer and source are required arguments for adding or updating a network configuration', + ), + ); + }); + }); - it('checks whether the previous network supports EIP-1559 again and updates state accordingly', async () => { - await withController( + it('throws if a falsy source is given', async () => { + await withController(async ({ controller }) => { + await expect( + controller.upsertNetworkConfiguration( { - state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - }), - }, - infuraProjectId: 'some-infura-project-id', + rpcUrl: 'https://test.network', + chainId: toHex(111), + ticker: 'TICKER', }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider([ - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: PRE_1559_BLOCK, - }, - }, - ]), - buildFakeProvider([ - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: POST_1559_BLOCK, - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: InfuraNetworkType.goerli, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setProviderType('goerli'); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].EIPS[1559], - ).toBe(false); + { + referrer: 'https://test-dapp.com', + // @ts-expect-error We are intentionally passing bad input. + source: false, + }, + ), + ).rejects.toThrow( + new Error( + 'referrer and source are required arguments for adding or updating a network configuration', + ), + ); + }); + }); - await controller.rollbackToPreviousProvider(); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].EIPS[1559], - ).toBe(true); + it('throws if no source is given', async () => { + await withController(async ({ controller }) => { + await expect( + controller.upsertNetworkConfiguration( + { + rpcUrl: 'https://test.network', + chainId: toHex(111), + ticker: 'TICKER', }, - ); - }); + // @ts-expect-error We are intentionally passing bad input. + { + referrer: 'https://test-dapp.com', + }, + ), + ).rejects.toThrow( + new Error( + 'referrer and source are required arguments for adding or updating a network configuration', + ), + ); }); }); - }); - describe('loadBackup', () => { - it('merges the network configurations from the given backup into state', async () => { - await withController( - { - state: { - networkConfigurations: { - networkConfigurationId1: { - id: 'networkConfigurationId1', - rpcUrl: 'https://rpc-url1.com', - chainId: toHex(1), - ticker: 'TEST1', - }, + it('throws if a falsy ticker is given', async () => { + await withController(async ({ controller }) => { + await expect( + controller.upsertNetworkConfiguration( + { + rpcUrl: 'https://test.network', + chainId: toHex(111), + // @ts-expect-error We are intentionally passing bad input. + ticker: false, }, - }, - }, - ({ controller }) => { - controller.loadBackup({ - networkConfigurations: { - networkConfigurationId2: { - id: 'networkConfigurationId2', - rpcUrl: 'https://rpc-url2.com', - chainId: toHex(2), - ticker: 'TEST2', - }, + { + referrer: 'https://test-dapp.com', + source: 'dapp', }, - }); + ), + ).rejects.toThrow( + new Error( + 'A ticker is required to add or update networkConfiguration', + ), + ); + }); + }); - expect(controller.state.networkConfigurations).toStrictEqual({ - networkConfigurationId1: { - id: 'networkConfigurationId1', - rpcUrl: 'https://rpc-url1.com', - chainId: toHex(1), - ticker: 'TEST1', + it('throws if no ticker is given', async () => { + await withController(async ({ controller }) => { + await expect( + controller.upsertNetworkConfiguration( + // @ts-expect-error We are intentionally passing bad input. + { + rpcUrl: 'https://test.network', + chainId: toHex(111), }, - networkConfigurationId2: { - id: 'networkConfigurationId2', - rpcUrl: 'https://rpc-url2.com', - chainId: toHex(2), - ticker: 'TEST2', + { + referrer: 'https://test-dapp.com', + source: 'dapp', }, - }); - }, - ); + ), + ).rejects.toThrow( + new Error( + 'A ticker is required to add or update networkConfiguration', + ), + ); + }); }); }); -}); -/** - * Creates a mocked version of `createNetworkClient` where multiple mock - * invocations can be specified. A default implementation is provided so that if - * none of the actual invocations of the function match the mock invocations - * then an error will be thrown. - * - * @returns The mocked version of `createNetworkClient`. - */ -function mockCreateNetworkClient() { - return when(createNetworkClientMock).mockImplementation((options) => { - const inspectedOptions = inspect(options, { depth: null, compact: true }); - const lines = [ - `No fake network client was specified for ${inspectedOptions}.`, - 'Make sure to mock this invocation of `createNetworkClient`.', - ]; - if ('infuraProjectId' in options) { - lines.push( - '(You might have forgotten to pass an `infuraProjectId` to `withController`.)', - ); - } - throw new Error(lines.join('\n')); - }); -} + describe('removeNetworkConfiguration', () => { + describe('given an ID that identifies a network configuration in state', () => { + it('removes the network configuration from state', async () => { + await withController( + { + state: { + networkConfigurations: { + 'AAAA-AAAA-AAAA-AAAA': { + rpcUrl: 'https://test.network', + ticker: 'TICKER', + chainId: toHex(111), + id: 'AAAA-AAAA-AAAA-AAAA', + }, + }, + }, + }, + async ({ controller }) => { + controller.removeNetworkConfiguration('AAAA-AAAA-AAAA-AAAA'); -/** - * Creates a mocked version of `createNetworkClient` where multiple mock - * invocations can be specified. Requests for built-in networks are already - * mocked. - * - * @param options - The options. - * @param options.builtInNetworkClient - The network client to use for requests - * to built-in networks. - * @param options.infuraProjectId - The Infura project ID that each network - * client is expected to be created with. - * @returns The mocked version of `createNetworkClient`. - */ -function mockCreateNetworkClientWithDefaultsForBuiltInNetworkClients({ - builtInNetworkClient = buildFakeClient(), - infuraProjectId = 'infura-project-id', -} = {}) { - return mockCreateNetworkClient() - .calledWith({ - network: NetworkType.mainnet, - infuraProjectId, - type: NetworkClientType.Infura, - chainId: BUILT_IN_NETWORKS[InfuraNetworkType.mainnet].chainId, - }) - .mockReturnValue(builtInNetworkClient) - .calledWith({ - network: NetworkType.goerli, - infuraProjectId, - chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, - type: NetworkClientType.Infura, - }) - .mockReturnValue(builtInNetworkClient) - .calledWith({ - network: NetworkType.sepolia, - infuraProjectId, - chainId: BUILT_IN_NETWORKS[InfuraNetworkType.sepolia].chainId, - type: NetworkClientType.Infura, - }) - .mockReturnValue(builtInNetworkClient); -} + expect(controller.state.networkConfigurations).toStrictEqual({}); + }, + ); + }); -/** - * Test an operation that performs a `#refreshNetwork` call with the given - * provider configuration. All effects of the `#refreshNetwork` call should be - * covered by these tests. - * - * @param args - Arguments. - * @param args.expectedProviderConfig - The provider configuration that the - * operation is expected to set. - * @param args.initialState - The initial state of the network controller. - * @param args.operation - The operation to test. - */ -function refreshNetworkTests({ - expectedProviderConfig, - initialState, - operation, -}: { - expectedProviderConfig: ProviderConfig; - initialState?: Partial; - operation: (controller: NetworkController) => Promise; -}) { - it('emits networkWillChange', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); + it('destroys and removes the network client in the network client registry that corresponds to the given ID', async () => { + await withController( + { + state: { + networkConfigurations: { + 'AAAA-AAAA-AAAA-AAAA': { + rpcUrl: 'https://test.network', + ticker: 'TICKER', + chainId: toHex(111), + id: 'AAAA-AAAA-AAAA-AAAA', + }, + }, + }, + }, + async ({ controller }) => { + mockCreateNetworkClientWithDefaultsForBuiltInNetworkClients() + .calledWith({ + chainId: toHex(111), + rpcUrl: 'https://test.network', + type: NetworkClientType.Custom, + }) + .mockReturnValue(buildFakeClient()); + const networkClientToDestroy = Object.values( + controller.getNetworkClientRegistry(), + ).find(({ configuration }) => { + return ( + configuration.type === NetworkClientType.Custom && + configuration.chainId === toHex(111) && + configuration.rpcUrl === 'https://test.network' + ); + }); + assert(networkClientToDestroy); + jest.spyOn(networkClientToDestroy, 'destroy'); - const networkWillChange = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:networkWillChange', - operation: () => { - // Intentionally not awaited because we're capturing an event - // emitted partway through the operation - operation(controller); + controller.removeNetworkConfiguration('AAAA-AAAA-AAAA-AAAA'); + + expect(networkClientToDestroy.destroy).toHaveBeenCalled(); + expect(controller.getNetworkClientRegistry()).not.toMatchObject({ + 'https://test.network': expect.objectContaining({ + configuration: { + chainId: toHex(111), + rpcUrl: 'https://test.network', + type: NetworkClientType.Custom, + }, + }), + }); }, + ); + }); + }); + + describe('given an ID that does not identify a network configuration in state', () => { + it('throws', async () => { + await withController(async ({ controller }) => { + expect(() => + controller.removeNetworkConfiguration('NONEXISTENT'), + ).toThrow( + `networkConfigurationId NONEXISTENT does not match a configured networkConfiguration`, + ); }); + }); - await expect(networkWillChange).toBeFulfilled(); - }, - ); - }); + it('does not update the network client registry', async () => { + await withController(async ({ controller }) => { + mockCreateNetworkClientWithDefaultsForBuiltInNetworkClients(); + const networkClients = controller.getNetworkClientRegistry(); - it('emits networkDidChange', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); + try { + controller.removeNetworkConfiguration('NONEXISTENT'); + } catch { + // ignore error (it is tested elsewhere) + } - const networkDidChange = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:networkDidChange', - operation: () => { - // Intentionally not awaited because we're capturing an event - // emitted partway through the operation - operation(controller); - }, + expect(controller.getNetworkClientRegistry()).toStrictEqual( + networkClients, + ); }); - - await expect(networkDidChange).toBeFulfilled(); - }, - ); + }); + }); }); - it('clears network id from state', async () => { - await withController( - { - infuraProjectId: 'infura-project-id', - state: initialState, - }, - async ({ controller, messenger }) => { - const fakeProvider = buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - }, - // Called during network lookup after resetting connection. - // Delayed to ensure that we can check the network id - // before this resolves. - { - delay: 1, - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: '0x1', - }, + describe('rollbackToPreviousProvider', () => { + describe('if a provider has not been set', () => { + [NetworkType.mainnet, NetworkType.goerli, NetworkType.sepolia].forEach( + (networkType) => { + describe(`when the type in the provider configuration is "${networkType}"`, () => { + refreshNetworkTests({ + expectedProviderConfig: buildProviderConfig({ + type: networkType, + }), + initialState: { + providerConfig: buildProviderConfig({ type: networkType }), + }, + operation: async (controller) => { + await controller.rollbackToPreviousProvider(); + }, + }); + }); + }, + ); + + describe(`when the type in the provider configuration is "rpc"`, () => { + refreshNetworkTests({ + expectedProviderConfig: buildProviderConfig({ + type: NetworkType.rpc, + }), + initialState: { + providerConfig: buildProviderConfig({ type: NetworkType.rpc }), }, - ]); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - await controller.initializeProvider(); - expect(controller.state.networkId).toBe('1'); - - await waitForStateChanges({ - messenger, - propertyPath: ['networkId'], - // We only care about the first state change, because it - // happens before the network lookup - count: 1, - operation: () => { - // Intentionally not awaited because we want to check state - // partway through the operation - operation(controller); + operation: async (controller) => { + await controller.rollbackToPreviousProvider(); }, }); + }); + }); - expect(controller.state.networkId).toBeNull(); - }, - ); - }); + describe('if a provider has been set', () => { + for (const { networkType } of INFURA_NETWORKS) { + describe(`if the previous provider configuration had a type of "${networkType}"`, () => { + it('emits networkWillChange', async () => { + await withController( + { + state: { + providerConfig: buildProviderConfig({ + type: networkType, + }), + networkConfigurations: { + testNetworkConfiguration: { + id: 'testNetworkConfiguration', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', + nickname: 'test network', + rpcPrefs: { + blockExplorerUrl: 'https://test-block-explorer.com', + }, + }, + }, + }, + }, + async ({ controller, messenger }) => { + const fakeProvider = buildFakeProvider(); + const fakeNetworkClient = buildFakeClient(fakeProvider); + mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); + await controller.setActiveNetwork('testNetworkConfiguration'); - if (expectedProviderConfig.type === NetworkType.rpc) { - it('sets the provider to a custom RPC provider initialized with the RPC target and chain ID', async () => { - await withController( - { - infuraProjectId: 'infura-project-id', - state: initialState, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider([ - { - request: { - method: 'eth_chainId', + const networkWillChange = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:networkWillChange', + operation: () => { + // Intentionally not awaited because we're capturing an event + // emitted partway through the operation + controller.rollbackToPreviousProvider(); + }, + }); + + await expect(networkWillChange).toBeFulfilled(); }, - response: { - result: toHex(111), + ); + }); + + it('emits networkDidChange', async () => { + await withController( + { + state: { + providerConfig: buildProviderConfig({ + type: networkType, + }), + networkConfigurations: { + testNetworkConfiguration: { + id: 'testNetworkConfiguration', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', + nickname: 'test network', + rpcPrefs: { + blockExplorerUrl: 'https://test-block-explorer.com', + }, + }, + }, + }, }, - }, - ]); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); + async ({ controller, messenger }) => { + const fakeProvider = buildFakeProvider(); + const fakeNetworkClient = buildFakeClient(fakeProvider); + mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); + await controller.setActiveNetwork('testNetworkConfiguration'); - await operation(controller); + const networkDidChange = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:networkDidChange', + operation: () => { + // Intentionally not awaited because we're capturing an event + // emitted partway through the operation + controller.rollbackToPreviousProvider(); + }, + }); - expect(createNetworkClientMock).toHaveBeenCalledWith({ - chainId: expectedProviderConfig.chainId, - rpcUrl: expectedProviderConfig.rpcUrl, - type: NetworkClientType.Custom, - }); - 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: [], + await expect(networkDidChange).toBeFulfilled(); + }, + ); }); - expect(chainIdResult.result).toBe(toHex(111)); - }, - ); - }); - } else { - it(`sets the provider to an Infura provider pointed to ${expectedProviderConfig.type}`, async () => { - await withController( - { - infuraProjectId: 'infura-project-id', - state: initialState, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider([ - { - request: { - method: 'eth_chainId', + + it('overwrites the the current provider configuration with the previous provider configuration', async () => { + await withController( + { + state: { + providerConfig: buildProviderConfig({ + type: networkType, + }), + networkConfigurations: { + testNetworkConfiguration: { + id: 'testNetworkConfiguration', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', + nickname: 'test network', + rpcPrefs: { + blockExplorerUrl: 'https://test-block-explorer.com', + }, + }, + }, + }, + infuraProjectId: 'some-infura-project-id', }, - response: { - result: toHex(1337), + async ({ controller }) => { + const fakeProviders = [ + buildFakeProvider(), + buildFakeProvider(), + ]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + type: NetworkClientType.Custom, + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + network: networkType, + infuraProjectId: 'some-infura-project-id', + chainId: BUILT_IN_NETWORKS[networkType].chainId, + type: NetworkClientType.Infura, + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.setActiveNetwork('testNetworkConfiguration'); + expect(controller.state.providerConfig).toStrictEqual({ + type: 'rpc', + id: 'testNetworkConfiguration', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', + nickname: 'test network', + rpcPrefs: { + blockExplorerUrl: 'https://test-block-explorer.com', + }, + }); + + await controller.rollbackToPreviousProvider(); + + expect(controller.state.providerConfig).toStrictEqual( + buildProviderConfig({ + type: networkType, + }), + ); + }, + ); + }); + + it('resets the network status to "unknown" before updating the provider', async () => { + await withController( + { + state: { + providerConfig: buildProviderConfig({ + type: networkType, + }), + networkConfigurations: { + testNetworkConfiguration: { + id: 'testNetworkConfiguration', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', + }, + }, + }, + infuraProjectId: 'some-infura-project-id', }, - }, - ]); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - - await operation(controller); + async ({ controller, messenger }) => { + const fakeProviders = [ + buildFakeProvider([ + { + request: { + method: 'eth_getBlockByNumber', + }, + response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, + }, + ]), + buildFakeProvider(), + ]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + type: NetworkClientType.Custom, + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + network: networkType, + chainId: BUILT_IN_NETWORKS[networkType].chainId, + infuraProjectId: 'some-infura-project-id', + type: NetworkClientType.Infura, + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.setActiveNetwork('testNetworkConfiguration'); + expect( + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ].status, + ).toBe('available'); - expect(createNetworkClientMock).toHaveBeenCalledWith({ - network: expectedProviderConfig.type, - infuraProjectId: 'infura-project-id', - chainId: BUILT_IN_NETWORKS[expectedProviderConfig.type].chainId, - type: NetworkClientType.Infura, - }); - 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: [], + await waitForStateChanges({ + messenger, + propertyPath: ['networksMetadata', networkType, 'status'], + // We only care about the first state change, because it + // happens before networkDidChange + count: 1, + operation: () => { + // Intentionally not awaited because we want to check state + // while this operation is in-progress + controller.rollbackToPreviousProvider(); + }, + beforeResolving: () => { + expect( + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ].status, + ).toBe('unknown'); + }, + }); + }, + ); }); - expect(chainIdResult.result).toBe(toHex(1337)); - }, - ); - }); - } - - it('replaces the provider object underlying the provider proxy without creating a new instance of the proxy itself', async () => { - await withController( - { - infuraProjectId: 'infura-project-id', - state: initialState, - }, - async ({ controller }) => { - const fakeProviders = [buildFakeProvider(), buildFakeProvider()]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - const initializationNetworkClientOptions: Parameters< - typeof createNetworkClient - >[0] = - controller.state.providerConfig.type === NetworkType.rpc - ? { - chainId: toHex(controller.state.providerConfig.chainId), - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - rpcUrl: controller.state.providerConfig.rpcUrl!, - type: NetworkClientType.Custom, - } - : { - network: controller.state.providerConfig.type, - infuraProjectId: 'infura-project-id', - chainId: - BUILT_IN_NETWORKS[controller.state.providerConfig.type] - .chainId, - type: NetworkClientType.Infura, - }; - const operationNetworkClientOptions: Parameters< - typeof createNetworkClient - >[0] = - expectedProviderConfig.type === NetworkType.rpc - ? { - chainId: toHex(expectedProviderConfig.chainId), - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - rpcUrl: expectedProviderConfig.rpcUrl!, - type: NetworkClientType.Custom, - } - : { - network: expectedProviderConfig.type, - infuraProjectId: 'infura-project-id', - chainId: BUILT_IN_NETWORKS[expectedProviderConfig.type].chainId, - type: NetworkClientType.Infura, - }; - mockCreateNetworkClient() - .calledWith(initializationNetworkClientOptions) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith(operationNetworkClientOptions) - .mockReturnValue(fakeNetworkClients[1]); - await controller.initializeProvider(); - const { provider: providerBefore } = - controller.getProviderAndBlockTracker(); - await operation(controller); + it(`initializes a provider pointed to the "${networkType}" Infura network`, async () => { + await withController( + { + state: { + providerConfig: buildProviderConfig({ + type: networkType, + }), + networkConfigurations: { + testNetworkConfiguration: { + id: 'testNetworkConfiguration', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', + }, + }, + }, + infuraProjectId: 'some-infura-project-id', + }, + async ({ controller }) => { + const fakeProviders = [ + buildFakeProvider(), + buildFakeProvider([ + { + request: { + method: 'test', + }, + response: { + result: 'test response', + }, + }, + ]), + ]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + type: NetworkClientType.Custom, + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + network: networkType, + infuraProjectId: 'some-infura-project-id', + chainId: BUILT_IN_NETWORKS[networkType].chainId, + type: NetworkClientType.Infura, + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.setActiveNetwork('testNetworkConfiguration'); - const { provider: providerAfter } = - controller.getProviderAndBlockTracker(); - expect(providerBefore).toBe(providerAfter); - }, - ); - }); + await controller.rollbackToPreviousProvider(); - lookupNetworkTests({ expectedProviderConfig, initialState, operation }); -} + const { provider } = controller.getProviderAndBlockTracker(); + assert(provider, 'Provider is somehow unset'); + const promisifiedSendAsync = promisify(provider.sendAsync).bind( + provider, + ); + const response = await promisifiedSendAsync({ + id: '1', + jsonrpc: '2.0', + method: 'test', + }); + expect(response.result).toBe('test response'); + }, + ); + }); -/** - * Test an operation that performs a `lookupNetwork` call with the given - * provider configuration. All effects of the `lookupNetwork` call should be - * covered by these tests. - * - * @param args - Arguments. - * @param args.expectedProviderConfig - The provider configuration that the - * operation is expected to set. - * @param args.initialState - The initial state of the network controller. - * @param args.operation - The operation to test. - */ -function lookupNetworkTests({ - expectedProviderConfig, - initialState, - operation, -}: { - expectedProviderConfig: ProviderConfig; - initialState?: Partial; - operation: (controller: NetworkController) => Promise; -}) { - describe('if the network ID and network details requests resolve successfully', () => { - const validNetworkIds = [12345, '12345', toHex(12345)]; - for (const networkId of validNetworkIds) { - describe(`with a network id of '${networkId}'`, () => { - describe('if the current network is different from the network in state', () => { - it('updates the network in state to match', async () => { + it('replaces the provider object underlying the provider proxy without creating a new instance of the proxy itself', async () => { await withController( { - state: initialState, + state: { + providerConfig: buildProviderConfig({ + type: networkType, + }), + networkConfigurations: { + testNetworkConfiguration: { + id: 'testNetworkConfiguration', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', + }, + }, + }, + infuraProjectId: 'some-infura-project-id', }, async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - response: { result: networkId }, - }, - ], - stubLookupNetworkWhileSetting: true, - }); + const fakeProviders = [ + buildFakeProvider(), + buildFakeProvider(), + ]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + type: NetworkClientType.Custom, + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + network: networkType, + infuraProjectId: 'some-infura-project-id', + chainId: BUILT_IN_NETWORKS[networkType].chainId, + type: NetworkClientType.Infura, + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.setActiveNetwork('testNetworkConfiguration'); + const { provider: providerBefore } = + controller.getProviderAndBlockTracker(); - await operation(controller); + await controller.rollbackToPreviousProvider(); - expect(controller.state.networkId).toBe('12345'); + const { provider: providerAfter } = + controller.getProviderAndBlockTracker(); + expect(providerBefore).toBe(providerAfter); }, ); }); - }); - describe('if the version of the current network is the same as that in state', () => { - it('does not change network ID in state', async () => { + it('emits infuraIsBlocked or infuraIsUnblocked, depending on whether Infura is blocking requests for the previous network', async () => { await withController( { - state: initialState, + state: { + providerConfig: buildProviderConfig({ + type: networkType, + }), + networkConfigurations: { + testNetworkConfiguration: { + id: 'testNetworkConfiguration', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', + }, + }, + }, + infuraProjectId: 'some-infura-project-id', }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ + async ({ controller, messenger }) => { + const fakeProviders = [ + buildFakeProvider(), + buildFakeProvider([ { - request: { method: 'net_version' }, - response: { result: networkId }, + request: { + method: 'eth_getBlockByNumber', + }, + error: BLOCKED_INFURA_JSON_RPC_ERROR, }, - ], - stubLookupNetworkWhileSetting: true, + ]), + ]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + type: NetworkClientType.Custom, + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + network: networkType, + infuraProjectId: 'some-infura-project-id', + chainId: BUILT_IN_NETWORKS[networkType].chainId, + type: NetworkClientType.Infura, + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.setActiveNetwork('testNetworkConfiguration'); + const promiseForNoInfuraIsUnblockedEvents = + waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + count: 0, + }); + const promiseForInfuraIsBlocked = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsBlocked', }); - await operation(controller); + await controller.rollbackToPreviousProvider(); - await expect(controller.state.networkId).toBe('12345'); + await expect( + promiseForNoInfuraIsUnblockedEvents, + ).toBeFulfilled(); + await expect(promiseForInfuraIsBlocked).toBeFulfilled(); }, ); }); - it('updates the network details', async () => { + it('checks the status of the previous network again and updates state accordingly', async () => { await withController( { - state: initialState, - }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: { result: networkId }, + state: { + providerConfig: buildProviderConfig({ + type: networkType, + }), + networkConfigurations: { + testNetworkConfiguration: { + id: 'testNetworkConfiguration', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', }, + }, + }, + infuraProjectId: 'some-infura-project-id', + }, + async ({ controller, messenger }) => { + const fakeProviders = [ + buildFakeProvider([ { request: { method: 'eth_getBlockByNumber', }, - response: { - result: PRE_1559_BLOCK, - }, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: { result: networkId }, + error: ethErrors.rpc.methodNotFound(), }, + ]), + buildFakeProvider([ { request: { method: 'eth_getBlockByNumber', }, - response: { - result: POST_1559_BLOCK, - }, + response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, }, - ], - }); - - await operation(controller); + ]), + ]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + type: NetworkClientType.Custom, + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + network: networkType, + infuraProjectId: 'some-infura-project-id', + chainId: BUILT_IN_NETWORKS[networkType].chainId, + type: NetworkClientType.Infura, + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.setActiveNetwork('testNetworkConfiguration'); + expect( + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ].status, + ).toBe('unavailable'); + await waitForStateChanges({ + messenger, + propertyPath: ['networksMetadata', networkType, 'status'], + operation: async () => { + await controller.rollbackToPreviousProvider(); + }, + }); expect( controller.state.networksMetadata[ controller.state.selectedNetworkClientId - ].EIPS[1559], - ).toBe(true); + ].status, + ).toBe('available'); }, ); }); - }); - }); - } - describe('if the network details of the current network are different from the network details in state', () => { - it('updates the network in state to match', async () => { - await withController( - { - state: { - ...initialState, - networksMetadata: { - mainnet: { - EIPS: { 1559: false }, - status: NetworkStatus.Unknown, - }, - }, - }, - }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: { - baseFeePerGas: '0x1', + it('checks whether the previous network supports EIP-1559 again and updates state accordingly', async () => { + await withController( + { + state: { + providerConfig: buildProviderConfig({ + type: networkType, + }), + networkConfigurations: { + testNetworkConfiguration: { + id: 'testNetworkConfiguration', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', }, }, }, - ], - stubLookupNetworkWhileSetting: true, - }); - - await operation(controller); - - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].EIPS[1559], - ).toBe(true); - }, - ); - }); - }); - - describe('if the network details of the current network are the same as the network details in state', () => { - it('does not change network details in state', async () => { - await withController( - { - state: { - ...initialState, - networksMetadata: { - mainnet: { - EIPS: { 1559: true }, - status: NetworkStatus.Unknown, - }, + infuraProjectId: 'some-infura-project-id', }, - }, - }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: { - baseFeePerGas: '0x1', + async ({ controller, messenger }) => { + const fakeProviders = [ + buildFakeProvider([ + { + request: { + method: 'eth_getBlockByNumber', + }, + response: { + result: PRE_1559_BLOCK, + }, }, - }, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - await operation(controller); - - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].EIPS[1559], - ).toBe(true); - }, - ); - }); - }); + ]), + buildFakeProvider([ + { + request: { + method: 'eth_getBlockByNumber', + }, + response: { + result: POST_1559_BLOCK, + }, + }, + ]), + ]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + type: NetworkClientType.Custom, + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + network: networkType, + infuraProjectId: 'some-infura-project-id', + chainId: BUILT_IN_NETWORKS[networkType].chainId, + type: NetworkClientType.Infura, + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.setActiveNetwork('testNetworkConfiguration'); + expect( + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ].EIPS[1559], + ).toBe(false); - it('emits infuraIsUnblocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubLookupNetworkWhileSetting: true, + await waitForStateChanges({ + messenger, + propertyPath: ['networksMetadata', networkType, 'EIPS'], + count: 2, + operation: async () => { + await controller.rollbackToPreviousProvider(); + }, + }); + expect( + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ].EIPS[1559], + ).toBe(true); + }, + ); }); + }); + } - const infuraIsUnblocked = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - operation: async () => { - await operation(controller); + describe(`if the previous provider configuration had a type of "rpc"`, () => { + it('emits networkWillChange', async () => { + await withController( + { + state: { + providerConfig: buildProviderConfig({ + type: NetworkType.rpc, + }), + }, }, - }); - - await expect(infuraIsUnblocked).toBeFulfilled(); - }, - ); - }); + async ({ controller, messenger }) => { + const fakeProvider = buildFakeProvider(); + const fakeNetworkClient = buildFakeClient(fakeProvider); + mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); + await controller.setProviderType(InfuraNetworkType.goerli); - it('does not emit infuraIsBlocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubLookupNetworkWhileSetting: true, - }); + const networkWillChange = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:networkWillChange', + operation: () => { + // Intentionally not awaited because we're capturing an event + // emitted partway through the operation + controller.rollbackToPreviousProvider(); + }, + }); - const infuraIsBlocked = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', - count: 0, - operation: async () => { - await operation(controller); + await expect(networkWillChange).toBeFulfilled(); }, - }); - - await expect(infuraIsBlocked).toBeFulfilled(); - }, - ); - }); - }); + ); + }); - describe('if an RPC error is encountered while retrieving the version of the current network', () => { - it('updates the network in state to "unavailable"', async () => { - await withController( - { - state: initialState, - }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - error: ethErrors.rpc.limitExceeded('some error'), + it('emits networkDidChange', async () => { + await withController( + { + state: { + providerConfig: buildProviderConfig({ + type: NetworkType.rpc, + }), }, - ], - stubLookupNetworkWhileSetting: true, - }); + }, + async ({ controller, messenger }) => { + const fakeProvider = buildFakeProvider(); + const fakeNetworkClient = buildFakeClient(fakeProvider); + mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); + await controller.setProviderType(InfuraNetworkType.goerli); - await operation(controller); + const networkDidChange = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:networkDidChange', + operation: () => { + // Intentionally not awaited because we're capturing an event + // emitted partway through the operation + controller.rollbackToPreviousProvider(); + }, + }); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].status, - ).toBe(NetworkStatus.Unavailable); - }, - ); - }); + await expect(networkDidChange).toBeFulfilled(); + }, + ); + }); - it('resets the network details in state', async () => { - await withController( - { - state: initialState, - }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - // Called during provider initialization - { - request: { method: 'net_version' }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: PRE_1559_BLOCK, - }, - }, - // Called when calling the operation directly - { - request: { method: 'net_version' }, - error: ethErrors.rpc.limitExceeded('some error'), + it('overwrites the the current provider configuration with the previous provider configuration', async () => { + await withController( + { + state: { + providerConfig: buildProviderConfig({ + type: NetworkType.rpc, + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + nickname: 'network', + ticker: 'TEST', + rpcPrefs: { + blockExplorerUrl: 'https://test-block-explorer.com', + }, + }), }, - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: POST_1559_BLOCK, + infuraProjectId: 'some-infura-project-id', + }, + async ({ controller }) => { + const fakeProviders = [buildFakeProvider(), buildFakeProvider()]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ + network: InfuraNetworkType.goerli, + infuraProjectId: 'some-infura-project-id', + type: NetworkClientType.Infura, + chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + type: NetworkClientType.Custom, + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.setProviderType('goerli'); + expect(controller.state.providerConfig).toStrictEqual({ + type: 'goerli', + rpcUrl: undefined, + chainId: toHex(5), + ticker: 'GoerliETH', + nickname: undefined, + rpcPrefs: { + blockExplorerUrl: 'https://goerli.etherscan.io', }, - }, - ], - }); - - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].EIPS[1559], - ).toBe(false); + id: undefined, + }); - await operation(controller); + await controller.rollbackToPreviousProvider(); + expect(controller.state.providerConfig).toStrictEqual( + buildProviderConfig({ + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + nickname: 'network', + ticker: 'TEST', + rpcPrefs: { + blockExplorerUrl: 'https://test-block-explorer.com', + }, + }), + ); + }, + ); + }); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].EIPS, - ).toStrictEqual({}); - }, - ); - }); + it('resets the network state to "unknown" before updating the provider', async () => { + await withController( + { + state: { + providerConfig: buildProviderConfig({ + type: NetworkType.rpc, + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + }), + }, + infuraProjectId: 'some-infura-project-id', + }, + async ({ controller, messenger }) => { + const fakeProviders = [ + buildFakeProvider([ + { + request: { + method: 'eth_getBlockByNumber', + }, + response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, + }, + ]), + buildFakeProvider(), + ]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ + network: InfuraNetworkType.goerli, + infuraProjectId: 'some-infura-project-id', + type: NetworkClientType.Infura, + chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + type: NetworkClientType.Custom, + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.setProviderType('goerli'); + expect( + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ].status, + ).toBe('available'); - if (expectedProviderConfig.type === NetworkType.rpc) { - it('emits infuraIsUnblocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - error: ethErrors.rpc.limitExceeded('some error'), + await waitForStateChanges({ + messenger, + propertyPath: [ + 'networksMetadata', + 'https://mock-rpc-url', + 'status', + ], + // We only care about the first state change, because it + // happens before networkDidChange + count: 1, + operation: () => { + // Intentionally not awaited because we want to check state + // while this operation is in-progress + controller.rollbackToPreviousProvider(); }, - ], - stubLookupNetworkWhileSetting: true, - }); + beforeResolving: () => { + expect( + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ].status, + ).toBe('unknown'); + }, + }); + }, + ); + }); - const infuraIsUnblocked = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - operation: async () => { - await operation(controller); + it('initializes a provider pointed to the given RPC URL', async () => { + await withController( + { + state: { + providerConfig: buildProviderConfig({ + type: NetworkType.rpc, + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + }), }, - }); + infuraProjectId: 'some-infura-project-id', + }, + async ({ controller }) => { + const fakeProviders = [ + buildFakeProvider(), + buildFakeProvider([ + { + request: { + method: 'test', + }, + response: { + result: 'test response', + }, + }, + ]), + ]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ + network: InfuraNetworkType.goerli, + infuraProjectId: 'some-infura-project-id', + type: NetworkClientType.Infura, + chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + type: NetworkClientType.Custom, + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.setProviderType('goerli'); - await expect(infuraIsUnblocked).toBeFulfilled(); - }, - ); - }); - } else { - it('does not emit infuraIsUnblocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - error: ethErrors.rpc.limitExceeded('some error'), - }, - ], - stubLookupNetworkWhileSetting: true, - }); + await controller.rollbackToPreviousProvider(); - const infuraIsUnblocked = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - count: 0, - operation: async () => { - await operation(controller); - }, - }); + const { provider } = controller.getProviderAndBlockTracker(); + assert(provider, 'Provider is somehow unset'); + const promisifiedSendAsync = promisify(provider.sendAsync).bind( + provider, + ); + const response = await promisifiedSendAsync({ + id: '1', + jsonrpc: '2.0', + method: 'test', + }); + expect(response.result).toBe('test response'); + }, + ); + }); - await expect(infuraIsUnblocked).toBeFulfilled(); - }, - ); - }); - } + it('replaces the provider object underlying the provider proxy without creating a new instance of the proxy itself', async () => { + await withController( + { + state: { + providerConfig: buildProviderConfig({ + type: NetworkType.rpc, + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + }), + }, + infuraProjectId: 'some-infura-project-id', + }, + async ({ controller }) => { + const fakeProviders = [buildFakeProvider(), buildFakeProvider()]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ + network: InfuraNetworkType.goerli, + infuraProjectId: 'some-infura-project-id', + type: NetworkClientType.Infura, + chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + type: NetworkClientType.Custom, + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.setProviderType('goerli'); + const { provider: providerBefore } = + controller.getProviderAndBlockTracker(); - it('does not emit infuraIsBlocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - error: ethErrors.rpc.limitExceeded('some error'), - }, - ], - stubLookupNetworkWhileSetting: true, - }); + await controller.rollbackToPreviousProvider(); - const infuraIsBlocked = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', - count: 0, - operation: async () => { - await operation(controller); + const { provider: providerAfter } = + controller.getProviderAndBlockTracker(); + expect(providerBefore).toBe(providerAfter); }, - }); + ); + }); - await expect(infuraIsBlocked).toBeFulfilled(); - }, - ); - }); - }); + it('emits infuraIsUnblocked', async () => { + await withController( + { + state: { + providerConfig: buildProviderConfig({ + type: NetworkType.rpc, + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + }), + }, + infuraProjectId: 'some-infura-project-id', + }, + async ({ controller, messenger }) => { + const fakeProviders = [buildFakeProvider(), buildFakeProvider()]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ + network: InfuraNetworkType.goerli, + infuraProjectId: 'some-infura-project-id', + chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, + type: NetworkClientType.Infura, + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + type: NetworkClientType.Custom, + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.setProviderType('goerli'); - describe('if a country blocked error is encountered while retrieving the version of the current network', () => { - if (expectedProviderConfig.type === NetworkType.rpc) { - it('updates the network in state to "unknown"', async () => { - await withController( - { - state: initialState, - }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, + const promiseForInfuraIsUnblocked = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + operation: async () => { + await controller.rollbackToPreviousProvider(); }, - ], - stubLookupNetworkWhileSetting: true, - }); + }); - await operation(controller); + await expect(promiseForInfuraIsUnblocked).toBeFulfilled(); + }, + ); + }); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].status, - ).toBe(NetworkStatus.Unknown); - }, - ); - }); + it('checks the status of the previous network again and updates state accordingly', async () => { + await withController( + { + state: { + providerConfig: buildProviderConfig({ + type: NetworkType.rpc, + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + }), + }, + infuraProjectId: 'some-infura-project-id', + }, + async ({ controller }) => { + const fakeProviders = [ + buildFakeProvider([ + { + request: { + method: 'eth_getBlockByNumber', + }, + error: ethErrors.rpc.methodNotFound(), + }, + ]), + buildFakeProvider([ + { + request: { + method: 'eth_getBlockByNumber', + }, + response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, + }, + ]), + ]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ + network: InfuraNetworkType.goerli, + infuraProjectId: 'some-infura-project-id', + chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, + type: NetworkClientType.Infura, + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + type: NetworkClientType.Custom, + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.setProviderType('goerli'); + expect( + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ].status, + ).toBe('unavailable'); - it('emits infuraIsUnblocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, - }, - ], - stubLookupNetworkWhileSetting: true, - }); + await controller.rollbackToPreviousProvider(); + expect( + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ].status, + ).toBe('available'); + }, + ); + }); - const infuraIsUnblocked = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - operation: async () => { - await operation(controller); + it('checks whether the previous network supports EIP-1559 again and updates state accordingly', async () => { + await withController( + { + state: { + providerConfig: buildProviderConfig({ + type: NetworkType.rpc, + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + }), }, - }); + infuraProjectId: 'some-infura-project-id', + }, + async ({ controller }) => { + const fakeProviders = [ + buildFakeProvider([ + { + request: { + method: 'eth_getBlockByNumber', + }, + response: { + result: PRE_1559_BLOCK, + }, + }, + ]), + buildFakeProvider([ + { + request: { + method: 'eth_getBlockByNumber', + }, + response: { + result: POST_1559_BLOCK, + }, + }, + ]), + ]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ + network: InfuraNetworkType.goerli, + infuraProjectId: 'some-infura-project-id', + chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, + type: NetworkClientType.Infura, + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + type: NetworkClientType.Custom, + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.setProviderType('goerli'); + expect( + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ].EIPS[1559], + ).toBe(false); - await expect(infuraIsUnblocked).toBeFulfilled(); - }, - ); + await controller.rollbackToPreviousProvider(); + expect( + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ].EIPS[1559], + ).toBe(true); + }, + ); + }); }); + }); + }); - it('does not emit infuraIsBlocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const infuraIsBlocked = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', - count: 0, - operation: async () => { - await operation(controller); + describe('loadBackup', () => { + it('merges the network configurations from the given backup into state', async () => { + await withController( + { + state: { + networkConfigurations: { + networkConfigurationId1: { + id: 'networkConfigurationId1', + rpcUrl: 'https://rpc-url1.com', + chainId: toHex(1), + ticker: 'TEST1', }, - }); - - await expect(infuraIsBlocked).toBeFulfilled(); - }, - ); - }); - } else { - it('updates the network in state to "blocked"', async () => { - await withController( - { - state: initialState, + }, }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, - }, - ], - stubLookupNetworkWhileSetting: true, - }); + }, + ({ controller }) => { + controller.loadBackup({ + networkConfigurations: { + networkConfigurationId2: { + id: 'networkConfigurationId2', + rpcUrl: 'https://rpc-url2.com', + chainId: toHex(2), + ticker: 'TEST2', + }, + }, + }); - await operation(controller); + expect(controller.state.networkConfigurations).toStrictEqual({ + networkConfigurationId1: { + id: 'networkConfigurationId1', + rpcUrl: 'https://rpc-url1.com', + chainId: toHex(1), + ticker: 'TEST1', + }, + networkConfigurationId2: { + id: 'networkConfigurationId2', + rpcUrl: 'https://rpc-url2.com', + chainId: toHex(2), + ticker: 'TEST2', + }, + }); + }, + ); + }); + }); +}); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].status, - ).toBe(NetworkStatus.Blocked); - }, - ); - }); +/** + * Creates a mocked version of `createNetworkClient` where multiple mock + * invocations can be specified. A default implementation is provided so that if + * none of the actual invocations of the function match the mock invocations + * then an error will be thrown. + * + * @returns The mocked version of `createNetworkClient`. + */ +function mockCreateNetworkClient() { + return when(createNetworkClientMock).mockImplementation((options) => { + const inspectedOptions = inspect(options, { depth: null, compact: true }); + const lines = [ + `No fake network client was specified for ${inspectedOptions}.`, + 'Make sure to mock this invocation of `createNetworkClient`.', + ]; + if ('infuraProjectId' in options) { + lines.push( + '(You might have forgotten to pass an `infuraProjectId` to `withController`.)', + ); + } + throw new Error(lines.join('\n')); + }); +} - it('does not emit infuraIsUnblocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, - }, - ], - stubLookupNetworkWhileSetting: true, - }); +/** + * Creates a mocked version of `createNetworkClient` where multiple mock + * invocations can be specified. Requests for built-in networks are already + * mocked. + * + * @param options - The options. + * @param options.builtInNetworkClient - The network client to use for requests + * to built-in networks. + * @param options.infuraProjectId - The Infura project ID that each network + * client is expected to be created with. + * @returns The mocked version of `createNetworkClient`. + */ +function mockCreateNetworkClientWithDefaultsForBuiltInNetworkClients({ + builtInNetworkClient = buildFakeClient(), + infuraProjectId = 'infura-project-id', +} = {}) { + return mockCreateNetworkClient() + .calledWith({ + network: NetworkType.mainnet, + infuraProjectId, + type: NetworkClientType.Infura, + chainId: BUILT_IN_NETWORKS[InfuraNetworkType.mainnet].chainId, + }) + .mockReturnValue(builtInNetworkClient) + .calledWith({ + network: NetworkType.goerli, + infuraProjectId, + chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, + type: NetworkClientType.Infura, + }) + .mockReturnValue(builtInNetworkClient) + .calledWith({ + network: NetworkType.sepolia, + infuraProjectId, + chainId: BUILT_IN_NETWORKS[InfuraNetworkType.sepolia].chainId, + type: NetworkClientType.Infura, + }) + .mockReturnValue(builtInNetworkClient); +} - const infuraIsUnblocked = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - count: 0, - operation: async () => { - await operation(controller); - }, - }); +/** + * Test an operation that performs a `#refreshNetwork` call with the given + * provider configuration. All effects of the `#refreshNetwork` call should be + * covered by these tests. + * + * @param args - Arguments. + * @param args.expectedProviderConfig - The provider configuration that the + * operation is expected to set. + * @param args.initialState - The initial state of the network controller. + * @param args.operation - The operation to test. + */ +function refreshNetworkTests({ + expectedProviderConfig, + initialState, + operation, +}: { + expectedProviderConfig: ProviderConfig; + initialState?: Partial; + operation: (controller: NetworkController) => Promise; +}) { + it('emits networkWillChange', async () => { + await withController( + { + state: initialState, + }, + async ({ controller, messenger }) => { + const fakeProvider = buildFakeProvider(); + const fakeNetworkClient = buildFakeClient(fakeProvider); + mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - await expect(infuraIsUnblocked).toBeFulfilled(); + const networkWillChange = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:networkWillChange', + operation: () => { + // Intentionally not awaited because we're capturing an event + // emitted partway through the operation + operation(controller); }, - ); - }); + }); - it('emits infuraIsBlocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, - }, - ], - stubLookupNetworkWhileSetting: true, - }); + await expect(networkWillChange).toBeFulfilled(); + }, + ); + }); - const infuraIsBlocked = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', - operation: async () => { - await operation(controller); - }, - }); + it('emits networkDidChange', async () => { + await withController( + { + state: initialState, + }, + async ({ controller, messenger }) => { + const fakeProvider = buildFakeProvider(); + const fakeNetworkClient = buildFakeClient(fakeProvider); + mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - await expect(infuraIsBlocked).toBeFulfilled(); + const networkDidChange = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:networkDidChange', + operation: () => { + // Intentionally not awaited because we're capturing an event + // emitted partway through the operation + operation(controller); }, - ); - }); - } + }); - it('resets the network details in state', async () => { + await expect(networkDidChange).toBeFulfilled(); + }, + ); + }); + + if (expectedProviderConfig.type === NetworkType.rpc) { + it('sets the provider to a custom RPC provider initialized with the RPC target and chain ID', async () => { await withController( { + infuraProjectId: 'infura-project-id', state: initialState, }, async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - // Called during provider initialization - { - request: { method: 'net_version' }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: PRE_1559_BLOCK, - }, - }, - // Called when calling the operation directly - { - request: { method: 'net_version' }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, + const fakeProvider = buildFakeProvider([ + { + request: { + method: 'eth_chainId', }, - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: POST_1559_BLOCK, - }, + response: { + result: toHex(111), }, - ], - }); - - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].EIPS[1559], - ).toBe(false); + }, + ]); + const fakeNetworkClient = buildFakeClient(fakeProvider); + createNetworkClientMock.mockReturnValue(fakeNetworkClient); await operation(controller); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].EIPS, - ).toStrictEqual({}); + expect(createNetworkClientMock).toHaveBeenCalledWith({ + chainId: expectedProviderConfig.chainId, + rpcUrl: expectedProviderConfig.rpcUrl, + type: NetworkClientType.Custom, + }); + 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(toHex(111)); }, ); }); - }); - - describe('if an internal error is encountered while retrieving the version of the current network', () => { - it('updates the network in state to "unknown"', async () => { + } else { + it(`sets the provider to an Infura provider pointed to ${expectedProviderConfig.type}`, async () => { await withController( { + infuraProjectId: 'infura-project-id', state: initialState, }, async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - error: GENERIC_JSON_RPC_ERROR, + const fakeProvider = buildFakeProvider([ + { + request: { + method: 'eth_chainId', }, - ], - stubLookupNetworkWhileSetting: true, - }); + response: { + result: toHex(1337), + }, + }, + ]); + const fakeNetworkClient = buildFakeClient(fakeProvider); + createNetworkClientMock.mockReturnValue(fakeNetworkClient); await operation(controller); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].status, - ).toBe(NetworkStatus.Unknown); + expect(createNetworkClientMock).toHaveBeenCalledWith({ + network: expectedProviderConfig.type, + infuraProjectId: 'infura-project-id', + chainId: BUILT_IN_NETWORKS[expectedProviderConfig.type].chainId, + type: NetworkClientType.Infura, + }); + 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(toHex(1337)); }, ); }); + } - it('resets the network details in state', async () => { - await withController( - { - state: initialState, - }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - // Called during provider initialization - { - request: { method: 'net_version' }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: PRE_1559_BLOCK, - }, - }, - // Called when calling the operation directly - { - request: { method: 'net_version' }, - error: GENERIC_JSON_RPC_ERROR, - }, - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: POST_1559_BLOCK, - }, - }, - ], - }); + it('replaces the provider object underlying the provider proxy without creating a new instance of the proxy itself', async () => { + await withController( + { + infuraProjectId: 'infura-project-id', + state: initialState, + }, + async ({ controller }) => { + const fakeProviders = [buildFakeProvider(), buildFakeProvider()]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + const initializationNetworkClientOptions: Parameters< + typeof createNetworkClient + >[0] = + controller.state.providerConfig.type === NetworkType.rpc + ? { + chainId: toHex(controller.state.providerConfig.chainId), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + rpcUrl: controller.state.providerConfig.rpcUrl!, + type: NetworkClientType.Custom, + } + : { + network: controller.state.providerConfig.type, + infuraProjectId: 'infura-project-id', + chainId: + BUILT_IN_NETWORKS[controller.state.providerConfig.type] + .chainId, + type: NetworkClientType.Infura, + }; + const operationNetworkClientOptions: Parameters< + typeof createNetworkClient + >[0] = + expectedProviderConfig.type === NetworkType.rpc + ? { + chainId: toHex(expectedProviderConfig.chainId), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + rpcUrl: expectedProviderConfig.rpcUrl!, + type: NetworkClientType.Custom, + } + : { + network: expectedProviderConfig.type, + infuraProjectId: 'infura-project-id', + chainId: BUILT_IN_NETWORKS[expectedProviderConfig.type].chainId, + type: NetworkClientType.Infura, + }; + mockCreateNetworkClient() + .calledWith(initializationNetworkClientOptions) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith(operationNetworkClientOptions) + .mockReturnValue(fakeNetworkClients[1]); + await controller.initializeProvider(); + const { provider: providerBefore } = + controller.getProviderAndBlockTracker(); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].EIPS[1559], - ).toBe(false); + await operation(controller); - await operation(controller); + const { provider: providerAfter } = + controller.getProviderAndBlockTracker(); + expect(providerBefore).toBe(providerAfter); + }, + ); + }); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].EIPS, - ).toStrictEqual({}); - }, - ); - }); + lookupNetworkTests({ expectedProviderConfig, initialState, operation }); +} - if (expectedProviderConfig.type === NetworkType.rpc) { - it('emits infuraIsUnblocked', async () => { +/** + * Test an operation that performs a `lookupNetwork` call with the given + * provider configuration. All effects of the `lookupNetwork` call should be + * covered by these tests. + * + * @param args - Arguments. + * @param args.expectedProviderConfig - The provider configuration that the + * operation is expected to set. + * @param args.initialState - The initial state of the network controller. + * @param args.operation - The operation to test. + */ +function lookupNetworkTests({ + expectedProviderConfig, + initialState, + operation, +}: { + expectedProviderConfig: ProviderConfig; + initialState?: Partial; + operation: (controller: NetworkController) => Promise; +}) { + describe('if the network details request resolve successfully', () => { + describe('if the network details of the current network are different from the network details in state', () => { + it('updates the network in state to match', async () => { await withController( { - state: initialState, + state: { + ...initialState, + networksMetadata: { + mainnet: { + EIPS: { 1559: false }, + status: NetworkStatus.Unknown, + }, + }, + }, }, - async ({ controller, messenger }) => { + async ({ controller }) => { await setFakeProvider(controller, { stubs: [ { - request: { method: 'net_version' }, - error: GENERIC_JSON_RPC_ERROR, + request: { + method: 'eth_getBlockByNumber', + params: ['latest', false], + }, + response: { + result: { + baseFeePerGas: '0x1', + }, + }, }, ], stubLookupNetworkWhileSetting: true, }); - const infuraIsUnblocked = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - operation: async () => { - await operation(controller); - }, - }); + await operation(controller); - await expect(infuraIsUnblocked).toBeFulfilled(); + expect( + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ].EIPS[1559], + ).toBe(true); }, ); }); - } else { - it('does not emit infuraIsUnblocked', async () => { + }); + + describe('if the network details of the current network are the same as the network details in state', () => { + it('does not change network details in state', async () => { await withController( { - state: initialState, + state: { + ...initialState, + networksMetadata: { + mainnet: { + EIPS: { 1559: true }, + status: NetworkStatus.Unknown, + }, + }, + }, }, - async ({ controller, messenger }) => { + async ({ controller }) => { await setFakeProvider(controller, { stubs: [ { - request: { method: 'net_version' }, - error: GENERIC_JSON_RPC_ERROR, + request: { + method: 'eth_getBlockByNumber', + params: ['latest', false], + }, + response: { + result: { + baseFeePerGas: '0x1', + }, + }, }, ], stubLookupNetworkWhileSetting: true, }); - const infuraIsUnblocked = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - count: 0, - operation: async () => { - await operation(controller); - }, - }); + await operation(controller); - await expect(infuraIsUnblocked).toBeFulfilled(); + expect( + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ].EIPS[1559], + ).toBe(true); }, ); }); - } + }); - it('does not emit infuraIsBlocked', async () => { + it('emits infuraIsUnblocked', async () => { await withController( { state: initialState, }, async ({ controller, messenger }) => { await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - error: GENERIC_JSON_RPC_ERROR, - }, - ], stubLookupNetworkWhileSetting: true, }); - const infuraIsBlocked = waitForPublishedEvents({ + const infuraIsUnblocked = waitForPublishedEvents({ messenger, - eventType: 'NetworkController:infuraIsBlocked', - count: 0, + eventType: 'NetworkController:infuraIsUnblocked', operation: async () => { await operation(controller); }, }); - await expect(infuraIsBlocked).toBeFulfilled(); - }, - ); - }); - }); - - describe('if an invalid network ID is returned', () => { - it('updates the network in state to "unknown"', async () => { - await withController( - { - state: initialState, - }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - response: { result: 'invalid' }, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - await operation(controller); - - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].status, - ).toBe(NetworkStatus.Unknown); - }, - ); - }); - - it('resets the network details in state', async () => { - await withController( - { - state: initialState, - }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - // Called during provider initialization - { - request: { method: 'net_version' }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: PRE_1559_BLOCK, - }, - }, - // Called when calling the operation directly - { - request: { method: 'net_version' }, - response: { result: 'invalid' }, - }, - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: POST_1559_BLOCK, - }, - }, - ], - }); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].EIPS[1559], - ).toBe(false); - - await operation(controller); - - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].EIPS, - ).toStrictEqual({}); + await expect(infuraIsUnblocked).toBeFulfilled(); }, ); }); - if (expectedProviderConfig.type === NetworkType.rpc) { - it('emits infuraIsUnblocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - response: { result: 'invalid' }, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const infuraIsUnblocked = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - operation: async () => { - await operation(controller); - }, - }); - - await expect(infuraIsUnblocked).toBeFulfilled(); - }, - ); - }); - } else { - it('does not emit infuraIsUnblocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - response: { result: 'invalid' }, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const infuraIsUnblocked = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - count: 0, - operation: async () => { - await operation(controller); - }, - }); - - await expect(infuraIsUnblocked).toBeFulfilled(); - }, - ); - }); - } - it('does not emit infuraIsBlocked', async () => { await withController( { @@ -8397,12 +6218,6 @@ function lookupNetworkTests({ }, async ({ controller, messenger }) => { await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - response: { result: 'invalid' }, - }, - ], stubLookupNetworkWhileSetting: true, }); @@ -9155,8 +6970,7 @@ function buildFakeClient( * optionally provided for certain RPC methods. * * @param stubs - The list of RPC methods you want to stub along with their - * responses. `eth_getBlockByNumber` and `net_version` will be stubbed by - * default. + * responses. `eth_getBlockByNumber` will be stubbed by default. * @returns The object. */ function buildFakeProvider(stubs: FakeProviderStub[] = []): Provider { @@ -9168,18 +6982,6 @@ function buildFakeProvider(stubs: FakeProviderStub[] = []): Provider { discardAfterMatching: false, }); } - if (!stubs.some((stub) => stub.request.method === 'net_version')) { - completeStubs.unshift({ - request: { method: 'net_version' }, - response: { result: '1' }, - discardAfterMatching: false, - }); - completeStubs.unshift({ - request: { method: 'net_version' }, - response: { result: '1' }, - discardAfterMatching: false, - }); - } return new FakeProvider({ stubs: completeStubs }); } diff --git a/packages/network-controller/tests/provider-api-tests/shared-tests.ts b/packages/network-controller/tests/provider-api-tests/shared-tests.ts index a6207eaf21..10e8d34ad6 100644 --- a/packages/network-controller/tests/provider-api-tests/shared-tests.ts +++ b/packages/network-controller/tests/provider-api-tests/shared-tests.ts @@ -266,14 +266,14 @@ export function testsForProviderType(providerType: ProviderType) { describe('eth_chainId', () => { it('does not hit the RPC endpoint, instead returning the configured chain id', async () => { - const networkId = await withNetworkClient( + const chainId = await withNetworkClient( { providerType: 'custom', customChainId: '0x1' }, ({ makeRpcCall }) => { return makeRpcCall({ method: 'eth_chainId' }); }, ); - expect(networkId).toBe('0x1'); + expect(chainId).toBe('0x1'); }); }); }); @@ -316,44 +316,29 @@ export function testsForProviderType(providerType: ProviderType) { describe('other methods', () => { describe('net_version', () => { - // The Infura middleware includes `net_version` in its scaffold - // middleware, whereas the custom RPC middleware does not. - if (providerType === 'infura') { - it('does not hit Infura, instead returning the network ID that maps to the Infura network, as a decimal string', async () => { + const networkArgs = { + providerType, + infuraNetwork: providerType === 'infura' ? 'goerli' : undefined, + } as const; + it('hits the RPC endpoint', async () => { + await withMockedCommunications(networkArgs, async (comms) => { + comms.mockRpcCall({ + request: { method: 'net_version' }, + response: { result: '1' }, + }); + const networkId = await withNetworkClient( - { providerType: 'infura', infuraNetwork: 'goerli' }, + networkArgs, ({ makeRpcCall }) => { return makeRpcCall({ method: 'net_version', }); }, ); - expect(networkId).toBe('5'); - }); - } else { - it('hits the RPC endpoint', async () => { - await withMockedCommunications( - { providerType: 'custom' }, - async (comms) => { - comms.mockRpcCall({ - request: { method: 'net_version' }, - response: { result: '1' }, - }); - const networkId = await withNetworkClient( - { providerType: 'custom' }, - ({ makeRpcCall }) => { - return makeRpcCall({ - method: 'net_version', - }); - }, - ); - - expect(networkId).toBe('1'); - }, - ); + expect(networkId).toBe('1'); }); - } + }); }); }); }); diff --git a/packages/selected-network-controller/tests/SelectedNetworkMiddleware.test.ts b/packages/selected-network-controller/tests/SelectedNetworkMiddleware.test.ts index bfbbdd5472..8d7a19e86c 100644 --- a/packages/selected-network-controller/tests/SelectedNetworkMiddleware.test.ts +++ b/packages/selected-network-controller/tests/SelectedNetworkMiddleware.test.ts @@ -98,10 +98,10 @@ describe('createSelectedNetworkMiddleware', () => { end(); }); engine.push(mockNextMiddleware); - const testNetworkId = 'testNetworkId'; + const testNetworkClientId = 'testNetworkClientId'; const mockGetNetworkClientIdForDomain = jest .fn() - .mockReturnValue(testNetworkId); + .mockReturnValue(testNetworkClientId); messenger.registerActionHandler( SelectedNetworkControllerActionTypes.getNetworkClientIdForDomain, mockGetNetworkClientIdForDomain, @@ -114,14 +114,14 @@ describe('createSelectedNetworkMiddleware', () => { }); expect(mockNextMiddleware).toHaveBeenCalledWith( expect.objectContaining({ - networkClientId: testNetworkId, + networkClientId: testNetworkClientId, }), expect.any(Object), expect.any(Function), expect.any(Function), ); expect(result).toStrictEqual( - expect.objectContaining({ result: testNetworkId }), + expect.objectContaining({ result: testNetworkClientId }), ); }); }); diff --git a/packages/transaction-controller/jest.config.js b/packages/transaction-controller/jest.config.js index 1ad58aefdd..0eb086af4c 100644 --- a/packages/transaction-controller/jest.config.js +++ b/packages/transaction-controller/jest.config.js @@ -18,9 +18,9 @@ module.exports = merge(baseConfig, { coverageThreshold: { global: { branches: 84.86, - functions: 93.12, - lines: 96.26, - statements: 96.33, + functions: 93.03, + lines: 96.07, + statements: 96.15, }, }, diff --git a/packages/transaction-controller/src/EtherscanRemoteTransactionSource.test.ts b/packages/transaction-controller/src/EtherscanRemoteTransactionSource.test.ts index 27bbed42cc..75b7f878f3 100644 --- a/packages/transaction-controller/src/EtherscanRemoteTransactionSource.test.ts +++ b/packages/transaction-controller/src/EtherscanRemoteTransactionSource.test.ts @@ -104,10 +104,10 @@ const EXPECTED_NORMALISED_TRANSACTION_BASE = { chainId: undefined, hash: ETHERSCAN_TRANSACTION_SUCCESS_MOCK.hash, id: ID_MOCK, - networkID: undefined, status: TransactionStatus.confirmed, time: 1543596356000, txParams: { + chainId: undefined, from: ETHERSCAN_TRANSACTION_SUCCESS_MOCK.from, gas: '0x51d68', gasPrice: '0x4a817c800', @@ -175,7 +175,6 @@ describe('EtherscanRemoteTransactionSource', () => { expect( new EtherscanRemoteTransactionSource().isSupportedNetwork( CHAIN_IDS.MAINNET, - '1', ), ).toBe(true); }); @@ -184,7 +183,6 @@ describe('EtherscanRemoteTransactionSource', () => { expect( new EtherscanRemoteTransactionSource().isSupportedNetwork( '0x1324567891234', - '1', ), ).toBe(false); }); diff --git a/packages/transaction-controller/src/EtherscanRemoteTransactionSource.ts b/packages/transaction-controller/src/EtherscanRemoteTransactionSource.ts index 16dd0b4a78..7ab2460ff8 100644 --- a/packages/transaction-controller/src/EtherscanRemoteTransactionSource.ts +++ b/packages/transaction-controller/src/EtherscanRemoteTransactionSource.ts @@ -40,7 +40,7 @@ export class EtherscanRemoteTransactionSource this.#isTokenRequestPending = false; } - isSupportedNetwork(chainId: Hex, _networkId: string): boolean { + isSupportedNetwork(chainId: Hex): boolean { return Object.keys(ETHERSCAN_SUPPORTED_NETWORKS).includes(chainId); } @@ -71,14 +71,14 @@ export class EtherscanRemoteTransactionSource request: RemoteTransactionSourceRequest, etherscanRequest: EtherscanTransactionRequest, ) => { - const { currentNetworkId, currentChainId } = request; + const { currentChainId } = request; const etherscanTransactions = await fetchEtherscanTransactions( etherscanRequest, ); return this.#getResponseTransactions(etherscanTransactions).map((tx) => - this.#normalizeTransaction(tx, currentNetworkId, currentChainId), + this.#normalizeTransaction(tx, currentChainId), ); }; @@ -86,14 +86,14 @@ export class EtherscanRemoteTransactionSource request: RemoteTransactionSourceRequest, etherscanRequest: EtherscanTransactionRequest, ) => { - const { currentNetworkId, currentChainId } = request; + const { currentChainId } = request; const etherscanTransactions = await fetchEtherscanTokenTransactions( etherscanRequest, ); return this.#getResponseTransactions(etherscanTransactions).map((tx) => - this.#normalizeTokenTransaction(tx, currentNetworkId, currentChainId), + this.#normalizeTokenTransaction(tx, currentChainId), ); }; @@ -118,14 +118,9 @@ export class EtherscanRemoteTransactionSource #normalizeTransaction( txMeta: EtherscanTransactionMeta, - currentNetworkId: string, currentChainId: Hex, ): TransactionMeta { - const base = this.#normalizeTransactionBase( - txMeta, - currentNetworkId, - currentChainId, - ); + const base = this.#normalizeTransactionBase(txMeta, currentChainId); return { ...base, @@ -144,14 +139,9 @@ export class EtherscanRemoteTransactionSource #normalizeTokenTransaction( txMeta: EtherscanTokenTransactionMeta, - currentNetworkId: string, currentChainId: Hex, ): TransactionMeta { - const base = this.#normalizeTransactionBase( - txMeta, - currentNetworkId, - currentChainId, - ); + const base = this.#normalizeTransactionBase(txMeta, currentChainId); return { ...base, @@ -166,7 +156,6 @@ export class EtherscanRemoteTransactionSource #normalizeTransactionBase( txMeta: EtherscanTransactionMetaBase, - currentNetworkId: string, currentChainId: Hex, ): TransactionMeta { const time = parseInt(txMeta.timeStamp, 10) * 1000; @@ -176,10 +165,10 @@ export class EtherscanRemoteTransactionSource chainId: currentChainId, hash: txMeta.hash, id: random({ msecs: time }), - networkID: currentNetworkId, status: TransactionStatus.confirmed, time, txParams: { + chainId: currentChainId, from: txMeta.from, gas: BNToHex(new BN(txMeta.gas)), gasPrice: BNToHex(new BN(txMeta.gasPrice)), diff --git a/packages/transaction-controller/src/IncomingTransactionHelper.test.ts b/packages/transaction-controller/src/IncomingTransactionHelper.test.ts index 6c174d288b..f9de513561 100644 --- a/packages/transaction-controller/src/IncomingTransactionHelper.test.ts +++ b/packages/transaction-controller/src/IncomingTransactionHelper.test.ts @@ -24,7 +24,6 @@ const NETWORK_STATE_MOCK: NetworkState = { chainId: '0x1', type: NetworkType.mainnet, }, - networkId: '1', } as unknown as NetworkState; const ADDERSS_MOCK = '0x1'; @@ -155,7 +154,6 @@ describe('IncomingTransactionHelper', () => { expect(remoteTransactionSource.fetchTransactions).toHaveBeenCalledWith({ address: ADDERSS_MOCK, currentChainId: NETWORK_STATE_MOCK.providerConfig.chainId, - currentNetworkId: NETWORK_STATE_MOCK.networkId, fromBlock: undefined, limit: CONTROLLER_ARGS_MOCK.transactionLimit, }); diff --git a/packages/transaction-controller/src/IncomingTransactionHelper.ts b/packages/transaction-controller/src/IncomingTransactionHelper.ts index 0686a7bf66..c5be8b81c8 100644 --- a/packages/transaction-controller/src/IncomingTransactionHelper.ts +++ b/packages/transaction-controller/src/IncomingTransactionHelper.ts @@ -133,7 +133,6 @@ export class IncomingTransactionHelper { const address = this.#getCurrentAccount(); const currentChainId = this.#getCurrentChainId(); - const currentNetworkId = this.#getCurrentNetworkId(); let remoteTransactions = []; @@ -142,7 +141,6 @@ export class IncomingTransactionHelper { await this.#remoteTransactionSource.fetchTransactions({ address, currentChainId, - currentNetworkId, fromBlock, limit: this.#transactionLimit, }); @@ -295,12 +293,9 @@ export class IncomingTransactionHelper { #canStart(): boolean { const isEnabled = this.#isEnabled(); const currentChainId = this.#getCurrentChainId(); - const currentNetworkId = this.#getCurrentNetworkId(); - const isSupportedNetwork = this.#remoteTransactionSource.isSupportedNetwork( - currentChainId, - currentNetworkId, - ); + const isSupportedNetwork = + this.#remoteTransactionSource.isSupportedNetwork(currentChainId); return isEnabled && isSupportedNetwork; } @@ -308,8 +303,4 @@ export class IncomingTransactionHelper { #getCurrentChainId(): Hex { return this.#getNetworkState().providerConfig.chainId; } - - #getCurrentNetworkId(): string { - return this.#getNetworkState().networkId as string; - } } diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 19b9ec4f79..d8024f8e7a 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -266,7 +266,6 @@ const MOCK_NETWORK: MockNetwork = { blockTracker: buildMockBlockTracker('0x102833C'), state: { selectedNetworkClientId: NetworkType.goerli, - networkId: '5', networksMetadata: { [NetworkType.goerli]: { EIPS: { 1559: false }, @@ -287,7 +286,6 @@ const MOCK_NETWORK_WITHOUT_CHAIN_ID: MockNetwork = { blockTracker: buildMockBlockTracker('0x102833C'), state: { selectedNetworkClientId: NetworkType.goerli, - networkId: '5', networksMetadata: { [NetworkType.goerli]: { EIPS: { 1559: false }, @@ -306,7 +304,6 @@ const MOCK_MAINNET_NETWORK: MockNetwork = { blockTracker: buildMockBlockTracker('0x102833C'), state: { selectedNetworkClientId: NetworkType.mainnet, - networkId: '1', networksMetadata: { [NetworkType.mainnet]: { EIPS: { 1559: false }, @@ -328,7 +325,6 @@ const MOCK_LINEA_MAINNET_NETWORK: MockNetwork = { blockTracker: buildMockBlockTracker('0xA6EDFC'), state: { selectedNetworkClientId: NetworkType['linea-mainnet'], - networkId: '59144', networksMetadata: { [NetworkType['linea-mainnet']]: { EIPS: { 1559: false }, @@ -350,7 +346,6 @@ const MOCK_LINEA_GOERLI_NETWORK: MockNetwork = { blockTracker: buildMockBlockTracker('0xA6EDFC'), state: { selectedNetworkClientId: NetworkType['linea-goerli'], - networkId: '59140', networksMetadata: { [NetworkType['linea-goerli']]: { EIPS: { 1559: false }, @@ -372,7 +367,6 @@ const MOCK_CUSTOM_NETWORK: MockNetwork = { blockTracker: buildMockBlockTracker('0xA6EDFC'), state: { selectedNetworkClientId: 'uuid-1', - networkId: '11297108109', networksMetadata: { 'uuid-1': { EIPS: { 1559: false }, @@ -943,7 +937,6 @@ describe('TransactionController', () => { const transactionMeta = controller.state.transactions[0]; expect(transactionMeta.txParams.from).toBe(ACCOUNT_MOCK); - expect(transactionMeta.networkID).toBe(MOCK_NETWORK.state.networkId); expect(transactionMeta.chainId).toBe( MOCK_NETWORK.state.providerConfig.chainId, ); @@ -973,7 +966,6 @@ describe('TransactionController', () => { dappSuggestedGasFees: undefined, deviceConfirmedOn: undefined, id: expect.any(String), - networkID: expect.any(String), origin: undefined, originalGasEstimate: expect.any(String), securityAlertResponse: undefined, @@ -1075,9 +1067,6 @@ describe('TransactionController', () => { expect(controller.state.transactions[0].txParams.from).toBe( ACCOUNT_MOCK, ); - expect(controller.state.transactions[0].networkID).toBe( - newNetwork.state.networkId, - ); expect(controller.state.transactions[0].chainId).toBe( newNetwork.state.providerConfig.chainId, ); @@ -1413,26 +1402,6 @@ describe('TransactionController', () => { expect(controller.state.transactions).toHaveLength(0); }); - // This tests the fallback to networkId only when there is no chainId present. - // It should be removed when networkID is completely removed. - it('removes all transactions with matching networkId when there is no chainId', async () => { - const controller = newController(); - - controller.wipeTransactions(); - - controller.state.transactions.push({ - from: MOCK_PREFERENCES.state.selectedAddress, - hash: '1337', - id: 'foo', - networkID: '5', - status: TransactionStatus.submitted, - } as any); - - controller.wipeTransactions(); - - expect(controller.state.transactions).toHaveLength(0); - }); - it('removes only txs with given address', async () => { const controller = newController(); @@ -1505,32 +1474,6 @@ describe('TransactionController', () => { from: MOCK_PREFERENCES.state.selectedAddress, hash: '1337', id: 'foo', - networkID: '5', - status: TransactionStatus.submitted, - } as any); - - controller.state.transactions.push({} as any); - - const confirmedPromise = waitForTransactionFinished(controller, { - confirmed: true, - }); - - await controller.queryTransactionStatuses(); - - const { status } = await confirmedPromise; - expect(status).toBe(TransactionStatus.confirmed); - }); - - // This tests the fallback to networkId only when there is no chainId present. - // It should be removed when networkId is completely removed. - it('uses networkId only when there is no chainId', async () => { - const controller = newController(); - - controller.state.transactions.push({ - from: MOCK_PREFERENCES.state.selectedAddress, - hash: '1337', - id: 'foo', - networkID: '5', status: TransactionStatus.submitted, } as any); @@ -1552,7 +1495,6 @@ describe('TransactionController', () => { controller.state.transactions.push({ from: MOCK_PREFERENCES.state.selectedAddress, id: 'foo', - networkID: '5', status: TransactionStatus.submitted, hash: '1338', } as any); @@ -1571,7 +1513,6 @@ describe('TransactionController', () => { from: MOCK_PREFERENCES.state.selectedAddress, hash: '1337', id: 'foo', - networkID: '5', status: TransactionStatus.confirmed, txParams: { gasUsed: undefined, @@ -1858,7 +1799,7 @@ describe('TransactionController', () => { from: ACCOUNT_MOCK, hash: '1337', id: 'mocked', - networkID: '5', + chainId: toHex(5), status: TransactionStatus.unapproved, }; const controller = newController(); @@ -1912,7 +1853,6 @@ describe('TransactionController', () => { from: ACCOUNT_MOCK, to: ACCOUNT_2_MOCK, id: '1', - networkID: '1', chainId: toHex(1), status: TransactionStatus.confirmed, txParams: { @@ -1950,7 +1890,6 @@ describe('TransactionController', () => { from: ACCOUNT_MOCK, to: ACCOUNT_2_MOCK, id: '1', - networkID: '1', chainId: toHex(1), status: TransactionStatus.confirmed, txParams: { @@ -1974,7 +1913,6 @@ describe('TransactionController', () => { chainId: '0x1', from: ACCOUNT_MOCK, id: '1', - networkID: '1', status: TransactionStatus.confirmed, to: ACCOUNT_2_MOCK, txParams: { @@ -2022,7 +1960,6 @@ describe('TransactionController', () => { to: ACCOUNT_2_MOCK, hash: externalTransactionHash, id: externalTransactionId, - networkID: '5', chainId: toHex(5), status: TransactionStatus.confirmed, txParams: { @@ -2042,7 +1979,6 @@ describe('TransactionController', () => { from: ACCOUNT_MOCK, to: ACCOUNT_2_MOCK, id: localTransactionIdWithSameNonce, - networkID: '5', chainId: toHex(5), status: TransactionStatus.unapproved, txParams: { @@ -2078,7 +2014,6 @@ describe('TransactionController', () => { to: ACCOUNT_2_MOCK, hash: externalTransactionHash, id: externalTransactionId, - networkID: '5', chainId: toHex(5), status: TransactionStatus.confirmed, txParams: { @@ -2098,7 +2033,6 @@ describe('TransactionController', () => { from: ACCOUNT_MOCK, to: ACCOUNT_2_MOCK, id: localTransactionIdWithSameNonce, - networkID: '5', chainId: toHex(5), status: TransactionStatus.failed, txParams: { @@ -2273,7 +2207,6 @@ describe('TransactionController', () => { controller.state.transactions.push({ from: MOCK_PREFERENCES.state.selectedAddress, id: 'foo', - networkID: '5', chainId: toHex(5), status: TransactionStatus.submitted, transactionHash: '1337', diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 2553368ade..0cd605fea7 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -61,7 +61,6 @@ import { isEIP1559Transaction, isFeeMarketEIP1559Values, isGasPriceValue, - transactionMatchesNetwork, validateGasValues, validateIfTransactionUnapproved, validateMinimumIncrease, @@ -424,7 +423,7 @@ export class TransactionController extends BaseController< type?: TransactionType; } = {}, ): Promise { - const { chainId, networkId } = this.getChainAndNetworkId(); + const chainId = this.getChainId(); const { transactions } = this.state; txParams = normalizeTxParams(txParams); validateTxParams(txParams); @@ -446,7 +445,6 @@ export class TransactionController extends BaseController< dappSuggestedGasFees, deviceConfirmedOn, id: random(), - networkID: networkId ?? undefined, origin, securityAlertResponse, status: TransactionStatus.unapproved as TransactionStatus.unapproved, @@ -508,11 +506,11 @@ export class TransactionController extends BaseController< * Creates approvals for all unapproved transactions persisted. */ initApprovals() { - const { networkId, chainId } = this.getChainAndNetworkId(); + const chainId = this.getChainId(); const unapprovedTxs = this.state.transactions.filter( (transaction) => transaction.status === TransactionStatus.unapproved && - transactionMatchesNetwork(transaction, chainId, networkId), + transaction.chainId === chainId, ); for (const txMeta of unapprovedTxs) { @@ -859,19 +857,12 @@ export class TransactionController extends BaseController< */ async queryTransactionStatuses() { const { transactions } = this.state; - const { chainId: currentChainId, networkId: currentNetworkID } = - this.getChainAndNetworkId(); + const currentChainId = this.getChainId(); let gotUpdates = false; await safelyExecute(() => Promise.all( transactions.map(async (meta, index) => { - // Using fallback to networkID only when there is no chainId present. - // Should be removed when networkID is completely removed. - const txBelongsToCurrentChain = - meta.chainId === currentChainId || - (!meta.chainId && meta.networkID === currentNetworkID); - - if (!meta.verifiedOnBlockchain && txBelongsToCurrentChain) { + if (!meta.verifiedOnBlockchain && meta.chainId === currentChainId) { const [reconciledTx, updateRequired] = await this.blockchainTransactionStateReconciler(meta); if (updateRequired) { @@ -923,15 +914,10 @@ export class TransactionController extends BaseController< this.update({ transactions: [] }); return; } - const { chainId: currentChainId, networkId: currentNetworkID } = - this.getChainAndNetworkId(); + const currentChainId = this.getChainId(); const newTransactions = this.state.transactions.filter( - ({ networkID, chainId, txParams }) => { - // Using fallback to networkID only when there is no chainId present. Should be removed when networkID is completely removed. - const isMatchingNetwork = - ignoreNetwork || - chainId === currentChainId || - (!chainId && networkID === currentNetworkID); + ({ chainId, txParams }) => { + const isMatchingNetwork = ignoreNetwork || chainId === currentChainId; if (!isMatchingNetwork) { return true; @@ -1227,7 +1213,7 @@ export class TransactionController extends BaseController< private async approveTransaction(transactionId: string) { const { transactions } = this.state; const releaseLock = await this.mutex.acquire(); - const { chainId } = this.getChainAndNetworkId(); + const chainId = this.getChainId(); const index = transactions.findIndex(({ id }) => transactionId === id); const transactionMeta = transactions[index]; const { @@ -1362,12 +1348,12 @@ export class TransactionController extends BaseController< const txsToKeep = transactions .sort((a, b) => (a.time > b.time ? -1 : 1)) // Descending time order .filter((tx) => { - const { chainId, networkID, status, txParams, time } = tx; + const { chainId, status, txParams, time } = tx; if (txParams) { - const key = `${txParams.nonce}-${ - chainId ? convertHexToDecimal(chainId) : networkID - }-${new Date(time).toDateString()}`; + const key = `${txParams.nonce}-${convertHexToDecimal( + chainId, + )}-${new Date(time).toDateString()}`; if (nonceNetworkSet.has(key)) { return true; @@ -1559,13 +1545,9 @@ export class TransactionController extends BaseController< return { meta: transaction, isCompleted }; } - private getChainAndNetworkId(): { - networkId: string | null; - chainId: Hex; - } { - const { networkId, providerConfig } = this.getNetworkState(); - const chainId = providerConfig?.chainId; - return { networkId, chainId }; + private getChainId(): Hex { + const { providerConfig } = this.getNetworkState(); + return providerConfig.chainId; } private prepareUnsignedEthTx( @@ -1588,7 +1570,6 @@ export class TransactionController extends BaseController< */ private getCommonConfiguration(): Common { const { - networkId, providerConfig: { type: chain, chainId, nickname: name }, } = this.getNetworkState(); @@ -1603,7 +1584,6 @@ export class TransactionController extends BaseController< const customChainParams: Partial = { name, chainId: parseInt(chainId, 16), - networkId: networkId === null ? NaN : parseInt(networkId, undefined), defaultHardfork: HARDFORK, }; @@ -1692,13 +1672,13 @@ export class TransactionController extends BaseController< * @param transactionMeta - Nominated external transaction to be added to state. */ private async addExternalTransaction(transactionMeta: TransactionMeta) { - const { networkId, chainId } = this.getChainAndNetworkId(); + const chainId = this.getChainId(); const { transactions } = this.state; const fromAddress = transactionMeta?.txParams?.from; const sameFromAndNetworkTransactions = transactions.filter( (transaction) => transaction.txParams.from === fromAddress && - transactionMatchesNetwork(transaction, chainId, networkId), + transaction.chainId === chainId, ); const confirmedTxs = sameFromAndNetworkTransactions.filter( (transaction) => transaction.status === TransactionStatus.confirmed, @@ -1733,7 +1713,7 @@ export class TransactionController extends BaseController< * @param transactionId - Used to identify original transaction. */ private markNonceDuplicatesDropped(transactionId: string) { - const { networkId, chainId } = this.getChainAndNetworkId(); + const chainId = this.getChainId(); const transactionMeta = this.getTransaction(transactionId); const nonce = transactionMeta?.txParams?.nonce; const from = transactionMeta?.txParams?.from; @@ -1741,7 +1721,7 @@ export class TransactionController extends BaseController< (transaction) => transaction.txParams.from === from && transaction.txParams.nonce === nonce && - transactionMatchesNetwork(transaction, chainId, networkId), + transaction.chainId === chainId, ); if (!sameNonceTxs.length) { diff --git a/packages/transaction-controller/src/constants.ts b/packages/transaction-controller/src/constants.ts index 35ead59ad1..a6deee741a 100644 --- a/packages/transaction-controller/src/constants.ts +++ b/packages/transaction-controller/src/constants.ts @@ -29,97 +29,78 @@ export const ETHERSCAN_SUPPORTED_NETWORKS = { [CHAIN_IDS.GOERLI]: { domain: DEFAULT_ETHERSCAN_DOMAIN, subdomain: `${DEFAULT_ETHERSCAN_SUBDOMAIN_PREFIX}-goerli`, - networkId: parseInt(CHAIN_IDS.GOERLI, 16).toString(), }, [CHAIN_IDS.MAINNET]: { domain: DEFAULT_ETHERSCAN_DOMAIN, subdomain: DEFAULT_ETHERSCAN_SUBDOMAIN_PREFIX, - networkId: parseInt(CHAIN_IDS.MAINNET, 16).toString(), }, [CHAIN_IDS.SEPOLIA]: { domain: DEFAULT_ETHERSCAN_DOMAIN, subdomain: `${DEFAULT_ETHERSCAN_SUBDOMAIN_PREFIX}-sepolia`, - networkId: parseInt(CHAIN_IDS.SEPOLIA, 16).toString(), }, [CHAIN_IDS.LINEA_GOERLI]: { domain: 'lineascan.build', subdomain: 'goerli', - networkId: parseInt(CHAIN_IDS.LINEA_GOERLI, 16).toString(), }, [CHAIN_IDS.LINEA_MAINNET]: { domain: 'lineascan.build', subdomain: DEFAULT_ETHERSCAN_SUBDOMAIN_PREFIX, - networkId: parseInt(CHAIN_IDS.LINEA_MAINNET, 16).toString(), }, [CHAIN_IDS.BSC]: { domain: 'bscscan.com', subdomain: DEFAULT_ETHERSCAN_SUBDOMAIN_PREFIX, - networkId: parseInt(CHAIN_IDS.BSC, 16).toString(), }, [CHAIN_IDS.BSC_TESTNET]: { domain: 'bscscan.com', subdomain: `${DEFAULT_ETHERSCAN_SUBDOMAIN_PREFIX}-testnet`, - networkId: parseInt(CHAIN_IDS.BSC_TESTNET, 16).toString(), }, [CHAIN_IDS.OPTIMISM]: { domain: DEFAULT_ETHERSCAN_DOMAIN, subdomain: `${DEFAULT_ETHERSCAN_SUBDOMAIN_PREFIX}-optimistic`, - networkId: parseInt(CHAIN_IDS.OPTIMISM, 16).toString(), }, [CHAIN_IDS.OPTIMISM_TESTNET]: { domain: DEFAULT_ETHERSCAN_DOMAIN, subdomain: `${DEFAULT_ETHERSCAN_SUBDOMAIN_PREFIX}-goerli-optimistic`, - networkId: parseInt(CHAIN_IDS.OPTIMISM_TESTNET, 16).toString(), }, [CHAIN_IDS.POLYGON]: { domain: 'polygonscan.com', subdomain: DEFAULT_ETHERSCAN_SUBDOMAIN_PREFIX, - networkId: parseInt(CHAIN_IDS.POLYGON, 16).toString(), }, [CHAIN_IDS.POLYGON_TESTNET]: { domain: 'polygonscan.com', subdomain: `${DEFAULT_ETHERSCAN_SUBDOMAIN_PREFIX}-mumbai`, - networkId: parseInt(CHAIN_IDS.POLYGON_TESTNET, 16).toString(), }, [CHAIN_IDS.AVALANCHE]: { domain: 'snowtrace.io', subdomain: DEFAULT_ETHERSCAN_SUBDOMAIN_PREFIX, - networkId: parseInt(CHAIN_IDS.AVALANCHE, 16).toString(), }, [CHAIN_IDS.AVALANCHE_TESTNET]: { domain: 'snowtrace.io', subdomain: `${DEFAULT_ETHERSCAN_SUBDOMAIN_PREFIX}-testnet`, - networkId: parseInt(CHAIN_IDS.AVALANCHE_TESTNET, 16).toString(), }, [CHAIN_IDS.FANTOM]: { domain: 'ftmscan.com', subdomain: DEFAULT_ETHERSCAN_SUBDOMAIN_PREFIX, - networkId: parseInt(CHAIN_IDS.FANTOM, 16).toString(), }, [CHAIN_IDS.FANTOM_TESTNET]: { domain: 'ftmscan.com', subdomain: `${DEFAULT_ETHERSCAN_SUBDOMAIN_PREFIX}-testnet`, - networkId: parseInt(CHAIN_IDS.FANTOM_TESTNET, 16).toString(), }, [CHAIN_IDS.MOONBEAM]: { domain: 'moonscan.io', subdomain: `${DEFAULT_ETHERSCAN_SUBDOMAIN_PREFIX}-moonbeam`, - networkId: parseInt(CHAIN_IDS.MOONBEAM, 16).toString(), }, [CHAIN_IDS.MOONBEAM_TESTNET]: { domain: 'moonscan.io', subdomain: `${DEFAULT_ETHERSCAN_SUBDOMAIN_PREFIX}-moonbase`, - networkId: parseInt(CHAIN_IDS.MOONBEAM_TESTNET, 16).toString(), }, [CHAIN_IDS.MOONRIVER]: { domain: 'moonscan.io', subdomain: `${DEFAULT_ETHERSCAN_SUBDOMAIN_PREFIX}-moonriver`, - networkId: parseInt(CHAIN_IDS.MOONRIVER, 16).toString(), }, [CHAIN_IDS.GNOSIS]: { domain: 'gnosisscan.io', subdomain: `${DEFAULT_ETHERSCAN_SUBDOMAIN_PREFIX}-gnosis`, - networkId: parseInt(CHAIN_IDS.GNOSIS, 16).toString(), }, }; diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index 6f3d402df0..5cf4544d57 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -37,7 +37,7 @@ type TransactionMetaBase = { /** * Network code as per EIP-155 for this transaction. */ - chainId?: Hex; + chainId: Hex; /** * Gas values provided by the dApp. @@ -90,9 +90,11 @@ type TransactionMetaBase = { isTransfer?: boolean; /** - * Network code as per EIP-155 for this transaction. + * Network code as per EIP-155 for this transaction + * + * @deprecated Use `chainId` instead. */ - networkID?: string; + readonly networkID?: string; /** * Origin this transaction was sent from. @@ -503,11 +505,6 @@ export interface RemoteTransactionSourceRequest { */ currentChainId: Hex; - /** - * The networkId of the current network. - */ - currentNetworkId: string; - /** * Block number to start fetching transactions from. */ @@ -526,10 +523,9 @@ export interface RemoteTransactionSourceRequest { export interface RemoteTransactionSource { /** * @param chainId - The chainId of the current network. - * @param networkId - The networkId of the current network. * @returns Whether the remote transaction source supports the specified network. */ - isSupportedNetwork: (chainId: Hex, networkId: string) => boolean; + isSupportedNetwork: (chainId: Hex) => boolean; /** * @returns An array of additional keys to use when caching the last fetched block number. diff --git a/packages/transaction-controller/src/utils.test.ts b/packages/transaction-controller/src/utils.test.ts index c326af2532..d0932826a1 100644 --- a/packages/transaction-controller/src/utils.test.ts +++ b/packages/transaction-controller/src/utils.test.ts @@ -7,10 +7,6 @@ import type { import type { TransactionParams, TransactionMeta } from './types'; import { TransactionStatus } from './types'; import * as util from './utils'; -import { - getAndFormatTransactionsForNonceTracker, - transactionMatchesNetwork, -} from './utils'; const MAX_FEE_PER_GAS = 'maxFeePerGas'; const MAX_PRIORITY_FEE_PER_GAS = 'maxPriorityFeePerGas'; @@ -19,6 +15,10 @@ const FAIL = 'lol'; const PASS = '0x1'; describe('utils', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + it('normalizeTxParams', () => { const normalized = util.normalizeTxParams({ data: 'data', @@ -255,6 +255,7 @@ describe('utils', () => { const inputTransactions: TransactionMeta[] = [ { id: '1', + chainId: '0x1', time: 123456, txParams: { from: fromAddress, @@ -266,6 +267,7 @@ describe('utils', () => { }, { id: '2', + chainId: '0x1', time: 123457, txParams: { from: '0x124', @@ -277,6 +279,7 @@ describe('utils', () => { }, { id: '3', + chainId: '0x1', time: 123458, txParams: { from: fromAddress, @@ -301,7 +304,7 @@ describe('utils', () => { }, ]; - const result = getAndFormatTransactionsForNonceTracker( + const result = util.getAndFormatTransactionsForNonceTracker( fromAddress, TransactionStatus.confirmed, inputTransactions, @@ -309,75 +312,4 @@ describe('utils', () => { expect(result).toStrictEqual(expectedResult); }); }); - - describe('transactionMatchesNetwork', () => { - const transaction: TransactionMeta = { - chainId: '0x1', - networkID: '1', - id: '1', - time: 123456, - txParams: { - from: '0x123', - gas: '0x100', - value: '0x200', - nonce: '0x1', - }, - status: TransactionStatus.unapproved, - }; - it('returns true if chainId matches', () => { - const chainId = '0x1'; - const networkId = '1'; - expect(transactionMatchesNetwork(transaction, chainId, networkId)).toBe( - true, - ); - }); - - it('returns false if chainId does not match', () => { - const chainId = '0x1'; - const networkId = '1'; - expect( - transactionMatchesNetwork( - { ...transaction, chainId: '0x2' }, - chainId, - networkId, - ), - ).toBe(false); - }); - - it('returns true if networkID matches', () => { - const chainId = '0x1'; - const networkId = '1'; - expect( - transactionMatchesNetwork( - { ...transaction, chainId: undefined }, - chainId, - networkId, - ), - ).toBe(true); - }); - - it('returns false if networkID does not match', () => { - const chainId = '0x1'; - const networkId = '1'; - expect( - transactionMatchesNetwork( - { ...transaction, networkID: '2', chainId: undefined }, - chainId, - networkId, - ), - ).toBe(false); - }); - - it('returns true if chainId and networkID are undefined', () => { - const chainId = '0x2'; - const networkId = '1'; - expect( - transactionMatchesNetwork( - { ...transaction, chainId: undefined, networkID: undefined }, - chainId, - networkId, - ), - ).toBe(false); - }); - }); }); diff --git a/packages/transaction-controller/src/utils.ts b/packages/transaction-controller/src/utils.ts index 1a289ed1b6..7bab7d91e4 100644 --- a/packages/transaction-controller/src/utils.ts +++ b/packages/transaction-controller/src/utils.ts @@ -2,7 +2,6 @@ import { convertHexToDecimal, isValidHexAddress, } from '@metamask/controller-utils'; -import type { Hex } from '@metamask/utils'; import { addHexPrefix, isHexString } from 'ethereumjs-util'; import type { Transaction as NonceTrackerTransaction } from 'nonce-tracker/dist/NonceTracker'; @@ -208,30 +207,6 @@ export function getAndFormatTransactionsForNonceTracker( }); } -/** - * Checks whether a given transaction matches the specified network or chain ID. - * This function is used to determine if a transaction is relevant to the current network or chain. - * - * @param transaction - The transaction metadata to check. - * @param chainId - The chain ID of the current network. - * @param networkId - The network ID of the current network. - * @returns A boolean value indicating whether the transaction matches the current network or chain ID. - */ -export function transactionMatchesNetwork( - transaction: TransactionMeta, - chainId: Hex, - networkId: string | null, -) { - if (transaction.chainId) { - return transaction.chainId === chainId; - } - - if (transaction.networkID) { - return transaction.networkID === networkId; - } - return false; -} - /** * Validates that a transaction is unapproved. * Throws if the transaction is not unapproved.