UNPKG

blockchain-api

Version:

API utilities for interacting with the Exatechl2 blockchain

567 lines (476 loc) 17 kB
import { callRpc } from '../../core/rpc'; import { getLatestBlockNumber, getBlockByNumber } from '../blocks/blocks'; import { getTransactionReceipt } from '../transactions/transactions'; import { formatTokenAmount, parseTimestampToMillis, parseHexToDecimal, formatDecimalToHex, formatWeiToEth } from '../../utils/formatters'; import { isRegisteredAddress, getAddressName, } from '../../utils/registry'; import { AddressInfo, AddressTransaction, TransactionObject } from '../../types/address'; import { TokenBalance } from '../../types/token'; import { callExplorerApi } from '../../core/explorerApi'; import { logger } from '../../utils/logger'; /** * * @param value * @returns */ function isTransactionObject(value: any): value is TransactionObject { return typeof value !== 'string' && value !== null && typeof value === 'object' && 'hash' in value; } /** * * @param {string} address * @returns {Promise<string>} */ export async function getAddressBalance(address: string): Promise<string> { return callRpc<string>('eth_getBalance', [address, 'latest']); } /** * * @param {string} address * @returns {Promise<boolean>} */ export async function isContract(address: string): Promise<boolean> { try { const code = await callRpc<string>('eth_getCode', [address, 'latest']); return Boolean(code && code !== '0x' && code !== '0x0'); } catch (error) { console.error('Error checking if address is contract:', error); return false; } } /** * * @param {string} address * @returns {Promise<number>} */ export async function getAddressTransactionCount(address: string): Promise<number> { const result = await callRpc<string>('eth_getTransactionCount', [address, 'latest']); return parseHexToDecimal(result); } /** * * @param {string} address * @param {number} limit * @param {number} page * @returns {Promise<any>} */ export async function getAddressTransactions( address: string, limit: number = 20, page: number = 1 ): Promise<any> { try { const response = await callExplorerApi<any>( `address/${address}/transactions`, { page, limit } ); if (response.status === 'success' && Array.isArray(response.transactions)) { response.transactions = response.transactions.map((tx: any) => { let txFee = '0'; const gasUsed = tx.gas_used || '0'; const gasPrice = tx.gas_price || '0'; if (gasUsed && gasPrice) { const gasUsedNum = typeof gasUsed === 'string' && gasUsed.startsWith('0x') ? parseHexToDecimal(gasUsed) : Number(gasUsed); const gasPriceNum = typeof gasPrice === 'string' && gasPrice.startsWith('0x') ? parseHexToDecimal(gasPrice) : Number(gasPrice); const feeValue = gasUsedNum * gasPriceNum; txFee = formatWeiToEth(feeValue, 8); } return { ...tx, txFee }; }); } return response; } catch (error) { console.error('Error fetching transactions from Explorer API:', error); try { const emptyResponse = { status: 'success', transactions: [], pagination: { currentPage: page, totalPages: 0, totalItems: 0, itemsPerPage: limit } }; const { transactions, totalCount } = await fetchTransactionsViaRpc(address, limit, page); const enhancedTransactions = transactions.map(async (tx: AddressTransaction) => { try { const receipt = await getTransactionReceipt(tx.hash); if (receipt && receipt.gasUsed && receipt.effectiveGasPrice) { const gasUsedNum = parseHexToDecimal(receipt.gasUsed); const gasPriceNum = parseHexToDecimal(receipt.effectiveGasPrice); const feeValue = gasUsedNum * gasPriceNum; return { ...tx, txFee: formatWeiToEth(feeValue, 8) }; } return tx; } catch (e) { console.warn(`Error calculating fee for transaction ${tx.hash}:`, e); return tx; } }); const txsWithFees = await Promise.all(enhancedTransactions); if (txsWithFees.length > 0) { return { status: 'success', transactions: txsWithFees, pagination: { currentPage: page, totalPages: Math.ceil(totalCount / limit), totalItems: totalCount, itemsPerPage: limit } }; } return emptyResponse; } catch (fallbackError) { console.error('Fallback mechanism also failed:', fallbackError); throw error; } } } async function fetchTransactionsViaRpc( address: string, limit: number = 20, page: number = 1 ): Promise<{ transactions: AddressTransaction[], totalCount: number }> { try { const latestBlockHex = await getLatestBlockNumber(); const latestBlock = parseHexToDecimal(latestBlockHex); const transactions: AddressTransaction[] = []; const processedTxHashes = new Set<string>(); const estimatedBlocksToScan = 500; for (let i = 0; i < estimatedBlocksToScan; i++) { if (transactions.length >= limit * page) { break; } const blockNumber = latestBlock - i; if (blockNumber < 0) break; try { const block = await getBlockByNumber(blockNumber, true); if (!block || !block.transactions || !Array.isArray(block.transactions)) { continue; } const blockTimestamp = Math.floor(parseTimestampToMillis(block.timestamp) / 1000); const relevantTxs = block.transactions.filter(tx => { if (!isTransactionObject(tx)) return false; return ( (tx.from && tx.from.toLowerCase() === address.toLowerCase()) || (tx.to && tx.to.toLowerCase() === address.toLowerCase()) ); }) as TransactionObject[]; for (const tx of relevantTxs) { if (processedTxHashes.has(tx.hash)) continue; processedTxHashes.add(tx.hash); try { const receipt = await getTransactionReceipt(tx.hash); let status: 'success' | 'failed' | 'pending' = 'pending'; if (receipt) { status = receipt.status === '0x1' ? 'success' : 'failed'; } const value = tx.value || '0x0'; const isContractCreation = !tx.to; transactions.push({ hash: tx.hash, blockNumber: parseHexToDecimal(tx.blockNumber), timestamp: blockTimestamp, from: tx.from, to: tx.to, value: value, isContractCreation, status }); } catch (e) { console.warn(`Error processing transaction ${tx.hash}:`, e); } } } catch (e) { console.warn(`Error processing block ${blockNumber}:`, e); continue; } } const totalCount = transactions.length; const startIndex = (page - 1) * limit; const paginatedTransactions = transactions.slice(startIndex, startIndex + limit); return { transactions: paginatedTransactions, totalCount }; } catch (error) { console.error('Error getting address transactions via RPC:', error); return { transactions: [], totalCount: 0 }; } } /** * * @param {string} address * @returns {Promise<TokenBalance[]>} */ export async function getAddressTokenBalances( address: string ): Promise<TokenBalance[]> { try { if (!address || typeof address !== 'string') { console.warn('Invalid address provided to getAddressTokenBalances:', address); return []; } const normalizedAddress = address.startsWith('0x') ? address : `0x${address}`; if (normalizedAddress.length !== 42) { console.warn('Invalid address length:', normalizedAddress); return []; } logger.debug('Getting token balances for address:', normalizedAddress); try { await getAddressBalance(normalizedAddress); } catch (error) { console.error('Error validating address with eth_getBalance:', error); return []; } const latestBlockHex = await getLatestBlockNumber(); const transferEventSignature = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'; const addressWithoutPrefix = normalizedAddress.slice(2).toLowerCase(); const toTopicAddress = '0x000000000000000000000000' + addressWithoutPrefix; const blockRange = 10000; const fromBlock = Math.max(0, parseHexToDecimal(latestBlockHex) - blockRange); const fromBlockHex = formatDecimalToHex(fromBlock); logger.debug(`Querying token transfers from block ${fromBlockHex} to latest for address ${normalizedAddress}`); const logs = await callRpc<any[]>('eth_getLogs', [{ fromBlock: fromBlockHex, toBlock: 'latest', topics: [ transferEventSignature, null, toTopicAddress ] }]); const tokenAddresses = [...new Set(logs.map(log => log.address))]; logger.debug(`Found ${tokenAddresses.length} potential token contracts for address ${normalizedAddress}`); if (tokenAddresses.length === 0) { return []; } const tokenPromises = tokenAddresses.map(async (tokenAddress) => { try { const balanceOfSignature = '0x70a08231'; const nameSignature = '0x06fdde03'; const symbolSignature = '0x95d89b41'; const decimalsSignature = '0x313ce567'; const paddedAddress = '000000000000000000000000' + addressWithoutPrefix; const [balanceResult, nameResult, symbolResult, decimalsResult] = await Promise.all([ callRpc<string>('eth_call', [{ to: tokenAddress, data: balanceOfSignature + paddedAddress }, 'latest']), callRpc<string>('eth_call', [{ to: tokenAddress, data: nameSignature }, 'latest']).catch(() => '0x'), callRpc<string>('eth_call', [{ to: tokenAddress, data: symbolSignature }, 'latest']).catch(() => '0x'), callRpc<string>('eth_call', [{ to: tokenAddress, data: decimalsSignature }, 'latest']).catch(() => '0x0000000000000000000000000000000000000000000000000000000000000012') // Default to 18 decimals ]); let balance = balanceResult ? parseHexToDecimal(balanceResult) : 0; if (balance === 0) { return null; } let decimals = 18; if (decimalsResult && decimalsResult !== '0x') { decimals = parseHexToDecimal(decimalsResult); } let name = 'Unknown Token'; let symbol = 'UNKNOWN'; if (nameResult && nameResult !== '0x') { try { const nameHex = nameResult.slice(2); const nameLength = parseHexToDecimal('0x' + nameHex.slice(64, 128)); const nameData = nameHex.slice(128, 128 + nameLength * 2); name = Buffer.from(nameData, 'hex').toString('utf8'); } catch (e) { console.error('Error parsing token name:', e); } } if (symbolResult && symbolResult !== '0x') { try { const symbolHex = symbolResult.slice(2); const symbolLength = parseHexToDecimal('0x' + symbolHex.slice(64, 128)); const symbolData = symbolHex.slice(128, 128 + symbolLength * 2); symbol = Buffer.from(symbolData, 'hex').toString('utf8'); } catch (e) { console.error('Error parsing token symbol:', e); } } const balanceInTokens = balance / Math.pow(10, decimals); if (balanceInTokens < 0.000001) { return null; } const formattedBalance = formatTokenAmount(balance.toString(), decimals); return { token: symbol, name: name, balance: formattedBalance, rawBalance: balanceInTokens.toString(), address: tokenAddress, value: `${formattedBalance} ${symbol}`, decimals: decimals }; } catch (error) { console.error(`Error fetching data for token at ${tokenAddress}:`, error); return null; } }); const tokenResults = await Promise.all(tokenPromises); const tokenBalances = tokenResults .filter((result): result is TokenBalance => result !== null) .sort((a, b) => parseFloat(b.rawBalance) - parseFloat(a.rawBalance)); logger.debug(`Found ${tokenBalances.length} tokens with non-zero balance for address ${normalizedAddress}`); return tokenBalances; } catch (error) { console.error('Error fetching token balances:', error); return []; } } /** * * @param {string} address * @returns {Promise<{creator: string | null, creationTx: string | null, creationTimestamp: number | null}>} */ export async function getContractCreationInfo(address: string): Promise<{ creator: string | null, creationTx: string | null, creationTimestamp: number | null }> { try { const response = await callExplorerApi<any>(`contracts/${address}`); if (response && response.data) { const contractData = response.data; return { creator: contractData.creator_address || null, creationTx: contractData.creation_transaction || null, creationTimestamp: contractData.creation_timestamp ? parseInt(contractData.creation_timestamp) : null }; } return { creator: null, creationTx: null, creationTimestamp: null }; } catch (error) { console.warn(`Could not fetch contract info from Explorer DB: ${error}`); return { creator: null, creationTx: null, creationTimestamp: null }; } } /** * * @param {string} address * @returns {Promise<AddressInfo>} */ export async function getAddressInfo(address: string): Promise<AddressInfo> { try { const [balance, isContractAddr, txCount, tokenBalances] = await Promise.all([ getAddressBalance(address), isContract(address), getAddressTransactionCount(address), getAddressTokenBalances(address) ]); const isSystemAddr = isRegisteredAddress(address); const description = getAddressName(address); let creator = null; let creationTx = null; let creationTimestamp = null; if (isContractAddr) { const contractInfo = await getContractCreationInfo(address); creator = contractInfo.creator; creationTx = contractInfo.creationTx; creationTimestamp = contractInfo.creationTimestamp; } return { address, balance, transactionCount: txCount, isContract: isContractAddr, isSystemAddress: isSystemAddr, description: description || undefined, tokenBalances, creator, creationTx, creationTimestamp }; } catch (error) { console.error('Error getting address info:', error); throw error; } } /** * * @param {string} address * @param {string} methodName * @param {object} options * @returns {Promise<any>} */ export async function getAddressTransactionsByMethod( address: string, methodName: string, options: Record<string, any> = {} ): Promise<any> { try { const normalizedAddress = address.toLowerCase(); const params: Record<string, any> = { ...options, method: methodName }; const response = await callExplorerApi<any>(`address/${normalizedAddress}/transactions`, params); if (response && response.transactions && Array.isArray(response.transactions)) { if (methodName.toLowerCase() === 'approve') { response.transactions = await Promise.all(response.transactions.map(async (tx: any) => { if (tx.to_address) { try { return { ...tx, token_symbol: 'tEXT' }; } catch (error) { console.warn(`Could not get token info for ${tx.to_address}:`, error); } } return tx; })); } } return response; } catch (error: any) { console.error(`Error fetching ${methodName} transactions for address ${address}:`, error); throw new Error(`Failed to get ${methodName} transactions: ${error.message}`); } }