UNPKG

blockchain-api

Version:

API utilities for interacting with the Exatechl2 blockchain

469 lines (468 loc) 19.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getAddressBalance = getAddressBalance; exports.isContract = isContract; exports.getAddressTransactionCount = getAddressTransactionCount; exports.getAddressTransactions = getAddressTransactions; exports.getAddressTokenBalances = getAddressTokenBalances; exports.getContractCreationInfo = getContractCreationInfo; exports.getAddressInfo = getAddressInfo; exports.getAddressTransactionsByMethod = getAddressTransactionsByMethod; const rpc_1 = require("../../core/rpc"); const blocks_1 = require("../blocks/blocks"); const transactions_1 = require("../transactions/transactions"); const formatters_1 = require("../../utils/formatters"); const registry_1 = require("../../utils/registry"); const explorerApi_1 = require("../../core/explorerApi"); const logger_1 = require("../../utils/logger"); /** * * @param value * @returns */ function isTransactionObject(value) { return typeof value !== 'string' && value !== null && typeof value === 'object' && 'hash' in value; } /** * * @param {string} address * @returns {Promise<string>} */ async function getAddressBalance(address) { return (0, rpc_1.callRpc)('eth_getBalance', [address, 'latest']); } /** * * @param {string} address * @returns {Promise<boolean>} */ async function isContract(address) { try { const code = await (0, rpc_1.callRpc)('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>} */ async function getAddressTransactionCount(address) { const result = await (0, rpc_1.callRpc)('eth_getTransactionCount', [address, 'latest']); return (0, formatters_1.parseHexToDecimal)(result); } /** * * @param {string} address * @param {number} limit * @param {number} page * @returns {Promise<any>} */ async function getAddressTransactions(address, limit = 20, page = 1) { try { const response = await (0, explorerApi_1.callExplorerApi)(`address/${address}/transactions`, { page, limit }); if (response.status === 'success' && Array.isArray(response.transactions)) { response.transactions = response.transactions.map((tx) => { 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') ? (0, formatters_1.parseHexToDecimal)(gasUsed) : Number(gasUsed); const gasPriceNum = typeof gasPrice === 'string' && gasPrice.startsWith('0x') ? (0, formatters_1.parseHexToDecimal)(gasPrice) : Number(gasPrice); const feeValue = gasUsedNum * gasPriceNum; txFee = (0, formatters_1.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) => { try { const receipt = await (0, transactions_1.getTransactionReceipt)(tx.hash); if (receipt && receipt.gasUsed && receipt.effectiveGasPrice) { const gasUsedNum = (0, formatters_1.parseHexToDecimal)(receipt.gasUsed); const gasPriceNum = (0, formatters_1.parseHexToDecimal)(receipt.effectiveGasPrice); const feeValue = gasUsedNum * gasPriceNum; return { ...tx, txFee: (0, formatters_1.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, limit = 20, page = 1) { try { const latestBlockHex = await (0, blocks_1.getLatestBlockNumber)(); const latestBlock = (0, formatters_1.parseHexToDecimal)(latestBlockHex); const transactions = []; const processedTxHashes = new Set(); 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 (0, blocks_1.getBlockByNumber)(blockNumber, true); if (!block || !block.transactions || !Array.isArray(block.transactions)) { continue; } const blockTimestamp = Math.floor((0, formatters_1.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())); }); for (const tx of relevantTxs) { if (processedTxHashes.has(tx.hash)) continue; processedTxHashes.add(tx.hash); try { const receipt = await (0, transactions_1.getTransactionReceipt)(tx.hash); let status = 'pending'; if (receipt) { status = receipt.status === '0x1' ? 'success' : 'failed'; } const value = tx.value || '0x0'; const isContractCreation = !tx.to; transactions.push({ hash: tx.hash, blockNumber: (0, formatters_1.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[]>} */ async function getAddressTokenBalances(address) { 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_1.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 (0, blocks_1.getLatestBlockNumber)(); const transferEventSignature = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'; const addressWithoutPrefix = normalizedAddress.slice(2).toLowerCase(); const toTopicAddress = '0x000000000000000000000000' + addressWithoutPrefix; const blockRange = 10000; const fromBlock = Math.max(0, (0, formatters_1.parseHexToDecimal)(latestBlockHex) - blockRange); const fromBlockHex = (0, formatters_1.formatDecimalToHex)(fromBlock); logger_1.logger.debug(`Querying token transfers from block ${fromBlockHex} to latest for address ${normalizedAddress}`); const logs = await (0, rpc_1.callRpc)('eth_getLogs', [{ fromBlock: fromBlockHex, toBlock: 'latest', topics: [ transferEventSignature, null, toTopicAddress ] }]); const tokenAddresses = [...new Set(logs.map(log => log.address))]; logger_1.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([ (0, rpc_1.callRpc)('eth_call', [{ to: tokenAddress, data: balanceOfSignature + paddedAddress }, 'latest']), (0, rpc_1.callRpc)('eth_call', [{ to: tokenAddress, data: nameSignature }, 'latest']).catch(() => '0x'), (0, rpc_1.callRpc)('eth_call', [{ to: tokenAddress, data: symbolSignature }, 'latest']).catch(() => '0x'), (0, rpc_1.callRpc)('eth_call', [{ to: tokenAddress, data: decimalsSignature }, 'latest']).catch(() => '0x0000000000000000000000000000000000000000000000000000000000000012') // Default to 18 decimals ]); let balance = balanceResult ? (0, formatters_1.parseHexToDecimal)(balanceResult) : 0; if (balance === 0) { return null; } let decimals = 18; if (decimalsResult && decimalsResult !== '0x') { decimals = (0, formatters_1.parseHexToDecimal)(decimalsResult); } let name = 'Unknown Token'; let symbol = 'UNKNOWN'; if (nameResult && nameResult !== '0x') { try { const nameHex = nameResult.slice(2); const nameLength = (0, formatters_1.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 = (0, formatters_1.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 = (0, formatters_1.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 !== null) .sort((a, b) => parseFloat(b.rawBalance) - parseFloat(a.rawBalance)); logger_1.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}>} */ async function getContractCreationInfo(address) { try { const response = await (0, explorerApi_1.callExplorerApi)(`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>} */ async function getAddressInfo(address) { try { const [balance, isContractAddr, txCount, tokenBalances] = await Promise.all([ getAddressBalance(address), isContract(address), getAddressTransactionCount(address), getAddressTokenBalances(address) ]); const isSystemAddr = (0, registry_1.isRegisteredAddress)(address); const description = (0, registry_1.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>} */ async function getAddressTransactionsByMethod(address, methodName, options = {}) { try { const normalizedAddress = address.toLowerCase(); const params = { ...options, method: methodName }; const response = await (0, explorerApi_1.callExplorerApi)(`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) => { 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) { console.error(`Error fetching ${methodName} transactions for address ${address}:`, error); throw new Error(`Failed to get ${methodName} transactions: ${error.message}`); } }