blockchain-api
Version:
API utilities for interacting with the Exatechl2 blockchain
502 lines (429 loc) • 16.8 kB
text/typescript
import { Block, PaginatedBlocks, BlockchainStats, EnhancedBlock } from '../../types';
import { callRpc } from '../../core/rpc';
import { callExplorerApi } from '../../core/explorerApi';
import { formatBlockForDisplay, FormattedBlock } from './utils';
import {
parseBlockchainTimestamp,
parseHexToDecimal,
standardizeTimestamp
} from '../../utils/formatters';
import { logger } from '../../utils/logger';
/**
*
* @returns {Promise<string>}
*/
export async function getLatestBlockNumber(): Promise<string> {
return callRpc<string>('eth_blockNumber');
}
/**
*
* @param {string|number} blockNumber
* @param {boolean} includeTransactions
* @returns {Promise<Block>}
*/
export async function getBlockByNumber(
blockNumber: string | number,
includeTransactions = false
): Promise<Block> {
const blockNumberHex = typeof blockNumber === 'number'
? '0x' + blockNumber.toString(16)
: blockNumber;
const block = await callRpc<Block>('eth_getBlockByNumber', [blockNumberHex, includeTransactions]);
return block;
}
/**
*
* @param {string} blockHash
* @param {boolean} includeTransactions
* @returns {Promise<Block>}
*/
export async function getBlockByHash(
blockHash: string,
includeTransactions = false
): Promise<Block> {
const block = await callRpc<Block>('eth_getBlockByHash', [blockHash, includeTransactions]);
return block;
}
/**
*
* @param {string} blockNumber
* @returns {Promise<number>}
*/
export async function getBlockTransactionCount(blockNumber: string): Promise<number> {
const result = await callRpc<string>('eth_getBlockTransactionCountByNumber', [blockNumber]);
return parseHexToDecimal(result);
}
/**
*
* @param {number} page
* @param {number} perPage
* @param {boolean} includeTransactions
* @param {boolean} debug
* @returns {Promise<PaginatedBlocks>}
*/
export async function getLatestBlocks(
page = 1,
perPage = 10,
includeTransactions = false,
debug = false
): Promise<PaginatedBlocks> {
try {
interface ExplorerBlocksResponse {
data?: any[];
blocks?: any[];
meta?: {
total?: number;
count?: number;
total_pages?: number;
};
totalCount?: number;
total?: number;
count?: number;
status?: string;
}
if (debug) logger.debug(`Fetching blocks: page=${page}, perPage=${perPage}, includeTransactions=${includeTransactions}`);
const response = await callExplorerApi<ExplorerBlocksResponse>('blocks', {
limit: perPage,
offset: (page - 1) * perPage,
include_transactions: includeTransactions ? 1 : 0
});
if (debug) {
logger.debug('Full API Response Structure:', JSON.stringify(response, null, 2).substring(0, 300) + '...');
logger.debug('Response received, processing blocks...');
}
let blockData: any[] = [];
if (Array.isArray(response)) {
blockData = response;
} else if (response.data && Array.isArray(response.data)) {
blockData = response.data;
} else if (response.blocks && Array.isArray(response.blocks)) {
blockData = response.blocks;
} else {
logger.error('Unexpected API response format:', response);
throw new Error('Unexpected API response format. Check logs for details.');
}
if (debug) logger.debug(`Processing ${blockData.length} blocks...`);
const blocks: FormattedBlock[] = blockData.map((blockData: any, index: number) => {
const block: Block = {
number: blockData.number
? (blockData.number.startsWith?.('0x') ? blockData.number : `0x${parseInt(blockData.number).toString(16)}`)
: '0x0',
hash: blockData.hash || '',
timestamp: standardizeTimestamp(blockData.timestamp, true) || Date.now(),
parentHash: blockData.parent_hash || blockData.parentHash || '',
miner: blockData.miner || blockData.validator || '',
transactions: blockData.transactions || [],
txCount: blockData.transactions_count || blockData.transactionsCount || (blockData.transactions ? blockData.transactions.length : 0),
size: blockData.size
? (blockData.size.startsWith?.('0x') ? blockData.size : `0x${parseInt(blockData.size).toString(16)}`)
: undefined,
gasUsed: blockData.gas_used || blockData.gasUsed
? (String(blockData.gas_used || blockData.gasUsed).startsWith('0x')
? String(blockData.gas_used || blockData.gasUsed)
: `0x${parseInt(String(blockData.gas_used || blockData.gasUsed)).toString(16)}`)
: undefined,
gasLimit: blockData.gas_limit || blockData.gasLimit
? (String(blockData.gas_limit || blockData.gasLimit).startsWith('0x')
? String(blockData.gas_limit || blockData.gasLimit)
: `0x${parseInt(String(blockData.gas_limit || blockData.gasLimit)).toString(16)}`)
: undefined,
baseFeePerGas: blockData.base_fee_per_gas || blockData.baseFeePerGas
? (String(blockData.base_fee_per_gas || blockData.baseFeePerGas).startsWith('0x')
? String(blockData.base_fee_per_gas || blockData.baseFeePerGas)
: `0x${parseInt(String(blockData.base_fee_per_gas || blockData.baseFeePerGas)).toString(16)}`)
: undefined,
difficulty: blockData.difficulty || '',
totalDifficulty: blockData.total_difficulty || blockData.totalDifficulty || '',
extraData: blockData.extra_data || blockData.extraData || '',
stateRoot: blockData.state_root || blockData.stateRoot || '',
receiptsRoot: blockData.receipts_root || blockData.receiptsRoot || '',
logsBloom: blockData.logs_bloom || blockData.logsBloom || '',
nonce: blockData.nonce || '',
mixHash: blockData.mix_hash || blockData.mixHash || '',
sha3Uncles: blockData.sha3_uncles || blockData.sha3Uncles || '',
status: blockData.status || blockData.batch_status || '',
batchId: blockData.batch_id || blockData.batchId,
batchStatus: blockData.batch_status || blockData.batchStatus
};
return formatBlockForDisplay(block);
});
const totalCount =
(response.meta && response.meta.total) ||
response.total ||
response.totalCount ||
response.count ||
(response.meta && response.meta.count) ||
blocks.length;
const totalPages =
(response.meta && response.meta.total_pages) ||
Math.ceil(totalCount / perPage);
if (debug) logger.debug(`Returning ${blocks.length} blocks, totalCount: ${totalCount}, totalPages: ${totalPages}`);
return {
blocks,
totalCount,
totalPages,
page,
perPage
};
} catch (error: any) {
logger.error('Error fetching blocks from Explorer DB:', error);
throw new Error(`Failed to get blocks from Explorer DB: ${error.message}`);
}
}
/**
*
* @param {number} sampleSize
* @returns {Promise<BlockchainStats>}
*/
export async function getBlockchainStats(
sampleSize = 10
): Promise<BlockchainStats> {
try {
const latestBlockHex = await getLatestBlockNumber();
const latestBlockNumber = parseHexToDecimal(latestBlockHex);
const recentBlocks: Block[] = [];
for (let i = 0; i < sampleSize; i++) {
const blockNumber = latestBlockNumber - i;
if (blockNumber < 0) break;
const block = await getBlockByNumber(blockNumber, false);
if (block) recentBlocks.push(block);
}
let avgBlockTime = 0;
let blockTimeChange = 0;
if (recentBlocks.length >= 2) {
const blockTimes = [];
for (let i = 1; i < recentBlocks.length; i++) {
try {
const prevDate = parseBlockchainTimestamp(recentBlocks[i-1].timestamp);
const currDate = parseBlockchainTimestamp(recentBlocks[i].timestamp);
const prevTimestamp = prevDate.getTime() / 1000;
const currTimestamp = currDate.getTime() / 1000;
blockTimes.push(prevTimestamp - currTimestamp);
} catch (error) {
logger.error('Error parsing block timestamp:', error);
continue;
}
}
avgBlockTime = blockTimes.reduce((sum, time) => sum + time, 0) / blockTimes.length;
if (blockTimes.length >= 6) {
const halfIndex = Math.floor(blockTimes.length / 2);
const firstHalf = blockTimes.slice(0, halfIndex).reduce((sum, time) => sum + time, 0) / halfIndex;
const secondHalf = blockTimes.slice(halfIndex).reduce((sum, time) => sum + time, 0) / (blockTimes.length - halfIndex);
blockTimeChange = ((firstHalf - secondHalf) / secondHalf) * 100;
}
}
const txCounts = await Promise.all(
recentBlocks.slice(0, 5).map(block => {
if (Array.isArray(block.transactions)) {
return block.transactions.length;
} else {
return getBlockTransactionCount(block.number);
}
})
);
const avgTxPerBlock = txCounts.reduce((sum, count) => sum + count, 0) / txCounts.length;
const totalTransactions = Math.round(latestBlockNumber * avgTxPerBlock);
let txPerBlockChange = 0;
if (txCounts.length >= 6) {
const halfIndex = Math.floor(txCounts.length / 2);
const firstHalf = txCounts.slice(0, halfIndex).reduce((sum, count) => sum + count, 0) / halfIndex;
const secondHalf = txCounts.slice(halfIndex).reduce((sum, count) => sum + count, 0) / (txCounts.length - halfIndex);
txPerBlockChange = ((firstHalf - secondHalf) / secondHalf) * 100;
}
return {
totalBlocks: latestBlockNumber + 1,
avgBlockTime,
blockTimeChange,
avgTxPerBlock,
txPerBlockChange,
totalTransactions
};
} catch (error) {
logger.error('Error calculating blockchain stats:', error);
throw error;
}
}
async function enhanceTransactionsWithReceipts(transactions: any[]): Promise<any[]> {
try {
if (!transactions || transactions.length === 0 || typeof transactions[0] === 'string') {
return transactions;
}
const { getTransactionReceipt } = require('../transactions');
const enhancedTransactions = await Promise.all(
transactions.map(async (tx: any) => {
try {
const receipt = await getTransactionReceipt(tx.hash);
return {
...tx,
gasUsed: receipt.gasUsed,
effectiveGasPrice: receipt.effectiveGasPrice,
status: receipt.status,
cumulativeGasUsed: receipt.cumulativeGasUsed,
contractAddress: receipt.contractAddress
};
} catch (error) {
logger.error(`Error getting receipt for transaction ${tx.hash}:`, error);
return tx;
}
})
);
return enhancedTransactions;
} catch (error) {
logger.error('Error enhancing transactions with receipts:', error);
return transactions;
}
}
/**
*
* @param {string} blockId
* @param {boolean} includeTransactions
* @returns {Promise<EnhancedBlock>}
*/
export async function getEnhancedBlockDetails(
blockId: string,
includeTransactions = true
): Promise<EnhancedBlock> {
let blockData: Block;
let blockNumber: number;
if (blockId.startsWith('B-')) {
blockNumber = parseInt(blockId.replace('B-', ''));
if (isNaN(blockNumber)) {
throw new Error('Invalid block number format');
}
blockData = await getBlockByNumber(blockNumber, includeTransactions);
} else if (blockId.startsWith('0x')) {
blockData = await getBlockByHash(blockId, includeTransactions);
blockNumber = parseHexToDecimal(blockData.number);
} else {
blockNumber = parseInt(blockId);
if (isNaN(blockNumber)) {
throw new Error('Invalid block identifier');
}
blockData = await getBlockByNumber(blockNumber, includeTransactions);
}
if (!blockData) {
throw new Error('Block data not found');
}
let batchId = undefined;
let batchStatus = undefined;
try {
logger.debug(`Fetching batch information for block ${blockNumber} from Explorer DB`);
const explorerBlockData = await callExplorerApi<any>(`blocks/${blockNumber}`);
if (explorerBlockData && explorerBlockData.data) {
batchId = explorerBlockData.data.batch_id;
batchStatus = explorerBlockData.data.batch_status;
logger.debug(`Found batch info for block ${blockNumber}: ID=${batchId}, Status=${batchStatus}`);
blockData.batchId = batchId;
blockData.batchStatus = batchStatus;
}
} catch (error) {
logger.warn(`Could not fetch batch information from Explorer DB for block ${blockNumber}:`, error);
}
if (Array.isArray(blockData.transactions) && blockData.transactions.length > 0 && typeof blockData.transactions[0] === 'object') {
try {
const blockDate = parseBlockchainTimestamp(blockData.timestamp);
const blockTimestamp = blockDate.getTime();
blockData.transactions = blockData.transactions.map((tx: any) => {
if (!tx.timestamp) {
return { ...tx, timestamp: blockTimestamp };
}
return tx;
});
if (includeTransactions) {
blockData.transactions = await enhanceTransactionsWithReceipts(blockData.transactions);
}
} catch (error) {
logger.error('Error processing block timestamp for transactions:', error);
}
}
const formattedBlock = formatBlockForDisplay(blockData);
let formattedTransactions: any[] = [];
if (Array.isArray(blockData.transactions)) {
if (blockData.transactions.length > 0 && typeof blockData.transactions[0] === 'object') {
try {
const { formatTransactionsForDisplay } = require('../transactions');
if (typeof formatTransactionsForDisplay === 'function') {
formattedTransactions = formatTransactionsForDisplay(blockData.transactions);
} else {
formattedTransactions = blockData.transactions.map((tx: any) => ({
hash: tx.hash,
from: tx.from,
to: tx.to,
value: tx.value || '0',
status: tx.status || 'unknown',
gasUsed: tx.gasUsed,
gasPrice: tx.gasPrice,
gas: tx.gas,
nonce: tx.nonce,
input: tx.input,
timestamp: formattedBlock.timestamp
}));
}
} catch (error) {
logger.error('Error formatting transactions:', error);
formattedTransactions = blockData.transactions.map((tx: any) => ({
hash: tx.hash,
from: tx.from || 'Unknown',
to: tx.to || 'Unknown',
value: tx.value || '0',
status: tx.status || 'unknown',
gasUsed: tx.gasUsed,
gasPrice: tx.gasPrice,
gas: tx.gas,
nonce: tx.nonce,
input: tx.input,
timestamp: formattedBlock.timestamp
}));
}
} else {
formattedTransactions = (blockData.transactions as string[]).map((hash: string) => ({
hash,
from: 'Unknown',
to: 'Unknown',
value: '0',
status: 'unknown',
timestamp: formattedBlock.timestamp
}));
}
}
const rootHash = blockData.stateRoot || blockData.receiptsRoot || undefined;
let l1BlockNumber: number | undefined = undefined;
if (blockData.l1BlockNumber) {
if (typeof blockData.l1BlockNumber === 'string' && blockData.l1BlockNumber.startsWith('0x')) {
l1BlockNumber = parseInt(blockData.l1BlockNumber, 16);
} else {
l1BlockNumber = Number(blockData.l1BlockNumber);
}
}
const enhancedBlock: EnhancedBlock = {
...formattedBlock,
transactions: formattedTransactions,
txCount: formattedTransactions.length,
transferTxCount: formattedTransactions.filter((tx: any) =>
tx.method === 'Transfer'
).length,
contractTxCount: formattedTransactions.filter((tx: any) =>
tx.method === 'Contract Call' || tx.method === 'Contract Creation'
).length,
miner: blockData.miner,
validator: blockData.validator || formattedBlock.validator,
parentHash: blockData.parentHash,
rootHash: rootHash,
stateRoot: blockData.stateRoot,
receiptsRoot: blockData.receiptsRoot,
extraData: blockData.extraData,
validatorBlocks: Math.floor(Math.random() * 100) + 50,
difficulty: blockData.difficulty,
totalDifficulty: blockData.totalDifficulty,
nonce: blockData.nonce,
mixHash: blockData.mixHash,
sha3Uncles: blockData.sha3Uncles,
logsBloom: blockData.logsBloom,
sendCount: blockData.sendCount,
sendRoot: blockData.sendRoot,
l1BlockNumber,
batchId: blockData.batchId,
batchStatus: blockData.batchStatus
};
return enhancedBlock;
}