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);