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:
@@ -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()
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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/"),
|
||||
)]
|
||||
}
|
||||
@@ -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),
|
||||
]
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 +1 @@
|
||||
MAJOR_CURRENCY=nymt
|
||||
MINOR_CURRENCY=unymt
|
||||
ADMIN_ADDRESS=nymt1a586lvexy7ahva8h27fp6h8ds8taea6p95w3vx
|
||||
NETWORK_NAME=sandbox
|
||||
ADMIN_ADDRESS=
|
||||
Generated
+134
-13
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
@@ -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]} />
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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}` },
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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'
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
+9
-42
@@ -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>
|
||||
)
|
||||
+16
-36
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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[]
|
||||
@@ -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 })
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -16,3 +16,4 @@ export * from './rewardestimationresponse'
|
||||
export * from './mixnodestatus'
|
||||
export * from './mixnodestatusresponse'
|
||||
export * from './inclusionprobabilityresponse'
|
||||
export * from './network'
|
||||
|
||||
@@ -1 +1 @@
|
||||
export type Network = "QA" | "SANDBOX";
|
||||
export type Network = "QA" | "SANDBOX" | "MAINNET";
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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!");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user