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