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';
3535const LRU = require ( 'lru-cache' ) ;
3636const _ = require ( 'lodash' ) ;
3737const 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