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
}
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)
}
pub fn save(&self, watchlist: &WatchlistFile) -> Result<()> {
let json = serde_json::to_string_pretty(watchlist)?;
std::fs::write(&self.file_path, json)?;
Ok(())
}
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
}
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(),
}
}
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)
}
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)
}
}