Compare commits

...

22 Commits

Author SHA1 Message Date
Dave Hrycyszyn 1b5cc1bb54 Changelog tweaks 2023-02-08 08:43:20 +01:00
joeiacono2021 e980441e5f Merge branch 'release/v1.1.9' of https://github.com/nymtech/nym into release/v1.1.9 2023-02-08 08:43:20 +01:00
joeiacono2021 f1214e29f9 changelog changes for release 1.1.9 2023-02-08 08:43:20 +01:00
Jon Häggblad 604742bddb changelog: add note about fix for unexpected shutdown 2023-02-08 08:43:20 +01:00
Fouad e935b2c60f Feature/nym connect health status frontend (#2969)
* filter services on rust side by gateway performance

* format rust code

* create events hook

* use events hook

* remove unused component

remove unused component

update prop names in svg

* display errors in connected screen when needed

update failed health check message
2023-02-08 08:43:20 +01:00
Fouad 4cb04e9192 link owner field to ng explorer (#2970)
* link owner field to ng explorer
2023-02-08 08:43:20 +01:00
Fouad 4fa5c7f2d3 filter services on rust side by gateway performance (#2966)
* filter services on rust side by gateway performance

* update changelog for NC
2023-02-08 08:43:20 +01:00
Mark Sinclair 3321331cb0 GitHub Actions: fix up build-and-upload-binaries-ci.yml 2023-02-08 08:43:20 +01:00
Jędrzej Stuczyński 1e031391fa introduced '/circulating-supply/total-supply-value' and '/circulating-supply/circulating-supply-value' endpoints (#2965) 2023-02-08 08:43:20 +01:00
Mark Sinclair e9f5f0223f Update build-and-upload-binaries-ci.yml 2023-02-08 08:43:20 +01:00
Mark Sinclair 8e8adc45f3 Update build-and-upload-binaries-ci.yml 2023-02-08 08:43:20 +01:00
Mark Sinclair 158ad15d37 Update build-and-upload-binaries-ci.yml 2023-02-08 08:43:20 +01:00
Mark Sinclair 9954334b30 GitHub Actions: add action to build and upload binaries to CI server 2023-02-08 08:43:20 +01:00
Jon Häggblad 8ddd9e37e0 Don't drop in mixnet connection handlers (#2963) 2023-02-08 08:43:20 +01:00
Fouad d7deb9819b NymConnect - Add button animations (#2950)
* add button animations

* pulse and disable button on connecting/disconnecting status

* update button component story

* disabled hover on connecting/disconnecting

* add transition delay

fix up overflow
2023-02-08 08:43:20 +01:00
Jędrzej Stuczyński 3ec0fcb0a4 Added an option to set custom 'host' for the native client (#2939)
* Added an option to set custom 'host' for the native client

* Changelog entry
2023-02-08 08:43:20 +01:00
Pierre Dommerc ba6584e4b6 build(nc-android): prepare for apk release (#2943)
* chore(nc-android): prepare for production build

* refactor(nc-android): remove dead code

* feat(nc-android): update native color theme

* feat(nc-android): update native color theme

* build(nc-android): fix rfd version issue

* build(nc-android): fix dist dir no such file error

* fix(nc-android): post rebase changes
2023-02-08 08:43:20 +01:00
Bogdan-Ștefan Neacşu 44bec774c3 Fix typo during merge back to develop (#2956) 2023-02-08 08:43:20 +01:00
cgi-bin/ a1f15cda16 typo: electrum (#2954) 2023-02-08 08:43:20 +01:00
farbanas 2a42090eb7 fix: formatting 2023-02-08 08:43:20 +01:00
farbanas d31d4630b7 fix: wrong parameter type for addresses in generator commands (should be AccountId instead of String) 2023-02-08 08:43:20 +01:00
Fran Arbanas a1890e49cf Update CHANGELOG.md 2023-02-08 08:43:20 +01:00
89 changed files with 876 additions and 2898 deletions
@@ -0,0 +1,112 @@
name: Build and upload binaries to CI
on:
workflow_dispatch:
push:
paths:
- 'clients/**'
- 'common/**'
- 'contracts/**'
- 'explorer-api/**'
- 'gateway/**'
- 'integrations/**'
- 'mixnode/**'
- 'sdk/rust/nym-sdk/**'
- 'service-providers/**'
- 'nym-api/**'
- 'nym-outfox/**'
- 'tools/nym-cli/**'
- 'tools/ts-rs-cli/**'
pull_request:
paths:
- 'clients/**'
- 'common/**'
- 'contracts/**'
- 'explorer-api/**'
- 'gateway/**'
- 'integrations/**'
- 'mixnode/**'
- 'sdk/rust/nym-sdk/**'
- 'service-providers/**'
- 'nym-api/**'
- 'nym-outfox/**'
- 'tools/nym-cli/**'
- 'tools/ts-rs-cli/**'
env:
NETWORK: mainnet
jobs:
publish-nym:
strategy:
fail-fast: false
matrix:
platform: [ubuntu-20.04]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v3
- name: Prepare build output directory
shell: bash
env:
OUTPUT_DIR: ci-builds/${{ github.ref_name }}
run: |
rm -rf ci-builds || true
mkdir -p $OUTPUT_DIR
echo $OUTPUT_DIR
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools
continue-on-error: true
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Build all binaries
uses: actions-rs/cargo@v1
with:
command: build
args: --workspace --release
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: wasm32-unknown-unknown
override: true
components: rustfmt, clippy
- name: Build release contracts
run: make wasm
- name: Prepare build output
shell: bash
env:
OUTPUT_DIR: ci-builds/${{ github.ref_name }}
run: |
cp target/release/nym-client $OUTPUT_DIR
cp target/release/nym-gateway $OUTPUT_DIR
cp target/release/nym-mixnode $OUTPUT_DIR
cp target/release/nym-socks5-client $OUTPUT_DIR
cp target/release/nym-api $OUTPUT_DIR
cp target/release/nym-network-requester $OUTPUT_DIR
cp target/release/nym-network-statistics $OUTPUT_DIR
cp target/release/nym-cli $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/mixnet_contract.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/vesting_contract.wasm $OUTPUT_DIR
- name: Deploy branch to CI www
continue-on-error: true
uses: easingthemes/ssh-deploy@main
env:
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
ARGS: "-avzr"
SOURCE: "ci-builds/"
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/builds/
EXCLUDE: "/dist/, /node_modules/"
+14 -3
View File
@@ -4,11 +4,22 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
# [Unreleased]
# [v1.1.9] (2023-02-07)
### Added
- remove coconut feature and unify builds ([#2890])
- Separate `nym-api` endpoints with values of "total-supply" and "circulating-supply" in `nym` ([#2964])
- Add `host` option to client init ([#2912])
- Remove Coconut feature flag ([#2793])
- Don't drop in mixnet connection handler ([#2963])
### Changed
- native-client: is now capable of listening for requests on sockets different than `127.0.0.1` ([#2939]). This can be specified via `--host` flag during `init` or `run`. Alternatively a custom `host` can be set in `config.toml` file under `socket` section.
- mixnode, gateway: fix unexpected shutdown on corrupted connection ([#2963])
[#2939]: https://github.com/nymtech/nym/pull/2939
[#2963]: https://github.com/nymtech/nym/pull/2963
[#2890]: https://github.com/nymtech/nym/pull/2890
# [v1.1.8] (2023-01-31)
@@ -24,7 +35,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
- nym-api: an `--id` flag is now always explicitly required ([#2873])
[#2754]: https://github.com/nymtech/nym/issues/2754
[#2839]: https://github.com/nymtech/nym/issues/2810
[#2810]: https://github.com/nymtech/nym/issues/2810
[#2931]: https://github.com/nymtech/nym/issues/2931
[#1902]: https://github.com/nymtech/nym/issues/1902
[#2873]: https://github.com/nymtech/nym/issues/2873
+13 -1
View File
@@ -9,6 +9,7 @@ use config::defaults::DEFAULT_WEBSOCKET_LISTENING_PORT;
use config::{NymConfig, OptionalSet};
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use std::net::{IpAddr, Ipv4Addr};
use std::path::PathBuf;
use std::str::FromStr;
@@ -104,6 +105,11 @@ impl Config {
self
}
pub fn with_host(mut self, host: IpAddr) -> Self {
self.socket.host = host;
self
}
pub fn with_port(mut self, port: u16) -> Self {
self.socket.listening_port = port;
self
@@ -130,6 +136,10 @@ impl Config {
self.socket.socket_type
}
pub fn get_listening_ip(&self) -> IpAddr {
self.socket.host
}
pub fn get_listening_port(&self) -> u16 {
self.socket.listening_port
}
@@ -180,9 +190,10 @@ impl Config {
}
#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
#[serde(default, deny_unknown_fields)]
pub struct Socket {
socket_type: SocketType,
host: IpAddr,
listening_port: u16,
}
@@ -190,6 +201,7 @@ impl Default for Socket {
fn default() -> Self {
Socket {
socket_type: SocketType::WebSocket,
host: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
listening_port: DEFAULT_WEBSOCKET_LISTENING_PORT,
}
}
@@ -93,6 +93,9 @@ socket_type = '{{ socket.socket_type }}'
# will be listening for incoming requests
listening_port = {{ socket.listening_port }}
# if applicable (for the case of 'WebSocket'), the ip address on which the client
# will be listening for incoming requests
host = '{{ socket.host }}'
##### logging configuration options #####
+2 -1
View File
@@ -102,7 +102,8 @@ impl SocketClient {
reply_controller_sender,
);
websocket::Listener::new(config.get_listening_port()).start(websocket_handler, shutdown);
websocket::Listener::new(config.get_listening_ip(), config.get_listening_port())
.start(websocket_handler, shutdown);
}
/// blocking version of `start_socket` method. Will run forever (or until SIGINT is sent)
+6
View File
@@ -12,6 +12,7 @@ use crypto::asymmetric::identity;
use nymsphinx::addressing::clients::Recipient;
use serde::Serialize;
use std::fmt::Display;
use std::net::IpAddr;
use tap::TapFallible;
#[derive(Args, Clone)]
@@ -46,6 +47,10 @@ pub(crate) struct Init {
#[clap(short, long)]
port: Option<u16>,
/// Ip for the socket (if applicable) to listen for requests.
#[clap(long)]
host: Option<IpAddr>,
/// Mostly debug-related option to increase default traffic rate so that you would not need to
/// modify config post init
#[clap(long, hide = true)]
@@ -71,6 +76,7 @@ impl From<Init> for OverrideConfig {
nym_apis: init_config.nym_apis,
disable_socket: init_config.disable_socket,
port: init_config.port,
host: init_config.host,
fastmode: init_config.fastmode,
no_cover: init_config.no_cover,
+3
View File
@@ -9,6 +9,7 @@ use completions::{fig_generate, ArgShell};
use config::OptionalSet;
use lazy_static::lazy_static;
use std::error::Error;
use std::net::IpAddr;
pub(crate) mod init;
pub(crate) mod run;
@@ -56,6 +57,7 @@ pub(crate) struct OverrideConfig {
nym_apis: Option<Vec<url::Url>>,
disable_socket: Option<bool>,
port: Option<u16>,
host: Option<IpAddr>,
fastmode: bool,
no_cover: bool,
nyxd_urls: Option<Vec<url::Url>>,
@@ -81,6 +83,7 @@ pub(crate) fn override_config(config: Config, args: OverrideConfig) -> Config {
.with_base(BaseConfig::with_high_default_traffic_volume, args.fastmode)
.with_base(BaseConfig::with_disabled_cover_traffic, args.no_cover)
.with_optional(Config::with_port, args.port)
.with_optional(Config::with_host, args.host)
.with_optional_custom_env_ext(
BaseConfig::with_custom_nym_apis,
args.nym_apis,
+6
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use std::error::Error;
use std::net::IpAddr;
use crate::{
client::{config::Config, SocketClient},
@@ -43,6 +44,10 @@ pub(crate) struct Run {
#[clap(short, long)]
port: Option<u16>,
/// Ip for the socket (if applicable) to listen for requests.
#[clap(long)]
host: Option<IpAddr>,
/// Mostly debug-related option to increase default traffic rate so that you would not need to
/// modify config post init
#[clap(long, hide = true)]
@@ -64,6 +69,7 @@ impl From<Run> for OverrideConfig {
nym_apis: run_config.nym_apis,
disable_socket: run_config.disable_socket,
port: run_config.port,
host: run_config.host,
fastmode: run_config.fastmode,
no_cover: run_config.no_cover,
nyxd_urls: run_config.nyxd_urls,
+3 -3
View File
@@ -3,6 +3,7 @@
use super::handler::HandlerBuilder;
use log::*;
use std::net::IpAddr;
use std::{net::SocketAddr, process, sync::Arc};
use tokio::io::AsyncWriteExt;
use tokio::{sync::Notify, task::JoinHandle};
@@ -24,10 +25,9 @@ pub(crate) struct Listener {
}
impl Listener {
pub(crate) fn new(port: u16) -> Self {
pub(crate) fn new(host: IpAddr, port: u16) -> Self {
Listener {
// unless we find compelling reason not to, just listen on local only
address: SocketAddr::new("127.0.0.1".parse().unwrap(), port),
address: SocketAddr::new(host, port),
state: State::AwaitingConnection,
}
}
@@ -1,10 +1,13 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::str::FromStr;
use clap::Parser;
use log::{debug, info};
use coconut_bandwidth_contract_common::msg::InstantiateMsg;
use validator_client::nyxd::AccountId;
#[derive(Debug, Parser)]
pub struct Args {
@@ -12,7 +15,7 @@ pub struct Args {
pub pool_addr: String,
#[clap(long)]
pub multisig_addr: Option<String>,
pub multisig_addr: Option<AccountId>,
#[clap(long)]
pub mix_denom: Option<String>,
@@ -24,8 +27,10 @@ pub async fn generate(args: Args) {
debug!("Received arguments: {:?}", args);
let multisig_addr = args.multisig_addr.unwrap_or_else(|| {
std::env::var(network_defaults::var_names::REWARDING_VALIDATOR_ADDRESS)
.expect("Multisig address has to be set")
let address = std::env::var(network_defaults::var_names::REWARDING_VALIDATOR_ADDRESS)
.expect("Multisig address has to be set");
AccountId::from_str(address.as_str())
.expect("Failed converting multisig address to AccountId")
});
let mix_denom = args.mix_denom.unwrap_or_else(|| {
@@ -34,7 +39,7 @@ pub async fn generate(args: Args) {
let instantiate_msg = InstantiateMsg {
pool_addr: args.pool_addr,
multisig_addr,
multisig_addr: multisig_addr.to_string(),
mix_denom,
};
@@ -7,6 +7,7 @@ use std::str::FromStr;
use coconut_dkg_common::msg::InstantiateMsg;
use coconut_dkg_common::types::TimeConfiguration;
use validator_client::nyxd::AccountId;
#[derive(Debug, Parser)]
pub struct Args {
@@ -14,7 +15,7 @@ pub struct Args {
pub group_addr: String,
#[clap(long)]
pub multisig_addr: Option<String>,
pub multisig_addr: Option<AccountId>,
#[clap(long)]
pub public_key_submission_time_secs: Option<u64>,
@@ -44,8 +45,10 @@ pub async fn generate(args: Args) {
debug!("Received arguments: {:?}", args);
let multisig_addr = args.multisig_addr.unwrap_or_else(|| {
std::env::var(network_defaults::var_names::REWARDING_VALIDATOR_ADDRESS)
.expect("Multisig address has to be set")
let address = std::env::var(network_defaults::var_names::REWARDING_VALIDATOR_ADDRESS)
.expect("Multisig address has to be set");
AccountId::from_str(address.as_str())
.expect("Failed converting multisig address to AccountId")
});
let mix_denom = args.mix_denom.unwrap_or_else(|| {
@@ -86,7 +89,7 @@ pub async fn generate(args: Args) {
let instantiate_msg = InstantiateMsg {
group_addr: args.group_addr,
multisig_addr,
multisig_addr: multisig_addr.to_string(),
time_configuration: Some(time_configuration),
mix_denom,
};
@@ -6,15 +6,17 @@ use log::{debug, info};
use cosmwasm_std::Decimal;
use mixnet_contract_common::{InitialRewardingParams, InstantiateMsg, Percent};
use std::str::FromStr;
use std::time::Duration;
use validator_client::nyxd::AccountId;
#[derive(Debug, Parser)]
pub struct Args {
#[clap(long)]
pub rewarding_validator_address: Option<String>,
pub rewarding_validator_address: Option<AccountId>,
#[clap(long)]
pub vesting_contract_address: Option<String>,
pub vesting_contract_address: Option<AccountId>,
#[clap(long)]
pub rewarding_denom: Option<String>,
@@ -77,13 +79,17 @@ pub async fn generate(args: Args) {
debug!("initial_rewarding_params: {:?}", initial_rewarding_params);
let rewarding_validator_address = args.rewarding_validator_address.unwrap_or_else(|| {
std::env::var(network_defaults::var_names::REWARDING_VALIDATOR_ADDRESS)
.expect("Rewarding validator address has to be set")
let address = std::env::var(network_defaults::var_names::REWARDING_VALIDATOR_ADDRESS)
.expect("Rewarding validator address has to be set");
AccountId::from_str(address.as_str())
.expect("Failed converting rewarding validator address to AccountId")
});
let vesting_contract_address = args.vesting_contract_address.unwrap_or_else(|| {
std::env::var(network_defaults::var_names::VESTING_CONTRACT_ADDRESS)
.expect("Vesting contract address has to be set")
let address = std::env::var(network_defaults::var_names::VESTING_CONTRACT_ADDRESS)
.expect("Vesting contract address has to be set");
AccountId::from_str(address.as_str())
.expect("Failed converting vesting contract address to AccountId")
});
let rewarding_denom = args.rewarding_denom.unwrap_or_else(|| {
@@ -92,8 +98,8 @@ pub async fn generate(args: Args) {
});
let instantiate_msg = InstantiateMsg {
rewarding_validator_address,
vesting_contract_address,
rewarding_validator_address: rewarding_validator_address.to_string(),
vesting_contract_address: vesting_contract_address.to_string(),
rewarding_denom,
epochs_in_interval: args.epochs_in_interval,
epoch_duration: Duration::from_secs(args.epoch_duration),
@@ -1,12 +1,14 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use clap::Parser;
use log::{debug, info};
use std::str::FromStr;
use clap::Parser;
use cosmwasm_std::Decimal;
use cw_utils::{Duration, Threshold};
use log::{debug, info};
use multisig_contract_common::msg::InstantiateMsg;
use validator_client::nyxd::AccountId;
#[derive(Debug, Parser)]
pub struct Args {
@@ -20,10 +22,10 @@ pub struct Args {
pub max_voting_period: u64,
#[clap(long)]
pub coconut_bandwidth_contract_address: Option<String>,
pub coconut_bandwidth_contract_address: Option<AccountId>,
#[clap(long)]
pub coconut_dkg_contract_address: Option<String>,
pub coconut_dkg_contract_address: Option<AccountId>,
}
pub async fn generate(args: Args) {
@@ -33,13 +35,18 @@ pub async fn generate(args: Args) {
let coconut_bandwidth_contract_address =
args.coconut_bandwidth_contract_address.unwrap_or_else(|| {
std::env::var(network_defaults::var_names::COCONUT_BANDWIDTH_CONTRACT_ADDRESS)
.expect("Coconut bandwidth contract address has to be set")
let address =
std::env::var(network_defaults::var_names::COCONUT_BANDWIDTH_CONTRACT_ADDRESS)
.expect("Coconut bandwidth contract address has to be set");
AccountId::from_str(address.as_str())
.expect("Failed converting bandwidth contract address to AccountId")
});
let coconut_dkg_contract_address = args.coconut_dkg_contract_address.unwrap_or_else(|| {
std::env::var(network_defaults::var_names::COCONUT_DKG_CONTRACT_ADDRESS)
.expect("Coconut DKG contract address has to be set")
let address = std::env::var(network_defaults::var_names::COCONUT_DKG_CONTRACT_ADDRESS)
.expect("Coconut DKG contract address has to be set");
AccountId::from_str(address.as_str())
.expect("Failed converting DKG contract address to AccountId")
});
let instantiate_msg = InstantiateMsg {
@@ -49,8 +56,8 @@ pub async fn generate(args: Args) {
.expect("threshold can't be converted to Decimal"),
},
max_voting_period: Duration::Time(args.max_voting_period),
coconut_bandwidth_contract_address,
coconut_dkg_contract_address,
coconut_bandwidth_contract_address: coconut_bandwidth_contract_address.to_string(),
coconut_dkg_contract_address: coconut_dkg_contract_address.to_string(),
};
debug!("instantiate_msg: {:?}", instantiate_msg);
@@ -1,15 +1,18 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::str::FromStr;
use clap::Parser;
use log::{debug, info};
use validator_client::nyxd::AccountId;
use vesting_contract_common::InitMsg;
#[derive(Debug, Parser)]
pub struct Args {
#[clap(long)]
pub mixnet_contract_address: Option<String>,
pub mixnet_contract_address: Option<AccountId>,
#[clap(long)]
pub mix_denom: Option<String>,
@@ -21,8 +24,10 @@ pub async fn generate(args: Args) {
debug!("Received arguments: {:?}", args);
let mixnet_contract_address = args.mixnet_contract_address.unwrap_or_else(|| {
std::env::var(network_defaults::var_names::MIXNET_CONTRACT_ADDRESS)
.expect("Mixnet contract address has to be set")
let address = std::env::var(network_defaults::var_names::MIXNET_CONTRACT_ADDRESS)
.expect("Mixnet contract address has to be set");
AccountId::from_str(address.as_str())
.expect("Failed converting mixnet address to AccountId")
});
let mix_denom = args.mix_denom.unwrap_or_else(|| {
@@ -30,7 +35,7 @@ pub async fn generate(args: Args) {
});
let instantiate_msg = InitMsg {
mixnet_contract_address,
mixnet_contract_address: mixnet_contract_address.to_string(),
mix_denom,
};
+10
View File
@@ -1,5 +1,15 @@
## UNRELEASED
- nothing yet
## [nym-explorer-v1.0.5](https://github.com/nymtech/nym/tree/nym-explorer-v1.0.5) (2023-02-07)
- NE - link `Owner` field on the node detail page to the account details on NG explorer ([#2923])
- NE - Upgrade Sandbox and make below changes: ([#2332])
[#2923]: https://github.com/nymtech/nym/issues/2923
[#2332]: https://github.com/nymtech/nym/issues/2332
## [nym-explorer-v1.0.4](https://github.com/nymtech/nym/tree/nym-explorer-v1.0.4) (2023-01-31)
- Add routing score on gateway list ([#2913])
+11 -1
View File
@@ -1,5 +1,5 @@
import * as React from 'react';
import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material';
import { Link, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { Tooltip } from '@nymproject/react/tooltip/Tooltip';
import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard';
@@ -37,9 +37,19 @@ function formatCellValues(val: string | number, field: string) {
</Box>
);
}
if (field === 'bond') {
return unymToNym(val, 6);
}
if (field === 'owner') {
return (
<Link underline="none" color="inherit" target="_blank" href={`https://mixnet.explorers.guru/account/${val}`}>
{val}
</Link>
);
}
return val;
}
@@ -181,6 +181,7 @@ impl<St: Storage> ConnectionHandler<St> {
mut shutdown: TaskClient,
) {
debug!("Starting connection handler for {:?}", remote);
shutdown.mark_as_success();
let mut framed_conn = Framed::new(conn, SphinxCodec);
while !shutdown.is_shutdown() {
tokio::select! {
@@ -77,6 +77,7 @@ impl ConnectionHandler {
mut shutdown: TaskClient,
) {
debug!("Starting connection handler for {:?}", remote);
shutdown.mark_as_success();
let mut framed_conn = Framed::new(conn, SphinxCodec);
while !shutdown.is_shutdown() {
tokio::select! {
+5 -1
View File
@@ -15,7 +15,11 @@ pub(crate) mod routes;
/// Merges the routes with http information and returns it to Rocket for serving
pub(crate) fn circulating_supply_routes(settings: &OpenApiSettings) -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![settings: routes::get_circulating_supply]
openapi_get_routes_spec![
settings: routes::get_full_circulating_supply,
routes::get_total_supply,
routes::get_circulating_supply
]
}
/// Spawn the circulating supply cache refresher.
+59 -4
View File
@@ -1,15 +1,30 @@
use rocket::http::Status;
use rocket::serde::json::Json;
use rocket::State;
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::circulating_supply_api::cache::CirculatingSupplyCache;
use crate::node_status_api::models::ErrorResponse;
use nym_api_requests::models::CirculatingSupplyResponse;
use rocket::http::Status;
use rocket::serde::json::Json;
use rocket::State;
use rocket_okapi::openapi;
use validator_client::nyxd::Coin;
// TODO: this is not the best place to put it, it should be more centralised,
// but for a quick fix, that's good enough for now...
// (for proper solution we should be managing `NymNetworkDetails` via rocket and grabbing display exponent
// value from the mix denom here.
const UNYM_RATIO: f64 = 1000000.;
fn unym_coin_to_float_unym(coin: Coin) -> f64 {
// our total supply can't exceed 1B so an overflow here is impossible
// (if it happened, then we SHOULD crash)
coin.amount as f64 / UNYM_RATIO
}
#[openapi(tag = "circulating-supply")]
#[get("/circulating-supply")]
pub(crate) async fn get_circulating_supply(
pub(crate) async fn get_full_circulating_supply(
cache: &State<CirculatingSupplyCache>,
) -> Result<Json<CirculatingSupplyResponse>, ErrorResponse> {
match cache.get_circulating_supply().await {
@@ -20,3 +35,43 @@ pub(crate) async fn get_circulating_supply(
)),
}
}
#[openapi(tag = "circulating-supply")]
#[get("/circulating-supply/total-supply-value")]
pub(crate) async fn get_total_supply(
cache: &State<CirculatingSupplyCache>,
) -> Result<Json<f64>, ErrorResponse> {
let full_circulating_supply = match cache.get_circulating_supply().await {
Some(res) => res,
None => {
return Err(ErrorResponse::new(
"unavailable",
Status::InternalServerError,
))
}
};
Ok(Json(unym_coin_to_float_unym(
full_circulating_supply.total_supply.into(),
)))
}
#[openapi(tag = "circulating-supply")]
#[get("/circulating-supply/circulating-supply-value")]
pub(crate) async fn get_circulating_supply(
cache: &State<CirculatingSupplyCache>,
) -> Result<Json<f64>, ErrorResponse> {
let full_circulating_supply = match cache.get_circulating_supply().await {
Some(res) => res,
None => {
return Err(ErrorResponse::new(
"unavailable",
Status::InternalServerError,
))
}
};
Ok(Json(unym_coin_to_float_unym(
full_circulating_supply.circulating_supply.into(),
)))
}
+1 -1
View File
@@ -122,7 +122,7 @@ pub(crate) fn build_config(args: CliArgs) -> Result<Config> {
let config = override_config(config_from_file, args);
if already_initialized {
if !already_initialized {
fs::create_dir_all(Config::default_config_directory(&id))
.expect("Could not create config directory");
fs::create_dir_all(Config::default_data_directory(&id))
@@ -1,11 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Nym Connect</title>
</head>
<body style="background: rgb(29, 33, 37);">
<div id="root-growth"></div>
</body>
</html>
-11
View File
@@ -1,11 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Nym Wallet Logs</title>
</head>
<body>
<div id="root-log"></div>
</body>
</html>
+8 -2
View File
@@ -41,8 +41,8 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_repr = "0.1"
tap = "1.0.1"
tauri = { git = "https://github.com/tauri-apps/tauri", branch = "next", features = ["clipboard-write-text", "native-tls-vendored", "notification-all", "shell-open", "system-tray", "window-close", "window-minimize", "window-start-dragging"] }
# tauri = { version = "2.0.0-alpha.0", features = ["clipboard-write-text", "native-tls-vendored", "notification-all", "shell-open", "system-tray", "window-close", "window-minimize", "window-start-dragging"] }
# TODO swithing to `rfd101` temporarily, untill https://github.com/tauri-apps/tauri/pull/6174 is merged
tauri = { git = "https://github.com/tauri-apps/tauri", branch = "rfd101", features = ["clipboard-write-text", "native-tls-vendored", "notification-all", "shell-open", "system-tray", "window-close", "window-minimize", "window-start-dragging"] }
tendermint-rpc = "0.23.0"
thiserror = "1.0"
tokio = { version = "1.24.1", features = ["sync", "time"] }
@@ -64,3 +64,9 @@ tempfile = "3.3.0"
[features]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
# [profile.dev]
# strip = true
# opt-level = "s"
# lto = true
@@ -1,111 +1,112 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("rustPlugin")
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("rustPlugin")
}
android {
compileSdk = 33
defaultConfig {
manifestPlaceholders["usesCleartextTraffic"] = "false"
applicationId = "net.nymtech.nym_connect_android"
minSdk = 24
targetSdk = 33
versionCode = 1
versionName = "1.0"
compileSdk = 33
defaultConfig {
manifestPlaceholders["usesCleartextTraffic"] = "false"
applicationId = "net.nymtech.nym_connect_android"
minSdk = 24
targetSdk = 33
versionCode = 1
versionName = "1.0"
}
sourceSets.getByName("main") {
// Vulkan validation layers
val ndkHome = System.getenv("NDK_HOME")
jniLibs.srcDir("${ndkHome}/sources/third_party/vulkan/src/build-android/jniLibs")
}
buildTypes {
getByName("debug") {
manifestPlaceholders["usesCleartextTraffic"] = "true"
isDebuggable = true
isJniDebuggable = true
isMinifyEnabled = false
packagingOptions {
jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
jniLibs.keepDebugSymbols.add("*/x86/*.so")
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
}
}
sourceSets.getByName("main") {
// Vulkan validation layers
val ndkHome = System.getenv("NDK_HOME")
jniLibs.srcDir("${ndkHome}/sources/third_party/vulkan/src/build-android/jniLibs")
getByName("release") {
isMinifyEnabled = false
// proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
}
buildTypes {
getByName("debug") {
manifestPlaceholders["usesCleartextTraffic"] = "true"
isDebuggable = true
isJniDebuggable = true
isMinifyEnabled = false
packagingOptions {
jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
}
flavorDimensions.add("abi")
productFlavors {
create("universal") {
val abiList = findProperty("abiList") as? String
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
jniLibs.keepDebugSymbols.add("*/x86/*.so")
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
}
}
getByName("release") {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
}
dimension = "abi"
ndk {
abiFilters += abiList?.split(",")?.map { it.trim() } ?: listOf(
"arm64-v8a", "armeabi-v7a", "x86", "x86_64",
)
}
}
flavorDimensions.add("abi")
productFlavors {
create("universal") {
val abiList = findProperty("abiList") as? String
dimension = "abi"
ndk {
abiFilters += abiList?.split(",")?.map { it.trim() } ?: listOf( "arm64-v8a", "armeabi-v7a", "x86", "x86_64",
)
}
}
create("arm64") {
dimension = "abi"
ndk {
abiFilters += listOf("arm64-v8a")
}
}
create("arm") {
dimension = "abi"
ndk {
abiFilters += listOf("armeabi-v7a")
}
}
create("x86") {
dimension = "abi"
ndk {
abiFilters += listOf("x86")
}
}
create("x86_64") {
dimension = "abi"
ndk {
abiFilters += listOf("x86_64")
}
}
create("arm64") {
dimension = "abi"
ndk {
abiFilters += listOf("arm64-v8a")
}
}
assetPacks += mutableSetOf()
namespace = "net.nymtech.nym_connect_android"
create("arm") {
dimension = "abi"
ndk {
abiFilters += listOf("armeabi-v7a")
}
}
create("x86") {
dimension = "abi"
ndk {
abiFilters += listOf("x86")
}
}
create("x86_64") {
dimension = "abi"
ndk {
abiFilters += listOf("x86_64")
}
}
}
assetPacks += mutableSetOf()
namespace = "net.nymtech.nym_connect_android"
}
rust {
rootDirRel = "../../../../"
targets = listOf("aarch64", "armv7", "i686", "x86_64")
arches = listOf("arm64", "arm", "x86", "x86_64")
rootDirRel = "../../../../"
targets = listOf("aarch64", "armv7", "i686", "x86_64")
arches = listOf("arm64", "arm", "x86", "x86_64")
}
dependencies {
implementation("androidx.webkit:webkit:1.5.0")
implementation("androidx.appcompat:appcompat:1.5.1")
implementation("com.google.android.material:material:1.7.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.4")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
implementation("androidx.webkit:webkit:1.5.0")
implementation("androidx.appcompat:appcompat:1.5.1")
implementation("com.google.android.material:material:1.7.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.4")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
}
afterEvaluate {
android.applicationVariants.all {
tasks["mergeUniversalReleaseJniLibFolders"].dependsOn(tasks["rustBuildRelease"])
tasks["mergeUniversalDebugJniLibFolders"].dependsOn(tasks["rustBuildDebug"])
productFlavors.filter{ it.name != "universal" }.forEach { _ ->
val archAndBuildType = name.capitalize()
tasks["merge${archAndBuildType}JniLibFolders"].dependsOn(tasks["rustBuild${archAndBuildType}"])
}
android.applicationVariants.all {
tasks["mergeUniversalReleaseJniLibFolders"].dependsOn(tasks["rustBuildRelease"])
tasks["mergeUniversalDebugJniLibFolders"].dependsOn(tasks["rustBuildDebug"])
productFlavors.filter { it.name != "universal" }.forEach { _ ->
val archAndBuildType = name.capitalize()
tasks["merge${archAndBuildType}JniLibFolders"].dependsOn(tasks["rustBuild${archAndBuildType}"])
}
}
}
@@ -0,0 +1 @@
/home/pierre/Documents/nym/nym/nym-connect-android/src-tauri/target/aarch64-linux-android/debug/libnym_connect_android.so
@@ -0,0 +1 @@
/home/pierre/Documents/nym/nym/nym-connect-android/src-tauri/target/armv7-linux-androideabi/debug/libnym_connect_android.so
@@ -0,0 +1 @@
/home/pierre/Documents/nym/nym/nym-connect-android/src-tauri/target/i686-linux-android/debug/libnym_connect_android.so
@@ -2,15 +2,15 @@
<!-- Base application theme. -->
<style name="Theme.nym_connect_android" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<item name="colorPrimary">@color/grey_800</item>
<item name="colorPrimaryVariant">@color/grey_900</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorSecondary">@color/green_500</item>
<item name="colorSecondaryVariant">@color/green_900</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>
</resources>
@@ -1,10 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="grey_900">#212121</color>
<color name="grey_800">#424242</color>
<color name="green_500">#4caf50</color>
<color name="green_900">#1b5e20</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
@@ -1,3 +1,3 @@
<resources>
<string name="app_name">nym-connect-android</string>
<string name="app_name">Nym Connect</string>
</resources>
@@ -2,12 +2,12 @@
<!-- Base application theme. -->
<style name="Theme.nym_connect_android" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorPrimary">@color/grey_800</item>
<item name="colorPrimaryVariant">@color/grey_900</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorSecondary">@color/green_500</item>
<item name="colorSecondaryVariant">@color/green_900</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
@@ -40,11 +40,6 @@ pub enum BackendError {
#[from]
source: ClientCoreError,
},
#[error("{source}")]
ApiClientError {
#[from]
source: crate::operations::growth::api_client::ApiClientError,
},
#[error("could not send disconnect signal to the SOCKS5 client")]
CoundNotSendDisconnectSignal,
@@ -1,270 +0,0 @@
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[allow(unused)]
#[derive(Error, Debug)]
pub enum ApiClientError {
#[error("{source}")]
Reqwest {
#[from]
source: reqwest::Error,
},
#[error("{source}")]
SerdeJson {
#[from]
source: serde_json::Error,
},
#[error("{0}")]
Status(String),
}
const API_BASE_URL: &str = "https://growth-api.nymtech.net";
// For development mode, switch to this
// const API_BASE_URL: &str = "http://localhost:8000";
#[derive(Debug, Clone)]
pub struct GrowthApiClient {
base_url: String,
}
impl GrowthApiClient {
pub fn new(resource_base: &str) -> Self {
let base_url = std::env::var("API_BASE_URL").unwrap_or_else(|_| API_BASE_URL.to_string());
GrowthApiClient {
base_url: format!("{base_url}{resource_base}"),
}
}
pub fn registrations() -> Registrations {
Registrations::new(GrowthApiClient::new("/v1/tne"))
}
pub fn daily_draws() -> DailyDraws {
DailyDraws::new(GrowthApiClient::new("/v1/tne/daily_draw"))
}
pub(crate) async fn get<T: DeserializeOwned>(&self, url: &str) -> Result<T, ApiClientError> {
log::info!(">>> GET {}", url);
let proxy = reqwest::Proxy::all("socks5h://127.0.0.1:1080")?;
let client = reqwest::Client::builder()
.proxy(proxy)
.timeout(std::time::Duration::from_secs(10))
.build()?;
match client.get(format!("{}{}", self.base_url, url)).send().await {
Ok(res) => {
if res.status().is_client_error() || res.status().is_server_error() {
log::error!("<<< {}", res.status());
return Err(ApiClientError::Status(res.status().to_string()));
}
match res.text().await {
Ok(response_body) => {
log::info!("<<< {}", response_body);
match serde_json::from_str(&response_body) {
Ok(res) => Ok(res),
Err(e) => {
log::error!("<<< JSON parsing error: {}", e);
Err(e.into())
}
}
}
Err(e) => {
log::error!("<<< Request error: {}", e);
Err(e.into())
}
}
}
Err(e) => {
log::error!("<<< Response parsing error: {}", e);
Err(e.into())
}
}
}
// TODO: use the method in `operations::http` instead
pub(crate) async fn post<REQ: Serialize + ?Sized, RESP: DeserializeOwned>(
&self,
url: &str,
body: &REQ,
) -> Result<RESP, ApiClientError> {
log::info!(">>> POST {}", url);
let proxy = reqwest::Proxy::all("socks5h://127.0.0.1:1080")?;
let client = reqwest::Client::builder()
.proxy(proxy)
.timeout(std::time::Duration::from_secs(10))
.build()?;
match client
.post(format!("{}{}", self.base_url, url))
.json(body)
.send()
.await
{
Ok(res) => {
if res.status().is_client_error() || res.status().is_server_error() {
log::error!("<<< {}", res.status());
return Err(ApiClientError::Status(res.status().to_string()));
}
match res.text().await {
Ok(response_body) => {
log::info!("<<< {}", response_body);
match serde_json::from_str(&response_body) {
Ok(res) => Ok(res),
Err(e) => {
log::error!("<<< JSON parsing error: {}", e);
Err(e.into())
}
}
}
Err(e) => {
log::error!("<<< Request error: {}", e);
Err(e.into())
}
}
}
Err(e) => {
log::error!("<<< Response parsing error: {}", e);
Err(e.into())
}
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ClientIdPartial {
pub client_id: String,
pub client_id_signature: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Registration {
pub id: String,
pub client_id: String,
pub timestamp: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Ping {
pub client_id: String,
pub timestamp: String,
}
pub struct Registrations {
client: GrowthApiClient,
}
impl Registrations {
pub fn new(client: GrowthApiClient) -> Self {
Registrations { client }
}
pub async fn register(
&self,
registration: &ClientIdPartial,
) -> Result<Registration, ApiClientError> {
self.client.post("/register", &registration).await
}
#[allow(dead_code)]
pub async fn unregister(&self, registration: &ClientIdPartial) -> Result<(), ApiClientError> {
self.client.post("/unregister", &registration).await
}
#[allow(dead_code)]
pub async fn status(&self, registration: &ClientIdPartial) -> Result<(), ApiClientError> {
self.client.post("/status", &registration).await
}
pub async fn ping(&self, registration: &ClientIdPartial) -> Result<(), ApiClientError> {
self.client.post("/ping", &registration).await
}
#[allow(dead_code)]
pub async fn health(&self) -> Result<(), ApiClientError> {
self.client.get("/health").await
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DrawEntryPartial {
pub draw_id: String,
pub client_id: String,
pub client_id_signature: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DrawEntry {
pub id: String,
pub draw_id: String,
pub timestamp: String,
pub status: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DrawWithWordOfTheDay {
pub id: String,
pub start_utc: String,
pub end_utc: String,
pub word_of_the_day: Option<String>,
pub last_modified: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ClaimPartial {
pub draw_id: String,
pub registration_id: String,
pub client_id: String,
pub client_id_signature: String,
pub wallet_address: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Winner {
pub id: String,
pub client_id: String,
pub draw_id: String,
pub timestamp: String,
pub winner_reg_id: String,
pub winner_wallet_address: Option<String>,
pub winner_claim_timestamp: Option<String>,
}
pub struct DailyDraws {
client: GrowthApiClient,
}
impl DailyDraws {
pub fn new(client: GrowthApiClient) -> Self {
DailyDraws { client }
}
pub async fn current(&self) -> Result<DrawWithWordOfTheDay, ApiClientError> {
self.client.get("/current").await
}
pub async fn next(&self) -> Result<DrawWithWordOfTheDay, ApiClientError> {
self.client.get("/next").await
}
#[allow(dead_code)]
pub async fn status(&self, draw_id: &str) -> Result<DrawWithWordOfTheDay, ApiClientError> {
self.client.get(format!("/status/{draw_id}").as_str()).await
}
pub async fn enter(&self, entry: &DrawEntryPartial) -> Result<DrawEntry, ApiClientError> {
self.client.post("/enter", entry).await
}
pub async fn entries(
&self,
client_id: &ClientIdPartial,
) -> Result<Vec<DrawEntry>, ApiClientError> {
self.client.post("/entries", client_id).await
}
pub async fn claim(&self, claim: &ClaimPartial) -> Result<Winner, ApiClientError> {
self.client.post("/claim", claim).await
}
}
@@ -1,57 +0,0 @@
use rust_embed::RustEmbed;
extern crate yaml_rust;
use yaml_rust::YamlLoader;
#[derive(RustEmbed)]
#[folder = "../src/components/Growth/content/"]
#[include = "*.yaml"]
#[exclude = "*.mdx"]
struct Asset;
#[derive(Debug)]
pub struct NotificationContent {
pub title: String,
pub body: String,
}
#[derive(Debug)]
pub struct Notifications {
pub you_are_in_draw: NotificationContent,
pub take_part: NotificationContent,
}
pub struct Content {}
const RESOURCE_ERROR: &str = "❌ RESOURCE ERROR";
fn get_as_string_or_error_message(value: &yaml_rust::Yaml) -> String {
value.as_str().unwrap_or(RESOURCE_ERROR).to_string()
}
impl Content {
pub fn get_notifications() -> Notifications {
let content = Asset::get("en.yaml").unwrap();
let s = std::str::from_utf8(content.data.as_ref()).unwrap();
let content = YamlLoader::load_from_str(s).unwrap();
let content = &content[0];
Notifications {
you_are_in_draw: NotificationContent {
title: get_as_string_or_error_message(
&content["testAndEarn"]["notifications"]["youAreInDraw"]["title"],
),
body: get_as_string_or_error_message(
&content["testAndEarn"]["notifications"]["youAreInDraw"]["body"],
),
},
take_part: NotificationContent {
title: get_as_string_or_error_message(
&content["testAndEarn"]["notifications"]["takePart"]["title"],
),
body: get_as_string_or_error_message(
&content["testAndEarn"]["notifications"]["takePart"]["body"],
),
},
}
}
}
@@ -1,3 +0,0 @@
pub mod api_client;
pub mod assets;
pub mod test_and_earn;
@@ -1,159 +0,0 @@
use crate::error::BackendError;
use crate::operations::export::get_identity_key;
use crate::operations::growth::api_client::{
ClaimPartial, ClientIdPartial, DrawEntry, DrawEntryPartial, DrawWithWordOfTheDay,
GrowthApiClient, Registration, Winner,
};
use crate::State;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[cfg(desktop)]
use tauri::api::notification::Notification;
use tauri::Manager;
use tokio::sync::RwLock;
async fn get_client_id(
state: &tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<ClientIdPartial, BackendError> {
let keypair = get_identity_key(state).await?;
let client_id = keypair.public_key().to_base58_string();
let client_id_signature = keypair
.private_key()
.sign(client_id.as_bytes())
.to_base58_string();
Ok(ClientIdPartial {
client_id,
client_id_signature,
})
}
#[tauri::command]
pub async fn growth_tne_get_client_id(
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<ClientIdPartial, BackendError> {
get_client_id(&state).await
}
#[tauri::command]
pub async fn growth_tne_take_part(
app_handle: tauri::AppHandle,
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<Registration, BackendError> {
let notifications = super::assets::Content::get_notifications();
let client_id = get_client_id(&state).await?;
let registration = GrowthApiClient::registrations()
.register(&client_id)
.await?;
log::info!("<<< Test&Earn: registration details: {:?}", registration);
#[cfg(desktop)]
if let Err(e) = Notification::new(&app_handle.config().tauri.bundle.identifier)
.title(notifications.take_part.title)
.body(notifications.take_part.body)
.show()
{
log::error!("Could not show notification. Error = {:?}", e);
}
Ok(registration)
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Draws {
pub current: Option<DrawWithWordOfTheDay>,
pub next: Option<DrawWithWordOfTheDay>,
pub draws: Vec<DrawEntry>,
}
#[tauri::command]
pub async fn growth_tne_get_draws(client_details: ClientIdPartial) -> Result<Draws, BackendError> {
let draws_api = GrowthApiClient::daily_draws();
let current = draws_api.current().await.ok();
let next = draws_api.next().await.ok();
let draws = draws_api.entries(&client_details).await?;
Ok(Draws {
current,
next,
draws,
})
}
#[tauri::command]
pub async fn growth_tne_enter_draw(
client_details: ClientIdPartial,
draw_id: String,
) -> Result<DrawEntry, BackendError> {
Ok(GrowthApiClient::daily_draws()
.enter(&DrawEntryPartial {
draw_id,
client_id: client_details.client_id,
client_id_signature: client_details.client_id_signature,
})
.await?)
}
#[tauri::command]
pub async fn growth_tne_submit_wallet_address(
client_details: ClientIdPartial,
draw_id: String,
wallet_address: String,
registration_id: String,
) -> Result<Winner, BackendError> {
Ok(GrowthApiClient::daily_draws()
.claim(&ClaimPartial {
draw_id,
client_id: client_details.client_id,
client_id_signature: client_details.client_id_signature,
wallet_address,
registration_id,
})
.await?)
}
#[tauri::command]
pub async fn growth_tne_ping(client_details: ClientIdPartial) -> Result<(), BackendError> {
log::info!("Test&Earn is sending a ping...");
Ok(GrowthApiClient::registrations()
.ping(&client_details)
.await?)
}
#[cfg(desktop)]
#[tauri::command]
pub async fn growth_tne_toggle_window(
app_handle: tauri::AppHandle,
window_title: Option<String>,
) -> Result<(), BackendError> {
if let Some(window) = app_handle.windows().get("growth") {
log::info!("Closing growth window...");
if let Err(e) = window.close() {
log::error!("Unable to close growth window: {:?}", e);
}
return Ok(());
}
log::info!("Creating growth window...");
match tauri::WindowBuilder::new(
&app_handle,
"growth",
tauri::WindowUrl::App("growth.html".into()),
)
.title(window_title.unwrap_or_else(|| "NymConnect Test&Earn".to_string()))
.build()
{
Ok(window) => {
if let Err(e) = window.set_focus() {
log::error!("Unable to focus growth window: {:?}", e);
}
Ok(())
}
Err(e) => {
log::error!("Unable to create growth window: {:?}", e);
Err(BackendError::NewWindowError)
}
}
}
@@ -1,7 +1,6 @@
pub mod connection;
pub mod directory;
pub mod export;
pub mod growth;
pub mod help;
pub mod http;
#[cfg(desktop)]
+1 -1
View File
@@ -161,7 +161,7 @@ impl State {
pub fn load_socks5_config(&self) -> Result<Socks5Config> {
let id = self.get_config_id()?;
let config = Socks5Config::load_from_file(Some(&id))
let config = Socks5Config::load_from_file(&id)
.tap_err(|_| log::warn!("Failed to load configuration file"))?;
Ok(config)
}
@@ -1,84 +0,0 @@
import React from 'react';
import { Badge, Box, Button, Tooltip } from '@mui/material';
import MonetizationOnIcon from '@mui/icons-material/MonetizationOn';
import { invoke } from '@tauri-apps/api';
import Content from './content/en.yaml';
import { useClientContext } from '../../context/main';
import { useTestAndEarnContext } from './context/TestAndEarnContext';
import { NymShipyardTheme } from '../../theme';
import { ConnectionStatusKind } from '../../types';
export const Wrapper: FCWithChildren<{ disabled: boolean }> = ({ disabled, children }) => {
if (disabled) {
return (
<Badge badgeContent="!" color="warning">
<Tooltip arrow title={disabled ? Content.testAndEarn.mainWindow.button.popup.disconnected : undefined}>
<div>{children}</div>
</Tooltip>
</Badge>
);
}
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{children}</>;
};
export const TestAndEarnButtonArea: FCWithChildren = () => {
const clientContext = useClientContext();
const context = useTestAndEarnContext();
const disabled = clientContext.connectionStatus !== ConnectionStatusKind.connected;
const pinger = React.useRef<NodeJS.Timer | null>();
const doPing = async () => {
if (context.clientDetails) {
try {
await invoke('growth_tne_ping', { clientDetails: context.clientDetails });
} catch (_e) {
// console.error('Failed to ping: ', e);
}
}
};
React.useEffect(() => {
(async () => {
if (!disabled) {
// sleep a little until the SOCKS5 proxy connects
setTimeout(() => {
doPing();
}, 1000 * 10);
// update every 15 mins
pinger.current = setInterval(doPing, 1000 * 60 * 15);
} else if (pinger.current) {
clearInterval(pinger.current);
pinger.current = null;
}
})();
}, [disabled, context.clientDetails]);
const handleClick = async () => {
if (!disabled) {
await context.toggleGrowthWindow(Content.testAndEarn.popupWindow.title);
}
};
return (
<NymShipyardTheme>
<Box justifyContent="center" display="grid">
<Wrapper disabled={disabled}>
<Button
color={disabled ? 'secondary' : undefined}
variant="contained"
size="small"
endIcon={<MonetizationOnIcon />}
sx={{ width: '150px', mb: 4, opacity: disabled ? 0.4 : undefined }}
onClick={handleClick}
>
{context.registration
? Content.testAndEarn.mainWindow.button.text.entered
: Content.testAndEarn.mainWindow.button.text.default}
</Button>
</Wrapper>
</Box>
</NymShipyardTheme>
);
};
@@ -1,94 +0,0 @@
import * as React from 'react';
import { ComponentMeta } from '@storybook/react';
import { DateTime, Duration } from 'luxon';
import {
TestAndEarnCurrentDraw,
TestAndEarnCurrentDrawEntered,
TestAndEarnCurrentDrawFuture,
} from './TestAndEarnCurrentDraw';
import { NymShipyardTheme } from '../../theme';
import { DrawEntryStatus } from './context/types';
import { testMarkdown } from './context/mocks/TestAndEarnContext';
export default {
title: 'Growth/TestAndEarn/Components/Cards/Current Draw',
component: TestAndEarnCurrentDraw,
} as ComponentMeta<typeof TestAndEarnCurrentDraw>;
export const Valid = () => (
<NymShipyardTheme>
<TestAndEarnCurrentDraw
draw={{
id: '1',
start_utc: DateTime.now().toISO(),
end_utc: DateTime.now()
.plus(Duration.fromMillis(1000 * 3600))
.toISO(),
last_modified: DateTime.now().toISO(),
word_of_the_day: 'words words words',
}}
/>
</NymShipyardTheme>
);
export const EnteredMalformedDraw = () => (
<NymShipyardTheme>
<TestAndEarnCurrentDrawEntered
draw={{
id: '1',
start_utc: DateTime.now().toISO(),
end_utc: DateTime.now()
.plus(Duration.fromMillis(1000 * 3600))
.toISO(),
last_modified: DateTime.now().toISO(),
word_of_the_day: undefined,
entry: {
draw_id: '1',
status: DrawEntryStatus.pending,
id: 'aaaa',
timestamp: DateTime.now().toISO(),
},
}}
/>
</NymShipyardTheme>
);
export const EnteredDraw = () => (
<NymShipyardTheme>
<TestAndEarnCurrentDrawEntered
draw={{
id: '1',
start_utc: DateTime.now().toISO(),
end_utc: DateTime.now()
.plus(Duration.fromMillis(1000 * 3600))
.toISO(),
last_modified: DateTime.now().toISO(),
word_of_the_day: testMarkdown,
entry: {
draw_id: '1',
status: DrawEntryStatus.pending,
id: 'aaaa',
timestamp: DateTime.now().toISO(),
},
}}
/>
</NymShipyardTheme>
);
export const Future = () => (
<NymShipyardTheme>
<TestAndEarnCurrentDrawFuture
draw={{
id: '1',
start_utc: DateTime.now()
.plus(Duration.fromMillis(1000 * 3600))
.toISO(),
end_utc: DateTime.now()
.plus(Duration.fromMillis(1000 * 3600 * 2))
.toISO(),
last_modified: DateTime.now().toISO(),
word_of_the_day: 'words words words',
}}
/>
</NymShipyardTheme>
);
@@ -1,192 +0,0 @@
import React from 'react';
import LoadingButton from '@mui/lab/LoadingButton';
import { Alert, AlertTitle, Box, Card, CardContent, CardMedia, Link, Typography } from '@mui/material';
import { SxProps } from '@mui/system';
import { DateTime } from 'luxon';
import ReactMarkdown from 'react-markdown';
import assetAnimation from './content/assets/matrix.webp';
import { CopyToClipboard } from '../CopyToClipboard';
import { useTestAndEarnContext } from './context/TestAndEarnContext';
import { DrawEntryStatus, DrawWithWordOfTheDay } from './context/types';
import Content from './content/en.yaml';
export const TestAndEarnCurrentDrawFuture: FCWithChildren<{ draw?: DrawWithWordOfTheDay }> = ({ draw }) => {
const startsUtc = React.useMemo(() => draw && DateTime.fromISO(draw.start_utc), [draw?.start_utc]);
const startsIn = React.useMemo(() => {
if (draw && startsUtc) {
return startsUtc.toRelative();
}
return undefined;
}, [draw?.start_utc]);
if (!draw || !startsUtc) {
return null;
}
return (
<Card sx={{ mb: 2 }} elevation={10}>
<CardContent>
<h3>
{Content.testAndEarn.draw.next.header} {startsIn}
</h3>
<p>on {startsUtc.toLocaleString(DateTime.DATETIME_FULL)}</p>
</CardContent>
</Card>
);
};
export const TestAndEarnCurrentDrawEnter: FCWithChildren<{ draw?: DrawWithWordOfTheDay }> = ({ draw }) => {
const context = useTestAndEarnContext();
const [busy, setBusy] = React.useState(false);
const [error, setError] = React.useState<string>();
const handleClick = async () => {
if (!draw) {
setError('No draw selected');
return;
}
setBusy(true);
try {
await context.enterDraw(draw.id);
} catch (e) {
const message = `${e}`;
console.error('Could not enter draw', message);
setError(message);
}
setBusy(false);
};
return (
<Box display="flex" flexDirection="column" alignItems="center" py={3} px={2} mx={6} my={2}>
<Typography mb={4}>Complete todays task for the chance to earn 1000 NYMs.</Typography>
<LoadingButton variant="contained" size="large" loading={busy} onClick={handleClick}>
Start task
</LoadingButton>
{error && (
<Box mt={2}>
<Alert variant="filled" severity="error">
<AlertTitle>Oh no! Something went wrong.</AlertTitle>
{error}
</Alert>
</Box>
)}
</Box>
);
};
export const TestAndEarnCurrentDrawEntered: FCWithChildren<{ draw?: DrawWithWordOfTheDay }> = ({ draw }) => {
if (!draw || !draw.entry) {
return null;
}
if (!draw.word_of_the_day) {
return (
<Alert severity="error" variant="filled">
<AlertTitle>Oh no! Something is wrong</AlertTitle>
Someone configured the wrong instructions for the task, you will not be able to see it until this is fixed
</Alert>
);
}
return (
<Box
display="flex"
flexDirection="column"
alignItems="center"
sx={{ background: 'rgba(255,255,255,0.1)' }}
py={4}
mx={6}
my={2}
borderRadius={2}
>
<Box py={2} px={4} color="warning.light">
<ReactMarkdown>{draw.word_of_the_day}</ReactMarkdown>
</Box>
<Typography>{Content.testAndEarn.task.afterText}</Typography>
<Typography mt={2} fontFamily="monospace" fontWeight="bold" color="warning.main">
{draw.entry.id} <CopyToClipboard iconButton light text={draw.entry.id} />
</Typography>
<Typography mt={2}>{Content.testAndEarn.task.beforeSocials}</Typography>
<Typography mt={2} mx={1} textAlign="center">
<Typography component="span" color="info.light" fontWeight="bold">
Twitter
</Typography>{' '}
- remember to
<Typography component="span" color="info.light">
@nymproject
</Typography>{' '}
and use the hashtag{' '}
<Typography component="span" color="info.light">
#PrivacyLovesCompany
</Typography>
</Typography>
<Typography mt={2}>or</Typography>
<Typography textAlign="center" fontWeight="bold">
Nym{' '}
<Link target="_blank" href="https://t.me/nymchan" color="info.light">
Telegram channel
</Link>
</Typography>
</Box>
);
};
export const TestAndEarnCurrentDraw: FCWithChildren<{
draw?: DrawWithWordOfTheDay;
sx?: SxProps;
}> = ({ draw }) => {
const [trigger, setTrigger] = React.useState(DateTime.now().toISO());
const endsUtc = React.useMemo(() => draw && DateTime.fromISO(draw.end_utc), [draw?.end_utc]);
const closesIn = React.useMemo(() => {
if (draw && endsUtc) {
return endsUtc.toRelative();
}
return undefined;
}, [trigger, endsUtc]);
React.useEffect(() => {
const timer = setInterval(() => setTrigger(DateTime.now().toISO()), 1000 * 3600 * 15);
return () => clearInterval(timer);
}, []);
if (draw && closesIn && endsUtc) {
return (
<Card elevation={10}>
<CardContent>
<h3>
{"Today's task ends "}
{closesIn}
<Typography sx={{ opacity: 0.5 }}>
{endsUtc.weekdayLong} {endsUtc.toLocaleString(DateTime.DATETIME_FULL)}
</Typography>
</h3>
{!draw.entry && <TestAndEarnCurrentDrawEnter draw={draw} />}
{draw.entry && <TestAndEarnCurrentDrawEntered draw={draw} />}
</CardContent>
<CardMedia component="img" height="150" image={assetAnimation} alt="lottery" />
</Card>
);
}
return null;
};
export const TestAndEarnCurrentDrawWithState: FCWithChildren<{
sx?: SxProps;
}> = ({ sx }) => {
const context = useTestAndEarnContext();
if (
context.draws?.current?.entry?.status === DrawEntryStatus.winner ||
context.draws?.current?.entry?.status === DrawEntryStatus.claimed ||
context.draws?.current?.entry?.status === DrawEntryStatus.noWin
) {
return null;
}
if (!context.draws?.current) {
return <TestAndEarnCurrentDrawFuture draw={context.draws?.next} />;
}
return <TestAndEarnCurrentDraw sx={sx} draw={context.draws.current} />;
};
@@ -1,19 +0,0 @@
/* eslint-disable react/jsx-pascal-case */
import * as React from 'react';
import { ComponentMeta } from '@storybook/react';
import { NymShipyardTheme } from 'src/theme';
import { TestAndEarnDraws } from './TestAndEarnDraws';
import { MockTestAndEarnProvider_RegisteredWithAllDraws } from './context/mocks/TestAndEarnContext';
export default {
title: 'Growth/TestAndEarn/Components/Cards/Draws',
component: TestAndEarnDraws,
} as ComponentMeta<typeof TestAndEarnDraws>;
export const Draws = () => (
<NymShipyardTheme>
<MockTestAndEarnProvider_RegisteredWithAllDraws>
<TestAndEarnDraws />
</MockTestAndEarnProvider_RegisteredWithAllDraws>
</NymShipyardTheme>
);
@@ -1,196 +0,0 @@
/* eslint-disable react/jsx-no-useless-fragment */
import React from 'react';
import LoadingButton from '@mui/lab/LoadingButton';
import {
Alert,
AlertTitle,
Button,
Card,
CardContent,
Chip,
Dialog,
Table,
TableBody,
TableCell,
TableContainer,
TableRow,
Tooltip,
Typography,
} from '@mui/material';
import { SxProps } from '@mui/system';
import { DateTime } from 'luxon';
import { useTestAndEarnContext } from './context/TestAndEarnContext';
import { DrawEntry, DrawEntryStatus } from './context/types';
import { CopyToClipboard } from '../CopyToClipboard';
import { TestAndEarnEnterWalletAddress } from './TestAndEarnEnterWalletAddress';
import Content from './content/en.yaml';
const statusToText = (status: string): string => Content.testAndEarn.status.chip[status] || '-';
const statusToColor = (status: string): 'info' | 'success' | 'warning' | undefined => {
switch (status) {
case DrawEntryStatus.pending:
return 'info';
case DrawEntryStatus.winner:
return 'warning';
case DrawEntryStatus.claimed:
return 'success';
default:
return undefined;
}
};
const StatusText: FCWithChildren<{ entry: DrawEntry }> = ({ entry }) => {
const context = useTestAndEarnContext();
const [busy, setBusy] = React.useState(false);
const [error, setError] = React.useState<string>();
const [showWalletCapture, setShowWalletCapture] = React.useState(false);
const clear = () => {
setShowWalletCapture(false);
setError(undefined);
setBusy(false);
};
const handleStartWalletCapture = async () => {
setBusy(true);
setShowWalletCapture(true);
};
const cancelEndWalletCapture = async () => {
setBusy(false);
setShowWalletCapture(false);
};
const handleEndWalletCapture = async () => {
setBusy(true);
setShowWalletCapture(false);
if (!context.walletAddress) {
setError('Wallet address is not set');
return;
}
if (!entry.draw_id) {
setError('Task id is not set');
return;
}
try {
await context.claim(entry.draw_id, context.walletAddress);
} catch (e) {
const message = `${e}`;
console.error('Failed to submit claim');
setError(message);
}
setBusy(false);
};
if (error) {
return (
<Alert severity="error" variant="filled">
<AlertTitle>Oh no! Failed to submit claim</AlertTitle>
{error}
<Button variant="contained" color="secondary" size="small" onClick={() => clear()} sx={{ mx: 2 }}>
Try again!
</Button>
</Alert>
);
}
if (showWalletCapture) {
return (
<Dialog open fullWidth onBackdropClick={cancelEndWalletCapture}>
<TestAndEarnEnterWalletAddress onSubmit={handleEndWalletCapture} />
</Dialog>
);
}
switch (entry.status) {
case DrawEntryStatus.pending:
return <>{Content.testAndEarn.status.text.Pending}</>;
case DrawEntryStatus.winner:
return (
<>
{Content.testAndEarn.status.text.Winner}
<LoadingButton
loading={busy}
disabled={busy}
variant="contained"
sx={{ ml: 2 }}
size="small"
onClick={handleStartWalletCapture}
>
{Content.testAndEarn.winner.claimButton.text}
</LoadingButton>
</>
);
case DrawEntryStatus.claimed:
return <>{Content.testAndEarn.status.text.Claimed}</>;
case DrawEntryStatus.noWin:
return <>{Content.testAndEarn.status.text.NoWin}</>;
default:
return null;
}
};
export const TestAndEarnDraws: FCWithChildren<{
sx?: SxProps;
}> = () => {
const context = useTestAndEarnContext();
const draws = React.useMemo<DrawEntry[]>(
() =>
(context.draws?.draws || []).map((item) => ({
...item,
timestamp: DateTime.fromISO(item.timestamp).toLocaleString(DateTime.DATETIME_FULL),
})),
[context.draws?.draws],
);
if (!context.draws) {
return null;
}
return (
<Card sx={{ mb: 2 }}>
<CardContent>
<Typography mb={2}>Here is a history of the tasks you have completed:</Typography>
<TableContainer>
<Table>
<TableBody>
{draws.map((entry) => (
<TableRow key={entry.draw_id}>
<TableCell width="150px">{entry.timestamp}</TableCell>
<TableCell width="150px">
<Tooltip arrow title={`Task Id: ${entry.draw_id}`}>
<Chip label={statusToText(entry.status)} color={statusToColor(entry.status)} />
</Tooltip>
</TableCell>
<TableCell>
<StatusText entry={entry} />
</TableCell>
<TableCell>
{entry.id} <CopyToClipboard iconButton light text={entry.id} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</CardContent>
</Card>
);
};
export const TestAndEarnDrawsWithState: FCWithChildren<{
sx?: SxProps;
}> = ({ sx }) => {
const context = useTestAndEarnContext();
const drawCount = context.draws?.draws?.length || 0;
if (drawCount < 1) {
return null;
}
return <TestAndEarnDraws sx={sx} />;
};
@@ -1,41 +0,0 @@
import * as React from 'react';
import { ComponentMeta } from '@storybook/react';
import { Box } from '@mui/material';
import { TestAndEarnEnterWalletAddress } from './TestAndEarnEnterWalletAddress';
import { TestAndEarnContextProvider } from './context/TestAndEarnContext';
import { NymShipyardTheme } from '../../theme';
export default {
title: 'Growth/TestAndEarn/Components/Enter wallet address',
component: TestAndEarnEnterWalletAddress,
} as ComponentMeta<typeof TestAndEarnEnterWalletAddress>;
export const Empty = () => (
<NymShipyardTheme>
<TestAndEarnContextProvider>
<Box minWidth="25vw" maxWidth={500}>
<TestAndEarnEnterWalletAddress sx={{ width: '100%' }} />
</Box>
</TestAndEarnContextProvider>
</NymShipyardTheme>
);
export const ErrorValue = () => (
<NymShipyardTheme>
<TestAndEarnContextProvider>
<Box minWidth="25vw" maxWidth={500}>
<TestAndEarnEnterWalletAddress initialValue="this is a bad value" sx={{ width: '100%' }} />
</Box>
</TestAndEarnContextProvider>
</NymShipyardTheme>
);
export const ValidValue = () => (
<NymShipyardTheme>
<TestAndEarnContextProvider>
<Box minWidth="25vw" maxWidth={500}>
<TestAndEarnEnterWalletAddress initialValue="n1xr4w0kddak8d8zlfmu8sl6dk2r4p9uhhzzlaec" sx={{ width: '100%' }} />
</Box>
</TestAndEarnContextProvider>
</NymShipyardTheme>
);
@@ -1,37 +0,0 @@
import React from 'react';
import { WalletAddressFormField } from '@nymproject/react/account/WalletAddressFormField';
import { SxProps } from '@mui/system';
import { Box, Button, Paper, Stack } from '@mui/material';
import ArrowCircleRightIcon from '@mui/icons-material/ArrowCircleRight';
import { useTestAndEarnContext } from './context/TestAndEarnContext';
export const TestAndEarnEnterWalletAddress: FCWithChildren<{
initialValue?: string;
placeholder?: string;
onSubmit?: () => Promise<void> | void;
sx?: SxProps;
}> = ({ initialValue, placeholder, onSubmit }) => {
const context = useTestAndEarnContext();
const [isAddressValid, setAddressIsValid] = React.useState(false);
return (
<Paper sx={{ py: 4, px: 2 }}>
<Stack spacing={4}>
<Box>
<WalletAddressFormField
label="Wallet address"
initialValue={initialValue}
placeholder={placeholder || 'Please enter your wallet address'}
onChanged={context.setWalletAddress}
onValidate={setAddressIsValid}
sx={{ width: '80%' }}
/>
</Box>
<Box>
<Button variant="contained" endIcon={<ArrowCircleRightIcon />} disabled={!isAddressValid} onClick={onSubmit}>
Submit
</Button>
</Box>
</Stack>
</Paper>
);
};
@@ -1,13 +0,0 @@
import React from 'react';
import { Box, Button } from '@mui/material';
export const TestAndEarnError: FCWithChildren<{ error?: string }> = ({ error = 'An error has occurred' }) => (
<Box>
<Box mb={4} fontWeight="bold">
{error}
</Box>
<Button variant="outlined" color="secondary">
Send us an error report
</Button>
</Box>
);
@@ -1,162 +0,0 @@
/* eslint-disable react/jsx-pascal-case */
import * as React from 'react';
import { ComponentMeta } from '@storybook/react';
import { Alert, Box } from '@mui/material';
import { NymShipyardTheme } from 'src/theme';
import { TestAndEarnPopup, TestAndEarnPopupContent } from './TestAndEarnPopup';
import { TestAndEarnContextProvider } from './context/TestAndEarnContext';
import { MockProvider } from '../../context/mocks/main';
import { ConnectionStatusKind } from '../../types';
import {
MockTestAndEarnProvider_NotRegistered,
MockTestAndEarnProvider_RegisteredAndError,
MockTestAndEarnProvider_RegisteredWithDraws,
MockTestAndEarnProvider_RegisteredWithDrawsAndEntry,
MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndNoWinner,
MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinner,
MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinnerClaimed,
MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinnerCollectWallet,
MockTestAndEarnProvider_RegisteredWithDrawsNoCurrent,
} from './context/mocks/TestAndEarnContext';
export default {
title: 'Growth/TestAndEarn/Content/Popup',
component: TestAndEarnPopupContent,
} as ComponentMeta<typeof TestAndEarnPopupContent>;
const MacOSWindow: FCWithChildren<{
width?: string | number;
height?: string | number;
title?: string;
children: React.ReactNode;
}> = ({ title, width, height, children }) => (
<Box sx={{ border: '1px solid #EEEEEE', width, height }}>
<Box sx={{ background: '#EEEEEE', display: 'grid', gridTemplateColumns: 'auto auto', gridTemplateRows: 'auto' }}>
<Box ml={1}>
<svg width="52px" height="12px" viewBox="0 0 52 12" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g id="Components" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<g id="macOS" transform="translate(-600.000000, -220.000000)">
<g id="Group" transform="translate(600.000000, 220.000000)" strokeWidth="0.5">
<g id="Traffic-Lights">
<circle id="Traffic-Light---Zoom" stroke="#1BAC2C" fill="#2ACB42" cx="46" cy="6" r="5.75" />
<circle id="Traffic-Light---Minimise" stroke="#DFA023" fill="#FFC12F" cx="26" cy="6" r="5.75" />
<circle id="Traffic-Light---Close" stroke="#E24640" fill="#FF6157" cx="6" cy="6" r="5.75" />
</g>
</g>
</g>
</g>
</svg>
</Box>
<Box
sx={{
alignSelf: 'center',
color: '#000000',
opacity: 0.848675272,
fontSize: 13,
}}
>
{title || 'Window title'}
</Box>
</Box>
<Box sx={{ overflowY: 'scroll', height: 'calc(100% - 25px)' }}>{children}</Box>
</Box>
);
const Wrapper: FCWithChildren<{ text: React.ReactNode }> = ({ text }) => (
<NymShipyardTheme>
<Alert severity="info" sx={{ mb: 4 }}>
{text}
</Alert>
<MacOSWindow width={700} height={600} title="Test&Earn">
<TestAndEarnPopup />
</MacOSWindow>
</NymShipyardTheme>
);
export const Stage0 = () => (
<MockProvider connectionStatus={ConnectionStatusKind.connected}>
<MockTestAndEarnProvider_NotRegistered>
<Wrapper text="The user sees this content when they have not joined Test&Earn." />
</MockTestAndEarnProvider_NotRegistered>
</MockProvider>
);
export const Stage1EnterDraw = () => (
<MockProvider connectionStatus={ConnectionStatusKind.connected}>
<MockTestAndEarnProvider_RegisteredWithDraws>
<Wrapper text="The user has signed up and can see the next draw and choose the enter." />
</MockTestAndEarnProvider_RegisteredWithDraws>
</MockProvider>
);
export const Stage2GetTask = () => (
<MockProvider connectionStatus={ConnectionStatusKind.connected}>
<MockTestAndEarnProvider_RegisteredWithDrawsAndEntry>
<Wrapper text="The user has entered a draw and can view the word of the day if they missed the popup notification." />
</MockTestAndEarnProvider_RegisteredWithDrawsAndEntry>
</MockProvider>
);
export const Stage3Winner = () => (
<MockProvider connectionStatus={ConnectionStatusKind.connected}>
<MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinner>
<Wrapper text="The user has won and can claim their prize." />
</MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinner>
</MockProvider>
);
export const Stage3NoPrize = () => (
<MockProvider connectionStatus={ConnectionStatusKind.connected}>
<MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndNoWinner>
<Wrapper text="The user has not won. A winner has been announced." />
</MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndNoWinner>
</MockProvider>
);
export const Stage4EnterWalletAddress = () => (
<MockProvider connectionStatus={ConnectionStatusKind.connected}>
<MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinnerCollectWallet>
<Wrapper text="The user is a winner, claims their prize and enters their wallet address." />
</MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinnerCollectWallet>
</MockProvider>
);
export const Stage5ClaimedPrize = () => (
<MockProvider connectionStatus={ConnectionStatusKind.connected}>
<MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinnerClaimed>
<Wrapper text="The user is a winner and has claimed their prize." />
</MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinnerClaimed>
</MockProvider>
);
export const Stage6DrawsFinished = () => (
<MockProvider connectionStatus={ConnectionStatusKind.connected}>
<MockTestAndEarnProvider_RegisteredWithDrawsNoCurrent>
<Wrapper text="There are no more draws. The user can see their entries and prizes they have claimed." />
</MockTestAndEarnProvider_RegisteredWithDrawsNoCurrent>
</MockProvider>
);
export const Connecting = () => (
<MockProvider connectionStatus={ConnectionStatusKind.connecting}>
<TestAndEarnContextProvider>
<Wrapper text="Test&Earn requires the user to be connected to talk the API. This is shown while connecting." />
</TestAndEarnContextProvider>
</MockProvider>
);
export const Disconnected = () => (
<MockProvider connectionStatus={ConnectionStatusKind.disconnected}>
<TestAndEarnContextProvider>
<Wrapper text="Test&Earn requires the user to be connected to talk the API. This is shown when not connected." />
</TestAndEarnContextProvider>
</MockProvider>
);
export const Error = () => (
<MockProvider>
<MockTestAndEarnProvider_RegisteredAndError>
<Wrapper text="The user see this with details about errors. They can submit an error report." />
</MockTestAndEarnProvider_RegisteredAndError>
</MockProvider>
);
@@ -1,118 +0,0 @@
import React from 'react';
import { Box, CircularProgress, LinearProgress, Stack, Typography } from '@mui/material';
import { useClientContext } from '../../context/main';
import ErrorContent from './content/TestAndEarn/Error.mdx';
import ContentStep0 from './content/TestAndEarn/Stage0_intro.mdx';
import ContentNotAvailable from './content/TestAndEarnNotAvaialble.mdx';
import { ConnectionStatusKind } from '../../types';
import { useTestAndEarnContext } from './context/TestAndEarnContext';
import { TestAndEarnWinnerWithState } from './TestAndEarnWinner';
import { TestAndEarnCurrentDrawWithState } from './TestAndEarnCurrentDraw';
import { TestAndEarnDrawsWithState } from './TestAndEarnDraws';
enum Stages {
mustRegister = 'mustRegister',
registered = 'registered',
}
export const TestAndEarnPopupContent: FCWithChildren<{
stage?: string;
connectionStatus?: ConnectionStatusKind;
error?: string;
}> = ({ connectionStatus, error, stage = Stages.mustRegister }) => {
if (error) {
return (
<Box p={4}>
<ErrorContent error={error} />
</Box>
);
}
if (!connectionStatus || connectionStatus === ConnectionStatusKind.disconnected) {
return (
<Box p={4}>
<ContentNotAvailable />
</Box>
);
}
if (connectionStatus === ConnectionStatusKind.connecting || connectionStatus === ConnectionStatusKind.disconnecting) {
return (
<Box p={4} justifyContent="center" alignItems="center" display="flex">
<CircularProgress />
<Typography ml={3}>Please wait...</Typography>
</Box>
);
}
switch (stage) {
case Stages.mustRegister:
return (
<Box p={4}>
<ContentStep0 />
</Box>
);
case Stages.registered:
return (
<Box p={4}>
<TestAndEarnWinnerWithState />
<TestAndEarnCurrentDrawWithState />
<TestAndEarnDrawsWithState />
</Box>
);
default:
return (
<Box p={4}>
<Stack direction="row" spacing={2} display="flex" alignItems="center">
<CircularProgress />
<Box>Waiting for task information...</Box>
</Stack>
</Box>
);
}
};
export const TestAndEarnPopup: FCWithChildren = () => {
const clientContext = useClientContext();
const context = useTestAndEarnContext();
React.useEffect(() => {
if (clientContext.connectionStatus === ConnectionStatusKind.connected) {
context.refresh();
}
}, [clientContext.connectionStatus]);
const stage = React.useMemo<Stages>(() => {
if (context.registration) {
return Stages.registered;
}
return Stages.mustRegister;
}, [context.registration?.id]);
React.useEffect(() => {
const interval = setInterval(context.refresh, 1000 * 60 * 5);
return () => clearInterval(interval);
}, []);
if (!context.loadedOnce && clientContext.connectionStatus === ConnectionStatusKind.connected) {
const message = 'Waiting for data to be transferred over the mixnet...';
return (
<Box p={4}>
<Stack direction="row" spacing={2} display="flex" alignItems="center">
<CircularProgress />
<Box>{message}</Box>
{/* {process.env.NODE_ENV === 'development' && <pre>{JSON.stringify(context, null, 2)}</pre>} */}
</Stack>
</Box>
);
}
return (
<>
{context.loading && <LinearProgress />}
{/* <Button onClick={context.refresh}>Refresh</Button> */}
<TestAndEarnPopupContent connectionStatus={clientContext.connectionStatus} stage={stage} error={context.error} />
{/* {process.env.NODE_ENV === 'development' && <pre>{JSON.stringify(context, null, 2)}</pre>} */}
</>
);
};
@@ -1,75 +0,0 @@
import React from 'react';
import { Alert, AlertTitle, Box, Checkbox, Link, Stack } from '@mui/material';
import LoadingButton from '@mui/lab/LoadingButton';
import { SxProps } from '@mui/system';
import ArrowCircleRightIcon from '@mui/icons-material/ArrowCircleRight';
import { invoke } from '@tauri-apps/api';
import { useTestAndEarnContext } from './context/TestAndEarnContext';
import { Registration } from './context/types';
export const TestAndEarnTakePart: FCWithChildren<{
websiteLinkUrl: string;
websiteLinkText: string;
content: string;
sx?: SxProps;
}> = ({ content, websiteLinkText, websiteLinkUrl, sx }) => {
const [agree, setAgree] = React.useState(false);
const [busy, setBusy] = React.useState(false);
const [error, setError] = React.useState<string>();
const context = useTestAndEarnContext();
const handleNext = async () => {
try {
setBusy(true);
if (context.clientDetails) {
const registration: Registration = await invoke('growth_tne_take_part');
console.log('Registration: ', { registration });
await context.setAndStoreRegistration(registration);
if (registration) {
console.log('Registered...');
} else {
setError('Failed to get registration details');
}
} else {
setError('Failed to get client details');
}
} catch (e) {
const message = `${e}`;
console.error('An error occurred', message);
setError(message);
setBusy(false); // the busy state only resets on errors, for success stats, the context will navigate the window away
}
};
return (
<>
<Stack direction="row" spacing={6} alignItems="center" sx={{ ...(Array.isArray(sx) ? sx : [sx]) }}>
<Stack alignItems="center" direction="row">
<Checkbox onChange={(_event, checked) => setAgree(checked)} />
<Box color="primary.light" fontWeight="bold">
{content}
</Box>
</Stack>
<Box>
<Link href={websiteLinkUrl} target="_blank" color="secondary" sx={{ opacity: 0.5 }}>
{websiteLinkText}
</Link>
</Box>
<LoadingButton
loading={busy}
disabled={!agree || busy}
variant="contained"
sx={{ justifySelf: 'end' }}
endIcon={<ArrowCircleRightIcon />}
onClick={handleNext}
>
Next
</LoadingButton>
</Stack>
{error && (
<Alert severity="error" variant="filled">
<AlertTitle>Oh no! Something went wrong</AlertTitle>
{error}
</Alert>
)}
</>
);
};
@@ -1,19 +0,0 @@
/* eslint-disable react/jsx-pascal-case */
import * as React from 'react';
import { ComponentMeta } from '@storybook/react';
import { NymShipyardTheme } from 'src/theme';
import { TestAndEarnWinner, TestAndEarnWinnerWithState } from './TestAndEarnWinner';
import { MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinner } from './context/mocks/TestAndEarnContext';
export default {
title: 'Growth/TestAndEarn/Components/Cards/Winner',
component: TestAndEarnWinner,
} as ComponentMeta<typeof TestAndEarnWinner>;
export const Winner = () => (
<NymShipyardTheme>
<MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinner>
<TestAndEarnWinnerWithState />
</MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinner>
</NymShipyardTheme>
);
@@ -1,114 +0,0 @@
import React from 'react';
import { Alert, AlertTitle, Button, Card, CardContent, CardMedia, Dialog, Typography } from '@mui/material';
import { SxProps } from '@mui/system';
import LoadingButton from '@mui/lab/LoadingButton';
import winner from './content/assets/winner.webp';
import { useTestAndEarnContext } from './context/TestAndEarnContext';
import { DrawEntry, DrawEntryStatus } from './context/types';
import { TestAndEarnEnterWalletAddress } from './TestAndEarnEnterWalletAddress';
import Content from './content/en.yaml';
export const TestAndEarnWinner: FCWithChildren<{
sx?: SxProps;
entry?: DrawEntry;
}> = ({ entry }) => {
const context = useTestAndEarnContext();
const [busy, setBusy] = React.useState(false);
const [error, setError] = React.useState<string>();
const [showWalletCapture, setShowWalletCapture] = React.useState(false);
const clear = () => {
setShowWalletCapture(false);
setError(undefined);
setBusy(false);
};
const handleStartWalletCapture = async () => {
setBusy(true);
setShowWalletCapture(true);
};
const cancelEndWalletCapture = async () => {
setBusy(false);
setShowWalletCapture(false);
};
const handleEndWalletCapture = async () => {
setBusy(true);
setShowWalletCapture(false);
if (!context.walletAddress) {
setError('Wallet address is not set');
return;
}
if (!entry?.draw_id) {
setError('Draw id is not set');
return;
}
try {
await context.claim(entry.draw_id, context.walletAddress);
} catch (e) {
const message = `${e}`;
console.error('Failed to submit claim', entry.draw_id, context.walletAddress);
setError(message);
}
setBusy(false);
};
return (
<>
{showWalletCapture && (
<Dialog open fullWidth onBackdropClick={cancelEndWalletCapture}>
<TestAndEarnEnterWalletAddress onSubmit={handleEndWalletCapture} />
</Dialog>
)}
<Card sx={{ mb: 2 }}>
<CardMedia component="img" height="165" image={winner} alt="winner" />
<CardContent>
<Typography color="warning.main" fontSize={20} fontWeight="bold">
{Content.testAndEarn.winner.card.header}
</Typography>
<Typography mt={2}>
{entry && (
<>
{Content.testAndEarn.winner.card.text} {entry.draw_id}.
</>
)}
<LoadingButton
loading={busy}
variant="contained"
sx={{ ml: 2, my: 2 }}
size="small"
onClick={handleStartWalletCapture}
>
{Content.testAndEarn.winner.claimButton.text}
</LoadingButton>
</Typography>
{error && (
<Alert severity="error" variant="filled">
<AlertTitle>Oh no! Failed to submit claim</AlertTitle>
{error}
<Button variant="contained" color="secondary" size="small" onClick={() => clear()} sx={{ mx: 2 }}>
Try again!
</Button>
</Alert>
)}
</CardContent>
</Card>
</>
);
};
export const TestAndEarnWinnerWithState: FCWithChildren<{
sx?: SxProps;
}> = ({ sx }) => {
const context = useTestAndEarnContext();
if (context.draws?.current?.entry?.status === DrawEntryStatus.winner) {
return <TestAndEarnWinner sx={sx} entry={context.draws.current.entry} />;
}
// when the user does not have any unclaimed prizes, don't render anything
return null;
};
@@ -1,12 +0,0 @@
import React from 'react';
import { Box } from '@mui/material';
import { SxProps } from '@mui/system';
import Content from './content/TestAndEarn/WinnerEntersWalletAddress.mdx';
export const TestAndEarnWinnerWalletAddress: FCWithChildren<{
sx?: SxProps;
}> = () => (
<Box>
<Content />
</Box>
);
@@ -1,17 +0,0 @@
import { Alert, AlertTitle, Link } from '@mui/material';
import { TestAndEarnError } from '../../TestAndEarnError';
<Alert severity="error" sx={{ mb: 4 }}>
<AlertTitle>Oh no! Something went wrong</AlertTitle>
Sorry about that. Here is some more information about the error that occurred:
<TestAndEarnError/>
Any error reports that you send us will contain information about your client, the gateway you're using and your IP address.
We need this information to make Nym better for everyone.
</Alert>
If you'd like more information about Test&Earn please look on the <Link href="http://shipyard.nymtech.net/test-and-win">Shipyard website</Link>.
@@ -1,25 +0,0 @@
import { Card, CardContent, Link, Typography } from '@mui/material';
import { TestAndEarnTakePart } from '../../TestAndEarnTakePart';
### Test privacy & Earn tokens!
<Typography color="primary.light" component="span">
Help us stress test the Nym privacy system and have the chance to earn 1000 NYMs per day!
</Typography>
All you need to do is:
1. Make sure you're running NymConnect and it is connected.
2. Note your reference number.
3. NymConnect will ping you a task every day.
Complete the task, post it on Twitter <Typography color="primary.light" component="span">#PrivacyLovesCompany</Typography> or <Link target="_blank" href="https://t.me/nymchan">Telegram</Link> along with your reference number!
Thank you for being part of the Nym community and helping to build a flourishing and free digital society. #PrivacyLovesCompany and we love you!
<Card>
<CardContent sx={{ py: 2 }}>
<TestAndEarnTakePart content={"I want to take part"} websiteLinkText={"Terms and conditions"}
websiteLinkUrl={"https://shipyard.nymtech.net/test-and-win"} sx={{ py: 2 }} />
</CardContent>
</Card>
@@ -1,9 +0,0 @@
import { TestAndEarnEnterWalletAddress } from '../../TestAndEarnEnterWalletAddress';
### 🎉 Congratulations! One more thing...
We need one more thing from you, and that is your wallet address:
<TestAndEarnEnterWalletAddress/>
Once you've submitting your wallet address over the mixnet, we will be in touch to arrange sending your tokens to you.
@@ -1,3 +0,0 @@
## 😕 Sorry, Test&Earn is only accessible while you are connected to the mixnet
Please connect to any service provider and try again once the connection has been established.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 498 KiB

@@ -1,43 +0,0 @@
testAndEarn:
mainWindow:
button:
text:
default: Join Test&Earn
entered: Test&Earn
claim: Claim your reward
popup:
disconnected: Test&Earn is only available when connected. Please connect to any service provider.
help:
url: https://shipyard.nymtech.net/test-and-win
popupWindow:
title: NymConnect Test&Earn 🌈
notifications:
takePart:
title: Thanks for taking part in Ter&Earn
body: Watch out for new tasks 👀 and take part to earn
youAreInDraw:
title: Thanks for completing the task ✨
body: Post a message on Telegram, Discord or Twitter for a chance to to be selected 🤞 good luck
task:
afterText: Copy your reference number
beforeSocials: "And include it in your post to:"
draw:
next:
header: The next task starts
winner:
claimButton:
text: Claim your reward!
card:
header: You are a top contributor!
text: Congratulations, you have earned the reward for
status:
chip:
Pending: Good luck 🤞
Winner: Rewarded!
Claimed: Claimed
NoWin: No rewards
text:
Pending: Task completed. Good luck 🤞
Winner: Well done 🎉
Claimed: You have claimed the reward 💰
NoWin: Sorry you were not a top contributor, better luck next time!
@@ -1,272 +0,0 @@
/* eslint-disable @typescript-eslint/naming-convention */
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
import { forage } from '@tauri-apps/tauri-forage';
import { invoke } from '@tauri-apps/api';
import { ClientId, DrawEntry, Draws, Registration } from './types';
import { useClientContext } from '../../../context/main';
import { ConnectionStatusKind } from '../../../types';
export type TTestAndEarnContext = {
loadedOnce: boolean;
loading: boolean;
clientDetails?: ClientId;
registration?: Registration;
walletAddress?: string;
draws?: Draws;
isWinnerWithUnclaimedPrize?: boolean;
isEnterWallet?: boolean;
error?: string;
setWalletAddress: (newWalletAddress: string) => void;
clearStorage: () => Promise<void>;
toggleGrowthWindow: (windowTitle?: string) => Promise<void>;
setAndStoreClientId: (newClientId: ClientId) => Promise<void>;
setAndStoreRegistration: (registration: Registration) => Promise<void>;
enterDraw: (drawId: string) => Promise<DrawEntry>;
claim: (drawId: string, walletAddress: string) => Promise<void>;
refresh: () => Promise<void>;
};
const defaultValue: TTestAndEarnContext = {
loadedOnce: false,
loading: true,
setWalletAddress: () => undefined,
clearStorage: async () => undefined,
toggleGrowthWindow: async () => undefined,
setAndStoreRegistration: async () => undefined,
setAndStoreClientId: async () => undefined,
enterDraw: async () => ({} as DrawEntry),
claim: async () => undefined,
refresh: async () => undefined,
};
export const TestAndEarnContext = createContext(defaultValue);
const CLIENT_ID_KEY = 'tne_client_id';
const REGISTRATION_KEY = 'tne_registration';
export const TestAndEarnContextProvider: FCWithChildren = ({ children }) => {
const clientContext = useClientContext();
const [loadedOnce, setLoadedOnce] = useState(false);
const [loading, setLoading] = useState(true);
const [walletAddress, setWalletAddress] = useState<string>();
const [registration, setRegistration] = useState<Registration>();
const [clientDetails, setClientDetails] = useState<ClientId>();
const [draws, setDraws] = useState<Draws>();
const setAndStoreClientId = async (newClientId: ClientId) => {
await forage.setItem({ key: CLIENT_ID_KEY, value: newClientId } as any)();
setClientDetails((prevState) => {
if (
prevState?.client_id !== newClientId.client_id ||
prevState?.client_id_signature !== newClientId.client_id_signature
) {
console.log('Setting client details');
return newClientId;
}
console.log('Skipping client details');
return prevState;
});
};
const loadClientDetails = async () => {
const data: ClientId | undefined = await forage.getItem({ key: CLIENT_ID_KEY })();
if (data) {
try {
setClientDetails((prevState) => {
if (prevState?.client_id !== data.client_id || prevState?.client_id_signature !== data.client_id_signature) {
console.log('Setting client details');
return data;
}
console.log('Skipping client details');
return prevState;
});
} catch (e) {
console.error('Failed to get registration');
}
} else {
const clientId: ClientId = await invoke('growth_tne_get_client_id');
await setAndStoreClientId(clientId);
}
};
const loadRegistration = async () => {
const data: Registration | undefined = await forage.getItem({ key: REGISTRATION_KEY })();
if (data) {
try {
setRegistration((prevState) => {
if (
prevState?.timestamp !== data.timestamp ||
prevState.client_id_signature !== data.client_id_signature ||
prevState.id !== data.id
) {
console.log('Setting registration');
return data;
}
console.log('Skipping registration');
return prevState;
});
} catch (e) {
console.error('Failed to get registration');
}
}
};
const loadDraws = React.useCallback(async () => {
setLoading(true);
let clientDetailsForDraws = clientDetails;
try {
if (!clientDetailsForDraws) {
console.log('[loadDraws] client details not set, trying to get...');
clientDetailsForDraws = await invoke('growth_tne_get_client_id');
}
if (!clientDetailsForDraws) {
console.log('[loadDraws] failed to get client details not set, skipping...');
setLoading(false);
setLoadedOnce(true);
return;
}
const newDraws: Draws = await invoke('growth_tne_get_draws', { clientDetails: clientDetailsForDraws });
console.log('[loadDraws] draws = ', newDraws);
// find the entered draw and keep a reference
const entered = newDraws.draws.find((draw) => draw.draw_id === newDraws.current?.id);
if (newDraws.current) {
newDraws.current.entry = entered;
}
console.log('[loadDraws] setting draws');
setDraws(newDraws);
} catch (e) {
console.error('Could not get draws: ', e);
}
setLoading(false);
setLoadedOnce(true);
console.log('[loadDraws] done, loaded once');
}, [clientDetails]);
React.useEffect(() => {
loadClientDetails().catch(console.error);
loadRegistration().catch(console.error);
}, []);
React.useEffect(() => {
if (registration && clientContext.connectionStatus === ConnectionStatusKind.connected) {
setTimeout(() => {
loadDraws().catch(console.error);
}, 1000 * 3);
}
}, [registration?.id, registration?.timestamp, clientContext.connectionStatus]);
const refresh = React.useCallback(async () => {
console.log('Refreshing...');
console.log('Loading client details...');
await loadClientDetails();
console.log('Loading registration...');
await loadRegistration();
console.log('Loading draws...');
await loadDraws();
console.log('Refresh complete.');
}, [clientDetails]);
const clearStorage = async () => {
await forage.setItem({ key: REGISTRATION_KEY, value: undefined })();
};
const toggleGrowthWindow = useCallback(async (windowTitle?: string) => {
try {
await invoke('growth_tne_toggle_window', { windowTitle });
} catch (e) {
console.error('Failed to toggle growth window', e);
}
}, []);
const setAndStoreRegistration = async (newRegistration: Registration) => {
await forage.setItem({ key: REGISTRATION_KEY, value: newRegistration } as any)();
setRegistration(newRegistration);
};
const enterDraw = async (drawId: string): Promise<DrawEntry> => {
if (!clientDetails) {
throw new Error('No client details set');
}
if (!draws) {
throw new Error('No draws set');
}
const existingEntry: DrawEntry | undefined = draws.draws.filter((d) => d.draw_id === drawId)[0];
if (existingEntry) {
throw new Error('Already entered into draw');
}
const entry: DrawEntry = await invoke('growth_tne_enter_draw', { clientDetails, drawId });
console.log('Entered draw', { entry });
await loadDraws();
return entry;
};
const claim = async (drawId: string, newWalletAddress: string) => {
if (!clientDetails) {
throw new Error('No client details set');
}
if (!draws) {
throw new Error('No draws set');
}
if (!registration) {
throw new Error('No registration set');
}
const registrationId = registration.id;
const args = {
registrationId,
clientDetails,
drawId,
walletAddress: newWalletAddress,
};
console.log({ args });
await invoke('growth_tne_submit_wallet_address', args);
await loadDraws();
};
const contextValue = useMemo<TTestAndEarnContext>(
() => ({
loadedOnce,
loading,
clientDetails,
registration,
walletAddress,
draws,
clearStorage,
toggleGrowthWindow,
setWalletAddress,
setAndStoreClientId,
setAndStoreRegistration,
enterDraw,
refresh,
claim,
}),
[
loadedOnce,
loading,
walletAddress,
registration,
refresh,
draws,
draws?.current?.last_modified,
draws?.current?.entry,
draws?.draws.length,
clientDetails,
],
);
return <TestAndEarnContext.Provider value={contextValue}>{children}</TestAndEarnContext.Provider>;
};
export const useTestAndEarnContext = () => useContext(TestAndEarnContext);
@@ -1,262 +0,0 @@
/* eslint-disable @typescript-eslint/naming-convention */
import React from 'react';
import { DateTime } from 'luxon';
import { TTestAndEarnContext, TestAndEarnContext } from '../TestAndEarnContext';
import { DrawEntry, DrawEntryStatus, DrawWithWordOfTheDay } from '../types';
const methodDefaults = {
loadedOnce: true,
loading: false,
refresh: async () => undefined,
setAndStoreClientId: async () => undefined,
setAndStoreRegistration: async () => undefined,
clearStorage: async () => undefined,
toggleGrowthWindow: async () => undefined,
setWalletAddress: async () => undefined,
enterDraw: async () => ({} as DrawEntry),
claim: async () => undefined,
};
const mockValues_NotRegistered: TTestAndEarnContext = {
...methodDefaults,
};
export const MockTestAndEarnProvider_NotRegistered: FCWithChildren = ({ children }) => (
<TestAndEarnContext.Provider value={mockValues_NotRegistered}>{children}</TestAndEarnContext.Provider>
);
export const testMarkdown = `**Create a sentence including "Nym" and one or more of the following words** *(in any language)*:
- Privacy
- Pleasure
- Pineapple
- Mix
`;
const mockValues_Registered: TTestAndEarnContext = {
...methodDefaults,
registration: {
id: '1234',
client_id_signature: 'signature',
client_id: '5678',
timestamp: '2022-12-12T18:17:37.840Z',
},
};
export const MockTestAndEarnProvider_Registered: FCWithChildren = ({ children }) => (
<TestAndEarnContext.Provider value={mockValues_Registered}>{children}</TestAndEarnContext.Provider>
);
const allDraws: DrawEntry[] = [
{
draw_id: '1111',
timestamp: DateTime.now().toISO(),
id: 'AAAA',
status: DrawEntryStatus.pending,
},
{
draw_id: '2222',
timestamp: DateTime.now().toISO(),
id: 'BBBB',
status: DrawEntryStatus.noWin,
},
{
draw_id: '2222',
timestamp: DateTime.now().toISO(),
id: 'BBBB',
status: DrawEntryStatus.claimed,
},
{
draw_id: '2222',
timestamp: DateTime.now().toISO(),
id: 'BBBB',
status: DrawEntryStatus.winner,
},
];
const draws: DrawEntry[] = [
{
draw_id: '1111',
timestamp: DateTime.now().toISO(),
id: 'AAAA',
status: DrawEntryStatus.pending,
},
{
draw_id: '2222',
timestamp: DateTime.now().toISO(),
id: 'BBBB',
status: DrawEntryStatus.noWin,
},
];
const drawsWithWin: DrawEntry[] = [
{
draw_id: '1111',
timestamp: DateTime.now().toISO(),
id: 'AAAA',
status: DrawEntryStatus.winner,
},
{
draw_id: '2222',
timestamp: DateTime.now().toISO(),
id: 'BBBB',
status: DrawEntryStatus.noWin,
},
];
const drawsWithClaim: DrawEntry[] = [
{
draw_id: '1111',
timestamp: DateTime.now().toISO(),
id: 'AAAA',
status: DrawEntryStatus.claimed,
},
{
draw_id: '2222',
timestamp: DateTime.now().toISO(),
id: 'BBBB',
status: DrawEntryStatus.noWin,
},
];
const current: DrawWithWordOfTheDay = {
id: '1111',
start_utc: DateTime.now().toISO(),
end_utc: DateTime.now().plus({ day: 1 }).minus({ second: 25 }).toISO(),
last_modified: DateTime.now().toISO(),
word_of_the_day: testMarkdown,
};
const mockValues_RegisteredWithAllDrawsAndEntry: TTestAndEarnContext = {
...mockValues_Registered,
draws: {
current: {
...current,
},
draws: allDraws,
},
};
export const MockTestAndEarnProvider_RegisteredWithAllDraws: FCWithChildren = ({ children }) => (
<TestAndEarnContext.Provider value={mockValues_RegisteredWithAllDrawsAndEntry}>
{children}
</TestAndEarnContext.Provider>
);
const mockValues_RegisteredWithDrawsNoCurrent: TTestAndEarnContext = {
...mockValues_Registered,
draws: {
draws: drawsWithClaim,
},
};
export const MockTestAndEarnProvider_RegisteredWithDrawsNoCurrent: FCWithChildren = ({ children }) => (
<TestAndEarnContext.Provider value={mockValues_RegisteredWithDrawsNoCurrent}>{children}</TestAndEarnContext.Provider>
);
const mockValues_RegisteredWithDraws: TTestAndEarnContext = {
...mockValues_Registered,
draws: {
current,
draws,
},
};
export const MockTestAndEarnProvider_RegisteredWithDraws: FCWithChildren = ({ children }) => (
<TestAndEarnContext.Provider value={mockValues_RegisteredWithDraws}>{children}</TestAndEarnContext.Provider>
);
const mockValues_RegisteredWithDrawsAndEntry: TTestAndEarnContext = {
...mockValues_Registered,
draws: {
current: {
...current,
entry: mockValues_RegisteredWithDraws.draws!.draws[0],
},
draws,
},
};
export const MockTestAndEarnProvider_RegisteredWithDrawsAndEntry: FCWithChildren = ({ children }) => (
<TestAndEarnContext.Provider value={mockValues_RegisteredWithDrawsAndEntry}>{children}</TestAndEarnContext.Provider>
);
const mockValues_RegisteredWithDrawsAndEntryAndWinner: TTestAndEarnContext = {
...mockValues_Registered,
draws: {
current: {
...current,
entry: drawsWithWin[0],
},
draws: drawsWithWin,
},
isWinnerWithUnclaimedPrize: true,
};
export const MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinner = ({
children,
}: {
children: React.ReactNode;
}) => (
<TestAndEarnContext.Provider value={mockValues_RegisteredWithDrawsAndEntryAndWinner}>
{children}
</TestAndEarnContext.Provider>
);
const mockValues_RegisteredWithDrawsAndEntryAndNoWinner: TTestAndEarnContext = {
...mockValues_RegisteredWithDrawsAndEntry,
isWinnerWithUnclaimedPrize: false,
};
export const MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndNoWinner = ({
children,
}: {
children: React.ReactNode;
}) => (
<TestAndEarnContext.Provider value={mockValues_RegisteredWithDrawsAndEntryAndNoWinner}>
{children}
</TestAndEarnContext.Provider>
);
const mockValues_RegisteredWithDrawsAndEntryAndWinnerCollectWallet: TTestAndEarnContext = {
...mockValues_Registered,
draws: {
draws: drawsWithWin,
},
};
export const MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinnerCollectWallet = ({
children,
}: {
children: React.ReactNode;
}) => (
<TestAndEarnContext.Provider value={mockValues_RegisteredWithDrawsAndEntryAndWinnerCollectWallet}>
{children}
</TestAndEarnContext.Provider>
);
const mockValues_RegisteredWithDrawsAndEntryAndWinnerClaimed: TTestAndEarnContext = {
...mockValues_Registered,
draws: {
draws: drawsWithClaim,
},
};
export const MockTestAndEarnProvider_RegisteredWithDrawsAndEntryAndWinnerClaimed = ({
children,
}: {
children: React.ReactNode;
}) => (
<TestAndEarnContext.Provider value={mockValues_RegisteredWithDrawsAndEntryAndWinnerClaimed}>
{children}
</TestAndEarnContext.Provider>
);
const mockValues_RegisteredAndError: TTestAndEarnContext = {
...mockValues_Registered,
error: 'Error message text will go here',
};
export const MockTestAndEarnProvider_RegisteredAndError: FCWithChildren = ({ children }) => (
<TestAndEarnContext.Provider value={mockValues_RegisteredAndError}>{children}</TestAndEarnContext.Provider>
);
@@ -1,64 +0,0 @@
export interface ClientId {
client_id: string;
client_id_signature: string;
}
export interface Registration {
id: string;
client_id: string;
client_id_signature: string;
timestamp: string;
}
export interface DrawEntryPartial {
draw_id: string;
client_id: string;
client_id_signature: string;
}
export enum DrawEntryStatus {
pending = 'Pending',
winner = 'Winner',
noWin = 'NoWin',
claimed = 'Claimed',
}
export interface DrawEntry {
id: string;
draw_id: string;
timestamp: string;
status: DrawEntryStatus;
}
export interface DrawWithWordOfTheDay {
id: string;
start_utc: string;
end_utc: string;
word_of_the_day?: string;
last_modified: string;
entry?: DrawEntry;
}
export interface ClaimPartial {
draw_id: string;
registration_id: string;
client_id: string;
client_id_signature: string;
wallet_address: string;
}
export interface Winner {
id: string;
client_id: string;
draw_id: string;
timestamp: string;
winner_reg_id: string;
winner_wallet_address?: string;
winner_claim_timestamp?: string;
}
export interface Draws {
current?: DrawWithWordOfTheDay;
next?: DrawWithWordOfTheDay;
draws: DrawEntry[];
}
-23
View File
@@ -1,23 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { ErrorBoundary } from 'react-error-boundary';
import { ClientContextProvider } from './context/main';
import { ErrorFallback } from './components/Error';
import { NymShipyardTheme } from './theme';
import { TestAndEarnPopup } from './components/Growth/TestAndEarnPopup';
import { TestAndEarnContextProvider } from './components/Growth/context/TestAndEarnContext';
const root = document.getElementById('root-growth');
ReactDOM.render(
<ErrorBoundary FallbackComponent={ErrorFallback}>
<ClientContextProvider>
<TestAndEarnContextProvider>
<NymShipyardTheme mode="dark">
<TestAndEarnPopup />
</NymShipyardTheme>
</TestAndEarnContextProvider>
</ClientContextProvider>
</ErrorBoundary>,
root,
);
-18
View File
@@ -1,18 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { ErrorBoundary } from 'react-error-boundary';
import { LogViewer } from './components/LogViewer';
import { ErrorFallback } from './components/ErrorFallback';
import { NymMixnetTheme } from './theme';
const Log = () => (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<NymMixnetTheme mode="dark">
<LogViewer />
</NymMixnetTheme>
</ErrorBoundary>
);
const root = document.getElementById('root-log');
ReactDOM.render(<Log />, root);
+2 -2
View File
@@ -13,11 +13,11 @@
"resolveJsonModule": true,
"isolatedModules": false,
"jsx": "react-jsx",
"sourceMap": false,
"sourceMap": true,
"baseUrl": ".",
"paths": {
"@assets/*": ["../assets/*"]
}
},
"exclude": ["node_modules", "dist", "jest.config.js", "webpack.config.js", "webpack.prod.js", "webpack.common.js", "target"]
"exclude": ["node_modules", "dist", "jest.config.js", "webpack.config.js", "webpack.prod.js", "webpack.common.js", "target", "src-tauri"]
}
+10 -14
View File
@@ -4,8 +4,6 @@ const { webpackCommon } = require('@nymproject/webpack');
const entry = {
app: path.resolve(__dirname, 'src/index.tsx'),
// growth: path.resolve(__dirname, 'src/growth.tsx'),
// log: path.resolve(__dirname, 'src/log.tsx'),
};
module.exports = mergeWithRules({
@@ -18,22 +16,20 @@ module.exports = mergeWithRules({
})(
webpackCommon(__dirname, [
{ filename: 'index.html', chunks: ['app'], template: path.resolve(__dirname, 'public/index.html') },
// { filename: 'log.html', chunks: ['log'], template: path.resolve(__dirname, 'public/log.html') },
// { filename: 'growth.html', chunks: ['growth'], template: path.resolve(__dirname, 'public/growth.html') },
]),
{
module: {
rules: [
// {
// test: /\.mdx?$/,
// use: [
// {
// loader: '@mdx-js/loader',
// /** @type {import('@mdx-js/loader').Options} */
// options: {},
// },
// ],
// },
{
test: /\.mdx?$/,
use: [
{
loader: '@mdx-js/loader',
/** @type {import('@mdx-js/loader').Options} */
options: {},
},
],
},
{
test: /\.ya?ml$/,
type: 'asset/resource',
+14 -13
View File
@@ -1,7 +1,7 @@
const { mergeWithRules } = require('webpack-merge');
const webpack = require('webpack');
// const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
// const ReactRefreshTypeScript = require('react-refresh-typescript');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const ReactRefreshTypeScript = require('react-refresh-typescript');
const commonConfig = require('./webpack.common');
module.exports = mergeWithRules({
@@ -13,7 +13,7 @@ module.exports = mergeWithRules({
},
})(commonConfig, {
mode: 'development',
devtool: false,
devtool: 'inline-source-map',
module: {
rules: [
{
@@ -21,9 +21,9 @@ module.exports = mergeWithRules({
use: 'ts-loader',
exclude: /node_modules/,
options: {
// getCustomTransformers: () => ({
// before: [ReactRefreshTypeScript()],
// }),
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,
@@ -32,14 +32,15 @@ module.exports = mergeWithRules({
],
},
plugins: [
// new ReactRefreshWebpackPlugin(),
new ReactRefreshWebpackPlugin(),
// this can be included automatically by the dev server, however build mode fails if missing
// new webpack.HotModuleReplacementPlugin(),
new webpack.HotModuleReplacementPlugin(),
],
// recommended for faster rebuild
optimization: {
// runtimeChunk: true,
runtimeChunk: true,
removeAvailableModules: false,
removeEmptyChunks: false,
splitChunks: false,
@@ -55,14 +56,14 @@ module.exports = mergeWithRules({
devServer: {
port: 9000,
// compress: true,
compress: true,
historyApiFallback: true,
// disable this because on android it makes reloading infinity loop
hot: false,
host: 'local-ipv4',
allowedHosts: 'all',
// client: {
// overlay: false,
// },
client: {
overlay: false,
},
},
});
-2
View File
@@ -4,8 +4,6 @@ const common = require('./webpack.common');
const entry = {
app: path.resolve(__dirname, 'src/index.tsx'),
growth: path.resolve(__dirname, 'src/growth.tsx'),
log: path.resolve(__dirname, 'src/log.tsx'),
};
module.exports = merge(common, {
+12
View File
@@ -2,6 +2,18 @@
## UNRELEASED
## [nym-connect-v1.1.9](https://github.com/nymtech/nym/tree/nym-connect-v1.1.9) (2023-02-07)
- NC - Button animations ([#2949])
- NC - add effect when the button is clicked ([#2947])
- NC - UI to select gateways based on some performance criteria by checking gateways' routing score from nym-api ([#2942])
- NC - client health check when connecting ([#2859])
[#2949]: https://github.com/nymtech/nym/issues/2949
[#2947]: https://github.com/nymtech/nym/issues/2947
[#2942]: https://github.com/nymtech/nym/issues/2942
[#2859]: https://github.com/nymtech/nym/issues/2859
## [nym-connect-v1.1.8](https://github.com/nymtech/nym/tree/nym-connect-v1.1.8) (2023-01-31)
- Add supported apps in the menu + update guide ([#2868])
+2
View File
@@ -45,6 +45,8 @@ url = "2.2"
yaml-rust = "0.4"
client-core = { path = "../../clients/client-core" }
nym-api-requests = { path = "../../nym-api/nym-api-requests" }
contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common"}
config-common = { path = "../../common/config", package = "config" }
crypto = { path = "../../common/crypto" }
logging = { path = "../../common/logging"}
+1
View File
@@ -52,6 +52,7 @@ fn main() {
crate::operations::connection::status::get_gateway_connection_status,
crate::operations::connection::status::start_connection_health_check_task,
crate::operations::directory::get_services,
crate::operations::directory::get_gateways_detailed,
crate::operations::export::export_keys,
crate::operations::window::hide_window,
crate::operations::growth::test_and_earn::growth_tne_get_client_id,
@@ -1,21 +1,35 @@
use itertools::Itertools;
use crate::error::Result;
use crate::models::{DirectoryService, HarbourMasterService, PagedResult};
use crate::models::{
DirectoryService, DirectoryServiceProvider, HarbourMasterService, PagedResult,
};
use contracts_common::types::Percent;
use nym_api_requests::models::GatewayBondAnnotated;
static SERVICE_PROVIDER_WELLKNOWN_URL: &str =
"https://nymtech.net/.wellknown/connect/service-providers.json";
static HARBOUR_MASTER_URL: &str = "https://harbourmaster.nymtech.net/v1/services/?size=100";
static GATEWAYS_DETAILED_URL: &str =
"https://validator.nymtech.net/api/v1/status/gateways/detailed";
#[tauri::command]
pub async fn get_services() -> Result<Vec<DirectoryService>> {
pub async fn get_services() -> Result<Vec<DirectoryServiceProvider>> {
log::trace!("Fetching services");
let res = reqwest::get(SERVICE_PROVIDER_WELLKNOWN_URL)
let services_res = reqwest::get(SERVICE_PROVIDER_WELLKNOWN_URL)
.await?
.json::<Vec<DirectoryService>>()
.await?;
log::trace!("Received: {:#?}", res);
log::trace!("Received: {:#?}", services_res);
log::trace!("Fetching gateways");
let gateway_res = reqwest::get(GATEWAYS_DETAILED_URL)
.await?
.json::<Vec<GatewayBondAnnotated>>()
.await?;
log::trace!("Received: {:#?}", gateway_res);
// TODO: get paged
log::trace!("Fetching active services");
@@ -27,7 +41,7 @@ pub async fn get_services() -> Result<Vec<DirectoryService>> {
let mut filtered: Vec<DirectoryService> = vec![];
for service in &res {
for service in &services_res {
let items: _ = service
.items
.clone()
@@ -47,5 +61,32 @@ pub async fn get_services() -> Result<Vec<DirectoryService>> {
})
}
Ok(filtered)
let perf_threshold = Percent::from_percentage_value(90).unwrap();
// Use only services that are active AND have a performance of >= 90%
let services_with_good_performance: Vec<DirectoryServiceProvider> = filtered
.iter_mut()
.fold(vec![], |mut acc, sp| {
acc.append(&mut sp.items);
acc
})
.into_iter()
.filter(|sp| {
gateway_res.iter().any(|gateway| {
gateway.gateway_bond.gateway.identity_key == sp.gateway
&& gateway.performance >= perf_threshold
})
})
.collect();
Ok(services_with_good_performance)
}
#[tauri::command]
pub async fn get_gateways_detailed() -> Result<Vec<GatewayBondAnnotated>> {
let res = reqwest::get(GATEWAYS_DETAILED_URL)
.await?
.json::<Vec<GatewayBondAnnotated>>()
.await?;
Ok(res)
}
-74
View File
@@ -1,74 +0,0 @@
import React, { useEffect } from 'react';
import { DateTime } from 'luxon';
import { forage } from '@tauri-apps/tauri-forage';
import { useClientContext } from './context/main';
import { useTauriEvents } from './utils';
import { AppRoutes } from './routes';
import { Connected } from './pages/connection/Connected';
export const App: FCWithChildren = () => {
const context = useClientContext();
const [busy, setBusy] = React.useState<boolean>();
useTauriEvents('help://clear-storage', (_event) => {
console.log('About to clear local storage...');
// clear local storage
try {
forage.clear()();
console.log('Local storage cleared');
} catch (e) {
console.error('Failed to clear local storage', e);
}
});
const handleConnectClick = React.useCallback(async () => {
const currentStatus = context.connectionStatus;
if (currentStatus === 'connected' || currentStatus === 'disconnected') {
setBusy(true);
// eslint-disable-next-line default-case
switch (currentStatus) {
case 'disconnected':
await context.startConnecting();
context.setConnectedSince(DateTime.now());
break;
case 'connected':
await context.startDisconnecting();
context.setConnectedSince(undefined);
break;
}
setBusy(false);
}
}, [context.connectionStatus]);
if (context.connectionStatus === 'disconnected' || context.connectionStatus === 'connecting') {
return <AppRoutes />;
}
return (
<Connected
status={context.connectionStatus}
showInfoModal={context.showInfoModal}
closeInfoModal={() => context.setShowInfoModal(false)}
busy={busy}
onConnectClick={handleConnectClick}
ipAddress="127.0.0.1"
port={1080}
gatewayPerformance={context.gatewayPerformance}
connectedSince={context.connectedSince}
serviceProvider={context.selectedProvider}
stats={[
{
label: 'in:',
totalBytes: 1024,
rateBytesPerSecond: 1024 * 1024 * 1024 + 10,
},
{
label: 'out:',
totalBytes: 1024 * 1024 * 1024 * 1024 * 20,
rateBytesPerSecond: 1024 * 1024 + 10,
},
]}
/>
);
};
@@ -83,7 +83,6 @@ export const ConnectionStatus: FCWithChildren<{
serviceProvider?: ServiceProvider;
}> = ({ status, serviceProvider, gatewayPerformance }) => {
const color = status === 'connected' || status === 'disconnecting' ? '#21D072' : 'white';
console.log(gatewayPerformance);
return (
<>
-132
View File
@@ -1,132 +0,0 @@
import React from 'react';
import { ConnectionStatusKind } from 'src/types';
const getStatusFillColor = (status: ConnectionStatusKind, hover: boolean, isError: boolean): string => {
if (isError && hover) {
return '#21D072';
}
if (isError) {
return '#40475C';
}
switch (status) {
case 'disconnected':
if (hover) {
return '#FFF';
}
return '#BBB';
case 'connecting':
return '#FFF';
case 'disconnecting':
return '#FFF';
default:
// connected
if (hover) {
return '#E43E3E';
}
return '#21D072';
}
};
export const PowerButton: FCWithChildren<{
onClick?: (status: ConnectionStatusKind) => void;
isError?: boolean;
disabled?: boolean;
status: ConnectionStatusKind;
busy?: boolean;
}> = ({ onClick, disabled, status, isError }) => {
const [hover, setHover] = React.useState<boolean>(false);
const handleClick = () => {
if (disabled === true) {
return;
}
if (onClick) {
onClick(status);
}
};
const statusFillColor = getStatusFillColor(status, hover, Boolean(isError));
return (
<svg
width="190"
height="190"
viewBox="0 0 200 200"
fill="none"
xmlns="http://www.w3.org/2000/svg"
onClick={handleClick}
style={{ cursor: disabled ? 'not-allowed' : 'pointer' }}
onMouseEnter={() => !disabled && setHover(true)}
onMouseLeave={() => !disabled && setHover(false)}
>
<g transform="translate(-30, -25) ">
<circle cx={131} cy={131} r={70} strokeWidth={2} stroke={statusFillColor} filter="url(#blur)" opacity="0.6" />
<circle cx={131} cy={131} r={22} strokeWidth={1} stroke={statusFillColor} filter="url(#blur)" opacity="0.3" />
<circle opacity={0.6} cx={131} cy={131} r={68.5} stroke={statusFillColor} />
<g filter="url(#filter1_d_944_9033)">
<circle cx={131} cy={131} r={64} fill="url(#paint1_radial_944_9033)" />
<circle cx={131} cy={131} r={63} stroke={statusFillColor} strokeWidth={2} />
</g>
<g opacity={0.5} filter="url(#filter2_f_944_9033)">
<g clipPath="url(#clip0_944_9033)">
<path
d="M131 113C129.9 113 129 113.9 129 115V131C129 132.1 129.9 133 131 133C132.1 133 133 132.1 133 131V115C133 113.9 132.1 113 131 113ZM141.28 118.72C140.5 119.5 140.52 120.72 141.26 121.5C143.52 123.9 144.92 127.1 145 130.64C145.18 138.3 138.84 144.9 131.18 144.98C123.36 145.1 117 138.8 117 131C117 127.32 118.42 123.98 120.74 121.48C121.48 120.7 121.48 119.48 120.72 118.72C119.92 117.92 118.62 117.94 117.86 118.76C114.96 121.84 113.14 125.94 113 130.48C112.72 140.24 120.66 148.68 130.42 148.98C140.62 149.3 149 141.12 149 130.98C149 126.24 147.16 121.96 144.16 118.76C143.4 117.94 142.08 117.92 141.28 118.72Z"
stroke={statusFillColor}
/>
</g>
</g>
<g clipPath="url(#clip1_944_9033)">
<path
d="M131 113C129.9 113 129 113.9 129 115V131C129 132.1 129.9 133 131 133C132.1 133 133 132.1 133 131V115C133 113.9 132.1 113 131 113ZM141.28 118.72C140.5 119.5 140.52 120.72 141.26 121.5C143.52 123.9 144.92 127.1 145 130.64C145.18 138.3 138.84 144.9 131.18 144.98C123.36 145.1 117 138.8 117 131C117 127.32 118.42 123.98 120.74 121.48C121.48 120.7 121.48 119.48 120.72 118.72C119.92 117.92 118.62 117.94 117.86 118.76C114.96 121.84 113.14 125.94 113 130.48C112.72 140.24 120.66 148.68 130.42 148.98C140.62 149.3 149 141.12 149 130.98C149 126.24 147.16 121.96 144.16 118.76C143.4 117.94 142.08 117.92 141.28 118.72Z"
fill={statusFillColor}
/>
</g>
<defs>
<filter
id="filter0_f_944_9033"
x={0}
y={0}
width={240}
height={240}
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity={0} result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation={40} result="effect1_foregroundBlur_944_9033" />
</filter>
<filter
id="filter1_d_944_9033"
x={52}
y={58}
width={158}
height={158}
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity={0} result="BackgroundImageFix" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_944_9033" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_944_9033" result="shape" />
</filter>
<filter
id="filter2_f_944_9033"
x={97}
y={97}
width={68}
height={68}
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity={0} result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation={5} result="effect1_foregroundBlur_944_9033" />
</filter>
<filter id="blur">
<feGaussianBlur stdDeviation="5" />
</filter>
</defs>
</g>
</svg>
);
};
@@ -0,0 +1,123 @@
import React, { useCallback } from 'react';
import { ConnectionStatusKind } from 'src/types';
import './power-button.css';
const getStatusFillColor = (status: ConnectionStatusKind, hover: boolean, isError: boolean): string => {
if (isError && hover) {
return '#21D072';
}
if (isError) {
return '#40475C';
}
switch (status) {
case 'disconnected':
if (hover) {
return '#FFF';
}
return '#BBB';
case 'connecting':
return '#FFF';
case 'disconnecting':
return '#FFF';
default:
// connected
if (hover) {
return '#E43E3E';
}
return '#21D072';
}
};
export const PowerButton: FCWithChildren<{
onClick?: (status: ConnectionStatusKind) => void;
isError?: boolean;
disabled?: boolean;
status: ConnectionStatusKind;
busy?: boolean;
}> = ({ onClick, disabled, status, isError }) => {
const [hover, setHover] = React.useState<boolean>(false);
const handleClick = () => {
if (disabled === true) {
return;
}
if (onClick) {
onClick(status);
}
};
const statusFillColor = getStatusFillColor(status, hover, Boolean(isError));
const getClassName = useCallback(() => {
if (hover) {
switch (status) {
case 'disconnected':
return 'expand';
default:
return 'contract';
}
}
if (!hover) {
switch (status) {
case 'connected':
return 'expand';
default:
return 'contract';
}
}
}, [status, hover]);
const buttonPulse = () => {
if (status === 'connecting' || status === 'disconnecting') return 'pulse';
return undefined;
};
return (
<svg
width="190"
height="190"
viewBox="0 0 200 200"
fill="none"
xmlns="http://www.w3.org/2000/svg"
onClick={handleClick}
style={{ cursor: disabled ? 'not-allowed' : 'pointer' }}
onMouseEnter={() => !disabled && setHover(true)}
onMouseLeave={() => !disabled && setHover(false)}
>
<g transform="translate(-30, -25) ">
<circle cx={131} cy={131} r={75} strokeWidth={4} stroke={statusFillColor} filter="url(#blur)" opacity="0.6" />
<circle cx={131} cy={131} r={25} strokeWidth={2} stroke={statusFillColor} filter="url(#blur)" opacity="0.5" />
<g id="Button power">
<circle cx="131" cy="131" r="68.5" stroke={statusFillColor} strokeWidth="0.5" />
<circle id="ring-one" className={getClassName()} cx="131" cy="131" r="73" stroke={statusFillColor} />
<circle id="ring-two" className={getClassName()} cx="131" cy="131" r="77" stroke={statusFillColor} />
<circle id="ring-three" className={getClassName()} cx="131" cy="131" r="81" stroke={statusFillColor} />
<circle id="ring-four" className={getClassName()} cx="131" cy="131" r="85" stroke={statusFillColor} />
<g id="button bg">
<circle cx="131" cy="131" r="63" stroke={statusFillColor} strokeWidth="3" className={buttonPulse()} />
</g>
<g id="Power icon">
<g id="Icon">
<g id="Group 672_2">
<g id="power_settings_new_black_24dp (1) 1_2" clipPath="url(#clip1_944_8739)">
<path
id="Vector_2"
d="M131 113C129.9 113 129 113.9 129 115V131C129 132.1 129.9 133 131 133C132.1 133 133 132.1 133 131V115C133 113.9 132.1 113 131 113ZM141.28 118.72C140.5 119.5 140.52 120.72 141.26 121.5C143.52 123.9 144.92 127.1 145 130.64C145.18 138.3 138.84 144.9 131.18 144.98C123.36 145.1 117 138.8 117 131C117 127.32 118.42 123.98 120.74 121.48C121.48 120.7 121.48 119.48 120.72 118.72C119.92 117.92 118.62 117.94 117.86 118.76C114.96 121.84 113.14 125.94 113 130.48C112.72 140.24 120.66 148.68 130.42 148.98C140.62 149.3 149 141.12 149 130.98C149 126.24 147.16 121.96 144.16 118.76C143.4 117.94 142.08 117.92 141.28 118.72Z"
fill={statusFillColor}
className={buttonPulse()}
/>
</g>
</g>
</g>
</g>
</g>
<defs>
<filter id="blur" width="200%" height="200%" x="-50%" y="-50%">
<feGaussianBlur stdDeviation="12.5" />
</filter>
</defs>
</g>
</svg>
);
};
@@ -0,0 +1,72 @@
#ring-expand {
animation-name: rings-expand;
animation-duration: 0.4s;
animation-timing-function: ease-in-out;
animation-play-state: paused;
}
#ring-one.expand {
opacity: 0.3;
transition: opacity 0.1s ease-in-out;
}
#ring-two.expand {
opacity: 0.2;
transition: opacity 0.2s ease-in-out;
}
#ring-three.expand {
opacity: 0.1;
transition: opacity 0.3s ease-in-out;
}
#ring-four.expand {
opacity: 0.05;
transition: opacity 0.4s ease-in-out;
}
#ring-one.contract {
opacity: 0;
transition: opacity 0.4s;
transition-delay: 0.1s;
}
#ring-two.contract {
opacity: 0;
transition: opacity 0.3s;
transition-delay: 0.1s;
}
#ring-three.contract {
opacity: 0;
transition: opacity 0.1s;
transition-delay: 0.1s;
}
#ring-four.contract {
opacity: 0;
transition: opacity 0.1s;
transition-delay: 0.1s;
}
circle,
path {
transition: stroke 0.5s, fill 0.5s;
}
.pulse {
animation-name: pulse;
animation-duration: 0.5s;
animation-iteration-count: infinite;
animation-direction: alternate;
}
@keyframes pulse {
from {
opacity: 1;
}
to {
opacity: 0.3;
}
}
+9 -54
View File
@@ -1,15 +1,13 @@
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState, useRef } from 'react';
import { DateTime } from 'luxon';
import { invoke } from '@tauri-apps/api';
import type { UnlistenFn } from '@tauri-apps/api/event';
import { listen } from '@tauri-apps/api/event';
import { forage } from '@tauri-apps/tauri-forage';
import { Error } from 'src/types/error';
import { TauriEvent } from 'src/types/event';
import { getVersion } from '@tauri-apps/api/app';
import { ConnectionStatusKind, GatewayPerformance } from '../types';
import { ConnectionStatsItem } from '../components/ConnectionStats';
import { ServiceProvider, Services } from '../types/directory';
import { useEvents } from 'src/hooks/events';
const TAURI_EVENT_STATUS_CHANGED = 'app:connection-status-changed';
@@ -57,21 +55,21 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
const timerId = useRef<NodeJS.Timeout>();
const flattenProviders = (services: Services) => {
return services.reduce((a: ServiceProvider[], b) => {
return [...a, ...b.items];
}, []);
};
const initialiseApp = async () => {
const services = await invoke('get_services');
const allServiceProviders = flattenProviders(services as Services);
const AppVersion = await getAppVersion();
console.log(services);
setAppVersion(AppVersion);
setServiceProviders(allServiceProviders);
setServiceProviders(services as ServiceProvider[]);
};
useEvents({
onError: (error) => setError(error),
onGatewayPerformanceChange: (performance) => setGatewayPerformance(performance),
onStatusChange: (status) => setConnectionStatus(status),
});
useEffect(() => {
initialiseApp();
}, []);
@@ -84,49 +82,6 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
})();
}, []);
useEffect(() => {
const unlisten: UnlistenFn[] = [];
// TODO: fix typings
listen(TAURI_EVENT_STATUS_CHANGED, (event) => {
const { status } = event.payload as any;
console.log(TAURI_EVENT_STATUS_CHANGED, { status, event });
setConnectionStatus(status);
})
.then((result) => {
unlisten.push(result);
})
.catch((e) => console.log(e));
listen('socks5-event', (e: TauriEvent) => {
console.log(e);
setError(e.payload);
}).then((result) => {
unlisten.push(result);
});
listen('socks5-status-event', (e: TauriEvent) => {
if (e.payload.message.includes('slow')) {
setGatewayPerformance('Poor');
if (timerId.current) {
clearTimeout(timerId.current);
}
timerId.current = setTimeout(() => {
setGatewayPerformance('Good');
}, 10000);
}
}).then((result) => {
unlisten.push(result);
});
return () => {
unlisten.forEach((unsubscribe) => unsubscribe());
};
}, []);
const startConnecting = useCallback(async () => {
try {
await invoke('start_connecting');
+68
View File
@@ -0,0 +1,68 @@
import { useEffect, useRef } from 'react';
import { listen, UnlistenFn } from '@tauri-apps/api/event';
import { ConnectionStatusKind, GatewayPerformance } from 'src/types';
import { Error } from 'src/types/error';
import { TauriEvent } from 'src/types/event';
const TAURI_EVENT_STATUS_CHANGED = 'app:connection-status-changed';
export const useEvents = ({
onError,
onStatusChange,
onGatewayPerformanceChange,
}: {
onError: (error: Error) => void;
onStatusChange: (status: ConnectionStatusKind) => void;
onGatewayPerformanceChange: (status: GatewayPerformance) => void;
}) => {
const timerId = useRef<NodeJS.Timeout>();
useEffect(() => {
const unlisten: UnlistenFn[] = [];
// TODO: fix typings
listen(TAURI_EVENT_STATUS_CHANGED, (event) => {
const { status } = event.payload as any;
console.log(TAURI_EVENT_STATUS_CHANGED, { status, event });
onStatusChange(status);
})
.then((result) => {
unlisten.push(result);
})
.catch((e) => console.log(e));
listen('socks5-event', (e: TauriEvent) => {
console.log(e);
onError(e.payload);
}).then((result) => {
unlisten.push(result);
});
listen('socks5-status-event', (e: TauriEvent) => {
if (e.payload.message.includes('slow')) {
onGatewayPerformanceChange('Poor');
if (timerId?.current) {
clearTimeout(timerId.current);
}
timerId.current = setTimeout(() => {
onGatewayPerformanceChange('Good');
}, 10000);
}
}).then((result) => {
unlisten.push(result);
});
listen('socks5-connection-fail-event', (e: TauriEvent) => {
onError({ title: 'Connection failed', message: `${e.payload.message} - Please disconnect and reconnect.` });
onGatewayPerformanceChange('Poor');
}).then((result) => {
unlisten.push(result);
});
return () => {
unlisten.forEach((unsubscribe) => unsubscribe());
};
}, []);
};
-1
View File
@@ -4,7 +4,6 @@ import { ErrorBoundary } from 'react-error-boundary';
import { ClientContextProvider } from './context/main';
import { ErrorFallback } from './components/Error';
import { NymMixnetTheme } from './theme';
import { App } from './App';
import { AppWindowFrame } from './components/AppWindowFrame';
import { TestAndEarnContextProvider } from './components/Growth/context/TestAndEarnContext';
import { BrowserRouter as Router } from 'react-router-dom';
@@ -10,9 +10,12 @@ import { IpAddressAndPort } from 'src/components/IpAddressAndPort';
import { ServiceProvider } from 'src/types/directory';
import { ExperimentalWarning } from 'src/components/ExperimentalWarning';
import { ConnectionLayout } from 'src/layouts/ConnectionLayout';
import { PowerButton } from 'src/components/PowerButton';
import { PowerButton } from 'src/components/PowerButton/PowerButton';
import { Error } from 'src/types/error';
import { InfoModal } from 'src/components/InfoModal';
export const Connected: FCWithChildren<{
error?: Error;
status: ConnectionStatusKind;
showInfoModal: boolean;
gatewayPerformance: GatewayPerformance;
@@ -23,9 +26,11 @@ export const Connected: FCWithChildren<{
busy?: boolean;
isError?: boolean;
serviceProvider?: ServiceProvider;
clearError: () => void;
onConnectClick: (status: ConnectionStatusKind) => void;
closeInfoModal: () => void;
}> = ({
error,
status,
showInfoModal,
gatewayPerformance,
@@ -35,11 +40,13 @@ export const Connected: FCWithChildren<{
busy,
isError,
serviceProvider,
clearError,
onConnectClick,
closeInfoModal,
}) => {
return (
<>
{error && <InfoModal show title={error.title} description={error.message} onClose={clearError} />}
<IpAddressAndPortModal show={showInfoModal} onClose={closeInfoModal} ipAddress={ipAddress} port={port} />
<ConnectionLayout
TopContent={
@@ -58,7 +65,7 @@ export const Connected: FCWithChildren<{
busy={busy}
onClick={onConnectClick}
isError={isError}
disabled={status === 'connecting' || status === 'disconnecting'}
disabled={status === 'disconnecting'}
/>
}
BottomContent={
@@ -2,14 +2,12 @@ import React from 'react';
import { Stack, Typography } from '@mui/material';
import { ConnectionStatus } from 'src/components/ConnectionStatus';
import { ConnectionTimer } from 'src/components/ConntectionTimer';
import { useClientContext } from 'src/context/main';
import { InfoModal } from 'src/components/InfoModal';
import { Error } from 'src/types/error';
import { ExperimentalWarning } from 'src/components/ExperimentalWarning';
import { ServiceProvider, Services } from 'src/types/directory';
import { ConnectionStatusKind } from 'src/types';
import { ConnectionButton } from 'src/components/ConnectionButton';
import { PowerButton } from 'src/components/PowerButton';
import { PowerButton } from 'src/components/PowerButton/PowerButton';
import { Box } from '@mui/system';
import { ConnectionLayout } from 'src/layouts/ConnectionLayout';
@@ -22,7 +20,7 @@ export const Disconnected: FCWithChildren<{
serviceProvider?: ServiceProvider;
clearError: () => void;
onConnectClick: (status: ConnectionStatusKind) => void;
}> = ({ status, error, onConnectClick, clearError, serviceProvider }) => {
}> = ({ status, error, onConnectClick, clearError }) => {
return (
<>
{error && <InfoModal show title={error.title} description={error.message} onClose={clearError} />}
@@ -33,7 +31,7 @@ export const Disconnected: FCWithChildren<{
<ConnectionTimer />
</Box>
}
ConnectButton={<PowerButton onClick={onConnectClick} status={status} disabled={false} />}
ConnectButton={<PowerButton onClick={onConnectClick} status={status} disabled={status === 'connecting'} />}
BottomContent={
<Stack justifyContent="space-between" pt={1}>
<Typography
@@ -47,6 +47,8 @@ export const ConnectionPage = () => {
if (context.connectionStatus === 'connected')
return (
<Connected
error={context.error}
clearError={context.clearError}
status={context.connectionStatus}
showInfoModal={context.showInfoModal}
busy={busy}
+1 -1
View File
@@ -4,7 +4,7 @@ import { Box } from '@mui/system';
const appsSchema = {
messagingApps: ['Telegram', 'Keybase'],
wallets: ['Blockstream', 'Electum'],
wallets: ['Blockstream', 'Electrum'],
};
export const CompatibleApps = () => (
@@ -87,6 +87,7 @@ export const Mock: ComponentStory<typeof AppWindowFrame> = () => {
return (
<AppWindowFrame>
<Connected
clearError={() => {}}
gatewayPerformance="Good"
showInfoModal={false}
closeInfoModal={() => undefined}
@@ -15,6 +15,7 @@ export default {
export const Default: ComponentStory<typeof Connected> = () => (
<Box p={2} width={242} sx={{ bgcolor: 'nym.background.dark' }}>
<Connected
clearError={() => {}}
gatewayPerformance="Good"
showInfoModal={false}
closeInfoModal={() => {
@@ -1,6 +1,6 @@
import React from 'react';
import { ComponentMeta, ComponentStory } from '@storybook/react';
import { PowerButton } from 'src/components/PowerButton';
import { PowerButton } from 'src/components/PowerButton/PowerButton';
import { ConnectionStatusKind } from 'src/types';
export default {
@@ -13,7 +13,7 @@ export const Disconnected: ComponentStory<typeof PowerButton> = () => (
);
export const Connecting: ComponentStory<typeof PowerButton> = () => (
<PowerButton status={ConnectionStatusKind.connecting} />
<PowerButton status={ConnectionStatusKind.connecting} disabled />
);
export const Connected: ComponentStory<typeof PowerButton> = () => (
@@ -21,9 +21,9 @@ export const Connected: ComponentStory<typeof PowerButton> = () => (
);
export const Disconnecting: ComponentStory<typeof PowerButton> = () => (
<PowerButton status={ConnectionStatusKind.disconnecting} />
<PowerButton status={ConnectionStatusKind.disconnecting} disabled />
);
export const Disabled: ComponentStory<typeof PowerButton> = () => (
<PowerButton status={ConnectionStatusKind.connecting} disabled />
<PowerButton status={ConnectionStatusKind.disconnected} disabled />
);
+71 -4
View File
@@ -10715,6 +10715,18 @@ hex-rgb@^4.1.0:
resolved "https://registry.yarnpkg.com/hex-rgb/-/hex-rgb-4.3.0.tgz#af5e974e83bb2fefe44d55182b004ec818c07776"
integrity sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==
history@^4.9.0:
version "4.10.1"
resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
dependencies:
"@babel/runtime" "^7.1.2"
loose-envify "^1.2.0"
resolve-pathname "^3.0.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
value-equal "^1.0.1"
hmac-drbg@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@@ -10724,7 +10736,7 @@ hmac-drbg@^1.0.1:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -11685,6 +11697,11 @@ is-wsl@^2.1.1, is-wsl@^2.2.0:
dependencies:
is-docker "^2.0.0"
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==
isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
@@ -12952,7 +12969,7 @@ longest-streak@^3.0.0:
resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4"
integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==
loose-envify@^1.1.0, loose-envify@^1.4.0:
loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@@ -15061,6 +15078,13 @@ path-to-regexp@0.1.7:
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==
path-to-regexp@^1.7.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
dependencies:
isarray "0.0.1"
path-type@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
@@ -16036,7 +16060,7 @@ react-is@17.0.2, react-is@^17.0.1, react-is@^17.0.2:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
react-is@^16.10.2, react-is@^16.13.1, react-is@^16.7.0:
react-is@^16.10.2, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -16112,6 +16136,34 @@ react-router-dom@6, react-router-dom@^6.7.0:
"@remix-run/router" "1.3.0"
react-router "6.7.0"
react-router-dom@^5.2.0:
version "5.3.4"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.4.tgz#2ed62ffd88cae6db134445f4a0c0ae8b91d2e5e6"
integrity sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==
dependencies:
"@babel/runtime" "^7.12.13"
history "^4.9.0"
loose-envify "^1.3.1"
prop-types "^15.6.2"
react-router "5.3.4"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-router@5.3.4:
version "5.3.4"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.3.4.tgz#8ca252d70fcc37841e31473c7a151cf777887bb5"
integrity sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==
dependencies:
"@babel/runtime" "^7.12.13"
history "^4.9.0"
hoist-non-react-statics "^3.1.0"
loose-envify "^1.3.1"
path-to-regexp "^1.7.0"
prop-types "^15.6.2"
react-is "^16.6.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-router@6.7.0:
version "6.7.0"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.7.0.tgz#db262684c13b5c2970694084ae9e8531718a0681"
@@ -16690,6 +16742,11 @@ resolve-from@^5.0.0:
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
resolve-pathname@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd"
integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
resolve-url@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
@@ -18123,7 +18180,12 @@ timers-browserify@^2.0.4:
dependencies:
setimmediate "^1.0.4"
tiny-warning@^1.0.2:
tiny-invariant@^1.0.2:
version "1.3.1"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642"
integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==
tiny-warning@^1.0.0, tiny-warning@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
@@ -18923,6 +18985,11 @@ validate-npm-package-name@^3.0.0:
dependencies:
builtins "^1.0.3"
value-equal@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==
vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"