A Metamask fork with Infura removed and default networks editable
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ciphermask/app/scripts/controllers/network/provider-api-tests/helpers.js

334 lines
11 KiB

Add initial provider API tests for Infura client (#15556) We are working on migrating the extension to a unified network controller, but before we do so we want to extract some of the existing pieces, specifically `createInfuraClient` and `createJsonRpcClient`, which provide the majority of the behavior exhibited within the provider API that the existing NetworkController exposes. This necessitates that we understand and test that behavior as a whole. With that in mind, this commit starts with the Infura-specific network client and adds some initial functional tests for `createInfuraClient`, specifically covering three pieces of middleware provided by `eth-json-rpc-middleware`: `createNetworkAndChainIdMiddleware`, `createBlockCacheMiddleware`, and `createBlockRefMiddleware`. These tests exercise logic that originate from multiple different places and combine in sometimes surprising ways, and as a result, understanding the nature of the tests can be tricky. I've tried to explain the logic (both of the implementation and the tests) via comments. Additionally, debugging why a certain test is failing is not the most fun thing in the world, so to aid with this, I've added some logging to the underlying packages used when a request passes through the middleware stack. Because some middleware change the request being made, or make new requests altogether, this greatly helps to peel back the curtain, as failures from Nock do not supply much meaningful information on their own. This logging is disabled by default, but can be activated by setting `DEBUG=metamask:*,eth-query DEBUG_COLORS=1` alongside the `jest` command. We use this logging by bumping `eth-block-tracker`, and `eth-json-rpc-middleware`.
2 years ago
import nock from 'nock';
import sinon from 'sinon';
import { JsonRpcEngine } from 'json-rpc-engine';
import { providerFromEngine } from 'eth-json-rpc-middleware';
import EthQuery from 'eth-query';
import createInfuraClient from '../createInfuraClient';
/**
* @typedef {import('nock').Scope} NockScope
*
* A object returned by `nock(...)` for mocking requests to a particular base
* URL.
*/
/**
* @typedef {{makeRpcCall: (request: Partial<JsonRpcRequest>) => Promise<any>, makeRpcCallsInSeries: (requests: Partial<JsonRpcRequest>[]) => Promise<any>}} InfuraClient
*
* Provides methods to interact with the suite of middleware that
* `createInfuraClient` exposes.
*/
/**
* @typedef {{network: string}} WithInfuraClientOptions
*
* The options bag that `withInfuraClient` takes.
*/
/**
* @typedef {(client: InfuraClient) => Promise<any>} WithInfuraClientCallback
*
* The callback that `withInfuraClient` takes.
*/
/**
* @typedef {[WithInfuraClientOptions, WithInfuraClientCallback] | [WithInfuraClientCallback]} WithInfuraClientArgs
*
* The arguments to `withInfuraClient`.
*/
/**
* @typedef {{ nockScope: NockScope, blockNumber: string }} MockNextBlockTrackerRequestOptions
*
* The options to `mockNextBlockTrackerRequest`.
*/
/**
* @typedef {{ nockScope: NockScope, request: object, response: object, delay?: number }} MockSuccessfulInfuraRpcCallOptions
*
* The options to `mockSuccessfulInfuraRpcCall`.
*/
/**
* @typedef {{mockNextBlockTrackerRequest: (options: Omit<MockNextBlockTrackerRequestOptions, 'nockScope'>) => void, mockSuccessfulInfuraRpcCall: (options: Omit<MockSuccessfulInfuraRpcCallOptions, 'nockScope'>) => NockScope}} InfuraCommunications
*
* Provides methods to mock different kinds of requests to Infura.
*/
/**
* @typedef {{network: string}} MockingInfuraCommunicationsOptions
*
* The options bag that `mockingInfuraCommunications` takes.
*/
/**
* @typedef {(comms: InfuraCommunications) => Promise<any>} MockingInfuraCommunicationsCallback
*
* The callback that `mockingInfuraCommunications` takes.
*/
/**
* @typedef {[MockingInfuraCommunicationsOptions, MockingInfuraCommunicationsCallback] | [MockingInfuraCommunicationsCallback]} MockingInfuraCommunicationsArgs
*
* The arguments to `mockingInfuraCommunications`.
*/
const INFURA_PROJECT_ID = 'abc123';
const DEFAULT_LATEST_BLOCK_NUMBER = '0x42';
/**
* If you're having trouble writing a test and you're wondering why the test
* keeps failing, you can set `process.env.DEBUG_PROVIDER_TESTS` to `1`. This
* will turn on some extra logging.
*
* @param {any[]} args - The arguments that `console.log` takes.
*/
function debug(...args) {
if (process.env.DEBUG_PROVIDER_TESTS === '1') {
console.log(...args);
}
}
/**
* Builds a Nock scope object for mocking requests to a particular network that
* Infura supports.
*
* @param {object} options - The options.
* @param {string} options.network - The Infura network you're testing with
* (default: "mainnet").
* @returns {NockScope} The nock scope.
*/
function buildScopeForMockingInfuraRequests({ network = 'mainnet' } = {}) {
return nock(`https://${network}.infura.io`).filteringRequestBody((body) => {
const copyOfBody = JSON.parse(body);
// some ids are random, so remove them entirely from the request to
// make it possible to mock these requests
delete copyOfBody.id;
return JSON.stringify(copyOfBody);
});
}
/**
* Mocks the next request for the latest block that the block tracker will make.
*
* @param {MockNextBlockTrackerRequestOptions} args - The arguments.
* @param {NockScope} args.nockScope - A nock scope (a set of mocked requests
* scoped to a certain base URL).
* @param {string} args.blockNumber - The block number that the block tracker
* should report, as a 0x-prefixed hex string.
*/
async function mockNextBlockTrackerRequest({
nockScope,
blockNumber = DEFAULT_LATEST_BLOCK_NUMBER,
}) {
await mockSuccessfulInfuraRpcCall({
nockScope,
request: { method: 'eth_blockNumber', params: [] },
response: { result: blockNumber },
});
}
/**
* Mocks a JSON-RPC request sent to Infura with the given response.
*
* @param {MockSuccessfulInfuraRpcCallOptions} args - The arguments.
* @param {NockScope} args.nockScope - A nock scope (a set of mocked requests
* scoped to a certain base URL).
* @param {object} args.request - The request data.
* @param {object} args.response - The response that the request should have.
* @param {number} args.delay - The amount of time that should pass before the
* request resolves with the response.
* @returns {NockScope} The nock scope.
*/
function mockSuccessfulInfuraRpcCall({ nockScope, request, response, delay }) {
// eth-query always passes `params`, so even if we don't supply this property
// for consistency with makeRpcCall, assume that the `body` contains it
const { method, params = [], ...rest } = request;
const completeResponse = {
id: 1,
jsonrpc: '2.0',
...response,
};
const nockRequest = nockScope.post(`/v3/${INFURA_PROJECT_ID}`, {
jsonrpc: '2.0',
method,
params,
...rest,
});
if (delay !== undefined) {
nockRequest.delay(delay);
}
return nockRequest.reply(200, completeResponse);
}
/**
* Makes a JSON-RPC call through the given eth-query object.
*
* @param {any} ethQuery - The eth-query object.
* @param {object} request - The request data.
* @returns {Promise<any>} A promise that either resolves with the result from
* the JSON-RPC response if it is successful or rejects with the error from the
* JSON-RPC response otherwise.
*/
function makeRpcCall(ethQuery, request) {
return new Promise((resolve, reject) => {
debug('[makeRpcCall] making request', request);
ethQuery.sendAsync(request, (error, result) => {
debug('[makeRpcCall > ethQuery handler] error', error, 'result', result);
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
}
/**
* Sets up request mocks for requests to Infura.
*
* @param {MockingInfuraCommunicationsArgs} args - Either an options bag + a
* function, or just a function. The options bag, at the moment, may contain
* `network` (that is, the Infura network; defaults to "mainnet"). The function
* is called with an object that allows you to mock different kinds of requests.
* @returns {Promise<any>} The return value of the given function.
*/
export async function withMockedInfuraCommunications(...args) {
const [options, fn] = args.length === 2 ? args : [{}, args[0]];
const { network = 'mainnet' } = options;
const nockScope = buildScopeForMockingInfuraRequests({ network });
const curriedMockNextBlockTrackerRequest = (localOptions) =>
mockNextBlockTrackerRequest({ nockScope, ...localOptions });
const curriedMockSuccessfulInfuraRpcCall = (localOptions) =>
mockSuccessfulInfuraRpcCall({ nockScope, ...localOptions });
const comms = {
mockNextBlockTrackerRequest: curriedMockNextBlockTrackerRequest,
mockSuccessfulInfuraRpcCall: curriedMockSuccessfulInfuraRpcCall,
};
try {
return await fn(comms);
} finally {
nock.isDone();
nock.cleanAll();
}
}
/**
* Builds a provider from the Infura middleware along with a block tracker, runs
* the given function with those two things, and then ensures the block tracker
* is stopped at the end.
*
* @param {WithInfuraClientArgs} args - Either an options bag + a function, or
* just a function. The options bag, at the moment, may contain `network` (that
* is, the Infura network; defaults to "mainnet"). The function is called with
* an object that allows you to interact with the client via a couple of methods
* on that object.
* @returns {Promise<any>} The return value of the given function.
*/
export async function withInfuraClient(...args) {
const [options, fn] = args.length === 2 ? args : [{}, args[0]];
const { network = 'mainnet' } = options;
const { networkMiddleware, blockTracker } = createInfuraClient({
network,
projectId: INFURA_PROJECT_ID,
});
const engine = new JsonRpcEngine();
engine.push(networkMiddleware);
const provider = providerFromEngine(engine);
const ethQuery = new EthQuery(provider);
const curriedMakeRpcCall = (request) => makeRpcCall(ethQuery, request);
const makeRpcCallsInSeries = async (requests) => {
const responses = [];
for (const request of requests) {
responses.push(await curriedMakeRpcCall(request));
}
return responses;
};
// 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`)
const clock = sinon.useFakeTimers();
const client = {
makeRpcCall: curriedMakeRpcCall,
makeRpcCallsInSeries,
clock,
};
try {
return await fn(client);
} finally {
await blockTracker.destroy();
clock.restore();
}
}
/**
* Some JSON-RPC endpoints take a "block" param (example: `eth_blockNumber`)
* which can optionally be left out. Additionally, the endpoint may support some
* number of arguments, although the "block" param will always be last, even if
* it is optional. Given this, this function builds a mock `params` array for
* such an endpoint, filling it with arbitrary values, but with the "block"
* param missing.
*
* @param {number} index - The index within the `params` array where the "block"
* param *would* appear.
* @returns {string[]} The mock params.
*/
export function buildMockParamsWithoutBlockParamAt(index) {
const params = [];
for (let i = 0; i < index; i++) {
params.push('some value');
}
return params;
}
/**
* Some JSON-RPC endpoints take a "block" param (example: `eth_blockNumber`)
* which can optionally be left out. Additionally, the endpoint may support some
* number of arguments, although the "block" param will always be last, even if
* it is optional. Given this, this function builds a `params` array for such an
* endpoint with the given "block" param added at the end.
*
* @param {number} index - The index within the `params` array to add the
* "block" param.
* @param {any} blockParam - The desired "block" param to add.
* @returns {any[]} The mock params.
*/
export function buildMockParamsWithBlockParamAt(index, blockParam) {
const params = buildMockParamsWithoutBlockParamAt(index);
params.push(blockParam);
return params;
}
/**
* Returns a partial JSON-RPC request object, with the "block" param replaced
* with the given value.
*
* @param {object} request - The request object.
* @param {string} request.method - The request method.
* @param {params} [request.params] - The request params.
* @param {number} blockParamIndex - The index within the `params` array of the
* block param.
* @param {any} blockParam - The desired block param value.
* @returns {object} The updated request object.
*/
export function buildRequestWithReplacedBlockParam(
{ method, params = [] },
blockParamIndex,
blockParam,
) {
const updatedParams = params.slice();
updatedParams[blockParamIndex] = blockParam;
return { method, params: updatedParams };
}