File size: 6,813 Bytes
f104fee
 
 
 
9b006e9
 
 
f104fee
 
 
 
 
 
 
ea46ec8
f104fee
 
 
 
 
 
 
9b006e9
 
 
 
f104fee
c52b367
9b006e9
 
f104fee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9b006e9
 
f104fee
 
9b006e9
 
 
 
 
f104fee
 
9b006e9
 
 
 
 
f104fee
 
9b006e9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f104fee
 
 
 
 
9b006e9
f104fee
 
9b006e9
f104fee
 
 
 
 
 
 
 
 
 
 
 
 
 
9b006e9
f104fee
9b006e9
 
 
 
 
 
f104fee
 
9b006e9
f104fee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9b006e9
f104fee
 
9b006e9
f104fee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
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