Spaces:
Sleeping
Sleeping
| import os | |
| import requests | |
| import json | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| class VertexWeb3Engine: | |
| def __init__(self, contract_address, network="ethereum"): | |
| self.address = contract_address.strip() | |
| self.network = network | |
| self.api_key = os.getenv("ETHERSCAN_API_KEY", "").strip() | |
| # Configuración V2 (Obligatoria en 2026) | |
| self.networks = { | |
| "ethereum": {"url": "https://api.etherscan.io/v2/api", "id": "1"}, | |
| "bsc": {"url": "https://api.bscscan.com/v2/api", "id": "56"}, | |
| "polygon": {"url": "https://api.polygonscan.com/v2/api", "id": "137"} | |
| } | |
| config = self.networks.get(network, self.networks["ethereum"]) | |
| self.base_url = config["url"] | |
| self.chain_id = config["id"] | |
| def get_contract_source(self): | |
| """Descarga el código fuente usando Etherscan API V2""" | |
| params = { | |
| "chainid": self.chain_id, # Requerido para V2 | |
| "module": "contract", | |
| "action": "getsourcecode", | |
| "address": self.address, | |
| "apikey": self.api_key | |
| } | |
| try: | |
| response = requests.get(self.base_url, params=params, timeout=15) | |
| data = response.json() | |
| # Verificamos status de Etherscan | |
| if data.get("status") == "1" and data.get("result"): | |
| result = data["result"][0] | |
| raw_source = result.get("SourceCode", "") | |
| if not raw_source: | |
| return {"success": False, "error": "Contract not verified"} | |
| # Parseo corregido para evitar el error de 'slice' | |
| source_code = self._parse_source_code(raw_source) | |
| return { | |
| "success": True, | |
| "source_code": source_code, | |
| "contract_info": { | |
| "name": result.get("ContractName", "Unknown"), | |
| "compiler": result.get("CompilerVersion", "Unknown") | |
| } | |
| } | |
| return {"success": False, "error": data.get("result", "API Error")} | |
| except Exception as e: | |
| return {"success": False, "error": str(e)} | |
| def _parse_source_code(self, raw_source): | |
| """Maneja formatos plano y multi-archivo (JSON)""" | |
| # Verificamos que sea un string antes de recortar | |
| if not isinstance(raw_source, str) or not raw_source.startswith("{"): | |
| return raw_source | |
| try: | |
| # FIX: Aseguramos que el recorte se haga solo si es un string de verdad | |
| if raw_source.startswith("{{") and raw_source.endswith("}}"): | |
| clean_json = raw_source[1:-1] # Quitamos solo una pareja de llaves | |
| else: | |
| clean_json = raw_source | |
| parsed = json.loads(clean_json) | |
| if "sources" in parsed: | |
| all_code = [] | |
| for filename, file_data in parsed["sources"].items(): | |
| if "content" in file_data: | |
| all_code.append(f"// FILE: {filename}\n{file_data['content']}") | |
| return "\n\n".join(all_code) | |
| return raw_source | |
| except: | |
| return raw_source | |
| def scan_basic_vulnerabilities(self, source_code): | |
| """Análisis de patrones de riesgo""" | |
| if not source_code or not isinstance(source_code, str): | |
| return [] | |
| red_flags = [] | |
| patterns = { | |
| "selfdestruct": "CRITICAL: Contract can be destroyed.", | |
| "mint(": "HIGH: Infinite minting risk.", | |
| "delegatecall": "HIGH: Proxy execution risk." | |
| } | |
| for pattern, risk in patterns.items(): | |
| if pattern in source_code.lower(): | |
| red_flags.append({"pattern": pattern, "description": risk}) | |
| return red_flags |