bitcore-node
Version:
A blockchain indexing node with extended capabilities using bitcore
897 lines (813 loc) • 33.2 kB
text/typescript
import { CryptoRpc } from 'crypto-rpc';
import { ObjectID } from 'mongodb';
import Web3 from 'web3';
import { Transaction } from 'web3-eth';
import { AbiItem } from 'web3-utils';
import Config from '../../../../config';
import {
historical,
internal,
realtime
} from '../../../../decorators/decorators';
import logger from '../../../../logger';
import { MongoBound } from '../../../../models/base';
import { ITransaction } from '../../../../models/baseTransaction';
import { CacheStorage } from '../../../../models/cache';
import { WalletAddressStorage } from '../../../../models/walletAddress';
import { InternalStateProvider } from '../../../../providers/chain-state/internal/internal';
import { Storage } from '../../../../services/storage';
import { IBlock } from '../../../../types/Block';
import { ChainId } from '../../../../types/ChainNetwork';
import { SpentHeightIndicators } from '../../../../types/Coin';
import { IChainConfig, IEVMNetworkConfig, IProvider } from '../../../../types/Config';
import {
BroadcastTransactionParams,
GetBalanceForAddressParams,
GetBlockParams,
GetWalletBalanceParams,
IChainStateService,
StreamAddressUtxosParams,
StreamTransactionParams,
StreamTransactionsParams,
StreamWalletTransactionsArgs,
StreamWalletTransactionsParams,
UpdateWalletParams,
WalletBalanceType
} from '../../../../types/namespaces/ChainStateProvider';
import { partition, range } from '../../../../utils';
import { StatsUtil } from '../../../../utils/stats';
import { TransformWithEventPipe } from '../../../../utils/streamWithEventPipe';
import { ExternalApiStream } from '../../external/streams/apiStream';
import { ERC20Abi } from '../abi/erc20';
import { MultisendAbi } from '../abi/multisend';
import { EVMBlockStorage } from '../models/block';
import { EVMTransactionStorage } from '../models/transaction';
import { ERC20Transfer, EVMTransactionJSON, IEVMBlock, IEVMTransaction, IEVMTransactionInProcess } from '../types';
import { Erc20RelatedFilterTransform } from './erc20Transform';
import { InternalTxRelatedFilterTransform } from './internalTxTransform';
import { PopulateEffectsTransform } from './populateEffectsTransform';
import { PopulateReceiptTransform } from './populateReceiptTransform';
import {
getProvider,
isValidProviderType
} from './provider';
import { EVMListTransactionsStream } from './transform';
export interface GetWeb3Response { rpc: CryptoRpc; web3: Web3; dataType: string; lastPingTime?: number; };
export interface BuildWalletTxsStreamParams {
transactionStream: TransformWithEventPipe;
populateEffects: PopulateEffectsTransform;
walletAddresses: string[];
}
export class BaseEVMStateProvider extends InternalStateProvider implements IChainStateService {
config: IChainConfig<IEVMNetworkConfig>;
static rpcs = {} as { [chain: string]: { [network: string]: GetWeb3Response[] } };
constructor(public chain: string = 'ETH') {
super(chain);
this.config = Config.chains[this.chain] as IChainConfig<IEVMNetworkConfig>;
}
async getWeb3(network: string, params?: { type: IProvider['dataType'] }): Promise<GetWeb3Response> {
for (const rpc of BaseEVMStateProvider.rpcs[this.chain]?.[network] || []) {
if (!isValidProviderType(params?.type, rpc.dataType)) {
continue;
}
try {
if (Date.now() - (rpc.lastPingTime || 0) < 10000) { // Keep the rpc from being blasted with ping calls
return rpc;
}
await Promise.race([
rpc.web3.eth.getBlockNumber(),
new Promise((_, reject) => setTimeout(reject, 5000))
]);
rpc.lastPingTime = Date.now();
return rpc; // return the first applicable rpc that's responsive
} catch (e) {
// try reconnecting
logger.info(`Reconnecting to ${this.chain}:${network}`);
if (typeof (rpc.web3.currentProvider as any)?.disconnect === 'function') {
(rpc.web3.currentProvider as any)?.disconnect?.();
(rpc.web3.currentProvider as any)?.connect?.();
if ((rpc.web3.currentProvider as any)?.connected) {
return rpc;
}
}
const idx = BaseEVMStateProvider.rpcs[this.chain][network].indexOf(rpc);
BaseEVMStateProvider.rpcs[this.chain][network].splice(idx, 1);
}
}
logger.info(`Making a new connection for ${this.chain}:${network}`);
const dataType = params?.type;
const providerConfig = getProvider({ network, dataType, config: this.config });
// Default to using ETH CryptoRpc with all EVM chain configs
const rpcConfig = { ...providerConfig, chain: 'ETH', currencyConfig: {} };
const rpc = new CryptoRpc(rpcConfig, {}).get('ETH');
const rpcObj = { rpc, web3: rpc.web3, dataType: rpcConfig.dataType || 'combined' };
if (!BaseEVMStateProvider.rpcs[this.chain]) {
BaseEVMStateProvider.rpcs[this.chain] = {};
}
if (!BaseEVMStateProvider.rpcs[this.chain][network]) {
BaseEVMStateProvider.rpcs[this.chain][network] = [];
}
BaseEVMStateProvider.rpcs[this.chain][network].push(rpcObj);
return rpcObj;
}
async erc20For(network: string, address: string) {
const { web3 } = await this.getWeb3(network);
const contract = new web3.eth.Contract(ERC20Abi as AbiItem[], address);
return contract;
}
async getMultisendContract(network: string, address: string) {
const { web3 } = await this.getWeb3(network);
const contract = new web3.eth.Contract(MultisendAbi as AbiItem[], address);
return contract;
}
async getERC20TokenInfo(network: string, tokenAddress: string) {
const token = await this.erc20For(network, tokenAddress);
const [name, decimals, symbol] = await Promise.all([
token.methods.name().call(),
token.methods.decimals().call(),
token.methods.symbol().call()
]);
return {
name,
decimals,
symbol
};
}
async getERC20TokenAllowance(network: string, tokenAddress: string, ownerAddress: string, spenderAddress: string) {
const token = await this.erc20For(network, tokenAddress);
return await token.methods.allowance(ownerAddress, spenderAddress).call();
}
async getFee(params) {
let { network, target = 4, txType } = params;
const chain = this.chain;
if (network === 'livenet') {
network = 'mainnet';
}
let cacheKey = `getFee-${chain}-${network}-${target}`;
if (txType) {
cacheKey += `-type${txType}`;
}
return CacheStorage.getGlobalOrRefresh(
cacheKey,
async () => {
let feerate;
if (txType?.toString() === '2') {
const { rpc } = await this.getWeb3(network, { type: 'historical' });
feerate = await rpc.estimateFee({ nBlocks: target, txType });
} else {
const txs = await EVMTransactionStorage.collection
.find({ chain, network, blockHeight: { $gt: 0 } })
.project({ gasPrice: 1, blockHeight: 1 })
.sort({ blockHeight: -1 })
.limit(20 * 200)
.toArray();
const blockGasPrices = txs
.map(tx => Number(tx.gasPrice))
.filter(gasPrice => gasPrice)
.sort((a, b) => b - a);
const whichQuartile = Math.min(target, 4) || 1;
const quartileMedian = StatsUtil.getNthQuartileMedian(blockGasPrices, whichQuartile);
const roundedGwei = (quartileMedian / 1e9).toFixed(2);
const gwei = Number(roundedGwei) || 0;
feerate = gwei * 1e9;
}
return { feerate, blocks: target };
},
CacheStorage.Times.Minute
);
}
async getPriorityFee(params) {
let { network, percentile } = params;
const chain = this.chain;
const priorityFeePercentile = percentile || 15;
if (network === 'livenet') {
network = 'mainnet';
}
let cacheKey = `getFee-${chain}-${network}-priorityFee-${priorityFeePercentile}`;
return CacheStorage.getGlobalOrRefresh(
cacheKey,
async () => {
const { rpc } = await this.getWeb3(network);
let feerate = await rpc.estimateMaxPriorityFee({ percentile: priorityFeePercentile });
return { feerate };
},
CacheStorage.Times.Minute
);
}
async getBalanceForAddress(params: GetBalanceForAddressParams): Promise<WalletBalanceType> {
const { chain, network, address, args } = params;
const { web3 } = await this.getWeb3(network, { type: 'realtime' });
const tokenAddress = args?.tokenAddress;
const addressLower = address.toLowerCase();
const hex = args?.hex === 'true' || args?.hex === '1';
const cacheKey = tokenAddress
? `getBalanceForAddress-${chain}-${network}-${addressLower}-${tokenAddress.toLowerCase()}`
: `getBalanceForAddress-${chain}-${network}-${addressLower}`;
const balance = await CacheStorage.getGlobalOrRefresh(
cacheKey,
async () => {
if (tokenAddress) {
const token = await this.erc20For(network, tokenAddress);
const balance = await token.methods.balanceOf(address).call();
const numberBalance = '0x' + BigInt(balance).toString(16);
return { confirmed: numberBalance, unconfirmed: '0x0', balance: numberBalance };
} else {
const balance = await web3.eth.getBalance(address);
const numberBalance = '0x' + BigInt(balance).toString(16);
return { confirmed: numberBalance, unconfirmed: '0x0', balance: numberBalance };
}
},
CacheStorage.Times.Minute
);
return {
confirmed: hex ? balance.confirmed : Number(balance.confirmed),
unconfirmed: hex ? balance.unconfirmed : Number(balance.unconfirmed),
balance: hex ? balance.balance : Number(balance.balance)
};
}
async getLocalTip({ chain, network }): Promise<IBlock> {
return EVMBlockStorage.getLocalTip({ chain, network });
}
async getReceipt(network: string, txid: string) {
const { web3 } = await this.getWeb3(network, { type: 'historical' });
return web3.eth.getTransactionReceipt(txid);
}
async populateReceipt(tx: MongoBound<IEVMTransaction>) {
if (!tx.receipt) {
const receipt = await this.getReceipt(tx.network, tx.txid);
if (receipt) {
const fee = receipt.gasUsed * tx.gasPrice;
await EVMTransactionStorage.collection.updateOne({ _id: tx._id }, { $set: { receipt, fee } });
tx.receipt = receipt;
tx.fee = fee;
}
}
return tx;
}
populateEffects(tx: MongoBound<IEVMTransaction>) {
if (!tx.effects || (tx.effects && tx.effects.length == 0)) {
tx.effects = EVMTransactionStorage.getEffects(tx as IEVMTransactionInProcess);
}
return tx;
}
async getTransaction(params: StreamTransactionParams) {
try {
params.network = params.network.toLowerCase();
let { chain, network, txId } = params;
if (typeof txId !== 'string') {
throw new Error('Missing required param: txId');
}
if (!chain) {
throw new Error('Missing required param: chain');
}
if (!network) {
throw new Error('Missing required param: network');
}
let { tipHeight, found } = await this._getTransaction(params);
if (found) {
let confirmations = 0;
if (found.blockHeight && found.blockHeight >= 0) {
confirmations = tipHeight - found.blockHeight + 1;
}
found = await this.populateReceipt(found);
// Add effects to old db entries
found = this.populateEffects(found);
const convertedTx = EVMTransactionStorage._apiTransform(found, { object: true }) as EVMTransactionJSON;
return { ...convertedTx, confirmations };
}
} catch (err) {
console.error(err);
}
return undefined;
}
async _getTransaction(params: StreamTransactionParams) {
let { chain, network, txId } = params;
let query = { chain, network, txid: txId };
const tip = await this.getLocalTip(params);
const tipHeight = tip ? tip.height : 0;
const found = await EVMTransactionStorage.collection.findOne(query);
return { tipHeight, found };
}
async broadcastTransaction(params: BroadcastTransactionParams) {
const { network, rawTx } = params;
const { web3 } = await this.getWeb3(network, { type: 'realtime' });
const rawTxs = typeof rawTx === 'string' ? [rawTx] : rawTx;
const txids = new Array<string>();
for (const tx of rawTxs) {
const txid = await new Promise<string>((resolve, reject) => {
web3.eth
.sendSignedTransaction(tx)
.on('transactionHash', resolve)
.on('error', reject)
.catch(e => {
logger.error('%o', e);
reject(e);
});
});
txids.push(txid);
}
return txids.length === 1 ? txids[0] : txids;
}
async streamAddressTransactions(params: StreamAddressUtxosParams) {
return new Promise<void>(async (resolve, reject) => {
try {
await this._buildAddressTransactionsStream(params);
return resolve();
} catch (err) {
return reject(err);
}
});
}
async _buildAddressTransactionsStream(params: StreamAddressUtxosParams) {
const { req, res, args, chain, network, address } = params;
const { limit, /*since,*/ tokenAddress } = args;
if (!args.tokenAddress) {
const query = {
$or: [
{ chain, network, from: address },
{ chain, network, to: address },
{ chain, network, 'internal.action.to': address }, // Retained for old db entries
{ chain, network, 'effects.to': address }
]
};
// NOTE: commented out since and paging for now b/c they were causing extra long query times on insight.
// The case where an address has >1000 txns is an edge case ATM and can be addressed later
Storage.apiStreamingFind(EVMTransactionStorage, query, { limit /*since, paging: '_id'*/ }, req!, res!);
} else {
try {
const tokenTransfers = await this.getErc20Transfers(network, address, tokenAddress, args);
res!.json(tokenTransfers);
} catch (err: any) {
logger.error('Error streaming address transactions: %o', err.stack || err.message || err);
throw err;
}
}
}
async streamTransactions(params: StreamTransactionsParams) {
const { chain, network, req, res, args } = params;
let { blockHash, blockHeight } = args;
if (!chain || !network) {
throw new Error('Missing chain or network');
}
let query: any = {
chain,
network: network.toLowerCase()
};
if (blockHeight !== undefined) {
query.blockHeight = Number(blockHeight);
}
if (blockHash !== undefined) {
query.blockHash = blockHash;
}
const tip = await this.getLocalTip(params);
const tipHeight = tip ? tip.height : 0;
return Storage.apiStreamingFind(EVMTransactionStorage, query, args, req, res, t => {
let confirmations = 0;
if (t.blockHeight !== undefined && t.blockHeight >= 0) {
confirmations = tipHeight - t.blockHeight + 1;
}
// Add effects to old db entries
if (!t.effects || (t.effects && t.effects.length == 0)) {
t.effects = EVMTransactionStorage.getEffects(t as IEVMTransactionInProcess);
}
const convertedTx = EVMTransactionStorage._apiTransform(t, { object: true }) as Partial<ITransaction>;
return JSON.stringify({ ...convertedTx, confirmations });
});
}
async getWalletBalance(params: GetWalletBalanceParams): Promise<WalletBalanceType> {
const { network, args, wallet } = params;
const hex = args.hex === 'true' || args.hex === '1';
if (wallet._id === undefined) {
throw new Error('Wallet balance can only be retrieved for wallets with the _id property');
}
let addresses = await this.getWalletAddresses(wallet._id);
addresses = !args.address ? addresses : addresses.filter(({ address }) => address.toLowerCase() === args.address.toLowerCase());
let addressBalances = await Promise.all<WalletBalanceType>(addresses.map(({ address }) =>
this.getBalanceForAddress({ chain: this.chain, network, address, args })
));
let balance = addressBalances.reduce(
(prev, cur) => ({
unconfirmed: BigInt(prev.unconfirmed) + BigInt(cur.unconfirmed),
confirmed: BigInt(prev.confirmed) + BigInt(cur.confirmed),
balance: BigInt(prev.balance) + BigInt(cur.balance)
}),
{ unconfirmed: 0n, confirmed: 0n, balance: 0n }
);
return {
unconfirmed: hex ? '0x' + balance.unconfirmed.toString(16) : Number(balance.unconfirmed),
confirmed: hex ? '0x' + balance.confirmed.toString(16) : Number(balance.confirmed),
balance: hex ? '0x' + balance.balance.toString(16) : Number(balance.balance)
};
}
getWalletTransactionQuery(params: StreamWalletTransactionsParams) {
const { chain, network, wallet, args } = params;
let query = {
chain,
network,
wallets: wallet._id,
'wallets.0': { $exists: true },
blockHeight: { $gt: -3 } // Exclude invalid transactions
} as any;
if (args) {
if (args.startBlock || args.endBlock) {
query.$or = [];
if (args.includeMempool) {
query.$or.push({ blockHeight: SpentHeightIndicators.pending });
}
let blockRangeQuery = {} as any;
if (args.startBlock) {
blockRangeQuery.$gte = Number(args.startBlock);
}
if (args.endBlock) {
blockRangeQuery.$lte = Number(args.endBlock);
}
query.$or.push({ blockHeight: blockRangeQuery });
} else {
if (args.startDate) {
const startDate = new Date(args.startDate);
if (startDate.getTime()) {
query.blockTimeNormalized = { $gte: new Date(args.startDate) };
}
}
if (args.endDate) {
const endDate = new Date(args.endDate);
if (endDate.getTime()) {
query.blockTimeNormalized = query.blockTimeNormalized || {};
query.blockTimeNormalized.$lt = new Date(args.endDate);
}
}
}
if (args.includeInvalidTxs) {
delete query.blockHeight;
}
}
return query;
}
async streamWalletTransactions(params: StreamWalletTransactionsParams) {
return new Promise<void>(async (resolve, reject) => {
const { network, wallet, req, res, args } = params;
const { web3 } = await this.getWeb3(network);
let transactionStream = new TransformWithEventPipe({ objectMode: true, passThrough: true });
const walletAddresses = (await this.getWalletAddresses(wallet._id!)).map(waddres => waddres.address);
const ethTransactionTransform = new EVMListTransactionsStream(walletAddresses);
const populateReceipt = new PopulateReceiptTransform();
const populateEffects = new PopulateEffectsTransform();
const streamParams: BuildWalletTxsStreamParams = {
transactionStream,
populateEffects,
walletAddresses
};
transactionStream = await this._buildWalletTransactionsStream(params, streamParams);
if (!args.tokenAddress && wallet._id) {
const internalTxTransform = new InternalTxRelatedFilterTransform(web3, wallet._id);
transactionStream = transactionStream.eventPipe(internalTxTransform);
}
transactionStream = transactionStream
.eventPipe(populateReceipt)
.eventPipe(ethTransactionTransform);
try {
const result = await ExternalApiStream.onStream(transactionStream, req!, res!, { jsonl: true });
if (!result?.success) {
logger.error('Error mid-stream (streamWalletTransactions): %o', result.error?.log || result.error);
}
return resolve();
} catch (err) {
return reject(err);
}
});
}
async _buildWalletTransactionsStream(params: StreamWalletTransactionsParams, streamParams: BuildWalletTxsStreamParams) {
const query = this.getWalletTransactionQuery(params);
let { transactionStream, populateEffects } = streamParams;
transactionStream = EVMTransactionStorage.collection
.find(query)
.sort({ blockTimeNormalized: 1 })
.addCursorFlag('noCursorTimeout', true)
.pipe(new TransformWithEventPipe({ objectMode: true, passThrough: true }));
transactionStream = transactionStream.eventPipe(populateEffects); // For old db entires
if (params.args.tokenAddress) {
const erc20Transform = new Erc20RelatedFilterTransform(params.args.tokenAddress);
transactionStream = transactionStream.eventPipe(erc20Transform);
}
return transactionStream;
}
async getErc20Transfers(
network: string,
address: string,
tokenAddress: string,
args: Partial<StreamWalletTransactionsArgs> = {}
): Promise<Array<Partial<Transaction>>> {
const token = await this.erc20For(network, tokenAddress);
let windowSize = 100;
const { web3 } = await this.getWeb3(network);
const tip = await web3.eth.getBlockNumber();
// If endBlock or startBlock is negative, it is a block offset from the tip
if (args.endBlock! < 0) {
args.endBlock = tip + Number(args.endBlock!);
}
if (args.startBlock! < 0) {
args.startBlock = tip + Number(args.startBlock!);
}
args.endBlock = Math.min(args.endBlock ?? tip, tip);
args.startBlock = Math.max(args.startBlock != null ? Number(args.startBlock) : args.endBlock - 10000, 0);
if (isNaN(args.startBlock!) || isNaN(args.endBlock!)) {
throw new Error('startBlock and endBlock must be numbers');
} else if (args.endBlock < args.startBlock) {
throw new Error('startBlock cannot be greater than endBlock');
} else if (args.endBlock - args.startBlock > 10000) {
throw new Error('Cannot scan more than 10000 blocks at a time. Please limit your search with startBlock and endBlock');
}
windowSize = Math.min(windowSize, args.endBlock - args.startBlock);
let endBlock = args.endBlock;
const tokenTransfers: Partial<Transaction>[] = [];
while (windowSize > 0) {
const [sent, received] = await Promise.all([
token.getPastEvents('Transfer', {
filter: { _from: address },
fromBlock: endBlock - windowSize,
toBlock: endBlock
}),
token.getPastEvents('Transfer', {
filter: { _to: address },
fromBlock: endBlock - windowSize,
toBlock: endBlock
})
]);
tokenTransfers.push(...this.convertTokenTransfers([...sent, ...received]));
endBlock -= windowSize + 1;
windowSize = Math.min(windowSize, endBlock - args.startBlock);
}
return tokenTransfers;
}
convertTokenTransfers(tokenTransfers: Array<ERC20Transfer>) {
return tokenTransfers.map(this.convertTokenTransfer);
}
convertTokenTransfer(transfer: ERC20Transfer) {
const { blockHash, blockNumber, transactionHash, returnValues, transactionIndex } = transfer;
return {
blockHash,
blockNumber,
transactionHash,
transactionIndex,
hash: transactionHash,
from: returnValues['_from'],
to: returnValues['_to'],
value: returnValues['_value']
} as Partial<Transaction>;
}
async getAccountNonce(network: string, address: string) {
const { web3 } = await this.getWeb3(network, { type: 'realtime' });
const count = await web3.eth.getTransactionCount(address);
return count;
/*
*return EthTransactionStorage.collection.countDocuments({
* chain: 'ETH',
* network,
* from: address,
* blockHeight: { $gt: -1 }
*});
*/
}
async getWalletTokenTransactions(
network: string,
walletId: ObjectID,
tokenAddress: string,
args: StreamWalletTransactionsArgs
) {
const addresses = await this.getWalletAddresses(walletId);
const allTokenQueries = Array<Promise<Array<Partial<Transaction>>>>();
for (const walletAddress of addresses) {
const transfers = this.getErc20Transfers(network, walletAddress.address, tokenAddress, args);
allTokenQueries.push(transfers);
}
let batches = await Promise.all(allTokenQueries);
let txs = batches.reduce((agg, batch) => agg.concat(batch));
return txs.sort((tx1, tx2) => tx1.blockNumber! - tx2.blockNumber!);
}
async estimateGas(params): Promise<number> {
return new Promise(async (resolve, reject) => {
try {
let { network, value, from, data, /*gasPrice,*/ to } = params;
const { web3 } = await this.getWeb3(network, { type: 'realtime' });
const dataDecoded = EVMTransactionStorage.abiDecode(data);
if (dataDecoded && dataDecoded.type === 'INVOICE' && dataDecoded.name === 'pay') {
value = dataDecoded.params[0].value;
// gasPrice = dataDecoded.params[1].value;
} else if (data && data.type === 'MULTISEND') {
try {
let method, gasLimit;
const contract = await this.getMultisendContract(network, to);
const addresses = web3.eth.abi.decodeParameter('address[]', data.addresses);
const amounts = web3.eth.abi.decodeParameter('uint256[]', data.amounts);
switch (data.method) {
case 'sendErc20':
method = contract.methods.sendErc20(data.tokenAddress, addresses, amounts);
gasLimit = method ? await method.estimateGas({ from }) : undefined;
break;
case 'sendEth':
method = contract.methods.sendEth(addresses, amounts);
gasLimit = method ? await method.estimateGas({ from, value }) : undefined;
break;
default:
break;
}
return resolve(Number(gasLimit));
} catch (err) {
return reject(err);
}
}
let _value;
if (data) {
// Gas estimation might fail with `insufficient funds` if value is higher than balance for a normal send.
// We want this method to give a blind fee estimation, though, so we should not include the value
// unless it's needed for estimating smart contract execution.
_value = web3.utils.toHex(value)
}
const opts = {
method: 'eth_estimateGas',
params: [
{
data,
to: to && to.toLowerCase(),
from: from && from.toLowerCase(),
// gasPrice: web3.utils.toHex(gasPrice), // Setting this lower than the baseFee of the last block will cause an error. Better to just leave it out.
value: _value
}
],
jsonrpc: '2.0',
id: 'bitcore-' + Date.now()
};
let provider = web3.currentProvider as any;
provider.send(opts, (err, data) => {
if (err) return reject(err);
if (!data.result) return reject(data.error || data);
return resolve(Number(data.result));
});
} catch (err) {
return reject(err);
}
});
}
async getBlocks(params: GetBlockParams) {
const { tipHeight, blocks } = await this._getBlocks(params);
const blockTransform = (b: IEVMBlock) => {
let confirmations = 0;
if (b.height && b.height >= 0) {
confirmations = tipHeight - b.height + 1;
}
const convertedBlock = EVMBlockStorage._apiTransform(b, { object: true }) as IEVMBlock;
return { ...convertedBlock, confirmations };
};
return blocks.map(blockTransform);
}
async _getBlocks(params: GetBlockParams) {
const { query, options } = this.getBlocksQuery(params);
let cursor = EVMBlockStorage.collection.find(query, options).addCursorFlag('noCursorTimeout', true);
if (options.sort) {
cursor = cursor.sort(options.sort);
}
const blocks = await cursor.toArray();
const tip = await this.getLocalTip(params);
const tipHeight = tip ? tip.height : 0;
return { tipHeight, blocks };
}
async updateWallet(params: UpdateWalletParams) {
const { chain, network } = params;
const addressBatches = partition(params.addresses, 500);
for (let addressBatch of addressBatches) {
const walletAddressInserts = addressBatch.map(address => {
return {
insertOne: {
document: { chain, network, wallet: params.wallet._id, address, processed: false }
}
};
});
try {
await WalletAddressStorage.collection.bulkWrite(walletAddressInserts);
} catch (err: any) {
if (err.code !== 11000) {
throw err;
}
}
const addressBatchLC = addressBatch.map(address => address.toLowerCase());
await EVMTransactionStorage.collection.updateMany(
{
$or: [
{ chain, network, from: { $in: addressBatch } },
{ chain, network, to: { $in: addressBatch } },
{ chain, network, 'internal.action.to': { $in: addressBatchLC } }, // Support old db entries
{ chain, network, 'calls.to': { $in: addressBatchLC } }, // Support old db entries
{ // Support old db entries
chain,
network,
'calls.abiType.type': 'ERC20',
'calls.abiType.name': { $in: ['transfer', 'transferFrom'] },
'calls.abiType.params.type': 'address',
'calls.abiType.params.value': { $in: addressBatchLC }
},
{ chain, network, 'effects.to': { $in: addressBatch } },
{ chain, network, 'effects.from': { $in: addressBatch } },
]
},
{ $addToSet: { wallets: params.wallet._id } }
);
await WalletAddressStorage.collection.updateMany(
{ chain, network, address: { $in: addressBatch }, wallet: params.wallet._id },
{ $set: { processed: true } }
);
}
}
protected async getBlocksRange(params: GetBlockParams & ChainId) {
const { chain, network, chainId, sinceBlock, args = {} } = params;
let { blockId } = params;
let { startDate, endDate, date, limit = 10, sort = { height: -1 } } = args;
const query: { startBlock?: number; endBlock?: number } = {};
if (!chain || !network) {
throw new Error('Missing required chain and/or network param');
}
// limit - 1 because startBlock is inclusive; ensure limit is >= 0
limit = Math.max(limit - 1, 0);
let height: number | null = null;
if (blockId && blockId.length < 64) {
height = parseInt(blockId, 10);
if (isNaN(height) || height.toString(10) != blockId) {
throw new Error('invalid block id provided');
}
blockId = undefined;
}
if (date) {
startDate = new Date(date);
endDate = new Date(date);
endDate.setDate(endDate.getDate() + 1);
}
if (startDate || endDate) {
if (startDate) {
query.startBlock = await this._getBlockNumberByDate({ date: startDate, chainId }) || 0;
}
if (endDate) {
query.endBlock = await this._getBlockNumberByDate({ date: endDate, chainId }) || 0;
}
}
// Get range
if (sinceBlock) {
let height = Number(sinceBlock);
if (isNaN(height) || height.toString(10) != sinceBlock) {
throw new Error('invalid block id provided');
}
const { web3 } = await this.getWeb3(network);
const tipHeight = await web3.eth.getBlockNumber();
if (tipHeight < height) {
return [];
}
query.endBlock = query.endBlock ?? tipHeight;
query.startBlock = query.startBlock ?? query.endBlock - limit;
} else if (blockId) {
const { web3 } = await this.getWeb3(network);
const blk = await web3.eth.getBlock(blockId);
if (!blk || blk.number == null) {
throw new Error(`Could not get block ${blockId}`);
}
height = blk.number;
}
if (height != null) {
query.startBlock = height;
query.endBlock = height + limit;
}
if (query.startBlock == null || query.endBlock == null) {
// Calaculate range with options
const { web3 } = await this.getWeb3(network);
const tipHeight = await web3.eth.getBlockNumber();
query.endBlock = query.endBlock ?? tipHeight;
query.startBlock = query.startBlock ?? query.endBlock - limit;
}
if (query.endBlock - query.startBlock > limit) {
query.endBlock = query.startBlock + limit;
}
const r = range(query.startBlock, query.endBlock + 1); // +1 since range is [start, end)
if (sort?.height === -1 && query.startBlock < query.endBlock) {
return r.reverse();
}
return r;
}
async _getBlockNumberByDate(params) {
const { date, network } = params;
const block = await EVMBlockStorage.collection.findOne({ chain: this.chain, network, timeNormalized: { $gte: date } }, { sort: { timeNormalized: 1 } });
return block?.height;
}
async getChainId({ network }) {
const { web3 } = await this.getWeb3(network);
return web3.eth.getChainId();
}
async getCoinsForTx() {
return {
inputs: [],
outputs: []
};
}
}