#!/usr/bin/env python3 """#2 — Cross-Chain Address Profiler. Input one address, get activity across all chains. Shows portfolio, active chains, protocol usage, risk score. Uses DataBus + eth-labels.""" import asyncio import os from typing import Any import httpx from fastapi import APIRouter, Query from pydantic import BaseModel router = APIRouter(prefix="/api/v1/address-profiler", tags=["address-profiler"]) CHAINS = { "ethereum": "1", "solana": "solana", "bsc": "56", "base": "8453", "arbitrum": "42161", "polygon": "137", "avalanche": "43114", "optimism": "10", "fantom": "250", "linea": "59144", } DATABUS_BASE = os.environ.get("DATABUS_URL", "http://localhost:8000/api/v1/databus") ETH_LABELS_URL = os.environ.get("ETH_LABELS_URL", "http://localhost:8000/api/v1/eth-labels") class AddressProfile(BaseModel): address: str labels: list[dict[str, str]] = [] chains_activity: list[dict[str, Any]] = [] total_value_usd: float = 0 risk_score: int = 50 risk_factors: list[str] = [] async def _fetch_chain_balance(chain: str, chain_id: str, address: str) -> dict: """Fetch balance/activity for an address on one chain via DataBus.""" try: async with httpx.AsyncClient(timeout=8) as client: resp = await client.get(f"{DATABUS_BASE}/{chain}/balance/{address}") if resp.status_code == 200: return {"chain": chain, **resp.json()} except Exception: pass return {"chain": chain, "balance_usd": 0, "tx_count": 0, "error": "unavailable"} async def _fetch_eth_labels(address: str) -> list[dict]: """Look up address in eth-labels database.""" try: async with httpx.AsyncClient(timeout=5) as client: resp = await client.get(f"{ETH_LABELS_URL}/lookup/{address}") if resp.status_code == 200: return resp.json().get("labels", []) except Exception: pass return [] @router.get("/profile/{address}") async def get_address_profile(address: str, chains: str = Query(None)): """Get cross-chain profile for an address. Use ?chains=ethereum,solana to filter.""" chain_list = chains.split(",") if chains else list(CHAINS.keys()) # Fetch all chains in parallel tasks = [] for c in chain_list: if c in CHAINS: tasks.append(_fetch_chain_balance(c, CHAINS[c], address)) chain_results = await asyncio.gather(*tasks) # Fetch labels labels = await _fetch_eth_labels(address) # Compute profile chains_active = [r for r in chain_results if r.get("balance_usd", 0) > 0 or r.get("tx_count", 0) > 0] total_value = sum(r.get("balance_usd", 0) or 0 for r in chains_active) # Risk scoring (simple heuristic) risk_factors: list[str] = [] risk = 30 # lower = riskier for label in labels: lbl = label.get("label", "").lower() if any(w in lbl for w in ["scam", "phish", "hack", "exploit", "sanction", "mixer"]): risk_factors.append(f"Labeled: {label.get('label')}") risk = max(risk, 65) if len(chains_active) == 0: risk = max(risk, 50) risk_factors.append("No on-chain activity found") if len(chains_active) > 5: risk -= 10 # diverse activity = less risky profile = { "address": address, "labels": labels, "chains_active": len(chains_active), "chains_activity": chains_active, "total_value_usd": round(total_value, 2), "risk_score": min(100, max(0, risk)), "risk_factors": risk_factors, } return profile