ui + wallets: modal width detection fix, connections refactoring, mnemonic location refactoring, wallet creation callback, wallet loading, wallet list state refactoring, fix integrated node wallet connection, update translations, remove zeroize dependency

This commit is contained in:
ardocrat 2023-08-03 00:00:23 +03:00
parent 65c27d250b
commit b34654ab00
28 changed files with 730 additions and 474 deletions

1
Cargo.lock generated
View file

@ -2267,7 +2267,6 @@ dependencies = [
"uuid", "uuid",
"wgpu", "wgpu",
"winit", "winit",
"zeroize",
] ]
[[package]] [[package]]

View file

@ -50,7 +50,6 @@ lazy_static = "1.4.0"
toml = "0.7.4" toml = "0.7.4"
serde = "1" serde = "1"
pnet = "0.34.0" pnet = "0.34.0"
zeroize = "1.6.0"
url = "2.4.0" url = "2.4.0"
parking_lot = "0.10.2" parking_lot = "0.10.2"
uuid = { version = "0.8.2", features = ["serde", "v4"] } uuid = { version = "0.8.2", features = ["serde", "v4"] }

View file

@ -29,6 +29,7 @@ wallets:
wrong_pass: Entered password is wrong wrong_pass: Entered password is wrong
locked: Locked locked: Locked
unlocked: Unlocked unlocked: Unlocked
enable_node_required: 'Enable integrated node to use the wallet or change connection settings by selecting %{settings} at the bottom of the screen.'
network: network:
self: Network self: Network
node: Integrated node node: Integrated node

View file

@ -29,6 +29,7 @@ wallets:
wrong_pass: Введён неправильный пароль wrong_pass: Введён неправильный пароль
locked: Заблокирован locked: Заблокирован
unlocked: Разблокирован unlocked: Разблокирован
enable_node_required: 'Чтобы использовать кошелёк, включите встроенный узел или измените настройки подключения, выбрав %{settings} внизу экрана.'
network: network:
self: Сеть self: Сеть
node: Встроенный узел node: Встроенный узел

View file

@ -12,8 +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 std::sync::RwLock;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::RwLock;
use egui::{Align2, RichText, Rounding, Stroke, Vec2}; use egui::{Align2, RichText, Rounding, Stroke, Vec2};
use egui::epaint::RectShape; use egui::epaint::RectShape;
@ -167,17 +167,17 @@ impl Modal {
} }
/// Draw opened [`Modal`] content. /// Draw opened [`Modal`] content.
pub fn ui(ui: &mut egui::Ui, add_content: impl FnOnce(&mut egui::Ui, &Modal)) { pub fn ui(ctx: &egui::Context, add_content: impl FnOnce(&mut egui::Ui, &Modal)) {
if let Some(modal) = &MODAL_STATE.read().unwrap().modal { if let Some(modal) = &MODAL_STATE.read().unwrap().modal {
if modal.is_open() { if modal.is_open() {
modal.window_ui(ui, add_content); modal.window_ui(ctx, add_content);
} }
} }
} }
/// Draw [`egui::Window`] with provided content. /// Draw [`egui::Window`] with provided content.
fn window_ui(&self, ui: &mut egui::Ui, add_content: impl FnOnce(&mut egui::Ui, &Modal)) { fn window_ui(&self, ctx: &egui::Context, add_content: impl FnOnce(&mut egui::Ui, &Modal)) {
let rect = ui.ctx().screen_rect(); let rect = ctx.screen_rect();
egui::Window::new("modal_bg_window") egui::Window::new("modal_bg_window")
.title_bar(false) .title_bar(false)
.resizable(false) .resizable(false)
@ -187,13 +187,13 @@ impl Modal {
fill: Colors::SEMI_TRANSPARENT, fill: Colors::SEMI_TRANSPARENT,
..Default::default() ..Default::default()
}) })
.show(ui.ctx(), |ui| { .show(ctx, |ui| {
ui.set_min_size(rect.size()); ui.set_min_size(rect.size());
}); });
// Setup width of modal content. // Setup width of modal content.
let side_insets = View::get_left_inset() + View::get_right_inset(); let side_insets = View::get_left_inset() + View::get_right_inset();
let available_width = ui.available_width() - (side_insets + Self::DEFAULT_MARGIN); let available_width = rect.width() - (side_insets + Self::DEFAULT_MARGIN);
let width = f32::min(available_width, Self::DEFAULT_WIDTH); let width = f32::min(available_width, Self::DEFAULT_WIDTH);
// Show main content Window at given position. // Show main content Window at given position.
@ -209,7 +209,7 @@ impl Modal {
fill: Colors::YELLOW, fill: Colors::YELLOW,
..Default::default() ..Default::default()
}) })
.show(ui.ctx(), |ui| { .show(ctx, |ui| {
if self.title.is_some() { if self.title.is_some() {
self.title_ui(ui); self.title_ui(ui);
} }
@ -217,7 +217,7 @@ impl Modal {
}).unwrap().response.layer_id; }).unwrap().response.layer_id;
// Always show main content Window above background Window. // Always show main content Window above background Window.
ui.ctx().move_to_top(layer_id); ctx.move_to_top(layer_id);
} }

View file

@ -85,7 +85,7 @@ impl NetworkContent {
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() {
Modal::ui(ui, |ui, modal| { Modal::ui(ui.ctx(), |ui, modal| {
self.current_tab.as_mut().on_modal_ui(ui, modal, cb); self.current_tab.as_mut().on_modal_ui(ui, modal, cb);
}); });
} }

View file

@ -149,7 +149,7 @@ impl Root {
/// Draw exit confirmation modal content. /// Draw exit confirmation modal content.
fn exit_modal_content(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { fn exit_modal_content(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
Modal::ui(ui, |ui, modal| { Modal::ui(ui.ctx(), |ui, modal| {
if self.show_exit_progress { if self.show_exit_progress {
if !Node::is_running() { if !Node::is_running() {
self.exit(frame); self.exit(frame);

View file

@ -14,18 +14,25 @@
use egui::{Align, Align2, Layout, Margin, RichText, Rounding, ScrollArea, TextStyle, Widget}; use egui::{Align, Align2, Layout, Margin, RichText, Rounding, ScrollArea, TextStyle, Widget};
use egui_extras::{Size, StripBuilder}; use egui_extras::{Size, StripBuilder};
use grin_core::global::ChainTypes;
use crate::AppConfig;
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::{ARROW_LEFT, CARET_RIGHT, COMPUTER_TOWER, EYE, EYE_SLASH, FOLDER_LOCK, FOLDER_OPEN, GEAR, GLOBE, GLOBE_SIMPLE, LOCK_KEY, PLUS, SIDEBAR_SIMPLE, SUITCASE}; use crate::gui::icons::{ARROW_LEFT, CARET_RIGHT, COMPUTER_TOWER, EYE, EYE_SLASH, FOLDER_LOCK, FOLDER_OPEN, GEAR, GLOBE, GLOBE_SIMPLE, LOCK_KEY, PLUS, SIDEBAR_SIMPLE, SUITCASE};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, ModalContainer, ModalPosition, Root, TitlePanel, TitleType, View}; use crate::gui::views::{Modal, ModalContainer, ModalPosition, 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::setup::ConnectionSetup;
use crate::gui::views::wallets::wallet::WalletContent; use crate::gui::views::wallets::WalletContent;
use crate::wallet::{Wallet, Wallets}; use crate::wallet::{Wallet, Wallets};
/// Wallets content. /// Wallets content.
pub struct WalletsContent { pub struct WalletsContent {
/// Chain type for loaded wallets.
chain_type: ChainTypes,
/// Loaded list of wallets.
wallets: Wallets,
/// Password to open wallet for [`Modal`]. /// Password to open wallet for [`Modal`].
pass_edit: String, pass_edit: String,
/// Flag to show/hide password at [`egui::TextEdit`] field. /// Flag to show/hide password at [`egui::TextEdit`] field.
@ -48,6 +55,8 @@ pub struct WalletsContent {
impl Default for WalletsContent { impl Default for WalletsContent {
fn default() -> Self { fn default() -> Self {
Self { Self {
chain_type: AppConfig::chain_type(),
wallets: Wallets::default(),
pass_edit: "".to_string(), pass_edit: "".to_string(),
hide_pass: true, hide_pass: true,
wrong_pass: false, wrong_pass: false,
@ -72,12 +81,12 @@ impl ModalContainer for WalletsContent {
impl WalletsContent { impl WalletsContent {
/// Identifier for wallet opening [`Modal`]. /// Identifier for wallet opening [`Modal`].
pub const OPEN_WALLET_MODAL: &'static str = "open_wallet_modal"; const OPEN_WALLET_MODAL: &'static str = "open_wallet_modal";
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() {
Modal::ui(ui, |ui, modal| { Modal::ui(ui.ctx(), |ui, modal| {
match modal.id { match modal.id {
Self::OPEN_WALLET_MODAL => { Self::OPEN_WALLET_MODAL => {
self.open_wallet_modal_ui(ui, modal, cb); self.open_wallet_modal_ui(ui, modal, cb);
@ -96,17 +105,17 @@ impl WalletsContent {
}); });
} }
// Get wallets. // Setup list of wallets if chain type was changed.
let wallets = Wallets::list(); let chain_type = AppConfig::chain_type();
let empty_list = wallets.is_empty(); if self.chain_type != chain_type {
self.wallets.reinit(&chain_type);
self.chain_type = chain_type;
}
let empty_list = self.wallets.list.is_empty();
// Setup wallet content flags. // Setup wallet content flags.
let create_wallet = self.creation_content.can_go_back(); let create_wallet = self.creation_content.can_go_back();
let show_wallet = if let Some(id) = Wallets::selected_id() { let show_wallet = self.wallets.is_selected_open();
Wallets::is_open(id)
} else {
false
};
// Setup panels parameters. // Setup panels parameters.
let dual_panel = is_dual_panel_mode(ui, frame); let dual_panel = is_dual_panel_mode(ui, frame);
@ -135,15 +144,17 @@ impl WalletsContent {
if available_width_zero { if available_width_zero {
return; return;
} }
if create_wallet || !show_wallet { if create_wallet || !show_wallet {
// Show wallet creation content // Show wallet creation content.
self.creation_content.ui(ui, cb); self.creation_content.ui(ui, cb, |wallet| {
// Add created wallet to list.
self.wallets.add(wallet);
});
} else { } else {
for wallet in wallets.iter() { for mut wallet in self.wallets.list.clone() {
// Show content for selected wallet. // Show content for selected wallet.
if Some(wallet.config.id) == Wallets::selected_id() { if self.wallets.is_selected(wallet.config.id) {
self.wallet_content.ui(ui, frame, &wallet, cb); self.wallet_content.ui(ui, frame, &mut wallet, cb);
break; break;
} }
} }
@ -189,7 +200,7 @@ impl WalletsContent {
= (!show_wallet && !dual_panel) || (dual_panel && show_wallet); = (!show_wallet && !dual_panel) || (dual_panel && show_wallet);
// Show list of wallets. // Show list of wallets.
let scroll = self.list_ui(ui, dual_panel, show_creation_btn, &wallets, cb); let scroll = self.list_ui(ui, dual_panel, show_creation_btn, cb);
if show_creation_btn { if show_creation_btn {
// Setup right margin for button. // Setup right margin for button.
@ -221,7 +232,7 @@ impl WalletsContent {
TitlePanel::ui(title_content, |ui, frame| { TitlePanel::ui(title_content, |ui, frame| {
if show_wallet && !dual_panel { if show_wallet && !dual_panel {
View::title_button(ui, ARROW_LEFT, || { View::title_button(ui, ARROW_LEFT, || {
Wallets::select(None); self.wallets.select(None);
}); });
} else if create_wallet { } else if create_wallet {
View::title_button(ui, ARROW_LEFT, || { View::title_button(ui, ARROW_LEFT, || {
@ -277,7 +288,6 @@ impl WalletsContent {
ui: &mut egui::Ui, ui: &mut egui::Ui,
dual_panel: bool, dual_panel: bool,
show_creation_btn: bool, show_creation_btn: bool,
wallets: &Vec<Wallet>,
cb: &dyn PlatformCallbacks) -> bool { cb: &dyn PlatformCallbacks) -> bool {
let mut scroller_showing = false; let mut scroller_showing = false;
ui.scope(|ui| { ui.scope(|ui| {
@ -303,18 +313,14 @@ impl WalletsContent {
rect.set_width(width); rect.set_width(width);
ui.allocate_ui(rect.size(), |ui| { ui.allocate_ui(rect.size(), |ui| {
for (index, w) in wallets.iter().enumerate() { for mut wallet in self.wallets.list.clone() {
// Draw wallet list item. // Draw wallet list item.
self.wallet_item_ui(ui, w, cb); self.wallet_item_ui(ui, &mut wallet, cb);
// Add space after last item. ui.add_space(5.0);
let last_item = index == wallets.len() - 1; }
if !last_item { // Add space for wallet creation button.
ui.add_space(5.0); if show_creation_btn {
} ui.add_space(52.0);
// Add space for wallet creation button.
if show_creation_btn && last_item {
ui.add_space(57.0);
}
} }
}); });
}); });
@ -326,11 +332,13 @@ impl WalletsContent {
} }
/// Draw wallet list item. /// Draw wallet list item.
fn wallet_item_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { fn wallet_item_ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
cb: &dyn PlatformCallbacks) {
let id = wallet.config.id; let id = wallet.config.id;
let is_selected = Some(id) == Wallets::selected_id(); let is_selected = self.wallets.is_selected(id);
let is_open = Wallets::is_open(id); let is_current = wallet.is_open() && is_selected;
let is_current = is_open && is_selected;
// Draw round background. // Draw round background.
let mut rect = ui.available_rect_before_wrap(); let mut rect = ui.available_rect_before_wrap();
@ -348,21 +356,21 @@ impl WalletsContent {
ui.style_mut().visuals.widgets.hovered.rounding = Rounding::same(8.0); ui.style_mut().visuals.widgets.hovered.rounding = Rounding::same(8.0);
ui.style_mut().visuals.widgets.active.rounding = Rounding::same(8.0); ui.style_mut().visuals.widgets.active.rounding = Rounding::same(8.0);
if !is_open { if !wallet.is_open() {
// Show button to open closed wallet. // Show button to open closed wallet.
View::item_button(ui, [false, true], FOLDER_OPEN, || { View::item_button(ui, [false, true], FOLDER_OPEN, || {
Wallets::select(Some(id)); self.wallets.select(Some(id));
self.show_open_wallet_modal(cb); self.show_open_wallet_modal(cb);
}); });
} else if !is_selected { } else if !is_selected {
// Show button to select opened wallet. // Show button to select opened wallet.
View::item_button(ui, [false, true], CARET_RIGHT, || { View::item_button(ui, [false, true], CARET_RIGHT, || {
Wallets::select(Some(id)); self.wallets.select(Some(id));
}); });
// Show button to close opened wallet. // Show button to close opened wallet.
View::item_button(ui, [false, false], LOCK_KEY, || { View::item_button(ui, [false, false], LOCK_KEY, || {
Wallets::close(id).unwrap() let _ = wallet.close();
}); });
} }
@ -386,7 +394,7 @@ impl WalletsContent {
ui.add_space(1.0); ui.add_space(1.0);
// Setup wallet status text. // Setup wallet status text.
let status_text = if Wallets::is_open(id) { let status_text = if wallet.is_open() {
format!("{} {}", FOLDER_OPEN, t!("wallets.unlocked")) format!("{} {}", FOLDER_OPEN, t!("wallets.unlocked"))
} else { } else {
format!("{} {}", FOLDER_LOCK, t!("wallets.locked")) format!("{} {}", FOLDER_LOCK, t!("wallets.locked"))
@ -497,23 +505,18 @@ impl WalletsContent {
ui.columns(2, |columns| { ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| { columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::WHITE, || { View::button(ui, t!("modal.cancel"), Colors::WHITE, || {
// Clear values.
self.pass_edit = "".to_string();
self.wrong_pass = false;
self.hide_pass = true;
// Close modal. // Close modal.
cb.hide_keyboard(); cb.hide_keyboard();
modal.close(); modal.close();
}); });
}); });
columns[1].vertical_centered_justified(|ui| { columns[1].vertical_centered_justified(|ui| {
// Callback for continue button. // Callback for button to continue.
let mut on_continue = || { let mut on_continue = || {
if self.pass_edit.is_empty() { if self.pass_edit.is_empty() {
return; return;
} }
let selected_id = Wallets::selected_id().unwrap(); match self.wallets.open_selected(self.pass_edit.clone()) {
match Wallets::open(selected_id, self.pass_edit.clone()) {
Ok(_) => { Ok(_) => {
// Clear values. // Clear values.
self.pass_edit = "".to_string(); self.pass_edit = "".to_string();
@ -526,6 +529,7 @@ impl WalletsContent {
Err(_) => self.wrong_pass = true Err(_) => self.wrong_pass = true
} }
}; };
// Continue on Enter key press. // Continue on Enter key press.
View::on_enter_key(ui, || { View::on_enter_key(ui, || {
(on_continue)(); (on_continue)();

View file

@ -21,9 +21,10 @@ use crate::gui::icons::{CHECK, EYE, EYE_SLASH, FOLDER_PLUS, 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::MnemonicSetup; use crate::gui::views::wallets::creation::MnemonicSetup;
use crate::gui::views::wallets::creation::types::{PhraseMode, Step}; use crate::gui::views::wallets::creation::types::Step;
use crate::gui::views::wallets::setup::ConnectionSetup; use crate::gui::views::wallets::setup::ConnectionSetup;
use crate::wallet::Wallets; use crate::wallet::types::PhraseMode;
use crate::wallet::Wallet;
/// Wallet creation content. /// Wallet creation content.
pub struct WalletCreation { pub struct WalletCreation {
@ -61,7 +62,7 @@ impl Default for WalletCreation {
logo: RetainedImage::from_image_bytes( logo: RetainedImage::from_image_bytes(
"logo.png", "logo.png",
include_bytes!("../../../../../img/logo.png"), include_bytes!("../../../../../img/logo.png"),
).unwrap(), ).unwrap()
} }
} }
} }
@ -70,7 +71,10 @@ impl WalletCreation {
/// Wallet name/password input modal identifier. /// Wallet name/password input modal identifier.
pub const NAME_PASS_MODAL: &'static str = "name_pass_modal"; pub const NAME_PASS_MODAL: &'static str = "name_pass_modal";
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { pub fn ui(&mut self,
ui: &mut egui::Ui,
cb: &dyn PlatformCallbacks,
on_create: impl FnOnce(Wallet)) {
// Show wallet creation step description and confirmation panel. // Show wallet creation step description and confirmation panel.
if self.step.is_some() { if self.step.is_some() {
egui::TopBottomPanel::bottom("wallet_creation_step_panel") egui::TopBottomPanel::bottom("wallet_creation_step_panel")
@ -87,7 +91,7 @@ impl WalletCreation {
}) })
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
self.step_control_ui(ui); self.step_control_ui(ui, on_create);
}); });
}); });
} }
@ -109,8 +113,16 @@ impl WalletCreation {
}); });
} }
/// Reset wallet creation to default values.
fn reset(&mut self) {
self.step = None;
self.name_edit = String::from("");
self.pass_edit = String::from("");
self.mnemonic_setup.reset();
}
/// Draw [`Step`] description and confirmation control. /// Draw [`Step`] description and confirmation control.
fn step_control_ui(&mut self, ui: &mut egui::Ui) { fn step_control_ui(&mut self, ui: &mut egui::Ui, on_create: impl FnOnce(Wallet)) {
if let Some(step) = &self.step { if let Some(step) = &self.step {
// Setup step description text and availability. // Setup step description text and availability.
let (step_text, mut step_available) = match step { let (step_text, mut step_available) = match step {
@ -164,7 +176,7 @@ impl WalletCreation {
ui.add_space(4.0); ui.add_space(4.0);
// Show button. // Show button.
View::button(ui, next_text.to_uppercase(), color, || { View::button(ui, next_text.to_uppercase(), color, || {
self.forward(); self.forward(Some(on_create));
}); });
ui.add_space(4.0); ui.add_space(4.0);
} }
@ -223,13 +235,7 @@ impl WalletCreation {
None => {} None => {}
Some(step) => { Some(step) => {
match step { match step {
Step::EnterMnemonic => { Step::EnterMnemonic => self.reset(),
// Clear values if it needs to go back on first step.
self.step = None;
self.name_edit = String::from("");
self.pass_edit = String::from("");
self.mnemonic_setup.reset();
}
Step::ConfirmMnemonic => self.step = Some(Step::EnterMnemonic), Step::ConfirmMnemonic => self.step = Some(Step::EnterMnemonic),
Step::SetupConnection => self.step = Some(Step::EnterMnemonic) Step::SetupConnection => self.step = Some(Step::EnterMnemonic)
} }
@ -238,37 +244,41 @@ impl WalletCreation {
} }
/// Go to the next wallet creation [`Step`]. /// Go to the next wallet creation [`Step`].
fn forward(&mut self) { fn forward(&mut self, on_create: Option<impl FnOnce(Wallet)>) {
self.step = match &self.step { self.step = if let Some(step) = &self.step {
None => Some(Step::EnterMnemonic), match step {
Some(step) => { Step::EnterMnemonic => {
match step { if self.mnemonic_setup.mnemonic.mode == PhraseMode::Generate {
Step::EnterMnemonic => { Some(Step::ConfirmMnemonic)
if self.mnemonic_setup.mnemonic.mode == PhraseMode::Generate { } else {
Some(Step::ConfirmMnemonic) // Check if entered phrase was valid.
if self.mnemonic_setup.valid_phrase {
Some(Step::SetupConnection)
} else { } else {
// Check if entered phrase was valid. Some(Step::EnterMnemonic)
if self.mnemonic_setup.valid_phrase {
Some(Step::SetupConnection)
} else {
Some(Step::EnterMnemonic)
}
} }
} }
Step::ConfirmMnemonic => Some(Step::SetupConnection), }
Step::SetupConnection => { Step::ConfirmMnemonic => Some(Step::SetupConnection),
// Create wallet at last step. Step::SetupConnection => {
Wallets::create_wallet( // Create wallet at last step.
self.name_edit.clone(), let name = self.name_edit.clone();
self.pass_edit.clone(), let pass = self.pass_edit.clone();
self.mnemonic_setup.mnemonic.get_phrase(), let phrase = self.mnemonic_setup.mnemonic.get_phrase();
self.network_setup.get_ext_conn_url() let ext_conn = self.network_setup.get_ext_conn_url();
).unwrap(); let wallet = Wallet::create(name, pass.clone(), phrase, ext_conn).unwrap();
None // Open created wallet.
} wallet.open(pass).unwrap();
// Pass created wallet to callback.
(on_create.unwrap())(wallet);
// Reset creation data.
self.reset();
None
} }
} }
} } else {
Some(Step::EnterMnemonic)
};
} }
/// Start wallet creation from showing [`Modal`] to enter name and password. /// Start wallet creation from showing [`Modal`] to enter name and password.
@ -379,7 +389,7 @@ impl WalletCreation {
if self.name_edit.is_empty() || self.pass_edit.is_empty() { if self.name_edit.is_empty() || self.pass_edit.is_empty() {
return; return;
} }
self.forward(); self.step = Some(Step::EnterMnemonic);
cb.hide_keyboard(); cb.hide_keyboard();
modal.close(); modal.close();
}; };

View file

@ -18,7 +18,8 @@ use crate::gui::Colors;
use crate::gui::icons::PENCIL; use crate::gui::icons::PENCIL;
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, ModalPosition, Root, View}; use crate::gui::views::{Modal, ModalPosition, Root, View};
use crate::gui::views::wallets::creation::types::{Mnemonic, PhraseMode, PhraseSize}; use crate::wallet::Mnemonic;
use crate::wallet::types::{PhraseMode, PhraseSize};
/// Mnemonic phrase setup content. /// Mnemonic phrase setup content.
pub struct MnemonicSetup { pub struct MnemonicSetup {

View file

@ -12,10 +12,6 @@
// 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 grin_keychain::mnemonic::{from_entropy, search, to_entropy};
use rand::{Rng, thread_rng};
use zeroize::{Zeroize, ZeroizeOnDrop};
/// Wallet creation step. /// Wallet creation step.
#[derive(PartialEq)] #[derive(PartialEq)]
pub enum Step { pub enum Step {
@ -26,140 +22,3 @@ pub enum Step {
/// Wallet connection setup. /// Wallet connection setup.
SetupConnection SetupConnection
} }
/// Mnemonic phrase setup mode.
/// Will be completely cleaned from memory on drop.
#[derive(PartialEq, Clone, Zeroize, ZeroizeOnDrop)]
pub enum PhraseMode {
/// Generate new mnemonic phrase.
Generate,
/// Import existing mnemonic phrase.
Import
}
/// Mnemonic phrase size based on words count.
/// Will be completely cleaned from memory on drop.
#[derive(PartialEq, Clone, Zeroize, ZeroizeOnDrop)]
pub enum PhraseSize { Words12, Words15, Words18, Words21, Words24 }
impl PhraseSize {
pub const VALUES: [PhraseSize; 5] = [
PhraseSize::Words12,
PhraseSize::Words15,
PhraseSize::Words18,
PhraseSize::Words21,
PhraseSize::Words24
];
/// Gen words count number.
pub fn value(&self) -> usize {
match *self {
PhraseSize::Words12 => 12,
PhraseSize::Words15 => 15,
PhraseSize::Words18 => 18,
PhraseSize::Words21 => 21,
PhraseSize::Words24 => 24
}
}
/// Gen entropy size for current phrase size.
pub fn entropy_size(&self) -> usize {
match *self {
PhraseSize::Words12 => 16,
PhraseSize::Words15 => 20,
PhraseSize::Words18 => 24,
PhraseSize::Words21 => 28,
PhraseSize::Words24 => 32
}
}
}
/// Mnemonic phrase container.
/// Will be completely cleaned from memory on drop.
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct Mnemonic {
/// Phrase setup mode.
pub(crate) mode: PhraseMode,
/// Size of phrase based on words count.
pub(crate) size: PhraseSize,
/// Generated words.
pub(crate) words: Vec<String>,
/// Words to confirm the phrase.
pub(crate) confirm_words: Vec<String>
}
impl Default for Mnemonic {
fn default() -> Self {
let size = PhraseSize::Words24;
let mode = PhraseMode::Generate;
let words = Self::generate_words(&mode, &size);
let confirm_words = Self::empty_words(&size);
Self { mode, size, words, confirm_words }
}
}
impl Mnemonic {
/// Change mnemonic phrase setup [`PhraseMode`].
pub fn set_mode(&mut self, mode: PhraseMode) {
self.mode = mode;
self.words = Self::generate_words(&self.mode, &self.size);
self.confirm_words = Self::empty_words(&self.size);
}
/// Change mnemonic phrase words [`PhraseSize`].
pub fn set_size(&mut self, size: PhraseSize) {
self.size = size;
self.words = Self::generate_words(&self.mode, &self.size);
self.confirm_words = Self::empty_words(&self.size);
}
/// Check if provided word is in BIP39 format and equal to non-empty generated word at index.
pub fn is_valid_word(&self, word: &String, index: usize) -> bool {
let valid = search(word).is_ok();
let equal = if let Some(gen_word) = self.words.get(index) {
gen_word.is_empty() || gen_word == word
} else {
false
};
valid && equal
}
/// Check if current phrase is valid.
pub fn is_valid_phrase(&self) -> bool {
to_entropy(self.get_phrase().as_str()).is_ok()
}
/// Get phrase from words.
pub fn get_phrase(&self) -> String {
self.words.iter().map(|x| x.to_string() + " ").collect::<String>()
}
/// Generate list of words based on provided [`PhraseMode`] and [`PhraseSize`].
fn generate_words(mode: &PhraseMode, size: &PhraseSize) -> Vec<String> {
match mode {
PhraseMode::Generate => {
let mut rng = thread_rng();
let mut entropy: Vec<u8> = Vec::with_capacity(size.entropy_size());
for _ in 0..size.entropy_size() {
entropy.push(rng.gen());
}
from_entropy(&entropy).unwrap()
.split(" ")
.map(|s| String::from(s))
.collect::<Vec<String>>()
},
PhraseMode::Import => {
Self::empty_words(size)
}
}
}
/// Generate empty list of words based on provided [`PhraseSize`].
fn empty_words(size: &PhraseSize) -> Vec<String> {
let mut words = Vec::with_capacity(size.value());
for _ in 0..size.value() {
words.push(String::from(""))
}
words
}
}

View file

@ -13,8 +13,10 @@
// limitations under the License. // limitations under the License.
mod creation; mod creation;
mod wallet;
mod setup; mod setup;
mod content; mod content;
pub use content::*; pub use content::*;
mod wallet;
use wallet::WalletContent;

View file

@ -15,13 +15,12 @@
use egui::{Id, RichText, ScrollArea, TextStyle, Widget}; use egui::{Id, RichText, ScrollArea, TextStyle, Widget};
use url::Url; use url::Url;
use crate::AppConfig;
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::{GLOBE, GLOBE_SIMPLE}; use crate::gui::icons::{GLOBE, GLOBE_SIMPLE};
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::setup::ConnectionMethod; use crate::gui::views::wallets::setup::ConnectionMethod;
use crate::wallet::ExternalConnection; use crate::wallet::{ConnectionsConfig, ExternalConnection};
/// Wallet node connection method setup content. /// Wallet node connection method setup content.
pub struct ConnectionSetup { pub struct ConnectionSetup {
@ -106,7 +105,7 @@ impl ConnectionSetup {
ui.add_space(12.0); ui.add_space(12.0);
// Show external nodes URLs selection. // Show external nodes URLs selection.
for conn in AppConfig::external_connections() { for conn in ConnectionsConfig::external_connections() {
View::radio_value(ui, View::radio_value(ui,
&mut self.method, &mut self.method,
ConnectionMethod::External(conn.url.clone()), ConnectionMethod::External(conn.url.clone()),
@ -196,7 +195,7 @@ impl ConnectionSetup {
Some(self.ext_node_secret_edit.to_owned()) Some(self.ext_node_secret_edit.to_owned())
}; };
let ext_conn = ExternalConnection::new(url.clone(), secret); let ext_conn = ExternalConnection::new(url.clone(), secret);
AppConfig::add_external_connection(ext_conn); ConnectionsConfig::add_external_connection(ext_conn);
// Set added method as current. // Set added method as current.
self.method = ConnectionMethod::External(url); self.method = ConnectionMethod::External(url);

View file

@ -34,7 +34,7 @@ impl WalletContent {
pub fn ui(&mut self, pub fn ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
frame: &mut eframe::Frame, frame: &mut eframe::Frame,
wallet: &Wallet, wallet: &mut Wallet,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
// Show wallet content. // Show wallet content.
egui::CentralPanel::default() egui::CentralPanel::default()
@ -54,4 +54,6 @@ impl WalletContent {
//TODO: wallet content //TODO: wallet content
}); });
} }
} }

View file

@ -0,0 +1,21 @@
// 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.
mod content;
mod txs;
mod send;
mod settings;
mod receive;
pub use content::WalletContent;

View file

@ -0,0 +1,13 @@
// 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.

View file

@ -0,0 +1,13 @@
// 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.

View file

@ -0,0 +1,13 @@
// 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.

View file

@ -0,0 +1,13 @@
// 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.

View file

@ -24,7 +24,6 @@ use serde::{Deserialize, Serialize};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use crate::node::NodeConfig; use crate::node::NodeConfig;
use crate::wallet::{ExternalConnection, Wallets};
lazy_static! { lazy_static! {
/// Static settings state to be accessible globally. /// Static settings state to be accessible globally.
@ -41,36 +40,18 @@ pub struct AppConfig {
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 connections for wallets.
external_connections: Vec<ExternalConnection>
} }
impl Default for AppConfig { impl Default for AppConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
auto_start_node: false, auto_start_node: false,
chain_type: ChainTypes::default(), chain_type: ChainTypes::default()
external_connections: vec![
ExternalConnection::default()
],
} }
} }
} }
impl AppConfig { impl AppConfig {
/// 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::<AppConfig>(path.clone());
if !path.exists() || parsed.is_err() {
let default_config = AppConfig::default();
Settings::write_to_file(&default_config, path);
default_config
} else {
parsed.unwrap()
}
}
/// Save app config to file. /// 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));
@ -93,8 +74,6 @@ impl AppConfig {
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.
Wallets::reload(chain_type);
} }
} }
@ -117,42 +96,6 @@ 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 connections for the wallet.
pub fn external_connections() -> Vec<ExternalConnection> {
let r_config = Settings::app_config_to_read();
r_config.external_connections.clone()
}
/// Save external connection for the wallet in app config.
pub fn add_external_connection(conn: ExternalConnection) {
let mut w_config = Settings::app_config_to_update();
let mut exists = false;
for mut c in w_config.external_connections.iter_mut() {
// Update connection if URL exists.
if c.url == conn.url {
c.secret = conn.secret.clone();
exists = true;
break;
}
}
// Create new connection if URL not exists.
if !exists {
w_config.external_connections.insert(0, conn);
}
w_config.save();
}
/// Get external node connection secret from provided URL.
pub fn get_external_connection_secret(url: String) -> Option<String> {
let r_config = Settings::app_config_to_read();
for c in &r_config.external_connections {
if c.url == url {
return c.secret.clone();
}
}
None
}
} }
/// Main application directory name. /// Main application directory name.
@ -169,13 +112,27 @@ pub struct Settings {
impl Settings { impl Settings {
/// Initialize settings with app and node configs. /// Initialize settings with app and node configs.
fn init() -> Self { fn init() -> Self {
let app_config = AppConfig::init(); let path = Settings::get_config_path(APP_CONFIG_FILE_NAME, None);
let app_config = Self::init_config::<AppConfig>(path);
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)),
} }
} }
/// Initialize config from provided file path or load default if file not exists.
pub fn init_config<T: Default + Serialize + DeserializeOwned>(path: PathBuf) -> T {
let parsed = Self::read_from_file::<T>(path.clone());
if !path.exists() || !parsed.is_err() {
let default_config = T::default();
Settings::write_to_file(&default_config, path);
default_config
} else {
parsed.unwrap()
}
}
/// Get node config to read values. /// Get node config to read values.
pub fn node_config_to_read() -> RwLockReadGuard<'static, NodeConfig> { pub fn node_config_to_read() -> RwLockReadGuard<'static, NodeConfig> {
SETTINGS_STATE.node_config.read().unwrap() SETTINGS_STATE.node_config.read().unwrap()

View file

@ -19,7 +19,6 @@ use grin_core::global::ChainTypes;
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use crate::{AppConfig, Settings}; use crate::{AppConfig, Settings};
use crate::wallet::Wallets;
/// Wallet configuration. /// Wallet configuration.
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
@ -36,6 +35,8 @@ pub struct WalletConfig {
/// Wallet configuration file name. /// Wallet configuration file name.
const CONFIG_FILE_NAME: &'static str = "grim-wallet.toml"; const CONFIG_FILE_NAME: &'static str = "grim-wallet.toml";
/// Base wallets directory name.
pub const BASE_DIR_NAME: &'static str = "wallets";
impl WalletConfig { impl WalletConfig {
/// Create wallet config. /// Create wallet config.
@ -59,9 +60,20 @@ impl WalletConfig {
None None
} }
/// Get wallets base directory path for provided [`ChainTypes`].
pub fn get_base_path(chain_type: &ChainTypes) -> PathBuf {
let mut wallets_path = Settings::get_base_path(Some(chain_type.shortname()));
wallets_path.push(BASE_DIR_NAME);
// Create wallets base directory if it doesn't exist.
if !wallets_path.exists() {
let _ = fs::create_dir_all(wallets_path.clone());
}
wallets_path
}
/// Get config file path for provided [`ChainTypes`] and wallet identifier. /// Get config file path for provided [`ChainTypes`] and wallet identifier.
fn get_config_file_path(chain_type: &ChainTypes, id: i64) -> PathBuf { fn get_config_file_path(chain_type: &ChainTypes, id: i64) -> PathBuf {
let mut config_path = Wallets::get_base_path(chain_type); let mut config_path = Self::get_base_path(chain_type);
config_path.push(id.to_string()); config_path.push(id.to_string());
// Create if the config path doesn't exist. // Create if the config path doesn't exist.
if !config_path.exists() { if !config_path.exists() {
@ -74,7 +86,7 @@ impl WalletConfig {
/// Get current wallet data path. /// Get current wallet data path.
pub fn get_data_path(&self) -> String { pub fn get_data_path(&self) -> String {
let chain_type = AppConfig::chain_type(); let chain_type = AppConfig::chain_type();
let mut config_path = Wallets::get_base_path(&chain_type); let mut config_path = Self::get_base_path(&chain_type);
config_path.push(self.id.to_string()); config_path.push(self.id.to_string());
config_path.to_str().unwrap().to_string() config_path.to_str().unwrap().to_string()
} }

View file

@ -0,0 +1,93 @@
// 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::sync::{Arc, RwLock};
use lazy_static::lazy_static;
use serde_derive::{Deserialize, Serialize};
use crate::Settings;
use crate::wallet::ExternalConnection;
lazy_static! {
/// Static settings state to be accessible globally.
static ref CONNECTIONS_STATE: Arc<RwLock<ConnectionsConfig>> = Arc::new(
RwLock::new(
Settings::init_config(Settings::get_config_path(CONFIG_FILE_NAME, None))
)
);
}
/// Wallet connections configuration.
#[derive(Serialize, Deserialize, Clone)]
pub struct ConnectionsConfig {
/// URLs of external connections for wallets.
external: Vec<ExternalConnection>
}
/// Wallet configuration file name.
const CONFIG_FILE_NAME: &'static str = "connections.toml";
impl Default for ConnectionsConfig {
fn default() -> Self {
Self {
external: vec![
ExternalConnection::default()
],
}
}
}
impl ConnectionsConfig {
/// Save connections config to file.
pub fn save(&self) {
Settings::write_to_file(self, Settings::get_config_path(CONFIG_FILE_NAME, None));
}
/// Get external connections for the wallet.
pub fn external_connections() -> Vec<ExternalConnection> {
let r_config = CONNECTIONS_STATE.read().unwrap();
r_config.external.clone()
}
/// Save external connection for the wallet in app config.
pub fn add_external_connection(conn: ExternalConnection) {
let mut w_config = CONNECTIONS_STATE.write().unwrap();
let mut exists = false;
for mut c in w_config.external.iter_mut() {
// Update connection if URL exists.
if c.url == conn.url {
c.secret = conn.secret.clone();
exists = true;
break;
}
}
// Create new connection if URL not exists.
if !exists {
w_config.external.insert(0, conn);
}
w_config.save();
}
/// Get external node connection secret from provided URL.
pub fn get_external_connection_secret(url: String) -> Option<String> {
let r_config = CONNECTIONS_STATE.read().unwrap();
for c in &r_config.external {
if c.url == url {
return c.secret.clone();
}
}
None
}
}

View file

@ -0,0 +1,39 @@
// 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 serde_derive::{Deserialize, Serialize};
/// External node connection for the wallet.
#[derive(Serialize, Deserialize, Clone)]
pub struct ExternalConnection {
/// Node URL.
pub url: String,
/// Optional API secret key.
pub secret: Option<String>
}
impl ExternalConnection {
/// Default external node URL.
const DEFAULT_EXTERNAL_NODE_URL: &'static str = "https://grinnnode.live:3413";
pub fn new(url: String, secret: Option<String>) -> Self {
Self { url, secret }
}
}
impl Default for ExternalConnection {
fn default() -> Self {
Self { url: Self::DEFAULT_EXTERNAL_NODE_URL.to_string(), secret: None }
}
}

View file

@ -0,0 +1,19 @@
// 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.
mod external;
pub use external::ExternalConnection;
mod config;
pub use config::ConnectionsConfig;

106
src/wallet/mnemonic.rs Normal file
View file

@ -0,0 +1,106 @@
// 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 grin_keychain::mnemonic::{from_entropy, search, to_entropy};
use rand::{Rng, thread_rng};
use crate::wallet::types::{PhraseMode, PhraseSize};
/// Mnemonic phrase container.
pub struct Mnemonic {
/// Phrase setup mode.
pub(crate) mode: PhraseMode,
/// Size of phrase based on words count.
pub(crate) size: PhraseSize,
/// Generated words.
pub(crate) words: Vec<String>,
/// Words to confirm the phrase.
pub(crate) confirm_words: Vec<String>
}
impl Default for Mnemonic {
fn default() -> Self {
let size = PhraseSize::Words24;
let mode = PhraseMode::Generate;
let words = Self::generate_words(&mode, &size);
let confirm_words = Self::empty_words(&size);
Self { mode, size, words, confirm_words }
}
}
impl Mnemonic {
/// Change mnemonic phrase setup [`PhraseMode`].
pub fn set_mode(&mut self, mode: PhraseMode) {
self.mode = mode;
self.words = Self::generate_words(&self.mode, &self.size);
self.confirm_words = Self::empty_words(&self.size);
}
/// Change mnemonic phrase words [`PhraseSize`].
pub fn set_size(&mut self, size: PhraseSize) {
self.size = size;
self.words = Self::generate_words(&self.mode, &self.size);
self.confirm_words = Self::empty_words(&self.size);
}
/// Check if provided word is in BIP39 format and equal to non-empty generated word at index.
pub fn is_valid_word(&self, word: &String, index: usize) -> bool {
let valid = search(word).is_ok();
let equal = if let Some(gen_word) = self.words.get(index) {
gen_word.is_empty() || gen_word == word
} else {
false
};
valid && equal
}
/// Check if current phrase is valid.
pub fn is_valid_phrase(&self) -> bool {
to_entropy(self.get_phrase().as_str()).is_ok()
}
/// Get phrase from words.
pub fn get_phrase(&self) -> String {
self.words.iter().map(|x| x.to_string() + " ").collect::<String>()
}
/// Generate list of words based on provided [`PhraseMode`] and [`PhraseSize`].
fn generate_words(mode: &PhraseMode, size: &PhraseSize) -> Vec<String> {
match mode {
PhraseMode::Generate => {
let mut rng = thread_rng();
let mut entropy: Vec<u8> = Vec::with_capacity(size.entropy_size());
for _ in 0..size.entropy_size() {
entropy.push(rng.gen());
}
from_entropy(&entropy).unwrap()
.split(" ")
.map(|s| String::from(s))
.collect::<Vec<String>>()
},
PhraseMode::Import => {
Self::empty_words(size)
}
}
}
/// Generate empty list of words based on provided [`PhraseSize`].
fn empty_words(size: &PhraseSize) -> Vec<String> {
let mut words = Vec::with_capacity(size.value());
for _ in 0..size.value() {
words.push(String::from(""))
}
words
}
}

View file

@ -12,16 +12,20 @@
// 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.
pub mod types;
pub mod updater; pub mod updater;
pub mod selection; pub mod selection;
pub mod tx; pub mod tx;
pub mod keys; pub mod keys;
mod mnemonic;
pub use mnemonic::Mnemonic;
mod connections;
pub use connections::*;
mod wallets; mod wallets;
pub use wallets::{Wallet, Wallets}; pub use wallets::*;
mod config; mod config;
pub use config::*; pub use config::*;
mod types;
pub use types::*;

View file

@ -12,28 +12,47 @@
// 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 serde_derive::{Deserialize, Serialize}; /// Mnemonic phrase setup mode.
#[derive(PartialEq, Clone)]
/// External node connection for the wallet. pub enum PhraseMode {
#[derive(Serialize, Deserialize, Clone)] /// Generate new mnemonic phrase.
pub struct ExternalConnection { Generate,
/// Node URL. /// Import existing mnemonic phrase.
pub url: String, Import
/// Optional API secret key.
pub secret: Option<String>
} }
impl ExternalConnection { /// Mnemonic phrase size based on words count.
/// Default external node URL. #[derive(PartialEq, Clone)]
const DEFAULT_EXTERNAL_NODE_URL: &'static str = "https://grinnnode.live:3413"; pub enum PhraseSize { Words12, Words15, Words18, Words21, Words24 }
pub fn new(url: String, secret: Option<String>) -> Self { impl PhraseSize {
Self { url, secret } pub const VALUES: [PhraseSize; 5] = [
} PhraseSize::Words12,
} PhraseSize::Words15,
PhraseSize::Words18,
impl Default for ExternalConnection { PhraseSize::Words21,
fn default() -> Self { PhraseSize::Words24
Self { url: Self::DEFAULT_EXTERNAL_NODE_URL.to_string(), secret: None } ];
/// Gen words count number.
pub fn value(&self) -> usize {
match *self {
PhraseSize::Words12 => 12,
PhraseSize::Words15 => 15,
PhraseSize::Words18 => 18,
PhraseSize::Words21 => 21,
PhraseSize::Words24 => 24
}
}
/// Gen entropy size for current phrase size.
pub fn entropy_size(&self) -> usize {
match *self {
PhraseSize::Words12 => 16,
PhraseSize::Words15 => 20,
PhraseSize::Words18 => 24,
PhraseSize::Words21 => 28,
PhraseSize::Words24 => 32
}
} }
} }

View file

@ -12,10 +12,10 @@
// 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::collections::BTreeSet; use std::{cmp, thread};
use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::{Arc, RwLock}; use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use grin_core::global; use grin_core::global;
use grin_core::global::ChainTypes; use grin_core::global::ChainTypes;
@ -23,68 +23,40 @@ use grin_keychain::{ExtKeychain, Identifier, Keychain};
use grin_util::types::ZeroingString; use grin_util::types::ZeroingString;
use grin_wallet_api::{Foreign, ForeignCheckMiddlewareFn, Owner}; use grin_wallet_api::{Foreign, ForeignCheckMiddlewareFn, Owner};
use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl, HTTPNodeClient}; use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl, HTTPNodeClient};
use grin_wallet_libwallet::{Error, NodeClient, NodeVersionInfo, OutputStatus, Slate, slate_versions, SlatepackArmor, Slatepacker, SlatepackerArgs, TxLogEntry, wallet_lock, WalletBackend, WalletInfo, WalletInst, WalletLCProvider}; use grin_wallet_libwallet::{Error, NodeClient, NodeVersionInfo, OutputStatus, scan, Slate, slate_versions, SlatepackArmor, Slatepacker, SlatepackerArgs, TxLogEntry, wallet_lock, WalletBackend, WalletInfo, WalletInst, WalletLCProvider};
use grin_wallet_libwallet::Error::GenericError;
use lazy_static::lazy_static;
use log::debug; use log::debug;
use parking_lot::Mutex; use parking_lot::Mutex;
use uuid::Uuid; use uuid::Uuid;
use crate::{AppConfig, Settings}; use crate::AppConfig;
use crate::node::NodeConfig; use crate::node::NodeConfig;
use crate::wallet::{ConnectionsConfig, WalletConfig};
use crate::wallet::selection::lock_tx_context; use crate::wallet::selection::lock_tx_context;
use crate::wallet::tx::{add_inputs_to_slate, new_tx_slate}; use crate::wallet::tx::{add_inputs_to_slate, new_tx_slate};
use crate::wallet::updater::{cancel_tx, refresh_output_state, retrieve_txs}; use crate::wallet::updater::{cancel_tx, refresh_output_state, retrieve_txs};
use crate::wallet::WalletConfig;
lazy_static! { /// [`Wallet`] list wrapper.
/// Global wallets state.
static ref WALLETS_STATE: Arc<RwLock<Wallets>> = Arc::new(RwLock::new(Wallets::init()));
}
/// Manages [`Wallet`] list and state.
pub struct Wallets { pub struct Wallets {
/// List of wallets. /// List of wallets.
list: Vec<Wallet>, pub(crate) list: Vec<Wallet>,
/// Selected [`Wallet`] identifier. /// Selected [`Wallet`] identifier.
selected_id: Option<i64>, selected_id: Option<i64>,
/// Identifiers of opened wallets. }
opened_ids: BTreeSet<i64>
impl Default for Wallets {
fn default() -> Self {
Self {
list: Self::init(&AppConfig::chain_type()),
selected_id: None
}
}
} }
impl Wallets { impl Wallets {
/// Base wallets directory name. /// Initialize wallets from base directory for provided [`ChainType`].
pub const BASE_DIR_NAME: &'static str = "wallets"; fn init(chain_type: &ChainTypes) -> Vec<Wallet> {
/// Initialize manager by loading list of wallets into state.
fn init() -> Self {
Self {
list: Self::load_wallets(&AppConfig::chain_type()),
selected_id: None,
opened_ids: BTreeSet::default()
}
}
/// Create new wallet and add it to state.
pub fn create_wallet(
name: String,
password: String,
mnemonic: String,
external_node_url: Option<String>
)-> Result<(), Error> {
let wallet = Wallet::create(name, password, mnemonic, external_node_url)?;
let mut w_state = WALLETS_STATE.write().unwrap();
let id = wallet.config.id;
w_state.opened_ids.insert(id);
w_state.selected_id = Some(id);
w_state.list.insert(0, wallet);
Ok(())
}
/// Load wallets for provided [`ChainType`].
fn load_wallets(chain_type: &ChainTypes) -> Vec<Wallet> {
let mut wallets = Vec::new(); let mut wallets = Vec::new();
let wallets_dir = Self::get_base_path(chain_type); let wallets_dir = WalletConfig::get_base_path(chain_type);
// Load wallets from base directory. // Load wallets from base directory.
for dir in wallets_dir.read_dir().unwrap() { for dir in wallets_dir.read_dir().unwrap() {
let wallet_dir = dir.unwrap().path(); let wallet_dir = dir.unwrap().path();
@ -98,89 +70,118 @@ impl Wallets {
wallets wallets
} }
/// Get list of wallets. /// Reinitialize wallets for provided [`ChainTypes`].
pub fn list() -> Vec<Wallet> { pub fn reinit(&mut self, chain_type: &ChainTypes) {
let r_state = WALLETS_STATE.read().unwrap(); self.list = Self::init(chain_type);
r_state.list.clone()
} }
/// Select [`Wallet`] with provided identifier. /// Add created [`Wallet`] to the list.
pub fn select(id: Option<i64>) { pub fn add(&mut self, wallet: Wallet) {
let mut w_state = WALLETS_STATE.write().unwrap(); self.selected_id = Some(wallet.config.id);
w_state.selected_id = id; self.list.insert(0, wallet);
} }
/// Get selected [`Wallet`] identifier. /// Select wallet with provided identifier.
pub fn selected_id() -> Option<i64> { pub fn select(&mut self, id: Option<i64>) {
let r_state = WALLETS_STATE.read().unwrap(); self.selected_id = id;
r_state.selected_id
} }
/// Open [`Wallet`] with provided identifier and password. /// Check if wallet is selected for provided identifier.
pub fn open(id: i64, password: String) -> Result<(), Error> { pub fn is_selected(&self, id: i64) -> bool {
let list = Self::list(); return Some(id) == self.selected_id;
let mut w_state = WALLETS_STATE.write().unwrap(); }
for mut w in list {
if w.config.id == id { /// Check if selected wallet is open.
w.open(password)?; pub fn is_selected_open(&self) -> bool {
break; for w in &self.list {
if Some(w.config.id) == self.selected_id {
return w.is_open()
} }
} }
w_state.opened_ids.insert(id); false
Ok(())
} }
/// Close [`Wallet`] with provided identifier. /// Open selected wallet.
pub fn close(id: i64) -> Result<(), Error> { pub fn open_selected(&mut self, password: String) -> Result<(), Error> {
let list = Self::list(); for mut w in self.list.iter_mut() {
let mut w_state = WALLETS_STATE.write().unwrap(); if Some(w.config.id) == self.selected_id {
for mut w in list { return w.open(password);
if w.config.id == id {
w.close()?;
break;
} }
} }
w_state.opened_ids.remove(&id); Err(Error::GenericError("Wallet is not selected".to_string()))
Ok(())
} }
/// Check if [`Wallet`] with provided identifier was open. /// Load the wallet by scanning available outputs at separate thread.
pub fn is_open(id: i64) -> bool { pub fn load(w: &mut Wallet) {
let r_state = WALLETS_STATE.read().unwrap(); if !w.is_open() {
r_state.opened_ids.contains(&id) return;
}
/// Get wallets base directory path for provided [`ChainTypes`].
pub fn get_base_path(chain_type: &ChainTypes) -> PathBuf {
let mut wallets_path = Settings::get_base_path(Some(chain_type.shortname()));
wallets_path.push(Self::BASE_DIR_NAME);
// Create wallets base directory if it doesn't exist.
if !wallets_path.exists() {
let _ = fs::create_dir_all(wallets_path.clone());
} }
wallets_path let mut wallet = w.clone();
} thread::spawn(move || {
// Get pmmr range output indexes.
match wallet.pmmr_range() {
Ok((mut lowest_index, highest_index)) => {
println!("pmmr_range {} {}", lowest_index, highest_index);
let mut from_index = lowest_index;
loop {
// Scan outputs for last retrieved index.
println!("scan_outputs {} {}", from_index, highest_index);
match wallet.scan_outputs(from_index, highest_index) {
Ok(last_index) => {
println!("last_index {}", last_index);
if lowest_index == 0 {
lowest_index = last_index;
}
if last_index == highest_index {
wallet.loading_progress = 100;
break;
} else {
from_index = last_index;
}
/// Reload list of wallets for provided [`ChainTypes`]. // Update loading progress.
pub fn reload(chain_type: &ChainTypes) { let range = highest_index - lowest_index;
let wallets = Self::load_wallets(chain_type); let progress = last_index - lowest_index;
let mut w_state = WALLETS_STATE.write().unwrap(); wallet.loading_progress = cmp::min(
w_state.selected_id = None; (progress / range) as u8 * 100,
w_state.opened_ids = BTreeSet::default(); 99
w_state.list = wallets; );
println!("progress {}", wallet.loading_progress);
}
Err(e) => {
wallet.loading_error = Some(e);
break;
}
}
}
wallet.is_loaded.store(true, Ordering::Relaxed);
}
Err(e) => {
wallet.loading_error = Some(e);
}
}
});
} }
} }
/// Wallet instance and config wrapper. /// Contains wallet instance and config.
#[derive(Clone)] #[derive(Clone)]
pub struct Wallet { pub struct Wallet {
/// Wallet instance. /// Wallet instance.
instance: WalletInstance, instance: WalletInstance,
/// Wallet data path.
path: String,
/// Wallet configuration. /// Wallet configuration.
pub(crate) config: WalletConfig, pub(crate) config: WalletConfig,
/// Flag to check if wallet is open.
is_open: Arc<AtomicBool>,
/// Flag to check if wallet is loaded and ready to use.
is_loaded: Arc<AtomicBool>,
/// Error on wallet loading.
loading_error: Option<Error>,
/// Loading progress in percents
loading_progress: u8,
} }
/// Wallet instance type. /// Wallet instance type.
@ -198,37 +199,38 @@ type WalletInstance = Arc<
>; >;
impl Wallet { impl Wallet {
/// Create new wallet, make it open and selected. /// Create wallet from provided instance and config.
fn create( fn new(instance: WalletInstance, config: WalletConfig) -> Self {
Self {
instance,
config,
is_loaded: Arc::new(AtomicBool::new(false)),
is_open: Arc::new(AtomicBool::new(false)),
loading_error: None,
loading_progress: 0,
}
}
/// Create new wallet.
pub fn create(
name: String, name: String,
password: String, password: String,
mnemonic: String, mnemonic: String,
external_node_url: Option<String> external_node_url: Option<String>
) -> Result<Wallet, Error> { ) -> Result<Wallet, Error> {
let config = WalletConfig::create(name, external_node_url); let config = WalletConfig::create(name, external_node_url);
let wallet = Self::create_wallet_instance(config.clone())?; let instance = Self::create_wallet_instance(config.clone())?;
let w = Wallet { let w = Wallet::new(instance, config);
instance: wallet,
path: config.get_data_path(),
config,
};
{ {
let mut w_lock = w.instance.lock(); let mut w_lock = w.instance.lock();
let p = w_lock.lc_provider()?; let p = w_lock.lc_provider()?;
// Create wallet.
p.create_wallet(None, p.create_wallet(None,
Some(ZeroingString::from(mnemonic.clone())), Some(ZeroingString::from(mnemonic.clone())),
mnemonic.len(), mnemonic.len(),
ZeroingString::from(password.clone()), ZeroingString::from(password),
false, false,
)?; )?;
// Open wallet.
p.open_wallet(None, ZeroingString::from(password), false, false)?;
} }
Ok(w) Ok(w)
} }
@ -236,14 +238,20 @@ impl Wallet {
fn init(data_path: PathBuf) -> Option<Wallet> { fn init(data_path: PathBuf) -> Option<Wallet> {
let wallet_config = WalletConfig::load(data_path.clone()); let wallet_config = WalletConfig::load(data_path.clone());
if let Some(config) = wallet_config { if let Some(config) = wallet_config {
let path = data_path.to_str().unwrap().to_string();
if let Ok(instance) = Self::create_wallet_instance(config.clone()) { if let Ok(instance) = Self::create_wallet_instance(config.clone()) {
return Some(Self { instance, path, config }); return Some(Wallet::new(instance, config));
} }
} }
None None
} }
/// Reinitialize wallet instance to apply new config e.g. on change connection settings.
pub fn reinit(&mut self) -> Result<(), Error> {
self.close()?;
self.instance = Self::create_wallet_instance(self.config.clone())?;
Ok(())
}
/// Create wallet instance from provided config. /// Create wallet instance from provided config.
fn create_wallet_instance(config: WalletConfig) -> Result<WalletInstance, Error> { fn create_wallet_instance(config: WalletConfig) -> Result<WalletInstance, Error> {
// Assume global chain type has already been initialized. // Assume global chain type has already been initialized.
@ -257,9 +265,11 @@ impl Wallet {
// Setup node client. // Setup node client.
let (node_api_url, node_secret) = if let Some(url) = &config.external_node_url { let (node_api_url, node_secret) = if let Some(url) = &config.external_node_url {
(url.to_owned(), None) (url.to_owned(), ConnectionsConfig::get_external_connection_secret(url.to_owned()))
} else { } else {
(NodeConfig::get_api_address(), NodeConfig::get_api_secret()) let api_url = format!("http://{}", NodeConfig::get_api_address());
let api_secret = NodeConfig::get_foreign_api_secret();
(api_url, api_secret)
}; };
let node_client = HTTPNodeClient::new(&node_api_url, node_secret)?; let node_client = HTTPNodeClient::new(&node_api_url, node_secret)?;
@ -290,22 +300,69 @@ impl Wallet {
Ok(Arc::new(Mutex::new(wallet))) Ok(Arc::new(Mutex::new(wallet)))
} }
/// Open wallet. /// Open wallet instance.
fn open(&mut self, password: String) -> Result<(), Error> { pub fn open(&self, password: String) -> Result<(), Error> {
let mut wallet_lock = self.instance.lock();
let lc = wallet_lock.lc_provider()?;
lc.open_wallet(None, ZeroingString::from(password), false, false)?;
Ok(())
}
/// Close wallet.
fn close(&mut self) -> Result<(), Error> {
let mut wallet_lock = self.instance.lock(); let mut wallet_lock = self.instance.lock();
let lc = wallet_lock.lc_provider()?; let lc = wallet_lock.lc_provider()?;
lc.close_wallet(None)?; lc.close_wallet(None)?;
lc.open_wallet(None, ZeroingString::from(password), false, false)?;
self.is_open.store(true, Ordering::Relaxed);
Ok(()) Ok(())
} }
/// Check if wallet is open.
pub fn is_open(&self) -> bool {
self.is_open.load(Ordering::Relaxed)
}
/// Close wallet.
pub fn close(&mut self) -> Result<(), Error> {
if self.is_open() {
let mut wallet_lock = self.instance.lock();
let lc = wallet_lock.lc_provider()?;
lc.close_wallet(None)?;
self.is_open.store(false, Ordering::Relaxed);
}
Ok(())
}
/// Scan wallet outputs to check/repair the wallet.
fn scan_outputs(
&self,
last_retrieved_index: u64,
highest_index: u64
) -> Result<u64, Error> {
let wallet = self.instance.clone();
let info = scan(
wallet.clone(),
None,
false,
last_retrieved_index,
highest_index,
&None,
)?;
let result = info.last_pmmr_index;
let parent_key_id = {
wallet_lock!(wallet.clone(), w);
w.parent_key_id().clone()
};
{
wallet_lock!(wallet, w);
let mut batch = w.batch(None)?;
batch.save_last_confirmed_height(&parent_key_id, info.height)?;
batch.commit()?;
};
Ok(result)
}
/// Get pmmr indices representing the outputs for the wallet.
fn pmmr_range(&self) -> Result<(u64, u64), Error> {
wallet_lock!(self.instance.clone(), w);
let pmmr_range = w.w2n_client().height_range_to_pmmr_indices(0, None)?;
Ok(pmmr_range)
}
/// Create transaction. /// Create transaction.
pub fn tx_create( pub fn tx_create(
&self, &self,
@ -463,10 +520,10 @@ impl Wallet {
let tx_uuid = Uuid::parse_str(tx_slate_id).unwrap(); let tx_uuid = Uuid::parse_str(tx_slate_id).unwrap();
let (_, txs) = api.retrieve_txs(None, true, None, Some(tx_uuid.clone()), None)?; let (_, txs) = api.retrieve_txs(None, true, None, Some(tx_uuid.clone()), None)?;
if txs[0].confirmed { if txs[0].confirmed {
return Err(Error::from(GenericError(format!( return Err(Error::GenericError(format!(
"Transaction with id {} is already confirmed. Not posting.", "Transaction with id {} is already confirmed. Not posting.",
tx_slate_id tx_slate_id
)))); )));
} }
let stored_tx = api.get_stored_tx(None, None, Some(&tx_uuid))?; let stored_tx = api.get_stored_tx(None, None, Some(&tx_uuid))?;
match stored_tx { match stored_tx {
@ -474,10 +531,10 @@ impl Wallet {
api.post_tx(None, &stored_tx, true)?; api.post_tx(None, &stored_tx, true)?;
Ok(()) Ok(())
} }
None => Err(Error::from(GenericError(format!( None => Err(Error::GenericError(format!(
"Transaction with id {} does not have transaction data. Not posting.", "Transaction with id {} does not have transaction data. Not posting.",
tx_slate_id tx_slate_id
)))), ))),
} }
} }