UNPKG

blockchain-api

Version:

API utilities for interacting with the Exatechl2 blockchain

144 lines (110 loc) 5.11 kB
import { TokenHolder } from '../../types'; import { callRpc } from '../../core/rpc'; import { getBlockByNumber, getLatestBlockNumber } from '../blocks/blocks'; import { formatTokenAmount, parseBlockchainTimestamp, parseHexToDecimal, formatDecimalToHex } from '../../utils/formatters'; import { getTokenInfo } from './tokens'; /** * @param {string} tokenAddress * @param {number} limit * @returns {Promise<TokenHolder[]>} */ export async function getTokenHolders( tokenAddress: string, limit: number = 20 ): Promise<TokenHolder[]> { 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 { decimals } = await getTokenInfo(normalizedAddress); const transferEventSignature = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'; const balanceOfSignature = '0x70a08231'; const latestBlockHex = await getLatestBlockNumber(); 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 balanceMap = new Map<string, bigint>(); const lastUpdateMap = new Map<string, number>(); const totalSupplyBigInt = BigInt(await callRpc<string>('eth_call', [{ to: normalizedAddress, data: '0x18160ddd' }, 'latest']).catch(() => '0x0')); for (const log of logs) { if (log.topics && log.topics.length >= 3) { const fromAddress = '0x' + log.topics[1].slice(26).toLowerCase(); const toAddress = '0x' + log.topics[2].slice(26).toLowerCase(); const value = BigInt(log.data); const blockNumber = parseHexToDecimal(log.blockNumber); const block = await getBlockByNumber(blockNumber, false); let timestamp = Math.floor(Date.now() / 1000); if (block && block.timestamp) { try { const date = parseBlockchainTimestamp(block.timestamp); timestamp = Math.floor(date.getTime() / 1000); } catch (error) { console.error('Error parsing block timestamp:', error); } } const currentFromBalance = balanceMap.get(fromAddress) || BigInt(0); balanceMap.set(fromAddress, currentFromBalance - value); lastUpdateMap.set(fromAddress, timestamp); const currentToBalance = balanceMap.get(toAddress) || BigInt(0); balanceMap.set(toAddress, currentToBalance + value); lastUpdateMap.set(toAddress, timestamp); } } const holders: TokenHolder[] = []; for (const [address, estimatedBalance] of balanceMap.entries()) { if (estimatedBalance <= BigInt(0) && address !== '0x0000000000000000000000000000000000000000') continue; try { const paddedAddress = '000000000000000000000000' + address.slice(2); const balanceHex = await callRpc<string>('eth_call', [{ to: normalizedAddress, data: balanceOfSignature + paddedAddress }, 'latest']); const balanceBigInt = BigInt(balanceHex); if (balanceBigInt <= BigInt(0)) continue; const percentage = totalSupplyBigInt > BigInt(0) ? Number((balanceBigInt * BigInt(10000)) / totalSupplyBigInt) / 100 : 0; const balanceInTokens = Number(balanceBigInt) / Math.pow(10, decimals); const formattedBalance = formatTokenAmount(balanceBigInt.toString(), decimals); const lastUpdated = lastUpdateMap.get(address) || Math.floor(Date.now() / 1000); const isZeroAddress = address === '0x0000000000000000000000000000000000000000'; holders.push({ address, balance: formattedBalance, rawBalance: balanceBigInt.toString(), percentage: percentage.toFixed(2) + '%', lastUpdated, label: isZeroAddress ? 'Burned Tokens' : undefined }); } catch (e) { console.warn(`Error getting balance for ${address}:`, e); } } return holders .sort((a, b) => BigInt(b.rawBalance) > BigInt(a.rawBalance) ? 1 : -1) .slice(0, limit); } catch (error) { console.error('Error getting token holders:', error); return []; } }