rmi-backend/app/routers/address_profiler.py
opencode c762564d40 style(rmi-backend): complete lint cleanup — 1175→0 ruff errors
- Fix 71 invalid-syntax files (class-body newline-broken assignments)
- Add from/None chain to 307 B904 raise-without-from sites
- Add B008 ignore to ruff.toml (already in pyproject.toml)
- Noqa F401 on __init__.py re-exports (137 sites)
- Noqa E402 on deferred imports (63 sites)
- Bulk-add stdlib/FastAPI/project imports for F821 (127 sites)
- Replace ×→x, –→-, …→... in docstrings (4093 chars)
- Manual refactor of 5 SIM103/SIM116 patterns

Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py)
Co-authored-by: opencode <opencode@rugmunch.io>
2026-07-06 15:43:20 +02:00

109 lines
3.5 KiB
Python

#!/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