From 260f5edd962038bf25bff7a8858fafa7027653db Mon Sep 17 00:00:00 2001
From: hinto-janaiyo <hinto.janaiyo@protonmail.com>
Date: Mon, 26 Dec 2022 17:37:45 -0500
Subject: [PATCH] Status Submenu: add [GupaxP2poolApi] to [disk.rs], add to
 [App]

---
 README.md     |   2 +-
 src/disk.rs   | 236 +++++++++++++++++++++++++++++++++++++++++++++++++-
 src/helper.rs |   3 +-
 src/main.rs   |  47 ++++++++--
 4 files changed, 276 insertions(+), 12 deletions(-)

diff --git a/README.md b/README.md
index 7190b17..21c57ce 100644
--- a/README.md
+++ b/README.md
@@ -482,7 +482,7 @@ You need [`cargo`](https://www.rust-lang.org/learn/get-started), Rust's build to
 
 The `--release` profile in Gupax is set to prefer code performance & small binary sizes over compilation speed (see [`Cargo.toml`](https://github.com/hinto-janaiyo/gupax/blob/main/Cargo.toml)). Gupax itself (with all dependencies already built) takes around 1m30s to build (vs 10s on a normal `--release`) with a Ryzen 5950x.
 
-There are `21` unit tests throughout the codebase files, you should probably run:
+There are `24` unit tests throughout the codebase files, you should probably run:
 ```
 cargo test
 ```
diff --git a/src/disk.rs b/src/disk.rs
index 6a4c76c..8e7c048 100644
--- a/src/disk.rs
+++ b/src/disk.rs
@@ -42,6 +42,7 @@ use serde::{Serialize,Deserialize};
 use figment::Figment;
 use figment::providers::{Format,Toml};
 use crate::{
+	human::*,
 	constants::*,
 	gupax::Ratio,
 	Tab,
@@ -59,6 +60,37 @@ const DIRECTORY: &str = "Gupax/";
 #[cfg(target_os = "linux")]
 const DIRECTORY: &str = "gupax/";
 
+// File names
+pub const STATE_TOML: &str = "state.toml";
+pub const NODE_TOML: &str = "node.toml";
+pub const POOL_TOML: &str = "pool.toml";
+
+// P2Pool API
+// Lives within the Gupax OS data directory.
+// ~/.local/share/gupax/p2pool/
+// ├─ payout        // Raw log lines of payouts received
+// ├─ xmr           // Raw log lines of XMR mined
+// ├─ block         // Raw log lines of actual blocks found
+// ├─ total_payout  // Single [u128] representing total payouts
+// ├─ total_xmr     // Single [u128] representing total XMR mined in atomic units
+// ├─ total_block   // Single [u128] representing total blocks foundpub const GUPAX_P2POOL_API_: &str = "state.toml";
+#[cfg(target_os = "windows")]
+pub const GUPAX_P2POOL_API_DIRECTORY:    &str = r"p2pool\";
+#[cfg(target_family = "unix")]
+pub const GUPAX_P2POOL_API_DIRECTORY:    &str = "p2pool/";
+pub const GUPAX_P2POOL_API_PAYOUT:       &str = "payout";
+pub const GUPAX_P2POOL_API_BLOCK:        &str = "block";
+pub const GUPAX_P2POOL_API_TOTAL_PAYOUT: &str = "total_payout";
+pub const GUPAX_P2POOL_API_TOTAL_XMR:    &str = "total_xmr";
+pub const GUPAX_P2POOL_API_TOTAL_BLOCK:  &str = "total_block";
+pub const GUPAX_P2POOL_API_FILE_ARRAY: [&str; 5] = [
+	GUPAX_P2POOL_API_PAYOUT,
+	GUPAX_P2POOL_API_BLOCK,
+	GUPAX_P2POOL_API_TOTAL_PAYOUT,
+	GUPAX_P2POOL_API_TOTAL_XMR,
+	GUPAX_P2POOL_API_TOTAL_BLOCK,
+];
+
 #[cfg(target_os = "windows")]
 pub const DEFAULT_P2POOL_PATH: &str = r"P2Pool\p2pool.exe";
 #[cfg(target_os = "macos")]
@@ -96,20 +128,37 @@ pub fn get_gupax_data_path() -> Result<PathBuf, TomlError> {
 			path.push(DIRECTORY);
 			info!("OS | Data path ... {}", path.display());
 			create_gupax_dir(&path)?;
+			let mut gupax_p2pool_dir = path.clone();
+			gupax_p2pool_dir.push(GUPAX_P2POOL_API_DIRECTORY);
+			create_gupax_p2pool_dir(&gupax_p2pool_dir)?;
 			Ok(path)
 		},
 		None => { error!("OS | Data path ... FAIL"); Err(TomlError::Path(PATH_ERROR.to_string())) },
 	}
 }
 
+pub fn get_gupax_p2pool_path(os_data_path: &PathBuf) -> PathBuf {
+	let mut gupax_p2pool_dir = os_data_path.clone();
+	gupax_p2pool_dir.push(GUPAX_P2POOL_API_DIRECTORY);
+	gupax_p2pool_dir
+}
+
 pub fn create_gupax_dir(path: &PathBuf) -> Result<(), TomlError> {
-	// Create directory
+	// Create Gupax directory
 	match fs::create_dir_all(path) {
 		Ok(_) => { info!("OS | Create data path ... OK"); Ok(()) },
 		Err(e) => { error!("OS | Create data path ... FAIL ... {}", e); Err(TomlError::Io(e)) },
 	}
 }
 
+pub fn create_gupax_p2pool_dir(path: &PathBuf) -> Result<(), TomlError> {
+	// Create Gupax directory
+	match fs::create_dir_all(path) {
+		Ok(_) => { info!("OS | Create Gupax-P2Pool API path ... OK"); Ok(()) },
+		Err(e) => { error!("OS | Create Gupax-P2Pool API path ... FAIL ... {}", e); Err(TomlError::Io(e)) },
+	}
+}
+
 // Convert a [File] path to a [String]
 pub fn read_to_string(file: File, path: &PathBuf) -> Result<String, TomlError> {
 	match fs::read_to_string(path) {
@@ -511,6 +560,138 @@ impl Pool {
 	}
 }
 
+//---------------------------------------------------------------------------------------------------- Gupax-P2Pool API
+#[derive(Clone,Eq,PartialEq,Debug)]
+pub struct GupaxP2poolApi {
+	pub payout: HumanNumber,
+	pub xmr: HumanNumber,
+	pub block: HumanNumber,
+	pub int_payout: u128,
+	pub int_xmr: u128,
+	pub int_block: u128,
+	pub log_payout: String,
+	pub log_block: String,
+	pub path_int_payout: PathBuf,
+	pub path_int_xmr: PathBuf,
+	pub path_int_block: PathBuf,
+	pub path_log_payout: PathBuf,
+	pub path_log_block: PathBuf,
+}
+
+impl Default for GupaxP2poolApi { fn default() -> Self { Self::new() } }
+
+impl GupaxP2poolApi {
+	pub fn new() -> Self {
+		Self {
+			payout: HumanNumber::unknown(),
+			xmr: HumanNumber::unknown(),
+			block: HumanNumber::unknown(),
+			int_payout: 0,
+			int_xmr: 0,
+			int_block: 0,
+			log_payout: String::new(),
+			log_block: String::new(),
+			path_int_payout: PathBuf::new(),
+			path_int_xmr: PathBuf::new(),
+			path_int_block: PathBuf::new(),
+			path_log_payout: PathBuf::new(),
+			path_log_block: PathBuf::new(),
+		}
+	}
+
+	pub fn fill_paths(&mut self, gupax_p2pool_dir: &PathBuf) {
+		let mut path_int_payout = gupax_p2pool_dir.clone();
+		let mut path_int_xmr    = gupax_p2pool_dir.clone();
+		let mut path_int_block  = gupax_p2pool_dir.clone();
+		let mut path_log_payout = gupax_p2pool_dir.clone();
+		let mut path_log_block  = gupax_p2pool_dir.clone();
+		path_int_payout.push(GUPAX_P2POOL_API_TOTAL_PAYOUT);
+		path_int_xmr.push(GUPAX_P2POOL_API_TOTAL_XMR);
+		path_int_block.push(GUPAX_P2POOL_API_TOTAL_BLOCK);
+		path_log_payout.push(GUPAX_P2POOL_API_PAYOUT);
+		path_log_block.push(GUPAX_P2POOL_API_BLOCK);
+		*self = Self {
+			path_int_payout,
+			path_int_xmr,
+			path_int_block,
+			path_log_payout,
+			path_log_block,
+			..std::mem::take(self)
+		};
+	}
+
+	pub fn create_all_files(gupax_p2pool_dir: &PathBuf) -> Result<(), TomlError> {
+		use std::io::Write;
+		for file in GUPAX_P2POOL_API_FILE_ARRAY {
+			let mut path = gupax_p2pool_dir.clone();
+			path.push(file);
+			if path.exists() {
+				info!("GupaxP2poolApi | [{}] already exists, skipping...", path.display());
+				continue
+			}
+			match std::fs::File::create(&path) {
+				Ok(mut f)  => {
+					match file {
+						GUPAX_P2POOL_API_TOTAL_PAYOUT|GUPAX_P2POOL_API_TOTAL_XMR|GUPAX_P2POOL_API_TOTAL_BLOCK => f.write_all(b"0")?,
+						_ => (),
+					}
+					info!("GupaxP2poolApi | [{}] create ... OK", path.display());
+				},
+				Err(e) => { warn!("GupaxP2poolApi | [{}] create ... FAIL: {}", path.display(), e); return Err(TomlError::Io(e)) },
+			}
+		}
+		Ok(())
+	}
+
+	pub fn read_all_files_and_update(&mut self) -> Result<(), TomlError> {
+		let int_payout = match read_to_string(File::IntPayout, &self.path_int_payout)?.as_str().parse::<u128>() {
+			Ok(o)  => o,
+			Err(e) => { warn!("GupaxP2poolApi | [int_payout] parse error: {}", e); return Err(TomlError::Parse("int_payout")) }
+		};
+		let int_xmr = match read_to_string(File::IntXmr, &self.path_int_xmr)?.as_str().parse::<u128>() {
+			Ok(o)  => o,
+			Err(e) => { warn!("GupaxP2poolApi | [int_xmr] parse error: {}", e); return Err(TomlError::Parse("int_xmr")) }
+		};
+		let int_block = match read_to_string(File::IntBlock, &self.path_int_block)?.as_str().parse::<u128>() {
+			Ok(o)  => o,
+			Err(e) => { warn!("GupaxP2poolApi | [int_block] parse error: {}", e); return Err(TomlError::Parse("int_block")) }
+		};
+		let payout     = HumanNumber::from_u128(int_payout);
+		let xmr        = HumanNumber::from_u128(int_xmr);
+		let block      = HumanNumber::from_u128(int_block);
+		let log_payout = read_to_string(File::LogPayout, &self.path_log_payout)?;
+		let log_block  = read_to_string(File::LogBlock, &self.path_log_block)?;
+		*self = Self {
+			payout,
+			xmr,
+			block,
+			int_payout,
+			int_xmr,
+			int_block,
+			log_payout,
+			log_block,
+			..std::mem::take(self)
+		};
+		Ok(())
+	}
+
+	pub fn write_to_all_files(&self) -> Result<(), TomlError> {
+		Self::save(&self.int_payout.to_string(), &self.path_int_payout)?;
+		Self::save(&self.int_xmr.to_string(),    &self.path_int_xmr)?;
+		Self::save(&self.int_block.to_string(),  &self.path_int_block)?;
+		Self::save(&self.log_payout, &self.path_log_payout)?;
+		Self::save(&self.log_block,  &self.path_log_block)?;
+		Ok(())
+	}
+
+	pub fn save(string: &str, path: &PathBuf) -> Result<(), TomlError> {
+		match fs::write(path, string) {
+			Ok(_) => { info!("GupaxP2poolApi | Save [{}] ... OK", path.display()); Ok(()) },
+			Err(e) => { error!("GupaxP2poolApi | Save [{}] ... FAIL: {}", path.display(), e); Err(TomlError::Io(e)) },
+		}
+	}
+}
+
 //---------------------------------------------------------------------------------------------------- Custom Error [TomlError]
 #[derive(Debug)]
 pub enum TomlError {
@@ -553,9 +734,17 @@ impl From<std::fmt::Error> for TomlError {
 //---------------------------------------------------------------------------------------------------- [File] Enum (for matching which file)
 #[derive(Clone,Copy,Eq,PartialEq,Debug,Deserialize,Serialize)]
 pub enum File {
-	State, // state.toml -> Gupax state
-	Node, // node.toml -> P2Pool manual node selector
-	Pool, // pool.toml -> XMRig manual pool selector
+	// State files
+	State,       // state.toml   | Gupax state
+	Node,        // node.toml    | P2Pool manual node selector
+	Pool,        // pool.toml    | XMRig manual pool selector
+
+	// Gupax-P2Pool API
+	LogPayout, // payout       | Raw log lines of P2Pool payouts received
+	LogBlock,  // block        | Raw log lines of actual blocks found via P2Pool
+	IntPayout, // total_payout | Single [u128] representing total payouts
+	IntXmr,    // total_xmr    | Single [u128] representing total XMR mined in atomic units
+	IntBlock,  // total_block  | Single [u128] representing total blocks found
 }
 
 //---------------------------------------------------------------------------------------------------- [Node] Struct
@@ -973,4 +1162,43 @@ mod test {
 		assert!(!merged_state.contains("SETTING_THAT_DOESNT_EXIST_ANYMORE"));
 		assert!(merged_state.contains("44hintoFpuo3ugKfcqJvh5BmrsTRpnTasJmetKC4VXCt6QDtbHVuixdTtsm6Ptp7Y8haXnJ6j8Gj2dra8CKy5ewz7Vi9CYW"));
 	}
+
+	#[test]
+	fn create_and_serde_gupax_p2pool_api() {
+		use crate::disk::GupaxP2poolApi;
+
+		// Get API dir, fill paths.
+		let mut api = GupaxP2poolApi::new();
+		let mut path = crate::disk::get_gupax_data_path().unwrap();
+		path.push(crate::disk::GUPAX_P2POOL_API_DIRECTORY);
+		GupaxP2poolApi::fill_paths(&mut api, &path);
+		println!("{:#?}", api);
+
+		// Create and read all files, write some fake data.
+		GupaxP2poolApi::create_all_files(&path).unwrap();
+		GupaxP2poolApi::read_all_files_and_update(&mut api).unwrap();
+		api.int_payout = 1;
+		api.int_xmr    = 2;
+		api.int_block  = 3;
+		api.log_payout = "P2Pool You received a payout of 0.000000000001 XMR in block 2642816".to_string();
+		api.log_block  = "client 127.0.0.1:51111 user asdf found a mainchain block at height 2642816, submitting it".to_string();
+		GupaxP2poolApi::write_to_all_files(&api).unwrap();
+		println!("AFTER WRITE: {:#?}", api);
+
+		// Reset internal stats, read file data.
+		api.int_payout = 0;
+		api.int_xmr    = 0;
+		api.int_block  = 0;
+		api.log_payout.clear();
+		api.log_block.clear();
+		GupaxP2poolApi::read_all_files_and_update(&mut api).unwrap();
+		println!("AFTER READ: {:#?}", api);
+
+		// Assert that the file read mutated the internal struct correctly.
+		assert_eq!(api.int_payout, 1);
+		assert_eq!(api.int_xmr,    2);
+		assert_eq!(api.int_block,  3);
+		assert_eq!(api.log_payout, "P2Pool You received a payout of 0.000000000001 XMR in block 2642816");
+		assert_eq!(api.log_block,  "client 127.0.0.1:51111 user asdf found a mainchain block at height 2642816, submitting it");
+	}
 }
diff --git a/src/helper.rs b/src/helper.rs
index 4d09d2d..86328a0 100644
--- a/src/helper.rs
+++ b/src/helper.rs
@@ -53,8 +53,6 @@ use sysinfo::{CpuExt,ProcessExt};
 use log::*;
 
 //---------------------------------------------------------------------------------------------------- Constants
-// The locale numbers are formatting in is English, which looks like: [1,000]
-const LOCALE: num_format::Locale = num_format::Locale::en;
 // The max amount of bytes of process output we are willing to
 // hold in memory before it's too much and we need to reset.
 const MAX_GUI_OUTPUT_BYTES: usize = 500_000;
@@ -1486,6 +1484,7 @@ impl PrivP2poolPoolApi {
 	}
 }
 
+#[allow(non_snake_case)]
 #[derive(Debug, Serialize, Deserialize, Clone, Copy)]
 struct PoolStatistics {
 	hashRate: u128,
diff --git a/src/main.rs b/src/main.rs
index 259f45f..4796d4e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -129,6 +129,12 @@ pub struct App {
 	sudo: Arc<Mutex<SudoState>>, // This is just a dummy struct on [Windows].
 	// State from [--flags]
 	no_startup: bool,
+	// Gupax-P2Pool API
+	// Gupax's P2Pool API (e.g: ~/.local/share/gupax/p2pool/)
+	// This is a file-based API that contains data for permanent stats.
+	// The below struct holds everything needed for it, the paths, the
+	// actual stats, and all the functions needed to mutate them.
+	gupax_p2pool_api: GupaxP2poolApi,
 	// Static stuff
 	pid: sysinfo::Pid, // Gupax's PID
 	max_threads: usize, // Max amount of detected system threads
@@ -138,7 +144,8 @@ pub struct App {
 	resolution: Vec2, // Frame resolution
 	os: &'static str, // OS
 	admin: bool, // Are we admin? (for Windows)
-	os_data_path: PathBuf, // OS data path (e.g: ~/.local/share/gupax)
+	os_data_path: PathBuf, // OS data path (e.g: ~/.local/share/gupax/)
+	gupax_p2pool_api_path: PathBuf, // Gupax-P2Pool API path (e.g: ~/.local/share/gupax/p2pool/)
 	state_path: PathBuf, // State file path
 	node_path: PathBuf, // Node file path
 	pool_path: PathBuf, // Pool file path
@@ -219,6 +226,7 @@ impl App {
 			resizing: false,
 			alpha: 0,
 			no_startup: false,
+			gupax_p2pool_api: GupaxP2poolApi::new(),
 			pub_sys,
 			pid,
 			max_threads: num_cpus::get(),
@@ -229,6 +237,7 @@ impl App {
 			resolution: Vec2::new(APP_DEFAULT_HEIGHT, APP_DEFAULT_WIDTH),
 			os: OS,
 			os_data_path: PathBuf::new(),
+			gupax_p2pool_api_path: PathBuf::new(),
 			state_path: PathBuf::new(),
 			node_path: PathBuf::new(),
 			pool_path: PathBuf::new(),
@@ -259,11 +268,11 @@ impl App {
 		debug!("App Init | Setting TOML path...");
 		// Set [*.toml] path
 		app.state_path = app.os_data_path.clone();
-		app.state_path.push("state.toml");
+		app.state_path.push(STATE_TOML);
 		app.node_path = app.os_data_path.clone();
-		app.node_path.push("node.toml");
+		app.node_path.push(NODE_TOML);
 		app.pool_path = app.os_data_path.clone();
-		app.pool_path.push("pool.toml");
+		app.pool_path.push(POOL_TOML);
 
 		// Apply arg state
 		// It's not safe to [--reset] if any of the previous variables
@@ -334,6 +343,34 @@ impl App {
 		debug!("Pool Vec:");
 		debug!("{:#?}", app.pool_vec);
 
+		//----------------------------------------------------------------------------------------------------
+		// Read [GupaxP2poolApi] disk files
+		app.gupax_p2pool_api_path = crate::disk::get_gupax_p2pool_path(&app.os_data_path);
+		app.gupax_p2pool_api.fill_paths(&app.gupax_p2pool_api_path);
+		GupaxP2poolApi::create_all_files(&app.gupax_p2pool_api_path);
+		debug!("App Init | Reading Gupax-P2Pool API files...");
+		match app.gupax_p2pool_api.read_all_files_and_update() {
+			Ok(_) => {
+				info!(
+					"GupaxP2poolApi ... Payouts: {} | XMR (atomic-units): {} | Blocks: {}",
+					app.gupax_p2pool_api.payout,
+					app.gupax_p2pool_api.xmr,
+					app.gupax_p2pool_api.block,
+				);
+			},
+			Err(err) => {
+				error!("GupaxP2poolApi ... {}", err);
+				match err {
+					Io(e) => app.error_state.set(format!("GupaxP2poolApi: {}", e), ErrorFerris::Panic, ErrorButtons::Quit),
+					Path(e) => app.error_state.set(format!("GupaxP2poolApi: {}", e), ErrorFerris::Panic, ErrorButtons::Quit),
+					Serialize(e) => app.error_state.set(format!("GupaxP2poolApi: {}", e), ErrorFerris::Panic, ErrorButtons::Quit),
+					Deserialize(e) => app.error_state.set(format!("GupaxP2poolApi: {}", e), ErrorFerris::Panic, ErrorButtons::Quit),
+					Format(e) => app.error_state.set(format!("GupaxP2poolApi: {}", e), ErrorFerris::Panic, ErrorButtons::Quit),
+					Merge(e) => app.error_state.set(format!("GupaxP2poolApi: {}", e), ErrorFerris::Error, ErrorButtons::ResetState),
+					Parse(e) => app.error_state.set(format!("GupaxP2poolApi: {}", e), ErrorFerris::Panic, ErrorButtons::Quit),
+				};
+			},
+		};
 
 		//----------------------------------------------------------------------------------------------------
 		let mut og = app.og.lock().unwrap(); // Lock [og]
@@ -930,7 +967,7 @@ fn main() {
 		Ok(_) => info!("Temporary folder cleanup ... OK"),
 		Err(e) => warn!("Could not cleanup [gupax_tmp] folders: {}", e),
 	}
-	info!("Init ... DONE");
+	info!("/*************************************/ Init ... OK /*************************************/");
 	eframe::run_native(&app.name_version.clone(), options, Box::new(|cc| Box::new(App::cc(cc, app))),);
 }