Compare commits

...

11 Commits

Author SHA1 Message Date
Jędrzej Stuczyński 67384eefb2 introduced '/circulating-supply/total-supply-value' and '/circulating-supply/circulating-supply-value' endpoints 2023-02-03 10:13:13 +00:00
Fouad 7d44c32419 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-02 17:42:11 +01:00
Jędrzej Stuczyński 2093789c5c 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-02 16:38:39 +00:00
Pierre Dommerc d09864b99f 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-02 15:18:58 +01:00
Bogdan-Ștefan Neacşu f6e8278592 Fix typo during merge back to develop (#2956) 2023-02-02 13:55:58 +02:00
cgi-bin/ b085a8a636 typo: electrum (#2954) 2023-02-02 09:34:08 +00:00
farbanas e6ce531aeb fix: formatting 2023-02-01 17:16:53 +01:00
farbanas 1a860eb3f5 fix: wrong parameter type for addresses in generator commands (should be AccountId instead of String) 2023-02-01 12:45:51 +01:00
Fran Arbanas 09cfd9ff05 Update CHANGELOG.md 2023-01-31 14:42:16 +00:00
Bogdan-Ștefan Neacșu 2c88ca137a Resolve merge conflict 2023-01-31 16:19:34 +02:00
farbanas ca3bfc859a merge conflicts 2023-01-31 15:09:35 +01:00
169 changed files with 1463 additions and 3216 deletions
+23 -8
View File
@@ -4,18 +4,33 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
# [Unreleased]
### Added
- remove coconut feature and unify builds ([#2890])
- 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.
[#2890]: https://github.com/nymtech/nym/pull/2890
[#2939]: https://github.com/nymtech/nym/pull/2939
# [v1.1.8] (2023-01-31)
### Added
- dkg rerun from scratch and dkg-specific epochs ([#2839])
- nym-sdk: add support for surb storage ([#2870])
- nym-sdk: enable reply-SURBs by default ([#2874])
- remove coconut feature and unify builds ([#2890])
- Rust SDK - Support SURBS (anonymous send + storage) ([#2754])
- dkg rerun from scratch and dkg-specific epochs ([#2810])
- Rename `'initial_supply'` field to `'total_supply'` in the circulating supply endpoint ([#2931])
- Circulating supply api endpoint (read the note inside before testing/deploying) ([#1902])
### Changed
- nym-api: an `--id` flag is now always explicitly required ([#2873])
[#2754]: https://github.com/nymtech/nym/issues/2754
[#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
[#2839]: https://github.com/nymtech/nym/pull/2839
[#2870]: https://github.com/nymtech/nym/pull/2870
[#2874]: https://github.com/nymtech/nym/pull/2874
[#2890]: https://github.com/nymtech/nym/pull/2890
# [v1.1.7] (2023-01-24)
Generated
+11 -10
View File
@@ -701,7 +701,7 @@ dependencies = [
[[package]]
name = "client-core"
version = "1.1.7"
version = "1.1.8"
dependencies = [
"async-trait",
"client-connections",
@@ -1891,7 +1891,7 @@ dependencies = [
[[package]]
name = "explorer-api"
version = "1.1.7"
version = "1.1.8"
dependencies = [
"chrono",
"clap 4.1.3",
@@ -3314,6 +3314,7 @@ dependencies = [
"cw4",
"schemars",
"serde",
"thiserror",
]
[[package]]
@@ -3437,7 +3438,7 @@ dependencies = [
[[package]]
name = "nym-api"
version = "1.1.7"
version = "1.1.8"
dependencies = [
"anyhow",
"async-trait",
@@ -3535,7 +3536,7 @@ dependencies = [
[[package]]
name = "nym-cli"
version = "1.1.7"
version = "1.1.8"
dependencies = [
"anyhow",
"base64 0.13.1",
@@ -3593,7 +3594,7 @@ dependencies = [
[[package]]
name = "nym-client"
version = "1.1.7"
version = "1.1.8"
dependencies = [
"build-information",
"clap 4.1.3",
@@ -3633,7 +3634,7 @@ dependencies = [
[[package]]
name = "nym-gateway"
version = "1.1.7"
version = "1.1.8"
dependencies = [
"anyhow",
"async-trait",
@@ -3685,7 +3686,7 @@ dependencies = [
[[package]]
name = "nym-mixnode"
version = "1.1.8"
version = "1.1.9"
dependencies = [
"anyhow",
"atty",
@@ -3730,7 +3731,7 @@ dependencies = [
[[package]]
name = "nym-network-requester"
version = "1.1.7"
version = "1.1.8"
dependencies = [
"async-trait",
"clap 4.1.3",
@@ -3762,7 +3763,7 @@ dependencies = [
[[package]]
name = "nym-network-statistics"
version = "1.1.7"
version = "1.1.8"
dependencies = [
"dirs",
"log",
@@ -3819,7 +3820,7 @@ dependencies = [
[[package]]
name = "nym-socks5-client"
version = "1.1.7"
version = "1.1.8"
dependencies = [
"build-information",
"clap 4.1.3",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "client-core"
version = "1.1.7"
version = "1.1.8"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
edition = "2021"
rust-version = "1.66"
+8 -8
View File
@@ -546,35 +546,35 @@ impl<T: NymConfig> Default for Client<T> {
impl<T: NymConfig> Client<T> {
fn default_private_identity_key_file(id: &str) -> PathBuf {
T::default_data_directory(Some(id)).join("private_identity.pem")
T::default_data_directory(id).join("private_identity.pem")
}
fn default_public_identity_key_file(id: &str) -> PathBuf {
T::default_data_directory(Some(id)).join("public_identity.pem")
T::default_data_directory(id).join("public_identity.pem")
}
fn default_private_encryption_key_file(id: &str) -> PathBuf {
T::default_data_directory(Some(id)).join("private_encryption.pem")
T::default_data_directory(id).join("private_encryption.pem")
}
fn default_public_encryption_key_file(id: &str) -> PathBuf {
T::default_data_directory(Some(id)).join("public_encryption.pem")
T::default_data_directory(id).join("public_encryption.pem")
}
fn default_gateway_shared_key_file(id: &str) -> PathBuf {
T::default_data_directory(Some(id)).join("gateway_shared.pem")
T::default_data_directory(id).join("gateway_shared.pem")
}
fn default_ack_key_file(id: &str) -> PathBuf {
T::default_data_directory(Some(id)).join("ack_key.pem")
T::default_data_directory(id).join("ack_key.pem")
}
fn default_reply_surb_database_path(id: &str) -> PathBuf {
T::default_data_directory(Some(id)).join("persistent_reply_store.sqlite")
T::default_data_directory(id).join("persistent_reply_store.sqlite")
}
fn default_database_path(id: &str) -> PathBuf {
T::default_data_directory(Some(id)).join(DB_FILE_NAME)
T::default_data_directory(id).join(DB_FILE_NAME)
}
}
+1 -1
View File
@@ -149,7 +149,7 @@ pub fn load_existing_gateway_config<T>(id: &str) -> Result<GatewayEndpointConfig
where
T: NymConfig + ClientCoreConfigTrait,
{
T::load_from_file(Some(id))
T::load_from_file(id)
.map(|existing_config| existing_config.get_gateway_endpoint().clone())
.map_err(|err| {
log::error!(
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.7"
version = "1.1.8"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
+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)
+7 -1
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,
@@ -108,7 +114,7 @@ pub(crate) async fn execute(args: &Init) -> Result<(), ClientError> {
let id = &args.id;
let already_init = Config::default_config_file_path(Some(id)).exists();
let already_init = Config::default_config_file_path(id).exists();
if already_init {
println!("Client \"{id}\" was already initialised before");
}
+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,
+7 -1
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,
@@ -94,7 +100,7 @@ fn version_check(cfg: &Config) -> bool {
pub(crate) async fn execute(args: &Run) -> Result<(), Box<dyn Error + Send + Sync>> {
let id = &args.id;
let mut config = match Config::load_from_file(Some(id)) {
let mut config = match Config::load_from_file(id) {
Ok(cfg) => cfg,
Err(err) => {
error!("Failed to load config for {}. Are you sure you have run `init` before? (Error was: {err})", id);
+1 -1
View File
@@ -131,7 +131,7 @@ pub(crate) fn execute(args: &Upgrade) {
let id = &args.id;
let existing_config = Config::load_from_file(Some(id)).unwrap_or_else(|err| {
let existing_config = Config::load_from_file(id).unwrap_or_else(|err| {
eprintln!("failed to load existing config file! - {err}");
process::exit(1)
});
+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 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.7"
version = "1.1.8"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021"
+1 -1
View File
@@ -117,7 +117,7 @@ pub(crate) async fn execute(args: &Init) -> Result<(), Socks5ClientError> {
let id = &args.id;
let provider_address = &args.provider;
let already_init = Config::default_config_file_path(Some(id)).exists();
let already_init = Config::default_config_file_path(id).exists();
if already_init {
println!("SOCKS5 client \"{id}\" was already initialised before");
}
+1 -1
View File
@@ -108,7 +108,7 @@ fn version_check(cfg: &Config) -> bool {
pub(crate) async fn execute(args: &Run) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let id = &args.id;
let mut config = match Config::load_from_file(Some(id)) {
let mut config = match Config::load_from_file(id) {
Ok(cfg) => cfg,
Err(err) => {
error!("Failed to load config for {}. Are you sure you have run `init` before? (Error was: {err})", id);
+1 -1
View File
@@ -144,7 +144,7 @@ pub(crate) fn execute(args: &Upgrade) {
let id = &args.id;
let existing_config = Config::load_from_file(Some(id)).unwrap_or_else(|err| {
let existing_config = Config::load_from_file(id).unwrap_or_else(|err| {
eprintln!("failed to load existing config file! - {err}");
process::exit(1)
});
@@ -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,
};
+11 -27
View File
@@ -30,23 +30,15 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned {
fn default_root_directory() -> PathBuf;
// default, most probable, implementations; can be easily overridden where required
fn default_config_directory(id: Option<&str>) -> PathBuf {
if let Some(id) = id {
Self::default_root_directory().join(id).join(CONFIG_DIR)
} else {
Self::default_root_directory().join(CONFIG_DIR)
}
fn default_config_directory(id: &str) -> PathBuf {
Self::default_root_directory().join(id).join(CONFIG_DIR)
}
fn default_data_directory(id: Option<&str>) -> PathBuf {
if let Some(id) = id {
Self::default_root_directory().join(id).join(DATA_DIR)
} else {
Self::default_root_directory().join(DATA_DIR)
}
fn default_data_directory(id: &str) -> PathBuf {
Self::default_root_directory().join(id).join(DATA_DIR)
}
fn default_config_file_path(id: Option<&str>) -> PathBuf {
fn default_config_file_path(id: &str) -> PathBuf {
Self::default_config_directory(id).join(Self::config_file_name())
}
@@ -54,23 +46,15 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned {
fn try_default_root_directory() -> Option<PathBuf>;
fn try_default_config_directory(id: Option<&str>) -> Option<PathBuf> {
if let Some(id) = id {
Self::try_default_root_directory().map(|d| d.join(id).join(CONFIG_DIR))
} else {
Self::try_default_root_directory().map(|d| d.join(CONFIG_DIR))
}
fn try_default_config_directory(id: &str) -> Option<PathBuf> {
Self::try_default_root_directory().map(|d| d.join(id).join(CONFIG_DIR))
}
fn try_default_data_directory(id: Option<&str>) -> Option<PathBuf> {
if let Some(id) = id {
Self::try_default_root_directory().map(|d| d.join(id).join(DATA_DIR))
} else {
Self::try_default_root_directory().map(|d| d.join(DATA_DIR))
}
fn try_default_data_directory(id: &str) -> Option<PathBuf> {
Self::try_default_root_directory().map(|d| d.join(id).join(DATA_DIR))
}
fn try_default_config_file_path(id: Option<&str>) -> Option<PathBuf> {
fn try_default_config_file_path(id: &str) -> Option<PathBuf> {
Self::try_default_config_directory(id).map(|d| d.join(Self::config_file_name()))
}
@@ -113,7 +97,7 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned {
Ok(())
}
fn load_from_file(id: Option<&str>) -> io::Result<Self> {
fn load_from_file(id: &str) -> io::Result<Self> {
let file = Self::default_config_file_path(id);
log::trace!("Loading from file: {:#?}", file);
let config_contents = fs::read_to_string(file)?;
@@ -13,3 +13,4 @@ cw4 = { version = "0.13.4" }
cosmwasm-std = "1.0.0"
schemars = "0.8"
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
thiserror = { version = "1.0.23" }
@@ -1,3 +1,6 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::StdError;
use cw_utils::ThresholdError;
@@ -1 +1,2 @@
pub mod error;
pub mod msg;
+2 -1
View File
@@ -2,8 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::{entry_point, Addr, Coin, DepsMut, Empty, Env, Response};
use cw3_flex_multisig::{state::CONFIG, ContractError};
use cw3_flex_multisig::state::CONFIG;
use cw_multi_test::{App, AppBuilder, Contract, ContractWrapper};
use multisig_contract_common::error::ContractError;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -26,7 +26,6 @@ cw-storage-plus = { version = "0.13.4" }
cosmwasm-std = { version = "1.0.0" }
schemars = "0.8.1"
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
thiserror = { version = "1.0.23" }
group-contract-common = { path = "../../../common/cosmwasm-smart-contracts/group-contract" }
multisig-contract-common = { path= "../../../common/cosmwasm-smart-contracts/multisig-contract" }
@@ -17,8 +17,8 @@ use cw4::{Cw4Contract, MemberChangedHookMsg, MemberDiff};
use cw_storage_plus::Bound;
use cw_utils::{maybe_addr, Expiration, ThresholdResponse};
use crate::error::ContractError;
use crate::state::{Config, CONFIG};
use multisig_contract_common::error::ContractError;
use multisig_contract_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
// version info for migration info
@@ -1,5 +1,2 @@
pub mod contract;
pub mod error;
pub mod state;
pub use crate::error::ContractError;
+4 -4
View File
@@ -12,10 +12,10 @@ DENOMS_EXPONENT=6
MIXNET_CONTRACT_ADDRESS=n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g
VESTING_CONTRACT_ADDRESS=n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw
BANDWIDTH_CLAIM_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0
COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n12ckdkm3q7eytefs7rwu4ue3t9hxgvl9v08jddmtwgct2ve0pv50q0t8dlt
GROUP_CONTRACT_ADDRESS=n1rw8fw2mpcpzzq3jpa4e52ufawnmj5a4u68p35umvgskewuw0nlzsaa5w4m
MULTISIG_CONTRACT_ADDRESS=n14krxe8ukzagwhvec0rmteexu62w8k9kp9sra9ww6em2hnmzcukqsa0utc8
COCONUT_DKG_CONTRACT_ADDRESS=n1rl5n6cxuz2hdy3f7d9hsnw8zn0zwwwr0r4dxfz7tktgpgkcnz9zshmvksc
COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n19d2nwj7fdhxqmyvgy8lf3ad49a6vmww4shryhrkj2mqk36att66s6xzszw
GROUP_CONTRACT_ADDRESS=n1fqquzw4mk0pkamgr2ywt2v7h2j9nuyjjn4gvpy8zlpp6xn0uyuzqfm28l5
MULTISIG_CONTRACT_ADDRESS=n1gaq3666chd5348apj8cka8t2mckv7azp9espyr7wgpxyuzur5d0sazpysy
COCONUT_DKG_CONTRACT_ADDRESS=n18yadscxw8v35dds7ksv3j0svmjh3h6e7tmxpadk96mvgz27zygkshuf4vs
REWARDING_VALIDATOR_ADDRESS=n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy
STATISTICS_SERVICE_DOMAIN_ADDRESS="https://mainnet-stats.nymte.ch:8090"
NYXD="https://qwerty-validator.qa.nymte.ch/"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "explorer-api"
version = "1.1.7"
version = "1.1.8"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+10
View File
@@ -1,5 +1,15 @@
## UNRELEASED
## [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])
- Add gateway's last Routing Score to the gateways list page ([#2186])
- Upgrade Sandbox and make below changes: ([#2332])
[#2913]: https://github.com/nymtech/nym/pull/2913
[#2186]: https://github.com/nymtech/nym/issues/2186
[#2332]: https://github.com/nymtech/nym/issues/2332
## [nym-explorer-v1.0.3](https://github.com/nymtech/nym/tree/nym-explorer-v1.0.3) (2023-01-24)
- Stake Saturation tooltip on node list and node pages updated ([#2877])
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@nym/network-explorer",
"version": "1.0.3",
"version": "1.0.4",
"private": true,
"license": "Apache-2.0",
"dependencies": {
@@ -120,4 +120,4 @@
"last 1 safari version"
]
}
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ export const OVERVIEW_API = `${API_BASE_URL}/overview`;
export const MIXNODE_PING = `${API_BASE_URL}/ping`;
export const MIXNODES_API = `${API_BASE_URL}/mix-nodes`;
export const MIXNODE_API = `${API_BASE_URL}/mix-node`;
export const GATEWAYS_API = `${NYM_API_BASE_URL}/api/v1/gateways`;
export const GATEWAYS_API = `${NYM_API_BASE_URL}/api/v1/status/gateways/detailed`;
export const VALIDATORS_API = `${VALIDATOR_BASE_URL}/validators`;
export const BLOCK_API = `${NYM_API_BASE_URL}/block`;
export const COUNTRY_DATA_API = `${API_BASE_URL}/countries`;
+9 -3
View File
@@ -15,7 +15,6 @@ import {
CountryDataResponse,
DelegationsResponse,
UniqDelegationsResponse,
GatewayResponse,
GatewayReportResponse,
UptimeStoryResponse,
MixNodeDescriptionResponse,
@@ -27,7 +26,10 @@ import {
StatusResponse,
SummaryOverviewResponse,
ValidatorsResponse,
GatewayBondAnnotated,
GatewayBond,
} from '../typeDefs/explorer-api';
import { toPercentIntegerString } from '../utils';
function getFromCache(key: string) {
const ts = Number(localStorage.getItem('ts'));
@@ -89,9 +91,13 @@ export class Api {
return response.json();
};
static fetchGateways = async (): Promise<GatewayResponse> => {
static fetchGateways = async (): Promise<GatewayBond[]> => {
const res = await fetch(GATEWAYS_API);
return res.json();
const gatewaysAnnotated: GatewayBondAnnotated[] = await res.json();
return gatewaysAnnotated.map(({ gateway_bond, performance }) => ({
...gateway_bond,
performance: toPercentIntegerString(performance),
}));
};
static fetchGatewayUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> =>
+5 -5
View File
@@ -1,4 +1,4 @@
import { GatewayResponse, GatewayResponseItem, GatewayReportResponse } from '../typeDefs/explorer-api';
import { GatewayResponse, GatewayBond, GatewayReportResponse } from '../typeDefs/explorer-api';
export type GatewayRowType = {
id: string;
@@ -8,6 +8,7 @@ export type GatewayRowType = {
host: string;
location: string;
version: string;
performance: string;
};
export type GatewayEnrichedRowType = GatewayRowType & {
@@ -28,13 +29,11 @@ export function gatewayToGridRow(arrayOfGateways: GatewayResponse): GatewayRowTy
bond: gw.pledge_amount.amount || 0,
host: gw.gateway.host || '',
version: gw.gateway.version || '',
performance: gw.performance,
}));
}
export function gatewayEnrichedToGridRow(
gateway: GatewayResponseItem,
report: GatewayReportResponse,
): GatewayEnrichedRowType {
export function gatewayEnrichedToGridRow(gateway: GatewayBond, report: GatewayReportResponse): GatewayEnrichedRowType {
return {
id: gateway.owner,
owner: gateway.owner,
@@ -47,5 +46,6 @@ export function gatewayEnrichedToGridRow(
mixPort: gateway.gateway.mix_port || 0,
routingScore: `${report.most_recent}%`,
avgUptime: `${report.last_day || report.last_hour}%`,
performance: gateway.performance,
};
}
+3 -3
View File
@@ -1,7 +1,7 @@
import * as React from 'react';
import { Alert, AlertTitle, Box, CircularProgress, Grid } from '@mui/material';
import { useParams } from 'react-router-dom';
import { GatewayResponseItem } from '../../typeDefs/explorer-api';
import { GatewayBond } from '../../typeDefs/explorer-api';
import { ColumnsType, DetailTable } from '../../components/DetailTable';
import { gatewayEnrichedToGridRow, GatewayEnrichedRowType } from '../../components/Gateways';
import { ComponentError } from '../../components/ComponentError';
@@ -69,7 +69,7 @@ const columns: ColumnsType[] = [
/**
* Shows gateway details
*/
const PageGatewayDetailsWithState = ({ selectedGateway }: { selectedGateway: GatewayResponseItem | undefined }) => {
const PageGatewayDetailsWithState = ({ selectedGateway }: { selectedGateway: GatewayBond | undefined }) => {
const [enrichGateway, setEnrichGateway] = React.useState<GatewayEnrichedRowType>();
const [status, setStatus] = React.useState<number[] | undefined>();
const { uptimeReport, uptimeStory } = useGatewayContext();
@@ -130,7 +130,7 @@ const PageGatewayDetailsWithState = ({ selectedGateway }: { selectedGateway: Gat
* Guard component to handle loading and not found states
*/
const PageGatewayDetailGuard: FCWithChildren = () => {
const [selectedGateway, setSelectedGateway] = React.useState<GatewayResponseItem | undefined>();
const [selectedGateway, setSelectedGateway] = React.useState<GatewayBond | undefined>();
const { gateways } = useMainContext();
const { id } = useParams<{ id: string | undefined }>();
+18
View File
@@ -81,6 +81,24 @@ export const PageGateways: FCWithChildren = () => {
</MuiLink>
),
},
{
field: 'performance',
headerName: 'Routing Score',
renderHeader: () => <CustomColumnHeading headingTitle="Routing Score" />,
width: 150,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<MuiLink
sx={{ ...cellStyles }}
component={RRDLink}
to={`/network-components/gateway/${params.row.identityKey}`}
data-testid="pledge-amount"
>
{`${params.value}%`}
</MuiLink>
),
},
{
field: 'host',
renderHeader: () => <CustomColumnHeading headingTitle="IP:Port" />,
+8 -2
View File
@@ -116,15 +116,21 @@ export interface StatsResponse {
export type MixNodeHistoryResponse = StatsResponse;
export interface GatewayResponseItem {
export interface GatewayBond {
block_height: number;
pledge_amount: Amount;
total_delegation: Amount;
owner: string;
gateway: Gateway;
performance: string;
}
export type GatewayResponse = GatewayResponseItem[];
export interface GatewayBondAnnotated {
gateway_bond: GatewayBond;
performance: string;
}
export type GatewayResponse = GatewayBond[];
export interface GatewayReportResponse {
identity: string;
+1 -1
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-gateway"
version = "1.1.7"
version = "1.1.8"
authors = [
"Dave Hrycyszyn <futurechimp@users.noreply.github.com>",
"Jędrzej Stuczyński <andrew@nymtech.net>",
+1 -1
View File
@@ -102,7 +102,7 @@ impl From<Init> for OverrideConfig {
pub async fn execute(args: Init, output: OutputFormat) -> Result<(), Box<dyn Error + Send + Sync>> {
println!("Initialising gateway {}...", args.id);
let already_init = if Config::default_config_file_path(Some(&args.id)).exists() {
let already_init = if Config::default_config_file_path(&args.id).exists() {
eprintln!(
"Gateway \"{}\" was already initialised before! Config information will be \
overwritten (but keys will be kept)!",
+1 -1
View File
@@ -139,7 +139,7 @@ fn do_upgrade(mut config: Config, args: &Upgrade, package_version: Version) {
pub async fn execute(args: &Upgrade) {
let package_version = parse_package_version();
let existing_config = Config::load_from_file(Some(&args.id)).unwrap_or_else(|err| {
let existing_config = Config::load_from_file(&args.id).unwrap_or_else(|err| {
eprintln!("failed to load existing config file! - {err}");
process::exit(1)
});
+5 -5
View File
@@ -374,23 +374,23 @@ pub struct Gateway {
impl Gateway {
fn default_private_sphinx_key_file(id: &str) -> PathBuf {
Config::default_data_directory(Some(id)).join("private_sphinx.pem")
Config::default_data_directory(id).join("private_sphinx.pem")
}
fn default_public_sphinx_key_file(id: &str) -> PathBuf {
Config::default_data_directory(Some(id)).join("public_sphinx.pem")
Config::default_data_directory(id).join("public_sphinx.pem")
}
fn default_private_identity_key_file(id: &str) -> PathBuf {
Config::default_data_directory(Some(id)).join("private_identity.pem")
Config::default_data_directory(id).join("private_identity.pem")
}
fn default_public_identity_key_file(id: &str) -> PathBuf {
Config::default_data_directory(Some(id)).join("public_identity.pem")
Config::default_data_directory(id).join("public_identity.pem")
}
fn default_database_path(id: &str) -> PathBuf {
Config::default_data_directory(Some(id)).join("db.sqlite")
Config::default_data_directory(id).join("db.sqlite")
}
}
+2 -2
View File
@@ -11,14 +11,14 @@ pub(crate) fn build_config<O: Into<OverrideConfig>>(
id: String,
override_args: O,
) -> Result<Config, GatewayError> {
let config = match Config::load_from_file(Some(&id)) {
let config = match Config::load_from_file(&id) {
Ok(cfg) => cfg,
Err(err) => {
error!(
"Failed to load config for {id}. Are you sure you have run `init` before? (Error was: {err})",
);
return Err(GatewayError::ConfigLoadFailure {
path: Config::default_config_file_path(Some(&id)),
path: Config::default_config_file_path(&id),
id,
source: err,
});
+1 -1
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-mixnode"
version = "1.1.8"
version = "1.1.9"
authors = [
"Dave Hrycyszyn <futurechimp@users.noreply.github.com>",
"Jędrzej Stuczyński <andrew@nymtech.net>",
+2 -2
View File
@@ -41,7 +41,7 @@ fn read_user_input() -> String {
pub(crate) fn execute(args: Describe) {
// ensure that the mixnode has in fact been initialized
match Config::load_from_file(Some(&args.id)) {
match Config::load_from_file(&args.id) {
Ok(cfg) => cfg,
Err(err) => {
error!("Failed to load config for {}. Are you sure you have run `init` before? (Error was: {err})", &args.id);
@@ -83,7 +83,7 @@ pub(crate) fn execute(args: Describe) {
// save the struct
NodeDescription::save_to_file(
&node_description,
Config::default_config_directory(Some(&args.id)),
Config::default_config_directory(&args.id),
)
.unwrap()
}
+1 -1
View File
@@ -68,7 +68,7 @@ pub(crate) fn execute(args: &Init, output: OutputFormat) {
let id = &override_config_fields.id;
eprintln!("Initialising mixnode {id}...");
let already_init = if Config::default_config_file_path(Some(id)).exists() {
let already_init = if Config::default_config_file_path(id).exists() {
eprintln!("Mixnode \"{id}\" was already initialised before! Config information will be overwritten (but keys will be kept)!");
true
} else {
+1 -1
View File
@@ -15,7 +15,7 @@ pub(crate) struct NodeDetails {
}
pub(crate) fn execute(args: &NodeDetails, output: OutputFormat) {
let config = match Config::load_from_file(Some(&args.id)) {
let config = match Config::load_from_file(&args.id) {
Ok(cfg) => cfg,
Err(err) => {
error!(
+1 -1
View File
@@ -79,7 +79,7 @@ fn special_addresses() -> Vec<&'static str> {
pub(crate) async fn execute(args: &Run, output: OutputFormat) {
eprintln!("Starting mixnode {}...", args.id);
let mut config = match Config::load_from_file(Some(&args.id)) {
let mut config = match Config::load_from_file(&args.id) {
Ok(cfg) => cfg,
Err(err) => {
error!(
+1 -1
View File
@@ -71,7 +71,7 @@ fn print_signed_text(private_key: &identity::PrivateKey, text: &str) {
}
pub(crate) fn execute(args: &Sign) {
let config = match Config::load_from_file(Some(&args.id)) {
let config = match Config::load_from_file(&args.id) {
Ok(cfg) => cfg,
Err(err) => {
error!(
+1 -1
View File
@@ -125,7 +125,7 @@ fn do_upgrade(mut config: Config, args: &Upgrade, package_version: Version) {
pub(crate) fn execute(args: &Upgrade) {
let package_version = parse_package_version();
let existing_config = Config::load_from_file(Some(&args.id)).unwrap_or_else(|err| {
let existing_config = Config::load_from_file(&args.id).unwrap_or_else(|err| {
eprintln!("failed to load existing config file! - {err}");
process::exit(1)
});
+4 -4
View File
@@ -376,19 +376,19 @@ struct MixNode {
impl MixNode {
fn default_private_identity_key_file(id: &str) -> PathBuf {
Config::default_data_directory(Some(id)).join("private_identity.pem")
Config::default_data_directory(id).join("private_identity.pem")
}
fn default_public_identity_key_file(id: &str) -> PathBuf {
Config::default_data_directory(Some(id)).join("public_identity.pem")
Config::default_data_directory(id).join("public_identity.pem")
}
fn default_private_sphinx_key_file(id: &str) -> PathBuf {
Config::default_data_directory(Some(id)).join("private_sphinx.pem")
Config::default_data_directory(id).join("private_sphinx.pem")
}
fn default_public_sphinx_key_file(id: &str) -> PathBuf {
Config::default_data_directory(Some(id)).join("public_sphinx.pem")
Config::default_data_directory(id).join("public_sphinx.pem")
}
}
+1 -1
View File
@@ -58,7 +58,7 @@ impl MixNode {
}
fn load_node_description(config: &Config) -> NodeDescription {
NodeDescription::load_from_file(Config::default_config_directory(Some(&config.get_id())))
NodeDescription::load_from_file(Config::default_config_directory(&config.get_id()))
.unwrap_or_default()
}
+1 -1
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-api"
version = "1.1.7"
version = "1.1.8"
authors = [
"Dave Hrycyszyn <futurechimp@users.noreply.github.com>",
"Jędrzej Stuczyński <andrew@nymtech.net>",
+1 -1
View File
@@ -291,7 +291,7 @@ pub struct GatewayUptimeHistoryResponse {
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct CirculatingSupplyResponse {
pub initial_supply: Coin,
pub total_supply: Coin,
pub mixmining_reserve: Coin,
pub vesting_tokens: Coin,
pub circulating_supply: Coin,
+3 -3
View File
@@ -7,7 +7,7 @@ use validator_client::nyxd::Coin;
pub(crate) struct CirculatingSupplyCacheData {
// no need to cache that one as it's constant, but let's put it here for consistency sake
pub(crate) initial_supply: Coin,
pub(crate) total_supply: Coin,
pub(crate) mixmining_reserve: Cache<Coin>,
pub(crate) vesting_tokens: Cache<Coin>,
pub(crate) circulating_supply: Cache<Coin>,
@@ -18,7 +18,7 @@ impl CirculatingSupplyCacheData {
let zero_coin = Coin::new(0, &mix_denom);
CirculatingSupplyCacheData {
initial_supply: Coin::new(1_000_000_000_000_000, mix_denom),
total_supply: Coin::new(1_000_000_000_000_000, mix_denom),
mixmining_reserve: Cache::new(zero_coin.clone()),
vesting_tokens: Cache::new(zero_coin.clone()),
circulating_supply: Cache::new(zero_coin),
@@ -29,7 +29,7 @@ impl CirculatingSupplyCacheData {
impl<'a> From<&'a CirculatingSupplyCacheData> for CirculatingSupplyResponse {
fn from(value: &'a CirculatingSupplyCacheData) -> Self {
CirculatingSupplyResponse {
initial_supply: value.initial_supply.clone().into(),
total_supply: value.total_supply.clone().into(),
mixmining_reserve: value.mixmining_reserve.clone().into_inner().into(),
vesting_tokens: value.vesting_tokens.clone().into_inner().into(),
circulating_supply: value.circulating_supply.clone().into_inner().into(),
+1 -1
View File
@@ -76,7 +76,7 @@ impl CirculatingSupplyCache {
pub(crate) async fn update(&self, mixmining_reserve: Coin, vesting_tokens: Coin) {
let mut cache = self.data.write().await;
let mut circulating_supply = cache.initial_supply.clone();
let mut circulating_supply = cache.total_supply.clone();
circulating_supply.amount -= mixmining_reserve.amount;
circulating_supply.amount -= vesting_tokens.amount;
+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(),
)))
}
+9 -6
View File
@@ -5,6 +5,7 @@ use crate::coconut::dkg::client::DkgClient;
use crate::coconut::dkg::complaints::ComplaintReason;
use crate::coconut::dkg::state::{ConsistentState, State};
use crate::coconut::error::CoconutError;
use crate::coconut::helpers::accepted_vote_err;
use coconut_dkg_common::event_attributes::DKG_PROPOSAL_ID;
use coconut_dkg_common::types::{NodeIndex, TOTAL_DEALINGS};
use coconut_dkg_common::verification_key::owner_from_cosmos_msgs;
@@ -203,21 +204,23 @@ pub(crate) async fn verification_key_validation(
.iter()
.position(|node_index| contract_share.node_index == *node_index)
{
if !check_vk_pairing(&params, &recovered_partials[idx], &vk) {
let ret = if !check_vk_pairing(&params, &recovered_partials[idx], &vk) {
dkg_client
.vote_verification_key_share(proposal_id, false)
.await?;
.await
} else {
dkg_client
.vote_verification_key_share(proposal_id, true)
.await?;
}
.await
};
accepted_vote_err(ret)?;
}
}
Err(_) => {
dkg_client
let ret = dkg_client
.vote_verification_key_share(proposal_id, false)
.await?
.await;
accepted_vote_err(ret)?;
}
}
}
+18
View File
@@ -0,0 +1,18 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::coconut::error::CoconutError;
use validator_client::nyxd::error::NyxdError::AbciError;
// If the result is already established, the vote might be redundant and
// thus the transaction might fail
pub(crate) fn accepted_vote_err(ret: Result<(), CoconutError>) -> Result<(), CoconutError> {
if let Err(CoconutError::NyxdError(AbciError { ref log, .. })) = ret {
let accepted_err = multisig_contract_common::error::ContractError::NotOpen {}.to_string();
// If redundant voting is not the case, error out on all other error variants
if !log.value().contains(&accepted_err) {
ret?;
}
}
Ok(())
}
+5 -2
View File
@@ -5,6 +5,7 @@ use self::comm::APICommunicationChannel;
use crate::coconut::client::Client as LocalClient;
use crate::coconut::deposit::extract_encryption_key;
use crate::coconut::error::{CoconutError, Result};
use crate::coconut::helpers::accepted_vote_err;
use crate::support::storage::NymApiStorage;
use coconut_bandwidth_contract_common::spend_credential::{
funds_from_cosmos_msgs, SpendCredentialStatus,
@@ -40,6 +41,7 @@ pub(crate) mod comm;
mod deposit;
pub(crate) mod dkg;
pub(crate) mod error;
pub(crate) mod helpers;
pub(crate) mod keypair;
#[cfg(test)]
pub(crate) mod tests;
@@ -301,7 +303,7 @@ pub async fn verify_bandwidth_credential(
);
// Vote yes or no on the proposal based on the verification result
state
let ret = state
.client
.vote_proposal(
proposal_id,
@@ -312,7 +314,8 @@ pub async fn verify_bandwidth_credential(
Some(verify_credential_body.gateway_cosmos_addr().to_owned()),
)),
)
.await?;
.await;
accepted_vote_err(ret)?;
Ok(Json(VerifyCredentialResponse::new(vote_yes)))
}
+12 -14
View File
@@ -44,7 +44,7 @@ pub(crate) struct CliArgs {
/// Id of the nym-api we want to run
#[clap(long)]
pub(crate) id: Option<String>,
pub(crate) id: String,
/// Specifies whether network monitoring is enabled on this API
#[clap(short = 'm', long)]
@@ -102,12 +102,13 @@ pub(crate) struct CliArgs {
}
pub(crate) fn build_config(args: CliArgs) -> Result<Config> {
let id = args.id.clone();
// try to load config from the file, if it doesn't exist, use default values
let id = args.id.as_deref();
let (config_from_file, _already_initialized) = match Config::load_from_file(id) {
let (config_from_file, already_initialized) = match Config::load_from_file(&id) {
Ok(cfg) => (cfg, true),
Err(_) => {
let config_path = Config::default_config_file_path(id)
let config_path = Config::default_config_file_path(&id)
.into_os_string()
.into_string()
.unwrap();
@@ -121,23 +122,20 @@ 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))
.expect("Could not create data directory");
crate::coconut::dkg::controller::init_keypair(&config)?;
}
Ok(config)
}
pub(crate) fn override_config(mut config: Config, args: CliArgs) -> Config {
if let Some(id) = args.id {
fs::create_dir_all(Config::default_config_directory(Some(&id)))
.expect("Could not create config directory");
fs::create_dir_all(Config::default_data_directory(Some(&id)))
.expect("Could not create data directory");
config = config.with_id(&id);
}
pub(crate) fn override_config(config: Config, args: CliArgs) -> Config {
config
.with_id(&args.id)
.with_optional(Config::with_custom_nyxd_validator, args.nyxd_validator)
.with_optional_env(
Config::with_custom_mixnet_contract,
+30 -32
View File
@@ -201,8 +201,8 @@ pub struct NetworkMonitor {
impl NetworkMonitor {
pub const DB_FILE: &'static str = "credentials_database.db";
fn default_credentials_database_path() -> PathBuf {
Config::default_data_directory(None).join(Self::DB_FILE)
fn default_credentials_database_path(id: &str) -> PathBuf {
Config::default_data_directory(id).join(Self::DB_FILE)
}
}
@@ -220,7 +220,7 @@ impl Default for NetworkMonitor {
gateway_response_timeout: DEFAULT_GATEWAY_RESPONSE_TIMEOUT,
gateway_connection_timeout: DEFAULT_GATEWAY_CONNECTION_TIMEOUT,
packet_delivery_timeout: DEFAULT_PACKET_DELIVERY_TIMEOUT,
credentials_database_path: Self::default_credentials_database_path(),
credentials_database_path: Default::default(),
test_routes: DEFAULT_TEST_ROUTES,
minimum_test_routes: DEFAULT_MINIMUM_TEST_ROUTES,
route_test_packets: DEFAULT_ROUTE_TEST_PACKETS,
@@ -242,15 +242,15 @@ pub struct NodeStatusAPI {
impl NodeStatusAPI {
pub const DB_FILE: &'static str = "db.sqlite";
fn default_database_path() -> PathBuf {
Config::default_data_directory(None).join(Self::DB_FILE)
fn default_database_path(id: &str) -> PathBuf {
Config::default_data_directory(id).join(Self::DB_FILE)
}
}
impl Default for NodeStatusAPI {
fn default() -> Self {
NodeStatusAPI {
database_path: Self::default_database_path(),
database_path: Default::default(),
caching_interval: DEFAULT_NODE_STATUS_CACHE_INTERVAL,
}
}
@@ -342,24 +342,24 @@ impl CoconutSigner {
pub const COCONUT_VERIFICATION_KEY_FILE: &'static str = "coconut_verification_key.pem";
pub const COCONUT_SECRET_KEY_FILE: &'static str = "coconut_secret_key.pem";
fn default_coconut_verification_key_path() -> PathBuf {
Config::default_data_directory(None).join(Self::COCONUT_VERIFICATION_KEY_FILE)
fn default_coconut_verification_key_path(id: &str) -> PathBuf {
Config::default_data_directory(id).join(Self::COCONUT_VERIFICATION_KEY_FILE)
}
fn default_coconut_secret_key_path() -> PathBuf {
Config::default_data_directory(None).join(Self::COCONUT_SECRET_KEY_FILE)
fn default_coconut_secret_key_path(id: &str) -> PathBuf {
Config::default_data_directory(id).join(Self::COCONUT_SECRET_KEY_FILE)
}
fn default_dkg_persistent_state_path() -> PathBuf {
Config::default_data_directory(None).join(Self::DKG_PERSISTENT_STATE_FILE)
fn default_dkg_persistent_state_path(id: &str) -> PathBuf {
Config::default_data_directory(id).join(Self::DKG_PERSISTENT_STATE_FILE)
}
fn default_dkg_decryption_key_path() -> PathBuf {
Config::default_data_directory(None).join(Self::DKG_DECRYPTION_KEY_FILE)
fn default_dkg_decryption_key_path(id: &str) -> PathBuf {
Config::default_data_directory(id).join(Self::DKG_DECRYPTION_KEY_FILE)
}
fn default_dkg_public_key_with_proof_path() -> PathBuf {
Config::default_data_directory(None).join(Self::DKG_PUBLIC_KEY_WITH_PROOF_FILE)
fn default_dkg_public_key_with_proof_path(id: &str) -> PathBuf {
Config::default_data_directory(id).join(Self::DKG_PUBLIC_KEY_WITH_PROOF_FILE)
}
}
@@ -367,11 +367,11 @@ impl Default for CoconutSigner {
fn default() -> Self {
Self {
enabled: Default::default(),
dkg_persistent_state_path: CoconutSigner::default_dkg_persistent_state_path(),
verification_key_path: CoconutSigner::default_coconut_verification_key_path(),
secret_key_path: CoconutSigner::default_coconut_secret_key_path(),
decryption_key_path: CoconutSigner::default_dkg_decryption_key_path(),
public_key_with_proof_path: CoconutSigner::default_dkg_public_key_with_proof_path(),
dkg_persistent_state_path: Default::default(),
verification_key_path: Default::default(),
secret_key_path: Default::default(),
decryption_key_path: Default::default(),
public_key_with_proof_path: Default::default(),
dkg_contract_polling_rate: DEFAULT_DKG_CONTRACT_POLLING_RATE,
}
}
@@ -384,20 +384,18 @@ impl Config {
pub fn with_id(mut self, id: &str) -> Self {
self.base.id = id.to_string();
self.node_status_api.database_path =
Config::default_data_directory(Some(id)).join(NodeStatusAPI::DB_FILE);
self.node_status_api.database_path = NodeStatusAPI::default_database_path(id);
self.network_monitor.credentials_database_path =
Config::default_data_directory(Some(id)).join(NetworkMonitor::DB_FILE);
NetworkMonitor::default_credentials_database_path(id);
self.coconut_signer.dkg_persistent_state_path =
Config::default_data_directory(Some(id)).join(CoconutSigner::DKG_PERSISTENT_STATE_FILE);
self.coconut_signer.verification_key_path = Config::default_data_directory(Some(id))
.join(CoconutSigner::COCONUT_VERIFICATION_KEY_FILE);
self.coconut_signer.secret_key_path =
Config::default_data_directory(Some(id)).join(CoconutSigner::COCONUT_SECRET_KEY_FILE);
CoconutSigner::default_dkg_persistent_state_path(id);
self.coconut_signer.verification_key_path =
CoconutSigner::default_coconut_verification_key_path(id);
self.coconut_signer.secret_key_path = CoconutSigner::default_coconut_secret_key_path(id);
self.coconut_signer.decryption_key_path =
Config::default_data_directory(Some(id)).join(CoconutSigner::DKG_DECRYPTION_KEY_FILE);
self.coconut_signer.public_key_with_proof_path = Config::default_data_directory(Some(id))
.join(CoconutSigner::DKG_PUBLIC_KEY_WITH_PROOF_FILE);
CoconutSigner::default_dkg_decryption_key_path(id);
self.coconut_signer.public_key_with_proof_path =
CoconutSigner::default_dkg_public_key_with_proof_path(id);
self
}
@@ -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>

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