mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-30 08:09:15 +01:00
73efd2edeb
The network controller method `rollbackToPreviousProvider` is now async. It will resolve when the network switch has completed. Relates to https://github.com/MetaMask/metamask-extension/issues/18587
8088 lines
270 KiB
TypeScript
8088 lines
270 KiB
TypeScript
import { inspect, isDeepStrictEqual, promisify } from 'util';
|
|
import assert from 'assert';
|
|
import { get, isMatch, omit } from 'lodash';
|
|
import { v4 } from 'uuid';
|
|
import nock, { Scope as NockScope } from 'nock';
|
|
import sinon, { SinonFakeTimers } from 'sinon';
|
|
import { isPlainObject } from '@metamask/utils';
|
|
import { ControllerMessenger } from '@metamask/base-controller';
|
|
import {
|
|
BuiltInInfuraNetwork,
|
|
BUILT_IN_NETWORKS,
|
|
NETWORK_TYPES,
|
|
} from '../../../../shared/constants/network';
|
|
import { MetaMetricsNetworkEventSource } from '../../../../shared/constants/metametrics';
|
|
import {
|
|
NetworkController,
|
|
NetworkControllerEvent,
|
|
NetworkControllerEventType,
|
|
NetworkControllerOptions,
|
|
NetworkControllerState,
|
|
ProviderConfiguration,
|
|
ProviderType,
|
|
} from './network-controller';
|
|
|
|
jest.mock('uuid', () => {
|
|
return {
|
|
__esModule: true,
|
|
...jest.requireActual('uuid'),
|
|
v4: jest.fn(),
|
|
};
|
|
});
|
|
|
|
const uuidV4Mock = jest.mocked(v4);
|
|
|
|
/**
|
|
* A block header object that `eth_getBlockByNumber` can be mocked to return.
|
|
* Note that this type does not specify all of the properties present within the
|
|
* block header; within these tests, we are only interested in `number` and
|
|
* `baseFeePerGas`.
|
|
*/
|
|
type Block = {
|
|
number: string;
|
|
baseFeePerGas?: string;
|
|
};
|
|
|
|
/**
|
|
* A partial form of a prototypical JSON-RPC request body.
|
|
*/
|
|
type MockJsonRpcRequestBody = {
|
|
id?: number;
|
|
jsonrpc?: string;
|
|
method: string;
|
|
params?: unknown[];
|
|
};
|
|
|
|
/**
|
|
* A composite form of a prototypical JSON-RPC response body.
|
|
*/
|
|
type MockJsonRpcResponseBody = {
|
|
id?: number | string;
|
|
jsonrpc?: '2.0';
|
|
result?: unknown;
|
|
error?: string | null;
|
|
};
|
|
|
|
/**
|
|
* Arguments to `mockRpcCall` which specify the behavior of a mocked RPC
|
|
* request.
|
|
*/
|
|
type RpcCallMockSpec = {
|
|
request: MockJsonRpcRequestBody;
|
|
response: MockJsonRpcResponseBody & { httpStatus?: number };
|
|
error?: unknown;
|
|
delay?: number;
|
|
times?: number;
|
|
beforeCompleting?: () => void | Promise<void>;
|
|
};
|
|
|
|
/**
|
|
* A partial form of `RpcCallMockSpec`, which is preferred in
|
|
* `mockEssentialRpcCalls` for brevity.
|
|
*/
|
|
type PartialRpcCallMockSpec = {
|
|
request?: Partial<MockJsonRpcRequestBody>;
|
|
response?: Partial<MockJsonRpcResponseBody & { httpStatus?: number }>;
|
|
error?: unknown;
|
|
delay?: number;
|
|
times?: number;
|
|
beforeCompleting?: () => void | Promise<void>;
|
|
};
|
|
|
|
/**
|
|
* An RPC method that `mockEssentialRpcCalls` recognizes.
|
|
*/
|
|
enum KnownMockableRpcMethod {
|
|
EthBlockNumber = 'eth_blockNumber',
|
|
EthGetBlockByNumber = 'eth_getBlockByNumber',
|
|
NetVersion = 'net_version',
|
|
}
|
|
|
|
/**
|
|
* The callback that `withController` takes.
|
|
*/
|
|
type WithControllerCallback<NetworkCommunications, ReturnValue> = (args: {
|
|
controller: NetworkController;
|
|
network: NetworkCommunications;
|
|
}) => Promise<ReturnValue> | ReturnValue;
|
|
|
|
/**
|
|
* A variant of the options that the NetworkController constructor takes, where
|
|
* the provider state has been preconfigured with an Infura network. This is
|
|
* extracted so that we can give `withController` a better signature.
|
|
*/
|
|
type NetworkControllerOptionsWithInfuraProviderConfig =
|
|
Partial<NetworkControllerOptions> & {
|
|
state: Partial<NetworkControllerState> & {
|
|
provider: ProviderConfiguration & {
|
|
type: Exclude<ProviderType, typeof NETWORK_TYPES.RPC>;
|
|
};
|
|
};
|
|
};
|
|
|
|
/**
|
|
* A variant of the options that `withController` takes, where the provider
|
|
* state has been preconfigured with an Infura network. This is
|
|
* extracted so that we know which code path to take in `withController`
|
|
* depending on the given options.
|
|
*/
|
|
type WithControllerArgsWithConfiguredInfuraProvider<ReturnValue> = [
|
|
options: NetworkControllerOptionsWithInfuraProviderConfig,
|
|
callback: WithControllerCallback<InfuraNetworkCommunications, ReturnValue>,
|
|
];
|
|
|
|
/**
|
|
* The arguments that `withController` takes.
|
|
*/
|
|
type WithControllerArgs<ReturnValue> =
|
|
| WithControllerArgsWithConfiguredInfuraProvider<ReturnValue>
|
|
| [
|
|
options: Partial<NetworkControllerOptions>,
|
|
callback: WithControllerCallback<
|
|
CustomNetworkCommunications,
|
|
ReturnValue
|
|
>,
|
|
]
|
|
| [
|
|
callback: WithControllerCallback<
|
|
CustomNetworkCommunications,
|
|
ReturnValue
|
|
>,
|
|
];
|
|
|
|
/**
|
|
* The options that the InfuraNetworkCommunications constructor takes.
|
|
*/
|
|
type InfuraNetworkCommunicationsOptions = {
|
|
infuraNetwork: BuiltInInfuraNetwork;
|
|
infuraProjectId?: string;
|
|
};
|
|
|
|
/**
|
|
* The options that the CustomNetworkCommunications constructor takes.
|
|
*/
|
|
type CustomNetworkCommunicationsOptions = {
|
|
customRpcUrl: string;
|
|
};
|
|
|
|
/**
|
|
* As we use fake timers in these tests, we need a reference to the global
|
|
* `setTimeout` function so that we can still use it in test helpers.
|
|
*/
|
|
const originalSetTimeout = global.setTimeout;
|
|
|
|
/**
|
|
* A dummy block that matches the pre-EIP-1559 format (i.e. it doesn't have the
|
|
* `baseFeePerGas` property).
|
|
*/
|
|
const PRE_1559_BLOCK: Block = {
|
|
number: '0x42',
|
|
};
|
|
|
|
/**
|
|
* A dummy block that matches the pre-EIP-1559 format (i.e. it has the
|
|
* `baseFeePerGas` property).
|
|
*/
|
|
const POST_1559_BLOCK: Block = {
|
|
...PRE_1559_BLOCK,
|
|
baseFeePerGas: '0x63c498a46',
|
|
};
|
|
|
|
/**
|
|
* An alias for `POST_1559_BLOCK`, for tests that don't care about which kind of
|
|
* block they're looking for.
|
|
*/
|
|
const BLOCK: Block = POST_1559_BLOCK;
|
|
|
|
/**
|
|
* A dummy value for the `projectId` option that `createInfuraClient` needs.
|
|
* (Infura should not be hit during tests, but just in case, this should not
|
|
* refer to a real project ID.)
|
|
*/
|
|
const DEFAULT_INFURA_PROJECT_ID = 'fake-infura-project-id';
|
|
|
|
/**
|
|
* The set of networks that, when specified, create an Infura provider as
|
|
* opposed to a "standard" provider (one suited for a custom RPC endpoint).
|
|
*/
|
|
const INFURA_NETWORKS = [
|
|
{
|
|
networkType: NETWORK_TYPES.MAINNET,
|
|
chainId: '0x1',
|
|
networkId: '1',
|
|
ticker: 'ETH',
|
|
},
|
|
{
|
|
networkType: NETWORK_TYPES.GOERLI,
|
|
chainId: '0x5',
|
|
networkId: '5',
|
|
ticker: 'GoerliETH',
|
|
},
|
|
{
|
|
networkType: NETWORK_TYPES.SEPOLIA,
|
|
chainId: '0xaa36a7',
|
|
networkId: '11155111',
|
|
ticker: 'SepoliaETH',
|
|
},
|
|
];
|
|
|
|
/**
|
|
* A response object for a successful request to `eth_getBlockByNumber`. It is
|
|
* assumed that the block number here is insignificant to the test.
|
|
*/
|
|
const SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE = {
|
|
result: BLOCK,
|
|
error: null,
|
|
};
|
|
|
|
/**
|
|
* A response object for a request that has been geoblocked by Infura.
|
|
*/
|
|
const BLOCKED_INFURA_RESPONSE = {
|
|
result: null,
|
|
error: 'countryBlocked',
|
|
httpStatus: 500,
|
|
};
|
|
|
|
/**
|
|
* 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',
|
|
error: null,
|
|
};
|
|
|
|
/**
|
|
* A response object for a unsuccessful request to any RPC method. It is assumed
|
|
* that the error here is insignificant to the test.
|
|
*/
|
|
const UNSUCCESSFUL_JSON_RPC_RESPONSE = {
|
|
result: null,
|
|
error: 'oops',
|
|
httpStatus: 500,
|
|
};
|
|
|
|
/**
|
|
* Handles mocking requests made by NetworkController for a particular network.
|
|
*/
|
|
abstract class NetworkCommunications<Options> {
|
|
/**
|
|
* Holds the options used to construct the instance. Employed by `with`.
|
|
*/
|
|
protected options: Options;
|
|
|
|
/**
|
|
* The path used for all requests. Customized per network type.
|
|
*/
|
|
#requestPath: string;
|
|
|
|
/**
|
|
* The Nock scope object that holds the mocked requests.
|
|
*/
|
|
nockScope: NockScope;
|
|
|
|
/**
|
|
* Constructs a NetworkCommunications. Don't use this directly; instead
|
|
* instantiate either {@link InfuraNetworkCommunications} or {@link
|
|
* CustomNetworkCommunications}.
|
|
*
|
|
* @param args - The arguments.
|
|
* @param args.options - Options to customize request mocks.
|
|
* @param args.requestBaseUrl - The base URL to use for all requests.
|
|
* @param args.requestPath - The path to use for all requests.
|
|
*/
|
|
constructor({
|
|
options,
|
|
requestBaseUrl,
|
|
requestPath,
|
|
}: {
|
|
options: Options;
|
|
requestBaseUrl: string;
|
|
requestPath: string;
|
|
}) {
|
|
this.options = options;
|
|
this.nockScope = nock(requestBaseUrl);
|
|
this.#requestPath = requestPath;
|
|
}
|
|
|
|
/**
|
|
* Mocks the RPC calls that NetworkController makes internally.
|
|
*
|
|
* @param args - The arguments.
|
|
* @param args.latestBlock - The block object that will be used to mock
|
|
* `eth_blockNumber` and `eth_getBlockByNumber`. If null, then both
|
|
* `eth_blockNumber` and `eth_getBlockByNumber` will respond with null.
|
|
* @param args.eth_blockNumber - Options for mocking the `eth_blockNumber` RPC
|
|
* method (see `mockRpcCall` for valid properties). By default, the number
|
|
* from the `latestBlock` will be used as the result. Use `null` to prevent
|
|
* this method from being mocked.
|
|
* @param args.eth_getBlockByNumber - Options for mocking the
|
|
* `eth_getBlockByNumber` RPC method (see `mockRpcCall` for valid properties).
|
|
* By default, the `latestBlock` will be used as the result. Use `null` to
|
|
* prevent this method from being mocked.
|
|
* @param args.net_version - Options for mocking the `net_version` RPC method
|
|
* (see `mockRpcCall` for valid properties). By default, "1" will be used as
|
|
* the result. Use `null` to prevent this method from being mocked.
|
|
*/
|
|
mockEssentialRpcCalls({
|
|
latestBlock = BLOCK,
|
|
eth_blockNumber: ethBlockNumberMocks = [],
|
|
eth_getBlockByNumber: ethGetBlockByNumberMocks = [],
|
|
net_version: netVersionMocks = [],
|
|
}: {
|
|
latestBlock?: Block | null;
|
|
eth_blockNumber?: PartialRpcCallMockSpec | PartialRpcCallMockSpec[];
|
|
eth_getBlockByNumber?: PartialRpcCallMockSpec | PartialRpcCallMockSpec[];
|
|
net_version?: PartialRpcCallMockSpec | PartialRpcCallMockSpec[];
|
|
} = {}) {
|
|
const latestBlockNumber = latestBlock === null ? null : latestBlock.number;
|
|
const defaultMocksByRpcMethod: Record<
|
|
KnownMockableRpcMethod,
|
|
RpcCallMockSpec
|
|
> = {
|
|
eth_getBlockByNumber: {
|
|
request: {
|
|
method: 'eth_getBlockByNumber',
|
|
params: [latestBlockNumber, false],
|
|
},
|
|
response: {
|
|
result: latestBlock,
|
|
},
|
|
},
|
|
net_version: {
|
|
request: {
|
|
method: 'net_version',
|
|
params: [],
|
|
},
|
|
response: {
|
|
result: '1',
|
|
},
|
|
},
|
|
// The request that the block tracker makes always occurs after any
|
|
// request that the network controller makes (because such a request goes
|
|
// through the block cache middleware and that is what spawns the block
|
|
// tracker).
|
|
eth_blockNumber: {
|
|
request: {
|
|
method: 'eth_blockNumber',
|
|
params: [],
|
|
},
|
|
response: {
|
|
result: latestBlockNumber,
|
|
},
|
|
// If there is no latest block number then the request that spawned the
|
|
// block tracker won't be cached inside of the block tracker, so the
|
|
// block tracker makes another request when it is asked for the latest
|
|
// block.
|
|
times: latestBlockNumber === null ? 2 : 1,
|
|
},
|
|
};
|
|
const providedMocksByRpcMethod = {
|
|
eth_getBlockByNumber: ethGetBlockByNumberMocks,
|
|
net_version: netVersionMocks,
|
|
eth_blockNumber: ethBlockNumberMocks,
|
|
};
|
|
|
|
const allMocks: RpcCallMockSpec[] = [];
|
|
for (const rpcMethod of knownOwnKeysOf(defaultMocksByRpcMethod)) {
|
|
const defaultMock = defaultMocksByRpcMethod[rpcMethod];
|
|
const providedMockOrMocks = providedMocksByRpcMethod[rpcMethod];
|
|
const providedMocks = Array.isArray(providedMockOrMocks)
|
|
? providedMockOrMocks
|
|
: [providedMockOrMocks];
|
|
if (providedMocks.length > 0) {
|
|
for (const providedMock of providedMocks) {
|
|
// Using the spread operator seems to confuse TypeScript because
|
|
// it doesn't know that `request` and `response` will be non-optional
|
|
// in the end, even though it is non-optional only in RpcCallMockSpec
|
|
// and not PartialRpcCallMockSpec. However, `Object.assign` assigns
|
|
// the correct type.
|
|
/* eslint-disable-next-line prefer-object-spread */
|
|
const completeMock = Object.assign({}, defaultMock, providedMock);
|
|
allMocks.push(completeMock);
|
|
}
|
|
} else {
|
|
allMocks.push(defaultMock);
|
|
}
|
|
}
|
|
|
|
allMocks.forEach((mock) => {
|
|
this.mockRpcCall(mock);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Uses Nock to mock a JSON-RPC request with the given response.
|
|
*
|
|
* @param args - The arguments.
|
|
* @param args.request - The request data. Must
|
|
* include a `method`. Note that EthQuery's `sendAsync` method implicitly uses
|
|
* an empty array for `params` if it is not provided in the original request,
|
|
* so make sure to include this.
|
|
* @param args.response - Information concerning the response that the request
|
|
* should have. Takes one of two forms. The simplest form is an object that
|
|
* represents the response body; the second form allows you to specify the
|
|
* HTTP status, as well as a potentially async function to generate the
|
|
* response body.
|
|
* @param args.error - An error to throw while making the request. Takes
|
|
* precedence over `response`.
|
|
* @param args.delay - The amount of time that should pass before the request
|
|
* resolves with the response.
|
|
* @param args.times - The number of times that the request is expected to be
|
|
* made.
|
|
* @param args.beforeCompleting - Sometimes it is useful to do something after
|
|
* the request is kicked off but before it ends (or, in terms of a `fetch`
|
|
* promise, when the promise is initiated but before it is resolved). You can
|
|
* pass an (async) function for this option to do this.
|
|
* @returns The nock scope object that represents all of the mocks for the
|
|
* network, or null if `times` is 0.
|
|
*/
|
|
mockRpcCall({
|
|
request,
|
|
response,
|
|
error,
|
|
delay,
|
|
times,
|
|
beforeCompleting,
|
|
}: RpcCallMockSpec): nock.Scope | null {
|
|
if (times === 0) {
|
|
return null;
|
|
}
|
|
|
|
const httpStatus = response?.httpStatus ?? 200;
|
|
const partialResponseBody = omit(response, 'httpStatus');
|
|
|
|
let nockInterceptor = this.nockScope.post(
|
|
this.#requestPath,
|
|
(actualBody) => {
|
|
const expectedPartialBody = { jsonrpc: '2.0', ...request };
|
|
return isMatch(actualBody, expectedPartialBody);
|
|
},
|
|
);
|
|
|
|
if (delay !== undefined) {
|
|
nockInterceptor = nockInterceptor.delay(delay);
|
|
}
|
|
|
|
if (times !== undefined) {
|
|
nockInterceptor = nockInterceptor.times(times);
|
|
}
|
|
|
|
if (
|
|
error !== undefined &&
|
|
(typeof error === 'string' || isPlainObject(error))
|
|
) {
|
|
return nockInterceptor.replyWithError(error);
|
|
}
|
|
if (response !== undefined) {
|
|
return nockInterceptor.reply(async (_uri, requestBody) => {
|
|
if (beforeCompleting !== undefined) {
|
|
await beforeCompleting();
|
|
}
|
|
|
|
const completeResponseBody = {
|
|
id:
|
|
isPlainObject(requestBody) && 'id' in requestBody
|
|
? requestBody.id
|
|
: undefined,
|
|
jsonrpc: '2.0',
|
|
...partialResponseBody,
|
|
};
|
|
|
|
return [httpStatus, completeResponseBody];
|
|
});
|
|
}
|
|
throw new Error(
|
|
'Neither `response` nor `error` was given. Please specify one of them.',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* The number of times to mock `eth_blockNumber` by default. Customized
|
|
* for Infura.
|
|
*/
|
|
protected getDefaultNumTimesToMockEthBlockNumber(): number {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles mocking requests made by NetworkController for an Infura network.
|
|
*/
|
|
class InfuraNetworkCommunications extends NetworkCommunications<InfuraNetworkCommunicationsOptions> {
|
|
/**
|
|
* Constructs an InfuraNetworkCommunications.
|
|
*
|
|
* @param args - The arguments.
|
|
* @param args.infuraProjectId - TODO.
|
|
* @param args.infuraNetwork - TODO.
|
|
*/
|
|
constructor({
|
|
infuraProjectId = DEFAULT_INFURA_PROJECT_ID,
|
|
infuraNetwork,
|
|
}: InfuraNetworkCommunicationsOptions) {
|
|
super({
|
|
options: { infuraProjectId, infuraNetwork },
|
|
requestBaseUrl: `https://${infuraNetwork}.infura.io`,
|
|
requestPath: `/v3/${infuraProjectId}`,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Constructs a new InfuraNetworkCommunications object using a different set
|
|
* of options, using the options from this instance as a base.
|
|
*
|
|
* @param overrides - Options with which you want to extend the new
|
|
* InfuraNetworkCommunications.
|
|
*/
|
|
with(
|
|
overrides: Partial<InfuraNetworkCommunicationsOptions> = {},
|
|
): InfuraNetworkCommunications {
|
|
return new InfuraNetworkCommunications({
|
|
...this.options,
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
protected getDefaultNumTimesToMockEthBlockNumber(): number {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles mocking requests made by NetworkController for a non-Infura network.
|
|
*/
|
|
class CustomNetworkCommunications extends NetworkCommunications<CustomNetworkCommunicationsOptions> {
|
|
/**
|
|
* Constructs a CustomNetworkCommunications.
|
|
*
|
|
* @param args - The arguments.
|
|
* @param args.customRpcUrl - The URL that points to the RPC endpoint.
|
|
*/
|
|
constructor({ customRpcUrl }: CustomNetworkCommunicationsOptions) {
|
|
super({
|
|
options: { customRpcUrl },
|
|
requestBaseUrl: customRpcUrl,
|
|
requestPath: '/',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Constructs a new CustomNetworkCommunications object using a different set
|
|
* of options, using the options from this instance as a base.
|
|
*
|
|
* @param overrides - Options with which you want to extend the new
|
|
* CustomNetworkCommunications.
|
|
*/
|
|
with(
|
|
overrides: Partial<CustomNetworkCommunicationsOptions> = {},
|
|
): CustomNetworkCommunications {
|
|
return new CustomNetworkCommunications({
|
|
...this.options,
|
|
...overrides,
|
|
});
|
|
}
|
|
}
|
|
|
|
describe('NetworkController', () => {
|
|
let clock: SinonFakeTimers;
|
|
|
|
beforeEach(() => {
|
|
// Disable all requests, even those to localhost
|
|
nock.disableNetConnect();
|
|
// Faking timers ends up doing two things:
|
|
// 1. Halting the block tracker (which depends on `setTimeout` to
|
|
// periodically request the latest block) set up in
|
|
// `eth-json-rpc-middleware`
|
|
// 2. Halting the retry logic in `@metamask/eth-json-rpc-infura` (which
|
|
// also depends on `setTimeout`)
|
|
clock = sinon.useFakeTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
nock.enableNetConnect('localhost');
|
|
clock.restore();
|
|
nock.cleanAll();
|
|
});
|
|
|
|
describe('constructor', () => {
|
|
const invalidInfuraProjectIds = [undefined, null, {}, 1];
|
|
invalidInfuraProjectIds.forEach((invalidProjectId) => {
|
|
it(`throws if an invalid Infura ID of "${inspect(
|
|
invalidProjectId,
|
|
)}" is provided`, () => {
|
|
expect(
|
|
// @ts-expect-error We are intentionally passing bad input.
|
|
() => new NetworkController({ infuraProjectId: invalidProjectId }),
|
|
).toThrow('Invalid Infura project ID');
|
|
});
|
|
});
|
|
|
|
it('accepts initial state', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'http://example-custom-rpc.metamask.io',
|
|
chainId: '0x9999' as const,
|
|
nickname: 'Test initial state',
|
|
},
|
|
networkDetails: {
|
|
EIPS: {
|
|
1559: false,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
({ controller }) => {
|
|
expect(controller.store.getState()).toMatchInlineSnapshot(`
|
|
{
|
|
"networkConfigurations": {},
|
|
"networkDetails": {
|
|
"EIPS": {
|
|
"1559": false,
|
|
},
|
|
},
|
|
"networkId": null,
|
|
"networkStatus": "unknown",
|
|
"provider": {
|
|
"chainId": "0x9999",
|
|
"nickname": "Test initial state",
|
|
"rpcUrl": "http://example-custom-rpc.metamask.io",
|
|
"type": "rpc",
|
|
},
|
|
}
|
|
`);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('sets default state without initial state', async () => {
|
|
await withController(({ controller }) => {
|
|
expect(controller.store.getState()).toMatchInlineSnapshot(`
|
|
{
|
|
"networkConfigurations": {},
|
|
"networkDetails": {
|
|
"EIPS": {
|
|
"1559": undefined,
|
|
},
|
|
},
|
|
"networkId": null,
|
|
"networkStatus": "unknown",
|
|
"provider": {
|
|
"chainId": "0x539",
|
|
"nickname": "Localhost 8545",
|
|
"rpcUrl": "http://localhost:8545",
|
|
"ticker": "ETH",
|
|
"type": "rpc",
|
|
},
|
|
}
|
|
`);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('destroy', () => {
|
|
it('does not throw if called before the provider is initialized', async () => {
|
|
const controller = new NetworkController(
|
|
buildDefaultNetworkControllerOptions(),
|
|
);
|
|
|
|
expect(await controller.destroy()).toBeUndefined();
|
|
});
|
|
|
|
it('stops the block tracker for the currently selected network as long as the provider has been initialized', async () => {
|
|
await withController(async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
eth_blockNumber: {
|
|
times: 2,
|
|
},
|
|
});
|
|
await controller.initializeProvider();
|
|
const { blockTracker } = controller.getProviderAndBlockTracker();
|
|
assert(blockTracker, 'Block tracker is somehow unset');
|
|
// The block tracker starts running after a listener is attached
|
|
blockTracker.addListener('latest', () => {
|
|
// do nothing
|
|
});
|
|
expect(blockTracker.isRunning()).toBeTruthy();
|
|
|
|
await controller.destroy();
|
|
|
|
expect(blockTracker.isRunning()).toBe(false);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('initializeProvider', () => {
|
|
it('throws if the provider configuration is invalid', async () => {
|
|
const invalidProviderConfig = {};
|
|
await withController(
|
|
{
|
|
state: {
|
|
/* @ts-expect-error We're intentionally passing bad input. */
|
|
provider: invalidProviderConfig,
|
|
},
|
|
},
|
|
async ({ controller }) => {
|
|
await expect(async () => {
|
|
await controller.initializeProvider();
|
|
}).rejects.toThrow(
|
|
'NetworkController - _configureProvider - unknown type "undefined"',
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
for (const { networkType, chainId } of INFURA_NETWORKS) {
|
|
describe(`when the type in the provider configuration is "${networkType}"`, () => {
|
|
it(`initializes a provider pointed to the "${networkType}" Infura network (chainId: ${chainId})`, async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID
|
|
// of the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls();
|
|
|
|
await controller.initializeProvider();
|
|
|
|
const { provider } = controller.getProviderAndBlockTracker();
|
|
assert(provider, 'Provider is somehow unset');
|
|
const promisifiedSendAsync = promisify(provider.sendAsync).bind(
|
|
provider,
|
|
);
|
|
const { result: chainIdResult } = await promisifiedSendAsync({
|
|
id: '1',
|
|
jsonrpc: '2.0',
|
|
method: 'eth_chainId',
|
|
});
|
|
expect(chainIdResult).toBe(chainId);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('emits infuraIsBlocked or infuraIsUnblocked, depending on whether Infura is blocking requests', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID
|
|
// of the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls();
|
|
|
|
const infuraIsUnblocked = await waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsUnblocked,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
expect(infuraIsUnblocked).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('determines the status of the network, storing it in state', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID
|
|
// of the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls();
|
|
expect(controller.store.getState().networkStatus).toBe('unknown');
|
|
|
|
await controller.initializeProvider();
|
|
|
|
expect(controller.store.getState().networkStatus).toBe(
|
|
'available',
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('determines whether the network supports EIP-1559 and stores the result in state without overwriting other state in the networkDetails store', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID
|
|
// of the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
networkDetails: {
|
|
EIPS: {},
|
|
other: 'details',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
latestBlock: POST_1559_BLOCK,
|
|
});
|
|
|
|
await controller.initializeProvider();
|
|
|
|
expect(controller.store.getState().networkDetails).toStrictEqual({
|
|
EIPS: {
|
|
1559: true,
|
|
},
|
|
other: 'details',
|
|
});
|
|
},
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
describe(`when the type in the provider configuration is "rpc"`, () => {
|
|
it('initializes a provider pointed to the given RPC URL whose chain ID matches the configured chain ID', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls();
|
|
network.mockRpcCall({
|
|
request: {
|
|
method: 'test',
|
|
params: [],
|
|
},
|
|
response: {
|
|
result: 'test response',
|
|
},
|
|
});
|
|
|
|
await controller.initializeProvider();
|
|
|
|
const { provider } = controller.getProviderAndBlockTracker();
|
|
assert(provider, 'Provider is somehow unset');
|
|
const promisifiedSendAsync = promisify(provider.sendAsync).bind(
|
|
provider,
|
|
);
|
|
const { result: testResult } = await promisifiedSendAsync({
|
|
id: '1',
|
|
jsonrpc: '2.0',
|
|
method: 'test',
|
|
params: [],
|
|
});
|
|
expect(testResult).toBe('test response');
|
|
const { result: chainIdResult } = await promisifiedSendAsync({
|
|
id: '2',
|
|
jsonrpc: '2.0',
|
|
method: 'eth_chainId',
|
|
});
|
|
expect(chainIdResult).toBe('0xtest');
|
|
},
|
|
);
|
|
});
|
|
|
|
it('emits infuraIsUnblocked', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls();
|
|
|
|
const infuraIsUnblocked = await waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsUnblocked,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
expect(infuraIsUnblocked).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('does not emit infuraIsBlocked', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls();
|
|
|
|
const promiseForNoInfuraIsBlockedEvents = waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsBlocked,
|
|
count: 0,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
expect(await promiseForNoInfuraIsBlockedEvents).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('determines the status of the network, storing it in state', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId1',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
net_version: {
|
|
response: SUCCESSFUL_NET_VERSION_RESPONSE,
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe('unknown');
|
|
|
|
await controller.initializeProvider();
|
|
|
|
expect(controller.store.getState().networkStatus).toBe('available');
|
|
},
|
|
);
|
|
});
|
|
|
|
it('determines whether the network supports EIP-1559, storing it in state', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
},
|
|
networkDetails: {
|
|
EIPS: {},
|
|
other: 'details',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
latestBlock: POST_1559_BLOCK,
|
|
});
|
|
|
|
await controller.initializeProvider();
|
|
|
|
expect(controller.store.getState().networkDetails).toStrictEqual({
|
|
EIPS: {
|
|
1559: true,
|
|
},
|
|
other: 'details',
|
|
});
|
|
},
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getProviderAndBlockTracker', () => {
|
|
it('returns objects that proxy to the provider and block tracker as long as the provider has been initialized', async () => {
|
|
await withController(async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls();
|
|
await controller.initializeProvider();
|
|
|
|
const { provider, blockTracker } =
|
|
controller.getProviderAndBlockTracker();
|
|
|
|
expect(provider).toHaveProperty('sendAsync');
|
|
expect(blockTracker).toHaveProperty('checkForLatestBlock');
|
|
});
|
|
});
|
|
|
|
it("returns null for both the provider and block tracker if the provider hasn't been initialized yet", async () => {
|
|
await withController(async ({ controller }) => {
|
|
const { provider, blockTracker } =
|
|
controller.getProviderAndBlockTracker();
|
|
|
|
expect(provider).toBeNull();
|
|
expect(blockTracker).toBeNull();
|
|
});
|
|
});
|
|
|
|
for (const { networkType, chainId } of INFURA_NETWORKS) {
|
|
describe(`when the type in the provider configuration is changed to "${networkType}"`, () => {
|
|
it(`returns a provider object that was pointed to another network before the switch and is pointed to "${networkType}" afterward`, async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: network1 }) => {
|
|
network1.mockEssentialRpcCalls();
|
|
const network2 = new InfuraNetworkCommunications({
|
|
infuraNetwork: networkType,
|
|
});
|
|
network2.mockEssentialRpcCalls();
|
|
await controller.initializeProvider();
|
|
const { provider } = controller.getProviderAndBlockTracker();
|
|
assert(provider, 'Provider is somehow unset');
|
|
|
|
const promisifiedSendAsync1 = promisify(provider.sendAsync).bind(
|
|
provider,
|
|
);
|
|
const { result: oldChainIdResult } = await promisifiedSendAsync1({
|
|
id: '1',
|
|
jsonrpc: '2.0',
|
|
method: 'eth_chainId',
|
|
});
|
|
expect(oldChainIdResult).toBe('0x1337');
|
|
|
|
controller.setProviderType(networkType);
|
|
const promisifiedSendAsync2 = promisify(provider.sendAsync).bind(
|
|
provider,
|
|
);
|
|
const { result: newChainIdResult } = await promisifiedSendAsync2({
|
|
id: '2',
|
|
jsonrpc: '2.0',
|
|
method: 'eth_chainId',
|
|
});
|
|
expect(newChainIdResult).toBe(chainId);
|
|
},
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
describe('when the type in the provider configuration is changed to "rpc"', () => {
|
|
it('returns a provider object that was pointed to another network before the switch and is pointed to the new network', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'goerli',
|
|
// NOTE: This doesn't need to match the logical chain ID
|
|
// of the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
ticker: 'ABC',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: network1 }) => {
|
|
network1.mockEssentialRpcCalls();
|
|
const network2 = new CustomNetworkCommunications({
|
|
customRpcUrl: 'https://mock-rpc-url',
|
|
});
|
|
network2.mockEssentialRpcCalls();
|
|
await controller.initializeProvider();
|
|
const { provider } = controller.getProviderAndBlockTracker();
|
|
assert(provider, 'Provider is somehow unset');
|
|
|
|
const promisifiedSendAsync1 = promisify(provider.sendAsync).bind(
|
|
provider,
|
|
);
|
|
const { result: oldChainIdResult } = await promisifiedSendAsync1({
|
|
id: '1',
|
|
jsonrpc: '2.0',
|
|
method: 'eth_chainId',
|
|
});
|
|
expect(oldChainIdResult).toBe('0x5');
|
|
|
|
controller.setActiveNetwork('testNetworkConfigurationId');
|
|
const promisifiedSendAsync2 = promisify(provider.sendAsync).bind(
|
|
provider,
|
|
);
|
|
const { result: newChainIdResult } = await promisifiedSendAsync2({
|
|
id: '2',
|
|
jsonrpc: '2.0',
|
|
method: 'eth_chainId',
|
|
});
|
|
expect(newChainIdResult).toBe('0x1337');
|
|
},
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getEIP1559Compatibility', () => {
|
|
describe('when the latest block has a baseFeePerGas property', () => {
|
|
it('stores the fact that the network supports EIP-1559', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
networkDetails: {
|
|
EIPS: {},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
latestBlock: POST_1559_BLOCK,
|
|
});
|
|
await controller.initializeProvider();
|
|
|
|
await controller.getEIP1559Compatibility();
|
|
|
|
expect(controller.store.getState().networkDetails.EIPS[1559]).toBe(
|
|
true,
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('returns true', async () => {
|
|
await withController(async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
latestBlock: POST_1559_BLOCK,
|
|
});
|
|
await controller.initializeProvider();
|
|
|
|
const supportsEIP1559 = await controller.getEIP1559Compatibility();
|
|
|
|
expect(supportsEIP1559).toBeTruthy();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('when the latest block does not have a baseFeePerGas property', () => {
|
|
it('stores the fact that the network does not support EIP-1559', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
networkDetails: {
|
|
EIPS: {},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
latestBlock: PRE_1559_BLOCK,
|
|
});
|
|
await controller.initializeProvider();
|
|
|
|
await controller.getEIP1559Compatibility();
|
|
|
|
expect(controller.store.getState().networkDetails.EIPS[1559]).toBe(
|
|
false,
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('returns false', async () => {
|
|
await withController(async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
latestBlock: PRE_1559_BLOCK,
|
|
});
|
|
await controller.initializeProvider();
|
|
|
|
const supportsEIP1559 = await controller.getEIP1559Compatibility();
|
|
|
|
expect(supportsEIP1559).toBe(false);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('when the request for the latest block responds with null', () => {
|
|
it('persists false to state as whether the network supports EIP-1559', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
networkDetails: {
|
|
EIPS: {},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
latestBlock: null,
|
|
});
|
|
await controller.initializeProvider();
|
|
|
|
await controller.getEIP1559Compatibility();
|
|
|
|
expect(controller.store.getState().networkDetails.EIPS[1559]).toBe(
|
|
false,
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('returns false', async () => {
|
|
await withController(async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
latestBlock: null,
|
|
});
|
|
await controller.initializeProvider();
|
|
|
|
const supportsEIP1559 = await controller.getEIP1559Compatibility();
|
|
|
|
expect(supportsEIP1559).toBe(false);
|
|
});
|
|
});
|
|
});
|
|
|
|
it('does not make multiple requests to eth_getBlockByNumber when called multiple times and the request to eth_getBlockByNumber succeeded the first time', async () => {
|
|
await withController(async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
eth_getBlockByNumber: {
|
|
times: 1,
|
|
},
|
|
});
|
|
await withoutCallingGetEIP1559Compatibility({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
await controller.getEIP1559Compatibility();
|
|
await controller.getEIP1559Compatibility();
|
|
|
|
expect(network.nockScope.isDone()).toBeTruthy();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('lookupNetwork', () => {
|
|
describe('if the provider has not been initialized', () => {
|
|
it('does not update state in any way', async () => {
|
|
const providerConfig = {
|
|
type: NETWORK_TYPES.RPC,
|
|
rpcUrl: 'http://example-custom-rpc.metamask.io',
|
|
chainId: '0x9999' as const,
|
|
nickname: 'Test initial state',
|
|
};
|
|
const initialState = {
|
|
provider: providerConfig,
|
|
networkDetails: {
|
|
EIPS: {
|
|
1559: true,
|
|
},
|
|
},
|
|
};
|
|
|
|
await withController(
|
|
{
|
|
state: initialState,
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls();
|
|
const stateAfterConstruction = controller.store.getState();
|
|
|
|
await controller.lookupNetwork();
|
|
|
|
expect(controller.store.getState()).toStrictEqual(
|
|
stateAfterConstruction,
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('does not emit infuraIsUnblocked', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{ messenger: restrictedMessenger },
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls();
|
|
|
|
const promiseForNoInfuraIsUnblockedEvents = waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsUnblocked,
|
|
count: 0,
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('does not emit infuraIsBlocked', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{ messenger: restrictedMessenger },
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls();
|
|
|
|
const promiseForNoInfuraIsBlockedEvents = waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsBlocked,
|
|
count: 0,
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(await promiseForNoInfuraIsBlockedEvents).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('if the provider has initialized, but the current network has no chainId', () => {
|
|
it('does not update state in any way', async () => {
|
|
/* @ts-expect-error We are intentionally not including a chainId in the provider config. */
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'http://example-custom-rpc.metamask.io',
|
|
},
|
|
networkDetails: {
|
|
EIPS: {
|
|
1559: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls();
|
|
await controller.initializeProvider();
|
|
const stateAfterInitialization = controller.store.getState();
|
|
|
|
await controller.lookupNetwork();
|
|
|
|
expect(controller.store.getState()).toStrictEqual(
|
|
stateAfterInitialization,
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('does not emit infuraIsUnblocked', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
/* @ts-expect-error We are intentionally not including a chainId in the provider config. */
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'http://example-custom-rpc.metamask.io',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls();
|
|
await controller.initializeProvider();
|
|
|
|
const promiseForNoInfuraIsUnblockedEvents = waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsUnblocked,
|
|
count: 0,
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('does not emit infuraIsBlocked', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
/* @ts-expect-error We are intentionally not including a chainId in the provider config. */
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'http://example-custom-rpc.metamask.io',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls();
|
|
await controller.initializeProvider();
|
|
|
|
const promiseForNoInfuraIsBlockedEvents = waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsBlocked,
|
|
count: 0,
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(await promiseForNoInfuraIsBlockedEvents).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
INFURA_NETWORKS.forEach(({ networkType, networkId }) => {
|
|
describe(`when the type in the provider configuration is "${networkType}"`, () => {
|
|
describe('if the request for eth_getBlockByNumber responds successfully', () => {
|
|
it('stores the fact that the network is available', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
// This results in a successful call to eth_getBlockByNumber
|
|
// implicitly
|
|
latestBlock: BLOCK,
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe(
|
|
'unknown',
|
|
);
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(controller.store.getState().networkStatus).toBe(
|
|
'available',
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('stores the ID of the network', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
// This results in a successful call to eth_getBlockByNumber
|
|
// implicitly
|
|
latestBlock: BLOCK,
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkId).toBeNull();
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkId'],
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(controller.store.getState().networkId).toBe(networkId);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('stores the fact that the network supports EIP-1559 when baseFeePerGas is in the block header', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
networkDetails: {
|
|
EIPS: {},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
// This results in a successful call to eth_getBlockByNumber
|
|
// implicitly
|
|
latestBlock: POST_1559_BLOCK,
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkDetails'],
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(
|
|
controller.store.getState().networkDetails.EIPS[1559],
|
|
).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('stores the fact that the network does not support EIP-1559 when baseFeePerGas is not in the block header', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
networkDetails: {
|
|
EIPS: {},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
// This results in a successful call to eth_getBlockByNumber
|
|
// implicitly
|
|
latestBlock: PRE_1559_BLOCK,
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkDetails'],
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(
|
|
controller.store.getState().networkDetails.EIPS[1559],
|
|
).toBe(false);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('emits infuraIsUnblocked', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID
|
|
// of the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
// This results in a successful call to eth_getBlockByNumber
|
|
// implicitly
|
|
latestBlock: POST_1559_BLOCK,
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
const infuraIsUnblocked = await waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsUnblocked,
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(infuraIsUnblocked).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('if the request for eth_blockNumber responds with a "countryBlocked" error', () => {
|
|
it('stores the fact that the network is blocked', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID
|
|
// of the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
eth_getBlockByNumber: {
|
|
response: BLOCKED_INFURA_RESPONSE,
|
|
},
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(controller.store.getState().networkStatus).toBe(
|
|
'blocked',
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('clears the ID of the network from state', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
// Ensure that each call to eth_blockNumber returns a
|
|
// different block number, otherwise the first
|
|
// eth_getBlockByNumber response will get cached under the
|
|
// first block number
|
|
eth_blockNumber: [
|
|
{
|
|
response: {
|
|
result: '0x1',
|
|
},
|
|
},
|
|
{
|
|
response: {
|
|
result: '0x2',
|
|
},
|
|
},
|
|
],
|
|
eth_getBlockByNumber: [
|
|
{
|
|
request: {
|
|
params: ['0x1', false],
|
|
},
|
|
response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE,
|
|
},
|
|
{
|
|
request: {
|
|
params: ['0x2', false],
|
|
},
|
|
response: BLOCKED_INFURA_RESPONSE,
|
|
},
|
|
],
|
|
});
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkId'],
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkId).toBe(networkId);
|
|
|
|
// Force the block tracker to request a new block to clear the
|
|
// block cache
|
|
clock.runAll();
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkId'],
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkId).toBeNull();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('clears whether the network supports EIP-1559 from state', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
networkDetails: {
|
|
EIPS: {
|
|
1559: true,
|
|
},
|
|
other: 'details',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
eth_getBlockByNumber: {
|
|
response: BLOCKED_INFURA_RESPONSE,
|
|
},
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkDetails'],
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
expect(
|
|
controller.store.getState().networkDetails,
|
|
).toStrictEqual({
|
|
EIPS: {
|
|
1559: undefined,
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('emits infuraIsBlocked', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID
|
|
// of the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
eth_getBlockByNumber: {
|
|
response: BLOCKED_INFURA_RESPONSE,
|
|
},
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
const infuraIsBlocked = await waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsBlocked,
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(infuraIsBlocked).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('does not emit infuraIsUnblocked', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID
|
|
// of the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
eth_getBlockByNumber: {
|
|
response: BLOCKED_INFURA_RESPONSE,
|
|
},
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
const promiseForNoInfuraIsUnblockedEvents =
|
|
waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsUnblocked,
|
|
count: 0,
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('if the request for eth_getBlockByNumber responds with a generic error', () => {
|
|
it('stores the network status as unavailable if the error does not translate to an internal RPC error', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
net_version: {
|
|
times: 2,
|
|
},
|
|
// Ensure that each call to eth_blockNumber returns a different
|
|
// block number, otherwise the first eth_getBlockByNumber
|
|
// response will get cached under the first block number
|
|
eth_blockNumber: [
|
|
{
|
|
response: {
|
|
result: '0x1',
|
|
},
|
|
},
|
|
{
|
|
response: {
|
|
result: '0x2',
|
|
},
|
|
},
|
|
],
|
|
eth_getBlockByNumber: [
|
|
{
|
|
request: {
|
|
params: ['0x1', false],
|
|
},
|
|
response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE,
|
|
},
|
|
{
|
|
request: {
|
|
params: ['0x2', false],
|
|
},
|
|
response: {
|
|
error: 'some error',
|
|
httpStatus: 405,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe(
|
|
'available',
|
|
);
|
|
|
|
// Force the block tracker to request a new block to clear the
|
|
// block cache
|
|
clock.runAll();
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe(
|
|
'unavailable',
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('stores the network status as unknown if the error translates to an internal RPC error', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
net_version: {
|
|
times: 2,
|
|
},
|
|
// Ensure that each call to eth_blockNumber returns a different
|
|
// block number, otherwise the first eth_getBlockByNumber
|
|
// response will get cached under the first block number
|
|
eth_blockNumber: [
|
|
{
|
|
response: {
|
|
result: '0x1',
|
|
},
|
|
},
|
|
{
|
|
response: {
|
|
result: '0x2',
|
|
},
|
|
},
|
|
],
|
|
eth_getBlockByNumber: [
|
|
{
|
|
request: {
|
|
params: ['0x1', false],
|
|
},
|
|
response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE,
|
|
},
|
|
{
|
|
request: {
|
|
params: ['0x2', false],
|
|
},
|
|
response: {
|
|
error: 'some error',
|
|
httpStatus: 500,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe(
|
|
'available',
|
|
);
|
|
|
|
// Force the block tracker to request a new block to clear the
|
|
// block cache
|
|
clock.runAll();
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe(
|
|
'unknown',
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('clears the ID of the network from state', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
eth_getBlockByNumber: [
|
|
{
|
|
request: {
|
|
params: ['0x1', false],
|
|
},
|
|
response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE,
|
|
},
|
|
{
|
|
request: {
|
|
params: ['0x2', false],
|
|
},
|
|
response: UNSUCCESSFUL_JSON_RPC_RESPONSE,
|
|
},
|
|
],
|
|
// Ensure that each call to eth_blockNumber returns a
|
|
// different block number, otherwise the first
|
|
// eth_getBlockByNumber response will get cached under the
|
|
// first block number
|
|
eth_blockNumber: [
|
|
{
|
|
response: {
|
|
result: '0x1',
|
|
},
|
|
},
|
|
{
|
|
response: {
|
|
result: '0x2',
|
|
},
|
|
},
|
|
],
|
|
});
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkId'],
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkId).toBe(networkId);
|
|
|
|
// Advance block tracker loop to force a fresh call to
|
|
// eth_getBlockByNumber
|
|
clock.runAll();
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkId'],
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkId).toBeNull();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('clears whether the network supports EIP-1559 from state', async () => {
|
|
const intentionalErrorMessage =
|
|
'intentional error from eth_getBlockByNumber';
|
|
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
networkDetails: {
|
|
EIPS: {
|
|
1559: true,
|
|
},
|
|
other: 'details',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
eth_getBlockByNumber: {
|
|
response: UNSUCCESSFUL_JSON_RPC_RESPONSE,
|
|
},
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkDetails'],
|
|
operation: async () => {
|
|
try {
|
|
await controller.lookupNetwork();
|
|
} catch (error) {
|
|
if (error !== intentionalErrorMessage) {
|
|
console.error(error);
|
|
}
|
|
}
|
|
},
|
|
});
|
|
expect(
|
|
controller.store.getState().networkDetails,
|
|
).toStrictEqual({
|
|
EIPS: {
|
|
1559: undefined,
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('does not emit infuraIsBlocked', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
eth_getBlockByNumber: {
|
|
response: UNSUCCESSFUL_JSON_RPC_RESPONSE,
|
|
},
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
const promiseForNoInfuraIsBlockedEvents =
|
|
waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsBlocked,
|
|
count: 0,
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(await promiseForNoInfuraIsBlockedEvents).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('does not emit infuraIsUnblocked', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
eth_getBlockByNumber: {
|
|
response: UNSUCCESSFUL_JSON_RPC_RESPONSE,
|
|
},
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
const promiseForNoInfuraIsUnblockedEvents =
|
|
waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsUnblocked,
|
|
count: 0,
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
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: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
id: 'testNetworkConfigurationId',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
ticker: 'ABC',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: network1 }) => {
|
|
network1.mockEssentialRpcCalls({
|
|
net_version: {
|
|
times: 2,
|
|
},
|
|
// Ensure that each call to eth_blockNumber returns a different
|
|
// block number, otherwise the first eth_getBlockByNumber
|
|
// response will get cached under the first block number
|
|
eth_blockNumber: [
|
|
{
|
|
response: {
|
|
result: '0x1',
|
|
},
|
|
},
|
|
{
|
|
response: {
|
|
result: '0x2',
|
|
},
|
|
},
|
|
],
|
|
eth_getBlockByNumber: [
|
|
{
|
|
request: {
|
|
params: ['0x1', false],
|
|
},
|
|
response: {
|
|
result: {
|
|
...BLOCK,
|
|
number: '0x1',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
request: {
|
|
params: ['0x2', false],
|
|
},
|
|
response: {
|
|
result: {
|
|
...BLOCK,
|
|
number: '0x2',
|
|
},
|
|
},
|
|
beforeCompleting: async () => {
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: () => {
|
|
controller.setActiveNetwork(
|
|
'testNetworkConfigurationId',
|
|
);
|
|
},
|
|
});
|
|
},
|
|
},
|
|
],
|
|
});
|
|
const network2 = new CustomNetworkCommunications({
|
|
customRpcUrl: 'https://mock-rpc-url',
|
|
});
|
|
network2.mockEssentialRpcCalls({
|
|
net_version: {
|
|
response: UNSUCCESSFUL_JSON_RPC_RESPONSE,
|
|
},
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe(
|
|
'available',
|
|
);
|
|
|
|
// Force the block tracker to request a new block to clear the
|
|
// block cache
|
|
clock.runAll();
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe(
|
|
'unknown',
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('stores the ID of the second network, not the first', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
id: 'testNetworkConfigurationId',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
ticker: 'ABC',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: network1 }) => {
|
|
network1.mockEssentialRpcCalls({
|
|
eth_getBlockByNumber: {
|
|
beforeCompleting: async () => {
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: () => {
|
|
controller.setActiveNetwork(
|
|
'testNetworkConfigurationId',
|
|
);
|
|
},
|
|
});
|
|
},
|
|
},
|
|
net_version: {
|
|
response: {
|
|
result: '111',
|
|
},
|
|
},
|
|
});
|
|
const network2 = new CustomNetworkCommunications({
|
|
customRpcUrl: 'https://mock-rpc-url',
|
|
});
|
|
network2.mockEssentialRpcCalls({
|
|
net_version: {
|
|
response: {
|
|
result: '222',
|
|
},
|
|
},
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkId'],
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(controller.store.getState().networkId).toBe('222');
|
|
},
|
|
);
|
|
});
|
|
|
|
it('stores the EIP-1559 support of the second network, not the first', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
id: 'testNetworkConfigurationId',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
ticker: 'ABC',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: network1 }) => {
|
|
network1.mockEssentialRpcCalls({
|
|
latestBlock: POST_1559_BLOCK,
|
|
eth_getBlockByNumber: {
|
|
beforeCompleting: async () => {
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: () => {
|
|
controller.setActiveNetwork(
|
|
'testNetworkConfigurationId',
|
|
);
|
|
},
|
|
});
|
|
},
|
|
},
|
|
});
|
|
const network2 = new CustomNetworkCommunications({
|
|
customRpcUrl: 'https://mock-rpc-url',
|
|
});
|
|
network2.mockEssentialRpcCalls({
|
|
latestBlock: PRE_1559_BLOCK,
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkDetails'],
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(
|
|
controller.store.getState().networkDetails,
|
|
).toStrictEqual({
|
|
EIPS: {
|
|
1559: false,
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('emits infuraIsBlocked, not infuraIsUnblocked, if the second network is blocked, even if the first one is not', async () => {
|
|
const anotherNetwork = INFURA_NETWORKS.find(
|
|
(network) => network.networkType !== networkType,
|
|
);
|
|
/* eslint-disable-next-line jest/no-if */
|
|
if (!anotherNetwork) {
|
|
throw new Error(
|
|
"Could not find another network to use. You've probably commented out all INFURA_NETWORKS but one. Please uncomment another one.",
|
|
);
|
|
}
|
|
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID
|
|
// of the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: network1 }) => {
|
|
network1.mockEssentialRpcCalls({
|
|
eth_getBlockByNumber: {
|
|
beforeCompleting: async () => {
|
|
await waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.NetworkDidChange,
|
|
operation: async () => {
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: () => {
|
|
controller.setProviderType(
|
|
anotherNetwork.networkType,
|
|
);
|
|
},
|
|
});
|
|
},
|
|
});
|
|
},
|
|
},
|
|
});
|
|
const network2 = network1.with({
|
|
infuraNetwork: anotherNetwork.networkType,
|
|
});
|
|
network2.mockEssentialRpcCalls({
|
|
eth_getBlockByNumber: {
|
|
response: BLOCKED_INFURA_RESPONSE,
|
|
},
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
const promiseForNoInfuraIsUnblockedEvents =
|
|
waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsUnblocked,
|
|
count: 0,
|
|
});
|
|
const promiseForInfuraIsBlocked = waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsBlocked,
|
|
});
|
|
|
|
await controller.lookupNetwork();
|
|
|
|
expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy();
|
|
expect(await promiseForInfuraIsBlocked).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('when the type in the provider configuration is "rpc"', () => {
|
|
describe('if both net_version and eth_getBlockByNumber respond successfully', () => {
|
|
it('stores the fact the network is available', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
net_version: {
|
|
response: SUCCESSFUL_NET_VERSION_RESPONSE,
|
|
},
|
|
eth_getBlockByNumber: {
|
|
response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE,
|
|
},
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe('unknown');
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(controller.store.getState().networkStatus).toBe(
|
|
'available',
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('stores the ID of the network', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
// This results in a successful call to eth_getBlockByNumber
|
|
// implicitly
|
|
latestBlock: POST_1559_BLOCK,
|
|
net_version: {
|
|
response: {
|
|
result: '42',
|
|
},
|
|
},
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkId).toBe(null);
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkId'],
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(controller.store.getState().networkId).toBe('42');
|
|
},
|
|
);
|
|
});
|
|
|
|
it('stores the fact that the network supports EIP-1559 when baseFeePerGas is in the block header', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
// This results in a successful call to eth_getBlockByNumber
|
|
// implicitly
|
|
latestBlock: POST_1559_BLOCK,
|
|
net_version: {
|
|
response: SUCCESSFUL_NET_VERSION_RESPONSE,
|
|
},
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkDetails'],
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(
|
|
controller.store.getState().networkDetails.EIPS[1559],
|
|
).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('stores the fact that the network does not support EIP-1559 when baseFeePerGas is not in the block header', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
// This results in a successful call to eth_getBlockByNumber
|
|
// implicitly
|
|
latestBlock: PRE_1559_BLOCK,
|
|
net_version: {
|
|
response: SUCCESSFUL_NET_VERSION_RESPONSE,
|
|
},
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkDetails'],
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(
|
|
controller.store.getState().networkDetails.EIPS[1559],
|
|
).toBe(false);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('emits infuraIsUnblocked', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
// This results in a successful call to eth_getBlockByNumber
|
|
// implicitly
|
|
latestBlock: PRE_1559_BLOCK,
|
|
net_version: {
|
|
response: SUCCESSFUL_NET_VERSION_RESPONSE,
|
|
},
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
const infuraIsUnblocked = await waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsUnblocked,
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(infuraIsUnblocked).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('if the request for eth_getBlockByNumber responds successfully, but the request for net_version responds with a generic error', () => {
|
|
it('stores the network status as available if the error does not translate to an internal RPC error', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
net_version: [
|
|
{
|
|
response: SUCCESSFUL_NET_VERSION_RESPONSE,
|
|
},
|
|
{
|
|
response: {
|
|
error: 'some error',
|
|
httpStatus: 405,
|
|
},
|
|
},
|
|
],
|
|
eth_blockNumber: {
|
|
times: 2,
|
|
},
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe(
|
|
'available',
|
|
);
|
|
|
|
// Force the block tracker to request a new block to clear the
|
|
// block cache
|
|
clock.runAll();
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe(
|
|
'unavailable',
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('stores the network status as unknown if the error translates to an internal RPC error', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
net_version: [
|
|
{
|
|
response: SUCCESSFUL_NET_VERSION_RESPONSE,
|
|
},
|
|
{
|
|
response: UNSUCCESSFUL_JSON_RPC_RESPONSE,
|
|
},
|
|
],
|
|
eth_blockNumber: {
|
|
times: 2,
|
|
},
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe(
|
|
'available',
|
|
);
|
|
|
|
// Force the block tracker to request a new block to clear the
|
|
// block cache
|
|
clock.runAll();
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe('unknown');
|
|
},
|
|
);
|
|
});
|
|
|
|
it('clears the ID of the network from state', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
// This results in a successful call to eth_getBlockByNumber
|
|
// implicitly
|
|
latestBlock: BLOCK,
|
|
net_version: [
|
|
{
|
|
response: {
|
|
result: '42',
|
|
},
|
|
},
|
|
{
|
|
response: UNSUCCESSFUL_JSON_RPC_RESPONSE,
|
|
},
|
|
],
|
|
eth_blockNumber: {
|
|
times: 2,
|
|
},
|
|
});
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkId'],
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkId).toBe('42');
|
|
|
|
// Advance block tracker loop to force a fresh call to
|
|
// eth_getBlockByNumber
|
|
clock.runAll();
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkId'],
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkId).toBeNull();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('clears whether the network supports EIP-1559 from state', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
},
|
|
networkDetails: {
|
|
EIPS: {
|
|
1559: true,
|
|
},
|
|
other: 'details',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
// This results in a successful call to eth_getBlockByNumber
|
|
// implicitly
|
|
latestBlock: BLOCK,
|
|
net_version: {
|
|
response: UNSUCCESSFUL_JSON_RPC_RESPONSE,
|
|
},
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkDetails'],
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkDetails).toStrictEqual({
|
|
EIPS: {
|
|
1559: undefined,
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('does not emit infuraIsBlocked', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
// This results in a successful call to eth_getBlockByNumber
|
|
// implicitly
|
|
latestBlock: BLOCK,
|
|
net_version: {
|
|
response: UNSUCCESSFUL_JSON_RPC_RESPONSE,
|
|
},
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
const promiseForNoInfuraIsBlockedEvents = waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsBlocked,
|
|
count: 0,
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(await promiseForNoInfuraIsBlockedEvents).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('emits infuraIsUnblocked', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
// This results in a successful call to eth_getBlockByNumber
|
|
// implicitly
|
|
latestBlock: BLOCK,
|
|
net_version: {
|
|
response: UNSUCCESSFUL_JSON_RPC_RESPONSE,
|
|
},
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
const infuraIsUnblocked = await waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsUnblocked,
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(infuraIsUnblocked).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('if the request for net_version responds successfully, but the request for eth_getBlockByNumber responds with a generic error', () => {
|
|
it('stores the fact that the network is unavailable', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
net_version: {
|
|
times: 2,
|
|
},
|
|
// Ensure that each call to eth_blockNumber returns a different
|
|
// block number, otherwise the first eth_getBlockByNumber
|
|
// response will get cached under the first block number
|
|
eth_blockNumber: [
|
|
{
|
|
response: {
|
|
result: '0x1',
|
|
},
|
|
},
|
|
{
|
|
response: {
|
|
result: '0x2',
|
|
},
|
|
},
|
|
],
|
|
eth_getBlockByNumber: [
|
|
{
|
|
request: {
|
|
params: ['0x1', false],
|
|
},
|
|
response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE,
|
|
},
|
|
{
|
|
request: {
|
|
params: ['0x2', false],
|
|
},
|
|
response: UNSUCCESSFUL_JSON_RPC_RESPONSE,
|
|
},
|
|
],
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe(
|
|
'available',
|
|
);
|
|
|
|
// Force the block tracker to request a new block to clear the
|
|
// block cache
|
|
clock.runAll();
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe('unknown');
|
|
},
|
|
);
|
|
});
|
|
|
|
it('clears the ID of the network from state', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
latestBlock: BLOCK,
|
|
net_version: {
|
|
response: {
|
|
result: '42',
|
|
},
|
|
},
|
|
eth_getBlockByNumber: [
|
|
{
|
|
response: {
|
|
result: BLOCK,
|
|
},
|
|
},
|
|
{
|
|
response: UNSUCCESSFUL_JSON_RPC_RESPONSE,
|
|
},
|
|
],
|
|
eth_blockNumber: {
|
|
times: 2,
|
|
},
|
|
});
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkId'],
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkId).toBe('42');
|
|
|
|
// Advance block tracker loop to force a fresh call to
|
|
// eth_getBlockByNumber
|
|
clock.runAll();
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkId'],
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkId).toBeNull();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('clears whether the network supports EIP-1559 from state', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
},
|
|
networkDetails: {
|
|
EIPS: {
|
|
1559: true,
|
|
},
|
|
other: 'details',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
eth_getBlockByNumber: {
|
|
response: UNSUCCESSFUL_JSON_RPC_RESPONSE,
|
|
},
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkDetails'],
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkDetails).toStrictEqual({
|
|
EIPS: {
|
|
1559: undefined,
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('does not emit infuraIsBlocked', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
eth_getBlockByNumber: {
|
|
response: UNSUCCESSFUL_JSON_RPC_RESPONSE,
|
|
},
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
const promiseForNoInfuraIsBlockedEvents = waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsBlocked,
|
|
count: 0,
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(await promiseForNoInfuraIsBlockedEvents).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('emits infuraIsUnblocked', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
eth_getBlockByNumber: {
|
|
response: UNSUCCESSFUL_JSON_RPC_RESPONSE,
|
|
},
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
const infuraIsUnblocked = await waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsUnblocked,
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(infuraIsUnblocked).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
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 () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: network1 }) => {
|
|
network1.mockEssentialRpcCalls({
|
|
net_version: [
|
|
{
|
|
response: SUCCESSFUL_NET_VERSION_RESPONSE,
|
|
},
|
|
{
|
|
response: SUCCESSFUL_NET_VERSION_RESPONSE,
|
|
beforeCompleting: async () => {
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: () => {
|
|
controller.setProviderType('goerli');
|
|
},
|
|
});
|
|
},
|
|
},
|
|
],
|
|
// Ensure that each call to eth_blockNumber returns a different
|
|
// block number, otherwise the first eth_getBlockByNumber
|
|
// response will get cached under the first block number
|
|
eth_blockNumber: [
|
|
{
|
|
response: {
|
|
result: '0x1',
|
|
},
|
|
},
|
|
{
|
|
response: {
|
|
result: '0x2',
|
|
},
|
|
},
|
|
],
|
|
eth_getBlockByNumber: [
|
|
{
|
|
request: {
|
|
params: ['0x1', false],
|
|
},
|
|
response: {
|
|
result: {
|
|
...BLOCK,
|
|
number: '0x1',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
request: {
|
|
params: ['0x2', false],
|
|
},
|
|
response: {
|
|
result: {
|
|
...BLOCK,
|
|
number: '0x2',
|
|
},
|
|
},
|
|
},
|
|
],
|
|
});
|
|
const network2 = new InfuraNetworkCommunications({
|
|
infuraNetwork: 'goerli',
|
|
});
|
|
network2.mockEssentialRpcCalls({
|
|
eth_getBlockByNumber: {
|
|
response: UNSUCCESSFUL_JSON_RPC_RESPONSE,
|
|
},
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe(
|
|
'available',
|
|
);
|
|
|
|
// Force the block tracker to request a new block to clear the
|
|
// block cache
|
|
clock.runAll();
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe('unknown');
|
|
},
|
|
);
|
|
});
|
|
|
|
it('stores the ID of the second network, not the first', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url-2',
|
|
chainId: '0x1337',
|
|
ticker: 'RPC',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: network1 }) => {
|
|
network1.mockEssentialRpcCalls({
|
|
net_version: {
|
|
response: {
|
|
result: '111',
|
|
},
|
|
beforeCompleting: async () => {
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: () => {
|
|
controller.setProviderType('goerli');
|
|
},
|
|
});
|
|
},
|
|
},
|
|
});
|
|
const network2 = new InfuraNetworkCommunications({
|
|
infuraNetwork: 'goerli',
|
|
});
|
|
network2.mockEssentialRpcCalls();
|
|
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkId'],
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(controller.store.getState().networkId).toBe('5');
|
|
},
|
|
);
|
|
});
|
|
|
|
it('stores the EIP-1559 support of the second network, not the first', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
ticker: 'RPC',
|
|
},
|
|
networkDetails: {
|
|
EIPS: {},
|
|
other: 'details',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: network1 }) => {
|
|
network1.mockEssentialRpcCalls({
|
|
latestBlock: POST_1559_BLOCK,
|
|
net_version: {
|
|
response: SUCCESSFUL_NET_VERSION_RESPONSE,
|
|
beforeCompleting: async () => {
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkDetails'],
|
|
operation: () => {
|
|
controller.setProviderType('goerli');
|
|
},
|
|
});
|
|
},
|
|
},
|
|
});
|
|
const network2 = new InfuraNetworkCommunications({
|
|
infuraNetwork: 'goerli',
|
|
});
|
|
network2.mockEssentialRpcCalls({
|
|
latestBlock: PRE_1559_BLOCK,
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkDetails'],
|
|
// setProviderType clears networkDetails first, and then updates
|
|
// it to what we expect it to be
|
|
count: 2,
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(controller.store.getState().networkDetails).toStrictEqual({
|
|
EIPS: {
|
|
1559: false,
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('emits infuraIsBlocked, not infuraIsUnblocked, if the second network is blocked, even if the first one is not', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
ticker: 'RPC',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: network1 }) => {
|
|
network1.mockEssentialRpcCalls({
|
|
net_version: {
|
|
beforeCompleting: async () => {
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkDetails'],
|
|
operation: () => {
|
|
controller.setProviderType('goerli');
|
|
},
|
|
});
|
|
},
|
|
},
|
|
});
|
|
const network2 = new InfuraNetworkCommunications({
|
|
infuraNetwork: 'goerli',
|
|
});
|
|
network2.mockEssentialRpcCalls({
|
|
eth_getBlockByNumber: {
|
|
response: BLOCKED_INFURA_RESPONSE,
|
|
},
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
const promiseForNoInfuraIsUnblockedEvents =
|
|
waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsUnblocked,
|
|
count: 0,
|
|
});
|
|
const promiseForInfuraIsBlocked = waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsBlocked,
|
|
});
|
|
|
|
await controller.lookupNetwork();
|
|
|
|
expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy();
|
|
expect(await promiseForInfuraIsBlocked).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
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: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url-1',
|
|
chainId: '0x1337',
|
|
ticker: 'RPC',
|
|
},
|
|
networkDetails: {
|
|
EIPS: {},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: network1 }) => {
|
|
network1.mockEssentialRpcCalls({
|
|
net_version: {
|
|
times: 2,
|
|
},
|
|
// Ensure that each call to eth_blockNumber returns a different
|
|
// block number, otherwise the first eth_getBlockByNumber
|
|
// response will get cached under the first block number
|
|
eth_blockNumber: [
|
|
{
|
|
response: {
|
|
result: '0x1',
|
|
},
|
|
},
|
|
{
|
|
response: {
|
|
result: '0x2',
|
|
},
|
|
},
|
|
],
|
|
eth_getBlockByNumber: [
|
|
{
|
|
request: {
|
|
params: ['0x1', false],
|
|
},
|
|
response: {
|
|
result: {
|
|
...BLOCK,
|
|
number: '0x1',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
request: {
|
|
params: ['0x2', false],
|
|
},
|
|
response: {
|
|
result: {
|
|
...BLOCK,
|
|
number: '0x2',
|
|
},
|
|
},
|
|
beforeCompleting: async () => {
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: () => {
|
|
controller.setProviderType('goerli');
|
|
},
|
|
});
|
|
},
|
|
},
|
|
],
|
|
});
|
|
const network2 = new InfuraNetworkCommunications({
|
|
infuraNetwork: 'goerli',
|
|
});
|
|
network2.mockEssentialRpcCalls({
|
|
eth_getBlockByNumber: {
|
|
response: UNSUCCESSFUL_JSON_RPC_RESPONSE,
|
|
},
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe(
|
|
'available',
|
|
);
|
|
|
|
// Force the block tracker to request a new block to clear the
|
|
// block cache
|
|
clock.runAll();
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe('unknown');
|
|
},
|
|
);
|
|
});
|
|
|
|
it('stores the network ID of the second network, not the first', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url-2',
|
|
chainId: '0x1337',
|
|
ticker: 'RPC',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: network1 }) => {
|
|
network1.mockEssentialRpcCalls({
|
|
eth_getBlockByNumber: {
|
|
beforeCompleting: async () => {
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: () => {
|
|
controller.setProviderType('goerli');
|
|
},
|
|
});
|
|
},
|
|
},
|
|
net_version: {
|
|
response: {
|
|
result: '111',
|
|
},
|
|
},
|
|
});
|
|
const network2 = new InfuraNetworkCommunications({
|
|
infuraNetwork: 'goerli',
|
|
});
|
|
network2.mockEssentialRpcCalls();
|
|
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkId'],
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(controller.store.getState().networkId).toBe('5');
|
|
},
|
|
);
|
|
});
|
|
|
|
it('stores the EIP-1559 support of the second network, not the first', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
ticker: 'RPC',
|
|
},
|
|
networkDetails: {
|
|
EIPS: {},
|
|
other: 'details',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: network1 }) => {
|
|
network1.mockEssentialRpcCalls({
|
|
latestBlock: POST_1559_BLOCK,
|
|
eth_getBlockByNumber: {
|
|
beforeCompleting: async () => {
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkDetails'],
|
|
operation: () => {
|
|
controller.setProviderType('goerli');
|
|
},
|
|
});
|
|
},
|
|
},
|
|
});
|
|
const network2 = new InfuraNetworkCommunications({
|
|
infuraNetwork: 'goerli',
|
|
});
|
|
network2.mockEssentialRpcCalls({
|
|
latestBlock: PRE_1559_BLOCK,
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkDetails'],
|
|
// setProviderType clears networkDetails first, and then updates
|
|
// it to what we expect it to be
|
|
count: 2,
|
|
operation: async () => {
|
|
await controller.lookupNetwork();
|
|
},
|
|
});
|
|
|
|
expect(controller.store.getState().networkDetails).toStrictEqual({
|
|
EIPS: {
|
|
1559: false,
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('emits infuraIsBlocked, not infuraIsUnblocked, if the second network is blocked, even if the first one is not', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
ticker: 'RPC',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: network1 }) => {
|
|
network1.mockEssentialRpcCalls({
|
|
eth_getBlockByNumber: {
|
|
beforeCompleting: async () => {
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkDetails'],
|
|
operation: () => {
|
|
controller.setProviderType('goerli');
|
|
},
|
|
});
|
|
},
|
|
},
|
|
});
|
|
const network2 = new InfuraNetworkCommunications({
|
|
infuraNetwork: 'goerli',
|
|
});
|
|
network2.mockEssentialRpcCalls({
|
|
eth_getBlockByNumber: {
|
|
response: BLOCKED_INFURA_RESPONSE,
|
|
},
|
|
});
|
|
await withoutCallingLookupNetwork({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
const promiseForNoInfuraIsUnblockedEvents =
|
|
waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsUnblocked,
|
|
count: 0,
|
|
});
|
|
const promiseForInfuraIsBlocked = waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsBlocked,
|
|
});
|
|
|
|
await controller.lookupNetwork();
|
|
|
|
expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy();
|
|
expect(await promiseForInfuraIsBlocked).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('setActiveNetwork', () => {
|
|
it('throws if the given networkConfigurationId does not match one in networkConfigurations', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
id: 'testNetworkConfigurationId',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls();
|
|
|
|
expect(() =>
|
|
controller.setActiveNetwork('invalid-network-configuration-id'),
|
|
).toThrow(
|
|
new Error(
|
|
'networkConfigurationId invalid-network-configuration-id does not match a configured networkConfiguration',
|
|
),
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('overwrites the provider configuration given a networkConfigurationId that matches a configured networkConfiguration', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'http://example-custom-rpc.metamask.io',
|
|
chainId: '0x9999',
|
|
ticker: 'RPC',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId1: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId1',
|
|
},
|
|
testNetworkConfigurationId2: {
|
|
rpcUrl: 'http://example-custom-rpc.metamask.io',
|
|
chainId: '0x9999',
|
|
ticker: 'RPC',
|
|
id: 'testNetworkConfigurationId2',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller }) => {
|
|
const network = new CustomNetworkCommunications({
|
|
customRpcUrl: 'https://mock-rpc-url',
|
|
});
|
|
network.mockEssentialRpcCalls();
|
|
|
|
controller.setActiveNetwork('testNetworkConfigurationId1');
|
|
|
|
expect(controller.store.getState().provider).toStrictEqual({
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId1',
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('emits networkWillChange before making any changes to the network status', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url-1',
|
|
chainId: '0x111',
|
|
ticker: 'TEST2',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId1: {
|
|
rpcUrl: 'https://mock-rpc-url-1',
|
|
chainId: '0x111',
|
|
ticker: 'TEST1',
|
|
id: 'testNetworkConfigurationId1',
|
|
},
|
|
testNetworkConfigurationId2: {
|
|
rpcUrl: 'https://mock-rpc-url-2',
|
|
chainId: '0x222',
|
|
ticker: 'TEST2',
|
|
id: 'testNetworkConfigurationId2',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: network1 }) => {
|
|
network1.mockEssentialRpcCalls({
|
|
net_version: {
|
|
response: SUCCESSFUL_NET_VERSION_RESPONSE,
|
|
},
|
|
});
|
|
const network2 = network1.with({
|
|
customRpcUrl: 'https://mock-rpc-url-2',
|
|
});
|
|
network2.mockEssentialRpcCalls({
|
|
net_version: UNSUCCESSFUL_JSON_RPC_RESPONSE,
|
|
});
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.initializeProvider();
|
|
},
|
|
});
|
|
const initialNetworkStatus =
|
|
controller.store.getState().networkStatus;
|
|
expect(initialNetworkStatus).toBe('available');
|
|
|
|
const networkWillChange = await waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.NetworkWillChange,
|
|
operation: () => {
|
|
controller.setActiveNetwork('testNetworkConfigurationId2');
|
|
},
|
|
beforeResolving: () => {
|
|
expect(controller.store.getState().networkStatus).toBe(
|
|
initialNetworkStatus,
|
|
);
|
|
},
|
|
});
|
|
expect(networkWillChange).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('resets the network status to "unknown" before emitting networkDidChange', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'http://mock-rpc-url-2',
|
|
chainId: '0xtest2',
|
|
ticker: 'TEST2',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId1: {
|
|
rpcUrl: 'https://mock-rpc-url-1',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId1',
|
|
},
|
|
testNetworkConfigurationId2: {
|
|
rpcUrl: 'http://mock-rpc-url-2',
|
|
chainId: '0xtest2',
|
|
ticker: 'TEST2',
|
|
id: 'testNetworkConfigurationId2',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: network1 }) => {
|
|
network1.mockEssentialRpcCalls({
|
|
net_version: {
|
|
response: SUCCESSFUL_NET_VERSION_RESPONSE,
|
|
},
|
|
});
|
|
const network2 = network1.with({
|
|
customRpcUrl: 'https://mock-rpc-url-1',
|
|
});
|
|
network2.mockEssentialRpcCalls({
|
|
net_version: {
|
|
response: SUCCESSFUL_NET_VERSION_RESPONSE,
|
|
},
|
|
});
|
|
|
|
await controller.initializeProvider();
|
|
expect(controller.store.getState().networkStatus).toBe('available');
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
// We only care about the first state change, because it happens
|
|
// before networkDidChange
|
|
count: 1,
|
|
operation: () => {
|
|
controller.setActiveNetwork('testNetworkConfigurationId1');
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe('unknown');
|
|
},
|
|
);
|
|
});
|
|
|
|
it('clears EIP-1559 support for the network from state before emitting networkDidChange', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url-1',
|
|
chainId: '0x111',
|
|
ticker: 'TEST1',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId1: {
|
|
rpcUrl: 'https://mock-rpc-url-1',
|
|
chainId: '0x1111',
|
|
ticker: 'TEST1',
|
|
id: 'testNetworkConfigurationId1',
|
|
},
|
|
testNetworkConfigurationId2: {
|
|
rpcUrl: 'https://mock-rpc-url-2',
|
|
chainId: '0x222',
|
|
ticker: 'TEST2',
|
|
id: 'testNetworkConfigurationId2',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: network1 }) => {
|
|
network1.mockEssentialRpcCalls({
|
|
latestBlock: POST_1559_BLOCK,
|
|
});
|
|
const network2 = network1.with({
|
|
customRpcUrl: 'https://mock-rpc-url-2',
|
|
});
|
|
network2.mockEssentialRpcCalls({
|
|
latestBlock: PRE_1559_BLOCK,
|
|
});
|
|
|
|
await controller.initializeProvider();
|
|
expect(controller.store.getState().networkDetails).toStrictEqual({
|
|
EIPS: {
|
|
1559: true,
|
|
},
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkDetails'],
|
|
// We only care about the first state change, because it happens
|
|
// before networkDidChange
|
|
count: 1,
|
|
operation: () => {
|
|
controller.setActiveNetwork('testNetworkConfigurationId2');
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkDetails).toStrictEqual({
|
|
EIPS: {
|
|
1559: undefined,
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('initializes a provider pointed to the given RPC URL whose chain ID matches the configured chain ID', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
id: 'testNetworkConfigurationId',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller }) => {
|
|
const network = new CustomNetworkCommunications({
|
|
customRpcUrl: 'https://mock-rpc-url',
|
|
});
|
|
network.mockEssentialRpcCalls();
|
|
network.mockRpcCall({
|
|
request: {
|
|
method: 'test',
|
|
params: [],
|
|
},
|
|
response: {
|
|
result: 'test response',
|
|
},
|
|
});
|
|
|
|
controller.setActiveNetwork('testNetworkConfigurationId');
|
|
|
|
const { provider } = controller.getProviderAndBlockTracker();
|
|
assert(provider, 'Provider is somehow unset');
|
|
const promisifiedSendAsync = promisify(provider.sendAsync).bind(
|
|
provider,
|
|
);
|
|
const { result: testResult } = await promisifiedSendAsync({
|
|
id: '1',
|
|
jsonrpc: '2.0',
|
|
method: 'test',
|
|
params: [],
|
|
});
|
|
expect(testResult).toBe('test response');
|
|
const { result: chainIdResult } = await promisifiedSendAsync({
|
|
id: '2',
|
|
jsonrpc: '2.0',
|
|
method: 'eth_chainId',
|
|
});
|
|
expect(chainIdResult).toBe('0xtest');
|
|
},
|
|
);
|
|
});
|
|
|
|
it('replaces the provider object underlying the provider proxy without creating a new instance of the proxy itself', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
id: 'testNetworkConfigurationId',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
ticker: 'TEST',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: network1 }) => {
|
|
network1.mockEssentialRpcCalls();
|
|
const network2 = network1.with({
|
|
customRpcUrl: 'https://mock-rpc-url',
|
|
});
|
|
network2.mockEssentialRpcCalls();
|
|
await controller.initializeProvider();
|
|
|
|
const { provider: providerBefore } =
|
|
controller.getProviderAndBlockTracker();
|
|
controller.setActiveNetwork('testNetworkConfigurationId');
|
|
const { provider: providerAfter } =
|
|
controller.getProviderAndBlockTracker();
|
|
|
|
expect(providerBefore).toBe(providerAfter);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('emits networkDidChange', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
id: 'testNetworkConfigurationId',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller }) => {
|
|
const network = new CustomNetworkCommunications({
|
|
customRpcUrl: 'https://mock-rpc-url',
|
|
});
|
|
network.mockEssentialRpcCalls();
|
|
|
|
const networkDidChange = await waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.NetworkDidChange,
|
|
operation: () => {
|
|
controller.setActiveNetwork('testNetworkConfigurationId');
|
|
},
|
|
});
|
|
|
|
expect(networkDidChange).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('emits infuraIsUnblocked', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
id: 'testNetworkConfigurationId',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller }) => {
|
|
const network = new CustomNetworkCommunications({
|
|
customRpcUrl: 'https://mock-rpc-url',
|
|
});
|
|
network.mockEssentialRpcCalls();
|
|
|
|
const infuraIsUnblocked = await waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsUnblocked,
|
|
operation: () => {
|
|
controller.setActiveNetwork('testNetworkConfigurationId');
|
|
},
|
|
});
|
|
|
|
expect(infuraIsUnblocked).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('determines the status of the network, storing it in state', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
id: 'testNetworkConfigurationId',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller }) => {
|
|
const network = new CustomNetworkCommunications({
|
|
customRpcUrl: 'https://mock-rpc-url',
|
|
});
|
|
network.mockEssentialRpcCalls({
|
|
net_version: {
|
|
response: SUCCESSFUL_NET_VERSION_RESPONSE,
|
|
},
|
|
});
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: () => {
|
|
controller.setActiveNetwork('testNetworkConfigurationId');
|
|
},
|
|
});
|
|
|
|
expect(controller.store.getState().networkStatus).toBe('available');
|
|
},
|
|
);
|
|
});
|
|
|
|
it('determines whether the network supports EIP-1559, storing it in state', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
networkDetails: {
|
|
EIPS: {},
|
|
other: 'details',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
id: 'testNetworkConfigurationId',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller }) => {
|
|
const network = new CustomNetworkCommunications({
|
|
customRpcUrl: 'https://mock-rpc-url',
|
|
});
|
|
network.mockEssentialRpcCalls({
|
|
latestBlock: POST_1559_BLOCK,
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkDetails'],
|
|
// setActiveNetwork clears networkDetails first, and then updates it
|
|
// to what we expect it to be
|
|
count: 2,
|
|
operation: () => {
|
|
controller.setActiveNetwork('testNetworkConfigurationId');
|
|
},
|
|
});
|
|
|
|
expect(controller.store.getState().networkDetails).toStrictEqual({
|
|
EIPS: {
|
|
1559: true,
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('setProviderType', () => {
|
|
for (const { networkType, chainId, ticker } of INFURA_NETWORKS) {
|
|
describe(`given a type of "${networkType}"`, () => {
|
|
it(`overwrites the provider configuration using type: "${networkType}", chainId: "${chainId}", and ticker "${ticker}", clearing rpcUrl and nickname, and removing rpcPrefs`, async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'http://mock-rpc-url-2',
|
|
chainId: '0xtest2',
|
|
nickname: 'test-chain-2',
|
|
ticker: 'TEST2',
|
|
rpcPrefs: {
|
|
blockExplorerUrl: 'test-block-explorer-2.com',
|
|
},
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId1: {
|
|
rpcUrl: 'https://mock-rpc-url-1',
|
|
chainId: '0xtest',
|
|
nickname: 'test-chain',
|
|
ticker: 'TEST',
|
|
rpcPrefs: {
|
|
blockExplorerUrl: 'test-block-explorer.com',
|
|
},
|
|
id: 'testNetworkConfigurationId1',
|
|
},
|
|
testNetworkConfigurationId2: {
|
|
rpcUrl: 'http://mock-rpc-url-2',
|
|
chainId: '0xtest2',
|
|
nickname: 'test-chain-2',
|
|
ticker: 'TEST2',
|
|
rpcPrefs: {
|
|
blockExplorerUrl: 'test-block-explorer-2.com',
|
|
},
|
|
id: 'testNetworkConfigurationId2',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller }) => {
|
|
const network = new InfuraNetworkCommunications({
|
|
infuraNetwork: networkType,
|
|
});
|
|
network.mockEssentialRpcCalls();
|
|
|
|
controller.setProviderType(networkType);
|
|
|
|
expect(controller.store.getState().provider).toStrictEqual({
|
|
type: networkType,
|
|
rpcUrl: '',
|
|
chainId,
|
|
ticker,
|
|
nickname: '',
|
|
rpcPrefs: {
|
|
blockExplorerUrl:
|
|
BUILT_IN_NETWORKS[networkType].blockExplorerUrl,
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('emits networkWillChange', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{ messenger: restrictedMessenger },
|
|
async ({ controller }) => {
|
|
const network = new InfuraNetworkCommunications({
|
|
infuraNetwork: networkType,
|
|
});
|
|
network.mockEssentialRpcCalls();
|
|
|
|
const networkWillChange = await waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.NetworkWillChange,
|
|
operation: () => {
|
|
controller.setProviderType(networkType);
|
|
},
|
|
});
|
|
|
|
expect(networkWillChange).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('resets the network status to "unknown" before emitting networkDidChange', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: network1 }) => {
|
|
network1.mockEssentialRpcCalls({
|
|
net_version: {
|
|
response: SUCCESSFUL_NET_VERSION_RESPONSE,
|
|
},
|
|
});
|
|
const network2 = new InfuraNetworkCommunications({
|
|
infuraNetwork: networkType,
|
|
});
|
|
network2.mockEssentialRpcCalls();
|
|
|
|
await controller.initializeProvider();
|
|
expect(controller.store.getState().networkStatus).toBe(
|
|
'available',
|
|
);
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
// We only care about the first state change, because it
|
|
// happens before networkDidChange
|
|
count: 1,
|
|
operation: () => {
|
|
controller.setProviderType(networkType);
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe('unknown');
|
|
},
|
|
);
|
|
});
|
|
|
|
it('clears EIP-1559 support for the network from state before emitting networkDidChange', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: network1 }) => {
|
|
network1.mockEssentialRpcCalls({
|
|
latestBlock: POST_1559_BLOCK,
|
|
});
|
|
const network2 = new InfuraNetworkCommunications({
|
|
infuraNetwork: networkType,
|
|
});
|
|
network2.mockEssentialRpcCalls({
|
|
latestBlock: PRE_1559_BLOCK,
|
|
});
|
|
|
|
await controller.initializeProvider();
|
|
expect(controller.store.getState().networkDetails).toStrictEqual({
|
|
EIPS: {
|
|
1559: true,
|
|
},
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkDetails'],
|
|
// We only care about the first state change, because it
|
|
// happens before networkDidChange
|
|
count: 1,
|
|
operation: () => {
|
|
controller.setProviderType(networkType);
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkDetails).toStrictEqual({
|
|
EIPS: {
|
|
1559: undefined,
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it(`initializes a provider pointed to the "${networkType}" Infura network (chainId: ${chainId})`, async () => {
|
|
await withController(async ({ controller }) => {
|
|
const network = new InfuraNetworkCommunications({
|
|
infuraNetwork: networkType,
|
|
});
|
|
network.mockEssentialRpcCalls();
|
|
|
|
controller.setProviderType(networkType);
|
|
|
|
const { provider } = controller.getProviderAndBlockTracker();
|
|
assert(provider, 'Provider is somehow unset');
|
|
const promisifiedSendAsync = promisify(provider.sendAsync).bind(
|
|
provider,
|
|
);
|
|
const { result: chainIdResult } = await promisifiedSendAsync({
|
|
id: '1',
|
|
jsonrpc: '2.0',
|
|
method: 'eth_chainId',
|
|
});
|
|
expect(chainIdResult).toBe(chainId);
|
|
});
|
|
});
|
|
|
|
it('replaces the provider object underlying the provider proxy without creating a new instance of the proxy itself', async () => {
|
|
await withController(async ({ controller, network: network1 }) => {
|
|
network1.mockEssentialRpcCalls();
|
|
const network2 = new InfuraNetworkCommunications({
|
|
infuraNetwork: networkType,
|
|
});
|
|
network2.mockEssentialRpcCalls();
|
|
await controller.initializeProvider();
|
|
|
|
const { provider: providerBefore } =
|
|
controller.getProviderAndBlockTracker();
|
|
controller.setProviderType(networkType);
|
|
const { provider: providerAfter } =
|
|
controller.getProviderAndBlockTracker();
|
|
|
|
expect(providerBefore).toBe(providerAfter);
|
|
});
|
|
});
|
|
|
|
it('emits networkDidChange', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{ messenger: restrictedMessenger },
|
|
async ({ controller }) => {
|
|
const network = new InfuraNetworkCommunications({
|
|
infuraNetwork: networkType,
|
|
});
|
|
network.mockEssentialRpcCalls();
|
|
|
|
const networkDidChange = await waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.NetworkDidChange,
|
|
operation: () => {
|
|
controller.setProviderType(networkType);
|
|
},
|
|
});
|
|
|
|
expect(networkDidChange).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('emits infuraIsBlocked or infuraIsUnblocked, depending on whether Infura is blocking requests', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{ messenger: restrictedMessenger },
|
|
async ({ controller }) => {
|
|
const network = new InfuraNetworkCommunications({
|
|
infuraNetwork: networkType,
|
|
});
|
|
network.mockEssentialRpcCalls({
|
|
eth_getBlockByNumber: {
|
|
response: BLOCKED_INFURA_RESPONSE,
|
|
},
|
|
});
|
|
const promiseForNoInfuraIsUnblockedEvents =
|
|
waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsUnblocked,
|
|
count: 0,
|
|
});
|
|
const promiseForInfuraIsBlocked = waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsBlocked,
|
|
});
|
|
|
|
controller.setProviderType(networkType);
|
|
|
|
expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy();
|
|
expect(await promiseForInfuraIsBlocked).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('determines the status of the network, storing it in state', async () => {
|
|
await withController(async ({ controller }) => {
|
|
const network = new InfuraNetworkCommunications({
|
|
infuraNetwork: networkType,
|
|
});
|
|
network.mockEssentialRpcCalls({
|
|
// This results in a successful call to eth_getBlockByNumber
|
|
// implicitly
|
|
latestBlock: BLOCK,
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: () => {
|
|
controller.setProviderType(networkType);
|
|
},
|
|
});
|
|
|
|
expect(controller.store.getState().networkStatus).toBe('available');
|
|
});
|
|
});
|
|
|
|
it('determines whether the network supports EIP-1559, storing it in state', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
networkDetails: {
|
|
EIPS: {},
|
|
other: 'details',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller }) => {
|
|
const network = new InfuraNetworkCommunications({
|
|
infuraNetwork: networkType,
|
|
});
|
|
network.mockEssentialRpcCalls({
|
|
latestBlock: POST_1559_BLOCK,
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkDetails'],
|
|
// setProviderType clears networkDetails first, and then updates
|
|
// it to what we expect it to be
|
|
count: 2,
|
|
operation: () => {
|
|
controller.setProviderType(networkType);
|
|
},
|
|
});
|
|
|
|
expect(controller.store.getState().networkDetails).toStrictEqual({
|
|
EIPS: {
|
|
1559: true,
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
describe('given a type of "rpc"', () => {
|
|
it('throws', async () => {
|
|
await withController(async ({ controller }) => {
|
|
expect(() => controller.setProviderType('rpc')).toThrow(
|
|
new Error(
|
|
'NetworkController - cannot call "setProviderType" with type "rpc". Use "setActiveNetwork"',
|
|
),
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('given an invalid Infura network name', () => {
|
|
it('throws', async () => {
|
|
await withController(async ({ controller }) => {
|
|
expect(() => controller.setProviderType('sadlflaksdj')).toThrow(
|
|
new Error('Unknown Infura provider type "sadlflaksdj".'),
|
|
);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('resetConnection', () => {
|
|
for (const { networkType, chainId } of INFURA_NETWORKS) {
|
|
describe(`when the type in the provider configuration is "${networkType}"`, () => {
|
|
it('emits networkWillChange', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls();
|
|
|
|
const networkWillChange = await waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.NetworkWillChange,
|
|
operation: () => {
|
|
controller.resetConnection();
|
|
},
|
|
});
|
|
|
|
expect(networkWillChange).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('resets the network status to "unknown" before emitting networkDidChange', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
eth_blockNumber: {
|
|
times: 2,
|
|
},
|
|
});
|
|
|
|
await controller.initializeProvider();
|
|
expect(controller.store.getState().networkStatus).toBe(
|
|
'available',
|
|
);
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
// We only care about the first state change, because it
|
|
// happens before networkDidChange
|
|
count: 1,
|
|
operation: () => {
|
|
controller.resetConnection();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe('unknown');
|
|
},
|
|
);
|
|
});
|
|
|
|
it('clears EIP-1559 support for the network from state before emitting networkDidChange', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
latestBlock: POST_1559_BLOCK,
|
|
eth_blockNumber: {
|
|
times: 2,
|
|
},
|
|
});
|
|
|
|
await controller.initializeProvider();
|
|
expect(controller.store.getState().networkDetails).toStrictEqual({
|
|
EIPS: {
|
|
1559: true,
|
|
},
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkDetails'],
|
|
// We only care about the first state change, because it
|
|
// happens before networkDidChange
|
|
count: 1,
|
|
operation: () => {
|
|
controller.resetConnection();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkDetails).toStrictEqual({
|
|
EIPS: {
|
|
1559: undefined,
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it(`initializes a new provider object pointed to the current Infura network (type: "${networkType}", chain ID: ${chainId})`, async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls();
|
|
|
|
controller.resetConnection();
|
|
|
|
const { provider } = controller.getProviderAndBlockTracker();
|
|
assert(provider, 'Provider is somehow unset');
|
|
const promisifiedSendAsync = promisify(provider.sendAsync).bind(
|
|
provider,
|
|
);
|
|
const { result: chainIdResult } = await promisifiedSendAsync({
|
|
id: '1',
|
|
jsonrpc: '2.0',
|
|
method: 'eth_chainId',
|
|
});
|
|
expect(chainIdResult).toBe(chainId);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('replaces the provider object underlying the provider proxy without creating a new instance of the proxy itself', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
eth_blockNumber: {
|
|
times: 2,
|
|
},
|
|
});
|
|
await controller.initializeProvider();
|
|
|
|
const { provider: providerBefore } =
|
|
controller.getProviderAndBlockTracker();
|
|
controller.resetConnection();
|
|
const { provider: providerAfter } =
|
|
controller.getProviderAndBlockTracker();
|
|
|
|
expect(providerBefore).toBe(providerAfter);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('emits networkDidChange', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls();
|
|
|
|
const networkDidChange = await waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.NetworkDidChange,
|
|
operation: () => {
|
|
controller.resetConnection();
|
|
},
|
|
});
|
|
|
|
expect(networkDidChange).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('emits infuraIsBlocked or infuraIsUnblocked, depending on whether Infura is blocking requests', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
eth_getBlockByNumber: {
|
|
response: BLOCKED_INFURA_RESPONSE,
|
|
},
|
|
});
|
|
const promiseForNoInfuraIsUnblockedEvents =
|
|
waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsUnblocked,
|
|
count: 0,
|
|
});
|
|
const promiseForInfuraIsBlocked = waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsBlocked,
|
|
});
|
|
|
|
controller.resetConnection();
|
|
|
|
expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy();
|
|
expect(await promiseForInfuraIsBlocked).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('checks the status of the network again, updating state appropriately', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls();
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: () => {
|
|
controller.resetConnection();
|
|
},
|
|
});
|
|
|
|
expect(controller.store.getState().networkStatus).toBe(
|
|
'available',
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('checks whether the network supports EIP-1559 again, updating state appropriately', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
latestBlock: POST_1559_BLOCK,
|
|
});
|
|
|
|
expect(controller.store.getState().networkDetails).toStrictEqual({
|
|
EIPS: {
|
|
1559: undefined,
|
|
},
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkDetails'],
|
|
operation: () => {
|
|
controller.resetConnection();
|
|
},
|
|
});
|
|
|
|
expect(controller.store.getState().networkDetails).toStrictEqual({
|
|
EIPS: {
|
|
1559: true,
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
describe(`when the type in the provider configuration is "rpc"`, () => {
|
|
it('emits networkWillChange', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls();
|
|
|
|
const networkWillChange = await waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.NetworkWillChange,
|
|
operation: () => {
|
|
controller.resetConnection();
|
|
},
|
|
});
|
|
|
|
expect(networkWillChange).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('resets the network status to "unknown" before emitting networkDidChange', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
net_version: {
|
|
response: SUCCESSFUL_NET_VERSION_RESPONSE,
|
|
times: 2,
|
|
},
|
|
eth_blockNumber: {
|
|
times: 2,
|
|
},
|
|
});
|
|
|
|
await controller.initializeProvider();
|
|
|
|
expect(controller.store.getState().networkStatus).toBe('available');
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
// We only care about the first state change, because it happens
|
|
// before networkDidChange
|
|
count: 1,
|
|
operation: () => {
|
|
controller.resetConnection();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe('unknown');
|
|
},
|
|
);
|
|
});
|
|
|
|
it('clears EIP-1559 support for the network from state before emitting networkDidChange', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
latestBlock: POST_1559_BLOCK,
|
|
eth_blockNumber: {
|
|
times: 2,
|
|
},
|
|
});
|
|
|
|
await controller.initializeProvider();
|
|
expect(controller.store.getState().networkDetails).toStrictEqual({
|
|
EIPS: {
|
|
1559: true,
|
|
},
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkDetails'],
|
|
// We only care about the first state change, because it happens
|
|
// before networkDidChange
|
|
count: 1,
|
|
operation: () => {
|
|
controller.resetConnection();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkDetails).toStrictEqual({
|
|
EIPS: {
|
|
1559: undefined,
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('initializes a new provider object pointed to the same RPC URL as the current network and using the same chain ID', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
ticker: 'TEST',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls();
|
|
|
|
controller.resetConnection();
|
|
|
|
const { provider } = controller.getProviderAndBlockTracker();
|
|
assert(provider, 'Provider is somehow unset');
|
|
const promisifiedSendAsync = promisify(provider.sendAsync).bind(
|
|
provider,
|
|
);
|
|
const { result: chainIdResult } = await promisifiedSendAsync({
|
|
id: '1',
|
|
jsonrpc: '2.0',
|
|
method: 'eth_chainId',
|
|
});
|
|
expect(chainIdResult).toBe('0x1337');
|
|
},
|
|
);
|
|
});
|
|
|
|
it('replaces the provider object underlying the provider proxy without creating a new instance of the proxy itself', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
eth_blockNumber: {
|
|
times: 2,
|
|
},
|
|
});
|
|
await controller.initializeProvider();
|
|
|
|
const { provider: providerBefore } =
|
|
controller.getProviderAndBlockTracker();
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: () => {
|
|
controller.resetConnection();
|
|
},
|
|
});
|
|
const { provider: providerAfter } =
|
|
controller.getProviderAndBlockTracker();
|
|
|
|
expect(providerBefore).toBe(providerAfter);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('emits networkDidChange', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls();
|
|
|
|
const networkDidChange = await waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.NetworkDidChange,
|
|
operation: () => {
|
|
controller.resetConnection();
|
|
},
|
|
});
|
|
|
|
expect(networkDidChange).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('emits infuraIsUnblocked', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls();
|
|
|
|
const infuraIsUnblocked = await waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsUnblocked,
|
|
operation: () => {
|
|
controller.resetConnection();
|
|
},
|
|
});
|
|
|
|
expect(infuraIsUnblocked).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('checks the status of the network again, updating state appropriately', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
net_version: {
|
|
response: SUCCESSFUL_NET_VERSION_RESPONSE,
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe('unknown');
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: () => {
|
|
controller.resetConnection();
|
|
},
|
|
});
|
|
|
|
expect(controller.store.getState().networkStatus).toBe('available');
|
|
},
|
|
);
|
|
});
|
|
|
|
it('ensures that EIP-1559 support for the current network is up to date', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network }) => {
|
|
network.mockEssentialRpcCalls({
|
|
latestBlock: POST_1559_BLOCK,
|
|
});
|
|
|
|
expect(controller.store.getState().networkDetails).toStrictEqual({
|
|
EIPS: {
|
|
1559: undefined,
|
|
},
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkDetails'],
|
|
operation: () => {
|
|
controller.resetConnection();
|
|
},
|
|
});
|
|
|
|
expect(controller.store.getState().networkDetails).toStrictEqual({
|
|
EIPS: {
|
|
1559: true,
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('rollbackToPreviousProvider', () => {
|
|
for (const { networkType, chainId } of INFURA_NETWORKS) {
|
|
describe(`if the previous provider configuration had a type of "${networkType}"`, () => {
|
|
it('overwrites the the current provider configuration with the previous provider configuration', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
rpcUrl: 'https://mock-rpc-url-1',
|
|
chainId: '0x111',
|
|
nickname: 'network 1',
|
|
ticker: 'TEST1',
|
|
rpcPrefs: {
|
|
blockExplorerUrl: 'https://test-block-explorer-1.com',
|
|
},
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId1: {
|
|
rpcUrl: 'https://mock-rpc-url-1',
|
|
chainId: '0x111',
|
|
nickname: 'network 1',
|
|
ticker: 'TEST1',
|
|
rpcPrefs: {
|
|
blockExplorerUrl: 'https://test-block-explorer-1.com',
|
|
},
|
|
id: 'testNetworkConfigurationId1',
|
|
},
|
|
testNetworkConfigurationId2: {
|
|
rpcUrl: 'https://mock-rpc-url-2',
|
|
chainId: '0x222',
|
|
nickname: 'network 2',
|
|
ticker: 'TEST2',
|
|
rpcPrefs: {
|
|
blockExplorerUrl: 'https://test-block-explorer-2.com',
|
|
},
|
|
id: 'testNetworkConfigurationId2',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: previousNetwork }) => {
|
|
const currentNetwork = new CustomNetworkCommunications({
|
|
customRpcUrl: 'https://mock-rpc-url-2',
|
|
});
|
|
currentNetwork.mockEssentialRpcCalls();
|
|
previousNetwork.mockEssentialRpcCalls();
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: () => {
|
|
controller.setActiveNetwork('testNetworkConfigurationId2');
|
|
},
|
|
});
|
|
expect(controller.store.getState().provider).toStrictEqual({
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url-2',
|
|
chainId: '0x222',
|
|
nickname: 'network 2',
|
|
ticker: 'TEST2',
|
|
rpcPrefs: {
|
|
blockExplorerUrl: 'https://test-block-explorer-2.com',
|
|
},
|
|
id: 'testNetworkConfigurationId2',
|
|
});
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.rollbackToPreviousProvider();
|
|
},
|
|
});
|
|
expect(controller.store.getState().provider).toStrictEqual({
|
|
type: networkType,
|
|
rpcUrl: 'https://mock-rpc-url-1',
|
|
chainId: '0x111',
|
|
nickname: 'network 1',
|
|
ticker: 'TEST1',
|
|
rpcPrefs: {
|
|
blockExplorerUrl: 'https://test-block-explorer-1.com',
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('emits networkWillChange', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
networkDetails: {
|
|
EIPS: {
|
|
1559: false,
|
|
},
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
id: 'testNetworkConfigurationId',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: previousNetwork }) => {
|
|
const currentNetwork = new CustomNetworkCommunications({
|
|
customRpcUrl: 'https://mock-rpc-url',
|
|
});
|
|
currentNetwork.mockEssentialRpcCalls();
|
|
previousNetwork.mockEssentialRpcCalls();
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: () => {
|
|
controller.setActiveNetwork('testNetworkConfigurationId');
|
|
},
|
|
});
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: async () => {
|
|
const networkWillChange = await waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.NetworkWillChange,
|
|
operation: async () => {
|
|
await controller.rollbackToPreviousProvider();
|
|
},
|
|
});
|
|
|
|
expect(networkWillChange).toBeTruthy();
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('resets the network status to "unknown" before emitting networkDidChange', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
networkDetails: {
|
|
EIPS: {
|
|
1559: false,
|
|
},
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
id: 'testNetworkConfigurationId',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: previousNetwork }) => {
|
|
const currentNetwork = new CustomNetworkCommunications({
|
|
customRpcUrl: 'https://mock-rpc-url',
|
|
});
|
|
currentNetwork.mockEssentialRpcCalls();
|
|
previousNetwork.mockEssentialRpcCalls();
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: () => {
|
|
controller.setActiveNetwork('testNetworkConfigurationId');
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe(
|
|
'available',
|
|
);
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: async () => {
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
// 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();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe(
|
|
'unknown',
|
|
);
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('clears EIP-1559 support for the network from state before emitting networkDidChange', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
networkDetails: {
|
|
EIPS: {
|
|
1559: false,
|
|
},
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
id: 'testNetworkConfigurationId',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: previousNetwork }) => {
|
|
const currentNetwork = new CustomNetworkCommunications({
|
|
customRpcUrl: 'https://mock-rpc-url',
|
|
});
|
|
currentNetwork.mockEssentialRpcCalls({
|
|
latestBlock: POST_1559_BLOCK,
|
|
});
|
|
previousNetwork.mockEssentialRpcCalls();
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: () => {
|
|
controller.setActiveNetwork('testNetworkConfigurationId');
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkDetails).toStrictEqual({
|
|
EIPS: {
|
|
1559: true,
|
|
},
|
|
});
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: async () => {
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkDetails'],
|
|
// 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();
|
|
},
|
|
});
|
|
expect(
|
|
controller.store.getState().networkDetails,
|
|
).toStrictEqual({
|
|
EIPS: {
|
|
1559: undefined,
|
|
},
|
|
});
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it(`initializes a provider pointed to the "${networkType}" Infura network (chainId: ${chainId})`, async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
networkDetails: {
|
|
EIPS: {
|
|
1559: false,
|
|
},
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
id: 'testNetworkConfigurationId',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: previousNetwork }) => {
|
|
const currentNetwork = new CustomNetworkCommunications({
|
|
customRpcUrl: 'https://mock-rpc-url',
|
|
});
|
|
currentNetwork.mockEssentialRpcCalls();
|
|
previousNetwork.mockEssentialRpcCalls();
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: () => {
|
|
controller.setActiveNetwork('testNetworkConfigurationId');
|
|
},
|
|
});
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.rollbackToPreviousProvider();
|
|
},
|
|
});
|
|
|
|
const { provider } = controller.getProviderAndBlockTracker();
|
|
assert(provider, 'Provider is somehow unset');
|
|
const promisifiedSendAsync = promisify(provider.sendAsync).bind(
|
|
provider,
|
|
);
|
|
const { result: chainIdResult } = await promisifiedSendAsync({
|
|
id: '1',
|
|
jsonrpc: '2.0',
|
|
method: 'eth_chainId',
|
|
});
|
|
expect(chainIdResult).toBe(chainId);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('replaces the provider object underlying the provider proxy without creating a new instance of the proxy itself', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
networkDetails: {
|
|
EIPS: {
|
|
1559: false,
|
|
},
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
id: 'testNetworkConfigurationId',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: previousNetwork }) => {
|
|
const currentNetwork = new CustomNetworkCommunications({
|
|
customRpcUrl: 'https://mock-rpc-url',
|
|
});
|
|
currentNetwork.mockEssentialRpcCalls();
|
|
previousNetwork.mockEssentialRpcCalls();
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: () => {
|
|
controller.setActiveNetwork('testNetworkConfigurationId');
|
|
},
|
|
});
|
|
|
|
const { provider: providerBefore } =
|
|
controller.getProviderAndBlockTracker();
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.rollbackToPreviousProvider();
|
|
},
|
|
});
|
|
const { provider: providerAfter } =
|
|
controller.getProviderAndBlockTracker();
|
|
|
|
expect(providerBefore).toBe(providerAfter);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('emits networkDidChange', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
networkDetails: {
|
|
EIPS: {
|
|
1559: false,
|
|
},
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
id: 'testNetworkConfigurationId',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: previousNetwork }) => {
|
|
const currentNetwork = new CustomNetworkCommunications({
|
|
customRpcUrl: 'https://mock-rpc-url',
|
|
});
|
|
currentNetwork.mockEssentialRpcCalls();
|
|
previousNetwork.mockEssentialRpcCalls();
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: () => {
|
|
controller.setActiveNetwork('testNetworkConfigurationId');
|
|
},
|
|
});
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: async () => {
|
|
const networkDidChange = await waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.NetworkDidChange,
|
|
operation: async () => {
|
|
await controller.rollbackToPreviousProvider();
|
|
},
|
|
});
|
|
expect(networkDidChange).toBeTruthy();
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('emits infuraIsBlocked or infuraIsUnblocked, depending on whether Infura is blocking requests for the previous network', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
id: 'testNetworkConfigurationId',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: previousNetwork }) => {
|
|
const currentNetwork = new CustomNetworkCommunications({
|
|
customRpcUrl: 'https://mock-rpc-url',
|
|
});
|
|
currentNetwork.mockEssentialRpcCalls();
|
|
previousNetwork.mockEssentialRpcCalls({
|
|
eth_getBlockByNumber: {
|
|
response: BLOCKED_INFURA_RESPONSE,
|
|
},
|
|
});
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: () => {
|
|
controller.setActiveNetwork('testNetworkConfigurationId');
|
|
},
|
|
});
|
|
const promiseForNoInfuraIsUnblockedEvents =
|
|
waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsUnblocked,
|
|
count: 0,
|
|
});
|
|
const promiseForInfuraIsBlocked = waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsBlocked,
|
|
});
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.rollbackToPreviousProvider();
|
|
},
|
|
});
|
|
|
|
expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy();
|
|
expect(await promiseForInfuraIsBlocked).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('checks the status of the previous network again and updates state accordingly', async () => {
|
|
const previousProvider = {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999' as const,
|
|
};
|
|
const currentNetworkConfiguration = {
|
|
id: 'currentNetworkConfiguration',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337' as const,
|
|
ticker: 'TEST',
|
|
};
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: previousProvider,
|
|
networkConfigurations: {
|
|
currentNetworkConfiguration,
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: previousNetwork }) => {
|
|
const currentNetwork = new CustomNetworkCommunications({
|
|
customRpcUrl: 'https://mock-rpc-url',
|
|
});
|
|
currentNetwork.mockEssentialRpcCalls({
|
|
net_version: {
|
|
response: {
|
|
error: 'some error',
|
|
httpStatus: 405,
|
|
},
|
|
},
|
|
});
|
|
previousNetwork.mockEssentialRpcCalls({
|
|
eth_getBlockByNumber: {
|
|
response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE,
|
|
},
|
|
});
|
|
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
operation: () => {
|
|
controller.setActiveNetwork('currentNetworkConfiguration');
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe(
|
|
'unavailable',
|
|
);
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.rollbackToPreviousProvider();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe(
|
|
'available',
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('checks whether the previous network supports EIP-1559 again and updates state accordingly', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: networkType,
|
|
// NOTE: This doesn't need to match the logical chain ID of
|
|
// the network selected, it just needs to exist
|
|
chainId: '0x9999999',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
id: 'testNetworkConfigurationId',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: previousNetwork }) => {
|
|
const currentNetwork = new CustomNetworkCommunications({
|
|
customRpcUrl: 'https://mock-rpc-url',
|
|
});
|
|
currentNetwork.mockEssentialRpcCalls({
|
|
latestBlock: PRE_1559_BLOCK,
|
|
});
|
|
previousNetwork.mockEssentialRpcCalls({
|
|
latestBlock: POST_1559_BLOCK,
|
|
});
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: () => {
|
|
controller.setActiveNetwork('testNetworkConfigurationId');
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkDetails).toStrictEqual({
|
|
EIPS: {
|
|
1559: false,
|
|
},
|
|
});
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
numberOfNetworkDetailsChanges: 2,
|
|
operation: async () => {
|
|
await controller.rollbackToPreviousProvider();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkDetails).toStrictEqual({
|
|
EIPS: {
|
|
1559: true,
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
describe(`if the previous provider configuration had a type of "rpc"`, () => {
|
|
it('overwrites the the current provider configuration with the previous provider configuration', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url-2',
|
|
chainId: '0x1337',
|
|
nickname: 'test-chain-2',
|
|
ticker: 'TEST2',
|
|
rpcPrefs: {
|
|
blockExplorerUrl: 'test-block-explorer-2.com',
|
|
},
|
|
},
|
|
networkDetails: {
|
|
EIPS: {
|
|
1559: false,
|
|
},
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId1: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
nickname: 'test-chain',
|
|
ticker: 'TEST',
|
|
rpcPrefs: {
|
|
blockExplorerUrl: 'test-block-explorer.com',
|
|
},
|
|
id: 'testNetworkConfigurationId1',
|
|
},
|
|
testNetworkConfigurationId2: {
|
|
rpcUrl: 'https://mock-rpc-url-2',
|
|
chainId: '0x1337',
|
|
nickname: 'test-chain-2',
|
|
ticker: 'TEST2',
|
|
rpcPrefs: {
|
|
blockExplorerUrl: 'test-block-explorer-2.com',
|
|
},
|
|
id: 'testNetworkConfigurationId2',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: previousNetwork }) => {
|
|
const currentNetwork = new InfuraNetworkCommunications({
|
|
infuraNetwork: 'goerli',
|
|
});
|
|
currentNetwork.mockEssentialRpcCalls();
|
|
previousNetwork.mockEssentialRpcCalls();
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: () => {
|
|
controller.setProviderType('goerli');
|
|
},
|
|
});
|
|
expect(controller.store.getState().provider).toStrictEqual({
|
|
type: 'goerli',
|
|
rpcUrl: '',
|
|
chainId: '0x5',
|
|
ticker: 'GoerliETH',
|
|
nickname: '',
|
|
rpcPrefs: {
|
|
blockExplorerUrl: 'https://goerli.etherscan.io',
|
|
},
|
|
});
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.rollbackToPreviousProvider();
|
|
},
|
|
});
|
|
expect(controller.store.getState().provider).toStrictEqual({
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url-2',
|
|
chainId: '0x1337',
|
|
nickname: 'test-chain-2',
|
|
ticker: 'TEST2',
|
|
rpcPrefs: {
|
|
blockExplorerUrl: 'test-block-explorer-2.com',
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('emits networkWillChange', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url-2',
|
|
chainId: '0x1337',
|
|
ticker: 'TEST2',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId1: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId1',
|
|
},
|
|
testNetworkConfigurationId2: {
|
|
rpcUrl: 'https://mock-rpc-url-2',
|
|
chainId: '0x1337',
|
|
ticker: 'TEST2',
|
|
id: 'testNetworkConfigurationId2',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: previousNetwork }) => {
|
|
const currentNetwork = new InfuraNetworkCommunications({
|
|
infuraNetwork: 'goerli',
|
|
});
|
|
currentNetwork.mockEssentialRpcCalls();
|
|
previousNetwork.mockEssentialRpcCalls();
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: () => {
|
|
controller.setProviderType('goerli');
|
|
},
|
|
});
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: async () => {
|
|
const networkWillChange = await waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.NetworkWillChange,
|
|
operation: async () => {
|
|
await controller.rollbackToPreviousProvider();
|
|
},
|
|
});
|
|
|
|
expect(networkWillChange).toBeTruthy();
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('resets the network state to "unknown" before emitting networkDidChange', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: previousNetwork }) => {
|
|
const currentNetwork = new InfuraNetworkCommunications({
|
|
infuraNetwork: 'goerli',
|
|
});
|
|
currentNetwork.mockEssentialRpcCalls();
|
|
previousNetwork.mockEssentialRpcCalls();
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: () => {
|
|
controller.setProviderType('goerli');
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe('available');
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: async () => {
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkStatus'],
|
|
// 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();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe(
|
|
'unknown',
|
|
);
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('clears EIP-1559 support for the network from state before emitting networkDidChange', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: previousNetwork }) => {
|
|
const currentNetwork = new InfuraNetworkCommunications({
|
|
infuraNetwork: 'goerli',
|
|
});
|
|
currentNetwork.mockEssentialRpcCalls({
|
|
latestBlock: POST_1559_BLOCK,
|
|
});
|
|
previousNetwork.mockEssentialRpcCalls();
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: () => {
|
|
controller.setProviderType('goerli');
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkDetails).toStrictEqual({
|
|
EIPS: {
|
|
1559: true,
|
|
},
|
|
});
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: async () => {
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkDetails'],
|
|
// 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();
|
|
},
|
|
});
|
|
expect(
|
|
controller.store.getState().networkDetails,
|
|
).toStrictEqual({
|
|
EIPS: {
|
|
1559: undefined,
|
|
},
|
|
});
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('initializes a provider pointed to the given RPC URL whose chain ID matches the previously configured chain ID', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
ticker: 'TEST',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0x1337',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: previousNetwork }) => {
|
|
const currentNetwork = new InfuraNetworkCommunications({
|
|
infuraNetwork: 'goerli',
|
|
});
|
|
currentNetwork.mockEssentialRpcCalls();
|
|
previousNetwork.mockEssentialRpcCalls();
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: () => {
|
|
controller.setProviderType('goerli');
|
|
},
|
|
});
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.rollbackToPreviousProvider();
|
|
},
|
|
});
|
|
|
|
const { provider } = controller.getProviderAndBlockTracker();
|
|
assert(provider, 'Provider is somehow unset');
|
|
const promisifiedSendAsync = promisify(provider.sendAsync).bind(
|
|
provider,
|
|
);
|
|
const { result: chainIdResult } = await promisifiedSendAsync({
|
|
id: '1',
|
|
jsonrpc: '2.0',
|
|
method: 'eth_chainId',
|
|
});
|
|
expect(chainIdResult).toBe('0x1337');
|
|
},
|
|
);
|
|
});
|
|
|
|
it('replaces the provider object underlying the provider proxy without creating a new instance of the proxy itself', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: previousNetwork }) => {
|
|
const currentNetwork = new InfuraNetworkCommunications({
|
|
infuraNetwork: 'goerli',
|
|
});
|
|
currentNetwork.mockEssentialRpcCalls();
|
|
previousNetwork.mockEssentialRpcCalls();
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: () => {
|
|
controller.setProviderType('goerli');
|
|
},
|
|
});
|
|
|
|
const { provider: providerBefore } =
|
|
controller.getProviderAndBlockTracker();
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.rollbackToPreviousProvider();
|
|
},
|
|
});
|
|
const { provider: providerAfter } =
|
|
controller.getProviderAndBlockTracker();
|
|
|
|
expect(providerBefore).toBe(providerAfter);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('emits networkDidChange', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: previousNetwork }) => {
|
|
const currentNetwork = new InfuraNetworkCommunications({
|
|
infuraNetwork: 'goerli',
|
|
});
|
|
currentNetwork.mockEssentialRpcCalls();
|
|
previousNetwork.mockEssentialRpcCalls();
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: () => {
|
|
controller.setProviderType('goerli');
|
|
},
|
|
});
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: async () => {
|
|
const networkDidChange = await waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.NetworkDidChange,
|
|
operation: async () => {
|
|
await controller.rollbackToPreviousProvider();
|
|
},
|
|
});
|
|
expect(networkDidChange).toBeTruthy();
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('emits infuraIsUnblocked', async () => {
|
|
const { unrestrictedMessenger, restrictedMessenger } =
|
|
buildMessengerGroup();
|
|
|
|
await withController(
|
|
{
|
|
messenger: restrictedMessenger,
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: previousNetwork }) => {
|
|
const currentNetwork = new InfuraNetworkCommunications({
|
|
infuraNetwork: 'goerli',
|
|
});
|
|
currentNetwork.mockEssentialRpcCalls();
|
|
previousNetwork.mockEssentialRpcCalls();
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: () => {
|
|
controller.setProviderType('goerli');
|
|
},
|
|
});
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: async () => {
|
|
const infuraIsUnblocked = await waitForPublishedEvents({
|
|
messenger: unrestrictedMessenger,
|
|
eventType: NetworkControllerEventType.InfuraIsUnblocked,
|
|
operation: async () => {
|
|
await controller.rollbackToPreviousProvider();
|
|
},
|
|
});
|
|
|
|
expect(infuraIsUnblocked).toBeTruthy();
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('checks the status of the previous network again and updates state accordingly', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: previousNetwork }) => {
|
|
const currentNetwork = new InfuraNetworkCommunications({
|
|
infuraNetwork: 'goerli',
|
|
});
|
|
currentNetwork.mockEssentialRpcCalls({
|
|
// This results in a successful call to eth_getBlockByNumber
|
|
// implicitly
|
|
latestBlock: BLOCK,
|
|
});
|
|
previousNetwork.mockEssentialRpcCalls({
|
|
net_version: {
|
|
response: UNSUCCESSFUL_JSON_RPC_RESPONSE,
|
|
},
|
|
});
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: () => {
|
|
controller.setProviderType('goerli');
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe('available');
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.rollbackToPreviousProvider();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkStatus).toBe('unknown');
|
|
},
|
|
);
|
|
});
|
|
|
|
it('checks whether the previous network supports EIP-1559 again and updates state accordingly', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller, network: previousNetwork }) => {
|
|
const currentNetwork = new InfuraNetworkCommunications({
|
|
infuraNetwork: 'goerli',
|
|
});
|
|
currentNetwork.mockEssentialRpcCalls({
|
|
latestBlock: PRE_1559_BLOCK,
|
|
});
|
|
previousNetwork.mockEssentialRpcCalls({
|
|
latestBlock: POST_1559_BLOCK,
|
|
});
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: () => {
|
|
controller.setProviderType('goerli');
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkDetails).toStrictEqual({
|
|
EIPS: {
|
|
1559: false,
|
|
},
|
|
});
|
|
|
|
await waitForLookupNetworkToComplete({
|
|
controller,
|
|
operation: async () => {
|
|
await controller.rollbackToPreviousProvider();
|
|
},
|
|
});
|
|
expect(controller.store.getState().networkDetails).toStrictEqual({
|
|
EIPS: {
|
|
1559: true,
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('upsertNetworkConfiguration', () => {
|
|
it('throws if the given chain ID is not a 0x-prefixed hex number', async () => {
|
|
const invalidChainId = '1';
|
|
await withController(async ({ controller }) => {
|
|
expect(() =>
|
|
controller.upsertNetworkConfiguration(
|
|
{
|
|
/* @ts-expect-error We are intentionally passing bad input. */
|
|
chainId: invalidChainId,
|
|
nickname: 'RPC',
|
|
rpcPrefs: { blockExplorerUrl: 'test-block-explorer.com' },
|
|
rpcUrl: 'rpc_url',
|
|
ticker: 'RPC',
|
|
},
|
|
{
|
|
referrer: 'https://test-dapp.com',
|
|
source: MetaMetricsNetworkEventSource.Dapp,
|
|
},
|
|
),
|
|
).toThrow(
|
|
new Error(
|
|
`Invalid chain ID "${invalidChainId}": invalid hex string.`,
|
|
),
|
|
);
|
|
});
|
|
});
|
|
|
|
it('throws if the given chain ID is greater than the maximum allowed ID', async () => {
|
|
await withController(async ({ controller }) => {
|
|
expect(() =>
|
|
controller.upsertNetworkConfiguration(
|
|
{
|
|
chainId: '0xFFFFFFFFFFFFFFFF',
|
|
nickname: 'RPC',
|
|
rpcPrefs: { blockExplorerUrl: 'test-block-explorer.com' },
|
|
rpcUrl: 'rpc_url',
|
|
ticker: 'RPC',
|
|
},
|
|
{
|
|
referrer: 'https://test-dapp.com',
|
|
source: MetaMetricsNetworkEventSource.Dapp,
|
|
},
|
|
),
|
|
).toThrow(
|
|
new Error(
|
|
'Invalid chain ID "0xFFFFFFFFFFFFFFFF": numerical value greater than max safe value.',
|
|
),
|
|
);
|
|
});
|
|
});
|
|
|
|
it('throws if the no (or a falsy) rpcUrl is passed', async () => {
|
|
await withController(async ({ controller }) => {
|
|
expect(() =>
|
|
controller.upsertNetworkConfiguration(
|
|
/* @ts-expect-error We are intentionally passing bad input. */
|
|
{
|
|
chainId: '0x9999',
|
|
nickname: 'RPC',
|
|
rpcPrefs: { blockExplorerUrl: 'test-block-explorer.com' },
|
|
ticker: 'RPC',
|
|
},
|
|
{
|
|
referrer: 'https://test-dapp.com',
|
|
source: MetaMetricsNetworkEventSource.Dapp,
|
|
},
|
|
),
|
|
).toThrow(
|
|
new Error(
|
|
'An rpcUrl is required to add or update network configuration',
|
|
),
|
|
);
|
|
});
|
|
});
|
|
|
|
it('throws if rpcUrl passed is not a valid Url', async () => {
|
|
await withController(async ({ controller }) => {
|
|
expect(() =>
|
|
controller.upsertNetworkConfiguration(
|
|
{
|
|
chainId: '0x9999',
|
|
nickname: 'RPC',
|
|
rpcPrefs: { blockExplorerUrl: 'test-block-explorer.com' },
|
|
ticker: 'RPC',
|
|
rpcUrl: 'test',
|
|
},
|
|
{
|
|
referrer: 'https://test-dapp.com',
|
|
source: MetaMetricsNetworkEventSource.Dapp,
|
|
},
|
|
),
|
|
).toThrow(new Error('rpcUrl must be a valid URL'));
|
|
});
|
|
});
|
|
|
|
it('throws if the no (or a falsy) ticker is passed', async () => {
|
|
await withController(async ({ controller }) => {
|
|
expect(() =>
|
|
controller.upsertNetworkConfiguration(
|
|
/* @ts-expect-error We are intentionally passing bad input. */
|
|
{
|
|
chainId: '0x5',
|
|
nickname: 'RPC',
|
|
rpcPrefs: { blockExplorerUrl: 'test-block-explorer.com' },
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
},
|
|
{
|
|
referrer: 'https://test-dapp.com',
|
|
source: MetaMetricsNetworkEventSource.Dapp,
|
|
},
|
|
),
|
|
).toThrow(
|
|
new Error(
|
|
'A ticker is required to add or update networkConfiguration',
|
|
),
|
|
);
|
|
});
|
|
});
|
|
|
|
it('throws if an options object is not passed as a second argument', async () => {
|
|
await withController(async ({ controller }) => {
|
|
expect(() =>
|
|
/* @ts-expect-error We are intentionally passing bad input. */
|
|
controller.upsertNetworkConfiguration({
|
|
chainId: '0x5',
|
|
nickname: 'RPC',
|
|
rpcPrefs: { blockExplorerUrl: 'test-block-explorer.com' },
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
}),
|
|
).toThrow(
|
|
new Error(
|
|
"Cannot read properties of undefined (reading 'setActive')",
|
|
),
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should add the given network if all required properties are present but nither rpcPrefs nor nickname properties are passed', async () => {
|
|
uuidV4Mock.mockImplementationOnce(() => 'networkConfigurationId');
|
|
await withController(
|
|
{
|
|
state: {
|
|
networkConfigurations: {},
|
|
},
|
|
},
|
|
async ({ controller }) => {
|
|
const rpcUrlNetwork = {
|
|
chainId: '0x1' as const,
|
|
rpcUrl: 'https://test-rpc-url',
|
|
ticker: 'test_ticker',
|
|
};
|
|
|
|
controller.upsertNetworkConfiguration(rpcUrlNetwork, {
|
|
referrer: 'https://test-dapp.com',
|
|
source: MetaMetricsNetworkEventSource.Dapp,
|
|
});
|
|
|
|
expect(
|
|
Object.values(controller.store.getState().networkConfigurations),
|
|
).toStrictEqual(
|
|
expect.arrayContaining([
|
|
{
|
|
...rpcUrlNetwork,
|
|
nickname: undefined,
|
|
rpcPrefs: undefined,
|
|
id: 'networkConfigurationId',
|
|
},
|
|
]),
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('adds new networkConfiguration to networkController store, but only adds valid properties (rpcUrl, chainId, ticker, nickname, rpcPrefs) and fills any missing properties from this list as undefined', async function () {
|
|
uuidV4Mock.mockImplementationOnce(() => 'networkConfigurationId');
|
|
await withController(
|
|
{
|
|
state: {
|
|
networkConfigurations: {},
|
|
},
|
|
},
|
|
async ({ controller }) => {
|
|
const rpcUrlNetwork = {
|
|
chainId: '0x1' as const,
|
|
rpcUrl: 'https://test-rpc-url',
|
|
ticker: 'test_ticker',
|
|
invalidKey: 'new-chain',
|
|
invalidKey2: {},
|
|
};
|
|
|
|
controller.upsertNetworkConfiguration(rpcUrlNetwork, {
|
|
referrer: 'https://test-dapp.com',
|
|
source: MetaMetricsNetworkEventSource.Dapp,
|
|
});
|
|
|
|
expect(
|
|
Object.values(controller.store.getState().networkConfigurations),
|
|
).toStrictEqual(
|
|
expect.arrayContaining([
|
|
{
|
|
chainId: '0x1',
|
|
rpcUrl: 'https://test-rpc-url',
|
|
ticker: 'test_ticker',
|
|
nickname: undefined,
|
|
rpcPrefs: undefined,
|
|
id: 'networkConfigurationId',
|
|
},
|
|
]),
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('should add the given network configuration if its rpcURL does not match an existing configuration without changing or overwriting other configurations', async () => {
|
|
uuidV4Mock.mockImplementationOnce(() => 'networkConfigurationId2');
|
|
await withController(
|
|
{
|
|
state: {
|
|
networkConfigurations: {
|
|
networkConfigurationId: {
|
|
rpcUrl: 'https://test-rpc-url',
|
|
ticker: 'ticker',
|
|
nickname: 'nickname',
|
|
rpcPrefs: { blockExplorerUrl: 'testchainscan.io' },
|
|
chainId: '0x1',
|
|
id: 'networkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller }) => {
|
|
const rpcUrlNetwork = {
|
|
chainId: '0x1' as const,
|
|
nickname: 'RPC',
|
|
rpcPrefs: undefined,
|
|
rpcUrl: 'https://test-rpc-url-2',
|
|
ticker: 'RPC',
|
|
};
|
|
|
|
controller.upsertNetworkConfiguration(rpcUrlNetwork, {
|
|
referrer: 'https://test-dapp.com',
|
|
source: MetaMetricsNetworkEventSource.Dapp,
|
|
});
|
|
|
|
expect(
|
|
Object.values(controller.store.getState().networkConfigurations),
|
|
).toStrictEqual(
|
|
expect.arrayContaining([
|
|
{
|
|
rpcUrl: 'https://test-rpc-url',
|
|
ticker: 'ticker',
|
|
nickname: 'nickname',
|
|
rpcPrefs: { blockExplorerUrl: 'testchainscan.io' },
|
|
chainId: '0x1',
|
|
id: 'networkConfigurationId',
|
|
},
|
|
{ ...rpcUrlNetwork, id: 'networkConfigurationId2' },
|
|
]),
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('should use the given configuration to update an existing network configuration that has a matching rpcUrl', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
networkConfigurations: {
|
|
networkConfigurationId: {
|
|
rpcUrl: 'https://test-rpc-url',
|
|
ticker: 'old_rpc_ticker',
|
|
nickname: 'old_rpc_chainName',
|
|
rpcPrefs: { blockExplorerUrl: 'testchainscan.io' },
|
|
chainId: '0x1',
|
|
id: 'networkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
|
|
async ({ controller }) => {
|
|
const updatedConfiguration = {
|
|
rpcUrl: 'https://test-rpc-url',
|
|
ticker: 'new_rpc_ticker',
|
|
nickname: 'new_rpc_chainName',
|
|
rpcPrefs: { blockExplorerUrl: 'alternativetestchainscan.io' },
|
|
chainId: '0x1' as const,
|
|
};
|
|
controller.upsertNetworkConfiguration(updatedConfiguration, {
|
|
referrer: 'https://test-dapp.com',
|
|
source: MetaMetricsNetworkEventSource.Dapp,
|
|
});
|
|
expect(
|
|
Object.values(controller.store.getState().networkConfigurations),
|
|
).toStrictEqual([
|
|
{
|
|
rpcUrl: 'https://test-rpc-url',
|
|
nickname: 'new_rpc_chainName',
|
|
ticker: 'new_rpc_ticker',
|
|
rpcPrefs: { blockExplorerUrl: 'alternativetestchainscan.io' },
|
|
chainId: '0x1',
|
|
id: 'networkConfigurationId',
|
|
},
|
|
]);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('should use the given configuration to update an existing network configuration that has a matching rpcUrl without changing or overwriting other networkConfigurations', async () => {
|
|
await withController(
|
|
{
|
|
state: {
|
|
networkConfigurations: {
|
|
networkConfigurationId: {
|
|
rpcUrl: 'https://test-rpc-url',
|
|
ticker: 'ticker',
|
|
nickname: 'nickname',
|
|
rpcPrefs: { blockExplorerUrl: 'testchainscan.io' },
|
|
chainId: '0x1',
|
|
id: 'networkConfigurationId',
|
|
},
|
|
networkConfigurationId2: {
|
|
rpcUrl: 'https://test-rpc-url-2',
|
|
ticker: 'ticker-2',
|
|
nickname: 'nickname-2',
|
|
rpcPrefs: { blockExplorerUrl: 'testchainscan.io' },
|
|
chainId: '0x9999',
|
|
id: 'networkConfigurationId2',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller }) => {
|
|
controller.upsertNetworkConfiguration(
|
|
{
|
|
rpcUrl: 'https://test-rpc-url',
|
|
ticker: 'new-ticker',
|
|
nickname: 'new-nickname',
|
|
rpcPrefs: { blockExplorerUrl: 'alternativetestchainscan.io' },
|
|
chainId: '0x1',
|
|
},
|
|
{
|
|
referrer: 'https://test-dapp.com',
|
|
source: MetaMetricsNetworkEventSource.Dapp,
|
|
},
|
|
);
|
|
|
|
expect(
|
|
Object.values(controller.store.getState().networkConfigurations),
|
|
).toStrictEqual([
|
|
{
|
|
rpcUrl: 'https://test-rpc-url',
|
|
ticker: 'new-ticker',
|
|
nickname: 'new-nickname',
|
|
rpcPrefs: { blockExplorerUrl: 'alternativetestchainscan.io' },
|
|
chainId: '0x1',
|
|
id: 'networkConfigurationId',
|
|
},
|
|
{
|
|
rpcUrl: 'https://test-rpc-url-2',
|
|
ticker: 'ticker-2',
|
|
nickname: 'nickname-2',
|
|
rpcPrefs: { blockExplorerUrl: 'testchainscan.io' },
|
|
chainId: '0x9999',
|
|
id: 'networkConfigurationId2',
|
|
},
|
|
]);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('should add the given network and not set it to active if the setActive option is not passed (or a falsy value is passed)', async () => {
|
|
uuidV4Mock.mockImplementationOnce(() => 'networkConfigurationId');
|
|
const originalProvider = {
|
|
type: NETWORK_TYPES.RPC,
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest' as const,
|
|
ticker: 'TEST',
|
|
};
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: originalProvider,
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller }) => {
|
|
const rpcUrlNetwork = {
|
|
chainId: '0x1' as const,
|
|
rpcUrl: 'https://test-rpc-url',
|
|
ticker: 'test_ticker',
|
|
};
|
|
|
|
controller.upsertNetworkConfiguration(rpcUrlNetwork, {
|
|
referrer: 'https://test-dapp.com',
|
|
source: MetaMetricsNetworkEventSource.Dapp,
|
|
});
|
|
|
|
expect(controller.store.getState().provider).toStrictEqual(
|
|
originalProvider,
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('should add the given network and set it to active if the setActive option is passed as true', async () => {
|
|
uuidV4Mock.mockImplementationOnce(() => 'networkConfigurationId');
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller }) => {
|
|
const network = new CustomNetworkCommunications({
|
|
customRpcUrl: 'https://test-rpc-url',
|
|
});
|
|
network.mockEssentialRpcCalls();
|
|
const rpcUrlNetwork = {
|
|
chainId: '0x1' as const,
|
|
rpcUrl: 'https://test-rpc-url',
|
|
ticker: 'test_ticker',
|
|
};
|
|
|
|
controller.upsertNetworkConfiguration(rpcUrlNetwork, {
|
|
setActive: true,
|
|
referrer: 'https://test-dapp.com',
|
|
source: MetaMetricsNetworkEventSource.Dapp,
|
|
});
|
|
|
|
expect(controller.store.getState().provider).toStrictEqual({
|
|
...rpcUrlNetwork,
|
|
nickname: undefined,
|
|
rpcPrefs: undefined,
|
|
type: 'rpc',
|
|
id: 'networkConfigurationId',
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('adds new networkConfiguration to networkController store and calls to the metametrics event tracking with the correct values', async () => {
|
|
uuidV4Mock.mockImplementationOnce(() => 'networkConfigurationId');
|
|
const trackEventSpy = jest.fn();
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
trackMetaMetricsEvent: trackEventSpy,
|
|
},
|
|
async ({ controller }) => {
|
|
const newNetworkConfiguration = {
|
|
rpcUrl: 'https://new-chain-rpc-url',
|
|
chainId: '0x9999' as const,
|
|
ticker: 'NEW',
|
|
nickname: 'new-chain',
|
|
rpcPrefs: { blockExplorerUrl: 'https://block-explorer' },
|
|
};
|
|
|
|
controller.upsertNetworkConfiguration(newNetworkConfiguration, {
|
|
referrer: 'https://test-dapp.com',
|
|
source: MetaMetricsNetworkEventSource.Dapp,
|
|
});
|
|
|
|
expect(
|
|
Object.values(controller.store.getState().networkConfigurations),
|
|
).toStrictEqual([
|
|
{
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
{
|
|
...newNetworkConfiguration,
|
|
id: 'networkConfigurationId',
|
|
},
|
|
]);
|
|
expect(trackEventSpy).toHaveBeenCalledWith({
|
|
event: 'Custom Network Added',
|
|
category: 'Network',
|
|
referrer: {
|
|
url: 'https://test-dapp.com',
|
|
},
|
|
properties: {
|
|
chain_id: '0x9999',
|
|
symbol: 'NEW',
|
|
source: 'dapp',
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('throws if referrer and source arguments are not passed', async () => {
|
|
uuidV4Mock.mockImplementationOnce(() => 'networkConfigurationId');
|
|
const trackEventSpy = jest.fn();
|
|
await withController(
|
|
{
|
|
state: {
|
|
provider: {
|
|
type: 'rpc',
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
},
|
|
networkConfigurations: {
|
|
testNetworkConfigurationId: {
|
|
rpcUrl: 'https://mock-rpc-url',
|
|
chainId: '0xtest',
|
|
ticker: 'TEST',
|
|
id: 'testNetworkConfigurationId',
|
|
},
|
|
},
|
|
},
|
|
trackMetaMetricsEvent: trackEventSpy,
|
|
},
|
|
async ({ controller }) => {
|
|
const newNetworkConfiguration = {
|
|
rpcUrl: 'https://new-chain-rpc-url',
|
|
chainId: '0x9999' as const,
|
|
ticker: 'NEW',
|
|
nickname: 'new-chain',
|
|
rpcPrefs: { blockExplorerUrl: 'https://block-explorer' },
|
|
};
|
|
|
|
expect(() => {
|
|
/* @ts-expect-error We are intentionally passing bad input. */
|
|
controller.upsertNetworkConfiguration(newNetworkConfiguration, {});
|
|
}).toThrow(
|
|
'referrer and source are required arguments for adding or updating a network configuration',
|
|
);
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('removeNetworkConfigurations', () => {
|
|
it('should remove a network configuration', async () => {
|
|
const networkConfigurationId = 'networkConfigurationId';
|
|
await withController(
|
|
{
|
|
state: {
|
|
networkConfigurations: {
|
|
[networkConfigurationId]: {
|
|
id: 'aaaaaa',
|
|
rpcUrl: 'https://test-rpc-url',
|
|
ticker: 'old_rpc_ticker',
|
|
nickname: 'old_rpc_chainName',
|
|
rpcPrefs: { blockExplorerUrl: 'testchainscan.io' },
|
|
chainId: '0x1',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async ({ controller }) => {
|
|
expect(
|
|
Object.values(controller.store.getState().networkConfigurations),
|
|
).toStrictEqual([
|
|
{
|
|
id: 'aaaaaa',
|
|
rpcUrl: 'https://test-rpc-url',
|
|
ticker: 'old_rpc_ticker',
|
|
nickname: 'old_rpc_chainName',
|
|
rpcPrefs: { blockExplorerUrl: 'testchainscan.io' },
|
|
chainId: '0x1',
|
|
},
|
|
]);
|
|
controller.removeNetworkConfiguration(networkConfigurationId);
|
|
expect(
|
|
controller.store.getState().networkConfigurations,
|
|
).toStrictEqual({});
|
|
},
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Builds the set of controller messengers that recognizes the events that
|
|
* NetworkController emits: one designed to be used directly by
|
|
* NetworkController, and one designed to be used in tests.
|
|
*
|
|
* @returns The controller messenger.
|
|
*/
|
|
function buildMessengerGroup() {
|
|
const unrestrictedMessenger = new ControllerMessenger<
|
|
never,
|
|
NetworkControllerEvent
|
|
>();
|
|
const restrictedMessenger = unrestrictedMessenger.getRestricted<
|
|
'NetworkController',
|
|
never,
|
|
NetworkControllerEventType
|
|
>({
|
|
name: 'NetworkController',
|
|
allowedEvents: [
|
|
NetworkControllerEventType.NetworkDidChange,
|
|
NetworkControllerEventType.NetworkWillChange,
|
|
NetworkControllerEventType.InfuraIsBlocked,
|
|
NetworkControllerEventType.InfuraIsUnblocked,
|
|
],
|
|
});
|
|
return { unrestrictedMessenger, restrictedMessenger };
|
|
}
|
|
|
|
/**
|
|
* Despite the signature of its constructor, NetworkController must take an
|
|
* Infura project ID. The object that this function returns is mixed into the
|
|
* options first when a NetworkController is instantiated in tests.
|
|
*
|
|
* @returns The controller options.
|
|
*/
|
|
function buildDefaultNetworkControllerOptions() {
|
|
const { restrictedMessenger } = buildMessengerGroup();
|
|
return {
|
|
messenger: restrictedMessenger,
|
|
infuraProjectId: DEFAULT_INFURA_PROJECT_ID,
|
|
trackMetaMetricsEvent: jest.fn(),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* `withController` takes a callback as its last argument. It also takes an
|
|
* options bag, which may be specified before the callback. The callback itself
|
|
* is called with one of two variants of NetworkCommunications. If the options
|
|
* bag was specified and it is being used to configure an Infura provider based
|
|
* on the provider type, then the callback is called with an
|
|
* InfuraNetworkCommunications; otherwise it is called with a
|
|
* CustomNetworkCommunications.
|
|
*
|
|
* How do we test for the exact code path in `withController`? Because the type
|
|
* of the options bag is not a discriminated union, we can't "reach" into the
|
|
* bag and test for the provider type. Instead, we need to use a type guard.
|
|
* This is that type guard.
|
|
*
|
|
* @param args - The arguments to `withController`.
|
|
* @returns True if the arguments feature an options bag and this bag contains
|
|
* provider configuration for an Infura network.
|
|
*/
|
|
function hasOptionsWithInfuraProviderConfig<ReturnValue>(
|
|
args: WithControllerArgs<ReturnValue>,
|
|
): args is WithControllerArgsWithConfiguredInfuraProvider<ReturnValue> {
|
|
return (
|
|
args.length === 2 &&
|
|
args[0].state !== undefined &&
|
|
args[0].state.provider !== undefined &&
|
|
args[0].state.provider.type !== 'rpc'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Builds a controller based on the given options, and calls the given function
|
|
* with that controller.
|
|
*
|
|
* @param args - Either a function, or an options bag + a function. The options
|
|
* bag is the same that NetworkController takes; the function will be called
|
|
* with the built controller as well as an object that can be used to mock
|
|
* requests.
|
|
* @returns Whatever the callback returns.
|
|
*/
|
|
async function withController<ReturnValue>(
|
|
options: NetworkControllerOptionsWithInfuraProviderConfig,
|
|
callback: WithControllerCallback<InfuraNetworkCommunications, ReturnValue>,
|
|
): Promise<void>;
|
|
async function withController<ReturnValue>(
|
|
options: Partial<NetworkControllerOptions>,
|
|
callback: WithControllerCallback<CustomNetworkCommunications, ReturnValue>,
|
|
): Promise<void>;
|
|
async function withController<ReturnValue>(
|
|
callback: WithControllerCallback<CustomNetworkCommunications, ReturnValue>,
|
|
): Promise<void>;
|
|
async function withController<ReturnValue>(
|
|
...args: WithControllerArgs<ReturnValue>
|
|
) {
|
|
if (args.length === 2 && hasOptionsWithInfuraProviderConfig(args)) {
|
|
const [givenNetworkControllerOptions, callback] = args;
|
|
const constructorOptions = {
|
|
...buildDefaultNetworkControllerOptions(),
|
|
...givenNetworkControllerOptions,
|
|
};
|
|
const controller = new NetworkController(constructorOptions);
|
|
|
|
const providerType = givenNetworkControllerOptions.state.provider.type;
|
|
const network = new InfuraNetworkCommunications({
|
|
infuraProjectId: constructorOptions.infuraProjectId,
|
|
infuraNetwork: providerType,
|
|
});
|
|
|
|
try {
|
|
return await callback({ controller, network });
|
|
} finally {
|
|
await controller.destroy();
|
|
}
|
|
} else {
|
|
const [givenNetworkControllerOptions, callback] =
|
|
args.length === 2 ? args : [{}, args[0]];
|
|
const constructorOptions = {
|
|
...buildDefaultNetworkControllerOptions(),
|
|
...givenNetworkControllerOptions,
|
|
};
|
|
const controller = new NetworkController(constructorOptions);
|
|
const providerConfig = controller.store.getState().provider;
|
|
assert(providerConfig.rpcUrl, 'rpcUrl must be set');
|
|
const network = new CustomNetworkCommunications({
|
|
customRpcUrl: providerConfig.rpcUrl,
|
|
});
|
|
|
|
try {
|
|
return await callback({ controller, network });
|
|
} finally {
|
|
await controller.destroy();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* For each kind of way that the provider can be set, `lookupNetwork` is always
|
|
* called. This can cause difficulty when testing the behavior of
|
|
* `lookupNetwork` itself, as extra requests then have to be mocked.
|
|
* This function takes a function that presumably sets the provider,
|
|
* stubbing `lookupNetwork` before the function and releasing the stub
|
|
* afterward.
|
|
*
|
|
* @param args - The arguments.
|
|
* @param args.controller - The network controller.
|
|
* @param args.operation - The function that presumably involves
|
|
* `lookupNetwork`.
|
|
*/
|
|
async function withoutCallingLookupNetwork({
|
|
controller,
|
|
operation,
|
|
}: {
|
|
controller: NetworkController;
|
|
operation: () => void | Promise<void>;
|
|
}) {
|
|
const spy = jest
|
|
.spyOn(controller, 'lookupNetwork')
|
|
.mockResolvedValue(undefined);
|
|
await operation();
|
|
spy.mockRestore();
|
|
}
|
|
|
|
/**
|
|
* For each kind of way that the provider can be set, `getEIP1559Compatibility`
|
|
* is always called. This can cause difficulty when testing the behavior of
|
|
* `getEIP1559Compatibility` itself, as extra requests then have to be
|
|
* mocked. This function takes a function that presumably sets the provider,
|
|
* stubbing `getEIP1559Compatibility` before the function and releasing the stub
|
|
* afterward.
|
|
*
|
|
* @param args - The arguments.
|
|
* @param args.controller - The network controller.
|
|
* @param args.operation - The function that presumably involves
|
|
* `getEIP1559Compatibility`.
|
|
*/
|
|
async function withoutCallingGetEIP1559Compatibility({
|
|
controller,
|
|
operation,
|
|
}: {
|
|
controller: NetworkController;
|
|
operation: () => void | Promise<void>;
|
|
}) {
|
|
const spy = jest
|
|
.spyOn(controller, 'getEIP1559Compatibility')
|
|
.mockResolvedValue(false);
|
|
await operation();
|
|
spy.mockRestore();
|
|
}
|
|
|
|
/**
|
|
* Waits for changes to the primary observable store of a controller to occur
|
|
* before proceeding. May be called with a function, in which case waiting will
|
|
* occur after the function is called; or may be called standalone if you want
|
|
* to assert that no state changes occurred.
|
|
*
|
|
* @param args - The arguments.
|
|
* @param args.controller - The network controller.
|
|
* @param args.propertyPath - The path of the property you expect the state
|
|
* changes to concern.
|
|
* @param args.count - The number of events you expect to occur. If null, this
|
|
* function will wait until no events have occurred in `wait` number of
|
|
* milliseconds. Default: 1.
|
|
* @param args.duration - The amount of time in milliseconds to wait for the
|
|
* expected number of filtered state changes to occur before resolving the
|
|
* promise that this function returns (default: 150).
|
|
* @param args.operation - A function to run that will presumably produce the
|
|
* state changes in question.
|
|
* @returns A promise that resolves to an array of state objects (that is, the
|
|
* contents of the store) when the specified number of filtered state changes
|
|
* have occurred, or all of them if no number has been specified.
|
|
*/
|
|
async function waitForStateChanges({
|
|
controller,
|
|
propertyPath,
|
|
count: expectedInterestingStateCount = 1,
|
|
duration: timeBeforeAssumingNoMoreStateChanges = 150,
|
|
operation = () => {
|
|
// do nothing
|
|
},
|
|
}: {
|
|
controller: NetworkController;
|
|
propertyPath: string[];
|
|
count?: number | null;
|
|
duration?: number;
|
|
operation?: () => void | Promise<void>;
|
|
}) {
|
|
const initialState = { ...controller.store.getState() };
|
|
let isTimerRunning = false;
|
|
|
|
const promiseForStateChanges = new Promise((resolve, reject) => {
|
|
// We need to declare this variable first, then assign it later, so that
|
|
// ESLint won't complain that resetTimer is referring to this variable
|
|
// before it's declared. And we need to use let so that we can assign it
|
|
// below.
|
|
/* eslint-disable-next-line prefer-const */
|
|
let eventListener: (...args: any[]) => void;
|
|
let timer: NodeJS.Timeout | undefined;
|
|
const allStates: NetworkControllerState[] = [];
|
|
const interestingStates: NetworkControllerState[] = [];
|
|
|
|
const stopTimer = () => {
|
|
if (timer) {
|
|
clearTimeout(timer);
|
|
}
|
|
isTimerRunning = false;
|
|
};
|
|
|
|
const end = () => {
|
|
stopTimer();
|
|
|
|
controller.store.unsubscribe(eventListener);
|
|
|
|
const shouldEnd =
|
|
expectedInterestingStateCount === null
|
|
? interestingStates.length > 0
|
|
: interestingStates.length === expectedInterestingStateCount;
|
|
|
|
if (shouldEnd) {
|
|
resolve(interestingStates);
|
|
} else {
|
|
// Using a string instead of an Error leads to better backtraces.
|
|
/* eslint-disable-next-line prefer-promise-reject-errors */
|
|
const expectedInterestingStateCountFragment =
|
|
expectedInterestingStateCount === null
|
|
? 'any number of'
|
|
: expectedInterestingStateCount;
|
|
const propertyPathFragment =
|
|
propertyPath === undefined ? '' : ` on \`${propertyPath.join('.')}\``;
|
|
const actualInterestingStateCountFragment =
|
|
expectedInterestingStateCount === null
|
|
? 'none'
|
|
: interestingStates.length;
|
|
const primaryMessage = `Expected to receive ${expectedInterestingStateCountFragment} state change(s)${propertyPathFragment}, but received ${actualInterestingStateCountFragment} after ${timeBeforeAssumingNoMoreStateChanges}ms.`;
|
|
reject(
|
|
[
|
|
primaryMessage,
|
|
'Initial state:',
|
|
inspect(initialState, { depth: null }),
|
|
'All state changes (without filtering):',
|
|
inspect(allStates, { depth: null }),
|
|
'Filtered state changes:',
|
|
inspect(interestingStates, { depth: null }),
|
|
].join('\n\n'),
|
|
);
|
|
}
|
|
};
|
|
|
|
const resetTimer = () => {
|
|
stopTimer();
|
|
timer = originalSetTimeout(() => {
|
|
if (isTimerRunning) {
|
|
end();
|
|
}
|
|
}, timeBeforeAssumingNoMoreStateChanges);
|
|
isTimerRunning = true;
|
|
};
|
|
|
|
eventListener = (newState) => {
|
|
const isInteresting = isStateChangeInteresting(
|
|
newState,
|
|
allStates.length > 0 ? allStates[allStates.length - 1] : initialState,
|
|
propertyPath,
|
|
);
|
|
|
|
allStates.push({ ...newState });
|
|
|
|
if (isInteresting) {
|
|
interestingStates.push(newState);
|
|
if (interestingStates.length === expectedInterestingStateCount) {
|
|
end();
|
|
} else {
|
|
resetTimer();
|
|
}
|
|
}
|
|
};
|
|
|
|
controller.store.subscribe(eventListener);
|
|
resetTimer();
|
|
});
|
|
|
|
await operation();
|
|
|
|
return await promiseForStateChanges;
|
|
}
|
|
|
|
/**
|
|
* Waits for controller events to be emitted before proceeding.
|
|
*
|
|
* @param args - The arguments to this function.
|
|
* @param args.messenger - The messenger suited for NetworkController.
|
|
* @param args.eventType - The type of NetworkController event you want to wait
|
|
* for.
|
|
* @param args.count - The number of events you expect to occur (default: 1).
|
|
* @param args.filter - A function used to discard events that are not of
|
|
* interest.
|
|
* @param args.wait - The amount of time in milliseconds to wait for the
|
|
* expected number of filtered events to occur before resolving the promise that
|
|
* this function returns (default: 150).
|
|
* @param args.operation - A function to run that will presumably produce the
|
|
* events in question.
|
|
* @param args.beforeResolving - In some tests, state updates happen so fast, we
|
|
* need to make an assertion immediately after the event in question occurs.
|
|
* However, if we wait until the promise this function returns resolves to do
|
|
* so, some other state update to the same
|
|
* property may have happened. This option allows you to make an assertion
|
|
* _before_ the promise resolves. This has the added benefit of allowing you to
|
|
* maintain the "arrange, act, assert" ordering in your test, meaning that you
|
|
* can still call the method that kicks off the event and then make the
|
|
* assertion afterward instead of the other way around.
|
|
* @returns A promise that resolves to the list of payloads for the set of
|
|
* events, optionally filtered, when a specific number of them have occurred.
|
|
*/
|
|
async function waitForPublishedEvents<E extends NetworkControllerEvent>({
|
|
messenger,
|
|
eventType,
|
|
count: expectedNumberOfEvents = 1,
|
|
filter: isEventPayloadInteresting = () => true,
|
|
wait: timeBeforeAssumingNoMoreEvents = 150,
|
|
operation = () => {
|
|
// do nothing
|
|
},
|
|
beforeResolving = async () => {
|
|
// do nothing
|
|
},
|
|
}: {
|
|
messenger: ControllerMessenger<never, NetworkControllerEvent>;
|
|
eventType: E['type'];
|
|
count?: number;
|
|
filter?: (payload: E['payload']) => boolean;
|
|
wait?: number;
|
|
operation?: () => void | Promise<void>;
|
|
beforeResolving?: () => void | Promise<void>;
|
|
}): Promise<E['payload'][]> {
|
|
const promiseForEventPayloads = new Promise<E['payload'][]>(
|
|
(resolve, reject) => {
|
|
let timer: NodeJS.Timeout | undefined;
|
|
const allEventPayloads: E['payload'][] = [];
|
|
const interestingEventPayloads: E['payload'][] = [];
|
|
let alreadyEnded = false;
|
|
|
|
// We're using `any` here because there seems to be some mismatch between
|
|
// the signature of `subscribe` and the way that we're using it. Try
|
|
// changing `any` to either `((...args: E['payload']) => void)` or
|
|
// `ExtractEventHandler<E, E['type']>` to see the issue.
|
|
const eventListener: any = (...payload: E['payload']) => {
|
|
allEventPayloads.push(payload);
|
|
|
|
if (isEventPayloadInteresting(payload)) {
|
|
interestingEventPayloads.push(payload);
|
|
if (interestingEventPayloads.length === expectedNumberOfEvents) {
|
|
stopTimer();
|
|
end();
|
|
} else {
|
|
resetTimer();
|
|
}
|
|
}
|
|
};
|
|
|
|
function end() {
|
|
if (!alreadyEnded) {
|
|
alreadyEnded = true;
|
|
messenger.unsubscribe(eventType, eventListener);
|
|
Promise.resolve(beforeResolving()).then(() => {
|
|
if (interestingEventPayloads.length === expectedNumberOfEvents) {
|
|
resolve(interestingEventPayloads);
|
|
} else {
|
|
// Using a string instead of an Error leads to better backtraces.
|
|
/* eslint-disable-next-line prefer-promise-reject-errors */
|
|
reject(
|
|
`Expected to receive ${expectedNumberOfEvents} ${eventType} event(s), but received ${
|
|
interestingEventPayloads.length
|
|
} after ${timeBeforeAssumingNoMoreEvents}ms.\n\nAll payloads:\n\n${inspect(
|
|
allEventPayloads,
|
|
{ depth: null },
|
|
)}`,
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function stopTimer() {
|
|
if (timer) {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
function resetTimer() {
|
|
stopTimer();
|
|
timer = originalSetTimeout(() => {
|
|
end();
|
|
}, timeBeforeAssumingNoMoreEvents);
|
|
}
|
|
|
|
messenger.subscribe(eventType, eventListener);
|
|
resetTimer();
|
|
},
|
|
);
|
|
|
|
if (operation) {
|
|
await operation();
|
|
}
|
|
|
|
return await promiseForEventPayloads;
|
|
}
|
|
|
|
/**
|
|
* `lookupNetwork` is a method in NetworkController which is called internally
|
|
* by a few methods. `lookupNetwork` is asynchronous as it makes network
|
|
* requests under the hood, but unfortunately, the method is not awaited after
|
|
* being called. Hence, if it is called during a test, even if the network
|
|
* requests are initiated within the test, they may complete after that test
|
|
* ends. This is a problem because it may cause Nock mocks set up in a later
|
|
* test to get used up prematurely, causing failures.
|
|
*
|
|
* To fix this, we need to wait for `lookupNetwork` to fully finish before
|
|
* continuing. Since the latest thing that happens in `lookupNetwork` is to
|
|
* update EIP-1559 compatibility in state, we can wait for the `networkDetails`
|
|
* state to get updated specifically. Unfortunately, we don't know how many
|
|
* times this will happen, so this function does incur some time when it's used.
|
|
* To speed up tests, you can pass `numberOfNetworkDetailsChanges`.
|
|
*
|
|
* @param args - The arguments.
|
|
* @param args.controller - The network controller.
|
|
* @param args.numberOfNetworkDetailsChanges - The number of times that
|
|
* `networkDetails` is expected to be updated.
|
|
* @param args.operation - The function that presumably involves
|
|
* `lookupNetwork`.
|
|
*/
|
|
async function waitForLookupNetworkToComplete({
|
|
controller,
|
|
numberOfNetworkDetailsChanges = null,
|
|
operation,
|
|
}: {
|
|
controller: NetworkController;
|
|
numberOfNetworkDetailsChanges?: number | null;
|
|
operation: () => void | Promise<void>;
|
|
}) {
|
|
await waitForStateChanges({
|
|
controller,
|
|
propertyPath: ['networkDetails'],
|
|
operation,
|
|
count: numberOfNetworkDetailsChanges,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Returns whether two places in different state objects have different values.
|
|
*
|
|
* @param currentState - The current state object.
|
|
* @param prevState - The previous state object.
|
|
* @param propertyPath - A property path within both objects.
|
|
* @returns True or false, depending on the result.
|
|
*/
|
|
function isStateChangeInteresting(
|
|
currentState: Record<PropertyKey, unknown>,
|
|
prevState: Record<PropertyKey, unknown>,
|
|
propertyPath: PropertyKey[],
|
|
): boolean {
|
|
return !isDeepStrictEqual(
|
|
get(currentState, propertyPath),
|
|
get(prevState, propertyPath),
|
|
);
|
|
}
|
|
/**
|
|
* `Object.getOwnPropertyNames()` is intentionally generic: it returns the own
|
|
* property names of an object, but it cannot make guarantees about the contents
|
|
* of that object, so the type of the names is merely `string[]`. While this is
|
|
* technically accurate, it is also unnecessary if we have an object that we've
|
|
* created and whose contents we know exactly.
|
|
*
|
|
* TODO: Move this to @metamask/utils
|
|
*
|
|
* @param object - The object.
|
|
* @returns The own property names of an object, typed according to the type of
|
|
* the object itself.
|
|
*/
|
|
function knownOwnKeysOf<K extends PropertyKey>(
|
|
object: Partial<Record<K, unknown>>,
|
|
) {
|
|
return Object.getOwnPropertyNames(object) as K[];
|
|
}
|