solwatch 0.1.18

Real-data Solana memecoin auditor — rug/freeze/bundle scanner with a 0-100 risk score, plain-English flags, and a live dashboard. Sister tool to Hoodwatch.
Documentation
//! Solana mainnet constants + endpoint config. All endpoints are public and
//! tokenless; override via env for a paid RPC (Helius etc.) when rate limits bite.

pub const CHAIN: &str = "solana";

/// True when a dedicated (non-public) RPC is configured — an explicit
/// SOLWATCH_RPC_URL or a Helius key. Such endpoints tolerate far higher
/// concurrency than the public `api.mainnet-beta` (which throttles ~4 bursts).
pub fn has_dedicated_rpc() -> bool {
    std::env::var("SOLWATCH_RPC_URL")
        .map(|s| !s.trim().is_empty())
        .unwrap_or(false)
        || std::env::var("HELIUS_API_KEY")
            .map(|s| !s.trim().is_empty())
            .unwrap_or(false)
}

pub fn rpc_url() -> String {
    if let Ok(u) = std::env::var("SOLWATCH_RPC_URL") {
        if !u.trim().is_empty() {
            return u;
        }
    }
    // A Helius key (free tier) removes the public endpoint's method blocks —
    // used automatically when present, never required.
    if let Ok(k) = std::env::var("HELIUS_API_KEY") {
        if !k.trim().is_empty() {
            return format!("https://mainnet.helius-rpc.com/?api-key={}", k.trim());
        }
    }
    "https://api.mainnet-beta.solana.com".to_string()
}

/// Global RPC concurrency. Auto-scales to the endpoint: a dedicated/keyed RPC
/// (Helius etc.) tolerates a much wider burst than the public endpoint's ~4, so
/// when a key is present we USE that headroom — otherwise the key removes
/// throttling but the pass still crawls at the public-tuned width. Explicit
/// SOLWATCH_RPC_CONCURRENCY always wins.
pub fn rpc_concurrency() -> usize {
    if let Some(n) = std::env::var("SOLWATCH_RPC_CONCURRENCY")
        .ok()
        .and_then(|s| s.parse().ok())
        .filter(|n| *n >= 1 && *n <= 64)
    {
        return n;
    }
    // 8 suits a Helius FREE tier (~10 RPS) — 2× the public width without
    // constantly overrunning the RPS ceiling. A paid tier can push higher via
    // SOLWATCH_RPC_CONCURRENCY.
    if has_dedicated_rpc() {
        8
    } else {
        4
    }
}

/// Wall-clock budget for the whole audit, in milliseconds. Once elapsed, the
/// chain client stops issuing new RPC work and every pass winds down to emit an
/// honest PARTIAL result — a labeled partial beats the Rokha sandbox killing the
/// process at its command timeout and returning nothing. `0` disables the budget
/// (unbounded).
///
/// Auto-aligns to the sandbox's own per-command wall clock when present
/// (`ROKHA_CMD_TIMEOUT_MS`, which the runtime sets to 120s free / 240s pro):
/// we self-terminate a safety margin BEFORE that hard kill, so pro runs get
/// their full budget instead of being clipped at a hardcoded default. Explicit
/// SOLWATCH_DEADLINE_MS always wins.
pub fn deadline_ms() -> u64 {
    if let Ok(s) = std::env::var("SOLWATCH_DEADLINE_MS") {
        if let Ok(n) = s.parse() {
            return n;
        }
    }
    if let Ok(s) = std::env::var("ROKHA_CMD_TIMEOUT_MS") {
        if let Ok(n) = s.parse::<u64>() {
            // Leave ~10s to serialize + POST the partial result back.
            return n.saturating_sub(10_000).max(15_000);
        }
    }
    90_000
}

pub fn jupiter_base() -> String {
    std::env::var("SOLWATCH_JUPITER").unwrap_or_else(|_| "https://lite-api.jup.ag".to_string())
}

/// The audit's "view this in Rokha" link, or None.
///
/// There is deliberately NO fabricated default. An earlier version emitted
/// `https://rokha.ai/?app=solwatch&token=<mint>` — **that route does not
/// exist**, so every audit advertised a dead link. The real shareable surface
/// is the rig's product page (`rokha.ai/@<handle>/<rig-slug>`), which only
/// exists once its owner mints the link — an owner-specific fact this binary
/// cannot know. So the owner supplies it:
///
/// ```text
/// SOLWATCH_ROKHA_URL=https://rokha.ai/@you/solwatch-audit
/// ```
///
/// Unset (the default) => the field is omitted entirely. Never advertise a URL
/// we have not been told is real.
pub fn rokha_link() -> Option<String> {
    std::env::var("SOLWATCH_ROKHA_URL")
        .ok()
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty())
}

pub const DEXSCREENER_BASE: &str = "https://api.dexscreener.com";
pub const DEXSCREENER_CHAIN: &str = "solana";

// Core program ids.
pub const SYSTEM_PROGRAM: &str = "11111111111111111111111111111111";
pub const SPL_TOKEN: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
pub const TOKEN_2022: &str = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
pub const METAPLEX_METADATA: &str = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s";
pub const ASSOCIATED_TOKEN: &str = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
pub const PUMPFUN_PROGRAM: &str = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P";

/// Burn / dead destinations excluded from holder math.
pub const INCINERATOR: &str = "1nc1nerator11111111111111111111111111111111";

/// Shared vault-AUTHORITY PDAs that own an AMM's token vaults. Unlike the
/// per-pool programs in `pool_program_labels` (matched via the owner account's
/// program owner), these are matched on the OWNER ADDRESS itself: Raydium's
/// AMM v4 authority has no on-chain account at all and the CPMM authority is
/// system-owned, so neither is "program-owned" — without this list a Raydium
/// pool's vault ranks as a human whale and fabricates a concentration scam
/// (verified live 2026-07-15: 5Q544f… account absent, GpMZbSM… system-owned).
pub fn shared_pool_authorities() -> Vec<(&'static str, &'static str)> {
    vec![
        (
            "5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1",
            "Raydium AMM v4 vault",
        ),
        (
            "GpMZbSM2GgvTKHJirzeGfMFoaZ8UR2X7F4v8vHTvxFbL",
            "Raydium CPMM vault",
        ),
    ]
}

/// Token-locker / vesting program owners: a top holder whose owner account is
/// owned by one of these is a LOCK (team vesting, escrow), not a whale —
/// excluded from concentration math and never clustered.
pub fn locker_program_labels() -> Vec<(&'static str, &'static str)> {
    vec![
        (
            "strmRqUCoQUgGUan5YhzUZa6KqdzwX5L6FpUxfmKg5m",
            "Streamflow lock",
        ),
        (
            "LocpQgucEQHbqNABEYvBvwoxCPsSbG91A1QaQhQQqjn",
            "Jupiter Lock",
        ),
        (
            "TokenVestA6qNSLDGRc4KzMRWfPXGqmDFj2CJYhKRWH",
            "Bonfida token vesting",
        ),
        (
            "CChTq6PthWEHaGSa2cbKDBw3JfaYspbC24oZQYZ1kFa5",
            "Bonfida vesting (legacy)",
        ),
    ]
}

/// Jito tip accounts (static, 8). A launch-window transaction paying one of
/// these is a Jito-bundled transaction — the atomic-coordination fingerprint.
pub const JITO_TIP_ACCOUNTS: [&str; 8] = [
    "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5",
    "HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe",
    "Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY",
    "ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49",
    "DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh",
    "ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt",
    "DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL",
    "3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT",
];

/// Jito bundle-proof API (per-transaction bundle lookup). Third-party — every
/// use degrades gracefully to the tip heuristic when unreachable.
pub const JITO_BUNDLES_API: &str = "https://bundles.jito.wtf/api/v1/bundles/transaction";

/// Known CEX / bridge hot wallets — cluster STOP-NODES. Two wallets funded by
/// an exchange withdrawal wallet are NOT the same person; unioning through one
/// of these would merge half of Solana. Rendered as `cex` nodes, never unioned.
pub fn cex_hot_wallets() -> Vec<(&'static str, &'static str)> {
    vec![
        ("5tzFkiKscXHK5ZXCGbXZxdw7gTjjD1mBwuoFbhUvuAi9", "Binance"),
        ("2ojv9BAiHUrvsm9gxDe7fJSzbNZSJcxZvf8dqmWGHG8S", "Binance"),
        ("9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM", "Binance"),
        ("AC5RDfQFmDS1deWZos921JfqscXdByf8BKHs5ACWjtW2", "Bybit"),
        ("42brAgAVNzMBP7aaktPvAmBSPEkehnFQejiZc53EpJFd", "Bybit"),
        ("H8sMJSCQxfKiFTCfDR3DUMLPwcRbM61LGFJ8N4dK3WjS", "Coinbase"),
        ("2AQdpHJ2JpcEgPiATUXjQxA8QmafFegfQwSLWSprPicm", "Coinbase"),
        (
            "GJRs4FwHtemZ5ZE9x3FNvJ8TMwitKTh21yxdRPqn7npE",
            "Coinbase Hot",
        ),
        ("5VCwKtCXgCJ6kit5FybXjvriW3xELsFDhYrPSqtJNmcD", "OKX"),
        ("9un5wqE3q4oCjyrDkwsdD48KteCJitQX5978Vh7KKxHo", "OKX 2"),
        ("FWznbcNXWQuHTawe9RxvQ2LdCENssh12dsznf4RiouN5", "Kraken"),
        ("BmFdpraQhkiDQE6SnfG5omcA1VwzqfXrwtNYBwWTymy6", "Kucoin"),
        ("ASTyfSima4LLAdDgoFGkgqoKowG1LZFDr9fAQrg7iaJZ", "MEXC"),
        ("5PAhQiYdLBd6SVdjzBQDxUAEFyDdF5ExNPQfcscnPRj5", "Gate.io"),
        ("u6PJ8DtQuPFnfmwHbGFULQ4u4EgjDiyYKjVEsynXq2w", "Gate.io 2"),
    ]
}

/// Programs that own AMM pools / bonding curves / vault authorities. A top
/// holder whose OWNER account is owned by one of these is a liquidity pool,
/// not a person — excluded from concentration math (and labeled).
pub fn pool_program_labels() -> Vec<(&'static str, &'static str)> {
    vec![
        (
            "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P",
            "pump.fun bonding curve",
        ),
        (
            "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA",
            "PumpSwap pool",
        ),
        (
            "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8",
            "Raydium AMM v4",
        ),
        (
            "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK",
            "Raydium CLMM",
        ),
        (
            "CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C",
            "Raydium CPMM",
        ),
        (
            "LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj",
            "Raydium LaunchLab",
        ),
        (
            "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc",
            "Orca Whirlpool",
        ),
        ("9W959DqEETiGZocYWCQPaJ6sBmUzgfxXfqGeTEdp3aQP", "Orca v2"),
        (
            "Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB",
            "Meteora DAMM",
        ),
        (
            "LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo",
            "Meteora DLMM",
        ),
        (
            "cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG",
            "Meteora DAMM v2",
        ),
        ("dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN", "Meteora DBC"),
        (
            "srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX",
            "OpenBook/Serum",
        ),
        ("PhoeNiXZ8ByJGLkxNfZRnkUfjvmuYqLR89jjFHGqdXY", "Phoenix"),
        ("opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb", "OpenBook v2"),
        ("SSwpkEEcbUqx4vtoEByFjSkhKdCT862DNVb52nZg1UZ", "Saber"),
        ("MERLuDFBMmsHnsBPZw2sDQZHvXFMwp8EdjudcU2HKky", "Mercurial"),
        ("stkitrT1Uoy18Dk1fTrgPw8W6MVzoCfYoAFT4MLsmhq", "Sanctum"),
        (
            "5ocnV1qiCgaQR8Jb8xWnVbApfaygJ8tNoZfgPwsgx9kx",
            "Sanctum Infinity",
        ),
        ("HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny", "Bonkswap"),
        (
            "BSwp6bEBihVLdqJRKGgzjcGLHkcTuzmSo1TQkHepzH8p",
            "Bonk launchpad",
        ),
        (
            "boop8hVGQGqehUK2iVEMEnMrL5RbjywRzHKBmBE7ry4",
            "Boop launchpad",
        ),
        ("MoonCVVNZFSYkqNXP6bxHLPL6QQJiMagDL3qcqUQTrG", "Moonshot"),
    ]
}