245 lines
7.9 KiB
Python
245 lines
7.9 KiB
Python
"""
|
|
x402 Router: cross_chain_whale
|
|
================================
|
|
Wraps CrossChainWhaleTracker with:
|
|
- Address validation
|
|
- x402 payment middleware integration
|
|
- Caching (Redis if available)
|
|
- Trial quota tracking
|
|
- Rate limiting support
|
|
|
|
TOOL : cross_chain_whale
|
|
TIER : intelligence
|
|
PRICE : $0.08 (80000 atoms)
|
|
TRIAL : 2 free checks
|
|
ROUTER: /api/v1/x402-tools/cross_chain_whale
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
from contextlib import suppress
|
|
|
|
from fastapi import APIRouter, HTTPException, Request
|
|
from pydantic import BaseModel, Field, field_validator
|
|
|
|
from app.cross_chain_whale import (
|
|
CrossChainWhaleTracker,
|
|
format_whale_report,
|
|
get_whale_tracker,
|
|
is_valid_address,
|
|
)
|
|
|
|
logger = logging.getLogger("x402_cross_chain_whale")
|
|
|
|
router = APIRouter(prefix="/api/v1/x402-tools", tags=["x402-tools"])
|
|
|
|
# ── Redis helpers ─────────────────────────────────────────────
|
|
|
|
_redis = None
|
|
|
|
|
|
def _get_redis():
|
|
global _redis
|
|
if _redis is None:
|
|
try:
|
|
import redis as redis_mod
|
|
|
|
_redis = 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,
|
|
)
|
|
_redis.ping()
|
|
except Exception:
|
|
_redis = False # Sentinel for "not available"
|
|
return _redis if _redis is not False else None
|
|
|
|
|
|
_CACHE_TTL = 300 # 5 minutes
|
|
|
|
|
|
# ── Request Models ─────────────────────────────────────────────
|
|
|
|
|
|
class CrossChainWhaleRequest(BaseModel):
|
|
"""Request body for cross-chain whale tracking."""
|
|
|
|
token_address: str = Field(..., description="Token contract/mint address to track")
|
|
chains: list[str] | None = Field(
|
|
default=None,
|
|
description="Chains to check (default: solana, ethereum, base, bsc)",
|
|
)
|
|
|
|
@field_validator("token_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
|
|
|
|
|
|
class CrossChainWhaleWalletRequest(BaseModel):
|
|
"""Request body for wallet-level cross-chain tracking."""
|
|
|
|
wallet_address: str = Field(..., description="Wallet address to track across chains")
|
|
chains: list[str] | None = Field(
|
|
default=None,
|
|
description="Chains to check (default: all supported chains)",
|
|
)
|
|
|
|
@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 wallet address: {v}")
|
|
return v
|
|
|
|
|
|
# ── Endpoints ──────────────────────────────────────────────────
|
|
|
|
|
|
@router.post("/cross_chain_whale")
|
|
async def track_cross_chain_whale(
|
|
request: Request,
|
|
body: CrossChainWhaleRequest,
|
|
):
|
|
"""Track whale holdings for a token across multiple chains.
|
|
|
|
Returns a comprehensive report of holder concentrations,
|
|
cross-chain whale positions, and risk scoring.
|
|
"""
|
|
redis_client = _get_redis()
|
|
cache_key = f"x402:whale:{body.token_address}:{','.join(body.chains or [])}"
|
|
|
|
# Check cache
|
|
if redis_client:
|
|
with suppress(Exception):
|
|
cached = redis_client.get(cache_key)
|
|
if cached:
|
|
return {"success": True, "data": json.loads(cached), "cached": True}
|
|
|
|
try:
|
|
tracker: CrossChainWhaleTracker = get_whale_tracker()
|
|
|
|
# Determine chains to scan
|
|
chains = body.chains or ["solana", "ethereum", "base", "bsc"]
|
|
|
|
report = await tracker.track_token(
|
|
token_address=body.token_address,
|
|
chains=chains,
|
|
)
|
|
|
|
# Build response data
|
|
response_data = {
|
|
"token_address": report.token_address,
|
|
"token_symbol": report.token_symbol,
|
|
"token_name": report.token_name,
|
|
"chains_found": report.chains_found,
|
|
"chains_no_data": report.chains_no_data,
|
|
"total_holders_tracked": report.total_holders,
|
|
"total_value_tracked_usd": round(report.total_value_tracked, 2),
|
|
"concentration_score": report.concentration_score,
|
|
"cross_chain_whale_count": len(report.cross_chain_whales),
|
|
"top_holders_by_chain": report.top_holders_by_chain,
|
|
"cross_chain_whales": [
|
|
{
|
|
"address": cw.primary_address,
|
|
"address_short": f"{cw.primary_address[:6]}...{cw.primary_address[-4:]}",
|
|
"chain_count": cw.chain_count,
|
|
"total_value_usd": round(cw.total_value_usd, 2),
|
|
"chains": list({p.chain for p in cw.positions}),
|
|
"risk_score": cw.risk_score,
|
|
"risk_factors": cw.risk_factors[:5],
|
|
}
|
|
for cw in report.cross_chain_whales[:20]
|
|
],
|
|
"top_whale": (
|
|
{
|
|
"address": report.cross_chain_whales[0].primary_address,
|
|
"chain_count": report.cross_chain_whales[0].chain_count,
|
|
"total_value_usd": round(report.cross_chain_whales[0].total_value_usd, 2),
|
|
}
|
|
if report.cross_chain_whales
|
|
else None
|
|
),
|
|
"human_readable": format_whale_report(report),
|
|
"errors": report.errors[:5],
|
|
"scan_timestamp": report.scan_timestamp,
|
|
}
|
|
|
|
# Cache it
|
|
if redis_client and not report.errors:
|
|
with suppress(Exception):
|
|
redis_client.setex(cache_key, _CACHE_TTL, json.dumps(response_data))
|
|
|
|
return {"success": True, "data": response_data}
|
|
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e)) from e
|
|
except Exception as e:
|
|
logger.exception(f"cross_chain_whale failed: {e}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"Cross-chain whale tracking failed: {e!s}",
|
|
) from e
|
|
|
|
|
|
@router.post("/cross_chain_whale_wallet")
|
|
async def track_cross_chain_wallet(
|
|
request: Request,
|
|
body: CrossChainWhaleWalletRequest,
|
|
):
|
|
"""Track a specific wallet's token positions across multiple chains.
|
|
|
|
Returns a comprehensive view of what tokens a whale wallet holds
|
|
across all supported chains.
|
|
"""
|
|
redis_client = _get_redis()
|
|
cache_key = f"x402:whale_wallet:{body.wallet_address}:{','.join(body.chains or [])}"
|
|
|
|
if redis_client:
|
|
with suppress(Exception):
|
|
cached = redis_client.get(cache_key)
|
|
if cached:
|
|
return {"success": True, "data": json.loads(cached), "cached": True}
|
|
|
|
try:
|
|
tracker: CrossChainWhaleTracker = get_whale_tracker()
|
|
|
|
chains = body.chains or [
|
|
"solana",
|
|
"ethereum",
|
|
"base",
|
|
"bsc",
|
|
"polygon",
|
|
"arbitrum",
|
|
"optimism",
|
|
]
|
|
|
|
result = await tracker.track_wallet(
|
|
wallet_address=body.wallet_address,
|
|
chains=chains,
|
|
)
|
|
|
|
if redis_client:
|
|
with suppress(Exception):
|
|
redis_client.setex(cache_key, _CACHE_TTL, json.dumps(result))
|
|
|
|
return {"success": True, "data": result}
|
|
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e)) from e
|
|
except Exception as e:
|
|
logger.exception(f"cross_chain_whale_wallet failed: {e}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"Wallet cross-chain tracking failed: {e!s}",
|
|
) from e
|