diff --git a/Cargo.lock b/Cargo.lock index 069083d..a57b1c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2249,6 +2249,7 @@ dependencies = [ "tokio 1.29.1", "tokio-util 0.7.8", "toml 0.7.6", + "url", "wgpu", "winit", "zeroize", diff --git a/Cargo.toml b/Cargo.toml index 12f75aa..f927c1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ toml = "0.7.4" serde = "1" pnet = "0.33.0" zeroize = "1.6.0" +url = "2.4.0" # stratum server serde_derive = "1" diff --git a/locales/en.yml b/locales/en.yml index ff9c643..101459c 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -18,7 +18,11 @@ wallets: not_valid_word: Entered word is not valid create_phrase_desc: Safely write down and save your recovery phrase. restore_phrase_desc: Enter words from your saved recovery phrase. - setup_conn_desc: Choose wallet connection method. + setup_conn_desc: Choose how your wallet connects to the network. + conn_method: Connection method + ext_conn: 'External connections:' + add_node_url: Add node URL + invalid_url: Entered URL is invalid network: self: Network node: Integrated node @@ -150,6 +154,7 @@ modal: cancel: Cancel save: Save confirmation: Confirmation + add: Add modal_exit: description: Are you sure you want to quit the application? exit: Exit \ No newline at end of file diff --git a/locales/ru.yml b/locales/ru.yml index 4eda04a..0aedb39 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -18,7 +18,11 @@ wallets: not_valid_word: Введено недопустимое слово create_phrase_desc: Безопасно запишите и сохраните вашу фразу восстановления. restore_phrase_desc: Введите слова из вашей сохранённой фразы восстановления. - setup_conn_desc: Выберите способ подключения кошелька + setup_conn_desc: Выберите способ подключения вашего кошелька к сети. + conn_method: Способ подключения + ext_conn: 'Внешние подключения:' + add_node_url: Добавить URL узла + invalid_url: Введенный URL-адрес недействителен network: self: Сеть node: Встроенный узел @@ -150,6 +154,7 @@ modal: cancel: Отмена save: Сохранить confirmation: Подтверждение + add: Добавить modal_exit: description: Вы уверены, что хотите выйти из приложения? exit: Выход \ No newline at end of file diff --git a/src/gui/views/root.rs b/src/gui/views/root.rs index d502ae2..4d09174 100644 --- a/src/gui/views/root.rs +++ b/src/gui/views/root.rs @@ -20,7 +20,7 @@ use lazy_static::lazy_static; use crate::gui::Colors; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Wallets, Modal, ModalContainer, Network, View}; +use crate::gui::views::{WalletsContent, Modal, ModalContainer, Network, View}; use crate::node::Node; lazy_static! { @@ -32,8 +32,8 @@ lazy_static! { pub struct Root { /// Side panel [`Network`] content. network: Network, - /// Central panel [`Wallets`] content. - wallets: Wallets, + /// Central panel [`WalletsContent`] content. + wallets: WalletsContent, /// Check if app exit is allowed on close event of [`eframe::App`] implementation. pub(crate) exit_allowed: bool, @@ -52,7 +52,7 @@ impl Default for Root { let exit_allowed = os == OperatingSystem::Android || os == OperatingSystem::IOS; Self { network: Network::default(), - wallets: Wallets::default(), + wallets: WalletsContent::default(), exit_allowed, show_exit_progress: false, allowed_modal_ids: vec![ @@ -117,7 +117,7 @@ impl Root { (is_panel_open, panel_width) } - /// Check if ui can show [`Network`] and [`Wallets`] at same time. + /// Check if ui can show [`Network`] and [`WalletsContent`] at same time. pub fn is_dual_panel_mode(frame: &mut eframe::Frame) -> bool { let w = frame.info().window_info.size.x; let h = frame.info().window_info.size.y; diff --git a/src/gui/views/wallets/creation/creation.rs b/src/gui/views/wallets/creation/creation.rs index 72d394b..5f32558 100644 --- a/src/gui/views/wallets/creation/creation.rs +++ b/src/gui/views/wallets/creation/creation.rs @@ -14,14 +14,15 @@ use egui::{Margin, RichText, TextStyle, vec2, Widget}; use egui_extras::{RetainedImage, Size, StripBuilder}; -use crate::built_info; +use crate::built_info; use crate::gui::Colors; use crate::gui::icons::{CHECK, EYE, EYE_SLASH, PLUS_CIRCLE, SHARE_FAT}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, ModalPosition, View}; -use crate::gui::views::wallets::creation::{ConnectionSetup, MnemonicSetup}; +use crate::gui::views::wallets::creation::MnemonicSetup; use crate::gui::views::wallets::creation::types::{PhraseMode, Step}; +use crate::gui::views::wallets::setup::ConnectionSetup; /// Wallet creation content. pub struct WalletCreation { @@ -51,8 +52,8 @@ impl Default for WalletCreation { Self { step: None, modal_just_opened: true, - name_edit: "".to_string(), - pass_edit: "".to_string(), + name_edit: String::from(""), + pass_edit: String::from(""), hide_pass: true, mnemonic_setup: MnemonicSetup::default(), network_setup: ConnectionSetup::default(), @@ -89,17 +90,16 @@ impl WalletCreation { let (step_text, step_available) = match step { Step::EnterMnemonic => { let mode = &self.mnemonic_setup.mnemonic.mode; - let size_value = self.mnemonic_setup.mnemonic.size.value(); let text = if mode == &PhraseMode::Generate { - t!("wallets.create_phrase_desc", "number" => size_value) + t!("wallets.create_phrase_desc") } else { - t!("wallets.restore_phrase_desc", "number" => size_value) + t!("wallets.restore_phrase_desc") }; let available = !self .mnemonic_setup .mnemonic .words - .contains(&"".to_string()); + .contains(&String::from("")); (text, available) } Step::ConfirmMnemonic => { @@ -108,7 +108,7 @@ impl WalletCreation { .mnemonic_setup .mnemonic .confirm_words - .contains(&"".to_string()); + .contains(&String::from("")); (text, available) }, Step::SetupConnection => (t!("wallets.setup_conn_desc"), true) @@ -190,13 +190,9 @@ impl WalletCreation { } Some(step) => { match step { - Step::EnterMnemonic => { - self.mnemonic_setup.ui(ui); - } - Step::ConfirmMnemonic => { - self.mnemonic_setup.confirm_ui(ui); - } - Step::SetupConnection => {} + Step::EnterMnemonic => self.mnemonic_setup.ui(ui), + Step::ConfirmMnemonic => self.mnemonic_setup.confirm_ui(ui), + Step::SetupConnection => self.network_setup.ui(ui, cb) } } } @@ -216,8 +212,8 @@ impl WalletCreation { Step::EnterMnemonic => { // Clear values if it needs to go back on first step. self.step = None; - self.name_edit = "".to_string(); - self.pass_edit = "".to_string(); + self.name_edit = String::from(""); + self.pass_edit = String::from(""); self.mnemonic_setup.reset(); } Step::ConfirmMnemonic => self.step = Some(Step::EnterMnemonic), @@ -255,8 +251,8 @@ impl WalletCreation { // Reset modal values. self.hide_pass = false; self.modal_just_opened = true; - self.name_edit = "".to_string(); - self.pass_edit = "".to_string(); + self.name_edit = String::from(""); + self.pass_edit = String::from(""); // Show modal. Modal::new(Self::NAME_PASS_MODAL) .position(ModalPosition::CenterTop) diff --git a/src/gui/views/wallets/creation/mnemonic.rs b/src/gui/views/wallets/creation/mnemonic.rs index 37f12d0..82b4ea0 100644 --- a/src/gui/views/wallets/creation/mnemonic.rs +++ b/src/gui/views/wallets/creation/mnemonic.rs @@ -38,7 +38,7 @@ impl Default for MnemonicSetup { Self { mnemonic: Mnemonic::default(), word_num_edit: 0, - word_edit: "".to_string(), + word_edit: String::from(""), valid_word_edit: true } } @@ -192,12 +192,12 @@ impl MnemonicSetup { } /// Draw word list item for current mode. - fn word_item_ui(&mut self, ui: &mut egui::Ui, word_number: usize, word: &String, edit: bool) { + fn word_item_ui(&mut self, ui: &mut egui::Ui, num: usize, word: &String, edit: bool) { if edit { ui.add_space(6.0); View::button(ui, PENCIL.to_string(), Colors::BUTTON, || { // Setup modal values. - self.word_num_edit = word_number; + self.word_num_edit = num; self.word_edit = word.clone(); self.valid_word_edit = true; // Show word edit modal. @@ -206,12 +206,12 @@ impl MnemonicSetup { .title(t!("wallets.saved_phrase")) .show(); }); - ui.label(RichText::new(format!("#{} {}", word_number, word)) + ui.label(RichText::new(format!("#{} {}", num, word)) .size(17.0) .color(Colors::BLACK)); } else { ui.add_space(12.0); - let text = format!("#{} {}", word_number, word); + let text = format!("#{} {}", num, word); ui.label(RichText::new(text).size(17.0).color(Colors::BLACK)); } } @@ -274,14 +274,18 @@ impl MnemonicSetup { self.valid_word_edit = false; return; } + self.valid_word_edit = true; + // Select list where to save word. let words = match self.mnemonic.mode { PhraseMode::Generate => &mut self.mnemonic.confirm_words, PhraseMode::Import => &mut self.mnemonic.words }; + // Save word at list. words.remove(word_index); words.insert(word_index, self.word_edit.clone()); + // Close modal or go to next word to edit. let close_modal = words.len() == self.word_num_edit || !words.get(self.word_num_edit).unwrap().is_empty(); @@ -290,7 +294,7 @@ impl MnemonicSetup { modal.close(); } else { self.word_num_edit += 1; - self.word_edit = "".to_string(); + self.word_edit = String::from(""); } }; // Call save on Enter key press. diff --git a/src/gui/views/wallets/creation/mod.rs b/src/gui/views/wallets/creation/mod.rs index bc1b941..05ebd27 100644 --- a/src/gui/views/wallets/creation/mod.rs +++ b/src/gui/views/wallets/creation/mod.rs @@ -15,9 +15,6 @@ mod mnemonic; pub use mnemonic::MnemonicSetup; -mod connection; -pub use connection::ConnectionSetup; - mod creation; pub use creation::WalletCreation; diff --git a/src/gui/views/wallets/creation/types.rs b/src/gui/views/wallets/creation/types.rs index a120d51..212ef0d 100644 --- a/src/gui/views/wallets/creation/types.rs +++ b/src/gui/views/wallets/creation/types.rs @@ -14,6 +14,7 @@ use grin_keychain::mnemonic::{from_entropy, search}; use rand::{Rng, thread_rng}; +use zeroize::{Zeroize, ZeroizeOnDrop}; /// Wallet creation step. #[derive(PartialEq)] @@ -27,7 +28,8 @@ pub enum Step { } /// Mnemonic phrase setup mode. -#[derive(PartialEq, Clone)] +/// Will be completely cleaned from memory on drop. +#[derive(PartialEq, Clone, Zeroize, ZeroizeOnDrop)] pub enum PhraseMode { /// Generate new mnemonic phrase. Generate, @@ -36,7 +38,8 @@ pub enum PhraseMode { } /// Mnemonic phrase size based on words count. -#[derive(PartialEq, Clone)] +/// Will be completely cleaned from memory on drop. +#[derive(PartialEq, Clone, Zeroize, ZeroizeOnDrop)] pub enum PhraseSize { Words12, Words15, Words18, Words21, Words24 } impl PhraseSize { @@ -72,6 +75,8 @@ impl PhraseSize { } /// Mnemonic phrase container. +/// Will be completely cleaned from memory on drop. +#[derive(Zeroize, ZeroizeOnDrop)] pub struct Mnemonic { /// Phrase setup mode. pub(crate) mode: PhraseMode, @@ -130,7 +135,7 @@ impl Mnemonic { } from_entropy(&entropy).unwrap() .split(" ") - .map(|s| s.to_string()) + .map(|s| String::from(s)) .collect::>() }, PhraseMode::Import => { @@ -143,7 +148,7 @@ impl Mnemonic { fn empty_words(size: &PhraseSize) -> Vec { let mut words = Vec::with_capacity(size.value()); for _ in 0..size.value() { - words.push("".to_string()) + words.push(String::from("")) } words } diff --git a/src/gui/views/wallets/mod.rs b/src/gui/views/wallets/mod.rs index 3c3a46c..3ca8cb0 100644 --- a/src/gui/views/wallets/mod.rs +++ b/src/gui/views/wallets/mod.rs @@ -12,8 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -mod wallets; mod creation; mod wallet; +mod setup; +mod wallets; pub use wallets::*; \ No newline at end of file diff --git a/src/gui/views/wallets/setup/connection.rs b/src/gui/views/wallets/setup/connection.rs new file mode 100644 index 0000000..4d339be --- /dev/null +++ b/src/gui/views/wallets/setup/connection.rs @@ -0,0 +1,165 @@ +// Copyright 2023 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use egui::{Id, RichText, ScrollArea, TextStyle, Widget}; +use url::Url; + +use crate::AppConfig; +use crate::gui::Colors; +use crate::gui::icons::{GLOBE, GLOBE_SIMPLE}; +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::{Modal, View}; +use crate::gui::views::wallets::setup::ConnectionMethod; + +/// Wallet node connection method setup content. +pub struct ConnectionSetup { + /// Selected connection method. + method: ConnectionMethod, + + /// External node connection URL value for [`Modal`]. + ext_node_url_edit: String, + /// Flag to show URL format error. + ext_node_url_error: bool, +} + +impl Default for ConnectionSetup { + fn default() -> Self { + Self { + method: ConnectionMethod::Integrated, + ext_node_url_edit: "".to_string(), + ext_node_url_error: false + } + } +} + +impl ConnectionSetup { + /// External node connection [`Modal`] identifier. + pub const ADD_CONNECTION_URL_MODAL: &'static str = "add_connection_url_modal"; + + //TODO: Setup for provided wallet + // pub fn new() -> Self { + // Self { method: ConnectionMethod::Integrated } + // } + + pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + ScrollArea::vertical() + .id_source("wallet_connection_setup") + .auto_shrink([false; 2]) + .show(ui, |ui| { + View::sub_title(ui, format!("{} {}", GLOBE, t!("wallets.conn_method"))); + View::horizontal_line(ui, Colors::STROKE); + ui.add_space(4.0); + + ui.vertical_centered(|ui| { + // Show integrated node selection. + ui.add_space(6.0); + View::radio_value(ui, + &mut self.method, + ConnectionMethod::Integrated, + t!("network.node")); + + ui.add_space(10.0); + ui.label(RichText::new(t!("wallets.ext_conn")).size(16.0).color(Colors::GRAY)); + ui.add_space(6.0); + + // Show button to add new external node connection. + let add_node_text = format!("{} {}", GLOBE_SIMPLE, t!("wallets.add_node_url")); + View::button(ui, add_node_text, Colors::GOLD, || { + // Setup values for Modal. + self.ext_node_url_edit = "".to_string(); + self.ext_node_url_error = false; + // Show modal. + Modal::new(Self::ADD_CONNECTION_URL_MODAL) + .title(t!("wallets.ext_conn")) + .show(); + cb.show_keyboard(); + }); + ui.add_space(12.0); + + // Show external nodes URLs selection. + for conn in AppConfig::external_nodes_urls() { + View::radio_value(ui, + &mut self.method, + ConnectionMethod::External(conn.clone()), + conn); + ui.add_space(12.0); + } + }); + }); + } + + /// Draw external connections setup. + fn external_conn_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + + } + + /// Draw modal content. + pub fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + // Draw external node URL text edit. + let text_edit_resp = egui::TextEdit::singleline(&mut self.ext_node_url_edit) + .id(Id::from(modal.id)) + .font(TextStyle::Heading) + .desired_width(ui.available_width()) + .cursor_at_end(true) + .ui(ui); + text_edit_resp.request_focus(); + if text_edit_resp.clicked() { + cb.show_keyboard(); + } + + // Show error when specified URL is not valid. + if self.ext_node_url_error { + ui.add_space(12.0); + ui.label(RichText::new(t!("wallets.invalid_url")) + .size(17.0) + .color(Colors::RED)); + } + ui.add_space(12.0); + }); + + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + // Add button callback. + let on_add = || { + let error = Url::parse(self.ext_node_url_edit.as_str()).is_err(); + self.ext_node_url_error = error; + if !error { + AppConfig::add_external_node_url(self.ext_node_url_edit.clone()); + // Close modal. + cb.hide_keyboard(); + modal.close(); + } + }; + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.cancel"), Colors::WHITE, || { + // Close modal. + cb.hide_keyboard(); + modal.close(); + }); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.add"), Colors::WHITE, on_add); + }); + }); + ui.add_space(6.0); + }); + } +} \ No newline at end of file diff --git a/src/gui/views/wallets/creation/connection.rs b/src/gui/views/wallets/setup/mod.rs similarity index 74% rename from src/gui/views/wallets/creation/connection.rs rename to src/gui/views/wallets/setup/mod.rs index cabc67a..8ded50d 100644 --- a/src/gui/views/wallets/creation/connection.rs +++ b/src/gui/views/wallets/setup/mod.rs @@ -12,15 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::gui::platform::PlatformCallbacks; +mod connection; +pub use connection::ConnectionSetup; -#[derive(Default)] -pub struct ConnectionSetup { - -} - -impl ConnectionSetup { - pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - - } -} \ No newline at end of file +mod types; +pub use types::*; \ No newline at end of file diff --git a/src/gui/views/wallets/setup/types.rs b/src/gui/views/wallets/setup/types.rs new file mode 100644 index 0000000..fa29124 --- /dev/null +++ b/src/gui/views/wallets/setup/types.rs @@ -0,0 +1,22 @@ +// Copyright 2023 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Wallet node connection method type. +#[derive(PartialEq)] +pub enum ConnectionMethod { + /// Integrated node connection. + Integrated, + /// External node connection. + External(String) +} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet.rs b/src/gui/views/wallets/wallet.rs index 08cc3c4..731f091 100644 --- a/src/gui/views/wallets/wallet.rs +++ b/src/gui/views/wallets/wallet.rs @@ -17,16 +17,17 @@ use egui::Margin; use crate::gui::Colors; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::View; +use crate::wallet::Wallet; /// Selected wallet list item content. pub struct WalletContent { /// Current wallet instance. - item: String + wallet: Wallet } impl WalletContent { - fn new(item: String) -> Self { - Self { item } + fn new(wallet: Wallet) -> Self { + Self { wallet } } } diff --git a/src/gui/views/wallets/wallets.rs b/src/gui/views/wallets/wallets.rs index e6393cb..724016d 100644 --- a/src/gui/views/wallets/wallets.rs +++ b/src/gui/views/wallets/wallets.rs @@ -21,12 +21,14 @@ use crate::gui::icons::{ARROW_LEFT, GEAR, GLOBE, PLUS}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, ModalContainer, Root, TitlePanel, TitleType, View}; use crate::gui::views::wallets::creation::{MnemonicSetup, WalletCreation}; +use crate::gui::views::wallets::setup::ConnectionSetup; use crate::gui::views::wallets::wallet::WalletContent; +use crate::wallet::{Wallet, WalletList}; /// Wallets content. -pub struct Wallets { +pub struct WalletsContent { /// List of wallets. - list: Vec, + list: Vec, /// Selected list item content. item_content: Option, @@ -37,28 +39,28 @@ pub struct Wallets { modal_ids: Vec<&'static str> } -impl Default for Wallets { +impl Default for WalletsContent { fn default() -> Self { - //TODO load list. Self { - list: vec![], + list: WalletList::list(), item_content: None, creation_content: WalletCreation::default(), modal_ids: vec![ WalletCreation::NAME_PASS_MODAL, - MnemonicSetup::WORD_INPUT_MODAL + MnemonicSetup::WORD_INPUT_MODAL, + ConnectionSetup::ADD_CONNECTION_URL_MODAL ] } } } -impl ModalContainer for Wallets { +impl ModalContainer for WalletsContent { fn modal_ids(&self) -> &Vec<&'static str> { &self.modal_ids } } -impl Wallets { +impl WalletsContent { pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { // Show modal content for current ui container. if self.can_draw_modal() { @@ -70,6 +72,9 @@ impl Wallets { MnemonicSetup::WORD_INPUT_MODAL => { self.creation_content.mnemonic_setup.modal_ui(ui, modal, cb); } + ConnectionSetup::ADD_CONNECTION_URL_MODAL => { + self.creation_content.network_setup.modal_ui(ui, modal, cb); + } _ => {} } }); @@ -176,7 +181,7 @@ impl Wallets { } } - /// Check if ui can show [`Wallets`] list and [`WalletContent`] content at same time. + /// Check if ui can show [`WalletsContent`] list and [`WalletContent`] content at same time. fn is_dual_panel_mode(ui: &mut egui::Ui, frame: &mut eframe::Frame) -> bool { let dual_panel_root = Root::is_dual_panel_mode(frame); let max_width = ui.available_width(); diff --git a/src/lib.rs b/src/lib.rs index 5735930..74d00e9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -93,13 +93,13 @@ pub fn start(mut options: eframe::NativeOptions, app_creator: eframe::AppCreator options.default_theme = eframe::Theme::Light; options.renderer = eframe::Renderer::Wgpu; options.initial_window_size = Some(egui::Vec2::new(1200.0, 720.0)); - + // Setup translations. setup_i18n(); - + // Start integrated node if needed. if Settings::app_config_to_read().auto_start_node { Node::start(); } - + // Launch graphical interface. let _ = eframe::run_native("Grim", options, app_creator); } diff --git a/src/node/config.rs b/src/node/config.rs index c2ea0c2..f639036 100644 --- a/src/node/config.rs +++ b/src/node/config.rs @@ -45,34 +45,11 @@ impl PeersConfig { /// Save peers config to the file. pub fn save(&self) { let chain_type = AppConfig::chain_type(); - let config_path = Settings::get_config_path(Self::FILE_NAME, Some(&chain_type)); + let chain_name = Some(chain_type.shortname()); + let config_path = Settings::get_config_path(Self::FILE_NAME, chain_name); Settings::write_to_file(self, config_path); } - /// Save seed peer. - pub fn save_seed(&mut self, peer: String) { - self.seeds.insert(self.seeds.len(), peer); - self.save(); - } - - /// Save allowed peer. - pub fn save_allowed(&mut self, peer: String) { - self.allowed.insert(self.allowed.len(), peer); - self.save(); - } - - /// Save denied peer. - pub fn save_denied(&mut self, peer: String) { - self.denied.insert(self.denied.len(), peer); - self.save(); - } - - /// Save preferred peer. - pub fn save_preferred(&mut self, peer: String) { - self.preferred.insert(self.preferred.len(), peer); - self.save(); - } - /// Convert string to [`PeerAddr`] if address is in correct format (`host:port`) and available. pub fn peer_to_addr(peer: String) -> Option { match SocketAddr::from_str(peer.as_str()) { @@ -169,7 +146,8 @@ impl NodeConfig { // Initialize peers config. let peers_config = { - let path = Settings::get_config_path(PeersConfig::FILE_NAME, Some(chain_type)); + let chain_name = Some(chain_type.shortname()); + let path = Settings::get_config_path(PeersConfig::FILE_NAME, chain_name); let config = Settings::read_from_file::(path.clone()); if !path.exists() || config.is_err() { Self::save_default_peers_config(chain_type) @@ -180,7 +158,8 @@ impl NodeConfig { // Initialize node config. let node_config = { - let path = Settings::get_config_path(SERVER_CONFIG_FILE_NAME, Some(chain_type)); + let chain_name = Some(chain_type.shortname()); + let path = Settings::get_config_path(SERVER_CONFIG_FILE_NAME, chain_name); let config = Settings::read_from_file::(path.clone()); if !path.exists() || config.is_err() { Self::save_default_node_server_config(chain_type) @@ -194,9 +173,10 @@ impl NodeConfig { /// Save default node config for specified [`ChainTypes`]. fn save_default_node_server_config(chain_type: &ChainTypes) -> ConfigMembers { - let path = Settings::get_config_path(SERVER_CONFIG_FILE_NAME, Some(chain_type)); + let chain_name = Some(chain_type.shortname()); + let path = Settings::get_config_path(SERVER_CONFIG_FILE_NAME, chain_name.clone()); let mut default_config = GlobalConfig::for_chain(chain_type); - default_config.update_paths(&Settings::get_working_path(Some(chain_type))); + default_config.update_paths(&Settings::get_base_path(chain_name)); let config = default_config.members.unwrap(); Settings::write_to_file(&config, path); config @@ -204,7 +184,8 @@ impl NodeConfig { /// Save default peers config for specified [`ChainTypes`]. fn save_default_peers_config(chain_type: &ChainTypes) -> PeersConfig { - let path = Settings::get_config_path(PeersConfig::FILE_NAME, Some(chain_type)); + let chain_name = Some(chain_type.shortname()); + let path = Settings::get_config_path(PeersConfig::FILE_NAME, chain_name); let config = PeersConfig::default(); Settings::write_to_file(&config, path); config @@ -212,10 +193,8 @@ impl NodeConfig { /// Save node config to the file. pub fn save(&self) { - let config_path = Settings::get_config_path( - SERVER_CONFIG_FILE_NAME, - Some(&self.node.server.chain_type) - ); + let chain_name = Some(self.node.server.chain_type.shortname()); + let config_path = Settings::get_config_path(SERVER_CONFIG_FILE_NAME, chain_name); Settings::write_to_file(&self.node, config_path); } @@ -256,7 +235,7 @@ impl NodeConfig { /// Get path for secret file. fn get_secret_path(chain_type: &ChainTypes, secret_file_name: &str) -> PathBuf { - let grin_path = Settings::get_working_path(Some(chain_type)); + let grin_path = Settings::get_base_path(Some(chain_type.shortname())); let mut api_secret_path = grin_path; api_secret_path.push(secret_file_name); api_secret_path @@ -446,7 +425,7 @@ impl NodeConfig { /// Get API server IP and port. pub fn get_api_ip_port() -> (String, String) { - let saved_addr = Self::get_api_address().as_str(); + let saved_addr = Self::get_api_address(); let (addr, port) = saved_addr.split_once(":").unwrap(); (addr.into(), port.into()) } diff --git a/src/node/mod.rs b/src/node/mod.rs index 9074fc2..81266f3 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -12,12 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +mod stratum; +mod mine_block; + mod node; pub use node::Node; mod config; - -mod stratum; -mod mine_block; - -pub use config::{NodeConfig, PeersConfig}; \ No newline at end of file +pub use config::*; \ No newline at end of file diff --git a/src/settings.rs b/src/settings.rs index ad9e3ea..fe82ee8 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -24,21 +24,28 @@ use serde::{Deserialize, Serialize}; use serde::de::DeserializeOwned; use crate::node::NodeConfig; +use crate::wallet::WalletList; lazy_static! { /// Static settings state to be accessible globally. static ref SETTINGS_STATE: Arc = Arc::new(Settings::init()); } +/// Application configuration file name. const APP_CONFIG_FILE_NAME: &'static str = "app.toml"; +/// Default external node URL. +const DEFAULT_EXTERNAL_NODE_URL: &'static str = "https://grinnnode.live:3413"; + /// Common application settings. #[derive(Serialize, Deserialize)] pub struct AppConfig { /// Run node server on startup. pub auto_start_node: bool, /// Chain type for node and wallets. - chain_type: ChainTypes + chain_type: ChainTypes, + /// URLs of external nodes for wallets. + external_nodes_urls: Vec } impl Default for AppConfig { @@ -46,12 +53,15 @@ impl Default for AppConfig { Self { auto_start_node: false, chain_type: ChainTypes::default(), + external_nodes_urls: vec![ + DEFAULT_EXTERNAL_NODE_URL.to_string() + ], } } } impl AppConfig { - /// Initialize application config from the disk. + /// Initialize application config from the file. pub fn init() -> Self { let path = Settings::get_config_path(APP_CONFIG_FILE_NAME, None); let parsed = Settings::read_from_file::(path.clone()); @@ -64,7 +74,7 @@ impl AppConfig { } } - /// Save app config to disk. + /// Save app config to file. pub fn save(&self) { Settings::write_to_file(self, Settings::get_config_path(APP_CONFIG_FILE_NAME, None)); } @@ -82,6 +92,9 @@ impl AppConfig { let node_config = NodeConfig::for_chain_type(chain_type); w_node_config.node = node_config.node; w_node_config.peers = node_config.peers; + + // Reload wallets. + WalletList::reload(chain_type); } } @@ -104,14 +117,31 @@ impl AppConfig { w_app_config.auto_start_node = !autostart; w_app_config.save(); } + + /// Get external nodes URLs. + pub fn external_nodes_urls() -> Vec { + let r_config = Settings::app_config_to_read(); + r_config.external_nodes_urls.clone() + } + + /// Add external node URL. + pub fn add_external_node_url(address: String) { + let mut w_config = Settings::app_config_to_update(); + w_config.external_nodes_urls.insert(0, address); + w_config.save(); + } + } -const WORKING_DIRECTORY_NAME: &'static str = ".grim"; +/// Main application directory name. +const MAIN_DIR_NAME: &'static str = ".grim"; -/// Provides access to app and node configs. +/// Provides access to application, integrated node and wallets configs. pub struct Settings { + /// Application config instance. app_config: Arc>, - node_config: Arc> + /// Integrated node config instance. + node_config: Arc>, } impl Settings { @@ -120,7 +150,7 @@ impl Settings { let app_config = AppConfig::init(); Self { node_config: Arc::new(RwLock::new(NodeConfig::for_chain_type(&app_config.chain_type))), - app_config: Arc::new(RwLock::new(app_config)) + app_config: Arc::new(RwLock::new(app_config)), } } @@ -144,16 +174,16 @@ impl Settings { SETTINGS_STATE.app_config.write().unwrap() } - /// Get working directory path for the application. - pub fn get_working_path(chain_type: Option<&ChainTypes>) -> PathBuf { + /// Get base directory path for config. + pub fn get_base_path(sub_dir: Option) -> PathBuf { // Check if dir exists. let mut path = match dirs::home_dir() { Some(p) => p, None => PathBuf::new(), }; - path.push(WORKING_DIRECTORY_NAME); - if chain_type.is_some() { - path.push(chain_type.unwrap().shortname()); + path.push(MAIN_DIR_NAME); + if sub_dir.is_some() { + path.push(sub_dir.unwrap()); } // Create if the default path doesn't exist. if !path.exists() { @@ -162,15 +192,14 @@ impl Settings { path } - /// Get config file path from provided name and [`ChainTypes`] if needed. - pub fn get_config_path(config_name: &str, chain_type: Option<&ChainTypes>) -> PathBuf { - let main_path = Self::get_working_path(chain_type); - let mut settings_path = main_path.clone(); + /// Get config file path from provided name and sub-directory if needed. + pub fn get_config_path(config_name: &str, sub_dir: Option) -> PathBuf { + let mut settings_path = Self::get_base_path(sub_dir); settings_path.push(config_name); settings_path } - /// Read config from a file + /// Read config from the file. pub fn read_from_file(config_path: PathBuf) -> Result { let file_content = fs::read_to_string(config_path.clone())?; let parsed = toml::from_str::(file_content.as_str()); @@ -185,7 +214,7 @@ impl Settings { } } - /// Write config to a file + /// Write config to the file. pub fn write_to_file(config: &T, path: PathBuf) { let conf_out = toml::to_string(config).unwrap(); let mut file = File::create(path.to_str().unwrap()).unwrap(); diff --git a/src/wallet/config.rs b/src/wallet/config.rs new file mode 100644 index 0000000..aa17d79 --- /dev/null +++ b/src/wallet/config.rs @@ -0,0 +1,95 @@ +// Copyright 2023 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::ffi::OsString; +use std::path::PathBuf; + +use serde_derive::{Deserialize, Serialize}; +use crate::{AppConfig, Settings}; +use crate::wallet::WalletList; + +/// Wallet configuration. +#[derive(Serialize, Deserialize, Clone)] +pub struct WalletConfig { + /// Identifier for a wallet. + id: OsString, + /// Readable wallet name. + name: String, + /// External node connection URL. + external_node_url: Option, +} + +/// Wallet configuration file name. +const CONFIG_FILE_NAME: &'static str = "grim-wallet.toml"; + +impl WalletConfig { + /// Create wallet config. + pub fn create(id: OsString, name: String) -> WalletConfig { + let config_path = Self::get_config_path(&id); + let config = WalletConfig { + id, + name, + external_node_url: None, + }; + Settings::write_to_file(&config, config_path); + config + } + + /// Load config from provided wallet dir. + pub fn load(wallet_dir: PathBuf) -> Option { + let mut config_path: PathBuf = wallet_dir.clone(); + config_path.push(CONFIG_FILE_NAME); + if let Ok(config) = Settings::read_from_file::(config_path) { + return Some(config) + } + None + } + + /// Get config file path for provided wallet identifier. + fn get_config_path(id: &OsString) -> PathBuf { + let chain_type = AppConfig::chain_type(); + let mut config_path = WalletList::get_wallets_base_dir(&chain_type); + config_path.push(id); + config_path.push(CONFIG_FILE_NAME); + config_path + } + + /// Save wallet config. + fn save(&self) { + let config_path = Self::get_config_path(&self.id); + Settings::write_to_file(self, config_path); + } + + /// Get readable wallet name. + pub fn get_name(&self) -> &String { + &self.name + } + + /// Set readable wallet name. + pub fn set_name(&mut self, name: String) { + self.name = name; + self.save(); + } + + /// Get external node connection URL. + pub fn get_external_node_url(&self) -> &Option { + &self.external_node_url + } + + /// Set external node connection URL. + pub fn set_external_node_url(&mut self, url: Option) { + self.external_node_url = url; + self.save(); + } +} \ No newline at end of file diff --git a/src/wallet/list.rs b/src/wallet/list.rs new file mode 100644 index 0000000..5dc115a --- /dev/null +++ b/src/wallet/list.rs @@ -0,0 +1,81 @@ +// Copyright 2023 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fs; +use std::path::PathBuf; +use std::sync::{Arc, RwLock}; +use grin_core::global::ChainTypes; + +use lazy_static::lazy_static; + +use crate::{AppConfig, Settings}; +use crate::wallet::Wallet; + +lazy_static! { + /// Global wallets state. + static ref WALLETS_STATE: Arc> = Arc::new(RwLock::new(WalletList::load())); +} + +/// List of created wallets. +pub struct WalletList { + list: Vec +} + +/// Base wallets directory name. +pub const BASE_DIR_NAME: &'static str = "wallets"; + +impl WalletList { + /// Load list of wallets. + fn load() -> Self { + Self { list: Self::load_wallets(&AppConfig::chain_type()) } + } + + /// Load wallets for provided [`ChainType`]. + fn load_wallets(chain_type: &ChainTypes) -> Vec { + let mut wallets = Vec::new(); + let wallets_dir = Self::get_wallets_base_dir(chain_type); + // Load wallets from directory. + for dir in wallets_dir.read_dir().unwrap() { + let wallet = Wallet::load(dir.unwrap().path()); + if let Some(w) = wallet { + wallets.push(w); + } + continue; + } + wallets + } + + /// Get wallets base directory for provided [`ChainTypes`]. + pub fn get_wallets_base_dir(chain_type: &ChainTypes) -> PathBuf { + let mut wallets_path = Settings::get_base_path(Some(chain_type.shortname())); + wallets_path.push(BASE_DIR_NAME); + // Create wallets directory if it doesn't exist. + if !wallets_path.exists() { + let _ = fs::create_dir_all(wallets_path.clone()); + } + wallets_path + } + + /// Get list of wallets. + pub fn list() -> Vec { + let r_state = WALLETS_STATE.read().unwrap(); + r_state.list.clone() + } + + /// Reload list of wallets for provided [`ChainTypes`]. + pub fn reload(chain_type: &ChainTypes) { + let mut w_state = WALLETS_STATE.write().unwrap(); + w_state.list = Self::load_wallets(chain_type); + } +} diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 2cbbf0d..ed2f38b 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -13,5 +13,10 @@ // limitations under the License. mod wallet; +pub use wallet::Wallet; -// pub use self::wallet::{init, init_from_seed}; +mod config; +pub use config::*; + +mod list; +pub use list::WalletList; diff --git a/src/wallet/wallet.rs b/src/wallet/wallet.rs index 6ddbed9..e8ce48d 100644 --- a/src/wallet/wallet.rs +++ b/src/wallet/wallet.rs @@ -12,3 +12,51 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::ffi::OsString; +use std::path::PathBuf; +use crate::node::NodeConfig; + +use crate::wallet::WalletConfig; + +/// Wallet loaded from config. +#[derive(Clone)] +pub struct Wallet { + /// Identifier for a wallet, name of wallet directory. + id: OsString, + /// Base path for wallet data. + pub(crate) path: String, + /// Loaded file config. + pub(crate) config: WalletConfig, +} + +impl Wallet { + /// Create new wallet from provided name. + pub fn create(name: String ) { + + } + + /// Load wallet from provided data path. + pub fn load(data_path: PathBuf) -> Option { + if !data_path.is_dir() { + return None; + } + let wallet_config = WalletConfig::load(data_path.clone()); + if let Some(config) = wallet_config { + // Set id as wallet directory name. + let id = data_path.file_name().unwrap().to_os_string(); + let path = data_path.to_str().unwrap().to_string(); + return Some(Self { id, path, config }); + } + None + } + + /// Get wallet node connection URL. + pub fn get_connection_url(&self) -> String { + match self.config.get_external_node_url() { + None => { + format!("http://{}", NodeConfig::get_api_address()) + } + Some(url) => url.to_string() + } + } +} \ No newline at end of file