From de864fe8288afe61b3dee19a4cad5514ec4e2234 Mon Sep 17 00:00:00 2001 From: Fouad Date: Wed, 2 Feb 2022 19:25:02 +0100 Subject: [PATCH] Feature/wallet login with password (#1074) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * create nymlogo component * start scaffolding * set up mnemonic check pages * start on guess words components * mnemonic verification work * more mnemonic verification * hard code number of words to generate * mnemonic verification section 2 * add password strength indicator * add password confirmation * update text * disable word tiles on selection * add exisiting account page * finish exisiting account login * add back buttons to return to the initial screen * update button size * create network selection component * implement network selector component * update page types * make currency and network dynamic variables * remove unused env values * [ci skip] Generate TS types * lighten subtext * display network name in app * create network selector component * remove old network display component * update state in response to network selection * state updates * implement logout * refresh delegations on network change * Initial implementation of data encryption/decryption with provided password * Removing milhon network. * Removing more milhon references. * Adding in mainnet constants for network defaults. Contracts are not yet defined. * Allow encryption of EncryptedData, where T: Serialize + Deserialize It uses serde_json for representation of T * Making contract addresses optional * [ci skip] Generate TS types * Add mainnet as implicit network * Fix unreachable code warning * Using zeroize for encrypted data + password * Get denom dynamically * initialize network to undefined before login * Fix str parse to dynamic denom * Pass network in config all the way to hd wallet generation * Fixed encrypted data deserialization * Storing and loading accounts from the disk * Additional error variants * Fix clippy in wallet * Simplified contract address parsing * display error * fix typo * uncomment code * Use sandbox as backend default again * Reorganised wallet storage for easier testing + unit test * Fix denom showing * to_major call only on printable balance * fix up state changes Co-authored-by: Dave Hrycyszyn Co-authored-by: fmtabbara Co-authored-by: Jędrzej Stuczyński Co-authored-by: Bogdan-Ștefan Neacșu --- .../validator-client/src/client.rs | 8 + .../validator-client/src/nymd/mod.rs | 14 +- .../validator-client/src/nymd/wallet/mod.rs | 16 +- common/credentials/src/coconut/utils.rs | 2 +- common/network-defaults/build.rs | 2 +- common/network-defaults/src/all.rs | 63 ++-- common/network-defaults/src/lib.rs | 24 +- common/network-defaults/src/mainnet.rs | 20 ++ common/network-defaults/src/milhon.rs | 23 -- common/network-defaults/src/qa.rs | 2 +- common/network-defaults/src/sandbox.rs | 2 +- explorer-api/src/client.rs | 4 +- .../connection_handler/eth_events.rs | 1 + nym-wallet/.env.sample | 5 +- nym-wallet/Cargo.lock | 147 +++++++++- nym-wallet/config.ts | 3 - nym-wallet/src-tauri/Cargo.toml | 5 + nym-wallet/src-tauri/src/coin.rs | 16 +- nym-wallet/src-tauri/src/config/mod.rs | 8 +- nym-wallet/src-tauri/src/error.rs | 24 +- nym-wallet/src-tauri/src/main.rs | 3 + nym-wallet/src-tauri/src/network.rs | 18 +- .../src/operations/mixnet/account.rs | 15 +- nym-wallet/src-tauri/src/state.rs | 4 + .../src/wallet_storage/account_data.rs | 86 ++++++ .../src/wallet_storage/encryption.rs | 271 ++++++++++++++++++ .../src-tauri/src/wallet_storage/mod.rs | 199 +++++++++++++ .../src-tauri/src/wallet_storage/password.rs | 25 ++ nym-wallet/src/components/AppBar.tsx | 9 +- nym-wallet/src/components/Fee.tsx | 7 +- nym-wallet/src/components/NetworkSelector.tsx | 82 ++++++ nym-wallet/src/components/NymLogo.tsx | 10 + nym-wallet/src/components/index.ts | 3 + nym-wallet/src/context/main.tsx | 85 ++++-- nym-wallet/src/hooks/useCheckOwnership.ts | 20 +- nym-wallet/src/index.tsx | 24 +- nym-wallet/src/layouts/AppLayout.tsx | 14 +- nym-wallet/src/pages/balance/index.tsx | 4 +- nym-wallet/src/pages/bond/BondForm.tsx | 6 +- nym-wallet/src/pages/bond/SuccessView.tsx | 6 +- nym-wallet/src/pages/bond/validationSchema.ts | 3 +- .../src/pages/delegate/DelegateForm.tsx | 8 +- nym-wallet/src/pages/delegate/SuccessView.tsx | 6 +- nym-wallet/src/pages/delegate/index.tsx | 8 +- nym-wallet/src/pages/index.ts | 2 +- nym-wallet/src/pages/receive/index.tsx | 8 +- .../src/pages/send/SendConfirmation.tsx | 8 +- nym-wallet/src/pages/send/SendForm.tsx | 6 +- nym-wallet/src/pages/send/SendReview.tsx | 8 +- nym-wallet/src/pages/send/index.tsx | 9 +- nym-wallet/src/pages/settings/index.tsx | 2 +- nym-wallet/src/pages/settings/node-stats.tsx | 7 +- .../src/pages/settings/system-variables.tsx | 8 +- .../src/pages/settings/useSettingsState.ts | 67 ++--- nym-wallet/src/pages/sign-in/index.tsx | 36 --- .../src/pages/undelegate/UndelegateForm.tsx | 2 +- nym-wallet/src/pages/undelegate/index.tsx | 14 +- .../_legacy_create-account.tsx} | 51 +--- .../_legacy_sign-in.tsx} | 52 ++-- .../src/pages/welcome/components/heading.tsx | 16 ++ .../src/pages/welcome/components/index.ts | 4 + .../welcome/components/password-strength.tsx | 78 +++++ .../pages/welcome/components/render-page.tsx | 11 + .../pages/welcome/components/word-tiles.tsx | 91 ++++++ nym-wallet/src/pages/welcome/index.tsx | 75 +++++ .../pages/welcome/pages/create-password.tsx | 60 ++++ .../pages/welcome/pages/existing-account.tsx | 51 ++++ nym-wallet/src/pages/welcome/pages/index.ts | 6 + .../pages/welcome/pages/mnemonic-words.tsx | 37 +++ .../pages/welcome/pages/select-network.tsx | 25 ++ .../pages/welcome/pages/verify-mnemonic.tsx | 80 ++++++ .../pages/welcome/pages/welcome-content.tsx | 42 +++ nym-wallet/src/pages/welcome/types.ts | 19 ++ nym-wallet/src/requests/index.ts | 9 +- nym-wallet/src/theme/index.tsx | 10 + nym-wallet/src/types/global.ts | 5 + .../src/types/rust/corenodestatusresponse.ts | 4 + nym-wallet/src/types/rust/index.ts | 1 + nym-wallet/src/types/rust/network.ts | 2 +- nym-wallet/src/utils/index.ts | 28 +- testnet-faucet/src/context/index.tsx | 2 +- validator-api/src/nymd_client.rs | 8 +- 82 files changed, 1868 insertions(+), 381 deletions(-) create mode 100644 common/network-defaults/src/mainnet.rs delete mode 100644 common/network-defaults/src/milhon.rs create mode 100644 nym-wallet/src-tauri/src/wallet_storage/account_data.rs create mode 100644 nym-wallet/src-tauri/src/wallet_storage/encryption.rs create mode 100644 nym-wallet/src-tauri/src/wallet_storage/mod.rs create mode 100644 nym-wallet/src-tauri/src/wallet_storage/password.rs create mode 100644 nym-wallet/src/components/NetworkSelector.tsx create mode 100644 nym-wallet/src/components/NymLogo.tsx delete mode 100644 nym-wallet/src/pages/sign-in/index.tsx rename nym-wallet/src/pages/{sign-in/create-account.tsx => welcome/_legacy_create-account.tsx} (56%) rename nym-wallet/src/pages/{sign-in/sign-in.tsx => welcome/_legacy_sign-in.tsx} (61%) create mode 100644 nym-wallet/src/pages/welcome/components/heading.tsx create mode 100644 nym-wallet/src/pages/welcome/components/index.ts create mode 100644 nym-wallet/src/pages/welcome/components/password-strength.tsx create mode 100644 nym-wallet/src/pages/welcome/components/render-page.tsx create mode 100644 nym-wallet/src/pages/welcome/components/word-tiles.tsx create mode 100644 nym-wallet/src/pages/welcome/index.tsx create mode 100644 nym-wallet/src/pages/welcome/pages/create-password.tsx create mode 100644 nym-wallet/src/pages/welcome/pages/existing-account.tsx create mode 100644 nym-wallet/src/pages/welcome/pages/index.ts create mode 100644 nym-wallet/src/pages/welcome/pages/mnemonic-words.tsx create mode 100644 nym-wallet/src/pages/welcome/pages/select-network.tsx create mode 100644 nym-wallet/src/pages/welcome/pages/verify-mnemonic.tsx create mode 100644 nym-wallet/src/pages/welcome/pages/welcome-content.tsx create mode 100644 nym-wallet/src/pages/welcome/types.ts create mode 100644 nym-wallet/src/types/rust/corenodestatusresponse.ts diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index e21db6ab79..c291f22eb1 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -30,6 +30,7 @@ use std::str::FromStr; #[cfg(feature = "nymd-client")] #[must_use] pub struct Config { + network: network_defaults::all::Network, api_url: Url, nymd_url: Url, mixnet_contract_address: Option, @@ -44,12 +45,14 @@ pub struct Config { #[cfg(feature = "nymd-client")] impl Config { pub fn new( + network: network_defaults::all::Network, nymd_url: Url, api_url: Url, mixnet_contract_address: Option, vesting_contract_address: Option, ) -> Self { Config { + network, nymd_url, mixnet_contract_address, vesting_contract_address, @@ -84,6 +87,7 @@ impl Config { #[cfg(feature = "nymd-client")] pub struct Client { + network: network_defaults::all::Network, mixnet_contract_address: Option, vesting_contract_address: Option, mnemonic: Option, @@ -106,6 +110,7 @@ impl Client { ) -> Result, ValidatorClientError> { let validator_api_client = validator_api::Client::new(config.api_url.clone()); let nymd_client = NymdClient::connect_with_mnemonic( + config.network, config.nymd_url.as_str(), config.mixnet_contract_address.clone(), config.vesting_contract_address.clone(), @@ -114,6 +119,7 @@ impl Client { )?; Ok(Client { + network: config.network, mixnet_contract_address: config.mixnet_contract_address, vesting_contract_address: config.vesting_contract_address, mnemonic: Some(mnemonic), @@ -128,6 +134,7 @@ impl Client { pub fn change_nymd(&mut self, new_endpoint: Url) -> Result<(), ValidatorClientError> { self.nymd = NymdClient::connect_with_mnemonic( + self.network, new_endpoint.as_ref(), self.mixnet_contract_address.clone(), self.vesting_contract_address.clone(), @@ -155,6 +162,7 @@ impl Client { )?; Ok(Client { + network: config.network, mixnet_contract_address: config.mixnet_contract_address, vesting_contract_address: config.vesting_contract_address, mnemonic: None, diff --git a/common/client-libs/validator-client/src/nymd/mod.rs b/common/client-libs/validator-client/src/nymd/mod.rs index 22b49d68e5..dec4edcbc7 100644 --- a/common/client-libs/validator-client/src/nymd/mod.rs +++ b/common/client-libs/validator-client/src/nymd/mod.rs @@ -106,6 +106,7 @@ impl NymdClient { } pub fn connect_with_mnemonic( + network: config::defaults::all::Network, endpoint: U, mixnet_contract_address: Option, vesting_contract_address: Option, @@ -115,7 +116,8 @@ impl NymdClient { where U: TryInto, { - let wallet = DirectSecp256k1HdWallet::from_mnemonic(mnemonic)?; + let prefix = network.bech32_prefix(); + let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic)?; let client_address = wallet .try_derive_accounts()? .into_iter() @@ -259,16 +261,6 @@ impl NymdClient { self.client.get_balance(address, denom).await } - pub async fn get_mixnet_balance( - &self, - address: &AccountId, - ) -> Result, NymdError> - where - C: CosmWasmClient + Sync, - { - self.get_balance(address, self.denom()?).await - } - pub async fn get_total_supply(&self) -> Result, NymdError> where C: CosmWasmClient + Sync, diff --git a/common/client-libs/validator-client/src/nymd/wallet/mod.rs b/common/client-libs/validator-client/src/nymd/wallet/mod.rs index 8abbccb7a0..1cadbde59c 100644 --- a/common/client-libs/validator-client/src/nymd/wallet/mod.rs +++ b/common/client-libs/validator-client/src/nymd/wallet/mod.rs @@ -62,13 +62,15 @@ impl DirectSecp256k1HdWallet { } /// Restores a wallet from the given BIP39 mnemonic using default options. - pub fn from_mnemonic(mnemonic: bip39::Mnemonic) -> Result { - DirectSecp256k1HdWalletBuilder::new().build(mnemonic) + pub fn from_mnemonic(prefix: String, mnemonic: bip39::Mnemonic) -> Result { + DirectSecp256k1HdWalletBuilder::new() + .with_prefix(prefix) + .build(mnemonic) } - pub fn generate(word_count: usize) -> Result { + pub fn generate(prefix: String, word_count: usize) -> Result { let mneomonic = bip39::Mnemonic::generate(word_count)?; - Self::from_mnemonic(mneomonic) + Self::from_mnemonic(prefix, mneomonic) } fn derive_keypair(&self, hd_path: &DerivationPath) -> Result { @@ -203,7 +205,7 @@ impl DirectSecp256k1HdWalletBuilder { #[cfg(test)] mod tests { use super::*; - use network_defaults::BECH32_PREFIX; + use network_defaults::{default_network, BECH32_PREFIX}; #[test] fn generating_account_addresses() { @@ -228,7 +230,9 @@ mod tests { ]; for (mnemonic, address) in mnemonic_address.into_iter() { - let wallet = DirectSecp256k1HdWallet::from_mnemonic(mnemonic.parse().unwrap()).unwrap(); + let prefix = default_network().bech32_prefix(); + let wallet = + DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic.parse().unwrap()).unwrap(); assert_eq!( wallet.try_derive_accounts().unwrap()[0].address, address.parse().unwrap() diff --git a/common/credentials/src/coconut/utils.rs b/common/credentials/src/coconut/utils.rs index e65ddf8d38..c1f9897baf 100644 --- a/common/credentials/src/coconut/utils.rs +++ b/common/credentials/src/coconut/utils.rs @@ -28,7 +28,7 @@ use crate::error::Error; /// use credentials::obtain_aggregate_verification_key; /// /// async fn example() -> Result<(), ParseError> { -/// let validators = vec!["https://testnet-milhon-validator1.nymtech.net/api".parse()?, "https://testnet-milhon-validator2.nymtech.net/api".parse()?]; +/// let validators = vec!["https://sandbox-validator1.nymtech.net/api".parse()?, "https://sandbox-validator2.nymtech.net/api".parse()?]; /// let aggregated_key = obtain_aggregate_verification_key(&validators).await; /// // deal with the obtained Result /// Ok(()) diff --git a/common/network-defaults/build.rs b/common/network-defaults/build.rs index 6f24be9d3d..6d97abcfe6 100644 --- a/common/network-defaults/build.rs +++ b/common/network-defaults/build.rs @@ -3,7 +3,7 @@ fn main() { match option_env!("NETWORK") { - Some("milhon") => println!("cargo:rustc-cfg=network=\"milhon\"",), + Some("mainnet") => println!("cargo:rustc-cfg=network=\"mainnet\"",), None | Some("sandbox") => println!("cargo:rustc-cfg=network=\"sandbox\"",), Some("qa") => println!("cargo:rustc-cfg=network=\"qa\""), _ => panic!("No such network"), diff --git a/common/network-defaults/src/all.rs b/common/network-defaults/src/all.rs index e0b5f11127..f3e42442b2 100644 --- a/common/network-defaults/src/all.rs +++ b/common/network-defaults/src/all.rs @@ -4,13 +4,23 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use crate::{milhon, qa, sandbox, ValidatorDetails}; +use crate::{mainnet, qa, sandbox, ValidatorDetails}; -#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub enum Network { - MILHON, QA, SANDBOX, + MAINNET, +} + +impl Network { + pub fn bech32_prefix(&self) -> String { + match self { + Self::QA => String::from(qa::BECH32_PREFIX), + Self::SANDBOX => String::from(sandbox::BECH32_PREFIX), + Self::MAINNET => String::from(mainnet::BECH32_PREFIX), + } + } } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -35,36 +45,23 @@ impl SupportedNetworks { for network in support { match network { - Network::MILHON => networks.insert( - Network::MILHON, + Network::MAINNET => networks.insert( + Network::MAINNET, NetworkDetails { - bech32_prefix: String::from(milhon::BECH32_PREFIX), - denom: String::from(milhon::DENOM), - mixnet_contract_address: String::from(milhon::MIXNET_CONTRACT_ADDRESS), - vesting_contract_address: String::from(milhon::VESTING_CONTRACT_ADDRESS), + bech32_prefix: String::from(mainnet::BECH32_PREFIX), + denom: String::from(mainnet::DENOM), + mixnet_contract_address: String::from(mainnet::MIXNET_CONTRACT_ADDRESS), + vesting_contract_address: String::from(mainnet::VESTING_CONTRACT_ADDRESS), bandwidth_claim_contract_address: String::from( - milhon::BANDWIDTH_CLAIM_CONTRACT_ADDRESS, + mainnet::BANDWIDTH_CLAIM_CONTRACT_ADDRESS, ), rewarding_validator_address: String::from( - milhon::REWARDING_VALIDATOR_ADDRESS, + mainnet::REWARDING_VALIDATOR_ADDRESS, ), - validators: milhon::validators(), - }, - ), - Network::QA => networks.insert( - Network::QA, - NetworkDetails { - bech32_prefix: String::from(qa::BECH32_PREFIX), - denom: String::from(qa::DENOM), - mixnet_contract_address: String::from(qa::MIXNET_CONTRACT_ADDRESS), - vesting_contract_address: String::from(qa::VESTING_CONTRACT_ADDRESS), - bandwidth_claim_contract_address: String::from( - qa::BANDWIDTH_CLAIM_CONTRACT_ADDRESS, - ), - rewarding_validator_address: String::from(qa::REWARDING_VALIDATOR_ADDRESS), - validators: qa::validators(), + validators: mainnet::validators(), }, ), + Network::SANDBOX => networks.insert( Network::SANDBOX, NetworkDetails { @@ -81,6 +78,20 @@ impl SupportedNetworks { validators: sandbox::validators(), }, ), + Network::QA => networks.insert( + Network::QA, + NetworkDetails { + bech32_prefix: String::from(qa::BECH32_PREFIX), + denom: String::from(qa::DENOM), + mixnet_contract_address: String::from(qa::MIXNET_CONTRACT_ADDRESS), + vesting_contract_address: String::from(qa::VESTING_CONTRACT_ADDRESS), + bandwidth_claim_contract_address: String::from( + qa::BANDWIDTH_CLAIM_CONTRACT_ADDRESS, + ), + rewarding_validator_address: String::from(qa::REWARDING_VALIDATOR_ADDRESS), + validators: qa::validators(), + }, + ), }; } SupportedNetworks { networks } diff --git a/common/network-defaults/src/lib.rs b/common/network-defaults/src/lib.rs index 412d437c0d..b2e5c3a514 100644 --- a/common/network-defaults/src/lib.rs +++ b/common/network-defaults/src/lib.rs @@ -5,26 +5,26 @@ use url::Url; pub mod all; pub mod eth_contract; -mod milhon; -mod qa; -mod sandbox; +pub mod mainnet; +pub mod qa; +pub mod sandbox; cfg_if::cfg_if! { - if #[cfg(network = "milhon")] { - pub const BECH32_PREFIX: &str = milhon::BECH32_PREFIX; - pub const DENOM: &str = milhon::DENOM; + if #[cfg(network = "mainnet")] { + pub const BECH32_PREFIX: &str = mainnet::BECH32_PREFIX; + pub const DENOM: &str = mainnet::DENOM; - pub const DEFAULT_MIXNET_CONTRACT_ADDRESS: &str = milhon::MIXNET_CONTRACT_ADDRESS; - pub const DEFAULT_VESTING_CONTRACT_ADDRESS: &str = milhon::VESTING_CONTRACT_ADDRESS; - pub const DEFAULT_BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = milhon::BANDWIDTH_CLAIM_CONTRACT_ADDRESS; - pub const DEFAULT_REWARDING_VALIDATOR_ADDRESS: &str = milhon::REWARDING_VALIDATOR_ADDRESS; + pub const DEFAULT_MIXNET_CONTRACT_ADDRESS: &str = mainnet::MIXNET_CONTRACT_ADDRESS; + pub const DEFAULT_VESTING_CONTRACT_ADDRESS: &str = mainnet::VESTING_CONTRACT_ADDRESS; + pub const DEFAULT_BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = mainnet::BANDWIDTH_CLAIM_CONTRACT_ADDRESS; + pub const DEFAULT_REWARDING_VALIDATOR_ADDRESS: &str = mainnet::REWARDING_VALIDATOR_ADDRESS; pub fn default_validators() -> Vec { - milhon::validators() + mainnet::validators() } pub fn default_network() -> all::Network { - all::Network::MILHON + all::Network::MAINNET } } else if #[cfg(network = "qa")] { pub const BECH32_PREFIX: &str = qa::BECH32_PREFIX; diff --git a/common/network-defaults/src/mainnet.rs b/common/network-defaults/src/mainnet.rs new file mode 100644 index 0000000000..ebb30bc082 --- /dev/null +++ b/common/network-defaults/src/mainnet.rs @@ -0,0 +1,20 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ValidatorDetails; + +pub(crate) const BECH32_PREFIX: &str = "n"; +pub const DENOM: &str = "unym"; + +pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; +pub(crate) const VESTING_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; +pub(crate) const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = + "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; +pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "n17zujduc46wvkwvp6f062mm5xhr7jc3fewvqu9e"; + +pub(crate) fn validators() -> Vec { + vec![ValidatorDetails::new( + "https://rpc.nyx.nodes.guru/", + Some("https://api.nyx.nodes.guru/"), + )] +} diff --git a/common/network-defaults/src/milhon.rs b/common/network-defaults/src/milhon.rs deleted file mode 100644 index a4e4f3e640..0000000000 --- a/common/network-defaults/src/milhon.rs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::ValidatorDetails; - -pub(crate) const BECH32_PREFIX: &str = "punk"; -pub(crate) const DENOM: &str = "upunk"; - -pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = "punk10pyejy66429refv3g35g2t7am0was7yalwrzen"; -pub(crate) const VESTING_CONTRACT_ADDRESS: &str = ""; -pub(crate) const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = - "punk1jld76tqw4wnpfenmay2xkv86nr3j0w426eka82"; -pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "punk1v9qauwdq5terag6uvfsdytcs2d0sdmfdy7hgk3"; - -pub(crate) fn validators() -> Vec { - vec![ - ValidatorDetails::new( - "https://testnet-milhon-validator1.nymtech.net", - Some("https://testnet-milhon-validator1.nymtech.net/api"), - ), - ValidatorDetails::new("https://testnet-milhon-validator2.nymtech.net", None), - ] -} diff --git a/common/network-defaults/src/qa.rs b/common/network-defaults/src/qa.rs index 1e5fd029ae..206971b6a1 100644 --- a/common/network-defaults/src/qa.rs +++ b/common/network-defaults/src/qa.rs @@ -4,7 +4,7 @@ use crate::ValidatorDetails; pub(crate) const BECH32_PREFIX: &str = "nymt"; -pub(crate) const DENOM: &str = "unymt"; +pub const DENOM: &str = "unymt"; pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = "nymt14hj2tavq8fpesdwxxcu44rty3hh90vhuysqrsr"; pub(crate) const VESTING_CONTRACT_ADDRESS: &str = "nymt17p9rzwnnfxcjp32un9ug7yhhzgtkhvl9f8xzkv"; diff --git a/common/network-defaults/src/sandbox.rs b/common/network-defaults/src/sandbox.rs index 81b766882e..f010b9ac14 100644 --- a/common/network-defaults/src/sandbox.rs +++ b/common/network-defaults/src/sandbox.rs @@ -4,7 +4,7 @@ use crate::ValidatorDetails; pub(crate) const BECH32_PREFIX: &str = "nymt"; -pub(crate) const DENOM: &str = "unymt"; +pub const DENOM: &str = "unymt"; pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = "nymt1ghd753shjuwexxywmgs4xz7x2q732vcnstz02j"; pub(crate) const VESTING_CONTRACT_ADDRESS: &str = "nymt1nc5tatafv6eyq7llkr2gv50ff9e22mnfp9pc5s"; diff --git a/explorer-api/src/client.rs b/explorer-api/src/client.rs index 110067572c..9eb945c2a1 100644 --- a/explorer-api/src/client.rs +++ b/explorer-api/src/client.rs @@ -1,14 +1,16 @@ use network_defaults::{ - default_api_endpoints, default_nymd_endpoints, DEFAULT_MIXNET_CONTRACT_ADDRESS, + default_api_endpoints, default_network, default_nymd_endpoints, DEFAULT_MIXNET_CONTRACT_ADDRESS, }; use validator_client::nymd::QueryNymdClient; pub(crate) fn new_nymd_client() -> validator_client::Client { + let network = default_network(); let mixnet_contract = DEFAULT_MIXNET_CONTRACT_ADDRESS.to_string(); let nymd_url = default_nymd_endpoints()[0].clone(); let api_url = default_api_endpoints()[0].clone(); let client_config = validator_client::Config::new( + network, nymd_url, api_url, Some(mixnet_contract.parse().unwrap()), diff --git a/gateway/src/node/client_handling/websocket/connection_handler/eth_events.rs b/gateway/src/node/client_handling/websocket/connection_handler/eth_events.rs index f163daae6b..84b0de6510 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/eth_events.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/eth_events.rs @@ -43,6 +43,7 @@ impl ERC20Bridge { let mnemonic = Mnemonic::from_str(&cosmos_mnemonic).expect("Invalid Cosmos mnemonic provided"); let nymd_client = NymdClient::connect_with_mnemonic( + config::defaults::default_network(), nymd_url.as_ref(), AccountId::from_str(DEFAULT_BANDWIDTH_CLAIM_CONTRACT_ADDRESS).ok(), None, diff --git a/nym-wallet/.env.sample b/nym-wallet/.env.sample index e6c2d7524e..cc3febee19 100644 --- a/nym-wallet/.env.sample +++ b/nym-wallet/.env.sample @@ -1,4 +1 @@ -MAJOR_CURRENCY=nymt -MINOR_CURRENCY=unymt -ADMIN_ADDRESS=nymt1a586lvexy7ahva8h27fp6h8ds8taea6p95w3vx -NETWORK_NAME=sandbox \ No newline at end of file +ADMIN_ADDRESS= \ No newline at end of file diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 7afbc04e3d..2f6f0133ce 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -24,6 +24,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" +[[package]] +name = "aead" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" +dependencies = [ + "generic-array 0.14.4", +] + [[package]] name = "aes" version = "0.7.5" @@ -37,6 +46,20 @@ dependencies = [ "opaque-debug 0.3.0", ] +[[package]] +name = "aes-gcm" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df5f85a83a7d8b0442b6aa7b504b8212c1733da07b98aae43d4bc21b2cb3cdf6" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle 2.4.1", +] + [[package]] name = "aho-corasick" version = "0.7.18" @@ -61,6 +84,17 @@ version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203" +[[package]] +name = "argon2" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1574351abf0e4ef0de867b083a9f8e2f13618efcad6d3253c53554e4a887ed5" +dependencies = [ + "base64ct", + "blake2 0.10.2", + "password-hash", +] + [[package]] name = "arrayref" version = "0.3.6" @@ -166,9 +200,9 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "base64ct" -version = "1.3.3" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874f8444adcb4952a8bc51305c8be95c8ec8237bb0d2e78d2e039f771f8828a0" +checksum = "8a32fd6af2b5827bce66c29053ba0e7c42b9dcab01835835058558c10851a46b" [[package]] name = "bincode" @@ -234,6 +268,15 @@ dependencies = [ "opaque-debug 0.2.3", ] +[[package]] +name = "blake2" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94ba84325db59637ffc528bbe8c7f86c02c57cff5c0e2b9b00f9a851f42f309" +dependencies = [ + "digest 0.10.1", +] + [[package]] name = "blake3" version = "1.2.0" @@ -278,6 +321,15 @@ dependencies = [ "generic-array 0.14.4", ] +[[package]] +name = "block-buffer" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1d36a02058e76b040de25a4464ba1c80935655595b661505c8b39b664828b95" +dependencies = [ + "generic-array 0.14.4", +] + [[package]] name = "block-padding" version = "0.1.5" @@ -868,6 +920,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "crypto-common" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d6b536309245c849479fba3da410962a43ed8e51c26b729208ec0ac2798d0" +dependencies = [ + "generic-array 0.14.4", +] + [[package]] name = "crypto-mac" version = "0.7.0" @@ -1078,6 +1139,18 @@ dependencies = [ "generic-array 0.14.4", ] +[[package]] +name = "digest" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b697d66081d42af4fba142d56918a3cb21dc8eb63372c6b85d14f44fb9c5979b" +dependencies = [ + "block-buffer 0.10.0", + "crypto-common", + "generic-array 0.14.4", + "subtle 2.4.1", +] + [[package]] name = "dirs" version = "4.0.0" @@ -1330,9 +1403,9 @@ checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" [[package]] name = "fastrand" -version = "1.5.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b394ed3d285a429378d3b384b9eb1285267e7df4b166df24b7a6939a04dc392e" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" dependencies = [ "instant", ] @@ -1719,6 +1792,16 @@ dependencies = [ "syn", ] +[[package]] +name = "ghash" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99" +dependencies = [ + "opaque-debug 0.3.0", + "polyval", +] + [[package]] name = "gio" version = "0.14.8" @@ -2439,7 +2522,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae926706ba42c425c9457121178330d75e273df2e82e28b758faf3de3a9acb9" dependencies = [ "arrayref", - "blake2", + "blake2 0.8.1", "chacha", "keystream", ] @@ -2807,6 +2890,9 @@ dependencies = [ name = "nym_wallet" version = "1.0.0" dependencies = [ + "aes-gcm", + "argon2", + "base64", "bip39", "coconut-interface", "config", @@ -2822,12 +2908,14 @@ dependencies = [ "strum 0.23.0", "tauri", "tauri-build", + "tempfile", "tendermint-rpc", "thiserror", "tokio", "ts-rs", "url", "validator-client", + "zeroize", ] [[package]] @@ -3019,6 +3107,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "password-hash" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d791538a6dcc1e7cb7fe6f6b58aca40e7f79403c45b2bc274008b5e647af1d8" +dependencies = [ + "base64ct", + "rand_core 0.6.3", + "subtle 2.4.1", +] + [[package]] name = "paste" version = "1.0.6" @@ -3326,6 +3425,18 @@ dependencies = [ "miniz_oxide 0.3.7", ] +[[package]] +name = "polyval" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "opaque-debug 0.3.0", + "universal-hash", +] + [[package]] name = "ppv-lite86" version = "0.2.15" @@ -4292,7 +4403,7 @@ source = "git+https://github.com/nymtech/sphinx?rev=c494250f2a78bed33a618d470792 dependencies = [ "aes", "arrayref", - "blake2", + "blake2 0.8.1", "bs58", "byteorder", "chacha", @@ -4865,13 +4976,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ "cfg-if 1.0.0", + "fastrand", "libc", - "rand 0.8.4", "redox_syscall", "remove_dir_all", "winapi", @@ -5270,6 +5381,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +[[package]] +name = "universal-hash" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +dependencies = [ + "generic-array 0.14.4", + "subtle 2.4.1", +] + [[package]] name = "untrusted" version = "0.7.1" @@ -5698,9 +5819,9 @@ dependencies = [ [[package]] name = "x25519-dalek" -version = "1.2.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2392b6b94a576b4e2bf3c5b2757d63f10ada8020a2e4d08ac849ebcf6ea8e077" +checksum = "5a0c105152107e3b96f6a00a65e86ce82d9b125230e1c4302940eca58ff71f4f" dependencies = [ "curve25519-dalek", "rand_core 0.5.1", @@ -5718,9 +5839,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.3.0" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +checksum = "d68d9dcec5f9b43a30d38c49f91dfedfaac384cb8f085faca366c26207dd1619" dependencies = [ "zeroize_derive", ] diff --git a/nym-wallet/config.ts b/nym-wallet/config.ts index 76822a787c..0ae023cf84 100644 --- a/nym-wallet/config.ts +++ b/nym-wallet/config.ts @@ -1,6 +1,3 @@ export const config = { - MAJOR_CURRENCY: process.env.MAJOR_CURRENCY, - MINOR_CURRENCY: process.env.MINOR_CURRENCY, ADMIN_ADDRESS: process.env.ADMIN_ADDRESS, - NETWORK_NAME: process.env.NETWORK_NAME, } diff --git a/nym-wallet/src-tauri/Cargo.toml b/nym-wallet/src-tauri/Cargo.toml index 91a693d95e..7063375b8a 100644 --- a/nym-wallet/src-tauri/Cargo.toml +++ b/nym-wallet/src-tauri/Cargo.toml @@ -29,6 +29,10 @@ url = "2.2" rand = "0.6.5" eyre = "0.6.5" +aes-gcm = "0.9.4" +argon2 = { version = "0.3.2", features = ["std"] } +base64 = "0.13" +zeroize = "1.4.3" cosmrs = { version = "0.4.1", features = ["rpc", "bip32", "cosmwasm"] } cosmwasm-std = "1.0.0-beta3" @@ -43,6 +47,7 @@ credentials = { path = "../../common/credentials" } [dev-dependencies] ts-rs = "5.1" +tempfile = "3.3.0" [dev-dependencies.mixnet-contract-common] path = "../../common/cosmwasm-smart-contracts/mixnet-contract" diff --git a/nym-wallet/src-tauri/src/coin.rs b/nym-wallet/src-tauri/src/coin.rs index 5df1d9532b..5c27fd737e 100644 --- a/nym-wallet/src-tauri/src/coin.rs +++ b/nym-wallet/src-tauri/src/coin.rs @@ -1,6 +1,7 @@ // This should be moved out of the wallet, and used as a primary coin type throughout the codebase use crate::error::BackendError; +use crate::network::Network; use ::config::defaults::DENOM; use cosmrs::Decimal; use cosmrs::Denom as CosmosDenom; @@ -11,6 +12,7 @@ use std::convert::TryFrom; use std::fmt; use std::ops::{Add, Sub}; use std::str::FromStr; +use strum::IntoEnumIterator; use validator_client::nymd::{CosmosCoin, GasPrice}; #[cfg_attr(test, derive(ts_rs::TS))] @@ -36,13 +38,15 @@ impl FromStr for Denom { fn from_str(s: &str) -> Result { let s = s.to_lowercase(); - if s == DENOM.to_lowercase() || s == "minor" { - Ok(Denom::Minor) - } else if s == DENOM[1..].to_lowercase() || s == "major" { - Ok(Denom::Major) - } else { - Err(BackendError::InvalidDenom(s)) + for network in Network::iter() { + let denom = network.denom(); + if s == denom.as_ref().to_lowercase() || s == "minor" { + return Ok(Denom::Minor); + } else if s == denom.as_ref()[1..].to_lowercase() || s == "major" { + return Ok(Denom::Major); + } } + Err(BackendError::InvalidDenom(s)) } } diff --git a/nym-wallet/src-tauri/src/config/mod.rs b/nym-wallet/src-tauri/src/config/mod.rs index 95de5b345b..401bf07ab3 100644 --- a/nym-wallet/src-tauri/src/config/mod.rs +++ b/nym-wallet/src-tauri/src/config/mod.rs @@ -89,23 +89,23 @@ impl Config { } } - pub fn get_mixnet_contract_address(&self, network: Network) -> cosmrs::AccountId { + pub fn get_mixnet_contract_address(&self, network: Network) -> Option { self .base .networks .mixnet_contract_address(network.into()) .expect("No mixnet contract address found in config") .parse() - .expect("stored mixnet contract address is not a valid account address") + .ok() } - pub fn get_vesting_contract_address(&self, network: Network) -> cosmrs::AccountId { + pub fn get_vesting_contract_address(&self, network: Network) -> Option { self .base .networks .vesting_contract_address(network.into()) .expect("No vesting contract address found in config") .parse() - .expect("stored vesting contract address is not a valid account address") + .ok() } } diff --git a/nym-wallet/src-tauri/src/error.rs b/nym-wallet/src-tauri/src/error.rs index 9101549fb5..41d03532d2 100644 --- a/nym-wallet/src-tauri/src/error.rs +++ b/nym-wallet/src-tauri/src/error.rs @@ -1,4 +1,5 @@ use serde::{Serialize, Serializer}; +use std::io; use thiserror::Error; use validator_client::nymd::error::NymdError; use validator_client::validator_api::error::ValidatorAPIError; @@ -35,6 +36,25 @@ pub enum BackendError { #[from] source: ValidatorAPIError, }, + #[error("{source}")] + KeyDerivationError { + #[from] + source: argon2::Error, + }, + #[error("{source}")] + IOError { + #[from] + source: io::Error, + }, + #[error("{source}")] + SerdeJsonError { + #[from] + source: serde_json::Error, + }, + #[error("failed to encrypt the given data with the provided password")] + EncryptionError, + #[error("failed to decrypt the given data with the provided password")] + DecryptionError, #[error("Client has not been initialized yet, connect with mnemonic to initialize")] ClientNotInitialized, #[error("No balance available for address {0}")] @@ -43,6 +63,8 @@ pub enum BackendError { InvalidDenom(String), #[error("The provided network is not supported (yet)")] NetworkNotSupported(config::defaults::all::Network), + #[error("Could not access the local data storage directory")] + UnknownStorageDirectory, } impl Serialize for BackendError { @@ -50,6 +72,6 @@ impl Serialize for BackendError { where S: Serializer, { - serializer.serialize_str(&self.to_string()) + serializer.collect_str(self) } } diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index 610bc58b66..2f1328c434 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -17,6 +17,9 @@ mod network; mod operations; mod state; mod utils; +// temporarily until it is actually used +#[allow(unused)] +mod wallet_storage; use crate::menu::AddDefaultSubmenus; use crate::operations::mixnet; diff --git a/nym-wallet/src-tauri/src/network.rs b/nym-wallet/src-tauri/src/network.rs index 3c04f70253..278f432922 100644 --- a/nym-wallet/src-tauri/src/network.rs +++ b/nym-wallet/src-tauri/src/network.rs @@ -1,12 +1,15 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use cosmrs::Denom; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; +use std::str::FromStr; use strum::EnumIter; use crate::error::BackendError; use config::defaults::all::Network as ConfigNetwork; +use config::defaults::{mainnet, qa, sandbox}; #[allow(clippy::upper_case_acronyms)] #[cfg_attr(test, derive(ts_rs::TS))] @@ -14,6 +17,18 @@ use config::defaults::all::Network as ConfigNetwork; pub enum Network { QA, SANDBOX, + MAINNET, +} + +impl Network { + pub fn denom(&self) -> Denom { + match self { + // network defaults should be correctly formatted + Network::QA => Denom::from_str(qa::DENOM).unwrap(), + Network::SANDBOX => Denom::from_str(sandbox::DENOM).unwrap(), + Network::MAINNET => Denom::from_str(mainnet::DENOM).unwrap(), + } + } } impl Default for Network { @@ -28,6 +43,7 @@ impl Into for Network { match self { Network::QA => ConfigNetwork::QA, Network::SANDBOX => ConfigNetwork::SANDBOX, + Network::MAINNET => ConfigNetwork::MAINNET, } } } @@ -39,7 +55,7 @@ impl TryFrom for Network { match value { ConfigNetwork::QA => Ok(Network::QA), ConfigNetwork::SANDBOX => Ok(Network::SANDBOX), - _ => Err(BackendError::NetworkNotSupported(value)), + ConfigNetwork::MAINNET => Ok(Network::MAINNET), } } } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/account.rs b/nym-wallet/src-tauri/src/operations/mixnet/account.rs index c05739d761..348f351c70 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/account.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/account.rs @@ -57,8 +57,9 @@ pub async fn connect_with_mnemonic( pub async fn get_balance( state: tauri::State<'_, Arc>>, ) -> Result { + let denom = state.read().await.current_network().denom(); match nymd_client!(state) - .get_mixnet_balance(nymd_client!(state).address()) + .get_balance(nymd_client!(state).address(), denom.clone()) .await { Ok(Some(coin)) => { @@ -68,7 +69,7 @@ pub async fn get_balance( ); Ok(Balance { coin: coin.clone(), - printable_balance: coin.to_major().to_string(), + printable_balance: format!("{} {}", coin.to_major().amount(), &denom.as_ref()[1..]), }) } Ok(None) => Err(BackendError::NoBalance( @@ -98,11 +99,12 @@ pub async fn switch_network( let account = { let r_state = state.read().await; let client = r_state.client(network)?; + let denom = network.denom(); Account::new( client.nymd.mixnet_contract_address()?.to_string(), client.nymd.address().to_string(), - client.nymd.denom()?.try_into()?, + denom.try_into()?, ) }; @@ -134,10 +136,11 @@ async fn _connect_with_mnemonic( let config = state.read().await.config(); match validator_client::Client::new_signing( validator_client::Config::new( + network.into(), config.get_nymd_validator_url(network), config.get_validator_api_url(network), - Some(config.get_mixnet_contract_address(network)), - Some(config.get_vesting_contract_address(network)), + config.get_mixnet_contract_address(network), + config.get_vesting_contract_address(network), ), mnemonic.clone(), ) { @@ -150,7 +153,7 @@ async fn _connect_with_mnemonic( default_account = Some(Account::new( client.nymd.mixnet_contract_address()?.to_string(), client.nymd.address().to_string(), - client.nymd.denom()?.try_into()?, + network.denom().try_into()?, )); } diff --git a/nym-wallet/src-tauri/src/state.rs b/nym-wallet/src-tauri/src/state.rs index 430a81ca56..61c70d95a0 100644 --- a/nym-wallet/src-tauri/src/state.rs +++ b/nym-wallet/src-tauri/src/state.rs @@ -40,6 +40,10 @@ impl State { self.current_network = network; } + pub fn current_network(&self) -> Network { + self.current_network + } + pub fn logout(&mut self) { self.signing_clients = HashMap::new(); } diff --git a/nym-wallet/src-tauri/src/wallet_storage/account_data.rs b/nym-wallet/src-tauri/src/wallet_storage/account_data.rs new file mode 100644 index 0000000000..487b029eb0 --- /dev/null +++ b/nym-wallet/src-tauri/src/wallet_storage/account_data.rs @@ -0,0 +1,86 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmrs::bip32::DerivationPath; +use serde::de::Visitor; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::fmt::Formatter; +use zeroize::Zeroize; +use zeroize::Zeroizing; + +// future-proofing +#[derive(Serialize, Deserialize, Debug, Zeroize)] +#[serde(untagged)] +#[zeroize(drop)] +pub(crate) enum StoredAccount { + Mnemonic(MnemonicAccount), + // PrivateKey(PrivateKeyAccount) +} + +impl StoredAccount { + pub(crate) fn new_mnemonic_backed_account( + mnemonic: bip39::Mnemonic, + hd_path: DerivationPath, + ) -> StoredAccount { + StoredAccount::Mnemonic(MnemonicAccount { mnemonic, hd_path }) + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub(crate) struct MnemonicAccount { + mnemonic: bip39::Mnemonic, + #[serde(with = "display_hd_path")] + hd_path: DerivationPath, +} + +// we only ever want to expose those getters in the test code +#[cfg(test)] +impl MnemonicAccount { + pub(crate) fn mnemonic(&self) -> &bip39::Mnemonic { + &self.mnemonic + } + + pub(crate) fn hd_path(&self) -> &DerivationPath { + &self.hd_path + } +} + +impl Zeroize for MnemonicAccount { + fn zeroize(&mut self) { + // in ideal world, Mnemonic would have had zeroize defined on it (there's an almost year old PR that introduces it) + // and the memory would have been filled with zeroes. + // + // we really don't want to keep our real mnemonic in memory, so let's do the semi-nasty thing + // of overwriting it with a fresh mnemonic that was never used before + // + // note: this function can only fail on an invalid word count, which clearly is not the case here + self.mnemonic = bip39::Mnemonic::generate(self.mnemonic.word_count()).unwrap(); + + // further note: we don't really care about the hd_path, there's nothing secret about it. + } +} + +impl Drop for MnemonicAccount { + fn drop(&mut self) { + self.zeroize() + } +} + +mod display_hd_path { + use cosmrs::bip32::DerivationPath; + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize( + hd_path: &DerivationPath, + serializer: S, + ) -> Result { + serializer.collect_str(hd_path) + } + + pub fn deserialize<'de, D: Deserializer<'de>>( + deserializer: D, + ) -> Result { + let s = <&str>::deserialize(deserializer)?; + s.parse().map_err(serde::de::Error::custom) + } +} diff --git a/nym-wallet/src-tauri/src/wallet_storage/encryption.rs b/nym-wallet/src-tauri/src/wallet_storage/encryption.rs new file mode 100644 index 0000000000..af30b350af --- /dev/null +++ b/nym-wallet/src-tauri/src/wallet_storage/encryption.rs @@ -0,0 +1,271 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::password::UserPassword; +use crate::error::BackendError; +use aes_gcm::aead::generic_array::ArrayLength; +use aes_gcm::aead::{Aead, NewAead, Payload}; +use aes_gcm::{Aes256Gcm, Key, Nonce}; +use argon2::{ + password_hash::rand_core::{OsRng, RngCore}, + Algorithm, Argon2, Params, Version, +}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::convert::TryFrom; +use std::marker::PhantomData; +use zeroize::Zeroize; + +const MEMORY_COST: u32 = 16 * 1024; +const ITERATIONS: u32 = 3; +const PARALLELISM: u32 = 1; +const OUTPUT_LENGTH: usize = 32; + +// as per Argon2 recommendation +const SALT_LEN: usize = 16; + +// AES256GCM Nonce is 96 bit long. +const IV_LEN: usize = 12; + +#[derive(Debug, Serialize, Deserialize, Zeroize)] +pub(crate) struct EncryptedData { + #[serde(with = "base64")] + ciphertext: Vec, + #[serde(with = "base64")] + salt: Vec, + #[serde(with = "base64")] + iv: Vec, + + #[serde(skip)] + #[zeroize(skip)] + _marker: PhantomData, +} + +impl Drop for EncryptedData { + fn drop(&mut self) { + self.zeroize() + } +} + +// we only ever want to expose those getters in the test code +#[cfg(test)] +impl EncryptedData { + pub(crate) fn ciphertext(&self) -> &[u8] { + &self.ciphertext + } + + pub(crate) fn salt(&self) -> &[u8] { + &self.salt + } + + pub(crate) fn iv(&self) -> &[u8] { + &self.iv + } +} + +// helper to make Vec serialization use base64 representation to make it human readable +// so that it would be easier for users to copy contents from the disk if they wanted to use it elsewhere +mod base64 { + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(bytes: &[u8], serializer: S) -> Result { + serializer.serialize_str(&base64::encode(bytes)) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result, D::Error> { + let s = ::deserialize(deserializer)?; + base64::decode(&s).map_err(serde::de::Error::custom) + } +} + +impl EncryptedData { + pub(crate) fn encrypt_struct(data: &T, password: &UserPassword) -> Result + where + T: Serialize, + { + encrypt_struct(data, password) + } + + pub(crate) fn decrypt_struct(&self, password: &UserPassword) -> Result + where + T: for<'a> Deserialize<'a>, + { + decrypt_struct(self, password) + } +} + +impl EncryptedData> { + pub(crate) fn encrypt_data(data: &[u8], password: &UserPassword) -> Result { + encrypt_data(data, password) + } + + pub(crate) fn decrypt_data(&self, password: &UserPassword) -> Result, BackendError> { + decrypt_data(self, password) + } +} + +fn derive_cipher_key( + password: &UserPassword, + salt: &[u8], +) -> Result, BackendError> +where + KeySize: ArrayLength, +{ + // this can only fail if output length is either smaller than 4 or larger than 2^32 - 1 which is not the case here + let params = Params::new(MEMORY_COST, ITERATIONS, PARALLELISM, Some(OUTPUT_LENGTH)).unwrap(); + + let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params); + + let mut key = Key::default(); + argon2.hash_password_into(password.as_bytes(), salt, &mut key)?; + + Ok(key) +} + +fn random_salt_and_iv() -> (Vec, Vec) { + let mut rng = OsRng; + + let mut salt = vec![0u8; SALT_LEN]; + rng.fill_bytes(&mut salt); + + let mut iv = vec![0u8; IV_LEN]; + rng.fill_bytes(&mut iv); + + (salt, iv) +} + +fn encrypt( + data: &[u8], + password: &UserPassword, + salt: &[u8], + iv: &[u8], +) -> Result, BackendError> { + let key = derive_cipher_key(password, salt)?; + let cipher = Aes256Gcm::new(&key); + cipher + .encrypt(Nonce::from_slice(iv), data) + .map_err(|_| BackendError::EncryptionError) +} + +fn decrypt( + ciphertext: &[u8], + password: &UserPassword, + salt: &[u8], + iv: &[u8], +) -> Result, BackendError> { + let key = derive_cipher_key(password, salt)?; + let cipher = Aes256Gcm::new(&key); + cipher + .decrypt(Nonce::from_slice(iv), ciphertext) + .map_err(|_| BackendError::DecryptionError) +} + +pub(crate) fn encrypt_data( + data: &[u8], + password: &UserPassword, +) -> Result>, BackendError> { + let (salt, iv) = random_salt_and_iv(); + let ciphertext = encrypt(data, password, &salt, &iv)?; + + Ok(EncryptedData { + ciphertext, + salt, + iv, + _marker: Default::default(), + }) +} + +pub(crate) fn encrypt_struct( + data: &T, + password: &UserPassword, +) -> Result, BackendError> +where + T: Serialize, +{ + let bytes = serde_json::to_vec(data).map_err(|_| BackendError::EncryptionError)?; + + let (salt, iv) = random_salt_and_iv(); + let ciphertext = encrypt(&bytes, password, &salt, &iv)?; + + Ok(EncryptedData { + ciphertext, + salt, + iv, + _marker: Default::default(), + }) +} + +pub(crate) fn decrypt_data( + encrypted_data: &EncryptedData>, + password: &UserPassword, +) -> Result, BackendError> { + decrypt( + &encrypted_data.ciphertext, + password, + &encrypted_data.salt, + &encrypted_data.iv, + ) +} + +pub(crate) fn decrypt_struct( + encrypted_data: &EncryptedData, + password: &UserPassword, +) -> Result +where + T: for<'a> Deserialize<'a>, +{ + let bytes = decrypt( + &encrypted_data.ciphertext, + password, + &encrypted_data.salt, + &encrypted_data.iv, + )?; + + serde_json::from_slice(&bytes).map_err(|_| BackendError::DecryptionError) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct DummyData { + foo: String, + bar: String, + } + + #[test] + fn struct_encryption() { + let password = UserPassword::new("my-super-secret-password".to_string()); + let data = DummyData { + foo: "my secret mnemonic".to_string(), + bar: "totally-valid-hd-path".to_string(), + }; + + let wrong_password = UserPassword::new("brute-force-attempt-1".to_string()); + + let mut encrypted_data = encrypt_struct(&data, &password).unwrap(); + let recovered = decrypt_struct(&encrypted_data, &password).unwrap(); + assert_eq!(data, recovered); + + // decryption with wrong password fails + assert!(decrypt_struct(&encrypted_data, &wrong_password).is_err()); + + // decryption fails if ciphertext got malformed + encrypted_data.ciphertext[3] ^= 123; + assert!(decrypt_struct(&encrypted_data, &wrong_password).is_err()); + + // restore the ciphertext (for test purposes) + encrypted_data.ciphertext[3] ^= 123; + + // decryption fails if salt got malformed (it would result in incorrect key being derived) + encrypted_data.salt[3] ^= 123; + assert!(decrypt_struct(&encrypted_data, &password).is_err()); + + // restore the salt (for test purposes) + encrypted_data.salt[3] ^= 123; + + // decryption fails if iv got malformed + encrypted_data.iv[3] ^= 123; + assert!(decrypt_struct(&encrypted_data, &password).is_err()); + } +} diff --git a/nym-wallet/src-tauri/src/wallet_storage/mod.rs b/nym-wallet/src-tauri/src/wallet_storage/mod.rs new file mode 100644 index 0000000000..de5c95c1e0 --- /dev/null +++ b/nym-wallet/src-tauri/src/wallet_storage/mod.rs @@ -0,0 +1,199 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::BackendError; +use crate::wallet_storage::account_data::StoredAccount; +use crate::wallet_storage::encryption::{encrypt_struct, EncryptedData}; +use crate::wallet_storage::password::UserPassword; +use cosmrs::bip32::DerivationPath; +use std::fs::{create_dir_all, OpenOptions}; +use std::path::PathBuf; +pub(crate) mod account_data; +pub(crate) mod encryption; +mod password; + +const STORAGE_DIR_NAME: &str = "NymWallet"; +const WALLET_INFO_FILENAME: &str = "saved_wallet.json"; + +fn get_storage_directory() -> Result { + tauri::api::path::local_data_dir() + .map(|dir| dir.join(STORAGE_DIR_NAME)) + .ok_or(BackendError::UnknownStorageDirectory) +} + +pub(crate) fn wallet_login_filepath() -> Result { + get_storage_directory().map(|dir| dir.join(WALLET_INFO_FILENAME)) +} + +pub(crate) fn load_existing_wallet_login_information( + password: &UserPassword, +) -> Result, BackendError> { + let store_dir = get_storage_directory()?; + let filepath = store_dir.join(WALLET_INFO_FILENAME); + + load_existing_wallet_login_information_at_file(filepath, password) +} + +fn load_existing_wallet_login_information_at_file( + filepath: PathBuf, + password: &UserPassword, +) -> Result, BackendError> { + if !filepath.exists() { + return Ok(Vec::new()); + } + let file = OpenOptions::new().read(true).open(filepath)?; + let encrypted_data: EncryptedData> = serde_json::from_reader(file)?; + encrypted_data.decrypt_struct(password) +} + +pub(crate) fn store_wallet_login_information( + mnemonic: bip39::Mnemonic, + hd_path: DerivationPath, + password: UserPassword, +) -> Result<(), BackendError> { + // make sure the entire directory structure exists + let store_dir = get_storage_directory()?; + create_dir_all(&store_dir)?; + let filepath = store_dir.join(WALLET_INFO_FILENAME); + + store_wallet_login_information_at_file(filepath, mnemonic, hd_path, &password) +} + +fn store_wallet_login_information_at_file( + filepath: PathBuf, + mnemonic: bip39::Mnemonic, + hd_path: DerivationPath, + password: &UserPassword, +) -> Result<(), BackendError> { + let mut all_accounts = + load_existing_wallet_login_information_at_file(filepath.clone(), password)?; + let new_account = StoredAccount::new_mnemonic_backed_account(mnemonic, hd_path); + all_accounts.push(new_account); + + let encrypted = encrypt_struct(&all_accounts, password)?; + + let file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(filepath)?; + + serde_json::to_writer_pretty(file, &encrypted)?; + + Ok(()) +} + +// this function should probably exist, but I guess we need to discuss how it should behave in the context of the UX +// pub(crate) fn remove_wallet_login_information( +// +// ) + +#[cfg(test)] +mod tests { + use super::*; + use config::defaults::COSMOS_DERIVATION_PATH; + use std::path::Path; + use tempfile::tempdir; + + fn read_encrypted_blob(file: PathBuf) -> EncryptedData> { + let file = OpenOptions::new().read(true).open(&file).unwrap(); + serde_json::from_reader(file).unwrap() + } + + // I'm not 100% sure how to feel about having to touch the file system at all + #[test] + fn storing_wallet_information() { + let store_dir = tempdir().unwrap(); + let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); + + let dummy_account1 = bip39::Mnemonic::generate(24).unwrap(); + let dummy_account2 = bip39::Mnemonic::generate(24).unwrap(); + let cosmos_hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let different_hd_path: DerivationPath = "m".parse().unwrap(); + + let password = UserPassword::new("password".to_string()); + let bad_password = UserPassword::new("bad-password".to_string()); + + // nothing was stored on the disk, so regardless of password used, there will be no error, but + // returned list will be empty + assert!( + load_existing_wallet_login_information_at_file(wallet_file.clone(), &password) + .unwrap() + .is_empty() + ); + assert!( + load_existing_wallet_login_information_at_file(wallet_file.clone(), &bad_password) + .unwrap() + .is_empty() + ); + + // store the first account + store_wallet_login_information_at_file( + wallet_file.clone(), + dummy_account1.clone(), + cosmos_hd_path.clone(), + &password, + ) + .unwrap(); + + let encrypted_blob = read_encrypted_blob(wallet_file.clone()); + + // some actual ciphertext was saved + assert!(!encrypted_blob.ciphertext().is_empty()); + + // keep track of salt and iv for future assertion + let original_iv = encrypted_blob.iv().to_vec(); + let original_salt = encrypted_blob.salt().to_vec(); + + // trying to load it with wrong password now fails + assert!( + load_existing_wallet_login_information_at_file(wallet_file.clone(), &bad_password).is_err() + ); + + let loaded_accounts = + load_existing_wallet_login_information_at_file(wallet_file.clone(), &password).unwrap(); + println!("{:?}", loaded_accounts); + assert_eq!(1, loaded_accounts.len()); + + let StoredAccount::Mnemonic(acc) = &loaded_accounts[0]; + assert_eq!(&dummy_account1, acc.mnemonic()); + assert_eq!(&cosmos_hd_path, acc.hd_path()); + + // can't store extra account if you use different password + assert!(store_wallet_login_information_at_file( + wallet_file.clone(), + dummy_account2.clone(), + cosmos_hd_path.clone(), + &bad_password, + ) + .is_err()); + + // add extra account properly now + store_wallet_login_information_at_file( + wallet_file.clone(), + dummy_account2.clone(), + different_hd_path.clone(), + &password, + ) + .unwrap(); + + let encrypted_blob = read_encrypted_blob(wallet_file.clone()); + + // fresh IV and salt are used + assert_ne!(original_iv, encrypted_blob.iv()); + assert_ne!(original_salt, encrypted_blob.salt()); + + let loaded_accounts = + load_existing_wallet_login_information_at_file(wallet_file, &password).unwrap(); + assert_eq!(2, loaded_accounts.len()); + + // first account should be unchanged + let StoredAccount::Mnemonic(acc1) = &loaded_accounts[0]; + assert_eq!(&dummy_account1, acc1.mnemonic()); + assert_eq!(&cosmos_hd_path, acc1.hd_path()); + + let StoredAccount::Mnemonic(acc2) = &loaded_accounts[1]; + assert_eq!(&dummy_account2, acc2.mnemonic()); + assert_eq!(&different_hd_path, acc2.hd_path()); + } +} diff --git a/nym-wallet/src-tauri/src/wallet_storage/password.rs b/nym-wallet/src-tauri/src/wallet_storage/password.rs new file mode 100644 index 0000000000..0dd2b0f67c --- /dev/null +++ b/nym-wallet/src-tauri/src/wallet_storage/password.rs @@ -0,0 +1,25 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use zeroize::Zeroize; + +// simple wrapper for String that will get zeroized on drop +#[derive(Zeroize)] +#[zeroize(drop)] +pub(crate) struct UserPassword(String); + +impl UserPassword { + pub(crate) fn new(pass: String) -> UserPassword { + UserPassword(pass) + } + + pub(crate) fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +impl AsRef for UserPassword { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} diff --git a/nym-wallet/src/components/AppBar.tsx b/nym-wallet/src/components/AppBar.tsx index c30773a1fd..ce0d1f9729 100644 --- a/nym-wallet/src/components/AppBar.tsx +++ b/nym-wallet/src/components/AppBar.tsx @@ -3,12 +3,12 @@ import { AppBar as MuiAppBar, Divider, Grid, IconButton, Toolbar, Typography, us import { Box } from '@mui/system' import { Logout } from '@mui/icons-material' import { ClientContext } from '../context/main' -import { CopyToClipboard } from '.' +import { CopyToClipboard, NetworkSelector } from '.' import { Node as NodeIcon } from '../svg-icons/node' export const AppBar = () => { const { userBalance, clientDetails, showSettings, logOut, handleShowSettings } = useContext(ClientContext) - const matches = useMediaQuery('(min-width: 769px)') + const matches = useMediaQuery('(min-width: 900px)') return ( @@ -31,7 +31,10 @@ export const AppBar = () => { )} - + + + + { const [fee, setFee] = useState() + const {currency} = useContext(ClientContext) const getFee = async () => { const fee = await getGasFee(feeType) @@ -19,7 +20,7 @@ export const Fee = ({ feeType }: { feeType: Operation }) => { if (fee) { return ( - Fee for this transaction: {`${fee} ${MAJOR_CURRENCY}`}{' '} + Fee for this transaction: {`${fee} ${currency?.major}`}{' '} ) } diff --git a/nym-wallet/src/components/NetworkSelector.tsx b/nym-wallet/src/components/NetworkSelector.tsx new file mode 100644 index 0000000000..d615180729 --- /dev/null +++ b/nym-wallet/src/components/NetworkSelector.tsx @@ -0,0 +1,82 @@ +import React, { useState, useContext } from 'react' +import { Button, List, ListItem, ListItemIcon, ListItemText, ListSubheader, Popover } from '@mui/material' +import { CheckSharp, KeyboardArrowDown } from '@mui/icons-material' +import { ClientContext } from '../context/main' +import { Network } from 'src/types' + +const networks: { networkName: Network; name: string }[] = [ + { networkName: 'MAINNET', name: 'Nym Mainnet' }, + { networkName: 'SANDBOX', name: 'Testnet Sandbox' }, +] + +export const NetworkSelector = () => { + const { network, switchNetwork } = useContext(ClientContext) + + const [anchorEl, setAnchorEl] = useState(null) + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget) + } + + const handleClose = () => { + setAnchorEl(null) + } + + return ( + <> + + + + Network selection + {networks.map(({ name, networkName }) => ( + { + handleClose() + switchNetwork(networkName) + }} + /> + ))} + + + + ) +} + +const NetworkItem: React.FC<{ title: string; isSelected: boolean; onSelect: () => void }> = ({ + title, + isSelected, + onSelect, +}) => ( + + {isSelected && } + {title} + +) diff --git a/nym-wallet/src/components/NymLogo.tsx b/nym-wallet/src/components/NymLogo.tsx new file mode 100644 index 0000000000..af704dcc30 --- /dev/null +++ b/nym-wallet/src/components/NymLogo.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import Logo from '../images/logo-background.svg' + +const imgSize = { + ['small']: 40, + ['medium']: 80, + ['large']: 120, +} + +export const NymLogo = ({ size = 'medium' }: { size?: 'small' | 'medium' | 'large' }) => diff --git a/nym-wallet/src/components/index.ts b/nym-wallet/src/components/index.ts index d8f23958b1..4b69edd5d2 100644 --- a/nym-wallet/src/components/index.ts +++ b/nym-wallet/src/components/index.ts @@ -7,4 +7,7 @@ export * from './RequestStatus' export * from './NoClientError' export * from './SuccessResponse' export * from './TransactionDetails' +export * from './NymLogo' export * from './Fee' +export * from './AppBar' +export * from './NetworkSelector' diff --git a/nym-wallet/src/context/main.tsx b/nym-wallet/src/context/main.tsx index 14a45af145..1a294f19f5 100644 --- a/nym-wallet/src/context/main.tsx +++ b/nym-wallet/src/context/main.tsx @@ -1,70 +1,106 @@ import React, { createContext, useEffect, useState } from 'react' -import { useHistory } from 'react-router-dom' -import { TClientDetails, TMixnodeBondDetails, TSignInWithMnemonic } from '../types' +import { Account, Network, TCurrency, TMixnodeBondDetails } from '../types' import { TUseuserBalance, useGetBalance } from '../hooks/useGetBalance' import { config } from '../../config' -import { getMixnodeBondDetails } from '../requests' +import { getMixnodeBondDetails, selectNetwork, signOut } from '../requests' +import { currencyMap } from '../utils' +import { useHistory } from 'react-router-dom' -export const { MAJOR_CURRENCY, MINOR_CURRENCY, ADMIN_ADDRESS, NETWORK_NAME } = config +export const { ADMIN_ADDRESS } = config -export const urls = { - blockExplorer: `https://${NETWORK_NAME}-blocks.nymtech.net`, - networkExplorer: `https://${NETWORK_NAME}-explorer.nymtech.net`, -} +export const urls = (network: Network) => ({ + blockExplorer: `https://${network}-blocks.nymtech.net`, + networkExplorer: `https://${network}-explorer.nymtech.net`, +}) type TClientContext = { mode: 'light' | 'dark' - clientDetails?: TClientDetails + clientDetails?: Account mixnodeDetails?: TMixnodeBondDetails | null userBalance: TUseuserBalance showAdmin: boolean showSettings: boolean + network?: Network + currency?: TCurrency + switchNetwork: (network: Network) => void getBondDetails: () => Promise handleShowSettings: () => void handleShowAdmin: () => void - logIn: (clientDetails: TSignInWithMnemonic) => void + logIn: (network: Network) => void logOut: () => void } export const ClientContext = createContext({} as TClientContext) export const ClientContextProvider = ({ children }: { children: React.ReactNode }) => { - const [clientDetails, setClientDetails] = useState() + const [clientDetails, setClientDetails] = useState() const [mixnodeDetails, setMixnodeDetails] = useState() + const [network, setNetwork] = useState() + const [currency, setCurrency] = useState() const [showAdmin, setShowAdmin] = useState(false) const [showSettings, setShowSettings] = useState(false) const [mode, setMode] = useState<'light' | 'dark'>('light') - const history = useHistory() const userBalance = useGetBalance() + const history = useHistory() useEffect(() => { - if (!clientDetails) { - history.push('/signin') - } else { + if (clientDetails) { userBalance.fetchBalance() - history.push('/balance') } }, [clientDetails, userBalance.fetchBalance]) - const logIn = async (clientDetails: TSignInWithMnemonic) => { - await getBondDetails() - setClientDetails(clientDetails) + useEffect(() => { + const refreshAccount = async () => { + if (network) { + await loadAccount(network) + await getBondDetails() + userBalance.fetchBalance() + } + } + refreshAccount() + }, [network]) + + const logIn = async (network: Network) => { + try { + setNetwork(network) + history.push('/balance') + } catch (e) { + console.log({ e }) + } } - const logOut = () => { + const loadAccount = async (network: Network) => { + try { + const clientDetails = await selectNetwork(network) + setClientDetails(clientDetails) + } catch (e) { + } finally { + setCurrency(currencyMap(network)) + } + } + + const logOut = async () => { setClientDetails(undefined) - userBalance.clearBalance() + setNetwork(undefined) + await signOut() } const handleShowAdmin = () => setShowAdmin((show) => !show) const handleShowSettings = () => setShowSettings((show) => !show) const getBondDetails = async () => { - const mixnodeDetails = await getMixnodeBondDetails() - setMixnodeDetails(mixnodeDetails) + setMixnodeDetails(undefined) + try { + const mixnodeDetails = await getMixnodeBondDetails() + setMixnodeDetails(mixnodeDetails) + } catch (e) { + console.log(e) + } } + const switchNetwork = (network: Network) => setNetwork(network) + return ( { - const [ownership, setOwnership] = useState({ - hasOwnership: false, - nodeType: undefined, - }) + const { clientDetails } = useContext(ClientContext) + const [ownership, setOwnership] = useState(initial) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState() @@ -32,8 +36,14 @@ export const useCheckOwnership = () => { setOwnership(status) } catch (e) { setError(e as string) + setIsLoading(false) + setOwnership(initial) } }, []) + useEffect(() => { + checkOwnership() + }, [clientDetails]) + return { isLoading, error, ownership, checkOwnership } } diff --git a/nym-wallet/src/index.tsx b/nym-wallet/src/index.tsx index 383d3beab8..90490cda9f 100644 --- a/nym-wallet/src/index.tsx +++ b/nym-wallet/src/index.tsx @@ -5,24 +5,24 @@ import { BrowserRouter as Router } from 'react-router-dom' import { Routes } from './routes' import { ClientContext, ClientContextProvider } from './context/main' import { ApplicationLayout } from './layouts' -import { Admin, SignIn } from './pages' +import { Admin, Welcome } from './pages' import { ErrorFallback } from './components' -import { NymWalletTheme } from './theme' +import { NymWalletTheme, WelcomeTheme } from './theme' import { Settings } from './pages' const App = () => { const { clientDetails } = useContext(ClientContext) - return ( + return !clientDetails ? ( + + + + ) : ( - {!clientDetails ? ( - - ) : ( - - - - - - )} + + + + + ) } diff --git a/nym-wallet/src/layouts/AppLayout.tsx b/nym-wallet/src/layouts/AppLayout.tsx index d65ad6fab0..3ca5e46536 100644 --- a/nym-wallet/src/layouts/AppLayout.tsx +++ b/nym-wallet/src/layouts/AppLayout.tsx @@ -2,7 +2,7 @@ import React from 'react' import { Box } from '@mui/material' import { Nav } from '../components' import Logo from '../images/logo-background.svg' -import { AppBar } from '../components/AppBar' +import { AppBar } from '../components' export const ApplicationLayout: React.FC = ({ children }) => { return ( @@ -23,12 +23,16 @@ export const ApplicationLayout: React.FC = ({ children }) => { py: 4, px: 5, }} + display="flex" + flexDirection="column" + justifyContent="space-between" > - - + + + + +