blockchain-api
Version:
API utilities for interacting with the Exatechl2 blockchain
469 lines (468 loc) • 19.3 kB
JavaScript
;
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}`);
}
}