1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
use anyhow::Result;
use chrono::{DateTime, Local};
use log::*;
use serde::{Deserialize, Serialize};
use std::{
	collections::{HashMap, HashSet},
	fs::{self, File},
	io::{Read, Write},
	path::{Path, PathBuf},
};

use crate::{endpoint::Endpoint, Registry};

/// Local user data collected from the various regitries.
///
/// It contains the list of registries. Some may be disabled.
/// The data for each registry can be updated in order to keep
/// a fresh list of endpoints.
#[derive(PartialEq, Debug, Deserialize, Serialize)]
pub struct LocalData {
	/// File where the local data are stored
	pub file: PathBuf,

	/// List of the registries where the RPC endpoints are pulled from
	pub registries: HashMap<String, Registry>,

	/// DateTime of the last update of the data
	pub last_update: Option<DateTime<Local>>,
}

impl LocalData {
	pub fn get_default_file() -> PathBuf {
		let home = dirs::home_dir().expect("Failed fetching home dir");
		let dir = Path::new(&home).join(".subrpc");
		let _ = fs::create_dir_all(&dir);
		dir.join("data.json")
	}

	/// Returns true if the local file exists
	pub fn initialized(&self) -> bool {
		self.file.exists()
	}

	/// Initialize a DB based on a given file.
	/// After initializing a DB, you should ensure it contains
	/// at least one registry and call the [Self::refresh] function.
	pub fn init(file: &Path, force: bool) -> Result<Self> {
		debug!("Initializing local data in {} with force: {:?}", file.display(), force);

		let data = Self { file: file.to_path_buf(), ..Default::default() };

		if file.exists() && !force {
			info!("File already exists: {}", file.display());
			data.load()
		} else {
			data.save()
		}
	}

	/// Load [LocalData] and deserialize data from [file].
	pub fn load(self) -> Result<Self> {
		debug!("Loading data from {}", self.file.display());
		let mut fs = File::open(self.file)?;
		let mut s = String::new();
		fs.read_to_string(&mut s)?;
		serde_json::from_str(&s).map_err(anyhow::Error::msg)
	}

	/// Loops through each registry, each network/chain, each endpoint
	/// and update the endpoints lists.
	pub fn refresh(mut self) -> Self {
		debug!("Refreshing registries");

		self.registries.iter_mut().for_each(|(_registry_name, reg)| {
			debug!(" - {} - enabled: {:?}", &reg.name, &reg.enabled);
			// println!("reg = {:?}", &reg);
			match reg.update() {
				Ok(_) => {
					info!("Update of '{}' OK", reg.name);
				}
				Err(e) => {
					// eprintln!("{e:?}");
					error!("Update registry '{}' failed: {e:?}", reg.name);
				}
			}
		});

		self.last_update = Some(Local::now());
		self
	}

	/// Add a new registry. Registries are identitfied by their names, make sure the name is unique.
	pub fn add_registry(mut self, registry: Registry) -> Self {
		self.registries.insert(registry.name.clone(), registry);
		self
	}

	/// Save the current state to file
	pub fn save(self) -> Result<Self> {
		debug!("Saving data to {}", self.file.display());
		let json = serde_json::to_string_pretty(&self)?;
		let mut fs = File::create(&self.file)?;
		fs.write_all(json.as_bytes())?;
		Ok(self)
	}

	/// Get a list of endpoints matching an optional filter. If not
	/// `chain` filter is passed, all endpoints are returned.
	pub fn get_endpoints(&self, chain: Option<&str>) -> HashSet<Endpoint> {
		let mut endpoint_vec: HashSet<Endpoint> = HashSet::new();
		self.registries.iter().for_each(|(_, reg)| {
			if !reg.enabled {
				// skipping
			} else {
				reg.rpc_endpoints
					.iter()
					.filter(|(c, _)| {
						if let Some(filter) = chain {
							c.as_str().to_ascii_lowercase() == filter.to_ascii_lowercase()
						} else {
							true
						}
					})
					.for_each(|(_, e)| {
						let ee = e.clone();
						endpoint_vec.extend(ee.into_iter());
					});
			}
		});
		endpoint_vec
	}

	/// Print the list of registries.
	///
	/// See also [Self::print_summary].
	pub fn print_registries(&self) {
		// println!("self.registries = {:?}", self.registries);
		self.registries.iter().for_each(|(_name, reg)| {
			println!("- [{}] {:?} {:?}", if reg.enabled { "X" } else { " " }, reg.name, reg.url);
		})
	}

	/// Print a summary of your local db. It shows more information than [Self::print_registries].
	pub fn print_summary(&self) {
		self.registries.iter().for_each(|(_name, reg)| {
			println!(
				"- [{}] {} - {}",
				if reg.enabled { "X" } else { " " },
				reg.name,
				if let Some(url) = &reg.url { url } else { "n/a" }
			);
			println!("      rpc endpoints: {:?}", reg.rpc_endpoints.len());
			println!("      last update: {:?}", reg.last_update);
		})
	}
}

impl Default for LocalData {
	fn default() -> Self {
		Self { file: Self::get_default_file(), registries: HashMap::new(), last_update: None }
	}
}

#[cfg(test)]
mod test_local_data {
	use super::*;

	#[test]
	fn test_builder() {
		let data = LocalData::init(&LocalData::get_default_file(), true)
            .expect("Forced init should work")
            .save()
            .expect("Saving data should work")
            .load().expect("Load works")
            .add_registry(Registry::new("SubRPC", "https://raw.githubusercontent.com/chevdor/subrpc/master/registry/subrpc.json"))
            .add_registry(Registry::new("SubRPC Gist", "https://gist.githubusercontent.com/chevdor/a8b381911c28f6de02dde62ed1a17dec/raw/6992b0a2924f80f691e4844c1731564f0e2a62ec/data.json"))
            .refresh()
            .save().expect("Saving works");
		println!("{:#?}", data);
	}

	#[test]
	fn test_merge() {
		let data = LocalData::init(&LocalData::get_default_file(), true)
            .expect("Forced init should work")
            .add_registry(Registry::new("SubRPC Gist 1", "https://gist.githubusercontent.com/chevdor/a8b381911c28f6de02dde62ed1a17dec/raw/6992b0a2924f80f691e4844c1731564f0e2a62ec/data.json"))
            .add_registry(Registry::new("SubRPC Gist 2", "https://gist.githubusercontent.com/chevdor/a8b381911c28f6de02dde62ed1a17dec/raw/6992b0a2924f80f691e4844c1731564f0e2a62ec/data2.json"))
            .refresh()
            .save().expect("Saving works");
		assert_eq!(2, data.registries.len());
		println!("{:#?}", data);
	}
}