Spaces:
Sleeping
Sleeping
| 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 | |