From 3c6c9da6d37a52ed19376b9f0b273bdce95aa33e Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Mon, 20 Jan 2025 14:25:08 +0100 Subject: [PATCH 1/2] fix styling issues --- node-gui/src/main.rs | 16 +++--- .../main_window/main_widget/tabs/summary.rs | 3 +- .../main_widget/tabs/wallet/addresses.rs | 5 +- .../main_widget/tabs/wallet/console.rs | 1 + .../main_widget/tabs/wallet/delegation.rs | 54 ++++++++++++------- .../main_widget/tabs/wallet/left_panel.rs | 4 +- .../main_widget/tabs/wallet/stake.rs | 42 ++++++++++----- .../main_widget/tabs/wallet/top_panel.rs | 1 + .../main_widget/tabs/wallet/transactions.rs | 21 ++++---- node-gui/src/main_window/mod.rs | 26 +++++++-- node-gui/src/widgets/wallet_mnemonic.rs | 10 +++- node-gui/src/widgets/wallet_unlock.rs | 2 +- 12 files changed, 127 insertions(+), 58 deletions(-) diff --git a/node-gui/src/main.rs b/node-gui/src/main.rs index 863a323ec..dc2cb3599 100644 --- a/node-gui/src/main.rs +++ b/node-gui/src/main.rs @@ -23,7 +23,7 @@ use std::env; use common::time_getter::TimeGetter; use iced::advanced::graphics::core::window; -use iced::widget::{column, container, row, text, tooltip, Text}; +use iced::widget::{column, row, text, tooltip, Text}; use iced::{executor, Element, Length, Settings, Task, Theme}; use iced::{font, Subscription}; use iced_aw::widgets::spinner::Spinner; @@ -57,6 +57,7 @@ pub fn main() -> iced::Result { antialiasing: true, ..Settings::default() }) + .font(iced_fonts::REQUIRED_FONT_BYTES) .run_with(initialize) } @@ -303,6 +304,7 @@ fn view(state: &MintlayerNodeGUI) -> Element { tooltip::Position::Bottom ) .gap(10) + .style(iced::widget::container::bordered_box), ], row![ iced::widget::button(text("Testnet")).on_press(InitNetwork::Testnet), @@ -313,13 +315,14 @@ fn view(state: &MintlayerNodeGUI) -> Element { tooltip::Position::Bottom ) .gap(10) + .style(iced::widget::container::bordered_box), ], ] .align_x(iced::Alignment::Center) .spacing(5); let res: Element = - container(error_box).center_x(Length::Fill).center_y(Length::Fill).into(); + iced::widget::container(error_box).center(Length::Fill).into(); res.map(Message::InitNetwork) } @@ -336,6 +339,7 @@ fn view(state: &MintlayerNodeGUI) -> Element { tooltip::Position::Bottom ) .gap(10) + .style(iced::widget::container::bordered_box), ], row![ iced::widget::button(text("Cold")).on_press(WalletMode::Cold), @@ -346,19 +350,20 @@ fn view(state: &MintlayerNodeGUI) -> Element { tooltip::Position::Bottom ) .gap(10) + .style(iced::widget::container::bordered_box), ], ] .align_x(iced::Alignment::Center) .spacing(5); let res: Element = - container(error_box).center_x(Length::Fill).center_y(Length::Fill).into(); + iced::widget::container(error_box).center(Length::Fill).into(); res.map(Message::InitWalletMode) } MintlayerNodeGUI::Loading(_) => { - container(Spinner::new().width(Length::Fill).height(Length::Fill)).into() + iced::widget::container(Spinner::new().width(Length::Fill).height(Length::Fill)).into() } MintlayerNodeGUI::Loaded(_backend_sender, w) => w.view().map(Message::MainWindowMessage), @@ -373,8 +378,7 @@ fn view(state: &MintlayerNodeGUI) -> Element { .align_x(iced::Alignment::Center) .spacing(5); - let res: Element<()> = - container(error_box).center_x(Length::Fill).center_y(Length::Fill).into(); + let res: Element<()> = iced::widget::container(error_box).center(Length::Fill).into(); res.map(|_| Message::ShuttingDownFinished) } diff --git a/node-gui/src/main_window/main_widget/tabs/summary.rs b/node-gui/src/main_window/main_widget/tabs/summary.rs index 761995b1d..3aecfaa41 100644 --- a/node-gui/src/main_window/main_widget/tabs/summary.rs +++ b/node-gui/src/main_window/main_widget/tabs/summary.rs @@ -71,7 +71,8 @@ impl Tab for SummaryTab { NETWORK_TOOLTIP, tooltip::Position::Bottom, ) - .gap(10), + .gap(10) + .style(iced::widget::container::bordered_box), ), ) .push( diff --git a/node-gui/src/main_window/main_widget/tabs/wallet/addresses.rs b/node-gui/src/main_window/main_widget/tabs/wallet/addresses.rs index 5e7936696..d76244599 100644 --- a/node-gui/src/main_window/main_widget/tabs/wallet/addresses.rs +++ b/node-gui/src/main_window/main_widget/tabs/wallet/addresses.rs @@ -14,7 +14,7 @@ // limitations under the License. use iced::{ - widget::{button, column, container, row, tooltip, Text}, + widget::{button, column, row, tooltip, Text}, Element, }; use iced_aw::{Grid, GridRow}; @@ -29,7 +29,7 @@ pub fn view_addresses( account: &AccountInfo, still_syncing: Option, ) -> Element<'static, WalletMessage> { - let field = |text: String| container(Text::new(text)).padding(5); + let field = |text: String| iced::widget::container(Text::new(text)).padding(5); let addresses = account .addresses .iter() @@ -57,6 +57,7 @@ pub fn view_addresses( tooltip::Position::Bottom ) .gap(10) + .style(iced::widget::container::bordered_box), ], ] .into() diff --git a/node-gui/src/main_window/main_widget/tabs/wallet/console.rs b/node-gui/src/main_window/main_widget/tabs/wallet/console.rs index 6eee9bd90..933ff7291 100644 --- a/node-gui/src/main_window/main_widget/tabs/wallet/console.rs +++ b/node-gui/src/main_window/main_widget/tabs/wallet/console.rs @@ -77,6 +77,7 @@ pub fn view_console( tooltip::Position::Bottom ) .gap(10) + .style(iced::widget::container::bordered_box), ], ] .into() diff --git a/node-gui/src/main_window/main_widget/tabs/wallet/delegation.rs b/node-gui/src/main_window/main_widget/tabs/wallet/delegation.rs index cf20c5e1c..0ec7f3893 100644 --- a/node-gui/src/main_window/main_widget/tabs/wallet/delegation.rs +++ b/node-gui/src/main_window/main_widget/tabs/wallet/delegation.rs @@ -16,7 +16,7 @@ use std::collections::BTreeMap; use iced::{ - widget::{button, column, container, row, text_input, tooltip, tooltip::Position, Text}, + widget::{button, column, row, text_input, tooltip, tooltip::Position, Text}, Element, Length, }; use iced_aw::{Grid, GridRow}; @@ -61,7 +61,7 @@ pub fn view_delegation( delegate_staking_amounts: &BTreeMap, still_syncing: Option, ) -> Element<'static, WalletMessage> { - let field = |text: String| container(Text::new(text)).padding(5); + let field = |text: String| iced::widget::container(Text::new(text)).padding(5); let delegation_balance_grid = { // We print the table only if there are delegations @@ -96,19 +96,22 @@ pub fn view_delegation( GridRow::new() .push(row![ tooltip( - container(Text::new(delegation_address.to_short_string()).font( - iced::font::Font { - family: iced::font::Family::Monospace, - weight: Default::default(), - stretch: Default::default(), - style: iced::font::Style::Normal, - } - )) + iced::widget::container( + Text::new(delegation_address.to_short_string()).font( + iced::font::Font { + family: iced::font::Family::Monospace, + weight: Default::default(), + stretch: Default::default(), + style: iced::font::Style::Normal, + } + ) + ) .padding(5), Text::new(delegation_address.to_string()), Position::Bottom, ) - .gap(5), + .gap(5) + .style(iced::widget::container::bordered_box), button( Text::new(iced_fonts::Bootstrap::ClipboardCheck.to_string()) .font(iced_fonts::BOOTSTRAP_FONT), @@ -121,19 +124,22 @@ pub fn view_delegation( ]) .push(row![ tooltip( - container(Text::new(pool_address.to_short_string()).font( - iced::font::Font { - family: iced::font::Family::Monospace, - weight: Default::default(), - stretch: Default::default(), - style: iced::font::Style::Normal, - } - )) + iced::widget::container( + Text::new(pool_address.to_short_string()).font( + iced::font::Font { + family: iced::font::Family::Monospace, + weight: Default::default(), + stretch: Default::default(), + style: iced::font::Style::Normal, + } + ) + ) .padding(5), Text::new(pool_address.to_string()), Position::Bottom, ) - .gap(5), + .gap(5) + .style(iced::widget::container::bordered_box), button( Text::new(iced_fonts::Bootstrap::ClipboardCheck.to_string()) .font(iced_fonts::BOOTSTRAP_FONT), @@ -184,6 +190,7 @@ pub fn view_delegation( Position::Bottom ) .gap(10) + .style(iced::widget::container::bordered_box), ], // ----- Create delegation row![ @@ -203,6 +210,7 @@ pub fn view_delegation( Position::Bottom ) .gap(10) + .style(iced::widget::container::bordered_box), ], row![ text_input("Delegation address", delegation_address) @@ -221,6 +229,7 @@ pub fn view_delegation( Position::Bottom ) .gap(10) + .style(iced::widget::container::bordered_box), ], row![ iced::widget::button(Text::new("Create delegation")) @@ -233,6 +242,7 @@ pub fn view_delegation( Position::Bottom ) .gap(10) + .style(iced::widget::container::bordered_box), ], iced::widget::horizontal_rule(10), // ----- Send delegation to address @@ -253,6 +263,7 @@ pub fn view_delegation( Position::Bottom ) .gap(10) + .style(iced::widget::container::bordered_box), ], row![ text_input("Amount to send", send_delegation_amount) @@ -271,6 +282,7 @@ pub fn view_delegation( Position::Bottom ) .gap(10) + .style(iced::widget::container::bordered_box), ], row![ text_input("Delegation address", send_delegation_id) @@ -289,6 +301,7 @@ pub fn view_delegation( Position::Bottom ) .gap(10) + .style(iced::widget::container::bordered_box), ], row![ iced::widget::button(Text::new("Withdraw from delegation")) @@ -301,6 +314,7 @@ pub fn view_delegation( Position::Bottom ) .gap(10) + .style(iced::widget::container::bordered_box), ], iced::widget::horizontal_rule(10), // ----- Delegation balance grid diff --git a/node-gui/src/main_window/main_widget/tabs/wallet/left_panel.rs b/node-gui/src/main_window/main_widget/tabs/wallet/left_panel.rs index 6dae672ee..f2f5aff50 100644 --- a/node-gui/src/main_window/main_widget/tabs/wallet/left_panel.rs +++ b/node-gui/src/main_window/main_widget/tabs/wallet/left_panel.rs @@ -101,6 +101,7 @@ pub fn view_left_panel( tooltip::Position::Bottom ) .gap(10) + .style(iced::widget::container::bordered_box), ]; button(label) @@ -154,7 +155,7 @@ pub fn view_left_panel( column![ text(file_name).size(25), row![ - pick_list.width(100), + pick_list, button(Text::new("+")) .style(iced::widget::button::success) .on_press(WalletMessage::NewAccount), @@ -165,6 +166,7 @@ pub fn view_left_panel( tooltip::Position::Bottom ) .gap(10) + .style(iced::widget::container::bordered_box), ] .align_y(Alignment::Center) .spacing(10) diff --git a/node-gui/src/main_window/main_widget/tabs/wallet/stake.rs b/node-gui/src/main_window/main_widget/tabs/wallet/stake.rs index 71e52f5ba..30506ffda 100644 --- a/node-gui/src/main_window/main_widget/tabs/wallet/stake.rs +++ b/node-gui/src/main_window/main_widget/tabs/wallet/stake.rs @@ -14,7 +14,7 @@ // limitations under the License. use iced::{ - widget::{button, column, container, row, text_input, tooltip, tooltip::Position, Text}, + widget::{button, column, row, text_input, tooltip, tooltip::Position, Text}, Alignment, Element, Length, }; use iced_aw::{Grid, GridRow}; @@ -78,7 +78,7 @@ pub fn view_stake( decommission_pool_address: &str, still_syncing: Option, ) -> Element<'static, WalletMessage> { - let field = |text: String| container(Text::new(text)).padding(5); + let field = |text: String| iced::widget::container(Text::new(text)).padding(5); let staking_balance_grid = { // We print the table only if there are staking pools @@ -103,19 +103,22 @@ pub fn view_stake( GridRow::new() .push(row!( tooltip( - container(Text::new(pool_id_address.to_short_string()).font( - iced::font::Font { - family: iced::font::Family::Monospace, - weight: Default::default(), - stretch: Default::default(), - style: iced::font::Style::Normal, - } - )) + iced::widget::container( + Text::new(pool_id_address.to_short_string()).font( + iced::font::Font { + family: iced::font::Family::Monospace, + weight: Default::default(), + stretch: Default::default(), + style: iced::font::Style::Normal, + } + ) + ) .padding(5), Text::new(pool_id_address.to_string()), Position::Bottom, ) - .gap(5), + .gap(5) + .style(iced::widget::container::bordered_box), button( Text::new(iced_fonts::Bootstrap::ClipboardCheck.to_string()) .font(iced_fonts::BOOTSTRAP_FONT), @@ -159,6 +162,7 @@ pub fn view_stake( Position::Bottom ) .gap(10) + .style(iced::widget::container::bordered_box), ] } else { row![] @@ -184,13 +188,17 @@ pub fn view_stake( Text::new(iced_fonts::Bootstrap::Question.to_string()).font(iced_fonts::BOOTSTRAP_FONT), MIN_PLEDGE_AMOUNT_TOOLTIP_TEXT, Position::Bottom) - .gap(10)], + .gap(10) + .style(iced::widget::container::bordered_box), + ], row![Text::new(maturity_period_text).size(13), tooltip( Text::new(iced_fonts::Bootstrap::Question.to_string()).font(iced_fonts::BOOTSTRAP_FONT), MATURITY_PERIOD_TOOLTIP_TEXT, Position::Bottom) - .gap(10)], + .gap(10) + .style(iced::widget::container::bordered_box), + ], row![ text_input("Pledge amount for the new staking pool", stake_amount) .on_input(|value| { @@ -206,6 +214,7 @@ pub fn view_stake( PLEDGE_AMOUNT_TOOLTIP_TEXT, Position::Bottom) .gap(10) + .style(iced::widget::container::bordered_box), ], row![ @@ -223,6 +232,7 @@ pub fn view_stake( COST_PER_BLOCK_TOOLTIP_TEXT, Position::Bottom) .gap(10) + .style(iced::widget::container::bordered_box), ], row![ @@ -240,6 +250,7 @@ pub fn view_stake( MARGIN_PER_THOUSAND_TOOLTIP_TEXT, Position::Bottom) .gap(10) + .style(iced::widget::container::bordered_box), ], row![ @@ -257,6 +268,7 @@ pub fn view_stake( DECOMMISSION_ADDRESS_TOOLTIP_TEXT, Position::Bottom) .gap(10) + .style(iced::widget::container::bordered_box), ], row![ @@ -269,6 +281,7 @@ pub fn view_stake( Position::Bottom ) .gap(10) + .style(iced::widget::container::bordered_box), ], staking_enabled_row.spacing(10).align_y(Alignment::Center), @@ -290,6 +303,7 @@ pub fn view_stake( DECOMMISSION_POOL_ADDRESS_TOOLTIP_TEXT, Position::Bottom) .gap(10) + .style(iced::widget::container::bordered_box), ], row![ text_input("Address that will receive the proceeds from the staking pool", decommission_pool_address) @@ -306,6 +320,7 @@ pub fn view_stake( DECOMMISSION_COINS_DESTINATION_ADDRESS_TOOLTIP_TEXT, Position::Bottom) .gap(10) + .style(iced::widget::container::bordered_box), ], row![ iced::widget::button(Text::new("Decommission staking pool")) @@ -318,6 +333,7 @@ pub fn view_stake( Position::Bottom ) .gap(10) + .style(iced::widget::container::bordered_box), ], ] .spacing(10) diff --git a/node-gui/src/main_window/main_widget/tabs/wallet/top_panel.rs b/node-gui/src/main_window/main_widget/tabs/wallet/top_panel.rs index c0cf1dd0e..a59a2bb91 100644 --- a/node-gui/src/main_window/main_widget/tabs/wallet/top_panel.rs +++ b/node-gui/src/main_window/main_widget/tabs/wallet/top_panel.rs @@ -65,6 +65,7 @@ pub fn view_top_panel( tooltip::Position::Bottom ) .gap(10) + .style(iced::widget::container::bordered_box), ], } .align_y(Alignment::Center) diff --git a/node-gui/src/main_window/main_widget/tabs/wallet/transactions.rs b/node-gui/src/main_window/main_widget/tabs/wallet/transactions.rs index 6e9278157..5e55657d7 100644 --- a/node-gui/src/main_window/main_widget/tabs/wallet/transactions.rs +++ b/node-gui/src/main_window/main_widget/tabs/wallet/transactions.rs @@ -14,7 +14,7 @@ // limitations under the License. use iced::{ - widget::{button, container, row, tooltip, tooltip::Position, Column, Text}, + widget::{button, row, tooltip, tooltip::Position, Column, Text}, Alignment, Element, Length, }; use iced_aw::{Grid, GridRow}; @@ -30,7 +30,7 @@ pub fn view_transactions( chain_config: &ChainConfig, account: &AccountInfo, ) -> Element<'static, WalletMessage> { - let field = |text: String| container(Text::new(text)).padding(5); + let field = |text: String| iced::widget::container(Text::new(text)).padding(5); let mut transactions = Column::new(); let current_transaction_list = &account.transaction_list; @@ -59,17 +59,20 @@ pub fn view_transactions( .push(field(format!("{}", current_transaction_list.skip + index))) .push(row![ tooltip( - container(Text::new(tx.txid.to_string()).font(iced::font::Font { - family: iced::font::Family::Monospace, - weight: Default::default(), - stretch: Default::default(), - style: iced::font::Style::Normal, - })) + iced::widget::container(Text::new(tx.txid.to_string()).font( + iced::font::Font { + family: iced::font::Family::Monospace, + weight: Default::default(), + stretch: Default::default(), + style: iced::font::Style::Normal, + } + )) .padding(5), Text::new(full_tx_id_str.clone()), Position::Bottom, ) - .gap(5), + .gap(5) + .style(iced::widget::container::bordered_box), button( Text::new(iced_fonts::Bootstrap::ClipboardCheck.to_string()) .font(iced_fonts::BOOTSTRAP_FONT), diff --git a/node-gui/src/main_window/mod.rs b/node-gui/src/main_window/mod.rs index da9bbe028..5667baabe 100644 --- a/node-gui/src/main_window/mod.rs +++ b/node-gui/src/main_window/mod.rs @@ -411,12 +411,12 @@ impl MainWindow { let wallet_type = wallet_info.wallet_type; self.node_state.wallets.insert(wallet_id, wallet_info); - Task::perform(async {}, move |_| { - MainWindowMessage::MainWidgetMessage(MainWidgetMessage::WalletAdded { + Task::done(MainWindowMessage::MainWidgetMessage( + MainWidgetMessage::WalletAdded { wallet_id, wallet_type, - }) - }) + }, + )) } BackendEvent::OpenWallet(Err(error)) | BackendEvent::ImportWallet(Err(error)) => { @@ -726,6 +726,17 @@ impl MainWindow { import, wallet_type, } => { + self.active_dialog = match import { + ImportOrCreate::Create => ActiveDialog::WalletCreate { + generated_mnemonic: mnemonic.clone(), + wallet_type, + state: ImportState::new_importing(String::new()), + }, + ImportOrCreate::Import => ActiveDialog::WalletRecover { + wallet_type, + state: ImportState::new_importing(mnemonic.to_string()), + }, + }; self.file_dialog_active = false; backend_sender.send(BackendRequest::RecoverWallet { mnemonic, @@ -771,6 +782,13 @@ impl MainWindow { wallet_id, password, } => { + self.active_dialog = ActiveDialog::WalletUnlock { + wallet_id, + state: UnlockState { + password: password.clone(), + unlocking: true, + }, + }; backend_sender.send(BackendRequest::UpdateEncryption { wallet_id, action: EncryptionAction::Unlock(password), diff --git a/node-gui/src/widgets/wallet_mnemonic.rs b/node-gui/src/widgets/wallet_mnemonic.rs index ac1947416..e86554599 100644 --- a/node-gui/src/widgets/wallet_mnemonic.rs +++ b/node-gui/src/widgets/wallet_mnemonic.rs @@ -53,7 +53,8 @@ where Card::new( Text::new(action_text), iced::widget::column![text_input("Mnemonic", &mnemonic) - .on_input(on_mnemonic_change) + // only enable edit if there is not pre-generated mnemonic + .on_input_maybe(generated_mnemonic_opt.is_none().then_some(on_mnemonic_change)) .padding(15)], ) .foot(container(button).center_x(Length::Fill)) @@ -69,6 +70,13 @@ pub struct ImportState { } impl ImportState { + pub fn new_importing(entered_mnemonic: String) -> Self { + Self { + entered_mnemonic, + importing: true, + } + } + pub fn with_changed_mnemonic(&self, new_mnemonic: String) -> Self { Self { entered_mnemonic: new_mnemonic, diff --git a/node-gui/src/widgets/wallet_unlock.rs b/node-gui/src/widgets/wallet_unlock.rs index 98eeb51c7..4bfa7c48f 100644 --- a/node-gui/src/widgets/wallet_unlock.rs +++ b/node-gui/src/widgets/wallet_unlock.rs @@ -43,7 +43,7 @@ where let password = text_input("Password", &state.password) .secure(true) - .on_input_maybe(state.unlocking.then_some(on_edit_password)); + .on_input_maybe((!state.unlocking).then_some(on_edit_password)); Card::new( Text::new("Unlock"), From ca885affa4d0a9627594c3fe864079e14b18789c Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Mon, 20 Jan 2025 09:54:42 +0200 Subject: [PATCH 2/2] Misc improvements in node gui: * When the network type is passed from the command line, the app will actually use it, without asking the user. * Some `expect`s were replaced by errors to avoid crashes. * Logging is now initialized in the cold mode too (only the console logging is enabled). The app will also print a warning to the log when the log-to-file or clean-data options are specified in the cold mode. * The message "Data directory is now clean ... Please restart the node" is no longer shown as an error. * The log-to-file option was moved from RunOptions to the top-level options. --- Cargo.lock | 1 + common/src/chain/config/regtest_options.rs | 2 +- dns-server/src/main.rs | 2 +- node-daemon/src/main.rs | 2 +- node-gui/Cargo.toml | 1 + .../backend/src/chainstate_event_handler.rs | 12 +- node-gui/backend/src/lib.rs | 152 +++++---- node-gui/backend/src/p2p_event_handler.rs | 20 +- node-gui/src/main.rs | 292 +++++++++++++----- node-lib/src/lib.rs | 3 +- node-lib/src/options.rs | 131 ++++++-- node-lib/src/runner.rs | 37 +-- node-lib/tests/cli.rs | 1 - test/src/bin/test_node.rs | 2 +- utils/src/default_data_dir.rs | 48 ++- 15 files changed, 463 insertions(+), 243 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 605dbe29a..4d99993e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4829,6 +4829,7 @@ dependencies = [ "chrono", "common", "futures", + "heck 0.5.0", "iced", "iced_aw", "iced_fonts", diff --git a/common/src/chain/config/regtest_options.rs b/common/src/chain/config/regtest_options.rs index 17b70b3b4..f177c10e7 100644 --- a/common/src/chain/config/regtest_options.rs +++ b/common/src/chain/config/regtest_options.rs @@ -36,7 +36,7 @@ use super::{regtest::GenesisStakingSettings, ChainConfig}; use anyhow::{anyhow, ensure, Result}; use paste::paste; -#[derive(Args, Clone, Debug)] +#[derive(Args, Clone, Debug, Default)] pub struct ChainConfigOptions { /// Magic bytes. #[clap(long)] diff --git a/dns-server/src/main.rs b/dns-server/src/main.rs index 9abbcd78c..f0032512f 100644 --- a/dns-server/src/main.rs +++ b/dns-server/src/main.rs @@ -108,7 +108,7 @@ async fn run(options: DnsServerRunOptions) -> anyhow::Result { let data_dir = prepare_data_dir( || default_data_dir_for_chain(chain_type.name()), - &config.datadir, + config.datadir.as_ref(), None, ) .expect("Failed to prepare data directory"); diff --git a/node-daemon/src/main.rs b/node-daemon/src/main.rs index 049dde210..8b7f9acb6 100644 --- a/node-daemon/src/main.rs +++ b/node-daemon/src/main.rs @@ -15,7 +15,7 @@ pub async fn run() -> anyhow::Result<()> { let opts = node_lib::Options::from_args(std::env::args_os()); - let setup_result = node_lib::setup(opts).await?; + let setup_result = node_lib::setup(opts.with_resolved_command()).await?; match setup_result { node_lib::NodeSetupResult::Node(node) => { node.main().await; diff --git a/node-gui/Cargo.toml b/node-gui/Cargo.toml index 7057b5101..ec861ed31 100644 --- a/node-gui/Cargo.toml +++ b/node-gui/Cargo.toml @@ -27,6 +27,7 @@ wallet-cli-commands = { path = "../wallet/wallet-cli-commands"} anyhow.workspace = true chrono.workspace = true futures.workspace = true +heck.workspace = true iced = { workspace = true, features = ["canvas", "debug", "tokio", "lazy"] } iced_aw = { workspace = true, features = ["cupertino"] } iced_fonts = { workspace = true, features = ["bootstrap"] } diff --git a/node-gui/backend/src/chainstate_event_handler.rs b/node-gui/backend/src/chainstate_event_handler.rs index 4c876e42b..1177416bf 100644 --- a/node-gui/backend/src/chainstate_event_handler.rs +++ b/node-gui/backend/src/chainstate_event_handler.rs @@ -15,8 +15,10 @@ use std::sync::Arc; -use chainstate::ChainstateEvent; +use anyhow::Context as _; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; + +use chainstate::ChainstateEvent; use utils::tap_log::TapLog; use super::{backend_impl::Backend, messages::BackendEvent}; @@ -32,7 +34,7 @@ impl ChainstateEventHandler { pub async fn new( chainstate: chainstate::ChainstateHandle, event_tx: UnboundedSender, - ) -> Self { + ) -> anyhow::Result { let (chainstate_event_tx, chainstate_event_rx) = unbounded_channel(); chainstate .call_mut(|this| { @@ -45,14 +47,14 @@ impl ChainstateEventHandler { )); }) .await - .expect("Failed to subscribe to chainstate"); + .context("Error subscribing to chainstate events")?; - Self { + Ok(Self { chainstate, chainstate_event_rx, event_tx, chain_info_updated: false, - } + }) } pub async fn run(&mut self) { diff --git a/node-gui/backend/src/lib.rs b/node-gui/backend/src/lib.rs index e5fcb3d8b..b76cf9894 100644 --- a/node-gui/backend/src/lib.rs +++ b/node-gui/backend/src/lib.rs @@ -22,31 +22,35 @@ mod p2p_event_handler; mod wallet_events; mod account_id; -pub use account_id::AccountId; use std::fmt::Debug; use std::sync::Arc; +use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; + use chainstate::ChainInfo; use common::{ address::{Address, AddressError}, chain::{ChainConfig, Destination}, primitives::{Amount, BlockHeight}, - time_getter::TimeGetter, }; -use node_lib::{Command, RunOptions}; -use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; +use logging::log; +use node_lib::OptionsWithResolvedCommand; -use crate::chainstate_event_handler::ChainstateEventHandler; -use crate::p2p_event_handler::P2pEventHandler; +use crate::{chainstate_event_handler::ChainstateEventHandler, p2p_event_handler::P2pEventHandler}; -use self::error::BackendError; -use self::messages::{BackendEvent, BackendRequest}; +use self::{ + error::BackendError, + messages::{BackendEvent, BackendRequest}, +}; + +pub use account_id::AccountId; #[derive(Debug, Clone, Copy)] pub enum InitNetwork { Mainnet, Testnet, + Regtest, } #[derive(Debug, Clone, Copy)] @@ -111,11 +115,16 @@ pub struct InitializedNode { pub chain_info: ChainInfo, } +#[derive(Debug)] +pub enum NodeInitializationOutcome { + BackendControls(BackendControls), + DataDirCleanedUp, +} + pub async fn node_initialize( - _time_getter: TimeGetter, - network: InitNetwork, + opts: node_lib::OptionsWithResolvedCommand, mode: WalletMode, -) -> anyhow::Result { +) -> anyhow::Result { if std::env::var("RUST_LOG").is_err() { std::env::set_var( "RUST_LOG", @@ -124,20 +133,14 @@ pub async fn node_initialize( } let opts = { - let mut opts = node_lib::Options::from_args(std::env::args_os()); - let run_opts = { - // For the GUI, we configure different defaults, such as disabling RPC server binding - // and enabling logging to a file. - let mut run_opts = - opts.command.map_or(RunOptions::default(), |c| c.run_options().clone()); - run_opts.rpc_enabled = Some(run_opts.rpc_enabled.unwrap_or(false)); - run_opts.log_to_file = Some(run_opts.log_to_file.unwrap_or(true)); - run_opts - }; - opts.command = match network { - InitNetwork::Mainnet => Some(Command::Mainnet(run_opts)), - InitNetwork::Testnet => Some(Command::Testnet(run_opts)), - }; + let mut opts = opts; + let run_opts = opts.command.run_options_mut(); + + // For the GUI, we configure different defaults, such as disabling RPC server binding + // and enabling logging to a file. + run_opts.rpc_enabled = Some(run_opts.rpc_enabled.unwrap_or(false)); + opts.top_level.log_to_file = Some(opts.top_level.log_to_file.unwrap_or(true)); + opts }; @@ -152,8 +155,7 @@ pub async fn node_initialize( let node = match setup_result { node_lib::NodeSetupResult::Node(node) => node, node_lib::NodeSetupResult::DataDirCleanedUp => { - // TODO: find more friendly way to report the message and shut down GUI - anyhow::bail!("Data directory is now clean. Please restart the node without `--clean-data` flag"); + return Ok(NodeInitializationOutcome::DataDirCleanedUp); } }; @@ -163,9 +165,10 @@ pub async fn node_initialize( // Subscribe to chainstate before getting the current chain_info! let chainstate_event_handler = - ChainstateEventHandler::new(controller.chainstate.clone(), event_tx.clone()).await; + ChainstateEventHandler::new(controller.chainstate.clone(), event_tx.clone()) + .await?; - let p2p_event_handler = P2pEventHandler::new(&controller.p2p, event_tx.clone()).await; + let p2p_event_handler = P2pEventHandler::new(&controller.p2p, event_tx.clone()).await?; let chain_config = controller.chainstate.call(|this| Arc::clone(this.get_chain_config())).await?; @@ -192,35 +195,14 @@ pub async fn node_initialize( }); (chain_config, chain_info) } - WalletMode::Cold => { - let chain_config = Arc::new(match network { - InitNetwork::Mainnet => common::chain::config::create_mainnet(), - InitNetwork::Testnet => common::chain::config::create_testnet(), - }); - let chain_info = ChainInfo { - best_block_id: chain_config.genesis_block_id(), - best_block_height: BlockHeight::zero(), - median_time: chain_config.genesis_block().timestamp(), - best_block_timestamp: chain_config.genesis_block().timestamp(), - is_initial_block_download: false, - }; - - let manager_join_handle = tokio::spawn(async move {}); - - let backend = backend_impl::Backend::new_cold( - chain_config.clone(), - event_tx, - low_priority_event_tx, - wallet_updated_tx, - manager_join_handle, - ); - - tokio::spawn(async move { - backend_impl::run_cold(backend, request_rx, wallet_updated_rx).await; - }); - - (chain_config, chain_info) - } + WalletMode::Cold => spawn_cold_backend( + opts, + event_tx, + request_rx, + low_priority_event_tx, + wallet_updated_tx, + wallet_updated_rx, + )?, }; let initialized_node = InitializedNode { @@ -235,5 +217,57 @@ pub async fn node_initialize( low_priority_backend_receiver: low_priority_event_rx, }; - Ok(backend_controls) + Ok(NodeInitializationOutcome::BackendControls(backend_controls)) +} + +fn spawn_cold_backend( + options: OptionsWithResolvedCommand, + event_tx: UnboundedSender, + request_rx: UnboundedReceiver, + low_priority_event_tx: UnboundedSender, + wallet_updated_tx: UnboundedSender, + wallet_updated_rx: UnboundedReceiver, +) -> anyhow::Result<(Arc, ChainInfo)> { + logging::init_logging(); + + let chain_config = Arc::new(handle_options_in_cold_wallet_mode(options)?); + let chain_info = ChainInfo { + best_block_id: chain_config.genesis_block_id(), + best_block_height: BlockHeight::zero(), + median_time: chain_config.genesis_block().timestamp(), + best_block_timestamp: chain_config.genesis_block().timestamp(), + is_initial_block_download: false, + }; + + let manager_join_handle = tokio::spawn(async move {}); + + let backend = backend_impl::Backend::new_cold( + chain_config.clone(), + event_tx, + low_priority_event_tx, + wallet_updated_tx, + manager_join_handle, + ); + + tokio::spawn(async move { + backend_impl::run_cold(backend, request_rx, wallet_updated_rx).await; + }); + + Ok((chain_config, chain_info)) +} + +fn handle_options_in_cold_wallet_mode( + options: OptionsWithResolvedCommand, +) -> anyhow::Result { + if options.clean_data_option_set() { + log::warn!("Ignoring clean-data option in cold wallet mode"); + } + + if options.log_to_file_option_set() { + log::warn!("Log-to-file disabled in cold wallet mode"); + } + + // TODO: check all other options? + + options.command.create_chain_config() } diff --git a/node-gui/backend/src/p2p_event_handler.rs b/node-gui/backend/src/p2p_event_handler.rs index 9a205360f..5d8971e5d 100644 --- a/node-gui/backend/src/p2p_event_handler.rs +++ b/node-gui/backend/src/p2p_event_handler.rs @@ -15,9 +15,11 @@ use std::sync::Arc; +use anyhow::Context; +use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; + use p2p::{interface::p2p_interface::P2pInterface, P2pEvent}; use subsystem::Handle; -use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; use utils::tap_log::TapLog; use super::{backend_impl::Backend, messages::BackendEvent}; @@ -31,10 +33,16 @@ impl P2pEventHandler { pub async fn new( p2p: &Handle, event_tx: UnboundedSender, - ) -> Self { + ) -> anyhow::Result { // TODO: Fix race in p2p events subscribe (if some peers are connected before the subscription is complete) + // TODO: need a way to propagate subsystem initialization errors. E.g. if the p2p port is busy, we'll + // currently report "Error subscribing to P2P events: Callee subsystem did not respond", which is not + // very informative. + // Same for ChainstateEventHandler. + let (p2p_event_tx, p2p_event_rx) = unbounded_channel(); + let error_context = "Error subscribing to P2P events"; p2p.call_mut(|this| { this.subscribe_to_events(Arc::new(move |p2p_event: P2pEvent| { _ = p2p_event_tx @@ -43,13 +51,13 @@ impl P2pEventHandler { })) }) .await - .expect("Failed to subscribe to P2P event") - .expect("Failed to subscribe to P2P event"); + .context(error_context)? + .context(error_context)?; - Self { + Ok(Self { p2p_event_rx, event_tx, - } + }) } pub async fn run(&mut self) { diff --git a/node-gui/src/main.rs b/node-gui/src/main.rs index dc2cb3599..769149875 100644 --- a/node-gui/src/main.rs +++ b/node-gui/src/main.rs @@ -21,18 +21,23 @@ mod widgets; use std::convert::identity; use std::env; -use common::time_getter::TimeGetter; -use iced::advanced::graphics::core::window; -use iced::widget::{column, row, text, tooltip, Text}; -use iced::{executor, Element, Length, Settings, Task, Theme}; -use iced::{font, Subscription}; +use heck::ToUpperCamelCase as _; +use iced::{ + advanced::graphics::core::window, + executor, font, + widget::{column, row, text, tooltip, Text}, + Element, Length, Settings, Subscription, Task, Theme, +}; use iced_aw::widgets::spinner::Spinner; +use tokio::sync::mpsc::UnboundedReceiver; + +use common::chain::config::ChainType; use main_window::{MainWindow, MainWindowMessage}; use node_gui_backend::{ messages::{BackendEvent, BackendRequest}, - node_initialize, BackendControls, BackendSender, InitNetwork, WalletMode, + node_initialize, BackendControls, BackendSender, InitNetwork, NodeInitializationOutcome, + WalletMode, }; -use tokio::sync::mpsc::UnboundedReceiver; const COLD_WALLET_TOOLTIP_TEXT: &str = "Start the wallet in Cold mode without connecting to the network or any nodes. The Cold mode is made to run the wallet on an air-gapped machine without internet connection for storage of keys of high-value. For example, pool decommission keys."; @@ -44,6 +49,8 @@ const TEST_NETWORK_TOOLTIP: &str = "The 'Testnet' is the network with coins that pub fn main() -> iced::Result { utils::rust_backtrace::enable(); + let initial_opts = node_lib::Options::from_args(std::env::args_os()); + iced::application(title, update, view) .executor::() .subscription(subscription) @@ -58,16 +65,43 @@ pub fn main() -> iced::Result { ..Settings::default() }) .font(iced_fonts::REQUIRED_FONT_BYTES) - .run_with(initialize) + .run_with(|| initialize(initial_opts)) } -enum MintlayerNodeGUI { - Initial, - SelectNetwork, - SelectWalletMode(InitNetwork), - Loading(WalletMode), - Loaded(BackendSender, MainWindow), - IntializationError(String), +enum GuiState { + Initial { + initial_options: node_lib::Options, + }, + SelectNetwork { + top_level_options: node_lib::TopLevelOptions, + }, + SelectWalletMode { + resolved_options: node_lib::OptionsWithResolvedCommand, + }, + Loading { + wallet_mode: WalletMode, + chain_type: ChainType, + }, + Loaded { + backend_sender: BackendSender, + main_window: MainWindow, + }, + InitializationInterrupted(InitializationInterruptionReason), +} + +impl From for GuiState { + fn from(value: InitializationFailure) -> Self { + Self::InitializationInterrupted(InitializationInterruptionReason::Failure(value)) + } +} + +enum InitializationInterruptionReason { + Failure(InitializationFailure), + DataDirCleanedUp, +} + +struct InitializationFailure { + message: String, } #[derive(Debug)] @@ -79,46 +113,86 @@ pub enum Message { UnboundedReceiver, BackendEvent, ), - Loaded(anyhow::Result), + Loaded(anyhow::Result), FontLoaded(Result<(), font::Error>), EventOccurred(iced::Event), ShuttingDownFinished, MainWindowMessage(MainWindowMessage), } -fn initialize() -> (MintlayerNodeGUI, Task) { +fn initialize(initial_options: node_lib::Options) -> (GuiState, Task) { ( - MintlayerNodeGUI::Initial, + GuiState::Initial { initial_options }, font::load(iced_fonts::BOOTSTRAP_FONT_BYTES).map(Message::FontLoaded), ) } -fn title(state: &MintlayerNodeGUI) -> String { +fn chain_type_to_string(chain_type: ChainType) -> String { + chain_type.name().to_upper_camel_case() +} + +fn title(state: &GuiState) -> String { let version = env!("CARGO_PKG_VERSION"); match state { - MintlayerNodeGUI::Initial => "Mintlayer Node - Initializing...".to_string(), - MintlayerNodeGUI::SelectNetwork => "Mintlayer Node - Selecting network...".to_string(), - MintlayerNodeGUI::SelectWalletMode(_) => "Mintlayer Node - Selecting mode...".to_string(), - MintlayerNodeGUI::Loading(_) => "Mintlayer Node - Loading...".to_string(), - MintlayerNodeGUI::Loaded(_backend_sender, w) => { + GuiState::Initial { .. } => "Mintlayer Node - Initializing...".into(), + GuiState::SelectNetwork { .. } => "Mintlayer Node - Selecting network...".into(), + GuiState::SelectWalletMode { resolved_options } => { + format!( + "Mintlayer Node - {} - Selecting mode...", + chain_type_to_string(resolved_options.command.chain_type()) + ) + } + GuiState::Loading { + wallet_mode: _, + chain_type, + } => format!( + "Mintlayer Node - {} - Loading...", + chain_type_to_string(*chain_type) + ), + GuiState::Loaded { + backend_sender: _, + main_window, + } => { format!( "Mintlayer Node - {} - v{version}", - w.node_state().chain_config().chain_type().name() + chain_type_to_string(*main_window.node_state().chain_config().chain_type()) ) } - MintlayerNodeGUI::IntializationError(_) => "Mintlayer initialization error".to_string(), + GuiState::InitializationInterrupted(reason) => match reason { + InitializationInterruptionReason::Failure(_) => "Mintlayer initialization error".into(), + InitializationInterruptionReason::DataDirCleanedUp => { + "Mintlayer data directory cleaned up".into() + } + }, } } -fn update(state: &mut MintlayerNodeGUI, message: Message) -> Task { +fn update(state: &mut GuiState, message: Message) -> Task { match state { - MintlayerNodeGUI::Initial => match message { + GuiState::Initial { initial_options } => match message { Message::FontLoaded(Ok(())) => { - *state = MintlayerNodeGUI::SelectNetwork; + match &initial_options.command { + Some(command) => { + *state = GuiState::SelectWalletMode { + resolved_options: node_lib::OptionsWithResolvedCommand { + top_level: initial_options.top_level.clone(), + command: command.clone(), + }, + }; + } + None => { + *state = GuiState::SelectNetwork { + top_level_options: initial_options.top_level.clone(), + }; + } + } Task::none() } Message::FontLoaded(Err(_)) => { - *state = MintlayerNodeGUI::IntializationError("Failed to load font".into()); + *state = InitializationFailure { + message: "Failed to load font".into(), + } + .into(); Task::none() } Message::ShuttingDownFinished => { @@ -138,9 +212,19 @@ fn update(state: &mut MintlayerNodeGUI, message: Message) -> Task { | Message::FromBackend(_, _, _) | Message::MainWindowMessage(_) => unreachable!(), }, - MintlayerNodeGUI::SelectNetwork => match message { + GuiState::SelectNetwork { top_level_options } => match message { Message::InitNetwork(init) => { - *state = MintlayerNodeGUI::SelectWalletMode(init); + let opts = node_lib::OptionsWithResolvedCommand { + top_level: top_level_options.clone(), + command: match init { + InitNetwork::Mainnet => node_lib::Command::Mainnet(Default::default()), + InitNetwork::Testnet => node_lib::Command::Testnet(Default::default()), + InitNetwork::Regtest => node_lib::Command::Regtest(Default::default()), + }, + }; + *state = GuiState::SelectWalletMode { + resolved_options: opts, + }; Task::none() } Message::ShuttingDownFinished => { @@ -160,16 +244,17 @@ fn update(state: &mut MintlayerNodeGUI, message: Message) -> Task { | Message::FromBackend(_, _, _) | Message::MainWindowMessage(_) => unreachable!(), }, - MintlayerNodeGUI::SelectWalletMode(init) => { - let init = *init; + GuiState::SelectWalletMode { resolved_options } => { match message { Message::InitWalletMode(mode) => { - *state = MintlayerNodeGUI::Loading(mode); + let opts = resolved_options.clone(); - Task::perform( - node_initialize(TimeGetter::default(), init, mode), - Message::Loaded, - ) + *state = GuiState::Loading { + wallet_mode: mode, + chain_type: opts.command.chain_type(), + }; + + Task::perform(node_initialize(opts, mode), Message::Loaded) } Message::ShuttingDownFinished => { iced::window::get_latest().and_then(iced::window::close) @@ -189,30 +274,50 @@ fn update(state: &mut MintlayerNodeGUI, message: Message) -> Task { | Message::MainWindowMessage(_) => unreachable!(), } } - MintlayerNodeGUI::Loading(mode) => match message { + GuiState::Loading { + wallet_mode, + chain_type: _, + } => match message { Message::InitNetwork(_) | Message::InitWalletMode(_) | Message::FromBackend(_, _, _) => unreachable!(), - Message::Loaded(Ok(backend_controls)) => { - let BackendControls { - initialized_node, - backend_sender, - backend_receiver, - low_priority_backend_receiver, - } = backend_controls; - *state = MintlayerNodeGUI::Loaded( - backend_sender, - MainWindow::new(initialized_node, *mode), - ); - recv_backend_command(backend_receiver, low_priority_backend_receiver) - } + Message::Loaded(Ok(init_outcome)) => match init_outcome { + NodeInitializationOutcome::BackendControls(backend_controls) => { + let BackendControls { + initialized_node, + backend_sender, + backend_receiver, + low_priority_backend_receiver, + } = backend_controls; + *state = GuiState::Loaded { + backend_sender, + main_window: MainWindow::new(initialized_node, *wallet_mode), + }; + + recv_backend_command(backend_receiver, low_priority_backend_receiver) + } + NodeInitializationOutcome::DataDirCleanedUp => { + *state = GuiState::InitializationInterrupted( + InitializationInterruptionReason::DataDirCleanedUp, + ); + Task::none() + } + }, Message::Loaded(Err(e)) => { - *state = MintlayerNodeGUI::IntializationError(e.to_string()); + *state = InitializationFailure { + // Note: we need to use the alternate selector in order to show both anyhow::Error's context + // and the original error message. + message: format!("{e:#}"), + } + .into(); Task::none() } Message::FontLoaded(status) => { if status.is_err() { - *state = MintlayerNodeGUI::IntializationError("Failed to load font".into()); + *state = InitializationFailure { + message: "Failed to load font".into(), + } + .into(); } Task::none() } @@ -227,17 +332,21 @@ fn update(state: &mut MintlayerNodeGUI, message: Message) -> Task { Message::ShuttingDownFinished => Task::none(), Message::MainWindowMessage(_) => Task::none(), }, - MintlayerNodeGUI::Loaded(backend_sender, w) => match message { + GuiState::Loaded { + backend_sender, + main_window, + } => match message { Message::FromBackend( backend_receiver, low_priority_backend_receiver, backend_event, ) => Task::batch([ - w.update( - MainWindowMessage::FromBackend(backend_event), - backend_sender, - ) - .map(Message::MainWindowMessage), + main_window + .update( + MainWindowMessage::FromBackend(backend_event), + backend_sender, + ) + .map(Message::MainWindowMessage), recv_backend_command(backend_receiver, low_priority_backend_receiver), ]), Message::InitNetwork(_) | Message::InitWalletMode(_) | Message::Loaded(_) => { @@ -245,7 +354,10 @@ fn update(state: &mut MintlayerNodeGUI, message: Message) -> Task { } Message::FontLoaded(status) => { if status.is_err() { - *state = MintlayerNodeGUI::IntializationError("Failed to load font".into()); + *state = InitializationFailure { + message: "Failed to load font".into(), + } + .into(); } Task::none() } @@ -262,10 +374,10 @@ fn update(state: &mut MintlayerNodeGUI, message: Message) -> Task { iced::window::get_latest().and_then(iced::window::close) } Message::MainWindowMessage(msg) => { - w.update(msg, backend_sender).map(Message::MainWindowMessage) + main_window.update(msg, backend_sender).map(Message::MainWindowMessage) } }, - MintlayerNodeGUI::IntializationError(_) => match message { + GuiState::InitializationInterrupted { .. } => match message { Message::InitNetwork(_) | Message::InitWalletMode(_) | Message::FromBackend(_, _, _) => unreachable!(), @@ -286,12 +398,12 @@ fn update(state: &mut MintlayerNodeGUI, message: Message) -> Task { } } -fn view(state: &MintlayerNodeGUI) -> Element { +fn view(state: &GuiState) -> Element { match state { - MintlayerNodeGUI::Initial => { + GuiState::Initial { .. } => { iced::widget::text("Loading fonts...".to_string()).size(32).into() } - MintlayerNodeGUI::SelectNetwork => { + GuiState::SelectNetwork { .. } => { let error_box = column![ iced::widget::text("Please choose the network you want to use".to_string()) .size(32), @@ -327,7 +439,7 @@ fn view(state: &MintlayerNodeGUI) -> Element { res.map(Message::InitNetwork) } - MintlayerNodeGUI::SelectWalletMode(_) => { + GuiState::SelectWalletMode { .. } => { let error_box = column![ iced::widget::text("Please choose the wallet mode".to_string()).size(32), row![ @@ -362,21 +474,39 @@ fn view(state: &MintlayerNodeGUI) -> Element { res.map(Message::InitWalletMode) } - MintlayerNodeGUI::Loading(_) => { + GuiState::Loading { .. } => { iced::widget::container(Spinner::new().width(Length::Fill).height(Length::Fill)).into() } - MintlayerNodeGUI::Loaded(_backend_sender, w) => w.view().map(Message::MainWindowMessage), + GuiState::Loaded { + backend_sender: _, + main_window, + } => main_window.view().map(Message::MainWindowMessage), - MintlayerNodeGUI::IntializationError(e) => { - let error_box = column![ - iced::widget::text("Mintlayer-core node initialization failed".to_string()) - .size(32), - iced::widget::text(e.to_string()).size(20), - iced::widget::button(text("Close")).on_press(()) - ] - .align_x(iced::Alignment::Center) - .spacing(5); + GuiState::InitializationInterrupted(reason) => { + let header_font_size = 32; + let text_font_size = 20; + + let error_box = match reason { + InitializationInterruptionReason::Failure(InitializationFailure { message }) => { + column![ + iced::widget::text("Mintlayer-core node initialization failed".to_string()) + .size(header_font_size), + iced::widget::text(message.to_string()).size(text_font_size) + ] + } + InitializationInterruptionReason::DataDirCleanedUp => { + column![ + iced::widget::text("Data directory is now clean").size(header_font_size), + iced::widget::text("Please restart the node without `--clean-data` flag") + .size(text_font_size) + ] + } + }; + let error_box = error_box + .extend([iced::widget::button(text("Close")).on_press(()).into()]) + .align_x(iced::Alignment::Center) + .spacing(5); let res: Element<()> = iced::widget::container(error_box).center(Length::Fill).into(); @@ -385,11 +515,11 @@ fn view(state: &MintlayerNodeGUI) -> Element { } } -fn theme(_state: &MintlayerNodeGUI) -> Theme { +fn theme(_state: &GuiState) -> Theme { Theme::Light } -fn subscription(_state: &MintlayerNodeGUI) -> Subscription { +fn subscription(_state: &GuiState) -> Subscription { iced::event::listen().map(Message::EventOccurred) } diff --git a/node-lib/src/lib.rs b/node-lib/src/lib.rs index 37d4b68ef..ef289c161 100644 --- a/node-lib/src/lib.rs +++ b/node-lib/src/lib.rs @@ -25,10 +25,11 @@ mod runner; pub type Error = anyhow::Error; use chainstate_launcher::ChainConfig; + pub use config_files::{ NodeConfigFile, NodeTypeConfigFile, RpcConfigFile, StorageBackendConfigFile, }; -pub use options::{Command, Options, RunOptions}; +pub use options::{Command, Options, OptionsWithResolvedCommand, RunOptions, TopLevelOptions}; pub use runner::{setup, NodeSetupResult}; pub fn default_rpc_config(chain_config: &ChainConfig) -> RpcConfigFile { diff --git a/node-lib/src/options.rs b/node-lib/src/options.rs index a6900b66c..2a71c8865 100644 --- a/node-lib/src/options.rs +++ b/node-lib/src/options.rs @@ -23,7 +23,12 @@ use std::{ }; use clap::{Args, Parser, Subcommand}; -use common::chain::config::{regtest_options::ChainConfigOptions, ChainType}; + +use chainstate_launcher::ChainConfig; +use common::chain::config::{ + regtest_options::{regtest_chain_config, ChainConfigOptions}, + ChainType, +}; use utils::{ clap_utils, default_data_dir::default_data_dir_common, root_user::ForceRunAsRootOptions, }; @@ -36,10 +41,52 @@ const CONFIG_NAME: &str = "config.toml"; /// Mintlayer node executable // Note: this struct is shared between different node executables, namely, node-daemon and node-gui, // so the env vars for both of them will use the same infix; this is intended. -#[derive(Parser, Debug)] +#[derive(Parser, Debug, Clone)] #[clap(mut_args(clap_utils::env_adder("NODE")))] #[clap(author, version, about)] pub struct Options { + #[clap(flatten)] + pub top_level: TopLevelOptions, + + #[clap(subcommand)] + pub command: Option, +} + +impl Options { + /// Constructs an instance by parsing the given arguments. + pub fn from_args + Clone>(args: impl IntoIterator) -> Self { + Parser::parse_from(args) + } + + /// Returns a different representation of `self`, where `command` is no longer optional. + pub fn with_resolved_command(self) -> OptionsWithResolvedCommand { + OptionsWithResolvedCommand { + top_level: self.top_level, + command: self.command.unwrap_or(Command::Mainnet(RunOptions::default())), + } + } +} + +/// Same as `Options`, but with non-optional `command`. +#[derive(Debug, Clone)] +pub struct OptionsWithResolvedCommand { + pub top_level: TopLevelOptions, + pub command: Command, +} + +impl OptionsWithResolvedCommand { + pub fn clean_data_option_set(&self) -> bool { + self.command.run_options().clean_data.unwrap_or(false) + } + + pub fn log_to_file_option_set(&self) -> bool { + self.top_level.log_to_file.is_some_and(|log_to_file| log_to_file) + } +} + +/// The top-level options +#[derive(Parser, Debug, Clone)] +pub struct TopLevelOptions { /// The path to the data directory. #[clap(short, long = "datadir")] pub data_dir: Option, @@ -49,8 +96,26 @@ pub struct Options { #[clap(long = "create-datadir-if-missing", value_name = "VAL")] pub create_data_dir_if_missing: Option, - #[clap(subcommand)] - pub command: Option, + /// Log to a file. + /// + /// If enabled, application logs will also be written to a file inside the "logs" subdirectory + /// of the data directory. + /// The file will be rotated based on size. + /// The log level used in this case doesn't depend on the RUST_LOG env variable and is always INFO. + /// + /// By default, the option is enabled for node-gui and disabled for node-daemon. + #[clap(long, action = clap::ArgAction::Set)] + pub log_to_file: Option, +} + +impl TopLevelOptions { + /// Returns a path to the config file + pub fn config_path(&self, chain_type: ChainType) -> PathBuf { + self.data_dir + .clone() + .unwrap_or_else(|| default_data_dir(chain_type)) + .join(CONFIG_NAME) + } } #[derive(Subcommand, Clone, Debug)] @@ -73,9 +138,36 @@ impl Command { Command::Regtest(regtest_options) => ®test_options.run_options, } } + + pub fn run_options_mut(&mut self) -> &mut RunOptions { + match self { + Command::Mainnet(run_options) | Command::Testnet(run_options) => run_options, + Command::Regtest(regtest_options) => &mut regtest_options.run_options, + } + } + + pub fn create_chain_config(&self) -> anyhow::Result { + let chain_config = match self { + Command::Mainnet(_) => common::chain::config::create_mainnet(), + Command::Testnet(_) => common::chain::config::create_testnet(), + Command::Regtest(regtest_options) => { + regtest_chain_config(®test_options.chain_config)? + } + }; + + Ok(chain_config) + } + + pub fn chain_type(&self) -> ChainType { + match self { + Command::Mainnet(_) => ChainType::Mainnet, + Command::Testnet(_) => ChainType::Testnet, + Command::Regtest(_) => ChainType::Regtest, + } + } } -#[derive(Args, Clone, Debug)] +#[derive(Args, Clone, Debug, Default)] pub struct RegtestOptions { #[clap(flatten)] pub run_options: RunOptions, @@ -85,14 +177,10 @@ pub struct RegtestOptions { #[derive(Args, Clone, Debug, Default)] pub struct RunOptions { - /// A flag that will clean the data dir before starting + /// If specified, the application will clean the data directory and exit immediately. #[clap(long, short, action = clap::ArgAction::SetTrue)] pub clean_data: Option, - /// Log to a file - #[clap(long, action = clap::ArgAction::Set)] - pub log_to_file: Option, - /// Minimum number of connected peers to enable block production. #[clap(long, value_name = "COUNT")] pub blockprod_min_peers_to_produce_blocks: Option, @@ -257,29 +345,6 @@ pub struct RunOptions { pub enable_chainstate_heavy_checks: Option, } -impl Options { - /// Constructs an instance by parsing the given arguments. - /// - /// The data directory is created as a side-effect of the invocation. - /// Process is terminated on error. - pub fn from_args + Clone>(args: impl IntoIterator) -> Self { - Parser::parse_from(args) - } - - /// Returns the data directory - pub fn data_dir(&self) -> &Option { - &self.data_dir - } - - /// Returns a path to the config file - pub fn config_path(&self, chain_type: ChainType) -> PathBuf { - self.data_dir - .clone() - .unwrap_or_else(|| default_data_dir(chain_type)) - .join(CONFIG_NAME) - } -} - pub fn default_data_dir(chain_type: ChainType) -> PathBuf { default_data_dir_common().join(chain_type.name()) } diff --git a/node-lib/src/runner.rs b/node-lib/src/runner.rs index 6c7ca176a..b30ab3de9 100644 --- a/node-lib/src/runner.rs +++ b/node-lib/src/runner.rs @@ -21,30 +21,26 @@ use std::{ sync::Arc, }; +use anyhow::{anyhow, Context, Result}; use file_rotate::{compression::Compression, suffix::AppendCount, ContentLimit, FileRotate}; -use anyhow::{anyhow, Context, Result}; use blockprod::rpc::BlockProductionRpcServer; -use chainstate_launcher::{ChainConfig, StorageBackendConfig}; -use common::chain::config::regtest_options::regtest_chain_config; - use chainstate::{rpc::ChainstateRpcServer, ChainstateError, InitializationError}; +use chainstate_launcher::{ChainConfig, StorageBackendConfig}; use common::chain::config::{assert_no_ignore_consensus_in_chain_config, ChainType}; use logging::log; - use mempool::rpc::MempoolRpcServer; - -use test_rpc_functions::{empty::make_empty_rpc_test_functions, rpc::RpcTestFunctionsRpcServer}; - use p2p::{error::P2pError, rpc::P2pRpcServer}; use rpc::rpc_creds::RpcCreds; -use test_rpc_functions::make_rpc_test_functions; +use test_rpc_functions::{ + empty::make_empty_rpc_test_functions, make_rpc_test_functions, rpc::RpcTestFunctionsRpcServer, +}; use crate::{ config_files::{NodeConfigFile, DEFAULT_P2P_NETWORKING_ENABLED, DEFAULT_RPC_ENABLED}, mock_time::set_mock_time, node_controller::NodeController, - options::{default_data_dir, Command, Options, RunOptions}, + options::{default_data_dir, OptionsWithResolvedCommand, RunOptions}, RpcConfigFile, }; @@ -235,20 +231,15 @@ async fn initialize( } /// Processes options and potentially runs the node. -pub async fn setup(options: Options) -> Result { - let command = options.command.clone().unwrap_or(Command::Mainnet(RunOptions::default())); - let run_options = command.run_options(); - let chain_config = match &command { - Command::Mainnet(_) => common::chain::config::create_mainnet(), - Command::Testnet(_) => common::chain::config::create_testnet(), - Command::Regtest(regtest_options) => regtest_chain_config(®test_options.chain_config)?, - }; +pub async fn setup(options: OptionsWithResolvedCommand) -> Result { + let run_options = options.command.run_options(); + let chain_config = options.command.create_chain_config()?; // Prepare data dir let data_dir = utils::default_data_dir::prepare_data_dir( || default_data_dir(*chain_config.chain_type()), - &options.data_dir, - options.create_data_dir_if_missing, + options.top_level.data_dir.as_ref(), + options.top_level.create_data_dir_if_missing, ) .expect("Failed to prepare data directory"); @@ -256,7 +247,7 @@ pub async fn setup(options: Options) -> Result { let lock_file = lock_data_dir(&data_dir)?; // Clean data dir if needed - if run_options.clean_data.unwrap_or(false) { + if options.clean_data_option_set() { clean_data_dir( &data_dir, std::slice::from_ref(&data_dir.join(LOCK_FILE_NAME).as_path()), @@ -267,7 +258,7 @@ pub async fn setup(options: Options) -> Result { let main_log_writer_settings = logging::default_writer_settings(); // Init logging - if run_options.log_to_file.is_some_and(|log_to_file| log_to_file) { + if options.log_to_file_option_set() { let log_file_name = std::env::current_exe().map_or_else( |_| DEFAULT_LOG_FILE_NAME.to_owned(), |exe| { @@ -303,7 +294,7 @@ pub async fn setup(options: Options) -> Result { logging::log::info!("Command line options: {options:?}"); let (manager, controller) = start( - &options.config_path(*chain_config.chain_type()), + &options.top_level.config_path(*chain_config.chain_type()), &data_dir, run_options, chain_config, diff --git a/node-lib/tests/cli.rs b/node-lib/tests/cli.rs index b47539cdb..0ed1303aa 100644 --- a/node-lib/tests/cli.rs +++ b/node-lib/tests/cli.rs @@ -161,7 +161,6 @@ fn read_config_override_values() { min_tx_relay_fee_rate: Some(min_tx_relay_fee_rate), force_allow_run_as_root_outer: Default::default(), enable_chainstate_heavy_checks: Some(enable_chainstate_heavy_checks), - log_to_file: Some(false), }; let config = NodeConfigFile::read(&chain_config, &config_path, &options).unwrap(); diff --git a/test/src/bin/test_node.rs b/test/src/bin/test_node.rs index f911a187d..7906136cc 100644 --- a/test/src/bin/test_node.rs +++ b/test/src/bin/test_node.rs @@ -18,7 +18,7 @@ use std::env; #[tokio::main] async fn main() -> Result<(), node_lib::Error> { let opts = node_lib::Options::from_args(env::args_os()); - let setup_result = node_lib::setup(opts).await?; + let setup_result = node_lib::setup(opts.with_resolved_command()).await?; let node = match setup_result { node_lib::NodeSetupResult::Node(node) => node, node_lib::NodeSetupResult::DataDirCleanedUp => { diff --git a/utils/src/default_data_dir.rs b/utils/src/default_data_dir.rs index 6e31ec512..a3c6c328e 100644 --- a/utils/src/default_data_dir.rs +++ b/utils/src/default_data_dir.rs @@ -59,7 +59,7 @@ pub enum PrepareDataDirError { /// Additionally, `create_data_dir_if_missing` allows to override the default behavior. pub fn prepare_data_dir PathBuf>( default_data_dir_getter: F, - datadir_path_opt: &Option, + datadir_path_opt: Option<&PathBuf>, create_data_dir_if_missing: Option, ) -> Result { let (data_dir, create_if_missing_default) = match datadir_path_opt { @@ -109,12 +109,12 @@ mod test { assert!(!supposed_default_dir.is_dir()); // The call must fail if create_data_dir_if_missing is explicitly set to false. - let _err = prepare_data_dir(default_data_dir_getter, &None, Some(false)).unwrap_err(); + let _err = prepare_data_dir(default_data_dir_getter, None, Some(false)).unwrap_err(); // With create_data_dir_if_missing equal to None or true, the call must succeed. - let returned_data_dir1 = prepare_data_dir(default_data_dir_getter, &None, None).unwrap(); + let returned_data_dir1 = prepare_data_dir(default_data_dir_getter, None, None).unwrap(); let returned_data_dir2 = - prepare_data_dir(default_data_dir_getter, &None, Some(true)).unwrap(); + prepare_data_dir(default_data_dir_getter, None, Some(true)).unwrap(); assert_eq!(returned_data_dir1, returned_data_dir2); // The default directory must be returned. @@ -137,11 +137,11 @@ mod test { test_file_data(&file_path, &file_data); // Now we prepare again, and ensure that our file is unchanged - let returned_data_dir1 = prepare_data_dir(default_data_dir_getter, &None, None).unwrap(); + let returned_data_dir1 = prepare_data_dir(default_data_dir_getter, None, None).unwrap(); let returned_data_dir2 = - prepare_data_dir(default_data_dir_getter, &None, Some(true)).unwrap(); + prepare_data_dir(default_data_dir_getter, None, Some(true)).unwrap(); let returned_data_dir3 = - prepare_data_dir(default_data_dir_getter, &None, Some(false)).unwrap(); + prepare_data_dir(default_data_dir_getter, None, Some(false)).unwrap(); assert_eq!(returned_data_dir1, returned_data_dir2); assert_eq!(returned_data_dir1, returned_data_dir3); @@ -167,15 +167,11 @@ mod test { assert!(!supposed_custom_dir.is_dir()); // The calls fail because the directory doesn't exist + let _err = prepare_data_dir(default_data_dir_getter, Some(&supposed_custom_dir), None) + .unwrap_err(); let _err = prepare_data_dir( default_data_dir_getter, - &Some(supposed_custom_dir.clone()), - None, - ) - .unwrap_err(); - let _err = prepare_data_dir( - default_data_dir_getter, - &Some(supposed_custom_dir.clone()), + Some(&supposed_custom_dir), Some(false), ) .unwrap_err(); @@ -187,7 +183,7 @@ mod test { // Now set create_data_dir_if_missing to true, the directory should be created. let returned_data_dir = prepare_data_dir( default_data_dir_getter, - &Some(supposed_custom_dir.clone()), + Some(&supposed_custom_dir), Some(true), ) .unwrap(); @@ -204,15 +200,11 @@ mod test { // Passing None or false for create_data_dir_if_missing now also works, because the directory // already exists. - let returned_data_dir1 = prepare_data_dir( - default_data_dir_getter, - &Some(supposed_custom_dir.clone()), - None, - ) - .unwrap(); + let returned_data_dir1 = + prepare_data_dir(default_data_dir_getter, Some(&supposed_custom_dir), None).unwrap(); let returned_data_dir2 = prepare_data_dir( default_data_dir_getter, - &Some(supposed_custom_dir.clone()), + Some(&supposed_custom_dir), Some(false), ) .unwrap(); @@ -239,21 +231,17 @@ mod test { test_file_data(&file_path, &file_data); // Now we prepare again, and ensure that our file is unchanged - let returned_data_dir1 = prepare_data_dir( - default_data_dir_getter, - &Some(supposed_custom_dir.clone()), - None, - ) - .unwrap(); + let returned_data_dir1 = + prepare_data_dir(default_data_dir_getter, Some(&supposed_custom_dir), None).unwrap(); let returned_data_dir2 = prepare_data_dir( default_data_dir_getter, - &Some(supposed_custom_dir.clone()), + Some(&supposed_custom_dir), Some(false), ) .unwrap(); let returned_data_dir3 = prepare_data_dir( default_data_dir_getter, - &Some(supposed_custom_dir.clone()), + Some(&supposed_custom_dir), Some(true), ) .unwrap();