From 737b46b8e8130659c74497e4a7f803c338ac18d7 Mon Sep 17 00:00:00 2001 From: Zura Benashvili Date: Sun, 2 Feb 2025 09:51:45 +0400 Subject: [PATCH 001/135] fix(transaction/hash): deserialization failure --- mina-p2p-messages/src/v2/hashing.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/mina-p2p-messages/src/v2/hashing.rs b/mina-p2p-messages/src/v2/hashing.rs index f138383ce6..239063f26a 100644 --- a/mina-p2p-messages/src/v2/hashing.rs +++ b/mina-p2p-messages/src/v2/hashing.rs @@ -115,7 +115,7 @@ impl Serialize for TransactionHash { if serializer.is_human_readable() { serializer.serialize_str(&self.to_string()) } else { - self.0.serialize(serializer) + serde_bytes::serialize(&*self.0, serializer) } } } @@ -129,9 +129,7 @@ impl<'de> serde::Deserialize<'de> for TransactionHash { let b58: String = Deserialize::deserialize(deserializer)?; Ok(b58.parse().map_err(|err| serde::de::Error::custom(err))?) } else { - let v = Vec::deserialize(deserializer)?; - v.try_into() - .map_err(|_| serde::de::Error::custom("transaction hash wrong size")) + serde_bytes::deserialize(deserializer) .map(Arc::new) .map(Self) } From 2538a0a057ac411d5ead8e4b28f403e0af5c16ff Mon Sep 17 00:00:00 2001 From: Zura Benashvili Date: Sun, 2 Feb 2025 10:05:10 +0400 Subject: [PATCH 002/135] fix(replay): missing transaction verifier causes failure during replay --- node/native/src/replay.rs | 8 ++++++-- node/src/recorder/recorder.rs | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/node/native/src/replay.rs b/node/native/src/replay.rs index 6c4336d2fc..0e9cc9900d 100644 --- a/node/native/src/replay.rs +++ b/node/native/src/replay.rs @@ -1,8 +1,10 @@ use std::cell::RefCell; use node::{ - core::thread, recorder::StateWithInputActionsReader, snark::BlockVerifier, ActionWithMeta, - BuildEnv, Store, + core::thread, + recorder::StateWithInputActionsReader, + snark::{BlockVerifier, TransactionVerifier}, + ActionWithMeta, BuildEnv, Store, }; use crate::NodeService; @@ -31,6 +33,8 @@ pub fn replay_state_with_input_actions( // index/srs doesn't match deserialized one. state.snark.block_verify.verifier_index = BlockVerifier::make(); state.snark.block_verify.verifier_srs = node::snark::get_srs(); + state.snark.user_command_verify.verifier_index = TransactionVerifier::make(); + state.snark.user_command_verify.verifier_srs = node::snark::get_srs(); state }; diff --git a/node/src/recorder/recorder.rs b/node/src/recorder/recorder.rs index 081c88b011..2e423f8988 100644 --- a/node/src/recorder/recorder.rs +++ b/node/src/recorder/recorder.rs @@ -109,6 +109,8 @@ impl Recorder { let mut writer = BufWriter::new(file); let encoded = data.encode().unwrap(); + // RecordedActionWithMeta::decode(&encoded) + // .expect(&format!("failed to decode encoded message: {:?}", data)); writer .write_all(&(encoded.len() as u64).to_be_bytes()) .unwrap(); From b2db602aabe39d442cac4c26709a695790c15b86 Mon Sep 17 00:00:00 2001 From: Zura Benashvili Date: Sun, 2 Feb 2025 10:08:06 +0400 Subject: [PATCH 003/135] feat(ledger): add a bug condition in case of alive masks after commit being more than 294 --- node/src/ledger/ledger_reducer.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/node/src/ledger/ledger_reducer.rs b/node/src/ledger/ledger_reducer.rs index 1e8cbb0504..b958a7b3a7 100644 --- a/node/src/ledger/ledger_reducer.rs +++ b/node/src/ledger/ledger_reducer.rs @@ -1,3 +1,5 @@ +use openmina_core::bug_condition; + use crate::Substate; use super::{ @@ -17,6 +19,12 @@ impl LedgerState { } = action { if let Ok(state) = state_context.get_substate_mut() { + if result.alive_masks > 294 { + bug_condition!( + "ledger mask leak: more than 294 ledger masks ({}) detected!", + result.alive_masks + ); + } state.alive_masks = result.alive_masks; } } From 4f097795ce7f01b241d247d516c633d0fe4f48c9 Mon Sep 17 00:00:00 2001 From: Zura Benashvili Date: Sun, 2 Feb 2025 10:12:23 +0400 Subject: [PATCH 004/135] fix(ledger): storing extra mask for the ledger of the dropped empty block after fork --- ledger/src/mask/mod.rs | 5 +- ledger/src/staged_ledger/staged_ledger.rs | 1 - node/src/ledger/ledger_service.rs | 67 +++++++++++++------ node/src/ledger/write/mod.rs | 51 +++++++++++++- .../sync/transition_frontier_sync_effects.rs | 14 +--- 5 files changed, 105 insertions(+), 33 deletions(-) diff --git a/ledger/src/mask/mod.rs b/ledger/src/mask/mod.rs index 7397d79d8a..97a75911da 100644 --- a/ledger/src/mask/mod.rs +++ b/ledger/src/mask/mod.rs @@ -41,6 +41,9 @@ pub fn alive_len() -> usize { exec(|list| list.len()) } -pub fn alive_collect() -> Vec { +pub fn alive_collect() -> B +where + B: FromIterator, +{ exec(|list| list.iter().cloned().collect()) } diff --git a/ledger/src/staged_ledger/staged_ledger.rs b/ledger/src/staged_ledger/staged_ledger.rs index ec547f4c51..1076accc92 100644 --- a/ledger/src/staged_ledger/staged_ledger.rs +++ b/ledger/src/staged_ledger/staged_ledger.rs @@ -116,7 +116,6 @@ pub struct StagedLedger { } impl StagedLedger { - #[cfg(feature = "fuzzing")] pub fn ledger_ref(&self) -> &Mask { &self.ledger } diff --git a/node/src/ledger/ledger_service.rs b/node/src/ledger/ledger_service.rs index f8e34c15d3..c3065c3c3b 100644 --- a/node/src/ledger/ledger_service.rs +++ b/node/src/ledger/ledger_service.rs @@ -1,10 +1,7 @@ use super::{ ledger_empty_hash_at_depth, - read::LedgerReadResponse, - read::{LedgerReadId, LedgerReadRequest}, - write::CommitResult, - write::LedgerWriteRequest, - write::LedgerWriteResponse, + read::{LedgerReadId, LedgerReadRequest, LedgerReadResponse}, + write::{CommitResult, LedgerWriteRequest, LedgerWriteResponse, LedgersToKeep}, LedgerAddress, LedgerEvent, LEDGER_DEPTH, }; use crate::{ @@ -65,8 +62,8 @@ use mina_p2p_messages::{ }; use mina_signer::CompressedPubKey; use openmina_core::{ - block::AppliedBlock, - block::ArcBlockWithHash, + block::{AppliedBlock, ArcBlockWithHash}, + bug_condition, constants::constraint_constants, snark::{Snark, SnarkJobId}, thread, @@ -137,18 +134,18 @@ impl StagedLedgersStorage { fn retain(&mut self, fun: F) where - F: Fn(&LedgerHash, &[Arc]) -> bool, + F: Fn(&MinaBaseStagedLedgerHashStableV1) -> bool, { - self.by_merkle_root_hash - .retain(|merkle_root_hash, staged_ledger_hashes| { - let retain = fun(merkle_root_hash, staged_ledger_hashes.as_slice()); - if !retain { - for staged_ledger_hash in staged_ledger_hashes { - self.staged_ledgers.remove(staged_ledger_hash); - } + self.by_merkle_root_hash.retain(|_, staged_ledger_hashes| { + staged_ledger_hashes.retain(|hash| { + if fun(hash) { + return true; } - retain + self.staged_ledgers.remove(hash); + false }); + !staged_ledger_hashes.is_empty() + }); } fn extend(&mut self, iterator: I) @@ -857,7 +854,7 @@ impl LedgerCtx { pub fn commit( &mut self, - ledgers_to_keep: BTreeSet, + ledgers_to_keep: LedgersToKeep, root_snarked_ledger_updates: TransitionFrontierRootSnarkedLedgerUpdates, needed_protocol_states: BTreeMap, new_root: &ArcBlockWithHash, @@ -902,13 +899,13 @@ impl LedgerCtx { ); self.staged_ledgers - .retain(|hash, _| ledgers_to_keep.contains(hash)); + .retain(|hash| ledgers_to_keep.contains(hash)); self.staged_ledgers.extend( self.sync .staged_ledgers .take() .into_iter() - .filter(|(hash, _)| ledgers_to_keep.contains(&hash.non_snark.ledger_hash)), + .filter(|(hash, _)| ledgers_to_keep.contains(&**hash)), ); for ledger_hash in [ @@ -973,6 +970,8 @@ impl LedgerCtx { .unwrap_or_default(), ); + // self.check_alive_masks(); + CommitResult { alive_masks: ::ledger::mask::alive_len(), available_jobs, @@ -980,6 +979,36 @@ impl LedgerCtx { } } + #[allow(dead_code)] + fn check_alive_masks(&mut self) { + let mut alive: BTreeSet<_> = ::ledger::mask::alive_collect(); + let staged_ledgers = self + .staged_ledgers + .staged_ledgers + .iter() + .map(|(hash, ledger)| (&hash.non_snark.ledger_hash, ledger.ledger_ref())); + + let alive_ledgers = self + .snarked_ledgers + .iter() + .chain(staged_ledgers) + .map(|(hash, mask)| { + let uuid = mask.get_uuid(); + if !alive.remove(&uuid) { + bug_condition!("mask not found among alive masks! uuid: {uuid}, hash: {hash}"); + } + (uuid, hash) + }) + .collect::>(); + openmina_core::debug!(redux::Timestamp::global_now(); "alive_ledgers_after_commit: {alive_ledgers:#?}"); + + if !alive.is_empty() { + bug_condition!( + "masks alive which are no longer part of the ledger service: {alive:#?}" + ); + } + } + pub fn get_num_accounts( &mut self, ledger_hash: v2::LedgerHash, diff --git a/node/src/ledger/write/mod.rs b/node/src/ledger/write/mod.rs index 749f57c090..fbb810e049 100644 --- a/node/src/ledger/write/mod.rs +++ b/node/src/ledger/write/mod.rs @@ -54,7 +54,7 @@ pub enum LedgerWriteRequest { skip_verification: bool, }, Commit { - ledgers_to_keep: BTreeSet, + ledgers_to_keep: LedgersToKeep, root_snarked_ledger_updates: TransitionFrontierRootSnarkedLedgerUpdates, needed_protocol_states: BTreeMap, new_root: AppliedBlock, @@ -162,6 +162,55 @@ impl TryFrom<&BlockApplyResult> for v2::ArchiveTransitionFronntierDiff { } } +#[derive(Serialize, Deserialize, Debug, Ord, PartialOrd, Eq, PartialEq, Default, Clone)] +pub struct LedgersToKeep { + snarked: BTreeSet, + staged: BTreeSet>, +} + +impl LedgersToKeep { + pub fn new() -> Self { + Self::default() + } + + pub fn contains<'a, T>(&self, key: T) -> bool + where + T: 'a + Into>, + { + match key.into() { + LedgerToKeep::Snarked(hash) => self.snarked.contains(hash), + LedgerToKeep::Staged(hash) => self.staged.contains(hash), + } + } + + pub fn add_snarked(&mut self, hash: v2::LedgerHash) -> bool { + self.snarked.insert(hash) + } + + pub fn add_staged(&mut self, hash: Arc) -> bool { + self.staged.insert(hash) + } +} + +impl<'a> FromIterator<&'a ArcBlockWithHash> for LedgersToKeep { + fn from_iter>(iter: T) -> Self { + let mut res = Self::new(); + for block in iter { + res.add_snarked(block.snarked_ledger_hash().clone()); + res.add_snarked(block.staking_epoch_ledger_hash().clone()); + res.add_snarked(block.next_epoch_ledger_hash().clone()); + res.add_staged(Arc::new(block.staged_ledger_hashes().clone())); + } + res + } +} + +#[derive(derive_more::From)] +pub enum LedgerToKeep<'a> { + Snarked(&'a v2::LedgerHash), + Staged(&'a v2::MinaBaseStagedLedgerHashStableV1), +} + #[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct CommitResult { pub alive_masks: usize, diff --git a/node/src/transition_frontier/sync/transition_frontier_sync_effects.rs b/node/src/transition_frontier/sync/transition_frontier_sync_effects.rs index 8ba7ae8071..115cb6d146 100644 --- a/node/src/transition_frontier/sync/transition_frontier_sync_effects.rs +++ b/node/src/transition_frontier/sync/transition_frontier_sync_effects.rs @@ -4,7 +4,7 @@ use p2p::channels::rpc::{P2pChannelsRpcAction, P2pRpcId}; use p2p::{P2pNetworkPubsubAction, PeerId}; use redux::ActionMeta; -use crate::ledger::write::{LedgerWriteAction, LedgerWriteRequest}; +use crate::ledger::write::{LedgerWriteAction, LedgerWriteRequest, LedgersToKeep}; use crate::p2p::channels::rpc::P2pRpcRequest; use crate::service::TransitionFrontierSyncLedgerSnarkedService; use crate::{p2p_ready, Service, Store, TransitionFrontierAction}; @@ -354,16 +354,8 @@ impl TransitionFrontierSyncAction { }; let ledgers_to_keep = chain .iter() - .flat_map(|b| { - [ - b.snarked_ledger_hash(), - b.merkle_root_hash(), - b.staking_epoch_ledger_hash(), - b.next_epoch_ledger_hash(), - ] - }) - .cloned() - .collect(); + .map(|block| &block.block) + .collect::(); let mut root_snarked_ledger_updates = root_snarked_ledger_updates.clone(); if transition_frontier .best_chain From 8e9d582ef32f2bd3c04170f650bac5dcbab0f513 Mon Sep 17 00:00:00 2001 From: Zura Benashvili Date: Sun, 2 Feb 2025 10:14:32 +0400 Subject: [PATCH 005/135] fix(p2p/webrtc): non-determinism in disconnection logic causing replay to fail --- p2p/src/service_impl/webrtc/mod.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/p2p/src/service_impl/webrtc/mod.rs b/p2p/src/service_impl/webrtc/mod.rs index 1ca88e3a37..6cef3522a4 100644 --- a/p2p/src/service_impl/webrtc/mod.rs +++ b/p2p/src/service_impl/webrtc/mod.rs @@ -846,13 +846,14 @@ pub trait P2pServiceWebrtc: redux::Service { } fn disconnect(&mut self, peer_id: PeerId) -> bool { + // TODO(binier): improve // By removing the peer, `abort` gets dropped which will // cause `peer_loop` to end. - if let Some(peer) = self.peers().remove(&peer_id) { - if peer.abort.receiver_count() > 0 { - // peer disconnection not yet finished - return false; - } + if let Some(_peer) = self.peers().remove(&peer_id) { + // if peer.abort.receiver_count() > 0 { + // // peer disconnection not yet finished + // return false; + // } } else { openmina_core::error!(openmina_core::log::system_time(); "`disconnect` shouldn't be used for libp2p peers"); } From bce875a62404b202f60fd3062e2ad9b7eadf9cc1 Mon Sep 17 00:00:00 2001 From: Zura Benashvili Date: Sun, 2 Feb 2025 10:22:22 +0400 Subject: [PATCH 006/135] feat(replay): add cli arg to ignore build env mismatch --- .../replay/replay_state_with_input_actions.rs | 19 ++++++++++++++++--- node/native/src/replay.rs | 5 +++-- .../record_replay/block_production.rs | 3 ++- .../src/scenarios/record_replay/bootstrap.rs | 3 ++- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/cli/src/commands/replay/replay_state_with_input_actions.rs b/cli/src/commands/replay/replay_state_with_input_actions.rs index 637f378e2a..d5271eaa81 100644 --- a/cli/src/commands/replay/replay_state_with_input_actions.rs +++ b/cli/src/commands/replay/replay_state_with_input_actions.rs @@ -10,6 +10,9 @@ pub struct ReplayStateWithInputActions { #[arg(long, default_value = "./target/release/libreplay_dynamic_effects.so")] pub dynamic_effects_lib: String, + #[arg(long)] + pub ignore_mismatch: bool, + /// Verbosity level #[arg(long, short, default_value = "info")] pub verbosity: tracing::Level, @@ -30,13 +33,22 @@ impl ReplayStateWithInputActions { } }; - replay_state_with_input_actions(&dir, dynamic_effects_lib, check_build_env)?; + replay_state_with_input_actions( + &dir, + dynamic_effects_lib, + self.ignore_mismatch, + check_build_env, + )?; Ok(()) } } -pub fn check_build_env(record_env: &BuildEnv, replay_env: &BuildEnv) -> anyhow::Result<()> { +pub fn check_build_env( + record_env: &BuildEnv, + replay_env: &BuildEnv, + ignore_mismatch: bool, +) -> anyhow::Result<()> { let is_git_same = record_env.git.commit_hash == replay_env.git.commit_hash; let is_cargo_same = record_env.cargo == replay_env.cargo; let is_rustc_same = record_env.rustc == replay_env.rustc; @@ -47,7 +59,8 @@ pub fn check_build_env(record_env: &BuildEnv, replay_env: &BuildEnv) -> anyhow:: record_env.git, replay_env.git ); let msg = format!("git build env mismatch!\n{diff}"); - if console::user_attended() { + if ignore_mismatch { + } else if console::user_attended() { use dialoguer::Confirm; let prompt = format!("{msg}\nDo you want to continue?"); diff --git a/node/native/src/replay.rs b/node/native/src/replay.rs index 0e9cc9900d..0dcef6264e 100644 --- a/node/native/src/replay.rs +++ b/node/native/src/replay.rs @@ -12,7 +12,8 @@ use crate::NodeService; pub fn replay_state_with_input_actions( dir: &str, dynamic_effects_lib: Option, - mut check_build_env: impl FnMut(&BuildEnv, &BuildEnv) -> anyhow::Result<()>, + ignore_mismatch: bool, + mut check_build_env: impl FnMut(&BuildEnv, &BuildEnv, bool) -> anyhow::Result<()>, ) -> anyhow::Result { eprintln!("replaying node based on initial state and actions from the dir: {dir}"); let reader = StateWithInputActionsReader::new(dir); @@ -50,7 +51,7 @@ pub fn replay_state_with_input_actions( let store = node.store_mut(); let replay_env = BuildEnv::get(); - check_build_env(&store.state().config.build, &replay_env)?; + check_build_env(&store.state().config.build, &replay_env, ignore_mismatch)?; eprintln!("reading actions from dir: {dir}"); diff --git a/node/testing/src/scenarios/record_replay/block_production.rs b/node/testing/src/scenarios/record_replay/block_production.rs index 6f11922dee..a4230de90f 100644 --- a/node/testing/src/scenarios/record_replay/block_production.rs +++ b/node/testing/src/scenarios/record_replay/block_production.rs @@ -50,7 +50,8 @@ impl RecordReplayBlockProduction { let replayed_node = replay_state_with_input_actions( recording_dir.as_os_str().to_str().unwrap(), None, - |_, _| Ok(()), + false, + |_, _, _| Ok(()), ) .expect("replay failed"); diff --git a/node/testing/src/scenarios/record_replay/bootstrap.rs b/node/testing/src/scenarios/record_replay/bootstrap.rs index 382ac644ff..f081b98a6d 100644 --- a/node/testing/src/scenarios/record_replay/bootstrap.rs +++ b/node/testing/src/scenarios/record_replay/bootstrap.rs @@ -51,7 +51,8 @@ impl RecordReplayBootstrap { let replayed_node = replay_state_with_input_actions( recording_dir.as_os_str().to_str().unwrap(), None, - |_, _| Ok(()), + false, + |_, _, _| Ok(()), ) .expect("replay failed"); From 5778a5cf2e9e372affca13ecceb0fc2186cfd6b6 Mon Sep 17 00:00:00 2001 From: Zura Benashvili Date: Sun, 2 Feb 2025 10:46:17 +0400 Subject: [PATCH 007/135] fix(ledger): only keep staking/next epoch ledgers for best tip, not every block --- node/src/ledger/write/mod.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/node/src/ledger/write/mod.rs b/node/src/ledger/write/mod.rs index fbb810e049..9cb642b97d 100644 --- a/node/src/ledger/write/mod.rs +++ b/node/src/ledger/write/mod.rs @@ -195,12 +195,21 @@ impl LedgersToKeep { impl<'a> FromIterator<&'a ArcBlockWithHash> for LedgersToKeep { fn from_iter>(iter: T) -> Self { let mut res = Self::new(); - for block in iter { + let best_tip = iter.into_iter().fold(None, |best_tip, block| { res.add_snarked(block.snarked_ledger_hash().clone()); - res.add_snarked(block.staking_epoch_ledger_hash().clone()); - res.add_snarked(block.next_epoch_ledger_hash().clone()); res.add_staged(Arc::new(block.staged_ledger_hashes().clone())); + match best_tip { + None => Some(block), + Some(tip) if tip.height() < block.height() => Some(block), + old => old, + } + }); + + if let Some(best_tip) = best_tip { + res.add_snarked(best_tip.staking_epoch_ledger_hash().clone()); + res.add_snarked(best_tip.next_epoch_ledger_hash().clone()); } + res } } From 6efb1bb9de06a103135cfabcc6872a70f76f343b Mon Sep 17 00:00:00 2001 From: Zura Benashvili Date: Mon, 3 Feb 2025 12:50:00 +0400 Subject: [PATCH 008/135] fix(ledger): log alive_masks > 294 instead of bug_condition since we get false positive during testing --- node/src/ledger/ledger_reducer.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/node/src/ledger/ledger_reducer.rs b/node/src/ledger/ledger_reducer.rs index b958a7b3a7..7bdc1b00df 100644 --- a/node/src/ledger/ledger_reducer.rs +++ b/node/src/ledger/ledger_reducer.rs @@ -1,5 +1,3 @@ -use openmina_core::bug_condition; - use crate::Substate; use super::{ @@ -20,7 +18,11 @@ impl LedgerState { { if let Ok(state) = state_context.get_substate_mut() { if result.alive_masks > 294 { - bug_condition!( + // TODO(binier): should be a bug condition, but can't be + // because we get false positive during testing, since + // multiple nodes/ledger run in the same process. + openmina_core::log::warn!( + meta.time(); "ledger mask leak: more than 294 ledger masks ({}) detected!", result.alive_masks ); From 66a4ce48be1ebcf7943200ff99f853aae5bae6cc Mon Sep 17 00:00:00 2001 From: Daniel Kuehr Date: Mon, 3 Feb 2025 10:50:37 -0500 Subject: [PATCH 009/135] fix: replace unwrap for non-panicking code --- ledger/src/proofs/step.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ledger/src/proofs/step.rs b/ledger/src/proofs/step.rs index acb3613160..ca0e39fa1d 100644 --- a/ledger/src/proofs/step.rs +++ b/ledger/src/proofs/step.rs @@ -1948,8 +1948,10 @@ pub fn expand_deferred(params: ExpandDeferredParams) -> Result = - Radix2EvaluationDomain::new(1 << step_domain as u64).unwrap(); + let Some(domain) = Radix2EvaluationDomain::::new(1 << step_domain as u64) else { + return Err(InvalidBigInt); + }; + let zetaw = zeta * domain.group_gen; let plonk_minimal = PlonkMinimal:: { From 8feadec6545d7074c43b739e516f176eee532477 Mon Sep 17 00:00:00 2001 From: Bruno Deferrari Date: Mon, 3 Feb 2025 11:30:57 -0300 Subject: [PATCH 010/135] fix(heartbeats): Limit of end time for score calculation --- tools/heartbeats-processor/src/local_db.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/heartbeats-processor/src/local_db.rs b/tools/heartbeats-processor/src/local_db.rs index 6a9f62ffbd..0c920cee0a 100644 --- a/tools/heartbeats-processor/src/local_db.rs +++ b/tools/heartbeats-processor/src/local_db.rs @@ -498,6 +498,7 @@ pub async fn update_scores(pool: &SqlitePool) -> Result<()> { LEFT JOIN time_windows tw ON hp.window_id = tw.id LEFT JOIN produced_blocks pb ON pk.id = pb.public_key_id WHERE tw.disabled = FALSE + AND tw.end_time <= strftime('%s', 'now') GROUP BY pk.id ON CONFLICT(public_key_id) DO UPDATE SET score = excluded.score, From b26cad3b7cfbbfba655d6817a17d2bec26aa0676 Mon Sep 17 00:00:00 2001 From: Bruno Deferrari Date: Tue, 4 Feb 2025 09:48:49 -0300 Subject: [PATCH 011/135] fix(heartbeats): Use right time filters when computing scores --- tools/heartbeats-processor/src/local_db.rs | 25 +++++++++++++++------- tools/heartbeats-processor/src/main.rs | 14 +++++++----- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/tools/heartbeats-processor/src/local_db.rs b/tools/heartbeats-processor/src/local_db.rs index 0c920cee0a..1bba0ac722 100644 --- a/tools/heartbeats-processor/src/local_db.rs +++ b/tools/heartbeats-processor/src/local_db.rs @@ -485,26 +485,35 @@ pub async fn toggle_windows( // TODO: multiple blocks for the same slot should be counted as one // TODO: take into account the validated flag to count blocks -pub async fn update_scores(pool: &SqlitePool) -> Result<()> { +pub async fn update_scores(pool: &SqlitePool, config: &Config) -> Result<()> { + let window_start = to_unix_timestamp(config.window_range_start); + let current_time = chrono::Utc::now().timestamp(); + sqlx::query!( r#" INSERT INTO submitter_scores (public_key_id, score, blocks_produced) SELECT pk.id, - COUNT(DISTINCT hp.window_id) as score, + COUNT(DISTINCT CASE + WHEN tw.disabled = FALSE + AND tw.end_time <= ?2 + AND tw.start_time >= ?1 + THEN hp.window_id + ELSE NULL + END) as score, COUNT(DISTINCT pb.id) as blocks_produced FROM public_keys pk LEFT JOIN heartbeat_presence hp ON pk.id = hp.public_key_id LEFT JOIN time_windows tw ON hp.window_id = tw.id - LEFT JOIN produced_blocks pb ON pk.id = pb.public_key_id - WHERE tw.disabled = FALSE - AND tw.end_time <= strftime('%s', 'now') + LEFT JOIN produced_blocks pb ON pk.id = pb.public_key_id AND pb.window_id = tw.id GROUP BY pk.id ON CONFLICT(public_key_id) DO UPDATE SET score = excluded.score, blocks_produced = excluded.blocks_produced, last_updated = strftime('%s', 'now') - "# + "#, + window_start, + current_time ) .execute(pool) .await?; @@ -538,9 +547,9 @@ pub async fn get_max_scores(pool: &SqlitePool) -> Result { Ok(MaxScores { total, current }) } -pub async fn view_scores(pool: &SqlitePool) -> Result<()> { +pub async fn view_scores(pool: &SqlitePool, config: &Config) -> Result<()> { // Make sure scores are up to date - update_scores(pool).await?; + update_scores(pool, config).await?; let scores = sqlx::query!( r#" diff --git a/tools/heartbeats-processor/src/main.rs b/tools/heartbeats-processor/src/main.rs index 80e1c4ae7d..1b18451c99 100644 --- a/tools/heartbeats-processor/src/main.rs +++ b/tools/heartbeats-processor/src/main.rs @@ -61,9 +61,13 @@ enum Commands { }, } -async fn post_scores_to_firestore(pool: &SqlitePool, db: &FirestoreDb) -> Result<()> { +async fn post_scores_to_firestore( + pool: &SqlitePool, + db: &FirestoreDb, + config: &Config, +) -> Result<()> { // Make sure scores are up to date - local_db::update_scores(pool).await?; + local_db::update_scores(pool, config).await?; let scores = sqlx::query!( r#" @@ -109,7 +113,7 @@ async fn run_process_loop( local_db::process_heartbeats(db, pool, config).await?; println!("Posting scores..."); - post_scores_to_firestore(pool, db).await?; + post_scores_to_firestore(pool, db, config).await?; println!("Sleeping for {} seconds...", interval_seconds); tokio::time::sleep(interval).await; @@ -149,12 +153,12 @@ async fn main() -> Result<()> { local_db::toggle_windows(&pool, start, end, disabled).await?; } Commands::ViewScores => { - local_db::view_scores(&pool).await?; + local_db::view_scores(&pool, &config).await?; } Commands::PostScores => { println!("Initializing firestore connection..."); let db = remote_db::get_db(&config).await?; - post_scores_to_firestore(&pool, &db).await?; + post_scores_to_firestore(&pool, &db, &config).await?; } Commands::SetLastProcessed { time } => { local_db::set_last_processed_time(&pool, &time).await?; From ad95bbff53e31a09046781b35330f1c9dbdd9c57 Mon Sep 17 00:00:00 2001 From: Bruno Deferrari Date: Tue, 4 Feb 2025 10:31:02 -0300 Subject: [PATCH 012/135] fix(heartbeats): Count only one block per global slot per submitter --- tools/heartbeats-processor/src/local_db.rs | 54 +++++++++++++++------- 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/tools/heartbeats-processor/src/local_db.rs b/tools/heartbeats-processor/src/local_db.rs index 1bba0ac722..5fbef6a173 100644 --- a/tools/heartbeats-processor/src/local_db.rs +++ b/tools/heartbeats-processor/src/local_db.rs @@ -483,7 +483,6 @@ pub async fn toggle_windows( Ok(()) } -// TODO: multiple blocks for the same slot should be counted as one // TODO: take into account the validated flag to count blocks pub async fn update_scores(pool: &SqlitePool, config: &Config) -> Result<()> { let window_start = to_unix_timestamp(config.window_range_start); @@ -491,22 +490,45 @@ pub async fn update_scores(pool: &SqlitePool, config: &Config) -> Result<()> { sqlx::query!( r#" + WITH ValidWindows AS ( + SELECT id, start_time, end_time + FROM time_windows + WHERE disabled = FALSE + AND end_time <= ?2 + AND start_time >= ?1 + ), + BlockCounts AS ( + -- Count one block per global slot per producer + SELECT + public_key_id, + COUNT(DISTINCT block_global_slot) as blocks + FROM ( + -- Deduplicate blocks per global slot + SELECT + pb.public_key_id, + pb.block_global_slot + FROM produced_blocks pb + JOIN ValidWindows vw ON vw.id = pb.window_id + -- TODO: enable once block proof validation has been implemented + -- WHERE pb.validated = TRUE + GROUP BY pb.public_key_id, pb.block_global_slot + ) unique_blocks + GROUP BY public_key_id + ), + HeartbeatCounts AS ( + SELECT hp.public_key_id, COUNT(DISTINCT hp.window_id) as heartbeats + FROM heartbeat_presence hp + JOIN ValidWindows vw ON vw.id = hp.window_id + GROUP BY hp.public_key_id + ) INSERT INTO submitter_scores (public_key_id, score, blocks_produced) SELECT pk.id, - COUNT(DISTINCT CASE - WHEN tw.disabled = FALSE - AND tw.end_time <= ?2 - AND tw.start_time >= ?1 - THEN hp.window_id - ELSE NULL - END) as score, - COUNT(DISTINCT pb.id) as blocks_produced + COALESCE(hc.heartbeats, 0) as score, + COALESCE(bc.blocks, 0) as blocks_produced FROM public_keys pk - LEFT JOIN heartbeat_presence hp ON pk.id = hp.public_key_id - LEFT JOIN time_windows tw ON hp.window_id = tw.id - LEFT JOIN produced_blocks pb ON pk.id = pb.public_key_id AND pb.window_id = tw.id - GROUP BY pk.id + LEFT JOIN HeartbeatCounts hc ON hc.public_key_id = pk.id + LEFT JOIN BlockCounts bc ON bc.public_key_id = pk.id ON CONFLICT(public_key_id) DO UPDATE SET score = excluded.score, blocks_produced = excluded.blocks_produced, @@ -569,11 +591,11 @@ pub async fn view_scores(pool: &SqlitePool, config: &Config) -> Result<()> { let max_scores = get_max_scores(pool).await?; println!("\nSubmitter Scores:"); - println!("----------------------------------------"); + println!("--------------------------------------------------------"); println!( - "Public Key | Score | Blocks | Current Max | Total Max | Last Updated" + "Public Key | Score | Blocks | Current Max | Total Max | Last Updated" ); - println!("----------------------------------------"); + println!("--------------------------------------------------------"); for row in scores { println!( From f2e1b4de9fb5d71b60c63e425849fc4107d7d436 Mon Sep 17 00:00:00 2001 From: Bruno Deferrari Date: Tue, 4 Feb 2025 13:06:15 -0300 Subject: [PATCH 013/135] fix(heartbeats): Don't store scores that equal to 0 --- tools/heartbeats-processor/src/local_db.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/heartbeats-processor/src/local_db.rs b/tools/heartbeats-processor/src/local_db.rs index 5fbef6a173..4027f2f842 100644 --- a/tools/heartbeats-processor/src/local_db.rs +++ b/tools/heartbeats-processor/src/local_db.rs @@ -529,6 +529,7 @@ pub async fn update_scores(pool: &SqlitePool, config: &Config) -> Result<()> { FROM public_keys pk LEFT JOIN HeartbeatCounts hc ON hc.public_key_id = pk.id LEFT JOIN BlockCounts bc ON bc.public_key_id = pk.id + WHERE hc.heartbeats > 0 OR bc.blocks > 0 ON CONFLICT(public_key_id) DO UPDATE SET score = excluded.score, blocks_produced = excluded.blocks_produced, From db647cbb62a7f7b372b14f8d326102ae657f535c Mon Sep 17 00:00:00 2001 From: Bruno Deferrari Date: Tue, 4 Feb 2025 10:00:22 -0300 Subject: [PATCH 014/135] chore(heartbeats): Update .sqlx files --- ...70e15dbb0293366513edb3945b91c2ae62ca2827ecc5.json | 12 ++++++++++++ ...f09715e00c3638e403705c437816d56de4af3fcbdb17.json | 12 ------------ 2 files changed, 12 insertions(+), 12 deletions(-) create mode 100644 tools/heartbeats-processor/.sqlx/query-14d5d1973bb6a28e4be770e15dbb0293366513edb3945b91c2ae62ca2827ecc5.json delete mode 100644 tools/heartbeats-processor/.sqlx/query-bc586a064ad3094fe93bf09715e00c3638e403705c437816d56de4af3fcbdb17.json diff --git a/tools/heartbeats-processor/.sqlx/query-14d5d1973bb6a28e4be770e15dbb0293366513edb3945b91c2ae62ca2827ecc5.json b/tools/heartbeats-processor/.sqlx/query-14d5d1973bb6a28e4be770e15dbb0293366513edb3945b91c2ae62ca2827ecc5.json new file mode 100644 index 0000000000..14eccf3968 --- /dev/null +++ b/tools/heartbeats-processor/.sqlx/query-14d5d1973bb6a28e4be770e15dbb0293366513edb3945b91c2ae62ca2827ecc5.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n WITH ValidWindows AS (\n SELECT id, start_time, end_time\n FROM time_windows\n WHERE disabled = FALSE\n AND end_time <= ?2\n AND start_time >= ?1\n ),\n BlockCounts AS (\n -- Count one block per global slot per producer\n SELECT\n public_key_id,\n COUNT(DISTINCT block_global_slot) as blocks\n FROM (\n -- Deduplicate blocks per global slot\n SELECT\n pb.public_key_id,\n pb.block_global_slot\n FROM produced_blocks pb\n JOIN ValidWindows vw ON vw.id = pb.window_id\n -- TODO: enable once block proof validation has been implemented\n -- WHERE pb.validated = TRUE\n GROUP BY pb.public_key_id, pb.block_global_slot\n ) unique_blocks\n GROUP BY public_key_id\n ),\n HeartbeatCounts AS (\n SELECT hp.public_key_id, COUNT(DISTINCT hp.window_id) as heartbeats\n FROM heartbeat_presence hp\n JOIN ValidWindows vw ON vw.id = hp.window_id\n GROUP BY hp.public_key_id\n )\n INSERT INTO submitter_scores (public_key_id, score, blocks_produced)\n SELECT\n pk.id,\n COALESCE(hc.heartbeats, 0) as score,\n COALESCE(bc.blocks, 0) as blocks_produced\n FROM public_keys pk\n LEFT JOIN HeartbeatCounts hc ON hc.public_key_id = pk.id\n LEFT JOIN BlockCounts bc ON bc.public_key_id = pk.id\n WHERE hc.heartbeats > 0 OR bc.blocks > 0\n ON CONFLICT(public_key_id) DO UPDATE SET\n score = excluded.score,\n blocks_produced = excluded.blocks_produced,\n last_updated = strftime('%s', 'now')\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "14d5d1973bb6a28e4be770e15dbb0293366513edb3945b91c2ae62ca2827ecc5" +} diff --git a/tools/heartbeats-processor/.sqlx/query-bc586a064ad3094fe93bf09715e00c3638e403705c437816d56de4af3fcbdb17.json b/tools/heartbeats-processor/.sqlx/query-bc586a064ad3094fe93bf09715e00c3638e403705c437816d56de4af3fcbdb17.json deleted file mode 100644 index f8a843138a..0000000000 --- a/tools/heartbeats-processor/.sqlx/query-bc586a064ad3094fe93bf09715e00c3638e403705c437816d56de4af3fcbdb17.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO submitter_scores (public_key_id, score, blocks_produced)\n SELECT\n pk.id,\n COUNT(DISTINCT hp.window_id) as score,\n COUNT(DISTINCT pb.id) as blocks_produced\n FROM public_keys pk\n LEFT JOIN heartbeat_presence hp ON pk.id = hp.public_key_id\n LEFT JOIN time_windows tw ON hp.window_id = tw.id\n LEFT JOIN produced_blocks pb ON pk.id = pb.public_key_id\n WHERE tw.disabled = FALSE\n GROUP BY pk.id\n ON CONFLICT(public_key_id) DO UPDATE SET\n score = excluded.score,\n blocks_produced = excluded.blocks_produced,\n last_updated = strftime('%s', 'now')\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 0 - }, - "nullable": [] - }, - "hash": "bc586a064ad3094fe93bf09715e00c3638e403705c437816d56de4af3fcbdb17" -} From 91481e6086d53de8e2094f9008ed1093569c7c8b Mon Sep 17 00:00:00 2001 From: Zura Benashvili Date: Wed, 5 Feb 2025 00:04:54 +0400 Subject: [PATCH 015/135] fix(ledger): don't panic if staged ledger diff creation fails --- node/src/ledger/write/ledger_write_reducer.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/node/src/ledger/write/ledger_write_reducer.rs b/node/src/ledger/write/ledger_write_reducer.rs index b8019024f4..5584d36f79 100644 --- a/node/src/ledger/write/ledger_write_reducer.rs +++ b/node/src/ledger/write/ledger_write_reducer.rs @@ -1,3 +1,4 @@ +use openmina_core::bug_condition; use redux::Dispatcher; use crate::{ @@ -120,7 +121,9 @@ impl LedgerWriteState { && global_slot_since_genesis == expected_global_slot { match result { - Err(err) => todo!("handle staged ledger diff creation err: {err}"), + Err(err) => { + bug_condition!("StagedLedgerDiffCreate error: {err}"); + } Ok(output) => { dispatcher.push(BlockProducerAction::StagedLedgerDiffCreateSuccess { output, From 9579a9858b6358280faea2b90a9d7ecde62317b2 Mon Sep 17 00:00:00 2001 From: Daniel Kuehr Date: Tue, 4 Feb 2025 18:23:26 -0500 Subject: [PATCH 016/135] verification: prevent dump files during fuzzing --- ledger/src/proofs/step.rs | 7 ++++++- ledger/src/proofs/verification.rs | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/ledger/src/proofs/step.rs b/ledger/src/proofs/step.rs index ca0e39fa1d..2dc4f885c6 100644 --- a/ledger/src/proofs/step.rs +++ b/ledger/src/proofs/step.rs @@ -1948,7 +1948,12 @@ pub fn expand_deferred(params: ExpandDeferredParams) -> Result::new(1 << step_domain as u64) else { + + let Some(num_coeffs) = 1u64.checked_shl(step_domain as u32) else { + return Err(InvalidBigInt); + }; + + let Some(domain) = Radix2EvaluationDomain::::new(num_coeffs as usize) else { return Err(InvalidBigInt); }; diff --git a/ledger/src/proofs/verification.rs b/ledger/src/proofs/verification.rs index 9551ea7fe1..0e57d659d3 100644 --- a/ledger/src/proofs/verification.rs +++ b/ledger/src/proofs/verification.rs @@ -976,7 +976,7 @@ mod on_fail { #[allow(unreachable_code)] fn dump_to_file(data: &D, filename: &str) { - #[cfg(test)] + #[cfg(any(test, feature = "fuzzing"))] { let (_, _) = (data, filename); // avoid unused vars return; From 1116084e9eca05b5dd2633ac3cf7a3625e22ea50 Mon Sep 17 00:00:00 2001 From: Teofil Jolte Date: Wed, 5 Feb 2025 14:35:56 +0200 Subject: [PATCH 017/135] Leaderboard improvements and changes --- frontend/package.json | 2 +- frontend/src/app/app.component.html | 3 +- frontend/src/app/app.component.scss | 14 ++++- frontend/src/app/app.component.ts | 34 +++++------ frontend/src/app/app.module.ts | 2 + frontend/src/app/app.routing.ts | 21 ++++++- .../app/core/helpers/file-progress.helper.ts | 2 +- .../app/core/services/firestore.service.ts | 9 ++- .../src/app/core/services/rust.service.ts | 1 + .../src/app/core/services/web-node.service.ts | 6 ++ .../block-production-won-slots.effects.ts | 2 +- ...-production-won-slots-cards.component.html | 11 ++-- ...ck-production-won-slots-cards.component.ts | 19 +++++- ...oduction-won-slots-side-panel.component.ts | 11 ---- .../dashboard-blocks-sync.component.ts | 1 - .../leaderboard-apply.component.html | 4 +- .../leaderboard-apply.component.scss | 13 ++++ .../leaderboard-details.component.scss | 2 +- .../leaderboard-footer.component.html | 4 +- .../leaderboard-header.component.html | 2 +- .../leaderboard-impressum.component.scss | 2 +- .../leaderboard-landing-page.component.html | 25 +++++--- .../leaderboard-landing-page.component.scss | 18 +++++- .../leaderboard-landing-page.component.ts | 34 ++++++++++- .../leaderboard-page.component.html | 29 ++++++++- .../leaderboard-page.component.scss | 47 +++++++++++++- .../leaderboard-page.component.ts | 61 ++++++++++++++++++- .../leaderboard-privacy-policy.component.scss | 2 +- .../leaderboard-table.component.html | 2 +- .../leaderboard-table.component.scss | 18 ++++-- .../leaderboard-table.component.ts | 4 +- ...rboard-terms-and-conditions.component.scss | 2 +- .../leaderboard-title.component.html | 1 + .../leaderboard-title.component.scss | 12 +++- .../leaderboard/leaderboard.service.ts | 51 +++++++++++++--- .../web-node-file-upload.component.html | 23 ++++--- .../web-node-file-upload.component.ts | 2 +- .../server-status.component.html | 34 ++++++----- .../server-status/server-status.component.ts | 27 ++------ .../app/layout/toolbar/toolbar.component.html | 7 ++- .../app/layout/toolbar/toolbar.component.scss | 10 +++ .../app/layout/toolbar/toolbar.component.ts | 10 ++- .../uptime-pill/uptime-pill.component.html | 5 ++ .../uptime-pill/uptime-pill.component.scss | 44 +++++++++++++ .../uptime-pill/uptime-pill.component.ts | 38 ++++++++++++ .../app/shared/guards/landing-page.guard.ts | 46 ++++++++++++++ .../src/app/shared/helpers/date.helper.ts | 11 ++++ .../types/core/environment/mina-env.type.ts | 4 ++ .../leaderboard/heartbeat-summary.type.ts | 2 + .../src/assets/environments/leaderboard.js | 5 +- frontend/src/assets/environments/webnode.js | 3 - frontend/src/environments/environment.prod.ts | 3 + frontend/src/environments/environment.ts | 28 +++++---- frontend/src/index.html | 8 +-- 54 files changed, 616 insertions(+), 165 deletions(-) create mode 100644 frontend/src/app/layout/uptime-pill/uptime-pill.component.html create mode 100644 frontend/src/app/layout/uptime-pill/uptime-pill.component.scss create mode 100644 frontend/src/app/layout/uptime-pill/uptime-pill.component.ts create mode 100644 frontend/src/app/shared/guards/landing-page.guard.ts diff --git a/frontend/package.json b/frontend/package.json index 9003e5757a..5b184eab7b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "1.0.103", + "version": "1.0.121", "scripts": { "install:deps": "npm install", "start": "npm install && ng serve --configuration local --open", diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index 3b43ad7624..d292add176 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -28,7 +28,8 @@ class="overflow-hidden" [class.no-toolbar]="hideToolbar" [class.no-submenus]="subMenusLength < 2" - [class.mobile]="menu.isMobile"> + [class.mobile]="menu.isMobile" + [class.uptime]="showUptime"> @if (!isDesktop) { diff --git a/frontend/src/app/app.component.scss b/frontend/src/app/app.component.scss index c661e58c8c..2044b515e5 100644 --- a/frontend/src/app/app.component.scss +++ b/frontend/src/app/app.component.scss @@ -76,8 +76,10 @@ mat-sidenav-content { margin-bottom: 4px; border-top-right-radius: 6px; - &.no-toolbar { - height: calc(100% - #{$subMenus} - #{$tabs}); + + &.uptime { + $toolbar: 130px; + height: calc(100% - #{$toolbar} - #{$subMenus} - #{$tabs}); } &.no-submenus { @@ -86,6 +88,14 @@ mat-sidenav-content { &.no-toolbar { height: 100%; } + + &.uptime { + height: calc(100% - #{$toolbar} - #{$subMenus}); + } + } + + &.no-toolbar { + height: calc(100% - #{$subMenus} - #{$tabs}); } } } diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 0e5f1f9deb..9a3447549a 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -28,6 +28,7 @@ export class AppComponent extends StoreDispatcher implements OnInit { readonly showLeaderboardPage$: Observable = this.select$(getMergedRoute).pipe(filter(Boolean), map((route: MergedRoute) => route.url.startsWith(`/${Routes.LEADERBOARD}`))); subMenusLength: number = 0; hideToolbar: boolean = CONFIG.hideToolbar; + showUptime: boolean = CONFIG.showLeaderboard; loaded: boolean; isDesktop: boolean = isDesktop(); @@ -54,31 +55,30 @@ export class AppComponent extends StoreDispatcher implements OnInit { localStorage.setItem('webnodeArgs', args); } } + this.select(getMergedRoute, () => { + this.loaded = true; + this.detect(); + }, filter(Boolean), take(1)); - this.select( - getMergedRoute, - () => this.initAppFunctionalities(), - filter(Boolean), - take(1), - filter((route: MergedRoute) => route.url !== '/' && !route.url.startsWith('/?') && !route.url.startsWith('/leaderboard')), - ); - this.select( - getMergedRoute, - () => { - this.loaded = true; - this.detect(); - }, - filter(Boolean), - take(1), - ); + if (CONFIG.showLeaderboard && CONFIG.showWebNodeLandingPage) { + /* frontend with some landing page */ + this.select(getMergedRoute, () => { + this.initAppFunctionalities(); + }, filter((route: MergedRoute) => route?.url.startsWith('/loading-web-node')), take(1)); + + } else if (!CONFIG.showLeaderboard && !CONFIG.showWebNodeLandingPage) { + /* normal frontend (no landing pages) */ + this.initAppFunctionalities(); + } } goToWebNode(): void { - this.router.navigate([Routes.LOADING_WEB_NODE], { queryParamsHandling: 'merge' }); + // this.router.navigate([Routes.LOADING_WEB_NODE], { queryParamsHandling: 'merge' }); this.initAppFunctionalities(); } private initAppFunctionalities(): void { + console.log('initAppFunctionalities'); if (this.webNodeService.hasWebNodeConfig() && !this.webNodeService.isWebNodeLoaded()) { if (!getWindow()?.location.href.includes(`/${Routes.LOADING_WEB_NODE}`)) { this.router.navigate([Routes.LOADING_WEB_NODE], { queryParamsHandling: 'preserve' }); diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 734d520c3d..dba557a132 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -43,6 +43,7 @@ import { BlockProductionPillComponent } from '@app/layout/block-production-pill/ import { MenuTabsComponent } from '@app/layout/menu-tabs/menu-tabs.component'; import { getFirestore, provideFirestore } from '@angular/fire/firestore'; import { LeaderboardModule } from '@leaderboard/leaderboard.module'; +import { UptimePillComponent } from '@app/layout/uptime-pill/uptime-pill.component'; registerLocaleData(localeFr, 'fr'); registerLocaleData(localeEn, 'en'); @@ -166,6 +167,7 @@ export class AppGlobalErrorhandler implements ErrorHandler { BlockProductionPillComponent, MenuTabsComponent, LeaderboardModule, + UptimePillComponent, ], providers: [ THEME_PROVIDER, diff --git a/frontend/src/app/app.routing.ts b/frontend/src/app/app.routing.ts index 1a5d8eca6d..f057279939 100644 --- a/frontend/src/app/app.routing.ts +++ b/frontend/src/app/app.routing.ts @@ -2,6 +2,9 @@ import { NgModule } from '@angular/core'; import { NoPreloading, RouterModule, Routes } from '@angular/router'; import { CONFIG, getFirstFeature } from '@shared/constants/config'; import { WebNodeLandingPageComponent } from '@app/layout/web-node-landing-page/web-node-landing-page.component'; +import { getMergedRoute, MergedRoute } from '@openmina/shared'; +import { filter, take } from 'rxjs'; +import { landingPageGuard } from '@shared/guards/landing-page.guard'; const APP_TITLE: string = 'Open Mina'; @@ -24,6 +27,7 @@ function generateRoutes(): Routes { path: 'dashboard', loadChildren: () => import('@dashboard/dashboard.module').then(m => m.DashboardModule), title: DASHBOARD_TITLE, + canActivate: [landingPageGuard], }, { path: 'nodes', @@ -45,6 +49,7 @@ function generateRoutes(): Routes { path: 'state', loadChildren: () => import('@state/state.module').then(m => m.StateModule), title: STATE_TITLE, + canActivate: [landingPageGuard], }, { path: 'snarks', @@ -55,16 +60,19 @@ function generateRoutes(): Routes { path: 'block-production', loadChildren: () => import('@block-production/block-production.module').then(m => m.BlockProductionModule), title: BLOCK_PRODUCTION_TITLE, + canActivate: [landingPageGuard], }, { path: 'mempool', loadChildren: () => import('@mempool/mempool.module').then(m => m.MempoolModule), title: MEMPOOL_TITLE, + canActivate: [landingPageGuard], }, { path: 'benchmarks', loadChildren: () => import('@benchmarks/benchmarks.module').then(m => m.BenchmarksModule), title: BENCHMARKS_TITLE, + canActivate: [landingPageGuard], }, { path: 'fuzzing', @@ -75,12 +83,19 @@ function generateRoutes(): Routes { path: 'loading-web-node', loadChildren: () => import('@web-node/web-node.module').then(m => m.WebNodeModule), title: WEBNODE_TITLE, + canActivate: [landingPageGuard], }, - { + // { + // path: '', + // loadChildren: () => import('@leaderboard/leaderboard.module').then(m => m.LeaderboardModule), + // }, + ]; + if (CONFIG.showLeaderboard) { + routes.push({ path: '', loadChildren: () => import('@leaderboard/leaderboard.module').then(m => m.LeaderboardModule), - }, - ]; + }); + } if (CONFIG.showWebNodeLandingPage) { routes.push({ path: '', diff --git a/frontend/src/app/core/helpers/file-progress.helper.ts b/frontend/src/app/core/helpers/file-progress.helper.ts index 890310def7..fed9a8d31f 100644 --- a/frontend/src/app/core/helpers/file-progress.helper.ts +++ b/frontend/src/app/core/helpers/file-progress.helper.ts @@ -1,7 +1,7 @@ import { BehaviorSubject } from 'rxjs'; import { safelyExecuteInBrowser } from '@openmina/shared'; -const WASM_FILE_SIZE = 31705944; +const WASM_FILE_SIZE = 31556926; class AssetMonitor { readonly downloads: Map = new Map(); diff --git a/frontend/src/app/core/services/firestore.service.ts b/frontend/src/app/core/services/firestore.service.ts index 1073919e4e..3871c5a6d9 100644 --- a/frontend/src/app/core/services/firestore.service.ts +++ b/frontend/src/app/core/services/firestore.service.ts @@ -11,7 +11,7 @@ import { DocumentData, } from '@angular/fire/firestore'; import { HttpClient } from '@angular/common/http'; -import { Observable } from 'rxjs'; +import { catchError, EMPTY, Observable } from 'rxjs'; @Injectable({ providedIn: 'root', @@ -27,7 +27,12 @@ export class FirestoreService { addHeartbeat(data: any): Observable { console.log('Posting to cloud function:', data); - return this.http.post(this.cloudFunctionUrl, { data }); + return this.http.post(this.cloudFunctionUrl, { data }).pipe( + catchError(error => { + console.error('Error while posting to cloud function:', error); + return error; + }), + ); } updateHeartbeat(id: string, data: any): Promise { diff --git a/frontend/src/app/core/services/rust.service.ts b/frontend/src/app/core/services/rust.service.ts index 4a3fe69be5..0e6d26c04f 100644 --- a/frontend/src/app/core/services/rust.service.ts +++ b/frontend/src/app/core/services/rust.service.ts @@ -15,6 +15,7 @@ export class RustService { private webNodeService: WebNodeService) {} changeRustNode(node: MinaNode): void { + console.log('Changing Rust node to:', node); this.node = node; } diff --git a/frontend/src/app/core/services/web-node.service.ts b/frontend/src/app/core/services/web-node.service.ts index 549afe48cb..0a1c20e12c 100644 --- a/frontend/src/app/core/services/web-node.service.ts +++ b/frontend/src/app/core/services/web-node.service.ts @@ -60,6 +60,7 @@ export class WebNodeService { } loadWasm$(): Observable { + console.log('Loading wasm'); this.webNodeStartTime = Date.now(); if (isBrowser()) { @@ -142,9 +143,14 @@ export class WebNodeService { return throwError(() => new Error(error.message)); }), switchMap(() => this.webnode$.asObservable()), + // filter(() => CONFIG.globalConfig.heartbeats), switchMap(() => timer(0, 60000)), switchMap(() => this.heartBeat$), switchMap(heartBeat => this.firestore.addHeartbeat(heartBeat)), + catchError(error => { + console.log('Error from heartbeat api:', error); + return of(null); + }), ); } return EMPTY; diff --git a/frontend/src/app/features/block-production/won-slots/block-production-won-slots.effects.ts b/frontend/src/app/features/block-production/won-slots/block-production-won-slots.effects.ts index 68f2412866..71b703704f 100644 --- a/frontend/src/app/features/block-production/won-slots/block-production-won-slots.effects.ts +++ b/frontend/src/app/features/block-production/won-slots/block-production-won-slots.effects.ts @@ -39,7 +39,7 @@ export class BlockProductionWonSlotsEffects extends BaseEffect { ofType(BlockProductionWonSlotsActions.getSlots, BlockProductionWonSlotsActions.close), this.latestActionState(), switchMap(({ action, state }) => - action.type === BlockProductionWonSlotsActions.close.type + action.type.includes('Close') ? EMPTY : this.wonSlotsService.getSlots().pipe( switchMap(({ slots, epoch }) => { diff --git a/frontend/src/app/features/block-production/won-slots/cards/block-production-won-slots-cards.component.html b/frontend/src/app/features/block-production/won-slots/cards/block-production-won-slots-cards.component.html index bb13a67b4d..d8e245136a 100644 --- a/frontend/src/app/features/block-production/won-slots/cards/block-production-won-slots-cards.component.html +++ b/frontend/src/app/features/block-production/won-slots/cards/block-production-won-slots-cards.component.html @@ -26,10 +26,13 @@ class="mr-10">
Public Key
-
{{ card5.publicKey | truncateMid: 6: 6 }}
-
- content_copy - Copy +
+ {{ card5.publicKey | truncateMid: 4: 4 }} + content_copy +
+
+ open_in_new + Open in Explorer
diff --git a/frontend/src/app/features/block-production/won-slots/cards/block-production-won-slots-cards.component.ts b/frontend/src/app/features/block-production/won-slots/cards/block-production-won-slots-cards.component.ts index 24b339574b..91ae264521 100644 --- a/frontend/src/app/features/block-production/won-slots/cards/block-production-won-slots-cards.component.ts +++ b/frontend/src/app/features/block-production/won-slots/cards/block-production-won-slots-cards.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { StoreDispatcher } from '@shared/base-classes/store-dispatcher.class'; import { BlockProductionWonSlotsSelectors } from '@block-production/won-slots/block-production-won-slots.state'; -import { lastItem, ONE_BILLION, ONE_THOUSAND } from '@openmina/shared'; +import { lastItem, ONE_BILLION, ONE_THOUSAND, safelyExecuteInBrowser } from '@openmina/shared'; import { getTimeDiff } from '@shared/helpers/date.helper'; import { filter } from 'rxjs'; import { @@ -12,6 +12,8 @@ import { BlockProductionWonSlotsEpoch, } from '@shared/types/block-production/won-slots/block-production-won-slots-epoch.type'; import { BlockProductionWonSlotsActions } from '@block-production/won-slots/block-production-won-slots.actions'; +import { AppSelectors } from '@app/app.state'; +import { AppNodeDetails } from '@shared/types/app/app-node-details.type'; @Component({ selector: 'mina-block-production-won-slots-cards', @@ -28,9 +30,18 @@ export class BlockProductionWonSlotsCardsComponent extends StoreDispatcher imple card4: { epochProgress: string; endIn: string; } = { epochProgress: '-', endIn: null }; card5: { publicKey: string; totalRewards: string } = { publicKey: null, totalRewards: null }; + private minaExplorer: string; + ngOnInit(): void { this.listenToSlots(); this.listenToEpoch(); + this.listenToActiveNode(); + } + + private listenToActiveNode(): void { + this.select(AppSelectors.activeNodeDetails, (node: AppNodeDetails) => { + this.minaExplorer = node.network?.toLowerCase(); + }, filter(Boolean)); } private listenToEpoch(): void { @@ -80,4 +91,10 @@ export class BlockProductionWonSlotsCardsComponent extends StoreDispatcher imple toggleSidePanel(): void { this.dispatch2(BlockProductionWonSlotsActions.toggleSidePanel()); } + + openInExplorer(): void { + const network = this.minaExplorer !== 'mainnet' ? (this.minaExplorer + '.') : ''; + const url = `https://${network}minaexplorer.com/wallet/${this.card5.publicKey}`; + safelyExecuteInBrowser(() => window.open(url, '_blank')); + } } diff --git a/frontend/src/app/features/block-production/won-slots/side-panel/block-production-won-slots-side-panel.component.ts b/frontend/src/app/features/block-production/won-slots/side-panel/block-production-won-slots-side-panel.component.ts index 486440ef00..de0a10af33 100644 --- a/frontend/src/app/features/block-production/won-slots/side-panel/block-production-won-slots-side-panel.component.ts +++ b/frontend/src/app/features/block-production/won-slots/side-panel/block-production-won-slots-side-panel.component.ts @@ -136,7 +136,6 @@ export class BlockProductionWonSlotsSidePanelComponent extends StoreDispatcher i this.stopTimer = true; this.stateWhenReachedZero = { globalSlot: this.slot.globalSlot, status: this.slot.status }; this.remainingTime = '-'; - this.queryServerOftenToGetTheNewSlotState(); } this.detect(); } else { @@ -176,16 +175,6 @@ export class BlockProductionWonSlotsSidePanelComponent extends StoreDispatcher i } } - private queryServerOftenToGetTheNewSlotState(): void { - const timer = setInterval(() => { - if (!this.stateWhenReachedZero) { - clearInterval(timer); - return; - } - this.dispatch2(BlockProductionWonSlotsActions.getSlots()); - }, 1000); - } - closeSidePanel(): void { this.router.navigate([Routes.BLOCK_PRODUCTION, Routes.WON_SLOTS]); this.dispatch2(BlockProductionWonSlotsActions.toggleSidePanel()); diff --git a/frontend/src/app/features/dashboard/dashboard-blocks-sync/dashboard-blocks-sync.component.ts b/frontend/src/app/features/dashboard/dashboard-blocks-sync/dashboard-blocks-sync.component.ts index b044d6c810..5e19b77f0e 100644 --- a/frontend/src/app/features/dashboard/dashboard-blocks-sync/dashboard-blocks-sync.component.ts +++ b/frontend/src/app/features/dashboard/dashboard-blocks-sync/dashboard-blocks-sync.component.ts @@ -128,7 +128,6 @@ export class DashboardBlocksSyncComponent extends StoreDispatcher implements OnI blocks = blocks.slice(1); this.lengthWithoutRoot = blocks.length ?? null; // 290 or 291 - console.log(this.lengthWithoutRoot); this.fetched = blocks.filter(b => ![NodesOverviewNodeBlockStatus.MISSING, NodesOverviewNodeBlockStatus.FETCHING].includes(b.status)).length; this.applied = blocks.filter(b => b.status === NodesOverviewNodeBlockStatus.APPLIED).length; diff --git a/frontend/src/app/features/leaderboard/leaderboard-apply/leaderboard-apply.component.html b/frontend/src/app/features/leaderboard/leaderboard-apply/leaderboard-apply.component.html index e5e3da605c..fb849cc486 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-apply/leaderboard-apply.component.html +++ b/frontend/src/app/features/leaderboard/leaderboard-apply/leaderboard-apply.component.html @@ -1,3 +1,3 @@ - - Round 1 Applications Close in 5d 5h 12m - Apply arrow_right_alt + + Round 1 Applications Close Soon arrow_right_alt diff --git a/frontend/src/app/features/leaderboard/leaderboard-apply/leaderboard-apply.component.scss b/frontend/src/app/features/leaderboard/leaderboard-apply/leaderboard-apply.component.scss index 447df2fdcb..97e0746e11 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-apply/leaderboard-apply.component.scss +++ b/frontend/src/app/features/leaderboard/leaderboard-apply/leaderboard-apply.component.scss @@ -1,5 +1,14 @@ @import 'leaderboard-variables'; +@keyframes bounceLeftToRight { + 0%, 100% { + transform: translateX(0); + } + 50% { + transform: translateX(10px); + } +} + .gradient { height: 52px; background: $mina-brand-gradient; @@ -13,4 +22,8 @@ @media (max-width: 767px) { font-size: 3.1vw; } + + &:hover .mina-icon { + animation: bounceLeftToRight .85s infinite ease-out; + } } diff --git a/frontend/src/app/features/leaderboard/leaderboard-details/leaderboard-details.component.scss b/frontend/src/app/features/leaderboard/leaderboard-details/leaderboard-details.component.scss index b5c5a9483d..ca5b5b1ddb 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-details/leaderboard-details.component.scss +++ b/frontend/src/app/features/leaderboard/leaderboard-details/leaderboard-details.component.scss @@ -36,6 +36,6 @@ h1 { margin-bottom: 80px; color: $mina-base-primary; font-size: 80px; - font-weight: 400; + font-weight: 300; line-height: 80px; } diff --git a/frontend/src/app/features/leaderboard/leaderboard-footer/leaderboard-footer.component.html b/frontend/src/app/features/leaderboard/leaderboard-footer/leaderboard-footer.component.html index cffccd6d15..b0ce6d1876 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-footer/leaderboard-footer.component.html +++ b/frontend/src/app/features/leaderboard/leaderboard-footer/leaderboard-footer.component.html @@ -2,7 +2,7 @@
© 2025 Mina Foundation. All rights reserved.
diff --git a/frontend/src/app/features/leaderboard/leaderboard-header/leaderboard-header.component.html b/frontend/src/app/features/leaderboard/leaderboard-header/leaderboard-header.component.html index 2e93e9e1d0..0f448f9ca0 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-header/leaderboard-header.component.html +++ b/frontend/src/app/features/leaderboard/leaderboard-header/leaderboard-header.component.html @@ -8,6 +8,6 @@ (clickOutside)="closeMenu()"> Mina Web Node Leaderboard - Round 1 Details + Round 1 Details diff --git a/frontend/src/app/features/leaderboard/leaderboard-impressum/leaderboard-impressum.component.scss b/frontend/src/app/features/leaderboard/leaderboard-impressum/leaderboard-impressum.component.scss index b5c5a9483d..ca5b5b1ddb 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-impressum/leaderboard-impressum.component.scss +++ b/frontend/src/app/features/leaderboard/leaderboard-impressum/leaderboard-impressum.component.scss @@ -36,6 +36,6 @@ h1 { margin-bottom: 80px; color: $mina-base-primary; font-size: 80px; - font-weight: 400; + font-weight: 300; line-height: 80px; } diff --git a/frontend/src/app/features/leaderboard/leaderboard-landing-page/leaderboard-landing-page.component.html b/frontend/src/app/features/leaderboard/leaderboard-landing-page/leaderboard-landing-page.component.html index c5a364c04b..043b0cf0ea 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-landing-page/leaderboard-landing-page.component.html +++ b/frontend/src/app/features/leaderboard/leaderboard-landing-page/leaderboard-landing-page.component.html @@ -1,5 +1,9 @@ - -
+
+ +
+ +
+
@@ -10,10 +14,10 @@
-

We(b) Node
Do You?

+

We Node
Do You?

- Apply to be a node runner + Apply Now

Round 1 is limited to 100 seats

@@ -44,7 +48,7 @@

Node-To-Earn

[style.background-image]="'url(assets/images/landing-page/cta-section-bg.png)'">

Run a web node on Testnet and enter a 1000 MINA lottery

- Start Testing & Earn $500 USD + Apply Now

Apply by DATE. Not Selected? You're first in line next time.

@@ -56,13 +60,14 @@

Run a web node on Testnet and enter a 1000 MINA lottery

-

The Mina Web Node, part 1

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore . -

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore .

+

Join the Mina Web Node Testing Program

+

Discover all the details about Mina's Web Node Testing Program. Learn how to apply, participate, and earn rewards in Mina Web Node Testing Round + 1.

diff --git a/frontend/src/app/features/leaderboard/leaderboard-landing-page/leaderboard-landing-page.component.scss b/frontend/src/app/features/leaderboard/leaderboard-landing-page/leaderboard-landing-page.component.scss index 0ee7917bcf..63939df9ae 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-landing-page/leaderboard-landing-page.component.scss +++ b/frontend/src/app/features/leaderboard/leaderboard-landing-page/leaderboard-landing-page.component.scss @@ -4,12 +4,26 @@ :host { - padding-top: 52px; background-color: $mina-cta-primary; color: $mina-base-primary; font-family: "IBM Plex Sans", sans-serif; } +.floating-banner { + position: fixed; + bottom: -100%; + left: 20px; + right: 20px; + transition: bottom 0.5s ease; + z-index: 1000; + border-radius: 12px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + + &.show { + bottom: 20px; + } +} + main, mina-leaderboard-header, mina-leaderboard-footer { @@ -34,6 +48,7 @@ mina-leaderboard-footer { line-height: 20px; font-weight: 300; transition: .15s ease; + min-width: 240px; &:hover { background-color: $black; @@ -49,6 +64,7 @@ mina-leaderboard-footer { } .overflow-y-scroll { + padding-bottom: 72px; background-color: $mina-cta-primary; &::-webkit-scrollbar-track { diff --git a/frontend/src/app/features/leaderboard/leaderboard-landing-page/leaderboard-landing-page.component.ts b/frontend/src/app/features/leaderboard/leaderboard-landing-page/leaderboard-landing-page.component.ts index afce1668e7..233518cf56 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-landing-page/leaderboard-landing-page.component.ts +++ b/frontend/src/app/features/leaderboard/leaderboard-landing-page/leaderboard-landing-page.component.ts @@ -1,4 +1,7 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, DestroyRef, ElementRef, ViewChild } from '@angular/core'; +import { ManualDetection } from '@openmina/shared'; +import { debounceTime, fromEvent } from 'rxjs'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ selector: 'mina-leaderboard-landing-page', @@ -7,9 +10,34 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'flex-column h-100 align-center' }, }) -export class LeaderboardLandingPageComponent implements OnInit { +export class LeaderboardLandingPageComponent extends ManualDetection implements AfterViewInit { + showBanner: boolean = false; - ngOnInit(): void { + private readonly SCROLL_THRESHOLD = 100; + @ViewChild('scrollContainer') private scrollContainer!: ElementRef; + + constructor(private destroyRef: DestroyRef) { + super(); } + ngAfterViewInit(): void { + const container = this.scrollContainer.nativeElement; + + fromEvent(container, 'scroll') + .pipe( + debounceTime(100), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(() => { + const scrollPosition = container.scrollTop; + + if (scrollPosition > this.SCROLL_THRESHOLD && !this.showBanner) { + this.showBanner = true; + this.detect(); + } else if (scrollPosition <= this.SCROLL_THRESHOLD && this.showBanner) { + this.showBanner = false; + this.detect(); + } + }); + } } diff --git a/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.html b/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.html index dcdc8794c2..cd5acc0ee7 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.html +++ b/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.html @@ -1,11 +1,36 @@ - +
+ +
-
+
+
+
+ + +
+
+
+ info + Live results are not final because blockchain finality takes time +
+ chevron_right +
+
+
    +
  • New blocks can reorganize the chain, changing past data
  • +
  • Network nodes need time to reach consensus
  • +
  • Block confirmations become more certain over time
  • +
  • Final results will be published after the program ends and complete chain verification
  • +
+

To learn more about how uptime is tracked, please refer to the How Uptime Tracking Works section in the program details.

+
+
+
diff --git a/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.scss b/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.scss index 557f63b4eb..5e7f19ef29 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.scss +++ b/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.scss @@ -1,10 +1,41 @@ @import 'leaderboard-variables'; :host { - padding-top: 52px; background-color: $mina-cta-primary; } +.floating-banner { + position: fixed; + bottom: -100%; + left: 20px; + right: 20px; + transition: bottom 0.5s ease; + z-index: 1000; + border-radius: 12px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + + &.show { + bottom: 20px; + } +} + +.overflow-y-scroll { + padding-bottom: 72px; + background-color: $mina-cta-primary; + + &::-webkit-scrollbar-track { + background-color: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: $white4; + } + + &::-webkit-scrollbar-thumb:hover { + background-color: $mina-base-secondary; + } +} + main, mina-leaderboard-header, mina-leaderboard-footer { @@ -41,3 +72,17 @@ main { background-color: $mina-base-secondary; } } + +.accordion { + color: $mina-base-primary; + font-size: 16px; + font-weight: 600; + background: rgba(248, 214, 17, 0.10); + cursor: pointer; + padding: 16px; +} + +.accordion-content { + font-weight: 400; + line-height: 24px; +} diff --git a/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.ts b/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.ts index 15e87823ce..db10f03c3f 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.ts +++ b/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.ts @@ -1,8 +1,11 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, DestroyRef, ElementRef, OnInit, ViewChild } from '@angular/core'; import { StoreDispatcher } from '@shared/base-classes/store-dispatcher.class'; import { LeaderboardActions } from '@leaderboard/leaderboard.actions'; -import { timer } from 'rxjs'; +import { debounceTime, fromEvent, timer } from 'rxjs'; import { untilDestroyed } from '@ngneat/until-destroy'; +import { trigger, state, style, animate, transition } from '@angular/animations'; +import { ManualDetection } from '@openmina/shared'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ selector: 'mina-leaderboard-page', @@ -10,8 +13,40 @@ import { untilDestroyed } from '@ngneat/until-destroy'; styleUrl: './leaderboard-page.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'flex-column h-100' }, + animations: [ + trigger('expandCollapse', [ + state('false', style({ + height: '0', + overflow: 'hidden', + opacity: '0', + })), + state('true', style({ + height: '*', + opacity: '1', + })), + transition('false <=> true', [ + animate('200ms ease-in-out'), + ]), + ]), + trigger('rotateIcon', [ + state('false', style({ transform: 'rotate(0)' })), + state('true', style({ transform: 'rotate(90deg)' })), + transition('false <=> true', [ + animate('200ms'), + ]), + ]), + ], }) -export class LeaderboardPageComponent extends StoreDispatcher implements OnInit { +export class LeaderboardPageComponent extends StoreDispatcher implements OnInit, AfterViewInit { + isExpanded = false; + showBanner: boolean = false; + + private readonly SCROLL_THRESHOLD = 100; + @ViewChild('scrollContainer') private scrollContainer!: ElementRef; + + constructor(private destroyRef: DestroyRef) { + super(); + } ngOnInit(): void { timer(0, 5000) @@ -21,4 +56,24 @@ export class LeaderboardPageComponent extends StoreDispatcher implements OnInit }); } + ngAfterViewInit(): void { + const container = this.scrollContainer.nativeElement; + + fromEvent(container, 'scroll') + .pipe( + debounceTime(100), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(() => { + const scrollPosition = container.scrollTop; + + if (scrollPosition > this.SCROLL_THRESHOLD && !this.showBanner) { + this.showBanner = true; + this.detect(); + } else if (scrollPosition <= this.SCROLL_THRESHOLD && this.showBanner) { + this.showBanner = false; + this.detect(); + } + }); + } } diff --git a/frontend/src/app/features/leaderboard/leaderboard-privacy-policy/leaderboard-privacy-policy.component.scss b/frontend/src/app/features/leaderboard/leaderboard-privacy-policy/leaderboard-privacy-policy.component.scss index b5c5a9483d..ca5b5b1ddb 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-privacy-policy/leaderboard-privacy-policy.component.scss +++ b/frontend/src/app/features/leaderboard/leaderboard-privacy-policy/leaderboard-privacy-policy.component.scss @@ -36,6 +36,6 @@ h1 { margin-bottom: 80px; color: $mina-base-primary; font-size: 80px; - font-weight: 400; + font-weight: 300; line-height: 80px; } diff --git a/frontend/src/app/features/leaderboard/leaderboard-table/leaderboard-table.component.html b/frontend/src/app/features/leaderboard/leaderboard-table/leaderboard-table.component.html index 6c9f2f3686..054a40ce1b 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-table/leaderboard-table.component.html +++ b/frontend/src/app/features/leaderboard/leaderboard-table/leaderboard-table.component.html @@ -14,7 +14,7 @@ circle {{ row.publicKey | truncateMid: (desktop ? 15 : 6): 6 }} - + {{ row.uptimePercentage }}% @if (row.uptimePercentage > 33.33) { bookmark_check diff --git a/frontend/src/app/features/leaderboard/leaderboard-table/leaderboard-table.component.scss b/frontend/src/app/features/leaderboard/leaderboard-table/leaderboard-table.component.scss index a4868d0b6c..0b6ca4bff0 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-table/leaderboard-table.component.scss +++ b/frontend/src/app/features/leaderboard/leaderboard-table/leaderboard-table.component.scss @@ -36,13 +36,20 @@ font-size: 16px; @media (max-width: 480px) { - grid-template-columns: 48% 24% 1fr; + grid-template-columns: 43% 28% 1fr; + } + + @media (max-width: 676px) { + grid-template-columns: 43% 28% 1fr; } span { color: $black; &:not(.mina-icon) { + @media (max-width: 676px) { + font-size: 2.5vw; + } @media (max-width: 480px) { font-size: 3vw; } @@ -50,13 +57,16 @@ } .circle { - color: $black4; + color: $mina-brand-gray; } .perc { - width: 37px; + width: 58px; + @media (max-width: 676px) { + width: 55px; + } @media (max-width: 480px) { - width: 26px; + width: 48px; } } diff --git a/frontend/src/app/features/leaderboard/leaderboard-table/leaderboard-table.component.ts b/frontend/src/app/features/leaderboard/leaderboard-table/leaderboard-table.component.ts index a36b6fcc7a..898136a931 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-table/leaderboard-table.component.ts +++ b/frontend/src/app/features/leaderboard/leaderboard-table/leaderboard-table.component.ts @@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { LeaderboardSelectors } from '@leaderboard/leaderboard.state'; import { HeartbeatSummary } from '@shared/types/leaderboard/heartbeat-summary.type'; import { StoreDispatcher } from '@shared/base-classes/store-dispatcher.class'; -import { isDesktop } from '@openmina/shared'; +import { isDesktop, TooltipPosition } from '@openmina/shared'; import { animate, style, transition, trigger } from '@angular/animations'; @Component({ @@ -47,4 +47,6 @@ export class LeaderboardTableComponent extends StoreDispatcher implements OnInit this.detect(); }); } + + protected readonly TooltipPosition = TooltipPosition; } diff --git a/frontend/src/app/features/leaderboard/leaderboard-terms-and-conditions/leaderboard-terms-and-conditions.component.scss b/frontend/src/app/features/leaderboard/leaderboard-terms-and-conditions/leaderboard-terms-and-conditions.component.scss index b5c5a9483d..ca5b5b1ddb 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-terms-and-conditions/leaderboard-terms-and-conditions.component.scss +++ b/frontend/src/app/features/leaderboard/leaderboard-terms-and-conditions/leaderboard-terms-and-conditions.component.scss @@ -36,6 +36,6 @@ h1 { margin-bottom: 80px; color: $mina-base-primary; font-size: 80px; - font-weight: 400; + font-weight: 300; line-height: 80px; } diff --git a/frontend/src/app/features/leaderboard/leaderboard-title/leaderboard-title.component.html b/frontend/src/app/features/leaderboard/leaderboard-title/leaderboard-title.component.html index 243f8138fe..7ce680fc1d 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-title/leaderboard-title.component.html +++ b/frontend/src/app/features/leaderboard/leaderboard-title/leaderboard-title.component.html @@ -1 +1,2 @@ +

Mina Web Node Testing Program

Leaderboard

diff --git a/frontend/src/app/features/leaderboard/leaderboard-title/leaderboard-title.component.scss b/frontend/src/app/features/leaderboard/leaderboard-title/leaderboard-title.component.scss index e4872bf88f..bcca6c68f7 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-title/leaderboard-title.component.scss +++ b/frontend/src/app/features/leaderboard/leaderboard-title/leaderboard-title.component.scss @@ -11,9 +11,19 @@ h1 { font-weight: 300; font-size: 80px; color: $black6; - margin: 80px 0; + margin-bottom: 80px; + margin-top: 0; @media (max-width: 1023px) { font-size: 10vw; } } + +h2 { + margin-top: 72px; + margin-bottom: 16px; + color: $mina-base-secondary; + font-size: 20px; + font-weight: 500; + line-height: 28px; +} diff --git a/frontend/src/app/features/leaderboard/leaderboard.service.ts b/frontend/src/app/features/leaderboard/leaderboard.service.ts index 6c19937356..e9baaadf2f 100644 --- a/frontend/src/app/features/leaderboard/leaderboard.service.ts +++ b/frontend/src/app/features/leaderboard/leaderboard.service.ts @@ -2,6 +2,8 @@ import { Injectable } from '@angular/core'; import { combineLatest, map, Observable } from 'rxjs'; import { HeartbeatSummary } from '@shared/types/leaderboard/heartbeat-summary.type'; import { collection, collectionData, CollectionReference, Firestore } from '@angular/fire/firestore'; +import { WebNodeService } from '@core/services/web-node.service'; +import { getElapsedTimeInMinsAndHours } from '@shared/helpers/date.helper'; @Injectable({ providedIn: 'root', @@ -11,7 +13,8 @@ export class LeaderboardService { private scoresCollection: CollectionReference; private maxScoreCollection: CollectionReference; - constructor(private firestore: Firestore) { + constructor(private firestore: Firestore, + private webnodeService: WebNodeService) { this.scoresCollection = collection(this.firestore, 'scores'); this.maxScoreCollection = collection(this.firestore, 'maxScore'); } @@ -24,14 +27,18 @@ export class LeaderboardService { map(([scores, maxScore]) => { const maxScoreRightNow = maxScore.find(c => c.id === 'current')['value']; - const items = scores.map(score => ({ - publicKey: score['publicKey'], - blocksProduced: score['blocksProduced'], - isActive: score['lastUpdated'] > Date.now() - 120000, - uptimePercentage: Math.floor((score['score'] / maxScoreRightNow) * 100), - uptimePrize: false, - blocksPrize: false, - } as HeartbeatSummary)); + const items = scores.map(score => { + return ({ + publicKey: score['publicKey'], + blocksProduced: score['blocksProduced'], + isActive: score['lastUpdated'] > Date.now() - 120000, + uptimePercentage: this.getUptimePercentage(score['score'], maxScoreRightNow), + uptimePrize: false, + blocksPrize: false, + score: score['score'], + maxScore: maxScoreRightNow, + } as HeartbeatSummary); + }); const sortedItemsByUptime = [...items].sort((a, b) => b.uptimePercentage - a.uptimePercentage); const fifthPlacePercentageByUptime = sortedItemsByUptime[4]?.uptimePercentage ?? 0; @@ -44,4 +51,30 @@ export class LeaderboardService { }), ); } + + getUptime(): Observable { + const publicKey = this.webnodeService.privateStake.publicKey.replace('\n', ''); + + return combineLatest([ + collectionData(this.scoresCollection, { idField: 'id' }), + collectionData(this.maxScoreCollection, { idField: 'id' }), + ]).pipe( + map(([scores, maxScore]) => { + const activeEntry = scores.find(score => score['publicKey'] === publicKey); + + return { + uptimePercentage: this.getUptimePercentage(activeEntry['score'], maxScore[0]['value']), + uptimeTime: getElapsedTimeInMinsAndHours(activeEntry['score'] * 5), + }; + }), + ); + } + + private getUptimePercentage(score: number, maxScore: number): number { + let uptimePercentage = Number(((score / maxScore) * 100).toFixed(2)); + if (maxScore === 0) { + uptimePercentage = 0; + } + return uptimePercentage; + } } diff --git a/frontend/src/app/features/web-node/web-node-file-upload/web-node-file-upload.component.html b/frontend/src/app/features/web-node/web-node-file-upload/web-node-file-upload.component.html index 0d5208f617..a072a18d09 100644 --- a/frontend/src/app/features/web-node/web-node-file-upload/web-node-file-upload.component.html +++ b/frontend/src/app/features/web-node/web-node-file-upload/web-node-file-upload.component.html @@ -9,7 +9,7 @@

Set Up Your Web Node

@if (!validFiles) { @if (!error) { -
+
@@ -22,7 +22,7 @@

Set Up Your Web Node

Select configuration file (.zip)
- @@ -31,7 +31,7 @@

Set Up Your Web Node

(change)="onFileSelected($event)" accept=".zip">
-
Upload webnode-account-XY.zip we sent you
+
Upload webnode-account-XY.zip we sent you
} @else {
@@ -68,18 +68,17 @@

Set Up Your Web Node

}
- - + + + + + + + +
diff --git a/frontend/src/app/features/web-node/web-node-file-upload/web-node-file-upload.component.ts b/frontend/src/app/features/web-node/web-node-file-upload/web-node-file-upload.component.ts index b8425a6323..a52f0e9f93 100644 --- a/frontend/src/app/features/web-node/web-node-file-upload/web-node-file-upload.component.ts +++ b/frontend/src/app/features/web-node/web-node-file-upload/web-node-file-upload.component.ts @@ -54,7 +54,7 @@ export class WebNodeFileUploadComponent extends ManualDetection { const publicKey = files.find(f => f.name.includes('.pub'))?.content; const password = files.find(f => f.name.includes('password'))?.content.replace(/\r?\n|\r/g, ''); const stake = files.find(f => f.name.includes('stake') && !f.name.includes('.pub'))?.content; - if (this.error || !publicKey || !password || !stake) { + if (this.error || !publicKey || !stake) { this.error = true; } else { this.webnodeService.privateStake = { publicKey, password, stake: JSON.parse(stake) }; diff --git a/frontend/src/app/layout/server-status/server-status.component.html b/frontend/src/app/layout/server-status/server-status.component.html index 0198d5a15a..9cc8bd21a3 100644 --- a/frontend/src/app/layout/server-status/server-status.component.html +++ b/frontend/src/app/layout/server-status/server-status.component.html @@ -2,21 +2,25 @@
@if (!switchForbidden && !hideNodeStats && !isMobile) { -
- blur_circular -
{{ details.transactions }} Tx{{ details.transactions | plural }}
-
{{ details.snarks }} SNARK{{ details.snarks | plural }}
-
-
- language -
{{ details.peersConnected }} Peer{{ details.peersConnected | plural }}
-
+ @if (!hideTx) { +
+ blur_circular +
{{ details.transactions }} Tx{{ details.transactions | plural }}
+
{{ details.snarks }} SNARK{{ details.snarks | plural }}
+
+ } + @if (!hidePeers) { +
+ language +
{{ details.peersConnected }} Peer{{ details.peersConnected | plural }}
+
+ } }
; diff --git a/frontend/src/app/layout/toolbar/toolbar.component.html b/frontend/src/app/layout/toolbar/toolbar.component.html index 0bdd8f4ad4..cbe14e39be 100644 --- a/frontend/src/app/layout/toolbar/toolbar.component.html +++ b/frontend/src/app/layout/toolbar/toolbar.component.html @@ -10,10 +10,13 @@ }
-
- @if (!isMobile || (isMobile && errors.length)) { +
+ @if (!isMobile) { } + @if (showUptime) { + + }
@if (haveNextBP && !isAllNodesPage) { diff --git a/frontend/src/app/layout/toolbar/toolbar.component.scss b/frontend/src/app/layout/toolbar/toolbar.component.scss index 71c8f6be53..fab767af7d 100644 --- a/frontend/src/app/layout/toolbar/toolbar.component.scss +++ b/frontend/src/app/layout/toolbar/toolbar.component.scss @@ -4,6 +4,9 @@ height: 40px; @media (max-width: 767px) { height: 96px; + &.uptime { + height: 130px; + } } } @@ -50,6 +53,13 @@ } } } + + .pills-holder { + &.is-mobile { + width: 100%; + flex-direction: column !important; + } + } } @keyframes loading { diff --git a/frontend/src/app/layout/toolbar/toolbar.component.ts b/frontend/src/app/layout/toolbar/toolbar.component.ts index 4c66ec1860..8d07dfdad3 100644 --- a/frontend/src/app/layout/toolbar/toolbar.component.ts +++ b/frontend/src/app/layout/toolbar/toolbar.component.ts @@ -1,5 +1,5 @@ -import { ChangeDetectionStrategy, Component, ElementRef, OnInit, ViewChild } from '@angular/core'; -import { filter, map } from 'rxjs'; +import { ChangeDetectionStrategy, Component, ElementRef, HostBinding, OnInit, ViewChild } from '@angular/core'; +import { catchError, filter, map, of, switchMap, timer } from 'rxjs'; import { AppSelectors } from '@app/app.state'; import { getMergedRoute, hasValue, MergedRoute, removeParamsFromURL, TooltipService } from '@openmina/shared'; import { AppMenu } from '@shared/types/app/app-menu.type'; @@ -10,6 +10,9 @@ import { selectErrorPreviewErrors } from '@error-preview/error-preview.state'; import { MinaError } from '@shared/types/error-preview/mina-error.type'; import { AppNodeStatus } from '@shared/types/app/app-node-details.type'; import { Routes } from '@shared/enums/routes.enum'; +import { CONFIG } from '@shared/constants/config'; +import { LeaderboardService } from '@leaderboard/leaderboard.service'; +import { untilDestroyed } from '@ngneat/until-destroy'; @Component({ selector: 'mina-toolbar', @@ -26,6 +29,9 @@ export class ToolbarComponent extends StoreDispatcher implements OnInit { haveNextBP: boolean; isAllNodesPage: boolean; + @HostBinding('class.uptime') + showUptime: boolean = CONFIG.showLeaderboard; + @ViewChild('loadingRef') private loadingRef: ElementRef; constructor(private tooltipService: TooltipService) { super(); } diff --git a/frontend/src/app/layout/uptime-pill/uptime-pill.component.html b/frontend/src/app/layout/uptime-pill/uptime-pill.component.html new file mode 100644 index 0000000000..4aac49b5c2 --- /dev/null +++ b/frontend/src/app/layout/uptime-pill/uptime-pill.component.html @@ -0,0 +1,5 @@ +
+ beenhere +
Uptime {{ uptime.uptimePercentage }}% {{ uptime.uptimeTime }}
+
diff --git a/frontend/src/app/layout/uptime-pill/uptime-pill.component.scss b/frontend/src/app/layout/uptime-pill/uptime-pill.component.scss new file mode 100644 index 0000000000..36ace675f6 --- /dev/null +++ b/frontend/src/app/layout/uptime-pill/uptime-pill.component.scss @@ -0,0 +1,44 @@ +@import 'openmina'; + + +.chip { + gap: 4px; + background-color: $base-surface; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 6px; + background-color: $success-container; + } + + div { + color: $success-primary; + } + + span { + color: $success-secondary; + + &.mina-icon { + color: $success-primary; + } + } + + @media (max-width: 767px) { + width: 100%; + margin-bottom: 5px; + font-size: 12px; + + .mina-icon { + display: none; + } + + &.h-sm { + height: 32px !important; + } + } +} diff --git a/frontend/src/app/layout/uptime-pill/uptime-pill.component.ts b/frontend/src/app/layout/uptime-pill/uptime-pill.component.ts new file mode 100644 index 0000000000..86740d123e --- /dev/null +++ b/frontend/src/app/layout/uptime-pill/uptime-pill.component.ts @@ -0,0 +1,38 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { catchError, of, switchMap, timer } from 'rxjs'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { LeaderboardService } from '@leaderboard/leaderboard.service'; +import { ManualDetection, OpenminaEagerSharedModule } from '@openmina/shared'; + +@UntilDestroy() +@Component({ + selector: 'mina-uptime-pill', + standalone: true, + imports: [ + OpenminaEagerSharedModule, + ], + templateUrl: './uptime-pill.component.html', + styleUrl: './uptime-pill.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class UptimePillComponent extends ManualDetection implements OnInit { + + uptime: { uptimePercentage: number, uptimeTime: string } = { uptimePercentage: 0, uptimeTime: '' }; + + constructor(private leaderboardService: LeaderboardService) { super(); } + + ngOnInit(): void { + this.listenToUptime(); + } + + private listenToUptime(): void { + timer(0, 60000).pipe( + switchMap(() => this.leaderboardService.getUptime()), + catchError((err) => of({})), + untilDestroyed(this), + ).subscribe(uptime => { + this.uptime = uptime; + this.detect(); + }); + } +} diff --git a/frontend/src/app/shared/guards/landing-page.guard.ts b/frontend/src/app/shared/guards/landing-page.guard.ts new file mode 100644 index 0000000000..d381a585a7 --- /dev/null +++ b/frontend/src/app/shared/guards/landing-page.guard.ts @@ -0,0 +1,46 @@ +import { CanActivateFn, Router } from '@angular/router'; +import { inject } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { map, take } from 'rxjs/operators'; +import { CONFIG } from '@shared/constants/config'; +import { getMergedRoute } from '@openmina/shared'; +import { Routes } from '@shared/enums/routes.enum'; + +let isFirstLoad = true; + +export const landingPageGuard: CanActivateFn = (route, state) => { + + if (!isFirstLoad || !CONFIG.showWebNodeLandingPage) { + return true; + } + const router = inject(Router); + const store = inject(Store); + isFirstLoad = false; + + return store.select(getMergedRoute).pipe( + take(1), + map(route => { + if (!route) return true; + + const startsWith = (path: string) => route.url.startsWith(path); + + if ( + startsWith('/dashboard') || + startsWith('/block-production') || + startsWith('/state') || + startsWith('/mempool') || + startsWith('/loading-web-node') + ) { + return router.createUrlTree([Routes.LOADING_WEB_NODE], { + queryParamsHandling: 'preserve', + }); + } + + if (!startsWith('/') && !startsWith('/?') && !startsWith('/leaderboard')) { + return router.createUrlTree(['']); + } + + return true; + }), + ); +}; diff --git a/frontend/src/app/shared/helpers/date.helper.ts b/frontend/src/app/shared/helpers/date.helper.ts index 2dc15940dd..97243ff87a 100644 --- a/frontend/src/app/shared/helpers/date.helper.ts +++ b/frontend/src/app/shared/helpers/date.helper.ts @@ -98,3 +98,14 @@ export function getElapsedTime(timeInSeconds: number): string { return `${minutes}m ${seconds}s`; } + +export function getElapsedTimeInMinsAndHours(timeInMinutes: number): string { + if (timeInMinutes < 60) { + return `${timeInMinutes}m`; + } + + const hours = Math.floor(timeInMinutes / 60); + const minutes = timeInMinutes % 60; + + return `${hours}h ${minutes}m`; +} diff --git a/frontend/src/app/shared/types/core/environment/mina-env.type.ts b/frontend/src/app/shared/types/core/environment/mina-env.type.ts index febcec9c4b..06d4dd4ab6 100644 --- a/frontend/src/app/shared/types/core/environment/mina-env.type.ts +++ b/frontend/src/app/shared/types/core/environment/mina-env.type.ts @@ -6,6 +6,9 @@ export interface MinaEnv { hideNodeStats?: boolean; canAddNodes?: boolean; showWebNodeLandingPage?: boolean; + showLeaderboard?: boolean; + hidePeersPill?: boolean; + hideTxPill?: boolean; sentry?: { dsn: string; tracingOrigins: string[]; @@ -14,6 +17,7 @@ export interface MinaEnv { features?: FeaturesConfig; graphQL?: string; firebase?: any; + heartbeats?: boolean; }; } diff --git a/frontend/src/app/shared/types/leaderboard/heartbeat-summary.type.ts b/frontend/src/app/shared/types/leaderboard/heartbeat-summary.type.ts index a134981c2f..9993c968b9 100644 --- a/frontend/src/app/shared/types/leaderboard/heartbeat-summary.type.ts +++ b/frontend/src/app/shared/types/leaderboard/heartbeat-summary.type.ts @@ -5,4 +5,6 @@ export interface HeartbeatSummary { blocksProduced: number; uptimePrize: boolean; blocksPrize: boolean; + score: number; + maxScore: number; } diff --git a/frontend/src/assets/environments/leaderboard.js b/frontend/src/assets/environments/leaderboard.js index 904531dd35..713f1e1ef4 100644 --- a/frontend/src/assets/environments/leaderboard.js +++ b/frontend/src/assets/environments/leaderboard.js @@ -6,12 +6,14 @@ export default { production: true, canAddNodes: false, showWebNodeLandingPage: true, + showLeaderboard: true, + hidePeersPill: true, + hideTxPill: true, globalConfig: { features: { 'dashboard': [], 'block-production': ['won-slots'], 'mempool': [], - 'benchmarks': ['wallets'], 'state': ['actions'], }, firebase: { @@ -23,6 +25,7 @@ export default { appId: '1:1016673359357:web:bbd2cbf3f031756aec7594', measurementId: 'G-ENDBL923XT', }, + heartbeats: true, }, // sentry: { // dsn: 'https://69aba72a6290383494290cf285ab13b3@o4508216158584832.ingest.de.sentry.io/4508216160616528', diff --git a/frontend/src/assets/environments/webnode.js b/frontend/src/assets/environments/webnode.js index 14ed05ebc1..99848ea84f 100644 --- a/frontend/src/assets/environments/webnode.js +++ b/frontend/src/assets/environments/webnode.js @@ -10,9 +10,6 @@ export default { features: { 'dashboard': [], 'block-production': ['won-slots'], - 'mempool': [], - 'benchmarks': ['wallets'], - 'state': ['actions'], }, firebase: { 'projectId': 'openminawebnode', diff --git a/frontend/src/environments/environment.prod.ts b/frontend/src/environments/environment.prod.ts index 4325e3ef25..f1f0b8ef10 100644 --- a/frontend/src/environments/environment.prod.ts +++ b/frontend/src/environments/environment.prod.ts @@ -10,5 +10,8 @@ export const environment: Readonly = { hideToolbar: env.hideToolbar, canAddNodes: env.canAddNodes, showWebNodeLandingPage: env.showWebNodeLandingPage, + showLeaderboard: env.showLeaderboard, + hidePeersPill: env.hidePeersPill, + hideTxPill: env.hideTxPill, sentry: env.sentry, }; diff --git a/frontend/src/environments/environment.ts b/frontend/src/environments/environment.ts index 27fb7cddd8..c9d3c82a02 100644 --- a/frontend/src/environments/environment.ts +++ b/frontend/src/environments/environment.ts @@ -5,6 +5,9 @@ export const environment: Readonly = { identifier: 'Dev FE', canAddNodes: true, showWebNodeLandingPage: true, + showLeaderboard: true, + hidePeersPill: true, + hideTxPill: true, globalConfig: { features: { dashboard: [], @@ -27,6 +30,7 @@ export const environment: Readonly = { appId: '1:1016673359357:web:bbd2cbf3f031756aec7594', measurementId: 'G-ENDBL923XT', }, + heartbeats: true, graphQL: 'https://adonagy.com/graphql', // graphQL: 'https://api.minascan.io/node/devnet/v1/graphql', // graphQL: 'http://65.109.105.40:5000/graphql', @@ -87,18 +91,18 @@ export const environment: Readonly = { // resources: ['memory'], // }, // }, - { - name: 'Docker 11010', - url: 'http://localhost:11010', - }, - { - name: 'Docker 11012', - url: 'http://localhost:11012', - }, - { - name: 'Docker 11014', - url: 'http://localhost:11014', - }, + // { + // name: 'Docker 11010', + // url: 'http://localhost:11010', + // }, + // { + // name: 'Docker 11012', + // url: 'http://localhost:11012', + // }, + // { + // name: 'Docker 11014', + // url: 'http://localhost:11014', + // }, // { // name: 'Producer', // url: 'http://65.109.105.40:3000', diff --git a/frontend/src/index.html b/frontend/src/index.html index e627af2266..b04ef77d1f 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -4,7 +4,7 @@ - From 39d138d9557dd523eca4d6e34b5dbdd27a5cf30d Mon Sep 17 00:00:00 2001 From: Teofil Jolte Date: Wed, 5 Feb 2025 16:16:27 +0200 Subject: [PATCH 018/135] Configurable buttons for Webnode BP Starting Uptime pill --- frontend/package.json | 2 +- .../app/core/services/firestore.service.ts | 4 ++-- .../web-node-file-upload.component.html | 18 ++++++++++-------- .../web-node-file-upload.component.ts | 1 + .../uptime-pill/uptime-pill.component.html | 1 + .../uptime-pill/uptime-pill.component.scss | 19 +++++++++++++++++++ frontend/src/index.html | 4 ++-- 7 files changed, 36 insertions(+), 13 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 5b184eab7b..0b095271d9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "1.0.121", + "version": "1.0.124", "scripts": { "install:deps": "npm install", "start": "npm install && ng serve --configuration local --open", diff --git a/frontend/src/app/core/services/firestore.service.ts b/frontend/src/app/core/services/firestore.service.ts index 3871c5a6d9..438332b22c 100644 --- a/frontend/src/app/core/services/firestore.service.ts +++ b/frontend/src/app/core/services/firestore.service.ts @@ -11,7 +11,7 @@ import { DocumentData, } from '@angular/fire/firestore'; import { HttpClient } from '@angular/common/http'; -import { catchError, EMPTY, Observable } from 'rxjs'; +import { catchError, EMPTY, Observable, of } from 'rxjs'; @Injectable({ providedIn: 'root', @@ -30,7 +30,7 @@ export class FirestoreService { return this.http.post(this.cloudFunctionUrl, { data }).pipe( catchError(error => { console.error('Error while posting to cloud function:', error); - return error; + return of(null); }), ); } diff --git a/frontend/src/app/features/web-node/web-node-file-upload/web-node-file-upload.component.html b/frontend/src/app/features/web-node/web-node-file-upload/web-node-file-upload.component.html index a072a18d09..405b46d0ed 100644 --- a/frontend/src/app/features/web-node/web-node-file-upload/web-node-file-upload.component.html +++ b/frontend/src/app/features/web-node/web-node-file-upload/web-node-file-upload.component.html @@ -68,14 +68,16 @@

Set Up Your Web Node

}
- - - - - - - - + @if (!isLeaderboard) { + + + }