Add initial provider API tests for Infura client (#15556)
We are working on migrating the extension to a unified network controller, but before we do so we want to extract some of the existing pieces, specifically `createInfuraClient` and `createJsonRpcClient`, which provide the majority of the behavior exhibited within the provider API that the existing NetworkController exposes. This necessitates that we understand and test that behavior as a whole. With that in mind, this commit starts with the Infura-specific network client and adds some initial functional tests for `createInfuraClient`, specifically covering three pieces of middleware provided by `eth-json-rpc-middleware`: `createNetworkAndChainIdMiddleware`, `createBlockCacheMiddleware`, and `createBlockRefMiddleware`. These tests exercise logic that originate from multiple different places and combine in sometimes surprising ways, and as a result, understanding the nature of the tests can be tricky. I've tried to explain the logic (both of the implementation and the tests) via comments. Additionally, debugging why a certain test is failing is not the most fun thing in the world, so to aid with this, I've added some logging to the underlying packages used when a request passes through the middleware stack. Because some middleware change the request being made, or make new requests altogether, this greatly helps to peel back the curtain, as failures from Nock do not supply much meaningful information on their own. This logging is disabled by default, but can be activated by setting `DEBUG=metamask:*,eth-query DEBUG_COLORS=1` alongside the `jest` command. We use this logging by bumping `eth-block-tracker`, and `eth-json-rpc-middleware`.feature/default_network_editable
parent
5bfc2f379d
commit
d91eabfd16
@ -0,0 +1,332 @@ |
|||||||
|
/** |
||||||
|
* @jest-environment node |
||||||
|
*/ |
||||||
|
|
||||||
|
import { withInfuraClient } from './provider-api-tests/helpers'; |
||||||
|
import { |
||||||
|
testsForRpcMethodNotHandledByMiddleware, |
||||||
|
testsForRpcMethodAssumingNoBlockParam, |
||||||
|
testsForRpcMethodsThatCheckForBlockHashInResponse, |
||||||
|
testsForRpcMethodSupportingBlockParam, |
||||||
|
} from './provider-api-tests/shared-tests'; |
||||||
|
|
||||||
|
describe('createInfuraClient', () => { |
||||||
|
// Infura documentation: <https://docs.infura.io/infura/networks/ethereum/json-rpc-methods>
|
||||||
|
// Ethereum JSON-RPC spec: <https://ethereum.github.io/execution-apis/api-documentation/>
|
||||||
|
|
||||||
|
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: 'ropsten' }, |
||||||
|
({ makeRpcCall }) => { |
||||||
|
return makeRpcCall({ |
||||||
|
method: 'eth_chainId', |
||||||
|
}); |
||||||
|
}, |
||||||
|
); |
||||||
|
|
||||||
|
expect(chainId).toStrictEqual('0x3'); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
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', () => { |
||||||
|
testsForRpcMethodsThatCheckForBlockHashInResponse( |
||||||
|
'eth_getTransactionByHash', |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('eth_getTransactionCount', () => { |
||||||
|
testsForRpcMethodSupportingBlockParam('eth_getTransactionCount', { |
||||||
|
blockParamIndex: 1, |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('eth_getTransactionReceipt', () => { |
||||||
|
testsForRpcMethodsThatCheckForBlockHashInResponse( |
||||||
|
'eth_getTransactionReceipt', |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
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: 'ropsten' }, |
||||||
|
({ makeRpcCall }) => { |
||||||
|
return makeRpcCall({ |
||||||
|
method: 'net_version', |
||||||
|
}); |
||||||
|
}, |
||||||
|
); |
||||||
|
|
||||||
|
expect(chainId).toStrictEqual('3'); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
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, |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,333 @@ |
|||||||
|
import nock from 'nock'; |
||||||
|
import sinon from 'sinon'; |
||||||
|
import { JsonRpcEngine } from 'json-rpc-engine'; |
||||||
|
import { providerFromEngine } from 'eth-json-rpc-middleware'; |
||||||
|
import EthQuery from 'eth-query'; |
||||||
|
import createInfuraClient from '../createInfuraClient'; |
||||||
|
|
||||||
|
/** |
||||||
|
* @typedef {import('nock').Scope} NockScope |
||||||
|
* |
||||||
|
* A object returned by `nock(...)` for mocking requests to a particular base |
||||||
|
* URL. |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* @typedef {{makeRpcCall: (request: Partial<JsonRpcRequest>) => Promise<any>, makeRpcCallsInSeries: (requests: Partial<JsonRpcRequest>[]) => Promise<any>}} InfuraClient |
||||||
|
* |
||||||
|
* Provides methods to interact with the suite of middleware that |
||||||
|
* `createInfuraClient` exposes. |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* @typedef {{network: string}} WithInfuraClientOptions |
||||||
|
* |
||||||
|
* The options bag that `withInfuraClient` takes. |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* @typedef {(client: InfuraClient) => Promise<any>} WithInfuraClientCallback |
||||||
|
* |
||||||
|
* The callback that `withInfuraClient` takes. |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* @typedef {[WithInfuraClientOptions, WithInfuraClientCallback] | [WithInfuraClientCallback]} WithInfuraClientArgs |
||||||
|
* |
||||||
|
* The arguments to `withInfuraClient`. |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* @typedef {{ nockScope: NockScope, blockNumber: string }} MockNextBlockTrackerRequestOptions |
||||||
|
* |
||||||
|
* The options to `mockNextBlockTrackerRequest`. |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* @typedef {{ nockScope: NockScope, request: object, response: object, delay?: number }} MockSuccessfulInfuraRpcCallOptions |
||||||
|
* |
||||||
|
* The options to `mockSuccessfulInfuraRpcCall`. |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* @typedef {{mockNextBlockTrackerRequest: (options: Omit<MockNextBlockTrackerRequestOptions, 'nockScope'>) => void, mockSuccessfulInfuraRpcCall: (options: Omit<MockSuccessfulInfuraRpcCallOptions, 'nockScope'>) => NockScope}} InfuraCommunications |
||||||
|
* |
||||||
|
* Provides methods to mock different kinds of requests to Infura. |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* @typedef {{network: string}} MockingInfuraCommunicationsOptions |
||||||
|
* |
||||||
|
* The options bag that `mockingInfuraCommunications` takes. |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* @typedef {(comms: InfuraCommunications) => Promise<any>} MockingInfuraCommunicationsCallback |
||||||
|
* |
||||||
|
* The callback that `mockingInfuraCommunications` takes. |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* @typedef {[MockingInfuraCommunicationsOptions, MockingInfuraCommunicationsCallback] | [MockingInfuraCommunicationsCallback]} MockingInfuraCommunicationsArgs |
||||||
|
* |
||||||
|
* The arguments to `mockingInfuraCommunications`. |
||||||
|
*/ |
||||||
|
|
||||||
|
const INFURA_PROJECT_ID = 'abc123'; |
||||||
|
const DEFAULT_LATEST_BLOCK_NUMBER = '0x42'; |
||||||
|
|
||||||
|
/** |
||||||
|
* If you're having trouble writing a test and you're wondering why the test |
||||||
|
* keeps failing, you can set `process.env.DEBUG_PROVIDER_TESTS` to `1`. This |
||||||
|
* will turn on some extra logging. |
||||||
|
* |
||||||
|
* @param {any[]} args - The arguments that `console.log` takes. |
||||||
|
*/ |
||||||
|
function debug(...args) { |
||||||
|
if (process.env.DEBUG_PROVIDER_TESTS === '1') { |
||||||
|
console.log(...args); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Builds a Nock scope object for mocking requests to a particular network that |
||||||
|
* Infura supports. |
||||||
|
* |
||||||
|
* @param {object} options - The options. |
||||||
|
* @param {string} options.network - The Infura network you're testing with |
||||||
|
* (default: "mainnet"). |
||||||
|
* @returns {NockScope} The nock scope. |
||||||
|
*/ |
||||||
|
function buildScopeForMockingInfuraRequests({ network = 'mainnet' } = {}) { |
||||||
|
return nock(`https://${network}.infura.io`).filteringRequestBody((body) => { |
||||||
|
const copyOfBody = JSON.parse(body); |
||||||
|
// some ids are random, so remove them entirely from the request to
|
||||||
|
// make it possible to mock these requests
|
||||||
|
delete copyOfBody.id; |
||||||
|
return JSON.stringify(copyOfBody); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Mocks the next request for the latest block that the block tracker will make. |
||||||
|
* |
||||||
|
* @param {MockNextBlockTrackerRequestOptions} args - The arguments. |
||||||
|
* @param {NockScope} args.nockScope - A nock scope (a set of mocked requests |
||||||
|
* scoped to a certain base URL). |
||||||
|
* @param {string} args.blockNumber - The block number that the block tracker |
||||||
|
* should report, as a 0x-prefixed hex string. |
||||||
|
*/ |
||||||
|
async function mockNextBlockTrackerRequest({ |
||||||
|
nockScope, |
||||||
|
blockNumber = DEFAULT_LATEST_BLOCK_NUMBER, |
||||||
|
}) { |
||||||
|
await mockSuccessfulInfuraRpcCall({ |
||||||
|
nockScope, |
||||||
|
request: { method: 'eth_blockNumber', params: [] }, |
||||||
|
response: { result: blockNumber }, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Mocks a JSON-RPC request sent to Infura with the given response. |
||||||
|
* |
||||||
|
* @param {MockSuccessfulInfuraRpcCallOptions} args - The arguments. |
||||||
|
* @param {NockScope} args.nockScope - A nock scope (a set of mocked requests |
||||||
|
* scoped to a certain base URL). |
||||||
|
* @param {object} args.request - The request data. |
||||||
|
* @param {object} args.response - The response that the request should have. |
||||||
|
* @param {number} args.delay - The amount of time that should pass before the |
||||||
|
* request resolves with the response. |
||||||
|
* @returns {NockScope} The nock scope. |
||||||
|
*/ |
||||||
|
function mockSuccessfulInfuraRpcCall({ nockScope, request, response, delay }) { |
||||||
|
// eth-query always passes `params`, so even if we don't supply this property
|
||||||
|
// for consistency with makeRpcCall, assume that the `body` contains it
|
||||||
|
const { method, params = [], ...rest } = request; |
||||||
|
const completeResponse = { |
||||||
|
id: 1, |
||||||
|
jsonrpc: '2.0', |
||||||
|
...response, |
||||||
|
}; |
||||||
|
const nockRequest = nockScope.post(`/v3/${INFURA_PROJECT_ID}`, { |
||||||
|
jsonrpc: '2.0', |
||||||
|
method, |
||||||
|
params, |
||||||
|
...rest, |
||||||
|
}); |
||||||
|
|
||||||
|
if (delay !== undefined) { |
||||||
|
nockRequest.delay(delay); |
||||||
|
} |
||||||
|
|
||||||
|
return nockRequest.reply(200, completeResponse); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Makes a JSON-RPC call through the given eth-query object. |
||||||
|
* |
||||||
|
* @param {any} ethQuery - The eth-query object. |
||||||
|
* @param {object} request - The request data. |
||||||
|
* @returns {Promise<any>} A promise that either resolves with the result from |
||||||
|
* the JSON-RPC response if it is successful or rejects with the error from the |
||||||
|
* JSON-RPC response otherwise. |
||||||
|
*/ |
||||||
|
function makeRpcCall(ethQuery, request) { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
debug('[makeRpcCall] making request', request); |
||||||
|
ethQuery.sendAsync(request, (error, result) => { |
||||||
|
debug('[makeRpcCall > ethQuery handler] error', error, 'result', result); |
||||||
|
if (error) { |
||||||
|
reject(error); |
||||||
|
} else { |
||||||
|
resolve(result); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Sets up request mocks for requests to Infura. |
||||||
|
* |
||||||
|
* @param {MockingInfuraCommunicationsArgs} args - Either an options bag + a |
||||||
|
* function, or just a function. The options bag, at the moment, may contain |
||||||
|
* `network` (that is, the Infura network; defaults to "mainnet"). The function |
||||||
|
* is called with an object that allows you to mock different kinds of requests. |
||||||
|
* @returns {Promise<any>} The return value of the given function. |
||||||
|
*/ |
||||||
|
export async function withMockedInfuraCommunications(...args) { |
||||||
|
const [options, fn] = args.length === 2 ? args : [{}, args[0]]; |
||||||
|
const { network = 'mainnet' } = options; |
||||||
|
|
||||||
|
const nockScope = buildScopeForMockingInfuraRequests({ network }); |
||||||
|
const curriedMockNextBlockTrackerRequest = (localOptions) => |
||||||
|
mockNextBlockTrackerRequest({ nockScope, ...localOptions }); |
||||||
|
const curriedMockSuccessfulInfuraRpcCall = (localOptions) => |
||||||
|
mockSuccessfulInfuraRpcCall({ nockScope, ...localOptions }); |
||||||
|
const comms = { |
||||||
|
mockNextBlockTrackerRequest: curriedMockNextBlockTrackerRequest, |
||||||
|
mockSuccessfulInfuraRpcCall: curriedMockSuccessfulInfuraRpcCall, |
||||||
|
}; |
||||||
|
|
||||||
|
try { |
||||||
|
return await fn(comms); |
||||||
|
} finally { |
||||||
|
nock.isDone(); |
||||||
|
nock.cleanAll(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Builds a provider from the Infura middleware along with a block tracker, runs |
||||||
|
* the given function with those two things, and then ensures the block tracker |
||||||
|
* is stopped at the end. |
||||||
|
* |
||||||
|
* @param {WithInfuraClientArgs} args - Either an options bag + a function, or |
||||||
|
* just a function. The options bag, at the moment, may contain `network` (that |
||||||
|
* is, the Infura network; defaults to "mainnet"). The function is called with |
||||||
|
* an object that allows you to interact with the client via a couple of methods |
||||||
|
* on that object. |
||||||
|
* @returns {Promise<any>} The return value of the given function. |
||||||
|
*/ |
||||||
|
export async function withInfuraClient(...args) { |
||||||
|
const [options, fn] = args.length === 2 ? args : [{}, args[0]]; |
||||||
|
const { network = 'mainnet' } = options; |
||||||
|
|
||||||
|
const { networkMiddleware, blockTracker } = createInfuraClient({ |
||||||
|
network, |
||||||
|
projectId: INFURA_PROJECT_ID, |
||||||
|
}); |
||||||
|
|
||||||
|
const engine = new JsonRpcEngine(); |
||||||
|
engine.push(networkMiddleware); |
||||||
|
const provider = providerFromEngine(engine); |
||||||
|
const ethQuery = new EthQuery(provider); |
||||||
|
|
||||||
|
const curriedMakeRpcCall = (request) => makeRpcCall(ethQuery, request); |
||||||
|
const makeRpcCallsInSeries = async (requests) => { |
||||||
|
const responses = []; |
||||||
|
for (const request of requests) { |
||||||
|
responses.push(await curriedMakeRpcCall(request)); |
||||||
|
} |
||||||
|
return responses; |
||||||
|
}; |
||||||
|
// Faking timers ends up doing two things:
|
||||||
|
// 1. Halting the block tracker (which depends on `setTimeout` to periodically
|
||||||
|
// request the latest block) set up in `eth-json-rpc-middleware`
|
||||||
|
// 2. Halting the retry logic in `@metamask/eth-json-rpc-infura` (which also
|
||||||
|
// depends on `setTimeout`)
|
||||||
|
const clock = sinon.useFakeTimers(); |
||||||
|
const client = { |
||||||
|
makeRpcCall: curriedMakeRpcCall, |
||||||
|
makeRpcCallsInSeries, |
||||||
|
clock, |
||||||
|
}; |
||||||
|
|
||||||
|
try { |
||||||
|
return await fn(client); |
||||||
|
} finally { |
||||||
|
await blockTracker.destroy(); |
||||||
|
|
||||||
|
clock.restore(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Some JSON-RPC endpoints take a "block" param (example: `eth_blockNumber`) |
||||||
|
* which can optionally be left out. Additionally, the endpoint may support some |
||||||
|
* number of arguments, although the "block" param will always be last, even if |
||||||
|
* it is optional. Given this, this function builds a mock `params` array for |
||||||
|
* such an endpoint, filling it with arbitrary values, but with the "block" |
||||||
|
* param missing. |
||||||
|
* |
||||||
|
* @param {number} index - The index within the `params` array where the "block" |
||||||
|
* param *would* appear. |
||||||
|
* @returns {string[]} The mock params. |
||||||
|
*/ |
||||||
|
export function buildMockParamsWithoutBlockParamAt(index) { |
||||||
|
const params = []; |
||||||
|
for (let i = 0; i < index; i++) { |
||||||
|
params.push('some value'); |
||||||
|
} |
||||||
|
return params; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Some JSON-RPC endpoints take a "block" param (example: `eth_blockNumber`) |
||||||
|
* which can optionally be left out. Additionally, the endpoint may support some |
||||||
|
* number of arguments, although the "block" param will always be last, even if |
||||||
|
* it is optional. Given this, this function builds a `params` array for such an |
||||||
|
* endpoint with the given "block" param added at the end. |
||||||
|
* |
||||||
|
* @param {number} index - The index within the `params` array to add the |
||||||
|
* "block" param. |
||||||
|
* @param {any} blockParam - The desired "block" param to add. |
||||||
|
* @returns {any[]} The mock params. |
||||||
|
*/ |
||||||
|
export function buildMockParamsWithBlockParamAt(index, blockParam) { |
||||||
|
const params = buildMockParamsWithoutBlockParamAt(index); |
||||||
|
params.push(blockParam); |
||||||
|
return params; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns a partial JSON-RPC request object, with the "block" param replaced |
||||||
|
* with the given value. |
||||||
|
* |
||||||
|
* @param {object} request - The request object. |
||||||
|
* @param {string} request.method - The request method. |
||||||
|
* @param {params} [request.params] - The request params. |
||||||
|
* @param {number} blockParamIndex - The index within the `params` array of the |
||||||
|
* block param. |
||||||
|
* @param {any} blockParam - The desired block param value. |
||||||
|
* @returns {object} The updated request object. |
||||||
|
*/ |
||||||
|
export function buildRequestWithReplacedBlockParam( |
||||||
|
{ method, params = [] }, |
||||||
|
blockParamIndex, |
||||||
|
blockParam, |
||||||
|
) { |
||||||
|
const updatedParams = params.slice(); |
||||||
|
updatedParams[blockParamIndex] = blockParam; |
||||||
|
return { method, params: updatedParams }; |
||||||
|
} |
@ -0,0 +1,708 @@ |
|||||||
|
/* eslint-disable jest/require-top-level-describe, jest/no-export, jest/no-identical-title */ |
||||||
|
|
||||||
|
import { fill } from 'lodash'; |
||||||
|
import { |
||||||
|
withMockedInfuraCommunications, |
||||||
|
withInfuraClient, |
||||||
|
buildMockParamsWithoutBlockParamAt, |
||||||
|
buildMockParamsWithBlockParamAt, |
||||||
|
buildRequestWithReplacedBlockParam, |
||||||
|
} from './helpers'; |
||||||
|
|
||||||
|
export function testsForRpcMethodNotHandledByMiddleware( |
||||||
|
method, |
||||||
|
{ numberOfParameters }, |
||||||
|
) { |
||||||
|
it('attempts to pass the request off to Infura', async () => { |
||||||
|
const request = { |
||||||
|
method, |
||||||
|
params: fill(Array(numberOfParameters), 'some value'), |
||||||
|
}; |
||||||
|
const expectedResult = 'the result'; |
||||||
|
|
||||||
|
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.mockSuccessfulInfuraRpcCall({ |
||||||
|
request, |
||||||
|
response: { result: expectedResult }, |
||||||
|
}); |
||||||
|
const actualResult = await withInfuraClient(({ makeRpcCall }) => |
||||||
|
makeRpcCall(request), |
||||||
|
); |
||||||
|
|
||||||
|
expect(actualResult).toStrictEqual(expectedResult); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Defines tests which exercise the behavior exhibited by an RPC method which is |
||||||
|
* assumed to not take a block parameter. Even if it does, the value of this |
||||||
|
* parameter will not be used in determining how to cache the method. |
||||||
|
* |
||||||
|
* @param method - The name of the RPC method under test. |
||||||
|
*/ |
||||||
|
export function testsForRpcMethodAssumingNoBlockParam(method) { |
||||||
|
it('does not hit Infura more than once for identical requests', async () => { |
||||||
|
const requests = [{ method }, { method }]; |
||||||
|
const mockResults = ['first result', 'second result']; |
||||||
|
|
||||||
|
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.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[0], |
||||||
|
response: { result: mockResults[0] }, |
||||||
|
}); |
||||||
|
const results = await withInfuraClient(({ 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 () => { |
||||||
|
const requests = [{ method }, { method }]; |
||||||
|
const mockResults = ['first result', 'second result']; |
||||||
|
|
||||||
|
await withMockedInfuraCommunications(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.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[0], |
||||||
|
response: { result: mockResults[0] }, |
||||||
|
}); |
||||||
|
comms.mockNextBlockTrackerRequest({ blockNumber: '0x2' }); |
||||||
|
comms.mockSuccessfulInfuraRpcCall({ |
||||||
|
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]; |
||||||
|
}); |
||||||
|
|
||||||
|
expect(results).toStrictEqual(mockResults); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
it.each([null, undefined, '\u003cnil\u003e'])( |
||||||
|
'does not reuse the result of a previous request if it was `%s`', |
||||||
|
async (emptyValue) => { |
||||||
|
const requests = [{ method }, { method }]; |
||||||
|
const mockResults = [emptyValue, 'some result']; |
||||||
|
|
||||||
|
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.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[0], |
||||||
|
response: { result: mockResults[0] }, |
||||||
|
}); |
||||||
|
comms.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[1], |
||||||
|
response: { result: mockResults[1] }, |
||||||
|
}); |
||||||
|
|
||||||
|
const results = await withInfuraClient(({ makeRpcCallsInSeries }) => |
||||||
|
makeRpcCallsInSeries(requests), |
||||||
|
); |
||||||
|
|
||||||
|
expect(results).toStrictEqual(mockResults); |
||||||
|
}); |
||||||
|
}, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Defines tests which exercise the behavior exhibited by an RPC method that |
||||||
|
* use `blockHash` in the response data to determine whether the response is |
||||||
|
* cacheable. |
||||||
|
* |
||||||
|
* @param method - The name of the RPC method under test. |
||||||
|
*/ |
||||||
|
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' }]; |
||||||
|
|
||||||
|
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.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[0], |
||||||
|
response: { result: mockResults[0] }, |
||||||
|
}); |
||||||
|
|
||||||
|
const results = await withInfuraClient(({ 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 () => { |
||||||
|
const requests = [{ method }, { method }]; |
||||||
|
const mockResults = [{ blockHash: '0x100' }, { blockHash: '0x200' }]; |
||||||
|
|
||||||
|
await withMockedInfuraCommunications(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.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[0], |
||||||
|
response: { result: mockResults[0] }, |
||||||
|
}); |
||||||
|
comms.mockNextBlockTrackerRequest({ blockNumber: '0x2' }); |
||||||
|
comms.mockSuccessfulInfuraRpcCall({ |
||||||
|
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]; |
||||||
|
}); |
||||||
|
|
||||||
|
expect(results).toStrictEqual(mockResults); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
it.each([null, undefined, '\u003cnil\u003e'])( |
||||||
|
'does not reuse the result of a previous request if it was `%s`', |
||||||
|
async (emptyValue) => { |
||||||
|
const requests = [{ method }, { method }]; |
||||||
|
const mockResults = [emptyValue, { blockHash: '0x100' }]; |
||||||
|
|
||||||
|
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.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[0], |
||||||
|
response: { result: mockResults[0] }, |
||||||
|
}); |
||||||
|
comms.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[1], |
||||||
|
response: { result: mockResults[1] }, |
||||||
|
}); |
||||||
|
|
||||||
|
const results = await withInfuraClient(({ makeRpcCallsInSeries }) => |
||||||
|
makeRpcCallsInSeries(requests), |
||||||
|
); |
||||||
|
|
||||||
|
expect(results).toStrictEqual(mockResults); |
||||||
|
}); |
||||||
|
}, |
||||||
|
); |
||||||
|
|
||||||
|
it('does not reuse the result of a previous request if result.blockHash was null', async () => { |
||||||
|
const requests = [{ method }, { method }]; |
||||||
|
const mockResults = [ |
||||||
|
{ blockHash: null, extra: 'some value' }, |
||||||
|
{ blockHash: '0x100', extra: 'some other value' }, |
||||||
|
]; |
||||||
|
|
||||||
|
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.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[0], |
||||||
|
response: { result: mockResults[0] }, |
||||||
|
}); |
||||||
|
comms.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[1], |
||||||
|
response: { result: mockResults[1] }, |
||||||
|
}); |
||||||
|
|
||||||
|
const results = await withInfuraClient(({ makeRpcCallsInSeries }) => |
||||||
|
makeRpcCallsInSeries(requests), |
||||||
|
); |
||||||
|
|
||||||
|
expect(results).toStrictEqual(mockResults); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
it('does not reuse the result of a previous request if result.blockHash was undefined', async () => { |
||||||
|
const requests = [{ method }, { method }]; |
||||||
|
const mockResults = [ |
||||||
|
{ extra: 'some value' }, |
||||||
|
{ blockHash: '0x100', extra: 'some other value' }, |
||||||
|
]; |
||||||
|
|
||||||
|
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.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[0], |
||||||
|
response: { result: mockResults[0] }, |
||||||
|
}); |
||||||
|
comms.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[1], |
||||||
|
response: { result: mockResults[1] }, |
||||||
|
}); |
||||||
|
|
||||||
|
const results = await withInfuraClient(({ makeRpcCallsInSeries }) => |
||||||
|
makeRpcCallsInSeries(requests), |
||||||
|
); |
||||||
|
|
||||||
|
expect(results).toStrictEqual(mockResults); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
it('does not reuse the result of a previous request if result.blockHash was "0x0000000000000000000000000000000000000000000000000000000000000000"', async () => { |
||||||
|
const requests = [{ method }, { method }]; |
||||||
|
const mockResults = [ |
||||||
|
{ |
||||||
|
blockHash: |
||||||
|
'0x0000000000000000000000000000000000000000000000000000000000000000', |
||||||
|
extra: 'some value', |
||||||
|
}, |
||||||
|
{ blockHash: '0x100', extra: 'some other value' }, |
||||||
|
]; |
||||||
|
|
||||||
|
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.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[0], |
||||||
|
response: { result: mockResults[0] }, |
||||||
|
}); |
||||||
|
comms.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[1], |
||||||
|
response: { result: mockResults[1] }, |
||||||
|
}); |
||||||
|
|
||||||
|
const results = await withInfuraClient(({ makeRpcCallsInSeries }) => |
||||||
|
makeRpcCallsInSeries(requests), |
||||||
|
); |
||||||
|
|
||||||
|
expect(results).toStrictEqual(mockResults); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Defines tests which exercise the behavior exhibited by an RPC method that |
||||||
|
* 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. |
||||||
|
*/ |
||||||
|
/* eslint-disable-next-line jest/no-export */ |
||||||
|
export function testsForRpcMethodSupportingBlockParam( |
||||||
|
method, |
||||||
|
{ blockParamIndex }, |
||||||
|
) { |
||||||
|
describe.each([ |
||||||
|
['given no block tag', 'none'], |
||||||
|
['given a block tag of "latest"', 'latest', 'latest'], |
||||||
|
])('%s', (_desc, blockParamType, blockParam) => { |
||||||
|
const params = |
||||||
|
blockParamType === 'none' |
||||||
|
? buildMockParamsWithoutBlockParamAt(blockParamIndex) |
||||||
|
: buildMockParamsWithBlockParamAt(blockParamIndex, blockParam); |
||||||
|
|
||||||
|
it('does not hit Infura more than once for identical requests', async () => { |
||||||
|
const requests = [ |
||||||
|
{ method, params }, |
||||||
|
{ method, params }, |
||||||
|
]; |
||||||
|
const mockResults = ['first result', 'second result']; |
||||||
|
|
||||||
|
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.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: buildRequestWithReplacedBlockParam( |
||||||
|
requests[0], |
||||||
|
blockParamIndex, |
||||||
|
'0x100', |
||||||
|
), |
||||||
|
response: { result: mockResults[0] }, |
||||||
|
}); |
||||||
|
// Note that the block-ref middleware will still allow the original
|
||||||
|
// request to go through.
|
||||||
|
comms.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[0], |
||||||
|
response: { result: mockResults[0] }, |
||||||
|
}); |
||||||
|
|
||||||
|
const results = await withInfuraClient(({ 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 () => { |
||||||
|
const requests = [ |
||||||
|
{ method, params }, |
||||||
|
{ method, params }, |
||||||
|
]; |
||||||
|
const mockResults = ['first result', 'second result']; |
||||||
|
|
||||||
|
await withMockedInfuraCommunications(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: '0x100' }); |
||||||
|
// The block-ref middleware will make the request as specified
|
||||||
|
// except that the block param is replaced with the latest block
|
||||||
|
// number.
|
||||||
|
comms.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: buildRequestWithReplacedBlockParam( |
||||||
|
requests[0], |
||||||
|
blockParamIndex, |
||||||
|
'0x100', |
||||||
|
), |
||||||
|
response: { result: mockResults[0] }, |
||||||
|
}); |
||||||
|
// Note that the block-ref middleware will still allow the original
|
||||||
|
// request to go through.
|
||||||
|
comms.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[0], |
||||||
|
response: { result: mockResults[0] }, |
||||||
|
}); |
||||||
|
comms.mockNextBlockTrackerRequest({ blockNumber: '0x200' }); |
||||||
|
comms.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[1], |
||||||
|
response: { result: mockResults[1] }, |
||||||
|
}); |
||||||
|
// The previous two requests will happen again, with a different block
|
||||||
|
// number, in the same order.
|
||||||
|
comms.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: buildRequestWithReplacedBlockParam( |
||||||
|
requests[0], |
||||||
|
blockParamIndex, |
||||||
|
'0x200', |
||||||
|
), |
||||||
|
response: { result: mockResults[1] }, |
||||||
|
}); |
||||||
|
comms.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[0], |
||||||
|
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]; |
||||||
|
}); |
||||||
|
|
||||||
|
expect(results).toStrictEqual(mockResults); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
it.each([null, undefined, '\u003cnil\u003e'])( |
||||||
|
'does not reuse the result of a previous request if it was `%s`', |
||||||
|
async (emptyValue) => { |
||||||
|
const requests = [ |
||||||
|
{ method, params }, |
||||||
|
{ method, params }, |
||||||
|
]; |
||||||
|
const mockResults = [emptyValue, 'some result']; |
||||||
|
|
||||||
|
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.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: buildRequestWithReplacedBlockParam( |
||||||
|
requests[0], |
||||||
|
blockParamIndex, |
||||||
|
'0x100', |
||||||
|
), |
||||||
|
response: { result: mockResults[0] }, |
||||||
|
}); |
||||||
|
// Note that the block-ref middleware will still allow the original
|
||||||
|
// request to go through.
|
||||||
|
comms.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[0], |
||||||
|
response: { result: mockResults[0] }, |
||||||
|
}); |
||||||
|
// The previous two requests will happen again, in the same order.
|
||||||
|
comms.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: buildRequestWithReplacedBlockParam( |
||||||
|
requests[0], |
||||||
|
blockParamIndex, |
||||||
|
'0x100', |
||||||
|
), |
||||||
|
response: { result: mockResults[1] }, |
||||||
|
}); |
||||||
|
comms.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[0], |
||||||
|
response: { result: mockResults[1] }, |
||||||
|
}); |
||||||
|
|
||||||
|
const results = await withInfuraClient(({ makeRpcCallsInSeries }) => |
||||||
|
makeRpcCallsInSeries(requests), |
||||||
|
); |
||||||
|
|
||||||
|
expect(results).toStrictEqual(mockResults); |
||||||
|
}); |
||||||
|
}, |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
describe.each([ |
||||||
|
['given a block tag of "earliest"', 'earliest', 'earliest'], |
||||||
|
['given a block number', 'block number', '0x100'], |
||||||
|
])('%s', (_desc, blockParamType, blockParam) => { |
||||||
|
const params = buildMockParamsWithBlockParamAt(blockParamIndex, blockParam); |
||||||
|
|
||||||
|
it('does not hit Infura more than once for identical requests', async () => { |
||||||
|
const requests = [ |
||||||
|
{ method, params }, |
||||||
|
{ method, params }, |
||||||
|
]; |
||||||
|
const mockResults = ['first result', 'second result']; |
||||||
|
|
||||||
|
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. This block number doesn't
|
||||||
|
// matter.
|
||||||
|
comms.mockNextBlockTrackerRequest(); |
||||||
|
comms.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[0], |
||||||
|
response: { result: mockResults[0] }, |
||||||
|
}); |
||||||
|
|
||||||
|
const results = await withInfuraClient(({ makeRpcCallsInSeries }) => |
||||||
|
makeRpcCallsInSeries(requests), |
||||||
|
); |
||||||
|
|
||||||
|
expect(results).toStrictEqual([mockResults[0], mockResults[0]]); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
it('reuses the result of a previous request even if the latest block number was updated since', async () => { |
||||||
|
const requests = [ |
||||||
|
{ method, params }, |
||||||
|
{ method, params }, |
||||||
|
]; |
||||||
|
const mockResults = ['first result', 'second result']; |
||||||
|
|
||||||
|
await withMockedInfuraCommunications(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.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[0], |
||||||
|
response: { result: mockResults[0] }, |
||||||
|
}); |
||||||
|
comms.mockNextBlockTrackerRequest({ blockNumber: '0x2' }); |
||||||
|
comms.mockSuccessfulInfuraRpcCall({ |
||||||
|
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]; |
||||||
|
}); |
||||||
|
|
||||||
|
expect(results).toStrictEqual([mockResults[0], mockResults[0]]); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
it.each([null, undefined, '\u003cnil\u003e'])( |
||||||
|
'does not reuse the result of a previous request if it was `%s`', |
||||||
|
async (emptyValue) => { |
||||||
|
const requests = [ |
||||||
|
{ method, params }, |
||||||
|
{ method, params }, |
||||||
|
]; |
||||||
|
const mockResults = [emptyValue, 'some result']; |
||||||
|
|
||||||
|
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.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[0], |
||||||
|
response: { result: mockResults[0] }, |
||||||
|
}); |
||||||
|
comms.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[1], |
||||||
|
response: { result: mockResults[1] }, |
||||||
|
}); |
||||||
|
|
||||||
|
const results = await withInfuraClient(({ makeRpcCallsInSeries }) => |
||||||
|
makeRpcCallsInSeries(requests), |
||||||
|
); |
||||||
|
|
||||||
|
expect(results).toStrictEqual(mockResults); |
||||||
|
}); |
||||||
|
}, |
||||||
|
); |
||||||
|
|
||||||
|
if (blockParamType === 'earliest') { |
||||||
|
it('treats "0x00" as a synonym for "earliest"', async () => { |
||||||
|
const requests = [ |
||||||
|
{ |
||||||
|
method, |
||||||
|
params: buildMockParamsWithBlockParamAt( |
||||||
|
blockParamIndex, |
||||||
|
blockParam, |
||||||
|
), |
||||||
|
}, |
||||||
|
{ |
||||||
|
method, |
||||||
|
params: buildMockParamsWithBlockParamAt(blockParamIndex, '0x00'), |
||||||
|
}, |
||||||
|
]; |
||||||
|
const mockResults = ['first result', 'second result']; |
||||||
|
|
||||||
|
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.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[0], |
||||||
|
response: { result: mockResults[0] }, |
||||||
|
}); |
||||||
|
|
||||||
|
const results = await withInfuraClient(({ makeRpcCallsInSeries }) => |
||||||
|
makeRpcCallsInSeries(requests), |
||||||
|
); |
||||||
|
|
||||||
|
expect(results).toStrictEqual([mockResults[0], mockResults[0]]); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
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) => { |
||||||
|
const requests = [ |
||||||
|
{ |
||||||
|
method, |
||||||
|
params: buildMockParamsWithBlockParamAt(blockParamIndex, '0x100'), |
||||||
|
}, |
||||||
|
{ |
||||||
|
method, |
||||||
|
params: buildMockParamsWithBlockParamAt(blockParamIndex, '0x200'), |
||||||
|
}, |
||||||
|
]; |
||||||
|
|
||||||
|
// 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.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[0], |
||||||
|
response: { result: 'first result' }, |
||||||
|
}); |
||||||
|
comms.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[1], |
||||||
|
response: { result: 'second result' }, |
||||||
|
}); |
||||||
|
|
||||||
|
const results = await withInfuraClient(({ makeRpcCallsInSeries }) => |
||||||
|
makeRpcCallsInSeries(requests), |
||||||
|
); |
||||||
|
|
||||||
|
expect(results).toStrictEqual(['first result', 'second result']); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
describe('given a block tag of "pending"', () => { |
||||||
|
const params = buildMockParamsWithBlockParamAt(blockParamIndex, 'pending'); |
||||||
|
|
||||||
|
it('hits Infura 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) => { |
||||||
|
// 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.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[0], |
||||||
|
response: { result: mockResults[0] }, |
||||||
|
}); |
||||||
|
comms.mockSuccessfulInfuraRpcCall({ |
||||||
|
request: requests[1], |
||||||
|
response: { result: mockResults[1] }, |
||||||
|
}); |
||||||
|
|
||||||
|
const results = await withInfuraClient(({ makeRpcCallsInSeries }) => |
||||||
|
makeRpcCallsInSeries(requests), |
||||||
|
); |
||||||
|
|
||||||
|
expect(results).toStrictEqual(mockResults); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
@ -0,0 +1,22 @@ |
|||||||
|
diff --git a/node_modules/eth-query/index.js b/node_modules/eth-query/index.js
|
||||||
|
index 13e9f3c..303d703 100644
|
||||||
|
--- a/node_modules/eth-query/index.js
|
||||||
|
+++ b/node_modules/eth-query/index.js
|
||||||
|
@@ -1,5 +1,6 @@
|
||||||
|
const extend = require('xtend')
|
||||||
|
const createRandomId = require('json-rpc-random-id')()
|
||||||
|
+const debug = require('debug')('eth-query');
|
||||||
|
|
||||||
|
module.exports = EthQuery
|
||||||
|
|
||||||
|
@@ -63,7 +64,9 @@ EthQuery.prototype.submitHashrate = generateFnFor('eth_subm
|
||||||
|
|
||||||
|
EthQuery.prototype.sendAsync = function(opts, cb){
|
||||||
|
const self = this
|
||||||
|
- self.currentProvider.sendAsync(createPayload(opts), function(err, response){
|
||||||
|
+ const payload = createPayload(opts)
|
||||||
|
+ debug('making request %o', payload)
|
||||||
|
+ self.currentProvider.sendAsync(payload, function(err, response){
|
||||||
|
if (!err && response.error) err = new Error('EthQuery - RPC Error - '+response.error.message)
|
||||||
|
if (err) return cb(err)
|
||||||
|
cb(null, response.result)
|
Loading…
Reference in new issue