polynode 0.13.4

Rust SDK for the PolyNode API — real-time Polymarket data
Documentation
use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::PathBuf;

// ── Enums ──

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EntityType {
    Wallet,
    Market,
    Token,
}

impl fmt::Display for EntityType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Wallet => write!(f, "wallet"),
            Self::Market => write!(f, "market"),
            Self::Token => write!(f, "token"),
        }
    }
}

impl EntityType {
    pub fn from_str(s: &str) -> Option<Self> {
        match s {
            "wallet" => Some(Self::Wallet),
            "market" => Some(Self::Market),
            "token" => Some(Self::Token),
            _ => None,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BackfillStatus {
    Pending,
    InProgress,
    Complete,
    Failed,
}

impl fmt::Display for BackfillStatus {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Pending => write!(f, "pending"),
            Self::InProgress => write!(f, "in_progress"),
            Self::Complete => write!(f, "complete"),
            Self::Failed => write!(f, "failed"),
        }
    }
}

impl BackfillStatus {
    pub fn from_str(s: &str) -> Option<Self> {
        match s {
            "pending" => Some(Self::Pending),
            "in_progress" => Some(Self::InProgress),
            "complete" => Some(Self::Complete),
            "failed" => Some(Self::Failed),
            _ => None,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TradeSource {
    Settlement,
    TradeEvent,
    Backfill,
}

impl fmt::Display for TradeSource {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Settlement => write!(f, "settlement"),
            Self::TradeEvent => write!(f, "trade_event"),
            Self::Backfill => write!(f, "backfill"),
        }
    }
}

// ── Row types ──

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TradeRow {
    pub tx_hash: String,
    pub log_index: i64,
    pub block_number: Option<i64>,
    pub timestamp: f64,
    pub maker: String,
    pub taker: String,
    pub token_id: String,
    pub condition_id: String,
    pub market_title: String,
    pub market_slug: String,
    pub outcome: String,
    pub side: String,
    pub price: f64,
    pub size: f64,
    pub maker_amount: String,
    pub taker_amount: String,
    pub fee: Option<f64>,
    pub source: String,
    pub raw_json: Option<String>,
    pub cached_at: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SettlementRow {
    pub tx_hash: String,
    pub status: String,
    pub detected_at: f64,
    pub block_number: Option<i64>,
    pub taker_wallet: String,
    pub taker_token: String,
    pub taker_side: String,
    pub taker_price: f64,
    pub taker_size: f64,
    pub condition_id: String,
    pub market_title: String,
    pub market_slug: String,
    pub outcome: String,
    pub trade_count: i64,
    pub raw_json: String,
    pub cached_at: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackfillStateRow {
    pub entity_type: String,
    pub entity_id: String,
    pub label: String,
    pub status: String,
    pub last_offset: i64,
    pub fetched: i64,
    pub last_error: Option<String>,
    pub started_at: f64,
    pub updated_at: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatchlistSnapshotRow {
    pub entity_type: String,
    pub entity_id: String,
    pub label: String,
    pub backfill: bool,
    pub added_at: f64,
}

// ── Config ──

#[derive(Clone)]
pub struct CacheConfig {
    pub db_path: PathBuf,
    pub watchlist_path: PathBuf,
    pub ttl_seconds: u64,
    pub backfill_rate_per_second: f64,
    pub backfill_pages: u32,
    pub backfill_page_size: u32,
    pub purge_on_remove: bool,
    pub on_backfill_progress: Option<std::sync::Arc<dyn Fn(BackfillProgress) + Send + Sync>>,
}

impl Default for CacheConfig {
    fn default() -> Self {
        Self {
            db_path: PathBuf::from("./polynode-cache.db"),
            watchlist_path: PathBuf::from("./polynode.watch.json"),
            ttl_seconds: 30 * 86400,
            backfill_rate_per_second: 1.0,
            backfill_pages: 1,
            backfill_page_size: 500,
            purge_on_remove: false,
            on_backfill_progress: None,
        }
    }
}

// ── Query options ──

#[derive(Debug, Clone, Default)]
pub struct QueryOptions {
    pub limit: Option<u32>,
    pub offset: Option<u32>,
    pub since: Option<f64>,
    pub until: Option<f64>,
    pub side: Option<String>,
    pub order_by: Option<OrderBy>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OrderBy {
    TimestampAsc,
    TimestampDesc,
}

// ── Position summary ──

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PositionSummary {
    pub wallet: String,
    pub token_id: String,
    pub condition_id: String,
    pub market_title: String,
    pub market_slug: String,
    pub outcome: String,
    pub size: f64,
    pub avg_price: f64,
    pub cur_price: Option<f64>,
    pub current_value: Option<f64>,
    pub initial_value: Option<f64>,
    pub cash_pnl: Option<f64>,
    pub percent_pnl: Option<f64>,
    pub realized_pnl: Option<f64>,
    pub total_bought: Option<f64>,
    pub redeemable: bool,
    pub end_date: Option<String>,
    pub trade_count: Option<i64>,
    pub first_trade_at: Option<f64>,
    pub last_trade_at: Option<f64>,
}

// ── Realized P&L ──

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenPnl {
    pub token_id: String,
    pub condition_id: String,
    pub market_title: String,
    pub outcome: String,
    pub realized_pnl: f64,
    pub unrealized_pnl: f64,
    pub remaining_size: f64,
    pub avg_cost: f64,
    pub cur_price: Option<f64>,
    pub trades_analyzed: usize,
    pub buys: usize,
    pub sells: usize,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RealizedPnlResult {
    pub wallet: String,
    pub total_realized_pnl: f64,
    pub total_unrealized_pnl: f64,
    pub total_pnl: f64,
    pub tokens: Vec<TokenPnl>,
    pub trades_analyzed: usize,
    /// "full" if backfill complete for this wallet, "partial" otherwise
    pub confidence: String,
}

// ── Backfill progress ──

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackfillProgress {
    pub entity_type: String,
    pub entity_id: String,
    pub label: String,
    pub status: String,
    pub fetched: i64,
    pub offset: i64,
    pub message: Option<String>,
}

// ── Cache stats ──

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheStats {
    pub trade_count: i64,
    pub settlement_count: i64,
    pub db_size_bytes: u64,
    pub oldest_trade_at: Option<f64>,
    pub newest_trade_at: Option<f64>,
    pub backfill_entities: i64,
    pub backfill_complete: i64,
    pub backfill_pending: i64,
    pub backfill_in_progress: i64,
    pub backfill_failed: i64,
}

// ── Watchlist file format ──

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatchlistFile {
    pub version: u32,
    #[serde(default)]
    pub wallets: Vec<WatchlistWallet>,
    #[serde(default)]
    pub markets: Vec<WatchlistMarket>,
    #[serde(default)]
    pub tokens: Vec<WatchlistToken>,
    #[serde(default)]
    pub settings: Option<WatchlistSettings>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatchlistWallet {
    pub address: String,
    #[serde(default)]
    pub label: String,
    #[serde(default = "default_true")]
    pub backfill: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatchlistMarket {
    pub condition_id: String,
    #[serde(default)]
    pub label: String,
    #[serde(default = "default_true")]
    pub backfill: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatchlistToken {
    pub token_id: String,
    #[serde(default)]
    pub label: String,
    #[serde(default = "default_true")]
    pub backfill: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatchlistSettings {
    pub ttl_days: Option<u32>,
    pub backfill_rate: Option<f64>,
    pub purge_on_remove: Option<bool>,
}

fn default_true() -> bool {
    true
}

// ── Storage estimation ──

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageEstimate {
    pub estimated_trades: u64,
    pub estimated_size_mb: u64,
    pub ttl_days: u32,
    pub wallet_count: usize,
    pub market_count: usize,
    pub token_count: usize,
}

// ── Helpers ──

pub fn normalize_timestamp(value: &serde_json::Value) -> f64 {
    match value {
        serde_json::Value::Number(n) => {
            let v = n.as_f64().unwrap_or(0.0);
            if v > 1e12 { v / 1000.0 } else { v }
        }
        serde_json::Value::String(s) => {
            if let Ok(n) = s.parse::<f64>() {
                if n > 1e12 { n / 1000.0 } else { n }
            } else {
                now_secs()
            }
        }
        _ => now_secs(),
    }
}

pub fn normalize_f64(v: f64) -> f64 {
    if v > 1e12 { v / 1000.0 } else { v }
}

pub fn now_secs() -> f64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs_f64()
}