2023-12-22 12:50:38 -08:00
import {
EvmBlock ,
EvmLogFilter ,
EvmLog ,
EvmTag ,
ReceiptTransaction ,
} from "../../../domain/entities" ;
2023-12-06 08:07:14 -08:00
import { EvmBlockRepository } from "../../../domain/repositories" ;
import winston from "../../log" ;
import { HttpClient } from "../../rpc/http/HttpClient" ;
import { HttpClientError } from "../../errors/HttpClientError" ;
import { ChainRPCConfig } from "../../config" ;
2023-11-28 11:00:45 -08:00
/ * *
* EvmJsonRPCBlockRepository is a repository that uses a JSON RPC endpoint to fetch blocks .
* On the reliability side , only knows how to timeout .
* /
2023-11-30 07:05:43 -08:00
const HEXADECIMAL_PREFIX = "0x" ;
2023-11-28 11:00:45 -08:00
export class EvmJsonRPCBlockRepository implements EvmBlockRepository {
2023-12-11 14:11:42 -08:00
protected httpClient : HttpClient ;
2023-12-13 10:09:33 -08:00
protected cfg : EvmJsonRPCBlockRepositoryCfg ;
2023-12-11 14:11:42 -08:00
protected readonly logger ;
2023-11-28 11:00:45 -08:00
constructor ( cfg : EvmJsonRPCBlockRepositoryCfg , httpClient : HttpClient ) {
this . httpClient = httpClient ;
2023-12-04 04:47:02 -08:00
this . cfg = cfg ;
this . logger = winston . child ( { module : "EvmJsonRPCBlockRepository" } ) ;
this . logger . info ( ` Created for ${ Object . keys ( this . cfg . chains ) } ` ) ;
2023-11-28 11:00:45 -08:00
}
2023-12-04 04:47:02 -08:00
async getBlockHeight ( chain : string , finality : EvmTag ) : Promise < bigint > {
const block : EvmBlock = await this . getBlock ( chain , finality ) ;
2023-11-28 11:00:45 -08:00
return block . number ;
}
/ * *
* Get blocks by block number .
* @param blockNumbers
* @returns a record of block hash - > EvmBlock
* /
2023-12-04 04:47:02 -08:00
async getBlocks ( chain : string , blockNumbers : Set < bigint > ) : Promise < Record < string , EvmBlock > > {
2023-11-28 11:00:45 -08:00
if ( ! blockNumbers . size ) return { } ;
const reqs : any [ ] = [ ] ;
for ( let blockNumber of blockNumbers ) {
2023-11-30 07:05:43 -08:00
const blockNumberStrParam = ` ${ HEXADECIMAL_PREFIX } ${ blockNumber . toString ( 16 ) } ` ;
const blockNumberStrId = blockNumber . toString ( ) ;
2023-11-28 11:00:45 -08:00
reqs . push ( {
jsonrpc : "2.0" ,
2023-11-30 07:05:43 -08:00
id : blockNumberStrId ,
2023-11-28 11:00:45 -08:00
method : "eth_getBlockByNumber" ,
2023-11-30 07:05:43 -08:00
params : [ blockNumberStrParam , false ] ,
2023-11-28 11:00:45 -08:00
} ) ;
}
2023-12-04 04:47:02 -08:00
const chainCfg = this . getCurrentChain ( chain ) ;
2023-11-28 11:00:45 -08:00
let results : ( undefined | { id : string ; result? : EvmBlock ; error? : ErrorBlock } ) [ ] ;
try {
2023-12-06 08:07:14 -08:00
results = await this . httpClient . post < typeof results > ( chainCfg . rpc . href , reqs , {
timeout : chainCfg.timeout ,
retries : chainCfg.retries ,
} ) ;
2023-11-28 11:00:45 -08:00
} catch ( e : HttpClientError | any ) {
2023-12-11 14:11:42 -08:00
this . handleError ( chain , e , "getBlocks" , "eth_getBlockByNumber" ) ;
2023-11-28 11:00:45 -08:00
throw e ;
}
if ( results && results . length ) {
return results
. map (
(
response : undefined | { id : string ; result? : EvmBlock ; error? : ErrorBlock } ,
idx : number
) = > {
// Karura is getting 6969 errors for some blocks, so we'll just return empty blocks for those instead of throwing an error.
// We take the timestamp from the previous block, which is not ideal but should be fine.
if (
( response && response . result === null ) ||
( response ? . error && response . error ? . code && response . error . code === 6969 )
) {
return {
hash : "" ,
number : BigInt ( response . id ) ,
timestamp : Date.now ( ) ,
} ;
}
if (
response ? . result &&
response . result ? . hash &&
response . result . number &&
response . result . timestamp
) {
return {
hash : response.result.hash ,
number : BigInt ( response . result . number ) ,
timestamp : Number ( response . result . timestamp ) ,
} ;
}
2023-12-04 04:47:02 -08:00
const msg = ` [ ${ chain } ][getBlocks] Got error ${
2023-11-30 07:05:43 -08:00
response ? . error ? . message
2023-12-04 04:47:02 -08:00
} for eth_getBlockByNumber for $ { response ? . id ? ? reqs [ idx ] . id } on $ {
chainCfg . rpc . hostname
} ` ;
2023-11-28 11:00:45 -08:00
this . logger . error ( msg ) ;
throw new Error (
2023-12-04 04:47:02 -08:00
` Unable to parse result of eth_getBlockByNumber[ ${ chain } ] for ${
2023-11-28 11:00:45 -08:00
response ? . id ? ? reqs [ idx ] . id
} : $ { msg } `
) ;
}
)
. reduce ( ( acc : Record < string , EvmBlock > , block : EvmBlock ) = > {
acc [ block . hash ] = block ;
return acc ;
} , { } ) ;
}
throw new Error (
` Unable to parse ${
results ? . length ? ? 0
2023-12-04 04:47:02 -08:00
} blocks for eth_getBlockByNumber for numbers $ { blockNumbers } on $ { chainCfg . rpc . hostname } `
2023-11-28 11:00:45 -08:00
) ;
}
2023-12-04 04:47:02 -08:00
async getFilteredLogs ( chain : string , filter : EvmLogFilter ) : Promise < EvmLog [ ] > {
2023-11-28 11:00:45 -08:00
const parsedFilters = {
topics : filter.topics ,
address : filter.addresses ,
2023-11-30 07:05:43 -08:00
fromBlock : ` ${ HEXADECIMAL_PREFIX } ${ filter . fromBlock . toString ( 16 ) } ` ,
toBlock : ` ${ HEXADECIMAL_PREFIX } ${ filter . toBlock . toString ( 16 ) } ` ,
2023-11-28 11:00:45 -08:00
} ;
2023-12-04 04:47:02 -08:00
const chainCfg = this . getCurrentChain ( chain ) ;
2023-11-28 11:00:45 -08:00
let response : { result : Log [ ] ; error? : ErrorBlock } ;
try {
2023-12-06 08:07:14 -08:00
response = await this . httpClient . post < typeof response > (
chainCfg . rpc . href ,
{
jsonrpc : "2.0" ,
method : "eth_getLogs" ,
params : [ parsedFilters ] ,
id : 1 ,
} ,
{ timeout : chainCfg.timeout , retries : chainCfg.retries }
) ;
2023-11-28 11:00:45 -08:00
} catch ( e : HttpClientError | any ) {
2023-12-11 14:11:42 -08:00
this . handleError ( chain , e , "getFilteredLogs" , "eth_getLogs" ) ;
2023-11-28 11:00:45 -08:00
throw e ;
}
const logs = response ? . result ;
this . logger . info (
2023-12-04 04:47:02 -08:00
` [ ${ chain } ][getFilteredLogs] Got ${ logs ? . length } logs for ${ this . describeFilter (
filter
) } from $ { chainCfg . rpc . hostname } `
2023-11-28 11:00:45 -08:00
) ;
2023-12-05 04:34:25 -08:00
return logs
? logs . map ( ( log ) = > ( {
. . . log ,
blockNumber : BigInt ( log . blockNumber ) ,
transactionIndex : log.transactionIndex.toString ( ) ,
chainId : chainCfg.chainId ,
} ) )
: [ ] ;
2023-11-28 11:00:45 -08:00
}
private describeFilter ( filter : EvmLogFilter ) : string {
return ` [addresses: ${ filter . addresses } ][topics: ${ filter . topics } ][blocks: ${ filter . fromBlock } - ${ filter . toBlock } ] ` ;
}
/ * *
* Loosely based on the wormhole - dashboard implementation ( minus some specially crafted blocks when null result is obtained )
* /
2023-12-22 12:50:38 -08:00
async getBlock (
chain : string ,
blockNumberOrTag : EvmTag | bigint ,
isTransactionsPresent : boolean = false
) : Promise < EvmBlock > {
2023-12-12 05:27:30 -08:00
const blockNumberParam =
typeof blockNumberOrTag === "bigint"
? ` ${ HEXADECIMAL_PREFIX } ${ blockNumberOrTag . toString ( 16 ) } `
: blockNumberOrTag ;
2023-12-04 04:47:02 -08:00
const chainCfg = this . getCurrentChain ( chain ) ;
2023-11-28 11:00:45 -08:00
let response : { result? : EvmBlock ; error? : ErrorBlock } ;
try {
2023-12-06 08:07:14 -08:00
response = await this . httpClient . post < typeof response > (
chainCfg . rpc . href ,
{
jsonrpc : "2.0" ,
method : "eth_getBlockByNumber" ,
2023-12-22 12:50:38 -08:00
params : [ blockNumberParam , isTransactionsPresent ] , // this means we'll get a light block (no txs)
2023-12-06 08:07:14 -08:00
id : 1 ,
} ,
{ timeout : chainCfg.timeout , retries : chainCfg.retries }
) ;
2023-11-28 11:00:45 -08:00
} catch ( e : HttpClientError | any ) {
2023-12-11 14:11:42 -08:00
this . handleError ( chain , e , "getBlock" , "eth_getBlockByNumber" ) ;
2023-11-28 11:00:45 -08:00
throw e ;
}
const result = response ? . result ;
if ( result && result . hash && result . number && result . timestamp ) {
// Convert to our domain compatible type
return {
number : BigInt ( result . number ) ,
timestamp : Number ( result . timestamp ) ,
hash : result.hash ,
2023-12-22 12:50:38 -08:00
transactions : result.transactions ,
2023-11-28 11:00:45 -08:00
} ;
}
throw new Error (
2023-12-04 04:47:02 -08:00
` Unable to parse result of eth_getBlockByNumber for ${ blockNumberOrTag } on ${ chainCfg . rpc } `
2023-11-28 11:00:45 -08:00
) ;
}
2023-12-22 12:50:38 -08:00
/ * *
* Get the transaction ReceiptTransaction . Hash param refers to transaction hash
* /
async getTransactionReceipt (
chain : string ,
hashNumbers : Set < string >
) : Promise < Record < string , ReceiptTransaction > > {
const chainCfg = this . getCurrentChain ( chain ) ;
let results : { result : ReceiptTransaction ; error? : ErrorBlock } [ ] ;
const reqs : any [ ] = [ ] ;
for ( let hash of hashNumbers ) {
reqs . push ( {
jsonrpc : "2.0" ,
id : 1 ,
method : "eth_getTransactionReceipt" ,
params : [ hash ] ,
} ) ;
}
try {
results = await this . httpClient . post < typeof results > ( chainCfg . rpc . href , reqs , {
timeout : chainCfg.timeout ,
retries : chainCfg.retries ,
} ) ;
} catch ( e : HttpClientError | any ) {
this . handleError ( chain , e , "getTransactionReceipt" , "eth_getTransactionReceipt" ) ;
throw e ;
}
if ( results && results . length ) {
return results
. map ( ( response ) = > {
if ( response . result ? . status && response . result ? . transactionHash ) {
return {
status : response.result.status ,
transactionHash : response.result.transactionHash ,
} ;
}
const msg = ` [ ${ chain } ][getTransactionReceipt] Got error ${ response ? . error } for eth_getTransactionReceipt for ${ hashNumbers } on ${ chainCfg . rpc . hostname } ` ;
this . logger . error ( msg ) ;
throw new Error (
` Unable to parse result of eth_getTransactionReceipt[ ${ chain } ] for ${ response ? . result } : ${ msg } `
) ;
} )
. reduce (
( acc : Record < string , ReceiptTransaction > , receiptTransaction : ReceiptTransaction ) = > {
acc [ receiptTransaction . transactionHash ] = receiptTransaction ;
return acc ;
} ,
{ }
) ;
}
throw new Error (
` Unable to parse result of eth_getTransactionReceipt for ${ hashNumbers } on ${ chainCfg . rpc } `
) ;
}
2023-12-11 14:11:42 -08:00
protected handleError ( chain : string , e : any , method : string , apiMethod : string ) {
2023-12-04 04:47:02 -08:00
const chainCfg = this . getCurrentChain ( chain ) ;
2023-11-28 11:00:45 -08:00
if ( e instanceof HttpClientError ) {
this . logger . error (
2023-12-11 14:11:42 -08:00
` [ ${ chain } ][ ${ method } ] Got ${ e . status } from ${ chainCfg . rpc . hostname } / ${ apiMethod } . ${
2023-11-30 07:05:43 -08:00
e ? . message ? ? ` ${ e ? . message } `
} `
2023-11-28 11:00:45 -08:00
) ;
} else {
2023-12-04 04:47:02 -08:00
this . logger . error (
2023-12-11 14:11:42 -08:00
` [ ${ chain } ][ ${ method } ] Got error ${ e } from ${ chainCfg . rpc . hostname } / ${ apiMethod } `
2023-12-04 04:47:02 -08:00
) ;
2023-11-28 11:00:45 -08:00
}
}
2023-12-04 04:47:02 -08:00
2023-12-11 14:11:42 -08:00
protected getCurrentChain ( chain : string ) {
2023-12-04 04:47:02 -08:00
const cfg = this . cfg . chains [ chain ] ;
return {
chainId : cfg.chainId ,
rpc : new URL ( cfg . rpcs [ 0 ] ) ,
2023-12-06 08:07:14 -08:00
timeout : cfg.timeout ? ? 10 _000 ,
retries : cfg.retries ? ? 2 ,
2023-12-04 04:47:02 -08:00
} ;
}
2023-11-28 11:00:45 -08:00
}
export type EvmJsonRPCBlockRepositoryCfg = {
2023-12-04 04:47:02 -08:00
chains : Record < string , ChainRPCConfig > ;
2023-11-28 11:00:45 -08:00
} ;
type ErrorBlock = {
code : number ; //6969,
message : string ; //'Error: No response received from RPC endpoint in 60s'
} ;
type Log = {
blockNumber : string ;
blockHash : string ;
transactionIndex : number ;
removed : boolean ;
address : string ;
data : string ;
topics : Array < string > ;
transactionHash : string ;
logIndex : number ;
} ;