diff --git a/Cargo.lock b/Cargo.lock index ec17585c..08049175 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -388,9 +388,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.83" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "approx" @@ -2097,9 +2097,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -5108,9 +5108,9 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libmimalloc-sys" -version = "0.1.37" +version = "0.1.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81eb4061c0582dedea1cbc7aff2240300dd6982e0239d1c99e65c1dbf4a30ba7" +checksum = "0e7bb23d733dfcc8af652a78b7bf232f0e967710d044732185e561e47c0336b6" dependencies = [ "cc", "libc", @@ -6365,9 +6365,9 @@ dependencies = [ [[package]] name = "mimalloc" -version = "0.1.41" +version = "0.1.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f41a2280ded0da56c8cf898babb86e8f10651a34adcfff190ae9a1159c6908d" +checksum = "e9186d86b79b52f4a77af65604b51225e8db1d6ee7e3f41aec1e40829c71a176" dependencies = [ "libmimalloc-sys", ] @@ -6960,15 +6960,15 @@ dependencies = [ [[package]] name = "objc-sys" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da284c198fb9b7b0603f8635185e85fbd5b64ee154b1ed406d489077de2d6d60" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" [[package]] name = "objc2" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4b25e1034d0e636cd84707ccdaa9f81243d399196b8a773946dcffec0401659" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" dependencies = [ "objc-sys", "objc2-encode", @@ -6976,9 +6976,9 @@ dependencies = [ [[package]] name = "objc2-encode" -version = "4.0.1" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88658da63e4cc2c8adb1262902cd6af51094df0488b760d6fd27194269c0950a" +checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" [[package]] name = "objc_id" @@ -8758,9 +8758,9 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "safe-transmute" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98a01dab6acf992653be49205bdd549f32f17cb2803e8eacf1560bf97259aae8" +checksum = "3944826ff8fa8093089aba3acb4ef44b9446a99a16f3bf4e74af3f77d340ab7d" [[package]] name = "safe_arch" @@ -11969,18 +11969,18 @@ checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" [[package]] name = "thiserror" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 79a8f79d..4fa928f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -130,58 +130,11 @@ numa = [ # # This list is ordered alphabetically. [profile.dev.package] -bitvec = { opt-level = 3 } -blake2 = { opt-level = 3 } -blake3 = { opt-level = 3 } -blake2b_simd = { opt-level = 3 } -blst = { opt-level = 3 } -rust-kzg-blst = { opt-level = 3 } -chacha20 = { opt-level = 3 } -chacha20poly1305 = { opt-level = 3 } -cranelift-codegen = { opt-level = 3 } -cranelift-wasm = { opt-level = 3 } -crc32fast = { opt-level = 3 } -crossbeam-deque = { opt-level = 3 } -crypto-mac = { opt-level = 3 } -curve25519-dalek = { opt-level = 3 } -ed25519-zebra = { opt-level = 3 } -flate2 = { opt-level = 3 } -futures-channel = { opt-level = 3 } -hashbrown = { opt-level = 3 } -hash-db = { opt-level = 3 } -hmac = { opt-level = 3 } -httparse = { opt-level = 3 } -integer-sqrt = { opt-level = 3 } -k256 = { opt-level = 3 } -keccak = { opt-level = 3 } -kzg = { opt-level = 3 } -libsecp256k1 = { opt-level = 3 } -libz-sys = { opt-level = 3 } -mio = { opt-level = 3 } -nalgebra = { opt-level = 3 } -num-bigint = { opt-level = 3 } parking_lot = { opt-level = 3 } -parking_lot_core = { opt-level = 3 } -percent-encoding = { opt-level = 3 } -primitive-types = { opt-level = 3 } -ring = { opt-level = 3 } -rustls = { opt-level = 3 } -secp256k1 = { opt-level = 3 } -sha2 = { opt-level = 3 } -sha3 = { opt-level = 3 } -smallvec = { opt-level = 3 } -snow = { opt-level = 3 } -subspace-archiving = { opt-level = 3 } subspace-core-primitives = { opt-level = 3 } subspace-erasure-coding = { opt-level = 3 } subspace-farmer-components = { opt-level = 3 } subspace-proof-of-space = { opt-level = 3 } -subspace-proof-of-time = { opt-level = 3 } -twox-hash = { opt-level = 3 } -uint = { opt-level = 3 } -x25519-dalek = { opt-level = 3 } -yamux = { opt-level = 3 } -zeroize = { opt-level = 3 } [profile.release] # Substrate runtime requires unwinding. diff --git a/src/backend.rs b/src/backend.rs index 86ce4521..35c7ade2 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -21,6 +21,8 @@ use future::FutureExt; use futures::channel::mpsc; use futures::{future, select, SinkExt, StreamExt}; use sc_subspace_chain_specs::GEMINI_3H_CHAIN_SPEC; +use sp_api::ProvideRuntimeApi; +use sp_consensus_subspace::{ChainConstants, SubspaceApi}; use std::error::Error; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::path::{Path, PathBuf}; @@ -187,7 +189,11 @@ enum LoadedConsensusChainNode { #[derive(Debug, Clone)] pub enum NodeNotification { SyncStateUpdate(SyncState), - BlockImported(BlockImported), + BlockImported { + imported_block: BlockImported, + current_solution_range: u64, + max_pieces_in_sector: u16, + }, } /// Notification messages send from backend about its operation @@ -221,6 +227,7 @@ pub enum BackendNotification { reward_address_balance: Balance, initial_farm_states: Vec, chain_info: ChainInfo, + chain_constants: ChainConstants, }, Node(NodeNotification), Farmer(FarmerNotification), @@ -490,6 +497,7 @@ async fn run( "networking".to_string(), )?; + let runtime_api = consensus_node.full_node.client.runtime_api(); notifications_sender .send(BackendNotification::Running { raw_config, @@ -497,6 +505,7 @@ async fn run( reward_address_balance: consensus_node.account_balance(&config.reward_address), initial_farm_states: farmer.initial_farm_states().to_vec(), chain_info: consensus_node.chain_info().clone(), + chain_constants: runtime_api.chain_constants(consensus_node.best_block_hash())?, }) .await?; @@ -523,9 +532,14 @@ async fn run( let _on_imported_block_handler_id = consensus_node.on_block_imported({ let notifications_sender = notifications_sender.clone(); // let reward_address_storage_key = account_storage_key(&config.reward_address); - - Arc::new(move |&block_imported| { - let notification = NodeNotification::BlockImported(block_imported); + let (current_solution_range, max_pieces_in_sector) = consensus_node.tsp_metrics()?; + + Arc::new(move |&imported_block| { + let notification = NodeNotification::BlockImported { + imported_block, + current_solution_range, + max_pieces_in_sector, + }; let mut notifications_sender = notifications_sender.clone(); diff --git a/src/backend/farmer.rs b/src/backend/farmer.rs index c7e6f212..6b1bd17e 100644 --- a/src/backend/farmer.rs +++ b/src/backend/farmer.rs @@ -18,7 +18,7 @@ use std::hash::Hash; use std::num::{NonZeroU8, NonZeroUsize}; use std::path::PathBuf; use std::sync::Arc; -use std::time::Duration; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::{fmt, fs}; use subspace_core_primitives::crypto::kzg::Kzg; use subspace_core_primitives::{PublicKey, Record, SectorIndex}; @@ -693,3 +693,38 @@ where action_sender, }) } + +/// Calculate the ETA for reward payment. +/// The farmer can expect reward in secs/mins/hrs/days/weeks time. +pub(crate) fn calculate_expected_reward_duration_from_now( + total_space_pledged: u128, + space_pledged: u128, + last_reward_timestamp: Option, +) -> anyhow::Result { + // Time elapsed since the last reward payment timestamp. + let time_previous = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + .checked_sub(last_reward_timestamp.unwrap_or(0)) + .ok_or_else(|| anyhow!("Overflow occurred while subtracting the last reward timestamp"))?; + + // Ensure space_pledged is not zero to prevent division by zero. + if space_pledged == 0 { + return Err(anyhow!("Division by zero error: space_pledged is zero")); + } + + // Expected time duration for next reward payment since the last reward payment timestamp. + let expected_time_next = (total_space_pledged as u64) + .checked_div(space_pledged as u64) + .ok_or_else(|| anyhow!("Overflow occurred during division"))? + .checked_mul(time_previous) + .ok_or_else(|| anyhow!("Overflow occurred during multiplication"))?; + + // Subtract the duration till now from the expected time duration to get the ETA duration. + let eta_duration = expected_time_next + .checked_sub(time_previous) + .ok_or_else(|| anyhow!("Overflow occurred during final subtraction"))?; + + Ok(eta_duration) +} diff --git a/src/backend/node.rs b/src/backend/node.rs index 6791c949..8a5afbdd 100644 --- a/src/backend/node.rs +++ b/src/backend/node.rs @@ -20,6 +20,8 @@ use sc_network::config::{Ed25519Secret, NodeKeyConfig, NonReservedPeerMode, SetC use sc_service::{BlocksPruning, Configuration, GenericChainSpec}; use sc_storage_monitor::{StorageMonitorParams, StorageMonitorService}; use serde_json::Value; +use sp_api::ProvideRuntimeApi; +use sp_consensus_subspace::SubspaceApi; use sp_core::crypto::Ss58AddressFormat; use sp_core::storage::StorageKey; use sp_core::H256; @@ -30,7 +32,7 @@ use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; -use subspace_core_primitives::{BlockNumber, PublicKey}; +use subspace_core_primitives::{solution_range_to_sectors, BlockNumber, PublicKey}; use subspace_fake_runtime_api::RuntimeApi; use subspace_networking::libp2p::identity::ed25519::Keypair; use subspace_networking::libp2p::Multiaddr; @@ -120,8 +122,8 @@ struct Handlers { block_imported: Handler, } -pub(super) struct ConsensusNode { - full_node: NewFull>, +pub(crate) struct ConsensusNode { + pub full_node: NewFull>, pause_sync: Arc, chain_info: ChainInfo, handlers: Handlers, @@ -134,7 +136,7 @@ impl fmt::Debug for ConsensusNode { } impl ConsensusNode { - fn new( + pub(crate) fn new( full_node: NewFull>, pause_sync: Arc, chain_info: ChainInfo, @@ -235,6 +237,20 @@ impl ConsensusNode { self.full_node.client.info().best_number } + pub(super) fn best_block_hash(&self) -> H256 { + self.full_node.client.info().best_hash + } + + /// Returns current solution range & max. pieces in a sector + pub(super) fn tsp_metrics(&self) -> anyhow::Result<(u64, u16)> { + let runtime_api = self.full_node.client.runtime_api(); + let block_hash = self.full_node.client.info().best_hash; + let current_solution_range = runtime_api.solution_ranges(block_hash)?.current; + let max_pieces_in_sector = runtime_api.max_pieces_in_sector(block_hash)?; + + Ok((current_solution_range, max_pieces_in_sector)) + } + pub(super) fn account_balance(&self, account: &PublicKey) -> Balance { let reward_address_storage_key = account_storage_key(account); @@ -563,3 +579,19 @@ pub(super) async fn create_consensus_node( Ok(ConsensusNode::new(consensus_node, pause_sync, chain_info)) } + +pub(crate) fn total_space_pledged( + current_solution_range: u64, + slot_probability: (u64, u64), + max_pieces_in_sector: u16, +) -> u128 { + // calculate the sectors + let sectors = solution_range_to_sectors( + current_solution_range, + slot_probability, + max_pieces_in_sector, + ); + + // Calculate the total space pledged + sectors as u128 * max_pieces_in_sector as u128 * subspace_core_primitives::Piece::SIZE as u128 +} diff --git a/src/frontend.rs b/src/frontend.rs index e926519b..38b0dd77 100644 --- a/src/frontend.rs +++ b/src/frontend.rs @@ -1,4 +1,7 @@ pub mod configuration; pub mod loading; pub mod new_version; +#[path = "./frontend/widgets/progress_bar.rs"] +pub mod progress_bar; pub mod running; +pub mod utils; diff --git a/src/frontend/running.rs b/src/frontend/running.rs index 0fbcda16..1327bd9e 100644 --- a/src/frontend/running.rs +++ b/src/frontend/running.rs @@ -2,19 +2,30 @@ mod farm; mod node; use crate::backend::config::RawConfig; -use crate::backend::farmer::{FarmerNotification, InitialFarmState}; -use crate::backend::node::ChainInfo; +use crate::backend::farmer::{ + calculate_expected_reward_duration_from_now, FarmerNotification, InitialFarmState, +}; +use crate::backend::node::{total_space_pledged, ChainInfo}; use crate::backend::{FarmIndex, NodeNotification}; +use crate::frontend::progress_bar::{CircularProgressBar, DEFAULT_TOOLTIP_ETA_PROGRESS_BAR}; use crate::frontend::running::farm::{FarmWidget, FarmWidgetInit, FarmWidgetInput}; use crate::frontend::running::node::{NodeInput, NodeView}; +use crate::frontend::utils::format_eta; +use anyhow::anyhow; +use bytesize::ByteSize; +use futures::FutureExt; use gtk::prelude::*; use relm4::factory::FactoryHashMap; use relm4::prelude::*; use relm4_icons::icon_name; +use sp_consensus_subspace::ChainConstants; +use std::time::{SystemTime, UNIX_EPOCH}; use subspace_core_primitives::BlockNumber; use subspace_runtime_primitives::{Balance, SSC}; use tracing::debug; +use super::progress_bar::ProgressBarInput; + #[derive(Debug)] pub struct RunningInit { pub plotting_paused: bool, @@ -26,8 +37,9 @@ pub enum RunningInput { best_block_number: BlockNumber, reward_address_balance: Balance, initial_farm_states: Vec, - raw_config: RawConfig, + raw_config: Box, chain_info: ChainInfo, + chain_constants: ChainConstants, }, NodeNotification(NodeNotification), FarmerNotification(FarmerNotification), @@ -35,6 +47,14 @@ pub enum RunningInput { TogglePausePlotting, } +#[derive(Debug)] +pub enum CmdOut { + /// Progress update based on arc progress of ETA progress bar. + RewardEtaProgress(f64), + /// Finished signal from ETA progress bar. + RewardEtaProgressFinished, +} + #[derive(Debug)] pub enum RunningOutput { PausePlotting(bool), @@ -56,6 +76,11 @@ pub struct RunningView { farmer_state: FarmerState, farms: FactoryHashMap, plotting_paused: bool, + slot_probability: (u64, u64), + space_pledged: u128, + last_reward_timestamp: Option, + reward_eta_progress_bar: Controller, + reward_eta_progress_bar_moving: bool, } #[relm4::component(pub)] @@ -63,7 +88,7 @@ impl Component for RunningView { type Init = RunningInit; type Input = RunningInput; type Output = RunningOutput; - type CommandOutput = (); + type CommandOutput = CmdOut; view! { #[root] @@ -72,6 +97,7 @@ impl Component for RunningView { model.node_view.widget().clone(), + gtk::Separator { set_margin_all: 10, }, @@ -107,6 +133,8 @@ impl Component for RunningView { set_halign: gtk::Align::End, set_hexpand: true, + model.reward_eta_progress_bar.widget().clone(), + gtk::LinkButton { remove_css_class: "link", set_tooltip: "Total account balance and coins farmed since application started, click to see details in Astral", @@ -192,6 +220,7 @@ impl Component for RunningView { let farms = FactoryHashMap::builder() .launch(gtk::Box::default()) .detach(); + let reward_eta_progress_bar = CircularProgressBar::builder().launch(20.0).detach(); let model = Self { node_view, @@ -199,6 +228,11 @@ impl Component for RunningView { farmer_state: FarmerState::default(), farms, plotting_paused: init.plotting_paused, + slot_probability: (0, 0), + space_pledged: 0, + last_reward_timestamp: None, + reward_eta_progress_bar, + reward_eta_progress_bar_moving: false, }; let farms_box = model.farms.widget(); @@ -210,6 +244,27 @@ impl Component for RunningView { fn update(&mut self, input: Self::Input, sender: ComponentSender, _root: &Self::Root) { self.process_input(input, sender); } + + fn update_cmd( + &mut self, + message: Self::CommandOutput, + _sender: ComponentSender, + _root: &Self::Root, + ) { + match message { + CmdOut::RewardEtaProgress(p) => { + self.reward_eta_progress_bar + .emit(ProgressBarInput::SetProgress(p)); + } + CmdOut::RewardEtaProgressFinished => { + self.reward_eta_progress_bar + .emit(ProgressBarInput::SetTooltip( + DEFAULT_TOOLTIP_ETA_PROGRESS_BAR.to_string(), + )); + self.reward_eta_progress_bar_moving = !self.reward_eta_progress_bar_moving; + } + } + } } impl RunningView { @@ -221,7 +276,9 @@ impl RunningView { initial_farm_states, raw_config, chain_info, + chain_constants, } => { + let mut space_pledged: u128 = 0; for (farm_index, (initial_farm_state, farm)) in initial_farm_states .iter() .copied() @@ -234,13 +291,22 @@ impl RunningView { backend; qed", ), FarmWidgetInit { - farm, + farm: farm.clone(), total_sectors: initial_farm_state.total_sectors_count, plotted_total_sectors: initial_farm_state.plotted_sectors_count, plotting_paused: self.plotting_paused, }, ); + + // Want this to panic in case of any error which I don't expect to happen + space_pledged += farm + .size + .parse::() + .expect("Failed to parse farm size") + .0 as u128; } + self.slot_probability = chain_constants.slot_probability(); + self.space_pledged = space_pledged; self.farmer_state = FarmerState { initial_reward_address_balance: reward_address_balance, @@ -277,7 +343,11 @@ impl RunningView { } self.node_synced = new_synced; } - NodeNotification::BlockImported(imported_block) => { + NodeNotification::BlockImported { + imported_block, + current_solution_range, + max_pieces_in_sector, + } => { if !self.node_synced { // Do not count balance increase during sync as increase related to // farming, but preserve accumulated diff @@ -285,7 +355,29 @@ impl RunningView { - self.farmer_state.initial_reward_address_balance; self.farmer_state.initial_reward_address_balance = imported_block.reward_address_balance - previous_diff; + } else { + // only move the progress bar if it was stopped & the node is synced ofcourse + if !self.reward_eta_progress_bar_moving { + self.reward_eta_progress_bar_moving = + !self.reward_eta_progress_bar_moving; + + let total_space_pledged = total_space_pledged( + current_solution_range, + self.slot_probability, + max_pieces_in_sector, + ); + + let eta = calculate_expected_reward_duration_from_now( + total_space_pledged, + self.space_pledged, + self.last_reward_timestamp, + ) + .map_err(|_| anyhow!("Failed to calculate ETA for reward payment")) + .unwrap_or(0); + self.move_progress_bar(sender.clone(), eta); + } } + // In case balance decreased, subtract it from initial balance to ignore, // this typically happens due to chain reorg when reward is "disappears" if let Some(decreased_by) = self @@ -295,6 +387,22 @@ impl RunningView { { self.farmer_state.initial_reward_address_balance -= decreased_by; } + + // In case of balance increase or decrease, update the last reward timestamp + if self + .farmer_state + .reward_address_balance + .abs_diff(imported_block.reward_address_balance) + > 0 + { + self.last_reward_timestamp = Some( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + ); + } + self.farmer_state.reward_address_balance = imported_block.reward_address_balance; } @@ -333,6 +441,14 @@ impl RunningView { }, RunningInput::ToggleFarmDetails => { self.farms.broadcast(FarmWidgetInput::ToggleFarmDetails); + + // CLEANUP: remove later + // 10s ETA for testing on clicking the farm-details toggle button + // if !self.reward_eta_progress_bar_moving { + // self.reward_eta_progress_bar_moving = !self.reward_eta_progress_bar_moving; + + // self.move_progress_bar(sender.clone(), 10000); + // } } RunningInput::TogglePausePlotting => { self.plotting_paused = !self.plotting_paused; @@ -347,4 +463,48 @@ impl RunningView { } } } + + /// Move the progress bar based on the ETA + fn move_progress_bar(&mut self, sender: ComponentSender, eta: u64) { + // Format the ETA + let eta_str = format_eta(eta); + + // Update the tooltip text of the progress bar + self.reward_eta_progress_bar + .emit(ProgressBarInput::SetTooltip(format!( + "Next reward in {}", + eta_str + ))); + + sender.command(move |out, shutdown| { + shutdown + // Performs this operation until a shutdown is triggered + .register(async move { + // Get the decrease value and interval from the helper function + let (decrease_value, interval) = calculate_progress_params(eta); + + let mut percentage = 1.0; + + while percentage > 0.0 { + out.send(CmdOut::RewardEtaProgress(percentage)).unwrap(); + percentage -= decrease_value; + tokio::time::sleep(std::time::Duration::from_millis(interval)).await; + } + + out.send(CmdOut::RewardEtaProgressFinished).unwrap(); + }) + // Perform task until a shutdown interrupts it + .drop_on_shutdown() + // Wrap into a `Pin>` for return + .boxed() + }); + } +} + +/// Calculate the decrease value and interval for the progress updates +fn calculate_progress_params(eta: u64) -> (f64, u64) { + let interval = 1000; // Default interval of 1 second (1000 milliseconds) + let steps = eta / interval; + let decrease_value = 1.0 / steps as f64; + (decrease_value, interval) } diff --git a/src/frontend/running/node.rs b/src/frontend/running/node.rs index e3545a31..2b6c67e3 100644 --- a/src/frontend/running/node.rs +++ b/src/frontend/running/node.rs @@ -291,7 +291,7 @@ impl NodeView { } self.sync_state = new_sync_state; } - NodeNotification::BlockImported(imported_block) => { + NodeNotification::BlockImported { imported_block, .. } => { self.best_block_number = imported_block.number; // Ensure target is never below current block if let SyncState::Syncing { target, .. } = &mut self.sync_state { diff --git a/src/frontend/utils.rs b/src/frontend/utils.rs new file mode 100644 index 00000000..8bca1a09 --- /dev/null +++ b/src/frontend/utils.rs @@ -0,0 +1,58 @@ +const WEEK: u64 = 604800000; +const DAY: u64 = 86400000; +const HOUR: u64 = 3600000; +const MINUTE: u64 = 60000; +const SECOND: u64 = 1000; + +/// Format ETA from milliseconds to a human-readable string with up to two time units +pub(crate) fn format_eta(eta_millis: u64) -> String { + if eta_millis == 0 || eta_millis < SECOND { + return "0 secs".to_string(); + } + + let weeks = eta_millis / WEEK; + let days = (eta_millis % WEEK) / DAY; + let hours = (eta_millis % DAY) / HOUR; + let minutes = (eta_millis % HOUR) / MINUTE; + let seconds = (eta_millis % MINUTE) / SECOND; + + let mut parts = Vec::new(); + + if weeks > 0 { + parts.push(format!("{} weeks", weeks)); + } + if days > 0 { + parts.push(format!("{} days", days)); + } + if hours > 0 { + parts.push(format!("{} hours", hours)); + } + if minutes > 0 { + parts.push(format!("{} mins", minutes)); + } + if seconds > 0 { + parts.push(format!("{} secs", seconds)); + } + + parts.into_iter().take(2).collect::>().join(" ") +} + +#[test] +fn test_format_eta() { + assert_eq!(format_eta(0), "0 secs"); + assert_eq!(format_eta(999), "0 secs"); + assert_eq!(format_eta(1000), "1 secs"); + assert_eq!(format_eta(60_000), "1 mins"); + assert_eq!(format_eta(60_999), "1 mins"); + assert_eq!(format_eta(120_000), "2 mins"); + assert_eq!(format_eta(3_600_000), "1 hours"); + assert_eq!(format_eta(3_600_999), "1 hours"); + assert_eq!(format_eta(3_661_000), "1 hours 1 mins"); + assert_eq!(format_eta(86_400_000), "1 days"); + assert_eq!(format_eta(86_460_000), "1 days 1 mins"); + assert_eq!(format_eta(604_800_000), "1 weeks"); + assert_eq!(format_eta(604_860_000), "1 weeks 1 mins"); + assert_eq!(format_eta(1_000_000), "16 mins 40 secs"); + assert_eq!(format_eta(10_000_000), "2 hours 46 mins"); + assert_eq!(format_eta(864_000_000), "1 weeks 3 days"); +} diff --git a/src/frontend/widgets/progress_bar.rs b/src/frontend/widgets/progress_bar.rs new file mode 100644 index 00000000..b4ab4eaf --- /dev/null +++ b/src/frontend/widgets/progress_bar.rs @@ -0,0 +1,117 @@ +use gtk::prelude::*; +use relm4::prelude::*; +use std::cell::RefCell; +use std::f64::consts::PI; +use std::rc::Rc; + +pub(crate) const DEFAULT_TOOLTIP_ETA_PROGRESS_BAR: &str = "ETA for next reward payment"; + +#[derive(Debug, Clone)] +pub struct CircularProgressBar { + progress: Rc>, + tooltip_text: String, + diameter: f64, +} + +#[derive(Debug)] +pub enum ProgressBarInput { + SetProgress(f64), + SetTooltip(String), +} + +#[relm4::component(pub)] +impl Component for CircularProgressBar { + type Init = f64; // Diameter of the progress bar + type Input = ProgressBarInput; + type Output = (); + type CommandOutput = (); + + view! { + #[root] + gtk::DrawingArea { + set_content_width: (model.diameter + 1.0) as i32, + set_content_height: (model.diameter + 1.0) as i32, + set_margin_top: 10, + set_margin_bottom: 10, + set_margin_start: 10, + set_margin_end: 10, + set_visible: true, + set_draw_func: { + let progress_clone = model.progress.clone(); + let diameter = model.diameter; + move |_, cr, width, height| { + let percentage = *progress_clone.borrow(); + + // Center coordinates + let center_x = width as f64 / 2.0; + let center_y = height as f64 / 2.0; + + // Draw the background circle with respective color in dark/light themes + if matches!(dark_light::detect(), dark_light::Mode::Dark) { + // Grey for dark theme + cr.set_source_rgb(0.2078431373, 0.2078431373, 0.2078431373); + } else { + // White smoke for light theme + cr.set_source_rgb(0.965, 0.961, 0.957); + } + cr.arc(center_x, center_y, diameter / 2.0, 0.0, 2.0 * PI); + // let _ = cr.fill(); // NOTE: Fill w/o border color + let _ = cr.fill_preserve(); // Preserve the path for stroking + cr.set_source_rgb(0.0, 0.0, 0.0); // Black border color for both the themes. + cr.set_line_width(0.5); // Set the border width + let _ = cr.stroke(); // Draw the circle border + + // Draw the sweeping with respective color in dark/light themes + if matches!(dark_light::detect(), dark_light::Mode::Dark) { + // White smoke for dark theme + cr.set_source_rgb(0.965, 0.961, 0.957); + } else { + // Grey for light theme + cr.set_source_rgb(0.2078431373, 0.2078431373, 0.2078431373); + } + cr.arc( + center_x, + center_y, + diameter / 2.0, + -PI / 2.0, + -PI / 2.0 + 2.0 * PI * percentage, + ); + cr.line_to(center_x, center_y); + let _ = cr.fill(); + } + }, + set_tooltip_text: Some(&model.tooltip_text), + } + } + + fn init( + diameter: Self::Init, + _root: Self::Root, + _sender: ComponentSender, + ) -> ComponentParts { + let progress = Rc::new(RefCell::new(0.0)); + let tooltip_text = DEFAULT_TOOLTIP_ETA_PROGRESS_BAR.to_string(); + + let model = Self { + progress, + tooltip_text, + diameter, + }; + + let widgets = view_output!(); + ComponentParts { model, widgets } + } + + fn update(&mut self, input: Self::Input, _sender: ComponentSender, root: &Self::Root) { + match input { + ProgressBarInput::SetProgress(p) => { + *self.progress.borrow_mut() = p; + root.queue_draw(); + } + ProgressBarInput::SetTooltip(text) => { + self.tooltip_text = text; + root.set_tooltip_text(Some(&self.tooltip_text)); + } + } + } +} diff --git a/src/main.rs b/src/main.rs index 3c6df80a..8997fc28 100644 --- a/src/main.rs +++ b/src/main.rs @@ -710,6 +710,7 @@ impl App { reward_address_balance, initial_farm_states, chain_info, + chain_constants, } => { self.current_raw_config.replace(raw_config.clone()); self.current_view = View::Running; @@ -717,8 +718,9 @@ impl App { best_block_number, reward_address_balance, initial_farm_states, - raw_config, + raw_config: Box::new(raw_config), chain_info, + chain_constants, }); } BackendNotification::Node(node_notification) => {