merge: chore/cleanup-remove-bloat-and-secrets into main
This commit is contained in:
commit
bde2f3a97d
1173 changed files with 437609 additions and 0 deletions
109
app/routers/address_profiler.py
Normal file
109
app/routers/address_profiler.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
#!/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
|
||||
Loading…
Add table
Add a link
Reference in a new issue