- 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>
577 lines
22 KiB
Python
577 lines
22 KiB
Python
"""
|
|
All Connectors Router - Wires all 20+ unwired modules into API routes.
|
|
One file to rule them all.
|
|
"""
|
|
|
|
import logging
|
|
from datetime import UTC, datetime
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/api/v1", tags=["connectors"])
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# BIRDEYE - Token data, trending, OHLCV
|
|
# ═══════════════════════════════════════════════════════════
|
|
@router.get("/birdeye/token/{address}")
|
|
async def birdeye_token(address: str, chain: str = "solana"):
|
|
try:
|
|
from app.birdeye_client import BirdeyeClient
|
|
|
|
client = BirdeyeClient()
|
|
data = client.get_token_info(address)
|
|
return {"address": address, "chain": chain, "data": data, "source": "birdeye"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
@router.get("/birdeye/trending")
|
|
async def birdeye_trending(chain: str = "solana", limit: int = 20):
|
|
try:
|
|
from app.birdeye_client import BirdeyeClient
|
|
|
|
client = BirdeyeClient()
|
|
tokens = client.get_trending(limit=limit)
|
|
return {"tokens": tokens, "chain": chain, "source": "birdeye"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# ARKHAM INTELLIGENCE - Entity labeling, wallet attribution,
|
|
# institutional tracking, sanctions screening
|
|
# ═══════════════════════════════════════════════════════════
|
|
@router.get("/arkham/entity/{address}")
|
|
async def arkham_entity(address: str):
|
|
try:
|
|
from app.arkham_connector import ArkhamClient
|
|
|
|
client = ArkhamClient()
|
|
try:
|
|
data = await client.get_entity(address)
|
|
return {"address": address, "entity": data, "source": "arkham"}
|
|
finally:
|
|
await client.close()
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
@router.get("/arkham/labels")
|
|
async def arkham_labels(page: int = 0, limit: int = 100):
|
|
try:
|
|
from app.arkham_connector import ArkhamClient
|
|
|
|
client = ArkhamClient()
|
|
try:
|
|
data = await client.get_labels(page=page, limit=limit)
|
|
return {"labels": data, "page": page, "limit": limit, "source": "arkham"}
|
|
finally:
|
|
await client.close()
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
@router.get("/arkham/portfolio/{address}")
|
|
async def arkham_portfolio(address: str):
|
|
try:
|
|
from app.arkham_connector import ArkhamClient
|
|
|
|
client = ArkhamClient()
|
|
try:
|
|
data = await client.get_portfolio(address)
|
|
return {"address": address, "portfolio": data, "source": "arkham"}
|
|
finally:
|
|
await client.close()
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# BLOCKCHAIR - Multi-chain block explorer
|
|
# ═══════════════════════════════════════════════════════════
|
|
@router.get("/blockchair/balance/{address}")
|
|
async def blockchair_balance(address: str, chain: str = "bitcoin"):
|
|
try:
|
|
from app.blockchair_connector import get_address_balance
|
|
|
|
result = get_address_balance(address, chain)
|
|
return {"address": address, "chain": chain, "data": result, "source": "blockchair"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
@router.get("/blockchair/search")
|
|
async def blockchair_search(q: str):
|
|
try:
|
|
from app.blockchair_connector import search_blockchain
|
|
|
|
result = search_blockchain(q)
|
|
return {"query": q, "results": result, "source": "blockchair"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# DEFILLAMA - DeFi analytics
|
|
# ═══════════════════════════════════════════════════════════
|
|
@router.get("/defillama/tvl")
|
|
async def defillama_tvl():
|
|
try:
|
|
from app.defillama_connector import get_defi_tvl
|
|
|
|
result = get_defi_tvl()
|
|
return {"tvl": result, "source": "defillama"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
@router.get("/defillama/protocols")
|
|
async def defillama_protocols():
|
|
try:
|
|
from app.defillama_connector import get_defi_protocols
|
|
|
|
result = get_defi_protocols()
|
|
return {"protocols": result, "source": "defillama"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
@router.get("/defillama/chains")
|
|
async def defillama_chains():
|
|
try:
|
|
from app.defillama_connector import get_chain_tvls
|
|
|
|
result = get_chain_tvls()
|
|
return {"chains": result, "source": "defillama"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# ENTITY CLUSTERING - Wallet cluster analysis
|
|
# ═══════════════════════════════════════════════════════════
|
|
@router.get("/entity/clusters")
|
|
async def entity_clusters(address: str | None = None, min_size: int = 2):
|
|
try:
|
|
from app.entity_clustering import get_clustering_engine
|
|
|
|
engine = get_clustering_engine()
|
|
if address:
|
|
entity = engine.graph.get_entity(address)
|
|
return {"entity": entity, "address": address}
|
|
clusters = engine.graph.find_clusters(min_size=min_size)
|
|
return {"clusters": clusters, "total": len(clusters)}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
@router.post("/entity/link")
|
|
async def entity_link(data: dict):
|
|
try:
|
|
from app.entity_clustering import get_clustering_engine
|
|
|
|
engine = get_clustering_engine()
|
|
addr1 = data.get("address1", "")
|
|
addr2 = data.get("address2", "")
|
|
relationship = data.get("relationship", "related")
|
|
if not addr1 or not addr2:
|
|
raise HTTPException(status_code=400, detail="address1 and address2 required")
|
|
engine.graph.link_wallets(addr1, addr2, relationship)
|
|
return {
|
|
"status": "linked",
|
|
"address1": addr1,
|
|
"address2": addr2,
|
|
"relationship": relationship,
|
|
}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# THREAT INTEL - Sanctions, reputation, blocklists
|
|
# ═══════════════════════════════════════════════════════════
|
|
@router.get("/threat/reputation/{address}")
|
|
async def threat_reputation(address: str, chain: str = "ethereum"):
|
|
try:
|
|
from app.threat_intel import check_wallet_reputation
|
|
|
|
result = check_wallet_reputation(address, chain)
|
|
return {"address": address, "chain": chain, "reputation": result}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
@router.get("/threat/sanctions/{address}")
|
|
async def threat_sanctions(address: str):
|
|
try:
|
|
from app.threat_intel import check_sanctions
|
|
|
|
result = check_sanctions(address)
|
|
return {"address": address, "sanctions": result, "sanctioned": len(result) > 0}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
@router.post("/threat/blocklist")
|
|
async def threat_blocklist_add(data: dict):
|
|
try:
|
|
from app.threat_intel import add_to_blocklist
|
|
|
|
address = data.get("address", "")
|
|
reason = data.get("reason", "")
|
|
if not address:
|
|
raise HTTPException(status_code=400, detail="address required")
|
|
success = add_to_blocklist(address, reason)
|
|
return {"address": address, "added": success, "reason": reason}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# EXCHANGE FLOW - CEX inflows/outflows
|
|
# ═══════════════════════════════════════════════════════════
|
|
@router.get("/exchange/flow/{address}")
|
|
async def exchange_flow(address: str, chain: str = "ethereum"):
|
|
try:
|
|
from app.exchange_flow_analyzer import analyze_entity_flows
|
|
|
|
result = analyze_entity_flows(address, chain)
|
|
return {"address": address, "chain": chain, "flows": result}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
@router.get("/exchange/whales")
|
|
async def exchange_whales(chain: str = "ethereum"):
|
|
try:
|
|
from app.exchange_flow_analyzer import ExchangeFlowAnalyzer
|
|
|
|
analyzer = ExchangeFlowAnalyzer()
|
|
whale_movements = analyzer.detect_large_transfers(chain=chain, min_value_usd=1000000)
|
|
return {"whale_movements": whale_movements, "chain": chain}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# CROSS-CHAIN CORRELATOR
|
|
# ═══════════════════════════════════════════════════════════
|
|
@router.get("/crosschain/fingerprint/{address}")
|
|
async def crosschain_fingerprint(address: str, chains: str = "ethereum,base,bsc,polygon"):
|
|
try:
|
|
from app.cross_chain_correlator import CrossChainCorrelator
|
|
|
|
correlator = CrossChainCorrelator()
|
|
chain_list = [c.strip() for c in chains.split(",")]
|
|
results = {}
|
|
for chain in chain_list:
|
|
try:
|
|
fp = correlator.get_fingerprint(address, chain)
|
|
results[chain] = fp
|
|
except Exception:
|
|
results[chain] = {"error": f"Chain {chain} unavailable"}
|
|
return {"address": address, "fingerprints": results, "chains_checked": len(results)}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# AGENT MESH - 8 AI agents
|
|
# ═══════════════════════════════════════════════════════════
|
|
AGENTS = {
|
|
"nexus": {
|
|
"name": "NEXUS",
|
|
"role": "Strategic Coordinator",
|
|
"tier": "T0",
|
|
"triggers": ["strategize", "plan", "coordinate"],
|
|
},
|
|
"scout": {
|
|
"name": "SCOUT",
|
|
"role": "Alpha Hunter",
|
|
"tier": "T3",
|
|
"triggers": ["find", "scan", "hunt", "alpha"],
|
|
},
|
|
"tracer": {
|
|
"name": "TRACER",
|
|
"role": "Forensic Investigator",
|
|
"tier": "T1",
|
|
"triggers": ["trace", "investigate", "wallet"],
|
|
},
|
|
"cipher": {
|
|
"name": "CIPHER",
|
|
"role": "Contract Auditor",
|
|
"tier": "T1",
|
|
"triggers": ["audit", "security", "contract"],
|
|
},
|
|
"sentinel": {
|
|
"name": "SENTINEL",
|
|
"role": "Rug Detector",
|
|
"tier": "T2",
|
|
"triggers": ["monitor", "watch", "alert", "rug"],
|
|
},
|
|
"chronicler": {
|
|
"name": "CHRONICLER",
|
|
"role": "Investigative Reporter",
|
|
"tier": "T2",
|
|
"triggers": ["write", "document", "report"],
|
|
},
|
|
"forge": {
|
|
"name": "FORGE",
|
|
"role": "Implementation Architect",
|
|
"tier": "T1",
|
|
"triggers": ["code", "implement", "build"],
|
|
},
|
|
"relay": {
|
|
"name": "RELAY",
|
|
"role": "Communications Coordinator",
|
|
"tier": "T3",
|
|
"triggers": ["format", "relay", "dispatch"],
|
|
},
|
|
}
|
|
|
|
|
|
@router.get("/agents")
|
|
async def list_agents():
|
|
return {"agents": AGENTS, "total": len(AGENTS)}
|
|
|
|
|
|
@router.get("/agents/{agent_id}")
|
|
async def get_agent(agent_id: str):
|
|
agent = AGENTS.get(agent_id)
|
|
if not agent:
|
|
raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
|
|
return agent
|
|
|
|
|
|
@router.post("/agents/{agent_id}/command")
|
|
async def agent_command(agent_id: str, data: dict):
|
|
agent = AGENTS.get(agent_id)
|
|
if not agent:
|
|
raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
|
|
command = data.get("command", "")
|
|
return {
|
|
"agent": agent["name"],
|
|
"role": agent["role"],
|
|
"command": command,
|
|
"status": "queued",
|
|
"timestamp": datetime.now(UTC).isoformat(),
|
|
}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# MCP SERVERS - Multi-chain data gateways
|
|
# ═══════════════════════════════════════════════════════════
|
|
@router.get("/mcp/servers")
|
|
async def mcp_servers_list():
|
|
return {
|
|
"servers": {
|
|
"dexpaprika": "Real-time DEX data for 5M+ tokens across 20+ chains",
|
|
"solana": "Solana RPC - wallet balances, token prices, DeFi yields",
|
|
"dexscreener": "DEX pair data, token info, market stats",
|
|
"defillama": "DeFi TVL, protocols, yields, fees",
|
|
"coingecko": "13K+ tokens, global stats, historical data",
|
|
"helius": "Enhanced Solana RPC - parsed txs, webhooks",
|
|
"goplus": "Multi-chain token security - 700K+ tokens scanned",
|
|
"rugcheck": "Solana token safety audit",
|
|
},
|
|
"status": "operational",
|
|
}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# SENTIMENT - Crypto market sentiment analysis
|
|
# ═══════════════════════════════════════════════════════════
|
|
@router.get("/sentiment/market")
|
|
async def sentiment_market():
|
|
try:
|
|
from app.ml_anomaly import AnomalyDetector
|
|
|
|
detector = AnomalyDetector()
|
|
anomalies = detector.detect_market_anomalies()
|
|
return {"anomalies": anomalies, "timestamp": datetime.now(UTC).isoformat()}
|
|
except Exception:
|
|
# Fallback to fear & greed
|
|
import httpx
|
|
|
|
async with httpx.AsyncClient(timeout=8) as c:
|
|
r = await c.get("https://api.alternative.me/fng/?limit=1")
|
|
if r.status_code == 200:
|
|
data = r.json().get("data", [{}])[0]
|
|
return {
|
|
"sentiment": {
|
|
"fear_greed_index": int(data.get("value", 50)),
|
|
"classification": data.get("value_classification", "Neutral"),
|
|
},
|
|
"source": "alternative.me",
|
|
"timestamp": datetime.now(UTC).isoformat(),
|
|
}
|
|
return {"error": "Sentiment data unavailable"}
|
|
|
|
|
|
@router.get("/sentiment/token/{address}")
|
|
async def sentiment_token(address: str):
|
|
# On-chain sentiment from buy/sell ratio
|
|
try:
|
|
import httpx
|
|
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
r = await c.get(f"https://api.dexscreener.com/latest/dex/tokens/{address}")
|
|
if r.status_code == 200:
|
|
pairs = r.json().get("pairs", [])
|
|
if pairs:
|
|
p = pairs[0]
|
|
buys = p.get("txns", {}).get("h24", {}).get("buys", 0)
|
|
sells = p.get("txns", {}).get("h24", {}).get("sells", 0)
|
|
total = buys + sells
|
|
buy_ratio = buys / total if total > 0 else 0.5
|
|
sentiment = "bullish" if buy_ratio > 0.6 else ("bearish" if buy_ratio < 0.4 else "neutral")
|
|
return {
|
|
"token": address,
|
|
"sentiment": sentiment,
|
|
"buy_ratio": round(buy_ratio, 3),
|
|
"buys_24h": buys,
|
|
"sells_24h": sells,
|
|
"source": "dexscreener",
|
|
}
|
|
except Exception:
|
|
pass
|
|
return {"token": address, "sentiment": "unknown"}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# NANSEN - Wallet labels, smart money, token flow
|
|
# ═══════════════════════════════════════════════════════════
|
|
@router.get("/nansen/labels/{address}")
|
|
async def nansen_labels(address: str):
|
|
try:
|
|
from app.nansen_connector import get_wallet_labels
|
|
|
|
result = get_wallet_labels(address)
|
|
return {"address": address, "labels": result, "source": "nansen"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
@router.get("/nansen/smart-money")
|
|
async def nansen_smart_money():
|
|
try:
|
|
from app.nansen_connector import get_smart_money
|
|
|
|
result = get_smart_money()
|
|
return {"smart_money": result, "source": "nansen"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
@router.get("/nansen/activity/{address}")
|
|
async def nansen_activity(address: str):
|
|
try:
|
|
from app.nansen_connector import get_wallet_activity
|
|
|
|
result = get_wallet_activity(address)
|
|
return {"address": address, "activity": result, "source": "nansen"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# MEMPOOL - Bitcoin mempool monitoring
|
|
# ═══════════════════════════════════════════════════════════
|
|
@router.get("/mempool/status")
|
|
async def mempool_status():
|
|
try:
|
|
import httpx
|
|
|
|
async with httpx.AsyncClient(timeout=8) as c:
|
|
r = await c.get("https://mempool.space/api/v1/fees/recommended")
|
|
if r.status_code == 200:
|
|
fees = r.json()
|
|
r2 = await c.get("https://mempool.space/api/mempool")
|
|
mempool = r2.json() if r2.status_code == 200 else {}
|
|
return {
|
|
"fees": fees,
|
|
"mempool_tx_count": mempool.get("count", 0),
|
|
"mempool_size_mb": round(mempool.get("vsize", 0) / 1_000_000, 2),
|
|
"source": "mempool.space",
|
|
"timestamp": datetime.now(UTC).isoformat(),
|
|
}
|
|
except Exception:
|
|
pass
|
|
return {"error": "Mempool data unavailable"}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# COINGECKO - Price data, trending, global metrics
|
|
# ═══════════════════════════════════════════════════════════
|
|
@router.get("/coingecko/ping")
|
|
async def coingecko_ping():
|
|
try:
|
|
from app.coingecko_connector import get_coingecko_connector
|
|
|
|
cg = get_coingecko_connector()
|
|
result = await cg.ping()
|
|
return {"ping": result, "source": "coingecko"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
@router.get("/coingecko/trending")
|
|
async def coingecko_trending():
|
|
try:
|
|
from app.coingecko_connector import get_coingecko_connector
|
|
|
|
cg = get_coingecko_connector()
|
|
result = await cg.get_trending()
|
|
return {"trending": result, "source": "coingecko"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
@router.get("/coingecko/markets")
|
|
async def coingecko_markets(vs_currency: str = "usd", per_page: int = 50):
|
|
try:
|
|
from app.coingecko_connector import get_coingecko_connector
|
|
|
|
cg = get_coingecko_connector()
|
|
result = await cg.get_market_overview(vs_currency=vs_currency, per_page=per_page)
|
|
return {"markets": result, "vs_currency": vs_currency, "source": "coingecko"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
@router.get("/coingecko/price/{coin_id}")
|
|
async def coingecko_price(coin_id: str):
|
|
try:
|
|
from app.coingecko_connector import get_coingecko_connector
|
|
|
|
cg = get_coingecko_connector()
|
|
result = await cg.get_token_price(coin_id)
|
|
return {"coin_id": coin_id, "price": result, "source": "coingecko"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
@router.get("/coingecko/global")
|
|
async def coingecko_global():
|
|
try:
|
|
from app.coingecko_connector import get_coingecko_connector
|
|
|
|
cg = get_coingecko_connector()
|
|
result = await cg.get_global_metrics()
|
|
return {"global": result, "source": "coingecko"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
@router.get("/coingecko/coin/{coin_id}")
|
|
async def coingecko_coin(coin_id: str):
|
|
try:
|
|
from app.coingecko_connector import get_coingecko_connector
|
|
|
|
cg = get_coingecko_connector()
|
|
result = await cg.get_token_detail(coin_id)
|
|
return {"coin_id": coin_id, "coin": result, "source": "coingecko"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|