- 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>
186 lines
5.9 KiB
Python
186 lines
5.9 KiB
Python
"""
|
|
x402 Router: insider_network
|
|
=============================
|
|
Wraps InsiderNetworkAnalyzer with:
|
|
- Address validation
|
|
- x402 payment middleware integration
|
|
- Caching (Redis if available)
|
|
- Trial quota tracking
|
|
- Rate limiting support
|
|
|
|
TOOL : insider_network
|
|
TIER : premium / intelligence
|
|
PRICE : $0.10 (100000 atoms)
|
|
TRIAL : 1 free check
|
|
ROUTER: /api/v1/x402-tools/insider_network
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
from contextlib import suppress
|
|
|
|
import redis as _redis_mod
|
|
from fastapi import APIRouter, HTTPException, Request
|
|
from pydantic import BaseModel, Field, field_validator
|
|
|
|
from app.insider_network import (
|
|
format_insider_network_report,
|
|
get_insider_network_analyzer,
|
|
is_valid_address,
|
|
)
|
|
|
|
logger = logging.getLogger("x402_insider_network")
|
|
|
|
router = APIRouter(prefix="/api/v1/x402-tools", tags=["x402-tools"])
|
|
|
|
# ── Redis helpers ─────────────────────────────────────────────
|
|
|
|
_redis: "_redis_mod.Redis | None" = None
|
|
|
|
|
|
def _get_redis():
|
|
global _redis
|
|
if _redis is None:
|
|
try:
|
|
r = _redis_mod.Redis(
|
|
host=os.getenv("REDIS_HOST", "localhost"),
|
|
port=int(os.getenv("REDIS_PORT", 6379)),
|
|
db=int(os.getenv("REDIS_DB", 0)),
|
|
password=os.getenv("REDIS_PASSWORD", None),
|
|
decode_responses=True,
|
|
)
|
|
r.ping()
|
|
_redis = r
|
|
except Exception:
|
|
_redis = None # Not available
|
|
return _redis
|
|
|
|
|
|
_CACHE_TTL = 600 # 10 minutes - insider networks don't change rapidly
|
|
|
|
|
|
# ── Request Models ─────────────────────────────────────────────
|
|
|
|
|
|
class InsiderNetworkRequest(BaseModel):
|
|
"""Request body for insider network analysis."""
|
|
|
|
wallet_address: str = Field(
|
|
...,
|
|
description="Wallet address to investigate for insider connections",
|
|
)
|
|
chains: list[str] | None = Field(
|
|
default=None,
|
|
description="Chains to search (default: solana, ethereum, bsc, base)",
|
|
)
|
|
deep_scan: bool = Field(
|
|
default=False,
|
|
description="If True, perform deeper (slower) analysis with more API calls",
|
|
)
|
|
|
|
@field_validator("wallet_address")
|
|
@classmethod
|
|
def validate_address(cls, v: str) -> str:
|
|
v = v.strip()
|
|
if not is_valid_address(v):
|
|
raise ValueError(f"Invalid address: {v}. Must be a valid Solana (base58) or EVM (0x-prefixed hex) address.")
|
|
return v
|
|
|
|
@field_validator("chains")
|
|
@classmethod
|
|
def validate_chains(cls, v: list[str] | None) -> list[str] | None:
|
|
if v is not None:
|
|
valid = {"solana", "ethereum", "bsc", "polygon", "arbitrum", "base"}
|
|
for c in v:
|
|
if c not in valid:
|
|
raise ValueError(f"Unsupported chain: {c}. Supported: {', '.join(sorted(valid))}")
|
|
return v
|
|
|
|
|
|
# ── Endpoints ──────────────────────────────────────────────────
|
|
|
|
|
|
@router.post("/insider_network")
|
|
async def analyze_insider_network(request: Request, body: InsiderNetworkRequest) -> dict:
|
|
"""
|
|
Analyze a wallet for insider connections across token projects.
|
|
Maps shared funding sources, coordinated trading, and team relationships.
|
|
"""
|
|
wallet = body.wallet_address.strip()
|
|
|
|
# Check cache
|
|
cache_key = f"insider_network:{wallet}:{body.deep_scan}"
|
|
r = _get_redis()
|
|
if r:
|
|
with suppress(Exception):
|
|
cached = r.get(cache_key)
|
|
if cached:
|
|
return json.loads(cached)
|
|
|
|
# Check trial quota (via x402 middleware headers if available)
|
|
trial_used = False
|
|
x402_headers = getattr(request.state, "x402", None) or {}
|
|
quota = x402_headers.get("remaining_quota", {})
|
|
if isinstance(quota, dict) and quota.get("insider_network", 0) > 0:
|
|
trial_used = True
|
|
logger.info(
|
|
"Trial quota used for insider_network on wallet %s",
|
|
wallet[:10],
|
|
)
|
|
|
|
try:
|
|
analyzer = get_insider_network_analyzer()
|
|
report = await analyzer.analyze(
|
|
wallet_address=wallet,
|
|
chains=body.chains,
|
|
deep_scan=body.deep_scan,
|
|
)
|
|
|
|
response = {
|
|
"success": True,
|
|
"tool": "insider_network",
|
|
"wallet_address": wallet,
|
|
"total_connected_wallets": report.total_connected_wallets,
|
|
"total_clusters": report.total_clusters,
|
|
"highest_risk_score": round(report.highest_risk_score, 1),
|
|
"clusters": [
|
|
{
|
|
"cluster_id": c.cluster_id,
|
|
"member_count": c.member_count,
|
|
"risk_score": round(c.risk_score, 1),
|
|
"projects_involved": c.projects_involved[:5],
|
|
"risk_factors": c.risk_factors[:3],
|
|
"top_relationships": c.top_relationships[:5],
|
|
}
|
|
for c in report.clusters
|
|
],
|
|
"summary": report.summary,
|
|
"report_text": format_insider_network_report(report),
|
|
"trial_used": trial_used,
|
|
}
|
|
|
|
if not trial_used and r:
|
|
with suppress(Exception):
|
|
r.setex(cache_key, _CACHE_TTL, json.dumps(response))
|
|
|
|
return response
|
|
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e)) from e
|
|
except Exception as e:
|
|
logger.exception("Insider network analysis failed for %s", wallet[:10])
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"Analysis failed: {e!s}",
|
|
) from e
|
|
|
|
|
|
@router.get("/insider_network/health")
|
|
async def insider_network_health() -> dict:
|
|
"""Health check endpoint."""
|
|
return {
|
|
"status": "ok",
|
|
"tool": "insider_network",
|
|
"version": "1.0.0",
|
|
}
|