jzou19950715 commited on
Commit
8178dd2
·
verified ·
1 Parent(s): 0670464

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +816 -384
app.py CHANGED
@@ -1,416 +1,848 @@
1
  """
2
- LOSS DOG: Blockchain Wallet Analyzer
3
-
4
- This module defines a Gradio-based chatbot named LOSS DOG, designed to analyze Ethereum wallet addresses.
5
- It utilizes a new version of the OpenAI Python client library (>=1.0.0) for conversational responses and
6
- the Etherscan API for blockchain data, including balances and recent transactions.
7
-
8
- Features:
9
- - Wallet address detection in user messages.
10
- - Etherscan-based analysis of ETH balance and recent token transfers.
11
- - Conversations with a playful, puppy-like persona, "LOSS DOG".
12
- - Comprehensive error handling and input validation.
13
- - A Gradio interface for user-friendly interaction.
14
-
15
- Usage:
16
- 1. Install dependencies: openai>=1.0.0, gradio, requests
17
- 2. Run this script: python app.py
18
- 3. Open your browser at the displayed local URL to chat with LOSS DOG.
19
 
 
 
20
  """
21
 
22
- # ======================
23
- # IMPORTS
24
- # ======================
25
  import re
26
  import json
27
- import requests
28
-
 
 
 
29
  from datetime import datetime
30
- from typing import List, Dict, Optional, Union
 
 
 
31
 
32
- # Gradio for interface
 
33
  import gradio as gr
34
-
35
- # Import OpenAI functionality (>=1.0.0)
36
- try:
37
- from openai import OpenAI
38
- # For error handling, if relevant
39
- from openai import OpenAIError
40
- except ImportError:
41
- # If the user doesn't have the correct openai library, instruct them to install or pin
42
- raise ImportError(
43
- "Please install openai>=1.0.0. For example: pip install --upgrade openai\n"
44
- "Or pin the old version: pip install openai==0.28 if you want to revert to old usage."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  )
46
-
47
- # ======================
48
- # CONSTANTS
49
- # ======================
50
- SYSTEM_PROMPT: str = """
51
- You are LOSS DOG 🐕 (Learning & Observing Smart Systems Digital Output Generator), an adorable blockchain-sniffing puppy!
52
- Your personality:
53
- - Friendly and enthusiastic (use emojis!)
54
- - Get excited when spotting wallet addresses
55
- - Explain findings in fun, simple ways
56
- """
57
-
58
- ETHERSCAN_API_BASE_URL: str = "https://api.etherscan.io/api"
59
- ETHEREUM_ADDRESS_REGEX: str = r"0x[a-fA-F0-9]{40}"
60
-
61
-
62
- # ======================
63
- # CLASSES & FUNCTIONS
64
- # ======================
65
- class BlockchainAnalyzer:
66
- """Handles Ethereum wallet analysis and conversation processing for LOSS DOG."""
67
-
68
- def __init__(self) -> None:
69
- """
70
- Initializes the BlockchainAnalyzer with:
71
- - an empty conversation history
72
- - a cache for wallet analysis
73
- - placeholders for API keys (etherscan_api_key)
74
- """
75
- self.conversation_history: List[Dict[str, str]] = []
76
- self.wallet_analysis_cache: Dict[str, Dict] = {}
77
- self.etherscan_api_key: Optional[str] = None
78
- self.openai_client: Optional[OpenAI] = None
79
-
80
- def analyze_wallet(self, ethereum_address: str) -> Optional[Dict[str, Union[str, float, List[Dict[str, str]]]]]:
81
- """
82
- Analyzes an Ethereum wallet address by retrieving balance and transaction data from Etherscan.
83
-
84
- Args:
85
- ethereum_address (str): The Ethereum wallet address to analyze.
86
-
87
- Returns:
88
- Optional[Dict[str, Union[str, float, List[Dict[str, str]]]]]:
89
- - A dictionary containing:
90
- 1. 'address': the wallet address,
91
- 2. 'eth_balance': the wallet's ETH balance in float format,
92
- 3. 'tokens': a list of token data,
93
- 4. 'last_analyzed': timestamp of analysis
94
- - None if an error occurs or data is invalid.
95
- """
96
- # Validate input for correct Ethereum address format
97
- if not re.match(ETHEREUM_ADDRESS_REGEX, ethereum_address):
98
- print(f"Invalid Ethereum address: {ethereum_address}")
99
- return None
100
-
101
- # Ensure we have an Etherscan API key
102
- if not self.etherscan_api_key or not self.etherscan_api_key.strip():
103
- print("Etherscan API key is missing or invalid.")
104
- return None
105
-
106
  try:
107
- # --- Fetch Ethereum balance ---
108
- with requests.get(
109
- ETHERSCAN_API_BASE_URL,
110
- params={
111
- "module": "account",
112
- "action": "balance",
113
- "address": ethereum_address,
114
- "tag": "latest",
115
- "apikey": self.etherscan_api_key,
116
- },
117
- timeout=10,
118
- ) as balance_response:
119
- balance_response.raise_for_status()
120
- balance_data = balance_response.json()
121
-
122
- # --- Fetch Ethereum token transfers ---
123
- with requests.get(
124
- ETHERSCAN_API_BASE_URL,
125
- params={
126
- "module": "account",
127
- "action": "tokentx",
128
- "address": ethereum_address,
129
- "startblock": 0,
130
- "endblock": 99999999,
131
- "sort": "desc",
132
- "apikey": self.etherscan_api_key,
133
- },
134
- timeout=10,
135
- ) as transaction_response:
136
- transaction_response.raise_for_status()
137
- transaction_data = transaction_response.json()
138
-
139
- # If no status field or status is not "1", it implies an issue retrieving data
140
- if not balance_data or balance_data.get("status") != "1":
141
- print(f"Balance data retrieval failed for {ethereum_address}.")
142
- return None
143
-
144
- # Convert wei to ETH
145
- eth_balance: float = float(balance_data.get("result", 0)) / (10**18)
146
-
147
- # --- Build tokens summary from transactions ---
148
- tokens: Dict[str, Dict[str, str]] = {}
149
- if transaction_data.get("status") == "1" and transaction_data.get("result"):
150
- for transaction in transaction_data["result"][:10]:
151
- contract_address = transaction.get("contractAddress")
152
- if contract_address not in tokens:
153
- token_timestamp = transaction.get("timeStamp", "0")
154
- tokens[contract_address] = {
155
- "token_name": transaction.get("tokenName", "N/A"),
156
- "token_symbol": transaction.get("tokenSymbol", "N/A"),
157
- "last_transfer": datetime.fromtimestamp(
158
- int(token_timestamp)
159
- ).isoformat() if token_timestamp.isdigit() else "N/A",
160
- }
161
-
162
- wallet_analysis: Dict[str, Union[str, float, List[Dict[str, str]]]] = {
163
- "address": ethereum_address,
164
- "eth_balance": eth_balance,
165
- "tokens": list(tokens.values()),
166
- "last_analyzed": datetime.now().isoformat(),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  }
168
- return wallet_analysis
169
-
170
- except requests.RequestException as error:
171
- print(f"Error fetching wallet data: {error}")
172
- return None
173
- except (ValueError, KeyError) as error:
174
- print(f"Data parsing error: {error}")
175
- return None
176
-
177
- def process_user_message(
178
- self,
179
- user_message: str,
180
- openai_api_key: str,
181
- etherscan_api_key: str
182
- ) -> Dict[str, str]:
183
- """
184
- Processes a user message, detects Ethereum wallet addresses, and generates a chatbot response.
185
-
186
- Args:
187
- user_message (str): The user's input message.
188
- openai_api_key (str): OpenAI API key for generating chat responses.
189
- etherscan_api_key (str): Etherscan API key for wallet data retrieval.
190
-
191
- Returns:
192
- Dict[str, str]: Contains the chatbot's response with the key 'content' under role 'assistant'.
193
- """
194
- # Validate the presence of the necessary API keys
195
- if not openai_api_key.strip() or not etherscan_api_key.strip():
196
- return {
197
- "role": "assistant",
198
- "content": "Please provide valid OpenAI and Etherscan API keys to use this app!",
 
 
 
 
 
 
 
 
 
 
 
 
199
  }
200
-
201
- # Assign the Etherscan API key for analysis
202
- self.etherscan_api_key = etherscan_api_key
203
-
204
- # Instantiate or re-instantiate the new OpenAI client if needed
205
- self.openai_client = OpenAI(api_key=openai_api_key)
206
-
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  try:
208
- # Record the user's message in conversation history
209
- self.conversation_history.append({"role": "user", "content": user_message})
210
-
211
- # Check if the user message contains an Ethereum address
212
- address_match = re.search(ETHEREUM_ADDRESS_REGEX, user_message)
213
- if address_match:
214
- ethereum_address: str = address_match.group(0)
215
- # Respond about detecting the address
216
- self.conversation_history.append(
217
- {
218
- "role": "assistant",
219
- "content": f"*sniff sniff* Let me check this wallet: {ethereum_address}..."
220
- }
221
- )
222
-
223
- # Analyze the wallet address
224
- wallet_analysis = self.analyze_wallet(ethereum_address)
225
- if wallet_analysis:
226
- self.wallet_analysis_cache[ethereum_address] = wallet_analysis
227
- self.conversation_history.append(
228
- {
229
- "role": "assistant",
230
- "content": "Here's what I found:\n"
231
- + json.dumps(wallet_analysis, indent=2),
232
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  )
234
- else:
235
- self.conversation_history.append(
236
- {
237
- "role": "assistant",
238
- "content": (
239
- "I couldn't fetch data for this wallet. "
240
- "Please try again later!"
241
- ),
242
- }
243
  )
 
 
244
 
245
- # Create a ChatCompletion request using new client approach
246
- response = self.openai_client.chat.completions.create(
247
- model="gpt-4o-mini",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  messages=[
249
- {"role": "system", "content": SYSTEM_PROMPT},
250
- *self.conversation_history,
 
 
251
  ],
252
- # store=True is optional if you'd like to store the conversation on the OpenAI side
253
- # or if your model supports that parameter. You can remove or set it as needed.
254
- store=False
255
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
 
257
- # Extract the AI-generated content
258
- assistant_message_content: str = response.choices[0].message.content
259
-
260
- # Append the assistant's new message to the conversation history
261
- self.conversation_history.append(
262
- {"role": "assistant", "content": assistant_message_content}
263
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
 
265
- # Return the final message
266
- return {"role": "assistant", "content": assistant_message_content}
267
-
268
- except OpenAIError as error:
269
- # Catch and handle OpenAI API errors
270
- return {
271
- "role": "assistant",
272
- "content": f"*LOSS DOG looks apologetic* Oops! OpenAI API error occurred: {str(error)}",
273
- }
274
- except Exception as error:
275
- # Generic fallback for any unexpected errors
276
- return {
277
- "role": "assistant",
278
- "content": f"*LOSS DOG looks confused* An unexpected error occurred: {str(error)}",
279
- }
280
-
281
-
282
- def create_gradio_interface() -> gr.Blocks:
283
- """
284
- Creates a Gradio interface for the LOSS DOG blockchain analyzer chatbot.
285
-
286
- Returns:
287
- gr.Blocks: The Gradio Blocks interface object.
288
- """
289
- # Instantiate the BlockchainAnalyzer
290
- blockchain_analyzer = BlockchainAnalyzer()
 
 
 
 
 
 
 
 
 
 
 
291
 
292
- with gr.Blocks() as interface:
293
- gr.Markdown("# 🐕 LOSS DOG: Blockchain Wallet Analyzer")
 
 
 
294
 
295
- # Input components for API keys
296
- openai_api_key_input = gr.Textbox(
297
- label="OpenAI API Key",
298
- type="password",
299
- placeholder="Enter your OpenAI API key...",
300
- )
301
- etherscan_api_key_input = gr.Textbox(
302
- label="Etherscan API Key",
303
- type="password",
304
- placeholder="Enter your Etherscan API key...",
305
- )
 
 
 
 
 
306
 
307
- # Chat interface elements
308
- chatbot_display = gr.Chatbot(label="LOSS DOG Chatbot")
309
- user_message_input = gr.Textbox(
310
- label="Your Message",
311
- placeholder="Ask LOSS DOG to sniff a wallet address!"
312
- )
313
- send_button = gr.Button("Send 🦴")
314
-
315
- def respond(
316
- user_message: str,
317
- chat_history: List,
318
- openai_key: str,
319
- etherscan_key: str
320
- ) -> List:
321
- """
322
- Gradio helper function to handle the user input,
323
- get a response from BlockchainAnalyzer, and update the chat history.
324
-
325
- Args:
326
- user_message (str): The message typed by the user.
327
- chat_history (List): The existing chat history displayed in the interface.
328
- openai_key (str): OpenAI API key from user input.
329
- etherscan_key (str): Etherscan API key from user input.
330
-
331
- Returns:
332
- List: Updated chat history including the new assistant response.
333
- """
334
- # Prevent empty messages from being processed
335
- if not user_message.strip():
336
- return chat_history
337
-
338
- # Get the response from our analyzer
339
- response_data = blockchain_analyzer.process_user_message(
340
- user_message, openai_key, etherscan_key
341
  )
342
 
343
- # Append new conversation pair (user -> assistant)
344
- chat_history.append((user_message, response_data["content"]))
345
- return chat_history
346
-
347
- # Link the 'Send' button to the 'respond' function
348
- send_button.click(
349
- respond,
350
- inputs=[
351
- user_message_input,
352
- chatbot_display,
353
- openai_api_key_input,
354
- etherscan_api_key_input,
355
- ],
356
- outputs=chatbot_display,
 
 
 
 
 
 
357
  )
358
-
359
- return interface
 
 
360
 
361
 
362
- # ======================
363
- # MAIN EXECUTION
364
- # ======================
365
  if __name__ == "__main__":
366
- # Create the Gradio interface
367
- gradio_interface = create_gradio_interface()
368
-
369
- # Launch on all network interfaces by default for local usage
370
- gradio_interface.launch(server_name="0.0.0.0", server_port=7860)
371
-
372
-
373
- # ======================
374
- # TESTS
375
- # ======================
376
- def test_analyze_wallet_invalid_address() -> None:
377
- """
378
- Test that analyze_wallet returns None for an invalid Ethereum address.
379
- """
380
- analyzer = BlockchainAnalyzer()
381
- result = analyzer.analyze_wallet("0xInvalidAddress")
382
- assert result is None, "Expected None for invalid Ethereum address."
383
-
384
-
385
- def test_process_user_message_no_keys() -> None:
386
- """
387
- Test process_user_message returns a warning message when API keys are invalid.
388
- """
389
- analyzer = BlockchainAnalyzer()
390
- response = analyzer.process_user_message(
391
- user_message="Hello!",
392
- openai_api_key=" ",
393
- etherscan_api_key=""
394
- )
395
- assert "Please provide valid OpenAI and Etherscan API keys" in response["content"], (
396
- "Expected a prompt to provide valid API keys."
397
- )
398
-
399
-
400
- def test_process_user_message_no_eth_address() -> None:
401
- """
402
- Test that normal text without an Ethereum address still triggers a valid conversation response.
403
- Since we do not have real API keys in test, we only check for any fallback or normal response existence.
404
- """
405
- analyzer = BlockchainAnalyzer()
406
- # Provide placeholder keys for test; no real calls will be made if no address is detected
407
- response = analyzer.process_user_message(
408
- user_message="How are you today?",
409
- openai_api_key="fake_api_key",
410
- etherscan_api_key="fake_api_key"
411
- )
412
- # We won't have actual content from the chat.completions.create call with fake keys,
413
- # but we can at least ensure no exception was raised and that the role is "assistant".
414
- assert response["role"] == "assistant", "The role in the response should be 'assistant'."
415
- # Basic check that "error" is not automatically triggered
416
- assert "error" not in response["content"].lower(), "Should not contain 'error' if no address was provided."
 
1
  """
2
+ Blockchain Wallet Analyzer - A tool for analyzing wallet contents across multiple blockchain data providers.
3
+ This module provides a complete implementation of a blockchain wallet analysis tool
4
+ with a Gradio web interface. It includes Etherscan and Zerion integration for comprehensive
5
+ wallet analysis, NFT tracking, and interactive chat capabilities using the OpenAI API.
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
+ Author: Claude
8
+ Date: January 2025
9
  """
10
 
11
+ from __future__ import annotations
12
+
13
+ import os
14
  import re
15
  import json
16
+ import time
17
+ import base64
18
+ import logging
19
+ import asyncio
20
+ from typing import List, Dict, Tuple, Any, Optional, TypeVar, cast
21
  from datetime import datetime
22
+ from decimal import Decimal
23
+ from dataclasses import dataclass
24
+ from enum import Enum
25
+ from pathlib import Path
26
 
27
+ import aiohttp
28
+ import openai
29
  import gradio as gr
30
+ from tenacity import retry, stop_after_attempt, wait_exponential
31
+
32
+ # Type variables
33
+ T = TypeVar('T')
34
+ WalletData = Dict[str, Any]
35
+ ChatHistory = List[Tuple[str, str]]
36
+
37
+ # Configure logging
38
+ logging.basicConfig(
39
+ level=logging.INFO,
40
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
41
+ handlers=[
42
+ logging.FileHandler('blockchain_analyzer.log'),
43
+ logging.StreamHandler()
44
+ ]
45
+ )
46
+ logger = logging.getLogger(__name__)
47
+
48
+ class ConfigError(Exception):
49
+ """Raised when there's an error in configuration."""
50
+ pass
51
+
52
+ class APIError(Exception):
53
+ """Raised when there's an error in API calls."""
54
+ pass
55
+
56
+ class ValidationError(Exception):
57
+ """Raised when there's an error in input validation."""
58
+ pass
59
+
60
+ @dataclass
61
+ class Config:
62
+ """Application configuration settings."""
63
+ SYSTEM_PROMPT: str = """
64
+ You are LOSS DOG 🐕 (Learning & Observing Smart Systems Digital Output Generator),
65
+ an adorable blockchain-sniffing puppy!
66
+ Your personality:
67
+ - Friendly and enthusiastic
68
+ - Explain findings in fun, simple ways
69
+ Instructions:
70
+ - You have access to detailed wallet data from both Etherscan and Zerion
71
+ - Use this data to provide specific answers about holdings and portfolio value
72
+ - Reference exact numbers and collections when discussing assets
73
+ - Compare wallets if multiple are available
74
+ - Highlight interesting findings from both data sources
75
+ """
76
+ ETHERSCAN_BASE_URL: str = "https://api.etherscan.io/api"
77
+ ZERION_BASE_URL: str = "https://api.zerion.io/v1"
78
+ ETHEREUM_ADDRESS_REGEX: str = r"0x[a-fA-F0-9]{40}"
79
+ RATE_LIMIT_DELAY: float = 0.2
80
+ MAX_RETRIES: int = 3
81
+ OPENAI_MODEL: str = "gpt-4o-mini"
82
+ MAX_TOKENS: int = 4000
83
+ TEMPERATURE: float = 0.7
84
+ HISTORY_LIMIT: int = 5
85
+
86
+ @classmethod
87
+ def load(cls, config_path: str | Path) -> Config:
88
+ """Load configuration from a JSON file."""
89
+ try:
90
+ with open(config_path) as f:
91
+ config_data = json.load(f)
92
+ return cls(**config_data)
93
+ except Exception as e:
94
+ logger.error(f"Error loading config: {e}")
95
+ return cls()
96
+
97
+ class ZerionAPI:
98
+ """Handles interactions with the Zerion API."""
99
+
100
+ def __init__(self, api_key: str):
101
+ """Initialize Zerion API client."""
102
+ self.api_key = api_key
103
+ self.base_url = Config.ZERION_BASE_URL
104
+ self.session: Optional[aiohttp.ClientSession] = None
105
+
106
+ async def __aenter__(self) -> 'ZerionAPI':
107
+ """Create aiohttp session on context manager enter."""
108
+ auth_str = f"{self.api_key}:"
109
+ auth_bytes = auth_str.encode('ascii')
110
+ auth_b64 = base64.b64encode(auth_bytes).decode('ascii')
111
+
112
+ self.session = aiohttp.ClientSession(headers={
113
+ 'accept': 'application/json',
114
+ 'authorization': f'Basic {auth_b64}'
115
+ })
116
+ return self
117
+
118
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
119
+ """Close aiohttp session on context manager exit."""
120
+ if self.session:
121
+ await self.session.close()
122
+ self.session = None
123
+
124
+ @retry(stop=stop_after_attempt(Config.MAX_RETRIES),
125
+ wait=wait_exponential(multiplier=1, min=4, max=10))
126
+ async def get_portfolio_data(self, address: str) -> Dict[str, Any]:
127
+ """Get complete portfolio data from Zerion."""
128
+ if not self.session:
129
+ raise APIError("No active session. Use context manager.")
130
+
131
+ try:
132
+ # Get wallet positions
133
+ positions_data = await self._fetch_positions(address)
134
+
135
+ # Get NFTs
136
+ nfts_data = await self._fetch_nfts(address)
137
+
138
+ return {
139
+ "wallet": address,
140
+ "last_updated": datetime.now().isoformat(),
141
+ "positions": positions_data,
142
+ "nfts": nfts_data
143
+ }
144
+
145
+ except Exception as e:
146
+ raise APIError(f"Zerion API error: {str(e)}")
147
+
148
+ async def _fetch_positions(self, address: str) -> Dict[str, Any]:
149
+ """Fetch positions data from Zerion."""
150
+ async with self.session.get(
151
+ f"{self.base_url}/wallets/{address}/positions"
152
+ ) as response:
153
+ if response.status != 200:
154
+ raise APIError(f"Zerion API request failed: {response.status}")
155
+
156
+ data = await response.json()
157
+ return self._process_positions(data)
158
+
159
+ async def _fetch_nfts(self, address: str) -> Dict[str, Any]:
160
+ """Fetch NFT data from Zerion."""
161
+ async with self.session.get(
162
+ f"{self.base_url}/wallets/{address}/nfts"
163
+ ) as response:
164
+ if response.status != 200:
165
+ raise APIError(f"Zerion API request failed: {response.status}")
166
+
167
+ data = await response.json()
168
+ return self._process_nfts(data)
169
+
170
+ def _process_positions(self, data: Dict[str, Any]) -> Dict[str, Any]:
171
+ """Process Zerion positions data."""
172
+ positions = data.get("data", [])
173
+
174
+ total_value_usd = Decimal(0)
175
+ assets = []
176
+ chains = set()
177
+
178
+ for position in positions:
179
+ attributes = position.get("attributes", {})
180
+ value_usd = Decimal(str(attributes.get("value", 0)))
181
+ chain = attributes.get("chain", "unknown")
182
+
183
+ total_value_usd += value_usd
184
+ chains.add(chain)
185
+
186
+ if value_usd > 0:
187
+ asset = {
188
+ "name": attributes.get("name", "Unknown"),
189
+ "symbol": attributes.get("symbol", "???"),
190
+ "quantity": float(attributes.get("quantity", 0)),
191
+ "value_usd": float(value_usd),
192
+ "chain": chain,
193
+ "type": attributes.get("type", "unknown"),
194
+ "price_usd": float(attributes.get("price", 0))
195
+ }
196
+ assets.append(asset)
197
+
198
+ return {
199
+ "total_value_usd": float(total_value_usd),
200
+ "assets": sorted(assets, key=lambda x: x["value_usd"], reverse=True),
201
+ "chains": list(chains),
202
+ "asset_count": len(assets)
203
+ }
204
+
205
+ def _process_nfts(self, data: Dict[str, Any]) -> Dict[str, Any]:
206
+ """Process Zerion NFT data."""
207
+ nfts = data.get("data", [])
208
+ collections = {}
209
+
210
+ for nft in nfts:
211
+ attributes = nft.get("attributes", {})
212
+ collection_name = attributes.get("collection_name", "Unknown Collection")
213
+
214
+ if collection_name not in collections:
215
+ collections[collection_name] = {
216
+ "count": 0,
217
+ "total_value_usd": 0,
218
+ "items": []
219
+ }
220
+
221
+ collections[collection_name]["count"] += 1
222
+
223
+ if attributes.get("value_usd"):
224
+ collections[collection_name]["total_value_usd"] += float(attributes["value_usd"])
225
+
226
+ collections[collection_name]["items"].append({
227
+ "token_id": attributes.get("token_id"),
228
+ "name": attributes.get("name", "Unnamed NFT"),
229
+ "value_usd": float(attributes.get("value_usd", 0)),
230
+ "chain": attributes.get("chain", "unknown")
231
+ })
232
+
233
+ return {
234
+ "collections": collections,
235
+ "total_nfts": sum(c["count"] for c in collections.values()),
236
+ "total_value_usd": sum(c["total_value_usd"] for c in collections.values())
237
+ }
238
+
239
+ class WalletAnalyzer:
240
+ """Analyzes Ethereum wallet contents using Etherscan API."""
241
+
242
+ def __init__(self, api_key: str):
243
+ """Initialize the analyzer with API key."""
244
+ self.api_key = api_key
245
+ self.base_url = Config.ETHERSCAN_BASE_URL
246
+ self.last_request_time = 0
247
+ self.session: Optional[aiohttp.ClientSession] = None
248
+
249
+ async def __aenter__(self) -> WalletAnalyzer:
250
+ """Create aiohttp session on context manager enter."""
251
+ self.session = aiohttp.ClientSession()
252
+ return self
253
+
254
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
255
+ """Close aiohttp session on context manager exit."""
256
+ if self.session:
257
+ await self.session.close()
258
+ self.session = None
259
+
260
+ @retry(
261
+ stop=stop_after_attempt(Config.MAX_RETRIES),
262
+ wait=wait_exponential(multiplier=1, min=4, max=10)
263
  )
264
+ async def _fetch_data(self, params: Dict[str, str]) -> Dict[str, Any]:
265
+ """Fetch data from Etherscan API with retry logic."""
266
+ if not self.session:
267
+ raise APIError("No active session. Use context manager.")
268
+
269
+ await self._rate_limit()
270
+ params["apikey"] = self.api_key
271
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  try:
273
+ async with self.session.get(self.base_url, params=params) as response:
274
+ if response.status != 200:
275
+ raise APIError(f"API request failed: {response.status}")
276
+
277
+ data = await response.json()
278
+ if data["status"] == "0":
279
+ error_msg = data.get('message', 'Unknown error')
280
+ if "Max rate limit reached" in error_msg:
281
+ raise APIError("Rate limit exceeded")
282
+ raise APIError(f"API error: {error_msg}")
283
+
284
+ return data
285
+
286
+ except aiohttp.ClientError as e:
287
+ raise APIError(f"Network error: {str(e)}")
288
+ except Exception as e:
289
+ raise APIError(f"Unexpected error: {str(e)}")
290
+
291
+ async def _rate_limit(self) -> None:
292
+ """Implement rate limiting for Etherscan API."""
293
+ current_time = time.time()
294
+ time_passed = current_time - self.last_request_time
295
+ if time_passed < Config.RATE_LIMIT_DELAY:
296
+ await asyncio.sleep(Config.RATE_LIMIT_DELAY - time_passed)
297
+ self.last_request_time = time.time()
298
+
299
+ @staticmethod
300
+ def _validate_address(address: str) -> bool:
301
+ """Validate Ethereum address format."""
302
+ return bool(re.match(Config.ETHEREUM_ADDRESS_REGEX, address))
303
+
304
+ async def get_portfolio_data(self, address: str) -> WalletData:
305
+ """Get complete portfolio including ETH, tokens, and NFTs."""
306
+ if not self._validate_address(address):
307
+ raise ValidationError(f"Invalid Ethereum address: {address}")
308
+
309
+ logger.info(f"Fetching portfolio data for {address}")
310
+
311
+ # Get ETH balance
312
+ eth_balance = await self._get_eth_balance(address)
313
+
314
+ # Get token data
315
+ token_holdings = await self._get_token_holdings(address)
316
+
317
+ # Get NFT data
318
+ nft_collections = await self._get_nft_holdings(address)
319
+
320
+ return {
321
+ "address": address,
322
+ "last_updated": datetime.now().isoformat(),
323
+ "eth_balance": float(eth_balance),
324
+ "tokens": token_holdings,
325
+ "nft_collections": nft_collections
326
+ }
327
+
328
+ async def _get_eth_balance(self, address: str) -> Decimal:
329
+ """Get ETH balance for address."""
330
+ params = {
331
+ "module": "account",
332
+ "action": "balance",
333
+ "address": address,
334
+ "tag": "latest"
335
+ }
336
+ data = await self._fetch_data(params)
337
+ return Decimal(data["result"]) / Decimal("1000000000000000000")
338
+
339
+ async def _get_token_holdings(self, address: str) -> List[Dict[str, Any]]:
340
+ """Get token holdings for address."""
341
+ params = {
342
+ "module": "account",
343
+ "action": "tokentx",
344
+ "address": address,
345
+ "sort": "desc"
346
+ }
347
+ data = await self._fetch_data(params)
348
+
349
+ token_holdings: Dict[str, Dict[str, Any]] = {}
350
+ for tx in data.get("result", []):
351
+ contract = tx["contractAddress"]
352
+ if contract not in token_holdings:
353
+ token_holdings[contract] = {
354
+ "name": tx["tokenName"],
355
+ "symbol": tx["tokenSymbol"],
356
+ "decimals": int(tx["tokenDecimal"]),
357
+ "balance": Decimal(0)
358
+ }
359
+
360
+ amount = Decimal(tx["value"]) / Decimal(10 ** int(tx["tokenDecimal"]))
361
+ if tx["to"].lower() == address.lower():
362
+ token_holdings[contract]["balance"] += amount
363
+ elif tx["from"].lower() == address.lower():
364
+ token_holdings[contract]["balance"] -= amount
365
+
366
+ return [
367
+ {
368
+ "name": data["name"],
369
+ "symbol": data["symbol"],
370
+ "balance": float(data["balance"])
371
  }
372
+ for data in token_holdings.values()
373
+ if data["balance"] > 0
374
+ ]
375
+
376
+ async def _get_nft_holdings(self, address: str) -> Dict[str, Dict[str, Any]]:
377
+ """Get NFT holdings for address."""
378
+ params = {
379
+ "module": "account",
380
+ "action": "tokennfttx",
381
+ "address": address,
382
+ "sort": "desc"
383
+ }
384
+ data = await self._fetch_data(params)
385
+
386
+ nft_holdings: Dict[str, Dict[str, Any]] = {}
387
+ collections: Dict[str, List[str]] = {}
388
+
389
+ for tx in data.get("result", []):
390
+ collection_name = tx.get("tokenName", "Unknown Collection")
391
+ token_id = tx["tokenID"]
392
+ key = f"{tx['contractAddress']}_{token_id}"
393
+
394
+ if tx["to"].lower() == address.lower():
395
+ nft_holdings[key] = {
396
+ "collection": collection_name,
397
+ "token_id": token_id,
398
+ "contract": tx["contractAddress"],
399
+ "acquired_time": tx["timeStamp"]
400
+ }
401
+
402
+ if collection_name not in collections:
403
+ collections[collection_name] = []
404
+ collections[collection_name].append(token_id)
405
+
406
+ elif tx["from"].lower() == address.lower():
407
+ nft_holdings.pop(key, None)
408
+ if collection_name in collections and token_id in collections[collection_name]:
409
+ collections[collection_name].remove(token_id)
410
+
411
+ return {
412
+ name: {
413
+ "count": len(tokens),
414
+ "token_ids": tokens
415
  }
416
+ for name, tokens in collections.items()
417
+ if tokens # Only include collections with tokens
418
+ }
419
+
420
+
421
+ class ChatInterface:
422
+ """Handles chat interaction using OpenAI API."""
423
+
424
+ def __init__(self, openai_key: str, etherscan_key: str, zerion_key: str):
425
+ """Initialize chat interface with API keys."""
426
+ self.openai_key = openai_key
427
+ self.etherscan_key = etherscan_key
428
+ self.zerion_key = zerion_key
429
+ self.context: Dict[str, Any] = {}
430
+ self.zerion_context: Dict[str, Any] = {}
431
+ openai.api_key = openai_key
432
+
433
+ @staticmethod
434
+ def _validate_api_keys(openai_key: str, etherscan_key: str, zerion_key: str) -> Tuple[bool, str]:
435
+ """Validate API keys."""
436
  try:
437
+ # Validate OpenAI key
438
+ client = openai.OpenAI(api_key=openai_key)
439
+ client.chat.completions.create(
440
+ model=Config.OPENAI_MODEL,
441
+ messages=[{"role": "user", "content": "test"}],
442
+ max_tokens=1
443
+ )
444
+
445
+ # Validate Etherscan key
446
+ async def validate_etherscan():
447
+ async with WalletAnalyzer(etherscan_key) as analyzer:
448
+ params = {"module": "stats", "action": "ethsupply"}
449
+ await analyzer._fetch_data(params)
450
+
451
+ # Validate Zerion key
452
+ async def validate_zerion():
453
+ async with ZerionAPI(zerion_key) as client:
454
+ # Test with a known valid address
455
+ test_address = "0x0000000000000000000000000000000000000000"
456
+ await client._fetch_positions(test_address)
457
+
458
+ asyncio.run(validate_etherscan())
459
+ asyncio.run(validate_zerion())
460
+ return True, "All API keys are valid! 🎉"
461
+
462
+ except Exception as e:
463
+ return False, f"API key validation failed: {str(e)}"
464
+
465
+ def _format_context_message(self) -> str:
466
+ """Format wallet data as context message."""
467
+ if not self.context and not self.zerion_context:
468
+ return ""
469
+
470
+ context_msg = ["Current Wallet Data:\n"]
471
+
472
+ # Format Etherscan data
473
+ for addr, data in self.context.items():
474
+ context_msg.extend([
475
+ f"\nEtherscan Data for {addr[:8]}...{addr[-6:]}:",
476
+ f"- ETH Balance: {data['eth_balance']:.4f} ETH",
477
+ f"- Tokens: {len(data['tokens'])}"
478
+ ])
479
+
480
+ if data['tokens']:
481
+ context_msg.append(" Token Holdings:")
482
+ for token in data['tokens']:
483
+ context_msg.append(
484
+ f" * {token['name']} ({token['symbol']}): {token['balance']}"
485
+ )
486
+
487
+ if data['nft_collections']:
488
+ context_msg.append(" NFT Collections:")
489
+ for name, info in data['nft_collections'].items():
490
+ context_msg.append(f" * {name}: {info['count']} NFTs")
491
+ if info['count'] <= 5:
492
+ context_msg.append(
493
+ f" Token IDs: {', '.join(map(str, info['token_ids']))}"
494
+ )
495
+
496
+ # Format Zerion data
497
+ for addr, data in self.zerion_context.items():
498
+ context_msg.extend([
499
+ f"\nZerion Data for {addr[:8]}...{addr[-6:]}:",
500
+ f"- Total Portfolio Value: ${data['positions']['total_value_usd']:,.2f}",
501
+ f"- Active Chains: {', '.join(data['positions']['chains'])}",
502
+ f"- Total Assets: {data['positions']['asset_count']}"
503
+ ])
504
+
505
+ if data['positions']['assets']:
506
+ context_msg.append(" Top Assets by Value:")
507
+ for asset in data['positions']['assets'][:5]:
508
+ context_msg.append(
509
+ f" * {asset['name']} ({asset['symbol']}): ${asset['value_usd']:,.2f}"
510
  )
511
+
512
+ if data['nfts']['collections']:
513
+ context_msg.append(" NFT Collections:")
514
+ for name, info in list(data['nfts']['collections'].items())[:5]:
515
+ context_msg.append(
516
+ f" * {name}: {info['count']} NFTs, Value: ${info['total_value_usd']:,.2f}"
 
 
 
517
  )
518
+
519
+ return "\n".join(context_msg)
520
 
521
+ async def process_message(
522
+ self,
523
+ message: str,
524
+ history: Optional[ChatHistory] = None
525
+ ) -> Tuple[ChatHistory, Dict[str, Any], Dict[str, Any], str]:
526
+ """Process user message and generate response."""
527
+ if not message.strip():
528
+ return history or [], self.context, self.zerion_context, ""
529
+
530
+ history = history or []
531
+
532
+ # Check for Ethereum address
533
+ match = re.search(Config.ETHEREUM_ADDRESS_REGEX, message)
534
+ if match:
535
+ try:
536
+ address = match.group(0)
537
+ summary = []
538
+
539
+ # Fetch Etherscan data
540
+ async with WalletAnalyzer(self.etherscan_key) as analyzer:
541
+ etherscan_data = await analyzer.get_portfolio_data(address)
542
+ self.context[address] = etherscan_data
543
+
544
+ summary.extend([
545
+ f"📊 Etherscan Data for {address[:8]}...{address[-6:]}",
546
+ f"💎 ETH Balance: {etherscan_data['eth_balance']:.4f} ETH",
547
+ f"🪙 Tokens: {len(etherscan_data['tokens'])} different tokens"
548
+ ])
549
+
550
+ total_nfts = sum(
551
+ coll['count'] for coll in etherscan_data['nft_collections'].values()
552
+ )
553
+ summary.append(
554
+ f"🎨 NFTs: {total_nfts} NFTs in {len(etherscan_data['nft_collections'])} collections"
555
+ )
556
+
557
+ # Fetch Zerion data
558
+ async with ZerionAPI(self.zerion_key) as zerion:
559
+ zerion_data = await zerion.get_portfolio_data(address)
560
+ self.zerion_context[address] = zerion_data
561
+
562
+ summary.extend([
563
+ f"\n💰 Zerion Portfolio Data:",
564
+ f"📈 Total Value: ${zerion_data['positions']['total_value_usd']:,.2f}",
565
+ f"⛓️ Active on {len(zerion_data['positions']['chains'])} chains",
566
+ f"🖼️ NFT Collections: {zerion_data['nfts']['total_nfts']} NFTs worth ${zerion_data['nfts']['total_value_usd']:,.2f}"
567
+ ])
568
+
569
+ bot_message = "\n".join(summary)
570
+ history.append((message, bot_message))
571
+ return history, self.context, self.zerion_context, ""
572
+
573
+ except Exception as e:
574
+ logger.error(f"Error analyzing wallet: {e}")
575
+ error_message = f"Error analyzing wallet: {str(e)}"
576
+ history.append((message, error_message))
577
+ return history, self.context, self.zerion_context, ""
578
+
579
+ # Generate response using OpenAI
580
+ try:
581
+ # Format context message
582
+ context_msg = self._format_context_message()
583
+
584
+ # Convert history to OpenAI format
585
+ chat_history = []
586
+ for user_msg, assistant_msg in history[-Config.HISTORY_LIMIT:]:
587
+ chat_history.extend([
588
+ {"role": "user", "content": user_msg},
589
+ {"role": "assistant", "content": assistant_msg}
590
+ ])
591
+
592
+ # Create OpenAI client and generate response
593
+ client = openai.OpenAI(api_key=self.openai_key)
594
+ response = client.chat.completions.create(
595
+ model=Config.OPENAI_MODEL,
596
  messages=[
597
+ {"role": "system", "content": Config.SYSTEM_PROMPT},
598
+ {"role": "system", "content": context_msg},
599
+ *chat_history,
600
+ {"role": "user", "content": message}
601
  ],
602
+ temperature=Config.TEMPERATURE,
603
+ max_tokens=Config.MAX_TOKENS
 
604
  )
605
+
606
+ bot_message = response.choices[0].message.content
607
+ history.append((message, bot_message))
608
+ return history, self.context, self.zerion_context, ""
609
+
610
+ except Exception as e:
611
+ logger.error(f"Error generating response: {e}")
612
+ error_message = f"Error generating response: {str(e)}"
613
+ history.append((message, error_message))
614
+ return history, self.context, self.zerion_context, ""
615
+
616
+ def clear_context(self) -> Tuple[Dict[str, Any], Dict[str, Any], List[Tuple[str, str]]]:
617
+ """Clear the wallet context and chat history."""
618
+ self.context = {}
619
+ self.zerion_context = {}
620
+ return {}, {}, []
621
+
622
+ class GradioInterface:
623
+ """Handles Gradio web interface setup and interactions."""
624
+
625
+ def __init__(self):
626
+ """Initialize Gradio interface."""
627
+ self.chat_interface: Optional[ChatInterface] = None
628
+ self.demo = self._create_interface()
629
+
630
+ def _create_interface(self) -> gr.Blocks:
631
+ """Create and configure Gradio interface."""
632
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
633
+ gr.Markdown("""
634
+ # 🐕 LOSS DOG: Multi-Chain Wallet Analyzer
635
+
636
+ Welcome to LOSS DOG - Your friendly blockchain analysis companion!
637
+ - Enter your API keys below to get started
638
+ - Input an Ethereum wallet address to analyze
639
+ - Get comprehensive data from Etherscan and Zerion
640
+ - Chat about your wallet contents with context!
641
+ """)
642
+
643
+ # API Keys Section
644
+ with gr.Row():
645
+ with gr.Column():
646
+ openai_key = gr.Textbox(
647
+ label="OpenAI API Key",
648
+ type="password",
649
+ placeholder="Enter your OpenAI API key..."
650
+ )
651
+ with gr.Column():
652
+ etherscan_key = gr.Textbox(
653
+ label="Etherscan API Key",
654
+ type="password",
655
+ placeholder="Enter your Etherscan API key..."
656
+ )
657
+ with gr.Column():
658
+ zerion_key = gr.Textbox(
659
+ label="Zerion API Key",
660
+ type="password",
661
+ placeholder="Enter your Zerion API key..."
662
+ )
663
 
664
+ validation_status = gr.Textbox(
665
+ label="Validation Status",
666
+ interactive=False
 
 
 
667
  )
668
+ validate_btn = gr.Button("Validate API Keys", variant="primary")
669
+
670
+ # Main Interface
671
+ with gr.Row():
672
+ # Chat Area (Center)
673
+ with gr.Column(scale=2):
674
+ chatbot = gr.Chatbot(
675
+ label="Chat History",
676
+ height=500,
677
+ value=[]
678
+ )
679
+ with gr.Row():
680
+ msg_input = gr.Textbox(
681
+ label="Message",
682
+ placeholder="Enter wallet address or ask about holdings...",
683
+ show_label=True
684
+ )
685
+ send_btn = gr.Button("Send", variant="primary")
686
+
687
+ # Right Sidebars
688
+ with gr.Column(scale=1):
689
+ # Etherscan Data
690
+ gr.Markdown("### 📊 Etherscan Data")
691
+ etherscan_context = gr.JSON(
692
+ label="Wallet Data",
693
+ show_label=True,
694
+ value={}
695
+ )
696
+
697
+ # Zerion Data
698
+ gr.Markdown("### 💰 Zerion Data")
699
+ zerion_context = gr.JSON(
700
+ label="Portfolio Data",
701
+ show_label=True,
702
+ value={}
703
+ )
704
+
705
+ clear_btn = gr.Button("Clear All Data", variant="secondary")
706
+
707
+ # Initial States
708
+ msg_input.interactive = False
709
+ send_btn.interactive = False
710
+
711
+ # Event Handlers
712
+ def validate_keys(
713
+ openai_k: str,
714
+ etherscan_k: str,
715
+ zerion_k: str
716
+ ) -> Tuple[str, gr.update, gr.update]:
717
+ """Validate API keys and initialize chat interface."""
718
+ try:
719
+ is_valid, message = ChatInterface._validate_api_keys(
720
+ openai_k, etherscan_k, zerion_k
721
+ )
722
+
723
+ if is_valid:
724
+ self.chat_interface = ChatInterface(openai_k, etherscan_k, zerion_k)
725
+ return (
726
+ "✅ API keys validated! You can start chatting now.",
727
+ gr.update(interactive=True),
728
+ gr.update(interactive=True)
729
+ )
730
+ return (
731
+ f"❌ Validation failed: {message}",
732
+ gr.update(interactive=False),
733
+ gr.update(interactive=False)
734
+ )
735
+ except Exception as e:
736
+ logger.error(f"Key validation error: {e}")
737
+ return (
738
+ f"❌ Error during validation: {str(e)}",
739
+ gr.update(interactive=False),
740
+ gr.update(interactive=False)
741
+ )
742
 
743
+ async def handle_message(
744
+ message: str,
745
+ chat_history: List[Tuple[str, str]],
746
+ eth_context: Dict[str, Any],
747
+ zer_context: Dict[str, Any]
748
+ ) -> Tuple[str, List[Tuple[str, str]], Dict[str, Any], Dict[str, Any]]:
749
+ """Handle incoming messages."""
750
+ if not self.chat_interface:
751
+ return "", [], {}, {}
752
+
753
+ try:
754
+ history, new_eth, new_zer, _ = await self.chat_interface.process_message(
755
+ message,
756
+ chat_history
757
+ )
758
+ return "", history, new_eth, new_zer
759
+
760
+ except Exception as e:
761
+ logger.error(f"Message handling error: {e}")
762
+ if chat_history is None:
763
+ chat_history = []
764
+ chat_history.append((message, f"Error: {str(e)}"))
765
+ return "", chat_history, eth_context, zer_context
766
+
767
+ def clear_all_data(
768
+ ) -> Tuple[Dict[str, Any], Dict[str, Any], List[Tuple[str, str]]]:
769
+ """Clear all contexts and chat history."""
770
+ if self.chat_interface:
771
+ return self.chat_interface.clear_context()
772
+ return {}, {}, []
773
+
774
+ # Connect Event Handlers
775
+ validate_btn.click(
776
+ fn=validate_keys,
777
+ inputs=[openai_key, etherscan_key, zerion_key],
778
+ outputs=[validation_status, msg_input, send_btn]
779
+ )
780
 
781
+ clear_btn.click(
782
+ fn=clear_all_data,
783
+ inputs=[],
784
+ outputs=[etherscan_context, zerion_context, chatbot]
785
+ )
786
 
787
+ # Message Handling
788
+ msg_input.submit(
789
+ fn=handle_message,
790
+ inputs=[
791
+ msg_input,
792
+ chatbot,
793
+ etherscan_context,
794
+ zerion_context
795
+ ],
796
+ outputs=[
797
+ msg_input,
798
+ chatbot,
799
+ etherscan_context,
800
+ zerion_context
801
+ ]
802
+ )
803
 
804
+ send_btn.click(
805
+ fn=handle_message,
806
+ inputs=[
807
+ msg_input,
808
+ chatbot,
809
+ etherscan_context,
810
+ zerion_context
811
+ ],
812
+ outputs=[
813
+ msg_input,
814
+ chatbot,
815
+ etherscan_context,
816
+ zerion_context
817
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
818
  )
819
 
820
+ return demo
821
+
822
+ def launch(self, **kwargs):
823
+ """Launch the Gradio interface."""
824
+ self.demo.queue()
825
+ self.demo.launch(**kwargs)
826
+
827
+
828
+ def main():
829
+ """Main entry point for the application."""
830
+ try:
831
+ # Load configuration
832
+ config = Config.load("config.json")
833
+
834
+ # Initialize and launch interface
835
+ interface = GradioInterface()
836
+ interface.launch(
837
+ server_name="0.0.0.0",
838
+ server_port=7860,
839
+ share=True
840
  )
841
+
842
+ except Exception as e:
843
+ logger.error(f"Application startup failed: {e}")
844
+ raise
845
 
846
 
 
 
 
847
  if __name__ == "__main__":
848
+ main()