polynode 0.13.10

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

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

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)
    }
}