1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-05 07:24:56 +01:00
metamask-extension/app/scripts/controllers/network/provider-api-tests/shared-tests.js
Mark Stacey f6f8edfd15
Update eth-json-rpc-middleware to v9.0.1 (#16096)
This update includes fixes for our `block-ref` and `retry-on-empty`
middleware.

The `block-ref` middleware resolves the block reference `latest` to a
specific block number, the latest one we are aware of. This is meant to
protect against situations where the network gives inconsistent answers
for what the latest block number is due to some nodes being out-of-sync
with each other (this was a frequent problem years ago with Infura).

It was broken in that the `latest` resolution was failing, and we were
submitting an additional redundant request to Infura for each request.

The `retry-on-empty` middleware is meant to retry certain methods
when they return an empty response. This was also meant to deal with
network synchronization issues that were more common years ago. This
middleware works by making a "child" request over and over until either
a retry limit is reached, or a non-empty response is received.

It was broken in that the final response recieved was thrown away, so
it's as though the middleware was not used. Except that it did result
in additional redundant network requests.

As a result of this update we should see that the extension is more
resilient to certain network synchronization issues. But this is
difficult to test, and these issues may not happen in production
anymore today.

We should see a reduction in requests to Infura as well. This should
be easier to test.
2022-10-10 13:16:58 -02:30

1399 lines
53 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] },
});
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] },
});
comms.mockNextBlockTrackerRequest({ blockNumber: '0x200' });
comms.mockInfuraRpcCall({
request: buildRequestWithReplacedBlockParam(
requests[1],
blockParamIndex,
'0x200',
),
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] },
});
comms.mockInfuraRpcCall({
request: buildRequestWithReplacedBlockParam(
requests[1],
blockParamIndex,
'0x100',
),
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, and we delay it.
comms.mockInfuraRpcCall({
delay: 100,
request: buildRequestWithReplacedBlockParam(
requests[0],
blockParamIndex,
'0x100',
),
response: { result: mockResults[0] },
});
// 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: buildRequestWithReplacedBlockParam(
requests[2],
blockParamIndex,
'0x100',
),
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.
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.
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.
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,
},
});
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.
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,
},
});
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.
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' });
comms.mockInfuraRpcCall({
request,
response: { result: 'the result' },
});
const result = await withInfuraClient(({ makeRpcCall }) =>
makeRpcCall(request),
);
expect(result).toStrictEqual('the result');
});
});
it('makes an additional request to Infura if the given block number is less than the latest block number', async () => {
await withMockedInfuraCommunications(async (comms) => {
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' });
comms.mockInfuraRpcCall({
request,
response: { result: 'the result' },
});
const result = await withInfuraClient(({ makeRpcCall }) =>
makeRpcCall(request),
);
expect(result).toStrictEqual('the 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);
});
});
});
}