Skip to content

Commit 04be516

Browse files
ebadiereNana-EC
andauthored
Reapplying refactor/enhancement in a fresh branch off of main since t… (#1186)
Reapplying refactor/enhancement in a fresh branch off of main since t… (#1164) This PR optimizes the getBalance method by removing unnecessary calls to the mirror node. --------- Signed-off-by: ebadiere <[email protected]> Signed-off-by: Nana Essilfie-Conduah <[email protected]> Co-authored-by: Nana Essilfie-Conduah <[email protected]>
1 parent 5983a39 commit 04be516

File tree

6 files changed

+464
-95
lines changed

6 files changed

+464
-95
lines changed

.github/workflows/dapp.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
setup-local-hedera:
1212
name: Dapp Tests
1313
runs-on: ubuntu-latest
14-
timeout-minutes: 35
14+
timeout-minutes: 35 # Set to 35 minutes for now
1515
permissions:
1616
contents: write
1717
steps:

packages/relay/src/lib/clients/mirrorNodeClient.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ export class MirrorNodeClient {
129129
private mirrorResponseHistogram;
130130

131131
private readonly cache;
132+
static readonly EVM_ADDRESS_REGEX: RegExp = /\/accounts\/([\d\.]+)/;
132133

133134
protected createAxiosClient(
134135
baseUrl: string
@@ -342,6 +343,49 @@ export class MirrorNodeClient {
342343
MirrorNodeClient.GET_ACCOUNTS_ENDPOINT,
343344
requestId);
344345
}
346+
/*******************************************************************************
347+
* To be used to make paginated calls for the account information when the
348+
* transaction count exceeds the constant MIRROR_NODE_QUERY_LIMIT.
349+
*******************************************************************************/
350+
public async getAccountPaginated(url: string, requestId?: string) {
351+
const queryParamObject = {};
352+
const accountId = this.extractAccountIdFromUrl(url, requestId);
353+
const params = new URLSearchParams(url.split('?')[1]);
354+
355+
this.setQueryParam(queryParamObject, 'limit', constants.MIRROR_NODE_QUERY_LIMIT);
356+
this.setQueryParam(queryParamObject, 'timestamp', params.get('timestamp'));
357+
const queryParams = this.getQueryParams(queryParamObject);
358+
359+
return this.getPaginatedResults(
360+
`${MirrorNodeClient.GET_ACCOUNTS_ENDPOINT}${accountId}${queryParams}`,
361+
MirrorNodeClient.GET_ACCOUNTS_ENDPOINT,
362+
'transactions',
363+
requestId
364+
);
365+
}
366+
367+
public extractAccountIdFromUrl(url: string, requestId?: string): string | null {
368+
const substringStartIndex = url.indexOf("/accounts/") + "/accounts/".length;
369+
if (url.startsWith("0x", substringStartIndex)) {
370+
// evm addresss
371+
const regex = /\/accounts\/(0x[a-fA-F0-9]{40})/;
372+
const match = url.match(regex);
373+
const accountId = match ? match[1] : null;
374+
if (!accountId) {
375+
this.logger.error(`${formatRequestIdMessage(requestId)} Unable to extract evm address from url ${url}`);
376+
}
377+
return String(accountId);
378+
} else {
379+
// account id
380+
const match = url.match(MirrorNodeClient.EVM_ADDRESS_REGEX);
381+
const accountId = match ? match[1] : null;
382+
if (!accountId) {
383+
this.logger.error(`${formatRequestIdMessage(requestId)} Unable to extract account ID from url ${url}`);
384+
}
385+
return String(accountId);
386+
}
387+
}
388+
345389

346390
public async getTransactionsForAccount(accountId: string, timestampFrom: string, timestampTo: string, requestId?: string) {
347391
const queryParamObject = {};

packages/relay/src/lib/eth.ts

Lines changed: 112 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
*
33
* Hedera JSON RPC Relay
44
*
5-
* Copyright (C) 2022 Hedera Hashgraph, LLC
5+
* Copyright (C) 2023 Hedera Hashgraph, LLC
66
*
77
* Licensed under the Apache License, Version 2.0 (the "License");
88
* you may not use this file except in compliance with the License.
@@ -35,6 +35,10 @@ import crypto from 'crypto';
3535
const LRU = require('lru-cache');
3636
const _ = require('lodash');
3737
const createHash = require('keccak');
38+
interface LatestBlockNumberTimestamp {
39+
blockNumber: string;
40+
timeStampTo: string;
41+
}
3842

3943
/**
4044
* Implementation of the "eth_" methods from the Ethereum JSON-RPC API.
@@ -378,6 +382,35 @@ export class EthImpl implements Eth {
378382
throw predefined.COULD_NOT_RETRIEVE_LATEST_BLOCK;
379383
}
380384

385+
/**
386+
* Gets the most recent block number and timestamp.to which represents the block finality.
387+
*/
388+
async blockNumberTimestamp(requestId?: string): Promise<LatestBlockNumberTimestamp> {
389+
const requestIdPrefix = formatRequestIdMessage(requestId);
390+
this.logger.trace(`${requestIdPrefix} blockNumber()`);
391+
392+
const cacheKey = `${constants.CACHE_KEY.ETH_BLOCK_NUMBER}`;
393+
394+
const blocksResponse = await this.mirrorNodeClient.getLatestBlock(requestId);
395+
const blocks = blocksResponse !== null ? blocksResponse.blocks : null;
396+
if (Array.isArray(blocks) && blocks.length > 0) {
397+
const currentBlock = EthImpl.numberTo0x(blocks[0].number);
398+
const timestamp = blocks[0].timestamp.to;
399+
const blockTimeStamp: LatestBlockNumberTimestamp = { blockNumber: currentBlock, timeStampTo: timestamp };
400+
// save the latest block number in cache
401+
this.cache.set(cacheKey, currentBlock, { ttl: EthImpl.ethBlockNumberCacheTtlMs });
402+
this.logger.trace(
403+
`${requestIdPrefix} caching ${cacheKey}:${JSON.stringify(currentBlock)}:${JSON.stringify(timestamp)} for ${
404+
EthImpl.ethBlockNumberCacheTtlMs
405+
} ms`
406+
);
407+
408+
return blockTimeStamp;
409+
}
410+
411+
throw predefined.COULD_NOT_RETRIEVE_LATEST_BLOCK;
412+
}
413+
381414
/**
382415
* Gets the chain ID. This is a static value, in that it always returns
383416
* the same value. This can be specified via an environment variable
@@ -624,15 +657,31 @@ export class EthImpl implements Eth {
624657
const latestBlockTolerance = 1;
625658
this.logger.trace(`${requestIdPrefix} getBalance(account=${account}, blockNumberOrTag=${blockNumberOrTag})`);
626659

660+
let latestBlock: LatestBlockNumberTimestamp | null | undefined;
627661
// this check is required, because some tools like Metamask pass for parameter latest block, with a number (ex 0x30ea)
628662
// tolerance is needed, because there is a small delay between requesting latest block from blockNumber and passing it here
629663
if (!EthImpl.blockTagIsLatestOrPending(blockNumberOrTag)) {
630-
const latestBlock = await this.blockNumber(requestId);
631-
const blockDiff = Number(latestBlock) - Number(blockNumberOrTag);
664+
const cacheKey = `${constants.CACHE_KEY.ETH_BLOCK_NUMBER}`;
665+
const blockNumberCached = this.cache.get(cacheKey);
666+
667+
if(blockNumberCached) {
668+
this.logger.trace(`${requestIdPrefix} returning cached value ${cacheKey}:${JSON.stringify(blockNumberCached)}`);
669+
latestBlock = { blockNumber: blockNumberCached, timeStampTo: '0' };
670+
671+
} else {
672+
latestBlock = await this.blockNumberTimestamp(requestId);
673+
}
674+
const blockDiff = Number(latestBlock.blockNumber) - Number(blockNumberOrTag);
632675

633676
if (blockDiff <= latestBlockTolerance) {
634677
blockNumberOrTag = EthImpl.blockLatest;
635678
}
679+
680+
// If ever we get the latest block from cache, and blockNumberOrTag is not latest, then we need to get the block timestamp
681+
// This should rarely happen.
682+
if((blockNumberOrTag !== EthImpl.blockLatest) && (latestBlock.timeStampTo === "0")) {
683+
latestBlock = await this.blockNumberTimestamp(requestId);
684+
}
636685
}
637686

638687
// check cache first
@@ -657,71 +706,52 @@ export class EthImpl implements Eth {
657706

658707
// A blockNumberOrTag has been provided. If it is `latest` or `pending` retrieve the balance from /accounts/{account.id}
659708
if (mirrorAccount) {
660-
const latestBlock = await this.getHistoricalBlockResponse(EthImpl.blockLatest, true, requestId);
661-
662709
// If the parsed blockNumber is the same as the one from the latest block retrieve the balance from /accounts/{account.id}
663-
if (latestBlock && block.number !== latestBlock.number) {
664-
const latestTimestamp = Number(latestBlock.timestamp.from.split('.')[0]);
710+
if (latestBlock && block.number !== latestBlock.blockNumber) {
711+
const latestTimestamp = Number(latestBlock.timeStampTo.split('.')[0]);
665712
const blockTimestamp = Number(block.timestamp.from.split('.')[0]);
666713
const timeDiff = latestTimestamp - blockTimestamp;
667714
// The block is from the last 15 minutes, therefore the historical balance hasn't been imported in the Mirror Node yet
668715
if (timeDiff < constants.BALANCES_UPDATE_INTERVAL) {
669716
let currentBalance = 0;
670-
let currentTimestamp;
671717
let balanceFromTxs = 0;
672718
if (mirrorAccount.balance) {
673719
currentBalance = mirrorAccount.balance.balance;
674-
currentTimestamp = mirrorAccount.balance.timestamp;
675720
}
676721

677-
// Need to check if there are any transactions before the block.timestamp.to in the current account set returned from the inital
678-
// call to getAccountPageLimit. If there are we may need to paginate.
679-
let lastTransactionOnPageTimestamp;
680-
if(mirrorAccount.links.next !== null) {
681-
// Get the end of the page of transactions timestamp
682-
const params = new URLSearchParams(mirrorAccount.links.next.split('?')[1]);
683-
if((params === null) || (params === undefined)) {
684-
this.logger.debug(`${requestIdPrefix} Unable to find expected search parameters in account next page link ${mirrorAccount.links.next}), returning 0x0 balance`);
685-
return EthImpl.zeroHex;
686-
}
687-
688-
const timestampParameters = params.getAll('timestamp');
689-
lastTransactionOnPageTimestamp = timestampParameters[0].split(':')[1];
690-
if((lastTransactionOnPageTimestamp === null) || (lastTransactionOnPageTimestamp === undefined)) {
691-
this.logger.debug(`${requestIdPrefix} Unable to find expected beginning (gte:) timestamp in account next page link ${mirrorAccount.links.next}), returning 0x0 balance`);
692-
return EthImpl.zeroHex;
693-
}
694-
}
695-
696-
let transactionsInTimeWindow: any = [];
697-
if((typeof lastTransactionOnPageTimestamp !== "undefined") && (mirrorAccount.transactions[mirrorAccount.transactions.length -1].consensus_timestamp >= lastTransactionOnPageTimestamp)) {
698-
transactionsInTimeWindow = await this.mirrorNodeClient.getTransactionsForAccount(
699-
mirrorAccount.account,
700-
block.timestamp.to,
701-
currentTimestamp,
702-
requestId
703-
);
704-
} else {
705-
transactionsInTimeWindow = mirrorAccount.transactions.filter((tx: any) => {
706-
return tx.consensus_timestamp >= block.timestamp.to && tx.consensus_timestamp <= currentTimestamp;
707-
});
708-
}
709-
710-
for(const tx of transactionsInTimeWindow) {
711-
for (const transfer of tx.transfers) {
712-
if (transfer.account === mirrorAccount.account && !transfer.is_approval) {
713-
balanceFromTxs += transfer.amount;
714-
}
722+
// The balance in the account is real time, so we simply subtract the transactions to the block.timestamp.to to get a block relevant balance.
723+
// needs to be updated below.
724+
const nextPage: string = mirrorAccount.links.next;
725+
726+
if(nextPage) {
727+
// If we have a pagination link that falls within the block.timestamp.to, we need to paginate to get the transactions for the block.timestamp.to
728+
const nextPageParams = new URLSearchParams(nextPage.split('?')[1]);
729+
const nextPageTimeMarker = nextPageParams.get('timestamp');
730+
if (nextPageTimeMarker && nextPageTimeMarker?.split(':')[1] >= block.timestamp.to) {
731+
// If nextPageTimeMarker is greater than the block.timestamp.to, then we need to paginate to get the transactions for the block.timestamp.to
732+
const pagedTransactions = await this.mirrorNodeClient.getAccountPaginated(nextPage, requestId);
733+
mirrorAccount.transactions = mirrorAccount.transactions.concat(pagedTransactions);
715734
}
735+
// If nextPageTimeMarker is less than the block.timestamp.to, then just run the getBalanceAtBlockTimestamp function in this case as well.
716736
}
717737

738+
balanceFromTxs = this.getBalanceAtBlockTimestamp(
739+
mirrorAccount.account,
740+
mirrorAccount.transactions,
741+
block.timestamp.to
742+
);
743+
718744
balanceFound = true;
719745
weibars = BigInt(currentBalance - balanceFromTxs) * BigInt(constants.TINYBAR_TO_WEIBAR_COEF);
720746
}
721747

722748
// The block is NOT from the last 15 minutes, use /balances rest API
723749
else {
724-
const balance = await this.mirrorNodeClient.getBalanceAtTimestamp(mirrorAccount.account, block.timestamp.from, requestId);
750+
const balance = await this.mirrorNodeClient.getBalanceAtTimestamp(
751+
mirrorAccount.account,
752+
block.timestamp.from,
753+
requestId
754+
);
725755
balanceFound = true;
726756
if (balance.balances?.length) {
727757
weibars = BigInt(balance.balances[0].balance) * BigInt(constants.TINYBAR_TO_WEIBAR_COEF);
@@ -731,20 +761,28 @@ export class EthImpl implements Eth {
731761
}
732762
}
733763
}
734-
764+
735765
if (!balanceFound && mirrorAccount?.balance) {
736766
balanceFound = true;
737767
weibars = BigInt(mirrorAccount.balance.balance) * BigInt(constants.TINYBAR_TO_WEIBAR_COEF);
738768
}
739769

740770
if (!balanceFound) {
741-
this.logger.debug(`${requestIdPrefix} Unable to find account ${account} in block ${JSON.stringify(blockNumber)}(${blockNumberOrTag}), returning 0x0 balance`);
771+
this.logger.debug(
772+
`${requestIdPrefix} Unable to find account ${account} in block ${JSON.stringify(
773+
blockNumber
774+
)}(${blockNumberOrTag}), returning 0x0 balance`
775+
);
742776
return EthImpl.zeroHex;
743777
}
744778

745779
// save in cache the current balance for the account and blockNumberOrTag
746-
this.cache.set(cacheKey, EthImpl.numberTo0x(weibars), {ttl: EthImpl.ethGetBalanceCacheTtlMs});
747-
this.logger.trace(`${requestIdPrefix} caching ${cacheKey}:${JSON.stringify(cachedBalance)} for ${EthImpl.ethGetBalanceCacheTtlMs} ms`);
780+
this.cache.set(cacheKey, EthImpl.numberTo0x(weibars), { ttl: EthImpl.ethGetBalanceCacheTtlMs });
781+
this.logger.trace(
782+
`${requestIdPrefix} caching ${cacheKey}:${JSON.stringify(cachedBalance)} for ${
783+
EthImpl.ethGetBalanceCacheTtlMs
784+
} ms`
785+
);
748786

749787
return EthImpl.numberTo0x(weibars);
750788
} catch (error: any) {
@@ -1722,4 +1760,26 @@ export class EthImpl implements Eth {
17221760
return predefined.INTERNAL_ERROR();
17231761
}
17241762

1763+
/**************************************************
1764+
* Returns the difference between the balance of *
1765+
* the account and the transactions summed up *
1766+
* to the block number queried. *
1767+
*************************************************/
1768+
getBalanceAtBlockTimestamp(account: string, transactions: any[], blockTimestamp: number) {
1769+
return transactions
1770+
.filter((transaction) => {
1771+
return transaction.consensus_timestamp >= blockTimestamp;
1772+
})
1773+
.flatMap((transaction) => {
1774+
return transaction.transfers.filter((transfer) => {
1775+
return transfer.account === account && !transfer.is_approval;
1776+
});
1777+
})
1778+
.map((transfer) => {
1779+
return transfer.amount;
1780+
})
1781+
.reduce((total, amount) => {
1782+
return total + amount;
1783+
}, 0);
1784+
}
17251785
}

0 commit comments

Comments
 (0)