From cd2249f193ec94e085182105d4656d96625c67c6 Mon Sep 17 00:00:00 2001 From: Zachary Belford Date: Fri, 6 Jan 2023 09:10:17 -0800 Subject: [PATCH] Add tests for custom JSON-RPC network client (#16337) Previously we had written tests for `createInfuraClient`, which creates a middleware stack designed to connect to an Infura provider. These tests exercise various RPC methods that relate to the behavior that the middleware provides (mainly around caching). Now we need to write the same tests but for `createJsonRpcClient`, which creates a middleware stack designed to connect to a non-Infura RPC endpoint. To do this, we had to: - Consolidate the tests for both types of RPC client into a single test file. - Add conditions around tests or assertions in tests to account for differences in behavior between the two sets of middleware stacks. - Relocate code in `createJsonRpcClient` which slows down `eth_estimateGas` calls just for tests so that this behavior can be disabled in the network client tests. Eventually, as we unify the network controllers in this repo and in the core repo, we will move these tests into the core repo. Co-authored-by: Elliot Winkler --- .../network/createInfuraClient.test.js | 388 +-- .../network/createJsonRpcClient.js | 14 +- .../network/createJsonRpcClient.test.js | 5 + .../network/provider-api-tests/helpers.js | 197 +- .../provider-api-tests/shared-tests.js | 2282 ++++++++++++++--- 5 files changed, 2104 insertions(+), 782 deletions(-) create mode 100644 app/scripts/controllers/network/createJsonRpcClient.test.js diff --git a/app/scripts/controllers/network/createInfuraClient.test.js b/app/scripts/controllers/network/createInfuraClient.test.js index 404489ece..d1b1a7ccf 100644 --- a/app/scripts/controllers/network/createInfuraClient.test.js +++ b/app/scripts/controllers/network/createInfuraClient.test.js @@ -1,389 +1,5 @@ -/** - * @jest-environment node - */ - -import { - withMockedInfuraCommunications, - withInfuraClient, -} from './provider-api-tests/helpers'; -import { - testsForRpcMethodNotHandledByMiddleware, - testsForRpcMethodAssumingNoBlockParam, - testsForRpcMethodsThatCheckForBlockHashInResponse, - testsForRpcMethodSupportingBlockParam, -} from './provider-api-tests/shared-tests'; +import { testsForProviderType } from './provider-api-tests/shared-tests'; describe('createInfuraClient', () => { - // Infura documentation: - // Ethereum JSON-RPC spec: - - describe('RPC methods supported by Infura and listed in the JSON-RPC spec', () => { - describe('eth_accounts', () => { - testsForRpcMethodNotHandledByMiddleware('eth_accounts', { - numberOfParameters: 0, - }); - }); - - describe('eth_blockNumber', () => { - testsForRpcMethodAssumingNoBlockParam('eth_blockNumber'); - }); - - describe('eth_call', () => { - testsForRpcMethodSupportingBlockParam('eth_call', { - blockParamIndex: 1, - }); - }); - - describe('eth_chainId', () => { - it('does not hit Infura, instead returning the chain id that maps to the Infura network, as a hex string', async () => { - const chainId = await withInfuraClient( - { network: 'goerli' }, - ({ makeRpcCall }) => { - return makeRpcCall({ - method: 'eth_chainId', - }); - }, - ); - - expect(chainId).toStrictEqual('0x5'); - }); - }); - - describe('eth_coinbase', () => { - testsForRpcMethodNotHandledByMiddleware('eth_coinbase', { - numberOfParameters: 0, - }); - }); - - describe('eth_estimateGas', () => { - testsForRpcMethodAssumingNoBlockParam('eth_estimateGas'); - }); - - describe('eth_feeHistory', () => { - testsForRpcMethodNotHandledByMiddleware('eth_feeHistory', { - numberOfParameters: 3, - }); - }); - - describe('eth_getBalance', () => { - testsForRpcMethodSupportingBlockParam('eth_getBalance', { - blockParamIndex: 1, - }); - }); - - describe('eth_gasPrice', () => { - testsForRpcMethodAssumingNoBlockParam('eth_gasPrice'); - }); - - describe('eth_getBlockByHash', () => { - testsForRpcMethodAssumingNoBlockParam('eth_getBlockByHash'); - }); - - describe('eth_getBlockByNumber', () => { - testsForRpcMethodSupportingBlockParam('eth_getBlockByNumber', { - blockParamIndex: 0, - }); - }); - - describe('eth_getBlockTransactionCountByHash', () => { - testsForRpcMethodAssumingNoBlockParam( - 'eth_getBlockTransactionCountByHash', - ); - }); - - describe('eth_getBlockTransactionCountByNumber', () => { - // NOTE: eth_getBlockTransactionCountByNumber does take a block param at - // the 0th index, but this is not handled by our cache middleware - // currently - testsForRpcMethodAssumingNoBlockParam( - 'eth_getBlockTransactionCountByNumber', - ); - }); - - describe('eth_getCode', () => { - testsForRpcMethodSupportingBlockParam('eth_getCode', { - blockParamIndex: 1, - }); - }); - - describe('eth_getFilterChanges', () => { - testsForRpcMethodNotHandledByMiddleware('eth_getFilterChanges', { - numberOfParameters: 1, - }); - }); - - describe('eth_getFilterLogs', () => { - testsForRpcMethodAssumingNoBlockParam('eth_getFilterLogs'); - }); - - describe('eth_getLogs', () => { - testsForRpcMethodNotHandledByMiddleware('eth_getLogs', { - numberOfParameters: 1, - }); - }); - - describe('eth_getStorageAt', () => { - testsForRpcMethodSupportingBlockParam('eth_getStorageAt', { - blockParamIndex: 2, - }); - }); - - describe('eth_getTransactionByBlockHashAndIndex', () => { - testsForRpcMethodAssumingNoBlockParam( - 'eth_getTransactionByBlockHashAndIndex', - ); - }); - - describe('eth_getTransactionByBlockNumberAndIndex', () => { - // NOTE: eth_getTransactionByBlockNumberAndIndex does take a block param - // at the 0th index, but this is not handled by our cache middleware - // currently - testsForRpcMethodAssumingNoBlockParam( - 'eth_getTransactionByBlockNumberAndIndex', - ); - }); - - describe('eth_getTransactionByHash', () => { - const method = 'eth_getTransactionByHash'; - - testsForRpcMethodsThatCheckForBlockHashInResponse(method); - - it("refreshes the block tracker's current block if it is less than the block number that comes back in the response", async () => { - await withMockedInfuraCommunications(async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest - // block number is retrieved through the block tracker first. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // This is our request. - comms.mockInfuraRpcCall({ - request, - response: { - result: { - blockNumber: '0x200', - }, - }, - }); - // The block-tracker-inspector middleware will request the latest - // block through the block tracker again. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x300' }); - - await withInfuraClient(async ({ makeRpcCall, blockTracker }) => { - await makeRpcCall(request); - expect(blockTracker.getCurrentBlock()).toStrictEqual('0x300'); - }); - }); - }); - }); - - describe('eth_getTransactionCount', () => { - testsForRpcMethodSupportingBlockParam('eth_getTransactionCount', { - blockParamIndex: 1, - }); - }); - - describe('eth_getTransactionReceipt', () => { - const method = 'eth_getTransactionReceipt'; - - testsForRpcMethodsThatCheckForBlockHashInResponse(method); - - it("refreshes the block tracker's current block if it is less than the block number that comes back in the response", async () => { - await withMockedInfuraCommunications(async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest - // block number is retrieved through the block tracker first. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // This is our request. - comms.mockInfuraRpcCall({ - request, - response: { - result: { - blockNumber: '0x200', - }, - }, - }); - // The block-tracker-inspector middleware will request the latest - // block through the block tracker again. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x300' }); - - await withInfuraClient(async ({ makeRpcCall, blockTracker }) => { - await makeRpcCall(request); - expect(blockTracker.getCurrentBlock()).toStrictEqual('0x300'); - }); - }); - }); - }); - - describe('eth_getUncleByBlockHashAndIndex', () => { - testsForRpcMethodAssumingNoBlockParam('eth_getUncleByBlockHashAndIndex'); - }); - - describe('eth_getUncleByBlockNumberAndIndex', () => { - // NOTE: eth_getUncleByBlockNumberAndIndex does take a block param at the - // 0th index, but this is not handled by our cache middleware currently - testsForRpcMethodAssumingNoBlockParam( - 'eth_getUncleByBlockNumberAndIndex', - ); - }); - - describe('eth_getUncleCountByBlockHash', () => { - testsForRpcMethodAssumingNoBlockParam('eth_getUncleCountByBlockHash'); - }); - - describe('eth_getUncleCountByBlockNumber', () => { - // NOTE: eth_getUncleCountByBlockNumber does take a block param at the 0th - // index, but this is not handled by our cache middleware currently - testsForRpcMethodAssumingNoBlockParam('eth_getUncleCountByBlockNumber'); - }); - - describe('eth_getWork', () => { - testsForRpcMethodNotHandledByMiddleware('eth_getWork', { - numberOfParameters: 0, - }); - }); - - describe('eth_hashrate', () => { - testsForRpcMethodNotHandledByMiddleware('eth_hashrate', { - numberOfParameters: 0, - }); - }); - - describe('eth_mining', () => { - testsForRpcMethodNotHandledByMiddleware('eth_mining', { - numberOfParameters: 0, - }); - }); - - describe('eth_newBlockFilter', () => { - testsForRpcMethodNotHandledByMiddleware('eth_newBlockFilter', { - numberOfParameters: 0, - }); - }); - - describe('eth_newFilter', () => { - testsForRpcMethodNotHandledByMiddleware('eth_newFilter', { - numberOfParameters: 1, - }); - }); - - describe('eth_newPendingTransactionFilter', () => { - testsForRpcMethodNotHandledByMiddleware( - 'eth_newPendingTransactionFilter', - { numberOfParameters: 0 }, - ); - }); - - describe('eth_protocolVersion', () => { - testsForRpcMethodAssumingNoBlockParam('eth_protocolVersion'); - }); - - describe('eth_sendRawTransaction', () => { - testsForRpcMethodNotHandledByMiddleware('eth_sendRawTransaction', { - numberOfParameters: 1, - }); - }); - - describe('eth_sendTransaction', () => { - testsForRpcMethodNotHandledByMiddleware('eth_sendTransaction', { - numberOfParameters: 1, - }); - }); - - describe('eth_sign', () => { - testsForRpcMethodNotHandledByMiddleware('eth_sign', { - numberOfParameters: 2, - }); - }); - - describe('eth_submitWork', () => { - testsForRpcMethodNotHandledByMiddleware('eth_submitWork', { - numberOfParameters: 3, - }); - }); - - describe('eth_syncing', () => { - testsForRpcMethodNotHandledByMiddleware('eth_syncing', { - numberOfParameters: 0, - }); - }); - - describe('eth_uninstallFilter', () => { - testsForRpcMethodNotHandledByMiddleware('eth_uninstallFilter', { - numberOfParameters: 1, - }); - }); - }); - - describe('RPC methods supported by Infura but not listed in the JSON-RPC spec', () => { - describe('eth_subscribe', () => { - testsForRpcMethodNotHandledByMiddleware('eth_subscribe', { - numberOfParameters: 1, - }); - }); - - describe('eth_unsubscribe', () => { - testsForRpcMethodNotHandledByMiddleware('eth_unsubscribe', { - numberOfParameters: 1, - }); - }); - - describe('net_listening', () => { - testsForRpcMethodNotHandledByMiddleware('net_listening', { - numberOfParameters: 0, - }); - }); - - describe('net_peerCount', () => { - testsForRpcMethodNotHandledByMiddleware('net_peerCount', { - numberOfParameters: 0, - }); - }); - - describe('net_version', () => { - it('does not hit Infura, instead returning the chain id that maps to the Infura network, as a decimal string', async () => { - const chainId = await withInfuraClient( - { network: 'goerli' }, - ({ makeRpcCall }) => { - return makeRpcCall({ - method: 'net_version', - }); - }, - ); - - expect(chainId).toStrictEqual('5'); - }); - }); - - describe('parity_nextNonce', () => { - testsForRpcMethodNotHandledByMiddleware('parity_nextNonce', { - numberOfParameters: 1, - }); - }); - - describe('web3_clientVersion', () => { - testsForRpcMethodAssumingNoBlockParam('web3_clientVersion'); - }); - }); - - // NOTE: The following methods are omitted because although they are listed in - // the Ethereum spec, they do not seem to be supported by Infura: - // - // - debug_getBadBlocks - // - debug_getRawBlock - // - debug_getRawHeader - // - debug_getRawReceipts - // - eth_createAccessList - // - eth_compileLLL - // - eth_compileSerpent - // - eth_compileSolidity - // - eth_getCompilers - // - eth_getProof - // - eth_maxPriorityFeePerGas - // - eth_submitHashrate - // - web3_sha3 - - testsForRpcMethodNotHandledByMiddleware('custom_rpc_method', { - numberOfParameters: 1, - }); + testsForProviderType('infura'); }); diff --git a/app/scripts/controllers/network/createJsonRpcClient.js b/app/scripts/controllers/network/createJsonRpcClient.js index d4e412b8f..bd5f648d3 100644 --- a/app/scripts/controllers/network/createJsonRpcClient.js +++ b/app/scripts/controllers/network/createJsonRpcClient.js @@ -10,22 +10,22 @@ import { import { PollingBlockTracker } from 'eth-block-tracker'; import { SECOND } from '../../../../shared/constants/time'; -const inTest = process.env.IN_TEST; -const blockTrackerOpts = inTest ? { pollingInterval: SECOND } : {}; -const getTestMiddlewares = () => { - return inTest ? [createEstimateGasDelayTestMiddleware()] : []; -}; - export default function createJsonRpcClient({ rpcUrl, chainId }) { + const blockTrackerOpts = process.env.IN_TEST + ? { pollingInterval: SECOND } + : {}; const fetchMiddleware = createFetchMiddleware({ rpcUrl }); const blockProvider = providerFromMiddleware(fetchMiddleware); const blockTracker = new PollingBlockTracker({ ...blockTrackerOpts, provider: blockProvider, }); + const testMiddlewares = process.env.IN_TEST + ? [createEstimateGasDelayTestMiddleware()] + : []; const networkMiddleware = mergeMiddleware([ - ...getTestMiddlewares(), + ...testMiddlewares, createChainIdMiddleware(chainId), createBlockRefRewriteMiddleware({ blockTracker }), createBlockCacheMiddleware({ blockTracker }), diff --git a/app/scripts/controllers/network/createJsonRpcClient.test.js b/app/scripts/controllers/network/createJsonRpcClient.test.js new file mode 100644 index 000000000..1c3443d25 --- /dev/null +++ b/app/scripts/controllers/network/createJsonRpcClient.test.js @@ -0,0 +1,5 @@ +import { testsForProviderType } from './provider-api-tests/shared-tests'; + +describe('createJsonRpcClient', () => { + testsForProviderType('custom'); +}); diff --git a/app/scripts/controllers/network/provider-api-tests/helpers.js b/app/scripts/controllers/network/provider-api-tests/helpers.js index 5e38ad967..38bfe0d87 100644 --- a/app/scripts/controllers/network/provider-api-tests/helpers.js +++ b/app/scripts/controllers/network/provider-api-tests/helpers.js @@ -4,37 +4,32 @@ import { JsonRpcEngine } from 'json-rpc-engine'; import { providerFromEngine } from 'eth-json-rpc-middleware'; import EthQuery from 'eth-query'; import createInfuraClient from '../createInfuraClient'; +import createJsonRpcClient from '../createJsonRpcClient'; /** * @typedef {import('nock').Scope} NockScope * - * A object returned by `nock(...)` for mocking requests to a particular base - * URL. + * A object returned by the `nock` function for mocking requests to a particular + * base URL. */ /** - * @typedef {{makeRpcCall: (request: Partial) => Promise, makeRpcCallsInSeries: (requests: Partial[]) => Promise}} InfuraClient + * @typedef {{blockTracker: import('eth-block-tracker').PollingBlockTracker, clock: sinon.SinonFakeTimers, makeRpcCall: (request: Partial) => Promise, makeRpcCallsInSeries: (requests: Partial[]) => Promise}} Client * * Provides methods to interact with the suite of middleware that - * `createInfuraClient` exposes. + * `createInfuraClient` or `createJsonRpcClient` exposes. */ /** - * @typedef {{network: string}} WithInfuraClientOptions + * @typedef {{providerType: "infura" | "custom", infuraNetwork?: string, customRpcUrl?: string, customChainId?: string}} WithClientOptions * - * The options bag that `withInfuraClient` takes. + * The options bag that `withNetworkClient` takes. */ /** - * @typedef {(client: InfuraClient) => Promise} WithInfuraClientCallback + * @typedef {(client: Client) => Promise} WithClientCallback * - * The callback that `withInfuraClient` takes. - */ - -/** - * @typedef {[WithInfuraClientOptions, WithInfuraClientCallback] | [WithInfuraClientCallback]} WithInfuraClientArgs - * - * The arguments to `withInfuraClient`. + * The callback that `withNetworkClient` takes. */ /** @@ -44,36 +39,47 @@ import createInfuraClient from '../createInfuraClient'; */ /** - * @typedef {{ nockScope: NockScope, request: object, response: object, delay?: number }} MockInfuraRpcCallOptions + * @typedef {{ nockScope: NockScope, request: object, response: object, delay?: number }} MockRpcCallOptions * - * The options to `mockInfuraRpcCall`. + * The options to `mockRpcCall`. */ /** - * @typedef {{mockNextBlockTrackerRequest: (options: Omit) => void, mockAllBlockTrackerRequests: (options: Omit) => void, mockInfuraRpcCall: (options: Omit) => NockScope}} InfuraCommunications + * @typedef {{mockNextBlockTrackerRequest: (options: Omit) => void, mockAllBlockTrackerRequests: (options: Omit) => void, mockRpcCall: (options: Omit) => NockScope, rpcUrl: string, infuraNetwork: string}} Communications * - * Provides methods to mock different kinds of requests to Infura. + * Provides methods to mock different kinds of requests to the provider. */ /** - * @typedef {{network: string}} WithMockedInfuraCommunicationsOptions + * @typedef {{providerType: 'infura' | 'custom', infuraNetwork?: string}} WithMockedCommunicationsOptions * - * The options bag that `mockingInfuraCommunications` takes. + * The options bag that `Communications` takes. */ /** - * @typedef {(comms: InfuraCommunications) => Promise} WithMockedInfuraCommunicationsCallback + * @typedef {(comms: Communications) => Promise} WithMockedCommunicationsCallback * - * The callback that `mockingInfuraCommunications` takes. + * The callback that `mockingCommunications` takes. */ /** - * @typedef {[WithMockedInfuraCommunicationsOptions, WithMockedInfuraCommunicationsCallback] | [WithMockedInfuraCommunicationsCallback]} WithMockedInfuraCommunicationsArgs - * - * The arguments to `mockingInfuraCommunications`. + * A dummy value for the `infuraProjectId` 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 MOCK_INFURA_PROJECT_ID = 'abc123'; -const INFURA_PROJECT_ID = 'abc123'; +/** + * A dummy value for the `rpcUrl` option that `createJsonRpcClient` needs. (This + * should not be hit during tests, but just in case, this should also not refer + * to a real Infura URL.) + */ +const MOCK_RPC_URL = 'http://foo.com'; + +/** + * A default value for the `eth_blockNumber` request that the block tracker + * makes. + */ const DEFAULT_LATEST_BLOCK_NUMBER = '0x42'; /** @@ -90,19 +96,16 @@ function debug(...args) { } /** - * Builds a Nock scope object for mocking requests to a particular network that - * Infura supports. + * Builds a Nock scope object for mocking provider requests. * - * @param {object} options - The options. - * @param {string} options.network - The Infura network you're testing with - * (default: "mainnet"). + * @param {string} rpcUrl - The URL of the RPC endpoint. * @returns {NockScope} The nock scope. */ -function buildScopeForMockingInfuraRequests({ network = 'mainnet' } = {}) { - return nock(`https://${network}.infura.io`).filteringRequestBody((body) => { +function buildScopeForMockingRequests(rpcUrl) { + return nock(rpcUrl).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 + // 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); }); @@ -121,7 +124,7 @@ async function mockNextBlockTrackerRequest({ nockScope, blockNumber = DEFAULT_LATEST_BLOCK_NUMBER, }) { - await mockInfuraRpcCall({ + await mockRpcCall({ nockScope, request: { method: 'eth_blockNumber', params: [] }, response: { result: blockNumber }, @@ -141,7 +144,7 @@ async function mockAllBlockTrackerRequests({ nockScope, blockNumber = DEFAULT_LATEST_BLOCK_NUMBER, }) { - await mockInfuraRpcCall({ + await mockRpcCall({ nockScope, request: { method: 'eth_blockNumber', params: [] }, response: { result: blockNumber }, @@ -149,9 +152,10 @@ async function mockAllBlockTrackerRequests({ } /** - * Mocks a JSON-RPC request sent to Infura with the given response. + * Mocks a JSON-RPC request sent to the provider with the given response. + * Provider type is inferred from the base url set on the nockScope. * - * @param {MockInfuraRpcCallOptions} args - The arguments. + * @param {MockRpcCallOptions} 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. @@ -169,14 +173,7 @@ async function mockAllBlockTrackerRequests({ * expected to be made. * @returns {NockScope} The nock scope. */ -function mockInfuraRpcCall({ - nockScope, - request, - response, - error, - delay, - times, -}) { +function mockRpcCall({ nockScope, request, response, error, delay, times }) { // 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; @@ -194,7 +191,10 @@ function mockInfuraRpcCall({ completeResponse = response.body; } } - let nockRequest = nockScope.post(`/v3/${INFURA_PROJECT_ID}`, { + const url = nockScope.basePath.includes('infura.io') + ? `/v3/${MOCK_INFURA_PROJECT_ID}` + : '/'; + let nockRequest = nockScope.post(url, { jsonrpc: '2.0', method, params, @@ -241,29 +241,46 @@ function makeRpcCall(ethQuery, request) { } /** - * Sets up request mocks for requests to Infura. + * Sets up request mocks for requests to the provider. * - * @param {WithMockedInfuraCommunicationsArgs} 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. + * @param {WithMockedCommunicationsOptions} options - An options bag. + * @param {"infura" | "custom"} options.providerType - The type of network + * client being tested. + * @param {string} [options.infuraNetwork] - The name of the Infura network being + * tested, assuming that `providerType` is "infura" (default: "mainnet"). + * @param {string} [options.customRpcUrl] - The URL of the custom RPC endpoint, + * assuming that `providerType` is "custom". + * @param {WithMockedCommunicationsCallback} fn - A function which will be + * called with an object that allows interaction with the network client. * @returns {Promise} 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; +export async function withMockedCommunications( + { providerType, infuraNetwork = 'mainnet', customRpcUrl = MOCK_RPC_URL }, + fn, +) { + if (providerType !== 'infura' && providerType !== 'custom') { + throw new Error( + `providerType must be either "infura" or "custom", was "${providerType}" instead`, + ); + } - const nockScope = buildScopeForMockingInfuraRequests({ network }); + const rpcUrl = + providerType === 'infura' + ? `https://${infuraNetwork}.infura.io` + : customRpcUrl; + const nockScope = buildScopeForMockingRequests(rpcUrl); const curriedMockNextBlockTrackerRequest = (localOptions) => mockNextBlockTrackerRequest({ nockScope, ...localOptions }); const curriedMockAllBlockTrackerRequests = (localOptions) => mockAllBlockTrackerRequests({ nockScope, ...localOptions }); - const curriedMockInfuraRpcCall = (localOptions) => - mockInfuraRpcCall({ nockScope, ...localOptions }); + const curriedMockRpcCall = (localOptions) => + mockRpcCall({ nockScope, ...localOptions }); const comms = { mockNextBlockTrackerRequest: curriedMockNextBlockTrackerRequest, mockAllBlockTrackerRequests: curriedMockAllBlockTrackerRequests, - mockInfuraRpcCall: curriedMockInfuraRpcCall, + mockRpcCall: curriedMockRpcCall, + rpcUrl, + infuraNetwork, }; try { @@ -275,25 +292,55 @@ export async function withMockedInfuraCommunications(...args) { } /** - * 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. + * Builds a provider from the middleware (for the provider type) 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. + * @param {WithClientOptions} options - An options bag. + * @param {"infura" | "custom"} options.providerType - The type of network + * client being tested. + * @param {string} [options.infuraNetwork] - The name of the Infura network being + * tested, assuming that `providerType` is "infura" (default: "mainnet"). + * @param {string} [options.customRpcUrl] - The URL of the custom RPC endpoint, + * assuming that `providerType` is "custom". + * @param {string} [options.customChainId] - The chain id belonging to the + * custom RPC endpoint, assuming that `providerType` is "custom" (default: + * "0x1"). + * @param {WithClientCallback} fn - A function which will be called with an + * object that allows interaction with the network client. * @returns {Promise} 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; +export async function withNetworkClient( + { + providerType, + infuraNetwork = 'mainnet', + customRpcUrl = MOCK_RPC_URL, + customChainId = '0x1', + }, + fn, +) { + if (providerType !== 'infura' && providerType !== 'custom') { + throw new Error( + `providerType must be either "infura" or "custom", was "${providerType}" instead`, + ); + } - const { networkMiddleware, blockTracker } = createInfuraClient({ - network, - projectId: INFURA_PROJECT_ID, - }); + // The JSON-RPC client wraps `eth_estimateGas` so that it takes 2 seconds longer + // than it usually would to complete. Or at least it should — this doesn't + // appear to be working correctly. Unset `IN_TEST` on `process.env` to prevent + // this behavior. + const inTest = process.env.IN_TEST; + delete process.env.IN_TEST; + const clientUnderTest = + providerType === 'infura' + ? createInfuraClient({ + network: infuraNetwork, + projectId: MOCK_INFURA_PROJECT_ID, + }) + : createJsonRpcClient({ rpcUrl: customRpcUrl, chainId: customChainId }); + process.env.IN_TEST = inTest; + + const { networkMiddleware, blockTracker } = clientUnderTest; const engine = new JsonRpcEngine(); engine.push(networkMiddleware); diff --git a/app/scripts/controllers/network/provider-api-tests/shared-tests.js b/app/scripts/controllers/network/provider-api-tests/shared-tests.js index 5dd95ef80..42c8aefb5 100644 --- a/app/scripts/controllers/network/provider-api-tests/shared-tests.js +++ b/app/scripts/controllers/network/provider-api-tests/shared-tests.js @@ -2,14 +2,57 @@ import { fill } from 'lodash'; import { - withMockedInfuraCommunications, - withInfuraClient, + withMockedCommunications, + withNetworkClient, buildMockParams, buildRequestWithReplacedBlockParam, } from './helpers'; const originalSetTimeout = setTimeout; +/** + * 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. + */ +function buildInfuraClientRetriesExhaustedErrorMessage(reason) { + 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. + */ +function buildJsonRpcEngineEmptyResponseErrorMessage(method) { + 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. + */ +function buildFetchFailedErrorMessage(url, reason) { + return new RegExp( + `^request to ${url}(/[^/ ]*)+ failed, reason: ${reason}`, + 'us', + ); +} + /** * Some middleware contain logic which retries the request if some condition * applies. This retrying always happens out of band via `setTimeout`, and @@ -54,33 +97,311 @@ async function waitForPromiseToBeFulfilledAfterRunningAllTimers( return promise; } +/** + * 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. + */ +/* eslint-disable-next-line jest/no-export */ +export function testsForProviderType(providerType) { + // Ethereum JSON-RPC spec: + // Infura documentation: + + describe('methods included in the Ethereum JSON-RPC spec', () => { + describe('methods not handled by middleware', () => { + const notHandledByMiddleware = [ + { name: 'eth_accounts', numberOfParameters: 0 }, + { name: 'eth_coinbase', numberOfParameters: 0 }, + { name: 'eth_feeHistory', numberOfParameters: 3 }, + { name: 'eth_getFilterChanges', numberOfParameters: 1 }, + { name: 'eth_getLogs', numberOfParameters: 1 }, + { name: 'eth_getWork', numberOfParameters: 0 }, + { name: 'eth_hashrate', numberOfParameters: 0 }, + { name: 'eth_mining', numberOfParameters: 0 }, + { name: 'eth_newBlockFilter', numberOfParameters: 0 }, + { name: 'eth_newFilter', numberOfParameters: 1 }, + { name: 'eth_newPendingTransactionFilter', numberOfParameters: 0 }, + { name: 'eth_sendRawTransaction', numberOfParameters: 1 }, + { name: 'eth_sendTransaction', numberOfParameters: 1 }, + { name: 'eth_sign', numberOfParameters: 2 }, + { name: 'eth_submitWork', numberOfParameters: 3 }, + { name: 'eth_syncing', numberOfParameters: 0 }, + { name: 'eth_uninstallFilter', numberOfParameters: 1 }, + ]; + notHandledByMiddleware.forEach(({ name, numberOfParameters }) => { + describe(`method name: ${name}`, () => { + testsForRpcMethodNotHandledByMiddleware(name, { + providerType, + numberOfParameters, + }); + }); + }); + }); + + describe('methods that have a param to specify the block', () => { + const supportingBlockParam = [ + { name: 'eth_call', blockParamIndex: 1 }, + { name: 'eth_getBalance', blockParamIndex: 1 }, + { name: 'eth_getBlockByNumber', blockParamIndex: 0 }, + { name: 'eth_getCode', blockParamIndex: 1 }, + { name: 'eth_getStorageAt', blockParamIndex: 2 }, + { name: 'eth_getTransactionCount', blockParamIndex: 1 }, + ]; + supportingBlockParam.forEach(({ name, blockParamIndex }) => { + describe(`method name: ${name}`, () => { + testsForRpcMethodSupportingBlockParam(name, { + providerType, + blockParamIndex, + }); + }); + }); + }); + + describe('methods that assume there is no block param', () => { + const assumingNoBlockParam = [ + 'eth_blockNumber', + 'eth_estimateGas', + 'eth_gasPrice', + 'eth_getBlockByHash', + // NOTE: eth_getBlockTransactionCountByNumber does take a block param at + // the 0th index, but this is not handled by our cache middleware + // currently + 'eth_getBlockTransactionCountByNumber', + // NOTE: eth_getTransactionByBlockNumberAndIndex does take a block param + // at the 0th index, but this is not handled by our cache middleware + // currently + 'eth_getTransactionByBlockNumberAndIndex', + 'eth_getBlockTransactionCountByHash', + 'eth_getFilterLogs', + 'eth_getTransactionByBlockHashAndIndex', + 'eth_getUncleByBlockHashAndIndex', + // NOTE: eth_getUncleByBlockNumberAndIndex does take a block param at + // the 0th index, but this is not handled by our cache middleware + // currently + 'eth_getUncleByBlockNumberAndIndex', + 'eth_getUncleCountByBlockHash', + // NOTE: eth_getUncleCountByBlockNumber does take a block param at the + // 0th index, but this is not handled by our cache middleware currently + 'eth_getUncleCountByBlockNumber', + ]; + assumingNoBlockParam.forEach((name) => + describe(`method name: ${name}`, () => { + testsForRpcMethodAssumingNoBlockParam(name, { providerType }); + }), + ); + }); + + describe('methods with block hashes in their result', () => { + const methodsWithBlockHashInResponse = [ + 'eth_getTransactionByHash', + 'eth_getTransactionReceipt', + ]; + methodsWithBlockHashInResponse.forEach((method) => { + describe(`method name: ${method}`, () => { + testsForRpcMethodsThatCheckForBlockHashInResponse(method, { + providerType, + }); + }); + }); + }); + + 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 = [ + { name: 'custom_rpc_method', numberOfParameters: 1 }, + { name: 'eth_subscribe', numberOfParameters: 1 }, + { name: 'eth_unsubscribe', numberOfParameters: 1 }, + { name: 'net_listening', numberOfParameters: 0 }, + { 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 = [ + 'eth_protocolVersion', + 'web3_clientVersion', + ]; + assumingNoBlockParam.forEach((name) => + describe(`method name: ${name}`, () => { + testsForRpcMethodAssumingNoBlockParam(name, { providerType }); + }), + ); + }); + + describe('other methods', () => { + describe('net_version', () => { + // The Infura middleware includes `net_version` in its scaffold + // middleware, whereas the custom RPC middleware does not. + if (providerType === 'infura') { + it('does not hit Infura, instead returning the network ID that maps to the Infura network, as a decimal string', async () => { + const 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'); + }, + ); + }); + } + }); + }); + }); +} + /** * Defines tests which exercise the behavior exhibited by an RPC method that - * does not support params (which affects how the method is cached). + * is not handled specially by the network client middleware. + * + * @param method - The name of the RPC method under test. + * @param additionalArgs - Additional arguments. + * @param additionalArgs.providerType - The type of provider being tested; + * either `infura` or `custom`. + * @param additionalArgs.numberOfParameters - The number of parameters that this + * RPC method takes. */ /* eslint-disable-next-line jest/no-export */ export function testsForRpcMethodNotHandledByMiddleware( method, - { numberOfParameters }, + { providerType, numberOfParameters }, ) { - it('attempts to pass the request off to Infura', async () => { + if (providerType !== 'infura' && providerType !== 'custom') { + throw new Error( + `providerType must be either "infura" or "custom", was "${providerType}" instead`, + ); + } + + it('attempts to pass the request off to the RPC endpoint', async () => { const request = { method, params: fill(Array(numberOfParameters), 'some value'), }; const expectedResult = 'the result'; - await withMockedInfuraCommunications(async (comms) => { + await withMockedCommunications({ providerType }, async (comms) => { // The first time a block-cacheable request is made, the latest block // number is retrieved through the block tracker first. It doesn't // matter what this is — it's just used as a cache key. comms.mockNextBlockTrackerRequest(); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request, response: { result: expectedResult }, }); - const actualResult = await withInfuraClient(({ makeRpcCall }) => - makeRpcCall(request), + const actualResult = await withNetworkClient( + { providerType }, + ({ makeRpcCall }) => makeRpcCall(request), ); expect(actualResult).toStrictEqual(expectedResult); @@ -94,58 +415,75 @@ export function testsForRpcMethodNotHandledByMiddleware( * parameter will not be used in determining how to cache the method. * * @param method - The name of the RPC method under test. + * @param additionalArgs - Additional arguments. + * @param additionalArgs.providerType - The type of provider being tested; + * either `infura` or `custom` (default: "infura"). */ -export function testsForRpcMethodAssumingNoBlockParam(method) { - it('does not hit Infura more than once for identical requests', async () => { +export function testsForRpcMethodAssumingNoBlockParam( + method, + { providerType }, +) { + if (providerType !== 'infura' && providerType !== 'custom') { + throw new Error( + `providerType must be either "infura" or "custom", was "${providerType}" instead`, + ); + } + + it('does not hit the RPC endpoint more than once for identical requests', async () => { const requests = [{ method }, { method }]; const mockResults = ['first result', 'second result']; - await withMockedInfuraCommunications(async (comms) => { + await withMockedCommunications({ providerType }, async (comms) => { // The first time a block-cacheable request is made, the latest block // number is retrieved through the block tracker first. It doesn't // matter what this is — it's just used as a cache key. comms.mockNextBlockTrackerRequest(); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[0], response: { result: mockResults[0] }, }); - const results = await withInfuraClient(({ makeRpcCallsInSeries }) => - makeRpcCallsInSeries(requests), + + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), ); expect(results).toStrictEqual([mockResults[0], mockResults[0]]); }); }); - it('hits Infura and does not reuse the result of a previous request if the latest block number was updated since', async () => { + it('hits the RPC endpoint and does not reuse the result of a previous request if the latest block number was updated since', async () => { const requests = [{ method }, { method }]; const mockResults = ['first result', 'second result']; - await withMockedInfuraCommunications(async (comms) => { + await withMockedCommunications({ providerType }, async (comms) => { // Note that we have to mock these requests in a specific order. The // first block tracker request occurs because of the first RPC request. // The second block tracker request, however, does not occur because of // the second RPC request, but rather because we call `clock.runAll()` // below. comms.mockNextBlockTrackerRequest({ blockNumber: '0x1' }); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[0], response: { result: mockResults[0] }, }); comms.mockNextBlockTrackerRequest({ blockNumber: '0x2' }); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[1], response: { result: mockResults[1] }, }); - const results = await withInfuraClient(async (client) => { - const firstResult = await client.makeRpcCall(requests[0]); - // Proceed to the next iteration of the block tracker so that a new - // block is fetched and the current block is updated. - client.clock.runAll(); - const secondResult = await client.makeRpcCall(requests[1]); - return [firstResult, secondResult]; - }); + const results = await withNetworkClient( + { providerType }, + async (client) => { + const firstResult = await client.makeRpcCall(requests[0]); + // Proceed to the next iteration of the block tracker so that a new + // block is fetched and the current block is updated. + client.clock.runAll(); + const secondResult = await client.makeRpcCall(requests[1]); + return [firstResult, secondResult]; + }, + ); expect(results).toStrictEqual(mockResults); }); @@ -157,22 +495,23 @@ export function testsForRpcMethodAssumingNoBlockParam(method) { const requests = [{ method }, { method }]; const mockResults = [emptyValue, 'some result']; - await withMockedInfuraCommunications(async (comms) => { + await withMockedCommunications({ providerType }, async (comms) => { // The first time a block-cacheable request is made, the latest block // number is retrieved through the block tracker first. It doesn't // matter what this is — it's just used as a cache key. comms.mockNextBlockTrackerRequest(); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[0], response: { result: mockResults[0] }, }); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[1], response: { result: mockResults[1] }, }); - const results = await withInfuraClient(({ makeRpcCallsInSeries }) => - makeRpcCallsInSeries(requests), + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), ); expect(results).toStrictEqual(mockResults); @@ -184,38 +523,41 @@ export function testsForRpcMethodAssumingNoBlockParam(method) { const requests = [{ method }, { method }, { method }]; const mockResults = ['first result', 'second result', 'third result']; - await withMockedInfuraCommunications(async (comms) => { + await withMockedCommunications({ providerType }, async (comms) => { // The first time a block-cacheable request is made, the latest block // number is retrieved through the block tracker first. It doesn't // matter what this is — it's just used as a cache key. comms.mockNextBlockTrackerRequest(); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[0], response: { result: mockResults[0] }, delay: 100, }); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[1], response: { result: mockResults[1] }, }); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[2], response: { result: mockResults[2] }, }); - const results = await withInfuraClient(async (client) => { - const resultPromises = [ - client.makeRpcCall(requests[0]), - client.makeRpcCall(requests[1]), - client.makeRpcCall(requests[2]), - ]; - const firstResult = await resultPromises[0]; - // The inflight cache middleware uses setTimeout to run the handlers, - // so run them now - client.clock.runAll(); - const remainingResults = await Promise.all(resultPromises.slice(1)); - return [firstResult, ...remainingResults]; - }); + const results = await withNetworkClient( + { providerType }, + async (client) => { + const resultPromises = [ + client.makeRpcCall(requests[0]), + client.makeRpcCall(requests[1]), + client.makeRpcCall(requests[2]), + ]; + const firstResult = await resultPromises[0]; + // The inflight cache middleware uses setTimeout to run the handlers, + // so run them now + client.clock.runAll(); + const remainingResults = await Promise.all(resultPromises.slice(1)); + return [firstResult, ...remainingResults]; + }, + ); expect(results).toStrictEqual([ mockResults[0], @@ -225,22 +567,23 @@ export function testsForRpcMethodAssumingNoBlockParam(method) { }); }); - it('throws a custom error if the request to Infura returns a 405 response', async () => { - await withMockedInfuraCommunications(async (comms) => { + it('throws a custom error if the request to the RPC endpoint returns a 405 response', async () => { + await withMockedCommunications({ providerType }, async (comms) => { const request = { method }; // The first time a block-cacheable request is made, the latest block // number is retrieved through the block tracker first. It doesn't // matter what this is — it's just used as a cache key. comms.mockNextBlockTrackerRequest(); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request, response: { httpStatus: 405, }, }); - const promiseForResult = withInfuraClient(async ({ makeRpcCall }) => - makeRpcCall(request), + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall }) => makeRpcCall(request), ); await expect(promiseForResult).rejects.toThrow( @@ -249,39 +592,122 @@ export function testsForRpcMethodAssumingNoBlockParam(method) { }); }); - it('throws a custom error if the request to Infura returns a 429 response', async () => { - await withMockedInfuraCommunications(async (comms) => { - const request = { method }; + // There is a difference in how we are testing the Infura middleware vs. the + // custom RPC middleware (or, more specifically, the fetch middleware) because + // of what both middleware treat as rate limiting errors. In this case, the + // fetch middleware treats a 418 response from the RPC endpoint as such an + // error, whereas to the Infura middleware, it is a 429 response. + if (providerType === 'infura') { + it('throws an undescriptive error if the request to the RPC endpoint returns a 418 response', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockInfuraRpcCall({ - request, - response: { - httpStatus: 429, - }, + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + response: { + httpStatus: 418, + }, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + '{"id":1,"jsonrpc":"2.0"}', + ); }); - const promiseForResult = withInfuraClient(async ({ makeRpcCall }) => - makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - 'Request is being rate limited', - ); }); - }); - it('throws a custom error if the request to Infura returns a response that is not 405, 429, 503, or 504', async () => { - await withMockedInfuraCommunications(async (comms) => { + it('throws an error with a custom message if the request to the RPC endpoint returns a 429 response', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + response: { + httpStatus: 429, + }, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + 'Request is being rate limited', + ); + }); + }); + } else { + it('throws a custom error if the request to the RPC endpoint returns a 418 response', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + response: { + httpStatus: 418, + }, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + 'Request is being rate limited.', + ); + }); + }); + + it('throws an undescriptive error if the request to the RPC endpoint returns a 429 response', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + response: { + httpStatus: 429, + }, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + "Non-200 status code: '429'", + ); + }); + }); + } + + it('throws a generic, undescriptive error if the request to the RPC endpoint returns a response that is not 405, 418, 429, 503, or 504', async () => { + await withMockedCommunications({ providerType }, async (comms) => { const request = { method }; // The first time a block-cacheable request is made, the latest block // number is retrieved through the block tracker first. It doesn't // matter what this is — it's just used as a cache key. comms.mockNextBlockTrackerRequest(); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request, response: { id: 12345, @@ -290,19 +716,22 @@ export function testsForRpcMethodAssumingNoBlockParam(method) { httpStatus: 420, }, }); - const promiseForResult = withInfuraClient(async ({ makeRpcCall }) => - makeRpcCall(request), + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall }) => makeRpcCall(request), ); - await expect(promiseForResult).rejects.toThrow( - '{"id":12345,"jsonrpc":"2.0","error":"some error"}', - ); + const errorMessage = + providerType === 'infura' + ? '{"id":12345,"jsonrpc":"2.0","error":"some error"}' + : "Non-200 status code: '420'"; + await expect(promiseForResult).rejects.toThrow(errorMessage); }); }); [503, 504].forEach((httpStatus) => { - it(`retries the request to Infura up to 5 times if it returns a ${httpStatus} response, returning the successful result if there is one on the 5th try`, async () => { - await withMockedInfuraCommunications(async (comms) => { + it(`retries the request to the RPC endpoint up to 5 times if it returns a ${httpStatus} response, returning the successful result if there is one on the 5th try`, async () => { + await withMockedCommunications({ providerType }, async (comms) => { const request = { method }; // The first time a block-cacheable request is made, the latest block @@ -311,7 +740,7 @@ export function testsForRpcMethodAssumingNoBlockParam(method) { comms.mockNextBlockTrackerRequest(); // Here we have the request fail for the first 4 tries, then succeed // on the 5th try. - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request, response: { error: 'Some error', @@ -319,14 +748,15 @@ export function testsForRpcMethodAssumingNoBlockParam(method) { }, times: 4, }); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request, response: { result: 'the result', httpStatus: 200, }, }); - const result = await withInfuraClient( + const result = await withNetworkClient( + { providerType }, async ({ makeRpcCall, clock }) => { return await waitForPromiseToBeFulfilledAfterRunningAllTimers( makeRpcCall(request), @@ -339,15 +769,15 @@ export function testsForRpcMethodAssumingNoBlockParam(method) { }); }); - it(`causes a request to fail with a custom error if the request to Infura returns a ${httpStatus} response 5 times in a row`, async () => { - await withMockedInfuraCommunications(async (comms) => { + it(`causes a request to fail with a custom error if the request to the RPC endpoint returns a ${httpStatus} response 5 times in a row`, async () => { + await withMockedCommunications({ providerType }, async (comms) => { const request = { method }; // The first time a block-cacheable request is made, the latest block // number is retrieved through the block tracker first. It doesn't // matter what this is — it's just used as a cache key. comms.mockNextBlockTrackerRequest(); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request, response: { error: 'Some error', @@ -355,7 +785,82 @@ export function testsForRpcMethodAssumingNoBlockParam(method) { }, times: 5, }); - const promiseForResult = withInfuraClient( + comms.mockNextBlockTrackerRequest(); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + const err = + providerType === 'infura' + ? buildInfuraClientRetriesExhaustedErrorMessage('Gateway timeout') + : buildJsonRpcEngineEmptyResponseErrorMessage(method); + await expect(promiseForResult).rejects.toThrow(err); + }); + }); + }); + + it('retries the request to the RPC endpoint up to 5 times if an "ETIMEDOUT" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + // Here we have the request fail for the first 4 tries, then succeed + // on the 5th try. + comms.mockRpcCall({ + request, + error: 'ETIMEDOUT: Some message', + times: 4, + }); + comms.mockRpcCall({ + request, + response: { + result: 'the result', + httpStatus: 200, + }, + }); + + const result = await withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + expect(result).toStrictEqual('the result'); + }); + }); + + // Both the Infura and fetch middleware detect ETIMEDOUT errors and will + // automatically retry the request to the RPC endpoint in question, but both + // produce a different error if the number of retries is exhausted. + if (providerType === 'infura') { + it('causes a request to fail with a custom error if an "ETIMEDOUT" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const errorMessage = 'ETIMEDOUT: Some message'; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + error: errorMessage, + times: 5, + }); + const promiseForResult = withNetworkClient( + { providerType }, async ({ makeRpcCall, clock }) => { return await waitForPromiseToBeFulfilledAfterRunningAllTimers( makeRpcCall(request), @@ -365,15 +870,48 @@ export function testsForRpcMethodAssumingNoBlockParam(method) { ); await expect(promiseForResult).rejects.toThrow( - /^InfuraProvider - cannot complete request\. All retries exhausted\..+Gateway timeout/su, + buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), ); }); }); - }); + } else { + it('returns an empty response if an "ETIMEDOUT" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const errorMessage = 'ETIMEDOUT: Some message'; - ['ETIMEDOUT', 'ECONNRESET', 'SyntaxError'].forEach((errorMessagePrefix) => { - it(`retries the request to Infura up to 5 times if an "${errorMessagePrefix}" error is thrown while making the request, returning the successful result if there is one on the 5th try`, async () => { - await withMockedInfuraCommunications(async (comms) => { + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + error: errorMessage, + times: 5, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + await expect(promiseForResult).rejects.toThrow( + buildJsonRpcEngineEmptyResponseErrorMessage(method), + ); + }); + }); + } + + // The Infura middleware treats a response that contains an ECONNRESET message + // as an innocuous error that is likely to disappear on a retry. The custom + // RPC middleware, on the other hand, does not specially handle this error. + if (providerType === 'infura') { + it('retries the request to the RPC endpoint up to 5 times if an "ECONNRESET" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { + await withMockedCommunications({ providerType }, async (comms) => { const request = { method }; // The first time a block-cacheable request is made, the latest block @@ -382,19 +920,21 @@ export function testsForRpcMethodAssumingNoBlockParam(method) { comms.mockNextBlockTrackerRequest(); // Here we have the request fail for the first 4 tries, then succeed // on the 5th try. - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request, - error: `${errorMessagePrefix}: Some message`, + error: 'ECONNRESET: Some message', times: 4, }); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request, response: { result: 'the result', httpStatus: 200, }, }); - const result = await withInfuraClient( + + const result = await withNetworkClient( + { providerType }, async ({ makeRpcCall, clock }) => { return await waitForPromiseToBeFulfilledAfterRunningAllTimers( makeRpcCall(request), @@ -407,20 +947,22 @@ export function testsForRpcMethodAssumingNoBlockParam(method) { }); }); - it(`causes a request to fail with a custom error if an "${errorMessagePrefix}" error is thrown while making the request to Infura 5 times in a row`, async () => { - await withMockedInfuraCommunications(async (comms) => { + it('causes a request to fail with a custom error if an "ECONNRESET" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { const request = { method }; + const errorMessage = 'ECONNRESET: Some message'; // The first time a block-cacheable request is made, the latest block // number is retrieved through the block tracker first. It doesn't // matter what this is — it's just used as a cache key. comms.mockNextBlockTrackerRequest(); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request, - error: `${errorMessagePrefix}: Some message`, + error: errorMessage, times: 5, }); - const promiseForResult = withInfuraClient( + const promiseForResult = withNetworkClient( + { providerType }, async ({ makeRpcCall, clock }) => { return await waitForPromiseToBeFulfilledAfterRunningAllTimers( makeRpcCall(request), @@ -430,14 +972,332 @@ export function testsForRpcMethodAssumingNoBlockParam(method) { ); await expect(promiseForResult).rejects.toThrow( - new RegExp( - `^InfuraProvider - cannot complete request\\. All retries exhausted\\..+${errorMessagePrefix}: Some message`, - 'su', - ), + buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), ); }); }); - }); + } else { + it('does not retry the request to the RPC endpoint, but throws immediately, if an "ECONNRESET" error is thrown while making the request', async () => { + const customRpcUrl = 'http://example.com'; + + await withMockedCommunications( + { providerType, customRpcUrl }, + async (comms) => { + const request = { method }; + const errorMessage = 'ECONNRESET: Some message'; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + error: errorMessage, + }); + const promiseForResult = withNetworkClient( + { providerType, customRpcUrl }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + buildFetchFailedErrorMessage(customRpcUrl, errorMessage), + ); + }, + ); + }); + } + + // Both the Infura and fetch middleware will attempt to parse the response + // body as JSON, and if this step produces an error, both middleware will also + // attempt to retry the request. However, this error handling code is slightly + // different between the two. As the error in this case is a SyntaxError, the + // Infura middleware will catch it immediately, whereas the custom RPC + // middleware will catch it and re-throw a separate error, which it then + // catches later. + if (providerType === 'infura') { + it('retries the request to the RPC endpoint up to 5 times if an "SyntaxError" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + // Here we have the request fail for the first 4 tries, then succeed + // on the 5th try. + comms.mockRpcCall({ + request, + error: 'SyntaxError: Some message', + times: 4, + }); + comms.mockRpcCall({ + request, + response: { + result: 'the result', + httpStatus: 200, + }, + }); + + const result = await withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + expect(result).toStrictEqual('the result'); + }); + }); + + it('causes a request to fail with a custom error if an "SyntaxError" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const errorMessage = 'SyntaxError: Some message'; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + error: errorMessage, + times: 5, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + await expect(promiseForResult).rejects.toThrow( + buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), + ); + }); + }); + + it('does not retry the request to the RPC endpoint, but throws immediately, if a "failed to parse response body" error is thrown while making the request', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const errorMessage = 'failed to parse response body: some message'; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + error: errorMessage, + }); + const promiseForResult = withNetworkClient( + { providerType, infuraNetwork: comms.infuraNetwork }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + buildFetchFailedErrorMessage(comms.rpcUrl, errorMessage), + ); + }); + }); + } else { + it('does not retry the request to the RPC endpoint, but throws immediately, if a "SyntaxError" error is thrown while making the request', async () => { + const customRpcUrl = 'http://example.com'; + + await withMockedCommunications( + { providerType, customRpcUrl }, + async (comms) => { + const request = { method }; + const errorMessage = 'SyntaxError: Some message'; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + error: errorMessage, + }); + const promiseForResult = withNetworkClient( + { providerType, customRpcUrl }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + buildFetchFailedErrorMessage(customRpcUrl, errorMessage), + ); + }, + ); + }); + + it('retries the request to the RPC endpoint up to 5 times if a "failed to parse response body" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + // Here we have the request fail for the first 4 tries, then succeed + // on the 5th try. + comms.mockRpcCall({ + request, + error: 'failed to parse response body: some message', + times: 4, + }); + comms.mockRpcCall({ + request, + response: { + result: 'the result', + httpStatus: 200, + }, + }); + + const result = await withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + expect(result).toStrictEqual('the result'); + }); + }); + + it('returns an empty response if a "failed to parse response body" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const errorMessage = 'failed to parse response body: some message'; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + error: errorMessage, + times: 5, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + await expect(promiseForResult).rejects.toThrow( + buildJsonRpcEngineEmptyResponseErrorMessage(method), + ); + }); + }); + } + + // Only the custom RPC middleware will detect a "Failed to fetch" error and + // attempt to retry the request to the RPC endpoint; the Infura middleware + // does not. + if (providerType === 'infura') { + it('does not retry the request to the RPC endpoint, but throws immediately, if a "Failed to fetch" error is thrown while making the request', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const errorMessage = 'Failed to fetch: some message'; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + error: errorMessage, + }); + const promiseForResult = withNetworkClient( + { providerType, infuraNetwork: comms.infuraNetwork }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + buildFetchFailedErrorMessage(comms.rpcUrl, errorMessage), + ); + }); + }); + } else { + it('retries the request to the RPC endpoint up to 5 times if a "Failed to fetch" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + // Here we have the request fail for the first 4 tries, then succeed + // on the 5th try. + comms.mockRpcCall({ + request, + error: 'Failed to fetch: some message', + times: 4, + }); + comms.mockRpcCall({ + request, + response: { + result: 'the result', + httpStatus: 200, + }, + }); + + const result = await withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + expect(result).toStrictEqual('the result'); + }); + }); + + it('returns an empty response if a "Failed to fetch" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const errorMessage = 'Failed to fetch: some message'; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + error: errorMessage, + times: 5, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + await expect(promiseForResult).rejects.toThrow( + buildJsonRpcEngineEmptyResponseErrorMessage(method), + ); + }); + }); + } } /** @@ -446,59 +1306,75 @@ export function testsForRpcMethodAssumingNoBlockParam(method) { * cacheable. * * @param method - The name of the RPC method under test. + * @param additionalArgs - Additional arguments. + * @param additionalArgs.providerType - The type of provider being tested; + * either `infura` or `custom` (default: "infura"). */ -export function testsForRpcMethodsThatCheckForBlockHashInResponse(method) { - it('does not hit Infura more than once for identical requests and it has a valid blockHash', async () => { - const requests = [{ method }, { method }]; - const mockResults = [{ blockHash: '0x100' }, { blockHash: '0x200' }]; +export function testsForRpcMethodsThatCheckForBlockHashInResponse( + method, + { providerType }, +) { + if (providerType !== 'infura' && providerType !== 'custom') { + throw new Error( + `providerType must be either "infura" or "custom", was "${providerType}" instead`, + ); + } - await withMockedInfuraCommunications(async (comms) => { + it('does not hit the RPC endpoint more than once for identical requests and it has a valid blockHash', async () => { + const requests = [{ method }, { method }]; + const mockResult = { blockHash: '0x100' }; + + await withMockedCommunications({ providerType }, async (comms) => { // The first time a block-cacheable request is made, the latest block // number is retrieved through the block tracker first. It doesn't // matter what this is — it's just used as a cache key. comms.mockNextBlockTrackerRequest(); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[0], - response: { result: mockResults[0] }, + response: { result: mockResult }, }); - const results = await withInfuraClient(({ makeRpcCallsInSeries }) => - makeRpcCallsInSeries(requests), + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), ); - expect(results).toStrictEqual([mockResults[0], mockResults[0]]); + expect(results).toStrictEqual([mockResult, mockResult]); }); }); - it('hits Infura and does not reuse the result of a previous request if the latest block number was updated since', async () => { + it('hits the RPC endpoint and does not reuse the result of a previous request if the latest block number was updated since', async () => { const requests = [{ method }, { method }]; const mockResults = [{ blockHash: '0x100' }, { blockHash: '0x200' }]; - await withMockedInfuraCommunications(async (comms) => { + await withMockedCommunications({ providerType }, async (comms) => { // Note that we have to mock these requests in a specific order. The // first block tracker request occurs because of the first RPC // request. The second block tracker request, however, does not occur // because of the second RPC request, but rather because we call // `clock.runAll()` below. comms.mockNextBlockTrackerRequest({ blockNumber: '0x1' }); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[0], response: { result: mockResults[0] }, }); comms.mockNextBlockTrackerRequest({ blockNumber: '0x2' }); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[1], response: { result: mockResults[1] }, }); - const results = await withInfuraClient(async (client) => { - const firstResult = await client.makeRpcCall(requests[0]); - // Proceed to the next iteration of the block tracker so that a new - // block is fetched and the current block is updated. - client.clock.runAll(); - const secondResult = await client.makeRpcCall(requests[1]); - return [firstResult, secondResult]; - }); + const results = await withNetworkClient( + { providerType }, + async (client) => { + const firstResult = await client.makeRpcCall(requests[0]); + // Proceed to the next iteration of the block tracker so that a new + // block is fetched and the current block is updated. + client.clock.runAll(); + const secondResult = await client.makeRpcCall(requests[1]); + return [firstResult, secondResult]; + }, + ); expect(results).toStrictEqual(mockResults); }); @@ -510,25 +1386,25 @@ export function testsForRpcMethodsThatCheckForBlockHashInResponse(method) { const requests = [{ method }, { method }]; const mockResults = [emptyValue, { blockHash: '0x100' }]; - await withMockedInfuraCommunications(async (comms) => { + await withMockedCommunications({ providerType }, async (comms) => { // The first time a block-cacheable request is made, the latest block // number is retrieved through the block tracker first. It doesn't // matter what this is — it's just used as a cache key. comms.mockNextBlockTrackerRequest(); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[0], response: { result: mockResults[0] }, }); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[1], response: { result: mockResults[1] }, }); - const results = await withInfuraClient(({ makeRpcCallsInSeries }) => - makeRpcCallsInSeries(requests), + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), ); - // TODO: Does this work? expect(results).toStrictEqual(mockResults); }); }, @@ -541,22 +1417,23 @@ export function testsForRpcMethodsThatCheckForBlockHashInResponse(method) { { blockHash: '0x100', extra: 'some other value' }, ]; - await withMockedInfuraCommunications(async (comms) => { + await withMockedCommunications({ providerType }, async (comms) => { // The first time a block-cacheable request is made, the latest block // number is retrieved through the block tracker first. It doesn't // matter what this is — it's just used as a cache key. comms.mockNextBlockTrackerRequest(); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[0], response: { result: mockResults[0] }, }); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[1], response: { result: mockResults[1] }, }); - const results = await withInfuraClient(({ makeRpcCallsInSeries }) => - makeRpcCallsInSeries(requests), + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), ); expect(results).toStrictEqual(mockResults); @@ -570,22 +1447,23 @@ export function testsForRpcMethodsThatCheckForBlockHashInResponse(method) { { blockHash: '0x100', extra: 'some other value' }, ]; - await withMockedInfuraCommunications(async (comms) => { + await withMockedCommunications({ providerType }, async (comms) => { // The first time a block-cacheable request is made, the latest block // number is retrieved through the block tracker first. It doesn't // matter what this is — it's just used as a cache key. comms.mockNextBlockTrackerRequest(); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[0], response: { result: mockResults[0] }, }); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[1], response: { result: mockResults[1] }, }); - const results = await withInfuraClient(({ makeRpcCallsInSeries }) => - makeRpcCallsInSeries(requests), + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), ); expect(results).toStrictEqual(mockResults); @@ -603,22 +1481,23 @@ export function testsForRpcMethodsThatCheckForBlockHashInResponse(method) { { blockHash: '0x100', extra: 'some other value' }, ]; - await withMockedInfuraCommunications(async (comms) => { + await withMockedCommunications({ providerType }, async (comms) => { // The first time a block-cacheable request is made, the latest block // number is retrieved through the block tracker first. It doesn't // matter what this is — it's just used as a cache key. comms.mockNextBlockTrackerRequest(); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[0], response: { result: mockResults[0] }, }); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[1], response: { result: mockResults[1] }, }); - const results = await withInfuraClient(({ makeRpcCallsInSeries }) => - makeRpcCallsInSeries(requests), + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), ); expect(results).toStrictEqual(mockResults); @@ -631,17 +1510,22 @@ export function testsForRpcMethodsThatCheckForBlockHashInResponse(method) { * takes a block parameter. The value of this parameter can be either a block * number or a block tag ("latest", "earliest", or "pending") and affects how * the method is cached. + * + * @param method - The name of the RPC method under test. + * @param additionalArgs - Additional arguments. + * @param additionalArgs.providerType - The type of provider being tested; + * either `infura` or `custom` (default: "infura"). */ /* eslint-disable-next-line jest/no-export */ export function testsForRpcMethodSupportingBlockParam( method, - { blockParamIndex }, + { providerType, blockParamIndex }, ) { describe.each([ ['given no block tag', undefined], ['given a block tag of "latest"', 'latest'], ])('%s', (_desc, blockParam) => { - it('does not hit Infura more than once for identical requests', async () => { + it('does not hit the RPC endpoint more than once for identical requests', async () => { const requests = [ { method, @@ -651,7 +1535,7 @@ export function testsForRpcMethodSupportingBlockParam( ]; const mockResults = ['first result', 'second result']; - await withMockedInfuraCommunications(async (comms) => { + await withMockedCommunications({ providerType }, async (comms) => { // The first time a block-cacheable request is made, the block-cache // middleware will request the latest block number through the block // tracker to determine the cache key. Later, the block-ref @@ -662,7 +1546,7 @@ export function testsForRpcMethodSupportingBlockParam( // The block-ref middleware will make the request as specified // except that the block param is replaced with the latest block // number. - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: buildRequestWithReplacedBlockParam( requests[0], blockParamIndex, @@ -671,22 +1555,23 @@ export function testsForRpcMethodSupportingBlockParam( response: { result: mockResults[0] }, }); - const results = await withInfuraClient(({ makeRpcCallsInSeries }) => - makeRpcCallsInSeries(requests), + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), ); expect(results).toStrictEqual([mockResults[0], mockResults[0]]); }); }); - it('hits Infura and does not reuse the result of a previous request if the latest block number was updated since', async () => { + it('hits the RPC endpoint and does not reuse the result of a previous request if the latest block number was updated since', async () => { const requests = [ { method, params: buildMockParams({ blockParamIndex, blockParam }) }, { method, params: buildMockParams({ blockParamIndex, blockParam }) }, ]; const mockResults = ['first result', 'second result']; - await withMockedInfuraCommunications(async (comms) => { + await withMockedCommunications({ providerType }, async (comms) => { // Note that we have to mock these requests in a specific order. // The first block tracker request occurs because of the first RPC // request. The second block tracker request, however, does not @@ -696,7 +1581,7 @@ export function testsForRpcMethodSupportingBlockParam( // The block-ref middleware will make the request as specified // except that the block param is replaced with the latest block // number. - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: buildRequestWithReplacedBlockParam( requests[0], blockParamIndex, @@ -705,7 +1590,7 @@ export function testsForRpcMethodSupportingBlockParam( response: { result: mockResults[0] }, }); comms.mockNextBlockTrackerRequest({ blockNumber: '0x200' }); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: buildRequestWithReplacedBlockParam( requests[1], blockParamIndex, @@ -714,14 +1599,17 @@ export function testsForRpcMethodSupportingBlockParam( response: { result: mockResults[1] }, }); - const results = await withInfuraClient(async (client) => { - const firstResult = await client.makeRpcCall(requests[0]); - // Proceed to the next iteration of the block tracker so that a - // new block is fetched and the current block is updated. - client.clock.runAll(); - const secondResult = await client.makeRpcCall(requests[1]); - return [firstResult, secondResult]; - }); + const results = await withNetworkClient( + { providerType }, + async (client) => { + const firstResult = await client.makeRpcCall(requests[0]); + // Proceed to the next iteration of the block tracker so that a + // new block is fetched and the current block is updated. + client.clock.runAll(); + const secondResult = await client.makeRpcCall(requests[1]); + return [firstResult, secondResult]; + }, + ); expect(results).toStrictEqual(mockResults); }); @@ -736,7 +1624,7 @@ export function testsForRpcMethodSupportingBlockParam( ]; const mockResults = [emptyValue, 'some result']; - await withMockedInfuraCommunications(async (comms) => { + await withMockedCommunications({ providerType }, async (comms) => { // The first time a block-cacheable request is made, the // block-cache middleware will request the latest block number // through the block tracker to determine the cache key. Later, @@ -747,7 +1635,7 @@ export function testsForRpcMethodSupportingBlockParam( // The block-ref middleware will make the request as specified // except that the block param is replaced with the latest block // number. - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: buildRequestWithReplacedBlockParam( requests[0], blockParamIndex, @@ -755,7 +1643,7 @@ export function testsForRpcMethodSupportingBlockParam( ), response: { result: mockResults[0] }, }); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: buildRequestWithReplacedBlockParam( requests[1], blockParamIndex, @@ -764,8 +1652,9 @@ export function testsForRpcMethodSupportingBlockParam( response: { result: mockResults[1] }, }); - const results = await withInfuraClient(({ makeRpcCallsInSeries }) => - makeRpcCallsInSeries(requests), + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), ); expect(results).toStrictEqual(mockResults); @@ -777,7 +1666,7 @@ export function testsForRpcMethodSupportingBlockParam( const requests = [{ method }, { method }, { method }]; const mockResults = ['first result', 'second result', 'third result']; - await withMockedInfuraCommunications(async (comms) => { + await withMockedCommunications({ providerType }, async (comms) => { // The first time a block-cacheable request is made, the // block-cache middleware will request the latest block number // through the block tracker to determine the cache key. Later, @@ -788,7 +1677,7 @@ export function testsForRpcMethodSupportingBlockParam( // The block-ref middleware will make the request as specified // except that the block param is replaced with the latest block // number, and we delay it. - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ delay: 100, request: buildRequestWithReplacedBlockParam( requests[0], @@ -798,7 +1687,7 @@ export function testsForRpcMethodSupportingBlockParam( response: { result: mockResults[0] }, }); // The previous two requests will happen again, in the same order. - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: buildRequestWithReplacedBlockParam( requests[1], blockParamIndex, @@ -806,7 +1695,7 @@ export function testsForRpcMethodSupportingBlockParam( ), response: { result: mockResults[1] }, }); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: buildRequestWithReplacedBlockParam( requests[2], blockParamIndex, @@ -815,19 +1704,22 @@ export function testsForRpcMethodSupportingBlockParam( response: { result: mockResults[2] }, }); - const results = await withInfuraClient(async (client) => { - const resultPromises = [ - client.makeRpcCall(requests[0]), - client.makeRpcCall(requests[1]), - client.makeRpcCall(requests[2]), - ]; - const firstResult = await resultPromises[0]; - // The inflight cache middleware uses setTimeout to run the - // handlers, so run them now - client.clock.runAll(); - const remainingResults = await Promise.all(resultPromises.slice(1)); - return [firstResult, ...remainingResults]; - }); + const results = await withNetworkClient( + { providerType }, + async (client) => { + const resultPromises = [ + client.makeRpcCall(requests[0]), + client.makeRpcCall(requests[1]), + client.makeRpcCall(requests[2]), + ]; + const firstResult = await resultPromises[0]; + // The inflight cache middleware uses setTimeout to run the + // handlers, so run them now + client.clock.runAll(); + const remainingResults = await Promise.all(resultPromises.slice(1)); + return [firstResult, ...remainingResults]; + }, + ); expect(results).toStrictEqual([ mockResults[0], @@ -837,8 +1729,8 @@ export function testsForRpcMethodSupportingBlockParam( }); }); - it('throws a custom error if the request to Infura returns a 405 response', async () => { - await withMockedInfuraCommunications(async (comms) => { + it('throws an error with a custom message if the request to the RPC endpoint returns a 405 response', async () => { + await withMockedCommunications({ providerType }, async (comms) => { const request = { method }; // The first time a block-cacheable request is made, the @@ -851,7 +1743,7 @@ export function testsForRpcMethodSupportingBlockParam( // The block-ref middleware will make the request as specified // except that the block param is replaced with the latest block // number. - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: buildRequestWithReplacedBlockParam( request, blockParamIndex, @@ -861,8 +1753,9 @@ export function testsForRpcMethodSupportingBlockParam( httpStatus: 405, }, }); - const promiseForResult = withInfuraClient(async ({ makeRpcCall }) => - makeRpcCall(request), + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall }) => makeRpcCall(request), ); await expect(promiseForResult).rejects.toThrow( @@ -871,42 +1764,155 @@ export function testsForRpcMethodSupportingBlockParam( }); }); - it('throws a custom error if the request to Infura returns a 429 response', async () => { - await withMockedInfuraCommunications(async (comms) => { - const request = { method }; + // There is a difference in how we are testing the Infura middleware vs. the + // custom RPC middleware (or, more specifically, the fetch middleware) + // because of what both middleware treat as rate limiting errors. In this + // case, the fetch middleware treats a 418 response from the RPC endpoint as + // such an error, whereas to the Infura middleware, it is a 429 response. + if (providerType === 'infura') { + it('throws a generic, undescriptive error if the request to the RPC endpoint returns a 418 response', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockInfuraRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - httpStatus: 429, - }, + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + httpStatus: 418, + }, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + '{"id":1,"jsonrpc":"2.0"}', + ); }); - const promiseForResult = withInfuraClient(async ({ makeRpcCall }) => - makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - 'Request is being rate limited', - ); }); - }); - it('throws a custom error if the request to Infura returns a response that is not 405, 429, 503, or 504', async () => { - await withMockedInfuraCommunications(async (comms) => { + it('throws an error with a custom message if the request to the RPC endpoint returns a 429 response', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + httpStatus: 429, + }, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + 'Request is being rate limited', + ); + }); + }); + } else { + it('throws an error with a custom message if the request to the RPC endpoint returns a 418 response', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + httpStatus: 418, + }, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + 'Request is being rate limited.', + ); + }); + }); + + it('throws an undescriptive error if the request to the RPC endpoint returns a 429 response', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + httpStatus: 429, + }, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + "Non-200 status code: '429'", + ); + }); + }); + } + + it('throws an undescriptive error message if the request to the RPC endpoint returns a response that is not 405, 418, 429, 503, or 504', async () => { + await withMockedCommunications({ providerType }, async (comms) => { const request = { method }; // The first time a block-cacheable request is made, the @@ -919,7 +1925,7 @@ export function testsForRpcMethodSupportingBlockParam( // The block-ref middleware will make the request as specified // except that the block param is replaced with the latest block // number. - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: buildRequestWithReplacedBlockParam( request, blockParamIndex, @@ -932,19 +1938,22 @@ export function testsForRpcMethodSupportingBlockParam( httpStatus: 420, }, }); - const promiseForResult = withInfuraClient(async ({ makeRpcCall }) => - makeRpcCall(request), + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall }) => makeRpcCall(request), ); - await expect(promiseForResult).rejects.toThrow( - '{"id":12345,"jsonrpc":"2.0","error":"some error"}', - ); + const msg = + providerType === 'infura' + ? '{"id":12345,"jsonrpc":"2.0","error":"some error"}' + : "Non-200 status code: '420'"; + await expect(promiseForResult).rejects.toThrow(msg); }); }); [503, 504].forEach((httpStatus) => { - it(`retries the request to Infura up to 5 times if it returns a ${httpStatus} response, returning the successful result if there is one on the 5th try`, async () => { - await withMockedInfuraCommunications(async (comms) => { + it(`retries the request to the RPC endpoint up to 5 times if it returns a ${httpStatus} response, returning the successful result if there is one on the 5th try`, async () => { + await withMockedCommunications({ providerType }, async (comms) => { const request = { method }; // The first time a block-cacheable request is made, the @@ -960,7 +1969,7 @@ export function testsForRpcMethodSupportingBlockParam( // // Here we have the request fail for the first 4 tries, then succeed // on the 5th try. - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: buildRequestWithReplacedBlockParam( request, blockParamIndex, @@ -972,7 +1981,7 @@ export function testsForRpcMethodSupportingBlockParam( }, times: 4, }); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: buildRequestWithReplacedBlockParam( request, blockParamIndex, @@ -983,7 +1992,8 @@ export function testsForRpcMethodSupportingBlockParam( httpStatus: 200, }, }); - const result = await withInfuraClient( + const result = await withNetworkClient( + { providerType }, async ({ makeRpcCall, clock }) => { return await waitForPromiseToBeFulfilledAfterRunningAllTimers( makeRpcCall(request), @@ -996,9 +2006,155 @@ export function testsForRpcMethodSupportingBlockParam( }); }); - it(`causes a request to fail with a custom error if the request to Infura returns a ${httpStatus} response 5 times in a row`, async () => { - await withMockedInfuraCommunications(async (comms) => { + // Both the Infura middleware and custom RPC middleware detect a 503 or 504 + // response and retry the request to the RPC endpoint automatically but + // differ in what sort of response is returned when the number of retries is + // exhausted. + if (providerType === 'infura') { + it(`causes a request to fail with a custom error if the request to the RPC endpoint returns a ${httpStatus} response 5 times in a row`, async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + error: 'Some error', + httpStatus, + }, + times: 5, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + await expect(promiseForResult).rejects.toThrow( + buildInfuraClientRetriesExhaustedErrorMessage('Gateway timeout'), + ); + }); + }); + } else { + it(`produces an empty response if the request to the RPC endpoint returns a ${httpStatus} response 5 times in a row`, async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + error: 'Some error', + httpStatus, + }, + times: 5, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + await expect(promiseForResult).rejects.toThrow( + buildJsonRpcEngineEmptyResponseErrorMessage(method), + ); + }); + }); + } + }); + + it('retries the request to the RPC endpoint up to 5 times if an "ETIMEDOUT" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + // + // Here we have the request fail for the first 4 tries, then + // succeed on the 5th try. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error: 'ETIMEDOUT: Some message', + times: 4, + }); + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + result: 'the result', + httpStatus: 200, + }, + }); + + const result = await withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + expect(result).toStrictEqual('the result'); + }); + }); + + // Both the Infura and fetch middleware detect ETIMEDOUT errors and will + // automatically retry the request to the RPC endpoint in question, but each + // produces a different error if the number of retries is exhausted. + if (providerType === 'infura') { + it('causes a request to fail with a custom error if an "ETIMEDOUT" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { const request = { method }; + const errorMessage = 'ETIMEDOUT: Some message'; // The first time a block-cacheable request is made, the // block-cache middleware will request the latest block number @@ -1010,19 +2166,19 @@ export function testsForRpcMethodSupportingBlockParam( // The block-ref middleware will make the request as specified // except that the block param is replaced with the latest block // number. - comms.mockInfuraRpcCall({ + + comms.mockRpcCall({ request: buildRequestWithReplacedBlockParam( request, blockParamIndex, '0x100', ), - response: { - error: 'Some error', - httpStatus, - }, + error: errorMessage, times: 5, }); - const promiseForResult = withInfuraClient( + + const promiseForResult = withNetworkClient( + { providerType }, async ({ makeRpcCall, clock }) => { return await waitForPromiseToBeFulfilledAfterRunningAllTimers( makeRpcCall(request), @@ -1032,15 +2188,61 @@ export function testsForRpcMethodSupportingBlockParam( ); await expect(promiseForResult).rejects.toThrow( - /^InfuraProvider - cannot complete request\. All retries exhausted\..+Gateway timeout/su, + buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), ); }); }); - }); + } else { + it('produces an empty response if an "ETIMEDOUT" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const errorMessage = 'ETIMEDOUT: Some message'; - ['ETIMEDOUT', 'ECONNRESET', 'SyntaxError'].forEach((errorMessagePrefix) => { - it(`retries the request to Infura up to 5 times if an "${errorMessagePrefix}" error is thrown while making the request, returning the successful result if there is one on the 5th try`, async () => { - await withMockedInfuraCommunications(async (comms) => { + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error: errorMessage, + times: 5, + }); + + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + await expect(promiseForResult).rejects.toThrow( + buildJsonRpcEngineEmptyResponseErrorMessage(method), + ); + }); + }); + } + + // The Infura middleware treats a response that contains an ECONNRESET + // message as an innocuous error that is likely to disappear on a retry. The + // custom RPC middleware, on the other hand, does not specially handle this + // error. + if (providerType === 'infura') { + it('retries the request to the RPC endpoint up to 5 times if an "ECONNRESET" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { + await withMockedCommunications({ providerType }, async (comms) => { const request = { method }; // The first time a block-cacheable request is made, the @@ -1056,16 +2258,16 @@ export function testsForRpcMethodSupportingBlockParam( // // Here we have the request fail for the first 4 tries, then // succeed on the 5th try. - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: buildRequestWithReplacedBlockParam( request, blockParamIndex, '0x100', ), - error: `${errorMessagePrefix}: Some message`, + error: 'ECONNRESET: Some message', times: 4, }); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: buildRequestWithReplacedBlockParam( request, blockParamIndex, @@ -1076,7 +2278,9 @@ export function testsForRpcMethodSupportingBlockParam( httpStatus: 200, }, }); - const result = await withInfuraClient( + + const result = await withNetworkClient( + { providerType }, async ({ makeRpcCall, clock }) => { return await waitForPromiseToBeFulfilledAfterRunningAllTimers( makeRpcCall(request), @@ -1089,8 +2293,98 @@ export function testsForRpcMethodSupportingBlockParam( }); }); - it(`causes a request to fail with a custom error if an "${errorMessagePrefix}" error is thrown while making the request to Infura 5 times in a row`, async () => { - await withMockedInfuraCommunications(async (comms) => { + it('causes a request to fail with a custom error if an "ECONNRESET" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const errorMessage = 'ECONNRESET: Some message'; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error: errorMessage, + times: 5, + }); + + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + await expect(promiseForResult).rejects.toThrow( + buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), + ); + }); + }); + } else { + it('does not retry the request to the RPC endpoint, but throws immediately, if an "ECONNRESET" error is thrown while making the request', async () => { + const customRpcUrl = 'http://example.com'; + + await withMockedCommunications( + { providerType, customRpcUrl }, + async (comms) => { + const request = { method }; + const errorMessage = 'ECONNRESET: Some message'; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error: errorMessage, + }); + + const promiseForResult = withNetworkClient( + { providerType, customRpcUrl }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + buildFetchFailedErrorMessage(customRpcUrl, errorMessage), + ); + }, + ); + }); + } + + // Both the Infura and fetch middleware will attempt to parse the response + // body as JSON, and if this step produces an error, both middleware will + // also attempt to retry the request. However, this error handling code is + // slightly different between the two. As the error in this case is a + // SyntaxError, the Infura middleware will catch it immediately, whereas the + // custom RPC middleware will catch it and re-throw a separate error, which + // it then catches later. + if (providerType === 'infura') { + it('retries the request to the RPC endpoint up to 5 times if a "SyntaxError" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { + await withMockedCommunications({ providerType }, async (comms) => { const request = { method }; // The first time a block-cacheable request is made, the @@ -1103,16 +2397,70 @@ export function testsForRpcMethodSupportingBlockParam( // The block-ref middleware will make the request as specified // except that the block param is replaced with the latest block // number. - comms.mockInfuraRpcCall({ + // + // Here we have the request fail for the first 4 tries, then + // succeed on the 5th try. + comms.mockRpcCall({ request: buildRequestWithReplacedBlockParam( request, blockParamIndex, '0x100', ), - error: `${errorMessagePrefix}: Some message`, + error: 'SyntaxError: Some message', + times: 4, + }); + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + result: 'the result', + httpStatus: 200, + }, + }); + const result = await withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + expect(result).toStrictEqual('the result'); + }); + }); + + it('causes a request to fail with a custom error if a "SyntaxError" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const errorMessage = 'SyntaxError: Some message'; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error: errorMessage, times: 5, }); - const promiseForResult = withInfuraClient( + + const promiseForResult = withNetworkClient( + { providerType }, async ({ makeRpcCall, clock }) => { return await waitForPromiseToBeFulfilledAfterRunningAllTimers( makeRpcCall(request), @@ -1122,21 +2470,317 @@ export function testsForRpcMethodSupportingBlockParam( ); await expect(promiseForResult).rejects.toThrow( - new RegExp( - `^InfuraProvider - cannot complete request\\. All retries exhausted\\..+${errorMessagePrefix}: Some message`, - 'su', - ), + buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), ); }); }); - }); + + it('does not retry the request to the RPC endpoint, but throws immediately, if a "failed to parse response body" error is thrown while making the request', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const errorMessage = 'failed to parse response body: Some message'; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error: errorMessage, + }); + + const promiseForResult = withNetworkClient( + { providerType, infuraNetwork: comms.infuraNetwork }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + buildFetchFailedErrorMessage(comms.rpcUrl, errorMessage), + ); + }); + }); + } else { + it('does not retry the request to the RPC endpoint, but throws immediately, if a "SyntaxError" error is thrown while making the request', async () => { + const customRpcUrl = 'http://example.com'; + + await withMockedCommunications( + { providerType, customRpcUrl }, + async (comms) => { + const request = { method }; + const errorMessage = 'SyntaxError: Some message'; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error: errorMessage, + }); + + const promiseForResult = withNetworkClient( + { providerType, customRpcUrl }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + buildFetchFailedErrorMessage(customRpcUrl, errorMessage), + ); + }, + ); + }); + + it('retries the request to the RPC endpoint up to 5 times if a "failed to parse response body" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + // + // Here we have the request fail for the first 4 tries, then + // succeed on the 5th try. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error: 'failed to parse response body: Some message', + times: 4, + }); + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + result: 'the result', + httpStatus: 200, + }, + }); + + const result = await withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + expect(result).toStrictEqual('the result'); + }); + }); + + it('produces an empty response if a "failed to parse response body" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const errorMessage = 'failed to parse response body: some message'; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error: errorMessage, + times: 5, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + await expect(promiseForResult).rejects.toThrow( + buildJsonRpcEngineEmptyResponseErrorMessage(method), + ); + }); + }); + } + + // Only the custom RPC middleware will detect a "Failed to fetch" error and + // attempt to retry the request to the RPC endpoint; the Infura middleware + // does not. + if (providerType === 'infura') { + it('does not retry the request to the RPC endpoint, but throws immediately, if a "Failed to fetch" error is thrown while making the request', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const errorMessage = 'Failed to fetch: Some message'; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error: errorMessage, + }); + + const promiseForResult = withNetworkClient( + { providerType, infuraNetwork: comms.infuraNetwork }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + buildFetchFailedErrorMessage(comms.rpcUrl, errorMessage), + ); + }); + }); + } else { + it('retries the request to the RPC endpoint up to 5 times if a "Failed to fetch" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + // + // Here we have the request fail for the first 4 tries, then + // succeed on the 5th try. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error: 'Failed to fetch: Some message', + times: 4, + }); + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + result: 'the result', + httpStatus: 200, + }, + }); + + const result = await withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + expect(result).toStrictEqual('the result'); + }); + }); + + it('produces an empty response if a "Failed to fetch" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const errorMessage = 'Failed to fetch: some message'; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error: errorMessage, + times: 5, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + await expect(promiseForResult).rejects.toThrow( + buildJsonRpcEngineEmptyResponseErrorMessage(method), + ); + }); + }); + } }); describe.each([ ['given a block tag of "earliest"', 'earliest', 'earliest'], ['given a block number', 'block number', '0x100'], ])('%s', (_desc, blockParamType, blockParam) => { - it('does not hit Infura more than once for identical requests', async () => { + it('does not hit the RPC endpoint more than once for identical requests', async () => { const requests = [ { method, @@ -1149,19 +2793,20 @@ export function testsForRpcMethodSupportingBlockParam( ]; const mockResults = ['first result', 'second result']; - await withMockedInfuraCommunications(async (comms) => { + await withMockedCommunications({ providerType }, async (comms) => { // The first time a block-cacheable request is made, the block-cache // middleware will request the latest block number through the block // tracker to determine the cache key. This block number doesn't // matter. comms.mockNextBlockTrackerRequest(); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[0], response: { result: mockResults[0] }, }); - const results = await withInfuraClient(({ makeRpcCallsInSeries }) => - makeRpcCallsInSeries(requests), + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), ); expect(results).toStrictEqual([mockResults[0], mockResults[0]]); @@ -1181,31 +2826,34 @@ export function testsForRpcMethodSupportingBlockParam( ]; const mockResults = ['first result', 'second result']; - await withMockedInfuraCommunications(async (comms) => { + await withMockedCommunications({ providerType }, async (comms) => { // Note that we have to mock these requests in a specific order. The // first block tracker request occurs because of the first RPC // request. The second block tracker request, however, does not // occur because of the second RPC request, but rather because we // call `clock.runAll()` below. comms.mockNextBlockTrackerRequest({ blockNumber: '0x1' }); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[0], response: { result: mockResults[0] }, }); comms.mockNextBlockTrackerRequest({ blockNumber: '0x2' }); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[1], response: { result: mockResults[1] }, }); - const results = await withInfuraClient(async (client) => { - const firstResult = await client.makeRpcCall(requests[0]); - // Proceed to the next iteration of the block tracker so that a - // new block is fetched and the current block is updated. - client.clock.runAll(); - const secondResult = await client.makeRpcCall(requests[1]); - return [firstResult, secondResult]; - }); + const results = await withNetworkClient( + { providerType }, + async (client) => { + const firstResult = await client.makeRpcCall(requests[0]); + // Proceed to the next iteration of the block tracker so that a + // new block is fetched and the current block is updated. + client.clock.runAll(); + const secondResult = await client.makeRpcCall(requests[1]); + return [firstResult, secondResult]; + }, + ); expect(results).toStrictEqual([mockResults[0], mockResults[0]]); }); @@ -1226,22 +2874,23 @@ export function testsForRpcMethodSupportingBlockParam( ]; const mockResults = [emptyValue, 'some result']; - await withMockedInfuraCommunications(async (comms) => { + await withMockedCommunications({ providerType }, async (comms) => { // The first time a block-cacheable request is made, the latest block // number is retrieved through the block tracker first. It doesn't // matter what this is — it's just used as a cache key. comms.mockNextBlockTrackerRequest(); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[0], response: { result: mockResults[0] }, }); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[1], response: { result: mockResults[1] }, }); - const results = await withInfuraClient(({ makeRpcCallsInSeries }) => - makeRpcCallsInSeries(requests), + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), ); expect(results).toStrictEqual(mockResults); @@ -1263,18 +2912,19 @@ export function testsForRpcMethodSupportingBlockParam( ]; const mockResults = ['first result', 'second result']; - await withMockedInfuraCommunications(async (comms) => { + await withMockedCommunications({ providerType }, async (comms) => { // The first time a block-cacheable request is made, the latest // block number is retrieved through the block tracker first. It // doesn't matter what this is — it's just used as a cache key. comms.mockNextBlockTrackerRequest(); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[0], response: { result: mockResults[0] }, }); - const results = await withInfuraClient(({ makeRpcCallsInSeries }) => - makeRpcCallsInSeries(requests), + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), ); expect(results).toStrictEqual([mockResults[0], mockResults[0]]); @@ -1284,7 +2934,7 @@ export function testsForRpcMethodSupportingBlockParam( if (blockParamType === 'block number') { it('does not reuse the result of a previous request if it was made with different arguments than this one', async () => { - await withMockedInfuraCommunications(async (comms) => { + await withMockedCommunications({ providerType }, async (comms) => { const requests = [ { method, @@ -1300,25 +2950,26 @@ export function testsForRpcMethodSupportingBlockParam( // number is retrieved through the block tracker first. It doesn't // matter what this is — it's just used as a cache key. comms.mockNextBlockTrackerRequest(); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[0], response: { result: 'first result' }, }); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[1], response: { result: 'second result' }, }); - const results = await withInfuraClient(({ makeRpcCallsInSeries }) => - makeRpcCallsInSeries(requests), + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), ); expect(results).toStrictEqual(['first result', 'second result']); }); }); - it('makes an additional request to Infura if the given block number matches the latest block number', async () => { - await withMockedInfuraCommunications(async (comms) => { + it('makes an additional request to the RPC endpoint if the given block number matches the latest block number', async () => { + await withMockedCommunications({ providerType }, async (comms) => { const request = { method, params: buildMockParams({ blockParamIndex, blockParam: '0x100' }), @@ -1329,21 +2980,22 @@ export function testsForRpcMethodSupportingBlockParam( // also happens within the retry-on-empty middleware (although the // latest block is cached by now). comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request, response: { result: 'the result' }, }); - const result = await withInfuraClient(({ makeRpcCall }) => - makeRpcCall(request), + const result = await withNetworkClient( + { providerType }, + ({ makeRpcCall }) => makeRpcCall(request), ); expect(result).toStrictEqual('the result'); }); }); - it('makes an additional request to Infura if the given block number is less than the latest block number', async () => { - await withMockedInfuraCommunications(async (comms) => { + it('makes an additional request to the RPC endpoint if the given block number is less than the latest block number', async () => { + await withMockedCommunications({ providerType }, async (comms) => { const request = { method, params: buildMockParams({ blockParamIndex, blockParam: '0x50' }), @@ -1354,13 +3006,14 @@ export function testsForRpcMethodSupportingBlockParam( // also happens within the retry-on-empty middleware (although the // latest block is cached by now). comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request, response: { result: 'the result' }, }); - const result = await withInfuraClient(({ makeRpcCall }) => - makeRpcCall(request), + const result = await withNetworkClient( + { providerType }, + ({ makeRpcCall }) => makeRpcCall(request), ); expect(result).toStrictEqual('the result'); @@ -1372,29 +3025,30 @@ export function testsForRpcMethodSupportingBlockParam( describe('given a block tag of "pending"', () => { const params = buildMockParams({ blockParamIndex, blockParam: 'pending' }); - it('hits Infura on all calls and does not cache anything', async () => { + it('hits the RPC endpoint on all calls and does not cache anything', async () => { const requests = [ { method, params }, { method, params }, ]; const mockResults = ['first result', 'second result']; - await withMockedInfuraCommunications(async (comms) => { + await withMockedCommunications({ providerType }, async (comms) => { // The first time a block-cacheable request is made, the latest // block number is retrieved through the block tracker first. It // doesn't matter what this is — it's just used as a cache key. comms.mockNextBlockTrackerRequest(); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[0], response: { result: mockResults[0] }, }); - comms.mockInfuraRpcCall({ + comms.mockRpcCall({ request: requests[1], response: { result: mockResults[1] }, }); - const results = await withInfuraClient(({ makeRpcCallsInSeries }) => - makeRpcCallsInSeries(requests), + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), ); expect(results).toStrictEqual(mockResults);