Spaces:
Running
Running
from typing import Dict, Any, Optional | |
from pydantic import BaseModel, PrivateAttr | |
from src.tools.base_tool import BaseWeb3Tool, Web3ToolInput | |
from src.utils.config import config | |
from src.utils.logger import get_logger | |
logger = get_logger(__name__) | |
class EtherscanTool(BaseWeb3Tool): | |
name: str = "etherscan_data" | |
description: str = """Get Ethereum blockchain data from Etherscan. | |
Useful for: transaction analysis, address information, gas prices, token data. | |
Input: Ethereum address, transaction hash, or general blockchain query.""" | |
args_schema: type[BaseModel] = Web3ToolInput | |
enabled: bool = True # Add enabled as a Pydantic field | |
_base_url: str = PrivateAttr(default="https://api.etherscan.io/api") | |
_api_key: Optional[str] = PrivateAttr(default=None) | |
def __init__(self): | |
super().__init__() | |
self._api_key = config.ETHERSCAN_API_KEY | |
self.enabled = bool(self._api_key) | |
if not self.enabled: | |
logger.warning("Etherscan API key not configured - limited functionality") | |
async def _arun(self, query: str, filters: Optional[Dict[str, Any]] = None, **kwargs) -> str: | |
if not self.enabled: | |
return "β οΈ **Etherscan Service Limited**\n\nEtherscan functionality requires an API key.\nGet yours free at: https://etherscan.io/apis\n\nSet environment variable: `ETHERSCAN_API_KEY=your_key`" | |
try: | |
filters = filters or {} | |
if filters.get("type") == "gas_prices": | |
return await self._get_gas_prices() | |
elif filters.get("type") == "eth_stats": | |
return await self._get_eth_stats() | |
elif self._is_address(query): | |
return await self._get_address_info(query) | |
elif self._is_tx_hash(query): | |
return await self._get_transaction_info(query) | |
else: | |
return await self._get_gas_prices() | |
except Exception as e: | |
logger.error(f"Etherscan error: {e}") | |
return f"β οΈ Etherscan service temporarily unavailable" | |
def _is_address(self, query: str) -> bool: | |
return ( | |
len(query) == 42 | |
and query.startswith("0x") | |
and all(c in "0123456789abcdefABCDEF" for c in query[2:]) | |
) | |
def _is_tx_hash(self, query: str) -> bool: | |
return ( | |
len(query) == 66 | |
and query.startswith("0x") | |
and all(c in "0123456789abcdefABCDEF" for c in query[2:]) | |
) | |
async def _get_gas_prices(self) -> str: | |
try: | |
params = { | |
"module": "gastracker", | |
"action": "gasoracle", | |
"apikey": self._api_key | |
} | |
data = await self.make_request(self._base_url, params) | |
if not data or data.get("status") != "1": | |
error_msg = data.get("message", "Unknown error") if data else "No response" | |
logger.warning(f"Etherscan gas price error: {error_msg}") | |
return "β οΈ Gas price data temporarily unavailable" | |
result_data = data.get("result", {}) | |
if not result_data: | |
return "β No gas price data in response" | |
safe_gas = result_data.get("SafeGasPrice", "N/A") | |
standard_gas = result_data.get("StandardGasPrice", "N/A") | |
fast_gas = result_data.get("FastGasPrice", "N/A") | |
# Validate gas prices are numeric | |
try: | |
if safe_gas != "N/A": | |
float(safe_gas) | |
if standard_gas != "N/A": | |
float(standard_gas) | |
if fast_gas != "N/A": | |
float(fast_gas) | |
except (ValueError, TypeError): | |
return "β οΈ Invalid gas price data received" | |
result = "β½ **Ethereum Gas Prices:**\n\n" | |
result += f"π **Safe**: {safe_gas} gwei\n" | |
result += f"β‘ **Standard**: {standard_gas} gwei\n" | |
result += f"π **Fast**: {fast_gas} gwei\n" | |
return result | |
except Exception as e: | |
logger.error(f"Gas prices error: {e}") | |
return "β οΈ Gas price service temporarily unavailable" | |
async def _get_eth_stats(self) -> str: | |
params = { | |
"module": "stats", | |
"action": "ethsupply", | |
"apikey": self._api_key | |
} | |
data = await self.make_request(self._base_url, params) | |
if data.get("status") != "1": | |
return "Ethereum stats unavailable" | |
eth_supply = int(data.get("result", 0)) / 1e18 | |
result = "π **Ethereum Network Stats:**\n\n" | |
result += f"π **ETH Supply**: {eth_supply:,.0f} ETH\n" | |
return result | |
async def _get_address_info(self, address: str) -> str: | |
params = { | |
"module": "account", | |
"action": "txlist", | |
"address": address, | |
"startblock": "0", | |
"endblock": "99999999", | |
"page": "1", | |
"offset": "10", | |
"sort": "desc", | |
"apikey": self._api_key | |
} | |
data = await self.make_request(self._base_url, params) | |
if data.get("status") != "1": | |
return f"Address information unavailable for {address}" | |
balance_wei = int(data.get("result", 0)) | |
balance_eth = balance_wei / 1e18 | |
result = f"π **Address Information:**\n\n" | |
result += f"**Address**: {address}\n" | |
result += f"π° **Balance**: {balance_eth:.4f} ETH\n" | |
return result | |
async def _get_transaction_info(self, tx_hash: str) -> str: | |
params = { | |
"module": "proxy", | |
"action": "eth_getTransactionByHash", | |
"txhash": tx_hash, | |
"apikey": self._api_key | |
} | |
data = await self.make_request(self._base_url, params) | |
if not data.get("result"): | |
return f"Transaction not found: {tx_hash}" | |
tx = data.get("result", {}) | |
value_wei = int(tx.get("value", "0x0"), 16) | |
value_eth = value_wei / 1e18 | |
gas_price = int(tx.get("gasPrice", "0x0"), 16) / 1e9 | |
result = f"π **Transaction Information:**\n\n" | |
result += f"**Hash**: {tx_hash}\n" | |
result += f"**From**: {tx.get('from', 'N/A')}\n" | |
result += f"**To**: {tx.get('to', 'N/A')}\n" | |
result += f"π° **Value**: {value_eth:.4f} ETH\n" | |
result += f"β½ **Gas Price**: {gas_price:.2f} gwei\n" | |
return result | |