mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-12 20:57:12 +01:00
d7a812f42f
Add tests for the `block-tracker-inspector` middleware — which makes sure that the block tracker never has a reference to the latest block which is less than a block number that shows up in an RPC method's response — and the Infura middleware — which takes care of sending the request to Infura, and will retry the request up to 5 times if Infura sends back a certain type of error. Note that the `retry-on-empty` middleware is not tested because it currently has a [bug][1] which is making it ineffective. [1]: https://github.com/MetaMask/eth-json-rpc-middleware/issues/139
1498 lines
57 KiB
JavaScript
1498 lines
57 KiB
JavaScript
/* 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';
|
|
|
|
const originalSetTimeout = setTimeout;
|
|
|
|
/**
|
|
* Some middleware contain logic which retries the request if some condition
|
|
* applies. This retrying always happens out of band via `setTimeout`, and
|
|
* because we are stubbing time via Jest's fake timers, we have to manually
|
|
* advance the clock so that the `setTimeout` handlers get fired. We don't know
|
|
* when these timers will get created, however, so we have to keep advancing
|
|
* timers until the request has been made an appropriate number of times.
|
|
* Unfortunately we don't have a good way to know how many times a request has
|
|
* been retried, but the good news is that the middleware won't end, and thus
|
|
* the promise which the RPC call returns won't get fulfilled, until all retries
|
|
* have been made.
|
|
*
|
|
* @param promise - The promise which is returned by the RPC call.
|
|
* @param clock - A Sinon clock object which can be used to advance to the next
|
|
* `setTimeout` handler.
|
|
*/
|
|
async function waitForPromiseToBeFulfilledAfterRunningAllTimers(
|
|
promise,
|
|
clock,
|
|
) {
|
|
let hasPromiseBeenFulfilled = false;
|
|
let numTimesClockHasBeenAdvanced = 0;
|
|
|
|
promise
|
|
.catch(() => {
|
|
// This is used to silence Node.js warnings about the rejection
|
|
// being handled asynchronously. The error is handled later when
|
|
// `promise` is awaited.
|
|
})
|
|
.finally(() => {
|
|
hasPromiseBeenFulfilled = true;
|
|
});
|
|
|
|
// `isPromiseFulfilled` is modified asynchronously.
|
|
/* eslint-disable-next-line no-unmodified-loop-condition */
|
|
while (!hasPromiseBeenFulfilled && numTimesClockHasBeenAdvanced < 15) {
|
|
clock.runAll();
|
|
await new Promise((resolve) => originalSetTimeout(resolve, 10));
|
|
numTimesClockHasBeenAdvanced += 1;
|
|
}
|
|
|
|
return promise;
|
|
}
|
|
|
|
/**
|
|
* Defines tests which exercise the behavior exhibited by an RPC method that
|
|
* does not support params (which affects how the method is cached).
|
|
*/
|
|
/* eslint-disable-next-line jest/no-export */
|
|
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.mockInfuraRpcCall({
|
|
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.mockInfuraRpcCall({
|
|
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.mockInfuraRpcCall({
|
|
request: requests[0],
|
|
response: { result: mockResults[0] },
|
|
});
|
|
comms.mockNextBlockTrackerRequest({ blockNumber: '0x2' });
|
|
comms.mockInfuraRpcCall({
|
|
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.mockInfuraRpcCall({
|
|
request: requests[0],
|
|
response: { result: mockResults[0] },
|
|
});
|
|
comms.mockInfuraRpcCall({
|
|
request: requests[1],
|
|
response: { result: mockResults[1] },
|
|
});
|
|
|
|
const results = await withInfuraClient(({ makeRpcCallsInSeries }) =>
|
|
makeRpcCallsInSeries(requests),
|
|
);
|
|
|
|
expect(results).toStrictEqual(mockResults);
|
|
});
|
|
},
|
|
);
|
|
|
|
it('queues requests while a previous identical call is still pending, then runs the queue when it finishes, reusing the result from the first request', async () => {
|
|
const requests = [{ method }, { method }, { method }];
|
|
const mockResults = ['first result', 'second result', 'third 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.mockInfuraRpcCall({
|
|
request: requests[0],
|
|
response: { result: mockResults[0] },
|
|
delay: 100,
|
|
});
|
|
comms.mockInfuraRpcCall({
|
|
request: requests[1],
|
|
response: { result: mockResults[1] },
|
|
});
|
|
comms.mockInfuraRpcCall({
|
|
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];
|
|
});
|
|
|
|
expect(results).toStrictEqual([
|
|
mockResults[0],
|
|
mockResults[0],
|
|
mockResults[0],
|
|
]);
|
|
});
|
|
});
|
|
|
|
it('throws a custom error if the request to Infura returns a 405 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. It doesn't
|
|
// matter what this is — it's just used as a cache key.
|
|
comms.mockNextBlockTrackerRequest();
|
|
comms.mockInfuraRpcCall({
|
|
request,
|
|
response: {
|
|
httpStatus: 405,
|
|
},
|
|
});
|
|
const promiseForResult = withInfuraClient(async ({ makeRpcCall }) =>
|
|
makeRpcCall(request),
|
|
);
|
|
|
|
await expect(promiseForResult).rejects.toThrow(
|
|
'The method does not exist / is not available',
|
|
);
|
|
});
|
|
});
|
|
|
|
it('throws a custom error if the request to Infura returns a 429 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. It doesn't
|
|
// matter what this is — it's just used as a cache key.
|
|
comms.mockNextBlockTrackerRequest();
|
|
comms.mockInfuraRpcCall({
|
|
request,
|
|
response: {
|
|
httpStatus: 429,
|
|
},
|
|
});
|
|
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) => {
|
|
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: {
|
|
id: 12345,
|
|
jsonrpc: '2.0',
|
|
error: 'some error',
|
|
httpStatus: 420,
|
|
},
|
|
});
|
|
const promiseForResult = withInfuraClient(async ({ makeRpcCall }) =>
|
|
makeRpcCall(request),
|
|
);
|
|
|
|
await expect(promiseForResult).rejects.toThrow(
|
|
'{"id":12345,"jsonrpc":"2.0","error":"some error"}',
|
|
);
|
|
});
|
|
});
|
|
|
|
[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) => {
|
|
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.mockInfuraRpcCall({
|
|
request,
|
|
response: {
|
|
error: 'Some error',
|
|
httpStatus,
|
|
},
|
|
times: 4,
|
|
});
|
|
comms.mockInfuraRpcCall({
|
|
request,
|
|
response: {
|
|
result: 'the result',
|
|
httpStatus: 200,
|
|
},
|
|
});
|
|
const result = await withInfuraClient(
|
|
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 the request to Infura returns a ${httpStatus} response 5 times in a row`, 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. It doesn't
|
|
// matter what this is — it's just used as a cache key.
|
|
comms.mockNextBlockTrackerRequest();
|
|
comms.mockInfuraRpcCall({
|
|
request,
|
|
response: {
|
|
error: 'Some error',
|
|
httpStatus,
|
|
},
|
|
times: 5,
|
|
});
|
|
const promiseForResult = withInfuraClient(
|
|
async ({ makeRpcCall, clock }) => {
|
|
return await waitForPromiseToBeFulfilledAfterRunningAllTimers(
|
|
makeRpcCall(request),
|
|
clock,
|
|
);
|
|
},
|
|
);
|
|
|
|
await expect(promiseForResult).rejects.toThrow(
|
|
/^InfuraProvider - cannot complete request\. All retries exhausted\..+Gateway timeout/su,
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
['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) => {
|
|
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.mockInfuraRpcCall({
|
|
request,
|
|
error: `${errorMessagePrefix}: Some message`,
|
|
times: 4,
|
|
});
|
|
comms.mockInfuraRpcCall({
|
|
request,
|
|
response: {
|
|
result: 'the result',
|
|
httpStatus: 200,
|
|
},
|
|
});
|
|
const result = await withInfuraClient(
|
|
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 "${errorMessagePrefix}" error is thrown while making the request to Infura 5 times in a row`, 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. It doesn't
|
|
// matter what this is — it's just used as a cache key.
|
|
comms.mockNextBlockTrackerRequest();
|
|
comms.mockInfuraRpcCall({
|
|
request,
|
|
error: `${errorMessagePrefix}: Some message`,
|
|
times: 5,
|
|
});
|
|
const promiseForResult = withInfuraClient(
|
|
async ({ makeRpcCall, clock }) => {
|
|
return await waitForPromiseToBeFulfilledAfterRunningAllTimers(
|
|
makeRpcCall(request),
|
|
clock,
|
|
);
|
|
},
|
|
);
|
|
|
|
await expect(promiseForResult).rejects.toThrow(
|
|
new RegExp(
|
|
`^InfuraProvider - cannot complete request\\. All retries exhausted\\..+${errorMessagePrefix}: Some message`,
|
|
'su',
|
|
),
|
|
);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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.mockInfuraRpcCall({
|
|
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.mockInfuraRpcCall({
|
|
request: requests[0],
|
|
response: { result: mockResults[0] },
|
|
});
|
|
comms.mockNextBlockTrackerRequest({ blockNumber: '0x2' });
|
|
comms.mockInfuraRpcCall({
|
|
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.mockInfuraRpcCall({
|
|
request: requests[0],
|
|
response: { result: mockResults[0] },
|
|
});
|
|
comms.mockInfuraRpcCall({
|
|
request: requests[1],
|
|
response: { result: mockResults[1] },
|
|
});
|
|
|
|
const results = await withInfuraClient(({ makeRpcCallsInSeries }) =>
|
|
makeRpcCallsInSeries(requests),
|
|
);
|
|
|
|
// TODO: Does this work?
|
|
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.mockInfuraRpcCall({
|
|
request: requests[0],
|
|
response: { result: mockResults[0] },
|
|
});
|
|
comms.mockInfuraRpcCall({
|
|
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.mockInfuraRpcCall({
|
|
request: requests[0],
|
|
response: { result: mockResults[0] },
|
|
});
|
|
comms.mockInfuraRpcCall({
|
|
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.mockInfuraRpcCall({
|
|
request: requests[0],
|
|
response: { result: mockResults[0] },
|
|
});
|
|
comms.mockInfuraRpcCall({
|
|
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.mockInfuraRpcCall({
|
|
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.mockInfuraRpcCall({
|
|
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.mockInfuraRpcCall({
|
|
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.mockInfuraRpcCall({
|
|
request: requests[0],
|
|
response: { result: mockResults[0] },
|
|
});
|
|
comms.mockNextBlockTrackerRequest({ blockNumber: '0x200' });
|
|
comms.mockInfuraRpcCall({
|
|
request: requests[1],
|
|
response: { result: mockResults[1] },
|
|
});
|
|
// The previous two requests will happen again, with a different block
|
|
// number, in the same order.
|
|
comms.mockInfuraRpcCall({
|
|
request: buildRequestWithReplacedBlockParam(
|
|
requests[0],
|
|
blockParamIndex,
|
|
'0x200',
|
|
),
|
|
response: { result: mockResults[1] },
|
|
});
|
|
comms.mockInfuraRpcCall({
|
|
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.mockInfuraRpcCall({
|
|
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.mockInfuraRpcCall({
|
|
request: requests[0],
|
|
response: { result: mockResults[0] },
|
|
});
|
|
// The previous two requests will happen again, in the same order.
|
|
comms.mockInfuraRpcCall({
|
|
request: buildRequestWithReplacedBlockParam(
|
|
requests[0],
|
|
blockParamIndex,
|
|
'0x100',
|
|
),
|
|
response: { result: mockResults[1] },
|
|
});
|
|
comms.mockInfuraRpcCall({
|
|
request: requests[0],
|
|
response: { result: mockResults[1] },
|
|
});
|
|
|
|
const results = await withInfuraClient(({ makeRpcCallsInSeries }) =>
|
|
makeRpcCallsInSeries(requests),
|
|
);
|
|
|
|
expect(results).toStrictEqual(mockResults);
|
|
});
|
|
},
|
|
);
|
|
|
|
it('queues requests while a previous identical call is still pending, then runs the queue when it finishes, reusing the result from the first request', async () => {
|
|
const requests = [{ method }, { method }, { method }];
|
|
const mockResults = ['first result', 'second result', 'third 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.mockInfuraRpcCall({
|
|
request: buildRequestWithReplacedBlockParam(
|
|
requests[0],
|
|
blockParamIndex,
|
|
'0x100',
|
|
),
|
|
response: { result: mockResults[0] },
|
|
});
|
|
// This is the original request as below, which the block-ref
|
|
// middleware will allow through, except that we delay it.
|
|
comms.mockInfuraRpcCall({
|
|
request: requests[0],
|
|
response: { result: mockResults[0] },
|
|
delay: 100,
|
|
});
|
|
// The previous two requests will happen again, in the same order.
|
|
comms.mockInfuraRpcCall({
|
|
request: buildRequestWithReplacedBlockParam(
|
|
requests[1],
|
|
blockParamIndex,
|
|
'0x100',
|
|
),
|
|
response: { result: mockResults[1] },
|
|
});
|
|
comms.mockInfuraRpcCall({
|
|
request: requests[1],
|
|
response: { result: mockResults[1] },
|
|
});
|
|
comms.mockInfuraRpcCall({
|
|
request: buildRequestWithReplacedBlockParam(
|
|
requests[2],
|
|
blockParamIndex,
|
|
'0x100',
|
|
),
|
|
response: { result: mockResults[2] },
|
|
});
|
|
comms.mockInfuraRpcCall({
|
|
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];
|
|
});
|
|
|
|
expect(results).toStrictEqual([
|
|
mockResults[0],
|
|
mockResults[0],
|
|
mockResults[0],
|
|
]);
|
|
});
|
|
});
|
|
|
|
if (blockParamType === 'none') {
|
|
it('throws a custom error if the request to Infura returns a 405 response', async () => {
|
|
await withMockedInfuraCommunications(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.
|
|
//
|
|
// Note, however, that the block-ref middleware doesn't run the
|
|
// original request, as it fails before it gets to that point, so
|
|
// there is no need to mock the request again.
|
|
comms.mockInfuraRpcCall({
|
|
request: buildRequestWithReplacedBlockParam(
|
|
request,
|
|
blockParamIndex,
|
|
'0x100',
|
|
),
|
|
response: {
|
|
httpStatus: 405,
|
|
},
|
|
});
|
|
const promiseForResult = withInfuraClient(async ({ makeRpcCall }) =>
|
|
makeRpcCall(request),
|
|
);
|
|
|
|
await expect(promiseForResult).rejects.toThrow(
|
|
'The method does not exist / is not available',
|
|
);
|
|
});
|
|
});
|
|
|
|
it('throws a custom error if the request to Infura returns a 429 response', async () => {
|
|
await withMockedInfuraCommunications(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.
|
|
//
|
|
// Note, however, that the block-ref middleware doesn't run the
|
|
// original request, as it fails before it gets to that point, so
|
|
// there is no need to mock the request again.
|
|
comms.mockInfuraRpcCall({
|
|
request: buildRequestWithReplacedBlockParam(
|
|
request,
|
|
blockParamIndex,
|
|
'0x100',
|
|
),
|
|
response: {
|
|
httpStatus: 429,
|
|
},
|
|
});
|
|
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) => {
|
|
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.
|
|
//
|
|
// Note, however, that the block-ref middleware doesn't run the
|
|
// original request, as it fails before it gets to that point, so
|
|
// there is no need to mock the request again.
|
|
comms.mockInfuraRpcCall({
|
|
request: buildRequestWithReplacedBlockParam(
|
|
request,
|
|
blockParamIndex,
|
|
'0x100',
|
|
),
|
|
response: {
|
|
id: 12345,
|
|
jsonrpc: '2.0',
|
|
error: 'some error',
|
|
httpStatus: 420,
|
|
},
|
|
});
|
|
const promiseForResult = withInfuraClient(async ({ makeRpcCall }) =>
|
|
makeRpcCall(request),
|
|
);
|
|
|
|
await expect(promiseForResult).rejects.toThrow(
|
|
'{"id":12345,"jsonrpc":"2.0","error":"some error"}',
|
|
);
|
|
});
|
|
});
|
|
|
|
[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) => {
|
|
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.mockInfuraRpcCall({
|
|
request: buildRequestWithReplacedBlockParam(
|
|
request,
|
|
blockParamIndex,
|
|
'0x100',
|
|
),
|
|
response: {
|
|
error: 'some error',
|
|
httpStatus,
|
|
},
|
|
times: 4,
|
|
});
|
|
comms.mockInfuraRpcCall({
|
|
request: buildRequestWithReplacedBlockParam(
|
|
request,
|
|
blockParamIndex,
|
|
'0x100',
|
|
),
|
|
response: {
|
|
result: 'the result',
|
|
httpStatus: 200,
|
|
},
|
|
});
|
|
// Note that the block-ref middleware will still allow the original
|
|
// request to go through.
|
|
comms.mockInfuraRpcCall({
|
|
request,
|
|
response: {
|
|
result: 'the result',
|
|
httpStatus: 200,
|
|
},
|
|
});
|
|
const result = await withInfuraClient(
|
|
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 the request to Infura returns a ${httpStatus} response 5 times in a row`, async () => {
|
|
await withMockedInfuraCommunications(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.
|
|
//
|
|
// Note, however, that the block-ref middleware doesn't run the
|
|
// original request, as it fails before it gets to that point, so
|
|
// there is no need to mock the request again.
|
|
comms.mockInfuraRpcCall({
|
|
request: buildRequestWithReplacedBlockParam(
|
|
request,
|
|
blockParamIndex,
|
|
'0x100',
|
|
),
|
|
response: {
|
|
error: 'Some error',
|
|
httpStatus,
|
|
},
|
|
times: 5,
|
|
});
|
|
const promiseForResult = withInfuraClient(
|
|
async ({ makeRpcCall, clock }) => {
|
|
return await waitForPromiseToBeFulfilledAfterRunningAllTimers(
|
|
makeRpcCall(request),
|
|
clock,
|
|
);
|
|
},
|
|
);
|
|
|
|
await expect(promiseForResult).rejects.toThrow(
|
|
/^InfuraProvider - cannot complete request\. All retries exhausted\..+Gateway timeout/su,
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
['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) => {
|
|
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.mockInfuraRpcCall({
|
|
request: buildRequestWithReplacedBlockParam(
|
|
request,
|
|
blockParamIndex,
|
|
'0x100',
|
|
),
|
|
error: `${errorMessagePrefix}: Some message`,
|
|
times: 4,
|
|
});
|
|
comms.mockInfuraRpcCall({
|
|
request: buildRequestWithReplacedBlockParam(
|
|
request,
|
|
blockParamIndex,
|
|
'0x100',
|
|
),
|
|
response: {
|
|
result: 'the result',
|
|
httpStatus: 200,
|
|
},
|
|
});
|
|
// Note that the block-ref middleware will still allow the
|
|
// original request to go through.
|
|
comms.mockInfuraRpcCall({
|
|
request,
|
|
response: {
|
|
result: 'the result',
|
|
httpStatus: 200,
|
|
},
|
|
});
|
|
const result = await withInfuraClient(
|
|
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 "${errorMessagePrefix}" error is thrown while making the request to Infura 5 times in a row`, async () => {
|
|
await withMockedInfuraCommunications(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.
|
|
//
|
|
// Note, however, that the block-ref middleware doesn't run the
|
|
// original request, as it fails before it gets to that point, so
|
|
// there is no need to mock the request again.
|
|
comms.mockInfuraRpcCall({
|
|
request: buildRequestWithReplacedBlockParam(
|
|
request,
|
|
blockParamIndex,
|
|
'0x100',
|
|
),
|
|
error: `${errorMessagePrefix}: Some message`,
|
|
times: 5,
|
|
});
|
|
const promiseForResult = withInfuraClient(
|
|
async ({ makeRpcCall, clock }) => {
|
|
return await waitForPromiseToBeFulfilledAfterRunningAllTimers(
|
|
makeRpcCall(request),
|
|
clock,
|
|
);
|
|
},
|
|
);
|
|
|
|
await expect(promiseForResult).rejects.toThrow(
|
|
new RegExp(
|
|
`^InfuraProvider - cannot complete request\\. All retries exhausted\\..+${errorMessagePrefix}: Some message`,
|
|
'su',
|
|
),
|
|
);
|
|
});
|
|
});
|
|
},
|
|
);
|
|
}
|
|
});
|
|
|
|
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.mockInfuraRpcCall({
|
|
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.mockInfuraRpcCall({
|
|
request: requests[0],
|
|
response: { result: mockResults[0] },
|
|
});
|
|
comms.mockNextBlockTrackerRequest({ blockNumber: '0x2' });
|
|
comms.mockInfuraRpcCall({
|
|
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.mockInfuraRpcCall({
|
|
request: requests[0],
|
|
response: { result: mockResults[0] },
|
|
});
|
|
comms.mockInfuraRpcCall({
|
|
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.mockInfuraRpcCall({
|
|
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.mockInfuraRpcCall({
|
|
request: requests[0],
|
|
response: { result: 'first result' },
|
|
});
|
|
comms.mockInfuraRpcCall({
|
|
request: requests[1],
|
|
response: { result: 'second result' },
|
|
});
|
|
|
|
const results = await withInfuraClient(({ 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) => {
|
|
const request = {
|
|
method,
|
|
params: buildMockParamsWithBlockParamAt(blockParamIndex, '0x100'),
|
|
};
|
|
|
|
// The first time a block-cacheable request is made, the latest
|
|
// block number is retrieved through the block tracker first. This
|
|
// also happens within the retry-on-empty middleware (although the
|
|
// latest block is cached by now).
|
|
comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' });
|
|
// The retry-on-empty middleware will make an explicit request.
|
|
comms.mockInfuraRpcCall({
|
|
request,
|
|
response: { result: 'this result gets overwritten' },
|
|
});
|
|
// Note that the retry-on-empty middleware will still allow the
|
|
// original request to go through.
|
|
comms.mockInfuraRpcCall({
|
|
request,
|
|
response: { result: 'the actual result' },
|
|
});
|
|
|
|
const result = await withInfuraClient(({ makeRpcCall }) =>
|
|
makeRpcCall(request),
|
|
);
|
|
|
|
expect(result).toStrictEqual('the actual 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) => {
|
|
const request = {
|
|
method,
|
|
params: buildMockParamsWithBlockParamAt(blockParamIndex, '0x50'),
|
|
};
|
|
|
|
// The first time a block-cacheable request is made, the latest
|
|
// block number is retrieved through the block tracker first. This
|
|
// also happens within the retry-on-empty middleware (although the
|
|
// latest block is cached by now).
|
|
comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' });
|
|
// The retry-on-empty middleware will make an explicit request.
|
|
comms.mockInfuraRpcCall({
|
|
request,
|
|
response: { result: 'this result gets overwritten' },
|
|
});
|
|
// Note that the retry-on-empty middleware will still allow the
|
|
// original request to go through.
|
|
comms.mockInfuraRpcCall({
|
|
request,
|
|
response: { result: 'the actual result' },
|
|
});
|
|
|
|
const result = await withInfuraClient(({ makeRpcCall }) =>
|
|
makeRpcCall(request),
|
|
);
|
|
|
|
expect(result).toStrictEqual('the actual 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.mockInfuraRpcCall({
|
|
request: requests[0],
|
|
response: { result: mockResults[0] },
|
|
});
|
|
comms.mockInfuraRpcCall({
|
|
request: requests[1],
|
|
response: { result: mockResults[1] },
|
|
});
|
|
|
|
const results = await withInfuraClient(({ makeRpcCallsInSeries }) =>
|
|
makeRpcCallsInSeries(requests),
|
|
);
|
|
|
|
expect(results).toStrictEqual(mockResults);
|
|
});
|
|
});
|
|
});
|
|
}
|