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 {};
|
|
|
|
|
2024-01-26 04:04:04 -08:00
|
|
|
let combinedResults: ResultBlocks[] = [];
|
2023-12-04 04:47:02 -08:00
|
|
|
const chainCfg = this.getCurrentChain(chain);
|
2024-01-26 04:04:04 -08:00
|
|
|
const batches = this.divideIntoBatches(blockNumbers, 9);
|
|
|
|
|
|
|
|
for (const batch of batches) {
|
|
|
|
const reqs: any[] = [];
|
|
|
|
for (let blockNumber of batch) {
|
|
|
|
const blockNumberStrParam = `${HEXADECIMAL_PREFIX}${blockNumber.toString(16)}`;
|
|
|
|
const blockNumberStrId = blockNumber.toString();
|
|
|
|
|
|
|
|
reqs.push({
|
|
|
|
jsonrpc: "2.0",
|
|
|
|
id: blockNumberStrId,
|
|
|
|
method: "eth_getBlockByNumber",
|
|
|
|
params: [blockNumberStrParam, false],
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
let results: (undefined | ResultBlocks)[] = [];
|
|
|
|
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, "getBlocks", "eth_getBlockByNumber");
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let result of results) {
|
|
|
|
if (result) {
|
|
|
|
combinedResults.push(result);
|
|
|
|
}
|
|
|
|
}
|
2023-11-28 11:00:45 -08:00
|
|
|
}
|
|
|
|
|
2024-01-26 04:04:04 -08:00
|
|
|
if (combinedResults && combinedResults.length) {
|
|
|
|
return combinedResults
|
2023-11-28 11:00:45 -08:00
|
|
|
.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
|
2024-01-26 04:04:04 -08:00
|
|
|
} for eth_getBlockByNumber for ${response?.id ?? idx} 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 ${
|
2024-01-26 04:04:04 -08:00
|
|
|
response?.id ?? idx
|
2023-11-28 11:00:45 -08:00
|
|
|
}: ${msg}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
)
|
|
|
|
.reduce((acc: Record<string, EvmBlock>, block: EvmBlock) => {
|
|
|
|
acc[block.hash] = block;
|
|
|
|
return acc;
|
|
|
|
}, {});
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new Error(
|
|
|
|
`Unable to parse ${
|
2024-01-26 04:04:04 -08:00
|
|
|
combinedResults?.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(
|
2024-01-16 07:47:10 -08:00
|
|
|
`Unable to parse result of eth_getBlockByNumber for ${blockNumberOrTag} on ${
|
|
|
|
chainCfg.rpc
|
|
|
|
}. Response error: ${JSON.stringify(response)}`
|
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);
|
2024-01-24 07:29:23 -08:00
|
|
|
let results: ResultTransactionReceipt[] = [];
|
2024-01-16 07:47:10 -08:00
|
|
|
let id = 1;
|
|
|
|
|
2024-01-24 07:29:23 -08:00
|
|
|
const batches = this.divideIntoBatches(hashNumbers);
|
|
|
|
let combinedResults: ResultTransactionReceipt[] = [];
|
2023-12-22 12:50:38 -08:00
|
|
|
|
2024-01-24 07:29:23 -08:00
|
|
|
for (const batch of batches) {
|
|
|
|
const reqs: any[] = [];
|
|
|
|
for (let hash of batch) {
|
|
|
|
reqs.push({
|
|
|
|
jsonrpc: "2.0",
|
|
|
|
id,
|
|
|
|
method: "eth_getTransactionReceipt",
|
|
|
|
params: [hash],
|
|
|
|
});
|
|
|
|
id++;
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let result of results) {
|
|
|
|
if (result) {
|
|
|
|
combinedResults.push(result);
|
|
|
|
}
|
|
|
|
}
|
2023-12-22 12:50:38 -08:00
|
|
|
}
|
|
|
|
|
2024-01-24 07:29:23 -08:00
|
|
|
if (combinedResults && combinedResults.length) {
|
|
|
|
return combinedResults
|
2023-12-22 12:50:38 -08:00
|
|
|
.map((response) => {
|
|
|
|
if (response.result?.status && response.result?.transactionHash) {
|
|
|
|
return {
|
|
|
|
status: response.result.status,
|
|
|
|
transactionHash: response.result.transactionHash,
|
2024-01-16 07:47:10 -08:00
|
|
|
logs: response.result.logs,
|
2023-12-22 12:50:38 -08:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-01-24 07:29:23 -08:00
|
|
|
const msg = `[${chain}][getTransactionReceipt] Got error ${
|
|
|
|
response?.error
|
|
|
|
} for eth_getTransactionReceipt for ${JSON.stringify(hashNumbers)} on ${
|
|
|
|
chainCfg.rpc.hostname
|
|
|
|
}`;
|
2023-12-22 12:50:38 -08:00
|
|
|
|
|
|
|
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(
|
2024-01-24 07:29:23 -08:00
|
|
|
`Unable to parse result of eth_getTransactionReceipt for ${JSON.stringify(hashNumbers)} on ${
|
2024-01-16 07:47:10 -08:00
|
|
|
chainCfg.rpc
|
2024-01-24 07:29:23 -08:00
|
|
|
}. Result error: ${JSON.stringify(combinedResults)}`
|
2023-12-22 12:50:38 -08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-01-24 07:29:23 -08:00
|
|
|
/**
|
|
|
|
* This method divide in batches the object to send, because we have one restriction about how many object send to the endpoint
|
|
|
|
* the maximum is 10 object per request
|
|
|
|
*/
|
2024-01-26 04:04:04 -08:00
|
|
|
private divideIntoBatches(set: Set<string | bigint>, batchSize = 10) {
|
2024-01-24 07:29:23 -08:00
|
|
|
const batches = [];
|
|
|
|
let batch: any[] = [];
|
|
|
|
|
|
|
|
set.forEach((item) => {
|
|
|
|
batch.push(item);
|
|
|
|
if (batch.length === batchSize) {
|
|
|
|
batches.push(new Set(batch));
|
|
|
|
batch = [];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (batch.length > 0) {
|
|
|
|
batches.push(new Set(batch));
|
|
|
|
}
|
|
|
|
return batches;
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
};
|
2024-01-24 07:29:23 -08:00
|
|
|
|
|
|
|
type ResultTransactionReceipt = {
|
|
|
|
result: ReceiptTransaction;
|
|
|
|
error?: ErrorBlock;
|
|
|
|
};
|
2024-01-26 04:04:04 -08:00
|
|
|
|
|
|
|
type ResultBlocks = {
|
|
|
|
id: string;
|
|
|
|
result?: EvmBlock;
|
|
|
|
error?: ErrorBlock;
|
|
|
|
};
|