Dhanyakumar commited on
Commit
97623a6
·
verified ·
1 Parent(s): 4a8bd50

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +151 -476
app.py CHANGED
@@ -1,527 +1,202 @@
1
- """
2
- Blockchain Wallet Analyzer - A tool for analyzing Ethereum wallet contents and NFT holdings.
3
-
4
- This module provides a complete implementation of a blockchain wallet analysis tool
5
- with a Gradio web interface. It includes wallet analysis, NFT tracking, and
6
- interactive chat capabilities using the Groq API.
7
 
8
- Author: Mane
9
- Date: March 2025
10
- """
11
 
12
- from __future__ import annotations
 
 
 
 
13
 
14
- import os
15
- import re
16
- import json
17
- import time
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 gradio as gr
29
- from tenacity import retry, stop_after_attempt, wait_exponential
30
- import groq
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
- # Type variables
33
- T = TypeVar('T')
34
- WalletData = Dict[str, Any]
35
- ChatHistory = List[Tuple[str, str]]
 
 
 
 
 
36
 
37
- # API Keys - Define as constants in the script
38
- GROQ_API_KEY = "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"
39
- ETHERSCAN_API_KEY = "V562U3P76KKEUJ2DV9Q3U2EPDK89T5CKNH"
 
 
 
 
 
40
 
41
- # Configure logging
42
- logging.basicConfig(
43
- level=logging.INFO,
44
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
45
- handlers=[
46
- logging.FileHandler('blockchain_analyzer.log'),
47
- logging.StreamHandler()
48
- ]
49
- )
50
- logger = logging.getLogger(__name__)
51
 
52
- class ConfigError(Exception):
53
- """Raised when there's an error in configuration."""
54
- pass
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
- class APIError(Exception):
57
- """Raised when there's an error in API calls."""
58
- pass
59
 
60
- class ValidationError(Exception):
61
- """Raised when there's an error in input validation."""
62
- pass
63
 
64
- @dataclass
65
- class Config:
66
- """Application configuration settings."""
67
- SYSTEM_PROMPT: str = """
68
- You are MANE 👑 (Learning & Observing Smart Systems Digital Output Generator),
69
- an adorable blockchain-sniffing puppy!
70
- Your personality:
71
- - Friendly and enthusiastic
72
- - Explain findings in fun, simple ways
73
 
74
- Instructions:
75
- - You have access to detailed wallet data in your context
76
- - Use this data to provide specific answers about holdings
77
- - Reference exact numbers and collections when discussing NFTs
78
- - Compare wallets if multiple are available
79
- """
80
- ETHERSCAN_BASE_URL: str = "https://api.etherscan.io/api"
81
- ETHEREUM_ADDRESS_REGEX: str = r"0x[a-fA-F0-9]{40}"
82
- RATE_LIMIT_DELAY: float = 0.2 # 5 requests per second max for free tier
83
- MAX_RETRIES: int = 3
84
- GROQ_MODEL: str = "llama3-70b-8192" # Groq's high-performance model
85
- MAX_TOKENS: int = 4000
86
- TEMPERATURE: float = 0.7
87
- HISTORY_LIMIT: int = 5
88
 
89
- @classmethod
90
- def load(cls, config_path: str | Path) -> Config:
91
- """Load configuration from a JSON file."""
92
- try:
93
- with open(config_path) as f:
94
- config_data = json.load(f)
95
- return cls(**config_data)
96
- except Exception as e:
97
- logger.error(f"Error loading config: {e}")
98
- return cls()
99
 
100
  class WalletAnalyzer:
101
  """Analyzes Ethereum wallet contents using Etherscan API."""
102
 
103
- def __init__(self):
104
- """Initialize the analyzer with API key."""
105
- self.api_key = ETHERSCAN_API_KEY
106
- self.base_url = Config.ETHERSCAN_BASE_URL
107
- self.last_request_time = 0
108
- self.session: Optional[aiohttp.ClientSession] = None
109
-
110
- async def __aenter__(self) -> WalletAnalyzer:
111
- """Create aiohttp session on context manager enter."""
112
- self.session = aiohttp.ClientSession()
113
- return self
114
-
115
- async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
116
- """Close aiohttp session on context manager exit."""
117
- if self.session:
118
- await self.session.close()
119
- self.session = None
120
-
121
- @retry(
122
- stop=stop_after_attempt(Config.MAX_RETRIES),
123
- wait=wait_exponential(multiplier=1, min=4, max=10)
124
- )
125
- async def _fetch_data(self, params: Dict[str, str]) -> Dict[str, Any]:
126
- """Fetch data from Etherscan API with retry logic."""
127
- if not self.session:
128
- raise APIError("No active session. Use context manager.")
129
-
130
- await self._rate_limit()
131
- params["apikey"] = self.api_key
132
-
133
- try:
134
- async with self.session.get(self.base_url, params=params) as response:
135
- if response.status != 200:
136
- raise APIError(f"API request failed: {response.status}")
137
-
138
- data = await response.json()
139
- if data["status"] == "0":
140
- error_msg = data.get('message', 'Unknown error')
141
- if "Max rate limit reached" in error_msg:
142
- raise APIError("Rate limit exceeded")
143
- raise APIError(f"API error: {error_msg}")
144
-
145
- return data
146
-
147
- except aiohttp.ClientError as e:
148
- raise APIError(f"Network error: {str(e)}")
149
- except Exception as e:
150
- raise APIError(f"Unexpected error: {str(e)}")
151
-
152
- async def _rate_limit(self) -> None:
153
- """Implement rate limiting for Etherscan API."""
154
- current_time = time.time()
155
- time_passed = current_time - self.last_request_time
156
- if time_passed < Config.RATE_LIMIT_DELAY:
157
- await asyncio.sleep(Config.RATE_LIMIT_DELAY - time_passed)
158
- self.last_request_time = time.time()
159
 
160
  @staticmethod
161
  def _validate_address(address: str) -> bool:
162
- """Validate Ethereum address format."""
163
- return bool(re.match(Config.ETHEREUM_ADDRESS_REGEX, address))
164
-
165
- async def get_portfolio_data(self, address: str) -> WalletData:
166
- """Get complete portfolio including ETH, tokens, and NFTs."""
167
- if not self._validate_address(address):
168
- raise ValidationError(f"Invalid Ethereum address: {address}")
169
-
170
- logger.info(f"Fetching portfolio data for {address}")
171
-
172
- # Get ETH balance
173
- eth_balance = await self._get_eth_balance(address)
174
-
175
- # Get token data
176
- token_holdings = await self._get_token_holdings(address)
177
-
178
- # Get NFT data
179
- nft_collections = await self._get_nft_holdings(address)
180
-
181
- return {
182
- "address": address,
183
- "last_updated": datetime.now().isoformat(),
184
- "eth_balance": float(eth_balance),
185
- "tokens": token_holdings,
186
- "nft_collections": nft_collections
187
- }
188
-
189
- async def _get_eth_balance(self, address: str) -> Decimal:
190
- """Get ETH balance for address."""
191
  params = {
192
  "module": "account",
193
  "action": "balance",
194
  "address": address,
195
  "tag": "latest"
196
  }
197
- data = await self._fetch_data(params)
198
- return Decimal(data["result"]) / Decimal("1000000000000000000")
199
 
200
  async def _get_token_holdings(self, address: str) -> List[Dict[str, Any]]:
201
- """Get token holdings for address."""
202
  params = {
203
  "module": "account",
204
  "action": "tokentx",
205
  "address": address,
 
 
 
 
206
  "sort": "desc"
207
  }
208
- data = await self._fetch_data(params)
209
-
210
- token_holdings: Dict[str, Dict[str, Any]] = {}
211
- for tx in data.get("result", []):
212
- contract = tx["contractAddress"]
213
- if contract not in token_holdings:
214
- token_holdings[contract] = {
215
- "name": tx["tokenName"],
216
- "symbol": tx["tokenSymbol"],
217
- "decimals": int(tx["tokenDecimal"]),
218
- "balance": Decimal(0)
219
- }
220
-
221
- amount = Decimal(tx["value"]) / Decimal(10 ** int(tx["tokenDecimal"]))
222
- if tx["to"].lower() == address.lower():
223
- token_holdings[contract]["balance"] += amount
224
- elif tx["from"].lower() == address.lower():
225
- token_holdings[contract]["balance"] -= amount
226
-
227
- return [
228
- {
229
- "name": data["name"],
230
- "symbol": data["symbol"],
231
- "balance": float(data["balance"])
232
- }
233
- for data in token_holdings.values()
234
- if data["balance"] > 0
235
- ]
236
 
237
- async def _get_nft_holdings(self, address: str) -> Dict[str, Dict[str, Any]]:
238
- """Get NFT holdings for address."""
239
  params = {
240
  "module": "account",
241
  "action": "tokennfttx",
242
  "address": address,
 
 
 
 
243
  "sort": "desc"
244
  }
245
- data = await self._fetch_data(params)
 
 
 
 
 
 
 
 
 
 
246
 
247
- nft_holdings: Dict[str, Dict[str, Any]] = {}
248
- collections: Dict[str, List[str]] = {}
249
 
250
- for tx in data.get("result", []):
251
- collection_name = tx.get("tokenName", "Unknown Collection")
252
- token_id = tx["tokenID"]
253
- key = f"{tx['contractAddress']}_{token_id}"
254
-
255
- if tx["to"].lower() == address.lower():
256
- nft_holdings[key] = {
257
- "collection": collection_name,
258
- "token_id": token_id,
259
- "contract": tx["contractAddress"],
260
- "acquired_time": tx["timeStamp"]
261
- }
262
-
263
- if collection_name not in collections:
264
- collections[collection_name] = []
265
- collections[collection_name].append(token_id)
266
-
267
- elif tx["from"].lower() == address.lower():
268
- nft_holdings.pop(key, None)
269
- if collection_name in collections and token_id in collections[collection_name]:
270
- collections[collection_name].remove(token_id)
271
 
272
- return {
273
- name: {
274
- "count": len(tokens),
275
- "token_ids": tokens
276
- }
277
- for name, tokens in collections.items()
278
- if tokens # Only include collections with tokens
 
 
 
279
  }
280
-
281
-
282
- class ChatInterface:
283
- """Handles chat interaction using Groq API."""
284
-
285
- def __init__(self):
286
- """Initialize chat interface with Groq client."""
287
- self.groq_client = groq.Client(api_key=GROQ_API_KEY)
288
- self.context: Dict[str, Any] = {}
289
-
290
- def _format_context_message(self) -> str:
291
- """Format wallet data as context message."""
292
- if not self.context:
293
- return ""
294
-
295
- context_msg = ["Current Wallet Data:\n"]
296
- for addr, data in self.context.items():
297
- context_msg.extend([
298
- f"Wallet {addr[:8]}...{addr[-6:]}:",
299
- f"- ETH Balance: {data['eth_balance']:.4f} ETH",
300
- f"- Tokens: {len(data['tokens'])}"
301
- ])
302
-
303
- if data['tokens']:
304
- context_msg.append(" Token Holdings:")
305
- for token in data['tokens']:
306
- context_msg.append(
307
- f" * {token['name']} ({token['symbol']}): {token['balance']}"
308
- )
309
-
310
- if data['nft_collections']:
311
- context_msg.append(" NFT Collections:")
312
- for name, info in data['nft_collections'].items():
313
- context_msg.append(f" * {name}: {info['count']} NFTs")
314
- if info['count'] <= 5:
315
- context_msg.append(
316
- f" Token IDs: {', '.join(map(str, info['token_ids']))}"
317
- )
318
-
319
- return "\n".join(context_msg)
320
-
321
- async def process_message(
322
- self,
323
- message: str,
324
- history: Optional[ChatHistory] = None
325
- ) -> Tuple[ChatHistory, Dict[str, Any], str]:
326
- """Process user message and generate response."""
327
- if not message.strip():
328
- return history or [], self.context, ""
329
-
330
- history = history or []
331
 
332
- # Check for Ethereum address
333
- match = re.search(Config.ETHEREUM_ADDRESS_REGEX, message)
334
- if match:
335
- try:
336
- address = match.group(0)
337
- async with WalletAnalyzer() as analyzer:
338
- wallet_data = await analyzer.get_portfolio_data(address)
339
- self.context[address] = wallet_data
340
-
341
- summary = [
342
- f"📊 Portfolio Summary for {address[:8]}...{address[-6:]}",
343
- f"💎 ETH Balance: {wallet_data['eth_balance']:.4f} ETH",
344
- f"🪙 Tokens: {len(wallet_data['tokens'])} different tokens"
345
- ]
346
-
347
- total_nfts = sum(
348
- coll['count'] for coll in wallet_data['nft_collections'].values())
349
-
350
- summary.append(
351
- f"🎨 NFTs: {total_nfts} NFTs in {len(wallet_data['nft_collections'])} collections"
352
- )
353
-
354
- bot_message = "\n".join(summary)
355
- history.append((message, bot_message))
356
- return history, self.context, ""
357
-
358
- except Exception as e:
359
- logger.error(f"Error analyzing wallet: {e}")
360
- error_message = f"Error analyzing wallet: {str(e)}"
361
- history.append((message, error_message))
362
- return history, self.context, ""
363
-
364
- # Generate response using Groq
365
- try:
366
- # Format context message
367
- context_msg = self._format_context_message()
368
-
369
- # Convert history to Groq format
370
- chat_history = []
371
- for user_msg, assistant_msg in history[-Config.HISTORY_LIMIT:]:
372
- chat_history.extend([
373
- {"role": "user", "content": user_msg},
374
- {"role": "assistant", "content": assistant_msg}
375
- ])
376
-
377
- # Generate response using Groq
378
- response = self.groq_client.chat.completions.create(
379
- model=Config.GROQ_MODEL,
380
- messages=[
381
- {"role": "system", "content": Config.SYSTEM_PROMPT},
382
- {"role": "system", "content": context_msg},
383
- *chat_history,
384
- {"role": "user", "content": message}
385
- ],
386
- temperature=Config.TEMPERATURE,
387
- max_tokens=Config.MAX_TOKENS
388
- )
389
-
390
- bot_message = response.choices[0].message.content
391
- history.append((message, bot_message))
392
- return history, self.context, ""
393
-
394
- except Exception as e:
395
- logger.error(f"Error generating response: {e}")
396
- error_message = f"Error generating response: {str(e)}"
397
- history.append((message, error_message))
398
- return history, self.context, ""
399
-
400
- def clear_context(self) -> Tuple[Dict[str, Any], List[Tuple[str, str]]]:
401
- """Clear the wallet context and chat history."""
402
- self.context = {}
403
- return {}, []
404
-
405
-
406
- class GradioInterface:
407
- """Handles Gradio web interface setup and interactions."""
408
-
409
- def __init__(self):
410
- """Initialize Gradio interface."""
411
- self.chat_interface = ChatInterface()
412
- self.demo = self._create_interface()
413
-
414
- def _create_interface(self) -> gr.Blocks:
415
- """Create and configure Gradio interface."""
416
- with gr.Blocks(theme=gr.themes.Soft()) as demo:
417
- gr.Markdown("""
418
- # MANE👑: Blockchain Wallet Analyzer
419
-
420
- Welcome to 👑 MANE WAR - Your friendly blockchain analysis companion!
421
- - Input an Ethereum wallet address to analyze
422
- - Chat about your wallet contents with context!
423
- """)
424
-
425
- # Main Interface
426
- with gr.Row():
427
- # Chat Area (Left Side)
428
- with gr.Column(scale=2):
429
- chatbot = gr.Chatbot(
430
- label="Chat History",
431
- height=500,
432
- value=[]
433
- )
434
- with gr.Row():
435
- msg_input = gr.Textbox(
436
- label="Message",
437
- placeholder="Enter wallet address or ask about holdings...",
438
- show_label=True
439
- )
440
- send_btn = gr.Button("Send", variant="primary")
441
-
442
- # Context Sidebar (Right Side)
443
- with gr.Column(scale=1):
444
- wallet_context = gr.JSON(
445
- label="Active Wallet Context",
446
- show_label=True,
447
- value={}
448
- )
449
- clear_btn = gr.Button("Clear Context", variant="secondary")
450
-
451
- async def handle_message(
452
- message: str,
453
- chat_history: List[Tuple[str, str]],
454
- context: Dict[str, Any]
455
- ) -> Tuple[List[Tuple[str, str]], Dict[str, Any]]:
456
- """Handle incoming messages."""
457
- try:
458
- history, new_context, _ = await self.chat_interface.process_message(
459
- message,
460
- chat_history
461
- )
462
- return history, new_context
463
-
464
- except Exception as e:
465
- logger.error(f"Error handling message: {e}")
466
- if chat_history is None:
467
- chat_history = []
468
- chat_history.append((message, f"Error: {str(e)}"))
469
- return chat_history, context
470
-
471
- # Connect Event Handlers
472
- clear_btn.click(
473
- fn=self.chat_interface.clear_context,
474
- inputs=[],
475
- outputs=[wallet_context, chatbot]
476
- )
477
-
478
- # Message Handling
479
- msg_input.submit(
480
- fn=handle_message,
481
- inputs=[msg_input, chatbot, wallet_context],
482
- outputs=[chatbot, wallet_context]
483
- ).then(
484
- lambda: gr.update(value=""),
485
- None,
486
- [msg_input]
487
- )
488
-
489
- send_btn.click(
490
- fn=handle_message,
491
- inputs=[msg_input, chatbot, wallet_context],
492
- outputs=[chatbot, wallet_context]
493
- ).then(
494
- lambda: gr.update(value=""),
495
- None,
496
- [msg_input]
497
- )
498
-
499
- return demo
500
 
501
- def launch(self, **kwargs):
502
- """Launch the Gradio interface."""
503
- self.demo.queue()
504
- self.demo.launch(**kwargs)
505
 
 
506
 
507
- def main():
508
- """Main entry point for the application."""
509
- try:
510
- # Load configuration
511
- config = Config.load("config.json")
512
-
513
- # Initialize and launch interface
514
- interface = GradioInterface()
515
- interface.launch(
516
- server_name="0.0.0.0",
517
- server_port=7860,
518
- share=True
519
- )
520
-
521
- except Exception as e:
522
- logger.error(f"Application startup failed: {e}")
523
- raise
524
 
 
 
 
 
 
 
525
 
526
- if __name__ == "__main__":
527
- main()
 
1
+ import requests
2
+ from decimal import Decimal
3
+ from datetime import datetime, timedelta
4
+ from typing import List, Dict, Any
5
+ import logging
 
6
 
7
+ logger = logging.getLogger(__name__)
 
 
8
 
9
+ # Fraud Detection Constants
10
+ BLACKLISTED_ADDRESSES = ["0x0000000000000000000000000000000000000000", "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"] # Example blacklisted addresses
11
+ FRAUDULENT_THRESHOLD = 10 # Flag transactions where the value is higher than this threshold in ETH
12
+ TRANSACTION_FREQUENCY_LIMIT = 50 # Maximum number of transactions per day to be considered "normal"
13
+ TIME_WINDOW = timedelta(hours=24) # Time window for detecting high-frequency transactions (24 hours)
14
 
15
+ class ValidationError(Exception):
16
+ pass
 
 
 
 
 
 
 
 
 
 
17
 
18
+ class FraudDetection:
19
+ """Handles fraud detection by analyzing wallet transactions."""
20
+
21
+ @staticmethod
22
+ def detect_high_frequency_transactions(transactions: List[Dict[str, Any]]) -> bool:
23
+ """Detect high-frequency transactions within a short time period (e.g., 24 hours)."""
24
+ recent_transactions = []
25
+ current_time = datetime.now()
26
+ for tx in transactions:
27
+ timestamp = datetime.utcfromtimestamp(int(tx['timeStamp']))
28
+ if current_time - timestamp < TIME_WINDOW:
29
+ recent_transactions.append(tx)
30
+
31
+ if len(recent_transactions) > TRANSACTION_FREQUENCY_LIMIT:
32
+ return True # Flag as high-frequency fraud
33
+ return False
34
 
35
+ @staticmethod
36
+ def detect_large_token_transfers(transactions: List[Dict[str, Any]]) -> List[str]:
37
+ """Detect large transfers (fraudulent tokens above a threshold)."""
38
+ flagged_transactions = []
39
+ for tx in transactions:
40
+ amount = Decimal(tx["value"]) / Decimal(10 ** int(tx["tokenDecimal"]))
41
+ if amount > FRAUDULENT_THRESHOLD:
42
+ flagged_transactions.append(tx)
43
+ return flagged_transactions
44
 
45
+ @staticmethod
46
+ def detect_suspicious_addresses(transactions: List[Dict[str, Any]]) -> List[str]:
47
+ """Detect transactions involving blacklisted or suspicious addresses."""
48
+ flagged_addresses = []
49
+ for tx in transactions:
50
+ if tx["to"].lower() in BLACKLISTED_ADDRESSES or tx["from"].lower() in BLACKLISTED_ADDRESSES:
51
+ flagged_addresses.append(tx)
52
+ return flagged_addresses
53
 
54
+ @staticmethod
55
+ def detect_unusual_nft_sales(nft_transactions: List[Dict[str, Any]]) -> List[str]:
56
+ """Detect unusual NFT transfers that may indicate fraudulent activity (high-value sales)."""
57
+ flagged_nfts = []
58
+ for tx in nft_transactions:
59
+ if int(tx['value']) > FRAUDULENT_THRESHOLD: # Assuming `value` here is the token value
60
+ flagged_nfts.append(tx)
61
+ return flagged_nfts
 
 
62
 
63
+ @staticmethod
64
+ def run_fraud_detection(wallet_data: Dict[str, Any]) -> Dict[str, Any]:
65
+ """Run all fraud detection checks and return flagged issues."""
66
+ transactions = wallet_data.get("transactions", [])
67
+ nft_transactions = wallet_data.get("nft_collections", [])
68
+
69
+ fraud_report = {
70
+ "high_frequency_transactions": False,
71
+ "large_token_transfers": [],
72
+ "suspicious_addresses": [],
73
+ "unusual_nft_sales": []
74
+ }
75
+
76
+ # Detect high-frequency transactions
77
+ fraud_report["high_frequency_transactions"] = FraudDetection.detect_high_frequency_transactions(transactions)
78
 
79
+ # Detect large token transfers
80
+ fraud_report["large_token_transfers"] = FraudDetection.detect_large_token_transfers(transactions)
 
81
 
82
+ # Detect suspicious addresses
83
+ fraud_report["suspicious_addresses"] = FraudDetection.detect_suspicious_addresses(transactions)
 
84
 
85
+ # Detect unusual NFT sales
86
+ fraud_report["unusual_nft_sales"] = FraudDetection.detect_unusual_nft_sales(nft_transactions)
 
 
 
 
 
 
 
87
 
88
+ return fraud_report
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
 
 
 
 
 
 
 
 
 
 
90
 
91
  class WalletAnalyzer:
92
  """Analyzes Ethereum wallet contents using Etherscan API."""
93
 
94
+ API_KEY = "Your_Etherscan_API_Key"
95
+ ETHERSCAN_URL = "https://api.etherscan.io/api"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
  @staticmethod
98
  def _validate_address(address: str) -> bool:
99
+ """Validate Ethereum address."""
100
+ if len(address) != 42 or not address.startswith("0x"):
101
+ return False
102
+ return True
103
+
104
+ async def _fetch_data(self, params: dict) -> dict:
105
+ """Fetch data from Etherscan API."""
106
+ params["apikey"] = self.API_KEY
107
+ response = requests.get(self.ETHERSCAN_URL, params=params)
108
+ return response.json()
109
+
110
+ async def _get_eth_balance(self, address: str) -> str:
111
+ """Fetch ETH balance of the given address."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  params = {
113
  "module": "account",
114
  "action": "balance",
115
  "address": address,
116
  "tag": "latest"
117
  }
118
+ return await self._fetch_data(params)["result"]
 
119
 
120
  async def _get_token_holdings(self, address: str) -> List[Dict[str, Any]]:
121
+ """Fetch token holdings of the given address."""
122
  params = {
123
  "module": "account",
124
  "action": "tokentx",
125
  "address": address,
126
+ "startblock": 0,
127
+ "endblock": 99999999,
128
+ "page": 1,
129
+ "offset": 10,
130
  "sort": "desc"
131
  }
132
+ return await self._fetch_data(params)["result"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
+ async def _get_nft_holdings(self, address: str) -> List[Dict[str, Any]]:
135
+ """Fetch NFT holdings of the given address."""
136
  params = {
137
  "module": "account",
138
  "action": "tokennfttx",
139
  "address": address,
140
+ "startblock": 0,
141
+ "endblock": 99999999,
142
+ "page": 1,
143
+ "offset": 10,
144
  "sort": "desc"
145
  }
146
+ return await self._fetch_data(params)["result"]
147
+
148
+ async def get_portfolio_data(self, address: str) -> Dict[str, Any]:
149
+ """Get complete portfolio including ETH, tokens, and NFTs."""
150
+ if not self._validate_address(address):
151
+ raise ValidationError(f"Invalid Ethereum address: {address}")
152
+
153
+ logger.info(f"Fetching portfolio data for {address}")
154
+
155
+ # Get ETH balance
156
+ eth_balance = await self._get_eth_balance(address)
157
 
158
+ # Get token data
159
+ token_holdings = await self._get_token_holdings(address)
160
 
161
+ # Get NFT data
162
+ nft_collections = await self._get_nft_holdings(address)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
 
164
+ # Get transactions data for fraud detection
165
+ params = {
166
+ "module": "account",
167
+ "action": "txlist",
168
+ "address": address,
169
+ "startblock": 0,
170
+ "endblock": 99999999,
171
+ "page": 1,
172
+ "offset": 10,
173
+ "sort": "desc",
174
  }
175
+ transaction_data = await self._fetch_data(params)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
 
177
+ # Prepare wallet data
178
+ wallet_data = {
179
+ "address": address,
180
+ "last_updated": datetime.now().isoformat(),
181
+ "eth_balance": float(eth_balance),
182
+ "tokens": token_holdings,
183
+ "nft_collections": nft_collections,
184
+ "transactions": transaction_data.get("result", [])
185
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
 
187
+ # Run fraud detection
188
+ fraud_report = FraudDetection.run_fraud_detection(wallet_data)
189
+ wallet_data["fraud_report"] = fraud_report
 
190
 
191
+ return wallet_data
192
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
 
194
+ # Example usage:
195
+ async def main():
196
+ address = "0xYourEthereumAddressHere"
197
+ wallet_analyzer = WalletAnalyzer()
198
+ portfolio_data = await wallet_analyzer.get_portfolio_data(address)
199
+ print(portfolio_data)
200
 
201
+ # Run the main function
202
+ # asyncio.run(main()) # Uncomment this line if using in an async environment.