Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enhanced eth_getLogs with timestamp range validation and new error handling #3431

Merged
5 changes: 5 additions & 0 deletions packages/relay/src/lib/errors/JsonRpcError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@
code: -32000,
message: `Exceeded maximum block range: ${blockRange}`,
}),
TIMESTAMP_RANGE_TOO_LARGE: (fromBlock: string, fromTimestamp: number, toBlock: string, toTimestamp: number) =>
quiet-node marked this conversation as resolved.
Show resolved Hide resolved
new JsonRpcError({

Check warning on line 137 in packages/relay/src/lib/errors/JsonRpcError.ts

View check run for this annotation

Codecov / codecov/patch

packages/relay/src/lib/errors/JsonRpcError.ts#L137

Added line #L137 was not covered by tests
code: -32000,
quiet-node marked this conversation as resolved.
Show resolved Hide resolved
message: `The provided fromBlock and toBlock contain timestamps that exceed the maximum allowed duration of 7 days (604800 seconds): fromBlock: ${fromBlock} (${fromTimestamp}), toBlock: ${toBlock} (${toTimestamp})`,
}),
REQUEST_BEYOND_HEAD_BLOCK: (requested: number, latest: number) =>
new JsonRpcError({
code: -32000,
Expand Down
31 changes: 31 additions & 0 deletions packages/relay/src/lib/eth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2767,6 +2767,37 @@ export class EthImpl implements Eth {
return await this.getAcccountNonceFromContractResult(address, blockNum, requestDetails);
}

/**
* Retrieves logs based on the provided parameters.
quiet-node marked this conversation as resolved.
Show resolved Hide resolved
*
* The function handles log retrieval as follows:
*
* - Using `blockHash`:
* - If `blockHash` is provided, logs are retrieved based on the timestamp of the block associated with the `blockHash`.
*
* - Without `blockHash`:
*
* - If only `fromBlock` is provided:
* - Logs are retrieved from `fromBlock` to the latest block.
* - If `fromBlock` does not exist, an empty array is returned.
*
* - If only `toBlock` is provided:
* - A predefined error `MISSING_FROM_BLOCK_PARAM` is thrown because `fromBlock` is required.
*
* - If both `fromBlock` and `toBlock` are provided:
* - Logs are retrieved from `fromBlock` to `toBlock`.
* - If `toBlock` does not exist, an empty array is returned.
* - If the timestamp range between `fromBlock` and `toBlock` exceeds 7 days, a predefined error `TIMESTAMP_RANGE_TOO_LARGE` is thrown.
*
* @param {string | null} blockHash - The block hash to prioritize log retrieval.
* @param {string | 'latest'} fromBlock - The starting block for log retrieval.
* @param {string | 'latest'} toBlock - The ending block for log retrieval.
* @param {string | string[] | null} address - The address(es) to filter logs by.
* @param {any[] | null} topics - The topics to filter logs by.
* @param {RequestDetails} requestDetails - The details of the request.
* @returns {Promise<Log[]>} - A promise that resolves to an array of logs or an empty array if no logs are found.
* @throws {Error} Throws specific errors like `MISSING_FROM_BLOCK_PARAM` or `TIMESTAMP_RANGE_TOO_LARGE` when applicable.
*/
async getLogs(
blockHash: string | null,
fromBlock: string | 'latest',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,29 @@
} else {
fromBlockNum = parseInt(fromBlockResponse.number);
const toBlockResponse = await this.getHistoricalBlockResponse(requestDetails, toBlock, true);
if (toBlockResponse != null) {
params.timestamp.push(`lte:${toBlockResponse.timestamp.to}`);
toBlockNum = parseInt(toBlockResponse.number);

/**
* If `toBlock` is not provided, the `lte` field cannot be set,
* resulting in a request to the Mirror Node that includes only the `gte` parameter.
* Such requests will be rejected, hence causing the whole request to fail.
* Return false to handle this gracefully and return an empty response to end client.
*/
if (!toBlockResponse) {
return false;

Check warning on line 151 in packages/relay/src/lib/services/ethService/ethCommonService/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/relay/src/lib/services/ethService/ethCommonService/index.ts#L151

Added line #L151 was not covered by tests
}

params.timestamp.push(`lte:${toBlockResponse.timestamp.to}`);
toBlockNum = parseInt(toBlockResponse.number);

Check warning on line 155 in packages/relay/src/lib/services/ethService/ethCommonService/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/relay/src/lib/services/ethService/ethCommonService/index.ts#L154-L155

Added lines #L154 - L155 were not covered by tests

// Add timestamp range validation (7 days = 604800 seconds)
const timestampDiff = toBlockResponse.timestamp.to - fromBlockResponse.timestamp.from;

Check warning on line 158 in packages/relay/src/lib/services/ethService/ethCommonService/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/relay/src/lib/services/ethService/ethCommonService/index.ts#L158

Added line #L158 was not covered by tests
if (timestampDiff > 604800) {
quiet-node marked this conversation as resolved.
Show resolved Hide resolved
throw predefined.TIMESTAMP_RANGE_TOO_LARGE(

Check warning on line 160 in packages/relay/src/lib/services/ethService/ethCommonService/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/relay/src/lib/services/ethService/ethCommonService/index.ts#L160

Added line #L160 was not covered by tests
`0x${fromBlockNum.toString(16)}`,
quiet-node marked this conversation as resolved.
Show resolved Hide resolved
fromBlockResponse.timestamp.from,
`0x${toBlockNum.toString(16)}`,
toBlockResponse.timestamp.to,
);
}

if (fromBlockNum > toBlockNum) {
Expand Down
42 changes: 40 additions & 2 deletions packages/relay/tests/lib/eth/eth_getLogs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ import {
} from '../../helpers';
import {
BLOCK_HASH,
BLOCK_NUMBER_2,
BLOCK_NUMBER_3,
BLOCKS_LIMIT_ORDER_URL,
CONTRACT_ADDRESS_1,
CONTRACT_ADDRESS_2,
Expand Down Expand Up @@ -430,7 +432,7 @@ describe('@ethGetLogs using MirrorNode', async function () {
expect(result).to.be.empty;
});

it('with non-existing toBlock filter', async function () {
it('should return empty response if toBlock is not existed', async function () {
const filteredLogs = {
logs: [DEFAULT_LOGS.logs[0]],
};
Expand All @@ -446,7 +448,7 @@ describe('@ethGetLogs using MirrorNode', async function () {
const result = await ethImpl.getLogs(null, '0x5', '0x10', null, null, requestDetails);

expect(result).to.exist;
expectLogData1(result[0]);
expect(result).to.be.empty;
});

it('when fromBlock > toBlock', async function () {
Expand Down Expand Up @@ -608,4 +610,40 @@ describe('@ethGetLogs using MirrorNode', async function () {
expect(result.length).to.eq(0);
expect(result).to.deep.equal([]);
});

it('Should throw TIMESTAMP_RANGE_TOO_LARGE predefined error if timestamp range between fromBlock and toBlock exceed the maximum allowed duration of 7 days', async () => {
const mockedFromTimeStamp = 1651560389;
const mockedToTimeStamp = mockedFromTimeStamp + 604800 * 2 + 1; // 7 days (604800 seconds) and 1 second greater than mockedFromTimeStamp

restMock.onGet(BLOCKS_LIMIT_ORDER_URL).reply(200, { blocks: [latestBlock] });
restMock.onGet(`blocks/${BLOCK_NUMBER_2}`).reply(200, {
...DEFAULT_BLOCK,
timestamp: { ...DEFAULT_BLOCK.timestamp, from: mockedFromTimeStamp.toString() },
number: BLOCK_NUMBER_2,
});

restMock.onGet(`blocks/${BLOCK_NUMBER_3}`).reply(200, {
...DEFAULT_BLOCK,
timestamp: { ...DEFAULT_BLOCK.timestamp, to: mockedToTimeStamp.toString() },
number: BLOCK_NUMBER_3,
});

await expect(
ethImpl.getLogs(
null,
BLOCK_NUMBER_2.toString(16),
BLOCK_NUMBER_3.toString(16),
ethers.ZeroAddress,
DEFAULT_LOG_TOPICS,
requestDetails,
),
).to.be.rejectedWith(
predefined.TIMESTAMP_RANGE_TOO_LARGE(
`0x${BLOCK_NUMBER_2.toString(16)}`,
mockedFromTimeStamp,
`0x${BLOCK_NUMBER_3.toString(16)}`,
mockedToTimeStamp,
).message,
);
});
});
18 changes: 18 additions & 0 deletions packages/server/tests/acceptance/rpc_batch1.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,24 @@ describe('@api-batch-1 RPC Server Acceptance Tests', function () {
}
});

it('should return empty logs if `toBlock` is not found', async () => {
const notExistedLog = latestBlock + 99;

const logs = await relay.call(
RelayCalls.ETH_ENDPOINTS.ETH_GET_LOGS,
[
{
fromBlock: log0Block.blockNumber,
toBlock: `0x${notExistedLog.toString(16)}`,
address: [contractAddress, contractAddress2],
},
],
requestIdPrefix,
);

expect(logs.length).to.eq(0);
});

it('should be able to use `address` param', async () => {
//when we pass only address, it defaults to the latest block
const logs = await relay.call(
Expand Down
Loading