ui + config: wallet connections setup, initial wallet config, wallet list state, update translations

This commit is contained in:
ardocrat 2023-07-25 03:42:52 +03:00
parent 5cefb61bb3
commit f461f27e4c
23 changed files with 564 additions and 127 deletions

1
Cargo.lock generated
View file

@ -2249,6 +2249,7 @@ dependencies = [
"tokio 1.29.1", "tokio 1.29.1",
"tokio-util 0.7.8", "tokio-util 0.7.8",
"toml 0.7.6", "toml 0.7.6",
"url",
"wgpu", "wgpu",
"winit", "winit",
"zeroize", "zeroize",

View file

@ -51,6 +51,7 @@ toml = "0.7.4"
serde = "1" serde = "1"
pnet = "0.33.0" pnet = "0.33.0"
zeroize = "1.6.0" zeroize = "1.6.0"
url = "2.4.0"
# stratum server # stratum server
serde_derive = "1" serde_derive = "1"

View file

@ -18,7 +18,11 @@ wallets:
not_valid_word: Entered word is not valid not_valid_word: Entered word is not valid
create_phrase_desc: Safely write down and save your recovery phrase. create_phrase_desc: Safely write down and save your recovery phrase.
restore_phrase_desc: Enter words from your saved 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: network:
self: Network self: Network
node: Integrated node node: Integrated node
@ -150,6 +154,7 @@ modal:
cancel: Cancel cancel: Cancel
save: Save save: Save
confirmation: Confirmation confirmation: Confirmation
add: Add
modal_exit: modal_exit:
description: Are you sure you want to quit the application? description: Are you sure you want to quit the application?
exit: Exit exit: Exit

View file

@ -18,7 +18,11 @@ wallets:
not_valid_word: Введено недопустимое слово not_valid_word: Введено недопустимое слово
create_phrase_desc: Безопасно запишите и сохраните вашу фразу восстановления. create_phrase_desc: Безопасно запишите и сохраните вашу фразу восстановления.
restore_phrase_desc: Введите слова из вашей сохранённой фразы восстановления. restore_phrase_desc: Введите слова из вашей сохранённой фразы восстановления.
setup_conn_desc: Выберите способ подключения кошелька setup_conn_desc: Выберите способ подключения вашего кошелька к сети.
conn_method: Способ подключения
ext_conn: 'Внешние подключения:'
add_node_url: Добавить URL узла
invalid_url: Введенный URL-адрес недействителен
network: network:
self: Сеть self: Сеть
node: Встроенный узел node: Встроенный узел
@ -150,6 +154,7 @@ modal:
cancel: Отмена cancel: Отмена
save: Сохранить save: Сохранить
confirmation: Подтверждение confirmation: Подтверждение
add: Добавить
modal_exit: modal_exit:
description: Вы уверены, что хотите выйти из приложения? description: Вы уверены, что хотите выйти из приложения?
exit: Выход exit: Выход

View file

@ -20,7 +20,7 @@ use lazy_static::lazy_static;
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks; 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; use crate::node::Node;
lazy_static! { lazy_static! {
@ -32,8 +32,8 @@ lazy_static! {
pub struct Root { pub struct Root {
/// Side panel [`Network`] content. /// Side panel [`Network`] content.
network: Network, network: Network,
/// Central panel [`Wallets`] content. /// Central panel [`WalletsContent`] content.
wallets: Wallets, wallets: WalletsContent,
/// Check if app exit is allowed on close event of [`eframe::App`] implementation. /// Check if app exit is allowed on close event of [`eframe::App`] implementation.
pub(crate) exit_allowed: bool, pub(crate) exit_allowed: bool,
@ -52,7 +52,7 @@ impl Default for Root {
let exit_allowed = os == OperatingSystem::Android || os == OperatingSystem::IOS; let exit_allowed = os == OperatingSystem::Android || os == OperatingSystem::IOS;
Self { Self {
network: Network::default(), network: Network::default(),
wallets: Wallets::default(), wallets: WalletsContent::default(),
exit_allowed, exit_allowed,
show_exit_progress: false, show_exit_progress: false,
allowed_modal_ids: vec![ allowed_modal_ids: vec![
@ -117,7 +117,7 @@ impl Root {
(is_panel_open, panel_width) (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 { pub fn is_dual_panel_mode(frame: &mut eframe::Frame) -> bool {
let w = frame.info().window_info.size.x; let w = frame.info().window_info.size.x;
let h = frame.info().window_info.size.y; let h = frame.info().window_info.size.y;

View file

@ -14,14 +14,15 @@
use egui::{Margin, RichText, TextStyle, vec2, Widget}; use egui::{Margin, RichText, TextStyle, vec2, Widget};
use egui_extras::{RetainedImage, Size, StripBuilder}; use egui_extras::{RetainedImage, Size, StripBuilder};
use crate::built_info;
use crate::built_info;
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::{CHECK, EYE, EYE_SLASH, PLUS_CIRCLE, SHARE_FAT}; use crate::gui::icons::{CHECK, EYE, EYE_SLASH, PLUS_CIRCLE, SHARE_FAT};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, ModalPosition, View}; 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::creation::types::{PhraseMode, Step};
use crate::gui::views::wallets::setup::ConnectionSetup;
/// Wallet creation content. /// Wallet creation content.
pub struct WalletCreation { pub struct WalletCreation {
@ -51,8 +52,8 @@ impl Default for WalletCreation {
Self { Self {
step: None, step: None,
modal_just_opened: true, modal_just_opened: true,
name_edit: "".to_string(), name_edit: String::from(""),
pass_edit: "".to_string(), pass_edit: String::from(""),
hide_pass: true, hide_pass: true,
mnemonic_setup: MnemonicSetup::default(), mnemonic_setup: MnemonicSetup::default(),
network_setup: ConnectionSetup::default(), network_setup: ConnectionSetup::default(),
@ -89,17 +90,16 @@ impl WalletCreation {
let (step_text, step_available) = match step { let (step_text, step_available) = match step {
Step::EnterMnemonic => { Step::EnterMnemonic => {
let mode = &self.mnemonic_setup.mnemonic.mode; let mode = &self.mnemonic_setup.mnemonic.mode;
let size_value = self.mnemonic_setup.mnemonic.size.value();
let text = if mode == &PhraseMode::Generate { let text = if mode == &PhraseMode::Generate {
t!("wallets.create_phrase_desc", "number" => size_value) t!("wallets.create_phrase_desc")
} else { } else {
t!("wallets.restore_phrase_desc", "number" => size_value) t!("wallets.restore_phrase_desc")
}; };
let available = !self let available = !self
.mnemonic_setup .mnemonic_setup
.mnemonic .mnemonic
.words .words
.contains(&"".to_string()); .contains(&String::from(""));
(text, available) (text, available)
} }
Step::ConfirmMnemonic => { Step::ConfirmMnemonic => {
@ -108,7 +108,7 @@ impl WalletCreation {
.mnemonic_setup .mnemonic_setup
.mnemonic .mnemonic
.confirm_words .confirm_words
.contains(&"".to_string()); .contains(&String::from(""));
(text, available) (text, available)
}, },
Step::SetupConnection => (t!("wallets.setup_conn_desc"), true) Step::SetupConnection => (t!("wallets.setup_conn_desc"), true)
@ -190,13 +190,9 @@ impl WalletCreation {
} }
Some(step) => { Some(step) => {
match step { match step {
Step::EnterMnemonic => { Step::EnterMnemonic => self.mnemonic_setup.ui(ui),
self.mnemonic_setup.ui(ui); Step::ConfirmMnemonic => self.mnemonic_setup.confirm_ui(ui),
} Step::SetupConnection => self.network_setup.ui(ui, cb)
Step::ConfirmMnemonic => {
self.mnemonic_setup.confirm_ui(ui);
}
Step::SetupConnection => {}
} }
} }
} }
@ -216,8 +212,8 @@ impl WalletCreation {
Step::EnterMnemonic => { Step::EnterMnemonic => {
// Clear values if it needs to go back on first step. // Clear values if it needs to go back on first step.
self.step = None; self.step = None;
self.name_edit = "".to_string(); self.name_edit = String::from("");
self.pass_edit = "".to_string(); self.pass_edit = String::from("");
self.mnemonic_setup.reset(); self.mnemonic_setup.reset();
} }
Step::ConfirmMnemonic => self.step = Some(Step::EnterMnemonic), Step::ConfirmMnemonic => self.step = Some(Step::EnterMnemonic),
@ -255,8 +251,8 @@ impl WalletCreation {
// Reset modal values. // Reset modal values.
self.hide_pass = false; self.hide_pass = false;
self.modal_just_opened = true; self.modal_just_opened = true;
self.name_edit = "".to_string(); self.name_edit = String::from("");
self.pass_edit = "".to_string(); self.pass_edit = String::from("");
// Show modal. // Show modal.
Modal::new(Self::NAME_PASS_MODAL) Modal::new(Self::NAME_PASS_MODAL)
.position(ModalPosition::CenterTop) .position(ModalPosition::CenterTop)

View file

@ -38,7 +38,7 @@ impl Default for MnemonicSetup {
Self { Self {
mnemonic: Mnemonic::default(), mnemonic: Mnemonic::default(),
word_num_edit: 0, word_num_edit: 0,
word_edit: "".to_string(), word_edit: String::from(""),
valid_word_edit: true valid_word_edit: true
} }
} }
@ -192,12 +192,12 @@ impl MnemonicSetup {
} }
/// Draw word list item for current mode. /// 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 { if edit {
ui.add_space(6.0); ui.add_space(6.0);
View::button(ui, PENCIL.to_string(), Colors::BUTTON, || { View::button(ui, PENCIL.to_string(), Colors::BUTTON, || {
// Setup modal values. // Setup modal values.
self.word_num_edit = word_number; self.word_num_edit = num;
self.word_edit = word.clone(); self.word_edit = word.clone();
self.valid_word_edit = true; self.valid_word_edit = true;
// Show word edit modal. // Show word edit modal.
@ -206,12 +206,12 @@ impl MnemonicSetup {
.title(t!("wallets.saved_phrase")) .title(t!("wallets.saved_phrase"))
.show(); .show();
}); });
ui.label(RichText::new(format!("#{} {}", word_number, word)) ui.label(RichText::new(format!("#{} {}", num, word))
.size(17.0) .size(17.0)
.color(Colors::BLACK)); .color(Colors::BLACK));
} else { } else {
ui.add_space(12.0); 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)); ui.label(RichText::new(text).size(17.0).color(Colors::BLACK));
} }
} }
@ -274,14 +274,18 @@ impl MnemonicSetup {
self.valid_word_edit = false; self.valid_word_edit = false;
return; return;
} }
self.valid_word_edit = true;
// Select list where to save word. // Select list where to save word.
let words = match self.mnemonic.mode { let words = match self.mnemonic.mode {
PhraseMode::Generate => &mut self.mnemonic.confirm_words, PhraseMode::Generate => &mut self.mnemonic.confirm_words,
PhraseMode::Import => &mut self.mnemonic.words PhraseMode::Import => &mut self.mnemonic.words
}; };
// Save word at list. // Save word at list.
words.remove(word_index); words.remove(word_index);
words.insert(word_index, self.word_edit.clone()); words.insert(word_index, self.word_edit.clone());
// Close modal or go to next word to edit. // Close modal or go to next word to edit.
let close_modal = words.len() == self.word_num_edit let close_modal = words.len() == self.word_num_edit
|| !words.get(self.word_num_edit).unwrap().is_empty(); || !words.get(self.word_num_edit).unwrap().is_empty();
@ -290,7 +294,7 @@ impl MnemonicSetup {
modal.close(); modal.close();
} else { } else {
self.word_num_edit += 1; self.word_num_edit += 1;
self.word_edit = "".to_string(); self.word_edit = String::from("");
} }
}; };
// Call save on Enter key press. // Call save on Enter key press.

View file

@ -15,9 +15,6 @@
mod mnemonic; mod mnemonic;
pub use mnemonic::MnemonicSetup; pub use mnemonic::MnemonicSetup;
mod connection;
pub use connection::ConnectionSetup;
mod creation; mod creation;
pub use creation::WalletCreation; pub use creation::WalletCreation;

View file

@ -14,6 +14,7 @@
use grin_keychain::mnemonic::{from_entropy, search}; use grin_keychain::mnemonic::{from_entropy, search};
use rand::{Rng, thread_rng}; use rand::{Rng, thread_rng};
use zeroize::{Zeroize, ZeroizeOnDrop};
/// Wallet creation step. /// Wallet creation step.
#[derive(PartialEq)] #[derive(PartialEq)]
@ -27,7 +28,8 @@ pub enum Step {
} }
/// Mnemonic phrase setup mode. /// Mnemonic phrase setup mode.
#[derive(PartialEq, Clone)] /// Will be completely cleaned from memory on drop.
#[derive(PartialEq, Clone, Zeroize, ZeroizeOnDrop)]
pub enum PhraseMode { pub enum PhraseMode {
/// Generate new mnemonic phrase. /// Generate new mnemonic phrase.
Generate, Generate,
@ -36,7 +38,8 @@ pub enum PhraseMode {
} }
/// Mnemonic phrase size based on words count. /// 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 } pub enum PhraseSize { Words12, Words15, Words18, Words21, Words24 }
impl PhraseSize { impl PhraseSize {
@ -72,6 +75,8 @@ impl PhraseSize {
} }
/// Mnemonic phrase container. /// Mnemonic phrase container.
/// Will be completely cleaned from memory on drop.
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct Mnemonic { pub struct Mnemonic {
/// Phrase setup mode. /// Phrase setup mode.
pub(crate) mode: PhraseMode, pub(crate) mode: PhraseMode,
@ -130,7 +135,7 @@ impl Mnemonic {
} }
from_entropy(&entropy).unwrap() from_entropy(&entropy).unwrap()
.split(" ") .split(" ")
.map(|s| s.to_string()) .map(|s| String::from(s))
.collect::<Vec<String>>() .collect::<Vec<String>>()
}, },
PhraseMode::Import => { PhraseMode::Import => {
@ -143,7 +148,7 @@ impl Mnemonic {
fn empty_words(size: &PhraseSize) -> Vec<String> { fn empty_words(size: &PhraseSize) -> Vec<String> {
let mut words = Vec::with_capacity(size.value()); let mut words = Vec::with_capacity(size.value());
for _ in 0..size.value() { for _ in 0..size.value() {
words.push("".to_string()) words.push(String::from(""))
} }
words words
} }

View file

@ -12,8 +12,9 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
mod wallets;
mod creation; mod creation;
mod wallet; mod wallet;
mod setup;
mod wallets;
pub use wallets::*; pub use wallets::*;

View file

@ -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);
});
}
}

View file

@ -12,15 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use crate::gui::platform::PlatformCallbacks; mod connection;
pub use connection::ConnectionSetup;
#[derive(Default)] mod types;
pub struct ConnectionSetup { pub use types::*;
}
impl ConnectionSetup {
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
}
}

View file

@ -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)
}

View file

@ -17,16 +17,17 @@ use egui::Margin;
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::View; use crate::gui::views::View;
use crate::wallet::Wallet;
/// Selected wallet list item content. /// Selected wallet list item content.
pub struct WalletContent { pub struct WalletContent {
/// Current wallet instance. /// Current wallet instance.
item: String wallet: Wallet
} }
impl WalletContent { impl WalletContent {
fn new(item: String) -> Self { fn new(wallet: Wallet) -> Self {
Self { item } Self { wallet }
} }
} }

View file

@ -21,12 +21,14 @@ use crate::gui::icons::{ARROW_LEFT, GEAR, GLOBE, PLUS};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, ModalContainer, Root, TitlePanel, TitleType, View}; use crate::gui::views::{Modal, ModalContainer, Root, TitlePanel, TitleType, View};
use crate::gui::views::wallets::creation::{MnemonicSetup, WalletCreation}; use crate::gui::views::wallets::creation::{MnemonicSetup, WalletCreation};
use crate::gui::views::wallets::setup::ConnectionSetup;
use crate::gui::views::wallets::wallet::WalletContent; use crate::gui::views::wallets::wallet::WalletContent;
use crate::wallet::{Wallet, WalletList};
/// Wallets content. /// Wallets content.
pub struct Wallets { pub struct WalletsContent {
/// List of wallets. /// List of wallets.
list: Vec<String>, list: Vec<Wallet>,
/// Selected list item content. /// Selected list item content.
item_content: Option<WalletContent>, item_content: Option<WalletContent>,
@ -37,28 +39,28 @@ pub struct Wallets {
modal_ids: Vec<&'static str> modal_ids: Vec<&'static str>
} }
impl Default for Wallets { impl Default for WalletsContent {
fn default() -> Self { fn default() -> Self {
//TODO load list.
Self { Self {
list: vec![], list: WalletList::list(),
item_content: None, item_content: None,
creation_content: WalletCreation::default(), creation_content: WalletCreation::default(),
modal_ids: vec![ modal_ids: vec![
WalletCreation::NAME_PASS_MODAL, 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> { fn modal_ids(&self) -> &Vec<&'static str> {
&self.modal_ids &self.modal_ids
} }
} }
impl Wallets { impl WalletsContent {
pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) {
// Show modal content for current ui container. // Show modal content for current ui container.
if self.can_draw_modal() { if self.can_draw_modal() {
@ -70,6 +72,9 @@ impl Wallets {
MnemonicSetup::WORD_INPUT_MODAL => { MnemonicSetup::WORD_INPUT_MODAL => {
self.creation_content.mnemonic_setup.modal_ui(ui, modal, cb); 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 { 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 dual_panel_root = Root::is_dual_panel_mode(frame);
let max_width = ui.available_width(); let max_width = ui.available_width();

View file

@ -93,13 +93,13 @@ pub fn start(mut options: eframe::NativeOptions, app_creator: eframe::AppCreator
options.default_theme = eframe::Theme::Light; options.default_theme = eframe::Theme::Light;
options.renderer = eframe::Renderer::Wgpu; options.renderer = eframe::Renderer::Wgpu;
options.initial_window_size = Some(egui::Vec2::new(1200.0, 720.0)); options.initial_window_size = Some(egui::Vec2::new(1200.0, 720.0));
// Setup translations.
setup_i18n(); setup_i18n();
// Start integrated node if needed.
if Settings::app_config_to_read().auto_start_node { if Settings::app_config_to_read().auto_start_node {
Node::start(); Node::start();
} }
// Launch graphical interface.
let _ = eframe::run_native("Grim", options, app_creator); let _ = eframe::run_native("Grim", options, app_creator);
} }

View file

@ -45,34 +45,11 @@ impl PeersConfig {
/// Save peers config to the file. /// Save peers config to the file.
pub fn save(&self) { pub fn save(&self) {
let chain_type = AppConfig::chain_type(); 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); 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. /// Convert string to [`PeerAddr`] if address is in correct format (`host:port`) and available.
pub fn peer_to_addr(peer: String) -> Option<PeerAddr> { pub fn peer_to_addr(peer: String) -> Option<PeerAddr> {
match SocketAddr::from_str(peer.as_str()) { match SocketAddr::from_str(peer.as_str()) {
@ -169,7 +146,8 @@ impl NodeConfig {
// Initialize peers config. // Initialize peers config.
let 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::<PeersConfig>(path.clone()); let config = Settings::read_from_file::<PeersConfig>(path.clone());
if !path.exists() || config.is_err() { if !path.exists() || config.is_err() {
Self::save_default_peers_config(chain_type) Self::save_default_peers_config(chain_type)
@ -180,7 +158,8 @@ impl NodeConfig {
// Initialize node config. // Initialize node config.
let 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::<ConfigMembers>(path.clone()); let config = Settings::read_from_file::<ConfigMembers>(path.clone());
if !path.exists() || config.is_err() { if !path.exists() || config.is_err() {
Self::save_default_node_server_config(chain_type) Self::save_default_node_server_config(chain_type)
@ -194,9 +173,10 @@ impl NodeConfig {
/// Save default node config for specified [`ChainTypes`]. /// Save default node config for specified [`ChainTypes`].
fn save_default_node_server_config(chain_type: &ChainTypes) -> ConfigMembers { 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); 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(); let config = default_config.members.unwrap();
Settings::write_to_file(&config, path); Settings::write_to_file(&config, path);
config config
@ -204,7 +184,8 @@ impl NodeConfig {
/// Save default peers config for specified [`ChainTypes`]. /// Save default peers config for specified [`ChainTypes`].
fn save_default_peers_config(chain_type: &ChainTypes) -> PeersConfig { 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(); let config = PeersConfig::default();
Settings::write_to_file(&config, path); Settings::write_to_file(&config, path);
config config
@ -212,10 +193,8 @@ impl NodeConfig {
/// Save node config to the file. /// Save node config to the file.
pub fn save(&self) { pub fn save(&self) {
let config_path = Settings::get_config_path( let chain_name = Some(self.node.server.chain_type.shortname());
SERVER_CONFIG_FILE_NAME, let config_path = Settings::get_config_path(SERVER_CONFIG_FILE_NAME, chain_name);
Some(&self.node.server.chain_type)
);
Settings::write_to_file(&self.node, config_path); Settings::write_to_file(&self.node, config_path);
} }
@ -256,7 +235,7 @@ impl NodeConfig {
/// Get path for secret file. /// Get path for secret file.
fn get_secret_path(chain_type: &ChainTypes, secret_file_name: &str) -> PathBuf { 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; let mut api_secret_path = grin_path;
api_secret_path.push(secret_file_name); api_secret_path.push(secret_file_name);
api_secret_path api_secret_path
@ -446,7 +425,7 @@ impl NodeConfig {
/// Get API server IP and port. /// Get API server IP and port.
pub fn get_api_ip_port() -> (String, String) { 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(); let (addr, port) = saved_addr.split_once(":").unwrap();
(addr.into(), port.into()) (addr.into(), port.into())
} }

View file

@ -12,12 +12,11 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
mod stratum;
mod mine_block;
mod node; mod node;
pub use node::Node; pub use node::Node;
mod config; mod config;
pub use config::*;
mod stratum;
mod mine_block;
pub use config::{NodeConfig, PeersConfig};

View file

@ -24,21 +24,28 @@ use serde::{Deserialize, Serialize};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use crate::node::NodeConfig; use crate::node::NodeConfig;
use crate::wallet::WalletList;
lazy_static! { lazy_static! {
/// Static settings state to be accessible globally. /// Static settings state to be accessible globally.
static ref SETTINGS_STATE: Arc<Settings> = Arc::new(Settings::init()); static ref SETTINGS_STATE: Arc<Settings> = Arc::new(Settings::init());
} }
/// Application configuration file name.
const APP_CONFIG_FILE_NAME: &'static str = "app.toml"; 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. /// Common application settings.
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct AppConfig { pub struct AppConfig {
/// Run node server on startup. /// Run node server on startup.
pub auto_start_node: bool, pub auto_start_node: bool,
/// Chain type for node and wallets. /// Chain type for node and wallets.
chain_type: ChainTypes chain_type: ChainTypes,
/// URLs of external nodes for wallets.
external_nodes_urls: Vec<String>
} }
impl Default for AppConfig { impl Default for AppConfig {
@ -46,12 +53,15 @@ impl Default for AppConfig {
Self { Self {
auto_start_node: false, auto_start_node: false,
chain_type: ChainTypes::default(), chain_type: ChainTypes::default(),
external_nodes_urls: vec![
DEFAULT_EXTERNAL_NODE_URL.to_string()
],
} }
} }
} }
impl AppConfig { impl AppConfig {
/// Initialize application config from the disk. /// Initialize application config from the file.
pub fn init() -> Self { pub fn init() -> Self {
let path = Settings::get_config_path(APP_CONFIG_FILE_NAME, None); let path = Settings::get_config_path(APP_CONFIG_FILE_NAME, None);
let parsed = Settings::read_from_file::<AppConfig>(path.clone()); let parsed = Settings::read_from_file::<AppConfig>(path.clone());
@ -64,7 +74,7 @@ impl AppConfig {
} }
} }
/// Save app config to disk. /// Save app config to file.
pub fn save(&self) { pub fn save(&self) {
Settings::write_to_file(self, Settings::get_config_path(APP_CONFIG_FILE_NAME, None)); 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); let node_config = NodeConfig::for_chain_type(chain_type);
w_node_config.node = node_config.node; w_node_config.node = node_config.node;
w_node_config.peers = node_config.peers; 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.auto_start_node = !autostart;
w_app_config.save(); w_app_config.save();
} }
/// Get external nodes URLs.
pub fn external_nodes_urls() -> Vec<String> {
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 { pub struct Settings {
/// Application config instance.
app_config: Arc<RwLock<AppConfig>>, app_config: Arc<RwLock<AppConfig>>,
node_config: Arc<RwLock<NodeConfig>> /// Integrated node config instance.
node_config: Arc<RwLock<NodeConfig>>,
} }
impl Settings { impl Settings {
@ -120,7 +150,7 @@ impl Settings {
let app_config = AppConfig::init(); let app_config = AppConfig::init();
Self { Self {
node_config: Arc::new(RwLock::new(NodeConfig::for_chain_type(&app_config.chain_type))), 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() SETTINGS_STATE.app_config.write().unwrap()
} }
/// Get working directory path for the application. /// Get base directory path for config.
pub fn get_working_path(chain_type: Option<&ChainTypes>) -> PathBuf { pub fn get_base_path(sub_dir: Option<String>) -> PathBuf {
// Check if dir exists. // Check if dir exists.
let mut path = match dirs::home_dir() { let mut path = match dirs::home_dir() {
Some(p) => p, Some(p) => p,
None => PathBuf::new(), None => PathBuf::new(),
}; };
path.push(WORKING_DIRECTORY_NAME); path.push(MAIN_DIR_NAME);
if chain_type.is_some() { if sub_dir.is_some() {
path.push(chain_type.unwrap().shortname()); path.push(sub_dir.unwrap());
} }
// Create if the default path doesn't exist. // Create if the default path doesn't exist.
if !path.exists() { if !path.exists() {
@ -162,15 +192,14 @@ impl Settings {
path path
} }
/// Get config file path from provided name and [`ChainTypes`] if needed. /// Get config file path from provided name and sub-directory if needed.
pub fn get_config_path(config_name: &str, chain_type: Option<&ChainTypes>) -> PathBuf { pub fn get_config_path(config_name: &str, sub_dir: Option<String>) -> PathBuf {
let main_path = Self::get_working_path(chain_type); let mut settings_path = Self::get_base_path(sub_dir);
let mut settings_path = main_path.clone();
settings_path.push(config_name); settings_path.push(config_name);
settings_path settings_path
} }
/// Read config from a file /// Read config from the file.
pub fn read_from_file<T: DeserializeOwned>(config_path: PathBuf) -> Result<T, ConfigError> { pub fn read_from_file<T: DeserializeOwned>(config_path: PathBuf) -> Result<T, ConfigError> {
let file_content = fs::read_to_string(config_path.clone())?; let file_content = fs::read_to_string(config_path.clone())?;
let parsed = toml::from_str::<T>(file_content.as_str()); let parsed = toml::from_str::<T>(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<T: Serialize>(config: &T, path: PathBuf) { pub fn write_to_file<T: Serialize>(config: &T, path: PathBuf) {
let conf_out = toml::to_string(config).unwrap(); let conf_out = toml::to_string(config).unwrap();
let mut file = File::create(path.to_str().unwrap()).unwrap(); let mut file = File::create(path.to_str().unwrap()).unwrap();

95
src/wallet/config.rs Normal file
View file

@ -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<String>,
}
/// 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<WalletConfig> {
let mut config_path: PathBuf = wallet_dir.clone();
config_path.push(CONFIG_FILE_NAME);
if let Ok(config) = Settings::read_from_file::<WalletConfig>(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<String> {
&self.external_node_url
}
/// Set external node connection URL.
pub fn set_external_node_url(&mut self, url: Option<String>) {
self.external_node_url = url;
self.save();
}
}

81
src/wallet/list.rs Normal file
View file

@ -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<RwLock<WalletList >> = Arc::new(RwLock::new(WalletList::load()));
}
/// List of created wallets.
pub struct WalletList {
list: Vec<Wallet>
}
/// 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<Wallet> {
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<Wallet> {
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);
}
}

View file

@ -13,5 +13,10 @@
// limitations under the License. // limitations under the License.
mod wallet; 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;

View file

@ -12,3 +12,51 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // 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<Wallet> {
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()
}
}
}