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",
"wgpu",
"winit",
"zeroize",
]
[[package]]

View file

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

View file

@ -29,6 +29,7 @@ wallets:
wrong_pass: Entered password is wrong
locked: Locked
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:
self: Network
node: Integrated node

View file

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

View file

@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::RwLock;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::RwLock;
use egui::{Align2, RichText, Rounding, Stroke, Vec2};
use egui::epaint::RectShape;
@ -167,17 +167,17 @@ impl Modal {
}
/// 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 modal.is_open() {
modal.window_ui(ui, add_content);
modal.window_ui(ctx, add_content);
}
}
}
/// Draw [`egui::Window`] with provided content.
fn window_ui(&self, ui: &mut egui::Ui, add_content: impl FnOnce(&mut egui::Ui, &Modal)) {
let rect = ui.ctx().screen_rect();
fn window_ui(&self, ctx: &egui::Context, add_content: impl FnOnce(&mut egui::Ui, &Modal)) {
let rect = ctx.screen_rect();
egui::Window::new("modal_bg_window")
.title_bar(false)
.resizable(false)
@ -187,13 +187,13 @@ impl Modal {
fill: Colors::SEMI_TRANSPARENT,
..Default::default()
})
.show(ui.ctx(), |ui| {
.show(ctx, |ui| {
ui.set_min_size(rect.size());
});
// Setup width of modal content.
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);
// Show main content Window at given position.
@ -209,7 +209,7 @@ impl Modal {
fill: Colors::YELLOW,
..Default::default()
})
.show(ui.ctx(), |ui| {
.show(ctx, |ui| {
if self.title.is_some() {
self.title_ui(ui);
}
@ -217,7 +217,7 @@ impl Modal {
}).unwrap().response.layer_id;
// 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) {
// Show modal content for current ui container.
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);
});
}

View file

@ -149,7 +149,7 @@ impl Root {
/// Draw exit confirmation modal content.
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 !Node::is_running() {
self.exit(frame);

View file

@ -14,18 +14,25 @@
use egui::{Align, Align2, Layout, Margin, RichText, Rounding, ScrollArea, TextStyle, Widget};
use egui_extras::{Size, StripBuilder};
use grin_core::global::ChainTypes;
use crate::AppConfig;
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::platform::PlatformCallbacks;
use crate::gui::views::{Modal, ModalContainer, ModalPosition, Root, TitlePanel, TitleType, View};
use crate::gui::views::wallets::creation::{MnemonicSetup, WalletCreation};
use crate::gui::views::wallets::setup::ConnectionSetup;
use crate::gui::views::wallets::wallet::WalletContent;
use crate::gui::views::wallets::WalletContent;
use crate::wallet::{Wallet, Wallets};
/// Wallets content.
pub struct WalletsContent {
/// Chain type for loaded wallets.
chain_type: ChainTypes,
/// Loaded list of wallets.
wallets: Wallets,
/// Password to open wallet for [`Modal`].
pass_edit: String,
/// Flag to show/hide password at [`egui::TextEdit`] field.
@ -48,6 +55,8 @@ pub struct WalletsContent {
impl Default for WalletsContent {
fn default() -> Self {
Self {
chain_type: AppConfig::chain_type(),
wallets: Wallets::default(),
pass_edit: "".to_string(),
hide_pass: true,
wrong_pass: false,
@ -72,12 +81,12 @@ impl ModalContainer for WalletsContent {
impl WalletsContent {
/// 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) {
// Show modal content for current ui container.
if self.can_draw_modal() {
Modal::ui(ui, |ui, modal| {
Modal::ui(ui.ctx(), |ui, modal| {
match modal.id {
Self::OPEN_WALLET_MODAL => {
self.open_wallet_modal_ui(ui, modal, cb);
@ -96,17 +105,17 @@ impl WalletsContent {
});
}
// Get wallets.
let wallets = Wallets::list();
let empty_list = wallets.is_empty();
// Setup list of wallets if chain type was changed.
let chain_type = AppConfig::chain_type();
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.
let create_wallet = self.creation_content.can_go_back();
let show_wallet = if let Some(id) = Wallets::selected_id() {
Wallets::is_open(id)
} else {
false
};
let show_wallet = self.wallets.is_selected_open();
// Setup panels parameters.
let dual_panel = is_dual_panel_mode(ui, frame);
@ -135,15 +144,17 @@ impl WalletsContent {
if available_width_zero {
return;
}
if create_wallet || !show_wallet {
// Show wallet creation content
self.creation_content.ui(ui, cb);
// Show wallet creation content.
self.creation_content.ui(ui, cb, |wallet| {
// Add created wallet to list.
self.wallets.add(wallet);
});
} else {
for wallet in wallets.iter() {
for mut wallet in self.wallets.list.clone() {
// Show content for selected wallet.
if Some(wallet.config.id) == Wallets::selected_id() {
self.wallet_content.ui(ui, frame, &wallet, cb);
if self.wallets.is_selected(wallet.config.id) {
self.wallet_content.ui(ui, frame, &mut wallet, cb);
break;
}
}
@ -189,7 +200,7 @@ impl WalletsContent {
= (!show_wallet && !dual_panel) || (dual_panel && show_wallet);
// 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 {
// Setup right margin for button.
@ -221,7 +232,7 @@ impl WalletsContent {
TitlePanel::ui(title_content, |ui, frame| {
if show_wallet && !dual_panel {
View::title_button(ui, ARROW_LEFT, || {
Wallets::select(None);
self.wallets.select(None);
});
} else if create_wallet {
View::title_button(ui, ARROW_LEFT, || {
@ -277,7 +288,6 @@ impl WalletsContent {
ui: &mut egui::Ui,
dual_panel: bool,
show_creation_btn: bool,
wallets: &Vec<Wallet>,
cb: &dyn PlatformCallbacks) -> bool {
let mut scroller_showing = false;
ui.scope(|ui| {
@ -303,18 +313,14 @@ impl WalletsContent {
rect.set_width(width);
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.
self.wallet_item_ui(ui, w, cb);
// Add space after last item.
let last_item = index == wallets.len() - 1;
if !last_item {
ui.add_space(5.0);
}
// Add space for wallet creation button.
if show_creation_btn && last_item {
ui.add_space(57.0);
}
self.wallet_item_ui(ui, &mut wallet, cb);
ui.add_space(5.0);
}
// Add space for wallet creation button.
if show_creation_btn {
ui.add_space(52.0);
}
});
});
@ -326,11 +332,13 @@ impl WalletsContent {
}
/// 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 is_selected = Some(id) == Wallets::selected_id();
let is_open = Wallets::is_open(id);
let is_current = is_open && is_selected;
let is_selected = self.wallets.is_selected(id);
let is_current = wallet.is_open() && is_selected;
// Draw round background.
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.active.rounding = Rounding::same(8.0);
if !is_open {
if !wallet.is_open() {
// Show button to open closed wallet.
View::item_button(ui, [false, true], FOLDER_OPEN, || {
Wallets::select(Some(id));
self.wallets.select(Some(id));
self.show_open_wallet_modal(cb);
});
} else if !is_selected {
// Show button to select opened wallet.
View::item_button(ui, [false, true], CARET_RIGHT, || {
Wallets::select(Some(id));
self.wallets.select(Some(id));
});
// Show button to close opened wallet.
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);
// 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"))
} else {
format!("{} {}", FOLDER_LOCK, t!("wallets.locked"))
@ -497,23 +505,18 @@ impl WalletsContent {
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
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.
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
// Callback for continue button.
// Callback for button to continue.
let mut on_continue = || {
if self.pass_edit.is_empty() {
return;
}
let selected_id = Wallets::selected_id().unwrap();
match Wallets::open(selected_id, self.pass_edit.clone()) {
match self.wallets.open_selected(self.pass_edit.clone()) {
Ok(_) => {
// Clear values.
self.pass_edit = "".to_string();
@ -526,6 +529,7 @@ impl WalletsContent {
Err(_) => self.wrong_pass = true
}
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
(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::views::{Modal, ModalPosition, View};
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::wallet::Wallets;
use crate::wallet::types::PhraseMode;
use crate::wallet::Wallet;
/// Wallet creation content.
pub struct WalletCreation {
@ -61,7 +62,7 @@ impl Default for WalletCreation {
logo: RetainedImage::from_image_bytes(
"logo.png",
include_bytes!("../../../../../img/logo.png"),
).unwrap(),
).unwrap()
}
}
}
@ -70,7 +71,10 @@ impl WalletCreation {
/// Wallet name/password input modal identifier.
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.
if self.step.is_some() {
egui::TopBottomPanel::bottom("wallet_creation_step_panel")
@ -87,7 +91,7 @@ impl WalletCreation {
})
.show_inside(ui, |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.
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 {
// Setup step description text and availability.
let (step_text, mut step_available) = match step {
@ -164,7 +176,7 @@ impl WalletCreation {
ui.add_space(4.0);
// Show button.
View::button(ui, next_text.to_uppercase(), color, || {
self.forward();
self.forward(Some(on_create));
});
ui.add_space(4.0);
}
@ -223,13 +235,7 @@ impl WalletCreation {
None => {}
Some(step) => {
match step {
Step::EnterMnemonic => {
// 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::EnterMnemonic => self.reset(),
Step::ConfirmMnemonic => 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`].
fn forward(&mut self) {
self.step = match &self.step {
None => Some(Step::EnterMnemonic),
Some(step) => {
match step {
Step::EnterMnemonic => {
if self.mnemonic_setup.mnemonic.mode == PhraseMode::Generate {
Some(Step::ConfirmMnemonic)
fn forward(&mut self, on_create: Option<impl FnOnce(Wallet)>) {
self.step = if let Some(step) = &self.step {
match step {
Step::EnterMnemonic => {
if self.mnemonic_setup.mnemonic.mode == PhraseMode::Generate {
Some(Step::ConfirmMnemonic)
} else {
// Check if entered phrase was valid.
if self.mnemonic_setup.valid_phrase {
Some(Step::SetupConnection)
} else {
// Check if entered phrase was valid.
if self.mnemonic_setup.valid_phrase {
Some(Step::SetupConnection)
} else {
Some(Step::EnterMnemonic)
}
Some(Step::EnterMnemonic)
}
}
Step::ConfirmMnemonic => Some(Step::SetupConnection),
Step::SetupConnection => {
// Create wallet at last step.
Wallets::create_wallet(
self.name_edit.clone(),
self.pass_edit.clone(),
self.mnemonic_setup.mnemonic.get_phrase(),
self.network_setup.get_ext_conn_url()
).unwrap();
None
}
}
Step::ConfirmMnemonic => Some(Step::SetupConnection),
Step::SetupConnection => {
// Create wallet at last step.
let name = self.name_edit.clone();
let pass = self.pass_edit.clone();
let phrase = self.mnemonic_setup.mnemonic.get_phrase();
let ext_conn = self.network_setup.get_ext_conn_url();
let wallet = Wallet::create(name, pass.clone(), phrase, ext_conn).unwrap();
// 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.
@ -379,7 +389,7 @@ impl WalletCreation {
if self.name_edit.is_empty() || self.pass_edit.is_empty() {
return;
}
self.forward();
self.step = Some(Step::EnterMnemonic);
cb.hide_keyboard();
modal.close();
};

View file

@ -18,7 +18,8 @@ use crate::gui::Colors;
use crate::gui::icons::PENCIL;
use crate::gui::platform::PlatformCallbacks;
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.
pub struct MnemonicSetup {

View file

@ -12,10 +12,6 @@
// 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 zeroize::{Zeroize, ZeroizeOnDrop};
/// Wallet creation step.
#[derive(PartialEq)]
pub enum Step {
@ -25,141 +21,4 @@ pub enum Step {
ConfirmMnemonic,
/// Wallet connection setup.
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.
mod creation;
mod wallet;
mod setup;
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 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, ModalPosition, View};
use crate::gui::views::wallets::setup::ConnectionMethod;
use crate::wallet::ExternalConnection;
use crate::wallet::{ConnectionsConfig, ExternalConnection};
/// Wallet node connection method setup content.
pub struct ConnectionSetup {
@ -106,7 +105,7 @@ impl ConnectionSetup {
ui.add_space(12.0);
// Show external nodes URLs selection.
for conn in AppConfig::external_connections() {
for conn in ConnectionsConfig::external_connections() {
View::radio_value(ui,
&mut self.method,
ConnectionMethod::External(conn.url.clone()),
@ -196,7 +195,7 @@ impl ConnectionSetup {
Some(self.ext_node_secret_edit.to_owned())
};
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.
self.method = ConnectionMethod::External(url);

View file

@ -34,7 +34,7 @@ impl WalletContent {
pub fn ui(&mut self,
ui: &mut egui::Ui,
frame: &mut eframe::Frame,
wallet: &Wallet,
wallet: &mut Wallet,
cb: &dyn PlatformCallbacks) {
// Show wallet content.
egui::CentralPanel::default()
@ -54,4 +54,6 @@ impl WalletContent {
//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 crate::node::NodeConfig;
use crate::wallet::{ExternalConnection, Wallets};
lazy_static! {
/// Static settings state to be accessible globally.
@ -41,36 +40,18 @@ pub struct AppConfig {
pub auto_start_node: bool,
/// Chain type for node and wallets.
chain_type: ChainTypes,
/// URLs of external connections for wallets.
external_connections: Vec<ExternalConnection>
}
impl Default for AppConfig {
fn default() -> Self {
Self {
auto_start_node: false,
chain_type: ChainTypes::default(),
external_connections: vec![
ExternalConnection::default()
],
chain_type: ChainTypes::default()
}
}
}
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.
pub fn save(&self) {
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.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.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.
@ -169,13 +112,27 @@ pub struct Settings {
impl Settings {
/// Initialize settings with app and node configs.
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 {
node_config: Arc::new(RwLock::new(NodeConfig::for_chain_type(&app_config.chain_type))),
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.
pub fn node_config_to_read() -> RwLockReadGuard<'static, NodeConfig> {
SETTINGS_STATE.node_config.read().unwrap()

View file

@ -19,7 +19,6 @@ use grin_core::global::ChainTypes;
use serde_derive::{Deserialize, Serialize};
use crate::{AppConfig, Settings};
use crate::wallet::Wallets;
/// Wallet configuration.
#[derive(Serialize, Deserialize, Clone)]
@ -36,6 +35,8 @@ pub struct WalletConfig {
/// Wallet configuration file name.
const CONFIG_FILE_NAME: &'static str = "grim-wallet.toml";
/// Base wallets directory name.
pub const BASE_DIR_NAME: &'static str = "wallets";
impl WalletConfig {
/// Create wallet config.
@ -59,9 +60,20 @@ impl WalletConfig {
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.
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());
// Create if the config path doesn't exist.
if !config_path.exists() {
@ -74,7 +86,7 @@ impl WalletConfig {
/// Get current wallet data path.
pub fn get_data_path(&self) -> String {
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.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
// limitations under the License.
pub mod types;
pub mod updater;
pub mod selection;
pub mod tx;
pub mod keys;
mod mnemonic;
pub use mnemonic::Mnemonic;
mod connections;
pub use connections::*;
mod wallets;
pub use wallets::{Wallet, Wallets};
pub use wallets::*;
mod config;
pub use config::*;
mod types;
pub use types::*;
pub use config::*;

View file

@ -12,28 +12,47 @@
// 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>
/// Mnemonic phrase setup mode.
#[derive(PartialEq, Clone)]
pub enum PhraseMode {
/// Generate new mnemonic phrase.
Generate,
/// Import existing mnemonic phrase.
Import
}
impl ExternalConnection {
/// Default external node URL.
const DEFAULT_EXTERNAL_NODE_URL: &'static str = "https://grinnnode.live:3413";
/// Mnemonic phrase size based on words count.
#[derive(PartialEq, Clone)]
pub enum PhraseSize { Words12, Words15, Words18, Words21, Words24 }
pub fn new(url: String, secret: Option<String>) -> Self {
Self { url, secret }
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
}
}
}
impl Default for ExternalConnection {
fn default() -> Self {
Self { url: Self::DEFAULT_EXTERNAL_NODE_URL.to_string(), secret: None }
/// 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
// limitations under the License.
use std::collections::BTreeSet;
use std::fs;
use std::{cmp, thread};
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::ChainTypes;
@ -23,68 +23,40 @@ use grin_keychain::{ExtKeychain, Identifier, Keychain};
use grin_util::types::ZeroingString;
use grin_wallet_api::{Foreign, ForeignCheckMiddlewareFn, Owner};
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::GenericError;
use lazy_static::lazy_static;
use grin_wallet_libwallet::{Error, NodeClient, NodeVersionInfo, OutputStatus, scan, Slate, slate_versions, SlatepackArmor, Slatepacker, SlatepackerArgs, TxLogEntry, wallet_lock, WalletBackend, WalletInfo, WalletInst, WalletLCProvider};
use log::debug;
use parking_lot::Mutex;
use uuid::Uuid;
use crate::{AppConfig, Settings};
use crate::AppConfig;
use crate::node::NodeConfig;
use crate::wallet::{ConnectionsConfig, WalletConfig};
use crate::wallet::selection::lock_tx_context;
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::WalletConfig;
lazy_static! {
/// Global wallets state.
static ref WALLETS_STATE: Arc<RwLock<Wallets>> = Arc::new(RwLock::new(Wallets::init()));
}
/// Manages [`Wallet`] list and state.
/// [`Wallet`] list wrapper.
pub struct Wallets {
/// List of wallets.
list: Vec<Wallet>,
pub(crate) list: Vec<Wallet>,
/// Selected [`Wallet`] identifier.
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 {
/// Base wallets directory name.
pub const BASE_DIR_NAME: &'static str = "wallets";
/// 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> {
/// Initialize wallets from base directory for provided [`ChainType`].
fn init(chain_type: &ChainTypes) -> Vec<Wallet> {
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.
for dir in wallets_dir.read_dir().unwrap() {
let wallet_dir = dir.unwrap().path();
@ -98,89 +70,118 @@ impl Wallets {
wallets
}
/// Get list of wallets.
pub fn list() -> Vec<Wallet> {
let r_state = WALLETS_STATE.read().unwrap();
r_state.list.clone()
/// Reinitialize wallets for provided [`ChainTypes`].
pub fn reinit(&mut self, chain_type: &ChainTypes) {
self.list = Self::init(chain_type);
}
/// Select [`Wallet`] with provided identifier.
pub fn select(id: Option<i64>) {
let mut w_state = WALLETS_STATE.write().unwrap();
w_state.selected_id = id;
/// Add created [`Wallet`] to the list.
pub fn add(&mut self, wallet: Wallet) {
self.selected_id = Some(wallet.config.id);
self.list.insert(0, wallet);
}
/// Get selected [`Wallet`] identifier.
pub fn selected_id() -> Option<i64> {
let r_state = WALLETS_STATE.read().unwrap();
r_state.selected_id
/// Select wallet with provided identifier.
pub fn select(&mut self, id: Option<i64>) {
self.selected_id = id;
}
/// Open [`Wallet`] with provided identifier and password.
pub fn open(id: i64, password: String) -> Result<(), Error> {
let list = Self::list();
let mut w_state = WALLETS_STATE.write().unwrap();
for mut w in list {
if w.config.id == id {
w.open(password)?;
break;
/// Check if wallet is selected for provided identifier.
pub fn is_selected(&self, id: i64) -> bool {
return Some(id) == self.selected_id;
}
/// Check if selected wallet is open.
pub fn is_selected_open(&self) -> bool {
for w in &self.list {
if Some(w.config.id) == self.selected_id {
return w.is_open()
}
}
w_state.opened_ids.insert(id);
Ok(())
false
}
/// Close [`Wallet`] with provided identifier.
pub fn close(id: i64) -> Result<(), Error> {
let list = Self::list();
let mut w_state = WALLETS_STATE.write().unwrap();
for mut w in list {
if w.config.id == id {
w.close()?;
break;
/// Open selected wallet.
pub fn open_selected(&mut self, password: String) -> Result<(), Error> {
for mut w in self.list.iter_mut() {
if Some(w.config.id) == self.selected_id {
return w.open(password);
}
}
w_state.opened_ids.remove(&id);
Ok(())
Err(Error::GenericError("Wallet is not selected".to_string()))
}
/// Check if [`Wallet`] with provided identifier was open.
pub fn is_open(id: i64) -> bool {
let r_state = WALLETS_STATE.read().unwrap();
r_state.opened_ids.contains(&id)
}
/// 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());
/// Load the wallet by scanning available outputs at separate thread.
pub fn load(w: &mut Wallet) {
if !w.is_open() {
return;
}
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`].
pub fn reload(chain_type: &ChainTypes) {
let wallets = Self::load_wallets(chain_type);
let mut w_state = WALLETS_STATE.write().unwrap();
w_state.selected_id = None;
w_state.opened_ids = BTreeSet::default();
w_state.list = wallets;
// Update loading progress.
let range = highest_index - lowest_index;
let progress = last_index - lowest_index;
wallet.loading_progress = cmp::min(
(progress / range) as u8 * 100,
99
);
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)]
pub struct Wallet {
/// Wallet instance.
instance: WalletInstance,
/// Wallet data path.
path: String,
/// Wallet configuration.
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.
@ -198,37 +199,38 @@ type WalletInstance = Arc<
>;
impl Wallet {
/// Create new wallet, make it open and selected.
fn create(
/// Create wallet from provided instance and config.
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,
password: String,
mnemonic: String,
external_node_url: Option<String>
) -> Result<Wallet, Error> {
let config = WalletConfig::create(name, external_node_url);
let wallet = Self::create_wallet_instance(config.clone())?;
let w = Wallet {
instance: wallet,
path: config.get_data_path(),
config,
};
let instance = Self::create_wallet_instance(config.clone())?;
let w = Wallet::new(instance, config);
{
let mut w_lock = w.instance.lock();
let p = w_lock.lc_provider()?;
// Create wallet.
p.create_wallet(None,
Some(ZeroingString::from(mnemonic.clone())),
mnemonic.len(),
ZeroingString::from(password.clone()),
ZeroingString::from(password),
false,
)?;
// Open wallet.
p.open_wallet(None, ZeroingString::from(password), false, false)?;
}
Ok(w)
}
@ -236,14 +238,20 @@ impl Wallet {
fn init(data_path: PathBuf) -> Option<Wallet> {
let wallet_config = WalletConfig::load(data_path.clone());
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()) {
return Some(Self { instance, path, config });
return Some(Wallet::new(instance, config));
}
}
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.
fn create_wallet_instance(config: WalletConfig) -> Result<WalletInstance, Error> {
// Assume global chain type has already been initialized.
@ -257,9 +265,11 @@ impl Wallet {
// Setup node client.
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 {
(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)?;
@ -290,22 +300,69 @@ impl Wallet {
Ok(Arc::new(Mutex::new(wallet)))
}
/// Open wallet.
fn open(&mut 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> {
/// Open wallet instance.
pub fn open(&self, password: String) -> Result<(), Error> {
let mut wallet_lock = self.instance.lock();
let lc = wallet_lock.lc_provider()?;
lc.close_wallet(None)?;
lc.open_wallet(None, ZeroingString::from(password), false, false)?;
self.is_open.store(true, Ordering::Relaxed);
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.
pub fn tx_create(
&self,
@ -463,10 +520,10 @@ impl Wallet {
let tx_uuid = Uuid::parse_str(tx_slate_id).unwrap();
let (_, txs) = api.retrieve_txs(None, true, None, Some(tx_uuid.clone()), None)?;
if txs[0].confirmed {
return Err(Error::from(GenericError(format!(
return Err(Error::GenericError(format!(
"Transaction with id {} is already confirmed. Not posting.",
tx_slate_id
))));
)));
}
let stored_tx = api.get_stored_tx(None, None, Some(&tx_uuid))?;
match stored_tx {
@ -474,10 +531,10 @@ impl Wallet {
api.post_tx(None, &stored_tx, true)?;
Ok(())
}
None => Err(Error::from(GenericError(format!(
None => Err(Error::GenericError(format!(
"Transaction with id {} does not have transaction data. Not posting.",
tx_slate_id
)))),
))),
}
}