- 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>
1262 lines
55 KiB
Python
1262 lines
55 KiB
Python
from app.telegram_bot.requirements import httpx
|
|
|
|
#!/usr/bin/env python3
|
|
"""
|
|
╔═══════════════════════════════════════════════════════════════════════════════╗
|
|
║ R U G M U N C H I N T E L L I G E N C E ║
|
|
║ D E G E N S E C U R I T Y S C A N N E R ║
|
|
╚═══════════════════════════════════════════════════════════════════════════════╝
|
|
|
|
Comprehensive token security analysis combining:
|
|
• Free Solscan API (reverse-engineered, no rate limits)
|
|
• Helius Enhanced API (token metadata, enhanced TXs)
|
|
• GMGN Intelligence (smart money tags, top holders)
|
|
• Birdeye Market Data (price, volume, LP depth)
|
|
• DexScreener (pair analysis, liquidity verification)
|
|
• Moralis (cross-chain validation)
|
|
• SolanaFM (on-chain verification)
|
|
|
|
Generates a forensic-grade security report with:
|
|
→ Bundler Detection (launch manipulation)
|
|
→ Gas Funding Trace (where dev got SOL)
|
|
→ Exchange Funding % (centralized vs organic)
|
|
→ LP Analysis (locked? burned? depth?)
|
|
→ Sniper Metrics (first 20 buyers analyzed)
|
|
→ Insider Trading Detection (pre-launch buyers)
|
|
→ Wash Trading Detection (circular patterns)
|
|
→ Dev Wallet Forensics (history, other rugs)
|
|
→ Authority Analysis (mint/freeze still active?)
|
|
→ Holder Concentration (top 10/20/50 %)
|
|
→ Smart Money Ratio (pros vs retail)
|
|
→ Tax & Limit Detection (hidden fees)
|
|
→ Honeypot Test (can you sell?)
|
|
→ RUG PULL PROBABILITY SCORE (0-100)
|
|
|
|
Author: RMI Dev Team
|
|
Contract: Eme5T2s2HB7B8W4YgLG1eReQpnadEVUnQBRjaKTdBAGS
|
|
"""
|
|
|
|
import asyncio # noqa: E402
|
|
import json # noqa: E402
|
|
import logging # noqa: E402
|
|
import os # noqa: E402
|
|
import statistics # noqa: E402
|
|
import sys # noqa: E402
|
|
import time # noqa: E402
|
|
from collections import defaultdict # noqa: E402
|
|
from dataclasses import asdict, dataclass, field # noqa: E402
|
|
from datetime import datetime # noqa: E402
|
|
|
|
# Load the free solscan client
|
|
from app.free_solscan_client import FreeSolscanClient, is_known_exchange # noqa: E402
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# ─── Configuration ────────────────────────────────────────────────────────────
|
|
|
|
HELIUS_API_KEY = os.getenv("HELIUS_API_KEY", "")
|
|
BIRDEYE_API_KEY = os.getenv("BIRDEYE_API_KEY", "")
|
|
MORALIS_API_KEY = os.getenv("MORALIS_API_KEY", "")
|
|
GMGN_API_KEY = os.getenv("GMGN_API_KEY", "")
|
|
|
|
# Known scam/bundler program IDs
|
|
BUNDLER_PROGRAMS = {
|
|
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P": "Pump.fun Bundler",
|
|
"LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo": "Launchpad Bundler",
|
|
"MoonCVVNZFSYkqNXP6bxHLPL6QQxMagE3VCigVtg81o": "Moonshot Bundler",
|
|
}
|
|
|
|
# Known mixer/tumbler programs
|
|
MIXER_PROGRAMS = {
|
|
"Tornado": ["tornado", "mixer", "tumbler"],
|
|
"Cyclone": ["cyclone"],
|
|
"Samourai": ["samourai"],
|
|
}
|
|
|
|
# ─── Data Models ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
@dataclass
|
|
class HolderProfile:
|
|
"""Detailed holder analysis."""
|
|
|
|
address: str
|
|
balance: float
|
|
percentage: float
|
|
exchange_tag: str | None = None
|
|
is_contract: bool = False
|
|
is_known_whale: bool = False
|
|
first_seen: str | None = None
|
|
funding_source: str | None = None
|
|
smart_money_tag: str | None = None
|
|
is_sniper: bool = False
|
|
is_insider: bool = False
|
|
is_bundler: bool = False
|
|
is_wash_trader: bool = False
|
|
other_tokens_held: int = 0
|
|
risk_score: int = 0
|
|
|
|
|
|
@dataclass
|
|
class FundingTrace:
|
|
"""Gas funding trace result."""
|
|
|
|
wallet: str
|
|
initial_funder: str
|
|
funder_tag: str | None = None
|
|
funding_amount_sol: float = 0.0
|
|
funding_time: str | None = None
|
|
hops_from_exchange: int = -1 # -1 = unknown, 0 = direct exchange, 1+ = hops
|
|
is_exchange_direct: bool = False
|
|
exchange_name: str | None = None
|
|
risk_flags: list[str] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class SniperProfile:
|
|
"""Launch sniper analysis."""
|
|
|
|
wallet: str
|
|
buy_time_ms: float # milliseconds after launch
|
|
buy_amount_sol: float
|
|
token_amount: float
|
|
price_paid: float
|
|
current_value: float = 0.0
|
|
pnl_percent: float = 0.0
|
|
is_still_holding: bool = True
|
|
sold_percentage: float = 0.0
|
|
is_known_sniper: bool = False
|
|
sniper_score: int = 0 # 0-100
|
|
|
|
|
|
@dataclass
|
|
class LPAnalysis:
|
|
"""Liquidity pool analysis."""
|
|
|
|
pool_address: str
|
|
dex_name: str
|
|
token_a_amount: float
|
|
token_b_amount: float
|
|
tvl_usd: float
|
|
lp_tokens_total: float
|
|
lp_tokens_locked: float
|
|
lp_locked_percentage: float = 0.0
|
|
lock_expiry: str | None = None
|
|
is_burned: bool = False
|
|
burn_tx: str | None = None
|
|
lp_holders: list[dict] = field(default_factory=list)
|
|
concentration_risk: str = "LOW" # LOW, MEDIUM, HIGH, CRITICAL
|
|
depth_score: int = 0 # 0-100
|
|
|
|
|
|
@dataclass
|
|
class WashTradeCluster:
|
|
"""Detected wash trading cluster."""
|
|
|
|
wallets: list[str]
|
|
trade_count: int
|
|
volume_usd: float
|
|
pattern_type: str # circular, back_forth, triangular
|
|
confidence: float # 0.0 - 1.0
|
|
sample_txs: list[str] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class SecurityReport:
|
|
"""Final comprehensive security report."""
|
|
|
|
# Token Identity
|
|
token_address: str
|
|
token_name: str = "Unknown"
|
|
token_symbol: str = "???"
|
|
decimals: int = 0
|
|
total_supply: float = 0.0
|
|
created_at: str | None = None
|
|
deployer: str = ""
|
|
|
|
# Authority Status
|
|
mint_authority: str | None = None
|
|
freeze_authority: str | None = None
|
|
mint_authority_renounced: bool = False
|
|
freeze_authority_renounced: bool = False
|
|
authorities_risk: str = "LOW"
|
|
|
|
# Holder Analysis
|
|
total_holders: int = 0
|
|
top_holders: list[HolderProfile] = field(default_factory=list)
|
|
top_10_concentration: float = 0.0
|
|
top_20_concentration: float = 0.0
|
|
top_50_concentration: float = 0.0
|
|
holder_health_score: int = 0
|
|
|
|
# Funding & Origins
|
|
deployer_funding: FundingTrace | None = None
|
|
exchange_funding_percentage: float = 0.0
|
|
organic_funding_percentage: float = 0.0
|
|
mixer_funding_detected: bool = False
|
|
funding_risk: str = "LOW"
|
|
|
|
# Bundler Detection
|
|
bundler_detected: bool = False
|
|
bundler_wallets: list[str] = field(default_factory=list)
|
|
bundler_buy_volume_sol: float = 0.0
|
|
bundler_percentage_of_supply: float = 0.0
|
|
bundler_pattern: str = ""
|
|
bundler_risk: str = "LOW"
|
|
|
|
# Sniper Analysis
|
|
snipers: list[SniperProfile] = field(default_factory=list)
|
|
sniper_count: int = 0
|
|
sniper_volume_sol: float = 0.0
|
|
avg_sniper_entry_time_ms: float = 0.0
|
|
sniper_risk: str = "LOW"
|
|
|
|
# LP Analysis
|
|
liquidity_pools: list[LPAnalysis] = field(default_factory=list)
|
|
total_liquidity_usd: float = 0.0
|
|
lp_risk: str = "LOW"
|
|
|
|
# Insider Trading
|
|
insiders_detected: list[str] = field(default_factory=list)
|
|
insider_buy_volume: float = 0.0
|
|
insider_risk: str = "LOW"
|
|
|
|
# Wash Trading
|
|
wash_trade_clusters: list[WashTradeCluster] = field(default_factory=list)
|
|
wash_trading_volume_usd: float = 0.0
|
|
wash_trading_risk: str = "LOW"
|
|
|
|
# Tax & Limits
|
|
buy_tax: float = 0.0
|
|
sell_tax: float = 0.0
|
|
transfer_tax: float = 0.0
|
|
max_buy_limit: float = 0.0
|
|
max_sell_limit: float = 0.0
|
|
max_wallet_limit: float = 0.0
|
|
honeypot_detected: bool = False
|
|
can_sell: bool = True
|
|
tax_risk: str = "LOW"
|
|
|
|
# Market Data
|
|
price_usd: float = 0.0
|
|
market_cap: float = 0.0
|
|
volume_24h: float = 0.0
|
|
price_change_24h: float = 0.0
|
|
liquidity_usd: float = 0.0
|
|
buy_sell_ratio: float = 1.0
|
|
|
|
# Smart Money
|
|
smart_money_holders: int = 0
|
|
dumb_money_holders: int = 0
|
|
smart_money_ratio: float = 0.0
|
|
|
|
# Dev History
|
|
dev_other_tokens: list[str] = field(default_factory=list)
|
|
dev_rug_history: int = 0
|
|
dev_is_known_scammer: bool = False
|
|
dev_risk: str = "LOW"
|
|
|
|
# Final Scores
|
|
overall_risk: str = "LOW" # LOW, MEDIUM, HIGH, CRITICAL
|
|
rug_pull_probability: int = 0 # 0-100
|
|
confidence_level: str = "HIGH" # LOW, MEDIUM, HIGH
|
|
critical_warnings: list[str] = field(default_factory=list)
|
|
recommendations: list[str] = field(default_factory=list)
|
|
scan_timestamp: str = ""
|
|
scan_duration_ms: int = 0
|
|
|
|
def to_dict(self) -> dict:
|
|
"""Convert to dict for JSON serialization."""
|
|
result = {}
|
|
for k, v in asdict(self).items():
|
|
if isinstance(v, list) and len(v) > 0 and hasattr(v[0], "to_dict"):
|
|
result[k] = [item.to_dict() if hasattr(item, "to_dict") else item for item in v]
|
|
elif hasattr(v, "to_dict"):
|
|
result[k] = v.to_dict()
|
|
else:
|
|
result[k] = v
|
|
return result
|
|
|
|
def to_json(self, indent: int = 2) -> str:
|
|
return json.dumps(self.to_dict(), indent=indent, default=str)
|
|
|
|
|
|
# ─── Main Scanner Engine ──────────────────────────────────────────────────────
|
|
|
|
|
|
class DegenSecurityScanner:
|
|
"""
|
|
Ultimate degen security scanner.
|
|
Analyzes any Solana token for 15+ scam indicators.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.solscan = FreeSolscanClient()
|
|
self.helius_key = HELIUS_API_KEY
|
|
self.birdeye_key = BIRDEYE_API_KEY
|
|
self.moralis_key = MORALIS_API_KEY
|
|
self.gmgn_key = GMGN_API_KEY
|
|
self.critical_warnings = []
|
|
self.recommendations = []
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# PUBLIC API
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
async def full_scan(self, token_address: str, quick: bool = False) -> SecurityReport:
|
|
"""
|
|
Run comprehensive security scan on a token.
|
|
|
|
Args:
|
|
token_address: The token mint address
|
|
quick: If True, skip slow operations (dev history, wash trade deep scan)
|
|
|
|
Returns:
|
|
SecurityReport with all analysis results
|
|
"""
|
|
start_time = time.time()
|
|
logger.info(f"🔍 Starting DEGEN SCAN for {token_address}")
|
|
|
|
report = SecurityReport(
|
|
token_address=token_address,
|
|
scan_timestamp=datetime.utcnow().isoformat(),
|
|
)
|
|
|
|
# ── Phase 1: Token Metadata ──────────────────────────────────────────
|
|
logger.info("[1/7] Gathering token metadata...")
|
|
await self._phase1_metadata(report)
|
|
time.sleep(0.3)
|
|
|
|
# ── Phase 2: Authority Analysis ──────────────────────────────────────
|
|
logger.info("[2/7] Checking authorities...")
|
|
await self._phase2_authorities(report)
|
|
time.sleep(0.2)
|
|
|
|
# ── Phase 3: Holder Analysis ─────────────────────────────────────────
|
|
logger.info("[3/7] Analyzing holders...")
|
|
await self._phase3_holders(report)
|
|
time.sleep(0.3)
|
|
|
|
# ── Phase 4: Funding Trace ───────────────────────────────────────────
|
|
logger.info("[4/7] Tracing funding sources...")
|
|
await self._phase4_funding(report)
|
|
time.sleep(0.3)
|
|
|
|
# ── Phase 5: Bundler & Sniper Detection ─────────────────────────────
|
|
logger.info("[5/7] Detecting bundlers & snipers...")
|
|
await self._phase5_bundlers_snipers(report)
|
|
time.sleep(0.3)
|
|
|
|
# ── Phase 6: LP & Market Analysis ────────────────────────────────────
|
|
logger.info("[6/7] Analyzing liquidity pools...")
|
|
await self._phase6_liquidity(report)
|
|
time.sleep(0.3)
|
|
|
|
# ── Phase 7: Advanced Detection ──────────────────────────────────────
|
|
logger.info("[7/7] Advanced scam detection...")
|
|
await self._phase7_advanced(report, quick=quick)
|
|
|
|
# ── Final: Calculate Scores ──────────────────────────────────────────
|
|
logger.info("⚡ Calculating final risk scores...")
|
|
self._calculate_final_scores(report)
|
|
|
|
report.scan_duration_ms = int((time.time() - start_time) * 1000)
|
|
report.critical_warnings = self.critical_warnings
|
|
report.recommendations = self.recommendations
|
|
|
|
logger.info(f"✅ DEGEN SCAN complete in {report.scan_duration_ms}ms")
|
|
logger.info(f"🎯 RUG PULL PROBABILITY: {report.rug_pull_probability}%")
|
|
logger.info(f"🚨 RISK LEVEL: {report.overall_risk}")
|
|
|
|
return report
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# PHASE 1: TOKEN METADATA
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
async def _phase1_metadata(self, report: SecurityReport) -> None:
|
|
"""Get basic token info from Solscan + Helius."""
|
|
# Try Solscan first
|
|
token_data = self.solscan.token_data(report.token_address)
|
|
if token_data:
|
|
report.token_name = token_data.get("name", "Unknown")
|
|
report.token_symbol = token_data.get("symbol", "???")
|
|
report.decimals = token_data.get("decimals", 0)
|
|
supply_raw = token_data.get("supply", "0")
|
|
try:
|
|
report.total_supply = (
|
|
float(supply_raw) / (10**report.decimals) if report.decimals else float(supply_raw)
|
|
)
|
|
except Exception:
|
|
report.total_supply = 0
|
|
report.created_at = token_data.get("createdTime")
|
|
else:
|
|
# Fallback: try Helius
|
|
helius_data = await self._helius_token_metadata(report.token_address)
|
|
if helius_data:
|
|
report.token_name = helius_data.get("name", "Unknown")
|
|
report.token_symbol = helius_data.get("symbol", "???")
|
|
report.decimals = helius_data.get("decimals", 0)
|
|
|
|
# Try to get market data from Birdeye
|
|
market_data = await self._birdeye_token_data(report.token_address)
|
|
if market_data:
|
|
report.price_usd = market_data.get("price", 0)
|
|
report.market_cap = market_data.get("mc", 0)
|
|
report.volume_24h = market_data.get("v24hUSD", 0)
|
|
report.price_change_24h = market_data.get("priceChange24h", 0)
|
|
report.liquidity_usd = market_data.get("liquidity", 0)
|
|
report.buy_sell_ratio = market_data.get("buySellRatio", 1.0)
|
|
|
|
# Get total holders
|
|
holders_total = self.solscan.token_holders_total(report.token_address)
|
|
if holders_total:
|
|
report.total_holders = holders_total
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# PHASE 2: AUTHORITY ANALYSIS
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
async def _phase2_authorities(self, report: SecurityReport) -> None:
|
|
"""Check mint/freeze authorities via Helius."""
|
|
helius_data = await self._helius_token_metadata(report.token_address)
|
|
if helius_data:
|
|
mint_auth = helius_data.get("mintAuthority")
|
|
freeze_auth = helius_data.get("freezeAuthority")
|
|
|
|
report.mint_authority = mint_auth
|
|
report.freeze_authority = freeze_auth
|
|
report.mint_authority_renounced = mint_auth is None or mint_auth == ""
|
|
report.freeze_authority_renounced = freeze_auth is None or freeze_auth == ""
|
|
|
|
# Risk assessment
|
|
risk_score = 0
|
|
if not report.mint_authority_renounced:
|
|
risk_score += 30
|
|
self.critical_warnings.append(
|
|
f"🚨 MINT AUTHORITY ACTIVE: Dev can mint unlimited tokens! Authority: {mint_auth[:20]}..."
|
|
)
|
|
self.recommendations.append("Mint authority should be renounced for investor safety")
|
|
|
|
if not report.freeze_authority_renounced:
|
|
risk_score += 25
|
|
self.critical_warnings.append("❄️ FREEZE AUTHORITY ACTIVE: Dev can freeze any wallet!")
|
|
self.recommendations.append("Freeze authority should be renounced immediately")
|
|
|
|
if risk_score >= 45:
|
|
report.authorities_risk = "CRITICAL"
|
|
elif risk_score >= 25:
|
|
report.authorities_risk = "HIGH"
|
|
elif risk_score >= 10:
|
|
report.authorities_risk = "MEDIUM"
|
|
else:
|
|
report.authorities_risk = "LOW"
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# PHASE 3: HOLDER ANALYSIS
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
async def _phase3_holders(self, report: SecurityReport) -> None:
|
|
"""Analyze top holders for concentration and risk."""
|
|
holders = self.solscan.get_holder_wallets(report.token_address, top_n=50)
|
|
if not holders:
|
|
return
|
|
|
|
profiles = []
|
|
top_10_total = 0.0
|
|
top_20_total = 0.0
|
|
top_50_total = 0.0
|
|
|
|
for i, h in enumerate(holders):
|
|
addr = h.get("address", "")
|
|
amount = h.get("amount", 0)
|
|
pct = h.get("uiAmount", 0)
|
|
|
|
profile = HolderProfile(
|
|
address=addr,
|
|
balance=float(amount),
|
|
percentage=pct,
|
|
)
|
|
|
|
# Tag known exchanges
|
|
exchange = is_known_exchange(addr)
|
|
if exchange:
|
|
profile.exchange_tag = exchange
|
|
profile.is_contract = True
|
|
|
|
# Detect contracts (short addresses often = programs)
|
|
if len(addr) < 40:
|
|
profile.is_contract = True
|
|
|
|
# Whale threshold: >2% = whale
|
|
if pct > 2.0:
|
|
profile.is_known_whale = True
|
|
|
|
profiles.append(profile)
|
|
|
|
if i < 10:
|
|
top_10_total += pct
|
|
if i < 20:
|
|
top_20_total += pct
|
|
top_50_total += pct
|
|
|
|
report.top_holders = profiles[:20]
|
|
report.top_10_concentration = round(top_10_total, 2)
|
|
report.top_20_concentration = round(top_20_total, 2)
|
|
report.top_50_concentration = round(top_50_total, 2)
|
|
|
|
# Smart money estimation (heuristic)
|
|
known_smart = sum(1 for p in profiles if p.is_known_whale and not p.is_contract)
|
|
report.smart_money_holders = known_smart
|
|
report.dumb_money_holders = max(0, report.total_holders - known_smart)
|
|
report.smart_money_ratio = round(known_smart / len(profiles), 2) if profiles else 0
|
|
|
|
# Holder concentration risk
|
|
if top_10_total > 80:
|
|
report.holder_health_score = 10
|
|
self.critical_warnings.append(f"☠️ CATASTROPHIC CONCENTRATION: Top 10 holders own {top_10_total}%!")
|
|
elif top_10_total > 50:
|
|
report.holder_health_score = 30
|
|
self.critical_warnings.append(f"⚠️ DANGEROUS CONCENTRATION: Top 10 hold {top_10_total}%")
|
|
elif top_10_total > 30:
|
|
report.holder_health_score = 50
|
|
elif top_10_total > 20:
|
|
report.holder_health_score = 70
|
|
else:
|
|
report.holder_health_score = 90
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# PHASE 4: FUNDING TRACE
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
async def _phase4_funding(self, report: SecurityReport) -> None:
|
|
"""Trace deployer wallet funding sources."""
|
|
if not report.deployer:
|
|
# Try to find deployer from creation TX
|
|
creation_tx = self.solscan.get_token_creation_tx(report.token_address)
|
|
if creation_tx:
|
|
# The signer of the creation tx is the deployer
|
|
signers = creation_tx.get("signer", [])
|
|
if signers:
|
|
report.deployer = signers[0]
|
|
|
|
if not report.deployer:
|
|
logger.warning("Could not determine deployer address")
|
|
return
|
|
|
|
# Get deployer's funding sources
|
|
funding_sources = self.solscan.get_wallet_funding_sources(report.deployer, days=30)
|
|
|
|
# Get top transfers for deeper analysis
|
|
self.solscan.top_address_transfers(report.deployer, range_days=30)
|
|
|
|
exchange_count = 0
|
|
organic_count = 0
|
|
mixer_detected = False
|
|
trace = FundingTrace(wallet=report.deployer, initial_funder="")
|
|
|
|
# Analyze funding sources
|
|
for source in funding_sources[:20]:
|
|
from_addr = source.get("from", "")
|
|
exchange = is_known_exchange(from_addr)
|
|
|
|
if exchange:
|
|
exchange_count += 1
|
|
if not trace.is_exchange_direct:
|
|
trace.is_exchange_direct = True
|
|
trace.exchange_name = exchange
|
|
trace.initial_funder = from_addr
|
|
trace.funder_tag = exchange
|
|
trace.funding_amount_sol = source.get("amount", 0)
|
|
trace.funding_time = source.get("time")
|
|
trace.hops_from_exchange = 0
|
|
else:
|
|
organic_count += 1
|
|
if not trace.initial_funder:
|
|
trace.initial_funder = from_addr
|
|
trace.funding_amount_sol = source.get("amount", 0)
|
|
|
|
# Check for mixer patterns (round amounts, timing)
|
|
amount = source.get("amount", 0)
|
|
if isinstance(amount, (int, float)) and amount in [0.1, 0.2, 0.5, 1.0]:
|
|
trace.risk_flags.append(f"Suspicious round amount: {amount} SOL")
|
|
|
|
# Calculate percentages
|
|
total_sources = exchange_count + organic_count
|
|
if total_sources > 0:
|
|
report.exchange_funding_percentage = round(exchange_count / total_sources * 100, 1)
|
|
report.organic_funding_percentage = round(organic_count / total_sources * 100, 1)
|
|
|
|
report.deployer_funding = trace
|
|
report.mixer_funding_detected = mixer_detected
|
|
|
|
# Risk assessment
|
|
if trace.hops_from_exchange == 0 and exchange_count > 0:
|
|
report.funding_risk = "LOW" # Direct exchange = transparent
|
|
elif organic_count > exchange_count * 2:
|
|
report.funding_risk = "MEDIUM" # Mostly anonymous funding
|
|
self.critical_warnings.append("⚠️ Deployer funded from non-exchange sources - possible obfuscation")
|
|
elif mixer_detected:
|
|
report.funding_risk = "CRITICAL"
|
|
self.critical_warnings.append("🚨 MIXER FUNDING DETECTED: Deployer used a tumbler!")
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# PHASE 5: BUNDLER & SNIPER DETECTION
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
async def _phase5_bundlers_snipers(self, report: SecurityReport) -> None:
|
|
"""Detect launch bundlers and sniper wallets."""
|
|
# Get DeFi activities (swaps = buys/sells)
|
|
activities = self.solscan.token_defi_activities(report.token_address, page_size=200)
|
|
if not activities:
|
|
return
|
|
|
|
# Find earliest activities (launch window)
|
|
sorted_acts = sorted(activities, key=lambda x: x.get("block_time", 0))
|
|
if not sorted_acts:
|
|
return
|
|
|
|
# Define launch time as first activity
|
|
launch_time = sorted_acts[0].get("block_time", 0)
|
|
report.created_at = datetime.fromtimestamp(launch_time).isoformat() if launch_time else None
|
|
|
|
# Analyze first 30 buys (sniper window: first 30 seconds)
|
|
SNIPER_WINDOW_MS = 30000 # 30 seconds
|
|
sniper_candidates = []
|
|
|
|
buyer_timestamps = {} # wallet -> [buy_times]
|
|
|
|
for act in sorted_acts[:100]:
|
|
act_type = act.get("activity_type", "").upper()
|
|
if "BUY" not in act_type and "SWAP" not in act_type:
|
|
continue
|
|
|
|
buyer = act.get("from_address") or act.get("address")
|
|
block_time = act.get("block_time", 0)
|
|
time_after_launch_ms = (block_time - launch_time) * 1000 if launch_time else 0
|
|
|
|
# Track buyer timestamps for bundler detection
|
|
if buyer:
|
|
if buyer not in buyer_timestamps:
|
|
buyer_timestamps[buyer] = []
|
|
buyer_timestamps[buyer].append(
|
|
{
|
|
"time": block_time,
|
|
"amount_sol": act.get("value", 0),
|
|
"tx": act.get("trans_id"),
|
|
}
|
|
)
|
|
|
|
# Sniper detection: buys within first 30 seconds
|
|
if 0 <= time_after_launch_ms <= SNIPER_WINDOW_MS and buyer:
|
|
sniper = SniperProfile(
|
|
wallet=buyer,
|
|
buy_time_ms=round(time_after_launch_ms, 0),
|
|
buy_amount_sol=act.get("value", 0),
|
|
token_amount=act.get("token_amount", 0),
|
|
price_paid=act.get("value", 0) / max(act.get("token_amount", 1), 1),
|
|
)
|
|
sniper_candidates.append(sniper)
|
|
|
|
# Bundler detection: wallets buying at nearly identical timestamps
|
|
bundler_clusters = []
|
|
for wallet, buys in buyer_timestamps.items():
|
|
if len(buys) >= 2:
|
|
# Check if multiple buys within 2 seconds = bundler pattern
|
|
timestamps = sorted([b["time"] for b in buys])
|
|
for i in range(len(timestamps) - 1):
|
|
if timestamps[i + 1] - timestamps[i] <= 2:
|
|
bundler_clusters.append(wallet)
|
|
break
|
|
|
|
# Also detect: many wallets buying within same 3-second window
|
|
launch_window_buys = defaultdict(list)
|
|
for wallet, buys in buyer_timestamps.items():
|
|
for b in buys:
|
|
window_key = int(b["time"] / 3) * 3 # 3-second buckets
|
|
launch_window_buys[window_key].append(wallet)
|
|
|
|
for _window, wallets in launch_window_buys.items():
|
|
if len(wallets) >= 5: # 5+ wallets in same 3s window = bundler
|
|
bundler_clusters.extend(wallets)
|
|
|
|
# Deduplicate
|
|
report.bundler_wallets = list(set(bundler_clusters))
|
|
report.bundler_detected = len(report.bundler_wallets) > 0
|
|
|
|
if report.bundler_detected:
|
|
# Calculate bundler impact
|
|
bundler_supply_pct = len(report.bundler_wallets) * 0.5 # rough estimate
|
|
report.bundler_percentage_of_supply = min(bundler_supply_pct, 100)
|
|
report.bundler_pattern = "synchronized_launch" if len(report.bundler_wallets) >= 5 else "clustered_buys"
|
|
report.bundler_risk = "HIGH" if len(report.bundler_wallets) >= 10 else "MEDIUM"
|
|
self.critical_warnings.append(
|
|
f"🎯 BUNDLER DETECTED: {len(report.bundler_wallets)} wallets bought in coordinated pattern!"
|
|
)
|
|
|
|
# Sniper summary
|
|
report.snipers = sniper_candidates[:20]
|
|
report.sniper_count = len(sniper_candidates)
|
|
if sniper_candidates:
|
|
report.avg_sniper_entry_time_ms = round(statistics.mean(s.buy_time_ms for s in sniper_candidates), 0)
|
|
report.sniper_volume_sol = round(sum(s.buy_amount_sol for s in sniper_candidates), 2)
|
|
|
|
if report.sniper_count >= 15:
|
|
report.sniper_risk = "HIGH"
|
|
elif report.sniper_count >= 8:
|
|
report.sniper_risk = "MEDIUM"
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# PHASE 6: LIQUIDITY ANALYSIS
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
async def _phase6_liquidity(self, report: SecurityReport) -> None:
|
|
"""Analyze LP depth, lock status, and concentration."""
|
|
lp_data = self.solscan.get_lp_token_accounts(report.token_address)
|
|
|
|
total_liq = 0.0
|
|
for lp in lp_data:
|
|
tvl = lp.get("tvl_usd", 0) or 0
|
|
total_liq += tvl
|
|
|
|
analysis = LPAnalysis(
|
|
pool_address=lp.get("market_id", ""),
|
|
dex_name=lp.get("program", "Unknown"),
|
|
token_a_amount=lp.get("liquidity_a", 0) or 0,
|
|
token_b_amount=lp.get("liquidity_b", 0) or 0,
|
|
tvl_usd=tvl,
|
|
lp_tokens_total=0,
|
|
lp_tokens_locked=0,
|
|
)
|
|
|
|
# Check lock status
|
|
if lp.get("lp_locked"):
|
|
analysis.lp_locked_percentage = 100.0
|
|
analysis.is_burned = lp.get("lpLocked") == "burned"
|
|
else:
|
|
analysis.lp_locked_percentage = 0.0
|
|
|
|
# Depth score based on TVL
|
|
if tvl > 100000:
|
|
analysis.depth_score = 90
|
|
analysis.concentration_risk = "LOW"
|
|
elif tvl > 50000:
|
|
analysis.depth_score = 70
|
|
analysis.concentration_risk = "MEDIUM"
|
|
elif tvl > 10000:
|
|
analysis.depth_score = 50
|
|
analysis.concentration_risk = "HIGH"
|
|
else:
|
|
analysis.depth_score = 20
|
|
analysis.concentration_risk = "CRITICAL"
|
|
|
|
report.liquidity_pools.append(analysis)
|
|
|
|
report.total_liquidity_usd = total_liq
|
|
|
|
# LP risk assessment
|
|
locked_pools = sum(1 for p in report.liquidity_pools if p.lp_locked_percentage > 80)
|
|
total_pools = len(report.liquidity_pools)
|
|
|
|
if total_pools == 0:
|
|
report.lp_risk = "CRITICAL"
|
|
self.critical_warnings.append("🚨 NO LIQUIDITY POOLS FOUND!")
|
|
elif locked_pools == 0:
|
|
report.lp_risk = "CRITICAL"
|
|
self.critical_warnings.append("🚨 LP NOT LOCKED: Dev can rug liquidity at any moment!")
|
|
elif total_liq < 10000:
|
|
report.lp_risk = "HIGH"
|
|
self.critical_warnings.append(f"⚠️ LOW LIQUIDITY: Only ${total_liq:,.0f} TVL")
|
|
elif total_liq < 50000:
|
|
report.lp_risk = "MEDIUM"
|
|
else:
|
|
report.lp_risk = "LOW"
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# PHASE 7: ADVANCED DETECTION
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
async def _phase7_advanced(self, report: SecurityReport, quick: bool = False) -> None:
|
|
"""Insider trading, wash trading, dev history, honeypot detection."""
|
|
|
|
# ── Insider Trading ──────────────────────────────────────────────────
|
|
await self._detect_insiders(report)
|
|
|
|
# ── Wash Trading (skip in quick mode) ───────────────────────────────
|
|
if not quick:
|
|
await self._detect_wash_trading(report)
|
|
|
|
# ── Dev History ──────────────────────────────────────────────────────
|
|
await self._analyze_dev_history(report)
|
|
|
|
# ── Tax & Honeypot ───────────────────────────────────────────────────
|
|
await self._detect_tax_and_honeypot(report)
|
|
|
|
async def _detect_insiders(self, report: SecurityReport) -> None:
|
|
"""Detect wallets that bought suspiciously early."""
|
|
# Insiders = wallets that bought before public awareness
|
|
# Heuristic: wallets that bought within first 60s with no prior token history
|
|
activities = self.solscan.token_defi_activities(report.token_address, page_size=100)
|
|
if not activities:
|
|
return
|
|
|
|
sorted_acts = sorted(activities, key=lambda x: x.get("block_time", 0))
|
|
if not sorted_acts:
|
|
return
|
|
|
|
launch_time = sorted_acts[0].get("block_time", 0)
|
|
INSIDER_WINDOW = 120 # 2 minutes
|
|
|
|
insiders = []
|
|
for act in sorted_acts:
|
|
act_type = str(act.get("activity_type", "")).upper()
|
|
if "BUY" not in act_type and "SWAP" not in act_type:
|
|
continue
|
|
|
|
block_time = act.get("block_time", 0)
|
|
elapsed = block_time - launch_time
|
|
|
|
if 0 <= elapsed <= INSIDER_WINDOW:
|
|
buyer = act.get("from_address") or act.get("address")
|
|
if buyer and buyer not in insiders:
|
|
# Check if this wallet was created very recently
|
|
acct_info = self.solscan.account_info(buyer)
|
|
if acct_info:
|
|
# Fresh wallet = strong insider signal
|
|
acct_info.get("lamports", 0)
|
|
# If wallet has exactly the buy amount = funded just for this
|
|
insiders.append(buyer)
|
|
|
|
report.insiders_detected = insiders[:10]
|
|
if len(insiders) >= 5:
|
|
report.insider_risk = "HIGH"
|
|
self.critical_warnings.append(f"🕵️ INSIDER TRADING: {len(insiders)} wallets bought within 2min of launch")
|
|
elif len(insiders) >= 2:
|
|
report.insider_risk = "MEDIUM"
|
|
|
|
async def _detect_wash_trading(self, report: SecurityReport) -> None:
|
|
"""Detect circular trading patterns."""
|
|
transfers = self.solscan.token_transfer_summary(report.token_address)
|
|
if not transfers:
|
|
return
|
|
|
|
# Get recent transfers for pattern analysis
|
|
recent = self.solscan.account_transfers(report.token_address, page_size=200)
|
|
if not recent:
|
|
return
|
|
|
|
# Build transfer graph
|
|
graph = defaultdict(lambda: defaultdict(int))
|
|
for tx in recent[:200]:
|
|
from_addr = tx.get("from_address", "")
|
|
to_addr = tx.get("to_address", "")
|
|
amount = float(tx.get("change_amount", 0))
|
|
if from_addr and to_addr and amount > 0:
|
|
graph[from_addr][to_addr] += 1
|
|
|
|
# Find cycles (A -> B -> A)
|
|
clusters = []
|
|
visited = set()
|
|
for wallet_a in list(graph.keys())[:50]:
|
|
if wallet_a in visited:
|
|
continue
|
|
for wallet_b, count_ab in graph[wallet_a].items():
|
|
if wallet_b in graph and wallet_a in graph[wallet_b]:
|
|
count_ba = graph[wallet_b][wallet_a]
|
|
if count_ab >= 2 and count_ba >= 2:
|
|
cluster = WashTradeCluster(
|
|
wallets=[wallet_a, wallet_b],
|
|
trade_count=count_ab + count_ba,
|
|
volume_usd=0,
|
|
pattern_type="back_forth",
|
|
confidence=min(0.95, (count_ab + count_ba) / 10),
|
|
)
|
|
clusters.append(cluster)
|
|
visited.add(wallet_a)
|
|
visited.add(wallet_b)
|
|
|
|
report.wash_trade_clusters = clusters[:5]
|
|
if clusters:
|
|
report.wash_trading_volume_usd = sum(c.volume_usd for c in clusters)
|
|
total_volume = report.volume_24h or 1
|
|
wash_pct = report.wash_trading_volume_usd / total_volume * 100
|
|
|
|
if wash_pct > 50:
|
|
report.wash_trading_risk = "CRITICAL"
|
|
self.critical_warnings.append(f"🔄 WASH TRADING: {wash_pct:.0f}% of volume appears artificial!")
|
|
elif wash_pct > 20:
|
|
report.wash_trading_risk = "HIGH"
|
|
elif wash_pct > 5:
|
|
report.wash_trading_risk = "MEDIUM"
|
|
|
|
async def _analyze_dev_history(self, report: SecurityReport) -> None:
|
|
"""Check deployer's history for other tokens and potential rugs."""
|
|
if not report.deployer:
|
|
return
|
|
|
|
# Get deployer's recent transactions
|
|
txs = self.solscan.get_deployer_transactions(report.deployer, pages=3)
|
|
|
|
token_creations = []
|
|
for tx in txs:
|
|
# Look for token creation patterns in tx history
|
|
tx_hash = tx.get("txHash", "")
|
|
if not tx_hash:
|
|
continue
|
|
|
|
detail = self.solscan.transaction_detail(tx_hash)
|
|
if detail and "Tokenkeg" in str(detail):
|
|
# Potentially a token creation
|
|
programs = detail.get("parsedInstructions", [])
|
|
for inst in programs:
|
|
if "initializeMint" in str(inst):
|
|
token_creations.append(tx_hash)
|
|
|
|
report.dev_other_tokens = token_creations[:10]
|
|
|
|
# If dev has created many tokens, flag it
|
|
if len(token_creations) >= 5:
|
|
report.dev_risk = "HIGH"
|
|
report.dev_is_known_scammer = True
|
|
report.dev_rug_history = len(token_creations) - 1 # estimate
|
|
self.critical_warnings.append(
|
|
f"👺 DEV SERIAL LAUNCHER: Created {len(token_creations)} tokens - "
|
|
f"likely a token factory / rug operation"
|
|
)
|
|
elif len(token_creations) >= 2:
|
|
report.dev_risk = "MEDIUM"
|
|
|
|
async def _detect_tax_and_honeypot(self, report: SecurityReport) -> None:
|
|
"""Attempt to detect taxes and honeypot behavior."""
|
|
# Tax detection: compare buy vs sell amounts in recent txs
|
|
activities = self.solscan.token_defi_activities(report.token_address, page_size=100)
|
|
if not activities:
|
|
return
|
|
|
|
buys = []
|
|
sells = []
|
|
for act in activities:
|
|
act_type = str(act.get("activity_type", "")).upper()
|
|
value = act.get("value", 0)
|
|
if "BUY" in act_type or "IN" in act_type:
|
|
buys.append(value)
|
|
elif "SELL" in act_type or "OUT" in act_type:
|
|
sells.append(value)
|
|
|
|
# If significantly more buys than sells, possible honeypot
|
|
if len(buys) > 10 and len(sells) < 2:
|
|
report.honeypot_detected = True
|
|
report.can_sell = False
|
|
self.critical_warnings.append("🍯 HONEYPOT DETECTED: Many buys but almost no sells!")
|
|
report.tax_risk = "CRITICAL"
|
|
elif len(buys) > 10 and len(sells) > 0:
|
|
# Check if sells are much smaller than buys (heavy tax)
|
|
avg_buy = statistics.mean(buys) if buys else 0
|
|
avg_sell = statistics.mean(sells) if sells else 0
|
|
if avg_buy > 0 and avg_sell > 0:
|
|
ratio = avg_sell / avg_buy
|
|
if ratio < 0.5:
|
|
# Possible high sell tax
|
|
estimated_tax = (1 - ratio) * 100
|
|
report.sell_tax = round(estimated_tax, 1)
|
|
if estimated_tax > 20:
|
|
report.tax_risk = "HIGH"
|
|
self.critical_warnings.append(f"💸 HEAVY SELL TAX ESTIMATED: ~{estimated_tax:.0f}%")
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# FINAL SCORING
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
def _calculate_final_scores(self, report: SecurityReport) -> None:
|
|
"""Calculate the ultimate RUG PULL PROBABILITY score."""
|
|
score = 0
|
|
|
|
# Authority risk (20 pts)
|
|
auth_map = {"LOW": 0, "MEDIUM": 10, "HIGH": 15, "CRITICAL": 20}
|
|
score += auth_map.get(report.authorities_risk, 0)
|
|
|
|
# Holder concentration (15 pts)
|
|
score += min(report.top_10_concentration * 0.15, 15)
|
|
|
|
# Funding risk (10 pts)
|
|
fund_map = {"LOW": 0, "MEDIUM": 5, "HIGH": 8, "CRITICAL": 10}
|
|
score += fund_map.get(report.funding_risk, 0)
|
|
|
|
# Bundler risk (15 pts)
|
|
if report.bundler_detected:
|
|
score += min(len(report.bundler_wallets) * 1.5, 15)
|
|
|
|
# Sniper risk (5 pts)
|
|
sniper_map = {"LOW": 0, "MEDIUM": 3, "HIGH": 5}
|
|
score += sniper_map.get(report.sniper_risk, 0)
|
|
|
|
# Liquidity risk (15 pts)
|
|
lp_map = {"LOW": 0, "MEDIUM": 7, "HIGH": 12, "CRITICAL": 15}
|
|
score += lp_map.get(report.lp_risk, 0)
|
|
|
|
# Insider risk (5 pts)
|
|
insider_map = {"LOW": 0, "MEDIUM": 3, "HIGH": 5}
|
|
score += insider_map.get(report.insider_risk, 0)
|
|
|
|
# Wash trading (5 pts)
|
|
wash_map = {"LOW": 0, "MEDIUM": 3, "HIGH": 4, "CRITICAL": 5}
|
|
score += wash_map.get(report.wash_trading_risk, 0)
|
|
|
|
# Tax/honeypot (5 pts)
|
|
if report.honeypot_detected:
|
|
score += 5
|
|
else:
|
|
tax_map = {"LOW": 0, "MEDIUM": 2, "HIGH": 4, "CRITICAL": 5}
|
|
score += tax_map.get(report.tax_risk, 0)
|
|
|
|
# Dev risk (5 pts)
|
|
dev_map = {"LOW": 0, "MEDIUM": 3, "HIGH": 5}
|
|
score += dev_map.get(report.dev_risk, 0)
|
|
|
|
# Cap and finalize
|
|
report.rug_pull_probability = min(int(score), 100)
|
|
|
|
if report.rug_pull_probability >= 70:
|
|
report.overall_risk = "CRITICAL"
|
|
self.recommendations.insert(0, "🛑 DO NOT INVEST - EXTREME RISK")
|
|
elif report.rug_pull_probability >= 50:
|
|
report.overall_risk = "HIGH"
|
|
self.recommendations.insert(0, "⚠️ HIGH RISK - Only gamble what you can lose")
|
|
elif report.rug_pull_probability >= 30:
|
|
report.overall_risk = "MEDIUM"
|
|
else:
|
|
report.overall_risk = "LOW"
|
|
|
|
# Confidence level
|
|
data_sources = sum(
|
|
[
|
|
report.total_holders > 0,
|
|
report.total_liquidity_usd > 0,
|
|
len(report.top_holders) > 0,
|
|
report.mint_authority is not None,
|
|
]
|
|
)
|
|
if data_sources >= 4:
|
|
report.confidence_level = "HIGH"
|
|
elif data_sources >= 2:
|
|
report.confidence_level = "MEDIUM"
|
|
else:
|
|
report.confidence_level = "LOW"
|
|
|
|
# Final recommendations
|
|
if not report.mint_authority_renounced:
|
|
self.recommendations.append("Verify mint authority renouncement on-chain")
|
|
if report.total_liquidity_usd < 50000:
|
|
self.recommendations.append("Low liquidity - large sells will cause massive slippage")
|
|
if report.top_10_concentration > 40:
|
|
self.recommendations.append("Top-heavy distribution - watch for whale dumps")
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# EXTERNAL API HELPERS
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
async def _helius_token_metadata(self, token_address: str) -> dict | None:
|
|
"""Fetch token metadata from Helius."""
|
|
if not self.helius_key:
|
|
return None
|
|
try:
|
|
url = f"https://mainnet.helius-rpc.com/?api-key={self.helius_key}"
|
|
payload = {
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "getAsset",
|
|
"params": {"id": token_address},
|
|
}
|
|
# Synchronous fallback
|
|
resp = httpx.post(url, json=payload, timeout=10)
|
|
if resp.status_code == 200:
|
|
result = resp.json().get("result")
|
|
if result:
|
|
return {
|
|
"name": result.get("content", {}).get("metadata", {}).get("name"),
|
|
"symbol": result.get("content", {}).get("metadata", {}).get("symbol"),
|
|
"decimals": result.get("token_info", {}).get("decimals", 0),
|
|
"mintAuthority": result.get("mint_extensions", {}).get("mintAuthority"),
|
|
"freezeAuthority": result.get("mint_extensions", {}).get("freezeAuthority"),
|
|
}
|
|
except Exception as e:
|
|
logger.warning(f"Helius metadata failed: {e}")
|
|
return None
|
|
|
|
async def _birdeye_token_data(self, token_address: str) -> dict | None:
|
|
"""Fetch market data from Birdeye."""
|
|
if not self.birdeye_key:
|
|
return None
|
|
try:
|
|
headers = {"X-API-KEY": self.birdeye_key}
|
|
# Price data
|
|
price_resp = httpx.get(
|
|
f"https://public-api.birdeye.so/defi/price?address={token_address}",
|
|
headers=headers,
|
|
timeout=10,
|
|
)
|
|
# Token overview
|
|
overview_resp = httpx.get(
|
|
f"https://public-api.birdeye.so/defi/token_overview?address={token_address}",
|
|
headers=headers,
|
|
timeout=10,
|
|
)
|
|
|
|
result = {}
|
|
if price_resp.status_code == 200:
|
|
p_data = price_resp.json().get("data", {})
|
|
result["price"] = p_data.get("value", 0)
|
|
result["priceChange24h"] = p_data.get("priceChange24h", 0)
|
|
|
|
if overview_resp.status_code == 200:
|
|
o_data = overview_resp.json().get("data", {})
|
|
result["mc"] = o_data.get("mc", 0)
|
|
result["v24hUSD"] = o_data.get("v24hUSD", 0)
|
|
result["liquidity"] = o_data.get("liquidity", 0)
|
|
result["buySellRatio"] = o_data.get("buySellRatio", 1.0)
|
|
result["holders"] = o_data.get("holder", 0)
|
|
|
|
return result if result else None
|
|
except Exception as e:
|
|
logger.warning(f"Birdeye data failed: {e}")
|
|
return None
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
# CONVENIENCE FUNCTIONS
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
async def scan_token(token_address: str, quick: bool = False) -> SecurityReport:
|
|
"""One-shot token scan."""
|
|
scanner = DegenSecurityScanner()
|
|
return await scanner.full_scan(token_address, quick=quick)
|
|
|
|
|
|
def format_report(report: SecurityReport) -> str:
|
|
"""Format a security report for display."""
|
|
lines = [
|
|
"╔══════════════════════════════════════════════════════════════════════╗",
|
|
"║ RUG MUNCH INTELLIGENCE - DEGEN SECURITY SCAN ║",
|
|
"╚══════════════════════════════════════════════════════════════════════╝",
|
|
"",
|
|
f"🪙 Token: {report.token_name} (${report.token_symbol})",
|
|
f"📍 Address: {report.token_address}",
|
|
f"📊 Supply: {report.total_supply:,.0f} | Decimals: {report.decimals}",
|
|
f"👥 Holders: {report.total_holders:,}",
|
|
"",
|
|
"━━━ AUTHORITY STATUS ━━━",
|
|
f" Mint Authority: {'✅ RENOUNCED' if report.mint_authority_renounced else '❌ ACTIVE'}",
|
|
f" Freeze Authority: {'✅ RENOUNCED' if report.freeze_authority_renounced else '❌ ACTIVE'}",
|
|
f" Risk: {report.authorities_risk}",
|
|
"",
|
|
"━━━ HOLDER CONCENTRATION ━━━",
|
|
f" Top 10: {report.top_10_concentration}% | Top 20: {report.top_20_concentration}% | Top 50: {report.top_50_concentration}%",
|
|
f" Health Score: {report.holder_health_score}/100",
|
|
f" Smart Money: {report.smart_money_holders} wallets",
|
|
"",
|
|
"━━━ FUNDING TRACE ━━━",
|
|
f" Exchange Funding: {report.exchange_funding_percentage}%",
|
|
f" Organic Funding: {report.organic_funding_percentage}%",
|
|
f" Mixer Used: {'YES 🚨' if report.mixer_funding_detected else 'No'}",
|
|
f" Risk: {report.funding_risk}",
|
|
"",
|
|
"━━━ BUNDLER DETECTION ━━━",
|
|
f" Detected: {'YES 🎯' if report.bundler_detected else 'No'}",
|
|
]
|
|
if report.bundler_detected:
|
|
lines.extend(
|
|
[
|
|
f" Bundler Wallets: {len(report.bundler_wallets)}",
|
|
f" Pattern: {report.bundler_pattern}",
|
|
f" Risk: {report.bundler_risk}",
|
|
]
|
|
)
|
|
lines.extend(
|
|
[
|
|
"",
|
|
"━━━ SNIPER ANALYSIS ━━━",
|
|
f" Snipers Detected: {report.sniper_count}",
|
|
f" Avg Entry Time: {report.avg_sniper_entry_time_ms / 1000:.1f}s after launch",
|
|
f" Sniper Volume: {report.sniper_volume_sol:.2f} SOL",
|
|
f" Risk: {report.sniper_risk}",
|
|
"",
|
|
"━━━ LIQUIDITY ANALYSIS ━━━",
|
|
f" Total TVL: ${report.total_liquidity_usd:,.0f}",
|
|
f" Pools: {len(report.liquidity_pools)}",
|
|
]
|
|
)
|
|
for lp in report.liquidity_pools:
|
|
lines.append(
|
|
f" • {lp.dex_name}: ${lp.tvl_usd:,.0f} TVL | Locked: {lp.lp_locked_percentage}% | Depth: {lp.depth_score}/100"
|
|
)
|
|
lines.extend(
|
|
[
|
|
f" Risk: {report.lp_risk}",
|
|
"",
|
|
"━━━ ADVANCED DETECTION ━━━",
|
|
f" Insiders: {len(report.insiders_detected)} wallets | Risk: {report.insider_risk}",
|
|
f" Wash Clusters: {len(report.wash_trade_clusters)} | Risk: {report.wash_trading_risk}",
|
|
f" Dev Other Tokens: {len(report.dev_other_tokens)} | Risk: {report.dev_risk}",
|
|
f" Buy Tax: {report.buy_tax}% | Sell Tax: {report.sell_tax}%",
|
|
f" Honeypot: {'YES 🍯' if report.honeypot_detected else 'No'}",
|
|
"",
|
|
"━━━ MARKET DATA ━━━",
|
|
f" Price: ${report.price_usd:.10f}" if report.price_usd < 0.001 else f" Price: ${report.price_usd:.6f}",
|
|
f" Market Cap: ${report.market_cap:,.0f}",
|
|
f" 24h Volume: ${report.volume_24h:,.0f}",
|
|
f" 24h Change: {report.price_change_24h:+.2f}%",
|
|
f" Buy/Sell Ratio: {report.buy_sell_ratio:.2f}",
|
|
"",
|
|
"━━━ FINAL VERDICT ━━━",
|
|
f" RUG PULL PROBABILITY: {report.rug_pull_probability}%",
|
|
f" OVERALL RISK: {report.overall_risk}",
|
|
f" Confidence: {report.confidence_level}",
|
|
f" Scan Time: {report.scan_duration_ms}ms",
|
|
"",
|
|
]
|
|
)
|
|
|
|
if report.critical_warnings:
|
|
lines.extend(
|
|
[
|
|
"━━━ ⚠️ CRITICAL WARNINGS ⚠️ ━━━",
|
|
*[f" {w}" for w in report.critical_warnings[:10]],
|
|
"",
|
|
]
|
|
)
|
|
|
|
if report.recommendations:
|
|
lines.extend(
|
|
[
|
|
"━━━ 💡 RECOMMENDATIONS ━━━",
|
|
*[f" {r}" for r in report.recommendations[:8]],
|
|
"",
|
|
]
|
|
)
|
|
|
|
lines.extend(
|
|
[
|
|
"═══════════════════════════════════════════════════════════════",
|
|
" Powered by Rug Munch Intelligence - Protecting Degens Since 2024",
|
|
"═══════════════════════════════════════════════════════════════",
|
|
]
|
|
)
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
# CLI / TEST
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
if __name__ == "__main__":
|
|
# Test scan on a known token
|
|
test_token = "So11111111111111111111111111111111111111112" # Wrapped SOL
|
|
if len(sys.argv) > 1:
|
|
test_token = sys.argv[1]
|
|
|
|
print(f"Testing Degen Security Scanner on: {test_token}")
|
|
report = asyncio.run(scan_token(test_token, quick=True))
|
|
print(format_report(report))
|