blockchain-api
Version:
API utilities for interacting with the Exatechl2 blockchain
144 lines (110 loc) • 5.11 kB
text/typescript
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 [];
}
}