Compare commits

...

31 Commits

Author SHA1 Message Date
Mark Sinclair 3e517b461c nym-wallet: release v1.0.7 with dark mode and new gas fee simulations 2022-07-11 14:45:52 +01:00
fmtabbara e42e4ddb9b remove hidden backdrops 2022-07-08 23:20:30 +01:00
fmtabbara a31f51b7ca only set stored mode in wallet if not undefined 2022-07-08 23:01:17 +01:00
Jon Häggblad c41a600bcb all: add clap test asserts (#1452) 2022-07-08 21:04:09 +02:00
Fouad 7aa34391fb use tauri forage (#1451)
* use tauri forage

* commit lock file

* update to set in storage directly in set state
2022-07-08 18:41:22 +01:00
fmtabbara f72b56d07e remove old fees ui 2022-07-08 17:06:08 +01:00
Jon Häggblad 44ddbf9ac3 nym-connect: update package versions to resolve conflict (#1450)
* nym-connect: update package versions to resolve conflict

* nym-connect: add pre-target to build shared packages when building storybook

Co-authored-by: Mark Sinclair <mmsinclair@gmail.com>
2022-07-08 16:34:25 +01:00
fmtabbara 27c377860a remove old property useBootstrapper. See https://newreleases.io/project/github/tauri-apps/tauri/release/tauri-utils-v1.0.0-rc.5 2022-07-08 15:33:06 +01:00
Mark Sinclair 9a3f60f224 nym-connect: at pre-build target that builds the shared packages 2022-07-08 14:16:10 +01:00
Gala 5776600db3 Wallet: Dark mode for Balance page, Side nav. bar & theming in Storybook. (#1396)
* dark mode for balance page

* nav bar dark mode styles

* test wallet stories modes view

* Action Modals and Simple Modal stories

* Redeem Modals stories

* Delegation Modals

* mode handling

* remove not used code

* updating modal historial after update with develop

* wallet: more modals refactor for storybook

* more refactor

* non use redundant boolean value

* dark mode for balance page

* nav bar dark mode styles

* test wallet stories modes view

* adding missing import

* fixing modals in stories

* Action Modals and Simple Modal stories

* Redeem Modals stories

* Delegation Modals

* wrapper dialog content with a paper
2022-07-08 14:08:54 +01:00
Mark Sinclair e851ad8b27 Nym Connect: move into the workspace (#1449)
* nym-connect: use monorepo workspace and dependent packages

* nym-wallet: fix up dependencies

* nym-connect: ip and port copy values to clipboard with UI hint in tooltip

* nym-connect: update icons and word mark

* nym-connect: add automatic updater

* nym-connect: update CHANGELOG
2022-07-08 12:31:30 +01:00
Jon Häggblad 2e0e319511 validator-api: fix typo in apy estimate endpoint (#1447)
* validator-api: fix bug in apy calculations

* validator-api: missing variable
2022-07-07 13:41:51 +02:00
Jon Häggblad 6dd3e36acb mixnode & gateway: move detailed build info back to --version (#1445)
* mixnode and gatway: move build info back to version instead of help

* changelog: add note
2022-07-07 13:16:46 +02:00
Jon Häggblad 40cbfccb73 client: upgrade clap to latest and use declarative form (#1444)
* socks5: upgrade clap to latest and use declarateive derive form

* socks5: rustfmt and clippy

* socks5: missing doc strings

* socks5: default values for eth arguments

* socks5: tidy

* client: upgrade native client to latest clap and declerative form

* changelog: add note
2022-07-07 11:44:08 +02:00
Fouad b49681ef1d Feature/wallet bonding with fees rebase (#1428)
* use confirmation modal for successful/failed unbond

* show loading modal when getting fees

* use new back button

* use new confirmation modal component
2022-07-06 22:34:46 +01:00
Fouad 7e9409bbef Feature/send with simulated fee (#1402)
* add simulate send function

* extract loading modal into its own component

* move appbar component into folder

* add additional prop for making modal list item text bold

* create new send UI and stories

* remove old send page

* use simulated fee in send request

* use new confirmation modal component
2022-07-06 21:08:40 +01:00
Fouad a11e674967 Wallet: Transfer token component with fee included (#1385)
* set up transfer token component with fee included

* open transfer modal after balance refresh

* create fee warning component

* use confirmation modal
2022-07-06 16:19:15 +01:00
Jon Häggblad 2f4e505aac connect: add export_keys tauri function (#1443)
* connect: tidy

* connect: add export_keys tauri function

* changelog: add note

* connect: remove unused
2022-07-06 14:02:40 +02:00
Jon Häggblad 0a4bbf2573 Merge pull request #1440 from nymtech/jon/chore/nym-connect/upgrade-tauri
nym-connect: upgrade tauri to 1.0.2
2022-07-05 21:33:12 +02:00
Jon Häggblad 2ae5ebb8cb nym-connect: upgrade rust tauri to 1.0.2 2022-07-05 21:03:03 +02:00
Jon Häggblad 11be1f8e3e nym-connect: upgrade frontend tauri to 1.0.2
yarn upgrade @tauri-apps/cli @tauri-apps/api --latest
2022-07-05 21:03:03 +02:00
Jon Häggblad 513baba3fd nym-connect: sort out error handling 2022-07-05 21:02:39 +02:00
Jon Häggblad 21b87f6af5 nym-connect: fix catching panic in init 2022-07-05 20:36:55 +02:00
Fouad fe59697aa9 Merge pull request #1435 from nymtech/feature/wallet-modal-back-button
Feature/wallet modal back button
2022-07-05 14:58:32 +01:00
Pierre Dommerc 566ceae325 feat(wallet): add confirmation modal component (#1434)
* feat(wallet): add confirmation modal component
2022-07-05 15:48:47 +02:00
fmtabbara 6bc4d32573 remove unused import 2022-07-05 14:25:53 +01:00
fmtabbara 1fc3c9f31c rename prop 2022-07-05 14:10:54 +01:00
fmtabbara 3cb08a410e fix conflicts 2022-07-05 13:59:46 +01:00
fmtabbara 1648aef1d3 add component story 2022-07-05 11:16:58 +01:00
fmtabbara d98b6b107c add back button option to simple modal component 2022-07-05 11:03:11 +01:00
Mark Sinclair d3db0fc2cd Update CODEOWNERS 2022-07-05 10:00:29 +01:00
184 changed files with 5496 additions and 18487 deletions
+4 -4
View File
@@ -19,10 +19,10 @@
Cargo.* @durch @futurechimp @jstuczyn @neacsu @octol
# JS rules:
*.js @mmsinclair @fmtabbara @Aid19801
*.ts @mmsinclair @fmtabbara @Aid19801
*.tsx @mmsinclair @fmtabbara @Aid19801
*.jsx @mmsinclair @fmtabbara @Aid19801
*.js @mmsinclair @fmtabbara
*.ts @mmsinclair @fmtabbara
*.tsx @mmsinclair @fmtabbara
*.jsx @mmsinclair @fmtabbara
# Something looking like possible documentation rules:
*.md @mfahampshire
+13 -4
View File
@@ -9,7 +9,9 @@ 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 select network requester and gateway ([#1427])
- nym-connect: add ability to export gateway keys as JSON
- nym-connect: add auto updater
- 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
@@ -22,7 +24,6 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
- validator-api: Added new endpoints for coconut spending flow and communications with coconut & multisig contracts ([#1261])
- validator-api: add `uptime`, `estimated_operator_apy`, `estimated_delegators_apy` to `/mixnodes/detailed` endpoint ([#1393])
- network-statistics: a new mixnet service that aggregates and exposes anonymized data about mixnet services ([#1328])
- wallet: when simulating gas costs, an automatic adjustment is being used ([#1388]).
- mixnode: Added basic mixnode hardware reporting to the HTTP API ([#1308]).
- validator-api: endpoint, in coconut mode, for returning the validator-api cosmos address ([#1404]).
@@ -38,12 +39,14 @@ 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
@@ -61,11 +64,17 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
[#1329]: https://github.com/nymtech/nym/pull/1329
[#1353]: https://github.com/nymtech/nym/pull/1353
[#1376]: https://github.com/nymtech/nym/pull/1376
[#1388]: https://github.com/nymtech/nym/pull/1388
[#1393]: https://github.com/nymtech/nym/pull/1393
[#1404]: https://github.com/nymtech/nym/pull/1404
[#1427]: https://github.com/nymtech/nym/pull/1427
## [nym-wallet-v1.0.7](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.7) (2022-06-21)
- wallet: dark mode
- wallet: when simulating gas costs, an automatic adjustment is being used ([#1388]).
[#1388]: https://github.com/nymtech/nym/pull/1388
## [nym-contracts-v1.0.1](https://github.com/nymtech/nym/tree/nym-contracts-v1.0.1) (2022-06-22)
### Added
Generated
+22 -16
View File
@@ -568,16 +568,16 @@ dependencies = [
[[package]]
name = "clap"
version = "3.1.8"
version = "3.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71c47df61d9e16dc010b55dba1952a57d8c215dbb533fd13cdd13369aac73b1c"
checksum = "190814073e85d238f31ff738fcb0bf6910cedeb73376c87cd69291028966fd83"
dependencies = [
"atty",
"bitflags",
"clap_derive",
"clap_lex",
"indexmap",
"lazy_static",
"os_str_bytes",
"once_cell",
"strsim 0.10.0",
"termcolor",
"textwrap 0.15.0",
@@ -585,9 +585,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "3.1.7"
version = "3.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3aab4734e083b809aaf5794e14e756d1c798d2c69c7f7de7a09a2f5214993c1"
checksum = "759bf187376e1afa7b85b959e6a664a3e7a95203415dba952ad19139e798f902"
dependencies = [
"heck 0.4.0",
"proc-macro-error",
@@ -596,6 +596,15 @@ 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"
@@ -884,7 +893,7 @@ dependencies = [
"async-trait",
"bip39",
"cfg-if 0.1.10",
"clap 3.1.8",
"clap 3.2.8",
"coconut-interface",
"credential-storage",
"credentials",
@@ -3049,7 +3058,7 @@ dependencies = [
name = "nym-client"
version = "1.0.1"
dependencies = [
"clap 2.34.0",
"clap 3.2.8",
"client-core",
"coconut-interface",
"config",
@@ -3089,7 +3098,7 @@ dependencies = [
"bandwidth-claim-contract",
"bip39",
"bs58",
"clap 3.1.8",
"clap 3.2.8",
"coconut-interface",
"colored",
"config",
@@ -3134,7 +3143,7 @@ version = "1.0.1"
dependencies = [
"anyhow",
"bs58",
"clap 3.1.8",
"clap 3.2.8",
"colored",
"config",
"crypto",
@@ -3216,7 +3225,7 @@ dependencies = [
name = "nym-socks5-client"
version = "1.0.1"
dependencies = [
"clap 2.34.0",
"clap 3.2.8",
"client-core",
"coconut-interface",
"config",
@@ -3500,9 +3509,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.10.0"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9"
checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1"
[[package]]
name = "oorandom"
@@ -3567,9 +3576,6 @@ 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"
+2 -1
View File
@@ -2,6 +2,7 @@
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"
@@ -19,7 +20,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 = "2.33.0" # for the command line arguments
clap = { version = "3.2.8", features = ["cargo", "derive"] }
dirs = "4.0"
dotenv = "0.15.0" # for obtaining environmental variables (only used for RUST_LOG for time being)
log = "0.4" # self explanatory
+88 -79
View File
@@ -1,88 +1,100 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use clap::{App, Arg, ArgMatches};
use clap::Args;
use client_core::config::GatewayEndpoint;
use config::NymConfig;
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,
use crate::{
client::config::Config,
commands::{override_config, OverrideConfig},
};
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)
);
#[cfg(all(feature = "eth", not(feature = "coconut")))]
use crate::commands::{DEFAULT_ETH_ENDPOINT, DEFAULT_ETH_PRIVATE_KEY};
app
#[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,
}
pub async fn execute(matches: ArgMatches<'static>) {
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) {
println!("Initialising client...");
let id = matches.value_of("id").unwrap(); // required for now
let id = &args.id;
let already_init = Config::default_config_file_path(Some(id)).exists();
if already_init {
@@ -95,7 +107,7 @@ pub async fn execute(matches: ArgMatches<'static>) {
// Usually you only register with the gateway on the first init, however you can force
// re-registering if wanted.
let user_wants_force_register = matches.is_present("force-register-gateway");
let user_wants_force_register = args.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).
@@ -103,14 +115,11 @@ pub async fn execute(matches: ArgMatches<'static>) {
let register_gateway = !already_init || user_wants_force_register;
// Attempt to use a user-provided gateway, if possible
let user_chosen_gateway_id = matches.value_of("gateway");
let user_chosen_gateway_id = args.gateway.as_deref();
let mut config = Config::new(id);
config = override_config(config, &matches);
if matches.is_present("fastmode") {
config.get_base_mut().set_high_default_traffic_volume();
}
let override_config_fields = OverrideConfig::from(args.clone());
config = override_config(config, override_config_fields);
let gateway = setup_gateway(id, register_gateway, user_chosen_gateway_id, &config).await;
config.get_base_mut().with_gateway_endpoint(gateway);
+123 -34
View File
@@ -2,14 +2,10 @@
// SPDX-License-Identifier: Apache-2.0
use crate::client::config::{Config, SocketType};
use clap::ArgMatches;
use clap::{Parser, Subcommand};
use network_defaults::DEFAULT_NETWORK;
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";
@@ -21,6 +17,86 @@ 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| {
@@ -32,45 +108,58 @@ fn parse_validators(raw: &str) -> Vec<Url> {
.collect()
}
pub(crate) fn override_config(mut config: Config, matches: &ArgMatches<'_>) -> Config {
if let Some(raw_validators) = matches.value_of("validators") {
pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Config {
if let Some(raw_validators) = args.validators {
config
.get_base_mut()
.set_custom_validator_apis(parse_validators(raw_validators));
.set_custom_validator_apis(parse_validators(&raw_validators));
}
if matches.is_present("disable-socket") {
if args.disable_socket {
config = config.with_socket(SocketType::None);
}
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);
if let Some(port) = args.port {
config = config.with_port(port);
}
#[cfg(all(not(feature = "eth"), not(feature = "coconut")))]
{
config
.get_base_mut()
.with_eth_endpoint(DEFAULT_ETH_ENDPOINT.to_string());
config
.get_base_mut()
.with_eth_private_key(DEFAULT_ETH_PRIVATE_KEY.to_string());
}
#[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);
}
config = config.with_port(port.unwrap());
}
#[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);
}
#[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);
}
if matches.is_present(ENABLED_CREDENTIALS_MODE_ARG_NAME) {
config.get_base_mut().with_disabled_credentials(false)
if args.fastmode {
config.get_base_mut().set_high_default_traffic_volume();
}
config
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn verify_cli() {
Cli::command().debug_assert();
}
}
+69 -59
View File
@@ -1,68 +1,77 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
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 crate::{
client::{config::Config, NymClient},
commands::{override_config, OverrideConfig},
};
use clap::{App, Arg, ArgMatches};
use clap::Args;
use config::NymConfig;
use log::*;
use version_checker::is_minor_version_compatible;
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));
#[derive(Args, Clone)]
pub(crate) struct Run {
/// Id of the nym-mixnet-client we want to run.
#[clap(long)]
id: String,
app
/// 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,
}
}
}
// this only checks compatibility between config the binary. It does not take into consideration
@@ -84,8 +93,8 @@ fn version_check(cfg: &Config) -> bool {
}
}
pub async fn execute(matches: ArgMatches<'static>) {
let id = matches.value_of("id").unwrap();
pub(crate) async fn execute(args: &Run) {
let id = &args.id;
let mut config = match Config::load_from_file(Some(id)) {
Ok(cfg) => cfg,
@@ -95,7 +104,8 @@ pub async fn execute(matches: ArgMatches<'static>) {
}
};
config = override_config(config, &matches);
let override_config_fields = OverrideConfig::from(args.clone());
config = override_config(config, override_config_fields);
if !version_check(&config) {
error!("failed the local version check");
+16 -18
View File
@@ -2,12 +2,13 @@
// SPDX-License-Identifier: Apache-2.0
use crate::client::config::{Config, MISSING_VALUE};
use clap::{App, Arg, ArgMatches};
use config::defaults::default_api_endpoints;
use config::NymConfig;
use config::{defaults::default_api_endpoints, NymConfig};
use version_checker::Version;
use clap::Args;
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) -> ! {
@@ -49,14 +50,11 @@ fn unsupported_upgrade(current_version: &Version, config_version: &Version) -> !
process::exit(1)
}
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),
)
#[derive(Args, Clone)]
pub(crate) struct Upgrade {
/// Id of the nym-client we want to upgrade
#[clap(long)]
id: String,
}
fn parse_config_version(config: &Config) -> Version {
@@ -95,7 +93,7 @@ fn parse_package_version() -> Version {
fn minor_0_12_upgrade(
mut config: Config,
_matches: &ArgMatches<'_>,
_matches: &Upgrade,
config_version: &Version,
package_version: &Version,
) -> Config {
@@ -131,7 +129,7 @@ fn minor_0_12_upgrade(
config
}
fn do_upgrade(mut config: Config, matches: &ArgMatches<'_>, package_version: &Version) {
fn do_upgrade(mut config: Config, args: &Upgrade, package_version: &Version) {
loop {
let config_version = parse_config_version(&config);
@@ -143,7 +141,7 @@ fn do_upgrade(mut config: Config, matches: &ArgMatches<'_>, package_version: &Ve
config = match config_version.major {
0 => match config_version.minor {
9 | 10 => outdated_upgrade(&config_version, package_version),
11 => minor_0_12_upgrade(config, matches, &config_version, package_version),
11 => minor_0_12_upgrade(config, args, &config_version, package_version),
_ => unsupported_upgrade(&config_version, package_version),
},
_ => unsupported_upgrade(&config_version, package_version),
@@ -151,10 +149,10 @@ fn do_upgrade(mut config: Config, matches: &ArgMatches<'_>, package_version: &Ve
}
}
pub fn execute(matches: &ArgMatches<'_>) {
pub(crate) fn execute(args: &Upgrade) {
let package_version = parse_package_version();
let id = matches.value_of("id").unwrap();
let id = &args.id;
let existing_config = Config::load_from_file(Some(id)).unwrap_or_else(|err| {
eprintln!("failed to load existing config file! - {:?}", err);
@@ -167,5 +165,5 @@ pub fn execute(matches: &ArgMatches<'_>) {
}
// here be upgrade path to 0.9.X and beyond based on version number from config
do_upgrade(existing_config, matches, &package_version)
do_upgrade(existing_config, args, &package_version)
}
+3 -60
View File
@@ -1,8 +1,7 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use clap::{crate_version, App, ArgMatches};
use network_defaults::DEFAULT_NETWORK;
use clap::{crate_version, Parser};
pub mod client;
pub mod commands;
@@ -14,30 +13,8 @@ async fn main() {
setup_logging();
println!("{}", banner());
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"
let args = commands::Cli::parse();
commands::execute(&args).await;
}
fn banner() -> String {
@@ -57,40 +34,6 @@ 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") {
+2 -1
View File
@@ -2,6 +2,7 @@
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"
@@ -10,7 +11,7 @@ name = "nym_socks5"
path = "src/lib.rs"
[dependencies]
clap = "2.33.0"
clap = { version = "3.2.8", features = ["cargo", "derive"] }
dirs = "4.0"
dotenv = "0.15.0"
futures = "0.3"
+8 -5
View File
@@ -287,12 +287,15 @@ impl NymClient {
pub async fn run_and_listen(&mut self, mut receiver: Socks5ControlMessageReceiver) {
self.start().await;
tokio::select! {
message = receiver.next() => match message {
Some(Socks5ControlMessage::Stop) => {
log::info!("Received: {:?}", message);
log::info!("Shutting down");
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"),
}
None => log::info!("none"),
}
}
}
+88 -82
View File
@@ -1,91 +1,100 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use clap::{App, Arg, ArgMatches};
use clap::Args;
use client_core::config::GatewayEndpoint;
use config::NymConfig;
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,
use crate::{
client::config::Config,
commands::{override_config, OverrideConfig},
};
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)
);
#[cfg(all(feature = "eth", not(feature = "coconut")))]
use crate::commands::{DEFAULT_ETH_ENDPOINT, DEFAULT_ETH_PRIVATE_KEY};
app
#[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,
}
pub async fn execute(matches: ArgMatches<'static>) {
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) {
println!("Initialising client...");
let id = matches.value_of("id").unwrap(); // required for now
let provider_address = matches.value_of("provider").unwrap();
let id = &args.id;
let provider_address = &args.provider;
let already_init = Config::default_config_file_path(Some(id)).exists();
if already_init {
@@ -98,7 +107,7 @@ pub async fn execute(matches: ArgMatches<'static>) {
// Usually you only register with the gateway on the first init, however you can force
// re-registering if wanted.
let user_wants_force_register = matches.is_present("force-register-gateway");
let user_wants_force_register = args.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).
@@ -106,14 +115,11 @@ pub async fn execute(matches: ArgMatches<'static>) {
let register_gateway = !already_init || user_wants_force_register;
// Attempt to use a user-provided gateway, if possible
let user_chosen_gateway_id = matches.value_of("gateway");
let user_chosen_gateway_id = args.gateway.as_deref();
let mut config = Config::new(id, provider_address);
config = override_config(config, &matches);
if matches.is_present("fastmode") {
config.get_base_mut().set_high_default_traffic_volume();
}
let override_config_fields = OverrideConfig::from(args.clone());
config = override_config(config, override_config_fields);
let gateway = setup_gateway(id, register_gateway, user_chosen_gateway_id, &config).await;
config.get_base_mut().with_gateway_endpoint(gateway);
+121 -33
View File
@@ -2,18 +2,14 @@
// SPDX-License-Identifier: Apache-2.0
use crate::client::config::Config;
use clap::ArgMatches;
use clap::{Parser, Subcommand};
use network_defaults::DEFAULT_NETWORK;
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";
@@ -21,6 +17,85 @@ 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| {
@@ -32,41 +107,54 @@ fn parse_validators(raw: &str) -> Vec<Url> {
.collect()
}
pub(crate) fn override_config(mut config: Config, matches: &ArgMatches<'_>) -> Config {
if let Some(raw_validators) = matches.value_of("validators") {
pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Config {
if let Some(raw_validators) = args.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) = 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);
if let Some(port) = args.port {
config = config.with_port(port);
}
#[cfg(all(not(feature = "eth"), not(feature = "coconut")))]
{
config
.get_base_mut()
.with_eth_endpoint(DEFAULT_ETH_ENDPOINT.to_string());
config
.get_base_mut()
.with_eth_private_key(DEFAULT_ETH_PRIVATE_KEY.to_string());
}
#[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);
}
config = config.with_port(port.unwrap());
}
#[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);
}
#[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);
}
if matches.is_present(ENABLED_CREDENTIALS_MODE_ARG_NAME) {
config.get_base_mut().with_disabled_credentials(false)
if args.fastmode {
config.get_base_mut().set_high_default_traffic_volume();
}
config
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn verify_cli() {
Cli::command().debug_assert();
}
}
+79 -69
View File
@@ -1,74 +1,80 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
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 crate::{
client::{config::Config, NymClient},
commands::{override_config, OverrideConfig},
};
use clap::{App, Arg, ArgMatches};
use clap::Args;
use config::NymConfig;
use log::*;
use version_checker::is_minor_version_compatible;
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));
#[derive(Args, Clone)]
pub(crate) struct Run {
/// Id of the nym-mixnet-client we want to run.
#[clap(long)]
id: String,
app
/// 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,
}
}
}
// this only checks compatibility between config the binary. It does not take into consideration
@@ -76,8 +82,13 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
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 {
warn!("The mixnode binary has different version than what is specified in config file! {} and {}", binary_version, config_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 is_minor_version_compatible(binary_version, config_version) {
info!("but they are still semver compatible. However, consider running the `upgrade` command");
true
@@ -85,13 +96,11 @@ 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 async fn execute(matches: ArgMatches<'static>) {
let id = matches.value_of("id").unwrap();
pub(crate) async fn execute(args: &Run) {
let id = &args.id;
let mut config = match Config::load_from_file(Some(id)) {
Ok(cfg) => cfg,
@@ -101,7 +110,8 @@ pub async fn execute(matches: ArgMatches<'static>) {
}
};
config = override_config(config, &matches);
let override_config_fields = OverrideConfig::from(args.clone());
config = override_config(config, override_config_fields);
if !version_check(&config) {
error!("failed the local version check");
+20 -23
View File
@@ -2,13 +2,13 @@
// SPDX-License-Identifier: Apache-2.0
use crate::client::config::{Config, MISSING_VALUE};
use clap::{App, Arg, ArgMatches};
use config::defaults::default_api_endpoints;
use config::NymConfig;
use std::fmt::Display;
use std::process;
use config::{defaults::default_api_endpoints, NymConfig};
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,14 +49,11 @@ fn unsupported_upgrade(current_version: &Version, config_version: &Version) -> !
process::exit(1)
}
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),
)
#[derive(Args, Clone)]
pub(crate) struct Upgrade {
/// Id of the nym-client we want to upgrade
#[clap(long)]
id: String,
}
fn parse_config_version(config: &Config) -> Version {
@@ -95,7 +92,7 @@ fn parse_package_version() -> Version {
fn minor_0_12_upgrade(
mut config: Config,
_matches: &ArgMatches<'_>,
_args: &Upgrade,
config_version: &Version,
package_version: &Version,
) -> Config {
@@ -131,30 +128,30 @@ fn minor_0_12_upgrade(
config
}
fn do_upgrade(mut config: Config, matches: &ArgMatches<'_>, package_version: Version) {
fn do_upgrade(mut config: Config, args: &Upgrade, 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, matches, &config_version, &package_version),
_ => unsupported_upgrade(&config_version, &package_version),
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),
},
_ => unsupported_upgrade(&config_version, &package_version),
_ => unsupported_upgrade(&config_version, package_version),
}
}
}
pub fn execute(matches: &ArgMatches<'_>) {
pub(crate) fn execute(args: &Upgrade) {
let package_version = parse_package_version();
let id = matches.value_of("id").unwrap();
let id = &args.id;
let existing_config = Config::load_from_file(Some(id)).unwrap_or_else(|err| {
eprintln!("failed to load existing config file! - {:?}", err);
@@ -167,5 +164,5 @@ pub fn execute(matches: &ArgMatches<'_>) {
}
// here be upgrade path to 0.9.X and beyond based on version number from config
do_upgrade(existing_config, matches, package_version)
do_upgrade(existing_config, args, &package_version)
}
+3 -60
View File
@@ -1,8 +1,7 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use clap::{crate_version, App, ArgMatches};
use network_defaults::DEFAULT_NETWORK;
use clap::{crate_version, Parser};
pub mod client;
mod commands;
@@ -14,30 +13,8 @@ async fn main() {
setup_logging();
println!("{}", banner());
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"
let args = commands::Cli::parse();
commands::execute(&args).await;
}
fn banner() -> String {
@@ -57,40 +34,6 @@ 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") {
+19 -5
View File
@@ -9,15 +9,15 @@ mod commands;
mod config;
mod node;
static LONG_ABOUT: OnceCell<String> = OnceCell::new();
static LONG_VERSION: OnceCell<String> = OnceCell::new();
// Helper for passing LONG_ABOUT to clap
fn long_about() -> &'static str {
LONG_ABOUT.get().expect("Failed to get long about text")
fn long_version_static() -> &'static str {
LONG_VERSION.get().expect("Failed to get long about text")
}
#[derive(Parser)]
#[clap(author = "Nymtech", version, about, long_about = Some(long_about()))]
#[clap(author = "Nymtech", version, about, long_version = long_version_static())]
struct Cli {
#[clap(subcommand)]
command: commands::Commands,
@@ -28,7 +28,7 @@ async fn main() {
dotenv::dotenv().ok();
setup_logging();
println!("{}", banner());
LONG_ABOUT
LONG_VERSION
.set(long_version())
.expect("Failed to set long about text");
@@ -107,3 +107,17 @@ 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();
}
}
+16 -5
View File
@@ -13,16 +13,16 @@ mod config;
mod node;
lazy_static! {
pub static ref LONG_ABOUT: String = long_version();
pub static ref LONG_VERSION: String = long_version();
}
// Helper for passing LONG_ABOUT to clap
fn long_about() -> &'static str {
&LONG_ABOUT
// Helper for passing LONG_VERSION to clap
fn long_version_static() -> &'static str {
&LONG_VERSION
}
#[derive(Parser)]
#[clap(author = "Nymtech", version, about, long_about = Some(long_about()))]
#[clap(author = "Nymtech", version, about, long_version = long_version_static())]
struct Cli {
#[clap(subcommand)]
command: commands::Commands,
@@ -106,3 +106,14 @@ 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();
}
}
+3 -107
View File
@@ -1,112 +1,8 @@
{
"env": {
"browser": true,
"es6": true,
"node": true,
"jest": true
},
"parserOptions": {
"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"
"@nymproject/eslint-config-react-typescript"
],
"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"]
}
}
"parserOptions": {
"project": "./tsconfig.eslint.json"
}
}
+56 -10
View File
@@ -1,14 +1,60 @@
/* 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"
],
framework: "@storybook/react",
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;
},
features: {
emotionAlias: false,
},
}
};
@@ -0,0 +1,8 @@
/**
* 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,
};
@@ -0,0 +1,8 @@
/**
* 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,
}
@@ -0,0 +1,8 @@
/**
* 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,
}
@@ -0,0 +1,14 @@
/**
* 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.`));
});
},
};
@@ -0,0 +1,10 @@
/**
* 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,
}
}
+950 -884
View File
File diff suppressed because it is too large Load Diff
+45 -27
View File
@@ -4,33 +4,36 @@
"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",
"@types/react-dom": "^17.0.9",
"bs58": "^4.0.1",
"@nymproject/react": "^1.0.0",
"@tauri-apps/api": "^1.0.2",
"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",
@@ -44,48 +47,63 @@
"@babel/plugin-transform-async-to-generator": "^7.14.5",
"@babel/preset-env": "^7.15.0",
"@babel/preset-react": "^7.14.5",
"@storybook/addon-actions": "^6.4.18",
"@storybook/addon-essentials": "^6.4.18",
"@storybook/addon-links": "^6.4.18",
"@storybook/react": "^6.4.18",
"@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/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",
"@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",
"@types/semver": "^7.3.8",
"@typescript-eslint/eslint-plugin": "4.31.0",
"@typescript-eslint/parser": "4.31.0",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.13.0",
"@typescript-eslint/parser": "^5.13.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": "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",
"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",
"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",
"typescript": "^4.4.2",
"ts-loader": "^9.2.5",
"tsconfig-paths-webpack-plugin": "^3.5.2",
"typescript": "^4.6.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"
}
}
+10 -9
View File
@@ -13,29 +13,30 @@ 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.0-rc.7", features = [] }
tauri-build = { version = "^1.0.2", features = [] }
tauri-codegen = "=1.0.0-rc.5"
tauri-macros = "=1.0.0-rc.5"
tauri-codegen = "^1.0.2"
tauri-macros = "^1.0.2"
[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"
rand = "0.7"
log = "0.4"
pretty_env_logger = "0.4.0"
rand = "0.8"
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tauri = { version = "=1.0.0-rc.8", features = ["ayatana-tray", "shell-open", "system-tray"] }
tap = "1.0.1"
tauri = { version = "^1.0.2", features = ["clipboard-write-text", "shell-open", "system-tray", "updater"] }
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: 11 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 60 KiB

+1 -1
View File
@@ -1,3 +1,3 @@
fn main() {
tauri_build::build()
tauri_build::build();
}
+32 -32
View File
@@ -1,46 +1,41 @@
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, state::State};
use crate::{
error::{BackendError, Result},
state::State,
};
pub static SOCKS5_CONFIG_ID: &str = "nym-connect";
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 append_config_id(gateway_id: &str) -> String {
pub fn socks5_config_id_appended_with(gateway_id: &str) -> Result<String> {
use std::fmt::Write as _;
let mut id = SOCKS5_CONFIG_ID.to_string();
write!(id, "-{}", gateway_id).expect("Failed to set config id");
id
write!(id, "-{}", gateway_id)?;
Ok(id)
}
#[tauri::command]
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))
pub async fn get_config_id(state: tauri::State<'_, Arc<RwLock<State>>>) -> Result<String> {
state.read().await.get_config_id()
}
#[tauri::command]
pub async fn get_config_file_location(
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<String, BackendError> {
) -> Result<String> {
let id = get_config_id(state).await?;
Ok(Config::config_file_location(&id)
.to_string_lossy()
@@ -76,21 +71,26 @@ impl Config {
self.socks5.get_base_mut()
}
pub async fn init(service_provider: &str, chosen_gateway_id: &str) -> Result<(), BackendError> {
info!("Initialising...");
pub async fn init(service_provider: &str, chosen_gateway_id: &str) -> Result<()> {
log::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.
std::panic::catch_unwind(move || {
futures::executor::block_on(init_socks5(service_provider, chosen_gateway_id));
// 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 },
)
})
.map_err(|_| BackendError::InitializationPanic)?;
.join()
.map_err(|_| BackendError::InitializationPanic)??;
info!("Configuration saved 🚀");
log::info!("Configuration saved 🚀");
Ok(())
}
@@ -99,11 +99,11 @@ impl Config {
}
}
pub async fn init_socks5(provider_address: String, chosen_gateway_id: String) {
log::info!("Initialising client...");
pub async fn init_socks5_config(provider_address: String, chosen_gateway_id: String) -> Result<()> {
log::trace!("Initialising client...");
// Append the gateway id to the name id that we store the config under
let id = append_config_id(&chosen_gateway_id);
let id = socks5_config_id_appended_with(&chosen_gateway_id)?;
log::debug!(
"Attempting to use config file location: {}",
@@ -145,10 +145,9 @@ pub async fn init_socks5(provider_address: String, chosen_gateway_id: String) {
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)
.expect("Failed to save the config file");
config.get_socks5().save_to_file(None).tap_err(|_| {
log::warn!("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());
@@ -166,9 +165,10 @@ pub async fn init_socks5(provider_address: String, chosen_gateway_id: String) {
"Service provider port: {}",
config.get_socks5().get_listening_port()
);
info!("Client configuration completed.");
log::info!("Client configuration completed.");
client_core::init::show_address(config.get_base());
Ok(())
}
// TODO: deduplicate with same functions in other client
+40 -6
View File
@@ -4,30 +4,64 @@ 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) -> Result<S::Ok, S::Error>
fn serialize<S>(&self, serializer: S) -> std::result::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>;
+2 -1
View File
@@ -43,8 +43,9 @@ fn main() {
crate::operations::connection::connect::set_service_provider,
crate::operations::connection::connect::start_connecting,
crate::operations::connection::disconnect::start_disconnecting,
crate::operations::window::hide_window,
crate::operations::directory::get_services,
crate::operations::export::export_keys,
crate::operations::window::hide_window,
])
.menu(Menu::new().add_default_app_submenu_if_macos())
.system_tray(create_tray_menu())
+13
View File
@@ -1,3 +1,5 @@
use core::fmt;
use serde::{Deserialize, Serialize};
#[cfg_attr(test, derive(ts_rs::TS))]
@@ -23,6 +25,17 @@ 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,4 +1,8 @@
use crate::{error::BackendError, models::ConnectResult, tasks::start_disconnect_listener, State};
use crate::{
error::{BackendError, Result},
models::ConnectResult,
tasks, State,
};
use std::sync::Arc;
use tokio::sync::RwLock;
@@ -6,30 +10,23 @@ use tokio::sync::RwLock;
pub async fn start_connecting(
state: tauri::State<'_, Arc<RwLock<State>>>,
window: tauri::Window<tauri::Wry>,
) -> Result<ConnectResult, BackendError> {
) -> Result<ConnectResult> {
let status_receiver = {
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?
let mut state_w = state.write().await;
state_w.start_connecting(&window).await?
};
// Setup task for checking status
let state = state.inner().clone();
start_disconnect_listener(state, window, status_receiver);
tasks::start_disconnect_listener(state, window, status_receiver);
Ok(ConnectResult {
// WIP(JON): fixme
address: "Test".to_string(),
address: "PLACEHOLDER".to_string(),
})
}
#[tauri::command]
pub async fn get_service_provider(
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<String, BackendError> {
pub async fn get_service_provider(state: tauri::State<'_, Arc<RwLock<State>>>) -> Result<String> {
let guard = state.read().await;
guard
.get_service_provider()
@@ -41,7 +38,7 @@ pub async fn get_service_provider(
pub async fn set_service_provider(
service_provider: String,
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<(), BackendError> {
) -> Result<()> {
log::trace!("Setting service_provider: {service_provider}");
let mut guard = state.write().await;
guard.set_service_provider(service_provider);
@@ -49,9 +46,7 @@ pub async fn set_service_provider(
}
#[tauri::command]
pub async fn get_gateway(
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<String, BackendError> {
pub async fn get_gateway(state: tauri::State<'_, Arc<RwLock<State>>>) -> Result<String> {
let guard = state.read().await;
guard
.get_gateway()
@@ -63,7 +58,7 @@ pub async fn get_gateway(
pub async fn set_gateway(
gateway: String,
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<(), BackendError> {
) -> Result<()> {
log::trace!("Setting gateway: {gateway}");
let mut guard = state.write().await;
guard.set_gateway(gateway);
@@ -1,4 +1,4 @@
use crate::error::BackendError;
use crate::error::Result;
use crate::models::ConnectResult;
use crate::State;
use std::sync::Arc;
@@ -8,13 +8,12 @@ use tokio::sync::RwLock;
pub async fn start_disconnecting(
state: tauri::State<'_, Arc<RwLock<State>>>,
window: tauri::Window<tauri::Wry>,
) -> Result<ConnectResult, BackendError> {
) -> Result<ConnectResult> {
let mut guard = state.write().await;
guard.start_disconnecting(&window).await;
guard.start_disconnecting(&window).await?;
Ok(ConnectResult {
// WIP(JON): fixme
address: "Test".to_string(),
address: "PLACEHOLDER".to_string(),
})
}
@@ -1,11 +1,11 @@
use crate::error::BackendError;
use crate::error::Result;
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>, BackendError> {
pub async fn get_services() -> Result<Vec<DirectoryService>> {
let res = reqwest::get(SERVICE_PROVIDER_WELLKNOWN_URL)
.await?
.json::<Vec<DirectoryService>>()
@@ -0,0 +1,73 @@
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,3 +1,4 @@
pub mod connection;
pub mod directory;
pub mod export;
pub mod window;
+80 -37
View File
@@ -1,24 +1,35 @@
use std::time::Duration;
use ::config::NymConfig;
use futures::SinkExt;
use tap::TapFallible;
use tauri::Manager;
use nym_socks5::client::{Socks5ControlMessage, Socks5ControlMessageSender};
use nym_socks5::client::{
config::Config as Socks5Config, Socks5ControlMessage, Socks5ControlMessageSender,
};
use crate::{
config::append_config_id,
error::BackendError,
config::{self, socks5_config_id_appended_with},
error::{BackendError, Result},
models::{
AppEventConnectionStatusChangedPayload, ConnectionStatusKind,
APP_EVENT_CONNECTION_STATUS_CHANGED,
},
tasks::{start_nym_socks5_client, StatusReceiver},
tasks::{self, 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>,
}
@@ -38,13 +49,15 @@ 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 },
)
.unwrap();
.tap_err(|err| log::warn!("{err}"))
.ok();
}
pub fn get_service_provider(&self) -> &Option<String> {
@@ -63,25 +76,28 @@ impl State {
self.gateway = Some(gateway);
}
pub async fn init_config(&self) -> Result<(), BackendError> {
let service_provider = self
.service_provider
/// 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()
.as_ref()
.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
.ok_or(BackendError::CouldNotGetIdWithoutGateway)
.and_then(|gateway_id| socks5_config_id_appended_with(gateway_id))
}
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, BackendError> {
log::info!("Connecting");
) -> Result<StatusReceiver> {
self.set_state(ConnectionStatusKind::Connecting, window);
self.status = ConnectionStatusKind::Connecting;
// Setup configuration by writing to file
if let Err(err) = self.init_config().await {
@@ -91,40 +107,67 @@ 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 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;
let status_receiver = self.start_nym_socks5_client().await?;
self.set_state(ConnectionStatusKind::Connected, window);
Ok(status_receiver)
}
pub async fn start_disconnecting(&mut self, window: &tauri::Window<tauri::Wry>) {
log::info!("Disconnecting");
/// 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<()> {
self.set_state(ConnectionStatusKind::Disconnecting, window);
self.status = ConnectionStatusKind::Disconnecting;
// Send shutdown message
if let Some(ref mut sender) = self.socks5_client_sender {
sender.send(Socks5ControlMessage::Stop).await.unwrap();
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)
}
}
}
pub async fn mark_disconnected(&mut self, window: &tauri::Window<tauri::Wry>) {
log::info!("Disconnected");
self.status = ConnectionStatusKind::Disconnected;
/// 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>) {
self.set_state(ConnectionStatusKind::Disconnected, window);
}
}
+24 -15
View File
@@ -1,34 +1,36 @@
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::Socks5ControlMessageSender;
use nym_socks5::client::{config::Config as Socks5Config, Socks5ControlMessageSender};
use crate::state::State;
use crate::{error::Result, 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,
) -> (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();
) -> 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"))?;
let used_gateway = config.get_base().get_gateway_endpoint().clone();
let mut socks5_client = Socks5NymClient::new(config);
info!("Starting socks5 client");
log::info!("Starting socks5 client");
// Channel to send control messages to the socks5 client
let (socks5_ctrl_tx, socks5_ctrl_rx) = mpsc::unbounded();
@@ -38,19 +40,24 @@ 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(|| {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async move {
socks5_client.run_and_listen(socks5_ctrl_rx).await;
});
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 });
log::info!("SOCKS5 task finished");
socks5_status_tx.send(Socks5StatusMessage::Stopped).unwrap();
socks5_status_tx
.send(Socks5StatusMessage::Stopped)
.expect("Failed to send status message back to main task");
});
(socks5_ctrl_tx, used_gateway, socks5_status_rx)
Ok((socks5_ctrl_tx, socks5_status_rx, used_gateway))
}
/// 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>,
@@ -64,10 +71,12 @@ 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).await;
state_w.mark_disconnected(&window);
});
}
+9 -1
View File
@@ -48,11 +48,19 @@
}
},
"updater": {
"active": false
"active": true,
"endpoints": [
"https://nymtech.net/.wellknown/connect/updater.json"
],
"dialog": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IENCNzQ2M0E5N0VFODE2NApSV1JrZ2U2WE9rYTNETTg1OTBKdE5uWUEra0hML2syOVUvQ2lxZmFZRzZ1T3NWbGM0eVRzUTVhVwo="
},
"allowlist": {
"shell": {
"open": true
},
"clipboard": {
"writeText": true
}
},
"windows": [
@@ -1,5 +1,6 @@
import React from 'react';
import { Box } from '@mui/material';
import { NymWordmark } from '@nymproject/react/logo/NymWordmark';
export const AppWindowFrame: React.FC = ({ children }) => (
<Box
@@ -13,11 +14,7 @@ export const AppWindowFrame: React.FC = ({ children }) => (
}}
>
<Box display="flex" justifyContent="space-between" alignItems="center">
<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>
<NymWordmark width={22} />
</Box>
{children}
</Box>
+76 -21
View File
@@ -1,6 +1,8 @@
import { Box, Tooltip, Typography } from '@mui/material';
import { Box, Stack, 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': {
@@ -16,23 +18,76 @@ export const IpAddressAndPort: React.FC<{
label: string;
ipAddress: string;
port: number;
}> = ({ 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>
);
}> = ({ 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>
);
};
-10
View File
@@ -1,10 +0,0 @@
import React from 'react';
import Logo from '../images/logo-background.svg';
const imgSize = {
small: 40,
medium: 80,
large: 120,
};
export const NymLogo = ({ size = 'medium' }: { size?: 'small' | 'medium' | 'large' }) => <Logo width={imgSize[size]} />;
+3 -2
View File
@@ -1,7 +1,8 @@
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
import { DateTime } from 'luxon';
import { invoke } from '@tauri-apps/api/tauri';
import { listen, UnlistenFn } from '@tauri-apps/api/event';
import { invoke } from '@tauri-apps/api';
import type { UnlistenFn } from '@tauri-apps/api/event';
import { listen } from '@tauri-apps/api/event';
import { ConnectionStatusKind } from '../types';
import { ConnectionStatsItem } from '../components/ConnectionStats';
import { ServiceProvider, Services } from '../types/directory';
@@ -1,41 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 349 KiB

-3
View File
@@ -1,3 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 470 B

-1
View File
@@ -1 +0,0 @@
// TODO
-10
View File
@@ -1,10 +0,0 @@
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,6 +26,7 @@ export const Mock: ComponentStory<typeof AppWindowFrame> = () => {
id: 'nym-keybase',
description: 'Nym Keybase Service Provider',
address: '1234.5678',
gateway: 'abcedf',
},
],
},
+20
View File
@@ -0,0 +1,20 @@
{
"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"
]
}
+5 -3
View File
@@ -1,5 +1,4 @@
{
"compileOnSave": false,
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
@@ -15,7 +14,10 @@
"isolatedModules": false,
"jsx": "react-jsx",
"sourceMap": true,
"baseUrl": "."
"baseUrl": ".",
"paths": {
"@assets/*": ["../assets/*"]
}
},
"exclude": ["node_modules", "dist", "jest.config.js", "webpack.config.js", "webpack.prod.js", "webpack.common.js"]
"exclude": ["node_modules", "dist", "jest.config.js", "webpack.config.js", "webpack.prod.js", "webpack.common.js", "target"]
}
+14 -46
View File
@@ -1,50 +1,18 @@
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FaviconsWebpackPlugin = require('favicons-webpack-plugin')
const Dotenv = require('dotenv-webpack')
var path = require('path')
const path = require('path');
const { mergeWithRules } = require('webpack-merge');
const { webpackCommon } = require('@nymproject/webpack');
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.exports = mergeWithRules({
module: {
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',
},
],
rules: {
test: 'match',
use: 'replace',
},
},
resolve: {
extensions: ['*', '.js', '.jsx', '.ts', '.tsx'],
})(webpackCommon(__dirname, 'public/index.html'), {
entry: path.resolve(__dirname, 'src/index.tsx'),
output: {
path: path.resolve(__dirname, 'dist'),
publicPath: '/',
},
}
});
+55 -6
View File
@@ -1,10 +1,59 @@
const common = require('./webpack.common')
const { merge } = require('webpack-merge')
var path = require('path')
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');
module.exports = merge(common, {
module.exports = mergeWithRules({
module: {
rules: {
test: 'match',
use: 'replace',
},
},
})(commonConfig, {
mode: 'development',
entry: path.resolve(__dirname, '/src/index'),
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'],
},
},
devServer: {
port: 9000,
compress: true,
@@ -14,4 +63,4 @@ module.exports = merge(common, {
overlay: false,
},
},
})
});
+4 -4
View File
@@ -1,6 +1,6 @@
var path = require('path')
const common = require('./webpack.common')
const { default: merge } = require('webpack-merge')
const path = require('path');
const { default: merge } = require('webpack-merge');
const common = require('./webpack.common');
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
+16 -6
View File
@@ -4,7 +4,7 @@
*/
module.exports = {
invoke: (operation, args) => {
switch(operation) {
switch (operation) {
case 'get_balance': {
return {
amount: {
@@ -12,20 +12,30 @@ 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.`));
});
},
}
};
+41 -5
View File
@@ -1,19 +1,55 @@
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) => (
<NymWalletThemeWithMode mode="light">
<Story {...context} />
</NymWalletThemeWithMode>
<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>
);
export const decorators = [withThemeProvider];
+21
View File
@@ -0,0 +1,21 @@
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%' };
};
+1 -1
View File
@@ -3056,7 +3056,7 @@ dependencies = [
[[package]]
name = "nym_wallet"
version = "1.0.6"
version = "1.0.7"
dependencies = [
"aes-gcm",
"argon2 0.3.4",
+4 -3
View File
@@ -21,7 +21,6 @@
"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",
@@ -32,8 +31,8 @@
"@nymproject/mui-theme": "^1.0.0",
"@nymproject/react": "^1.0.0",
"@nymproject/types": "^1.0.0",
"@storybook/react": "^6.5.8",
"@tauri-apps/api": "^1.0.0-rc.1",
"@tauri-apps/api": "^1.0.2",
"@tauri-apps/tauri-forage": "^1.0.0-beta.2",
"bs58": "^4.0.1",
"clsx": "^1.1.1",
"date-fns": "^2.28.0",
@@ -56,8 +55,10 @@
"@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",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym_wallet"
version = "1.0.6"
version = "1.0.7"
description = "Nym Native Wallet"
authors = ["Nym Technologies SA"]
license = ""
+6 -15
View File
@@ -1,7 +1,7 @@
{
"package": {
"productName": "nym-wallet",
"version": "1.0.6"
"version": "1.0.7"
},
"build": {
"distDir": "../dist",
@@ -14,13 +14,7 @@
"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",
@@ -28,13 +22,12 @@
"shortDescription": "Nym desktop wallet allows you to manage your NYM tokens",
"longDescription": "",
"deb": {
"depends": [],
"useBootstrapper": false
"depends": []
},
"macOS": {
"frameworks": [],
"minimumSystemVersion": "",
"useBootstrapper": false,
"exceptionDomain": "",
"signingIdentity": "Developer ID Application: Nym Technologies SA (VW5DZLFHM5)",
"entitlements": null
@@ -47,15 +40,13 @@
},
"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: 'nym.text.dark' }}
sx={{ color: 'text.primary' }}
onClick={onClick}
disableRipple
>
@@ -1,5 +1,5 @@
import React from 'react';
import { Alert, Box, Dialog, DialogContent, DialogTitle, IconButton, Stack, Typography } from '@mui/material';
import { Alert, Box, Paper, Dialog, DialogContent, DialogTitle, IconButton, Stack, Typography } from '@mui/material';
import { Close } from '@mui/icons-material';
const passwordCreationSteps = [
@@ -11,29 +11,35 @@ const passwordCreationSteps = [
];
export const MultiAccountHowTo = ({ show, handleClose }: { show: boolean; handleClose: () => void }) => (
<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 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>
);
@@ -1,5 +1,15 @@
import React, { useContext, useState } from 'react';
import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Typography } from '@mui/material';
import {
Box,
Button,
Paper,
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';
@@ -32,45 +42,47 @@ export const AccountsModal = () => {
);
return (
<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 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>
);
};
@@ -2,6 +2,7 @@ import React, { useContext, useEffect, useState } from 'react';
import {
Box,
Button,
Paper,
Dialog,
DialogActions,
DialogContent,
@@ -172,67 +173,64 @@ export const AddAccountModal = () => {
}, [step]);
return (
<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);
<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);
}
}
}
}}
isLoading={isLoading}
error={error}
/>
);
default:
return null;
}
})()}
}}
isLoading={isLoading}
error={error}
/>
);
default:
return null;
}
})()}
</Paper>
</Dialog>
);
};
@@ -1,5 +1,5 @@
import React, { useContext } from 'react';
import { Box, Dialog, DialogTitle, IconButton, Typography } from '@mui/material';
import { Box, Paper, 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,19 +16,21 @@ export const ConfirmPasswordModal = ({
const { isLoading, error } = useContext(AccountsContext);
return (
<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 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>
);
};
@@ -2,6 +2,7 @@ import React, { useContext, useEffect, useState } from 'react';
import {
Box,
Button,
Paper,
Dialog,
DialogActions,
DialogContent,
@@ -23,46 +24,48 @@ export const EditAccountModal = () => {
}, [accountToEdit]);
return (
<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"
<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
fullWidth
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>
disableElevation
variant="contained"
size="large"
onClick={() => {
if (accountToEdit) {
handleEditAccount({ ...accountToEdit, id: accountName });
setDialogToDisplay('Accounts');
}
}}
disabled={!accountName?.length}
>
Edit
</Button>
</DialogActions>
</Paper>
</Dialog>
);
};
@@ -2,6 +2,7 @@ import React, { useContext, useState } from 'react';
import {
Box,
Button,
Paper,
Dialog,
DialogActions,
DialogContent,
@@ -24,43 +25,45 @@ export const ImportAccountModal = () => {
};
return (
<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"
<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
fullWidth
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>
disableElevation
variant="contained"
size="large"
onClick={() => handleImportAccount({ id: '', address: '' })}
disabled={!mnemonic.length}
>
Import account
</Button>
</DialogActions>
</Paper>
</Dialog>
);
};
@@ -3,6 +3,7 @@ import {
Box,
Button,
CircularProgress,
Paper,
Dialog,
DialogActions,
DialogContent,
@@ -39,61 +40,63 @@ export const MnemonicModal = () => {
};
return (
<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>
<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>
)}
{!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>
</DialogActions>
</Paper>
</Dialog>
);
};
+20 -6
View File
@@ -1,8 +1,11 @@
import React, { useContext } from 'react';
import { AppBar as MuiAppBar, Grid, IconButton, Toolbar } from '@mui/material';
import { styled } from '@mui/material/styles';
import { AppBar as MuiAppBar, Grid, IconButton, Toolbar, FormGroup, FormControlLabel, Switch } 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';
@@ -10,10 +13,12 @@ import { MultiAccounts } from './Accounts';
import { config } from '../config';
export const AppBar = () => {
const { logOut, handleShowTerminal, appEnv, handleShowSettings, showSettings } = useContext(AppContext);
const { logOut, handleShowTerminal, appEnv, handleShowSettings, showSettings, mode, handleSwitchMode } =
useContext(AppContext);
const navigate = useNavigate();
return (
<MuiAppBar position="sticky" sx={{ boxShadow: 'none', bgcolor: 'transparent' }}>
<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}>
@@ -25,9 +30,18 @@ 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: 'nym.background.dark' }}>
<IconButton size="small" onClick={handleShowTerminal} sx={{ color: 'text.primary' }}>
<TerminalIcon fontSize="small" />
</IconButton>
</Grid>
@@ -35,7 +49,7 @@ export const AppBar = () => {
<Grid item>
<IconButton
onClick={handleShowSettings}
sx={{ color: showSettings ? 'primary.main' : 'nym.background.dark' }}
sx={{ color: showSettings ? 'primary.main' : 'text.primary' }}
size="small"
>
<NodeIcon fontSize="small" />
@@ -48,7 +62,7 @@ export const AppBar = () => {
await logOut();
navigate('/');
}}
sx={{ color: 'nym.background.dark' }}
sx={{ color: 'text.primary' }}
>
<Logout fontSize="small" />
</IconButton>
@@ -0,0 +1,65 @@
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>
);
};
@@ -0,0 +1 @@
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: 'grey.600' }}>
<Typography variant="body2" component="span" sx={{ color: 'text.primary' }}>
Address:
</Typography>{' '}
</>
)}
<AddressTooltip address={address} visible={!showEntireAddress}>
<Typography variant="body2" component="span" color="nym.background.dark" sx={{ mr: 1 }}>
<Typography variant="body2" component="span" sx={{ mr: 1, color: 'text.primary', fontWeight: 400 }}>
{showEntireAddress ? address || '' : splice(6, address)}
</Typography>
</AddressTooltip>
@@ -25,4 +25,5 @@ Default.args = {
onClose: () => {},
onConfirm: async () => {},
onPrev: () => {},
isStorybook: true,
};
+34 -21
View File
@@ -1,8 +1,19 @@
import React from 'react';
import { FeeDetails } from '@nymproject/types';
import { Box, Button } from '@mui/material';
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;
@@ -12,23 +23,25 @@ export const ConfirmTx: React.FC<{
onConfirm: () => Promise<void>;
onClose?: () => void;
onPrev: () => void;
}> = ({ open, fee, onConfirm, onClose, header, subheader, onPrev, children }) => (
<SimpleModal
open={open}
header={header}
subHeader={subheader}
okLabel="Confirm"
onOk={onConfirm}
onClose={onClose}
SecondaryAction={
<Button fullWidth sx={{ mt: 1 }} size="large" onClick={onPrev}>
Cancel
</Button>
}
>
<Box sx={{ mt: 3 }}>
{children}
<ModalFee fee={fee} isLoading={false} />
</Box>
</SimpleModal>
);
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>
);
};
+4 -12
View File
@@ -4,15 +4,7 @@ import { Check, ContentCopy } from '@mui/icons-material';
import { clipboard } from '@tauri-apps/api';
import { Console } from '../utils/console';
export const CopyToClipboard = ({
text = '',
light,
iconButton,
}: {
text?: string;
light?: boolean;
iconButton?: boolean;
}) => {
export const CopyToClipboard = ({ text = '', iconButton }: { text?: string; iconButton?: boolean }) => {
const [copied, setCopied] = useState(false);
const handleCopy = async (_text: string) => {
@@ -41,7 +33,7 @@ export const CopyToClipboard = ({
onClick={() => handleCopy(text)}
size="small"
sx={{
color: (theme) => (light ? theme.palette.common.white : theme.palette.nym.background.dark),
color: 'text.primary',
}}
>
{!copied ? <ContentCopy fontSize="small" /> : <Check color="success" />}
@@ -54,8 +46,8 @@ export const CopyToClipboard = ({
variant="outlined"
color="inherit"
sx={{
color: (theme) => (light ? theme.palette.common.white : theme.palette.nym.background.dark),
borderColor: (theme) => (light ? theme.palette.common.white : theme.palette.nym.background.dark),
color: 'text.primary',
borderColor: 'text.primary',
}}
onClick={() => handleCopy(text)}
endIcon={copied && <Check sx={{ color: (theme) => theme.palette.success.light }} />}
@@ -1,5 +1,8 @@
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';
@@ -8,4 +11,30 @@ export default {
component: OverSaturatedBlockerModal,
} as ComponentMeta<typeof OverSaturatedBlockerModal>;
export const Default = () => <OverSaturatedBlockerModal open header="Node saturation: 114%" />;
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)}
/>
</>
);
};
@@ -1,5 +1,6 @@
import React from 'react';
import { SimpleModal } from '../Modals/SimpleModal';
import { SxProps } from '@mui/material';
export const OverSaturatedBlockerModal: React.FC<{
open: boolean;
@@ -7,7 +8,9 @@ export const OverSaturatedBlockerModal: React.FC<{
header?: string;
subHeader?: string;
buttonText?: string;
}> = ({ open, onClose, header, subHeader, buttonText }) => (
sx?: SxProps;
backdropProps?: object;
}> = ({ open, onClose, header, subHeader, buttonText, sx, backdropProps }) => (
<SimpleModal
open={open}
hideCloseIcon
@@ -17,7 +20,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={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', ...sx }}
headerStyles={{
width: '100%',
mb: 3,
@@ -27,5 +30,6 @@ export const OverSaturatedBlockerModal: React.FC<{
textTransform: 'capitalize',
}}
subHeaderStyles={{ textAlign: 'center', color: 'text.primary', fontSize: 14, fontWeight: 400 }}
backdropProps={backdropProps}
/>
);
@@ -1,5 +1,6 @@
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';
@@ -33,6 +34,8 @@ export const DelegateModal: React.FC<{
currency: CurrencyDenom;
initialAmount?: string;
hasVestingContract: boolean;
sx?: SxProps;
backdropProps?: object;
}> = ({
open,
onIdentityKeyChanged,
@@ -50,6 +53,8 @@ export const DelegateModal: React.FC<{
nodeUptimePercentage,
initialAmount,
hasVestingContract,
sx,
backdropProps,
}) => {
const [identityKey, setIdentityKey] = useState<string | undefined>(initialIdentityKey);
const [amount, setAmount] = useState<string | undefined>(initialAmount);
@@ -183,6 +188,8 @@ 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: 'black' }}>{Icon}</ListItemIcon>
<ListItemText sx={{ color: 'black' }} primary={title} secondary={description} />
<ListItemIcon sx={{ color: 'text.primary' }}>{Icon}</ListItemIcon>
<ListItemText sx={{ color: 'text.primary' }} primary={title} secondary={description} />
</MenuItem>
);
@@ -98,9 +98,8 @@ export const DelegationsActionsMenu: React.FC<{
onActionClick?: (action: DelegationListItemActions) => void;
isPending?: DelegationEventKind;
disableRedeemingRewards?: boolean;
disableDelegateMore?: boolean;
disableCompoundRewards?: boolean;
}> = ({ disableRedeemingRewards, disableDelegateMore, disableCompoundRewards, onActionClick, isPending }) => {
}> = ({ disableRedeemingRewards, disableCompoundRewards, onActionClick, isPending }) => {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {

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