merge: chore/cleanup-remove-bloat-and-secrets into main

This commit is contained in:
Crypto Rug Munch 2026-07-02 01:24:22 +07:00
commit bde2f3a97d
1173 changed files with 437609 additions and 0 deletions

View file

@ -0,0 +1,51 @@
"""Token domain — public API + health check."""
from __future__ import annotations
from app.core import health as health_mod
from app.core.health import DomainHealth
async def _health_check() -> DomainHealth:
"""Token health: DataBus is importable."""
try:
import app.databus # noqa: F401
return DomainHealth(
name="token",
healthy=True,
details={"databus": "importable"},
)
except Exception as e:
return DomainHealth(name="token", healthy=False, error=str(e))
health_mod.register_health_check("token", _health_check)
# Public API
from app.domain.token.analyzer import TokenAnalyzer
from app.domain.token.models import (
RiskLevel,
Token,
TokenDetail,
TokenHolder,
TokenLiquidity,
TokenRisk,
TokenScanRequest,
TokenScanResult,
)
from app.domain.token.repository import TokenRepository
from app.domain.token.service import TokenService
__all__ = [
"RiskLevel",
"Token",
"TokenAnalyzer",
"TokenDetail",
"TokenHolder",
"TokenLiquidity",
"TokenRepository",
"TokenRisk",
"TokenScanRequest",
"TokenScanResult",
"TokenService",
]

View file

@ -0,0 +1,103 @@
"""Pure-Python token risk analysis. No I/O."""
from __future__ import annotations
from app.domain.token.models import (
RiskLevel,
TokenHolder,
TokenLiquidity,
TokenRisk,
)
class TokenAnalyzer:
"""Pure logic — given holders + liquidity + flags, return risk."""
@staticmethod
def compute_risk(
address: str,
chain: str,
holders: list[TokenHolder] | None = None,
liquidity: TokenLiquidity | None = None,
honeypot: bool = False,
can_sell: bool = True,
can_buy: bool = True,
owner_change_balance: bool = False,
transfer_pausable: bool = False,
) -> TokenRisk:
"""Compute TokenRisk from raw inputs. Pure function of inputs."""
flags: list[dict] = []
score = 0
# Honeypot = critical
if honeypot or not can_sell:
score += 100
flags.append({"code": "honeypot", "severity": "critical", "message": "Cannot sell — likely honeypot."})
# Owner can change balance = rug pull risk
if owner_change_balance:
score += 50
flags.append({"code": "owner_change_balance", "severity": "critical", "message": "Owner can modify balances."})
# Transfer pausable = rug risk
if transfer_pausable:
score += 30
flags.append({"code": "transfer_pausable", "severity": "high", "message": "Transfers can be paused."})
# Holder concentration
if holders:
top_holder_pct = max((h.percentage for h in holders), default=0)
if top_holder_pct > 50:
score += 30
flags.append({
"code": "concentrated_holders",
"severity": "high",
"message": f"Top holder owns {top_holder_pct:.1f}% of supply.",
})
elif top_holder_pct > 25:
score += 15
flags.append({
"code": "moderate_concentration",
"severity": "medium",
"message": f"Top holder owns {top_holder_pct:.1f}% of supply.",
})
# Locked holders reduce risk
locked_pct = sum(h.percentage for h in holders if h.is_locked)
if locked_pct < 20 and len(holders) < 100:
score += 5
flags.append({
"code": "few_holders",
"severity": "low",
"message": f"Only {len(holders)} holders, {locked_pct:.1f}% locked.",
})
# Liquidity lock
if liquidity:
if liquidity.total_liquidity_usd < 1000:
score += 20
flags.append({
"code": "low_liquidity",
"severity": "high",
"message": f"Total liquidity only ${liquidity.total_liquidity_usd:.0f}.",
})
elif liquidity.locked_percentage < 50:
score += 15
flags.append({
"code": "liquidity_unlocked",
"severity": "medium",
"message": f"Only {liquidity.locked_percentage:.1f}% of liquidity locked.",
})
score = max(0, min(100, score))
return TokenRisk(
address=address,
chain=chain,
risk_score=score,
risk_level=RiskLevel.from_score(score),
flags=flags,
honeypot=honeypot,
can_sell=can_sell,
can_buy=can_buy,
owner_change_balance=owner_change_balance,
transfer_pausable=transfer_pausable,
)

127
app/domain/token/models.py Normal file
View file

@ -0,0 +1,127 @@
"""Pydantic v2 models for the token domain.
No FastAPI imports. Pure data shapes.
"""
from __future__ import annotations
from datetime import datetime
from enum import StrEnum
from typing import Any
from pydantic import BaseModel, ConfigDict, Field, field_validator
class RiskLevel(StrEnum):
"""Token risk classification."""
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
@classmethod
def from_score(cls, score: int) -> RiskLevel:
if score < 20:
return cls.LOW
if score < 50:
return cls.MEDIUM
if score < 80:
return cls.HIGH
return cls.CRITICAL
class Token(BaseModel):
"""Token identity."""
model_config = ConfigDict(str_strip_whitespace=True)
address: str = Field(..., min_length=1, max_length=256)
chain: str = Field(default="solana")
name: str = ""
symbol: str = ""
decimals: int = 9
class TokenDetail(BaseModel):
"""Full token details — metadata + supply + verification."""
address: str
chain: str
name: str = ""
symbol: str = ""
decimals: int = 9
total_supply: float = 0.0
circulating_supply: float = 0.0
holders: int = 0
verified: bool = False
metadata: dict[str, Any] = Field(default_factory=dict)
fetched_at: datetime = Field(default_factory=datetime.utcnow)
class TokenHolder(BaseModel):
"""A token holder entry."""
address: str
balance: float = 0.0
percentage: float = 0.0
label: str | None = None
is_contract: bool = False
is_locked: bool = False
class TokenLiquidity(BaseModel):
"""Token liquidity summary."""
address: str
chain: str
total_liquidity_usd: float = 0.0
pools: list[dict[str, Any]] = Field(default_factory=list)
locked_percentage: float = 0.0
fetched_at: datetime = Field(default_factory=datetime.utcnow)
class TokenRisk(BaseModel):
"""Token risk assessment."""
address: str
chain: str
risk_score: int = Field(default=0, ge=0, le=100)
risk_level: RiskLevel
flags: list[dict[str, Any]] = Field(default_factory=list)
honeypot: bool = False
can_sell: bool = True
can_buy: bool = True
take_back: bool = False
owner_change_balance: bool = False
transfer_pausable: bool = False
analyzed_at: datetime = Field(default_factory=datetime.utcnow)
class TokenScanRequest(BaseModel):
"""Request body for the token scan endpoint."""
model_config = ConfigDict(str_strip_whitespace=True)
address: str = Field(..., min_length=1, max_length=256)
chain: str = Field(default="solana")
tier: str = Field(default="free")
@field_validator("chain", "tier")
@classmethod
def _lowercase(cls, v: str) -> str:
return v.lower().strip()
class TokenScanResult(BaseModel):
"""Full token scan result."""
address: str
chain: str
safety_score: float = 100.0
risk_score: int = 0
risk_level: RiskLevel
risk_flags: list[str] = Field(default_factory=list)
warnings: list[str] = Field(default_factory=list)
modules_run: list[str] = Field(default_factory=list)
metadata: dict[str, Any] = Field(default_factory=dict)
scanned_at: datetime = Field(default_factory=datetime.utcnow)

View file

@ -0,0 +1,94 @@
"""Repository — async data fetch for token info via Databus."""
from __future__ import annotations
from typing import Any
from app.core.logging import get_logger
from app.domain.token.models import (
TokenDetail,
TokenHolder,
TokenLiquidity,
)
log = get_logger(__name__)
class TokenRepository:
"""Async data access for token info via Databus."""
async def get_detail(self, address: str, chain: str) -> TokenDetail:
"""Fetch token metadata + supply."""
try:
from app.databus.client import get_databus_client
client = get_databus_client()
raw = await client.get_token_info(address, chain=chain)
return self._parse_detail(address, chain, raw or {})
except Exception as e:
log.warning("token_detail_fetch_failed", address=address[:12], error=str(e))
return TokenDetail(address=address, chain=chain)
async def get_holders(
self,
address: str,
chain: str,
limit: int = 100,
) -> list[TokenHolder]:
"""Fetch top holders."""
try:
from app.databus.client import get_databus_client
client = get_databus_client()
raw = await client.get_token_holders(address, chain=chain, limit=limit)
return [self._parse_holder(h) for h in (raw or [])]
except Exception as e:
log.warning("token_holders_fetch_failed", address=address[:12], error=str(e))
return []
async def get_liquidity(self, address: str, chain: str) -> TokenLiquidity:
"""Fetch liquidity + pool info."""
try:
from app.databus.client import get_databus_client
client = get_databus_client()
raw = await client.get_token_liquidity(address, chain=chain)
return self._parse_liquidity(address, chain, raw or {})
except Exception as e:
log.warning("token_liquidity_fetch_failed", address=address[:12], error=str(e))
return TokenLiquidity(address=address, chain=chain)
@staticmethod
def _parse_detail(address: str, chain: str, raw: dict[str, Any]) -> TokenDetail:
return TokenDetail(
address=address,
chain=chain,
name=raw.get("name", ""),
symbol=raw.get("symbol", ""),
decimals=int(raw.get("decimals", 9) or 9),
total_supply=float(raw.get("total_supply", 0) or 0),
circulating_supply=float(raw.get("circulating_supply", 0) or 0),
holders=int(raw.get("holders", 0) or 0),
verified=bool(raw.get("verified", False)),
metadata=raw.get("metadata", {}) or {},
)
@staticmethod
def _parse_holder(raw: dict[str, Any]) -> TokenHolder:
return TokenHolder(
address=raw.get("address", ""),
balance=float(raw.get("balance", 0) or 0),
percentage=float(raw.get("percentage", raw.get("pct", 0)) or 0),
label=raw.get("label"),
is_contract=bool(raw.get("is_contract", False)),
is_locked=bool(raw.get("is_locked", False)),
)
@staticmethod
def _parse_liquidity(address: str, chain: str, raw: dict[str, Any]) -> TokenLiquidity:
return TokenLiquidity(
address=address,
chain=chain,
total_liquidity_usd=float(raw.get("total_usd", 0) or 0),
pools=raw.get("pools", []) or [],
locked_percentage=float(raw.get("locked_percentage", 0) or 0),
)

106
app/domain/token/service.py Normal file
View file

@ -0,0 +1,106 @@
"""Service — business logic for token info, risk, and scans."""
from __future__ import annotations
from app.core.logging import get_logger
from app.domain.token.analyzer import TokenAnalyzer
from app.domain.token.models import (
TokenDetail,
TokenHolder,
TokenLiquidity,
TokenRisk,
TokenScanRequest,
TokenScanResult,
)
from app.domain.token.repository import TokenRepository
log = get_logger(__name__)
class TokenService:
"""Orchestrates token data fetch and analysis."""
def __init__(
self,
repo: TokenRepository | None = None,
analyzer: TokenAnalyzer | None = None,
) -> None:
self._repo = repo or TokenRepository()
self._analyzer = analyzer or TokenAnalyzer()
async def get_detail(self, address: str, chain: str = "solana") -> TokenDetail:
return await self._repo.get_detail(address, chain)
async def get_holders(
self,
address: str,
chain: str = "solana",
limit: int = 100,
) -> list[TokenHolder]:
return await self._repo.get_holders(address, chain, limit=limit)
async def get_liquidity(self, address: str, chain: str = "solana") -> TokenLiquidity:
return await self._repo.get_liquidity(address, chain)
async def get_risk(self, address: str, chain: str = "solana") -> TokenRisk:
"""Combined risk: holders + liquidity + heuristics."""
log.info("token_risk_started", address=address[:12], chain=chain)
holders = await self._repo.get_holders(address, chain, limit=20)
liquidity = await self._repo.get_liquidity(address, chain)
risk = self._analyzer.compute_risk(
address=address,
chain=chain,
holders=holders,
liquidity=liquidity,
)
log.info(
"token_risk_complete",
address=address[:12],
risk_score=risk.risk_score,
risk_level=risk.risk_level.value,
)
return risk
async def scan(self, req: TokenScanRequest) -> TokenScanResult:
"""Full token scan. Multi-module. Used for free-tier scanner."""
log.info("token_scan_started", address=req.address[:12], chain=req.chain, tier=req.tier)
detail = await self._repo.get_detail(req.address, req.chain)
risk = await self.get_risk(req.address, req.chain)
# Convert risk flags to the legacy format (list of strings)
risk_flags: list[str] = []
warnings: list[str] = []
for flag in risk.flags:
code = flag.get("code", "")
message = flag.get("message", "")
if code:
risk_flags.append(code)
if message:
warnings.append(message)
# Heuristic safety score (inverse of risk, 0-100)
safety_score = max(0.0, 100.0 - float(risk.risk_score))
result = TokenScanResult(
address=req.address,
chain=req.chain,
safety_score=safety_score,
risk_score=risk.risk_score,
risk_level=risk.risk_level,
risk_flags=risk_flags,
warnings=warnings,
modules_run=["analyzer", "holders", "liquidity"],
metadata={
"name": detail.name,
"symbol": detail.symbol,
"decimals": detail.decimals,
"total_supply": detail.total_supply,
"verified": detail.verified,
},
)
log.info(
"token_scan_complete",
address=req.address[:12],
safety_score=safety_score,
risk_level=risk.risk_level.value,
)
return result