UNPKG

blockchain-api

Version:

API utilities for interacting with the Exatechl2 blockchain

380 lines (328 loc) 11.8 kB
/** * Transaction Operations */ import { Transaction, RpcTxStatus } from '../../types'; import { callRpc } from '../../core/rpc'; import { callExplorerApi } from '../../core/explorerApi'; import { getBlockByNumber, getLatestBlockNumber } from '../blocks/blocks'; import { formatWeiToEth, parseTimestampToMillis, parseHexToDecimal, standardizeTimestamp, formatRelativeTime } from '../../utils/formatters'; import { logger } from '../../utils/logger'; export interface TransactionReceipt { transactionHash: string; transactionIndex: string; blockHash: string; blockNumber: string; from: string; to: string | null; cumulativeGasUsed: string; gasUsed: string; contractAddress: string | null; logs: unknown[]; logsBloom: string; status: string; effectiveGasPrice: string; type: string; } /** * @param {string} txHash * @returns {Promise<Transaction>} */ export async function getTransactionByHash(txHash: string): Promise<Transaction> { return callRpc<Transaction>('eth_getTransactionByHash', [txHash]); } /** * @param {string} txHash * @returns {Promise<TransactionReceipt>} */ export async function getTransactionReceipt(txHash: string): Promise<TransactionReceipt> { return callRpc<TransactionReceipt>('eth_getTransactionReceipt', [txHash]); } /** * @param {number} count * @returns {Promise<Transaction[]>} */ export async function getLatestTransactions(count = 10): Promise<Transaction[]> { try { const latestBlockHex = await getLatestBlockNumber(); const latestBlock = parseHexToDecimal(latestBlockHex); const transactions: Transaction[] = []; const blocksToFetch = Math.min(count, 10); for (let i = 0; i < blocksToFetch; i++) { const blockNumber = latestBlock - i; const blockData = await getBlockByNumber(blockNumber, true); if (blockData && blockData.transactions) { const blockTimestamp = blockData.timestamp; const txsWithTimestamp = (blockData.transactions as Transaction[]).map(tx => ({ ...tx, timestamp: blockTimestamp, blockHash: blockData.hash, })); transactions.push(...txsWithTimestamp); if (transactions.length >= count) { break; } } } return transactions.slice(0, count); } catch (error) { logger.error('Error getting latest transactions:', error); throw error; } } /** * @returns {Promise<string>} */ export async function getGasPrice(): Promise<string> { return callRpc('eth_gasPrice'); } /** * @param {Transaction[]} transactions * @returns {Transaction[]} */ export function formatTransactionsForDisplay(transactions: Transaction[]): Transaction[] { return transactions.map(tx => { let txFee = '0'; const gasUsed = tx.gasUsed || '0x0'; const gasPrice = tx.gasPrice || tx.effectiveGasPrice || '0x0'; const gas = tx.gas || '0x0'; if (tx.gasUsed && tx.effectiveGasPrice) { const gasUsedNum = parseHexToDecimal(tx.gasUsed); const gasPriceNum = parseHexToDecimal(tx.effectiveGasPrice); const feeValue = gasUsedNum * gasPriceNum; txFee = formatWeiToEth(feeValue, 8).replace(' tEXT', ''); } else if (tx.gas && tx.gasPrice) { const gasNum = parseHexToDecimal(tx.gas); const gasPriceNum = parseHexToDecimal(tx.gasPrice); const feeValue = gasNum * gasPriceNum; txFee = formatWeiToEth(feeValue, 8).replace(' tEXT', ''); } let status: 'success' | 'pending' | 'failed' = 'pending'; if (tx.blockNumber) { const txStatus = tx.status as RpcTxStatus; status = txStatus === '0x1' ? 'success' : txStatus === '0x0' ? 'failed' : 'success'; } const capitalizedStatus = status.charAt(0).toUpperCase() + status.slice(1); let valueFormatted; try { if (tx.value) { valueFormatted = formatWeiToEth(tx.value, 6); } else { valueFormatted = '0 tEXT'; } } catch (e) { logger.error('Error parsing transaction value:', e); valueFormatted = '0 tEXT'; } const timestamp = parseTimestampToMillis(tx.timestamp || Date.now()); const age = formatRelativeTime(timestamp); let blockNumberFormatted; let blockNumberValue; try { if (tx.blockNumber) { blockNumberFormatted = tx.blockNumber.toString(); blockNumberValue = typeof tx.blockNumber === 'string' ? (tx.blockNumber.startsWith('0x') ? parseInt(tx.blockNumber, 16) : parseInt(tx.blockNumber)) : tx.blockNumber; } else { blockNumberFormatted = 'Pending'; blockNumberValue = null; } } catch (e) { logger.error('Error parsing block number:', e); blockNumberFormatted = 'Unknown'; blockNumberValue = null; } let methodName = 'Transfer'; let methodSignature = ''; let methodDisplayName = ''; if (tx.method_name) { methodName = tx.method_name; if (methodName.includes('(')) { methodDisplayName = methodName.split('(')[0]; } else { methodDisplayName = methodName; } } else if (tx.method_signature) { methodName = tx.method_signature; methodSignature = tx.method_signature; if (methodName.includes('(')) { methodDisplayName = methodName.split('(')[0]; } else { methodDisplayName = methodName; } } else { if (tx.input && tx.input !== '0x') { methodName = 'Contract Call'; methodDisplayName = 'Contract Call'; if (tx.input.length >= 10) { methodSignature = tx.input.substring(0, 10); } if (!tx.to) { methodName = 'Contract Creation'; methodDisplayName = 'Contract Creation'; } } else { methodDisplayName = 'Transfer'; } } if (methodDisplayName.toLowerCase() === 'transfer') { methodDisplayName = 'Transfer'; } else if (methodDisplayName.toLowerCase() === 'approve') { methodDisplayName = 'Approve'; } else if (methodDisplayName.toLowerCase() === 'mint') { methodDisplayName = 'Mint'; } else if (methodDisplayName.toLowerCase() === 'withdraw') { methodDisplayName = 'Withdraw'; } else if (methodDisplayName.toLowerCase() === 'deposit') { methodDisplayName = 'Deposit'; } else if (methodDisplayName.toLowerCase() === 'swap') { methodDisplayName = 'Swap'; } const nonceDecimal = parseHexToDecimal(tx.nonce); return { hash: tx.hash, from: tx.from, to: tx.to || 'Contract Creation', value: valueFormatted, status, statusCapitalized: capitalizedStatus, timestamp, age, method: methodName, methodDisplayName, methodSignature: methodSignature, txFee: txFee === '0' ? '0 tEXT' : `${txFee} tEXT`, txnFee: txFee === '0' ? '0 tEXT' : `${txFee} tEXT`, blockNumber: blockNumberFormatted, block: blockNumberValue, gas, gasPrice, gasUsed, nonce: tx.nonce || '0x0', input: tx.input || '0x', gasUsedDecimal: parseHexToDecimal(gasUsed), gasPriceGwei: parseHexToDecimal(gasPrice) / 1e9 > 0 ? (parseHexToDecimal(gasPrice) / 1e9).toFixed(2) : '0', gasDecimal: parseHexToDecimal(gas), nonceDecimal }; }); } export interface PaginatedTransactions { transactions: Transaction[]; totalCount: number; totalPages: number; page: number; perPage: number; } /** * @param {number} page * @param {number} perPage * @param {boolean} debug * @returns {Promise<PaginatedTransactions>} */ export async function getLatestTransactionsFromExplorer( page = 1, perPage = 10, debug = false ): Promise<PaginatedTransactions> { try { interface ExplorerTransactionsResponse { data?: any[]; transactions?: any[]; meta?: { total?: number; count?: number; total_pages?: number; }; totalCount?: number; total?: number; count?: number; status?: string; } if (debug) logger.debug(`Fetching transactions: page=${page}, perPage=${perPage}`); const response = await callExplorerApi<ExplorerTransactionsResponse>('transactions', { limit: perPage, offset: (page - 1) * perPage }); if (debug) { logger.debug('API Response Structure:', JSON.stringify(response).substring(0, 200) + '...'); logger.debug('Response received, processing transactions...'); } let txData: any[] = []; if (Array.isArray(response)) { txData = response; } else if (response.data && Array.isArray(response.data)) { txData = response.data; } else if (response.transactions && Array.isArray(response.transactions)) { txData = response.transactions; } else { logger.error('Unexpected API response format:', response); throw new Error('Unexpected API response format. Check logs for details.'); } if (debug) logger.debug(`Processing ${txData.length} transactions...`); const transactions = txData.map((txData: any) => { const transaction: any = { hash: txData.hash, blockNumber: txData.block_number ? (txData.block_number.startsWith?.('0x') ? txData.block_number : `0x${parseInt(txData.block_number).toString(16)}`) : undefined, blockHash: txData.block_hash, from: txData.from_address || txData.from, to: txData.to_address || txData.to, value: txData.value ? (txData.value.startsWith?.('0x') ? txData.value : `0x${BigInt(txData.value).toString(16)}`) : '0x0', gas: txData.gas ? (txData.gas.startsWith?.('0x') ? txData.gas : `0x${parseInt(txData.gas).toString(16)}`) : undefined, gasPrice: txData.gas_price ? (txData.gas_price.startsWith?.('0x') ? txData.gas_price : `0x${BigInt(txData.gas_price).toString(16)}`) : undefined, gasUsed: txData.gas_used ? (txData.gas_used.startsWith?.('0x') ? txData.gas_used : `0x${parseInt(txData.gas_used).toString(16)}`) : undefined, nonce: txData.nonce ? (txData.nonce.startsWith?.('0x') ? txData.nonce : `0x${parseInt(txData.nonce).toString(16)}`) : undefined, transactionIndex: txData.transaction_index ? (txData.transaction_index.startsWith?.('0x') ? txData.transaction_index : `0x${parseInt(txData.transaction_index).toString(16)}`) : undefined, input: txData.input || txData.input_data || txData.data || '0x', status: txData.status, timestamp: standardizeTimestamp(txData.timestamp, true) || Date.now(), method_signature: txData.method_signature, method_name: txData.method_name }; return transaction; }); const formattedTransactions = formatTransactionsForDisplay(transactions); const totalCount = (response.meta && response.meta.total) || response.total || response.totalCount || response.count || (response.meta && response.meta.count) || transactions.length; const totalPages = (response.meta && response.meta.total_pages) || Math.ceil(totalCount / perPage); if (debug) logger.debug(`Returning ${transactions.length} transactions, totalCount: ${totalCount}, totalPages: ${totalPages}`); return { transactions: formattedTransactions, totalCount, totalPages, page, perPage }; } catch (error: any) { logger.error('Error fetching transactions from Explorer DB:', error); throw new Error(`Failed to get transactions from Explorer DB: ${error.message}`); } }