polynode 0.8.1

Rust SDK for the PolyNode API — real-time Polymarket data
Documentation
//! Testing utilities — find active wallets for integration testing.

use crate::error::{Error, Result};

/// Known-active wallets for testing (from Polymarket leaderboard).
const FALLBACK_WALLETS: &[&str] = &[
    "0x1a1A27de044faFFCCf68E28F03dCfCf5eB3d3cE6",
    "0xBB39C16C3fc54d3C9B1f9f9E8dF4a09Ee25AB7df",
    "0x7a25dA10f8cA3b67D5fF55e87E2B0C076D3Dd0bD",
    "0xe6Fa0E88Fc2cA3dEFecf0Ed3bE2e66e8E29f5d5E",
    "0x3cF9Db5B9e6bCb9cd1c38bA97B7DC1Daa7eb51F9",
];

/// Get a single active wallet for testing.
///
/// If `fresh` is true, attempts to scrape the Polymarket leaderboard for a
/// recently active wallet. Falls back to a hardcoded list.
pub async fn get_active_test_wallet(fresh: bool) -> String {
    if fresh {
        if let Ok(wallets) = scrape_leaderboard(1).await {
            if let Some(w) = wallets.first() {
                return w.clone();
            }
        }
    }
    FALLBACK_WALLETS[0].to_string()
}

/// Get multiple active wallets for testing.
pub async fn get_active_test_wallets(count: usize, fresh: bool) -> Vec<String> {
    if fresh {
        if let Ok(wallets) = scrape_leaderboard(count).await {
            if wallets.len() >= count {
                return wallets;
            }
        }
    }
    FALLBACK_WALLETS.iter()
        .take(count)
        .map(|s| s.to_string())
        .collect()
}

/// Scrape the Polymarket leaderboard SSR page for active wallet addresses.
async fn scrape_leaderboard(count: usize) -> Result<Vec<String>> {
    let url = "https://polymarket.com/leaderboard";
    let client = reqwest::Client::builder()
        .user_agent("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36")
        .build()?;

    let resp = client.get(url).send().await?;
    let body = resp.text().await?;

    // Look for wallet addresses in the SSR JSON (Next.js __NEXT_DATA__)
    let mut wallets = Vec::new();
    if let Some(start) = body.find("__NEXT_DATA__") {
        if let Some(json_start) = body[start..].find('{') {
            let json_str = &body[start + json_start..];
            if let Some(end) = find_closing_brace(json_str) {
                if let Ok(data) = serde_json::from_str::<serde_json::Value>(&json_str[..=end]) {
                    extract_wallets(&data, &mut wallets, count * 2);
                }
            }
        }
    }

    // Also try regex-like extraction of 0x addresses
    if wallets.len() < count {
        let mut i = 0;
        while i < body.len().saturating_sub(42) && wallets.len() < count * 2 {
            if &body[i..i+2] == "0x" {
                let candidate = &body[i..i+42];
                if candidate.chars().skip(2).all(|c| c.is_ascii_hexdigit()) {
                    let addr = candidate.to_string();
                    if !wallets.contains(&addr) {
                        wallets.push(addr);
                    }
                }
            }
            i += 1;
        }
    }

    wallets.truncate(count);
    if wallets.is_empty() {
        return Err(Error::Api { status: 0, message: "No wallets found in leaderboard".into() });
    }
    Ok(wallets)
}

fn extract_wallets(value: &serde_json::Value, wallets: &mut Vec<String>, max: usize) {
    if wallets.len() >= max { return; }
    match value {
        serde_json::Value::String(s) => {
            if s.starts_with("0x") && s.len() == 42 && s[2..].chars().all(|c| c.is_ascii_hexdigit()) {
                if !wallets.contains(s) {
                    wallets.push(s.clone());
                }
            }
        }
        serde_json::Value::Array(arr) => {
            for v in arr { extract_wallets(v, wallets, max); }
        }
        serde_json::Value::Object(map) => {
            for v in map.values() { extract_wallets(v, wallets, max); }
        }
        _ => {}
    }
}

fn find_closing_brace(s: &str) -> Option<usize> {
    let mut depth = 0;
    for (i, c) in s.char_indices() {
        match c {
            '{' => depth += 1,
            '}' => {
                depth -= 1;
                if depth == 0 { return Some(i); }
            }
            _ => {}
        }
    }
    None
}