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

eth_getLogs - add support for multiple addresses #719

Merged
merged 17 commits into from
Dec 7, 2022
Merged
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
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 @@ -140,5 +140,10 @@ export const predefined = {
code: -32008,
message: `execution reverted: ${decodeErrorMessage(errorMessage)}`,
data: errorMessage
}),
'MISSING_FROM_BLOCK_PARAM': new JsonRpcError({
name: 'Missing fromBlock parameter',
code: -32011,
message: 'Provided toBlock parameter without specifying fromBlock'
})
};
147 changes: 94 additions & 53 deletions packages/relay/src/lib/eth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1306,85 +1306,126 @@ export class EthImpl implements Eth {
});
}

async getLogs(blockHash: string | null, fromBlock: string | null, toBlock: string | null, address: string | null, topics: any[] | null, requestId?: string): Promise<Log[]> {
const params: any = {};
if (blockHash) {
try {
const block = await this.mirrorNodeClient.getBlock(blockHash, requestId);
if (block) {
params.timestamp = [
`gte:${block.timestamp.from}`,
`lte:${block.timestamp.to}`
];
}else {
return [];
}
private async validateBlockHashAndAddTimestampToParams(params: any, blockHash: string, requestId?: string) {
try {
const block = await this.mirrorNodeClient.getBlock(blockHash, requestId);
if (block) {
params.timestamp = [
`gte:${block.timestamp.from}`,
`lte:${block.timestamp.to}`
];
} else {
return false;
}
catch(e: any) {
if (e instanceof MirrorNodeClientError && e.isNotFound()) {
return [];
}

throw e;
}
catch(e: any) {
if (e instanceof MirrorNodeClientError && e.isNotFound()) {
return false;
}
} else {
const blockRangeLimit = Number(process.env.ETH_GET_LOGS_BLOCK_RANGE_LIMIT) || constants.DEFAULT_ETH_GET_LOGS_BLOCK_RANGE_LIMIT;
let fromBlockNum = 0;
let toBlockNum;

if (!fromBlock && !toBlock) {
const blockResponse = await this.getHistoricalBlockResponse("latest", true, requestId);
fromBlockNum = parseInt(blockResponse.number);
toBlockNum = parseInt(blockResponse.number);
params.timestamp = [`gte:${blockResponse.timestamp.from}`, `lte:${blockResponse.timestamp.to}`];
} else {
params.timestamp = [];

// Use the `toBlock` if it is the only passed tag, if not utilize the `fromBlock` or default to "latest"
const blockTag = toBlock && !fromBlock ? toBlock : fromBlock || "latest";
throw e;
}

const fromBlockResponse = await this.getHistoricalBlockResponse(blockTag, true, requestId);
if (fromBlockResponse != null) {
params.timestamp.push(`gte:${fromBlockResponse.timestamp.from}`);
fromBlockNum = parseInt(fromBlockResponse.number);
} else {
return [];
}
return true;
}

const toBlockResponse = await this.getHistoricalBlockResponse(toBlock || "latest", true, requestId);
if (toBlockResponse != null) {
params.timestamp.push(`lte:${toBlockResponse.timestamp.to}`);
toBlockNum = parseInt(toBlockResponse.number);
}
private async validateBlockRangeAndAddTimestampToParams(params: any, fromBlock: string | 'latest', toBlock: string | 'latest', requestId?: string) {
const blockRangeLimit = Number(process.env.ETH_GET_LOGS_BLOCK_RANGE_LIMIT) || constants.DEFAULT_ETH_GET_LOGS_BLOCK_RANGE_LIMIT;

if (EthImpl.blockTagIsLatestOrPending(toBlock)) {
toBlock = EthImpl.blockLatest;
}

// toBlock is a number and is less than the current block number and fromBlock is not defined
if (Number(toBlock) < Number(await this.blockNumber(requestId)) && !fromBlock) {
throw predefined.MISSING_FROM_BLOCK_PARAM;
}

if (EthImpl.blockTagIsLatestOrPending(fromBlock)) {
fromBlock = EthImpl.blockLatest;
}

let fromBlockNum = 0;
let toBlockNum;
params.timestamp = [];

const fromBlockResponse = await this.getHistoricalBlockResponse(fromBlock, true, requestId);
if (!fromBlockResponse) {
return false;
}

params.timestamp.push(`gte:${fromBlockResponse.timestamp.from}`);

if (fromBlock === toBlock) {
params.timestamp.push(`lte:${fromBlockResponse.timestamp.to}`);
}
else {
fromBlockNum = parseInt(fromBlockResponse.number);
const toBlockResponse = await this.getHistoricalBlockResponse(toBlock, true, requestId);
if (toBlockResponse != null) {
params.timestamp.push(`lte:${toBlockResponse.timestamp.to}`);
toBlockNum = parseInt(toBlockResponse.number);
}

if (fromBlockNum > toBlockNum) {
return [];
} else if ((toBlockNum - fromBlockNum) > blockRangeLimit) {
return false;
} else if (toBlockNum - fromBlockNum > blockRangeLimit) {
throw predefined.RANGE_TOO_LARGE(blockRangeLimit);
}
}

return true;
}

private addTopicsToParams(params: any, topics: any[] | null) {
if (topics) {
for (let i = 0; i < topics.length; i++) {
params[`topic${i}`] = topics[i];
}
}
}

private async getLogsByAddress(address: string | [string], params: any, requestId) {
const addresses = Array.isArray(address) ? address : [address];
const logPromises = addresses.map(addr => this.mirrorNodeClient.getContractResultsLogsByAddress(addr, params, undefined, requestId));

const logResults = await Promise.all(logPromises);
const logs = logResults.flatMap(logResult => logResult ? logResult : [] );
logs.sort((a: any, b: any) => {
return a.timestamp >= b.timestamp ? 1 : -1;
})

return logs;
}

async getLogs(blockHash: string | null, fromBlock: string | 'latest', toBlock: string | 'latest', address: string | [string] | null, topics: any[] | null, requestId?: string): Promise<Log[]> {
const EMPTY_RESPONSE = [];
const params: any = {};

if (blockHash) {
if ( !(await this.validateBlockHashAndAddTimestampToParams(params, blockHash, requestId)) ) {
return EMPTY_RESPONSE;
}
} else if ( !(await this.validateBlockRangeAndAddTimestampToParams(params, fromBlock, toBlock, requestId)) ) {
return EMPTY_RESPONSE;
}

this.addTopicsToParams(params, topics);

let results;
let logResults;
if (address) {
results = await this.mirrorNodeClient.getContractResultsLogsByAddress(address, params, undefined, requestId);
logResults = await this.getLogsByAddress(address, params, requestId);
}
else {
results = await this.mirrorNodeClient.getContractResultsLogs(params, undefined, requestId);
logResults = await this.mirrorNodeClient.getContractResultsLogs(params, undefined, requestId);
}

if (!results) {
return [];
if (!logResults) {
return EMPTY_RESPONSE;
}

const logs: Log[] = [];
for(const log of results) {
for(const log of logResults) {
logs.push(
new Log({
address: await this.getLogEvmAddress(log.address, requestId) || log.address,
Expand Down
2 changes: 1 addition & 1 deletion packages/relay/tests/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export const blockTransactionCount = 77;
export const gasUsed1 = 200000;
export const gasUsed2 = 800000;
export const maxGasLimit = 250000;
export const firstTransactionTimestampSeconds = '1653077547';
export const firstTransactionTimestampSeconds = '1653077541';
export const contractAddress1 = '0x000000000000000000000000000000000000055f';
export const contractTimestamp1 = `${firstTransactionTimestampSeconds}.983983199`;
export const contractHash1 = '0x4a563af33c4871b51a8b108aa2fe1dd5280a30dfb7236170ae5e5e7957eb6392';
Expand Down
69 changes: 67 additions & 2 deletions packages/relay/tests/lib/eth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ describe('Eth calls using MirrorNode', async function () {
const contractCallData = "0xef641f44";
const blockTimestamp = '1651560386';
const blockTimestampHex = EthImpl.numberTo0x(Number(blockTimestamp));
const firstTransactionTimestampSeconds = '1653077547';
const firstTransactionTimestampSeconds = '1653077541';
const contractAddress1 = '0x000000000000000000000000000000000000055f';
const htsTokenAddress = '0x0000000000000000000000000000000002dca431';
const contractTimestamp1 = `${firstTransactionTimestampSeconds}.983983199`;
Expand Down Expand Up @@ -445,6 +445,12 @@ describe('Eth calls using MirrorNode', async function () {
"runtime_bytecode": mirrorNodeDeployedBytecode
};

const defaultContract2 = {
...defaultContract,
"address": contractAddress2,
"contract_id": contractId2,
}

const defaultHTSToken =
{
"admin_key": null,
Expand Down Expand Up @@ -1537,6 +1543,15 @@ describe('Eth calls using MirrorNode', async function () {
});

describe('eth_getLogs', async function () {
const latestBlock = {
...defaultBlock,
number: 17,
'timestamp': {
'from': `1651560393.060890949`,
'to': '1651560395.060890949'
},
};

const expectLogData = (res, log, tx) => {
expect(res.address).to.eq(log.address);
expect(res.blockHash).to.eq(EthImpl.toHash32(tx.block_hash));
Expand Down Expand Up @@ -1733,6 +1748,32 @@ describe('Eth calls using MirrorNode', async function () {
expectLogData3(result[2]);
});

it('multiple addresses filter', async function () {
const filteredLogsAddress1 = {
logs: [defaultLogs.logs[0], defaultLogs.logs[1], defaultLogs.logs[2]]
};
const filteredLogsAddress2 = {
logs: defaultLogs3
};
mock.onGet("blocks?limit=1&order=desc").reply(200, { blocks: [defaultBlock] });
mock.onGet(`contracts/${contractAddress1}/results/logs?timestamp=gte:${defaultBlock.timestamp.from}&timestamp=lte:${defaultBlock.timestamp.to}&limit=100&order=asc`).reply(200, filteredLogsAddress1);
mock.onGet(`contracts/${contractAddress2}/results/logs?timestamp=gte:${defaultBlock.timestamp.from}&timestamp=lte:${defaultBlock.timestamp.to}&limit=100&order=asc`).reply(200, filteredLogsAddress2);
for (const log of filteredLogsAddress1.logs) {
mock.onGet(`contracts/${log.address}`).reply(200, defaultContract);
}
mock.onGet(`contracts/${contractAddress2}`).reply(200, defaultContract2);

const result = await ethImpl.getLogs(null, null, null, [contractAddress1, contractAddress2], null);

expect(result).to.exist;

expect(result.length).to.eq(4);
expectLogData1(result[0]);
expectLogData2(result[1]);
expectLogData3(result[2]);
expectLogData4(result[3]);
});

it('blockHash filter', async function () {
const filteredLogs = {
logs: [defaultLogs.logs[0], defaultLogs.logs[1]]
Expand Down Expand Up @@ -1764,6 +1805,7 @@ describe('Eth calls using MirrorNode', async function () {
},
};

mock.onGet('blocks?limit=1&order=desc').reply(200, {blocks: [latestBlock]});
mock.onGet('blocks/5').reply(200, defaultBlock);
mock.onGet('blocks/16').reply(200, toBlock);
mock.onGet(`contracts/results/logs?timestamp=gte:${defaultBlock.timestamp.from}&timestamp=lte:${toBlock.timestamp.to}&limit=100&order=asc`).reply(200, filteredLogs);
Expand All @@ -1779,6 +1821,8 @@ describe('Eth calls using MirrorNode', async function () {
});

it('with non-existing fromBlock filter', async function () {
mock.onGet('blocks?limit=1&order=desc').reply(200, {blocks: [latestBlock]});

mock.onGet('blocks/5').reply(200, defaultBlock);
mock.onGet('blocks/16').reply(404, {"_status": { "messages": [{"message": "Not found"}]}});

Expand All @@ -1793,6 +1837,7 @@ describe('Eth calls using MirrorNode', async function () {
logs: [defaultLogs.logs[0]]
};

mock.onGet('blocks?limit=1&order=desc').reply(200, {blocks: [latestBlock]});
mock.onGet('blocks/5').reply(200, defaultBlock);
mock.onGet('blocks/16').reply(404, {"_status": { "messages": [{"message": "Not found"}]}});
mock.onGet(`contracts/results/logs?timestamp=gte:${defaultBlock.timestamp.from}&limit=100&order=asc`).reply(200, filteredLogs);
Expand All @@ -1814,6 +1859,7 @@ describe('Eth calls using MirrorNode', async function () {
},
};

mock.onGet('blocks?limit=1&order=desc').reply(200, {blocks: [latestBlock]});
mock.onGet('blocks/16').reply(200, fromBlock);
mock.onGet('blocks/5').reply(200, defaultBlock);
const result = await ethImpl.getLogs(null, '0x10', '0x5', null, null);
Expand All @@ -1822,6 +1868,22 @@ describe('Eth calls using MirrorNode', async function () {
expect(result).to.be.empty;
});

it('with only toBlock', async function () {
mock.onGet('blocks?limit=1&order=desc').reply(200, {blocks: [latestBlock]});
mock.onGet('blocks/5').reply(200, {blocks: [defaultBlock]});

let hasError = false;
try {
await ethImpl.getLogs(null, null, '0x5', null, null);
} catch (e: any) {
hasError = true;
expect(e.code).to.equal(-32011);
expect(e.name).to.equal('Missing fromBlock parameter');
expect(e.message).to.equal('Provided toBlock parameter without specifying fromBlock');
}
expect(hasError).to.be.true;
});

it('with block tag', async function () {
const filteredLogs = {
logs: [defaultLogs.logs[0]]
Expand All @@ -1833,7 +1895,7 @@ describe('Eth calls using MirrorNode', async function () {
mock.onGet(`contracts/${log.address}`).reply(200, defaultContract);
}

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

expect(result).to.exist;
expectLogData1(result[0]);
Expand All @@ -1848,6 +1910,8 @@ describe('Eth calls using MirrorNode', async function () {
...defaultBlock,
number: 1003
};

mock.onGet('blocks?limit=1&order=desc').reply(200, {blocks: [latestBlock]});
mock.onGet('blocks/1').reply(200, fromBlock);
mock.onGet('blocks/1003').reply(200, toBlock);

Expand Down Expand Up @@ -1887,6 +1951,7 @@ describe('Eth calls using MirrorNode', async function () {
logs: [defaultLogs.logs[0], defaultLogs.logs[1]]
};

mock.onGet('blocks?limit=1&order=desc').reply(200, {blocks: [latestBlock]});
mock.onGet('blocks/5').reply(200, defaultBlock);
mock.onGet('blocks/16').reply(200, defaultBlock);
mock.onGet(
Expand Down
2 changes: 1 addition & 1 deletion packages/server/tests/acceptance/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ describe('RPC Server Acceptance Tests', function () {
logger.info(`OPERATOR_ID_MAIN: ${process.env.OPERATOR_ID_MAIN}`);
logger.info(`MIRROR_NODE_URL: ${process.env.MIRROR_NODE_URL}`);
logger.info(`E2E_RELAY_HOST: ${process.env.E2E_RELAY_HOST}`);

if (USE_LOCAL_NODE === 'true') {
runLocalHederaNetwork();
}
Expand Down
Loading