polynode 0.7.2

Rust SDK for the PolyNode API — real-time Polymarket data
Documentation
use std::collections::HashSet;
use std::path::{Path, PathBuf};

use crate::error::Result;
use super::types::*;

pub struct WatchlistDiff {
    pub added: Vec<WatchlistSnapshotRow>,
    pub removed: Vec<WatchlistSnapshotRow>,
    pub current: Vec<WatchlistSnapshotRow>,
}

pub struct WatchlistManager {
    file_path: PathBuf,
}

impl WatchlistManager {
    pub fn new(file_path: impl Into<PathBuf>) -> Self {
        Self { file_path: file_path.into() }
    }

    pub fn path(&self) -> &Path {
        &self.file_path
    }

    /// Load watchlist file. Creates empty template if missing.
    pub fn load(&self) -> Result<WatchlistFile> {
        if !self.file_path.exists() {
            let empty = WatchlistFile {
                version: 1,
                wallets: Vec::new(),
                markets: Vec::new(),
                tokens: Vec::new(),
                settings: None,
            };
            let json = serde_json::to_string_pretty(&empty)?;
            std::fs::write(&self.file_path, json)?;
            return Ok(empty);
        }
        let raw = std::fs::read_to_string(&self.file_path)?;
        let wl: WatchlistFile = serde_json::from_str(&raw)?;
        Ok(wl)
    }

    /// Save watchlist to disk.
    pub fn save(&self, watchlist: &WatchlistFile) -> Result<()> {
        let json = serde_json::to_string_pretty(watchlist)?;
        std::fs::write(&self.file_path, json)?;
        Ok(())
    }

    /// Convert watchlist file to snapshot rows.
    pub fn to_snapshot_rows(&self, watchlist: &WatchlistFile) -> Vec<WatchlistSnapshotRow> {
        let now = now_secs();
        let mut rows = Vec::new();

        for w in &watchlist.wallets {
            rows.push(WatchlistSnapshotRow {
                entity_type: "wallet".into(),
                entity_id: w.address.to_lowercase(),
                label: if w.label.is_empty() { w.address.clone() } else { w.label.clone() },
                backfill: w.backfill,
                added_at: now,
            });
        }
        for m in &watchlist.markets {
            rows.push(WatchlistSnapshotRow {
                entity_type: "market".into(),
                entity_id: m.condition_id.clone(),
                label: if m.label.is_empty() { m.condition_id.clone() } else { m.label.clone() },
                backfill: m.backfill,
                added_at: now,
            });
        }
        for t in &watchlist.tokens {
            rows.push(WatchlistSnapshotRow {
                entity_type: "token".into(),
                entity_id: t.token_id.clone(),
                label: if t.label.is_empty() { t.token_id.clone() } else { t.label.clone() },
                backfill: t.backfill,
                added_at: now,
            });
        }
        rows
    }

    /// Diff new snapshot against stored snapshot.
    pub fn diff(&self, stored: &[WatchlistSnapshotRow], current: &[WatchlistSnapshotRow]) -> WatchlistDiff {
        let stored_set: HashSet<String> = stored.iter()
            .map(|r| format!("{}:{}", r.entity_type, r.entity_id))
            .collect();
        let current_set: HashSet<String> = current.iter()
            .map(|r| format!("{}:{}", r.entity_type, r.entity_id))
            .collect();

        let added: Vec<_> = current.iter()
            .filter(|r| !stored_set.contains(&format!("{}:{}", r.entity_type, r.entity_id)))
            .cloned()
            .collect();
        let removed: Vec<_> = stored.iter()
            .filter(|r| !current_set.contains(&format!("{}:{}", r.entity_type, r.entity_id)))
            .cloned()
            .collect();

        WatchlistDiff {
            added,
            removed,
            current: current.to_vec(),
        }
    }

    /// Add entries to watchlist file. Returns new entries as snapshot rows.
    pub fn add_entries(&self, entries: &[(EntityType, String, String, bool)]) -> Result<Vec<WatchlistSnapshotRow>> {
        let mut watchlist = self.load()?;
        let mut added = Vec::new();
        let now = now_secs();

        for (entity_type, id, label, backfill) in entries {
            match entity_type {
                EntityType::Wallet => {
                    if !watchlist.wallets.iter().any(|w| w.address.to_lowercase() == id.to_lowercase()) {
                        watchlist.wallets.push(WatchlistWallet { address: id.clone(), label: label.clone(), backfill: *backfill });
                        added.push(WatchlistSnapshotRow { entity_type: "wallet".into(), entity_id: id.to_lowercase(), label: label.clone(), backfill: *backfill, added_at: now });
                    }
                }
                EntityType::Market => {
                    if !watchlist.markets.iter().any(|m| m.condition_id == *id) {
                        watchlist.markets.push(WatchlistMarket { condition_id: id.clone(), label: label.clone(), backfill: *backfill });
                        added.push(WatchlistSnapshotRow { entity_type: "market".into(), entity_id: id.clone(), label: label.clone(), backfill: *backfill, added_at: now });
                    }
                }
                EntityType::Token => {
                    if !watchlist.tokens.iter().any(|t| t.token_id == *id) {
                        watchlist.tokens.push(WatchlistToken { token_id: id.clone(), label: label.clone(), backfill: *backfill });
                        added.push(WatchlistSnapshotRow { entity_type: "token".into(), entity_id: id.clone(), label: label.clone(), backfill: *backfill, added_at: now });
                    }
                }
            }
        }

        if !added.is_empty() {
            self.save(&watchlist)?;
        }
        Ok(added)
    }

    /// Remove entries from watchlist file.
    pub fn remove_entries(&self, entries: &[(EntityType, String)]) -> Result<Vec<(EntityType, String)>> {
        let mut watchlist = self.load()?;
        let mut removed = Vec::new();

        for (entity_type, id) in entries {
            match entity_type {
                EntityType::Wallet => {
                    let before = watchlist.wallets.len();
                    watchlist.wallets.retain(|w| w.address.to_lowercase() != id.to_lowercase());
                    if watchlist.wallets.len() < before { removed.push((*entity_type, id.clone())); }
                }
                EntityType::Market => {
                    let before = watchlist.markets.len();
                    watchlist.markets.retain(|m| m.condition_id != *id);
                    if watchlist.markets.len() < before { removed.push((*entity_type, id.clone())); }
                }
                EntityType::Token => {
                    let before = watchlist.tokens.len();
                    watchlist.tokens.retain(|t| t.token_id != *id);
                    if watchlist.tokens.len() < before { removed.push((*entity_type, id.clone())); }
                }
            }
        }

        if !removed.is_empty() {
            self.save(&watchlist)?;
        }
        Ok(removed)
    }
}