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`.
2022-09-16 15:18:33 +02:00
|
|
|
/* eslint-disable jest/require-top-level-describe, jest/no-export, jest/no-identical-title */
|
|
|
|
|
2023-03-20 17:50:52 +01:00
|
|
|
import { testsForRpcMethodsThatCheckForBlockHashInResponse } from './block-hash-in-response';
|
|
|
|
import { testsForRpcMethodSupportingBlockParam } from './block-param';
|
2023-04-14 18:21:29 +02:00
|
|
|
import {
|
|
|
|
ProviderType,
|
|
|
|
withMockedCommunications,
|
|
|
|
withNetworkClient,
|
|
|
|
} from './helpers';
|
2023-03-20 17:50:52 +01:00
|
|
|
import { testsForRpcMethodAssumingNoBlockParam } from './no-block-param';
|
|
|
|
import { testsForRpcMethodNotHandledByMiddleware } from './not-handled-by-middleware';
|
2022-09-29 18:39:35 +02:00
|
|
|
|
2023-01-06 18:10:17 +01:00
|
|
|
/**
|
|
|
|
* Constructs an error message that the Infura client would produce in the event
|
|
|
|
* that it has attempted to retry the request to Infura and has failed.
|
|
|
|
*
|
|
|
|
* @param reason - The exact reason for failure.
|
|
|
|
* @returns The error message.
|
|
|
|
*/
|
2023-04-14 18:21:29 +02:00
|
|
|
export function buildInfuraClientRetriesExhaustedErrorMessage(reason: string) {
|
2023-01-06 18:10:17 +01:00
|
|
|
return new RegExp(
|
|
|
|
`^InfuraProvider - cannot complete request. All retries exhausted\\..+${reason}`,
|
|
|
|
'us',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Constructs an error message that JsonRpcEngine would produce in the event
|
|
|
|
* that the response object is empty as it leaves the middleware.
|
|
|
|
*
|
|
|
|
* @param method - The RPC method.
|
|
|
|
* @returns The error message.
|
|
|
|
*/
|
2023-04-14 18:21:29 +02:00
|
|
|
export function buildJsonRpcEngineEmptyResponseErrorMessage(method: string) {
|
2023-01-06 18:10:17 +01:00
|
|
|
return new RegExp(
|
|
|
|
`^JsonRpcEngine: Response has no error or result for request:.+"method": "${method}"`,
|
|
|
|
'us',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Constructs an error message that `fetch` with throw if it cannot make a
|
|
|
|
* request.
|
|
|
|
*
|
|
|
|
* @param url - The URL being fetched
|
|
|
|
* @param reason - The reason.
|
|
|
|
* @returns The error message.
|
|
|
|
*/
|
2023-04-14 18:21:29 +02:00
|
|
|
export function buildFetchFailedErrorMessage(url: string, reason: string) {
|
2023-01-06 18:10:17 +01:00
|
|
|
return new RegExp(
|
|
|
|
`^request to ${url}(/[^/ ]*)+ failed, reason: ${reason}`,
|
|
|
|
'us',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Defines tests that are common to both the Infura and JSON-RPC network client.
|
|
|
|
*
|
|
|
|
* @param providerType - The type of provider being tested, which determines
|
|
|
|
* which suite of middleware is being tested. If `infura`, then the middleware
|
|
|
|
* exposed by `createInfuraClient` is tested; if `custom`, then the middleware
|
|
|
|
* exposed by `createJsonRpcClient` will be tested.
|
|
|
|
*/
|
2023-04-14 18:21:29 +02:00
|
|
|
export function testsForProviderType(providerType: ProviderType) {
|
2023-01-06 18:10:17 +01:00
|
|
|
// Ethereum JSON-RPC spec: <https://ethereum.github.io/execution-apis/api-documentation/>
|
|
|
|
// Infura documentation: <https://docs.infura.io/infura/networks/ethereum/json-rpc-methods>
|
|
|
|
|
|
|
|
describe('methods included in the Ethereum JSON-RPC spec', () => {
|
|
|
|
describe('methods not handled by middleware', () => {
|
|
|
|
const notHandledByMiddleware = [
|
2023-03-20 17:50:52 +01:00
|
|
|
// This list is presented in the same order as in the network client
|
|
|
|
// tests on the core side.
|
|
|
|
|
|
|
|
{ name: 'eth_newFilter', numberOfParameters: 1 },
|
2023-01-06 18:10:17 +01:00
|
|
|
{ name: 'eth_getFilterChanges', numberOfParameters: 1 },
|
|
|
|
{ name: 'eth_newBlockFilter', numberOfParameters: 0 },
|
|
|
|
{ name: 'eth_newPendingTransactionFilter', numberOfParameters: 0 },
|
2023-03-20 17:50:52 +01:00
|
|
|
{ name: 'eth_uninstallFilter', numberOfParameters: 1 },
|
|
|
|
|
2023-01-06 18:10:17 +01:00
|
|
|
{ name: 'eth_sendRawTransaction', numberOfParameters: 1 },
|
|
|
|
{ name: 'eth_sendTransaction', numberOfParameters: 1 },
|
|
|
|
{ name: 'eth_sign', numberOfParameters: 2 },
|
2023-03-20 17:50:52 +01:00
|
|
|
|
|
|
|
{ name: 'eth_createAccessList', numberOfParameters: 2 },
|
|
|
|
{ name: 'eth_getLogs', numberOfParameters: 1 },
|
|
|
|
{ name: 'eth_getProof', numberOfParameters: 3 },
|
|
|
|
{ name: 'eth_getWork', numberOfParameters: 0 },
|
|
|
|
{ name: 'eth_maxPriorityFeePerGas', numberOfParameters: 0 },
|
2023-02-02 20:14:22 +01:00
|
|
|
{ name: 'eth_submitHashRate', numberOfParameters: 2 },
|
2023-01-06 18:10:17 +01:00
|
|
|
{ name: 'eth_submitWork', numberOfParameters: 3 },
|
|
|
|
{ name: 'eth_syncing', numberOfParameters: 0 },
|
2023-03-20 17:50:52 +01:00
|
|
|
{ name: 'eth_feeHistory', numberOfParameters: 3 },
|
2023-02-02 20:14:22 +01:00
|
|
|
{ name: 'debug_getRawHeader', numberOfParameters: 1 },
|
|
|
|
{ name: 'debug_getRawBlock', numberOfParameters: 1 },
|
|
|
|
{ name: 'debug_getRawTransaction', numberOfParameters: 1 },
|
|
|
|
{ name: 'debug_getRawReceipts', numberOfParameters: 1 },
|
|
|
|
{ name: 'debug_getBadBlocks', numberOfParameters: 0 },
|
2023-03-20 17:50:52 +01:00
|
|
|
|
|
|
|
{ name: 'eth_accounts', numberOfParameters: 0 },
|
|
|
|
{ name: 'eth_coinbase', numberOfParameters: 0 },
|
|
|
|
{ name: 'eth_hashrate', numberOfParameters: 0 },
|
|
|
|
{ name: 'eth_mining', numberOfParameters: 0 },
|
|
|
|
|
|
|
|
{ name: 'eth_signTransaction', numberOfParameters: 1 },
|
2023-01-06 18:10:17 +01:00
|
|
|
];
|
|
|
|
notHandledByMiddleware.forEach(({ name, numberOfParameters }) => {
|
|
|
|
describe(`method name: ${name}`, () => {
|
|
|
|
testsForRpcMethodNotHandledByMiddleware(name, {
|
|
|
|
providerType,
|
|
|
|
numberOfParameters,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2023-03-20 17:50:52 +01:00
|
|
|
describe('methods with block hashes in their result', () => {
|
|
|
|
const methodsWithBlockHashInResponse = [
|
|
|
|
{ name: 'eth_getTransactionByHash', numberOfParameters: 1 },
|
|
|
|
{ name: 'eth_getTransactionReceipt', numberOfParameters: 1 },
|
|
|
|
];
|
|
|
|
methodsWithBlockHashInResponse.forEach(({ name, numberOfParameters }) => {
|
|
|
|
describe(`method name: ${name}`, () => {
|
|
|
|
testsForRpcMethodsThatCheckForBlockHashInResponse(name, {
|
|
|
|
numberOfParameters,
|
|
|
|
providerType,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('methods that assume there is no block param', () => {
|
|
|
|
const assumingNoBlockParam = [
|
|
|
|
{ name: 'eth_getFilterLogs', numberOfParameters: 1 },
|
|
|
|
{ name: 'eth_blockNumber', numberOfParameters: 0 },
|
|
|
|
{ name: 'eth_estimateGas', numberOfParameters: 2 },
|
|
|
|
{ name: 'eth_gasPrice', numberOfParameters: 0 },
|
|
|
|
{ name: 'eth_getBlockByHash', numberOfParameters: 2 },
|
|
|
|
{
|
|
|
|
name: 'eth_getBlockTransactionCountByHash',
|
|
|
|
numberOfParameters: 1,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'eth_getTransactionByBlockHashAndIndex',
|
|
|
|
numberOfParameters: 2,
|
|
|
|
},
|
|
|
|
{ name: 'eth_getUncleByBlockHashAndIndex', numberOfParameters: 2 },
|
|
|
|
{ name: 'eth_getUncleCountByBlockHash', numberOfParameters: 1 },
|
|
|
|
];
|
|
|
|
const blockParamIgnored = [
|
|
|
|
{ name: 'eth_getUncleCountByBlockNumber', numberOfParameters: 1 },
|
|
|
|
{ name: 'eth_getUncleByBlockNumberAndIndex', numberOfParameters: 2 },
|
|
|
|
{
|
|
|
|
name: 'eth_getTransactionByBlockNumberAndIndex',
|
|
|
|
numberOfParameters: 2,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'eth_getBlockTransactionCountByNumber',
|
|
|
|
numberOfParameters: 1,
|
|
|
|
},
|
|
|
|
];
|
|
|
|
assumingNoBlockParam
|
|
|
|
.concat(blockParamIgnored)
|
|
|
|
.forEach(({ name, numberOfParameters }) =>
|
|
|
|
describe(`method name: ${name}`, () => {
|
|
|
|
testsForRpcMethodAssumingNoBlockParam(name, {
|
|
|
|
providerType,
|
|
|
|
numberOfParameters,
|
|
|
|
});
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2023-01-06 18:10:17 +01:00
|
|
|
describe('methods that have a param to specify the block', () => {
|
|
|
|
const supportingBlockParam = [
|
2023-01-26 16:57:46 +01:00
|
|
|
{
|
|
|
|
name: 'eth_call',
|
|
|
|
blockParamIndex: 1,
|
|
|
|
numberOfParameters: 2,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'eth_getBalance',
|
|
|
|
blockParamIndex: 1,
|
|
|
|
numberOfParameters: 2,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'eth_getBlockByNumber',
|
|
|
|
blockParamIndex: 0,
|
|
|
|
numberOfParameters: 2,
|
|
|
|
},
|
|
|
|
{ name: 'eth_getCode', blockParamIndex: 1, numberOfParameters: 2 },
|
|
|
|
{
|
|
|
|
name: 'eth_getStorageAt',
|
|
|
|
blockParamIndex: 2,
|
|
|
|
numberOfParameters: 3,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'eth_getTransactionCount',
|
|
|
|
blockParamIndex: 1,
|
|
|
|
numberOfParameters: 2,
|
|
|
|
},
|
2023-01-06 18:10:17 +01:00
|
|
|
];
|
2023-01-26 16:57:46 +01:00
|
|
|
supportingBlockParam.forEach(
|
|
|
|
({ name, blockParamIndex, numberOfParameters }) => {
|
|
|
|
describe(`method name: ${name}`, () => {
|
|
|
|
testsForRpcMethodSupportingBlockParam(name, {
|
|
|
|
providerType,
|
|
|
|
blockParamIndex,
|
|
|
|
numberOfParameters,
|
|
|
|
});
|
2023-01-06 18:10:17 +01:00
|
|
|
});
|
2023-01-26 16:57:46 +01:00
|
|
|
},
|
|
|
|
);
|
2023-01-06 18:10:17 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
describe('other methods', () => {
|
|
|
|
describe('eth_getTransactionByHash', () => {
|
|
|
|
it("refreshes the block tracker's current block if it is less than the block number that comes back in the response", async () => {
|
|
|
|
const method = 'eth_getTransactionByHash';
|
|
|
|
|
|
|
|
await withMockedCommunications({ providerType }, async (comms) => {
|
|
|
|
const request = { method };
|
|
|
|
|
|
|
|
comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' });
|
|
|
|
// This is our request.
|
|
|
|
comms.mockRpcCall({
|
|
|
|
request,
|
|
|
|
response: {
|
|
|
|
result: {
|
|
|
|
blockNumber: '0x200',
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
comms.mockNextBlockTrackerRequest({ blockNumber: '0x300' });
|
|
|
|
|
|
|
|
await withNetworkClient(
|
|
|
|
{ providerType },
|
|
|
|
async ({ makeRpcCall, blockTracker }) => {
|
|
|
|
await makeRpcCall(request);
|
|
|
|
expect(blockTracker.getCurrentBlock()).toStrictEqual('0x300');
|
|
|
|
},
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('eth_getTransactionReceipt', () => {
|
|
|
|
it("refreshes the block tracker's current block if it is less than the block number that comes back in the response", async () => {
|
|
|
|
const method = 'eth_getTransactionReceipt';
|
|
|
|
|
|
|
|
await withMockedCommunications({ providerType }, async (comms) => {
|
|
|
|
const request = { method };
|
|
|
|
|
|
|
|
comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' });
|
|
|
|
// This is our request.
|
|
|
|
comms.mockRpcCall({
|
|
|
|
request,
|
|
|
|
response: {
|
|
|
|
result: {
|
|
|
|
blockNumber: '0x200',
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
comms.mockNextBlockTrackerRequest({ blockNumber: '0x300' });
|
|
|
|
|
|
|
|
await withNetworkClient(
|
|
|
|
{ providerType },
|
|
|
|
async ({ makeRpcCall, blockTracker }) => {
|
|
|
|
await makeRpcCall(request);
|
|
|
|
expect(blockTracker.getCurrentBlock()).toStrictEqual('0x300');
|
|
|
|
},
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('eth_chainId', () => {
|
|
|
|
it('does not hit the RPC endpoint, instead returning the configured chain id', async () => {
|
|
|
|
const networkId = await withNetworkClient(
|
|
|
|
{ providerType: 'custom', customChainId: '0x1' },
|
|
|
|
({ makeRpcCall }) => {
|
|
|
|
return makeRpcCall({ method: 'eth_chainId' });
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(networkId).toStrictEqual('0x1');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('methods not included in the Ethereum JSON-RPC spec', () => {
|
|
|
|
describe('methods not handled by middleware', () => {
|
|
|
|
const notHandledByMiddleware = [
|
2023-03-20 17:50:52 +01:00
|
|
|
// This list is presented in the same order as in the network client
|
|
|
|
// tests on the core side.
|
|
|
|
|
|
|
|
{ name: 'net_listening', numberOfParameters: 0 },
|
2023-03-22 23:20:59 +01:00
|
|
|
// TODO: Methods to add back when we add testing for subscribe middleware
|
|
|
|
// { name: 'eth_subscribe', numberOfParameters: 1 },
|
|
|
|
// { name: 'eth_unsubscribe', numberOfParameters: 1 },
|
2023-03-20 17:50:52 +01:00
|
|
|
{ name: 'custom_rpc_method', numberOfParameters: 1 },
|
2023-01-06 18:10:17 +01:00
|
|
|
{ name: 'net_peerCount', numberOfParameters: 0 },
|
|
|
|
{ name: 'parity_nextNonce', numberOfParameters: 1 },
|
|
|
|
];
|
|
|
|
notHandledByMiddleware.forEach(({ name, numberOfParameters }) => {
|
|
|
|
describe(`method name: ${name}`, () => {
|
|
|
|
testsForRpcMethodNotHandledByMiddleware(name, {
|
|
|
|
providerType,
|
|
|
|
numberOfParameters,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('methods that assume there is no block param', () => {
|
|
|
|
const assumingNoBlockParam = [
|
2023-01-26 16:57:46 +01:00
|
|
|
{ name: 'web3_clientVersion', numberOfParameters: 0 },
|
2023-03-20 17:50:52 +01:00
|
|
|
{ name: 'eth_protocolVersion', numberOfParameters: 0 },
|
2023-01-06 18:10:17 +01:00
|
|
|
];
|
2023-01-26 16:57:46 +01:00
|
|
|
assumingNoBlockParam.forEach(({ name, numberOfParameters }) =>
|
2023-01-06 18:10:17 +01:00
|
|
|
describe(`method name: ${name}`, () => {
|
2023-01-26 16:57:46 +01:00
|
|
|
testsForRpcMethodAssumingNoBlockParam(name, {
|
|
|
|
providerType,
|
|
|
|
numberOfParameters,
|
|
|
|
});
|
2023-01-06 18:10:17 +01:00
|
|
|
}),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('other methods', () => {
|
|
|
|
describe('net_version', () => {
|
|
|
|
// The Infura middleware includes `net_version` in its scaffold
|
|
|
|
// middleware, whereas the custom RPC middleware does not.
|
|
|
|
if (providerType === 'infura') {
|
|
|
|
it('does not hit Infura, instead returning the network ID that maps to the Infura network, as a decimal string', async () => {
|
|
|
|
const networkId = await withNetworkClient(
|
|
|
|
{ providerType: 'infura', infuraNetwork: 'goerli' },
|
|
|
|
({ makeRpcCall }) => {
|
|
|
|
return makeRpcCall({
|
|
|
|
method: 'net_version',
|
|
|
|
});
|
|
|
|
},
|
|
|
|
);
|
|
|
|
expect(networkId).toStrictEqual('5');
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
it('hits the RPC endpoint', async () => {
|
|
|
|
await withMockedCommunications(
|
|
|
|
{ providerType: 'custom' },
|
|
|
|
async (comms) => {
|
|
|
|
comms.mockRpcCall({
|
|
|
|
request: { method: 'net_version' },
|
|
|
|
response: { result: '1' },
|
|
|
|
});
|
|
|
|
|
|
|
|
const networkId = await withNetworkClient(
|
|
|
|
{ providerType: 'custom' },
|
|
|
|
({ makeRpcCall }) => {
|
|
|
|
return makeRpcCall({
|
|
|
|
method: 'net_version',
|
|
|
|
});
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(networkId).toStrictEqual('1');
|
|
|
|
},
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|