import { ethers } from 'ethers';
import { V2RequestBuilder, parseApiResponse, isErrorResponse, type ApiResponse } from '../api/v2-request-builder.js';
import { getChainId, isValidNetwork, type NetworkSlug } from '../config/networks.js';
import type {
Transaction, TokenTransfer, GasPrice, MinedBlock,
InternalTransaction, ContractSourceCode, VerifiedContract,
BlockReward, BlockDetails, BeaconWithdrawal, TokenInfo,
TokenHolder, TokenBalance, EventLog, NetworkStats, DailyStats,
EtherscanServiceOptions, Address, TxHash
} from '../types/index.js';
/**
* Legacy network type for backward compatibility
* Now supports all networks defined in networks.ts
*/
export type SupportedNetwork =
| 'mainnet'
| 'ethereum'
| 'sepolia'
| 'arbitrum'
| 'optimism'
| 'polygon'
| NetworkSlug;
/**
* Legacy chain ID mapping for backward compatibility
* Use getChainId() from networks.ts for full network support
*/
export const NETWORK_CHAIN_IDS: Record<string, number> = {
mainnet: 1,
ethereum: 1,
sepolia: 11155111,
arbitrum: 42161,
optimism: 10,
polygon: 137
};
/**
* Pagination options for list-based queries
*/
export interface PaginationOptions {
/** Page number (1-indexed) */
page?: number;
/** Number of results per page */
offset?: number;
/** Starting block number */
startBlock?: number;
/** Ending block number */
endBlock?: number;
/** Sort order */
sort?: 'asc' | 'desc';
}
/**
* Parameters for event log queries
*/
export interface LogsParams {
/** Contract address filter */
address?: string;
/** Starting block */
fromBlock?: number;
/** Ending block */
toBlock?: number;
/** Event signature or first topic */
topic0?: string;
/** Second indexed parameter */
topic1?: string;
/** Third indexed parameter */
topic2?: string;
/** Fourth indexed parameter */
topic3?: string;
/** Operator for topic0 and topic1 */
topic0_1_opr?: 'and' | 'or';
/** Operator for topic0 and topic2 */
topic0_2_opr?: 'and' | 'or';
/** Operator for topic0 and topic3 */
topic0_3_opr?: 'and' | 'or';
/** Operator for topic1 and topic2 */
topic1_2_opr?: 'and' | 'or';
/** Operator for topic1 and topic3 */
topic1_3_opr?: 'and' | 'or';
/** Operator for topic2 and topic3 */
topic2_3_opr?: 'and' | 'or';
}
/**
* Etherscan API Service using v2 API
*
* Provides methods for querying blockchain data across 70+ networks
* supported by Etherscan's v2 unified API.
*/
export class EtherscanService {
private chainId: number;
private apiKey: string;
private requestBuilder: V2RequestBuilder;
/**
* Creates a new EtherscanService instance
*
* @param apiKey - Etherscan API key
* @param networkOrChainId - Network slug (e.g., 'ethereum', 'arbitrum') or chain ID number
* @throws {Error} If API key is missing or network is invalid
*
* @example
* ```typescript
* // Using network slug
* const service = new EtherscanService('your-api-key', 'ethereum');
*
* // Using chain ID
* const arbService = new EtherscanService('your-api-key', 42161);
*
* // Default to Ethereum mainnet
* const defaultService = new EtherscanService('your-api-key');
* ```
*/
constructor(apiKey: string, networkOrChainId: string | number = 'ethereum') {
if (!apiKey) {
throw new Error('Etherscan API key is required');
}
// Resolve network slug or chain ID to chain ID
try {
this.chainId = getChainId(networkOrChainId);
} catch (error) {
throw new Error(
`Invalid network or chain ID: ${networkOrChainId}. ` +
`Use a valid network slug (e.g., 'ethereum', 'arbitrum') or chain ID (e.g., 1, 42161). ` +
`Error: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
this.apiKey = apiKey;
this.requestBuilder = new V2RequestBuilder(this.chainId);
}
/**
* Make a request to the Etherscan v2 API
*
* @template T The expected type of the result data
* @param module - API module (e.g., 'account', 'contract', 'block')
* @param action - API action (e.g., 'balance', 'txlist', 'getabi')
* @param params - Additional query parameters
* @returns Parsed result data
* @throws {Error} If request fails or API returns error
*
* @internal
*/
private async makeRequest<T>(
module: string,
action: string,
params: Record<string, string | number | boolean> = {}
): Promise<T> {
// Build URL using V2RequestBuilder (automatically injects chainid)
const url = this.requestBuilder.buildUrl({
module,
action,
params,
apiKey: this.apiKey
});
try {
console.log(`[Chain ${this.chainId}] Fetching: ${module}.${action}`);
const response = await fetch(url);
if (!response.ok) {
throw new Error(
`HTTP error: ${response.status} ${response.statusText}`
);
}
const rawData = await response.json();
// Parse and validate response structure
const parsed = parseApiResponse<T>(rawData);
// Check for API-level errors
if (isErrorResponse(parsed)) {
console.error(
`[Chain ${this.chainId}] Etherscan API Error: ${parsed.message} - ${parsed.result}`
);
throw new Error(
`Etherscan API error (chain ${this.chainId}): ${parsed.message}. Details: ${parsed.result}`
);
}
return parsed.result;
} catch (error) {
// Re-throw our formatted errors
if (error instanceof Error && error.message.includes('Etherscan API error')) {
throw error;
}
// Wrap other errors with context
console.error(
`[Chain ${this.chainId}] Error fetching ${module}.${action}:`,
error
);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
throw new Error(
`Failed to fetch data from Etherscan (chain ${this.chainId}, ${module}.${action}): ${errorMessage}`
);
}
}
async getAddressBalance(address: string): Promise<{
address: string;
balanceInWei: bigint;
balanceInEth: string;
}> {
try {
// Validate the address
const validAddress = ethers.getAddress(address);
// Use v2 API directly instead of ethers provider
const result = await this.makeRequest<string>('account', 'balance', {
address: validAddress,
tag: 'latest'
});
const balanceInWei = BigInt(result);
const balanceInEth = ethers.formatEther(balanceInWei);
return {
address: validAddress,
balanceInWei,
balanceInEth
};
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to get balance: ${error.message}`);
}
throw error;
}
}
async getTransactionHistory(address: string, limit: number = 10): Promise<Transaction[]> {
try {
const validAddress = ethers.getAddress(address);
interface RawTransaction {
hash: string;
from: string;
to: string;
value: string;
timeStamp: string;
blockNumber: string;
}
const result = await this.makeRequest<RawTransaction[]>('account', 'txlist', {
address: validAddress,
startblock: 0,
endblock: 99999999,
page: 1,
offset: limit,
sort: 'desc'
});
// Format the results
return result.slice(0, limit).map((tx) => ({
hash: tx.hash as TxHash,
from: tx.from as Address,
to: (tx.to || 'Contract Creation') as Address | 'Contract Creation',
value: ethers.formatEther(tx.value),
timestamp: parseInt(tx.timeStamp) || 0,
blockNumber: parseInt(tx.blockNumber) || 0
}));
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to get transaction history: ${error.message}`);
}
throw error;
}
}
async getTokenTransfers(address: string, limit: number = 10): Promise<TokenTransfer[]> {
try {
const validAddress = ethers.getAddress(address);
interface RawTokenTransfer {
contractAddress: string;
tokenName: string;
tokenSymbol: string;
tokenDecimal: string;
from: string;
to: string;
value: string;
timeStamp: string;
blockNumber: string;
}
const result = await this.makeRequest<RawTokenTransfer[]>('account', 'tokentx', {
address: validAddress,
page: 1,
offset: limit,
sort: 'desc'
});
// Format the results
return result.slice(0, limit).map((tx) => ({
token: tx.contractAddress as Address,
tokenName: tx.tokenName,
tokenSymbol: tx.tokenSymbol,
from: tx.from as Address,
to: tx.to as Address,
value: ethers.formatUnits(tx.value, parseInt(tx.tokenDecimal)),
timestamp: parseInt(tx.timeStamp) || 0,
blockNumber: parseInt(tx.blockNumber) || 0
}));
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to get token transfers: ${error.message}`);
}
throw error;
}
}
async getContractABI(address: string): Promise<string> {
try {
const validAddress = ethers.getAddress(address);
return await this.makeRequest<string>('contract', 'getabi', {
address: validAddress
});
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to get contract ABI: ${error.message}`);
}
throw error;
}
}
async getGasOracle(): Promise<GasPrice> {
try {
interface RawGasPrice {
SafeGasPrice: string;
ProposeGasPrice: string;
FastGasPrice: string;
}
const result = await this.makeRequest<RawGasPrice>('gastracker', 'gasoracle', {});
return {
safeGwei: result.SafeGasPrice,
proposeGwei: result.ProposeGasPrice,
fastGwei: result.FastGasPrice
};
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to get gas prices: ${error.message}`);
}
throw error;
}
}
async getENSName(address: string): Promise<string | null> {
try {
const validAddress = ethers.getAddress(address);
// ENS is mainnet-only, use a public RPC provider instead of Etherscan API
// This avoids using the deprecated v1 API
const mainnetProvider = new ethers.JsonRpcProvider('https://eth.llamarpc.com');
return await mainnetProvider.lookupAddress(validAddress);
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to get ENS name: ${error.message}`);
}
throw error;
}
}
async getMinedBlocks(
address: string,
blockType: 'blocks' | 'uncles' = 'blocks',
page: number = 1,
offset: number = 10,
startBlock?: number,
endBlock?: number
): Promise<MinedBlock[]> {
try {
const validAddress = ethers.getAddress(address);
const params: Record<string, string | number> = {
address: validAddress,
blocktype: blockType,
page,
offset
};
if (startBlock !== undefined) {
params.startblock = startBlock;
}
if (endBlock !== undefined) {
params.endblock = endBlock;
}
interface RawMinedBlock {
blockNumber: string;
blockReward: string;
timeStamp: string;
blockMiner: string;
}
const result = await this.makeRequest<RawMinedBlock[]>('account', 'getminedblocks', params);
// Format the results
return result.map((block) => ({
blockNumber: parseInt(block.blockNumber) || 0,
blockReward: ethers.formatEther(block.blockReward || '0'),
timestamp: parseInt(block.timeStamp) || 0,
blockMiner: block.blockMiner as Address,
blockType
}));
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to get mined blocks: ${error.message}`);
}
throw error;
}
}
async getInternalTransactions(
address: string,
page: number = 1,
offset: number = 10,
startBlock?: number,
endBlock?: number
): Promise<InternalTransaction[]> {
try {
const validAddress = ethers.getAddress(address);
const params: Record<string, string | number> = {
address: validAddress,
page,
offset
};
if (startBlock !== undefined) {
params.startblock = startBlock;
}
if (endBlock !== undefined) {
params.endblock = endBlock;
}
interface RawInternalTransaction {
hash: string;
from: string;
to: string;
value: string;
timeStamp: string;
blockNumber: string;
type: string;
traceId: string;
isError: string;
errCode: string;
}
const result = await this.makeRequest<RawInternalTransaction[]>('account', 'txlistinternal', params);
// Format the results
return result.map((tx) => ({
hash: tx.hash as TxHash,
from: tx.from as Address,
to: (tx.to || 'Contract Creation') as Address | 'Contract Creation',
value: ethers.formatEther(tx.value),
timestamp: parseInt(tx.timeStamp) || 0,
blockNumber: parseInt(tx.blockNumber) || 0,
type: tx.type,
traceId: tx.traceId,
isError: tx.isError === '1',
errCode: tx.errCode
}));
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to get internal transactions: ${error.message}`);
}
throw error;
}
}
async getBlockReward(blockNumber: number): Promise<BlockReward> {
try {
interface RawBlockReward {
timeStamp: string;
blockMiner: string;
blockReward: string;
uncles: string[];
uncleInclusionReward: string;
}
const result = await this.makeRequest<RawBlockReward>('block', 'getblockreward', {
blockno: blockNumber
});
return {
blockNumber: blockNumber,
timeStamp: parseInt(result.timeStamp) || 0,
blockMiner: result.blockMiner as Address,
blockReward: ethers.formatEther(result.blockReward || '0'),
uncles: result.uncles || [],
uncleInclusionReward: ethers.formatEther(result.uncleInclusionReward || '0')
};
} catch (error) {
if(error instanceof Error) {
throw new Error(`Failed to get block reward information: ${error.message}`);
}
throw error;
}
}
async getBlockDetails(blockNumber: number): Promise<BlockDetails> {
try {
interface RawBlockInfo {
hash: string;
parentHash: string;
nonce: string;
sha3Uncles: string;
logsBloom: string;
transactionsRoot: string;
stateRoot: string;
receiptsRoot: string;
miner: string;
difficulty: string;
totalDifficulty: string;
size: string;
extraData: string;
gasLimit: string;
gasUsed: string;
timestamp: string;
transactions?: unknown[];
}
const blockInfo = await this.makeRequest<RawBlockInfo>('proxy', 'eth_getBlockByNumber', {
tag: '0x' + blockNumber.toString(16),
boolean: 'true'
});
return {
number: blockNumber,
hash: (blockInfo.hash || '') as TxHash,
parentHash: (blockInfo.parentHash || '') as TxHash,
nonce: blockInfo.nonce || '',
sha3Uncles: blockInfo.sha3Uncles || '',
logsBloom: blockInfo.logsBloom || '',
transactionsRoot: blockInfo.transactionsRoot || '',
stateRoot: blockInfo.stateRoot || '',
receiptsRoot: blockInfo.receiptsRoot || '',
miner: (blockInfo.miner || '') as Address,
difficulty: blockInfo.difficulty || '0',
totalDifficulty: blockInfo.totalDifficulty || '0',
size: parseInt(blockInfo.size || '0', 16),
extraData: blockInfo.extraData || '',
gasLimit: blockInfo.gasLimit || '0',
gasUsed: blockInfo.gasUsed || '0',
timestamp: parseInt(blockInfo.timestamp || '0', 16),
transactions: blockInfo.transactions?.length || 0
};
} catch (error) {
if(error instanceof Error) {
throw new Error(`Failed to get block details: ${error.message}`);
}
throw error;
}
}
async getContractSourceCode(address: string): Promise<ContractSourceCode> {
try {
const validAddress = ethers.getAddress(address);
interface RawContractSourceCode {
SourceCode: string;
ABI: string;
ContractName: string;
CompilerVersion: string;
OptimizationUsed: string;
Runs: string;
ConstructorArguments: string;
EVMVersion: string;
Library: string;
LicenseType: string;
Proxy: string;
Implementation: string;
SwarmSource: string;
}
const result = await this.makeRequest<RawContractSourceCode[]>('contract', 'getsourcecode', {
address: validAddress
});
// Etherscan returns an array with a single item
const sourceInfo = result[0];
return {
sourceName: sourceInfo.SourceCode ? 'Contract.sol' : '',
sourceCode: sourceInfo.SourceCode || 'Source code not verified',
abi: sourceInfo.ABI,
contractName: sourceInfo.ContractName,
compilerVersion: sourceInfo.CompilerVersion,
optimizationUsed: sourceInfo.OptimizationUsed === '1',
runs: parseInt(sourceInfo.Runs) || 0,
constructorArguments: sourceInfo.ConstructorArguments,
evmVersion: sourceInfo.EVMVersion,
library: sourceInfo.Library,
licenseType: sourceInfo.LicenseType,
proxy: sourceInfo.Proxy === '1',
implementation: (sourceInfo.Implementation || '') as Address | '',
swarmSource: sourceInfo.SwarmSource
};
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to get contract source code: ${error.message}`);
}
throw error;
}
}
async getContractCreation(addresses: string | string[]): Promise<Array<{
contractAddress: string;
contractCreator: string;
txHash: string;
blockNumber: string;
timestamp: string;
contractFactory: string;
creationBytecode: string;
}>> {
try {
// Convert single address to array
const addressArray = Array.isArray(addresses) ? addresses : [addresses];
// Validate all addresses
const validAddresses = addressArray.map(addr => ethers.getAddress(addr));
// API supports up to 5 addresses at a time
if (validAddresses.length > 5) {
throw new Error('Maximum of 5 contract addresses allowed per request');
}
interface RawContractCreation {
contractAddress: string;
contractCreator: string;
txHash: string;
blockNumber: string;
timestamp: string;
contractFactory: string;
creationBytecode: string;
}
const result = await this.makeRequest<RawContractCreation[]>('contract', 'getcontractcreation', {
contractaddresses: validAddresses.join(',')
});
return result.map((creationInfo) => ({
contractAddress: creationInfo.contractAddress,
contractCreator: creationInfo.contractCreator,
txHash: creationInfo.txHash,
blockNumber: creationInfo.blockNumber,
timestamp: creationInfo.timestamp,
contractFactory: creationInfo.contractFactory,
creationBytecode: creationInfo.creationBytecode
}));
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to get contract creation info: ${error.message}`);
}
throw error;
}
}
async verifyContract(params: {
sourceCode: string;
contractAddress: string;
contractName: string;
compilerVersion: string;
optimization?: boolean;
optimizationRuns?: number;
constructorArguments?: string;
evmVersion?: string;
licenseType?: string;
}): Promise<string> {
try {
const validAddress = ethers.getAddress(params.contractAddress);
const verifyParams = {
sourceCode: params.sourceCode,
contractaddress: validAddress,
contractname: params.contractName,
compilerversion: params.compilerVersion,
optimizationUsed: params.optimization ? '1' : '0',
runs: params.optimizationRuns?.toString() || '200',
constructorArguements: params.constructorArguments || '',
evmversion: params.evmVersion || '',
licenseType: params.licenseType || '1', // No License (1)
};
const result = await this.makeRequest<string>('contract', 'verifysourcecode', verifyParams);
return result; // Returns GUID
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to submit contract verification: ${error.message}`);
}
throw error;
}
}
async checkVerificationStatus(guid: string): Promise<{
status: string;
message: string;
}> {
try {
const result = await this.makeRequest<string>('contract', 'checkverifystatus', {
guid
});
return {
status: result === 'Pass' ? 'success' : 'failure',
message: result
};
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to check verification status: ${error.message}`);
}
throw error;
}
}
async verifyProxyContract(params: {
address: string;
expectedImplementation?: string;
}): Promise<string> {
try {
const validAddress = ethers.getAddress(params.address);
const verifyParams: Record<string, string> = {
address: validAddress
};
if (params.expectedImplementation) {
verifyParams.expectedimplementation = ethers.getAddress(params.expectedImplementation);
}
const result = await this.makeRequest<string>('contract', 'verifyproxycontract', verifyParams);
return result; // Returns GUID
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to submit proxy contract verification: ${error.message}`);
}
throw error;
}
}
async getVerifiedContracts(
page: number = 1,
offset: number = 10,
sortBy: string = 'timestamp'
): Promise<VerifiedContract[]> {
try {
interface RawVerifiedContract {
ContractAddress: string;
ContractName: string;
Compiler: string;
CompilerVersion: string;
TxCount: string;
VerifiedTimestamp?: string;
TimeStamp?: string;
}
// Get list of verified contracts
const result = await this.makeRequest<RawVerifiedContract[]>('contract', 'listverifiedcontracts', {
page,
offset,
sort: sortBy
});
// Process each contract
const contracts = await Promise.all(result.map(async (contract) => {
try {
// Get additional contract details
const sourceCode = await this.getContractSourceCode(contract.ContractAddress);
// Use v2 API to get balance instead of ethers provider
const balanceResult = await this.makeRequest<string>('account', 'balance', {
address: contract.ContractAddress,
tag: 'latest'
});
const balance = ethers.formatEther(BigInt(balanceResult));
return {
address: contract.ContractAddress,
name: contract.ContractName,
compiler: contract.Compiler,
version: contract.CompilerVersion,
balance: balance,
txCount: parseInt(contract.TxCount) || 0,
timestamp: parseInt(contract.VerifiedTimestamp || contract.TimeStamp || '0') || 0,
optimization: sourceCode.optimizationUsed,
optimizationRuns: sourceCode.runs,
license: sourceCode.licenseType,
constructorArguments: sourceCode.constructorArguments,
evmVersion: sourceCode.evmVersion,
proxy: sourceCode.proxy,
implementation: sourceCode.implementation,
swarmSource: sourceCode.swarmSource
};
} catch (error) {
console.error(`Error processing contract ${contract.ContractAddress}:`, error);
return null;
}
}));
// Filter out any failed contract fetches
return contracts.filter((contract): contract is VerifiedContract => contract !== null);
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to get verified contracts: ${error.message}`);
}
throw error;
}
}
// ============================================
// V2 API Specific Methods
// ============================================
/**
* Get beacon chain withdrawals for an address
*
* @param address - Ethereum address
* @param options - Pagination options
* @returns Array of beacon chain withdrawals
* @throws {Error} If request fails
*
* @example
* ```typescript
* const withdrawals = await service.getBeaconWithdrawals('0x123...', {
* page: 1,
* offset: 10,
* sort: 'desc'
* });
* ```
*/
async getBeaconWithdrawals(
address: string,
options: PaginationOptions = {}
): Promise<BeaconWithdrawal[]> {
try {
const validAddress = ethers.getAddress(address);
interface RawWithdrawal {
withdrawalIndex: string;
validatorIndex: string;
address: string;
amount: string;
blockNumber: string;
timestamp: string;
}
const params: Record<string, string | number> = {
address: validAddress,
page: options.page || 1,
offset: options.offset || 10
};
if (options.startBlock !== undefined) {
params.startblock = options.startBlock;
}
if (options.endBlock !== undefined) {
params.endblock = options.endBlock;
}
if (options.sort) {
params.sort = options.sort;
}
const result = await this.makeRequest<RawWithdrawal[]>(
'account',
'txsBeaconWithdrawal',
params
);
return result.map((withdrawal) => ({
withdrawalIndex: parseInt(withdrawal.withdrawalIndex) || 0,
validatorIndex: parseInt(withdrawal.validatorIndex) || 0,
address: withdrawal.address as Address,
amount: withdrawal.amount,
blockNumber: parseInt(withdrawal.blockNumber) || 0,
timestamp: parseInt(withdrawal.timestamp) || 0
}));
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to get beacon withdrawals: ${error.message}`);
}
throw error;
}
}
/**
* Get comprehensive token information
*
* @param contractAddress - Token contract address
* @returns Token metadata and social links
* @throws {Error} If request fails
*
* @example
* ```typescript
* const tokenInfo = await service.getTokenInfo('0xdac17f958d2ee523a2206206994597c13d831ec7'); // USDT
* console.log(tokenInfo.tokenName, tokenInfo.symbol, tokenInfo.totalSupply);
* ```
*/
async getTokenInfo(contractAddress: string): Promise<TokenInfo> {
try {
const validAddress = ethers.getAddress(contractAddress);
interface RawTokenInfo {
contractAddress: string;
tokenName: string;
symbol: string;
divisor: string;
tokenType: string;
totalSupply: string;
blueCheckmark?: string;
description?: string;
website?: string;
email?: string;
blog?: string;
reddit?: string;
slack?: string;
facebook?: string;
twitter?: string;
bitcointalk?: string;
github?: string;
telegram?: string;
wechat?: string;
linkedin?: string;
discord?: string;
whitepaper?: string;
tokenPriceUSD?: string;
}
const result = await this.makeRequest<RawTokenInfo>(
'token',
'tokeninfo',
{
contractaddress: validAddress
}
);
return {
contractAddress: result.contractAddress as Address,
tokenName: result.tokenName,
symbol: result.symbol,
divisor: result.divisor,
tokenType: result.tokenType as 'ERC20' | 'ERC721' | 'ERC1155',
totalSupply: result.totalSupply,
blueCheckmark: result.blueCheckmark === '1',
description: result.description,
website: result.website,
email: result.email,
blog: result.blog,
reddit: result.reddit,
slack: result.slack,
facebook: result.facebook,
twitter: result.twitter,
bitcointalk: result.bitcointalk,
github: result.github,
telegram: result.telegram,
wechat: result.wechat,
linkedin: result.linkedin,
discord: result.discord,
whitepaper: result.whitepaper,
tokenPriceUSD: result.tokenPriceUSD
};
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to get token info: ${error.message}`);
}
throw error;
}
}
/**
* Get top token holders
*
* @param contractAddress - Token contract address
* @param options - Pagination options
* @returns Array of token holders with balances
* @throws {Error} If request fails
*
* @example
* ```typescript
* const holders = await service.getTokenHolders('0xdac17f958d2ee523a2206206994597c13d831ec7', {
* page: 1,
* offset: 100
* });
* ```
*/
async getTokenHolders(
contractAddress: string,
options: PaginationOptions = {}
): Promise<TokenHolder[]> {
try {
const validAddress = ethers.getAddress(contractAddress);
interface RawTokenHolder {
address: string;
balance: string;
share: string;
}
const params: Record<string, string | number> = {
contractaddress: validAddress,
page: options.page || 1,
offset: options.offset || 10
};
const result = await this.makeRequest<RawTokenHolder[]>(
'token',
'tokenholderlist',
params
);
return result.map((holder) => ({
address: holder.address as Address,
balance: holder.balance,
share: parseFloat(holder.share) || 0
}));
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to get token holders: ${error.message}`);
}
throw error;
}
}
/**
* Get address token portfolio (all token balances)
*
* @param address - Ethereum address
* @returns Array of token balances
* @throws {Error} If request fails
*
* @example
* ```typescript
* const portfolio = await service.getTokenPortfolio('0x123...');
* portfolio.forEach(token => {
* console.log(`${token.tokenSymbol}: ${token.balance}`);
* });
* ```
*/
async getTokenPortfolio(address: string): Promise<TokenBalance[]> {
try {
const validAddress = ethers.getAddress(address);
interface RawTokenBalance {
tokenAddress: string;
tokenName: string;
tokenSymbol: string;
tokenDecimal: string;
balance: string;
}
const result = await this.makeRequest<RawTokenBalance[]>(
'account',
'addresstokenbalance',
{
address: validAddress
}
);
return result.map((token) => ({
tokenAddress: token.tokenAddress as Address,
tokenName: token.tokenName,
tokenSymbol: token.tokenSymbol,
tokenDecimal: parseInt(token.tokenDecimal) || 18,
balance: token.balance
}));
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to get token portfolio: ${error.message}`);
}
throw error;
}
}
/**
* Get event logs matching filter criteria
*
* @param params - Log filter parameters
* @returns Array of event logs
* @throws {Error} If request fails
*
* @example
* ```typescript
* // Get all Transfer events for a token
* const logs = await service.getLogs({
* address: '0xdac17f958d2ee523a2206206994597c13d831ec7',
* topic0: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
* fromBlock: 12345678,
* toBlock: 12346000
* });
* ```
*/
async getLogs(params: LogsParams): Promise<EventLog[]> {
try {
interface RawLog {
address: string;
topics: string[];
data: string;
blockNumber: string;
blockHash: string;
timeStamp: string;
gasPrice: string;
gasUsed: string;
logIndex: string;
transactionHash: string;
transactionIndex: string;
}
const requestParams: Record<string, string | number> = {};
if (params.address) {
requestParams.address = ethers.getAddress(params.address);
}
if (params.fromBlock !== undefined) {
requestParams.fromBlock = params.fromBlock;
}
if (params.toBlock !== undefined) {
requestParams.toBlock = params.toBlock;
}
// Add topic filters
if (params.topic0) requestParams.topic0 = params.topic0;
if (params.topic1) requestParams.topic1 = params.topic1;
if (params.topic2) requestParams.topic2 = params.topic2;
if (params.topic3) requestParams.topic3 = params.topic3;
// Add topic operators
if (params.topic0_1_opr) requestParams.topic0_1_opr = params.topic0_1_opr;
if (params.topic0_2_opr) requestParams.topic0_2_opr = params.topic0_2_opr;
if (params.topic0_3_opr) requestParams.topic0_3_opr = params.topic0_3_opr;
if (params.topic1_2_opr) requestParams.topic1_2_opr = params.topic1_2_opr;
if (params.topic1_3_opr) requestParams.topic1_3_opr = params.topic1_3_opr;
if (params.topic2_3_opr) requestParams.topic2_3_opr = params.topic2_3_opr;
const result = await this.makeRequest<RawLog[]>('logs', 'getLogs', requestParams);
return result.map((log) => ({
address: log.address as Address,
topics: log.topics,
data: log.data,
blockNumber: parseInt(log.blockNumber, 16) || 0,
blockHash: log.blockHash as TxHash,
timeStamp: parseInt(log.timeStamp, 16) || 0,
gasPrice: log.gasPrice,
gasUsed: log.gasUsed,
logIndex: parseInt(log.logIndex, 16) || 0,
transactionHash: log.transactionHash as TxHash,
transactionIndex: parseInt(log.transactionIndex, 16) || 0
}));
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to get logs: ${error.message}`);
}
throw error;
}
}
/**
* Get network-wide statistics
*
* @returns Network statistics and metrics
* @throws {Error} If request fails
*
* @example
* ```typescript
* const stats = await service.getNetworkStats();
* console.log(`ETH Supply: ${stats.ethSupply}`);
* console.log(`ETH Price: $${stats.ethPrice}`);
* ```
*/
async getNetworkStats(): Promise<NetworkStats> {
try {
interface RawStats {
EthSupply: string;
Eth2Staking?: string;
BurntFees?: string;
EthPrice: string;
EthBtcPrice: string;
MarketCap: string;
}
const result = await this.makeRequest<RawStats>('stats', 'chainsize', {});
return {
ethSupply: result.EthSupply,
eth2Staking: result.Eth2Staking,
burntFees: result.BurntFees,
ethPrice: result.EthPrice,
ethBtcPrice: result.EthBtcPrice,
marketCap: result.MarketCap
};
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to get network stats: ${error.message}`);
}
throw error;
}
}
/**
* Get daily transaction count for a date range
*
* @param startDate - Start date (YYYY-MM-DD)
* @param endDate - End date (YYYY-MM-DD)
* @param sort - Sort order
* @returns Array of daily statistics
* @throws {Error} If request fails
*
* @example
* ```typescript
* const dailyTxs = await service.getDailyTxCount('2024-01-01', '2024-01-31', 'desc');
* dailyTxs.forEach(day => {
* console.log(`${day.date}: ${day.value} transactions`);
* });
* ```
*/
async getDailyTxCount(
startDate: string,
endDate: string,
sort: 'asc' | 'desc' = 'desc'
): Promise<DailyStats[]> {
try {
interface RawDailyStats {
unixTimeStamp: string;
transactionCount: string;
}
const result = await this.makeRequest<RawDailyStats[]>('stats', 'dailytx', {
startdate: startDate,
enddate: endDate,
sort
});
return result.map((day) => {
const timestamp = parseInt(day.unixTimeStamp) || 0;
const date = new Date(timestamp * 1000).toISOString().split('T')[0];
return {
date,
value: day.transactionCount
};
});
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to get daily transaction count: ${error.message}`);
}
throw error;
}
}
/**
* Get the current chain ID
*
* @returns Chain ID number
*
* @example
* ```typescript
* const chainId = service.getChainId();
* console.log(`Connected to chain ${chainId}`);
* ```
*/
getChainId(): number {
return this.chainId;
}
/**
* Set a new chain ID (switches the network)
*
* @param chainId - New chain ID
* @throws {Error} If chain ID is invalid
*
* @example
* ```typescript
* service.setChainId(137); // Switch to Polygon
* ```
*/
setChainId(chainId: number): void {
this.chainId = chainId;
this.requestBuilder.setChainId(chainId);
}
}
// Export additional types for consumers
export type {
Transaction, TokenTransfer, GasPrice, MinedBlock,
InternalTransaction, ContractSourceCode, VerifiedContract,
BlockReward, BlockDetails, BeaconWithdrawal, TokenInfo,
TokenHolder, TokenBalance, EventLog, NetworkStats, DailyStats,
Address, TxHash
};
// Re-export PaginationOptions and LogsParams (defined above in this file)