2023-08-08 13:18:55 +00:00
|
|
|
//
|
|
|
|
// Copyright 2021 Vulcanize, Inc.
|
|
|
|
//
|
|
|
|
|
|
|
|
import assert from 'assert';
|
|
|
|
import { errors, providers, utils } from 'ethers';
|
|
|
|
|
|
|
|
import { Cache } from '@cerc-io/cache';
|
2024-05-15 13:41:22 +00:00
|
|
|
import {
|
|
|
|
encodeHeader, escapeHexString,
|
|
|
|
EthClient as EthClientInterface, EthFullBlock, EthFullTransaction,
|
|
|
|
MonitoredStaticJsonRpcProvider, FUTURE_BLOCK_ERROR, NULL_BLOCK_ERROR
|
|
|
|
} from '@cerc-io/util';
|
2023-08-08 13:18:55 +00:00
|
|
|
import { padKey } from '@cerc-io/ipld-eth-client';
|
|
|
|
|
|
|
|
export interface Config {
|
|
|
|
cache: Cache | undefined;
|
|
|
|
rpcEndpoint: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface Vars {
|
|
|
|
blockHash?: string;
|
|
|
|
blockNumber?: string;
|
|
|
|
contract?: string;
|
|
|
|
slot?: string;
|
|
|
|
addresses?: string[];
|
2023-10-26 11:55:56 +00:00
|
|
|
topics?: string[][];
|
2023-10-23 10:40:09 +00:00
|
|
|
fromBlock?: number;
|
|
|
|
toBlock?: number;
|
2023-08-08 13:18:55 +00:00
|
|
|
}
|
|
|
|
|
2023-10-23 03:53:20 +00:00
|
|
|
export class EthClient implements EthClientInterface {
|
2023-08-08 13:18:55 +00:00
|
|
|
_provider: providers.JsonRpcProvider;
|
|
|
|
_cache: Cache | undefined;
|
|
|
|
|
|
|
|
constructor (config: Config) {
|
|
|
|
const { rpcEndpoint, cache } = config;
|
|
|
|
assert(rpcEndpoint, 'Missing RPC endpoint');
|
2024-05-15 13:41:22 +00:00
|
|
|
this._provider = new MonitoredStaticJsonRpcProvider({
|
2023-11-09 13:12:37 +00:00
|
|
|
url: rpcEndpoint,
|
|
|
|
allowGzip: true
|
|
|
|
});
|
2023-08-08 13:18:55 +00:00
|
|
|
|
|
|
|
this._cache = cache;
|
|
|
|
}
|
|
|
|
|
|
|
|
async getStorageAt ({ blockHash, contract, slot }: { blockHash: string, contract: string, slot: string }): Promise<{ value: string, proof: { data: string } }> {
|
|
|
|
slot = `0x${padKey(slot)}`;
|
|
|
|
|
|
|
|
console.time(`time:eth-client#getStorageAt-${JSON.stringify({ blockHash, contract, slot })}`);
|
|
|
|
const value = await this._getCachedOrFetch(
|
|
|
|
'getStorageAt',
|
|
|
|
{ blockHash, contract, slot },
|
|
|
|
async () => {
|
|
|
|
// TODO: Check if blockHash works with Lotus RPC
|
|
|
|
return this._provider.getStorageAt(contract, slot, blockHash);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
console.timeEnd(`time:eth-client#getStorageAt-${JSON.stringify({ blockHash, contract, slot })}`);
|
|
|
|
|
|
|
|
return {
|
|
|
|
value,
|
|
|
|
proof: {
|
|
|
|
// TODO: Return proof with cid and ipldBlock
|
|
|
|
// To match getStorageAt method of ipld-eth-client which returns proof along with value.
|
|
|
|
data: JSON.stringify(null)
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
async getBlockWithTransactions ({ blockNumber, blockHash }: { blockNumber?: number, blockHash?: string }): Promise<any> {
|
|
|
|
const blockHashOrBlockNumber = blockHash ?? blockNumber;
|
|
|
|
assert(blockHashOrBlockNumber);
|
|
|
|
console.time(`time:eth-client#getBlockWithTransactions-${JSON.stringify({ blockNumber, blockHash })}`);
|
2023-11-13 06:52:23 +00:00
|
|
|
let nodes: any[] = [];
|
2023-08-08 13:18:55 +00:00
|
|
|
|
2023-11-13 06:52:23 +00:00
|
|
|
try {
|
|
|
|
const result = await this._provider.getBlockWithTransactions(blockHashOrBlockNumber);
|
|
|
|
|
|
|
|
nodes = [
|
2023-08-08 13:18:55 +00:00
|
|
|
{
|
|
|
|
blockNumber: result.number.toString(),
|
|
|
|
blockHash: result.hash,
|
|
|
|
parentHash: result.parentHash,
|
|
|
|
timestamp: result.timestamp.toString(),
|
|
|
|
ethTransactionCidsByHeaderId: {
|
|
|
|
nodes: result.transactions.map((transaction) => ({
|
|
|
|
txHash: transaction.hash,
|
|
|
|
// Transactions with block should be of type TransactionReceipt
|
2023-11-06 06:04:48 +00:00
|
|
|
index: (transaction as unknown as providers.TransactionReceipt).transactionIndex,
|
2023-08-08 13:18:55 +00:00
|
|
|
src: transaction.from,
|
|
|
|
dst: transaction.to
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
}
|
2023-11-13 06:52:23 +00:00
|
|
|
];
|
|
|
|
} catch (err: any) {
|
2023-11-17 06:28:43 +00:00
|
|
|
return this._handleGetBlockErrors(err);
|
2023-11-13 06:52:23 +00:00
|
|
|
} finally {
|
|
|
|
console.timeEnd(`time:eth-client#getBlockWithTransactions-${JSON.stringify({ blockNumber, blockHash })}`);
|
|
|
|
}
|
2023-08-08 13:18:55 +00:00
|
|
|
|
2023-11-13 06:52:23 +00:00
|
|
|
return {
|
|
|
|
allEthHeaderCids: {
|
|
|
|
nodes
|
|
|
|
}
|
|
|
|
};
|
2023-08-08 13:18:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async getBlocks ({ blockNumber, blockHash }: { blockNumber?: number, blockHash?: string }): Promise<any> {
|
2023-08-09 12:01:11 +00:00
|
|
|
const blockNumberHex = blockNumber ? utils.hexValue(blockNumber) : undefined;
|
|
|
|
const blockHashOrBlockNumber = blockHash ?? blockNumberHex;
|
2023-08-08 13:18:55 +00:00
|
|
|
assert(blockHashOrBlockNumber);
|
|
|
|
let nodes: any[] = [];
|
|
|
|
console.time(`time:eth-client#getBlocks-${JSON.stringify({ blockNumber, blockHash })}`);
|
|
|
|
|
|
|
|
try {
|
|
|
|
const rawBlock = await this._provider.send(
|
|
|
|
blockHash ? 'eth_getBlockByHash' : 'eth_getBlockByNumber',
|
2023-08-09 12:01:11 +00:00
|
|
|
[blockHashOrBlockNumber, false]
|
2023-08-08 13:18:55 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
if (rawBlock) {
|
|
|
|
const block = this._provider.formatter.block(rawBlock);
|
|
|
|
|
|
|
|
nodes = [
|
|
|
|
{
|
|
|
|
blockNumber: block.number.toString(),
|
|
|
|
blockHash: block.hash,
|
|
|
|
parentHash: block.parentHash,
|
|
|
|
timestamp: block.timestamp.toString(),
|
|
|
|
stateRoot: this._provider.formatter.hash(rawBlock.stateRoot),
|
|
|
|
td: this._provider.formatter.bigNumber(rawBlock.totalDifficulty).toString(),
|
|
|
|
txRoot: this._provider.formatter.hash(rawBlock.transactionsRoot),
|
|
|
|
receiptRoot: this._provider.formatter.hash(rawBlock.receiptsRoot)
|
|
|
|
}
|
|
|
|
];
|
|
|
|
}
|
|
|
|
} catch (err: any) {
|
2023-11-17 06:28:43 +00:00
|
|
|
return this._handleGetBlockErrors(err);
|
2023-08-08 13:18:55 +00:00
|
|
|
} finally {
|
|
|
|
console.timeEnd(`time:eth-client#getBlocks-${JSON.stringify({ blockNumber, blockHash })}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
allEthHeaderCids: {
|
|
|
|
nodes
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-11-17 06:28:43 +00:00
|
|
|
async getFullBlocks ({ blockNumber, blockHash }: { blockNumber?: number, blockHash?: string }): Promise<Array<EthFullBlock | null>> {
|
2023-08-09 12:01:11 +00:00
|
|
|
const blockNumberHex = blockNumber ? utils.hexValue(blockNumber) : undefined;
|
|
|
|
const blockHashOrBlockNumber = blockHash ?? blockNumberHex;
|
2023-08-08 13:18:55 +00:00
|
|
|
assert(blockHashOrBlockNumber);
|
|
|
|
console.time(`time:eth-client#getFullBlocks-${JSON.stringify({ blockNumber, blockHash })}`);
|
|
|
|
|
2023-11-13 06:52:23 +00:00
|
|
|
try {
|
|
|
|
const rawBlock = await this._provider.send(
|
|
|
|
blockHash ? 'eth_getBlockByHash' : 'eth_getBlockByNumber',
|
|
|
|
[blockHashOrBlockNumber, false]
|
|
|
|
);
|
|
|
|
|
|
|
|
if (rawBlock) {
|
|
|
|
// Create block header
|
|
|
|
// https://github.com/cerc-io/go-ethereum/blob/v1.11.6-statediff-5.0.8/core/types/block.go#L64
|
|
|
|
const header = {
|
|
|
|
Parent: rawBlock.parentHash,
|
|
|
|
UnclesDigest: rawBlock.sha3Uncles,
|
|
|
|
Beneficiary: rawBlock.miner,
|
|
|
|
StateRoot: rawBlock.stateRoot,
|
|
|
|
TxRoot: rawBlock.transactionsRoot,
|
|
|
|
RctRoot: rawBlock.receiptsRoot,
|
|
|
|
Bloom: rawBlock.logsBloom,
|
|
|
|
Difficulty: BigInt(rawBlock.difficulty),
|
|
|
|
Number: BigInt(rawBlock.number),
|
|
|
|
GasLimit: BigInt(rawBlock.gasLimit),
|
|
|
|
GasUsed: BigInt(rawBlock.gasUsed),
|
|
|
|
Time: Number(rawBlock.timestamp),
|
|
|
|
Extra: rawBlock.extraData,
|
|
|
|
MixDigest: rawBlock.mixHash,
|
|
|
|
Nonce: BigInt(rawBlock.nonce),
|
|
|
|
BaseFee: rawBlock.baseFeePerGas && BigInt(rawBlock.baseFeePerGas)
|
|
|
|
};
|
|
|
|
|
|
|
|
const rlpData = encodeHeader(header);
|
|
|
|
|
|
|
|
return [{
|
|
|
|
blockNumber: this._provider.formatter.number(rawBlock.number).toString(),
|
|
|
|
blockHash: this._provider.formatter.hash(rawBlock.hash),
|
|
|
|
parentHash: this._provider.formatter.hash(rawBlock.parentHash),
|
|
|
|
timestamp: this._provider.formatter.number(rawBlock.timestamp).toString(),
|
|
|
|
stateRoot: this._provider.formatter.hash(rawBlock.stateRoot),
|
|
|
|
td: this._provider.formatter.bigNumber(rawBlock.totalDifficulty).toString(),
|
|
|
|
txRoot: this._provider.formatter.hash(rawBlock.transactionsRoot),
|
|
|
|
receiptRoot: this._provider.formatter.hash(rawBlock.receiptsRoot),
|
|
|
|
uncleRoot: this._provider.formatter.hash(rawBlock.sha3Uncles),
|
|
|
|
bloom: escapeHexString(this._provider.formatter.hex(rawBlock.logsBloom)),
|
|
|
|
size: this._provider.formatter.number(rawBlock.size).toString(),
|
|
|
|
blockByMhKey: {
|
|
|
|
data: escapeHexString(rlpData)
|
|
|
|
}
|
|
|
|
}];
|
|
|
|
}
|
|
|
|
} catch (err: any) {
|
2023-11-17 06:28:43 +00:00
|
|
|
return this._handleGetBlockErrors(err);
|
2023-11-13 06:52:23 +00:00
|
|
|
} finally {
|
|
|
|
console.timeEnd(`time:eth-client#getFullBlocks-${JSON.stringify({ blockNumber, blockHash })}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
return [];
|
2023-08-08 13:18:55 +00:00
|
|
|
}
|
|
|
|
|
2023-11-09 13:12:37 +00:00
|
|
|
async getFullTransaction (txHash: string): Promise<EthFullTransaction> {
|
2023-08-08 13:18:55 +00:00
|
|
|
console.time(`time:eth-client#getFullTransaction-${JSON.stringify({ txHash })}`);
|
|
|
|
const tx = await this._provider.getTransaction(txHash);
|
|
|
|
console.timeEnd(`time:eth-client#getFullTransaction-${JSON.stringify({ txHash })}`);
|
|
|
|
|
|
|
|
return {
|
|
|
|
ethTransactionCidByTxHash: {
|
|
|
|
txHash: tx.hash,
|
2023-11-09 13:12:37 +00:00
|
|
|
index: (tx as any).transactionIndex,
|
2023-08-08 13:18:55 +00:00
|
|
|
src: tx.from,
|
2023-11-08 12:02:26 +00:00
|
|
|
dst: tx.to
|
|
|
|
},
|
|
|
|
data: tx
|
2023-08-08 13:18:55 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
async getBlockByHash (blockHash?: string): Promise<any> {
|
|
|
|
const blockTag: providers.BlockTag = blockHash ?? 'latest';
|
|
|
|
|
|
|
|
console.time(`time:eth-client#getBlockByHash-${blockHash}`);
|
|
|
|
const block = await this._provider.getBlock(blockTag);
|
|
|
|
console.timeEnd(`time:eth-client#getBlockByHash-${blockHash}`);
|
|
|
|
|
|
|
|
return {
|
|
|
|
block: {
|
|
|
|
number: block.number,
|
|
|
|
hash: block.hash,
|
|
|
|
parent: {
|
|
|
|
hash: block.parentHash
|
|
|
|
},
|
|
|
|
timestamp: block.timestamp
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-10-23 10:40:09 +00:00
|
|
|
async getLogs (vars: {
|
|
|
|
blockHash: string,
|
|
|
|
blockNumber: string,
|
2023-10-26 11:55:56 +00:00
|
|
|
addresses?: string[],
|
|
|
|
topics?: string[][]
|
2023-10-23 10:40:09 +00:00
|
|
|
}): Promise<any> {
|
2023-08-08 13:18:55 +00:00
|
|
|
console.time(`time:eth-client#getLogs-${JSON.stringify(vars)}`);
|
2023-10-26 11:55:56 +00:00
|
|
|
const result = await this._getLogs({
|
2023-11-06 06:04:48 +00:00
|
|
|
blockHash: vars.blockHash,
|
2023-10-26 11:55:56 +00:00
|
|
|
addresses: vars.addresses,
|
|
|
|
topics: vars.topics
|
|
|
|
});
|
2023-10-23 10:40:09 +00:00
|
|
|
console.timeEnd(`time:eth-client#getLogs-${JSON.stringify(vars)}`);
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
async getLogsForBlockRange (vars: {
|
|
|
|
fromBlock?: number,
|
|
|
|
toBlock?: number,
|
2023-10-26 11:55:56 +00:00
|
|
|
addresses?: string[],
|
|
|
|
topics?: string[][]
|
2023-10-23 10:40:09 +00:00
|
|
|
}): Promise<any> {
|
|
|
|
console.time(`time:eth-client#getLogsForBlockRange-${JSON.stringify(vars)}`);
|
2023-10-26 11:55:56 +00:00
|
|
|
const result = await this._getLogs({
|
|
|
|
fromBlock: Number(vars.fromBlock),
|
|
|
|
toBlock: Number(vars.toBlock),
|
|
|
|
addresses: vars.addresses,
|
|
|
|
topics: vars.topics
|
|
|
|
});
|
2023-10-23 10:40:09 +00:00
|
|
|
console.timeEnd(`time:eth-client#getLogsForBlockRange-${JSON.stringify(vars)}`);
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Implement return type
|
|
|
|
async _getLogs (vars: {
|
2023-11-06 06:04:48 +00:00
|
|
|
blockHash?: string,
|
2023-10-23 10:40:09 +00:00
|
|
|
fromBlock?: number,
|
|
|
|
toBlock?: number,
|
2023-10-26 11:55:56 +00:00
|
|
|
addresses?: string[],
|
|
|
|
topics?: string[][]
|
2023-10-23 10:40:09 +00:00
|
|
|
}): Promise<any> {
|
2023-11-06 06:04:48 +00:00
|
|
|
const { blockHash, fromBlock, toBlock, addresses = [], topics } = vars;
|
2023-10-23 10:40:09 +00:00
|
|
|
|
2023-08-08 13:18:55 +00:00
|
|
|
const result = await this._getCachedOrFetch(
|
|
|
|
'getLogs',
|
|
|
|
vars,
|
|
|
|
async () => {
|
2024-05-03 08:54:05 +00:00
|
|
|
const ethLogs = await this._provider.send(
|
|
|
|
'eth_getLogs',
|
|
|
|
[{
|
|
|
|
address: addresses.map(address => address.toLowerCase()),
|
|
|
|
fromBlock: fromBlock && utils.hexValue(fromBlock),
|
|
|
|
toBlock: toBlock && utils.hexValue(toBlock),
|
|
|
|
blockHash,
|
|
|
|
topics
|
|
|
|
}]
|
|
|
|
);
|
2023-11-06 06:04:48 +00:00
|
|
|
|
|
|
|
// Format raw eth_getLogs response
|
|
|
|
const logs: providers.Log[] = providers.Formatter.arrayOf(
|
|
|
|
this._provider.formatter.filterLog.bind(this._provider.formatter)
|
|
|
|
)(ethLogs);
|
2023-08-08 13:18:55 +00:00
|
|
|
|
2023-11-06 06:04:48 +00:00
|
|
|
return logs.map((log) => {
|
2023-08-08 13:18:55 +00:00
|
|
|
log.address = log.address.toLowerCase();
|
|
|
|
return log;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
const txHashesSet = result.reduce((acc, log) => {
|
|
|
|
acc.add(log.transactionHash);
|
|
|
|
return acc;
|
|
|
|
}, new Set<string>());
|
|
|
|
|
|
|
|
const txReceipts = await Promise.all(Array.from(txHashesSet).map(txHash => this._provider.getTransactionReceipt(txHash)));
|
|
|
|
|
|
|
|
const txReceiptMap = txReceipts.reduce((acc, txReceipt) => {
|
|
|
|
acc.set(txReceipt.transactionHash, txReceipt);
|
|
|
|
return acc;
|
|
|
|
}, new Map<string, providers.TransactionReceipt>());
|
|
|
|
|
|
|
|
return {
|
|
|
|
logs: result.map((log) => ({
|
2023-10-23 10:40:09 +00:00
|
|
|
// blockHash required for sorting logs fetched in a block range
|
|
|
|
blockHash: log.blockHash,
|
2023-08-08 13:18:55 +00:00
|
|
|
account: {
|
|
|
|
address: log.address
|
|
|
|
},
|
|
|
|
transaction: {
|
|
|
|
hash: log.transactionHash
|
|
|
|
},
|
|
|
|
topics: log.topics,
|
|
|
|
data: log.data,
|
|
|
|
index: log.logIndex,
|
|
|
|
status: txReceiptMap.get(log.transactionHash)?.status
|
|
|
|
}))
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
async _getCachedOrFetch<Result> (queryName: string, vars: Vars, fetch: () => Promise<Result>): Promise<Result> {
|
|
|
|
const keyObj = {
|
|
|
|
queryName,
|
|
|
|
vars
|
|
|
|
};
|
|
|
|
|
|
|
|
// Check if request cached in db, if cache is enabled.
|
|
|
|
if (this._cache) {
|
|
|
|
const [value, found] = await this._cache.get(keyObj) || [undefined, false];
|
|
|
|
if (found) {
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Result not cached or cache disabled, need to perform fetch.
|
|
|
|
const result = await fetch();
|
|
|
|
|
|
|
|
// Cache the result and return it, if cache is enabled.
|
|
|
|
if (this._cache) {
|
|
|
|
await this._cache.put(keyObj, result);
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
2023-11-17 06:28:43 +00:00
|
|
|
|
|
|
|
_handleGetBlockErrors (err: any): Array<null> {
|
|
|
|
if (err.code === errors.SERVER_ERROR && err.error) {
|
|
|
|
// Check null block error and return null array
|
|
|
|
if (err.error.message === NULL_BLOCK_ERROR) {
|
|
|
|
return [null];
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check and ignore future block error
|
|
|
|
if (err.error.message === FUTURE_BLOCK_ERROR) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
throw err;
|
|
|
|
}
|
2023-08-08 13:18:55 +00:00
|
|
|
}
|