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

getLogs add support for block tags #518

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions packages/relay/src/lib/clients/mirrorNodeClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export class MirrorNodeClient {
return response.data;
} catch (error: any) {
ms = Date.now() - start;
const effectiveStatusCode = error.response !== undefined ? error.response.status : MirrorNodeClient.unknownServerErrorHttpStatusCode;
const effectiveStatusCode = error.response !== undefined ? error.response.status : MirrorNodeClient.unknownServerErrorHttpStatusCode;
this.mirrorResponseHistogram.labels(pathLabel, effectiveStatusCode).observe(ms);
this.handleError(error, path, effectiveStatusCode, allowedErrorStatuses, requestId);
}
Expand Down Expand Up @@ -346,10 +346,10 @@ export class MirrorNodeClient {

public async getLatestContractResultsByAddress(address: string, blockEndTimestamp: string | undefined, limit: number) {
// retrieve the timestamp of the contract
const contractResultsParams: IContractResultsParams = blockEndTimestamp
? { timestamp: `lte:${blockEndTimestamp}` }
const contractResultsParams: IContractResultsParams = blockEndTimestamp
? { timestamp: `lte:${blockEndTimestamp}` }
: {};
const limitOrderParams: ILimitOrderParams = this.getLimitOrderQueryParam(limit, 'desc');
const limitOrderParams: ILimitOrderParams = this.getLimitOrderQueryParam(limit, 'desc');
return this.getContractResultsByAddress(address, contractResultsParams, limitOrderParams);
}

Expand Down
1 change: 1 addition & 0 deletions packages/relay/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export default {

DEFAULT_TINY_BAR_GAS: 72, // (853454 / 1000) * (1 / 12)
ETH_FUNCTIONALITY_CODE: 84,
ETH_GET_LOGS_BLOCK_RANGE_LIMIT: Number(process.env.ETH_GET_LOGS_BLOCK_RANGE_LIMIT || 500),
EXCHANGE_RATE_FILE_ID: "0.0.112",
FEE_SCHEDULE_FILE_ID: '0.0.111',

Expand Down
7 changes: 7 additions & 0 deletions packages/relay/src/lib/errors/JsonRpcError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
*
*/

import constants from "../constants";

const REQUEST_ID_STRING = `Request ID: `;
export class JsonRpcError {
public code: number;
Expand Down Expand Up @@ -117,4 +119,9 @@ export const predefined = {
code: -32001,
message: 'Requested resource not found'
}),
'RANGE_TOO_LONG': new JsonRpcError({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: let's use large instead of long

name: 'Block range too long',
code: -32000,
message: `Exceeded maximum block range: ${constants.ETH_GET_LOGS_BLOCK_RANGE_LIMIT}`
})
};
85 changes: 58 additions & 27 deletions packages/relay/src/lib/eth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ export class EthImpl implements Eth {

return result;
}

/**
* Gets the balance of an account as of the given block.
*
Expand Down Expand Up @@ -704,11 +704,11 @@ export class EthImpl implements Eth {
const result = await this.mirrorNodeClient.resolveEntityType(address, requestId);
if (result?.type === constants.TYPE_ACCOUNT) {
const accountInfo = await this.sdkClient.getAccountInfo(result?.entity.account, EthImpl.ethGetTransactionCount, requestId);
return EthImpl.numberTo0x(Number(accountInfo.ethereumNonce));
return EthImpl.numberTo0x(Number(accountInfo.ethereumNonce));
}
else if (result?.type === constants.TYPE_CONTRACT) {
return EthImpl.numberTo0x(1);
}
}
} catch (e: any) {
this.logger.error(e, `${requestIdPrefix} Error raised during getTransactionCount for address ${address}, block number or tag ${blockNumOrTag}`);
return predefined.INTERNAL_ERROR;
Expand Down Expand Up @@ -792,7 +792,7 @@ export class EthImpl implements Eth {
gas = call.gas;
}
}

// Execute the call and get the response
this.logger.debug(`${requestIdPrefix} Making eth_call on contract ${call.to} with gas ${gas} and call data "${call.data}" from "${call.from}"`, call.to, gas, call.data, call.from);
const contractCallResponse = await this.sdkClient.submitContractCallQuery(call.to, call.data, gas, call.from, EthImpl.ethCall, requestId);
Expand Down Expand Up @@ -1036,7 +1036,7 @@ export class EthImpl implements Eth {
* @param blockNumberOrTag
* @param returnLatest
*/
private async getHistoricalBlockResponse(blockNumberOrTag?: string | null, returnLatest?: boolean): Promise<any | null> {
private async getHistoricalBlockResponse(blockNumberOrTag?: string | null, returnLatest?: boolean, requestId?: string | undefined): Promise<any | null> {
let blockResponse: any;
// Determine if the latest block should be returned and if not then just return null
if (!returnLatest &&
Expand All @@ -1045,16 +1045,16 @@ export class EthImpl implements Eth {
}

if (blockNumberOrTag == null || blockNumberOrTag === EthImpl.blockLatest || blockNumberOrTag === EthImpl.blockPending) {
const blockPromise = this.mirrorNodeClient.getLatestBlock();
const blockPromise = this.mirrorNodeClient.getLatestBlock(requestId);
const blockAnswer = await blockPromise;
blockResponse = blockAnswer.blocks[0];
} else if (blockNumberOrTag == EthImpl.blockEarliest) {
blockResponse = await this.mirrorNodeClient.getBlock(0);
blockResponse = await this.mirrorNodeClient.getBlock(0, requestId);
} else if (blockNumberOrTag.length < 32) {
// anything less than 32 characters is treated as a number
blockResponse = await this.mirrorNodeClient.getBlock(Number(blockNumberOrTag));
blockResponse = await this.mirrorNodeClient.getBlock(Number(blockNumberOrTag), requestId);
} else {
blockResponse = await this.mirrorNodeClient.getBlock(blockNumberOrTag);
blockResponse = await this.mirrorNodeClient.getBlock(blockNumberOrTag, requestId);
}
if (_.isNil(blockResponse) || blockResponse.hash === undefined) {
// block not found.
Expand Down Expand Up @@ -1146,28 +1146,59 @@ export class EthImpl implements Eth {
throw e;
}
} else if (fromBlock || toBlock) {
const filters = [];
let order;
let fromBlockTimestamp;
let toBlockTimestamp;
let fromBlockNum = 0;
let toBlockNum;

if (toBlock) {
// @ts-ignore
filters.push(`lte:${parseInt(toBlock)}`);
order = constants.ORDER.DESC;
try {
const blockResponse = await this.getHistoricalBlockResponse(toBlock, true, requestId);
toBlockTimestamp = blockResponse.timestamp.to;
toBlockNum = parseInt(blockResponse.number);
} catch (e) {
// Check if block error is RESOURCE_NOT_FOUND, the most likely scenario this will happen if a block number > height is passed
Nana-EC marked this conversation as resolved.
Show resolved Hide resolved
// In this case eth_getLogs() returns logs up to the latest block.
// @ts-ignore
if (e.statusCode != 404) {
Nana-EC marked this conversation as resolved.
Show resolved Hide resolved
throw e;
Nana-EC marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
if (fromBlock) {
// @ts-ignore
filters.push(`gte:${parseInt(fromBlock)}`);
order = constants.ORDER.ASC;
try {
const blockResponse = await this.getHistoricalBlockResponse(fromBlock, undefined, requestId);
fromBlockTimestamp = blockResponse.timestamp.from;
fromBlockNum = parseInt(blockResponse.number);
} catch (e) {
// Check if block error is RESOURCE_NOT_FOUND, the most likely scenario this will happen if a block number > height is passed
// In this case eth_getLogs() returns an empty array.
// @ts-ignore
if (e.statusCode != 404) {
Nana-EC marked this conversation as resolved.
Show resolved Hide resolved
throw e;
Nana-EC marked this conversation as resolved.
Show resolved Hide resolved
}

return [];
}
}

if(fromBlockTimestamp) {
params.timestamp = [`gte:${fromBlockTimestamp}`];
}

if(toBlockTimestamp){
params.timestamp
? params.timestamp.push(`lte:${toBlockTimestamp}`)
: params.timestamp = [`lte:${toBlockTimestamp}`];
} else {
const blockResponse = await this.getHistoricalBlockResponse('latest', true, requestId);
toBlockNum = parseInt(blockResponse.number);
}
const blocksResult = await this.mirrorNodeClient.getBlocks(filters, undefined, {order}, requestId);

const blocks = blocksResult?.blocks;
if (blocks?.length) {
const firstBlock = (order == constants.ORDER.DESC) ? blocks[blocks.length - 1] : blocks[0];
const lastBlock = (order == constants.ORDER.DESC) ? blocks[0] : blocks[blocks.length - 1];
params.timestamp = [
`gte:${firstBlock.timestamp.from}`,
`lte:${lastBlock.timestamp.to}`
];

if(fromBlockNum > toBlockNum) {
return [];
} else if((toBlockNum - fromBlockNum) > constants.ETH_GET_LOGS_BLOCK_RANGE_LIMIT) {
throw predefined.RANGE_TOO_LONG;
}
}

Expand Down
105 changes: 86 additions & 19 deletions packages/relay/tests/lib/eth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1014,7 +1014,6 @@ describe('Eth calls using MirrorNode', async function () {
});

describe('eth_getLogs', async function () {

const expectLogData = (res, log, tx, blockLogIndexOffset) => {
expect(res.address).to.eq(log.address);
expect(res.blockHash).to.eq(EthImpl.toHash32(tx.block_hash));
Expand Down Expand Up @@ -1158,22 +1157,91 @@ describe('Eth calls using MirrorNode', async function () {
expectLogData2(result[1]);
});

it('fromBlock && toBlock filter', async function () {
it('with valid fromBlock && toBlock filter', async function () {
const filteredLogs = {
logs: [defaultLogs.logs[0], defaultLogs.logs[1]]
};
const toBlock = {
...defaultBlock,
number: '0x10',
'timestamp': {
'from': `1651560391.060890949`,
'to': '1651560393.060890949'
},
};

mock.onGet(`contracts/${contractId1}/results/${contractTimestamp1}`).reply(200, defaultDetailedContractResults);
mock.onGet(`contracts/results/logs?timestamp=gte:${defaultBlock.timestamp.from}&timestamp=lte:${defaultBlock.timestamp.to}`).reply(200, filteredLogs);
mock.onGet('blocks?block.number=lte:16&block.number=gte:5&order=asc').reply(200, {
blocks: [defaultBlock]
});
mock.onGet(`contracts/results/logs?timestamp=gte:${defaultBlock.timestamp.from}&timestamp=lte:${toBlock.timestamp.to}`).reply(200, filteredLogs);
mock.onGet('blocks/5').reply(200, defaultBlock);
mock.onGet('blocks/16').reply(200, toBlock);
const result = await ethImpl.getLogs(null, '0x5', '0x10', null, null);

expect(result).to.exist;
expectLogData1(result[0]);
expectLogData2(result[1]);
});

it('with non-existing fromBlock filter', async function () {
mock.onGet('blocks/5').reply(200, defaultBlock);
mock.onGet('blocks/16').reply(404, {"_status": { "messages": [{"message": "Not found"}]}});
const result = await ethImpl.getLogs(null, '0x10', '0x5', null, null);

expect(result).to.exist;
expect(result).to.be.empty;
});

it('when fromBlock > toBlock', async function () {
const fromBlock = {
...defaultBlock,
number: '0x10',
'timestamp': {
'from': `1651560391.060890949`,
'to': '1651560393.060890949'
},
};

mock.onGet('blocks/16').reply(200, fromBlock);
mock.onGet('blocks/5').reply(200, defaultBlock);
const result = await ethImpl.getLogs(null, '0x10', '0x5', null, null);

expect(result).to.exist;
expect(result).to.be.empty;
});

it('with block tag', async function () {
const filteredLogs = {
logs: [defaultLogs.logs[0]]
};

mock.onGet(`contracts/${contractId1}/results/${contractTimestamp1}`).reply(200, defaultDetailedContractResults);
mock.onGet(`contracts/results/logs?timestamp=lte:${defaultBlock.timestamp.to}`).reply(200, filteredLogs);
mock.onGet('blocks?limit=1&order=desc').reply(200, { blocks: [defaultBlock] });
const result = await ethImpl.getLogs(null, null, 'latest', null, null);

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

it('when block range is too large', async function () {
const fromBlock = {
...defaultBlock,
number: '0x1'
};
const toBlock = {
...defaultBlock,
number: '0x1f6'
};
mock.onGet('blocks/1').reply(200, fromBlock);
mock.onGet('blocks/502').reply(200, toBlock);

try {
await ethImpl.getLogs(null, '0x1', '0x1f6', null, null);
} catch (error: any) {
expect(error.message).to.equal('Exceeded maximum block range: 500');
}
});


it('topics filter', async function () {
const filteredLogs = {
logs: [defaultLogs.logs[0], defaultLogs.logs[1]]
Expand All @@ -1184,9 +1252,8 @@ describe('Eth calls using MirrorNode', async function () {
`?topic0=${defaultLogTopics[0]}&topic1=${defaultLogTopics[1]}` +
`&topic2=${defaultLogTopics[2]}&topic3=${defaultLogTopics[3]}`
).reply(200, filteredLogs);
mock.onGet('blocks?block.number=gte:0x5&block.number=lte:0x10').reply(200, {
blocks: [defaultBlock]
});
mock.onGet('blocks/5').reply(200, defaultBlock);
mock.onGet('blocks/16').reply(200, defaultBlock);

const result = await ethImpl.getLogs(null, null, null, null, defaultLogTopics);

Expand All @@ -1204,7 +1271,7 @@ describe('Eth calls using MirrorNode', async function () {
const latestBlock = {...defaultBlock, number: blockNumber3};
const previousFees = JSON.parse(JSON.stringify(defaultNetworkFees));
const latestFees = JSON.parse(JSON.stringify(defaultNetworkFees));

previousFees.fees[2].gas += 1;

mock.onGet('blocks?limit=1&order=desc').reply(200, {blocks: [latestBlock]});
Expand Down Expand Up @@ -1247,7 +1314,7 @@ describe('Eth calls using MirrorNode', async function () {
it('eth_feeHistory verify cached value', async function () {
const latestBlock = {...defaultBlock, number: blockNumber3};
const latestFees = defaultNetworkFees;

mock.onGet('blocks?limit=1&order=desc').reply(200, {blocks: [latestBlock]});
mock.onGet(`blocks/${latestBlock.number}`).reply(200, latestBlock);
mock.onGet(`network/fees?timestamp=lte:${latestBlock.timestamp.to}`).reply(200, latestFees);
Expand All @@ -1265,7 +1332,7 @@ describe('Eth calls using MirrorNode', async function () {

it('eth_feeHistory on mirror 404', async function () {
const latestBlock = {...defaultBlock, number: blockNumber3};

mock.onGet('blocks?limit=1&order=desc').reply(200, {blocks: [latestBlock]});
mock.onGet(`blocks/${latestBlock.number}`).reply(200, latestBlock);
mock.onGet(`network/fees?timestamp=lte:${latestBlock.timestamp.to}`).reply(404, {
Expand All @@ -1291,7 +1358,7 @@ describe('Eth calls using MirrorNode', async function () {

it('eth_feeHistory on mirror 500', async function () {
const latestBlock = {...defaultBlock, number: blockNumber3};

mock.onGet('blocks?limit=1&order=desc').reply(200, {blocks: [latestBlock]});
mock.onGet(`blocks/${latestBlock.number}`).reply(200, latestBlock);
mock.onGet(`network/fees?timestamp=lte:${latestBlock.timestamp.to}`).reply(404, {
Expand Down Expand Up @@ -1431,7 +1498,7 @@ describe('Eth calls using MirrorNode', async function () {
}
);

var result = await ethImpl.call({
const result = await ethImpl.call({
"from": contractAddress1,
"to": contractAddress2,
"gas": maxGasLimitHex
Expand Down Expand Up @@ -1507,7 +1574,7 @@ describe('Eth calls using MirrorNode', async function () {
const result = await ethImpl.getStorageAt(contractAddress1, defaultDetailedContractResults.state_changes[0].slot, EthImpl.numberTo0x(blockNumber));
expect(result).to.exist;
if (result == null) return;

// verify slot value
expect(result).equal(defaultDetailedContractResults.state_changes[0].value_written);
});
Expand All @@ -1520,7 +1587,7 @@ describe('Eth calls using MirrorNode', async function () {
const result = await ethImpl.getStorageAt(contractAddress1, defaultDetailedContractResults.state_changes[0].slot, "latest");
expect(result).to.exist;
if (result == null) return;

// verify slot value
expect(result).equal(defaultDetailedContractResults.state_changes[0].value_written);
});
Expand All @@ -1533,7 +1600,7 @@ describe('Eth calls using MirrorNode', async function () {
const result = await ethImpl.getStorageAt(contractAddress1, defaultDetailedContractResults.state_changes[0].slot);
expect(result).to.exist;
if (result == null) return;

// verify slot value
expect(result).equal(defaultDetailedContractResults.state_changes[0].value_written);
});
Expand All @@ -1543,7 +1610,7 @@ describe('Eth calls using MirrorNode', async function () {
let hasError = false;
try {
mock.onGet(`blocks/${blockNumber}`).reply(200, null);
const result = await ethImpl.getStorageAt(contractAddress1, defaultDetailedContractResults.state_changes[0].slot, EthImpl.numberTo0x(blockNumber));
const result = await ethImpl.getStorageAt(contractAddress1, defaultDetailedContractResults.state_changes[0].slot, EthImpl.numberTo0x(blockNumber));
} catch (e: any) {
hasError = true;
expect(e.code).to.equal(-32001);
Expand All @@ -1565,7 +1632,7 @@ describe('Eth calls using MirrorNode', async function () {
hasError = true;
expect(e.statusCode).to.equal(404);
expect(e.message).to.equal("Request failed with status code 404");
}
}
expect(hasError).to.be.true;
});
});
Expand Down