blockchain-api
Version:
API utilities for interacting with the Exatechl2 blockchain
290 lines (229 loc) • 9.34 kB
text/typescript
import { TokenInfo } from '../../types';
import { callRpc } from '../../core/rpc';
import { callExplorerApi } from '../../core/explorerApi';
import {
formatTokenAmount,
parseHexToDecimal,
formatDecimalToHex,
standardizeTimestamp
} from '../../utils/formatters';
import { logger } from '../../utils/logger';
/**
* @param {string} tokenAddress
* @returns {Promise<TokenInfo>}
*/
export async function getTokenInfo(tokenAddress: string): Promise<TokenInfo> {
try {
const normalizedAddress = tokenAddress.startsWith('0x') ? tokenAddress : `0x${tokenAddress}`;
if (normalizedAddress.length !== 42) {
throw new Error(`Invalid token address format: ${normalizedAddress}`);
}
const code = await callRpc<string>('eth_getCode', [normalizedAddress, 'latest']);
const isContract = code && code !== '0x' && code !== '0x0';
if (!isContract) {
throw new Error(`Address ${normalizedAddress} is not a contract`);
}
const nameSignature = '0x06fdde03';
const symbolSignature = '0x95d89b41';
const decimalsSignature = '0x313ce567';
const totalSupplySignature = '0x18160ddd';
const [nameResult, symbolResult, decimalsResult, totalSupplyResult] = await Promise.all([
callRpc<string>('eth_call', [{
to: normalizedAddress,
data: nameSignature
}, 'latest']).catch(() => '0x'),
callRpc<string>('eth_call', [{
to: normalizedAddress,
data: symbolSignature
}, 'latest']).catch(() => '0x'),
callRpc<string>('eth_call', [{
to: normalizedAddress,
data: decimalsSignature
}, 'latest']).catch(() => '0x0000000000000000000000000000000000000000000000000000000000000012'),
callRpc<string>('eth_call', [{
to: normalizedAddress,
data: totalSupplySignature
}, 'latest']).catch(() => '0x0')
]);
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);
}
}
let totalSupply = '0';
let formattedTotalSupply = '0';
if (totalSupplyResult && totalSupplyResult !== '0x') {
try {
const rawSupply = parseHexToDecimal(totalSupplyResult);
totalSupply = rawSupply.toString();
formattedTotalSupply = formatTokenAmount(totalSupply, decimals, '');
} catch (e) {
console.error('Error parsing total supply:', e);
}
}
let creator = null;
let creationTx = null;
let creationTimestamp = null;
try {
const tokenFromExplorer = await callExplorerApi<any>(`tokens/${normalizedAddress}`);
if (tokenFromExplorer && tokenFromExplorer.data) {
const tokenData = tokenFromExplorer.data;
creator = tokenData.creator_address || null;
creationTx = tokenData.creation_transaction || null;
if (tokenData.creation_timestamp) {
creationTimestamp = standardizeTimestamp(tokenData.creation_timestamp);
} else {
creationTimestamp = null;
}
if (tokenData.name && name === 'Unknown Token') {
name = tokenData.name;
}
if (tokenData.symbol && symbol === 'UNKNOWN') {
symbol = tokenData.symbol;
}
logger.debug(`Got token info from Explorer DB for ${normalizedAddress}`);
}
} catch (e: any) {
console.warn(`Could not fetch token info from Explorer DB: ${e.message}`);
}
const transferEventSignature = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';
const latestBlockHex = await callRpc<string>('eth_blockNumber');
const blockRange = 10000;
const fromBlock = Math.max(0, parseHexToDecimal(latestBlockHex) - blockRange);
const fromBlockHex = formatDecimalToHex(fromBlock);
const logs = await callRpc<any[]>('eth_getLogs', [{
address: normalizedAddress,
fromBlock: fromBlockHex,
toBlock: 'latest',
topics: [transferEventSignature]
}]);
const addresses = new Set<string>();
let transfersCount = logs.length;
logs.forEach(log => {
if (log.topics && log.topics.length >= 3) {
const fromAddress = '0x' + log.topics[1].slice(26);
const toAddress = '0x' + log.topics[2].slice(26);
if (fromAddress !== '0x0000000000000000000000000000000000000000') {
addresses.add(fromAddress);
}
if (toAddress !== '0x0000000000000000000000000000000000000000') {
addresses.add(toAddress);
}
}
});
const holdersCount = addresses.size;
const excludedAddresses = [
'0x0000000000000000000000000000000000000000',
normalizedAddress.toLowerCase(),
];
let lockedSupply = BigInt(0);
const balanceOfSignature = '0x70a08231';
try {
const balancePromises = excludedAddresses.map(async (address) => {
if (address === '0x0000000000000000000000000000000000000000') {
}
const normalizedExcludedAddress = address.startsWith('0x') ? address.toLowerCase() : `0x${address}`.toLowerCase();
const paddedAddress = '000000000000000000000000' + normalizedExcludedAddress.slice(2);
try {
const balanceHex = await callRpc<string>('eth_call', [{
to: normalizedAddress,
data: balanceOfSignature + paddedAddress
}, 'latest']);
return BigInt(balanceHex);
} catch (e) {
console.warn(`Error getting balance for excluded address ${normalizedExcludedAddress}:`, e);
return BigInt(0);
}
});
const excludedBalances = await Promise.all(balancePromises);
lockedSupply = excludedBalances.reduce((sum, balance) => sum + balance, BigInt(0));
logger.debug(`Calculated locked supply: ${lockedSupply.toString()}`);
} catch (e) {
console.warn('Error calculating locked supply:', e);
logger.debug('Falling back to estimated circulating supply');
}
let totalSupplyBigInt: bigint;
try {
if (totalSupply.includes('e+')) {
const parts = totalSupply.split('e+');
const base = parts[0];
const exponent = parseInt(parts[1]);
let fullNumberStr = base.replace('.', '');
const decimalIndex = base.indexOf('.');
const zerosToAdd = exponent - (base.length - decimalIndex - 1);
fullNumberStr += '0'.repeat(Math.max(0, zerosToAdd));
totalSupplyBigInt = BigInt(fullNumberStr);
} else {
totalSupplyBigInt = BigInt(totalSupply);
}
} catch (error) {
console.error('Error converting totalSupply to BigInt:', error);
totalSupplyBigInt = BigInt(totalSupplyResult);
}
let circulatingSupply: number;
let circulatingSupplyPct: number;
if (lockedSupply > BigInt(0) && lockedSupply < totalSupplyBigInt) {
const circulatingSupplyBigInt = totalSupplyBigInt - lockedSupply;
circulatingSupply = Number(circulatingSupplyBigInt);
circulatingSupplyPct = Number(circulatingSupplyBigInt * BigInt(100) / totalSupplyBigInt);
logger.debug(`Calculated circulating supply: ${circulatingSupplyBigInt.toString()} (${circulatingSupplyPct.toFixed(2)}% of total)`);
} else {
circulatingSupply = Math.round(Number(totalSupply) * 0.7);
circulatingSupplyPct = 70;
logger.debug(`Using estimated circulating supply: ${circulatingSupply} (70% of total)`);
}
const formattedCirculatingSupply = formatTokenAmount(circulatingSupply, decimals, '');
return {
address: normalizedAddress,
name,
symbol,
decimals,
totalSupply,
formattedTotalSupply: `${formattedTotalSupply} ${symbol}`,
circulatingSupply,
formattedCirculatingSupply: `${formattedCirculatingSupply} ${symbol}`,
contractType: 'ERC-20',
creator,
creationTx,
creationTimestamp,
price: null,
marketCap: null,
volume24h: null,
holdersCount,
transfersCount
};
} catch (error) {
console.error('Error fetching token info:', error);
throw error;
}
}
/**
*
* @param {string} tokenAddress
* @param {number} limit
* @param {number} [page]
* @returns {Promise<TokenTransfer[]>}
*
* @deprecated
*/