Compare commits

..

13 Commits

Author SHA1 Message Date
fmtabbara d811446b6a use back button component 2022-07-05 16:03:15 +01:00
fmtabbara 65e252416c Merge branch 'develop' into feature/wallet-bonding-with-fees 2022-07-05 16:00:15 +01:00
fmtabbara bfd86d1462 fix type errors 2022-07-04 13:46:00 +01:00
fmtabbara f4215d192f lint fixes 2022-07-04 12:27:42 +01:00
fmtabbara 2524051da9 update tauri functions 2022-07-04 12:01:02 +01:00
fmtabbara 57245aaffe centralize functions 2022-07-04 11:59:46 +01:00
fmtabbara 9398a055a2 refresh state 2022-07-04 11:59:20 +01:00
fmtabbara c8bf0fb853 use confirmation modal for successful/failed unbond 2022-07-04 11:58:58 +01:00
fmtabbara 91b9e2a1a7 update to bonding confirmation modal 2022-07-04 11:58:23 +01:00
fmtabbara 426c9ae026 centralize functions for unbonding gateways and mixnodes 2022-07-04 11:57:28 +01:00
fmtabbara 65e0b69a9d rebuild unbond page to allow logic split between gateways and mixnodes 2022-07-04 11:56:57 +01:00
fmtabbara b9001458de remove dummy data 2022-06-29 15:40:24 +01:00
fmtabbara 3a8214cdb7 split out bond mixnode and gateway forms. add fees for bonding to mixnode or gateway. some small UI additions 2022-06-29 15:15:49 +01:00
170 changed files with 18244 additions and 4474 deletions
+2 -6
View File
@@ -9,9 +9,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
- socks5 client/websocket client: add `--force-register-gateway` flag, useful when rerunning init ([#1353])
- nym-connect: initial proof-of-concept of a UI around the socks5 client was added
- nym-connect: add ability to select network requester and gateway ([#1427])
- nym-connect: add ability to export gateway keys as JSON
- nym-connect: add auto updater
- nym-connect: add ability to select network requester and gateway ([#1427]).
- all: added network compilation target to `--help` (or `--version`) commands ([#1256]).
- explorer-api: learned how to sum the delegations by owner in a new endpoint.
- explorer-api: add apy values to `mix_nodes` endpoint
@@ -40,14 +38,12 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
### Changed
- nym-connect: reuse config id instead of creating a new id on each connection
- nym-connect: reuse config id instead of creating a new id on each connection.
- validator-client: created internal `Coin` type that replaces coins from `cosmrs` and `cosmwasm` for API entrypoints [[#1295]]
- all: updated all `cosmwasm`-related dependencies to `1.0.0` and `cw-storage-plus` to `0.13.4` [[#1318]]
- all: updated `rocket` to `0.5.0-rc.2`.
- network-requester: allow to voluntarily store and send statistical data about the number of bytes the proxied server serves ([#1328])
- gateway: allow to voluntarily send statistical data about the number of active inboxes served by a gateway ([#1376])
- gateway & mixnode: move detailed build info back to `--version` from `--help`.
- socks5 client/websocket client: upgrade to latest clap and switched to declarative commandline parsing.
[#1249]: https://github.com/nymtech/nym/pull/1249
[#1256]: https://github.com/nymtech/nym/pull/1256
Generated
+16 -22
View File
@@ -568,16 +568,16 @@ dependencies = [
[[package]]
name = "clap"
version = "3.2.8"
version = "3.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "190814073e85d238f31ff738fcb0bf6910cedeb73376c87cd69291028966fd83"
checksum = "71c47df61d9e16dc010b55dba1952a57d8c215dbb533fd13cdd13369aac73b1c"
dependencies = [
"atty",
"bitflags",
"clap_derive",
"clap_lex",
"indexmap",
"once_cell",
"lazy_static",
"os_str_bytes",
"strsim 0.10.0",
"termcolor",
"textwrap 0.15.0",
@@ -585,9 +585,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "3.2.7"
version = "3.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "759bf187376e1afa7b85b959e6a664a3e7a95203415dba952ad19139e798f902"
checksum = "a3aab4734e083b809aaf5794e14e756d1c798d2c69c7f7de7a09a2f5214993c1"
dependencies = [
"heck 0.4.0",
"proc-macro-error",
@@ -596,15 +596,6 @@ dependencies = [
"syn",
]
[[package]]
name = "clap_lex"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
dependencies = [
"os_str_bytes",
]
[[package]]
name = "client-core"
version = "1.0.1"
@@ -893,7 +884,7 @@ dependencies = [
"async-trait",
"bip39",
"cfg-if 0.1.10",
"clap 3.2.8",
"clap 3.1.8",
"coconut-interface",
"credential-storage",
"credentials",
@@ -3058,7 +3049,7 @@ dependencies = [
name = "nym-client"
version = "1.0.1"
dependencies = [
"clap 3.2.8",
"clap 2.34.0",
"client-core",
"coconut-interface",
"config",
@@ -3098,7 +3089,7 @@ dependencies = [
"bandwidth-claim-contract",
"bip39",
"bs58",
"clap 3.2.8",
"clap 3.1.8",
"coconut-interface",
"colored",
"config",
@@ -3143,7 +3134,7 @@ version = "1.0.1"
dependencies = [
"anyhow",
"bs58",
"clap 3.2.8",
"clap 3.1.8",
"colored",
"config",
"crypto",
@@ -3225,7 +3216,7 @@ dependencies = [
name = "nym-socks5-client"
version = "1.0.1"
dependencies = [
"clap 3.2.8",
"clap 2.34.0",
"client-core",
"coconut-interface",
"config",
@@ -3509,9 +3500,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.13.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1"
checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9"
[[package]]
name = "oorandom"
@@ -3576,6 +3567,9 @@ name = "os_str_bytes"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
dependencies = [
"memchr",
]
[[package]]
name = "pairing"
+1 -2
View File
@@ -2,7 +2,6 @@
name = "nym-client"
version = "1.0.1"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
rust-version = "1.56"
@@ -20,7 +19,7 @@ futures = "0.3" # bunch of futures stuff, however, now that I think about it, it
# and the single instance of abortable we have should really be refactored anyway
url = "2.2"
clap = { version = "3.2.8", features = ["cargo", "derive"] }
clap = "2.33.0" # for the command line arguments
dirs = "4.0"
dotenv = "0.15.0" # for obtaining environmental variables (only used for RUST_LOG for time being)
log = "0.4" # self explanatory
+79 -88
View File
@@ -1,100 +1,88 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use clap::Args;
use clap::{App, Arg, ArgMatches};
use client_core::config::GatewayEndpoint;
use config::NymConfig;
use crate::{
client::config::Config,
commands::{override_config, OverrideConfig},
use crate::client::config::Config;
use crate::commands::override_config;
#[cfg(feature = "eth")]
#[cfg(not(feature = "coconut"))]
use crate::commands::{
DEFAULT_ETH_ENDPOINT, DEFAULT_ETH_PRIVATE_KEY, ENABLED_CREDENTIALS_MODE_ARG_NAME,
ETH_ENDPOINT_ARG_NAME, ETH_PRIVATE_KEY_ARG_NAME,
};
#[cfg(all(feature = "eth", not(feature = "coconut")))]
use crate::commands::{DEFAULT_ETH_ENDPOINT, DEFAULT_ETH_PRIVATE_KEY};
pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
let app = App::new("init")
.about("Initialise a Nym client. Do this first!")
.arg(Arg::with_name("id")
.long("id")
.help("Id of the nym-mixnet-client we want to create config for.")
.takes_value(true)
.required(true)
)
.arg(Arg::with_name("gateway")
.long("gateway")
.help("Id of the gateway we are going to connect to.")
.takes_value(true)
)
.arg(Arg::with_name("force-register-gateway")
.long("force-register-gateway")
.help("Force register gateway. WARNING: this will overwrite any existing keys for the given id, potentially causing loss of access.")
.takes_value(false)
)
.arg(Arg::with_name("validators")
.long("validators")
.help("Comma separated list of rest endpoints of the validators")
.takes_value(true),
)
.arg(Arg::with_name("disable-socket")
.long("disable-socket")
.help("Whether to not start the websocket")
)
.arg(Arg::with_name("port")
.short("p")
.long("port")
.help("Port for the socket (if applicable) to listen on in all subsequent runs")
.takes_value(true)
)
.arg(Arg::with_name("fastmode")
.long("fastmode")
.hidden(true) // this will prevent this flag from being displayed in `--help`
.help("Mostly debug-related option to increase default traffic rate so that you would not need to modify config post init")
);
#[cfg(feature = "eth")]
#[cfg(not(feature = "coconut"))]
let app = app
.arg(
Arg::with_name(ENABLED_CREDENTIALS_MODE_ARG_NAME)
.long(ENABLED_CREDENTIALS_MODE_ARG_NAME)
.help("Set this client to work in a disabled credentials mode that would attempt to use gateway without bandwidth credential requirement. If this value is set, --eth_endpoint and --eth_private_key don't need to be set.")
.conflicts_with_all(&[ETH_ENDPOINT_ARG_NAME, ETH_PRIVATE_KEY_ARG_NAME])
)
.arg(Arg::with_name(ETH_ENDPOINT_ARG_NAME)
.long(ETH_ENDPOINT_ARG_NAME)
.help("URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20 tokens. If you don't want to set this value, use --testnet-mode instead")
.takes_value(true)
.default_value_if(ENABLED_CREDENTIALS_MODE_ARG_NAME, None, DEFAULT_ETH_ENDPOINT)
.required(true))
.arg(Arg::with_name(ETH_PRIVATE_KEY_ARG_NAME)
.long(ETH_PRIVATE_KEY_ARG_NAME)
.help("Ethereum private key used for obtaining bandwidth tokens from ERC20 tokens. If you don't want to set this value, use --testnet-mode instead")
.takes_value(true)
.default_value_if(ENABLED_CREDENTIALS_MODE_ARG_NAME, None, DEFAULT_ETH_PRIVATE_KEY)
.required(true)
);
#[derive(Args, Clone)]
pub(crate) struct Init {
/// Id of the nym-mixnet-client we want to create config for.
#[clap(long)]
id: String,
/// Id of the gateway we are going to connect to.
#[clap(long)]
gateway: Option<String>,
/// Force register gateway. WARNING: this will overwrite any existing keys for the given id,
/// potentially causing loss of access.
#[clap(long)]
force_register_gateway: bool,
/// Comma separated list of rest endpoints of the validators
#[clap(long)]
validators: Option<String>,
/// Whether to not start the websocket
#[clap(long)]
disable_socket: bool,
/// Port for the socket (if applicable) to listen on in all subsequent runs
#[clap(short, long)]
port: Option<u16>,
/// Mostly debug-related option to increase default traffic rate so that you would not need to
/// modify config post init
#[clap(long, hidden = true)]
fastmode: bool,
/// Set this client to work in a enabled credentials mode that would attempt to use gateway
/// with bandwidth credential requirement. If this value is set, --eth-endpoint and
/// --eth-private_key don't need to be set.
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[clap(long, conflicts_with_all = &["eth-endpoint", "eth-private-key"])]
enabled_credentials_mode: bool,
/// URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20
/// tokens. If you don't want to set this value, use --enabled-credentials-mode instead
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[clap(
long,
default_value_if("enabled-credentials-mode", None, Some(DEFAULT_ETH_ENDPOINT))
)]
eth_endpoint: String,
/// Ethereum private key used for obtaining bandwidth tokens from ERC20 tokens. If you don't
/// want to set this value, use --enabled-credentials-mode instead")
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[clap(
long,
default_value_if("enabled-credentials-mode", None, Some(DEFAULT_ETH_PRIVATE_KEY))
)]
eth_private_key: String,
app
}
impl From<Init> for OverrideConfig {
fn from(init_config: Init) -> Self {
OverrideConfig {
validators: init_config.validators,
disable_socket: init_config.disable_socket,
port: init_config.port,
fastmode: init_config.fastmode,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
enabled_credentials_mode: init_config.enabled_credentials_mode,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
eth_private_key: Some(init_config.eth_private_key),
#[cfg(all(feature = "eth", not(feature = "coconut")))]
eth_endpoint: Some(init_config.eth_endpoint),
}
}
}
pub(crate) async fn execute(args: &Init) {
pub async fn execute(matches: ArgMatches<'static>) {
println!("Initialising client...");
let id = &args.id;
let id = matches.value_of("id").unwrap(); // required for now
let already_init = Config::default_config_file_path(Some(id)).exists();
if already_init {
@@ -107,7 +95,7 @@ pub(crate) async fn execute(args: &Init) {
// Usually you only register with the gateway on the first init, however you can force
// re-registering if wanted.
let user_wants_force_register = args.force_register_gateway;
let user_wants_force_register = matches.is_present("force-register-gateway");
// If the client was already initialized, don't generate new keys and don't re-register with
// the gateway (because this would create a new shared key).
@@ -115,11 +103,14 @@ pub(crate) async fn execute(args: &Init) {
let register_gateway = !already_init || user_wants_force_register;
// Attempt to use a user-provided gateway, if possible
let user_chosen_gateway_id = args.gateway.as_deref();
let user_chosen_gateway_id = matches.value_of("gateway");
let mut config = Config::new(id);
let override_config_fields = OverrideConfig::from(args.clone());
config = override_config(config, override_config_fields);
config = override_config(config, &matches);
if matches.is_present("fastmode") {
config.get_base_mut().set_high_default_traffic_volume();
}
let gateway = setup_gateway(id, register_gateway, user_chosen_gateway_id, &config).await;
config.get_base_mut().with_gateway_endpoint(gateway);
+29 -118
View File
@@ -2,10 +2,14 @@
// SPDX-License-Identifier: Apache-2.0
use crate::client::config::{Config, SocketType};
use clap::{Parser, Subcommand};
use network_defaults::DEFAULT_NETWORK;
use clap::ArgMatches;
use url::Url;
pub(crate) const ENABLED_CREDENTIALS_MODE_ARG_NAME: &str = "enabled-credentials-mode";
#[cfg(not(feature = "coconut"))]
pub(crate) const ETH_ENDPOINT_ARG_NAME: &str = "eth_endpoint";
#[cfg(not(feature = "coconut"))]
pub(crate) const ETH_PRIVATE_KEY_ARG_NAME: &str = "eth_private_key";
#[cfg(not(feature = "coconut"))]
pub(crate) const DEFAULT_ETH_ENDPOINT: &str =
"https://rinkeby.infura.io/v3/00000000000000000000000000000000";
@@ -17,86 +21,6 @@ pub(crate) mod init;
pub(crate) mod run;
pub(crate) mod upgrade;
fn long_version() -> String {
format!(
r#"
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
"#,
"Build Timestamp:",
env!("VERGEN_BUILD_TIMESTAMP"),
"Build Version:",
env!("VERGEN_BUILD_SEMVER"),
"Commit SHA:",
env!("VERGEN_GIT_SHA"),
"Commit Date:",
env!("VERGEN_GIT_COMMIT_TIMESTAMP"),
"Commit Branch:",
env!("VERGEN_GIT_BRANCH"),
"rustc Version:",
env!("VERGEN_RUSTC_SEMVER"),
"rustc Channel:",
env!("VERGEN_RUSTC_CHANNEL"),
"cargo Profile:",
env!("VERGEN_CARGO_PROFILE"),
"Network:",
DEFAULT_NETWORK
)
}
fn long_version_static() -> &'static str {
Box::leak(long_version().into_boxed_str())
}
#[derive(Parser)]
#[clap(author = "Nymtech", version, long_version = long_version_static(), about)]
pub(crate) struct Cli {
#[clap(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
pub(crate) enum Commands {
/// Initialise a Nym client. Do this first!
Init(init::Init),
/// Run the Nym client with provided configuration client optionally overriding set parameters
Run(run::Run),
/// Try to upgrade the client
Upgrade(upgrade::Upgrade),
}
// Configuration that can be overridden.
pub(crate) struct OverrideConfig {
validators: Option<String>,
disable_socket: bool,
port: Option<u16>,
fastmode: bool,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
enabled_credentials_mode: bool,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
eth_private_key: Option<String>,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
eth_endpoint: Option<String>,
}
pub(crate) async fn execute(args: &Cli) {
match &args.command {
Commands::Init(m) => init::execute(m).await,
Commands::Run(m) => run::execute(m).await,
Commands::Upgrade(m) => upgrade::execute(m),
}
}
fn parse_validators(raw: &str) -> Vec<Url> {
raw.split(',')
.map(|raw_validator| {
@@ -108,58 +32,45 @@ fn parse_validators(raw: &str) -> Vec<Url> {
.collect()
}
pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Config {
if let Some(raw_validators) = args.validators {
pub(crate) fn override_config(mut config: Config, matches: &ArgMatches<'_>) -> Config {
if let Some(raw_validators) = matches.value_of("validators") {
config
.get_base_mut()
.set_custom_validator_apis(parse_validators(&raw_validators));
.set_custom_validator_apis(parse_validators(raw_validators));
}
if args.disable_socket {
if matches.is_present("disable-socket") {
config = config.with_socket(SocketType::None);
}
if let Some(port) = args.port {
config = config.with_port(port);
if let Some(port) = matches.value_of("port").map(str::parse) {
if let Err(err) = port {
// if port was overridden, it must be parsable
panic!("Invalid port value provided - {:?}", err);
}
config = config.with_port(port.unwrap());
}
#[cfg(all(not(feature = "eth"), not(feature = "coconut")))]
{
#[cfg(not(feature = "coconut"))]
if let Some(eth_endpoint) = matches.value_of(ETH_ENDPOINT_ARG_NAME) {
config.get_base_mut().with_eth_endpoint(eth_endpoint);
} else if !cfg!(feature = "eth") {
config
.get_base_mut()
.with_eth_endpoint(DEFAULT_ETH_ENDPOINT.to_string());
.with_eth_endpoint(DEFAULT_ETH_ENDPOINT);
}
#[cfg(not(feature = "coconut"))]
if let Some(eth_private_key) = matches.value_of(ETH_PRIVATE_KEY_ARG_NAME) {
config.get_base_mut().with_eth_private_key(eth_private_key);
} else if !cfg!(feature = "eth") {
config
.get_base_mut()
.with_eth_private_key(DEFAULT_ETH_PRIVATE_KEY.to_string());
.with_eth_private_key(DEFAULT_ETH_PRIVATE_KEY);
}
#[cfg(all(feature = "eth", not(feature = "coconut")))]
{
if args.enabled_credentials_mode {
config.get_base_mut().with_disabled_credentials(false)
}
if let Some(eth_endpoint) = args.eth_endpoint {
config.get_base_mut().with_eth_endpoint(eth_endpoint);
}
if let Some(eth_private_key) = args.eth_private_key {
config.get_base_mut().with_eth_private_key(eth_private_key);
}
}
if args.fastmode {
config.get_base_mut().set_high_default_traffic_volume();
if matches.is_present(ENABLED_CREDENTIALS_MODE_ARG_NAME) {
config.get_base_mut().with_disabled_credentials(false)
}
config
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn verify_cli() {
Cli::command().debug_assert();
}
}
+59 -69
View File
@@ -1,77 +1,68 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{
client::{config::Config, NymClient},
commands::{override_config, OverrideConfig},
use crate::client::config::Config;
use crate::client::NymClient;
use crate::commands::override_config;
#[cfg(feature = "eth")]
#[cfg(not(feature = "coconut"))]
use crate::commands::{
ENABLED_CREDENTIALS_MODE_ARG_NAME, ETH_ENDPOINT_ARG_NAME, ETH_PRIVATE_KEY_ARG_NAME,
};
use clap::Args;
use clap::{App, Arg, ArgMatches};
use config::NymConfig;
use log::*;
use version_checker::is_minor_version_compatible;
#[derive(Args, Clone)]
pub(crate) struct Run {
/// Id of the nym-mixnet-client we want to run.
#[clap(long)]
id: String,
pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
let app = App::new("run")
.about("Run the Nym client with provided configuration client optionally overriding set parameters")
.arg(Arg::with_name("id")
.long("id")
.help("Id of the nym-mixnet-client we want to run.")
.takes_value(true)
.required(true)
)
// the rest of arguments are optional, they are used to override settings in config file
.arg(Arg::with_name("validators")
.long("validators")
.help("Comma separated list rest rest endpoints of the validators")
.takes_value(true),
)
.arg(Arg::with_name("gateway")
.long("gateway")
.help("Id of the gateway we want to connect to. If overridden, it is user's responsibility to ensure prior registration happened")
.takes_value(true)
)
.arg(Arg::with_name("disable-socket")
.long("disable-socket")
.help("Whether to not start the websocket")
)
.arg(Arg::with_name("port")
.short("p")
.long("port")
.help("Port for the socket (if applicable) to listen on")
.takes_value(true)
);
#[cfg(feature = "eth")]
#[cfg(not(feature = "coconut"))]
let app = app
.arg(
Arg::with_name(ENABLED_CREDENTIALS_MODE_ARG_NAME)
.long(ENABLED_CREDENTIALS_MODE_ARG_NAME)
.help("Set this client to work in a enabled credentials mode that would attempt to use gateway with bandwidth credential requirement. If this value is set, --eth_endpoint and --eth_private_key don't need to be set.")
.conflicts_with_all(&[ETH_ENDPOINT_ARG_NAME, ETH_PRIVATE_KEY_ARG_NAME])
)
.arg(Arg::with_name(ETH_ENDPOINT_ARG_NAME)
.long(ETH_ENDPOINT_ARG_NAME)
.help("URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20 tokens. If you don't want to set this value, use --testnet-mode instead")
.takes_value(true))
.arg(Arg::with_name(ETH_PRIVATE_KEY_ARG_NAME)
.long(ETH_PRIVATE_KEY_ARG_NAME)
.help("Ethereum private key used for obtaining bandwidth tokens from ERC20 tokens. If you don't want to set this value, use --testnet-mode instead")
.takes_value(true));
/// Comma separated list of rest endpoints of the validators
#[clap(long)]
validators: Option<String>,
/// Id of the gateway we want to connect to. If overridden, it is user's responsibility to
/// ensure prior registration happened
#[clap(long)]
gateway: Option<String>,
/// Whether to not start the websocket
#[clap(long)]
disable_socket: bool,
/// Port for the socket to listen on
#[clap(short, long)]
port: Option<u16>,
/// Set this client to work in a enabled credentials mode that would attempt to use gateway
/// with bandwidth credential requirement. If this value is set, --eth-endpoint and
/// --eth-private-key don't need to be set.
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[clap(long, conflicts_with_all = &["eth-endpoint", "eth-private-key"])]
enabled_credentials_mode: bool,
/// URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20
/// tokens. If you don't want to set this value, use --enabled-credentials-mode instead
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[clap(long)]
eth_endpoint: Option<String>,
/// Ethereum private key used for obtaining bandwidth tokens from ERC20 tokens. If you don't
/// want to set this value, use --enabled-credentials-mode instead")
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[clap(long)]
eth_private_key: Option<String>,
}
impl From<Run> for OverrideConfig {
fn from(run_config: Run) -> Self {
OverrideConfig {
validators: run_config.validators,
disable_socket: run_config.disable_socket,
port: run_config.port,
fastmode: false,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
enabled_credentials_mode: run_config.enabled_credentials_mode,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
eth_private_key: run_config.eth_private_key,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
eth_endpoint: run_config.eth_endpoint,
}
}
app
}
// this only checks compatibility between config the binary. It does not take into consideration
@@ -93,8 +84,8 @@ fn version_check(cfg: &Config) -> bool {
}
}
pub(crate) async fn execute(args: &Run) {
let id = &args.id;
pub async fn execute(matches: ArgMatches<'static>) {
let id = matches.value_of("id").unwrap();
let mut config = match Config::load_from_file(Some(id)) {
Ok(cfg) => cfg,
@@ -104,8 +95,7 @@ pub(crate) async fn execute(args: &Run) {
}
};
let override_config_fields = OverrideConfig::from(args.clone());
config = override_config(config, override_config_fields);
config = override_config(config, &matches);
if !version_check(&config) {
error!("failed the local version check");
+18 -16
View File
@@ -2,13 +2,12 @@
// SPDX-License-Identifier: Apache-2.0
use crate::client::config::{Config, MISSING_VALUE};
use config::{defaults::default_api_endpoints, NymConfig};
use version_checker::Version;
use clap::Args;
use clap::{App, Arg, ArgMatches};
use config::defaults::default_api_endpoints;
use config::NymConfig;
use std::fmt::Display;
use std::process;
use version_checker::Version;
#[allow(dead_code)]
fn fail_upgrade<D1: Display, D2: Display>(from_version: D1, to_version: D2) -> ! {
@@ -50,11 +49,14 @@ fn unsupported_upgrade(current_version: &Version, config_version: &Version) -> !
process::exit(1)
}
#[derive(Args, Clone)]
pub(crate) struct Upgrade {
/// Id of the nym-client we want to upgrade
#[clap(long)]
id: String,
pub fn command_args<'a, 'b>() -> App<'a, 'b> {
App::new("upgrade").about("Try to upgrade the client").arg(
Arg::with_name("id")
.long("id")
.help("Id of the nym-client we want to upgrade")
.takes_value(true)
.required(true),
)
}
fn parse_config_version(config: &Config) -> Version {
@@ -93,7 +95,7 @@ fn parse_package_version() -> Version {
fn minor_0_12_upgrade(
mut config: Config,
_matches: &Upgrade,
_matches: &ArgMatches<'_>,
config_version: &Version,
package_version: &Version,
) -> Config {
@@ -129,7 +131,7 @@ fn minor_0_12_upgrade(
config
}
fn do_upgrade(mut config: Config, args: &Upgrade, package_version: &Version) {
fn do_upgrade(mut config: Config, matches: &ArgMatches<'_>, package_version: &Version) {
loop {
let config_version = parse_config_version(&config);
@@ -141,7 +143,7 @@ fn do_upgrade(mut config: Config, args: &Upgrade, package_version: &Version) {
config = match config_version.major {
0 => match config_version.minor {
9 | 10 => outdated_upgrade(&config_version, package_version),
11 => minor_0_12_upgrade(config, args, &config_version, package_version),
11 => minor_0_12_upgrade(config, matches, &config_version, package_version),
_ => unsupported_upgrade(&config_version, package_version),
},
_ => unsupported_upgrade(&config_version, package_version),
@@ -149,10 +151,10 @@ fn do_upgrade(mut config: Config, args: &Upgrade, package_version: &Version) {
}
}
pub(crate) fn execute(args: &Upgrade) {
pub fn execute(matches: &ArgMatches<'_>) {
let package_version = parse_package_version();
let id = &args.id;
let id = matches.value_of("id").unwrap();
let existing_config = Config::load_from_file(Some(id)).unwrap_or_else(|err| {
eprintln!("failed to load existing config file! - {:?}", err);
@@ -165,5 +167,5 @@ pub(crate) fn execute(args: &Upgrade) {
}
// here be upgrade path to 0.9.X and beyond based on version number from config
do_upgrade(existing_config, args, &package_version)
do_upgrade(existing_config, matches, &package_version)
}
+60 -3
View File
@@ -1,7 +1,8 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use clap::{crate_version, Parser};
use clap::{crate_version, App, ArgMatches};
use network_defaults::DEFAULT_NETWORK;
pub mod client;
pub mod commands;
@@ -13,8 +14,30 @@ async fn main() {
setup_logging();
println!("{}", banner());
let args = commands::Cli::parse();
commands::execute(&args).await;
let arg_matches = App::new("Nym Client")
.version(crate_version!())
.long_version(&*long_version())
.author("Nymtech")
.about("Implementation of the Nym Client")
.subcommand(commands::init::command_args())
.subcommand(commands::run::command_args())
.subcommand(commands::upgrade::command_args())
.get_matches();
execute(arg_matches).await;
}
async fn execute(matches: ArgMatches<'static>) {
match matches.subcommand() {
("init", Some(m)) => commands::init::execute(m.clone()).await,
("run", Some(m)) => commands::run::execute(m.clone()).await,
("upgrade", Some(m)) => commands::upgrade::execute(m),
_ => println!("{}", usage()),
}
}
fn usage() -> &'static str {
"usage: --help to see available options.\n\n"
}
fn banner() -> String {
@@ -34,6 +57,40 @@ fn banner() -> String {
)
}
fn long_version() -> String {
format!(
r#"
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
"#,
"Build Timestamp:",
env!("VERGEN_BUILD_TIMESTAMP"),
"Build Version:",
env!("VERGEN_BUILD_SEMVER"),
"Commit SHA:",
env!("VERGEN_GIT_SHA"),
"Commit Date:",
env!("VERGEN_GIT_COMMIT_TIMESTAMP"),
"Commit Branch:",
env!("VERGEN_GIT_BRANCH"),
"rustc Version:",
env!("VERGEN_RUSTC_SEMVER"),
"rustc Channel:",
env!("VERGEN_RUSTC_CHANNEL"),
"cargo Profile:",
env!("VERGEN_CARGO_PROFILE"),
"Network:",
DEFAULT_NETWORK
)
}
fn setup_logging() {
let mut log_builder = pretty_env_logger::formatted_timed_builder();
if let Ok(s) = ::std::env::var("RUST_LOG") {
+1 -2
View File
@@ -2,7 +2,6 @@
name = "nym-socks5-client"
version = "1.0.1"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021"
rust-version = "1.56"
@@ -11,7 +10,7 @@ name = "nym_socks5"
path = "src/lib.rs"
[dependencies]
clap = { version = "3.2.8", features = ["cargo", "derive"] }
clap = "2.33.0"
dirs = "4.0"
dotenv = "0.15.0"
futures = "0.3"
+5 -8
View File
@@ -287,15 +287,12 @@ impl NymClient {
pub async fn run_and_listen(&mut self, mut receiver: Socks5ControlMessageReceiver) {
self.start().await;
tokio::select! {
message = receiver.next() => {
log::debug!("Received message: {:?}", message);
match message {
Some(Socks5ControlMessage::Stop) => {
log::info!("Shutting down");
log::info!("Graceful shutdown of tasks not yet implemented, you might see (harmless) panics until then");
}
None => log::debug!("None"),
message = receiver.next() => match message {
Some(Socks5ControlMessage::Stop) => {
log::info!("Received: {:?}", message);
log::info!("Shutting down");
}
None => log::info!("none"),
}
}
}
+82 -88
View File
@@ -1,100 +1,91 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use clap::Args;
use clap::{App, Arg, ArgMatches};
use client_core::config::GatewayEndpoint;
use config::NymConfig;
use crate::{
client::config::Config,
commands::{override_config, OverrideConfig},
use crate::client::config::Config;
use crate::commands::override_config;
#[cfg(feature = "eth")]
#[cfg(not(feature = "coconut"))]
use crate::commands::{
DEFAULT_ETH_ENDPOINT, DEFAULT_ETH_PRIVATE_KEY, ENABLED_CREDENTIALS_MODE_ARG_NAME,
ETH_ENDPOINT_ARG_NAME, ETH_PRIVATE_KEY_ARG_NAME,
};
#[cfg(all(feature = "eth", not(feature = "coconut")))]
use crate::commands::{DEFAULT_ETH_ENDPOINT, DEFAULT_ETH_PRIVATE_KEY};
pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
let app = App::new("init")
.about("Initialise a Nym client. Do this first!")
.arg(Arg::with_name("id")
.long("id")
.help("Id of the nym-mixnet-client we want to create config for.")
.takes_value(true)
.required(true)
)
.arg(Arg::with_name("provider")
.long("provider")
.help("Address of the socks5 provider to send messages to.")
.takes_value(true)
.required(true)
)
.arg(Arg::with_name("gateway")
.long("gateway")
.help("Id of the gateway we are going to connect to.")
.takes_value(true)
)
.arg(Arg::with_name("force-register-gateway")
.long("force-register-gateway")
.help("Force register gateway. WARNING: this will overwrite any existing keys for the given id, potentially causing loss of access.")
.takes_value(false)
)
.arg(Arg::with_name("validators")
.long("validators")
.help("Comma separated list of rest endpoints of the validators")
.takes_value(true),
)
.arg(Arg::with_name("port")
.short("p")
.long("port")
.help("Port for the socket to listen on in all subsequent runs")
.takes_value(true)
)
.arg(Arg::with_name("fastmode")
.long("fastmode")
.hidden(true) // this will prevent this flag from being displayed in `--help`
.help("Mostly debug-related option to increase default traffic rate so that you would not need to modify config post init")
);
#[cfg(feature = "eth")]
#[cfg(not(feature = "coconut"))]
let app = app
.arg(
Arg::with_name(ENABLED_CREDENTIALS_MODE_ARG_NAME)
.long(ENABLED_CREDENTIALS_MODE_ARG_NAME)
.help("Set this client to work in a enabled credentials mode that would attempt to use gateway with bandwidth credential requirement. If this value is set, --eth_endpoint and --eth_private_key don't need to be set.")
.conflicts_with_all(&[ETH_ENDPOINT_ARG_NAME, ETH_PRIVATE_KEY_ARG_NAME])
)
.arg(Arg::with_name(ETH_ENDPOINT_ARG_NAME)
.long(ETH_ENDPOINT_ARG_NAME)
.help("URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20 tokens. If you don't want to set this value, use --testnet-mode instead")
.takes_value(true)
.default_value_if(ENABLED_CREDENTIALS_MODE_ARG_NAME, None, DEFAULT_ETH_ENDPOINT)
.required(true))
.arg(Arg::with_name(ETH_PRIVATE_KEY_ARG_NAME)
.long(ETH_PRIVATE_KEY_ARG_NAME)
.help("Ethereum private key used for obtaining bandwidth tokens from ERC20 tokens. If you don't want to set this value, use --testnet-mode instead")
.takes_value(true)
.default_value_if(ENABLED_CREDENTIALS_MODE_ARG_NAME, None, DEFAULT_ETH_PRIVATE_KEY)
.required(true)
);
#[derive(Args, Clone)]
pub(crate) struct Init {
/// Id of the nym-mixnet-client we want to create config for.
#[clap(long)]
id: String,
/// Address of the socks5 provider to send messages to.
#[clap(long)]
provider: String,
/// Id of the gateway we are going to connect to.
#[clap(long)]
gateway: Option<String>,
/// Force register gateway. WARNING: this will overwrite any existing keys for the given id,
/// potentially causing loss of access.
#[clap(long)]
force_register_gateway: bool,
/// Comma separated list of rest endpoints of the validators
#[clap(long)]
validators: Option<String>,
/// Port for the socket to listen on in all subsequent runs
#[clap(short, long)]
port: Option<u16>,
/// Mostly debug-related option to increase default traffic rate so that you would not need to
/// modify config post init
#[clap(long, hidden = true)]
fastmode: bool,
/// Set this client to work in a enabled credentials mode that would attempt to use gateway
/// with bandwidth credential requirement. If this value is set, --eth-endpoint and
/// --eth-private_key don't need to be set.
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[clap(long, conflicts_with_all = &["eth-endpoint", "eth-private-key"])]
enabled_credentials_mode: bool,
/// URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20
/// tokens. If you don't want to set this value, use --enabled-credentials-mode instead
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[clap(
long,
default_value_if("enabled-credentials-mode", None, Some(DEFAULT_ETH_ENDPOINT))
)]
eth_endpoint: String,
/// Ethereum private key used for obtaining bandwidth tokens from ERC20 tokens. If you don't
/// want to set this value, use --enabled-credentials-mode instead")
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[clap(
long,
default_value_if("enabled-credentials-mode", None, Some(DEFAULT_ETH_PRIVATE_KEY))
)]
eth_private_key: String,
app
}
impl From<Init> for OverrideConfig {
fn from(init_config: Init) -> Self {
OverrideConfig {
validators: init_config.validators,
port: init_config.port,
fastmode: init_config.fastmode,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
enabled_credentials_mode: init_config.enabled_credentials_mode,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
eth_private_key: Some(init_config.eth_private_key),
#[cfg(all(feature = "eth", not(feature = "coconut")))]
eth_endpoint: Some(init_config.eth_endpoint),
}
}
}
pub(crate) async fn execute(args: &Init) {
pub async fn execute(matches: ArgMatches<'static>) {
println!("Initialising client...");
let id = &args.id;
let provider_address = &args.provider;
let id = matches.value_of("id").unwrap(); // required for now
let provider_address = matches.value_of("provider").unwrap();
let already_init = Config::default_config_file_path(Some(id)).exists();
if already_init {
@@ -107,7 +98,7 @@ pub(crate) async fn execute(args: &Init) {
// Usually you only register with the gateway on the first init, however you can force
// re-registering if wanted.
let user_wants_force_register = args.force_register_gateway;
let user_wants_force_register = matches.is_present("force-register-gateway");
// If the client was already initialized, don't generate new keys and don't re-register with
// the gateway (because this would create a new shared key).
@@ -115,11 +106,14 @@ pub(crate) async fn execute(args: &Init) {
let register_gateway = !already_init || user_wants_force_register;
// Attempt to use a user-provided gateway, if possible
let user_chosen_gateway_id = args.gateway.as_deref();
let user_chosen_gateway_id = matches.value_of("gateway");
let mut config = Config::new(id, provider_address);
let override_config_fields = OverrideConfig::from(args.clone());
config = override_config(config, override_config_fields);
config = override_config(config, &matches);
if matches.is_present("fastmode") {
config.get_base_mut().set_high_default_traffic_volume();
}
let gateway = setup_gateway(id, register_gateway, user_chosen_gateway_id, &config).await;
config.get_base_mut().with_gateway_endpoint(gateway);
+28 -116
View File
@@ -2,14 +2,18 @@
// SPDX-License-Identifier: Apache-2.0
use crate::client::config::Config;
use clap::{Parser, Subcommand};
use network_defaults::DEFAULT_NETWORK;
use clap::ArgMatches;
use url::Url;
pub mod init;
pub(crate) mod run;
pub(crate) mod upgrade;
pub(crate) const ENABLED_CREDENTIALS_MODE_ARG_NAME: &str = "enabled-credentials-mode";
#[cfg(not(feature = "coconut"))]
pub(crate) const ETH_ENDPOINT_ARG_NAME: &str = "eth_endpoint";
#[cfg(not(feature = "coconut"))]
pub(crate) const ETH_PRIVATE_KEY_ARG_NAME: &str = "eth_private_key";
#[cfg(not(feature = "coconut"))]
pub(crate) const DEFAULT_ETH_ENDPOINT: &str =
"https://rinkeby.infura.io/v3/00000000000000000000000000000000";
@@ -17,85 +21,6 @@ pub(crate) const DEFAULT_ETH_ENDPOINT: &str =
pub(crate) const DEFAULT_ETH_PRIVATE_KEY: &str =
"0000000000000000000000000000000000000000000000000000000000000001";
fn long_version() -> String {
format!(
r#"
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
"#,
"Build Timestamp:",
env!("VERGEN_BUILD_TIMESTAMP"),
"Build Version:",
env!("VERGEN_BUILD_SEMVER"),
"Commit SHA:",
env!("VERGEN_GIT_SHA"),
"Commit Date:",
env!("VERGEN_GIT_COMMIT_TIMESTAMP"),
"Commit Branch:",
env!("VERGEN_GIT_BRANCH"),
"rustc Version:",
env!("VERGEN_RUSTC_SEMVER"),
"rustc Channel:",
env!("VERGEN_RUSTC_CHANNEL"),
"cargo Profile:",
env!("VERGEN_CARGO_PROFILE"),
"Network:",
DEFAULT_NETWORK
)
}
fn long_version_static() -> &'static str {
Box::leak(long_version().into_boxed_str())
}
#[derive(Parser)]
#[clap(author = "Nymtech", version, long_version = long_version_static(), about)]
pub(crate) struct Cli {
#[clap(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
pub(crate) enum Commands {
/// Initialise a Nym client. Do this first!
Init(init::Init),
/// Run the Nym client with provided configuration client optionally overriding set parameters
Run(run::Run),
/// Try to upgrade the client
Upgrade(upgrade::Upgrade),
}
// Configuration that can be overridden.
pub(crate) struct OverrideConfig {
validators: Option<String>,
port: Option<u16>,
fastmode: bool,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
enabled_credentials_mode: bool,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
eth_private_key: Option<String>,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
eth_endpoint: Option<String>,
}
pub(crate) async fn execute(args: &Cli) {
match &args.command {
Commands::Init(m) => init::execute(m).await,
Commands::Run(m) => run::execute(m).await,
Commands::Upgrade(m) => upgrade::execute(m),
}
}
fn parse_validators(raw: &str) -> Vec<Url> {
raw.split(',')
.map(|raw_validator| {
@@ -107,54 +32,41 @@ fn parse_validators(raw: &str) -> Vec<Url> {
.collect()
}
pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Config {
if let Some(raw_validators) = args.validators {
pub(crate) fn override_config(mut config: Config, matches: &ArgMatches<'_>) -> Config {
if let Some(raw_validators) = matches.value_of("validators") {
config
.get_base_mut()
.set_custom_validator_apis(parse_validators(&raw_validators));
.set_custom_validator_apis(parse_validators(raw_validators));
}
if let Some(port) = args.port {
config = config.with_port(port);
if let Some(port) = matches.value_of("port").map(|port| port.parse::<u16>()) {
if let Err(err) = port {
// if port was overridden, it must be parsable
panic!("Invalid port value provided - {:?}", err);
}
config = config.with_port(port.unwrap());
}
#[cfg(all(not(feature = "eth"), not(feature = "coconut")))]
{
#[cfg(not(feature = "coconut"))]
if let Some(eth_endpoint) = matches.value_of(ETH_ENDPOINT_ARG_NAME) {
config.get_base_mut().with_eth_endpoint(eth_endpoint);
} else if !cfg!(feature = "eth") {
config
.get_base_mut()
.with_eth_endpoint(DEFAULT_ETH_ENDPOINT.to_string());
.with_eth_endpoint(DEFAULT_ETH_ENDPOINT);
}
#[cfg(not(feature = "coconut"))]
if let Some(eth_private_key) = matches.value_of(ETH_PRIVATE_KEY_ARG_NAME) {
config.get_base_mut().with_eth_private_key(eth_private_key);
} else if !cfg!(feature = "eth") {
config
.get_base_mut()
.with_eth_private_key(DEFAULT_ETH_PRIVATE_KEY.to_string());
.with_eth_private_key(DEFAULT_ETH_PRIVATE_KEY);
}
#[cfg(all(feature = "eth", not(feature = "coconut")))]
{
if args.enabled_credentials_mode {
config.get_base_mut().with_disabled_credentials(false)
}
if let Some(eth_endpoint) = args.eth_endpoint {
config.get_base_mut().with_eth_endpoint(eth_endpoint);
}
if let Some(eth_private_key) = args.eth_private_key {
config.get_base_mut().with_eth_private_key(eth_private_key);
}
}
if args.fastmode {
config.get_base_mut().set_high_default_traffic_volume();
if matches.is_present(ENABLED_CREDENTIALS_MODE_ARG_NAME) {
config.get_base_mut().with_disabled_credentials(false)
}
config
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn verify_cli() {
Cli::command().debug_assert();
}
}
+69 -79
View File
@@ -1,80 +1,74 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{
client::{config::Config, NymClient},
commands::{override_config, OverrideConfig},
use crate::client::config::Config;
use crate::client::NymClient;
use crate::commands::override_config;
#[cfg(feature = "eth")]
#[cfg(not(feature = "coconut"))]
use crate::commands::{
ENABLED_CREDENTIALS_MODE_ARG_NAME, ETH_ENDPOINT_ARG_NAME, ETH_PRIVATE_KEY_ARG_NAME,
};
use clap::Args;
use clap::{App, Arg, ArgMatches};
use config::NymConfig;
use log::*;
use version_checker::is_minor_version_compatible;
#[derive(Args, Clone)]
pub(crate) struct Run {
/// Id of the nym-mixnet-client we want to run.
#[clap(long)]
id: String,
pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
let app = App::new("run")
.about("Run the Nym client with provided configuration client optionally overriding set parameters")
.arg(Arg::with_name("id")
.long("id")
.help("Id of the nym-mixnet-client we want to run.")
.takes_value(true)
.required(true)
)
// the rest of arguments are optional, they are used to override settings in config file
.arg(Arg::with_name("config")
.long("config")
.help("Custom path to the nym-mixnet-client configuration file")
.takes_value(true)
)
.arg(Arg::with_name("provider")
.long("provider")
.help("Address of the socks5 provider to send messages to.")
.takes_value(true)
)
.arg(Arg::with_name("gateway")
.long("gateway")
.help("Id of the gateway we want to connect to. If overridden, it is user's responsibility to ensure prior registration happened")
.takes_value(true)
)
.arg(Arg::with_name("validators")
.long("validators")
.help("Comma separated list of rest endpoints of the validators")
.takes_value(true),
)
.arg(Arg::with_name("port")
.short("p")
.long("port")
.help("Port for the socket to listen on")
.takes_value(true)
);
#[cfg(feature = "eth")]
#[cfg(not(feature = "coconut"))]
let app = app
.arg(
Arg::with_name(ENABLED_CREDENTIALS_MODE_ARG_NAME)
.long(ENABLED_CREDENTIALS_MODE_ARG_NAME)
.help("Set this client to work in a disabled credentials mode that would attempt to use gateway without bandwidth credential requirement. If this value is set, --eth_endpoint and --eth_private_key don't need to be set.")
.conflicts_with_all(&[ETH_ENDPOINT_ARG_NAME, ETH_PRIVATE_KEY_ARG_NAME])
)
.arg(Arg::with_name(ETH_ENDPOINT_ARG_NAME)
.long(ETH_ENDPOINT_ARG_NAME)
.help("URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20 tokens. If you don't want to set this value, use --testnet-mode instead")
.takes_value(true))
.arg(Arg::with_name(ETH_PRIVATE_KEY_ARG_NAME)
.long(ETH_PRIVATE_KEY_ARG_NAME)
.help("Ethereum private key used for obtaining bandwidth tokens from ERC20 tokens. If you don't want to set this value, use --testnet-mode instead")
.takes_value(true));
/// Custom path to the nym-mixnet-client configuration file
#[clap(long)]
config: Option<String>,
/// Address of the socks5 provider to send messages to.
#[clap(long)]
provider: Option<String>,
/// Id of the gateway we want to connect to. If overridden, it is user's responsibility to
/// ensure prior registration happened
#[clap(long)]
gateway: Option<String>,
/// Comma separated list of rest endpoints of the validators
#[clap(long)]
validators: Option<String>,
/// Port for the socket to listen on
#[clap(short, long)]
port: Option<u16>,
/// Set this client to work in a enabled credentials mode that would attempt to use gateway
/// with bandwidth credential requirement. If this value is set, --eth-endpoint and
/// --eth-private-key don't need to be set.
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[clap(long, conflicts_with_all = &["eth-endpoint", "eth-private-key"])]
enabled_credentials_mode: bool,
/// URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20
/// tokens. If you don't want to set this value, use --enabled-credentials-mode instead
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[clap(long)]
eth_endpoint: Option<String>,
/// Ethereum private key used for obtaining bandwidth tokens from ERC20 tokens. If you don't
/// want to set this value, use --enabled-credentials-mode instead
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[clap(long)]
eth_private_key: Option<String>,
}
impl From<Run> for OverrideConfig {
fn from(run_config: Run) -> Self {
OverrideConfig {
validators: run_config.validators,
port: run_config.port,
fastmode: false,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
enabled_credentials_mode: run_config.enabled_credentials_mode,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
eth_private_key: run_config.eth_private_key,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
eth_endpoint: run_config.eth_endpoint,
}
}
app
}
// this only checks compatibility between config the binary. It does not take into consideration
@@ -82,13 +76,8 @@ impl From<Run> for OverrideConfig {
fn version_check(cfg: &Config) -> bool {
let binary_version = env!("CARGO_PKG_VERSION");
let config_version = cfg.get_base().get_version();
if binary_version == config_version {
true
} else {
warn!(
"The mixnode binary has different version than what is specified in config file! {} and {}",
binary_version, config_version
);
if binary_version != config_version {
warn!("The mixnode binary has different version than what is specified in config file! {} and {}", binary_version, config_version);
if is_minor_version_compatible(binary_version, config_version) {
info!("but they are still semver compatible. However, consider running the `upgrade` command");
true
@@ -96,11 +85,13 @@ fn version_check(cfg: &Config) -> bool {
error!("and they are semver incompatible! - please run the `upgrade` command before attempting `run` again");
false
}
} else {
true
}
}
pub(crate) async fn execute(args: &Run) {
let id = &args.id;
pub async fn execute(matches: ArgMatches<'static>) {
let id = matches.value_of("id").unwrap();
let mut config = match Config::load_from_file(Some(id)) {
Ok(cfg) => cfg,
@@ -110,8 +101,7 @@ pub(crate) async fn execute(args: &Run) {
}
};
let override_config_fields = OverrideConfig::from(args.clone());
config = override_config(config, override_config_fields);
config = override_config(config, &matches);
if !version_check(&config) {
error!("failed the local version check");
+23 -20
View File
@@ -2,13 +2,13 @@
// SPDX-License-Identifier: Apache-2.0
use crate::client::config::{Config, MISSING_VALUE};
use config::{defaults::default_api_endpoints, NymConfig};
use clap::{App, Arg, ArgMatches};
use config::defaults::default_api_endpoints;
use config::NymConfig;
use std::fmt::Display;
use std::process;
use version_checker::Version;
use clap::Args;
use std::{fmt::Display, process};
#[allow(dead_code)]
fn fail_upgrade<D1: Display, D2: Display>(from_version: D1, to_version: D2) -> ! {
print_failed_upgrade(from_version, to_version);
@@ -49,11 +49,14 @@ fn unsupported_upgrade(current_version: &Version, config_version: &Version) -> !
process::exit(1)
}
#[derive(Args, Clone)]
pub(crate) struct Upgrade {
/// Id of the nym-client we want to upgrade
#[clap(long)]
id: String,
pub fn command_args<'a, 'b>() -> App<'a, 'b> {
App::new("upgrade").about("Try to upgrade the client").arg(
Arg::with_name("id")
.long("id")
.help("Id of the nym-client we want to upgrade")
.takes_value(true)
.required(true),
)
}
fn parse_config_version(config: &Config) -> Version {
@@ -92,7 +95,7 @@ fn parse_package_version() -> Version {
fn minor_0_12_upgrade(
mut config: Config,
_args: &Upgrade,
_matches: &ArgMatches<'_>,
config_version: &Version,
package_version: &Version,
) -> Config {
@@ -128,30 +131,30 @@ fn minor_0_12_upgrade(
config
}
fn do_upgrade(mut config: Config, args: &Upgrade, package_version: &Version) {
fn do_upgrade(mut config: Config, matches: &ArgMatches<'_>, package_version: Version) {
loop {
let config_version = parse_config_version(&config);
if &config_version == package_version {
if config_version == package_version {
println!("You're using the most recent version!");
return;
}
config = match config_version.major {
0 => match config_version.minor {
9 | 10 => outdated_upgrade(&config_version, package_version),
11 => minor_0_12_upgrade(config, args, &config_version, package_version),
_ => unsupported_upgrade(&config_version, package_version),
9 | 10 => outdated_upgrade(&config_version, &package_version),
11 => minor_0_12_upgrade(config, matches, &config_version, &package_version),
_ => unsupported_upgrade(&config_version, &package_version),
},
_ => unsupported_upgrade(&config_version, package_version),
_ => unsupported_upgrade(&config_version, &package_version),
}
}
}
pub(crate) fn execute(args: &Upgrade) {
pub fn execute(matches: &ArgMatches<'_>) {
let package_version = parse_package_version();
let id = &args.id;
let id = matches.value_of("id").unwrap();
let existing_config = Config::load_from_file(Some(id)).unwrap_or_else(|err| {
eprintln!("failed to load existing config file! - {:?}", err);
@@ -164,5 +167,5 @@ pub(crate) fn execute(args: &Upgrade) {
}
// here be upgrade path to 0.9.X and beyond based on version number from config
do_upgrade(existing_config, args, &package_version)
do_upgrade(existing_config, matches, package_version)
}
+60 -3
View File
@@ -1,7 +1,8 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use clap::{crate_version, Parser};
use clap::{crate_version, App, ArgMatches};
use network_defaults::DEFAULT_NETWORK;
pub mod client;
mod commands;
@@ -13,8 +14,30 @@ async fn main() {
setup_logging();
println!("{}", banner());
let args = commands::Cli::parse();
commands::execute(&args).await;
let arg_matches = App::new("Nym Socks5 Proxy")
.version(env!("CARGO_PKG_VERSION"))
.author("Nymtech")
.long_version(&*long_version())
.about("A Socks5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address")
.subcommand(commands::init::command_args())
.subcommand(commands::run::command_args())
.subcommand(commands::upgrade::command_args())
.get_matches();
execute(arg_matches).await;
}
async fn execute(matches: ArgMatches<'static>) {
match matches.subcommand() {
("init", Some(m)) => commands::init::execute(m.clone()).await,
("run", Some(m)) => commands::run::execute(m.clone()).await,
("upgrade", Some(m)) => commands::upgrade::execute(m),
_ => println!("{}", usage()),
}
}
fn usage() -> &'static str {
"usage: --help to see available options.\n\n"
}
fn banner() -> String {
@@ -34,6 +57,40 @@ fn banner() -> String {
)
}
fn long_version() -> String {
format!(
r#"
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
"#,
"Build Timestamp:",
env!("VERGEN_BUILD_TIMESTAMP"),
"Build Version:",
env!("VERGEN_BUILD_SEMVER"),
"Commit SHA:",
env!("VERGEN_GIT_SHA"),
"Commit Date:",
env!("VERGEN_GIT_COMMIT_TIMESTAMP"),
"Commit Branch:",
env!("VERGEN_GIT_BRANCH"),
"rustc Version:",
env!("VERGEN_RUSTC_SEMVER"),
"rustc Channel:",
env!("VERGEN_RUSTC_CHANNEL"),
"cargo Profile:",
env!("VERGEN_CARGO_PROFILE"),
"Network:",
DEFAULT_NETWORK
)
}
fn setup_logging() {
let mut log_builder = pretty_env_logger::formatted_timed_builder();
if let Ok(s) = ::std::env::var("RUST_LOG") {
@@ -470,6 +470,7 @@ mod tests {
use crate::support::tests;
use crate::support::tests::test_helpers;
use super::storage;
use super::*;
#[cfg(test)]
@@ -1061,13 +1062,17 @@ mod tests {
#[cfg(test)]
mod removing_mix_stake_delegation {
use crate::delegations::queries::query_mixnode_delegation;
use cosmwasm_std::coin;
use cosmwasm_std::testing::mock_env;
use cosmwasm_std::testing::mock_info;
use cosmwasm_std::Addr;
use cosmwasm_std::Uint128;
use crate::mixnodes::transactions::try_remove_mixnode;
use crate::support::tests;
use super::storage;
use super::*;
// TODO: Probably delete due to reconciliation logic
+2 -2
View File
@@ -1225,7 +1225,7 @@ pub mod tests {
)
.unwrap();
let _info = mock_info(rewarding_validator_address.as_ref(), &[]);
let info = mock_info(rewarding_validator_address.as_ref(), &[]);
env.block.height += 2 * constants::MINIMUM_BLOCK_AGE_FOR_REWARDING;
let mix_1 = mixnodes_storage::read_full_mixnode_bond(&deps.storage, &node_identity_1)
@@ -1531,7 +1531,7 @@ pub mod tests {
assert_eq!(mix_2_reward_result.lambda(), U128::from_num(0.0001f64));
assert_eq!(mix_2_reward_result.reward().int(), 974456u128);
let _mix_3_reward_result = mix_3.reward(&params3);
let mix_3_reward_result = mix_3.reward(&params3);
// assert_eq!(mix_3_reward_result.reward().int(), mix_1_reward_result.reward().int() + mix_2_reward_result.reward().int());
}
+5 -19
View File
@@ -9,15 +9,15 @@ mod commands;
mod config;
mod node;
static LONG_VERSION: OnceCell<String> = OnceCell::new();
static LONG_ABOUT: OnceCell<String> = OnceCell::new();
// Helper for passing LONG_ABOUT to clap
fn long_version_static() -> &'static str {
LONG_VERSION.get().expect("Failed to get long about text")
fn long_about() -> &'static str {
LONG_ABOUT.get().expect("Failed to get long about text")
}
#[derive(Parser)]
#[clap(author = "Nymtech", version, about, long_version = long_version_static())]
#[clap(author = "Nymtech", version, about, long_about = Some(long_about()))]
struct Cli {
#[clap(subcommand)]
command: commands::Commands,
@@ -28,7 +28,7 @@ async fn main() {
dotenv::dotenv().ok();
setup_logging();
println!("{}", banner());
LONG_VERSION
LONG_ABOUT
.set(long_version())
.expect("Failed to set long about text");
@@ -107,17 +107,3 @@ fn setup_logging() {
.filter_module("tokio_tungstenite", log::LevelFilter::Warn)
.init();
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn verify_cli() {
LONG_VERSION
.set(long_version())
.expect("Failed to set long about text");
Cli::command().debug_assert();
}
}
+5 -16
View File
@@ -13,16 +13,16 @@ mod config;
mod node;
lazy_static! {
pub static ref LONG_VERSION: String = long_version();
pub static ref LONG_ABOUT: String = long_version();
}
// Helper for passing LONG_VERSION to clap
fn long_version_static() -> &'static str {
&LONG_VERSION
// Helper for passing LONG_ABOUT to clap
fn long_about() -> &'static str {
&LONG_ABOUT
}
#[derive(Parser)]
#[clap(author = "Nymtech", version, about, long_version = long_version_static())]
#[clap(author = "Nymtech", version, about, long_about = Some(long_about()))]
struct Cli {
#[clap(subcommand)]
command: commands::Commands,
@@ -106,14 +106,3 @@ fn setup_logging() {
.filter_module("want", log::LevelFilter::Warn)
.init();
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn verify_cli() {
Cli::command().debug_assert();
}
}
+108 -4
View File
@@ -1,8 +1,112 @@
{
"extends": [
"@nymproject/eslint-config-react-typescript"
],
"env": {
"browser": true,
"es6": true,
"node": true,
"jest": true
},
"parserOptions": {
"project": "./tsconfig.eslint.json"
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 2019,
"sourceType": "module"
},
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"plugins": ["react", "react-hooks", "jsx-a11y", "prettier", "jest"],
"extends": [
"plugin:react/recommended",
"airbnb",
"prettier",
"plugin:jest/recommended",
"plugin:jest/style"
],
"rules": {
"jest/prefer-strict-equal": "error",
"jest/prefer-to-have-length": "warn",
"prettier/prettier": "error",
"import/prefer-default-export": "off",
"react/prop-types": "off",
"react/require-default-props": "off",
"react/jsx-filename-extension": "off",
"react/jsx-props-no-spreading": "off",
"import/no-extraneous-dependencies": [
"error",
{
"devDependencies": [
"**/*.test.[jt]s",
"**/*.spec.[jt]s",
"**/*.test.[jt]sx",
"**/*.spec.[jt]sx"
]
}
],
"import/extensions": [
"error",
"ignorePackages",
{
"ts": "never",
"tsx": "never",
"js": "never",
"jsx": "never"
}
]
},
"overrides": [
{
"files": "**/*.+(ts|tsx)",
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json"
},
"plugins": ["@typescript-eslint/eslint-plugin"],
"extends": [
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"rules": {
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-var-requires": "off",
"no-use-before-define": [0],
"@typescript-eslint/no-use-before-define": [1],
"import/no-unresolved": 0,
"import/no-extraneous-dependencies": [
"error",
{
"devDependencies": [
"**/*.test.ts",
"**/*.spec.ts",
"**/*.test.tsx",
"**/*.spec.tsx",
"**/*.stories.*",
"**/.storybook/**/*.*"
]
}
],
"quotes": "off",
"@typescript-eslint/quotes": [
2,
"single",
{
"avoidEscape": true
}
],
"@typescript-eslint/no-unused-vars": [2, { "argsIgnorePattern": "^_" }]
}
}
],
"settings": {
"import/resolver": {
"root-import": {
"rootPathPrefix": "@",
"rootPathSuffix": "src",
"extensions": [".js", ".ts", ".tsx", ".jsx", ".mdx"]
}
}
}
}
+10 -56
View File
@@ -1,60 +1,14 @@
/* eslint-disable no-param-reassign */
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
module.exports = {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'],
framework: '@storybook/react',
core: {
builder: 'webpack5',
},
typescript: { reactDocgen: false },
// webpackFinal: async (config, { configType }) => {
// // `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION'
// // You can change the configuration based on that.
// // 'PRODUCTION' is used when building the static version of storybook.
webpackFinal: async (config) => {
config.module.rules.forEach((rule) => {
// look for SVG import rule and replace
// NOTE: the rule before modification is /\.(svg|ico|jpg|jpeg|png|apng|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/
if (rule.test?.toString().includes('svg')) {
rule.test = /\.(ico|jpg|jpeg|png|apng|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/;
}
});
// handle asset loading with this
config.module.rules.unshift({
test: /\.svg(\?.*)?$/i,
issuer: /\.[jt]sx?$/,
use: ['@svgr/webpack'],
});
config.resolve.extensions = ['.tsx', '.ts', '.js'];
config.resolve.plugins = [new TsconfigPathsPlugin()];
config.plugins.push(
new ForkTsCheckerWebpackPlugin({
typescript: {
mode: 'write-references',
diagnosticOptions: {
semantic: true,
syntactic: true,
},
},
}),
);
if (!config.resolve.alias) {
config.resolve.alias = {};
}
config.resolve.alias['@tauri-apps/api'] = `${__dirname}/mocks/tauri`;
// Return the altered config
return config;
},
stories: [
"../src/**/*.stories.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)"
],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials"
],
framework: "@storybook/react",
features: {
emotionAlias: false,
},
};
}
@@ -1,8 +0,0 @@
/**
* This is a mock for Tauri's API package (@tauri-apps/api/app), to prevent stories from being excluded, because they either use
* or import dependencies that use Tauri.
*/
module.exports = {
getVersion: () => undefined,
};
@@ -1,8 +0,0 @@
/**
* This is a mock for Tauri's API package (@tauri-apps/api/clipboard), to prevent stories from being excluded, because they either use
* or import dependencies that use Tauri.
*/
module.exports = {
writeText: async () => undefined,
}
@@ -1,8 +0,0 @@
/**
* This is a mock for Tauri's API package (@tauri-apps/api/clipboard), to prevent stories from being excluded, because they either use
* or import dependencies that use Tauri.
*/
module.exports = {
listen: async () => undefined,
}
@@ -1,14 +0,0 @@
/**
* This is a mock for Tauri's API package (@tauri-apps/api), to prevent stories from being excluded, because they either use
* or import dependencies that use Tauri.
*/
module.exports = {
invoke: (operation, args) => {
console.error(
`Tauri cannot be used in Storybook. The operation requested was "${operation}". You can add mock responses to "nym_connect/.storybook/mocks/tauri.js" if you need. The default response is "void".`,
);
return new Promise((resolve, reject) => {
reject(new Error(`Tauri operation ${operation} not available in storybook.`));
});
},
};
@@ -1,10 +0,0 @@
/**
* This is a mock for Tauri's API package (@tauri-apps/api/window), to prevent stories from being excluded, because they either use
* or import dependencies that use Tauri.
*/
module.exports = {
appWindow: {
maximize: () => undefined,
}
}
+882 -948
View File
File diff suppressed because it is too large Load Diff
+27 -45
View File
@@ -4,36 +4,33 @@
"main": "index.js",
"license": "MIT",
"scripts": {
"prewebpack:dev": "yarn --cwd .. build",
"webpack:dev": "yarn webpack serve --config webpack.dev.js",
"webpack:prod": "yarn webpack --progress --config webpack.prod.js",
"tauri:dev": "RUST_DEBUG=1 yarn tauri dev",
"tauri:build": "yarn tauri build",
"dev": "run-p webpack:dev tauri:dev",
"prebuild": "yarn --cwd .. build",
"build": "run-s webpack:prod tauri:build",
"storybook": "start-storybook -p 6006",
"prestorybook:build": "yarn --cwd .. build",
"storybook:build": "build-storybook",
"tsc": "tsc --noEmit true",
"tsc:watch": "tsc --noEmit true --watch",
"lint": "eslint src",
"lint:fix": "eslint src --fix",
"test": "jest",
"test:watch": "jest --watch"
},
"dependencies": {
"@babel/preset-typescript": "^7.15.0",
"@emotion/react": "^11.7.0",
"@emotion/styled": "^11.6.0",
"@hookform/resolvers": "^2.8.0",
"@mui/icons-material": "^5.2.0",
"@mui/material": "^5.2.2",
"@mui/styles": "^5.2.2",
"@nymproject/react": "^1.0.0",
"@tauri-apps/api": "^1.0.2",
"@types/react-dom": "^17.0.9",
"bs58": "^4.0.1",
"clsx": "^1.1.1",
"luxon": "^2.3.0",
"pretty-bytes": "^6.0.0",
"qrcode.react": "^1.0.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-error-boundary": "^3.1.3",
@@ -47,63 +44,48 @@
"@babel/plugin-transform-async-to-generator": "^7.14.5",
"@babel/preset-env": "^7.15.0",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.15.0",
"@nymproject/eslint-config-react-typescript": "^1.0.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
"@storybook/react": "^6.5.8",
"@storybook/addon-actions": "^6.4.18",
"@storybook/addon-essentials": "^6.4.18",
"@storybook/addon-links": "^6.4.18",
"@storybook/react": "^6.4.18",
"@svgr/webpack": "^6.1.1",
"@tauri-apps/cli": "^1.0.2",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@types/jest": "^27.0.1",
"@types/luxon": "^2.3.2",
"@types/node": "^16.7.13",
"@types/react": "^17.0.34",
"@types/react-dom": "^17.0.9",
"@tauri-apps/api": "^1.0.0-rc.4",
"@tauri-apps/cli": "^1.0.0-rc.9",
"@types/bs58": "^4.0.1",
"@types/luxon": "^2.0.9",
"@types/qrcode.react": "^1.0.2",
"@types/react-router-dom": "^5.1.8",
"@types/semver": "^7.3.8",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.13.0",
"@typescript-eslint/parser": "^5.13.0",
"@typescript-eslint/eslint-plugin": "4.31.0",
"@typescript-eslint/parser": "4.31.0",
"babel-loader": "^8.2.2",
"babel-plugin-root-import": "^6.6.0",
"clean-webpack-plugin": "^4.0.0",
"css-loader": "^6.2.0",
"css-minimizer-webpack-plugin": "^3.0.2",
"dotenv-webpack": "^7.0.3",
"eslint": "^8.10.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^16.1.0",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-root-import": "^1.0.4",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jest": "^26.1.1",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.29.2",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-storybook": "^0.5.12",
"eslint": "7.32.0",
"eslint-config-airbnb": "18.2.1",
"eslint-config-prettier": "8.3.0",
"eslint-import-resolver-root-import": "1.0.4",
"eslint-plugin-import": "2.24.2",
"eslint-plugin-jest": "^24.4.0",
"eslint-plugin-jsx-a11y": "6.4.1",
"eslint-plugin-prettier": "4.0.0",
"eslint-plugin-react": "7.25.1",
"eslint-plugin-react-hooks": "4.2.0",
"favicons": "^6.2.2",
"favicons-webpack-plugin": "^5.0.2",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^7.2.1",
"html-webpack-plugin": "^5.3.2",
"jest": "^27.1.0",
"mini-css-extract-plugin": "^2.2.2",
"npm-run-all": "^4.1.5",
"prettier": "2.3.2",
"react-refresh": "^0.10.0",
"react-refresh-typescript": "^2.0.2",
"style-loader": "^3.2.1",
"thread-loader": "^3.0.4",
"ts-jest": "^27.0.5",
"ts-loader": "^9.2.5",
"tsconfig-paths-webpack-plugin": "^3.5.2",
"typescript": "^4.6.2",
"typescript": "^4.4.2",
"url-loader": "^4.1.1",
"webpack": "^5.64.3",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^4.5.0",
"webpack-favicons": "^1.3.8",
"webpack-merge": "^5.8.0"
}
}
+9 -10
View File
@@ -13,30 +13,29 @@ rust-version = "1.58"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "^1.0.2", features = [] }
tauri-build = { version = "=1.0.0-rc.7", features = [] }
tauri-codegen = "^1.0.2"
tauri-macros = "^1.0.2"
tauri-codegen = "=1.0.0-rc.5"
tauri-macros = "=1.0.0-rc.5"
[dependencies]
bip39 = "1.0"
dirs = "4.0"
eyre = "0.6.5"
fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs", branch = "release"}
futures = "0.3"
log = "0.4"
pretty_env_logger = "0.4.0"
rand = "0.8"
reqwest = { version = "0.11", features = ["json"] }
rand = "0.7"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tap = "1.0.1"
tauri = { version = "^1.0.2", features = ["clipboard-write-text", "shell-open", "system-tray", "updater"] }
tauri = { version = "=1.0.0-rc.8", features = ["ayatana-tray", "shell-open", "system-tray"] }
tendermint-rpc = "0.23.0"
thiserror = "1.0"
tokio = { version = "1.19.1", features = ["sync", "time"] }
url = "2.2"
log = "0.4"
pretty_env_logger = "0.4.0"
fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs", branch = "release"}
reqwest = { version = "0.11", features = ["json"] }
client-core = { path = "../../clients/client-core" }
config = { path = "../../common/config" }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 47 KiB

+1 -1
View File
@@ -1,3 +1,3 @@
fn main() {
tauri_build::build();
tauri_build::build()
}
+32 -32
View File
@@ -1,41 +1,46 @@
use std::path::PathBuf;
use client_core::config::GatewayEndpoint;
use log::info;
use std::sync::Arc;
use tap::TapFallible;
use tokio::sync::RwLock;
use client_core::config::Config as BaseConfig;
use config::NymConfig;
use nym_socks5::client::config::Config as Socks5Config;
use crate::{
error::{BackendError, Result},
state::State,
};
use crate::{error::BackendError, state::State};
static SOCKS5_CONFIG_ID: &str = "nym-connect";
pub static SOCKS5_CONFIG_ID: &str = "nym-connect";
const DEFAULT_ETH_ENDPOINT: &str = "https://rinkeby.infura.io/v3/00000000000000000000000000000000";
const DEFAULT_ETH_PRIVATE_KEY: &str =
"0000000000000000000000000000000000000000000000000000000000000001";
pub fn socks5_config_id_appended_with(gateway_id: &str) -> Result<String> {
pub fn append_config_id(gateway_id: &str) -> String {
use std::fmt::Write as _;
let mut id = SOCKS5_CONFIG_ID.to_string();
write!(id, "-{}", gateway_id)?;
Ok(id)
write!(id, "-{}", gateway_id).expect("Failed to set config id");
id
}
#[tauri::command]
pub async fn get_config_id(state: tauri::State<'_, Arc<RwLock<State>>>) -> Result<String> {
state.read().await.get_config_id()
pub async fn get_config_id(
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<String, BackendError> {
let guard = state.read().await;
// TODO: return error instead
let gateway_id = guard
.get_gateway()
.as_ref()
.expect("The config id can not be determined before setting the gateway");
Ok(append_config_id(gateway_id))
}
#[tauri::command]
pub async fn get_config_file_location(
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<String> {
) -> Result<String, BackendError> {
let id = get_config_id(state).await?;
Ok(Config::config_file_location(&id)
.to_string_lossy()
@@ -71,26 +76,21 @@ impl Config {
self.socks5.get_base_mut()
}
pub async fn init(service_provider: &str, chosen_gateway_id: &str) -> Result<()> {
log::info!("Initialising...");
pub async fn init(service_provider: &str, chosen_gateway_id: &str) -> Result<(), BackendError> {
info!("Initialising...");
let service_provider = service_provider.to_owned();
let chosen_gateway_id = chosen_gateway_id.to_owned();
// The client initialization was originally not written for this use case, so there are
// lots of ways it can panic. Until we have proper error handling in the init code for the
// clients we'll catch any panics here by spawning a new runtime in a separate thread.
std::thread::spawn(move || {
tokio::runtime::Runtime::new()
.expect("Failed to create tokio runtime")
.block_on(
async move { init_socks5_config(service_provider, chosen_gateway_id).await },
)
// clients we'll catch any panics here.
std::panic::catch_unwind(move || {
futures::executor::block_on(init_socks5(service_provider, chosen_gateway_id));
})
.join()
.map_err(|_| BackendError::InitializationPanic)??;
.map_err(|_| BackendError::InitializationPanic)?;
log::info!("Configuration saved 🚀");
info!("Configuration saved 🚀");
Ok(())
}
@@ -99,11 +99,11 @@ impl Config {
}
}
pub async fn init_socks5_config(provider_address: String, chosen_gateway_id: String) -> Result<()> {
log::trace!("Initialising client...");
pub async fn init_socks5(provider_address: String, chosen_gateway_id: String) {
log::info!("Initialising client...");
// Append the gateway id to the name id that we store the config under
let id = socks5_config_id_appended_with(&chosen_gateway_id)?;
let id = append_config_id(&chosen_gateway_id);
log::debug!(
"Attempting to use config file location: {}",
@@ -145,9 +145,10 @@ pub async fn init_socks5_config(provider_address: String, chosen_gateway_id: Str
config.get_base_mut().with_gateway_endpoint(gateway);
let config_save_location = config.get_socks5().get_config_file_save_location();
config.get_socks5().save_to_file(None).tap_err(|_| {
log::warn!("Failed to save the config file");
})?;
config
.get_socks5()
.save_to_file(None)
.expect("Failed to save the config file");
log::info!("Saved configuration file to {:?}", config_save_location);
log::info!("Gateway id: {}", config.get_base().get_gateway_id());
@@ -165,10 +166,9 @@ pub async fn init_socks5_config(provider_address: String, chosen_gateway_id: Str
"Service provider port: {}",
config.get_socks5().get_listening_port()
);
log::info!("Client configuration completed.");
info!("Client configuration completed.");
client_core::init::show_address(config.get_base());
Ok(())
}
// TODO: deduplicate with same functions in other client
+6 -40
View File
@@ -4,64 +4,30 @@ use thiserror::Error;
#[allow(unused)]
#[derive(Error, Debug)]
pub enum BackendError {
#[error("{source}")]
ReqwestError {
#[from]
source: reqwest::Error,
},
#[error("I/O error: {source}")]
IoError {
#[from]
source: std::io::Error,
},
#[error("String formatting error: {source}")]
FmtError {
#[from]
source: std::fmt::Error,
},
#[error("Tauri error: {source}")]
TauriError {
#[from]
source: tauri::Error,
},
#[error("{source}")]
SerdeJsonError {
#[from]
source: serde_json::Error,
},
#[error("State error")]
StateError,
#[error("Could not connect")]
CouldNotConnect,
#[error("Could not disconnect")]
CouldNotDisconnect,
#[error("Could not send disconnect signal to the SOCKS5 client")]
CoundNotSendDisconnectSignal,
#[error("No service provider set")]
NoServiceProviderSet,
#[error("No gateway provider set")]
NoGatewaySet,
#[error("{source}")]
ReqwestError {
#[from]
source: reqwest::Error,
},
#[error("Initialization failed with a panic")]
InitializationPanic,
#[error("Could not get config id before gateway is set")]
CouldNotGetIdWithoutGateway,
#[error("Could initialize without gateway set")]
CouldNotInitWithoutGateway,
#[error("Could initialize without service provider set")]
CouldNotInitWithoutServiceProvider,
#[error("Could not get file name")]
CouldNotGetFilename,
}
impl Serialize for BackendError {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(self)
}
}
// Local crate level Result alias
pub(crate) type Result<T, E = BackendError> = std::result::Result<T, E>;
+1 -2
View File
@@ -43,9 +43,8 @@ fn main() {
crate::operations::connection::connect::set_service_provider,
crate::operations::connection::connect::start_connecting,
crate::operations::connection::disconnect::start_disconnecting,
crate::operations::directory::get_services,
crate::operations::export::export_keys,
crate::operations::window::hide_window,
crate::operations::directory::get_services,
])
.menu(Menu::new().add_default_app_submenu_if_macos())
.system_tray(create_tray_menu())
-13
View File
@@ -1,5 +1,3 @@
use core::fmt;
use serde::{Deserialize, Serialize};
#[cfg_attr(test, derive(ts_rs::TS))]
@@ -25,17 +23,6 @@ pub enum ConnectionStatusKind {
Connecting,
}
impl fmt::Display for ConnectionStatusKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
ConnectionStatusKind::Disconnected => write!(f, "Disconnected"),
ConnectionStatusKind::Disconnecting => write!(f, "Disconnecting"),
ConnectionStatusKind::Connected => write!(f, "Connected"),
ConnectionStatusKind::Connecting => write!(f, "Connecting"),
}
}
}
pub const APP_EVENT_CONNECTION_STATUS_CHANGED: &str = "app:connection-status-changed";
#[cfg_attr(test, derive(ts_rs::TS))]
@@ -1,8 +1,4 @@
use crate::{
error::{BackendError, Result},
models::ConnectResult,
tasks, State,
};
use crate::{error::BackendError, models::ConnectResult, tasks::start_disconnect_listener, State};
use std::sync::Arc;
use tokio::sync::RwLock;
@@ -10,23 +6,30 @@ use tokio::sync::RwLock;
pub async fn start_connecting(
state: tauri::State<'_, Arc<RwLock<State>>>,
window: tauri::Window<tauri::Wry>,
) -> Result<ConnectResult> {
) -> Result<ConnectResult, BackendError> {
let status_receiver = {
let mut state_w = state.write().await;
state_w.start_connecting(&window).await?
let mut guard = state.write().await;
log::trace!("Start connecting with:");
log::trace!(" service_provider: {:?}", guard.get_service_provider());
log::trace!(" gateway: {:?}", guard.get_gateway());
guard.start_connecting(&window).await?
};
// Setup task for checking status
let state = state.inner().clone();
tasks::start_disconnect_listener(state, window, status_receiver);
start_disconnect_listener(state, window, status_receiver);
Ok(ConnectResult {
address: "PLACEHOLDER".to_string(),
// WIP(JON): fixme
address: "Test".to_string(),
})
}
#[tauri::command]
pub async fn get_service_provider(state: tauri::State<'_, Arc<RwLock<State>>>) -> Result<String> {
pub async fn get_service_provider(
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<String, BackendError> {
let guard = state.read().await;
guard
.get_service_provider()
@@ -38,7 +41,7 @@ pub async fn get_service_provider(state: tauri::State<'_, Arc<RwLock<State>>>) -
pub async fn set_service_provider(
service_provider: String,
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<()> {
) -> Result<(), BackendError> {
log::trace!("Setting service_provider: {service_provider}");
let mut guard = state.write().await;
guard.set_service_provider(service_provider);
@@ -46,7 +49,9 @@ pub async fn set_service_provider(
}
#[tauri::command]
pub async fn get_gateway(state: tauri::State<'_, Arc<RwLock<State>>>) -> Result<String> {
pub async fn get_gateway(
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<String, BackendError> {
let guard = state.read().await;
guard
.get_gateway()
@@ -58,7 +63,7 @@ pub async fn get_gateway(state: tauri::State<'_, Arc<RwLock<State>>>) -> Result<
pub async fn set_gateway(
gateway: String,
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<()> {
) -> Result<(), BackendError> {
log::trace!("Setting gateway: {gateway}");
let mut guard = state.write().await;
guard.set_gateway(gateway);
@@ -1,4 +1,4 @@
use crate::error::Result;
use crate::error::BackendError;
use crate::models::ConnectResult;
use crate::State;
use std::sync::Arc;
@@ -8,12 +8,13 @@ use tokio::sync::RwLock;
pub async fn start_disconnecting(
state: tauri::State<'_, Arc<RwLock<State>>>,
window: tauri::Window<tauri::Wry>,
) -> Result<ConnectResult> {
) -> Result<ConnectResult, BackendError> {
let mut guard = state.write().await;
guard.start_disconnecting(&window).await?;
guard.start_disconnecting(&window).await;
Ok(ConnectResult {
address: "PLACEHOLDER".to_string(),
// WIP(JON): fixme
address: "Test".to_string(),
})
}
@@ -1,11 +1,11 @@
use crate::error::Result;
use crate::error::BackendError;
use crate::models::DirectoryService;
static SERVICE_PROVIDER_WELLKNOWN_URL: &str =
"https://nymtech.net/.wellknown/connect/service-providers.json";
#[tauri::command]
pub async fn get_services() -> Result<Vec<DirectoryService>> {
pub async fn get_services() -> Result<Vec<DirectoryService>, BackendError> {
let res = reqwest::get(SERVICE_PROVIDER_WELLKNOWN_URL)
.await?
.json::<Vec<DirectoryService>>()
@@ -1,73 +0,0 @@
use std::{ffi::OsStr, fs, sync::Arc};
use tokio::sync::RwLock;
use crate::{
error::{BackendError, Result},
state::State,
};
/// Export the gateway keys as a JSON string blob
#[tauri::command]
pub async fn export_keys(state: tauri::State<'_, Arc<RwLock<State>>>) -> Result<String> {
let config = {
let state = state.read().await;
state.load_socks5_config()?
};
// Get key paths
let ack_key_file = config.get_base().get_ack_key_file();
let gateway_shared_key_file = config.get_base().get_gateway_shared_key_file();
let pub_id_key_file = config.get_base().get_public_identity_key_file();
let priv_id_key_file = config.get_base().get_private_identity_key_file();
let pub_enc_key_file = config.get_base().get_public_encryption_key_file();
let priv_enc_key_file = config.get_base().get_private_encryption_key_file();
// Read file contents
let ack_key = fs::read_to_string(ack_key_file.clone())?;
let gateway_shared_key = fs::read_to_string(gateway_shared_key_file.clone())?;
let pub_id_key = fs::read_to_string(pub_id_key_file.clone())?;
let priv_id_key = fs::read_to_string(priv_id_key_file.clone())?;
let pub_enc_key = fs::read_to_string(pub_enc_key_file.clone())?;
let priv_enc_key = fs::read_to_string(priv_enc_key_file.clone())?;
let ack_key_file = ack_key_file
.file_name()
.map(OsStr::to_string_lossy)
.ok_or(BackendError::CouldNotGetFilename)?;
let gateway_shared_key_file = gateway_shared_key_file
.file_name()
.map(OsStr::to_string_lossy)
.ok_or(BackendError::CouldNotGetFilename)?;
let pub_id_key_file = pub_id_key_file
.file_name()
.map(OsStr::to_string_lossy)
.ok_or(BackendError::CouldNotGetFilename)?;
let priv_id_key_file = priv_id_key_file
.file_name()
.map(OsStr::to_string_lossy)
.ok_or(BackendError::CouldNotGetFilename)?;
let pub_enc_key_file = pub_enc_key_file
.file_name()
.map(OsStr::to_string_lossy)
.ok_or(BackendError::CouldNotGetFilename)?;
let priv_enc_key_file = priv_enc_key_file
.file_name()
.map(OsStr::to_string_lossy)
.ok_or(BackendError::CouldNotGetFilename)?;
// Format and return as json
let json = serde_json::json!({
ack_key_file: ack_key,
gateway_shared_key_file: gateway_shared_key,
pub_id_key_file: pub_id_key,
priv_id_key_file: priv_id_key,
pub_enc_key_file: pub_enc_key,
priv_enc_key_file: priv_enc_key,
});
Ok(serde_json::to_string_pretty(&json)?)
}
@@ -1,4 +1,3 @@
pub mod connection;
pub mod directory;
pub mod export;
pub mod window;
+37 -80
View File
@@ -1,35 +1,24 @@
use std::time::Duration;
use ::config::NymConfig;
use futures::SinkExt;
use tap::TapFallible;
use tauri::Manager;
use nym_socks5::client::{
config::Config as Socks5Config, Socks5ControlMessage, Socks5ControlMessageSender,
};
use nym_socks5::client::{Socks5ControlMessage, Socks5ControlMessageSender};
use crate::{
config::{self, socks5_config_id_appended_with},
error::{BackendError, Result},
config::append_config_id,
error::BackendError,
models::{
AppEventConnectionStatusChangedPayload, ConnectionStatusKind,
APP_EVENT_CONNECTION_STATUS_CHANGED,
},
tasks::{self, StatusReceiver},
tasks::{start_nym_socks5_client, StatusReceiver},
};
pub struct State {
/// The current connection status
status: ConnectionStatusKind,
/// The service provider
service_provider: Option<String>,
/// The gateway used. Note that this is also used to create the configuration id
gateway: Option<String>,
/// Channel that is used to send command messages to the SOCKS5 client, such as to disconnect
socks5_client_sender: Option<Socks5ControlMessageSender>,
}
@@ -49,15 +38,13 @@ impl State {
}
fn set_state(&mut self, status: ConnectionStatusKind, window: &tauri::Window<tauri::Wry>) {
log::info!("{status}");
self.status = status.clone();
window
.emit_all(
APP_EVENT_CONNECTION_STATUS_CHANGED,
AppEventConnectionStatusChangedPayload { status },
)
.tap_err(|err| log::warn!("{err}"))
.ok();
.unwrap();
}
pub fn get_service_provider(&self) -> &Option<String> {
@@ -76,28 +63,25 @@ impl State {
self.gateway = Some(gateway);
}
/// The effective config id is the static config id appended with the id of the gateway
pub fn get_config_id(&self) -> Result<String> {
self.get_gateway()
pub async fn init_config(&self) -> Result<(), BackendError> {
let service_provider = self
.service_provider
.as_ref()
.ok_or(BackendError::CouldNotGetIdWithoutGateway)
.and_then(|gateway_id| socks5_config_id_appended_with(gateway_id))
.expect("Attempting to init without service provider");
let gateway = self
.gateway
.as_ref()
.expect("Attempting to init without gateway");
crate::config::Config::init(service_provider, gateway).await
}
pub fn load_socks5_config(&self) -> Result<Socks5Config> {
let id = self.get_config_id()?;
let config = Socks5Config::load_from_file(Some(&id))
.tap_err(|_| log::warn!("Failed to load configuration file"))?;
Ok(config)
}
/// Start connecting by first creating a config file, followed by starting a thread running the
/// SOCKS5 client.
pub async fn start_connecting(
&mut self,
window: &tauri::Window<tauri::Wry>,
) -> Result<StatusReceiver> {
) -> Result<StatusReceiver, BackendError> {
log::info!("Connecting");
self.set_state(ConnectionStatusKind::Connecting, window);
self.status = ConnectionStatusKind::Connecting;
// Setup configuration by writing to file
if let Err(err) = self.init_config().await {
@@ -107,67 +91,40 @@ impl State {
// registered.
tokio::time::sleep(Duration::from_secs(1)).await;
self.set_state(ConnectionStatusKind::Disconnected, window);
self.status = ConnectionStatusKind::Disconnected;
return Err(err);
}
// Kick off the main task and get the channel for controlling it
let status_receiver = self.start_nym_socks5_client().await?;
let id = append_config_id(
self.gateway
.as_ref()
.expect("Attempting to start without gateway"),
);
let (sender, used_gateway, status_receiver) = start_nym_socks5_client(&id);
self.gateway = Some(used_gateway.gateway_id);
self.socks5_client_sender = Some(sender);
self.status = ConnectionStatusKind::Connected;
self.set_state(ConnectionStatusKind::Connected, window);
Ok(status_receiver)
}
/// Create a configuration file
async fn init_config(&self) -> Result<()> {
let service_provider = self
.get_service_provider()
.as_ref()
.ok_or(BackendError::CouldNotInitWithoutServiceProvider)?;
let gateway = self
.get_gateway()
.as_ref()
.ok_or(BackendError::CouldNotInitWithoutGateway)?;
log::trace!(" service_provider: {:?}", service_provider);
log::trace!(" gateway: {:?}", gateway);
config::Config::init(service_provider, gateway).await
}
/// Spawn a new thread running the SOCKS5 client
async fn start_nym_socks5_client(&mut self) -> Result<StatusReceiver> {
let id = self.get_config_id()?;
let (control_tx, status_rx, used_gateway) = tasks::start_nym_socks5_client(&id)?;
self.socks5_client_sender = Some(control_tx);
self.gateway = Some(used_gateway.gateway_id);
Ok(status_rx)
}
/// Disconnect by sending a message to the SOCKS5 client thread. Once it has finished and is
/// disconnected, the disconnect handler will mark it as disconnected.
pub async fn start_disconnecting(&mut self, window: &tauri::Window<tauri::Wry>) -> Result<()> {
pub async fn start_disconnecting(&mut self, window: &tauri::Window<tauri::Wry>) {
log::info!("Disconnecting");
self.set_state(ConnectionStatusKind::Disconnecting, window);
self.status = ConnectionStatusKind::Disconnecting;
// Send shutdown message
match self.socks5_client_sender {
Some(ref mut sender) => sender
.send(Socks5ControlMessage::Stop)
.await
.map_err(|err| {
log::warn!("Failed trying to send disconnect signal: {err}");
BackendError::CoundNotSendDisconnectSignal
}),
None => {
log::warn!(
"Trying to disconnect without being able to talk to the SOCKS5 client, \
is it running?"
);
Err(BackendError::CoundNotSendDisconnectSignal)
}
if let Some(ref mut sender) = self.socks5_client_sender {
sender.send(Socks5ControlMessage::Stop).await.unwrap();
}
}
/// Once the SOCKS5 client has stopped, this should be called by the disconnect handler to mark
/// the state as disconnected.
pub fn mark_disconnected(&mut self, window: &tauri::Window<tauri::Wry>) {
pub async fn mark_disconnected(&mut self, window: &tauri::Window<tauri::Wry>) {
log::info!("Disconnected");
self.status = ConnectionStatusKind::Disconnected;
self.set_state(ConnectionStatusKind::Disconnected, window);
}
}
+15 -24
View File
@@ -1,36 +1,34 @@
use client_core::config::GatewayEndpoint;
use futures::channel::mpsc;
use log::info;
use std::sync::Arc;
use tap::TapFallible;
use tokio::sync::RwLock;
use config::NymConfig;
#[cfg(not(feature = "coconut"))]
use nym_socks5::client::NymClient as Socks5NymClient;
use nym_socks5::client::{config::Config as Socks5Config, Socks5ControlMessageSender};
use nym_socks5::client::Socks5ControlMessageSender;
use crate::{error::Result, state::State};
use crate::state::State;
pub type StatusReceiver = futures::channel::oneshot::Receiver<Socks5StatusMessage>;
/// Status messages sent by the SOCKS5 client task to the main tauri task.
#[derive(Debug)]
pub enum Socks5StatusMessage {
/// The SOCKS5 task successfully stopped
Stopped,
}
/// The main SOCKS5 client task. It loads the configuration from file determined by the `id`.
pub fn start_nym_socks5_client(
id: &str,
) -> Result<(Socks5ControlMessageSender, StatusReceiver, GatewayEndpoint)> {
log::info!("Loading config from file: {id}");
let config = Socks5Config::load_from_file(Some(id))
.tap_err(|_| log::warn!("Failed to load configuration file"))?;
) -> (Socks5ControlMessageSender, GatewayEndpoint, StatusReceiver) {
info!("Loading config from file: {id}");
// TODO: handle this gracefully!
let config = nym_socks5::client::config::Config::load_from_file(Some(id)).unwrap();
let used_gateway = config.get_base().get_gateway_endpoint().clone();
let mut socks5_client = Socks5NymClient::new(config);
log::info!("Starting socks5 client");
info!("Starting socks5 client");
// Channel to send control messages to the socks5 client
let (socks5_ctrl_tx, socks5_ctrl_rx) = mpsc::unbounded();
@@ -40,24 +38,19 @@ pub fn start_nym_socks5_client(
// Spawn a separate runtime for the socks5 client so we can forcefully terminate.
// Once we can gracefully shutdown the socks5 client we can get rid of this.
// The status channel is used to both get the state of the task, and if it's closed, to check
// for panic.
std::thread::spawn(|| {
tokio::runtime::Runtime::new()
.expect("Failed to create runtime for SOCKS5 client")
.block_on(async move { socks5_client.run_and_listen(socks5_ctrl_rx).await });
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async move {
socks5_client.run_and_listen(socks5_ctrl_rx).await;
});
log::info!("SOCKS5 task finished");
socks5_status_tx
.send(Socks5StatusMessage::Stopped)
.expect("Failed to send status message back to main task");
socks5_status_tx.send(Socks5StatusMessage::Stopped).unwrap();
});
Ok((socks5_ctrl_tx, socks5_status_rx, used_gateway))
(socks5_ctrl_tx, used_gateway, socks5_status_rx)
}
/// The disconnect listener listens to the channel setup between the socks5 proxy task and the main
/// tauri task. Primarily it listens for shutdown messages, and updates the state accordingly.
pub fn start_disconnect_listener(
state: Arc<RwLock<State>>,
window: tauri::Window<tauri::Wry>,
@@ -71,12 +64,10 @@ pub fn start_disconnect_listener(
}
Err(_) => {
log::info!("SOCKS5 task appears to have stopped abruptly");
// TODO: we should probably generate some events here, or otherwise signal to the
// frontend.
}
}
let mut state_w = state.write().await;
state_w.mark_disconnected(&window);
state_w.mark_disconnected(&window).await;
});
}
+1 -9
View File
@@ -48,19 +48,11 @@
}
},
"updater": {
"active": true,
"endpoints": [
"https://nymtech.net/.wellknown/connect/updater.json"
],
"dialog": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IENCNzQ2M0E5N0VFODE2NApSV1JrZ2U2WE9rYTNETTg1OTBKdE5uWUEra0hML2syOVUvQ2lxZmFZRzZ1T3NWbGM0eVRzUTVhVwo="
"active": false
},
"allowlist": {
"shell": {
"open": true
},
"clipboard": {
"writeText": true
}
},
"windows": [
@@ -1,6 +1,5 @@
import React from 'react';
import { Box } from '@mui/material';
import { NymWordmark } from '@nymproject/react/logo/NymWordmark';
export const AppWindowFrame: React.FC = ({ children }) => (
<Box
@@ -14,7 +13,11 @@ export const AppWindowFrame: React.FC = ({ children }) => (
}}
>
<Box display="flex" justifyContent="space-between" alignItems="center">
<NymWordmark width={22} />
<svg width="22" height="6" viewBox="0 0 210 56" fill="#F2F2F2" xmlns="http://www.w3.org/2000/svg">
<path d="M45.8829 0.142822H45.7169V0.28114V48.637L25.3289 0.225818L25.3012 0.142822H25.1905H13.6272H0.652966H0.514648V0.28114V55.7189V55.8572H0.652966H13.6272H13.7655V55.7189V7.28002L34.2365 55.7742L34.2642 55.8572H34.3748H45.8829H58.8294H58.9677V55.7189V0.28114V0.142822H58.8294H45.8829Z" />
<path d="M209.347 0.142822H184.616H184.477L184.45 0.253483L171.78 48.8583L159.082 0.253483L159.054 0.142822H158.944H134.157H133.991V0.28114V55.7189V55.8572H134.157H147.104H147.242V55.7189V7.66731L159.774 55.7466L159.801 55.8572H159.94H183.564H183.675L183.703 55.7466L196.234 7.66731V55.7189V55.8572H196.373H209.347H209.485V55.7189V0.28114V0.142822H209.347Z" />
<path d="M112.663 0.142822H112.58L112.552 0.198153L96.8116 27.5574L80.988 0.198153L80.9604 0.142822H80.8774H65.9114H65.6348L65.7731 0.364136L90.1447 42.5787V55.7189V55.8572H90.283H103.257H103.396V55.7189V42.5787L127.767 0.364136L127.905 0.142822H127.629H112.663Z" />
</svg>
</Box>
{children}
</Box>
+21 -76
View File
@@ -1,8 +1,6 @@
import { Box, Stack, Tooltip, Typography } from '@mui/material';
import { Box, Tooltip, Typography } from '@mui/material';
import React from 'react';
import { styled } from '@mui/system';
import { writeText } from '@tauri-apps/api/clipboard';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
const IpAddressAndPortContainer = styled('div')({
'.hoverAddressCopy:hover': {
@@ -18,76 +16,23 @@ export const IpAddressAndPort: React.FC<{
label: string;
ipAddress: string;
port: number;
}> = ({ label, ipAddress, port }) => {
const [ipAddressCopied, setIpAddressCopied] = React.useState<boolean>(false);
const [portCopied, setPortCopied] = React.useState<boolean>(false);
React.useEffect(() => {
if (ipAddressCopied) {
setTimeout(() => setIpAddressCopied(false), 2000);
}
}, [ipAddressCopied]);
React.useEffect(() => {
if (portCopied) {
setTimeout(() => setPortCopied(false), 2000);
}
}, [portCopied]);
return (
<IpAddressAndPortContainer>
<Box display="flex" justifyContent="space-between" color="rgba(255,255,255,0.6)">
<Typography fontSize="14px">{label}</Typography>
<Typography fontSize="14px">Port</Typography>
</Box>
<Box display="flex" justifyContent="space-between">
<Tooltip
title={
ipAddressCopied ? (
<Stack direction="row" spacing={1} fontSize="inherit" alignItems="center">
<CheckCircleOutlineIcon color="success" fontSize="small" />
<Typography fontSize="inherit">SOCKS5 proxy hostname copied to the clipboard</Typography>
</Stack>
) : (
<>Click to copy SOCKS5 proxy hostname</>
)
}
>
<Typography
fontWeight="600"
className="hoverAddressCopy"
onClick={async () => {
await writeText(`${ipAddress}`);
setIpAddressCopied(true);
}}
>
{ipAddress}
</Typography>
</Tooltip>
<Tooltip
title={
portCopied ? (
<Stack direction="row" spacing={1} fontSize="inherit" alignItems="center">
<CheckCircleOutlineIcon color="success" fontSize="small" />
<Typography fontSize="inherit">SOCKS5 proxy port copied to the clipboard</Typography>
</Stack>
) : (
<>Click to copy SOCKS5 proxy port</>
)
}
>
<Typography
fontWeight="600"
className="hoverAddressCopy"
onClick={async () => {
await writeText(`${port}`);
setPortCopied(true);
}}
>
{port}
</Typography>
</Tooltip>
</Box>
</IpAddressAndPortContainer>
);
};
}> = ({ label, ipAddress, port }) => (
<IpAddressAndPortContainer>
<Box display="flex" justifyContent="space-between" color="rgba(255,255,255,0.6)">
<Typography fontSize="14px">{label}</Typography>
<Typography fontSize="14px">Port</Typography>
</Box>
<Box display="flex" justifyContent="space-between">
<Tooltip title="Click to copy SOCKS5 proxy hostname">
<Typography fontWeight="600" className="hoverAddressCopy">
{ipAddress}
</Typography>
</Tooltip>
<Tooltip title="Click to copy SOCKS5 proxy port">
<Typography fontWeight="600" className="hoverAddressCopy">
{port}
</Typography>
</Tooltip>
</Box>
</IpAddressAndPortContainer>
);
+10
View File
@@ -0,0 +1,10 @@
import React from 'react';
import Logo from '../images/logo-background.svg';
const imgSize = {
small: 40,
medium: 80,
large: 120,
};
export const NymLogo = ({ size = 'medium' }: { size?: 'small' | 'medium' | 'large' }) => <Logo width={imgSize[size]} />;
+2 -3
View File
@@ -1,8 +1,7 @@
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
import { DateTime } from 'luxon';
import { invoke } from '@tauri-apps/api';
import type { UnlistenFn } from '@tauri-apps/api/event';
import { listen } from '@tauri-apps/api/event';
import { invoke } from '@tauri-apps/api/tauri';
import { listen, UnlistenFn } from '@tauri-apps/api/event';
import { ConnectionStatusKind } from '../types';
import { ConnectionStatsItem } from '../components/ConnectionStats';
import { ServiceProvider, Services } from '../types/directory';
@@ -0,0 +1,41 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
viewBox="0 0 5389.9 5389.9" style="enable-background:new 0 0 5389.9 5389.9;" xml:space="preserve">
<style type="text/css">
.st0{fill:#121726;}
.st1{fill:url(#SVGID_1_);}
.st2{fill:#FFFFFF;}
</style>
<g>
<g>
<circle class="st0" cx="2695" cy="2695" r="2585"/>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="0" y1="8058.165" x2="5390" y2="8058.165" gradientTransform="matrix(1 0 0 -1 0 10753.165)">
<stop offset="0" style="stop-color:#F77846"/>
<stop offset="1" style="stop-color:#ED3572"/>
</linearGradient>
<path class="st1" d="M2695,5390c-182.8,0-365.5-18.4-543-54.8c-173.1-35.4-343.3-88.3-506-157.1
c-159.7-67.6-313.7-151.2-457.8-248.5c-142.7-96.4-276.8-207.1-398.8-329c-121.9-121.9-232.6-256.1-329-398.8
C363,4057.8,279.4,3903.8,211.8,3744C143,3581.4,90.2,3411.1,54.8,3238C18.4,3060.5,0,2877.8,0,2695c0-182.8,18.4-365.5,54.8-543
c35.4-173.1,88.3-343.3,157.1-506c67.6-159.7,151.2-313.7,248.5-457.8c96.4-142.7,207.1-276.8,329-398.8s256.1-232.6,398.8-329
c144.1-97.3,298.1-180.9,457.8-248.5c162.7-68.8,332.9-121.7,506-157.1C2329.5,18.4,2512.2,0,2695,0c182.8,0,365.5,18.4,543,54.8
c173.1,35.4,343.3,88.3,506,157.1c159.7,67.6,313.7,151.2,457.8,248.5c142.7,96.4,276.8,207.1,398.8,329
c121.9,121.9,232.6,256.1,329,398.8c97.3,144.1,180.9,298.1,248.5,457.8c68.8,162.7,121.7,332.9,157.1,506
c36.3,177.5,54.8,360.2,54.8,543c0,182.8-18.4,365.5-54.8,543c-35.4,173.1-88.3,343.3-157.1,506
c-67.6,159.7-151.2,313.7-248.5,457.8c-96.4,142.7-207.1,276.8-329,398.8c-121.9,121.9-256.1,232.6-398.8,329
c-144.1,97.3-298.1,180.9-457.8,248.5c-162.7,68.8-332.9,121.7-506,157.1C3060.5,5371.6,2877.8,5390,2695,5390z M2695,220
c-168,0-335.9,16.9-498.9,50.3c-158.9,32.5-315.1,81-464.4,144.2c-146.6,62-288.1,138.8-420.4,228.2
c-131.1,88.6-254.3,190.3-366.4,302.3c-112,112-213.7,235.3-302.3,366.4c-89.4,132.3-166.2,273.7-228.2,420.4
c-63.2,149.3-111.7,305.6-144.2,464.4C236.9,2359.1,220,2527,220,2695s16.9,335.9,50.3,498.9c32.5,158.9,81,315.1,144.2,464.4
c62,146.6,138.8,288.1,228.2,420.4c88.6,131.1,190.3,254.3,302.3,366.4c112,112,235.3,213.7,366.4,302.3
c132.3,89.4,273.7,166.2,420.4,228.2c149.3,63.2,305.6,111.7,464.4,144.2c163.1,33.4,330.9,50.3,498.9,50.3s335.9-16.9,498.9-50.3
c158.9-32.5,315.1-81,464.4-144.2c146.6-62,288.1-138.8,420.4-228.2c131.1-88.6,254.3-190.3,366.4-302.3
c112-112,213.7-235.3,302.3-366.4c89.4-132.3,166.2-273.7,228.2-420.4c63.2-149.3,111.7-305.6,144.2-464.4
c33.4-163.1,50.3-330.9,50.3-498.9s-16.9-335.9-50.3-498.9c-32.5-158.9-81-315.1-144.2-464.4c-62-146.6-138.8-288.1-228.2-420.4
c-88.6-131.1-190.3-254.3-302.3-366.4c-112-112-235.3-213.7-366.4-302.3c-132.3-89.4-273.7-166.2-420.4-228.2
c-149.3-63.2-305.6-111.7-464.4-144.2C3030.9,236.9,2863,220,2695,220z"/>
</g>
</g>
<path class="st2" d="M1958.5,3160.4h-269.6l-735.8-725.3v725.3H734.6v-930.9h276.2l735.8,725.1v-725.1h211.9V3160.4z M4378.9,2229.5
l-335.7,330.9l-335.7-330.9h-276.2v930.9h218.4v-725.3l345.4,340.6c26.7,26.3,69.6,26.3,96.3,0l345.4-340.6v725.3h218.4v-930.9
H4378.9z M2589.1,2715.4v445h218.4v-445l502.7-485.9H3034l-335.9,330.9l-335.7-330.9h-276.2L2589.1,2715.4z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="22" height="6" viewBox="0 0 22 6" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.86777 6H5.35495L1.22609 1.32517V6H0V0H1.54986L5.67872 4.67354V0H6.86777V6ZM20.4496 0L18.5658 2.13277L16.6821 0H15.1322V6H16.3578V1.32517L18.2959 3.52046C18.4457 3.68998 18.6865 3.68998 18.8363 3.52046L20.7745 1.32517V6H22V0H20.4496ZM10.4063 3.13181V6H11.6318V3.13181L14.4527 0H12.9028L11.018 2.13277L9.13421 0H7.58435L10.4063 3.13181Z" fill="#F2F2F2"/>
</svg>

After

Width:  |  Height:  |  Size: 470 B

+1
View File
@@ -0,0 +1 @@
// TODO
+10
View File
@@ -0,0 +1,10 @@
import React from 'react';
import { Route, Switch } from 'react-router-dom';
export const Routes: React.FC = () => (
<Switch>
<Route path="/">
<div>Root</div>
</Route>
</Switch>
);
@@ -26,7 +26,6 @@ export const Mock: ComponentStory<typeof AppWindowFrame> = () => {
id: 'nym-keybase',
description: 'Nym Keybase Service Provider',
address: '1234.5678',
gateway: 'abcedf',
},
],
},
-20
View File
@@ -1,20 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx",
"noEmit": true
},
"include": [
".storybook/*.js",
"webpack.*.js",
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.stories.*",
],
"exclude": [
"node_modules",
"dist"
]
}
+3 -5
View File
@@ -1,4 +1,5 @@
{
"compileOnSave": false,
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
@@ -14,10 +15,7 @@
"isolatedModules": false,
"jsx": "react-jsx",
"sourceMap": true,
"baseUrl": ".",
"paths": {
"@assets/*": ["../assets/*"]
}
"baseUrl": "."
},
"exclude": ["node_modules", "dist", "jest.config.js", "webpack.config.js", "webpack.prod.js", "webpack.common.js", "target"]
"exclude": ["node_modules", "dist", "jest.config.js", "webpack.config.js", "webpack.prod.js", "webpack.common.js"]
}
+46 -14
View File
@@ -1,18 +1,50 @@
const path = require('path');
const { mergeWithRules } = require('webpack-merge');
const { webpackCommon } = require('@nymproject/webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FaviconsWebpackPlugin = require('favicons-webpack-plugin')
const Dotenv = require('dotenv-webpack')
var path = require('path')
module.exports = mergeWithRules({
module.exports = {
entry: path.resolve(__dirname, '/src/index'),
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'public/index.html'),
filename: 'index.html',
}),
new FaviconsWebpackPlugin(path.resolve(__dirname, 'public/favicon.png')),
new Dotenv(),
],
module: {
rules: {
test: 'match',
use: 'replace',
},
rules: [
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: ['babel-loader'],
},
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|jpe?g|gif)$/i,
use: [
{
loader: 'file-loader',
},
],
},
{
test: /\.svg$/i,
issuer: /\.[jt]sx?$/,
use: ['@svgr/webpack'],
},
{
// See https://webpack.js.org/guides/asset-management/#loading-fonts
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
],
},
})(webpackCommon(__dirname, 'public/index.html'), {
entry: path.resolve(__dirname, 'src/index.tsx'),
output: {
path: path.resolve(__dirname, 'dist'),
publicPath: '/',
resolve: {
extensions: ['*', '.js', '.jsx', '.ts', '.tsx'],
},
});
}
+6 -55
View File
@@ -1,59 +1,10 @@
const { mergeWithRules } = require('webpack-merge');
const webpack = require('webpack');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const ReactRefreshTypeScript = require('react-refresh-typescript');
const commonConfig = require('./webpack.common');
const common = require('./webpack.common')
const { merge } = require('webpack-merge')
var path = require('path')
module.exports = mergeWithRules({
module: {
rules: {
test: 'match',
use: 'replace',
},
},
})(commonConfig, {
module.exports = merge(common, {
mode: 'development',
devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
options: {
getCustomTransformers: () => ({
before: [ReactRefreshTypeScript()],
}),
// `ts-loader` does not work with HMR unless `transpileOnly` is used.
// If you need type checking, `ForkTsCheckerWebpackPlugin` is an alternative.
transpileOnly: true,
},
},
],
},
plugins: [
new ReactRefreshWebpackPlugin(),
// this can be included automatically by the dev server, however build mode fails if missing
new webpack.HotModuleReplacementPlugin(),
],
// recommended for faster rebuild
optimization: {
runtimeChunk: true,
removeAvailableModules: false,
removeEmptyChunks: false,
splitChunks: false,
},
cache: {
type: 'filesystem',
buildDependencies: {
// restart on config change
config: ['./webpack.dev.js'],
},
},
entry: path.resolve(__dirname, '/src/index'),
devServer: {
port: 9000,
compress: true,
@@ -63,4 +14,4 @@ module.exports = mergeWithRules({
overlay: false,
},
},
});
})
+4 -4
View File
@@ -1,6 +1,6 @@
const path = require('path');
const { default: merge } = require('webpack-merge');
const common = require('./webpack.common');
var path = require('path')
const common = require('./webpack.common')
const { default: merge } = require('webpack-merge')
module.exports = merge(common, {
mode: 'production',
@@ -8,4 +8,4 @@ module.exports = merge(common, {
__dirname: false,
},
entry: path.resolve(__dirname, './src/index'),
});
})
+15405
View File
File diff suppressed because it is too large Load Diff
+6 -16
View File
@@ -4,7 +4,7 @@
*/
module.exports = {
invoke: (operation, args) => {
switch (operation) {
switch(operation) {
case 'get_balance': {
return {
amount: {
@@ -12,30 +12,20 @@ module.exports = {
denom: 'NYMT',
},
printable_balance: '100 NYMT',
};
}
}
case 'delegate_to_mixnode': {
return {
return ({
logs_json: '[]',
data_json: '{}',
transaction_hash: '12345',
};
}
case 'simulate_send': {
return {
amount: {
amount: '0.01',
denom: 'NYM',
},
};
});
}
}
console.error(
`Tauri cannot be used in Storybook. The operation requested was "${operation}". You can add mock responses to "nym_wallet/.storybook/mocks/tauri.js" if you need. The default response is "void".`,
);
console.error(`Tauri cannot be used in Storybook. The operation requested was "${operation}". You can add mock responses to "nym_wallet/.storybook/mocks/tauri.js" if you need. The default response is "void".`);
return new Promise((resolve, reject) => {
reject(new Error(`Tauri operation ${operation} not available in storybook.`));
});
},
};
}
+5 -41
View File
@@ -1,55 +1,19 @@
import { NymWalletThemeWithMode } from '../src/theme/NymWalletTheme';
import { Box } from '@mui/material';
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
};
}
const withThemeProvider = (Story, context) => (
<div style={{ display: 'grid', height: '100%', gridTemplateColumns: '50% 50%' }}>
<div>
<NymWalletThemeWithMode mode="light">
<Box
p={4}
sx={{
display: 'grid',
gridTemplateRows: '80vh 2rem',
background: (theme) => theme.palette.background.default,
color: 'text.primary',
}}
>
<Box sx={{ overflowY: 'auto' }}>
<Story {...context} />
</Box>
<h4 style={{ textAlign: 'center' }}>Light mode</h4>
</Box>
</NymWalletThemeWithMode>
</div>
<div>
<NymWalletThemeWithMode mode="dark">
<Box
p={4}
sx={{
display: 'grid',
gridTemplateRows: '80vh 2rem',
background: (theme) => theme.palette.background.default,
color: 'text.primary',
}}
>
<Box sx={{ overflowY: 'auto' }}>
<Story {...context} />
</Box>
<h4 style={{ textAlign: 'center' }}>Dark mode</h4>
</Box>
</NymWalletThemeWithMode>
</div>
</div>
<NymWalletThemeWithMode mode="light">
<Story {...context} />
</NymWalletThemeWithMode>
);
export const decorators = [withThemeProvider];
-21
View File
@@ -1,21 +0,0 @@
import { Theme } from '@mui/material/styles';
export const backDropStyles = (theme: Theme) => {
const { mode } = theme.palette;
return {
style: {
left: mode === 'light' ? '0' : '50%',
width: '50%',
},
};
};
export const modalStyles = (theme: Theme) => {
const { mode } = theme.palette;
return { left: mode === 'light' ? '25%' : '75%' };
};
export const dialogStyles = (theme: Theme) => {
const { mode } = theme.palette;
return { left: mode === 'light' ? '-50%' : '50%' };
};
+3 -4
View File
@@ -21,6 +21,7 @@
"storybook:build": "build-storybook"
},
"dependencies": {
"@babel/preset-typescript": "^7.15.0",
"@emotion/react": "^11.7.0",
"@emotion/styled": "^11.6.0",
"@hookform/resolvers": "^2.8.0",
@@ -31,8 +32,8 @@
"@nymproject/mui-theme": "^1.0.0",
"@nymproject/react": "^1.0.0",
"@nymproject/types": "^1.0.0",
"@tauri-apps/api": "^1.0.2",
"@tauri-apps/tauri-forage": "^1.0.0-beta.2",
"@storybook/react": "^6.5.8",
"@tauri-apps/api": "^1.0.0-rc.1",
"bs58": "^4.0.1",
"clsx": "^1.1.1",
"date-fns": "^2.28.0",
@@ -55,10 +56,8 @@
"@babel/plugin-transform-async-to-generator": "^7.14.5",
"@babel/preset-env": "^7.15.0",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.15.0",
"@nymproject/eslint-config-react-typescript": "^1.0.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
"@storybook/react": "^6.5.8",
"@svgr/webpack": "^6.1.1",
"@tauri-apps/cli": "^1.0.0-rc.5",
"@testing-library/jest-dom": "^5.14.1",
+14 -5
View File
@@ -14,7 +14,13 @@
"active": true,
"targets": "all",
"identifier": "net.nymtech.wallet",
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"resources": [],
"externalBin": [],
"copyright": "Copyright © 2021-2022 Nym Technologies SA",
@@ -22,12 +28,13 @@
"shortDescription": "Nym desktop wallet allows you to manage your NYM tokens",
"longDescription": "",
"deb": {
"depends": []
"depends": [],
"useBootstrapper": false
},
"macOS": {
"frameworks": [],
"minimumSystemVersion": "",
"useBootstrapper": false,
"exceptionDomain": "",
"signingIdentity": "Developer ID Application: Nym Technologies SA (VW5DZLFHM5)",
"entitlements": null
@@ -40,13 +47,15 @@
},
"updater": {
"active": true,
"endpoints": ["https://nymtech.net/.wellknown/wallet/updater.json"],
"endpoints": [
"https://nymtech.net/.wellknown/wallet/updater.json"
],
"dialog": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IENCNzQ2M0E5N0VFODE2NApSV1JrZ2U2WE9rYTNETTg1OTBKdE5uWUEra0hML2syOVUvQ2lxZmFZRzZ1T3NWbGM0eVRzUTVhVwo="
},
"allowlist": {
"window": {
"maximize": true
"maximize": true
},
"clipboard": {
"all": true
@@ -6,7 +6,7 @@ import { AccountAvatar } from './AccountAvatar';
export const AccountOverview = ({ account, onClick }: { account: AccountEntry; onClick: () => void }) => (
<Button
startIcon={<AccountAvatar name={account.id} />}
sx={{ color: 'text.primary' }}
sx={{ color: 'nym.text.dark' }}
onClick={onClick}
disableRipple
>
@@ -1,5 +1,5 @@
import React from 'react';
import { Alert, Box, Paper, Dialog, DialogContent, DialogTitle, IconButton, Stack, Typography } from '@mui/material';
import { Alert, Box, Dialog, DialogContent, DialogTitle, IconButton, Stack, Typography } from '@mui/material';
import { Close } from '@mui/icons-material';
const passwordCreationSteps = [
@@ -11,35 +11,29 @@ const passwordCreationSteps = [
];
export const MultiAccountHowTo = ({ show, handleClose }: { show: boolean; handleClose: () => void }) => (
<Dialog open={show} onClose={handleClose} fullWidth>
<Paper>
<DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">Multi accounts</Typography>
<IconButton onClick={handleClose}>
<Close />
</IconButton>
</Box>
<Typography variant="body1" sx={{ color: (t) => t.palette.nym.text.muted }}>
How to set up multiple accounts
</Typography>
</DialogTitle>
<DialogContent>
<Stack spacing={2}>
<Alert
severity="warning"
icon={false}
sx={(t) => (t.palette.mode === 'dark' ? { bgcolor: (t) => t.palette.background.paper } : {})}
>
<Typography>In order to create multiple accounts your wallet needs a password.</Typography>
<Typography>Follow steps below to create password.</Typography>
</Alert>
<Typography>How to create a password for your account</Typography>
{passwordCreationSteps.map((step, index) => (
<Typography key={step}>{`${index + 1}. ${step}`}</Typography>
))}
</Stack>
</DialogContent>
</Paper>
<Dialog open={show} onClose={handleClose} fullWidth hideBackdrop>
<DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">Multi accounts</Typography>
<IconButton onClick={handleClose}>
<Close />
</IconButton>
</Box>
<Typography variant="body1" sx={{ color: 'grey.600' }}>
How to set up multiple accounts
</Typography>
</DialogTitle>
<DialogContent>
<Stack spacing={2}>
<Alert severity="warning" icon={false}>
<Typography>In order to create multiple accounts your wallet needs a password.</Typography>
<Typography>Follow steps below to create password.</Typography>
</Alert>
<Typography>How to create a password for your account</Typography>
{passwordCreationSteps.map((step, index) => (
<Typography key={step}>{`${index + 1}. ${step}`}</Typography>
))}
</Stack>
</DialogContent>
</Dialog>
);
@@ -1,15 +1,5 @@
import React, { useContext, useState } from 'react';
import {
Box,
Button,
Paper,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
IconButton,
Typography,
} from '@mui/material';
import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Typography } from '@mui/material';
import { Add, ArrowDownwardSharp, Close } from '@mui/icons-material';
import { AccountsContext } from 'src/context';
import { AccountItem } from '../AccountItem';
@@ -42,47 +32,45 @@ export const AccountsModal = () => {
);
return (
<Dialog open={dialogToDisplay === 'Accounts'} onClose={handleClose} fullWidth>
<Paper>
<DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">Accounts</Typography>
<IconButton onClick={handleClose}>
<Close />
</IconButton>
</Box>
<Typography variant="body1" sx={{ color: (theme) => theme.palette.text.disabled }}>
Switch between accounts
</Typography>
</DialogTitle>
<DialogContent sx={{ padding: 0 }}>
{accounts?.map(({ id, address }) => (
<AccountItem
name={id}
address={address}
key={address}
onSelectAccount={() => {
if (selectedAccount?.id !== id) {
setAccountToSwitchTo(id);
}
}}
/>
))}
</DialogContent>
<DialogActions sx={{ p: 3 }}>
<Button startIcon={<ArrowDownwardSharp />} onClick={() => setDialogToDisplay('Import')}>
Import account
</Button>
<Button
disableElevation
variant="contained"
startIcon={<Add fontSize="small" />}
onClick={() => setDialogToDisplay('Add')}
>
Add new account
</Button>
</DialogActions>
</Paper>
<Dialog open={dialogToDisplay === 'Accounts'} onClose={handleClose} fullWidth hideBackdrop>
<DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">Accounts</Typography>
<IconButton onClick={handleClose}>
<Close />
</IconButton>
</Box>
<Typography variant="body1" sx={{ color: 'grey.600' }}>
Switch between accounts
</Typography>
</DialogTitle>
<DialogContent sx={{ padding: 0 }}>
{accounts?.map(({ id, address }) => (
<AccountItem
name={id}
address={address}
key={address}
onSelectAccount={() => {
if (selectedAccount?.id !== id) {
setAccountToSwitchTo(id);
}
}}
/>
))}
</DialogContent>
<DialogActions sx={{ p: 3 }}>
<Button startIcon={<ArrowDownwardSharp />} onClick={() => setDialogToDisplay('Import')}>
Import account
</Button>
<Button
disableElevation
variant="contained"
startIcon={<Add fontSize="small" />}
onClick={() => setDialogToDisplay('Add')}
>
Add new account
</Button>
</DialogActions>
</Dialog>
);
};
@@ -2,7 +2,6 @@ import React, { useContext, useEffect, useState } from 'react';
import {
Box,
Button,
Paper,
Dialog,
DialogActions,
DialogContent,
@@ -173,64 +172,67 @@ export const AddAccountModal = () => {
}, [step]);
return (
<Dialog open={dialogToDisplay === 'Add' || dialogToDisplay === 'Import'} onClose={handleClose} fullWidth>
<Paper>
<DialogTitle sx={{ pb: 0 }}>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">{`${dialogToDisplay} new account`}</Typography>
<IconButton onClick={() => (step === 0 ? handleClose() : setStep((s) => s - 1))}>
<ArrowBackSharp />
</IconButton>
</Box>
<Typography sx={{ mt: 2 }}>
{dialogToDisplay === 'Add' ? createAccountSteps[step] : importAccountSteps[step]}
</Typography>
</DialogTitle>
{(() => {
switch (step) {
case 0:
return dialogToDisplay === 'Add' ? (
<MnemonicStep mnemonic={data.mnemonic} onNext={() => setStep((s) => s + 1)} />
) : (
<ImportMnemonic
value={data.mnemonic}
onChange={(value) => setData((d) => ({ ...d, mnemonic: value }))}
onNext={() => setStep((s) => s + 1)}
/>
);
case 1:
return (
<NameAccount
onNext={(accountName) => {
setData((d) => ({ ...d, accountName }));
setStep((s) => s + 1);
}}
/>
);
case 2:
return (
<ConfirmPassword
buttonTitle="Add account"
onConfirm={async (password) => {
if (data.accountName && data.mnemonic) {
try {
await handleAddAccount({ accountName: data.accountName, mnemonic: data.mnemonic, password });
setStep(0);
setDialogToDisplay('Accounts');
} catch (e) {
Console.error(e as string);
}
<Dialog
open={dialogToDisplay === 'Add' || dialogToDisplay === 'Import'}
onClose={handleClose}
fullWidth
hideBackdrop
>
<DialogTitle sx={{ pb: 0 }}>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">{`${dialogToDisplay} new account`}</Typography>
<IconButton onClick={() => (step === 0 ? handleClose() : setStep((s) => s - 1))}>
<ArrowBackSharp />
</IconButton>
</Box>
<Typography sx={{ mt: 2 }}>
{dialogToDisplay === 'Add' ? createAccountSteps[step] : importAccountSteps[step]}
</Typography>
</DialogTitle>
{(() => {
switch (step) {
case 0:
return dialogToDisplay === 'Add' ? (
<MnemonicStep mnemonic={data.mnemonic} onNext={() => setStep((s) => s + 1)} />
) : (
<ImportMnemonic
value={data.mnemonic}
onChange={(value) => setData((d) => ({ ...d, mnemonic: value }))}
onNext={() => setStep((s) => s + 1)}
/>
);
case 1:
return (
<NameAccount
onNext={(accountName) => {
setData((d) => ({ ...d, accountName }));
setStep((s) => s + 1);
}}
/>
);
case 2:
return (
<ConfirmPassword
buttonTitle="Add account"
onConfirm={async (password) => {
if (data.accountName && data.mnemonic) {
try {
await handleAddAccount({ accountName: data.accountName, mnemonic: data.mnemonic, password });
setStep(0);
setDialogToDisplay('Accounts');
} catch (e) {
Console.error(e as string);
}
}}
isLoading={isLoading}
error={error}
/>
);
default:
return null;
}
})()}
</Paper>
}
}}
isLoading={isLoading}
error={error}
/>
);
default:
return null;
}
})()}
</Dialog>
);
};
@@ -1,5 +1,5 @@
import React, { useContext } from 'react';
import { Box, Paper, Dialog, DialogTitle, IconButton, Typography } from '@mui/material';
import { Box, Dialog, DialogTitle, IconButton, Typography } from '@mui/material';
import { ArrowBack } from '@mui/icons-material';
import { ConfirmPassword } from 'src/components/ConfirmPassword';
import { AccountsContext } from 'src/context';
@@ -16,21 +16,19 @@ export const ConfirmPasswordModal = ({
const { isLoading, error } = useContext(AccountsContext);
return (
<Dialog open={Boolean(accountName)} onClose={onClose} fullWidth>
<Paper>
<DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">Switch account</Typography>
<IconButton onClick={onClose}>
<ArrowBack />
</IconButton>
</Box>
<Typography variant="body1" sx={{ color: (theme) => theme.palette.text.disabled }}>
Confirm password
</Typography>
</DialogTitle>
<ConfirmPassword onConfirm={onConfirm} error={error} isLoading={isLoading} buttonTitle="Switch account" />
</Paper>
<Dialog open={Boolean(accountName)} onClose={onClose} fullWidth hideBackdrop>
<DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">Switch account</Typography>
<IconButton onClick={onClose}>
<ArrowBack />
</IconButton>
</Box>
<Typography variant="body1" sx={{ color: 'grey.600' }}>
Confirm password
</Typography>
</DialogTitle>
<ConfirmPassword onConfirm={onConfirm} error={error} isLoading={isLoading} buttonTitle="Switch account" />
</Dialog>
);
};
@@ -2,7 +2,6 @@ import React, { useContext, useEffect, useState } from 'react';
import {
Box,
Button,
Paper,
Dialog,
DialogActions,
DialogContent,
@@ -24,48 +23,46 @@ export const EditAccountModal = () => {
}, [accountToEdit]);
return (
<Dialog open={dialogToDisplay === 'Edit'} onClose={() => setDialogToDisplay('Accounts')} fullWidth>
<Paper>
<DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">Edit account name</Typography>
<IconButton onClick={() => setDialogToDisplay('Accounts')}>
<Close />
</IconButton>
</Box>
<Typography variant="body1" sx={{ color: (theme) => theme.palette.text.disabled }}>
New wallet address
</Typography>
</DialogTitle>
<DialogContent sx={{ p: 0 }}>
<Box sx={{ px: 3, mt: 1 }}>
<TextField
label="Account name"
fullWidth
value={accountName}
onChange={(e) => setAccountName(e.target.value)}
autoFocus
/>
</Box>
</DialogContent>
<DialogActions sx={{ p: 3 }}>
<Button
<Dialog open={dialogToDisplay === 'Edit'} onClose={() => setDialogToDisplay('Accounts')} fullWidth hideBackdrop>
<DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">Edit account name</Typography>
<IconButton onClick={() => setDialogToDisplay('Accounts')}>
<Close />
</IconButton>
</Box>
<Typography variant="body1" sx={{ color: 'grey.600' }}>
New wallet address
</Typography>
</DialogTitle>
<DialogContent sx={{ p: 0 }}>
<Box sx={{ px: 3, mt: 1 }}>
<TextField
label="Account name"
fullWidth
disableElevation
variant="contained"
size="large"
onClick={() => {
if (accountToEdit) {
handleEditAccount({ ...accountToEdit, id: accountName });
setDialogToDisplay('Accounts');
}
}}
disabled={!accountName?.length}
>
Edit
</Button>
</DialogActions>
</Paper>
value={accountName}
onChange={(e) => setAccountName(e.target.value)}
autoFocus
/>
</Box>
</DialogContent>
<DialogActions sx={{ p: 3 }}>
<Button
fullWidth
disableElevation
variant="contained"
size="large"
onClick={() => {
if (accountToEdit) {
handleEditAccount({ ...accountToEdit, id: accountName });
setDialogToDisplay('Accounts');
}
}}
disabled={!accountName?.length}
>
Edit
</Button>
</DialogActions>
</Dialog>
);
};
@@ -2,7 +2,6 @@ import React, { useContext, useState } from 'react';
import {
Box,
Button,
Paper,
Dialog,
DialogActions,
DialogContent,
@@ -25,45 +24,43 @@ export const ImportAccountModal = () => {
};
return (
<Dialog open={dialogToDisplay === 'Import'} onClose={handleClose} fullWidth>
<Paper>
<DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">Import account</Typography>
<IconButton onClick={handleClose}>
<Close />
</IconButton>
</Box>
<Typography variant="body1" sx={{ color: (theme) => theme.palette.text.disabled }}>
Provide mnemonic of account you want to import
</Typography>
</DialogTitle>
<DialogContent sx={{ p: 0 }}>
<Box sx={{ px: 3, mt: 1 }}>
<TextField
placeholder="Paste or type your mnemonic here"
fullWidth
value={mnemonic}
onChange={(e) => setMnemonic(e.target.value)}
autoFocus
multiline
rows={3}
/>
</Box>
</DialogContent>
<DialogActions sx={{ p: 3 }}>
<Button
<Dialog open={dialogToDisplay === 'Import'} onClose={handleClose} fullWidth hideBackdrop>
<DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">Import account</Typography>
<IconButton onClick={handleClose}>
<Close />
</IconButton>
</Box>
<Typography variant="body1" sx={{ color: 'grey.600' }}>
Provide mnemonic of account you want to import
</Typography>
</DialogTitle>
<DialogContent sx={{ p: 0 }}>
<Box sx={{ px: 3, mt: 1 }}>
<TextField
placeholder="Paste or type your mnemonic here"
fullWidth
disableElevation
variant="contained"
size="large"
onClick={() => handleImportAccount({ id: '', address: '' })}
disabled={!mnemonic.length}
>
Import account
</Button>
</DialogActions>
</Paper>
value={mnemonic}
onChange={(e) => setMnemonic(e.target.value)}
autoFocus
multiline
rows={3}
/>
</Box>
</DialogContent>
<DialogActions sx={{ p: 3 }}>
<Button
fullWidth
disableElevation
variant="contained"
size="large"
onClick={() => handleImportAccount({ id: '', address: '' })}
disabled={!mnemonic.length}
>
Import account
</Button>
</DialogActions>
</Dialog>
);
};
@@ -3,7 +3,6 @@ import {
Box,
Button,
CircularProgress,
Paper,
Dialog,
DialogActions,
DialogContent,
@@ -40,63 +39,61 @@ export const MnemonicModal = () => {
};
return (
<Dialog open={dialogToDisplay === 'Mnemonic'} onClose={handleClose} fullWidth>
<Paper>
<DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">Display mnemonic</Typography>
<IconButton onClick={handleClose}>
<ArrowBackSharp />
</IconButton>
</Box>
<Typography variant="body1" sx={{ color: (theme) => theme.palette.text.disabled }}>
{`Display mnemonic for: ${accountMnemonic?.accountName}`}
</Typography>
</DialogTitle>
<DialogContent sx={{ p: 0 }}>
<Box sx={{ px: 3, mt: 1 }}>
{error && (
<Typography variant="body1" sx={{ color: 'error.main', mb: 2 }}>
{error}
</Typography>
)}
{!accountMnemonic.value ? (
<>
<Typography sx={{ mb: 2 }}>Enter the password used to login to your wallet</Typography>
<PasswordInput
label="Password"
password={password}
onUpdatePassword={(pswrd) => setPassword(pswrd)}
autoFocus
/>
</>
) : (
<Mnemonic mnemonic={accountMnemonic.value} handleCopy={copy} copied={copied} />
)}
</Box>
</DialogContent>
<DialogActions sx={{ p: 3 }}>
{!accountMnemonic.value && (
<Button
disableRipple
disabled={!password.length || isLoading}
fullWidth
disableElevation
variant="contained"
size="large"
onClick={async () => {
if (accountMnemonic?.accountName) {
setError(undefined);
await handleGetAccountMnemonic({ password, accountName: accountMnemonic?.accountName });
}
}}
endIcon={isLoading && <CircularProgress size={20} />}
>
Display mnemonic
</Button>
<Dialog open={dialogToDisplay === 'Mnemonic'} onClose={handleClose} fullWidth hideBackdrop>
<DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">Display mnemonic</Typography>
<IconButton onClick={handleClose}>
<ArrowBackSharp />
</IconButton>
</Box>
<Typography variant="body1" sx={{ color: 'grey.600' }}>
{`Display mnemonic for: ${accountMnemonic?.accountName}`}
</Typography>
</DialogTitle>
<DialogContent sx={{ p: 0 }}>
<Box sx={{ px: 3, mt: 1 }}>
{error && (
<Typography variant="body1" sx={{ color: 'error.main', mb: 2 }}>
{error}
</Typography>
)}
</DialogActions>
</Paper>
{!accountMnemonic.value ? (
<>
<Typography sx={{ mb: 2 }}>Enter the password used to login to your wallet</Typography>
<PasswordInput
label="Password"
password={password}
onUpdatePassword={(pswrd) => setPassword(pswrd)}
autoFocus
/>
</>
) : (
<Mnemonic mnemonic={accountMnemonic.value} handleCopy={copy} copied={copied} />
)}
</Box>
</DialogContent>
<DialogActions sx={{ p: 3 }}>
{!accountMnemonic.value && (
<Button
disableRipple
disabled={!password.length || isLoading}
fullWidth
disableElevation
variant="contained"
size="large"
onClick={async () => {
if (accountMnemonic?.accountName) {
setError(undefined);
await handleGetAccountMnemonic({ password, accountName: accountMnemonic?.accountName });
}
}}
endIcon={isLoading && <CircularProgress size={20} />}
>
Display mnemonic
</Button>
)}
</DialogActions>
</Dialog>
);
};
+6 -20
View File
@@ -1,11 +1,8 @@
import React, { useContext } from 'react';
import { styled } from '@mui/material/styles';
import { AppBar as MuiAppBar, Grid, IconButton, Toolbar, FormGroup, FormControlLabel, Switch } from '@mui/material';
import { AppBar as MuiAppBar, Grid, IconButton, Toolbar } from '@mui/material';
import { useNavigate } from 'react-router-dom';
import { Logout } from '@mui/icons-material';
import TerminalIcon from '@mui/icons-material/Terminal';
import ModeNightOutlinedIcon from '@mui/icons-material/ModeNightOutlined';
import LightModeOutlinedIcon from '@mui/icons-material/LightModeOutlined';
import { AppContext } from '../context/main';
import { NetworkSelector } from './NetworkSelector';
import { Node as NodeIcon } from '../svg-icons/node';
@@ -13,12 +10,10 @@ import { MultiAccounts } from './Accounts';
import { config } from '../config';
export const AppBar = () => {
const { logOut, handleShowTerminal, appEnv, handleShowSettings, showSettings, mode, handleSwitchMode } =
useContext(AppContext);
const { logOut, handleShowTerminal, appEnv, handleShowSettings, showSettings } = useContext(AppContext);
const navigate = useNavigate();
return (
<MuiAppBar position="sticky" sx={{ boxShadow: 'none', bgcolor: 'transparent', backgroundImage: 'none' }}>
<MuiAppBar position="sticky" sx={{ boxShadow: 'none', bgcolor: 'transparent' }}>
<Toolbar disableGutters>
<Grid container justifyContent="space-between" alignItems="center" flexWrap="nowrap">
<Grid item container alignItems="center" spacing={1}>
@@ -30,18 +25,9 @@ export const AppBar = () => {
</Grid>
</Grid>
<Grid item container justifyContent="flex-end" md={12} lg={5} spacing={2}>
<Grid item>
<IconButton size="small" onClick={handleSwitchMode} sx={{ color: 'text.primary' }}>
{mode === 'light' ? (
<LightModeOutlinedIcon fontSize="small" />
) : (
<ModeNightOutlinedIcon fontSize="small" sx={{ transform: 'rotate(180deg)' }} />
)}
</IconButton>
</Grid>
{(appEnv?.SHOW_TERMINAL || config.IS_DEV_MODE) && (
<Grid item>
<IconButton size="small" onClick={handleShowTerminal} sx={{ color: 'text.primary' }}>
<IconButton size="small" onClick={handleShowTerminal} sx={{ color: 'nym.background.dark' }}>
<TerminalIcon fontSize="small" />
</IconButton>
</Grid>
@@ -49,7 +35,7 @@ export const AppBar = () => {
<Grid item>
<IconButton
onClick={handleShowSettings}
sx={{ color: showSettings ? 'primary.main' : 'text.primary' }}
sx={{ color: showSettings ? 'primary.main' : 'nym.background.dark' }}
size="small"
>
<NodeIcon fontSize="small" />
@@ -62,7 +48,7 @@ export const AppBar = () => {
await logOut();
navigate('/');
}}
sx={{ color: 'text.primary' }}
sx={{ color: 'nym.background.dark' }}
>
<Logout fontSize="small" />
</IconButton>
@@ -1,65 +0,0 @@
import React, { useContext } from 'react';
import { Logout } from '@mui/icons-material';
import TerminalIcon from '@mui/icons-material/Terminal';
import ModeNightOutlinedIcon from '@mui/icons-material/ModeNightOutlined';
import LightModeOutlinedIcon from '@mui/icons-material/LightModeOutlined';
import { AppBar as MuiAppBar, Grid, IconButton, Toolbar, FormGroup, FormControlLabel, Switch } from '@mui/material';
import { Node } from 'src/svg-icons/node';
import { config } from '../../config';
import { AppContext } from '../../context/main';
import { MultiAccounts } from '../Accounts';
import { NetworkSelector } from '../NetworkSelector';
export const AppBar = () => {
const { showSettings, handleShowTerminal, appEnv, handleShowSettings, logOut, mode, handleSwitchMode } =
useContext(AppContext);
return (
<MuiAppBar position="sticky" sx={{ boxShadow: 'none', bgcolor: 'transparent', backgroundImage: 'none' }}>
<Toolbar disableGutters>
<Grid container justifyContent="space-between" alignItems="center" flexWrap="nowrap">
<Grid item container alignItems="center" spacing={1}>
<Grid item>
<MultiAccounts />
</Grid>
<Grid item>
<NetworkSelector />
</Grid>
</Grid>
<Grid item container justifyContent="flex-end" md={12} lg={5} spacing={2}>
<Grid item>
<IconButton size="small" onClick={handleSwitchMode} sx={{ color: 'text.primary' }}>
{mode === 'light' ? (
<ModeNightOutlinedIcon fontSize="small" />
) : (
<LightModeOutlinedIcon fontSize="small" />
)}
</IconButton>
</Grid>
{(appEnv?.SHOW_TERMINAL || config.IS_DEV_MODE) && (
<Grid item>
<IconButton size="small" onClick={handleShowTerminal} sx={{ color: 'text.primary' }}>
<TerminalIcon fontSize="small" />
</IconButton>
</Grid>
)}
<Grid item>
<IconButton
onClick={handleShowSettings}
sx={{ color: showSettings ? 'primary.main' : 'text.primary' }}
size="small"
>
<Node fontSize="small" />
</IconButton>
</Grid>
<Grid item>
<IconButton size="small" onClick={logOut} sx={{ color: 'text.primary' }}>
<Logout fontSize="small" />
</IconButton>
</Grid>
</Grid>
</Grid>
</Toolbar>
</MuiAppBar>
);
};
@@ -1 +0,0 @@
export * from './AppBar';
+2 -2
View File
@@ -37,14 +37,14 @@ export const ClientAddressDisplay: FC<ClientAddressProps & { address?: string }>
<Box>
{withLabel && (
<>
<Typography variant="body2" component="span" sx={{ color: 'text.primary' }}>
<Typography variant="body2" component="span" sx={{ color: 'grey.600' }}>
Address:
</Typography>{' '}
</>
)}
<AddressTooltip address={address} visible={!showEntireAddress}>
<Typography variant="body2" component="span" sx={{ mr: 1, color: 'text.primary', fontWeight: 400 }}>
<Typography variant="body2" component="span" color="nym.background.dark" sx={{ mr: 1 }}>
{showEntireAddress ? address || '' : splice(6, address)}
</Typography>
</AddressTooltip>
@@ -25,5 +25,4 @@ Default.args = {
onClose: () => {},
onConfirm: async () => {},
onPrev: () => {},
isStorybook: true,
};
+16 -33
View File
@@ -1,19 +1,8 @@
import React from 'react';
import { FeeDetails } from '@nymproject/types';
import { Box } from '@mui/material';
import { useTheme, Theme } from '@mui/material/styles';
import { SimpleModal } from './Modals/SimpleModal';
import { ModalFee } from './Modals/ModalFee';
import { ModalDivider } from './Modals/ModalDivider';
import { backDropStyles, modalStyles } from '../../.storybook/storiesStyles';
const storybookStyles = (theme: Theme, isStorybook?: boolean, backdropProps?: object) =>
!!isStorybook
? {
backdropProps: { ...backDropStyles(theme), ...backdropProps },
sx: modalStyles(theme),
}
: {};
export const ConfirmTx: React.FC<{
open: boolean;
@@ -23,25 +12,19 @@ export const ConfirmTx: React.FC<{
onConfirm: () => Promise<void>;
onClose?: () => void;
onPrev: () => void;
isStorybook?: boolean;
}> = ({ open, fee, onConfirm, onClose, header, subheader, onPrev, children, isStorybook }) => {
const theme = useTheme();
return (
<SimpleModal
open={open}
header={header}
subHeader={subheader}
okLabel="Confirm"
onOk={onConfirm}
onClose={onClose}
onBack={onPrev}
{...storybookStyles(theme, isStorybook)}
>
<Box sx={{ mt: 3 }}>
{children}
<ModalFee fee={fee} isLoading={false} />
<ModalDivider />
</Box>
</SimpleModal>
);
};
}> = ({ open, fee, onConfirm, onClose, header, subheader, onPrev, children }) => (
<SimpleModal
open={open}
header={header}
subHeader={subheader}
okLabel="Confirm"
onOk={onConfirm}
onClose={onClose}
onBack={onPrev}
>
<Box sx={{ mt: 3 }}>
{children}
<ModalFee fee={fee} isLoading={false} />
</Box>
</SimpleModal>
);
+12 -4
View File
@@ -4,7 +4,15 @@ import { Check, ContentCopy } from '@mui/icons-material';
import { clipboard } from '@tauri-apps/api';
import { Console } from '../utils/console';
export const CopyToClipboard = ({ text = '', iconButton }: { text?: string; iconButton?: boolean }) => {
export const CopyToClipboard = ({
text = '',
light,
iconButton,
}: {
text?: string;
light?: boolean;
iconButton?: boolean;
}) => {
const [copied, setCopied] = useState(false);
const handleCopy = async (_text: string) => {
@@ -33,7 +41,7 @@ export const CopyToClipboard = ({ text = '', iconButton }: { text?: string; icon
onClick={() => handleCopy(text)}
size="small"
sx={{
color: 'text.primary',
color: (theme) => (light ? theme.palette.common.white : theme.palette.nym.background.dark),
}}
>
{!copied ? <ContentCopy fontSize="small" /> : <Check color="success" />}
@@ -46,8 +54,8 @@ export const CopyToClipboard = ({ text = '', iconButton }: { text?: string; icon
variant="outlined"
color="inherit"
sx={{
color: 'text.primary',
borderColor: 'text.primary',
color: (theme) => (light ? theme.palette.common.white : theme.palette.nym.background.dark),
borderColor: (theme) => (light ? theme.palette.common.white : theme.palette.nym.background.dark),
}}
onClick={() => handleCopy(text)}
endIcon={copied && <Check sx={{ color: (theme) => theme.palette.success.light }} />}
@@ -1,8 +1,5 @@
import React from 'react';
import { ComponentMeta } from '@storybook/react';
import { useTheme } from '@mui/material/styles';
import { Button, Paper, Typography } from '@mui/material';
import { backDropStyles, modalStyles } from '../../../.storybook/storiesStyles';
import { OverSaturatedBlockerModal } from './DelegateBlocker';
@@ -11,30 +8,4 @@ export default {
component: OverSaturatedBlockerModal,
} as ComponentMeta<typeof OverSaturatedBlockerModal>;
export const Default = () => {
const [open, setOpen] = React.useState<boolean>(false);
const handleClick = () => setOpen(true);
const theme = useTheme();
return (
<>
<Paper elevation={0} sx={{ px: 4, pt: 2, pb: 4 }}>
<h2>Lorem ipsum</h2>
<Button variant="contained" onClick={handleClick} sx={{ mb: 3 }}>
Show modal
</Button>
<Typography>
Veniam dolor laborum labore sit reprehenderit enim mollit magna nulla adipisicing fugiat. Est ex irure quis
sunt velit elit do minim mollit non duis reprehenderit. Eiusmod dolore adipisicing ex nostrud consectetur
culpa exercitation do. Ad elit esse ipsum aliqua labore irure laborum qui culpa.
</Typography>
</Paper>
<OverSaturatedBlockerModal
open={open}
header="Node saturation: 114%"
onClose={() => setOpen(false)}
backdropProps={backDropStyles(theme)}
sx={modalStyles(theme)}
/>
</>
);
};
export const Default = () => <OverSaturatedBlockerModal open header="Node saturation: 114%" />;
@@ -1,6 +1,5 @@
import React from 'react';
import { SimpleModal } from '../Modals/SimpleModal';
import { SxProps } from '@mui/material';
export const OverSaturatedBlockerModal: React.FC<{
open: boolean;
@@ -8,9 +7,7 @@ export const OverSaturatedBlockerModal: React.FC<{
header?: string;
subHeader?: string;
buttonText?: string;
sx?: SxProps;
backdropProps?: object;
}> = ({ open, onClose, header, subHeader, buttonText, sx, backdropProps }) => (
}> = ({ open, onClose, header, subHeader, buttonText }) => (
<SimpleModal
open={open}
hideCloseIcon
@@ -20,7 +17,7 @@ export const OverSaturatedBlockerModal: React.FC<{
header={header || 'Delegate'}
subHeader={subHeader || "This node is over saturated, you can't delegate more stake to it"}
okLabel={buttonText || 'Close'}
sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', ...sx }}
sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}
headerStyles={{
width: '100%',
mb: 3,
@@ -30,6 +27,5 @@ export const OverSaturatedBlockerModal: React.FC<{
textTransform: 'capitalize',
}}
subHeaderStyles={{ textAlign: 'center', color: 'text.primary', fontSize: 14, fontWeight: 400 }}
backdropProps={backdropProps}
/>
);
@@ -1,6 +1,5 @@
import React, { useState } from 'react';
import { Box, Typography } from '@mui/material';
import { SxProps } from '@mui/system';
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
import { CurrencyDenom, FeeDetails, MajorCurrencyAmount } from '@nymproject/types';
@@ -34,8 +33,6 @@ export const DelegateModal: React.FC<{
currency: CurrencyDenom;
initialAmount?: string;
hasVestingContract: boolean;
sx?: SxProps;
backdropProps?: object;
}> = ({
open,
onIdentityKeyChanged,
@@ -53,8 +50,6 @@ export const DelegateModal: React.FC<{
nodeUptimePercentage,
initialAmount,
hasVestingContract,
sx,
backdropProps,
}) => {
const [identityKey, setIdentityKey] = useState<string | undefined>(initialIdentityKey);
const [amount, setAmount] = useState<string | undefined>(initialAmount);
@@ -188,8 +183,6 @@ export const DelegateModal: React.FC<{
subHeader="Delegate to mixnode"
okLabel={buttonText || 'Delegate stake'}
okDisabled={!isValidated}
sx={sx}
backdropProps={backdropProps}
>
<IdentityKeyFormField
required
@@ -89,8 +89,8 @@ const DelegationActionsMenuItem = ({
disabled?: boolean;
}) => (
<MenuItem sx={{ p: 2 }} onClick={onClick} disabled={disabled}>
<ListItemIcon sx={{ color: 'text.primary' }}>{Icon}</ListItemIcon>
<ListItemText sx={{ color: 'text.primary' }} primary={title} secondary={description} />
<ListItemIcon sx={{ color: 'black' }}>{Icon}</ListItemIcon>
<ListItemText sx={{ color: 'black' }} primary={title} secondary={description} />
</MenuItem>
);
@@ -16,6 +16,7 @@ import {
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import { visuallyHidden } from '@mui/utils';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard';
import { DelegationWithEverything } from '@nymproject/types';
import { Link } from '@nymproject/react/link/Link';
import { format, formatDistanceToNow, parseISO } from 'date-fns';
@@ -53,12 +54,12 @@ interface HeadCell {
const headCells: HeadCell[] = [
{ id: 'node_identity', label: 'Node ID', sortable: true, align: 'left' },
{ id: 'avg_uptime_percent', label: 'Uptime', sortable: true, align: 'left' },
{ id: 'profit_margin_percent', label: 'Profit margin', sortable: true, align: 'left' },
{ id: 'stake_saturation', label: 'Stake saturation', sortable: true, align: 'left' },
{ id: 'delegated_on_iso_datetime', label: 'Delegated on', sortable: true, align: 'left' },
{ id: 'amount', label: 'Delegation', sortable: true, align: 'left' },
{ id: 'accumulated_rewards', label: 'Reward', sortable: true, align: 'left' },
{ id: 'avg_uptime_percent', label: 'Uptime', sortable: true, align: 'center' },
{ id: 'profit_margin_percent', label: 'Profit margin', sortable: true, align: 'center' },
{ id: 'stake_saturation', label: 'Stake saturation', sortable: true, align: 'center' },
{ id: 'delegated_on_iso_datetime', label: 'Delegated on', sortable: true, align: 'center' },
{ id: 'amount', label: 'Delegation', sortable: true, align: 'center' },
{ id: 'accumulated_rewards', label: 'Reward', sortable: true, align: 'center' },
];
function descendingComparator<T>(a: T, b: T, orderBy: keyof T) {
@@ -141,21 +142,40 @@ export const DelegationList: React.FC<{
items.sort(getComparator(order, orderBy)).map((item) => (
<TableRow key={item.node_identity}>
<TableCell>
<Link
target="_blank"
href={`${explorerUrl}/network-components/mixnode/${item.node_identity}`}
text={`${item.node_identity.slice(0, 6)}...${item.node_identity.slice(-6)}`}
color="text.primary"
noIcon
<CopyToClipboard
sx={{ fontSize: 16, mr: 1 }}
value={item.node_identity}
tooltip={
<>
Copy identity key <strong>{item.node_identity}</strong> to clipboard
</>
}
/>
<Tooltip
title={
<>
Click to view <strong>{item.node_identity}</strong> in the Network Explorer
</>
}
placement="right"
arrow
>
<Link
target="_blank"
href={`${explorerUrl}/network-components/mixnode/${item.node_identity}`}
text={`${item.node_identity.slice(0, 6)}...${item.node_identity.slice(-6)}`}
/>
</Tooltip>
</TableCell>
<TableCell>{!item.avg_uptime_percent ? '-' : `${item.avg_uptime_percent}%`}</TableCell>
<TableCell>{!item.profit_margin_percent ? '-' : `${item.profit_margin_percent}%`}</TableCell>
<TableCell>
<TableCell align="center">{!item.avg_uptime_percent ? '-' : `${item.avg_uptime_percent}%`}</TableCell>
<TableCell align="center">
{!item.profit_margin_percent ? '-' : `${item.profit_margin_percent}%`}
</TableCell>
<TableCell align="center">
{!item.stake_saturation ? '-' : `${Math.round(item.stake_saturation * 100000) / 1000}%`}
</TableCell>
<TableCell>{format(new Date(item.delegated_on_iso_datetime), 'dd/MM/yyyy')}</TableCell>
<TableCell>
<TableCell align="center">{format(new Date(item.delegated_on_iso_datetime), 'dd/MM/yyyy')}</TableCell>
<TableCell align="center">
<Tooltip
placement="right"
title={
@@ -196,7 +216,7 @@ export const DelegationList: React.FC<{
<span style={{ cursor: 'pointer' }}>{`${item.amount.amount} ${item.amount.denom}`}</span>
</Tooltip>
</TableCell>
<TableCell>
<TableCell align="center">
{!item.accumulated_rewards
? '-'
: `${item.accumulated_rewards.amount} ${item.accumulated_rewards.denom}`}

Some files were not shown because too many files have changed in this diff Show More