web3-copilot / src /tools /etherscan_tool.py
Priyanshi Saxena
feat: more features
c52b367
raw
history blame
6.81 kB
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