diff --git a/Cargo.lock b/Cargo.lock index 4f63e11..8416ce1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2432,7 +2432,7 @@ dependencies = [ [[package]] name = "grin-gui" -version = "0.1.0-alpha.5" +version = "0.1.0-alpha.6" dependencies = [ "anyhow", "async-std", @@ -2479,7 +2479,7 @@ dependencies = [ [[package]] name = "grin-gui-core" -version = "0.1.0-alpha.5" +version = "0.1.0-alpha.6" dependencies = [ "async-std", "backtrace", @@ -2537,7 +2537,7 @@ dependencies = [ [[package]] name = "grin-gui-widgets" -version = "0.1.0-alpha.5" +version = "0.1.0-alpha.6" dependencies = [ "iced", "iced_core", diff --git a/Cargo.toml b/Cargo.toml index 5ba3aac..5934778 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "grin-gui" -version = "0.1.0-alpha.5" +version = "0.1.0-alpha.6" authors = ["Grin Developers "] description = "GUI wrapping grin and grin-wallet. Simple, private and scalable cryptocurrency implementation based on the MimbleWimble chain format." license = "Apache-2.0" @@ -16,8 +16,8 @@ no-self-update = ["grin-gui-core/no-self-update"] debug = ["iced/debug"] [dependencies] -grin-gui-core = { version = "0.1.0-alpha.5", path = "crates/core", features = ["wgpu"]} -grin-gui-widgets = { version = "0.1.0-alpha.5", path = "crates/widgets" } +grin-gui-core = { version = "0.1.0-alpha.6", path = "crates/core", features = ["wgpu"]} +grin-gui-widgets = { version = "0.1.0-alpha.6", path = "crates/widgets" } iced = { version = "0.10", features = ["canvas", "tokio"] } iced_futures = { version = "0.7", features = ["async-std"] } diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index d2b37db..b971d14 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "grin-gui-core" description = "Core GUI library for Grin GUI" -version = "0.1.0-alpha.5" +version = "0.1.0-alpha.6" authors = ["Yeastplume", "Casper Rogild Storm"] license = "GPL-3.0" homepage = "https://github.com/mimblewimble/grin-gui" diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index df7e353..dd2e141 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -4,199 +4,201 @@ use grin_wallet_controller; #[derive(thiserror::Error, Debug)] pub enum GrinWalletInterfaceError { - #[error(transparent)] - WalletController(#[from] grin_wallet_controller::Error), - #[error(transparent)] - WalletLibWallet(#[from] grin_wallet_libwallet::Error), - #[error("Owner API not Instantiated")] - OwnerAPINotInstantiated, - #[error("Foreign API not Instantiated")] - ForeignAPINotInstantiated, - #[error("Invalid Slatepack Address")] - InvalidSlatepackAddress, - #[error("Can't load slatepack file at {file}")] - InvalidSlatepackFile{file: String}, - #[error("Invalid Tx Log State")] - InvalidTxLogState, - #[error("Invalid Invoice Proof")] - InvalidInvoiceProof, + #[error(transparent)] + WalletController(#[from] grin_wallet_controller::Error), + #[error(transparent)] + WalletLibWallet(#[from] grin_wallet_libwallet::Error), + #[error("Owner API not Instantiated")] + OwnerAPINotInstantiated, + #[error("Foreign API not Instantiated")] + ForeignAPINotInstantiated, + #[error("Invalid Slatepack Address")] + InvalidSlatepackAddress, + #[error("Can't load slatepack file at {file}")] + InvalidSlatepackFile { file: String }, + #[error("Invalid Tx Log State")] + InvalidTxLogState, + #[error("Invalid Invoice Proof")] + InvalidInvoiceProof, + #[error("Can't read wallet config file at {file}")] + ConfigReadError { file: String }, } #[derive(thiserror::Error, Debug)] pub enum FilesystemError { - #[error(transparent)] - Io(#[from] std::io::Error), - #[error(transparent)] - SerdeYaml(#[from] serde_yaml::Error), - #[error(transparent)] - Zip(#[from] zip::result::ZipError), - #[error(transparent)] - WalkDir(#[from] walkdir::Error), - #[error("File doesn't exist: {path:?}")] - FileDoesntExist { path: PathBuf }, - #[cfg(target_os = "macos")] - #[error("Could not file bin name {bin_name} in archive")] - BinMissingFromTar { bin_name: String }, - #[error("Failed to normalize path slashes for {path:?}")] - NormalizingPathSlash { path: PathBuf }, - #[error("Could not strip prefix {prefix:?} from {from:?}")] - StripPrefix { prefix: String, from: String }, + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + SerdeYaml(#[from] serde_yaml::Error), + #[error(transparent)] + Zip(#[from] zip::result::ZipError), + #[error(transparent)] + WalkDir(#[from] walkdir::Error), + #[error("File doesn't exist: {path:?}")] + FileDoesntExist { path: PathBuf }, + #[cfg(target_os = "macos")] + #[error("Could not file bin name {bin_name} in archive")] + BinMissingFromTar { bin_name: String }, + #[error("Failed to normalize path slashes for {path:?}")] + NormalizingPathSlash { path: PathBuf }, + #[error("Could not strip prefix {prefix:?} from {from:?}")] + StripPrefix { prefix: String, from: String }, } #[derive(thiserror::Error, Debug)] pub enum CacheError { - #[error("No repository information to create cache entry from addon {title}")] - AddonMissingRepo { title: String }, - #[error(transparent)] - Filesystem(#[from] FilesystemError), + #[error("No repository information to create cache entry from addon {title}")] + AddonMissingRepo { title: String }, + #[error(transparent)] + Filesystem(#[from] FilesystemError), } #[derive(thiserror::Error, Debug)] pub enum DownloadError { - #[error("Body len != content len: {body_length} != {content_length}")] - ContentLength { - content_length: u64, - body_length: u64, - }, - #[error("Invalid status code {code} for url {url}")] - InvalidStatusCode { - code: isahc::http::StatusCode, - url: String, - }, - #[error("No new release binary available for {bin_name}")] - MissingSelfUpdateRelease { bin_name: String }, - #[error("Catalog failed to download")] - CatalogFailed, - #[error("Self update for linux only works from AppImage")] - SelfUpdateLinuxNonAppImage, - #[error(transparent)] - Isahc(#[from] isahc::Error), - #[error(transparent)] - Http(#[from] isahc::http::Error), - #[error(transparent)] - Var(#[from] std::env::VarError), - #[error(transparent)] - SerdeJson(#[from] serde_json::Error), - #[error(transparent)] - Filesystem(#[from] FilesystemError), + #[error("Body len != content len: {body_length} != {content_length}")] + ContentLength { + content_length: u64, + body_length: u64, + }, + #[error("Invalid status code {code} for url {url}")] + InvalidStatusCode { + code: isahc::http::StatusCode, + url: String, + }, + #[error("No new release binary available for {bin_name}")] + MissingSelfUpdateRelease { bin_name: String }, + #[error("Catalog failed to download")] + CatalogFailed, + #[error("Self update for linux only works from AppImage")] + SelfUpdateLinuxNonAppImage, + #[error(transparent)] + Isahc(#[from] isahc::Error), + #[error(transparent)] + Http(#[from] isahc::http::Error), + #[error(transparent)] + Var(#[from] std::env::VarError), + #[error(transparent)] + SerdeJson(#[from] serde_json::Error), + #[error(transparent)] + Filesystem(#[from] FilesystemError), } impl From for DownloadError { - fn from(e: std::io::Error) -> Self { - DownloadError::Filesystem(FilesystemError::Io(e)) - } + fn from(e: std::io::Error) -> Self { + DownloadError::Filesystem(FilesystemError::Io(e)) + } } #[derive(thiserror::Error, Debug)] pub enum RepositoryError { - #[error("No repository set for addon")] - AddonNoRepository, - #[error("Failed to parse curse id as u32: {id}")] - CurseIdConversion { id: String }, - #[error("File id must be provided for curse changelog request")] - CurseChangelogFileId, - #[error("No package found for curse id {id}")] - CurseMissingPackage { id: String }, - #[error("No package found for WowI id {id}")] - WowIMissingPackage { id: String }, - #[error("No package found for Hub id {id}")] - HubMissingPackage { id: String }, - #[error("Git repo must be created with `from_source_url`")] - GitWrongConstructor, - #[error("Invalid url {url}")] - GitInvalidUrl { url: String }, - #[error("No valid host in {url}")] - GitMissingHost { url: String }, - #[error("Invalid host {host}, only github.com and gitlab.com are supported")] - GitInvalidHost { host: String }, - #[error("Author not present in {url}")] - GitMissingAuthor { url: String }, - #[error("Repo not present in {url}")] - GitMissingRepo { url: String }, - #[error("No release at {url}")] - GitMissingRelease { url: String }, - #[error("Tag name must be specified for git changelog")] - GitChangelogTagName, - #[error(transparent)] - Download(#[from] DownloadError), - #[error(transparent)] - Filesystem(#[from] FilesystemError), - #[error(transparent)] - Uri(#[from] isahc::http::uri::InvalidUri), - #[error(transparent)] - SerdeJson(#[from] serde_json::Error), + #[error("No repository set for addon")] + AddonNoRepository, + #[error("Failed to parse curse id as u32: {id}")] + CurseIdConversion { id: String }, + #[error("File id must be provided for curse changelog request")] + CurseChangelogFileId, + #[error("No package found for curse id {id}")] + CurseMissingPackage { id: String }, + #[error("No package found for WowI id {id}")] + WowIMissingPackage { id: String }, + #[error("No package found for Hub id {id}")] + HubMissingPackage { id: String }, + #[error("Git repo must be created with `from_source_url`")] + GitWrongConstructor, + #[error("Invalid url {url}")] + GitInvalidUrl { url: String }, + #[error("No valid host in {url}")] + GitMissingHost { url: String }, + #[error("Invalid host {host}, only github.com and gitlab.com are supported")] + GitInvalidHost { host: String }, + #[error("Author not present in {url}")] + GitMissingAuthor { url: String }, + #[error("Repo not present in {url}")] + GitMissingRepo { url: String }, + #[error("No release at {url}")] + GitMissingRelease { url: String }, + #[error("Tag name must be specified for git changelog")] + GitChangelogTagName, + #[error(transparent)] + Download(#[from] DownloadError), + #[error(transparent)] + Filesystem(#[from] FilesystemError), + #[error(transparent)] + Uri(#[from] isahc::http::uri::InvalidUri), + #[error(transparent)] + SerdeJson(#[from] serde_json::Error), } impl From for RepositoryError { - fn from(e: std::io::Error) -> Self { - RepositoryError::Filesystem(FilesystemError::Io(e)) - } + fn from(e: std::io::Error) -> Self { + RepositoryError::Filesystem(FilesystemError::Io(e)) + } } impl From for RepositoryError { - fn from(e: isahc::Error) -> Self { - RepositoryError::Download(DownloadError::Isahc(e)) - } + fn from(e: isahc::Error) -> Self { + RepositoryError::Download(DownloadError::Isahc(e)) + } } #[derive(thiserror::Error, Debug)] pub enum ParseError { - #[error("Addon directory not found: {path:?}")] - MissingAddonDirectory { path: PathBuf }, - #[error("No folders passed to addon")] - BuildAddonEmptyFolders, - #[error("No parent directory for {dir:?}")] - NoParentDirectory { dir: PathBuf }, - #[error("Invalid UTF8 path: {path:?}")] - InvalidUtf8Path { path: PathBuf }, - #[error("Path is not a file or doesn't exist: {path:?}")] - InvalidFile { path: PathBuf }, - #[error("Invalid extension for path: {path:?}")] - InvalidExt { path: PathBuf }, - #[error("Extension not in file parsing regex: {ext}")] - ParsingRegexMissingExt { ext: String }, - #[error("Inclusion regex error for group {group} on pos {pos}, line: {line}")] - InclusionRegexError { - group: usize, - pos: usize, - line: String, - }, - #[error(transparent)] - StripPrefix(#[from] std::path::StripPrefixError), - #[error(transparent)] - GlobPattern(#[from] glob::PatternError), - #[error(transparent)] - Glob(#[from] glob::GlobError), - #[error(transparent)] - FancyRegex(#[from] fancy_regex::Error), - #[error(transparent)] - Download(#[from] DownloadError), - #[error(transparent)] - Filesystem(#[from] FilesystemError), - #[error(transparent)] - Cache(#[from] CacheError), + #[error("Addon directory not found: {path:?}")] + MissingAddonDirectory { path: PathBuf }, + #[error("No folders passed to addon")] + BuildAddonEmptyFolders, + #[error("No parent directory for {dir:?}")] + NoParentDirectory { dir: PathBuf }, + #[error("Invalid UTF8 path: {path:?}")] + InvalidUtf8Path { path: PathBuf }, + #[error("Path is not a file or doesn't exist: {path:?}")] + InvalidFile { path: PathBuf }, + #[error("Invalid extension for path: {path:?}")] + InvalidExt { path: PathBuf }, + #[error("Extension not in file parsing regex: {ext}")] + ParsingRegexMissingExt { ext: String }, + #[error("Inclusion regex error for group {group} on pos {pos}, line: {line}")] + InclusionRegexError { + group: usize, + pos: usize, + line: String, + }, + #[error(transparent)] + StripPrefix(#[from] std::path::StripPrefixError), + #[error(transparent)] + GlobPattern(#[from] glob::PatternError), + #[error(transparent)] + Glob(#[from] glob::GlobError), + #[error(transparent)] + FancyRegex(#[from] fancy_regex::Error), + #[error(transparent)] + Download(#[from] DownloadError), + #[error(transparent)] + Filesystem(#[from] FilesystemError), + #[error(transparent)] + Cache(#[from] CacheError), } impl From for ParseError { - fn from(e: std::io::Error) -> Self { - ParseError::Filesystem(FilesystemError::Io(e)) - } + fn from(e: std::io::Error) -> Self { + ParseError::Filesystem(FilesystemError::Io(e)) + } } #[derive(thiserror::Error, Debug)] pub enum ThemeError { - #[error(transparent)] - InvalidUri(#[from] isahc::http::uri::InvalidUri), - #[error(transparent)] - UrlEncoded(#[from] serde_urlencoded::de::Error), - #[error(transparent)] - SerdeYaml(#[from] serde_yaml::Error), - #[error(transparent)] - SerdeJson(#[from] serde_json::Error), - #[error(transparent)] - Io(#[from] std::io::Error), - #[error("Url is missing theme from query")] - MissingQuery, - #[error("Theme already exists with name: {name}")] - NameCollision { name: String }, + #[error(transparent)] + InvalidUri(#[from] isahc::http::uri::InvalidUri), + #[error(transparent)] + UrlEncoded(#[from] serde_urlencoded::de::Error), + #[error(transparent)] + SerdeYaml(#[from] serde_yaml::Error), + #[error(transparent)] + SerdeJson(#[from] serde_json::Error), + #[error(transparent)] + Io(#[from] std::io::Error), + #[error("Url is missing theme from query")] + MissingQuery, + #[error("Theme already exists with name: {name}")] + NameCollision { name: String }, } diff --git a/crates/core/src/wallet/mod.rs b/crates/core/src/wallet/mod.rs index e2a0941..d355e35 100644 --- a/crates/core/src/wallet/mod.rs +++ b/crates/core/src/wallet/mod.rs @@ -2,7 +2,7 @@ /// Should eventually feature async calls that work via local wallet or remote owner API use grin_wallet::cmd::wallet_args::inst_wallet; use grin_wallet_api::{Foreign, Owner}; -use grin_wallet_config::{self, GlobalWalletConfig}; +use grin_wallet_config; use grin_wallet_controller::command::InitArgs; use grin_wallet_impls::DefaultLCProvider; use grin_wallet_libwallet::{NodeClient, WalletInst, WalletLCProvider}; @@ -26,6 +26,8 @@ pub use grin_wallet_libwallet::{ SlatepackAddress, StatusMessage, TxLogEntry, TxLogEntryType, WalletInfo, }; +pub use grin_wallet_config::GlobalWalletConfig; + pub use grin_wallet_libwallet::contract::types::{ ContractNewArgsAPI, ContractRevokeArgsAPI, ContractSetupArgsAPI, ProofArgs, }; @@ -134,6 +136,14 @@ pub fn parse_abs_tx_amount_fee(tx: &TxLogEntry, subtract_fee_from_amt: bool) -> ) } +pub fn get_wallet_config(path: &str) -> Result { + let res = GlobalWalletConfig::new(path); + match res { + Ok(c) => Ok(c), + Err(e) => Err(GrinWalletInterfaceError::ConfigReadError { file: path.into() }), + } +} + impl WalletInterface where L: WalletLCProvider<'static, C, grin_keychain::keychain::ExtKeychain>, diff --git a/crates/widgets/Cargo.toml b/crates/widgets/Cargo.toml index 7ee49f9..443ee90 100644 --- a/crates/widgets/Cargo.toml +++ b/crates/widgets/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "grin-gui-widgets" description = "Widget library for Grin Core GUI" -version = "0.1.0-alpha.5" +version = "0.1.0-alpha.6" authors = ["Yeastplume", "Casper Rogild Storm", "tarkah "] license = "GPL-3.0" homepage = "https://github.com/mimblewimble/grin-gui" diff --git a/locale/de.json b/locale/de.json index c94cd65..1839d76 100644 --- a/locale/de.json +++ b/locale/de.json @@ -88,11 +88,15 @@ "setup-grin-wallet-description": "To get started, you can either:", "setup-grin-first-time": "First-Time Setup", "setup-grin-wallet-title": "Create New Wallet", + "import-grin-wallet-title": "Import Wallet", "setup-grin-wallet-enter-password": "Please create a password for your new Grin wallet", + "setup-grin-wallet-enter-display-name": "Please create a display name for this wallet", "setup-grin-create-wallet": "Create Wallet", + "setup-grin-import-wallet": "Import Wallet", "setup-grin-passwords-dont-match": "Passwords do not match", "setup-grin-passwords-okay": "Password okay", "setup-grin-wallet-success": "Wallet Created!", + "import-grin-wallet-success": "Wallet Imported!", "setup-grin-wallet-done": "Done", "setup-grin-wallet-recovery-phrase-title": "Recovery phrase", "setup-grin-wallet-recovery-phrase": "Your wallet recovery phrase is shown below.\n\nStore these words in a non-digital format and be sure to keep them safe.", diff --git a/locale/en.json b/locale/en.json index df91075..102987e 100644 --- a/locale/en.json +++ b/locale/en.json @@ -88,11 +88,15 @@ "setup-grin-wallet-description": "To get started, you can either:", "setup-grin-first-time": "First-Time Setup", "setup-grin-wallet-title": "Create New Wallet", + "import-grin-wallet-title": "Import Wallet", "setup-grin-wallet-enter-password": "Please create a password for your new Grin wallet", + "setup-grin-wallet-enter-display-name": "Please create a display name for this wallet", "setup-grin-create-wallet": "Create Wallet", + "setup-grin-import-wallet": "Import Wallet", "setup-grin-passwords-dont-match": "Passwords do not match", "setup-grin-passwords-okay": "Password okay", "setup-grin-wallet-success": "Wallet Created", + "import-grin-wallet-success": "Wallet Imported!", "setup-grin-wallet-done": "Done", "setup-grin-wallet-recovery-phrase-title": "Recovery phrase", "setup-grin-wallet-recovery-phrase": "Your wallet recovery phrase is shown below.\nStore these words in a non-digital format and be sure to keep them safe.", diff --git a/src/gui/element/wallet/mod.rs b/src/gui/element/wallet/mod.rs index f61420a..02ff775 100644 --- a/src/gui/element/wallet/mod.rs +++ b/src/gui/element/wallet/mod.rs @@ -13,6 +13,7 @@ use { pub enum Mode { Init, CreateWallet(String), + ImportWallet, Operation, } @@ -59,6 +60,9 @@ pub fn data_container<'a>(state: &'a StateContainer, config: &'a Config) -> Cont &state.setup_state.setup_wallet_state, default_display_name, ), + Mode::ImportWallet => { + setup::wallet_import::data_container(config, &state.setup_state.import_wallet_state) + } }; let column = Column::new() diff --git a/src/gui/element/wallet/setup/mod.rs b/src/gui/element/wallet/setup/mod.rs index c5dfa6f..7fb8a7f 100644 --- a/src/gui/element/wallet/setup/mod.rs +++ b/src/gui/element/wallet/setup/mod.rs @@ -1,4 +1,6 @@ pub mod init; +pub mod wallet_import; +pub mod wallet_import_success; pub mod wallet_list; pub mod wallet_setup; pub mod wallet_success; @@ -19,7 +21,9 @@ pub struct StateContainer { pub mode: Mode, pub setup_init_state: init::StateContainer, pub setup_wallet_state: wallet_setup::StateContainer, + pub import_wallet_state: wallet_import::StateContainer, pub setup_wallet_success_state: wallet_success::StateContainer, + pub import_wallet_success_state: wallet_import_success::StateContainer, pub setup_wallet_list_state: wallet_list::StateContainer, } @@ -29,6 +33,7 @@ pub enum Mode { CreateWallet(String), ListWallets, WalletCreateSuccess, + WalletImportSuccess, } impl Default for StateContainer { @@ -37,7 +42,9 @@ impl Default for StateContainer { mode: Mode::Init, setup_init_state: Default::default(), setup_wallet_state: Default::default(), + import_wallet_state: Default::default(), setup_wallet_success_state: Default::default(), + import_wallet_success_state: Default::default(), setup_wallet_list_state: Default::default(), } } @@ -62,6 +69,9 @@ pub fn data_container<'a>(state: &'a StateContainer, config: &Config) -> Contain Mode::WalletCreateSuccess => { wallet_success::data_container(&state.setup_wallet_success_state) } + Mode::WalletImportSuccess => { + wallet_import_success::data_container(&state.import_wallet_success_state) + } Mode::ListWallets => wallet_list::data_container(&state.setup_wallet_list_state, config), }; diff --git a/src/gui/element/wallet/setup/wallet_import.rs b/src/gui/element/wallet/setup/wallet_import.rs new file mode 100644 index 0000000..f2bcefb --- /dev/null +++ b/src/gui/element/wallet/setup/wallet_import.rs @@ -0,0 +1,399 @@ +use crate::{gui::element, log_error}; +use grin_gui_core::wallet; +use iced_core::Widget; +use native_dialog::FileDialog; +use std::path::PathBuf; + +use { + super::super::super::{ + BUTTON_HEIGHT, BUTTON_WIDTH, DEFAULT_FONT_SIZE, DEFAULT_HEADER_FONT_SIZE, DEFAULT_PADDING, + }, + crate::gui::{GrinGui, Interaction, Message}, + crate::localization::localized_string, + crate::Result, + anyhow::Context, + grin_gui_core::theme::ColorPalette, + grin_gui_core::theme::{ + Button, Column, Container, Element, PickList, Row, Scrollable, Text, TextInput, + }, + grin_gui_core::{ + config::{Config, Wallet}, + fs::PersistentData, + node::ChainTypes::{self, Mainnet, Testnet}, + wallet::create_grin_wallet_path, + wallet::{GlobalWalletConfig, WalletInterface}, + }, + iced::widget::{button, pick_list, scrollable, text_input, Checkbox, Space}, + iced::{alignment, Alignment, Command, Length}, + std::sync::{Arc, RwLock}, +}; + +pub struct StateContainer { + pub toml_file: PathBuf, + pub password_state: PasswordState, + pub display_name_value: String, +} + +impl Default for StateContainer { + fn default() -> Self { + Self { + toml_file: Default::default(), + password_state: Default::default(), + display_name_value: Default::default(), + } + } +} + +impl StateContainer { + pub fn init_wallet_name(&mut self, config: &Config) { + let mut wallet_name = "wallet".to_string(); + let mut i = 1; + while config.wallets.iter().any(|w| w.display_name == wallet_name) { + i += 1; + wallet_name = format!("wallet{}", i); + } + self.display_name_value = wallet_name; + } +} + +#[derive(Debug, Clone)] +pub struct PasswordState { + pub input_value: String, +} + +impl Default for PasswordState { + fn default() -> Self { + PasswordState { + input_value: Default::default(), + } + } +} + +#[derive(Debug, Clone)] +pub enum LocalViewInteraction { + Back, + //TODO: ZeroingString these + PasswordInput(String), + PasswordInputEnterPressed, + DisplayName(String), + ImportWallet(String, PathBuf), + WalletImportedOk(String, GlobalWalletConfig), + WalletImportError(Arc>>), +} + +pub fn handle_message<'a>( + grin_gui: &mut GrinGui, + message: LocalViewInteraction, +) -> Result> { + let state = &mut grin_gui.wallet_state.setup_state.import_wallet_state; + match message { + LocalViewInteraction::Back => { + // reset user input values + grin_gui.wallet_state.setup_state.import_wallet_state = Default::default(); + + // return user to proper view + match grin_gui.wallet_state.mode { + // back to init screen + element::wallet::Mode::Init => { + grin_gui.wallet_state.setup_state.mode = super::Mode::Init + } + _ => { + // back to list view + grin_gui.wallet_state.mode = element::wallet::Mode::Init; + grin_gui.wallet_state.setup_state.mode = super::Mode::ListWallets; + } + }; + } + LocalViewInteraction::PasswordInput(password) => { + state.password_state.input_value = password; + } + LocalViewInteraction::PasswordInputEnterPressed => { + // state.password_state.input_state.unfocus(); + // state.password_state.repeat_input_state.focus(); + } + LocalViewInteraction::DisplayName(display_name_value) => { + state.display_name_value = display_name_value; + } + LocalViewInteraction::ImportWallet(display_name, toml_file) => { + grin_gui.error.take(); + + log::debug!( + "setup::wallet::LocalViewInteraction::ImportWallet {}", + display_name, + ); + + let fut_config = grin_gui.config.clone(); + let fut_display_name = display_name.clone(); + let fut_path = toml_file.clone(); + + let fut = || async move { + // check wallet name doesn't exist + if let Some(w) = fut_config + .wallets + .iter() + .find(|w| w.display_name == fut_display_name) + { + return Err(anyhow::Error::msg(format!( + "Wallet with name \"{}\" already exists", + fut_display_name + ))); + } + let global_config = wallet::get_wallet_config(fut_path.to_str().unwrap())?; + Ok((fut_display_name, global_config)) + }; + + return Ok(Command::perform(fut(), |r| { + match r.context("Failed to Import Wallet") { + Ok((display_name, config)) => { + Message::Interaction(Interaction::WalletSetupImportWalletViewInteraction( + LocalViewInteraction::WalletImportedOk(display_name, config), + )) + } + Err(e) => { + Message::Interaction(Interaction::WalletSetupImportWalletViewInteraction( + LocalViewInteraction::WalletImportError(Arc::new(RwLock::new(Some(e)))), + )) + } + } + })); + } + LocalViewInteraction::WalletImportedOk(display_name, global_wallet_config) => { + //debug!("Global config: {:?}", wallet_config); + let wallet_config = global_wallet_config + .members + .as_ref() + .unwrap() + .wallet + .clone(); + let chain_type = wallet_config.chain_type.unwrap_or_default(); + let mut tld = PathBuf::from(wallet_config.data_file_dir); + tld.pop(); + let tld = Some(tld); + let saved_wallet = Wallet::new(tld, display_name.clone(), chain_type); + + let index = grin_gui.config.add_wallet(saved_wallet); + grin_gui.config.current_wallet_index = Some(index); + grin_gui.wallet_state.clear_config_missing(); + + /*grin_gui + .wallet_state + .setup_state + .setup_wallet_success_state + .recovery_phrase = mnemonic;*/ + + // reset user input values + grin_gui.wallet_state.setup_state.import_wallet_state = Default::default(); + + let _ = grin_gui.config.save(); + + debug!("Wallet config imported successfully: {}", display_name); + + grin_gui.wallet_state.setup_state.mode = + crate::gui::element::wallet::setup::Mode::WalletImportSuccess; + + if grin_gui.wallet_state.mode != element::wallet::Mode::Init { + // set init state + grin_gui.wallet_state.mode = element::wallet::Mode::Init; + } + } + LocalViewInteraction::WalletImportError(err) => { + grin_gui.error = err.write().unwrap().take(); + if let Some(e) = grin_gui.error.as_ref() { + log_error(e); + } + } + } + + Ok(Command::none()) +} + +pub fn data_container<'a>(config: &'a Config, state: &'a StateContainer) -> Container<'a, Message> { + let title = Text::new(localized_string("import-grin-wallet-title")) + .size(DEFAULT_HEADER_FONT_SIZE) + .horizontal_alignment(alignment::Horizontal::Center); + + // we need 2 pts of padding here to match the position with other views: i.e. wallet list, settings. + // otherwise this title container will look like it shifts up slightly when the user toggles + // between views with buttons on the header row. + let title_container = Container::new(title) + .style(grin_gui_core::theme::ContainerStyle::BrightBackground) + .padding(iced::Padding::from([ + 2, // top + 0, // right + 2, // bottom + 5, // left + ])); + + // push more items on to header here: e.g. other buttons, things that belong on the header + let header_row = Row::new().push(title_container); + + let header_container = Container::new(header_row).padding(iced::Padding::from([ + 0, // top + 0, // right + DEFAULT_PADDING as u16, // bottom + 0, // left + ])); + + /*let password_column = { + let password_input = TextInput::new( + &localized_string("password")[..], + &state.password_state.input_value, + ) + .on_input(|s| { + Interaction::WalletSetupImportWalletViewInteraction( + LocalViewInteraction::PasswordInput(s), + ) + }) + .on_submit(Interaction::WalletSetupImportWalletViewInteraction( + LocalViewInteraction::PasswordInputEnterPressed, + )) + .size(DEFAULT_FONT_SIZE) + .padding(6) + .width(Length::Fixed(200.0)) + .style(grin_gui_core::theme::TextInputStyle::AddonsQuery) + .password(); + + let password_input: Element = password_input.into(); + + let mut password_input_col = Column::new() + .push(password_input.map(Message::Interaction)) + .spacing(DEFAULT_PADDING) + .align_items(Alignment::Start); + + Column::new().push(password_input_col) + };*/ + + let description = Text::new(localized_string("setup-grin-wallet-enter-display-name")) + .size(DEFAULT_FONT_SIZE) + .horizontal_alignment(alignment::Horizontal::Center); + let description_container = + Container::new(description).style(grin_gui_core::theme::ContainerStyle::NormalBackground); + + let display_name = Text::new(localized_string("display-name")) + .size(DEFAULT_FONT_SIZE) + .horizontal_alignment(alignment::Horizontal::Left); + + let display_name_container = + Container::new(display_name).style(grin_gui_core::theme::ContainerStyle::NormalBackground); + + let display_name_input = TextInput::new(&state.display_name_value, &state.display_name_value) + .on_input(|s| { + Interaction::WalletSetupImportWalletViewInteraction(LocalViewInteraction::DisplayName( + s, + )) + }) + .size(DEFAULT_FONT_SIZE) + .padding(6) + .width(Length::Fixed(200.0)) + .style(grin_gui_core::theme::TextInputStyle::AddonsQuery); + + let display_name_input: Element = display_name_input.into(); + + let mut display_name_col = Column::new() + .push(display_name_container) + .spacing(DEFAULT_PADDING) + .push(display_name_input.map(Message::Interaction)) + .spacing(DEFAULT_PADDING) + .align_items(Alignment::Start); + + let button_height = Length::Fixed(BUTTON_HEIGHT); + let button_width = Length::Fixed(BUTTON_WIDTH); + + let submit_button_label_container = Container::new( + Text::new(localized_string("setup-grin-import-wallet")).size(DEFAULT_FONT_SIZE), + ) + .width(button_width) + .height(button_height) + .center_x() + .center_y() + .align_x(alignment::Horizontal::Center); + + let mut submit_button = Button::new(submit_button_label_container) + .style(grin_gui_core::theme::ButtonStyle::Primary); + + if !state.display_name_value.is_empty() { + submit_button = + submit_button.on_press(Interaction::WalletSetupImportWalletViewInteraction( + LocalViewInteraction::ImportWallet( + state.display_name_value.clone(), + state.toml_file.clone(), + ), + )); + } + + let submit_button: Element = submit_button.into(); + + let cancel_button_label_container = + Container::new(Text::new(localized_string("cancel")).size(DEFAULT_FONT_SIZE)) + .width(button_width) + .height(button_height) + .center_x() + .center_y() + .align_x(alignment::Horizontal::Center); + + let cancel_button: Element = Button::new(cancel_button_label_container) + .style(grin_gui_core::theme::ButtonStyle::Primary) + .on_press(Interaction::WalletSetupImportWalletViewInteraction( + LocalViewInteraction::Back, + )) + .into(); + + let submit_container = Container::new(submit_button.map(Message::Interaction)).padding(1); + let submit_container = Container::new(submit_container) + .style(grin_gui_core::theme::ContainerStyle::Segmented) + .padding(1); + + let cancel_container = Container::new(cancel_button.map(Message::Interaction)).padding(1); + let cancel_container = Container::new(cancel_container) + .style(grin_gui_core::theme::ContainerStyle::Segmented) + .padding(1); + + let unit_spacing = 15.0; + let button_row = Row::new() + .push(submit_container) + .push(Space::new(Length::Fixed(unit_spacing), Length::Fixed(0.0))) + .push(cancel_container); + + let mut column = Column::new() + .push(Space::new(Length::Fixed(0.0), Length::Fixed(unit_spacing))) + .push(description_container) + .push(Space::new(Length::Fixed(0.0), Length::Fixed(unit_spacing))) + .push(display_name_col) + .push(Space::new( + Length::Fixed(0.0), + Length::Fixed(unit_spacing + 10.0), + )); + + column = column.push(button_row).align_items(Alignment::Start); + let form_container = Container::new(column) + .width(Length::Fill) + .padding(iced::Padding::from([ + 0, // top + 0, // right + 0, // bottom + 5, // left + ])); + + // form container should be scrollable in tiny windows + let scrollable = Scrollable::new(form_container) + .height(Length::Fill) + .style(grin_gui_core::theme::ScrollableStyle::Primary); + + let content = Container::new(scrollable) + .width(Length::Fill) + .height(Length::Shrink) + .style(grin_gui_core::theme::ContainerStyle::NormalBackground); + + let wrapper_column = Column::new() + .height(Length::Fill) + .push(header_container) + .push(content); + + // Returns the final container. + Container::new(wrapper_column).padding(iced::Padding::from([ + DEFAULT_PADDING, // top + DEFAULT_PADDING, // right + DEFAULT_PADDING, // bottom + DEFAULT_PADDING, // left + ])) +} diff --git a/src/gui/element/wallet/setup/wallet_import_success.rs b/src/gui/element/wallet/setup/wallet_import_success.rs new file mode 100644 index 0000000..b521a7d --- /dev/null +++ b/src/gui/element/wallet/setup/wallet_import_success.rs @@ -0,0 +1,90 @@ +use { + super::super::super::{DEFAULT_FONT_SIZE, DEFAULT_HEADER_FONT_SIZE, SMALLER_FONT_SIZE}, + crate::gui::{GrinGui, Interaction, Message}, + crate::localization::localized_string, + crate::Result, + grin_gui_core::theme::ColorPalette, + grin_gui_core::theme::{ + Column, Container, Element, PickList, Row, Scrollable, Text, TextInput, + }, + iced::widget::{button, pick_list, scrollable, text_input, Button, Checkbox, Space}, + iced::{alignment, Alignment, Command, Length}, + iced_aw::Card, +}; + +pub struct StateContainer {} + +impl Default for StateContainer { + fn default() -> Self { + Self {} + } +} + +#[derive(Debug, Clone)] +pub enum LocalViewInteraction { + Submit, +} + +pub fn handle_message( + grin_gui: &mut GrinGui, + message: LocalViewInteraction, +) -> Result> { + let state = &mut grin_gui.wallet_state.setup_state.import_wallet_state; + match message { + LocalViewInteraction::Submit => { + debug!("Wallet import success view submit"); + grin_gui.wallet_state.mode = super::super::Mode::Operation; + grin_gui.wallet_state.setup_state.mode = crate::gui::element::wallet::setup::Mode::Init; + } + } + Ok(Command::none()) +} + +pub fn data_container<'a>(state: &'a StateContainer) -> Container<'a, Message> { + // Title row + let title = Text::new(localized_string("import-grin-wallet-success")) + .size(DEFAULT_HEADER_FONT_SIZE) + .horizontal_alignment(alignment::Horizontal::Left); + + let title_container = + Container::new(title).style(grin_gui_core::theme::ContainerStyle::NormalBackground); + + let title_row = Row::new() + .push(title_container) + .align_items(Alignment::Center) + .spacing(20); + + let submit_button_label_container = Container::new( + Text::new(localized_string("setup-grin-wallet-done")).size(DEFAULT_FONT_SIZE), + ) + .center_x() + .align_x(alignment::Horizontal::Center); + + let next_button = Button::new(submit_button_label_container) + .style(grin_gui_core::theme::ButtonStyle::Bordered) + .on_press(Interaction::WalletImportWalletSuccessViewInteraction( + LocalViewInteraction::Submit, + )); + + let next_button: Element = next_button.into(); + + let unit_spacing = 15.0; + + let colum = Column::new() + .push(Space::new( + Length::Fixed(0.0), + Length::Fixed(unit_spacing + 5.0), + )) + .push(title_row) + .push(Space::new( + Length::Fixed(0.0), + Length::Fixed(unit_spacing + 5.0), + )) + .push(next_button.map(Message::Interaction)) + .align_items(Alignment::Center); + + Container::new(colum) + .center_y() + .center_x() + .width(Length::Fill) +} diff --git a/src/gui/element/wallet/setup/wallet_list.rs b/src/gui/element/wallet/setup/wallet_list.rs index f7339d6..f0f770d 100644 --- a/src/gui/element/wallet/setup/wallet_list.rs +++ b/src/gui/element/wallet/setup/wallet_list.rs @@ -5,6 +5,7 @@ use { crate::gui::{GrinGui, Interaction, Message}, crate::localization::localized_string, crate::Result, + anyhow::Context, grin_gui_core::config::Config, grin_gui_core::theme::{ Button, Column, Container, Element, Header, PickList, Row, Scrollable, TableRow, Text, @@ -18,6 +19,7 @@ use { iced::{alignment, Alignment, Command, Length}, native_dialog::FileDialog, std::path::PathBuf, + std::sync::{Arc, RwLock}, }; use grin_gui_widgets::widget::table_row; @@ -44,6 +46,7 @@ pub enum LocalViewInteraction { LoadWallet(usize), LocateWallet, CreateWallet, + WalletImportError(Arc>>), } pub fn handle_message<'a>( @@ -88,20 +91,27 @@ pub fn handle_message<'a>( } } LocalViewInteraction::LocateWallet => { - match FileDialog::new().show_open_single_file() { - Ok(path) => { - match path { - Some(d) => { - match validate_directory(d) { - Ok(wallet_was_imported) => {} - Err(err) => { - // tell the user why this directory failed - } + let file_dialogue = FileDialog::new().add_filter("grin-wallet.toml", &["toml"]); + match file_dialogue.show_open_single_file() { + Ok(path) => match path { + Some(d) => match validate_directory(d.clone()) { + true => { + let state = &mut grin_gui.wallet_state.setup_state.import_wallet_state; + state.toml_file = d; + state.init_wallet_name(&grin_gui.config); + + grin_gui.wallet_state.mode = + crate::gui::element::wallet::Mode::ImportWallet; + } + false => { + grin_gui.error = Some(anyhow::Error::msg("Invalid directory")); + if let Some(e) = grin_gui.error.as_ref() { + crate::log_error(e); } } - None => {} - } - } + }, + None => {} + }, Err(e) => { log::debug!("wallet_list.rs::LocalViewInteraction::LocateWallet {}", e); } @@ -135,15 +145,20 @@ pub fn handle_message<'a>( grin_gui.wallet_state.mode = crate::gui::element::wallet::Mode::CreateWallet(wallet_display_name); } + LocalViewInteraction::WalletImportError(err) => { + grin_gui.error = err.write().unwrap().take(); + if let Some(e) = grin_gui.error.as_ref() { + crate::log_error(e); + } + } } Ok(Command::none()) } -struct DirectoryValidationError; - -fn validate_directory(_d: PathBuf) -> Result { - Ok(true) +fn validate_directory(d: PathBuf) -> bool { + debug!("Validating directory: {:?}", d); + d.exists() } pub fn data_container<'a>(state: &'a StateContainer, config: &Config) -> Container<'a, Message> { diff --git a/src/gui/mod.rs b/src/gui/mod.rs index dd44ed2..57226b6 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -420,10 +420,16 @@ pub enum Interaction { WalletSetupViewInteraction(element::wallet::setup::LocalViewInteraction), WalletSetupInitViewInteraction(element::wallet::setup::init::LocalViewInteraction), WalletSetupWalletViewInteraction(element::wallet::setup::wallet_setup::LocalViewInteraction), + WalletSetupImportWalletViewInteraction( + element::wallet::setup::wallet_import::LocalViewInteraction, + ), WalletListWalletViewInteraction(element::wallet::setup::wallet_list::LocalViewInteraction), WalletSetupWalletSuccessViewInteraction( element::wallet::setup::wallet_success::LocalViewInteraction, ), + WalletImportWalletSuccessViewInteraction( + element::wallet::setup::wallet_import_success::LocalViewInteraction, + ), WalletOperationOpenViewInteraction(element::wallet::operation::open::LocalViewInteraction), WalletOperationHomeViewInteraction(element::wallet::operation::home::LocalViewInteraction), WalletOperationTxListInteraction(element::wallet::operation::tx_list::LocalViewInteraction), diff --git a/src/gui/update.rs b/src/gui/update.rs index 949ca11..50cce07 100644 --- a/src/gui/update.rs +++ b/src/gui/update.rs @@ -182,6 +182,10 @@ pub fn handle_message(grin_gui: &mut GrinGui, message: Message) -> Result { return element::wallet::setup::wallet_setup::handle_message(grin_gui, l); } + // Setup -> Wallet Import Settings + Message::Interaction(Interaction::WalletSetupImportWalletViewInteraction(l)) => { + return element::wallet::setup::wallet_import::handle_message(grin_gui, l); + } // Setup -> Wallet List Message::Interaction(Interaction::WalletListWalletViewInteraction(l)) => { return element::wallet::setup::wallet_list::handle_message(grin_gui, l); @@ -190,6 +194,10 @@ pub fn handle_message(grin_gui: &mut GrinGui, message: Message) -> Result { return element::wallet::setup::wallet_success::handle_message(grin_gui, l); } + // Import -> Wallet Success Settings + Message::Interaction(Interaction::WalletImportWalletSuccessViewInteraction(l)) => { + return element::wallet::setup::wallet_import_success::handle_message(grin_gui, l); + } // Wallet -> Operation -> Open Settings Message::Interaction(Interaction::WalletOperationOpenViewInteraction(l)) => { return element::wallet::operation::open::handle_message(grin_gui, l);