Feature/wallet login with password (#1074)

* 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<T>, 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 <futurechimp@users.noreply.github.com>
Co-authored-by: fmtabbara <fmtabbara@users.noreply.github.com>
Co-authored-by: Jędrzej Stuczyński <jedrzej.stuczynski@gmail.com>
Co-authored-by: Bogdan-Ștefan Neacșu <bogdan@nymtech.net>
This commit is contained in:
Fouad
2022-02-02 19:25:02 +01:00
committed by GitHub
parent 8bc8671678
commit de864fe828
82 changed files with 1868 additions and 381 deletions
@@ -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<cosmrs::AccountId>,
@@ -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<cosmrs::AccountId>,
vesting_contract_address: Option<cosmrs::AccountId>,
) -> Self {
Config {
network,
nymd_url,
mixnet_contract_address,
vesting_contract_address,
@@ -84,6 +87,7 @@ impl Config {
#[cfg(feature = "nymd-client")]
pub struct Client<C> {
network: network_defaults::all::Network,
mixnet_contract_address: Option<cosmrs::AccountId>,
vesting_contract_address: Option<cosmrs::AccountId>,
mnemonic: Option<bip39::Mnemonic>,
@@ -106,6 +110,7 @@ impl Client<SigningNymdClient> {
) -> Result<Client<SigningNymdClient>, 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<SigningNymdClient> {
)?;
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<SigningNymdClient> {
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<QueryNymdClient> {
)?;
Ok(Client {
network: config.network,
mixnet_contract_address: config.mixnet_contract_address,
vesting_contract_address: config.vesting_contract_address,
mnemonic: None,
@@ -106,6 +106,7 @@ impl NymdClient<SigningNymdClient> {
}
pub fn connect_with_mnemonic<U>(
network: config::defaults::all::Network,
endpoint: U,
mixnet_contract_address: Option<AccountId>,
vesting_contract_address: Option<AccountId>,
@@ -115,7 +116,8 @@ impl NymdClient<SigningNymdClient> {
where
U: TryInto<HttpClientUrl, Error = TendermintRpcError>,
{
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<C> NymdClient<C> {
self.client.get_balance(address, denom).await
}
pub async fn get_mixnet_balance(
&self,
address: &AccountId,
) -> Result<Option<CosmosCoin>, NymdError>
where
C: CosmWasmClient + Sync,
{
self.get_balance(address, self.denom()?).await
}
pub async fn get_total_supply(&self) -> Result<Vec<Coin>, NymdError>
where
C: CosmWasmClient + Sync,
@@ -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<Self, NymdError> {
DirectSecp256k1HdWalletBuilder::new().build(mnemonic)
pub fn from_mnemonic(prefix: String, mnemonic: bip39::Mnemonic) -> Result<Self, NymdError> {
DirectSecp256k1HdWalletBuilder::new()
.with_prefix(prefix)
.build(mnemonic)
}
pub fn generate(word_count: usize) -> Result<Self, NymdError> {
pub fn generate(prefix: String, word_count: usize) -> Result<Self, NymdError> {
let mneomonic = bip39::Mnemonic::generate(word_count)?;
Self::from_mnemonic(mneomonic)
Self::from_mnemonic(prefix, mneomonic)
}
fn derive_keypair(&self, hd_path: &DerivationPath) -> Result<Secp256k1Keypair, NymdError> {
@@ -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()
+1 -1
View File
@@ -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(())
+1 -1
View File
@@ -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"),
+37 -26
View File
@@ -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 }
+12 -12
View File
@@ -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<ValidatorDetails> {
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;
+20
View File
@@ -0,0 +1,20 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// 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<ValidatorDetails> {
vec![ValidatorDetails::new(
"https://rpc.nyx.nodes.guru/",
Some("https://api.nyx.nodes.guru/"),
)]
}
-23
View File
@@ -1,23 +0,0 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// 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<ValidatorDetails> {
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),
]
}
+1 -1
View File
@@ -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";
+1 -1
View File
@@ -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";
+3 -1
View File
@@ -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<QueryNymdClient> {
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()),
@@ -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,
+1 -4
View File
@@ -1,4 +1 @@
MAJOR_CURRENCY=nymt
MINOR_CURRENCY=unymt
ADMIN_ADDRESS=nymt1a586lvexy7ahva8h27fp6h8ds8taea6p95w3vx
NETWORK_NAME=sandbox
ADMIN_ADDRESS=
+134 -13
View File
@@ -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",
]
-3
View File
@@ -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,
}
+5
View File
@@ -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"
+10 -6
View File
@@ -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<Denom, BackendError> {
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))
}
}
+4 -4
View File
@@ -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<cosmrs::AccountId> {
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<cosmrs::AccountId> {
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()
}
}
+23 -1
View File
@@ -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)
}
}
+3
View File
@@ -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;
+17 -1
View File
@@ -1,12 +1,15 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// 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<ConfigNetwork> for Network {
match self {
Network::QA => ConfigNetwork::QA,
Network::SANDBOX => ConfigNetwork::SANDBOX,
Network::MAINNET => ConfigNetwork::MAINNET,
}
}
}
@@ -39,7 +55,7 @@ impl TryFrom<ConfigNetwork> for Network {
match value {
ConfigNetwork::QA => Ok(Network::QA),
ConfigNetwork::SANDBOX => Ok(Network::SANDBOX),
_ => Err(BackendError::NetworkNotSupported(value)),
ConfigNetwork::MAINNET => Ok(Network::MAINNET),
}
}
}
@@ -57,8 +57,9 @@ pub async fn connect_with_mnemonic(
pub async fn get_balance(
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<Balance, BackendError> {
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()?,
));
}
+4
View File
@@ -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();
}
@@ -0,0 +1,86 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// 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<S: Serializer>(
hd_path: &DerivationPath,
serializer: S,
) -> Result<S::Ok, S::Error> {
serializer.collect_str(hd_path)
}
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<DerivationPath, D::Error> {
let s = <&str>::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
@@ -0,0 +1,271 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// 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<T> {
#[serde(with = "base64")]
ciphertext: Vec<u8>,
#[serde(with = "base64")]
salt: Vec<u8>,
#[serde(with = "base64")]
iv: Vec<u8>,
#[serde(skip)]
#[zeroize(skip)]
_marker: PhantomData<T>,
}
impl<T> Drop for EncryptedData<T> {
fn drop(&mut self) {
self.zeroize()
}
}
// we only ever want to expose those getters in the test code
#[cfg(test)]
impl<T> EncryptedData<T> {
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<u8> 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<S: Serializer>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&base64::encode(bytes))
}
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<u8>, D::Error> {
let s = <String>::deserialize(deserializer)?;
base64::decode(&s).map_err(serde::de::Error::custom)
}
}
impl<T> EncryptedData<T> {
pub(crate) fn encrypt_struct(data: &T, password: &UserPassword) -> Result<Self, BackendError>
where
T: Serialize,
{
encrypt_struct(data, password)
}
pub(crate) fn decrypt_struct(&self, password: &UserPassword) -> Result<T, BackendError>
where
T: for<'a> Deserialize<'a>,
{
decrypt_struct(self, password)
}
}
impl EncryptedData<Vec<u8>> {
pub(crate) fn encrypt_data(data: &[u8], password: &UserPassword) -> Result<Self, BackendError> {
encrypt_data(data, password)
}
pub(crate) fn decrypt_data(&self, password: &UserPassword) -> Result<Vec<u8>, BackendError> {
decrypt_data(self, password)
}
}
fn derive_cipher_key<KeySize>(
password: &UserPassword,
salt: &[u8],
) -> Result<Key<KeySize>, BackendError>
where
KeySize: ArrayLength<u8>,
{
// 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<u8>, Vec<u8>) {
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<Vec<u8>, 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<Vec<u8>, 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<EncryptedData<Vec<u8>>, 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<T>(
data: &T,
password: &UserPassword,
) -> Result<EncryptedData<T>, 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<Vec<u8>>,
password: &UserPassword,
) -> Result<Vec<u8>, BackendError> {
decrypt(
&encrypted_data.ciphertext,
password,
&encrypted_data.salt,
&encrypted_data.iv,
)
}
pub(crate) fn decrypt_struct<T>(
encrypted_data: &EncryptedData<T>,
password: &UserPassword,
) -> Result<T, BackendError>
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());
}
}
@@ -0,0 +1,199 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// 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<PathBuf, BackendError> {
tauri::api::path::local_data_dir()
.map(|dir| dir.join(STORAGE_DIR_NAME))
.ok_or(BackendError::UnknownStorageDirectory)
}
pub(crate) fn wallet_login_filepath() -> Result<PathBuf, BackendError> {
get_storage_directory().map(|dir| dir.join(WALLET_INFO_FILENAME))
}
pub(crate) fn load_existing_wallet_login_information(
password: &UserPassword,
) -> Result<Vec<StoredAccount>, 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<Vec<StoredAccount>, BackendError> {
if !filepath.exists() {
return Ok(Vec::new());
}
let file = OpenOptions::new().read(true).open(filepath)?;
let encrypted_data: EncryptedData<Vec<StoredAccount>> = 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<Vec<StoredAccount>> {
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());
}
}
@@ -0,0 +1,25 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// 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<str> for UserPassword {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
+6 -3
View File
@@ -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 (
<MuiAppBar position="sticky" sx={{ boxShadow: 'none', bgcolor: 'nym.background.light' }}>
@@ -31,7 +31,10 @@ export const AppBar = () => {
</>
)}
</Grid>
<Grid item container justifyContent="flex-end" xs={3} spacing={2}>
<Grid item container justifyContent="flex-end" md={12} lg={5} spacing={2}>
<Grid item>
<NetworkSelector />
</Grid>
<Grid item>
<IconButton
onClick={handleShowSettings}
+4 -3
View File
@@ -1,11 +1,12 @@
import React, { useState, useEffect } from 'react'
import React, { useState, useEffect, useContext } from 'react'
import { Typography } from '@mui/material'
import { MAJOR_CURRENCY } from '../context/main'
import { Operation } from '../types'
import { getGasFee } from '../requests'
import { ClientContext } from '../context/main'
export const Fee = ({ feeType }: { feeType: Operation }) => {
const [fee, setFee] = useState<string>()
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 (
<Typography sx={{ color: 'nym.fee', fontWeight: 600 }}>
Fee for this transaction: {`${fee} ${MAJOR_CURRENCY}`}{' '}
Fee for this transaction: {`${fee} ${currency?.major}`}{' '}
</Typography>
)
}
@@ -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<HTMLButtonElement | null>(null)
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget)
}
const handleClose = () => {
setAnchorEl(null)
}
return (
<>
<Button
variant={network === 'MAINNET' ? 'contained' : 'outlined'}
color="primary"
sx={
network !== 'MAINNET'
? {
color: (theme) => `${theme.palette.nym.background.dark}`,
border: (theme) => `1px solid ${theme.palette.nym.background.dark}`,
'&:hover': { border: (theme) => `1px solid ${theme.palette.nym.background.dark}` },
}
: {}
}
onClick={handleClick}
disableElevation
endIcon={<KeyboardArrowDown sx={{ color: (theme) => `1px solid ${theme.palette.nym.background.dark}` }} />}
>
{networks.find((n) => n.networkName === network)?.name}
</Button>
<Popover
open={Boolean(anchorEl)}
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
onClose={handleClose}
>
<List>
<ListSubheader>Network selection</ListSubheader>
{networks.map(({ name, networkName }) => (
<NetworkItem
key={networkName}
title={name}
isSelected={networkName === network}
onSelect={() => {
handleClose()
switchNetwork(networkName)
}}
/>
))}
</List>
</Popover>
</>
)
}
const NetworkItem: React.FC<{ title: string; isSelected: boolean; onSelect: () => void }> = ({
title,
isSelected,
onSelect,
}) => (
<ListItem button onClick={onSelect}>
<ListItemIcon>{isSelected && <CheckSharp color="success" />}</ListItemIcon>
<ListItemText>{title}</ListItemText>
</ListItem>
)
+10
View File
@@ -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' }) => <Logo width={imgSize[size]} />
+3
View File
@@ -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'
+62 -23
View File
@@ -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<void>
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<TClientDetails>()
const [clientDetails, setClientDetails] = useState<Account>()
const [mixnodeDetails, setMixnodeDetails] = useState<TMixnodeBondDetails | null>()
const [network, setNetwork] = useState<Network | undefined>()
const [currency, setCurrency] = useState<TCurrency>()
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 (
<ClientContext.Provider
value={{
@@ -74,6 +110,9 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
userBalance,
showAdmin,
showSettings,
network,
currency,
switchNetwork,
getBondDetails,
handleShowSettings,
handleShowAdmin,
+15 -5
View File
@@ -1,12 +1,16 @@
import { useCallback, useState } from 'react'
import { useCallback, useContext, useEffect, useState } from 'react'
import { ClientContext } from '../context/main'
import { checkGatewayOwnership, checkMixnodeOwnership } from '../requests'
import { EnumNodeType, TNodeOwnership } from '../types'
const initial = {
hasOwnership: false,
nodeType: undefined,
}
export const useCheckOwnership = () => {
const [ownership, setOwnership] = useState<TNodeOwnership>({
hasOwnership: false,
nodeType: undefined,
})
const { clientDetails } = useContext(ClientContext)
const [ownership, setOwnership] = useState<TNodeOwnership>(initial)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string>()
@@ -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 }
}
+12 -12
View File
@@ -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 ? (
<WelcomeTheme>
<Welcome />
</WelcomeTheme>
) : (
<NymWalletTheme>
{!clientDetails ? (
<SignIn />
) : (
<ApplicationLayout>
<Settings />
<Admin />
<Routes />
</ApplicationLayout>
)}
<ApplicationLayout>
<Settings />
<Admin />
<Routes />
</ApplicationLayout>
</NymWalletTheme>
)
}
+9 -5
View File
@@ -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"
>
<Box sx={{ mb: 3 }}>
<Logo width={45} />
<Box>
<Box sx={{ mb: 3 }}>
<Logo width={45} />
</Box>
<Nav />
</Box>
<Nav />
</Box>
<Box
sx={{
+2 -2
View File
@@ -7,7 +7,7 @@ import { Layout } from '../../layouts'
import { ClientContext, urls } from '../../context/main'
export const Balance = () => {
const { userBalance, clientDetails } = useContext(ClientContext)
const { userBalance, clientDetails, network } = useContext(ClientContext)
useEffect(() => {
userBalance.fetchBalance()
@@ -37,7 +37,7 @@ export const Balance = () => {
<Grid item>
<Link
sx={{ pl: 1 }}
href={`${urls.blockExplorer}/account/${clientDetails?.client_address}`}
href={`${urls(network).blockExplorer}/account/${clientDetails?.client_address}`}
target="_blank"
>
<Button endIcon={<OpenInNew />}>Last transactions</Button>
+3 -3
View File
@@ -18,7 +18,7 @@ import { NodeTypeSelector } from '../../components/NodeTypeSelector'
import { bond, majorToMinor } from '../../requests'
import { validationSchema } from './validationSchema'
import { Gateway, MixNode } from '../../types'
import { ClientContext, MAJOR_CURRENCY } from '../../context/main'
import { ClientContext } from '../../context/main'
import { Fee } from '../../components'
type TBondFormFields = {
@@ -96,7 +96,7 @@ export const BondForm = ({
defaultValues,
})
const { userBalance, getBondDetails } = useContext(ClientContext)
const { userBalance, currency, getBondDetails } = useContext(ClientContext)
const watchNodeType = watch('nodeType', defaultValues.nodeType)
const watchAdvancedOptions = watch('withAdvancedOptions', defaultValues.withAdvancedOptions)
@@ -188,7 +188,7 @@ export const BondForm = ({
error={!!errors.amount}
helperText={errors.amount?.message}
InputProps={{
endAdornment: <InputAdornment position="end">{MAJOR_CURRENCY}</InputAdornment>,
endAdornment: <InputAdornment position="end">{currency?.major}</InputAdornment>,
}}
disabled={disabled}
/>
+3 -3
View File
@@ -1,10 +1,10 @@
import React, { useContext } from 'react'
import { Box } from '@mui/system'
import { SuccessReponse, TransactionDetails } from '../../components'
import { ClientContext, MAJOR_CURRENCY } from '../../context/main'
import { ClientContext } from '../../context/main'
export const SuccessView: React.FC<{ details?: { amount: string; address: string } }> = ({ details }) => {
const { userBalance } = useContext(ClientContext)
const { userBalance, currency } = useContext(ClientContext)
return (
<>
<SuccessReponse
@@ -17,7 +17,7 @@ export const SuccessView: React.FC<{ details?: { amount: string; address: string
<TransactionDetails
details={[
{ primary: 'Node', secondary: details.address },
{ primary: 'Amount', secondary: `${details.amount} ${MAJOR_CURRENCY}` },
{ primary: 'Amount', secondary: `${details.amount} ${currency?.major}` },
]}
/>
</Box>
@@ -1,5 +1,4 @@
import * as Yup from 'yup'
import { MAJOR_CURRENCY } from '../../context/main'
import {
checkHasEnoughFunds,
isValidHostname,
@@ -33,7 +32,7 @@ export const validationSchema = Yup.object().shape({
const isValid = await validateAmount(value || '', '100000000')
if (!isValid) {
return this.createError({ message: `A valid amount is required (min 100 ${MAJOR_CURRENCY})` })
return this.createError({ message: `A valid amount is required (min 100)` })
} else {
const hasEnough = await checkHasEnoughFunds(value || '')
if (!hasEnough) {
@@ -2,9 +2,9 @@ import React, { useContext } from 'react'
import { Box, Button, CircularProgress, FormControl, Grid, InputAdornment, TextField, Typography } from '@mui/material'
import { yupResolver } from '@hookform/resolvers/yup'
import { useForm } from 'react-hook-form'
import { EnumNodeType, TFee } from '../../types'
import { EnumNodeType } from '../../types'
import { validationSchema } from './validationSchema'
import { ClientContext, MAJOR_CURRENCY } from '../../context/main'
import { ClientContext } from '../../context/main'
import { delegate, majorToMinor } from '../../requests'
import { checkHasEnoughFunds } from '../../utils'
import { Fee } from '../../components'
@@ -41,7 +41,7 @@ export const DelegateForm = ({
const watchNodeType = watch('nodeType', defaultValues.nodeType)
const { userBalance } = useContext(ClientContext)
const { userBalance, currency } = useContext(ClientContext)
const onSubmit = async (data: TDelegateForm) => {
const hasEnoughFunds = await checkHasEnoughFunds(data.amount)
@@ -98,7 +98,7 @@ export const DelegateForm = ({
error={!!errors.amount}
helperText={errors?.amount?.message}
InputProps={{
endAdornment: <InputAdornment position="end">{MAJOR_CURRENCY}</InputAdornment>,
endAdornment: <InputAdornment position="end">{currency?.major}</InputAdornment>,
}}
/>
</Grid>
@@ -1,10 +1,10 @@
import React, { useContext } from 'react'
import { Box } from '@mui/system'
import { SuccessReponse, TransactionDetails } from '../../components'
import { ClientContext, MAJOR_CURRENCY } from '../../context/main'
import { ClientContext } from '../../context/main'
export const SuccessView: React.FC<{ details?: { amount: string; address: string } }> = ({ details }) => {
const { userBalance } = useContext(ClientContext)
const { userBalance, currency } = useContext(ClientContext)
return (
<>
<SuccessReponse
@@ -17,7 +17,7 @@ export const SuccessView: React.FC<{ details?: { amount: string; address: string
<TransactionDetails
details={[
{ primary: 'Node', secondary: details.address },
{ primary: 'Amount', secondary: `${details.amount} ${MAJOR_CURRENCY}` },
{ primary: 'Amount', secondary: `${details.amount} ${currency?.major}` },
]}
/>
</Box>
+5 -3
View File
@@ -1,4 +1,4 @@
import React, { useState } from 'react'
import React, { useContext, useState } from 'react'
import { Alert, AlertTitle, Box, Button, Link, Typography } from '@mui/material'
import { DelegateForm } from './DelegateForm'
import { Layout } from '../../layouts'
@@ -6,13 +6,15 @@ import { NymCard } from '../../components'
import { EnumRequestStatus, RequestStatus } from '../../components/RequestStatus'
import { SuccessView } from './SuccessView'
import { Delegate as DelegateIcon } from '../../svg-icons'
import { urls } from '../../context/main'
import { urls, ClientContext } from '../../context/main'
export const Delegate = () => {
const [status, setStatus] = useState<EnumRequestStatus>(EnumRequestStatus.initial)
const [error, setError] = useState<string>()
const [successDetails, setSuccessDetails] = useState<{ amount: string; address: string }>()
const {network} = useContext(ClientContext)
return (
<Layout>
<>
@@ -80,7 +82,7 @@ export const Delegate = () => {
</NymCard>
<Typography sx={{ p: 3 }}>
Checkout the{' '}
<Link href={`${urls.networkExplorer}/network-components/mixnodes`} target="_blank">
<Link href={`${urls(network).networkExplorer}/network-components/mixnodes`} target="_blank">
list of mixnodes
</Link>{' '}
for uptime and performances to help make delegation decisions
+1 -1
View File
@@ -5,7 +5,7 @@ export * from './delegate'
export * from './internal-docs'
export * from './receive'
export * from './send'
export * from './sign-in'
export * from './welcome'
export * from './settings'
export * from './unbond'
export * from './undelegate'
+4 -4
View File
@@ -3,15 +3,15 @@ import QRCode from 'qrcode.react'
import { Alert, Box, Stack, Typography } from '@mui/material'
import { CopyToClipboard, NymCard } from '../../components'
import { Layout } from '../../layouts'
import { ClientContext, MAJOR_CURRENCY } from '../../context/main'
import { ClientContext } from '../../context/main'
import { ArrowBack } from '@mui/icons-material'
export const Receive = () => {
const { clientDetails } = useContext(ClientContext)
const { clientDetails, currency } = useContext(ClientContext)
return (
<Layout>
<NymCard title={`Receive ${MAJOR_CURRENCY}`} Icon={ArrowBack}>
<NymCard title={`Receive ${currency?.major}`} Icon={ArrowBack}>
<Stack spacing={3} alignItems="center">
<Alert severity="info" data-testid="receive-nym" sx={{ width: '100%' }}>
You can receive tokens by providing this address to the sender
@@ -31,7 +31,7 @@ export const Receive = () => {
<CopyToClipboard text={clientDetails?.client_address || ''} iconButton />
</Box>
{clientDetails && <QRCode data-testid="qr-code" value={clientDetails.client_address} />}
{clientDetails && <QRCode data-testid="qr-code" value={clientDetails?.client_address} />}
</Stack>
</NymCard>
</Layout>
@@ -1,7 +1,7 @@
import React, { useContext } from 'react'
import { Box, CircularProgress, Link, Typography } from '@mui/material'
import { SendError } from './SendError'
import { ClientContext, MAJOR_CURRENCY, urls } from '../../context/main'
import { ClientContext, urls } from '../../context/main'
import { SuccessReponse } from '../../components'
import { TransactionDetails } from '../../components/TransactionDetails'
import { TransactionDetails as TTransactionDetails } from '../../types'
@@ -15,7 +15,7 @@ export const SendConfirmation = ({
error?: string
isLoading: boolean
}) => {
const { userBalance, clientDetails } = useContext(ClientContext)
const { userBalance, currency, network } = useContext(ClientContext)
return (
<Box
sx={{
@@ -35,7 +35,7 @@ export const SendConfirmation = ({
subtitle={
<>
Check the transaction hash{' '}
<Link href={`${urls.blockExplorer}/transactions/${data.tx_hash}`} target="_blank">
<Link href={`${urls(network).blockExplorer}/transactions/${data.tx_hash}`} target="_blank">
here
</Link>
</>
@@ -49,7 +49,7 @@ export const SendConfirmation = ({
<TransactionDetails
details={[
{ primary: 'Recipient', secondary: data.to_address },
{ primary: 'Amount', secondary: `${data.amount.amount} ${MAJOR_CURRENCY}` },
{ primary: 'Amount', secondary: `${data.amount.amount} ${currency?.major}` },
]}
/>
</>
+3 -3
View File
@@ -1,7 +1,7 @@
import React, { useContext } from 'react'
import { Grid, InputAdornment, TextField, Typography } from '@mui/material'
import { useFormContext } from 'react-hook-form'
import { ClientContext, MAJOR_CURRENCY } from '../../context/main'
import { ClientContext } from '../../context/main'
import { Fee } from '../../components'
export const SendForm = () => {
@@ -9,7 +9,7 @@ export const SendForm = () => {
register,
formState: { errors },
} = useFormContext()
const { clientDetails } = useContext(ClientContext)
const { clientDetails, currency } = useContext(ClientContext)
return (
<Grid container spacing={3}>
@@ -43,7 +43,7 @@ export const SendForm = () => {
error={!!errors.amount}
helperText={errors.amount?.message}
InputProps={{
endAdornment: <InputAdornment position="end">{MAJOR_CURRENCY}</InputAdornment>,
endAdornment: <InputAdornment position="end">{currency?.major}</InputAdornment>,
}}
/>
</Grid>
+4 -4
View File
@@ -1,11 +1,11 @@
import React, { useContext } from 'react'
import { Card, Divider, Grid, Typography } from '@mui/material'
import { useFormContext } from 'react-hook-form'
import { ClientContext, MAJOR_CURRENCY } from '../../context/main'
import { ClientContext } from '../../context/main'
export const SendReview = ({ transferFee }: { transferFee?: string }) => {
const { getValues } = useFormContext()
const { clientDetails } = useContext(ClientContext)
const { clientDetails, currency } = useContext(ClientContext)
const values = getValues()
@@ -34,13 +34,13 @@ export const SendReview = ({ transferFee }: { transferFee?: string }) => {
<Divider light />
</Grid>
<Grid item xs={12}>
<SendReviewField title="Amount" subtitle={`${values.amount} ${MAJOR_CURRENCY}`} />
<SendReviewField title="Amount" subtitle={`${values.amount} ${currency?.major}`} />
</Grid>
<Grid item xs={12}>
<Divider light />
</Grid>
<Grid item xs={12}>
<SendReviewField title="Transfer fee" subtitle={`${transferFee} ${MAJOR_CURRENCY}`} info />
<SendReviewField title="Transfer fee" subtitle={`${transferFee} ${currency?.major}`} info />
</Grid>
</Grid>
</Card>
+5 -4
View File
@@ -1,14 +1,15 @@
import React from 'react'
import React, {useContext} from 'react'
import { ArrowForward } from '@mui/icons-material'
import { NymCard } from '../../components'
import { SendWizard } from './SendWizard'
import { Layout } from '../../layouts'
import { MAJOR_CURRENCY } from '../../context/main'
import { ArrowForward } from '@mui/icons-material'
import { ClientContext } from '../../context/main'
export const Send = () => {
const {currency} = useContext(ClientContext)
return (
<Layout>
<NymCard title={`Send ${MAJOR_CURRENCY}`} noPadding Icon={ArrowForward}>
<NymCard title={`Send ${currency?.major}`} noPadding Icon={ArrowForward}>
<SendWizard />
</NymCard>
</Layout>
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react'
import React, { useContext, useState } from 'react'
import { Alert, Box, Dialog } from '@mui/material'
import { NymCard } from '../../components'
import { ClientContext } from '../../context/main'
+4 -3
View File
@@ -1,13 +1,14 @@
import React, { useContext } from 'react'
import { OpenInNew } from '@mui/icons-material'
import { Button, Link, Stack, Typography } from '@mui/material'
import React, { useEffect } from 'react'
import { urls } from '../../context/main'
import { urls, ClientContext} from '../../context/main'
export const NodeStats = ({ mixnodeId }: { mixnodeId?: string }) => {
const {network} = useContext(ClientContext)
return (
<Stack spacing={2} sx={{ p: 4 }}>
<Typography>All your node stats are available on the link below</Typography>
<Link href={`${urls.networkExplorer}/network-components/mixnode/${mixnodeId}`} target="_blank">
<Link href={`${urls(network).networkExplorer}/network-components/mixnode/${mixnodeId}`} target="_blank">
<Button endIcon={<OpenInNew />}>Network Explorer</Button>
</Link>
</Stack>
@@ -7,7 +7,7 @@ import { InfoTooltip } from '../../components/InfoToolTip'
import { InclusionProbabilityResponse, TMixnodeBondDetails } from '../../types'
import { validationSchema } from './validationSchema'
import { updateMixnode } from '../../requests'
import { ClientContext, MAJOR_CURRENCY } from '../../context/main'
import { ClientContext } from '../../context/main'
import { Fee } from '../../components'
type TFormData = {
@@ -38,7 +38,7 @@ export const SystemVariables = ({
defaultValues: { profitMarginPercent: mixnodeDetails.profit_margin_percent.toString() },
})
const { userBalance } = useContext(ClientContext)
const { userBalance, currency } = useContext(ClientContext)
const onSubmit = async (data: TFormData) => {
try {
@@ -73,7 +73,7 @@ export const SystemVariables = ({
info="Estimated reward per epoch for this profit margin if your node is selected in the active set."
Indicator={
<Typography sx={{ color: (theme) => theme.palette.nym.fee, fontWeight: '600' }}>
{rewardEstimation} {MAJOR_CURRENCY}
{rewardEstimation} {currency?.major}
</Typography>
}
/>
@@ -108,7 +108,7 @@ export const SystemVariables = ({
{nodeUpdateResponse === 'success' ? (
<Typography sx={{ color: 'success.main', fontWeight: 600 }}>Node successfully updated</Typography>
) : nodeUpdateResponse === 'failed' ? (
<Typography sx={{ color: 'error.main', fontWeight: 600 }}>Node updated failed</Typography>
<Typography sx={{ color: 'error.main', fontWeight: 600 }}>Node update failed</Typography>
) : (
<Fee feeType="UpdateMixnodeConfig" />
)}
@@ -19,51 +19,52 @@ export const useSettingsState = (shouldUpdate: boolean) => {
})
const { mixnodeDetails } = useContext(ClientContext)
console.log(inclusionProbability)
const getStatus = async () => {
if (mixnodeDetails?.mix_node.identity_key) {
const status = await getMixnodeStatus(mixnodeDetails?.mix_node.identity_key)
setStatus(status.status)
const getStatus = async (mixnodeKey: string) => {
const status = await getMixnodeStatus(mixnodeKey)
setStatus(status.status)
}
const getStakeSaturation = async (mixnodeKey: string) => {
const saturation = await getMixnodeStakeSaturation(mixnodeKey)
if (saturation) {
setSaturation(Math.round(saturation.saturation * 100))
}
}
const getStakeSaturation = async () => {
if (mixnodeDetails?.mix_node.identity_key) {
const saturation = await getMixnodeStakeSaturation(mixnodeDetails?.mix_node.identity_key)
if (saturation) {
setSaturation(Math.round(saturation.saturation * 100))
}
const getRewardEstimation = async (mixnodeKey: string) => {
const rewardEstimation = await getMixnodeRewardEstimation(mixnodeKey)
if (rewardEstimation) {
const toMajor = await minorToMajor(rewardEstimation.estimated_total_node_reward.toString())
setRewardEstimation(parseInt(toMajor.amount))
}
}
const getRewardEstimation = async () => {
if (mixnodeDetails?.mix_node.identity_key) {
const rewardEstimation = await getMixnodeRewardEstimation(mixnodeDetails?.mix_node.identity_key)
if (rewardEstimation) {
const toMajor = await minorToMajor(rewardEstimation.estimated_total_node_reward.toString())
setRewardEstimation(parseInt(toMajor.amount))
}
const getMixnodeInclusionProbability = async (mixnodeKey: string) => {
const probability = await getInclusionProbability(mixnodeKey)
if (probability) {
const in_active = Math.round(probability.in_active * 100)
const in_reserve = Math.round(probability.in_reserve * 100)
setInclusionProbability({ in_active, in_reserve })
}
}
const getMixnodeInclusionProbability = async () => {
if (mixnodeDetails?.mix_node.identity_key) {
const probability = await getInclusionProbability(mixnodeDetails?.mix_node.identity_key)
if (probability) {
const in_active = Math.round(probability.in_active * 100)
const in_reserve = Math.round(probability.in_reserve * 100)
setInclusionProbability({ in_active, in_reserve })
}
}
const reset = () => {
setStatus('not_found')
setSaturation(0)
setRewardEstimation(0)
setInclusionProbability({ in_active: 0, in_reserve: 0 })
}
useEffect(() => {
if (shouldUpdate) {
getStatus()
getStakeSaturation()
getRewardEstimation()
getMixnodeInclusionProbability()
if (shouldUpdate && mixnodeDetails?.mix_node.identity_key) {
getStatus(mixnodeDetails?.mix_node.identity_key)
getStakeSaturation(mixnodeDetails?.mix_node.identity_key)
getRewardEstimation(mixnodeDetails?.mix_node.identity_key)
getMixnodeInclusionProbability(mixnodeDetails?.mix_node.identity_key)
} else {
reset()
}
}, [shouldUpdate])
-36
View File
@@ -1,36 +0,0 @@
import React, { useContext, useState } from 'react'
import { Box } from '@mui/system'
import { SignInContent } from './sign-in'
import { CreateAccountContent } from './create-account'
export const SignIn = () => {
const [showCreateAccount, setShowCreateAccount] = useState(false)
return (
<Box
sx={{
height: '100vh',
width: '100vw',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
overflow: 'auto',
bgcolor: 'nym.background.dark',
}}
>
<Box
sx={{
width: 500,
display: 'flex',
justifyContent: 'center',
margin: 'auto',
}}
>
{showCreateAccount ? (
<CreateAccountContent showSignIn={() => setShowCreateAccount(false)} />
) : (
<SignInContent showCreateAccount={() => setShowCreateAccount(true)} />
)}
</Box>
</Box>
)
}
@@ -4,7 +4,7 @@ import { Box, Autocomplete, Button, CircularProgress, FormControl, Grid, TextFie
import { yupResolver } from '@hookform/resolvers/yup'
import { validationSchema } from './validationSchema'
import { EnumNodeType, TDelegation, TFee } from '../../types'
import { ClientContext, MAJOR_CURRENCY } from '../../context/main'
import { ClientContext } from '../../context/main'
import { undelegate } from '../../requests'
import { Fee } from '../../components'
+9 -5
View File
@@ -1,12 +1,13 @@
import React, { useEffect, useState } from 'react'
import React, { useContext, useEffect, useState } from 'react'
import { Alert, AlertTitle, Box, Button, CircularProgress } from '@mui/material'
import { NymCard } from '../../components'
import { UndelegateForm } from './UndelegateForm'
import { Layout } from '../../layouts'
import { EnumRequestStatus, RequestStatus } from '../../components/RequestStatus'
import { Alert, AlertTitle, Box, Button, CircularProgress } from '@mui/material'
import { getGasFee, getReverseMixDelegations } from '../../requests'
import { TFee, TPagedDelegations } from '../../types'
import { Undelegate as UndelegateIcon } from '../../svg-icons'
import { ClientContext } from '../../context/main'
export const Undelegate = () => {
const [message, setMessage] = useState<string>()
@@ -15,11 +16,14 @@ export const Undelegate = () => {
const [fees, setFees] = useState<TFee>()
const [pagedDelegations, setPagesDelegations] = useState<TPagedDelegations>()
const { clientDetails } = useContext(ClientContext)
useEffect(() => {
initialize()
}, [])
}, [clientDetails])
const initialize = async () => {
setStatus(EnumRequestStatus.initial)
setIsLoading(true)
try {
@@ -33,9 +37,9 @@ export const Undelegate = () => {
})
setPagesDelegations(mixnodeDelegations)
} catch {
} catch (e) {
setStatus(EnumRequestStatus.error)
setMessage('An error occured when initialising the page')
setMessage(e as string)
}
setIsLoading(false)
@@ -1,37 +1,20 @@
import React, { useEffect, useState } from 'react'
import {
Alert,
Button,
Card,
CardActions,
CardContent,
CardHeader,
CircularProgress,
Stack,
Typography,
} from '@mui/material'
import { Box } from '@mui/system'
import { ArrowBack } from '@mui/icons-material'
import { Alert, Button, Card, CardActions, CardContent, CardHeader, Stack, Typography } from '@mui/material'
import { createAccount } from '../../requests'
import { TCreateAccount } from '../../types'
import logo from '../../images/logo-background.svg'
import { CopyToClipboard } from '../../components'
export const CreateAccountContent: React.FC<{ showSignIn: () => void }> = ({ showSignIn }) => {
export const CreateAccountContent: React.FC<{ page: 'legacy create account'; showSignIn: () => void }> = ({
showSignIn,
}) => {
const [accountDetails, setAccountDetails] = useState<TCreateAccount>()
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<Error>()
const handleCreateAccount = async () => {
setIsLoading(true)
setError(undefined)
try {
const res = await createAccount()
console.log(res)
setTimeout(() => {
setAccountDetails(res)
setIsLoading(false)
}, 2500)
const account = await createAccount()
setAccountDetails(account)
} catch (e: any) {
setError(e)
}
@@ -41,11 +24,8 @@ export const CreateAccountContent: React.FC<{ showSignIn: () => void }> = ({ sho
handleCreateAccount()
}, [])
if (isLoading) return <CircularProgress size={70} />
return (
<Stack spacing={4} alignItems="center">
<img src={logo} width={80} />
<Stack spacing={4} alignItems="center" sx={{ width: 700 }}>
<Typography sx={{ color: 'common.white' }} variant="h4">
Congratulations
</Typography>
@@ -64,26 +44,13 @@ export const CreateAccountContent: React.FC<{ showSignIn: () => void }> = ({ sho
<CopyToClipboard text={accountDetails?.mnemonic || ''} light />
</CardActions>
</Card>
<Box sx={{ textAlign: 'center' }}>
<Typography sx={{ color: 'common.white' }}>Address:</Typography>
<Typography sx={{ color: 'common.white' }} data-testid="wallet-address">
{accountDetails?.account.client_address}
</Typography>
</Box>
{error && (
<Alert severity="error" variant="outlined">
{error}
</Alert>
)}
<Button
variant="contained"
onClick={showSignIn}
data-testid="sign-in-button"
startIcon={<ArrowBack />}
size="large"
sx={{ width: 360 }}
>
Back to Sign in
<Button variant="contained" onClick={showSignIn} data-testid="sign-in-button" size="large" sx={{ width: 360 }}>
Sign in
</Button>
</Stack>
)
@@ -1,9 +1,9 @@
import React, { useContext, useState } from 'react'
import { Button, CircularProgress, Grid, Stack, Link, TextField, Typography, Alert, SvgIcon } from '@mui/material'
import { Button, CircularProgress, Grid, Stack, TextField, Typography, Alert } from '@mui/material'
import { styled } from '@mui/material/styles'
import Logo from '../../images/logo-background.svg'
import { signInWithMnemonic } from '../../requests'
import { ClientContext } from '../../context/main'
import { NymLogo } from '../../components'
export const SignInContent: React.FC<{ showCreateAccount: () => void }> = ({ showCreateAccount }) => {
const [mnemonic, setMnemonic] = useState<string>('')
@@ -19,9 +19,9 @@ export const SignInContent: React.FC<{ showCreateAccount: () => void }> = ({ sho
setInputError(undefined)
try {
const res = await signInWithMnemonic(mnemonic || '')
await signInWithMnemonic(mnemonic || '')
setIsLoading(false)
logIn(res)
logIn()
} catch (e: any) {
setIsLoading(false)
setInputError(e)
@@ -30,28 +30,12 @@ export const SignInContent: React.FC<{ showCreateAccount: () => void }> = ({ sho
return (
<Stack spacing={3} alignItems="center" sx={{ width: '80%' }}>
<Logo width={80} />
<Typography sx={{ color: 'common.white' }}>Enter Mnemonic and sign in</Typography>
<Grid container direction="column" spacing={3} component="form">
<Grid item style={{ paddingTop: 0 }}>
<StyledInput
value={mnemonic}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setMnemonic(e.target.value)}
size="medium"
variant="outlined"
margin="normal"
required
fullWidth
id="mnemonic"
label="BIP-39 Mnemonic"
name="mnemonic"
autoComplete="mnemonic"
autoFocus
disabled={isLoading}
sx={{ m: 0 }}
/>
</Grid>
<NymLogo />
<Typography sx={{ color: 'common.white', fontWeight: 600 }}>Welcome to NYM</Typography>
<Typography variant="caption" sx={{ color: 'grey.800', textTransform: 'uppercase', letterSpacing: 4 }}>
Next generation of privacy
</Typography>
<Grid container direction="column" spacing={2}>
<Grid item>
<Button
fullWidth
@@ -64,7 +48,12 @@ export const SignInContent: React.FC<{ showCreateAccount: () => void }> = ({ sho
onClick={handleSignIn}
type="submit"
>
{!isLoading ? 'Sign In' : 'Signing in'}
Create Account
</Button>
</Grid>
<Grid item>
<Button fullWidth variant="outlined" size="large">
Use Existing Account
</Button>
</Grid>
{inputError && (
@@ -75,15 +64,6 @@ export const SignInContent: React.FC<{ showCreateAccount: () => void }> = ({ sho
</Grid>
)}
</Grid>
<div>
<Typography sx={{ color: 'common.white' }} component="span">
Don't have an account?
</Typography>{' '}
<Link href="#" onClick={showCreateAccount}>
Create one now
</Link>
</div>
</Stack>
)
}
@@ -0,0 +1,16 @@
import React from 'react'
import { Typography } from '@mui/material'
export const Title = ({ title }: { title: string }) => (
<Typography sx={{ color: 'common.white', fontWeight: 600 }}>{title}</Typography>
)
export const Subtitle = ({ subtitle }: { subtitle: string }) => (
<Typography sx={{ color: 'common.white', textAlign: 'center', maxWidth: 400 }}>{subtitle}</Typography>
)
export const SubtitleSlick = ({ subtitle }: { subtitle: string }) => (
<Typography variant="caption" sx={{ color: 'grey.600', textTransform: 'uppercase', letterSpacing: 4 }}>
{subtitle}
</Typography>
)
@@ -0,0 +1,4 @@
export * from './heading'
export * from './word-tiles'
export * from './render-page'
export * from './password-strength'
@@ -0,0 +1,78 @@
import React, { useEffect, useState } from 'react'
import { LockOutlined } from '@mui/icons-material'
import { LinearProgress, Stack, Typography } from '@mui/material'
import { Box } from '@mui/system'
type TStrength = 'weak' | 'medium' | 'strong' | 'init'
const strong = /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})/
const medium = /^(((?=.*[a-z])(?=.*[A-Z]))|((?=.*[a-z])(?=.*[0-9]))|((?=.*[A-Z])(?=.*[0-9])))(?=.{6,})/
const colorMap = {
init: "inherit" as "inherit",
weak: 'error' as 'error',
medium: 'warning' as 'warning',
strong: 'success' as 'success',
}
const getText = (strength: TStrength) => {
switch (strength) {
case 'strong':
return 'Strong password'
case 'medium':
return 'Medium strength password'
case 'weak':
return 'Weak password'
default:
return 'Password strength'
}
}
const getTextColor = (strength: TStrength) => {
switch (strength) {
case 'strong':
return 'success.main'
case 'medium':
return 'warning.main'
case 'weak':
return 'error.main'
default:
return 'grey.500'
}
}
export const PasswordStrength = ({ password }: { password: string }) => {
const [strength, setStrength] = useState<TStrength>('init')
useEffect(() => {
if (password.length === 0) {
return setStrength('init')
}
if (password.match(strong)) {
return setStrength('strong')
}
if (password.match(medium)) {
return setStrength('medium')
}
setStrength('weak')
}, [password])
return (
<Stack spacing={0.5}>
<LinearProgress
variant="determinate"
color={colorMap[strength]}
value={strength === 'strong' ? 100 : strength === 'medium' ? 50 : 0}
/>
<Box display="flex" alignItems="center">
<LockOutlined sx={{ fontSize: 15, color: getTextColor(strength) }} />
<Typography variant="caption" sx={{ ml: 0.5, color: getTextColor(strength) }}>
{getText(strength)}
</Typography>
</Box>
</Stack>
)
}
@@ -0,0 +1,11 @@
import React from 'react'
import { TPages } from '../types'
export const RenderPage = ({ children, page }: { children: React.ReactElement[]; page: TPages }) => (
<>
{React.Children.map(children, (Child) => {
if (page === Child?.props.page) return Child
return null
})}
</>
)
@@ -0,0 +1,91 @@
import React from 'react'
import { Box, Card, CardHeader, Grid, Typography, Stack, Fade } from '@mui/material'
import { THiddenMnemonicWord, THiddenMnemonicWords, TMnemonicWords } from '../types'
export const WordTiles = ({
mnemonicWords,
showIndex,
onClick,
}: {
mnemonicWords?: TMnemonicWords
showIndex?: boolean
onClick?: ({ name, index }: { name: string; index: number }) => void
}) => {
if (mnemonicWords) {
return (
<Grid container spacing={3} justifyContent="center">
{mnemonicWords.map(({ name, index, disabled }) => (
<Grid item xs={2} key={index} onClick={() => onClick?.({ name, index })}>
<WordTile mnemonicWord={name} index={showIndex ? index : undefined} onClick={!!onClick} disabled={disabled}/>
</Grid>
))}
</Grid>
)
}
return null
}
export const WordTile = ({
mnemonicWord,
index,
disabled,
onClick,
}: {
mnemonicWord: string
index?: number
disabled?: boolean
onClick?: boolean
}) => (
<Card
variant="outlined"
sx={{
background: '#151A2C',
border: '1px solid #3A4053',
cursor: onClick ? 'pointer' : 'default',
opacity: disabled ? 0.2 : 1,
}}
>
<CardHeader
title={mnemonicWord}
titleTypographyProps={{ sx: { fontWeight: 700 }, variant: 'body1', textAlign: index ? 'left' : 'center' }}
avatar={
index && (
<Typography variant="caption" color={'#3A4053'}>
{index}
</Typography>
)
}
/>
</Card>
)
export const HiddenWords = ({ mnemonicWords }: { mnemonicWords?: THiddenMnemonicWords }) => {
if (mnemonicWords) {
return (
<Grid container spacing={3} justifyContent="center">
{mnemonicWords.map((mnemonicWord) => (
<Grid item xs={2} key={mnemonicWord.index}>
<HiddenWord mnemonicWord={mnemonicWord} />
</Grid>
))}
</Grid>
)
}
return null
}
const HiddenWord = ({ mnemonicWord }: { mnemonicWord: THiddenMnemonicWord }) => {
return (
<Stack spacing={2} alignItems="center">
<Box borderBottom="1px solid #3A4053" sx={{ p: 2, width: '100%' }}>
<Fade in={!mnemonicWord.hidden}>
<Box>
<WordTile mnemonicWord={mnemonicWord.name} />
</Box>
</Fade>
</Box>
<Typography>{mnemonicWord.index}.</Typography>
</Stack>
)
}
+75
View File
@@ -0,0 +1,75 @@
import React, { useEffect, useState } from 'react'
import { Box } from '@mui/system'
import { Stack } from '@mui/material'
import { WelcomeContent, VerifyMnemonic, MnemonicWords, CreatePassword, ExistingAccount, SelectNetwork } from './pages'
import { NymLogo } from '../../components'
import { TMnemonicWords, TPages } from './types'
import { RenderPage } from './components'
import { CreateAccountContent } from './_legacy_create-account'
const testMnemonic =
'futuristic big receptive caption saw hug odd spoon internal dime bike rake helpless left distribution gusty eyes beg enormous word influence trashy pets curl'
const mnemonicToArray = (mnemonic: string): TMnemonicWords =>
mnemonic
.split(' ')
.reduce((a, c: string, index) => [...a, { name: c, index: index + 1, disabled: false }], [] as TMnemonicWords)
export const Welcome = () => {
const [page, setPage] = useState<TPages>('welcome')
const [mnemonicWords, setMnemonicWords] = useState<TMnemonicWords>()
// useEffect(() => {
// const mnemonicArray = mnemonicToArray(testMnemonic)
// setMnemonicWords(mnemonicArray)
// }, [])
return (
<Box
sx={{
height: '100vh',
width: '100vw',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
overflow: 'auto',
bgcolor: 'nym.background.dark',
}}
>
<Box
sx={{
width: '100%',
display: 'flex',
justifyContent: 'center',
margin: 'auto',
}}
>
<Stack spacing={3} alignItems="center" sx={{ width: 1080 }}>
<NymLogo />
<RenderPage page={page}>
<WelcomeContent
onUseExisting={() => setPage('existing account')}
onCreateAccountComplete={() => setPage('legacy create account')}
page="welcome"
/>
<CreateAccountContent page="legacy create account" showSignIn={() => setPage('existing account')} />
{/* <MnemonicWords
mnemonicWords={mnemonicWords}
onNext={() => setPage('verify mnemonic')}
onPrev={() => setPage('welcome')}
page="create account"
/>
<VerifyMnemonic
mnemonicWords={mnemonicWords}
onComplete={() => setPage('create password')}
page="verify mnemonic"
/>
<CreatePassword page="create password" /> */}
<ExistingAccount page="existing account" onPrev={() => setPage('welcome')} />
</RenderPage>
</Stack>
</Box>
</Box>
)
}
@@ -0,0 +1,60 @@
import React, { useState } from 'react'
import { Button, FormControl, Grid, IconButton, Stack, TextField } from '@mui/material'
import { VisibilityOff, Visibility } from '@mui/icons-material'
import { Subtitle, Title, PasswordStrength } from '../components'
export const CreatePassword = ({}: { page: 'create password' }) => {
const [password, setPassword] = useState<string>('')
const [confirmedPassword, setConfirmedPassword] = useState<string>()
const [showPassword, setShowPassword] = useState(false)
const [showConfirmedPassword, setShowConfirmedPassword] = useState(false)
return (
<>
<Title title="Create password" />
<Subtitle subtitle="Create a strong password. Min 8 characters, at least one capital letter, number and special symbol" />
<Grid container justifyContent="center">
<Grid item xs={6}>
<FormControl fullWidth>
<Stack spacing={2}>
<TextField
label="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
type={showPassword ? 'input' : 'password'}
InputProps={{
endAdornment: (
<IconButton onClick={() => setShowPassword((show) => !show)}>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
),
}}
/>
<PasswordStrength password={password} />
<TextField
label="Confirm password"
value={confirmedPassword}
onChange={(e) => setConfirmedPassword(e.target.value)}
type={showConfirmedPassword ? 'input' : 'password'}
InputProps={{
endAdornment: (
<IconButton onClick={() => setShowConfirmedPassword((show) => !show)}>
{showConfirmedPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
),
}}
/>
<Button
size="large"
variant="contained"
disabled={password !== confirmedPassword || password.length === 0}
>
Next
</Button>
</Stack>
</FormControl>
</Grid>
</Grid>
</>
)
}
@@ -0,0 +1,51 @@
import React, { useContext, useState } from 'react'
import { Alert, Button, Stack, TextField } from '@mui/material'
import { Subtitle } from '../components'
import { ClientContext } from '../../../context/main'
import { signInWithMnemonic } from '../../../requests'
export const ExistingAccount: React.FC<{ page: 'existing account'; onPrev: () => void }> = ({ onPrev }) => {
const [mnemonic, setMnemonic] = useState<string>()
const [inputError, setInputError] = useState<string>()
const { logIn } = useContext(ClientContext)
const handleSignIn = async (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault()
setInputError(undefined)
try {
await signInWithMnemonic(mnemonic || '')
logIn('MAINNET')
} catch (e: any) {
setInputError(e)
}
}
return (
<Stack spacing={2} sx={{ width: 400 }} alignItems="center">
<Subtitle subtitle="Enter your mnemonic from existing wallet" />
<TextField value={mnemonic} onChange={(e) => setMnemonic(e.target.value)} multiline rows={5} fullWidth />
{inputError && (
<Alert severity="error" variant="outlined" data-testid="error" sx={{ color: 'error.light', width: '100%' }}>
{inputError}
</Alert>
)}
<Button variant="contained" size="large" fullWidth onClick={handleSignIn}>
Sign in
</Button>
<Button
variant="outlined"
disableElevation
size="large"
onClick={onPrev}
fullWidth
sx={{ color: 'common.white', border: '1px solid white', '&:hover': { border: '1px solid white' } }}
>
Back
</Button>
</Stack>
)
}
@@ -0,0 +1,6 @@
export * from './welcome-content'
export * from './mnemonic-words'
export * from './verify-mnemonic'
export * from './create-password'
export * from './existing-account'
export * from './select-network'
@@ -0,0 +1,37 @@
import React from 'react'
import { Alert, Button, Typography } from '@mui/material'
import { WordTiles } from '../components/word-tiles'
import { TMnemonicWords } from '../types'
export const MnemonicWords = ({
mnemonicWords,
onNext,
onPrev,
}: {
mnemonicWords?: TMnemonicWords
page: 'create account'
onNext: () => void
onPrev: () => void
}) => {
return (
<>
<Typography sx={{ color: 'common.white', fontWeight: 600 }}>Write down your mnemonic</Typography>
<Alert icon={false} severity="info" sx={{ bgcolor: '#18263B', color: '#50ABFF', width: 625 }}>
Please store your mnemonic in a safe place. This is the only way to access your wallet!
</Alert>
<WordTiles mnemonicWords={mnemonicWords} showIndex />
<Button variant="contained" color="primary" disableElevation size="large" onClick={onNext} sx={{ width: 250 }}>
Verify mnemonic
</Button>
<Button
variant="outlined"
disableElevation
size="large"
onClick={onPrev}
sx={{ color: 'common.white', border: '1px solid white', '&:hover': { border: '1px solid white' }, width: 250 }}
>
Back
</Button>
</>
)
}
@@ -0,0 +1,25 @@
import React, { useContext } from 'react'
import { FormControl, FormControlLabel, Radio, RadioGroup, Stack } from '@mui/material'
import { Network } from '../../../types'
import { ClientContext } from '../../../context/main'
export const SelectNetwork: React.FC<{ page: 'select network' }> = () => {
const { network, switchNetwork } = useContext(ClientContext)
return (
<Stack alignItems="center" spacing={5}>
<FormControl>
<RadioGroup
aria-labelledby="demo-controlled-radio-buttons-group"
name="controlled-radio-buttons-group"
value={network}
onChange={(e) => switchNetwork(e.target.value as Network)}
row
>
<FormControlLabel value="SANDBOX" control={<Radio color="default" />} label="Testnet Sandbox" />
<FormControlLabel value="QA" control={<Radio color="default" />} label="QA" />
</RadioGroup>
</FormControl>
</Stack>
)
}
@@ -0,0 +1,80 @@
import React, { useEffect, useState } from 'react'
import { Button } from '@mui/material'
import { WordTiles, HiddenWords } from '../components/word-tiles'
import { THiddenMnemonicWords, THiddenMnemonicWord, TMnemonicWord, TMnemonicWords } from '../types'
import { randomNumberBetween } from '../../../utils'
import { Title, Subtitle } from '../components'
const numberOfRandomWords = 4
export const VerifyMnemonic = ({
mnemonicWords,
onComplete,
}: {
page: 'verify mnemonic'
mnemonicWords?: TMnemonicWords
onComplete: () => void
}) => {
const [randomWords, setRandomWords] = useState<TMnemonicWords>()
const [hiddenRandomWords, setHiddenRandomWords] = useState<THiddenMnemonicWords>()
const [currentSelection, setCurrentSelection] = useState(0)
useEffect(() => {
if (mnemonicWords) {
const randomWords = getRandomEntriesFromArray<TMnemonicWord>(mnemonicWords, numberOfRandomWords)
const withHiddenProperty = randomWords.map((word) => ({ ...word, hidden: true }))
const shuffled = getRandomEntriesFromArray<THiddenMnemonicWord>(withHiddenProperty, numberOfRandomWords)
setRandomWords(randomWords)
setHiddenRandomWords(shuffled)
}
}, [mnemonicWords])
const revealWord = ({ name }: { name: string }) => {
if (name === hiddenRandomWords![currentSelection].name) {
setHiddenRandomWords((hiddenWords) =>
hiddenWords?.map((word) => (word.name === name ? { ...word, hidden: false } : word)),
)
setRandomWords((randomWords) =>
randomWords?.map((word) => (word.name === name ? { ...word, disabled: true } : word)),
)
setCurrentSelection((current) => current + 1)
}
}
if (randomWords && hiddenRandomWords) {
return (
<>
<Title title="Verify your mnemonic" />
<Subtitle subtitle="Select the words from your mnmonic based on their order" />
<HiddenWords mnemonicWords={hiddenRandomWords} />
<WordTiles
mnemonicWords={randomWords}
onClick={currentSelection !== numberOfRandomWords ? revealWord : undefined}
/>
<Button
variant="contained"
sx={{ width: 300 }}
size="large"
disabled={currentSelection !== numberOfRandomWords}
onClick={onComplete}
>
Next
</Button>
</>
)
}
return null
}
function getRandomEntriesFromArray<T>(arr: T[], numberOfEntries: number) {
const init = [...arr]
let randomEntries: T[] = []
while (randomEntries.length !== numberOfEntries) {
const rand = randomNumberBetween(0, init.length - 1)
randomEntries.push(init[rand])
init.splice(rand, 1)
}
return randomEntries
}
@@ -0,0 +1,42 @@
import React from 'react'
import { Button, Stack } from '@mui/material'
import { SubtitleSlick, Title } from '../components'
export const WelcomeContent: React.FC<{
page: 'welcome'
onUseExisting: () => void
onCreateAccountComplete: () => void
}> = ({ onUseExisting, onCreateAccountComplete }) => {
return (
<>
<Title title="Welcome to NYM" />
<SubtitleSlick subtitle="Next generation of privacy" />
<Stack spacing={3} sx={{ width: 300 }}>
<Button
fullWidth
variant="contained"
color="primary"
disableElevation
size="large"
onClick={onCreateAccountComplete}
>
Create Account
</Button>
<Button
fullWidth
variant="outlined"
size="large"
sx={{
color: 'common.white',
border: '1px solid white',
'&:hover': { border: '1px solid white', '&:hover': { background: 'none' } },
}}
onClick={onUseExisting}
disableRipple
>
Sign in
</Button>
</Stack>
</>
)
}
+19
View File
@@ -0,0 +1,19 @@
export type TPages =
| 'welcome'
| 'create account'
| 'verify mnemonic'
| 'create password'
| 'existing account'
| 'select network'
| 'legacy create account'
export type TMnemonicWord = {
name: string
index: number
disabled: boolean
}
export type TMnemonicWords = TMnemonicWord[]
export type THiddenMnemonicWord = { hidden: boolean } & TMnemonicWord
export type THiddenMnemonicWords = THiddenMnemonicWord[]
+7 -2
View File
@@ -1,5 +1,6 @@
import { invoke } from '@tauri-apps/api'
import {
Account,
Balance,
Coin,
DelegationResult,
@@ -8,6 +9,7 @@ import {
InclusionProbabilityResponse,
MixNode,
MixnodeStatusResponse,
Network,
Operation,
RewardEstimationResponse,
StakeSaturationResponse,
@@ -16,14 +18,15 @@ import {
TCreateAccount,
TMixnodeBondDetails,
TPagedDelegations,
TSignInWithMnemonic,
} from '../types'
export const createAccount = async (): Promise<TCreateAccount> => await invoke('create_new_account')
export const signInWithMnemonic = async (mnemonic: string): Promise<TSignInWithMnemonic> =>
export const signInWithMnemonic = async (mnemonic: string): Promise<Account> =>
await invoke('connect_with_mnemonic', { mnemonic })
export const signOut = async () => await invoke('logout')
export const minorToMajor = async (amount: string): Promise<Coin> => await invoke('minor_to_major', { amount })
export const majorToMinor = async (amount: string): Promise<Coin> => await invoke('major_to_minor', { amount })
@@ -102,3 +105,5 @@ export const updateMixnode = async ({ profitMarginPercent }: { profitMarginPerce
export const getInclusionProbability = async (identity: string): Promise<InclusionProbabilityResponse> =>
await invoke('mixnode_inclusion_probability', { identity })
export const selectNetwork = async (network: Network): Promise<Account> => await invoke('switch_network', { network })
+10
View File
@@ -17,3 +17,13 @@ export const NymWalletTheme: React.FC = ({ children }) => {
</ThemeProvider>
)
}
export const WelcomeTheme: React.FC = ({ children }) => {
const theme = createTheme(getDesignTokens('dark'))
return (
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>
)
}
+5
View File
@@ -53,3 +53,8 @@ export type TMixnodeBondDetails = {
mix_node: MixNode
proxy: any
}
export type TCurrency = {
minor: 'unym' | 'unymt'
major: 'nym' | 'nymt'
}
@@ -0,0 +1,4 @@
export interface CoreNodeStatusResponse {
identity: string;
count: number;
}
+1
View File
@@ -16,3 +16,4 @@ export * from './rewardestimationresponse'
export * from './mixnodestatus'
export * from './mixnodestatusresponse'
export * from './inclusionprobabilityresponse'
export * from './network'
+1 -1
View File
@@ -1 +1 @@
export type Network = "QA" | "SANDBOX";
export type Network = "QA" | "SANDBOX" | "MAINNET";
+27 -1
View File
@@ -2,7 +2,7 @@ import { invoke } from '@tauri-apps/api'
import bs58 from 'bs58'
import { minor, valid } from 'semver'
import { userBalance, majorToMinor, getGasFee } from '../requests'
import { Coin } from '../types'
import { Coin, Network, TCurrency } from '../types'
export const validateKey = (key: string, bytesLength: number): boolean => {
// it must be a valid base58 key
@@ -98,3 +98,29 @@ export const checkHasEnoughFunds = async (allocationValue: string) => {
console.log(e)
}
}
export const randomNumberBetween = (min: number, max: number) => {
min = Math.ceil(min)
max = Math.floor(max)
return Math.floor(Math.random() * (max - min + 1) + min)
}
export const currencyMap = (network: Network) => {
let currency = {
minor: 'unym',
major: 'nym',
} as TCurrency
switch (network) {
case 'MAINNET':
currency.minor = 'unym'
currency.major = 'nym'
break
case 'SANDBOX':
currency.minor = 'unymt'
currency.major = 'nymt'
break
}
return currency
}
+1 -1
View File
@@ -4,7 +4,7 @@ import ClientValidator, {
} from '@nymproject/nym-validator-client'
export const urls = {
blockExplorer: 'https://testnet-milhon-blocks.nymtech.net',
blockExplorer: 'https://sanbox-blocks.nymtech.net',
}
type TGlobalContext = {
+5 -3
View File
@@ -3,7 +3,7 @@
use crate::config::Config;
use crate::rewarding::{error::RewardingError, IntervalRewardParams, MixnodeToReward};
use config::defaults::DEFAULT_VALIDATOR_API_PORT;
use config::defaults::{default_network, DEFAULT_VALIDATOR_API_PORT};
use mixnet_contract_common::{
ContractStateParams, Delegation, ExecuteMsg, GatewayBond, IdentityKey, Interval, MixNodeBond,
MixnodeRewardingStatusResponse, RewardedSetNodeStatus, RewardedSetUpdateDetails,
@@ -37,6 +37,7 @@ impl Client<QueryNymdClient> {
.parse()
.unwrap();
let nymd_url = config.get_nymd_validator_url();
let network = default_network();
let mixnet_contract = config
.get_mixnet_contract_address()
@@ -44,7 +45,7 @@ impl Client<QueryNymdClient> {
.expect("the mixnet contract address is invalid!");
let client_config =
validator_client::Config::new(nymd_url, api_url, Some(mixnet_contract), None);
validator_client::Config::new(network, nymd_url, api_url, Some(mixnet_contract), None);
let inner =
validator_client::Client::new_query(client_config).expect("Failed to connect to nymd!");
@@ -60,6 +61,7 @@ impl Client<SigningNymdClient> {
.parse()
.unwrap();
let nymd_url = config.get_nymd_validator_url();
let network = default_network();
let mixnet_contract = config
.get_mixnet_contract_address()
@@ -71,7 +73,7 @@ impl Client<SigningNymdClient> {
.expect("the mnemonic is invalid!");
let client_config =
validator_client::Config::new(nymd_url, api_url, Some(mixnet_contract), None);
validator_client::Config::new(network, nymd_url, api_url, Some(mixnet_contract), None);
let inner = validator_client::Client::new_signing(client_config, mnemonic)
.expect("Failed to connect to nymd!");