diff --git a/CHANGELOG.md b/CHANGELOG.md index 03fde5577f..09f459ca22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,11 +16,14 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - wasm-client: uses updated wasm-compatible `client-core` so that it's now capable of packet retransmission, cover traffic and poisson delay (among other things!) ([#1673]) - validator-api: add `interval_operating_cost` and `profit_margin_percent` to cmpute reward estimation endpoint - native-client/socks5-client/network-requester: improve handling error cases ([#1713]) +- vesting-contract: optional locked token pledge cap per account ([#1687]), defaults to 100_000 NYM +- clients: add testing-only support for two more extended packet sizes (8kb and 16kb). ### Fixed - validator-api, mixnode, gateway should now prefer values in config.toml over mainnet defaults ([#1645]) -- socks5-client: fix bug where in some cases packet reordering could trigger a connection being closed too early ([#1702]) +- validator-api should now correctly update historical uptimes for all mixnodes and gateways every 24h ([#1721]) +- socks5-client: fix bug where in some cases packet reordering could trigger a connection being closed too early ([#1702],[#1724]) ### Changed @@ -28,7 +31,9 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - socks5 client: graceful shutdown should fix error on disconnect in nym-connect ([#1591]) - wasm-client: fixed build errors on MacOS and changed example JS code to use mainnet ([#1585]) - gateway-client: will attempt to read now as many as 8 websocket messages at once, assuming they're already available on the socket ([#1669]) -- clients: bound the sphinx packet channel and reduce sending rate if gateway can't keep up ([#1703]) +- validator-api: changed error serialization on `inclusion_probability`, `stake-saturation` and `reward-estimation` endpoints to provide more accurate information ([#1681]) +- moved `Percent` struct to to `contracts-common`, change affects explorer-api +- clients: bound the sphinx packet channel and reduce sending rate if gateway can't keep up ([#1703],[#1725]) [#1541]: https://github.com/nymtech/nym/pull/1541 [#1558]: https://github.com/nymtech/nym/pull/1558 @@ -42,9 +47,14 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// [#1669]: https://github.com/nymtech/nym/pull/1669 [#1671]: https://github.com/nymtech/nym/pull/1671 [#1673]: https://github.com/nymtech/nym/pull/1673 +[#1681]: https://github.com/nymtech/nym/pull/1681 +[#1687]: https://github.com/nymtech/nym/pull/1687 [#1702]: https://github.com/nymtech/nym/pull/1702 [#1703]: https://github.com/nymtech/nym/pull/1703 [#1713]: https://github.com/nymtech/nym/pull/1713 +[#1721]: https://github.com/nymtech/nym/pull/1721 +[#1724]: https://github.com/nymtech/nym/pull/1724 +[#1725]: https://github.com/nymtech/nym/pull/1725 ## [nym-binaries-1.0.2](https://github.com/nymtech/nym/tree/nym-binaries-1.0.2) diff --git a/Cargo.lock b/Cargo.lock index 494afdb6c0..ed98962ef3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -737,7 +737,10 @@ name = "contracts-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "schemars", "serde", + "serde_json", + "thiserror", ] [[package]] @@ -1583,11 +1586,13 @@ version = "1.0.1" dependencies = [ "chrono", "clap 3.2.8", + "contracts-common", "dotenv", "humantime-serde", "isocountry", "itertools", "log", + "logging", "maxminddb", "mixnet-contract-common", "network-defaults", @@ -2726,6 +2731,14 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "logging" +version = "0.1.0" +dependencies = [ + "log", + "pretty_env_logger", +] + [[package]] name = "loom" version = "0.5.4" @@ -3068,6 +3081,7 @@ dependencies = [ "clap_complete_fig", "dotenv", "log", + "logging", "network-defaults", "nym-cli-commands", "pretty_env_logger", @@ -3123,6 +3137,7 @@ dependencies = [ "gateway-client", "gateway-requests", "log", + "logging", "network-defaults", "nymsphinx", "pemstore", @@ -3165,6 +3180,7 @@ dependencies = [ "gateway-requests", "humantime-serde", "log", + "logging", "mixnet-client", "mixnode-common", "network-defaults", @@ -3207,6 +3223,7 @@ dependencies = [ "humantime-serde", "lazy_static", "log", + "logging", "mixnet-client", "mixnode-common", "nonexhaustive-delayqueue", @@ -3241,6 +3258,7 @@ dependencies = [ "futures", "ipnetwork 0.20.0", "log", + "logging", "network-defaults", "nymsphinx", "ordered-buffer", @@ -3266,6 +3284,7 @@ version = "1.0.2" dependencies = [ "dirs", "log", + "logging", "pretty_env_logger", "rocket", "serde", @@ -3292,6 +3311,7 @@ dependencies = [ "gateway-client", "gateway-requests", "log", + "logging", "network-defaults", "nymsphinx", "ordered-buffer", @@ -3351,6 +3371,7 @@ dependencies = [ "coconut-interface", "config", "console-subscriber", + "contracts-common", "cosmwasm-std", "credential-storage", "credentials", @@ -3365,6 +3386,7 @@ dependencies = [ "humantime-serde", "inclusion-probability", "log", + "logging", "mixnet-contract-common", "multisig-contract-common", "nymcoconut", @@ -6388,6 +6410,7 @@ dependencies = [ "coconut-interface", "colored", "config", + "contracts-common", "cosmrs", "cosmwasm-std", "cw3", @@ -6476,12 +6499,14 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "vesting-contract" version = "1.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", "cw-storage-plus", "mixnet-contract-common", "schemars", "serde", "thiserror", + "vergen 5.1.17", "vesting-contract-common", ] @@ -6489,7 +6514,9 @@ dependencies = [ name = "vesting-contract-common" version = "0.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", + "log", "mixnet-contract-common", "schemars", "serde", diff --git a/Cargo.toml b/Cargo.toml index deb41ec808..4866366280 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ members = [ "common/execute", "common/inclusion-probability", "common/ledger", + "common/logging", "common/mixnode-common", "common/network-defaults", "common/nonexhaustive-delayqueue", diff --git a/clients/client-core/src/config/mod.rs b/clients/client-core/src/config/mod.rs index be5bce902b..678b70ef54 100644 --- a/clients/client-core/src/config/mod.rs +++ b/clients/client-core/src/config/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use config::NymConfig; +use nymsphinx::params::PacketSize; use serde::{Deserialize, Serialize}; use std::marker::PhantomData; use std::path::PathBuf; @@ -247,8 +248,8 @@ impl Config { self.debug.disable_main_poisson_packet_distribution } - pub fn get_use_extended_packet_size(&self) -> bool { - self.debug.use_extended_packet_size + pub fn get_use_extended_packet_size(&self) -> Option { + self.debug.use_extended_packet_size.clone() } pub fn get_version(&self) -> &str { @@ -470,8 +471,16 @@ pub struct Debug { /// poisson distribution. pub disable_main_poisson_packet_distribution: bool, - /// Controls whether the sent sphinx packet use the NON-DEFAULT bigger size. - pub use_extended_packet_size: bool, + /// Controls whether the sent sphinx packet use a NON-DEFAULT bigger size. + pub use_extended_packet_size: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum ExtendedPacketSize { + Extended8, + Extended16, + Extended32, } impl Default for Debug { @@ -488,7 +497,17 @@ impl Default for Debug { topology_resolution_timeout: DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT, disable_loop_cover_traffic_stream: false, disable_main_poisson_packet_distribution: false, - use_extended_packet_size: false, + use_extended_packet_size: None, + } + } +} + +impl From for PacketSize { + fn from(size: ExtendedPacketSize) -> PacketSize { + match size { + ExtendedPacketSize::Extended8 => PacketSize::ExtendedPacket8, + ExtendedPacketSize::Extended16 => PacketSize::ExtendedPacket16, + ExtendedPacketSize::Extended32 => PacketSize::ExtendedPacket32, } } } diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index 15b20eca18..2a7cd594ed 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -39,6 +39,7 @@ completions = { path = "../../common/completions" } credential-storage = { path = "../../common/credential-storage" } credentials = { path = "../../common/credentials", optional = true } crypto = { path = "../../common/crypto" } +logging = { path = "../../common/logging"} gateway-client = { path = "../../common/client-libs/gateway-client" } gateway-requests = { path = "../../gateway/gateway-requests" } network-defaults = { path = "../../common/network-defaults" } diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index 5f959d4c8e..fd27282841 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -30,7 +30,6 @@ use log::*; use nymsphinx::addressing::clients::Recipient; use nymsphinx::addressing::nodes::NodeIdentity; use nymsphinx::anonymous_replies::ReplySurb; -use nymsphinx::params::PacketSize; use nymsphinx::receiver::ReconstructedMessage; use task::{wait_for_signal, ShutdownListener, ShutdownNotifier}; @@ -103,8 +102,9 @@ impl NymClient { topology_accessor, ); - if self.config.get_base().get_use_extended_packet_size() { - stream.set_custom_packet_size(PacketSize::ExtendedPacket) + if let Some(size) = self.config.get_base().get_use_extended_packet_size() { + log::debug!("Setting extended packet size: {:?}", size); + stream.set_custom_packet_size(size.into()); } stream.start_with_shutdown(shutdown); @@ -132,8 +132,9 @@ impl NymClient { self.as_mix_recipient(), ); - if self.config.get_base().get_use_extended_packet_size() { - controller_config.set_custom_packet_size(PacketSize::ExtendedPacket) + if let Some(size) = self.config.get_base().get_use_extended_packet_size() { + log::debug!("Setting extended packet size: {:?}", size); + controller_config.set_custom_packet_size(size.into()); } info!("Starting real traffic stream..."); diff --git a/clients/native/src/main.rs b/clients/native/src/main.rs index 16db5c8b8b..00529494e7 100644 --- a/clients/native/src/main.rs +++ b/clients/native/src/main.rs @@ -3,6 +3,7 @@ use clap::{crate_version, Parser}; use error::ClientError; +use logging::setup_logging; use network_defaults::setup_env; pub mod client; @@ -36,25 +37,3 @@ fn banner() -> String { crate_version!() ) } - -fn setup_logging() { - let mut log_builder = pretty_env_logger::formatted_timed_builder(); - if let Ok(s) = ::std::env::var("RUST_LOG") { - log_builder.parse_filters(&s); - } else { - // default to 'Info' - log_builder.filter(None, log::LevelFilter::Info); - } - - log_builder - .filter_module("handlebars", log::LevelFilter::Warn) - .filter_module("hyper", log::LevelFilter::Warn) - .filter_module("mio", log::LevelFilter::Warn) - .filter_module("reqwest", log::LevelFilter::Warn) - .filter_module("sled", log::LevelFilter::Warn) - .filter_module("tokio_reactor", log::LevelFilter::Warn) - .filter_module("tokio_tungstenite", log::LevelFilter::Warn) - .filter_module("tungstenite", log::LevelFilter::Warn) - .filter_module("want", log::LevelFilter::Warn) - .init(); -} diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index a5dd4e41a1..96f48686e2 100644 --- a/clients/socks5/Cargo.toml +++ b/clients/socks5/Cargo.toml @@ -32,6 +32,7 @@ completions = { path = "../../common/completions" } credential-storage = { path = "../../common/credential-storage" } credentials = { path = "../../common/credentials", optional = true } crypto = { path = "../../common/crypto" } +logging = { path = "../../common/logging"} gateway-client = { path = "../../common/client-libs/gateway-client" } gateway-requests = { path = "../../gateway/gateway-requests" } network-defaults = { path = "../../common/network-defaults" } diff --git a/clients/socks5/src/client/mod.rs b/clients/socks5/src/client/mod.rs index 2aa400a73f..6992f688d4 100644 --- a/clients/socks5/src/client/mod.rs +++ b/clients/socks5/src/client/mod.rs @@ -36,7 +36,6 @@ use gateway_client::{ use log::*; use nymsphinx::addressing::clients::Recipient; use nymsphinx::addressing::nodes::NodeIdentity; -use nymsphinx::params::PacketSize; use task::{wait_for_signal, ShutdownListener, ShutdownNotifier}; pub mod config; @@ -103,8 +102,9 @@ impl NymClient { topology_accessor, ); - if self.config.get_base().get_use_extended_packet_size() { - stream.set_custom_packet_size(PacketSize::ExtendedPacket) + if let Some(size) = self.config.get_base().get_use_extended_packet_size() { + log::debug!("Setting extended packet size: {:?}", size); + stream.set_custom_packet_size(size.into()); } stream.start_with_shutdown(shutdown); @@ -132,8 +132,9 @@ impl NymClient { self.as_mix_recipient(), ); - if self.config.get_base().get_use_extended_packet_size() { - controller_config.set_custom_packet_size(PacketSize::ExtendedPacket) + if let Some(size) = self.config.get_base().get_use_extended_packet_size() { + log::debug!("Setting extended packet size: {:?}", size); + controller_config.set_custom_packet_size(size.into()); } info!("Starting real traffic stream..."); diff --git a/clients/socks5/src/main.rs b/clients/socks5/src/main.rs index eba135451a..d7a0fdf01f 100644 --- a/clients/socks5/src/main.rs +++ b/clients/socks5/src/main.rs @@ -3,6 +3,7 @@ use clap::{crate_version, Parser}; use error::Socks5ClientError; +use logging::setup_logging; use network_defaults::setup_env; pub mod client; @@ -36,25 +37,3 @@ fn banner() -> String { crate_version!() ) } - -fn setup_logging() { - let mut log_builder = pretty_env_logger::formatted_timed_builder(); - if let Ok(s) = ::std::env::var("RUST_LOG") { - log_builder.parse_filters(&s); - } else { - // default to 'Info' - log_builder.filter(None, log::LevelFilter::Info); - } - - log_builder - .filter_module("handlebars", log::LevelFilter::Warn) - .filter_module("hyper", log::LevelFilter::Warn) - .filter_module("mio", log::LevelFilter::Warn) - .filter_module("reqwest", log::LevelFilter::Warn) - .filter_module("sled", log::LevelFilter::Warn) - .filter_module("tokio_reactor", log::LevelFilter::Warn) - .filter_module("tokio_tungstenite", log::LevelFilter::Warn) - .filter_module("tungstenite", log::LevelFilter::Warn) - .filter_module("want", log::LevelFilter::Warn) - .init(); -} diff --git a/clients/webassembly/src/client/config.rs b/clients/webassembly/src/client/config.rs index d11b7544e7..2b8b8bd644 100644 --- a/clients/webassembly/src/client/config.rs +++ b/clients/webassembly/src/client/config.rs @@ -4,7 +4,7 @@ // due to expansion of #[wasm_bindgen] macro on `Debug` Config struct #![allow(clippy::drop_non_drop)] -use client_core::config::{Debug as ConfigDebug, GatewayEndpoint}; +use client_core::config::{Debug as ConfigDebug, ExtendedPacketSize, GatewayEndpoint}; use std::time::Duration; use url::Url; use wasm_bindgen::prelude::*; @@ -107,6 +107,11 @@ pub struct Debug { impl From for ConfigDebug { fn from(debug: Debug) -> Self { + // For now we just always use the (older) 32kb extended size + let use_extended_packet_size = debug + .use_extended_packet_size + .then(|| ExtendedPacketSize::Extended32); + ConfigDebug { average_packet_delay: Duration::from_millis(debug.average_packet_delay_ms), average_ack_delay: Duration::from_millis(debug.average_ack_delay_ms), @@ -126,7 +131,7 @@ impl From for ConfigDebug { disable_loop_cover_traffic_stream: debug.disable_loop_cover_traffic_stream, disable_main_poisson_packet_distribution: debug .disable_main_poisson_packet_distribution, - use_extended_packet_size: debug.use_extended_packet_size, + use_extended_packet_size, } } } @@ -148,7 +153,7 @@ impl From for Debug { disable_loop_cover_traffic_stream: debug.disable_loop_cover_traffic_stream, disable_main_poisson_packet_distribution: debug .disable_main_poisson_packet_distribution, - use_extended_packet_size: debug.use_extended_packet_size, + use_extended_packet_size: debug.use_extended_packet_size.is_some(), } } } diff --git a/clients/webassembly/src/client/mod.rs b/clients/webassembly/src/client/mod.rs index 967f0323ff..1fb324dc39 100644 --- a/clients/webassembly/src/client/mod.rs +++ b/clients/webassembly/src/client/mod.rs @@ -22,7 +22,6 @@ use gateway_client::{ MixnetMessageSender, }; use nymsphinx::addressing::clients::Recipient; -use nymsphinx::params::PacketSize; use rand::rngs::OsRng; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; @@ -110,8 +109,8 @@ impl NymClient { topology_accessor, ); - if self.config.debug.use_extended_packet_size { - stream.set_custom_packet_size(PacketSize::ExtendedPacket) + if let Some(size) = &self.config.debug.use_extended_packet_size { + stream.set_custom_packet_size(size.clone().into()); } stream.start(); @@ -135,8 +134,8 @@ impl NymClient { self.as_mix_recipient(), ); - if self.config.debug.use_extended_packet_size { - controller_config.set_custom_packet_size(PacketSize::ExtendedPacket) + if let Some(size) = &self.config.debug.use_extended_packet_size { + controller_config.set_custom_packet_size(size.clone().into()); } console_log!("Starting real traffic stream..."); diff --git a/common/client-libs/gateway-client/src/packet_router.rs b/common/client-libs/gateway-client/src/packet_router.rs index ed39c36984..6732d2202e 100644 --- a/common/client-libs/gateway-client/src/packet_router.rs +++ b/common/client-libs/gateway-client/src/packet_router.rs @@ -62,9 +62,19 @@ impl PacketRouter { trace!("routing regular packet"); received_messages.push(received_packet); } else if received_packet.len() - == PacketSize::ExtendedPacket.plaintext_size() - ack_overhead + == PacketSize::ExtendedPacket8.plaintext_size() - ack_overhead { - trace!("routing extended packet"); + trace!("routing extended8 packet"); + received_messages.push(received_packet); + } else if received_packet.len() + == PacketSize::ExtendedPacket16.plaintext_size() - ack_overhead + { + trace!("routing extended16 packet"); + received_messages.push(received_packet); + } else if received_packet.len() + == PacketSize::ExtendedPacket32.plaintext_size() - ack_overhead + { + trace!("routing extended32 packet"); received_messages.push(received_packet); } else { // this can happen if other clients are not padding their messages diff --git a/common/client-libs/mixnet-client/src/client.rs b/common/client-libs/mixnet-client/src/client.rs index 181eac65ec..600268b69b 100644 --- a/common/client-libs/mixnet-client/src/client.rs +++ b/common/client-libs/mixnet-client/src/client.rs @@ -23,6 +23,7 @@ pub struct Config { maximum_reconnection_backoff: Duration, initial_connection_timeout: Duration, maximum_connection_buffer_size: usize, + use_legacy_version: bool, } impl Config { @@ -31,12 +32,14 @@ impl Config { maximum_reconnection_backoff: Duration, initial_connection_timeout: Duration, maximum_connection_buffer_size: usize, + use_legacy_version: bool, ) -> Self { Config { initial_reconnection_backoff, maximum_reconnection_backoff, initial_connection_timeout, maximum_connection_buffer_size, + use_legacy_version, } } } @@ -201,7 +204,8 @@ impl SendWithoutResponse for Client { packet_mode: PacketMode, ) -> io::Result<()> { trace!("Sending packet to {:?}", address); - let framed_packet = FramedSphinxPacket::new(packet, packet_mode); + let framed_packet = + FramedSphinxPacket::new(packet, packet_mode, self.config.use_legacy_version); if let Some(sender) = self.conn_new.get_mut(&address) { if let Err(err) = sender.channel.try_send(framed_packet) { @@ -259,6 +263,7 @@ mod tests { maximum_reconnection_backoff: Duration::from_millis(300_000), initial_connection_timeout: Duration::from_millis(1_500), maximum_connection_buffer_size: 128, + use_legacy_version: false, }) } diff --git a/common/client-libs/mixnet-client/src/forwarder.rs b/common/client-libs/mixnet-client/src/forwarder.rs index ead3b25a41..d92f6ad790 100644 --- a/common/client-libs/mixnet-client/src/forwarder.rs +++ b/common/client-libs/mixnet-client/src/forwarder.rs @@ -24,12 +24,14 @@ impl PacketForwarder { maximum_reconnection_backoff: Duration, initial_connection_timeout: Duration, maximum_connection_buffer_size: usize, + use_legacy_version: bool, ) -> (PacketForwarder, MixForwardingSender) { let client_config = Config::new( initial_reconnection_backoff, maximum_reconnection_backoff, initial_connection_timeout, maximum_connection_buffer_size, + use_legacy_version, ); let (packet_sender, packet_receiver) = mpsc::unbounded(); diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index 0beeefe809..2b45232238 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -13,6 +13,7 @@ colored = "2.0" cw3 = "0.13.1" mixnet-contract-common = { path= "../../cosmwasm-smart-contracts/mixnet-contract" } vesting-contract-common = { path= "../../cosmwasm-smart-contracts/vesting-contract" } +contracts-common = { path = "../../cosmwasm-smart-contracts/contracts-common" } coconut-bandwidth-contract-common = { path= "../../cosmwasm-smart-contracts/coconut-bandwidth-contract" } multisig-contract-common = { path = "../../cosmwasm-smart-contracts/multisig-contract" } vesting-contract = { path = "../../../contracts/vesting" } diff --git a/common/client-libs/validator-client/src/nymd/mod.rs b/common/client-libs/validator-client/src/nymd/mod.rs index 151bb30b23..481cb021be 100644 --- a/common/client-libs/validator-client/src/nymd/mod.rs +++ b/common/client-libs/validator-client/src/nymd/mod.rs @@ -15,14 +15,12 @@ use cosmrs::rpc::query::Query; use cosmrs::rpc::Error as TendermintRpcError; use cosmrs::rpc::HttpClientUrl; use cosmrs::tx::Msg; -use cosmwasm_std::Uint128; use execute::execute; use network_defaults::{ChainDetails, NymNetworkDetails}; use serde::{Deserialize, Serialize}; use std::convert::TryInto; use std::time::SystemTime; use vesting_contract_common::ExecuteMsg as VestingExecuteMsg; -use vesting_contract_common::QueryMsg as VestingQueryMsg; pub use crate::nymd::cosmwasm_client::client::CosmWasmClient; pub use crate::nymd::cosmwasm_client::signing_client::SigningCosmWasmClient; @@ -47,6 +45,7 @@ pub use fee::{gas_price::GasPrice, GasAdjustable, GasAdjustment}; use mixnet_contract_common::MixId; pub use signing_client::Client as SigningNymdClient; pub use traits::{VestingQueryClient, VestingSigningClient}; +use vesting_contract_common::PledgeCap; pub mod coin; pub mod cosmwasm_client; @@ -482,16 +481,6 @@ impl NymdClient { self.client.get_total_supply().await } - pub async fn vesting_get_locked_pledge_cap(&self) -> Result - where - C: CosmWasmClient + Sync, - { - let request = VestingQueryMsg::GetLockedPledgeCap {}; - self.client - .query_contract_smart(self.vesting_contract_address(), &request) - .await - } - pub async fn simulate(&self, messages: I) -> Result where C: SigningCosmWasmClient + Sync, @@ -737,12 +726,16 @@ impl NymdClient { #[execute("vesting")] fn _vesting_update_locked_pledge_cap( &self, - amount: Uint128, + address: String, + cap: PledgeCap, fee: Option, ) -> (VestingExecuteMsg, Option) where C: SigningCosmWasmClient + Sync, { - (VestingExecuteMsg::UpdateLockedPledgeCap { amount }, fee) + ( + VestingExecuteMsg::UpdateLockedPledgeCap { address, cap }, + fee, + ) } } diff --git a/common/client-libs/validator-client/src/nymd/traits/vesting_query_client.rs b/common/client-libs/validator-client/src/nymd/traits/vesting_query_client.rs index 22dd42952c..598818197a 100644 --- a/common/client-libs/validator-client/src/nymd/traits/vesting_query_client.rs +++ b/common/client-libs/validator-client/src/nymd/traits/vesting_query_client.rs @@ -6,8 +6,10 @@ pub use crate::nymd::cosmwasm_client::client::CosmWasmClient; use crate::nymd::error::NymdError; use crate::nymd::NymdClient; use async_trait::async_trait; +use contracts_common::ContractBuildInformation; use cosmwasm_std::{Coin as CosmWasmCoin, Timestamp}; use mixnet_contract_common::MixId; +use serde::Deserialize; use vesting_contract::vesting::Account; use vesting_contract_common::{ messages::QueryMsg as VestingQueryMsg, AllDelegationsResponse, DelegationTimesResponse, @@ -16,6 +18,15 @@ use vesting_contract_common::{ #[async_trait] pub trait VestingQueryClient { + async fn query_vesting_contract(&self, query: VestingQueryMsg) -> Result + where + for<'a> T: Deserialize<'a>; + + async fn get_vesting_contract_version(&self) -> Result { + self.query_vesting_contract(VestingQueryMsg::GetContractVersion {}) + .await + } + async fn locked_coins( &self, address: &str, @@ -107,6 +118,15 @@ pub trait VestingQueryClient { #[async_trait] impl VestingQueryClient for NymdClient { + async fn query_vesting_contract(&self, query: VestingQueryMsg) -> Result + where + for<'a> T: Deserialize<'a>, + { + self.client + .query_contract_smart(self.vesting_contract_address(), &query) + .await + } + async fn locked_coins( &self, vesting_account_address: &str, diff --git a/common/client-libs/validator-client/src/nymd/traits/vesting_signing_client.rs b/common/client-libs/validator-client/src/nymd/traits/vesting_signing_client.rs index f1c22c0078..eebf0adfa3 100644 --- a/common/client-libs/validator-client/src/nymd/traits/vesting_signing_client.rs +++ b/common/client-libs/validator-client/src/nymd/traits/vesting_signing_client.rs @@ -9,6 +9,7 @@ use async_trait::async_trait; use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; use mixnet_contract_common::{Gateway, MixId, MixNode}; use vesting_contract_common::messages::{ExecuteMsg as VestingExecuteMsg, VestingSpecification}; +use vesting_contract_common::PledgeCap; #[async_trait] pub trait VestingSigningClient { @@ -105,6 +106,7 @@ pub trait VestingSigningClient { staking_address: Option, vesting_spec: Option, amount: Coin, + cap: Option, fee: Option, ) -> Result; } @@ -382,6 +384,7 @@ impl VestingSigningClient for NymdClient staking_address: Option, vesting_spec: Option, amount: Coin, + cap: Option, fee: Option, ) -> Result { let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); @@ -389,6 +392,7 @@ impl VestingSigningClient for NymdClient owner_address: owner_address.to_string(), staking_address, vesting_spec, + cap, }; self.client .execute( diff --git a/common/client-libs/validator-client/src/validator_api/error.rs b/common/client-libs/validator-client/src/validator_api/error.rs index 02704e6229..9d7cd1d50d 100644 --- a/common/client-libs/validator-client/src/validator_api/error.rs +++ b/common/client-libs/validator-client/src/validator_api/error.rs @@ -1,4 +1,5 @@ use thiserror::Error; +use validator_api_requests::models::RequestError; #[derive(Error, Debug)] pub enum ValidatorAPIError { @@ -10,4 +11,7 @@ pub enum ValidatorAPIError { #[error("Request failed with error message - {0}")] GenericRequestFailure(String), + + #[error("The validator API has failed to resolve our request. It returned status code {status} and additional error message: {}", error.message())] + ApiRequestFailure { status: u16, error: RequestError }, } diff --git a/common/client-libs/validator-client/src/validator_api/mod.rs b/common/client-libs/validator-client/src/validator_api/mod.rs index ee501a19e0..0f4ebc60db 100644 --- a/common/client-libs/validator-client/src/validator_api/mod.rs +++ b/common/client-libs/validator-client/src/validator_api/mod.rs @@ -5,6 +5,7 @@ use crate::validator_api::error::ValidatorAPIError; use crate::validator_api::routes::{CORE_STATUS_COUNT, SINCE_ARG}; use mixnet_contract_common::mixnode::MixNodeDetails; use mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId}; +use reqwest::Response; use serde::{Deserialize, Serialize}; use url::Url; use validator_api_requests::coconut::{ @@ -12,9 +13,10 @@ use validator_api_requests::coconut::{ VerifyCredentialBody, VerifyCredentialResponse, }; use validator_api_requests::models::{ - GatewayCoreStatusResponse, InclusionProbabilityResponse, MixNodeBondAnnotated, - MixnodeCoreStatusResponse, MixnodeStatusResponse, RewardEstimationResponse, - StakeSaturationResponse, UptimeResponse, + GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse, + InclusionProbabilityResponse, MixNodeBondAnnotated, MixnodeCoreStatusResponse, + MixnodeStatusReportResponse, MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RequestError, + RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, }; pub mod error; @@ -47,6 +49,19 @@ impl Client { &self.url } + async fn send_get_request( + &self, + path: PathSegments<'_>, + params: Params<'_, K, V>, + ) -> Result + where + K: AsRef, + V: AsRef, + { + let url = create_api_url(&self.url, path, params); + Ok(self.reqwest_client.get(url).send().await?) + } + async fn query_validator_api( &self, path: PathSegments<'_>, @@ -57,8 +72,36 @@ impl Client { K: AsRef, V: AsRef, { - let url = create_api_url(&self.url, path, params); - Ok(self.reqwest_client.get(url).send().await?.json().await?) + let res = self.send_get_request(path, params).await?; + if res.status().is_success() { + Ok(res.json().await?) + } else { + Err(ValidatorAPIError::GenericRequestFailure(res.text().await?)) + } + } + + // This works for endpoints returning Result, ErrorResponse> + async fn query_validator_api_fallible( + &self, + path: PathSegments<'_>, + params: Params<'_, K, V>, + ) -> Result + where + for<'a> T: Deserialize<'a>, + K: AsRef, + V: AsRef, + { + let res = self.send_get_request(path, params).await?; + let status = res.status(); + if res.status().is_success() { + Ok(res.json().await?) + } else { + let request_error: RequestError = res.json().await?; + Err(ValidatorAPIError::ApiRequestFailure { + status: status.as_u16(), + error: request_error, + }) + } } async fn post_validator_api( @@ -135,6 +178,74 @@ impl Client { .await } + pub async fn get_mixnode_report( + &self, + mix_id: MixId, + ) -> Result { + self.query_validator_api( + &[ + routes::API_VERSION, + routes::STATUS, + routes::MIXNODE, + &mix_id.to_string(), + routes::REPORT, + ], + NO_PARAMS, + ) + .await + } + + pub async fn get_gateway_report( + &self, + identity: IdentityKeyRef<'_>, + ) -> Result { + self.query_validator_api( + &[ + routes::API_VERSION, + routes::STATUS, + routes::GATEWAY, + identity, + routes::REPORT, + ], + NO_PARAMS, + ) + .await + } + + pub async fn get_mixnode_history( + &self, + mix_id: MixId, + ) -> Result { + self.query_validator_api( + &[ + routes::API_VERSION, + routes::STATUS, + routes::MIXNODE, + &mix_id.to_string(), + routes::HISTORY, + ], + NO_PARAMS, + ) + .await + } + + pub async fn get_gateway_history( + &self, + identity: IdentityKeyRef<'_>, + ) -> Result { + self.query_validator_api( + &[ + routes::API_VERSION, + routes::STATUS, + routes::GATEWAY, + identity, + routes::HISTORY, + ], + NO_PARAMS, + ) + .await + } + pub async fn get_rewarded_mixnodes_detailed( &self, ) -> Result, ValidatorAPIError> { @@ -234,7 +345,7 @@ impl Client { &self, mix_id: MixId, ) -> Result { - self.query_validator_api( + self.query_validator_api_fallible( &[ routes::API_VERSION, routes::STATUS_ROUTES, @@ -251,7 +362,7 @@ impl Client { &self, mix_id: MixId, ) -> Result { - self.query_validator_api( + self.query_validator_api_fallible( &[ routes::API_VERSION, routes::STATUS_ROUTES, @@ -268,7 +379,7 @@ impl Client { &self, mix_id: MixId, ) -> Result { - self.query_validator_api( + self.query_validator_api_fallible( &[ routes::API_VERSION, routes::STATUS_ROUTES, @@ -285,7 +396,7 @@ impl Client { &self, mix_id: MixId, ) -> Result { - self.query_validator_api( + self.query_validator_api_fallible( &[ routes::API_VERSION, routes::STATUS_ROUTES, diff --git a/common/client-libs/validator-client/src/validator_api/routes.rs b/common/client-libs/validator-client/src/validator_api/routes.rs index 8f1a125c80..64aa1b8cd9 100644 --- a/common/client-libs/validator-client/src/validator_api/routes.rs +++ b/common/client-libs/validator-client/src/validator_api/routes.rs @@ -10,7 +10,6 @@ pub const GATEWAYS: &str = "gateways"; pub const DETAILED: &str = "detailed"; pub const ACTIVE: &str = "active"; pub const REWARDED: &str = "rewarded"; - pub const COCONUT_ROUTES: &str = "coconut"; pub const BANDWIDTH: &str = "bandwidth"; @@ -28,6 +27,8 @@ pub const CORE_STATUS_COUNT: &str = "core-status-count"; pub const SINCE_ARG: &str = "since"; pub const STATUS: &str = "status"; +pub const REPORT: &str = "report"; +pub const HISTORY: &str = "history"; pub const REWARD_ESTIMATION: &str = "reward-estimation"; pub const AVG_UPTIME: &str = "avg_uptime"; pub const STAKE_SATURATION: &str = "stake-saturation"; diff --git a/common/commands/src/validator/vesting/create_vesting_schedule.rs b/common/commands/src/validator/vesting/create_vesting_schedule.rs index 65268d27ed..c7c133673d 100644 --- a/common/commands/src/validator/vesting/create_vesting_schedule.rs +++ b/common/commands/src/validator/vesting/create_vesting_schedule.rs @@ -12,6 +12,7 @@ use validator_client::nymd::AccountId; use validator_client::nymd::VestingSigningClient; use validator_client::nymd::{CosmosCoin, Denom}; use vesting_contract_common::messages::VestingSpecification; +use vesting_contract_common::PledgeCap; use crate::context::SigningClient; @@ -34,6 +35,12 @@ pub struct Args { #[clap(long)] pub staking_address: Option, + + #[clap( + long, + help = "Pledge cap as either absolute uNYM value or percentage, floats need to be in the 0.0 to 1.0 range and will be parsed as percentages, integers will be parsed as uNYM" + )] + pub pledge_cap: Option, } pub async fn create(args: Args, client: SigningClient, network_details: &NymNetworkDetails) { @@ -55,6 +62,7 @@ pub async fn create(args: Args, client: SigningClient, network_details: &NymNetw args.staking_address, Some(vesting), coin.into(), + args.pledge_cap, None, ) .await diff --git a/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml index 2145acc513..4f98451100 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml +++ b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml @@ -9,3 +9,8 @@ edition = "2021" [dependencies] cosmwasm-std = "1.0.0" serde = { version = "1.0", features = ["derive"] } +schemars = "0.8" +thiserror = "1" + +[dev-dependencies] +serde_json = "1.0.0" diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs index 6c25d95552..5acef76350 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs @@ -1,7 +1,132 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use serde::{Deserialize, Serialize}; +use cosmwasm_std::Decimal; +use cosmwasm_std::Uint128; +use schemars::JsonSchema; +use serde::de::Error; +use serde::{Deserialize, Deserializer, Serialize}; +use std::fmt::{self, Display, Formatter}; +use std::ops::Mul; +use std::str::FromStr; +use thiserror::Error; + +pub fn truncate_decimal(amount: Decimal) -> Uint128 { + amount * Uint128::new(1) +} + +#[derive(Error, Debug)] +pub enum ContractsCommonError { + #[error("Provided percent value ({0}) is greater than 100%")] + InvalidPercent(Decimal), + + #[error("{source}")] + StdErr { + #[from] + source: cosmwasm_std::StdError, + }, +} + +/// Percent represents a value between 0 and 100% +/// (i.e. between 0.0 and 1.0) +#[derive( + Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Serialize, Deserialize, JsonSchema, +)] +pub struct Percent(#[serde(deserialize_with = "de_decimal_percent")] Decimal); + +impl Percent { + pub fn new(value: Decimal) -> Result { + if value > Decimal::one() { + Err(ContractsCommonError::InvalidPercent(value)) + } else { + Ok(Percent(value)) + } + } + + pub fn is_zero(&self) -> bool { + self.0 == Decimal::zero() + } + + pub fn zero() -> Self { + Self(Decimal::zero()) + } + + pub fn hundred() -> Self { + Self(Decimal::one()) + } + + pub fn from_percentage_value(value: u64) -> Result { + Percent::new(Decimal::percent(value)) + } + + pub fn value(&self) -> Decimal { + self.0 + } + + pub fn round_to_integer(&self) -> u8 { + let hundred = Decimal::from_ratio(100u32, 1u32); + // we know the cast from u128 to u8 is a safe one since the internal value must be within 0 - 1 range + truncate_decimal(hundred * self.0).u128() as u8 + } +} + +impl Display for Percent { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + // Adding this as part of resolving release -> develop merge conflict. + // At the time, unwraps were forbidden in develop, but not (yet) in release. + // Clearly we should fix this. + #[allow(clippy::unwrap_used)] + let adjusted = Decimal::from_atomics(100u32, 0).unwrap() * self.0; + write!(f, "{}%", adjusted) + } +} + +impl FromStr for Percent { + type Err = ContractsCommonError; + + fn from_str(s: &str) -> Result { + Percent::new(Decimal::from_str(s)?) + } +} + +impl Mul for Percent { + type Output = Decimal; + + fn mul(self, rhs: Decimal) -> Self::Output { + self.0 * rhs + } +} + +impl Mul for Decimal { + type Output = Decimal; + + fn mul(self, rhs: Percent) -> Self::Output { + rhs * self + } +} + +impl Mul for Percent { + type Output = Uint128; + + fn mul(self, rhs: Uint128) -> Self::Output { + self.0 * rhs + } +} + +// implement custom Deserialize because we want to validate Percent has the correct range +fn de_decimal_percent<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let v = Decimal::deserialize(deserializer)?; + if v > Decimal::one() { + Err(D::Error::custom( + "provided decimal percent is larger than 100%", + )) + } else { + Ok(v) + } +} // TODO: there's no reason this couldn't be used for proper binaries, but in that case // perhaps the struct should get renamed and moved to a "more" common crate @@ -31,3 +156,47 @@ pub struct ContractBuildInformation { /// Provides the rustc version that was used for the build, for example `1.52.0-nightly`. pub rustc_version: String, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn percent_serde() { + let valid_value = Percent::from_percentage_value(80).unwrap(); + let serialized = serde_json::to_string(&valid_value).unwrap(); + + let deserialized: Percent = serde_json::from_str(&serialized).unwrap(); + assert_eq!(valid_value, deserialized); + + let invalid_values = vec!["\"42\"", "\"1.1\"", "\"1.00000001\"", "\"foomp\"", "\"1a\""]; + for invalid_value in invalid_values { + assert!(serde_json::from_str::<'_, Percent>(invalid_value).is_err()) + } + assert_eq!( + serde_json::from_str::<'_, Percent>("\"0.95\"").unwrap(), + Percent::from_percentage_value(95).unwrap() + ) + } + + #[test] + fn percent_to_absolute_integer() { + let p = serde_json::from_str::<'_, Percent>("\"0.0001\"").unwrap(); + assert_eq!(p.round_to_integer(), 0); + + let p = serde_json::from_str::<'_, Percent>("\"0.0099\"").unwrap(); + assert_eq!(p.round_to_integer(), 0); + + let p = serde_json::from_str::<'_, Percent>("\"0.0199\"").unwrap(); + assert_eq!(p.round_to_integer(), 1); + + let p = serde_json::from_str::<'_, Percent>("\"0.45123\"").unwrap(); + assert_eq!(p.round_to_integer(), 45); + + let p = serde_json::from_str::<'_, Percent>("\"0.999999999\"").unwrap(); + assert_eq!(p.round_to_integer(), 99); + + let p = serde_json::from_str::<'_, Percent>("\"1.00\"").unwrap(); + assert_eq!(p.round_to_integer(), 100); + } +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs index 436f4cad07..e0d369f933 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs @@ -13,9 +13,6 @@ pub enum MixnetContractError { source: cosmwasm_std::StdError, }, - #[error("Provided percent value is greater than 100%")] - InvalidPercent, - #[error("Attempted to subtract decimals with overflow ({minuend}.sub({subtrahend}))")] OverflowDecimalSubtraction { minuend: Decimal, diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs index 60e3f08495..f09a6cfe22 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs @@ -73,6 +73,9 @@ pub struct Interval { // TODO add a better TS type generation #[cfg_attr(feature = "generate-ts", ts(type = "string"))] #[serde(with = "string_rfc3339_offset_date_time")] + // note: the `ts-rs failed to parse this attribute. It will be ignored.` warning emitted during + // compilation is fine (I guess). `ts-rs` can't handle `with` serde attribute, but that's okay + // since we explicitly specified this field should correspond to typescript's string current_epoch_start: OffsetDateTime, current_epoch_id: EpochId, #[cfg_attr(feature = "generate-ts", ts(type = "{ secs: number; nanos: number; }"))] diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index 897f750569..c6fb28063e 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -33,6 +33,7 @@ pub struct InitialRewardingParams { pub initial_reward_pool: Decimal, pub initial_staking_supply: Decimal, + pub staking_supply_scale_factor: Percent, pub sybil_resistance: Percent, pub active_set_work_factor: Decimal, pub interval_pool_emission: Percent, @@ -56,6 +57,7 @@ impl InitialRewardingParams { interval: IntervalRewardParams { reward_pool: self.initial_reward_pool, staking_supply: self.initial_staking_supply, + staking_supply_scale_factor: self.staking_supply_scale_factor, epoch_reward_budget, stake_saturation_point, sybil_resistance: self.sybil_resistance, diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs index 5fc9d558ae..713a70468b 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs @@ -27,6 +27,11 @@ pub struct IntervalRewardParams { #[cfg_attr(feature = "generate-ts", ts(type = "string"))] pub staking_supply: Decimal, + /// Defines the percentage of stake needed to reach saturation for all of the nodes in the rewarded set. + /// Also known as `beta`. + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub staking_supply_scale_factor: Percent, + // computed values /// Current value of the computed reward budget per epoch, per node. /// It is expected to be constant throughout the interval. @@ -174,6 +179,10 @@ impl RewardingParams { self.interval.staking_supply = staking_supply; } + if let Some(staking_supply_scale_factor) = updates.staking_supply_scale_factor { + self.interval.staking_supply_scale_factor = staking_supply_scale_factor + } + if let Some(sybil_resistance_percent) = updates.sybil_resistance_percent { self.interval.sybil_resistance = sybil_resistance_percent; } @@ -245,6 +254,9 @@ pub struct IntervalRewardingParamsUpdate { #[cfg_attr(feature = "generate-ts", ts(type = "string | null"))] pub staking_supply: Option, + #[cfg_attr(feature = "generate-ts", ts(type = "string | null"))] + pub staking_supply_scale_factor: Option, + #[cfg_attr(feature = "generate-ts", ts(type = "string | null"))] pub sybil_resistance_percent: Option, @@ -262,6 +274,7 @@ impl IntervalRewardingParamsUpdate { // essentially at least a single field has to be a `Some` self.reward_pool.is_some() || self.staking_supply.is_some() + || self.staking_supply_scale_factor.is_some() || self.sybil_resistance_percent.is_some() || self.active_set_work_factor.is_some() || self.interval_pool_emission.is_some() diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs index 8a6a8f3366..4d9ca75459 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs @@ -1,6 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use contracts_common::truncate_decimal; use cosmwasm_std::{Coin, Decimal, Uint128}; /// Truncates all decimal points so that the reward would fit in a `Coin` and so that we would @@ -17,7 +18,3 @@ pub fn truncate_reward(reward: Decimal, denom: impl Into) -> Coin { pub fn truncate_reward_amount(reward: Decimal) -> Uint128 { truncate_decimal(reward) } - -pub fn truncate_decimal(amount: Decimal) -> Uint128 { - amount * Uint128::new(1) -} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/mod.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/mod.rs index 62352a5b7f..c6575661fd 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/mod.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/mod.rs @@ -41,7 +41,12 @@ impl Simulator { if self.interval.current_interval_id() + 1 == updated.current_interval_id() { let old = self.system_rewarding_params.interval; let reward_pool = old.reward_pool - self.pending_reward_pool_emission; - let staking_supply = old.staking_supply + self.pending_reward_pool_emission; + let staking_supply = old.staking_supply + + self + .system_rewarding_params + .interval + .staking_supply_scale_factor + * self.pending_reward_pool_emission; let epoch_reward_budget = reward_pool / self.interval.epochs_in_interval().into_base_decimal()? * old.interval_pool_emission.value(); @@ -55,6 +60,7 @@ impl Simulator { interval: IntervalRewardParams { reward_pool, staking_supply, + staking_supply_scale_factor: old.staking_supply_scale_factor, epoch_reward_budget, stake_saturation_point, sybil_resistance: old.sybil_resistance, @@ -250,6 +256,7 @@ mod tests { interval: IntervalRewardParams { reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens) staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M + staking_supply_scale_factor: Percent::hundred(), epoch_reward_budget, stake_saturation_point, sybil_resistance: Percent::from_percentage_value(30).unwrap(), @@ -613,6 +620,7 @@ mod tests { interval: IntervalRewardParams { reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), + staking_supply_scale_factor: Percent::hundred(), epoch_reward_budget, stake_saturation_point, sybil_resistance: Percent::from_percentage_value(30).unwrap(), diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs index 9b0090c014..09223a8e8a 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs @@ -2,15 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::MixnetContractError; -use crate::rewarding::helpers::truncate_decimal; use crate::{Layer, RewardedSetNodeStatus}; -use cosmwasm_std::{Addr, Uint128}; -use cosmwasm_std::{Coin, Decimal}; +use cosmwasm_std::Addr; +use cosmwasm_std::Coin; use schemars::JsonSchema; -use serde::de::Error; -use serde::{Deserialize, Deserializer, Serialize}; -use std::fmt::{self, Display, Formatter}; -use std::ops::{Index, Mul}; +use serde::{Deserialize, Serialize}; +use std::ops::Index; // type aliases for better reasoning about available data pub type IdentityKey = String; @@ -24,87 +21,6 @@ pub type BlockHeight = u64; pub type EpochEventId = u32; pub type IntervalEventId = u32; -/// Percent represents a value between 0 and 100% -/// (i.e. between 0.0 and 1.0) -#[derive( - Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Serialize, Deserialize, JsonSchema, -)] -pub struct Percent(#[serde(deserialize_with = "de_decimal_percent")] Decimal); - -impl Percent { - pub fn new(value: Decimal) -> Result { - if value > Decimal::one() { - Err(MixnetContractError::InvalidPercent) - } else { - Ok(Percent(value)) - } - } - - pub fn is_zero(&self) -> bool { - self.0 == Decimal::zero() - } - - pub fn from_percentage_value(value: u64) -> Result { - Percent::new(Decimal::percent(value)) - } - - pub fn value(&self) -> Decimal { - self.0 - } - - pub fn round_to_integer(&self) -> u8 { - let hundred = Decimal::from_ratio(100u32, 1u32); - // we know the cast from u128 to u8 is a safe one since the internal value must be within 0 - 1 range - truncate_decimal(hundred * self.0).u128() as u8 - } -} - -impl Display for Percent { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let adjusted = Decimal::from_ratio(100u32, 1u32) * self.0; - write!(f, "{}%", adjusted) - } -} - -impl Mul for Percent { - type Output = Decimal; - - fn mul(self, rhs: Decimal) -> Self::Output { - self.0 * rhs - } -} - -impl Mul for Decimal { - type Output = Decimal; - - fn mul(self, rhs: Percent) -> Self::Output { - rhs * self - } -} - -impl Mul for Percent { - type Output = Uint128; - - fn mul(self, rhs: Uint128) -> Self::Output { - self.0 * rhs - } -} - -// implement custom Deserialize because we want to validate Percent has the correct range -fn de_decimal_percent<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let v = Decimal::deserialize(deserializer)?; - if v > Decimal::one() { - Err(D::Error::custom( - "provided decimal percent is larger than 100%", - )) - } else { - Ok(v) - } -} - #[derive(Debug, Default, Serialize, Deserialize, Copy, Clone, Eq, PartialEq)] pub struct LayerDistribution { pub layer1: u64, @@ -210,48 +126,3 @@ pub struct PagedRewardedSetResponse { pub nodes: Vec<(MixId, RewardedSetNodeStatus)>, pub start_next_after: Option, } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn percent_serde() { - let valid_value = Percent::from_percentage_value(80).unwrap(); - let serialized = serde_json::to_string(&valid_value).unwrap(); - - println!("{}", serialized); - let deserialized: Percent = serde_json::from_str(&serialized).unwrap(); - assert_eq!(valid_value, deserialized); - - let invalid_values = vec!["\"42\"", "\"1.1\"", "\"1.00000001\"", "\"foomp\"", "\"1a\""]; - for invalid_value in invalid_values { - assert!(serde_json::from_str::<'_, Percent>(invalid_value).is_err()) - } - assert_eq!( - serde_json::from_str::<'_, Percent>("\"0.95\"").unwrap(), - Percent::from_percentage_value(95).unwrap() - ) - } - - #[test] - fn percent_to_absolute_integer() { - let p = serde_json::from_str::<'_, Percent>("\"0.0001\"").unwrap(); - assert_eq!(p.round_to_integer(), 0); - - let p = serde_json::from_str::<'_, Percent>("\"0.0099\"").unwrap(); - assert_eq!(p.round_to_integer(), 0); - - let p = serde_json::from_str::<'_, Percent>("\"0.0199\"").unwrap(); - assert_eq!(p.round_to_integer(), 1); - - let p = serde_json::from_str::<'_, Percent>("\"0.45123\"").unwrap(); - assert_eq!(p.round_to_integer(), 45); - - let p = serde_json::from_str::<'_, Percent>("\"0.999999999\"").unwrap(); - assert_eq!(p.round_to_integer(), 99); - - let p = serde_json::from_str::<'_, Percent>("\"1.00\"").unwrap(); - assert_eq!(p.round_to_integer(), 100); - } -} diff --git a/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml b/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml index 4bb2c9a123..c2fa50a159 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml +++ b/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml @@ -6,8 +6,10 @@ edition = "2021" [dependencies] cosmwasm-std = "1.0.0" mixnet-contract-common = { path = "../mixnet-contract" } +contracts-common = { path = "../contracts-common" } serde = { version = "1.0", features = ["derive"] } schemars = "0.8" +log = "0.4" ts-rs = {version = "6.1.2", optional = true} [features] diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs index 39376f33f9..20da138074 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs @@ -4,10 +4,13 @@ #![warn(clippy::expect_used)] #![warn(clippy::unwrap_used)] +use contracts_common::Percent; use cosmwasm_std::{Addr, Coin, Timestamp, Uint128}; +use log::warn; use mixnet_contract_common::MixId; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use std::str::FromStr; pub use messages::{ExecuteMsg, InitMsg, MigrateMsg, QueryMsg}; @@ -46,6 +49,46 @@ impl PledgeData { } } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub enum PledgeCap { + Percent(Percent), + Absolute(Uint128), // This has to be in unym +} + +impl FromStr for PledgeCap { + type Err = String; + + fn from_str(cap: &str) -> Result { + let cap = cap.replace('_', "").replace(',', "."); + match Percent::from_str(&cap) { + Ok(p) => { + if p.is_zero() { + warn!("Pledge cap set to 0%, are you sure this is right?") + } + Ok(PledgeCap::Percent(p)) + } + Err(_) => { + match cap.parse::() { + Ok(i) => { + if i < 100_000_000_000 { + warn!("PledgeCap set to less then 100_000 NYM, are you sure this is right?"); + } + Ok(PledgeCap::Absolute(Uint128::from(i))) + } + Err(_e) => Err(format!("Could not parse {} as Percent or Uint128", cap)), + } + } + } + } +} + +impl Default for PledgeCap { + fn default() -> Self { + PledgeCap::Absolute(Uint128::from(100_000_000_000u128)) + } +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct OriginalVestingResponse { pub amount: Coin, @@ -102,3 +145,32 @@ pub struct AllDelegationsResponse { pub delegations: Vec, pub start_next_after: Option<(u32, MixId, u64)>, } + +#[cfg(test)] +mod test { + use contracts_common::Percent; + use cosmwasm_std::Uint128; + use std::str::FromStr; + + use crate::PledgeCap; + + #[test] + fn test_pledge_cap_from_str() { + assert_eq!( + PledgeCap::from_str("0.1").unwrap(), + PledgeCap::Percent(Percent::from_percentage_value(10).unwrap()) + ); + assert_eq!( + PledgeCap::from_str("0,1").unwrap(), + PledgeCap::Percent(Percent::from_percentage_value(10).unwrap()) + ); + assert_eq!( + PledgeCap::from_str("100_000_000_000").unwrap(), + PledgeCap::Absolute(Uint128::new(100_000_000_000)) + ); + assert_eq!( + PledgeCap::from_str("100000000000").unwrap(), + PledgeCap::Absolute(Uint128::new(100_000_000_000)) + ); + } +} diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs index 9a9d21fdae..696df628aa 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Coin, Timestamp, Uint128}; +use cosmwasm_std::{Coin, Timestamp}; use mixnet_contract_common::{ mixnode::{MixNodeConfigUpdate, MixNodeCostParams}, Gateway, MixId, MixNode, @@ -6,6 +6,8 @@ use mixnet_contract_common::{ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use crate::PledgeCap; + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct InitMsg { @@ -83,6 +85,7 @@ pub enum ExecuteMsg { owner_address: String, staking_address: Option, vesting_spec: Option, + cap: Option, }, WithdrawVestedCoins { amount: Coin, @@ -120,7 +123,8 @@ pub enum ExecuteMsg { to_address: Option, }, UpdateLockedPledgeCap { - amount: Uint128, + address: String, + cap: PledgeCap, }, } @@ -156,6 +160,7 @@ impl ExecuteMsg { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum QueryMsg { + GetContractVersion {}, LockedCoins { vesting_account_address: String, block_time: Option, @@ -201,7 +206,6 @@ pub enum QueryMsg { GetCurrentVestingPeriod { address: String, }, - GetLockedPledgeCap {}, GetDelegationTimes { address: String, mix_id: MixId, diff --git a/common/logging/Cargo.toml b/common/logging/Cargo.toml new file mode 100644 index 0000000000..14d2e3a737 --- /dev/null +++ b/common/logging/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "logging" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +log = "0.4.0" +pretty_env_logger = "0.4.0" \ No newline at end of file diff --git a/common/logging/src/lib.rs b/common/logging/src/lib.rs new file mode 100644 index 0000000000..6c026e9b90 --- /dev/null +++ b/common/logging/src/lib.rs @@ -0,0 +1,25 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// I'd argue we should start transitioning from `log` to `tracing` +pub fn setup_logging() { + let mut log_builder = pretty_env_logger::formatted_timed_builder(); + if let Ok(s) = ::std::env::var("RUST_LOG") { + log_builder.parse_filters(&s); + } else { + // default to 'Info' + log_builder.filter(None, log::LevelFilter::Info); + } + + log_builder + .filter_module("hyper", log::LevelFilter::Warn) + .filter_module("tokio_reactor", log::LevelFilter::Warn) + .filter_module("reqwest", log::LevelFilter::Warn) + .filter_module("mio", log::LevelFilter::Warn) + .filter_module("want", log::LevelFilter::Warn) + .filter_module("tungstenite", log::LevelFilter::Warn) + .filter_module("tokio_tungstenite", log::LevelFilter::Warn) + .filter_module("handlebars", log::LevelFilter::Warn) + .filter_module("sled", log::LevelFilter::Warn) + .init(); +} diff --git a/common/mixnode-common/src/packet_processor/processor.rs b/common/mixnode-common/src/packet_processor/processor.rs index 344172dca4..918e001fec 100644 --- a/common/mixnode-common/src/packet_processor/processor.rs +++ b/common/mixnode-common/src/packet_processor/processor.rs @@ -116,7 +116,10 @@ impl SphinxPacketProcessor { trace!("received an ack packet!"); Ok((None, data)) } - PacketSize::RegularPacket | PacketSize::ExtendedPacket => { + PacketSize::RegularPacket + | PacketSize::ExtendedPacket8 + | PacketSize::ExtendedPacket16 + | PacketSize::ExtendedPacket32 => { trace!("received a normal packet!"); let (ack_data, message) = self.split_hop_data_into_ack_and_message(data)?; let (ack_first_hop, ack_packet) = SurbAck::try_recover_first_hop_packet(&ack_data)?; diff --git a/common/nymsphinx/forwarding/src/packet.rs b/common/nymsphinx/forwarding/src/packet.rs index 86cb7871fe..62df1028e3 100644 --- a/common/nymsphinx/forwarding/src/packet.rs +++ b/common/nymsphinx/forwarding/src/packet.rs @@ -25,8 +25,10 @@ impl Display for MixPacketFormattingError { InvalidPacketSize(actual) => write!( f, - "received request had invalid size. (actual: {}, but expected one of: {} (ACK), {} (REGULAR), {} (EXTENDED))", - actual, PacketSize::AckPacket.size(), PacketSize::RegularPacket.size(), PacketSize::ExtendedPacket.size() + "received request had invalid size. (actual: {}, but expected one of: {} (ACK), {} (REGULAR), {}, {}, {} (EXTENDED))", + actual, PacketSize::AckPacket.size(), PacketSize::RegularPacket.size(), + PacketSize::ExtendedPacket8.size(), PacketSize::ExtendedPacket16.size(), + PacketSize::ExtendedPacket32.size() ), MalformedSphinxPacket => write!(f, "received sphinx packet was malformed"), InvalidPacketMode => write!(f, "provided packet mode is invalid") diff --git a/common/nymsphinx/framing/src/codec.rs b/common/nymsphinx/framing/src/codec.rs index f03280091d..53bd77c3b7 100644 --- a/common/nymsphinx/framing/src/codec.rs +++ b/common/nymsphinx/framing/src/codec.rs @@ -6,7 +6,6 @@ use bytes::{Buf, BufMut, BytesMut}; use nymsphinx_params::packet_modes::InvalidPacketMode; use nymsphinx_params::packet_sizes::{InvalidPacketSize, PacketSize}; use nymsphinx_types::SphinxPacket; -use std::convert::TryFrom; use std::io; use tokio_util::codec::{Decoder, Encoder}; @@ -75,7 +74,7 @@ impl Decoder for SphinxCodec { if src.is_empty() { // can't do anything if we have no bytes, but let's reserve enough for the most // conservative case, i.e. receiving an ack packet - src.reserve(Header::SIZE + PacketSize::AckPacket.size()); + src.reserve(Header::LEGACY_SIZE + PacketSize::AckPacket.size()); return Ok(None); } @@ -87,7 +86,7 @@ impl Decoder for SphinxCodec { }; let sphinx_packet_size = header.packet_size.size(); - let frame_len = Header::SIZE + sphinx_packet_size; + let frame_len = header.size() + sphinx_packet_size; if src.len() < frame_len { // we don't have enough bytes to read the rest of frame @@ -96,7 +95,7 @@ impl Decoder for SphinxCodec { } // advance buffer past the header - at this point we have enough bytes - src.advance(Header::SIZE); + src.advance(header.size()); let sphinx_packet_bytes = src.split_to(sphinx_packet_size); let sphinx_packet = match SphinxPacket::from_bytes(&sphinx_packet_bytes) { Ok(sphinx_packet) => sphinx_packet, @@ -115,21 +114,27 @@ impl Decoder for SphinxCodec { // has appropriate capacity in anticipation of future calls to decode. // Failing to do so leads to inefficiency. - // if we have at least one more byte available, we can reserve enough bytes for + // if we have enough bytes to decode the header of the next packet, we can reserve enough bytes for // the entire next frame, if not, we assume the next frame is an ack packet and // reserve for that. + // we also assume the next packet coming from the same client will use exactly the same versioning + // as the current packet + let mut allocate_for_next_packet = header.size() + PacketSize::AckPacket.size(); if !src.is_empty() { - let next_packet_len = match PacketSize::try_from(src[0]) { - Ok(next_packet_len) => next_packet_len, + match Header::decode(src) { + Ok(Some(next_header)) => { + allocate_for_next_packet = next_header.size() + next_header.packet_size.size(); + } + Ok(None) => { + // we don't have enough information to know how much to reserve, fallback to the ack case + } + // the next frame will be malformed but let's leave handling the error to the next // call to 'decode', as presumably, the current sphinx packet is still valid Err(_) => return Ok(Some(nymsphinx_packet)), }; - let next_frame_len = next_packet_len.size() + Header::SIZE; - src.reserve(next_frame_len - 1); - } else { - src.reserve(Header::SIZE + PacketSize::AckPacket.size()); } + src.reserve(allocate_for_next_packet); Ok(Some(nymsphinx_packet)) } @@ -199,6 +204,8 @@ mod packet_encoding { #[cfg(test)] mod decode_will_allocate_enough_bytes_for_next_call { use super::*; + use nymsphinx_params::packet_version::PacketVersion; + use nymsphinx_params::PacketMode; #[test] fn for_empty_bytes() { @@ -207,20 +214,23 @@ mod packet_encoding { assert!(SphinxCodec.decode(&mut empty_bytes).unwrap().is_none()); assert_eq!( empty_bytes.capacity(), - Header::SIZE + PacketSize::AckPacket.size() + Header::LEGACY_SIZE + PacketSize::AckPacket.size() ); } #[test] - fn for_bytes_with_header() { + fn for_bytes_with_legacy_header() { // if header gets decoded there should be enough bytes for the entire frame let packet_sizes = vec![ PacketSize::AckPacket, PacketSize::RegularPacket, - PacketSize::ExtendedPacket, + PacketSize::ExtendedPacket8, + PacketSize::ExtendedPacket16, + PacketSize::ExtendedPacket32, ]; for packet_size in packet_sizes { let header = Header { + packet_version: PacketVersion::Legacy, packet_size, packet_mode: Default::default(), }; @@ -228,12 +238,60 @@ mod packet_encoding { header.encode(&mut bytes); assert!(SphinxCodec.decode(&mut bytes).unwrap().is_none()); - assert_eq!(bytes.capacity(), Header::SIZE + packet_size.size()) + assert_eq!(bytes.capacity(), Header::LEGACY_SIZE + packet_size.size()) } } #[test] - fn for_full_frame() { + fn for_bytes_with_versioned_header() { + // if header gets decoded there should be enough bytes for the entire frame + let packet_sizes = vec![ + PacketSize::AckPacket, + PacketSize::RegularPacket, + PacketSize::ExtendedPacket8, + PacketSize::ExtendedPacket16, + PacketSize::ExtendedPacket32, + ]; + for packet_size in packet_sizes { + let header = Header { + packet_version: PacketVersion::Versioned(123), + packet_size, + packet_mode: Default::default(), + }; + let mut bytes = BytesMut::new(); + header.encode(&mut bytes); + assert!(SphinxCodec.decode(&mut bytes).unwrap().is_none()); + + assert_eq!( + bytes.capacity(), + Header::VERSIONED_SIZE + packet_size.size() + ) + } + } + + #[test] + fn for_full_frame_with_legacy_header() { + // if full frame is used exactly, there should be enough space for header + ack packet + let packet = FramedSphinxPacket { + header: Header { + packet_version: PacketVersion::Legacy, + packet_size: Default::default(), + packet_mode: Default::default(), + }, + packet: make_valid_sphinx_packet(Default::default()), + }; + + let mut bytes = BytesMut::new(); + SphinxCodec.encode(packet, &mut bytes).unwrap(); + assert!(SphinxCodec.decode(&mut bytes).unwrap().is_some()); + assert_eq!( + bytes.capacity(), + Header::LEGACY_SIZE + PacketSize::AckPacket.size() + ); + } + + #[test] + fn for_full_frame_with_versioned_header() { // if full frame is used exactly, there should be enough space for header + ack packet let packet = FramedSphinxPacket { header: Header::default(), @@ -245,17 +303,50 @@ mod packet_encoding { assert!(SphinxCodec.decode(&mut bytes).unwrap().is_some()); assert_eq!( bytes.capacity(), - Header::SIZE + PacketSize::AckPacket.size() + Header::VERSIONED_SIZE + PacketSize::AckPacket.size() ); } #[test] - fn for_full_frame_with_extra_byte() { - // if there was at least 1 byte left, there should be enough space for entire next frame + fn for_full_frame_with_extra_bytes_with_legacy_header() { + // if there was at least 2 byte left, there should be enough space for entire next frame let packet_sizes = vec![ PacketSize::AckPacket, PacketSize::RegularPacket, - PacketSize::ExtendedPacket, + PacketSize::ExtendedPacket8, + PacketSize::ExtendedPacket16, + PacketSize::ExtendedPacket32, + ]; + + for packet_size in packet_sizes { + let first_packet = FramedSphinxPacket { + header: Header { + packet_version: PacketVersion::Legacy, + packet_size: Default::default(), + packet_mode: Default::default(), + }, + packet: make_valid_sphinx_packet(Default::default()), + }; + + let mut bytes = BytesMut::new(); + SphinxCodec.encode(first_packet, &mut bytes).unwrap(); + bytes.put_u8(packet_size as u8); + bytes.put_u8(PacketMode::default() as u8); + assert!(SphinxCodec.decode(&mut bytes).unwrap().is_some()); + + assert!(bytes.capacity() >= Header::LEGACY_SIZE + packet_size.size()) + } + } + + #[test] + fn for_full_frame_with_extra_bytes_with_versioned_header() { + // if there was at least 3 byte left, there should be enough space for entire next frame + let packet_sizes = vec![ + PacketSize::AckPacket, + PacketSize::RegularPacket, + PacketSize::ExtendedPacket8, + PacketSize::ExtendedPacket16, + PacketSize::ExtendedPacket32, ]; for packet_size in packet_sizes { @@ -266,10 +357,12 @@ mod packet_encoding { let mut bytes = BytesMut::new(); SphinxCodec.encode(first_packet, &mut bytes).unwrap(); + bytes.put_u8(PacketVersion::new_versioned(123).as_u8().unwrap()); bytes.put_u8(packet_size as u8); + bytes.put_u8(PacketMode::default() as u8); assert!(SphinxCodec.decode(&mut bytes).unwrap().is_some()); - assert!(bytes.capacity() >= Header::SIZE + packet_size.size()) + assert!(bytes.capacity() >= Header::VERSIONED_SIZE + packet_size.size()) } } } diff --git a/common/nymsphinx/framing/src/packet.rs b/common/nymsphinx/framing/src/packet.rs index 7c1881f318..997f2e6c35 100644 --- a/common/nymsphinx/framing/src/packet.rs +++ b/common/nymsphinx/framing/src/packet.rs @@ -4,6 +4,7 @@ use crate::codec::SphinxCodecError; use bytes::{BufMut, BytesMut}; use nymsphinx_params::packet_sizes::PacketSize; +use nymsphinx_params::packet_version::PacketVersion; use nymsphinx_params::PacketMode; use nymsphinx_types::SphinxPacket; use std::convert::TryFrom; @@ -17,12 +18,14 @@ pub struct FramedSphinxPacket { } impl FramedSphinxPacket { - pub fn new(packet: SphinxPacket, packet_mode: PacketMode) -> Self { + pub fn new(packet: SphinxPacket, packet_mode: PacketMode, use_legacy_version: bool) -> Self { // If this fails somebody is using the library in a super incorrect way, because they // already managed to somehow create a sphinx packet let packet_size = PacketSize::get_type(packet.len()).unwrap(); + FramedSphinxPacket { header: Header { + packet_version: PacketVersion::new(use_legacy_version), packet_size, packet_mode, }, @@ -48,6 +51,9 @@ impl FramedSphinxPacket { // but would that really be worth it? #[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] pub struct Header { + /// Represents the wire format version used to construct this packet. + pub(crate) packet_version: PacketVersion, + /// Represents type and consequently size of the included SphinxPacket. pub(crate) packet_size: PacketSize, @@ -64,11 +70,25 @@ pub struct Header { } impl Header { - pub(crate) const SIZE: usize = 2; + pub(crate) const LEGACY_SIZE: usize = 2; + pub(crate) const VERSIONED_SIZE: usize = 3; + + pub(crate) fn size(&self) -> usize { + if self.packet_version.is_legacy() { + Self::LEGACY_SIZE + } else { + Self::VERSIONED_SIZE + } + } pub(crate) fn encode(&self, dst: &mut BytesMut) { // we reserve one byte for `packet_size` and the other for `mode` - dst.reserve(Self::SIZE); + dst.reserve(Self::LEGACY_SIZE); + if let Some(version) = self.packet_version.as_u8() { + dst.reserve(Self::VERSIONED_SIZE); + dst.put_u8(version) + } + dst.put_u8(self.packet_size as u8); dst.put_u8(self.packet_mode as u8); // reserve bytes for the actual packet @@ -76,16 +96,30 @@ impl Header { } pub(crate) fn decode(src: &mut BytesMut) -> Result, SphinxCodecError> { - if src.len() < Self::SIZE { + if src.len() < Self::LEGACY_SIZE { // can't do anything if we don't have enough bytes - but reserve enough for the next call - src.reserve(Self::SIZE); + src.reserve(Self::LEGACY_SIZE); return Ok(None); } - Ok(Some(Header { - packet_size: PacketSize::try_from(src[0])?, - packet_mode: PacketMode::try_from(src[1])?, - })) + let packet_version = PacketVersion::from(src[0]); + if packet_version.is_legacy() { + Ok(Some(Header { + packet_version, + packet_size: PacketSize::try_from(src[0])?, + packet_mode: PacketMode::try_from(src[1])?, + })) + } else if src.len() < Self::VERSIONED_SIZE { + // we're missing that 1 byte to read the full header... + src.reserve(Self::VERSIONED_SIZE); + Ok(None) + } else { + Ok(Some(Header { + packet_version, + packet_size: PacketSize::try_from(src[1])?, + packet_mode: PacketMode::try_from(src[2])?, + })) + } } } @@ -108,7 +142,16 @@ mod header_encoding { // make sure this is still 'unknown' for if we make changes in the future assert!(PacketSize::try_from(unknown_packet_size).is_err()); - let mut bytes = BytesMut::from([unknown_packet_size, PacketMode::default() as u8].as_ref()); + // unfortunately this will only work for the 'versioned' variant + // due to the hack used to get legacy mode compatibility + let mut bytes = BytesMut::from( + [ + PacketVersion::new_versioned(123).as_u8().unwrap(), + unknown_packet_size, + PacketMode::default() as u8, + ] + .as_ref(), + ); assert!(Header::decode(&mut bytes).is_err()) } @@ -127,23 +170,47 @@ mod header_encoding { let mut empty_bytes = BytesMut::new(); let decode_attempt_1 = Header::decode(&mut empty_bytes).unwrap(); assert!(decode_attempt_1.is_none()); - assert!(empty_bytes.capacity() > Header::SIZE); + assert!(empty_bytes.capacity() > Header::LEGACY_SIZE); let mut empty_bytes = BytesMut::with_capacity(1); let decode_attempt_2 = Header::decode(&mut empty_bytes).unwrap(); assert!(decode_attempt_2.is_none()); - assert!(empty_bytes.capacity() > Header::SIZE); + assert!(empty_bytes.capacity() > Header::LEGACY_SIZE); } #[test] - fn header_encoding_reserves_enough_bytes_for_full_sphinx_packet() { + fn header_encoding_reserves_enough_bytes_for_full_sphinx_packet_in_legacy_mode() { let packet_sizes = vec![ PacketSize::AckPacket, PacketSize::RegularPacket, - PacketSize::ExtendedPacket, + PacketSize::ExtendedPacket8, + PacketSize::ExtendedPacket16, + PacketSize::ExtendedPacket32, ]; for packet_size in packet_sizes { let header = Header { + packet_version: PacketVersion::Legacy, + packet_size, + packet_mode: Default::default(), + }; + let mut bytes = BytesMut::new(); + header.encode(&mut bytes); + assert_eq!(bytes.capacity(), bytes.len() + packet_size.size()) + } + } + + #[test] + fn header_encoding_reserves_enough_bytes_for_full_sphinx_packet_in_versioned_mode() { + let packet_sizes = vec![ + PacketSize::AckPacket, + PacketSize::RegularPacket, + PacketSize::ExtendedPacket8, + PacketSize::ExtendedPacket16, + PacketSize::ExtendedPacket32, + ]; + for packet_size in packet_sizes { + let header = Header { + packet_version: PacketVersion::Versioned(123), packet_size, packet_mode: Default::default(), }; diff --git a/common/nymsphinx/params/src/lib.rs b/common/nymsphinx/params/src/lib.rs index fee4853e96..d954163861 100644 --- a/common/nymsphinx/params/src/lib.rs +++ b/common/nymsphinx/params/src/lib.rs @@ -13,6 +13,7 @@ pub use packet_sizes::PacketSize; pub mod packet_modes; pub mod packet_sizes; +pub mod packet_version; // If somebody can provide an argument why it might be reasonable to have more than 255 mix hops, // I will change this to [`usize`] @@ -24,6 +25,17 @@ pub const DEFAULT_NUM_MIX_HOPS: u8 = 3; pub const FRAG_ID_LEN: usize = 5; pub type SerializedFragmentIdentifier = [u8; FRAG_ID_LEN]; +// wait, wait, but why are we starting with version 7? +// when packet header gets serialized, the following bytes (in that order) are put onto the wire: +// - packet_version (starting with v1.1.0) +// - packet_size indicator +// - packet_mode +// it also just so happens that the only valid values for packet_size indicator include values 1-6 +// therefore if we receive byte `7` (or larger than that) we'll know we received a versioned packet, +// otherwise we should treat it as legacy +/// Increment it whenever we perform any breaking change in the wire format! +const CURRENT_PACKET_VERSION_NUMBER: u8 = 7; + // TODO: ask @AP about the choice of below algorithms /// Hashing algorithm used during hkdf for ephemeral shared key generation per sphinx packet payload. diff --git a/common/nymsphinx/params/src/packet_sizes.rs b/common/nymsphinx/params/src/packet_sizes.rs index c7fcd43e04..aa573ffe11 100644 --- a/common/nymsphinx/params/src/packet_sizes.rs +++ b/common/nymsphinx/params/src/packet_sizes.rs @@ -5,6 +5,7 @@ use crate::FRAG_ID_LEN; use nymsphinx_types::header::HEADER_SIZE; use nymsphinx_types::PAYLOAD_OVERHEAD_SIZE; use std::convert::TryFrom; +use std::str::FromStr; // it's up to the smart people to figure those values out : ) const REGULAR_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 2 * 1024; @@ -15,11 +16,16 @@ const REGULAR_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 2 * 102 const ACK_IV_SIZE: usize = 16; const ACK_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + ACK_IV_SIZE + FRAG_ID_LEN; -const EXTENDED_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 32 * 1024; +const EXTENDED_PACKET_SIZE_8: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 8 * 1024; +const EXTENDED_PACKET_SIZE_16: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 16 * 1024; +const EXTENDED_PACKET_SIZE_32: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 32 * 1024; #[derive(Debug)] pub struct InvalidPacketSize; +#[derive(Debug)] +pub struct InvalidExtendedPacketSize; + #[repr(u8)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum PacketSize { @@ -30,7 +36,28 @@ pub enum PacketSize { AckPacket = 2, // for example for streaming fast and furious in uncompressed 10bit 4K HDR quality - ExtendedPacket = 3, + ExtendedPacket32 = 3, + + // for example for streaming fast and furious in heavily compressed lossy RealPlayer quality + ExtendedPacket8 = 4, + + // for example for streaming fast and furious in compressed XviD quality + ExtendedPacket16 = 5, +} + +impl FromStr for PacketSize { + type Err = InvalidPacketSize; + + fn from_str(s: &str) -> Result { + match s { + "regular" => Ok(Self::RegularPacket), + "ack" => Ok(Self::AckPacket), + "extended8" => Ok(Self::ExtendedPacket8), + "extended16" => Ok(Self::ExtendedPacket16), + "extended32" => Ok(Self::ExtendedPacket32), + _ => Err(InvalidPacketSize), + } + } } impl TryFrom for PacketSize { @@ -40,7 +67,9 @@ impl TryFrom for PacketSize { match value { _ if value == (PacketSize::RegularPacket as u8) => Ok(Self::RegularPacket), _ if value == (PacketSize::AckPacket as u8) => Ok(Self::AckPacket), - _ if value == (PacketSize::ExtendedPacket as u8) => Ok(Self::ExtendedPacket), + _ if value == (PacketSize::ExtendedPacket8 as u8) => Ok(Self::ExtendedPacket8), + _ if value == (PacketSize::ExtendedPacket16 as u8) => Ok(Self::ExtendedPacket16), + _ if value == (PacketSize::ExtendedPacket32 as u8) => Ok(Self::ExtendedPacket32), _ => Err(InvalidPacketSize), } } @@ -51,7 +80,9 @@ impl PacketSize { match self { PacketSize::RegularPacket => REGULAR_PACKET_SIZE, PacketSize::AckPacket => ACK_PACKET_SIZE, - PacketSize::ExtendedPacket => EXTENDED_PACKET_SIZE, + PacketSize::ExtendedPacket8 => EXTENDED_PACKET_SIZE_8, + PacketSize::ExtendedPacket16 => EXTENDED_PACKET_SIZE_16, + PacketSize::ExtendedPacket32 => EXTENDED_PACKET_SIZE_32, } } @@ -68,12 +99,33 @@ impl PacketSize { Ok(PacketSize::RegularPacket) } else if PacketSize::AckPacket.size() == size { Ok(PacketSize::AckPacket) - } else if PacketSize::ExtendedPacket.size() == size { - Ok(PacketSize::ExtendedPacket) + } else if PacketSize::ExtendedPacket8.size() == size { + Ok(PacketSize::ExtendedPacket8) + } else if PacketSize::ExtendedPacket16.size() == size { + Ok(PacketSize::ExtendedPacket16) + } else if PacketSize::ExtendedPacket32.size() == size { + Ok(PacketSize::ExtendedPacket32) } else { Err(InvalidPacketSize) } } + + pub fn is_extended_size(&self) -> bool { + match self { + PacketSize::RegularPacket | PacketSize::AckPacket => false, + PacketSize::ExtendedPacket8 + | PacketSize::ExtendedPacket16 + | PacketSize::ExtendedPacket32 => true, + } + } + + pub fn as_extended_size(self) -> Option { + if self.is_extended_size() { + Some(self) + } else { + None + } + } } impl Default for PacketSize { diff --git a/common/nymsphinx/params/src/packet_version.rs b/common/nymsphinx/params/src/packet_version.rs new file mode 100644 index 0000000000..001f46c84d --- /dev/null +++ b/common/nymsphinx/params/src/packet_version.rs @@ -0,0 +1,59 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{PacketSize, CURRENT_PACKET_VERSION_NUMBER}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PacketVersion { + // this will allow updated mixnodes to still understand packets from before the update + Legacy, + Versioned(u8), +} + +impl PacketVersion { + pub fn new(use_legacy: bool) -> Self { + if use_legacy { + Self::new_legacy() + } else { + Self::new_versioned(CURRENT_PACKET_VERSION_NUMBER) + } + } + + pub fn new_legacy() -> Self { + PacketVersion::Legacy + } + + pub fn new_versioned(version: u8) -> Self { + PacketVersion::Versioned(version) + } + + pub fn is_legacy(&self) -> bool { + matches!(self, PacketVersion::Legacy) + } + + pub fn as_u8(&self) -> Option { + match self { + PacketVersion::Legacy => None, + PacketVersion::Versioned(version) => Some(*version), + } + } +} + +impl From for PacketVersion { + fn from(v: u8) -> Self { + match v { + n if n == PacketSize::RegularPacket as u8 => PacketVersion::Legacy, + n if n == PacketSize::AckPacket as u8 => PacketVersion::Legacy, + n if n == PacketSize::ExtendedPacket8 as u8 => PacketVersion::Legacy, + n if n == PacketSize::ExtendedPacket16 as u8 => PacketVersion::Legacy, + n if n == PacketSize::ExtendedPacket32 as u8 => PacketVersion::Legacy, + n => PacketVersion::Versioned(n), + } + } +} + +impl Default for PacketVersion { + fn default() -> Self { + PacketVersion::Versioned(CURRENT_PACKET_VERSION_NUMBER) + } +} diff --git a/common/types/src/delegation.rs b/common/types/src/delegation.rs index 8538d0559b..bee74dd646 100644 --- a/common/types/src/delegation.rs +++ b/common/types/src/delegation.rs @@ -62,6 +62,7 @@ pub struct DelegationWithEverything { // DEPRECATED, IF POSSIBLE TRY TO DISCONTINUE USE OF IT! pub pending_events: Vec, + pub mixnode_is_unbonding: Option, } #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md index 0050b844a1..361ceec70e 100644 --- a/contracts/CHANGELOG.md +++ b/contracts/CHANGELOG.md @@ -1,3 +1,11 @@ +## Unreleased + +### Added +- vesting-contract: added query for obtaining contract build information ([#1726]) + +[#1726]: https://github.com/nymtech/nym/pull/1726 + + ## [nym-contracts-v1.0.2](https://github.com/nymtech/nym/tree/nym-contracts-v1.0.2) (2022-09-13) ### Added diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 3490bbf2fd..706a7c5e36 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -221,7 +221,9 @@ name = "contracts-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "schemars", "serde", + "thiserror", ] [[package]] @@ -1606,12 +1608,14 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "vesting-contract" version = "1.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", "cw-storage-plus", "mixnet-contract-common", "schemars", "serde", "thiserror", + "vergen", "vesting-contract-common", ] @@ -1619,7 +1623,9 @@ dependencies = [ name = "vesting-contract-common" version = "0.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", + "log", "mixnet-contract-common", "schemars", "serde", diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index f9ce24a1e2..1951c535de 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -497,6 +497,7 @@ mod tests { initial_rewarding_params: InitialRewardingParams { initial_reward_pool: Decimal::from_atomics(100_000_000_000_000u128, 0).unwrap(), initial_staking_supply: Decimal::from_atomics(123_456_000_000_000u128, 0).unwrap(), + staking_supply_scale_factor: Percent::hundred(), sybil_resistance: Percent::from_percentage_value(23).unwrap(), active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), interval_pool_emission: Percent::from_percentage_value(1).unwrap(), @@ -535,6 +536,7 @@ mod tests { interval: IntervalRewardParams { reward_pool: Decimal::from_atomics(100_000_000_000_000u128, 0).unwrap(), staking_supply: Decimal::from_atomics(123_456_000_000_000u128, 0).unwrap(), + staking_supply_scale_factor: Percent::hundred(), epoch_reward_budget: expected_epoch_reward_budget, stake_saturation_point: expected_stake_saturation_point, sybil_resistance: Percent::from_percentage_value(23).unwrap(), diff --git a/contracts/mixnet/src/interval/pending_events.rs b/contracts/mixnet/src/interval/pending_events.rs index 8126168e71..8cb09566f2 100644 --- a/contracts/mixnet/src/interval/pending_events.rs +++ b/contracts/mixnet/src/interval/pending_events.rs @@ -1220,6 +1220,7 @@ mod tests { let update = IntervalRewardingParamsUpdate { reward_pool: Some(before.interval.reward_pool / two), staking_supply: Some(before.interval.staking_supply * four), + staking_supply_scale_factor: None, sybil_resistance_percent: Some(Percent::from_percentage_value(42).unwrap()), active_set_work_factor: None, interval_pool_emission: None, diff --git a/contracts/mixnet/src/rewards/helpers.rs b/contracts/mixnet/src/rewards/helpers.rs index 752b2714a6..91b8fbdeb0 100644 --- a/contracts/mixnet/src/rewards/helpers.rs +++ b/contracts/mixnet/src/rewards/helpers.rs @@ -21,7 +21,8 @@ pub(crate) fn apply_reward_pool_changes( let reward_pool = rewarding_params.interval.reward_pool - pending_pool_change.removed + pending_pool_change.added; - let staking_supply = rewarding_params.interval.staking_supply + pending_pool_change.removed; + let staking_supply = rewarding_params.interval.staking_supply + + rewarding_params.interval.staking_supply_scale_factor * pending_pool_change.removed; let epoch_reward_budget = reward_pool / interval.epochs_in_interval().into_base_decimal()? * rewarding_params.interval.interval_pool_emission; let stake_saturation_point = diff --git a/contracts/mixnet/src/rewards/transactions.rs b/contracts/mixnet/src/rewards/transactions.rs index 2099fcd629..e25e4c1bd8 100644 --- a/contracts/mixnet/src/rewards/transactions.rs +++ b/contracts/mixnet/src/rewards/transactions.rs @@ -1718,6 +1718,7 @@ pub mod tests { let update = IntervalRewardingParamsUpdate { reward_pool: None, staking_supply: None, + staking_supply_scale_factor: None, sybil_resistance_percent: None, active_set_work_factor: None, interval_pool_emission: None, @@ -1750,6 +1751,7 @@ pub mod tests { let empty_update = IntervalRewardingParamsUpdate { reward_pool: None, staking_supply: None, + staking_supply_scale_factor: None, sybil_resistance_percent: None, active_set_work_factor: None, interval_pool_emission: None, @@ -1769,6 +1771,7 @@ pub mod tests { let update = IntervalRewardingParamsUpdate { reward_pool: None, staking_supply: None, + staking_supply_scale_factor: None, sybil_resistance_percent: None, active_set_work_factor: None, interval_pool_emission: None, @@ -1803,6 +1806,7 @@ pub mod tests { let update = IntervalRewardingParamsUpdate { reward_pool: None, staking_supply: None, + staking_supply_scale_factor: None, sybil_resistance_percent: None, active_set_work_factor: None, interval_pool_emission: None, @@ -1827,6 +1831,7 @@ pub mod tests { let update = IntervalRewardingParamsUpdate { reward_pool: None, staking_supply: None, + staking_supply_scale_factor: None, sybil_resistance_percent: None, active_set_work_factor: None, interval_pool_emission: None, @@ -1867,6 +1872,7 @@ pub mod tests { let update = IntervalRewardingParamsUpdate { reward_pool: Some(old.interval.reward_pool / two), staking_supply: Some(old.interval.staking_supply * four), + staking_supply_scale_factor: None, sybil_resistance_percent: None, active_set_work_factor: None, interval_pool_emission: None, diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index 07b89671cd..a7cf4a3875 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -768,6 +768,7 @@ pub mod test_helpers { InitialRewardingParams { initial_reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens) initial_staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M + staking_supply_scale_factor: Percent::hundred(), sybil_resistance: Percent::from_percentage_value(30).unwrap(), active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), interval_pool_emission: Percent::from_percentage_value(2).unwrap(), diff --git a/contracts/vesting/Cargo.toml b/contracts/vesting/Cargo.toml index 3c03d92c91..e13491a4f6 100644 --- a/contracts/vesting/Cargo.toml +++ b/contracts/vesting/Cargo.toml @@ -15,6 +15,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" } +contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" } vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract" } cosmwasm-std = { version = "1.0.0 "} @@ -22,4 +23,7 @@ cw-storage-plus = { version = "0.13.4", features = ["iterator"] } schemars = "0.8" serde = { version = "1.0", default-features = false, features = ["derive"] } -thiserror = { version = "1.0" } \ No newline at end of file +thiserror = { version = "1.0" } + +[build-dependencies] +vergen = { version = "5", default-features = false, features = ["build", "git", "rustc"] } diff --git a/contracts/vesting/build.rs b/contracts/vesting/build.rs new file mode 100644 index 0000000000..01b3a20dc6 --- /dev/null +++ b/contracts/vesting/build.rs @@ -0,0 +1,8 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use vergen::{vergen, Config}; + +fn main() { + vergen(Config::default()).expect("failed to extract build metadata") +} diff --git a/contracts/vesting/src/contract.rs b/contracts/vesting/src/contract.rs index 27ea0eef6a..71829c76cf 100644 --- a/contracts/vesting/src/contract.rs +++ b/contracts/vesting/src/contract.rs @@ -1,13 +1,14 @@ use crate::errors::ContractError; use crate::queued_migrations::migrate_to_v2_mixnet_contract; use crate::storage::{ - account_from_address, locked_pledge_cap, update_locked_pledge_cap, BlockTimestampSecs, ADMIN, - DELEGATIONS, MIXNET_CONTRACT_ADDRESS, MIX_DENOM, + account_from_address, BlockTimestampSecs, ADMIN, DELEGATIONS, MIXNET_CONTRACT_ADDRESS, + MIX_DENOM, }; use crate::traits::{ DelegatingAccount, GatewayBondingAccount, MixnodeBondingAccount, VestingAccount, }; use crate::vesting::{populate_vesting_periods, Account}; +use contracts_common::ContractBuildInformation; use cosmwasm_std::{ coin, entry_point, to_binary, BankMsg, Coin, Deps, DepsMut, Env, MessageInfo, Order, QueryResponse, Response, StdResult, Timestamp, Uint128, @@ -25,8 +26,8 @@ use vesting_contract_common::messages::{ ExecuteMsg, InitMsg, MigrateMsg, QueryMsg, VestingSpecification, }; use vesting_contract_common::{ - AllDelegationsResponse, DelegationTimesResponse, OriginalVestingResponse, Period, PledgeData, - VestingDelegation, + AllDelegationsResponse, DelegationTimesResponse, OriginalVestingResponse, Period, PledgeCap, + PledgeData, VestingDelegation, }; pub const INITIAL_LOCKED_PLEDGE_CAP: Uint128 = Uint128::new(100_000_000_000); @@ -59,8 +60,8 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { - ExecuteMsg::UpdateLockedPledgeCap { amount } => { - try_update_locked_pledge_cap(amount, info, deps) + ExecuteMsg::UpdateLockedPledgeCap { address, cap } => { + try_update_locked_pledge_cap(address, cap, info, deps) } ExecuteMsg::TrackReward { amount, address } => { try_track_reward(deps, info, amount, &address) @@ -88,10 +89,12 @@ pub fn execute( owner_address, staking_address, vesting_spec, + cap, } => try_create_periodic_vesting_account( &owner_address, staking_address, vesting_spec, + cap, info, env, deps, @@ -144,14 +147,18 @@ pub fn execute( /// /// Callable by ADMIN only, see [instantiate]. pub fn try_update_locked_pledge_cap( - amount: Uint128, + address: String, + cap: PledgeCap, info: MessageInfo, deps: DepsMut, ) -> Result { if info.sender != ADMIN.load(deps.storage)? { return Err(ContractError::NotAdmin(info.sender.as_str().to_string())); } - update_locked_pledge_cap(amount, deps.storage)?; + let mut account = account_from_address(&address, deps.storage, deps.api)?; + + account.pledge_cap = Some(cap); + // update_locked_pledge_cap(amount, deps.storage)?; Ok(Response::default()) } @@ -430,6 +437,7 @@ fn try_create_periodic_vesting_account( owner_address: &str, staking_address: Option, vesting_spec: Option, + cap: Option, info: MessageInfo, env: Env, deps: DepsMut<'_>, @@ -437,6 +445,7 @@ fn try_create_periodic_vesting_account( if info.sender != ADMIN.load(deps.storage)? { return Err(ContractError::NotAdmin(info.sender.as_str().to_string())); } + let mix_denom = MIX_DENOM.load(deps.storage)?; let account_exists = account_from_address(owner_address, deps.storage, deps.api).is_ok(); @@ -452,6 +461,11 @@ fn try_create_periodic_vesting_account( let owner_address = deps.api.addr_validate(owner_address)?; let staking_address = if let Some(staking_address) = staking_address { + let staking_account_exists = + account_from_address(&staking_address, deps.storage, deps.api).is_ok(); + if staking_account_exists { + return Err(ContractError::StakingAccountAlreadyExists(staking_address)); + } Some(deps.api.addr_validate(&staking_address)?) } else { None @@ -472,6 +486,7 @@ fn try_create_periodic_vesting_account( coin.clone(), start_time, periods, + cap, deps.storage, )?; @@ -486,7 +501,7 @@ fn try_create_periodic_vesting_account( #[entry_point] pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result { let query_res = match msg { - QueryMsg::GetLockedPledgeCap {} => to_binary(&get_locked_pledge_cap(deps)), + QueryMsg::GetContractVersion {} => to_binary(&get_contract_version()), QueryMsg::LockedCoins { vesting_account_address, block_time, @@ -567,11 +582,6 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result) -> Uint128 { - locked_pledge_cap(deps.storage) -} - /// Get current vesting period for a given [crate::vesting::Account]. pub fn try_get_current_vesting_period( address: &str, @@ -598,6 +608,21 @@ pub fn try_get_account(address: &str, deps: Deps<'_>) -> Result ContractBuildInformation { + // as per docs + // env! macro will expand to the value of the named environment variable at + // compile time, yielding an expression of type `&'static str` + ContractBuildInformation { + build_timestamp: env!("VERGEN_BUILD_TIMESTAMP").to_string(), + build_version: env!("VERGEN_BUILD_SEMVER").to_string(), + commit_sha: env!("VERGEN_GIT_SHA").to_string(), + commit_timestamp: env!("VERGEN_GIT_COMMIT_TIMESTAMP").to_string(), + commit_branch: env!("VERGEN_GIT_BRANCH").to_string(), + rustc_version: env!("VERGEN_RUSTC_SEMVER").to_string(), + } +} + /// Gets currently locked coins, see [crate::traits::VestingAccount::locked_coins] pub fn try_get_locked_coins( vesting_account_address: &str, diff --git a/contracts/vesting/src/errors.rs b/contracts/vesting/src/errors.rs index 638a654bc8..5244f4330d 100644 --- a/contracts/vesting/src/errors.rs +++ b/contracts/vesting/src/errors.rs @@ -44,6 +44,8 @@ pub enum ContractError { InvalidAddress(String), #[error("VESTING ({}): Account already exists: {0}", line!())] AccountAlreadyExists(String), + #[error("VESTING ({}): Staking account already exists: {0}", line!())] + StakingAccountAlreadyExists(String), #[error("VESTING ({}): Too few coins sent for vesting account creation, sent {sent}, need at least {need}", line!())] MinVestingFunds { sent: u128, need: u128 }, #[error("VESTING ({}): Maximum amount of locked coins has already been pledged: {current}, cap is {cap}", line!())] diff --git a/contracts/vesting/src/storage.rs b/contracts/vesting/src/storage.rs index eda3f93d72..a1216fa87e 100644 --- a/contracts/vesting/src/storage.rs +++ b/contracts/vesting/src/storage.rs @@ -1,5 +1,5 @@ +use crate::errors::ContractError; use crate::vesting::Account; -use crate::{contract::INITIAL_LOCKED_PLEDGE_CAP, errors::ContractError}; use cosmwasm_std::{Addr, Api, Storage, Uint128}; use cw_storage_plus::{Item, Map}; use mixnet_contract_common::{IdentityKey, MixId}; @@ -20,21 +20,6 @@ pub const DELEGATIONS: Map<'_, (u32, MixId, BlockTimestampSecs), Uint128> = Map: pub const ADMIN: Item<'_, String> = Item::new("adm"); pub const MIXNET_CONTRACT_ADDRESS: Item<'_, String> = Item::new("mix"); pub const MIX_DENOM: Item<'_, String> = Item::new("den"); -pub const LOCKED_PLEDGE_CAP: Item<'_, Uint128> = Item::new("lck"); - -pub fn locked_pledge_cap(storage: &dyn Storage) -> Uint128 { - LOCKED_PLEDGE_CAP - .load(storage) - .unwrap_or(INITIAL_LOCKED_PLEDGE_CAP) -} - -pub fn update_locked_pledge_cap( - amount: Uint128, - storage: &mut dyn Storage, -) -> Result<(), ContractError> { - LOCKED_PLEDGE_CAP.save(storage, &amount)?; - Ok(()) -} pub fn save_delegation( key: (u32, MixId, BlockTimestampSecs), diff --git a/contracts/vesting/src/support/tests.rs b/contracts/vesting/src/support/tests.rs index 8084e48739..8f24359f52 100644 --- a/contracts/vesting/src/support/tests.rs +++ b/contracts/vesting/src/support/tests.rs @@ -2,9 +2,12 @@ pub mod helpers { use crate::contract::instantiate; use crate::vesting::{populate_vesting_periods, Account}; + use contracts_common::Percent; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier}; use cosmwasm_std::{Addr, Coin, Empty, Env, MemoryStorage, OwnedDeps, Storage, Uint128}; + use std::str::FromStr; use vesting_contract_common::messages::{InitMsg, VestingSpecification}; + use vesting_contract_common::PledgeCap; pub const TEST_COIN_DENOM: &str = "unym"; @@ -37,6 +40,7 @@ pub mod helpers { }, start_time_ts, periods, + None, storage, ) .unwrap() @@ -56,6 +60,29 @@ pub mod helpers { }, start_time, periods, + Some(PledgeCap::from_str("0.1").unwrap()), + storage, + ) + .unwrap() + } + + pub fn vesting_account_percent_fixture(storage: &mut dyn Storage, env: &Env) -> Account { + let start_time = env.block.time; + let periods = + populate_vesting_periods(start_time.seconds(), VestingSpecification::default()); + + Account::new( + Addr::unchecked("owner"), + Some(Addr::unchecked("staking")), + Coin { + amount: Uint128::new(1_000_000_000_000), + denom: TEST_COIN_DENOM.to_string(), + }, + start_time, + periods, + Some(PledgeCap::Percent( + Percent::from_percentage_value(10).unwrap(), + )), storage, ) .unwrap() diff --git a/contracts/vesting/src/vesting/account/delegating_account.rs b/contracts/vesting/src/vesting/account/delegating_account.rs index 1951dd6b61..62fa42b21e 100644 --- a/contracts/vesting/src/vesting/account/delegating_account.rs +++ b/contracts/vesting/src/vesting/account/delegating_account.rs @@ -1,5 +1,4 @@ use crate::errors::ContractError; -use crate::storage::locked_pledge_cap; use crate::storage::save_delegation; use crate::storage::MIXNET_CONTRACT_ADDRESS; use crate::traits::DelegatingAccount; @@ -38,8 +37,9 @@ impl DelegatingAccount for Account { storage: &mut dyn Storage, ) -> Result { let current_balance = self.load_balance(storage)?; - let total_pledged_after = self.total_pledged_locked(storage, env)? + coin.amount; - let locked_pledge_cap = locked_pledge_cap(storage); + let total_pledged_locked = self.total_pledged_locked(storage, env)?; + let total_pledged_after = total_pledged_locked + coin.amount; + let locked_pledge_cap = self.absolute_pledge_cap()?; if locked_pledge_cap < total_pledged_after { return Err(ContractError::LockedPledgeCapReached { diff --git a/contracts/vesting/src/vesting/account/gateway_bonding_account.rs b/contracts/vesting/src/vesting/account/gateway_bonding_account.rs index e9979b71a6..91cdd41c68 100644 --- a/contracts/vesting/src/vesting/account/gateway_bonding_account.rs +++ b/contracts/vesting/src/vesting/account/gateway_bonding_account.rs @@ -1,6 +1,5 @@ use super::PledgeData; use crate::errors::ContractError; -use crate::storage::locked_pledge_cap; use crate::storage::MIXNET_CONTRACT_ADDRESS; use crate::traits::GatewayBondingAccount; use crate::traits::VestingAccount; @@ -22,8 +21,9 @@ impl GatewayBondingAccount for Account { storage: &mut dyn Storage, ) -> Result { let current_balance = self.load_balance(storage)?; - let total_pledged_after = self.total_pledged_locked(storage, env)? + pledge.amount; - let locked_pledge_cap = locked_pledge_cap(storage); + let total_pledged_locked = self.total_pledged_locked(storage, env)?; + let total_pledged_after = total_pledged_locked + pledge.amount; + let locked_pledge_cap = self.absolute_pledge_cap()?; if locked_pledge_cap < total_pledged_after { return Err(ContractError::LockedPledgeCapReached { diff --git a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs index 7fd182e8c9..e3aec82584 100644 --- a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs +++ b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs @@ -3,7 +3,6 @@ use super::Account; use crate::errors::ContractError; -use crate::storage::locked_pledge_cap; use crate::storage::MIXNET_CONTRACT_ADDRESS; use crate::traits::MixnodeBondingAccount; use crate::traits::VestingAccount; @@ -39,8 +38,9 @@ impl MixnodeBondingAccount for Account { storage: &mut dyn Storage, ) -> Result { let current_balance = self.load_balance(storage)?; - let total_pledged_after = self.total_pledged_locked(storage, env)? + pledge.amount; - let locked_pledge_cap = locked_pledge_cap(storage); + let total_pledged_locked = self.total_pledged_locked(storage, env)?; + let total_pledged_after = total_pledged_locked + pledge.amount; + let locked_pledge_cap = self.absolute_pledge_cap()?; if locked_pledge_cap < total_pledged_after { return Err(ContractError::LockedPledgeCapReached { diff --git a/contracts/vesting/src/vesting/account/mod.rs b/contracts/vesting/src/vesting/account/mod.rs index 4f75c9461c..71663247b7 100644 --- a/contracts/vesting/src/vesting/account/mod.rs +++ b/contracts/vesting/src/vesting/account/mod.rs @@ -5,12 +5,13 @@ use crate::storage::{ remove_delegation, remove_gateway_pledge, save_account, save_balance, save_bond_pledge, save_gateway_pledge, save_withdrawn, BlockTimestampSecs, DELEGATIONS, KEY, }; +use crate::traits::VestingAccount; use cosmwasm_std::{Addr, Coin, Order, Storage, Timestamp, Uint128}; use cw_storage_plus::Bound; use mixnet_contract_common::MixId; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use vesting_contract_common::{Period, PledgeData}; +use vesting_contract_common::{Period, PledgeCap, PledgeData}; mod delegating_account; mod gateway_bonding_account; @@ -31,6 +32,8 @@ pub struct Account { pub periods: Vec, pub coin: Coin, storage_key: u32, + #[serde(default)] + pub pledge_cap: Option, } impl Account { @@ -40,6 +43,7 @@ impl Account { coin: Coin, start_time: Timestamp, periods: Vec, + pledge_cap: Option, storage: &mut dyn Storage, ) -> Result { let storage_key = generate_storage_key(storage)?; @@ -51,12 +55,24 @@ impl Account { periods, coin, storage_key, + pledge_cap, }; save_account(&account, storage)?; account.save_balance(amount, storage)?; Ok(account) } + pub fn pledge_cap(&self) -> PledgeCap { + self.pledge_cap.clone().unwrap_or_default() + } + + pub fn absolute_pledge_cap(&self) -> Result { + match self.pledge_cap() { + PledgeCap::Absolute(cap) => Ok(cap), + PledgeCap::Percent(p) => Ok(p * self.get_original_vesting()?.amount.amount), + } + } + pub fn coin(&self) -> Coin { self.coin.clone() } diff --git a/contracts/vesting/src/vesting/account/vesting_account.rs b/contracts/vesting/src/vesting/account/vesting_account.rs index 6ae13ef763..97c02c0214 100644 --- a/contracts/vesting/src/vesting/account/vesting_account.rs +++ b/contracts/vesting/src/vesting/account/vesting_account.rs @@ -155,15 +155,8 @@ impl VestingAccount for Account { let block_time = block_time.unwrap_or(env.block.time); let delegated_free = self.get_delegated_free(Some(block_time), env, storage)?; - let period = self.get_current_vesting_period(block_time)?; - let start_time = match period { - Period::Before => 0, - Period::After => u64::MAX, - Period::In(idx) => self.periods[idx].start_time, - }; - let delegations_before_start_time = - self.total_delegations_at_timestamp(storage, start_time)?; + self.total_delegations_at_timestamp(storage, block_time.seconds())?; let amount = delegations_before_start_time - delegated_free.amount; diff --git a/contracts/vesting/src/vesting/mod.rs b/contracts/vesting/src/vesting/mod.rs index 57175830bb..3eb6134e4e 100644 --- a/contracts/vesting/src/vesting/mod.rs +++ b/contracts/vesting/src/vesting/mod.rs @@ -38,6 +38,7 @@ pub fn populate_vesting_periods( mod tests { use crate::contract::*; use crate::storage::*; + use crate::support::tests::helpers::vesting_account_percent_fixture; use crate::support::tests::helpers::{ init_contract, vesting_account_mid_fixture, vesting_account_new_fixture, TEST_COIN_DENOM, }; @@ -51,6 +52,7 @@ mod tests { use mixnet_contract_common::{Gateway, MixNode, Percent}; use vesting_contract_common::messages::{ExecuteMsg, VestingSpecification}; use vesting_contract_common::Period; + use vesting_contract_common::PledgeCap; #[test] fn test_account_creation() { @@ -61,6 +63,7 @@ mod tests { owner_address: "owner".to_string(), staking_address: Some("staking".to_string()), vesting_spec: None, + cap: Some(PledgeCap::Absolute(Uint128::from(100_000_000_000u128))), }; // Try creating an account when not admin let response = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone()); @@ -429,12 +432,38 @@ mod tests { assert_eq!(locked_coins.amount, Uint128::new(660_000_000_000)); } + #[test] + fn test_percent_cap() { + let mut deps = init_contract(); + let env = mock_env(); + + let account = vesting_account_percent_fixture(&mut deps.storage, &env); + + assert_eq!( + account.absolute_pledge_cap().unwrap(), + Uint128::new(100_000_000_000) + ) + } + #[test] fn test_delegations() { let mut deps = init_contract(); let env = mock_env(); - let account = vesting_account_new_fixture(&mut deps.storage, &env); + // let account = vesting_account_new_fixture(&mut deps.storage, &env); + + let msg = ExecuteMsg::CreateAccount { + owner_address: "owner".to_string(), + staking_address: Some("staking".to_string()), + vesting_spec: None, + cap: Some(PledgeCap::Absolute(Uint128::from(100_000_000_000u128))), + }; + let info = mock_info("admin", &coins(1_000_000_000_000, TEST_COIN_DENOM)); + + let _response = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone()); + let account = load_account(&Addr::unchecked("owner"), &deps.storage) + .unwrap() + .unwrap(); // Try delegating too much let err = account.try_delegate_to_mixnode( @@ -474,7 +503,7 @@ mod tests { let balance = account.load_balance(&deps.storage).unwrap(); assert_eq!(balance, Uint128::new(910000000000)); - // Try delegating too much again + // Try delegating too much againcalca let err = account.try_delegate_to_mixnode( 1, Coin { @@ -489,6 +518,10 @@ mod tests { let total_delegations = account.total_delegations_for_mix(1, &deps.storage).unwrap(); assert_eq!(Uint128::new(90_000_000_000), total_delegations); + let account = load_account(&Addr::unchecked("owner"), &deps.storage) + .unwrap() + .unwrap(); + // Current period -> block_time: None let delegated_free = account .get_delegated_free(None, &env, &deps.storage) @@ -853,6 +886,7 @@ mod tests { }, Timestamp::from_seconds(account_creation_timestamp), periods, + Some(PledgeCap::Absolute(Uint128::from(100_000_000_000u128))), deps.as_mut().storage, ) .unwrap(); diff --git a/explorer-api/Cargo.toml b/explorer-api/Cargo.toml index f9edf01b44..217433d4e5 100644 --- a/explorer-api/Cargo.toml +++ b/explorer-api/Cargo.toml @@ -27,6 +27,8 @@ maxminddb = "0.23.0" dotenv = "0.15.0" mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract" } +contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common" } network-defaults = { path = "../common/network-defaults" } +logging = { path = "../common/logging"} task = { path = "../common/task" } validator-client = { path = "../common/client-libs/validator-client", features=["nymd-client"] } diff --git a/explorer-api/src/country_statistics/geolocate.rs b/explorer-api/src/country_statistics/geolocate.rs index 60603a2aaf..42fe9405be 100644 --- a/explorer-api/src/country_statistics/geolocate.rs +++ b/explorer-api/src/country_statistics/geolocate.rs @@ -58,7 +58,10 @@ impl GeoLocateTask { continue; } - match geo_ip.query(&cache_item.mix_node().host) { + match geo_ip.query( + &cache_item.mix_node().host, + Some(cache_item.mix_node().mix_port), + ) { Ok(opt) => match opt { Some(location) => { let location = Location::new(location); diff --git a/explorer-api/src/geo_ip/location.rs b/explorer-api/src/geo_ip/location.rs index 643de3e76d..a1afec55d6 100644 --- a/explorer-api/src/geo_ip/location.rs +++ b/explorer-api/src/geo_ip/location.rs @@ -4,9 +4,14 @@ use isocountry::CountryCode; use log::warn; use maxminddb::{geoip2::Country, MaxMindDBError, Reader}; -use std::{net::IpAddr, str::FromStr, sync::Arc}; +use std::{ + net::{IpAddr, ToSocketAddrs}, + str::FromStr, + sync::Arc, +}; const DEFAULT_DATABASE_PATH: &str = "./geo_ip/GeoLite2-Country.mmdb"; +const FAKE_PORT: u16 = 1234; #[derive(Debug)] pub enum GeoIpError { @@ -52,10 +57,27 @@ impl GeoIp { GeoIp { db: reader } } - pub fn query(&self, address: &str) -> Result, GeoIpError> { - let ip: IpAddr = FromStr::from_str(address).map_err(|e| { - error!("Fail to create IpAddr from {}: {}", &address, e); - GeoIpError::NoValidIP + pub fn query(&self, address: &str, port: Option) -> Result, GeoIpError> { + let ip: IpAddr = FromStr::from_str(address).or_else(|_| { + debug!( + "Fail to create IpAddr from {}. Trying using internal lookup...", + &address + ); + let p = port.unwrap_or(FAKE_PORT); + let socket_addr = (address, p) + .to_socket_addrs() + .map_err(|e| { + error!("Fail to resolve IP address from {}:{}: {}", &address, p, e); + GeoIpError::NoValidIP + })? + .next() + .ok_or_else(|| { + error!("Fail to resolve IP address from {}:{}", &address, p); + GeoIpError::NoValidIP + })?; + let ip = socket_addr.ip(); + debug!("Internal lookup succeed, resolved ip: {}", ip); + Ok(ip) })?; let result = self .db diff --git a/explorer-api/src/guards/location.rs b/explorer-api/src/guards/location.rs index dd1bfe1ff8..23bb92d941 100644 --- a/explorer-api/src/guards/location.rs +++ b/explorer-api/src/guards/location.rs @@ -39,7 +39,7 @@ fn find_location(request: &Request<'_>) -> Result (Status::Forbidden, LocationError::NoIP), GeoIpError::InternalError => { diff --git a/explorer-api/src/main.rs b/explorer-api/src/main.rs index d08059442e..47dddf9f54 100644 --- a/explorer-api/src/main.rs +++ b/explorer-api/src/main.rs @@ -6,6 +6,7 @@ extern crate rocket_okapi; use clap::Parser; use dotenv::dotenv; use log::info; +use logging::setup_logging; use network_defaults::setup_env; use task::ShutdownNotifier; @@ -115,18 +116,3 @@ async fn wait_for_signal() { }, } } - -fn setup_logging() { - let mut log_builder = pretty_env_logger::formatted_timed_builder(); - if let Ok(s) = ::std::env::var("RUST_LOG") { - log_builder.parse_filters(&s); - } else { - // default to 'Info' - log_builder.filter(None, log::LevelFilter::Info); - } - - log_builder - .filter_module("tokio_reactor", log::LevelFilter::Warn) - .filter_module("reqwest", log::LevelFilter::Warn) - .init(); -} diff --git a/explorer-api/src/mix_node/econ_stats.rs b/explorer-api/src/mix_node/econ_stats.rs index cb068b11a6..22f106b1aa 100644 --- a/explorer-api/src/mix_node/econ_stats.rs +++ b/explorer-api/src/mix_node/econ_stats.rs @@ -4,7 +4,7 @@ use crate::client::ThreadsafeValidatorClient; use crate::helpers::best_effort_small_dec_to_f64; use crate::mix_node::models::EconomicDynamicsStats; -use mixnet_contract_common::rewarding::helpers::truncate_decimal; +use contracts_common::truncate_decimal; use mixnet_contract_common::MixId; pub(crate) async fn retrieve_mixnode_econ_stats( diff --git a/explorer-api/src/mix_node/models.rs b/explorer-api/src/mix_node/models.rs index 0332aeaa17..1d709433b1 100644 --- a/explorer-api/src/mix_node/models.rs +++ b/explorer-api/src/mix_node/models.rs @@ -3,6 +3,7 @@ use crate::cache::Cache; use crate::mix_nodes::location::Location; +use contracts_common::Percent; use mixnet_contract_common::Delegation; use mixnet_contract_common::{Addr, Coin, Layer, MixId, MixNode}; use serde::Deserialize; @@ -24,6 +25,7 @@ pub(crate) enum MixnodeStatus { pub(crate) struct PrettyDetailedMixNodeBond { // I leave this to @MS to refactor this type as a lot of things here are redundant thanks to // the existence of `MixNodeDetails` + pub mix_id: MixId, pub location: Option, pub status: MixnodeStatus, pub pledge_amount: Coin, @@ -35,6 +37,8 @@ pub(crate) struct PrettyDetailedMixNodeBond { pub avg_uptime: u8, pub estimated_operator_apy: f64, pub estimated_delegators_apy: f64, + pub operating_cost: Coin, + pub profit_margin_percent: Percent, } #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] diff --git a/explorer-api/src/mix_nodes/models.rs b/explorer-api/src/mix_nodes/models.rs index 11d91a6eb1..07899742c9 100644 --- a/explorer-api/src/mix_nodes/models.rs +++ b/explorer-api/src/mix_nodes/models.rs @@ -126,19 +126,6 @@ impl ThreadsafeMixNodesCache { self.mixnodes.read().await.get_mixnode(mix_id) } - pub(crate) async fn get_mixnode_by_identity( - &self, - pubkey: &str, - ) -> Option { - let all_nodes = self.get_mixnodes().await?; - for (_, node) in all_nodes { - if node.mix_node().identity_key == pubkey { - return Some(node); - } - } - None - } - pub(crate) async fn get_mixnodes(&self) -> Option> { self.mixnodes.read().await.get_mixnodes() } @@ -154,6 +141,7 @@ impl ThreadsafeMixNodesCache { let rewarding_info = &node.mixnode_details.rewarding_details; PrettyDetailedMixNodeBond { + mix_id, location: location.and_then(|l| l.location.clone()), status: mixnodes_guard.determine_node_status(mix_id), pledge_amount: truncate_reward(rewarding_info.operator, denom), @@ -165,6 +153,8 @@ impl ThreadsafeMixNodesCache { stake_saturation: best_effort_small_dec_to_f64(node.stake_saturation) as f32, estimated_operator_apy: best_effort_small_dec_to_f64(node.estimated_operator_apy), estimated_delegators_apy: best_effort_small_dec_to_f64(node.estimated_delegators_apy), + operating_cost: rewarding_info.cost_params.interval_operating_cost.clone(), + profit_margin_percent: rewarding_info.cost_params.profit_margin_percent, } } diff --git a/explorer-api/src/ping/http.rs b/explorer-api/src/ping/http.rs index 8a23c4f082..7e7ebea06d 100644 --- a/explorer-api/src/ping/http.rs +++ b/explorer-api/src/ping/http.rs @@ -1,7 +1,7 @@ // Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use mixnet_contract_common::MixNode; +use mixnet_contract_common::{MixId, MixNode}; use rocket::serde::json::Json; use rocket::{Route, State}; use rocket_okapi::okapi::openapi3::OpenApi; @@ -20,41 +20,39 @@ pub fn ping_make_default_routes(settings: &OpenApiSettings) -> (Vec, Open openapi_get_routes_spec![settings: index] } -// TODO: I'm not deprecating this one explicitly since we don't have -// a decision on whether nodes should be accessed (as in using URL) by id or identity key #[openapi(tag = "ping")] -#[get("/")] +#[get("/")] pub(crate) async fn index( - pubkey: &str, + mix_id: MixId, state: &State, ) -> Option> { - match state.inner.ping.clone().get(pubkey).await { + match state.inner.ping.clone().get(mix_id).await { Some(cache_value) => { - trace!("Returning cached value for {}", pubkey); + trace!("Returning cached value for {}", mix_id); Some(Json(PingResponse { pending: cache_value.pending, ports: cache_value.ports, })) } None => { - trace!("No cache value for {}", pubkey); + trace!("No cache value for {}", mix_id); - match state.inner.get_mix_node_by_pubkey(pubkey).await { + match state.inner.get_mix_node(mix_id).await { Some(node) => { // set status to pending, so that any HTTP requests are pending - state.inner.ping.set_pending(pubkey).await; + state.inner.ping.set_pending(mix_id).await; // do the check let ports = Some(port_check(node.mix_node()).await); - trace!("Tested mix node {}: {:?}", pubkey, ports); + trace!("Tested mix node {}: {:?}", mix_id, ports); let response = PingResponse { ports, pending: false, }; // cache for 1 min - trace!("Caching value for {}", pubkey); - state.inner.ping.set(pubkey, response.clone()).await; + trace!("Caching value for {}", mix_id); + state.inner.ping.set(mix_id, response.clone()).await; // return response Some(Json(response)) diff --git a/explorer-api/src/ping/models.rs b/explorer-api/src/ping/models.rs index 28588d550f..9463e12bb6 100644 --- a/explorer-api/src/ping/models.rs +++ b/explorer-api/src/ping/models.rs @@ -2,11 +2,12 @@ use std::collections::HashMap; use std::sync::Arc; use std::time::{Duration, SystemTime}; +use mixnet_contract_common::MixId; use serde::Deserialize; use serde::Serialize; use tokio::sync::RwLock; -pub(crate) type PingCache = HashMap; +pub(crate) type PingCache = HashMap; const PING_TTL: Duration = Duration::from_secs(60 * 5); // 5 mins, before port check will be re-tried (only while pending) const CACHE_TTL: Duration = Duration::from_secs(60 * 60); // 1 hour, to cache result from port check @@ -23,11 +24,11 @@ impl ThreadsafePingCache { } } - pub(crate) async fn get(&self, identity_key: &str) -> Option { + pub(crate) async fn get(&self, mix_id: MixId) -> Option { self.inner .read() .await - .get(identity_key) + .get(&mix_id) .filter(|cache_item| cache_item.valid_until > SystemTime::now()) .map(|cache_item| { if cache_item.pending { @@ -43,9 +44,9 @@ impl ThreadsafePingCache { }) } - pub(crate) async fn set_pending(&self, identity_key: &str) { + pub(crate) async fn set_pending(&self, mix_id: MixId) { self.inner.write().await.insert( - identity_key.to_string(), + mix_id, PingCacheItem { pending: true, valid_until: SystemTime::now() + PING_TTL, @@ -54,9 +55,9 @@ impl ThreadsafePingCache { ); } - pub(crate) async fn set(&self, identity_key: &str, item: PingResponse) { + pub(crate) async fn set(&self, mix_id: MixId, item: PingResponse) { self.inner.write().await.insert( - identity_key.to_string(), + mix_id, PingCacheItem { pending: false, valid_until: SystemTime::now() + CACHE_TTL, diff --git a/explorer-api/src/state.rs b/explorer-api/src/state.rs index 7ca0ae3105..a511d5c0b8 100644 --- a/explorer-api/src/state.rs +++ b/explorer-api/src/state.rs @@ -3,7 +3,7 @@ use std::path::Path; use chrono::{DateTime, Utc}; use log::info; -use mixnet_contract_common::{IdentityKeyRef, MixId}; +use mixnet_contract_common::MixId; use serde::{Deserialize, Serialize}; use crate::client::ThreadsafeValidatorClient; @@ -41,13 +41,6 @@ impl ExplorerApiState { pub(crate) async fn get_mix_node(&self, mix_id: MixId) -> Option { self.mixnodes.get_mixnode(mix_id).await } - - pub(crate) async fn get_mix_node_by_pubkey( - &self, - pubkey: IdentityKeyRef<'_>, - ) -> Option { - self.mixnodes.get_mixnode_by_identity(pubkey).await - } } #[derive(Debug, Serialize, Deserialize)] diff --git a/explorer/package.json b/explorer/package.json index f391e05f79..b314878308 100644 --- a/explorer/package.json +++ b/explorer/package.json @@ -26,7 +26,8 @@ "react-router-dom": "6", "react-simple-maps": "^2.3.0", "react-tooltip": "^4.2.21", - "use-clipboard-copy": "^0.2.0" + "use-clipboard-copy": "^0.2.0", + "big.js": "^6.2.1" }, "devDependencies": { "@babel/core": "^7.15.0", diff --git a/explorer/src/api/constants.ts b/explorer/src/api/constants.ts index 5c2b044261..fb7216b64f 100644 --- a/explorer/src/api/constants.ts +++ b/explorer/src/api/constants.ts @@ -14,6 +14,7 @@ export const VALIDATORS_API = `${VALIDATOR_BASE_URL}/validators`; export const BLOCK_API = `${VALIDATOR_API_BASE_URL}/block`; export const COUNTRY_DATA_API = `${API_BASE_URL}/countries`; export const UPTIME_STORY_API = `${VALIDATOR_API_BASE_URL}/api/v1/status/mixnode`; // add ID then '/history' to this. +export const UPTIME_STORY_API_GATEWAY = `${VALIDATOR_API_BASE_URL}/api/v1/status/gateway`; // add ID then '/history' or '/report' to this. // errors export const MIXNODE_API_ERROR = "We're having trouble finding that record, please try again or Contact Us."; diff --git a/explorer/src/api/index.ts b/explorer/src/api/index.ts index 5a9b40ef9a..37e68b8096 100644 --- a/explorer/src/api/index.ts +++ b/explorer/src/api/index.ts @@ -2,6 +2,7 @@ import { BLOCK_API, COUNTRY_DATA_API, GATEWAYS_API, + UPTIME_STORY_API_GATEWAY, MIXNODE_API, MIXNODE_PING, MIXNODES_API, @@ -15,6 +16,8 @@ import { DelegationsResponse, UniqDelegationsResponse, GatewayResponse, + GatewayReportResponse, + UptimeStoryResponse, MixNodeDescriptionResponse, MixNodeResponse, MixNodeResponseItem, @@ -23,7 +26,6 @@ import { StatsResponse, StatusResponse, SummaryOverviewResponse, - UptimeStoryResponse, ValidatorsResponse, } from '../typeDefs/explorer-api'; @@ -92,6 +94,12 @@ export class Api { return res.json(); }; + static fetchGatewayUptimeStoryById = async (id: string): Promise => + (await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/history`)).json(); + + static fetchGatewayReportById = async (id: string): Promise => + (await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/report`)).json(); + static fetchValidators = async (): Promise => { const res = await fetch(VALIDATORS_API); const json = await res.json(); diff --git a/explorer/src/components/DetailTable.tsx b/explorer/src/components/DetailTable.tsx index 3957119cd3..1aab097a05 100644 --- a/explorer/src/components/DetailTable.tsx +++ b/explorer/src/components/DetailTable.tsx @@ -1,9 +1,12 @@ import * as React from 'react'; import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material'; +import { useTheme } from '@mui/material/styles'; +import { Tooltip } from '@nymproject/react/tooltip/Tooltip'; import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard'; import { Box } from '@mui/system'; import { cellStyles } from './Universal-DataGrid'; import { currencyToString } from '../utils/currency'; +import { GatewayEnrichedRowType } from './Gateways'; import { MixnodeRowType } from './MixNodes'; export type ColumnsType = { @@ -43,42 +46,60 @@ function formatCellValues(val: string | number, field: string) { export const DetailTable: React.FC<{ tableName: string; columnsData: ColumnsType[]; - rows: MixnodeRowType[]; -}> = ({ tableName, columnsData, rows }: UniversalTableProps) => ( - - - - - {columnsData?.map(({ field, title, flex }) => ( - - {title} - - ))} - - - - {rows.map((eachRow) => ( - - {columnsData?.map((_, index) => ( - - {formatCellValues(eachRow[columnsData[index].field], columnsData[index].field)} + rows: MixnodeRowType[] | GatewayEnrichedRowType[]; +}> = ({ tableName, columnsData, rows }: UniversalTableProps) => { + const theme = useTheme(); + return ( + +
+ + + {columnsData?.map(({ field, title, flex, tooltipInfo }) => ( + + + {tooltipInfo && ( + + + + )} + {title} + ))} - ))} - -
-
-); + + + {rows.map((eachRow) => ( + + {columnsData?.map((data, index) => ( + + {formatCellValues(eachRow[columnsData[index].field], columnsData[index].field)} + + ))} + + ))} + + + + ); +}; diff --git a/explorer/src/components/Gateways.ts b/explorer/src/components/Gateways.ts index 9f05ff51b0..e75c9aeb05 100644 --- a/explorer/src/components/Gateways.ts +++ b/explorer/src/components/Gateways.ts @@ -1,23 +1,48 @@ -import { GatewayResponse } from '../typeDefs/explorer-api'; +import { GatewayResponse, GatewayResponseItem, GatewayReportResponse } from '../typeDefs/explorer-api'; export type GatewayRowType = { id: string; owner: string; - identity_key: string; + identityKey: string; bond: number; host: string; location: string; }; +export type GatewayEnrichedRowType = GatewayRowType & { + routingScore: string; + avgUptime: string; + clientsPort: number; + mixPort: number; +}; + export function gatewayToGridRow(arrayOfGateways: GatewayResponse): GatewayRowType[] { return !arrayOfGateways ? [] : arrayOfGateways.map((gw) => ({ id: gw.owner, owner: gw.owner, - identity_key: gw.gateway.identity_key || '', + identityKey: gw.gateway.identity_key || '', location: gw?.gateway?.location || '', bond: gw.pledge_amount.amount || 0, host: gw.gateway.host || '', })); } + +export function gatewayEnrichedToGridRow( + gateway: GatewayResponseItem, + report: GatewayReportResponse, +): GatewayEnrichedRowType { + return { + id: gateway.owner, + owner: gateway.owner, + identityKey: gateway.gateway.identity_key || '', + location: gateway?.gateway?.location || '', + bond: gateway.pledge_amount.amount || 0, + host: gateway.gateway.host || '', + clientsPort: gateway.gateway.clients_port || 0, + mixPort: gateway.gateway.mix_port || 0, + routingScore: `${report.most_recent}%`, + avgUptime: `${report.last_day || report.last_hour}%`, + }; +} diff --git a/explorer/src/components/MixNodes/Economics/Columns.ts b/explorer/src/components/MixNodes/Economics/Columns.ts index c038b68bce..40fef70b68 100644 --- a/explorer/src/components/MixNodes/Economics/Columns.ts +++ b/explorer/src/components/MixNodes/Economics/Columns.ts @@ -39,6 +39,14 @@ export const EconomicsInfoColumns: ColumnsType[] = [ tooltipInfo: 'Percentage of the delegates rewards that the operator takes as fee before rewards are distributed to the delegates.', }, + { + field: 'operatingCost', + title: 'Operating Cost', + flex: 1, + headerAlign: 'left', + tooltipInfo: + 'Monthly operational cost of running this node. This cost is set by the operator and it influences how the rewards are split between the operator and delegators.', + }, { field: 'avgUptime', title: 'Routing Score', diff --git a/explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx b/explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx index f55207b87d..aa3156ccf1 100644 --- a/explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx +++ b/explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx @@ -26,6 +26,9 @@ const row: EconomicsInfoRowWithIndex = { profitMargin: { value: '10 %', }, + operatingCost: { + value: '11121 NYM', + }, stakeSaturation: { value: '80 %', progressBarValue: 80, @@ -64,6 +67,9 @@ const emptyRow: EconomicsInfoRowWithIndex = { profitMargin: { value: '-', }, + operatingCost: { + value: '-', + }, stakeSaturation: { value: '-', progressBarValue: 0, diff --git a/explorer/src/components/MixNodes/Economics/Rows.ts b/explorer/src/components/MixNodes/Economics/Rows.ts index ace2cec61a..a4f998a1f0 100644 --- a/explorer/src/components/MixNodes/Economics/Rows.ts +++ b/explorer/src/components/MixNodes/Economics/Rows.ts @@ -1,7 +1,8 @@ -import { currencyToString } from '../../../utils/currency'; +import { currencyToString, unymToNym } from '../../../utils/currency'; import { useMixnodeContext } from '../../../context/mixnode'; import { ApiState, MixNodeEconomicDynamicsStatsResponse } from '../../../typeDefs/explorer-api'; import { EconomicsInfoRowWithIndex } from './types'; +import { toPercentIntegerString } from '../../../utils'; const selectionChance = (economicDynamicsStats: ApiState | undefined) => { const inclusionProbability = economicDynamicsStats?.data?.active_set_inclusion_probability; @@ -29,9 +30,13 @@ export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => { const estimatedOperatorRewards = currencyToString((economicDynamicsStats?.data?.estimated_operator_reward || '').toString()) || '-'; const stakeSaturation = economicDynamicsStats?.data?.stake_saturation || '-'; - const profitMargin = mixNode?.data?.mix_node.profit_margin_percent || '-'; + const profitMargin = mixNode?.data?.profit_margin_percent + ? toPercentIntegerString(mixNode?.data?.profit_margin_percent) + : '-'; const avgUptime = economicDynamicsStats?.data?.current_interval_uptime; + const opCost = mixNode?.data?.operating_cost; + return { id: 1, estimatedTotalReward: { @@ -50,6 +55,9 @@ export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => { profitMargin: { value: profitMargin ? `${profitMargin} %` : '-', }, + operatingCost: { + value: opCost ? `${unymToNym(opCost.amount, 6)} NYM` : '-', + }, avgUptime: { value: avgUptime ? `${avgUptime} %` : '-', }, diff --git a/explorer/src/components/MixNodes/Economics/types.ts b/explorer/src/components/MixNodes/Economics/types.ts index b82b35f139..7c4bc722db 100644 --- a/explorer/src/components/MixNodes/Economics/types.ts +++ b/explorer/src/components/MixNodes/Economics/types.ts @@ -10,6 +10,7 @@ export interface EconomicsInfoRow { stakeSaturation: EconomicsRowsType; profitMargin: EconomicsRowsType; avgUptime: EconomicsRowsType; + operatingCost: EconomicsRowsType; } export type EconomicsInfoRowWithIndex = EconomicsInfoRow & { id: number }; diff --git a/explorer/src/components/MixNodes/index.ts b/explorer/src/components/MixNodes/index.ts index 112e55163a..3435c3bf3f 100644 --- a/explorer/src/components/MixNodes/index.ts +++ b/explorer/src/components/MixNodes/index.ts @@ -1,7 +1,10 @@ /* eslint-disable camelcase */ import { MixNodeResponse, MixNodeResponseItem, MixnodeStatus } from '../../typeDefs/explorer-api'; +import { toPercentIntegerString } from '../../utils'; +import { unymToNym } from '../../utils/currency'; export type MixnodeRowType = { + mix_id: number; id: string; status: MixnodeStatus; owner: string; @@ -15,6 +18,7 @@ export type MixnodeRowType = { profit_percentage: string; avg_uptime: string; stake_saturation: number; + operating_cost: string; }; export function mixnodeToGridRow(arrayOfMixnodes?: MixNodeResponse): MixnodeRowType[] { @@ -26,9 +30,11 @@ export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem): const delegations = Number(item.total_delegation.amount) || 0; const totalBond = pledge + delegations; const selfPercentage = ((pledge * 100) / totalBond).toFixed(2); - const profitPercentage = item.mix_node.profit_margin_percent || 0; + const profitPercentage = toPercentIntegerString(item.profit_margin_percent) || 0; const stakeSaturation = typeof item.stake_saturation === 'number' ? item.stake_saturation * 100 : 0; + return { + mix_id: item.mix_id, id: item.owner, status: item.status, owner: item.owner, @@ -42,5 +48,6 @@ export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem): profit_percentage: `${profitPercentage}%`, avg_uptime: `${item.avg_uptime}%` || '-', stake_saturation: stakeSaturation, + operating_cost: `${unymToNym(item.operating_cost.amount, 6)} NYM`, }; } diff --git a/explorer/src/components/Tooltip.tsx b/explorer/src/components/Tooltip.tsx new file mode 100644 index 0000000000..0febd9f891 --- /dev/null +++ b/explorer/src/components/Tooltip.tsx @@ -0,0 +1,36 @@ +import React, { ReactElement } from 'react'; +import { Tooltip as MUITooltip, TooltipComponentsPropsOverrides, TooltipProps } from '@mui/material'; + +type ValueType = T[keyof T]; + +type Props = { + text: string; + id: string; + placement?: ValueType>; + tooltipSx?: TooltipComponentsPropsOverrides; + children: React.ReactNode; +}; + +export const Tooltip = ({ text, id, placement, tooltipSx, children }: Props) => ( + t.palette.nym.networkExplorer.tooltip.background, + color: (t) => t.palette.nym.networkExplorer.tooltip.color, + '& .MuiTooltip-arrow': { + color: (t) => t.palette.nym.networkExplorer.tooltip.background, + }, + }, + ...tooltipSx, + }, + }} + arrow + > + {children as ReactElement} + +); diff --git a/explorer/src/context/gateway.tsx b/explorer/src/context/gateway.tsx new file mode 100644 index 0000000000..dd96634518 --- /dev/null +++ b/explorer/src/context/gateway.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import { ApiState, GatewayReportResponse, UptimeStoryResponse } from '../typeDefs/explorer-api'; +import { Api } from '../api'; +import { useApiState } from './hooks'; + +/** + * This context provides the state for a single gateway by identity key. + */ + +interface GatewayState { + uptimeReport?: ApiState; + uptimeStory?: ApiState; +} + +export const GatewayContext = React.createContext({}); + +export const useGatewayContext = (): React.ContextType => + React.useContext(GatewayContext); + +/** + * Provides a state context for a gateway by identity + * @param gatewayIdentityKey The identity key of the gateway + */ +export const GatewayContextProvider = ({ + gatewayIdentityKey, + children, +}: { + gatewayIdentityKey: string; + children: JSX.Element; +}) => { + const [uptimeReport, fetchUptimeReportById, clearUptimeReportById] = useApiState( + gatewayIdentityKey, + Api.fetchGatewayReportById, + 'Failed to fetch gateway uptime report by id', + ); + + const [uptimeStory, fetchUptimeHistory, clearUptimeHistory] = useApiState( + gatewayIdentityKey, + Api.fetchGatewayUptimeStoryById, + 'Failed to fetch gateway uptime history', + ); + + React.useEffect(() => { + // when the identity key changes, remove all previous data + clearUptimeReportById(); + clearUptimeHistory(); + Promise.all([fetchUptimeReportById(), fetchUptimeHistory()]); + }, [gatewayIdentityKey]); + + const state = React.useMemo( + () => ({ + uptimeReport, + uptimeStory, + }), + [uptimeReport, uptimeStory], + ); + + return {children}; +}; diff --git a/explorer/src/context/hooks.ts b/explorer/src/context/hooks.ts index f891a31ca3..9c5579c899 100644 --- a/explorer/src/context/hooks.ts +++ b/explorer/src/context/hooks.ts @@ -1,30 +1,47 @@ import * as React from 'react'; import { ApiState } from '../typeDefs/explorer-api'; -type WrappedApiFn = () => Promise>; - +/** + * Custom hook to get data from the API by passing an id to a delegate method that fetches the data asynchronously + * @param id The id to fetch + * @param fn Delegate the fetching to this method (must take `(id: string)` as a parameter) + * @param errorMessage A static error message, to use when no dynamic error message is returned + */ export const useApiState = ( - // eslint-disable-next-line @typescript-eslint/ban-types - fn: Function, + id: string, + fn: (argId: string) => Promise, errorMessage: string, -): [ApiState, WrappedApiFn] => { +): [ApiState | undefined, () => Promise>, () => void] => { + // stores the state const [value, setValue] = React.useState>(); - const wrappedFn = React.useCallback(async () => { + + // clear the value + const clearValueFn = () => setValue(undefined); + + // this provides a method to trigger the delegate to fetch data + const wrappedFetchFn = React.useCallback(async () => { + setValue({ isLoading: true }); try { + // keep previous state and set to loading setValue((prevState) => ({ ...prevState, isLoading: true })); - const data = await fn(); - setValue({ + + // delegate to user function to get data and set if successful + const data = await fn(id); + const newValue: ApiState = { isLoading: false, data, - }); - return data; + }; + setValue(newValue); + return newValue; } catch (error) { - setValue({ + // return the caught error or create a new error with the static error message + const newValue: ApiState = { error: error instanceof Error ? error : new Error(errorMessage), isLoading: false, - }); - return undefined; + }; + setValue(newValue); + return newValue; } - }, [setValue, fn]); - return [value || { isLoading: true }, wrappedFn]; + }, [setValue, fn, id, errorMessage]); + return [value, wrappedFetchFn, clearValueFn]; }; diff --git a/explorer/src/context/main.tsx b/explorer/src/context/main.tsx index 9255eb20e3..42e89b32ee 100644 --- a/explorer/src/context/main.tsx +++ b/explorer/src/context/main.tsx @@ -96,19 +96,22 @@ export const MainContextProvider: React.FC = ({ children }) => { const filterMixnodes = async (filters: { [key in EnumFilterKey]: number[] }, status?: MixnodeStatus) => { setMixnodes((d) => ({ ...d, isLoading: true })); const mxns = status ? await Api.fetchMixnodesActiveSetByStatus(status) : await Api.fetchMixnodes(); + const filtered = mxns?.filter( (m) => - m.mix_node.profit_margin_percent >= filters.profitMargin[0] && - m.mix_node.profit_margin_percent <= filters.profitMargin[1] && + +m.profit_margin_percent >= filters.profitMargin[0] / 100 && + +m.profit_margin_percent <= filters.profitMargin[1] / 100 && m.stake_saturation >= filters.stakeSaturation[0] && m.stake_saturation <= filters.stakeSaturation[1] && m.avg_uptime >= filters.routingScore[0] && m.avg_uptime <= filters.routingScore[1], ); + setMixnodes({ data: filtered, isLoading: false }); }; const fetchGateways = async () => { + setGateways((d) => ({ ...d, isLoading: true })); try { const data = await Api.fetchGateways(); setGateways({ data, isLoading: false }); diff --git a/explorer/src/context/mixnode.tsx b/explorer/src/context/mixnode.tsx index 6259b8c00d..a17366645c 100644 --- a/explorer/src/context/mixnode.tsx +++ b/explorer/src/context/mixnode.tsx @@ -11,6 +11,7 @@ import { UptimeStoryResponse, } from '../typeDefs/explorer-api'; import { Api } from '../api'; +import { useApiState } from './hooks'; import { mixNodeResponseItemToMixnodeRowType, MixnodeRowType } from '../components/MixNodes'; /** @@ -153,47 +154,3 @@ export const MixnodeContextProvider: React.FC = ({ return {children}; }; - -/** - * Custom hook to get data from the API by passing an id to a delegate method that fetches the data asynchronously - * @param id The id to fetch - * @param fn Delegate the fetching to this method (must take `(id: string)` as a parameter) - * @param errorMessage A static error message, to use when no dynamic error message is returned - */ -function useApiState( - id: string, - fn: (argId: string) => Promise, - errorMessage: string, -): [ApiState | undefined, () => Promise>, () => void] { - // stores the state - const [value, setValue] = React.useState | undefined>(); - - // clear the value - const clearValueFn = () => setValue(undefined); - - // this provides a method to trigger the delegate to fetch data - const wrappedFetchFn = React.useCallback(async () => { - try { - // keep previous state and set to loading - setValue((prevState) => ({ ...prevState, isLoading: true })); - - // delegate to user function to get data and set if successful - const data = await fn(id); - const newValue: ApiState = { - isLoading: false, - data, - }; - setValue(newValue); - return newValue; - } catch (error) { - // return the caught error or create a new error with the static error message - const newValue: ApiState = { - error: error instanceof Error ? error : new Error(errorMessage), - isLoading: false, - }; - setValue(newValue); - return newValue; - } - }, [setValue, fn]); - return [value || { isLoading: true }, wrappedFetchFn, clearValueFn]; -} diff --git a/explorer/src/pages/GatewayDetail/index.tsx b/explorer/src/pages/GatewayDetail/index.tsx new file mode 100644 index 0000000000..5fb7dd9c9f --- /dev/null +++ b/explorer/src/pages/GatewayDetail/index.tsx @@ -0,0 +1,179 @@ +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 { ColumnsType, DetailTable } from '../../components/DetailTable'; +import { gatewayEnrichedToGridRow, GatewayEnrichedRowType } from '../../components/Gateways'; +import { ComponentError } from '../../components/ComponentError'; +import { ContentCard } from '../../components/ContentCard'; +import { TwoColSmallTable } from '../../components/TwoColSmallTable'; +import { UptimeChart } from '../../components/UptimeChart'; +import { GatewayContextProvider, useGatewayContext } from '../../context/gateway'; +import { useMainContext } from '../../context/main'; +import { Title } from '../../components/Title'; + +const columns: ColumnsType[] = [ + { + field: 'identityKey', + title: 'Identity Key', + headerAlign: 'left', + width: 230, + }, + { + field: 'bond', + title: 'Bond', + flex: 1, + headerAlign: 'left', + }, + { + field: 'routingScore', + title: 'Routing Score', + flex: 1, + headerAlign: 'left', + tooltipInfo: + 'Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test.', + }, + { + field: 'avgUptime', + title: 'Avg. Score', + flex: 1, + headerAlign: 'left', + tooltipInfo: 'Is the average routing score in the last 24 hours', + }, + { + field: 'host', + title: 'IP', + headerAlign: 'left', + width: 99, + }, + { + field: 'location', + title: 'Location', + headerAlign: 'left', + flex: 1, + }, + { + field: 'owner', + title: 'Owner', + headerAlign: 'left', + flex: 1, + }, +]; + +/** + * Shows gateway details + */ +const PageGatewayDetailsWithState = ({ selectedGateway }: { selectedGateway: GatewayResponseItem | undefined }) => { + const [enrichGateway, setEnrichGateway] = React.useState(); + const [status, setStatus] = React.useState(); + const { uptimeReport, uptimeStory } = useGatewayContext(); + + React.useEffect(() => { + if (uptimeReport?.data && selectedGateway) { + setEnrichGateway(gatewayEnrichedToGridRow(selectedGateway, uptimeReport.data)); + } + }, [uptimeReport, selectedGateway]); + + React.useEffect(() => { + if (enrichGateway) { + setStatus([enrichGateway.mixPort, enrichGateway.clientsPort]); + } + }, [enrichGateway]); + + return ( + + + + <Grid container> + <Grid item xs={12}> + <DetailTable + columnsData={columns} + tableName="Gateway detail table" + rows={enrichGateway ? [enrichGateway] : []} + /> + </Grid> + </Grid> + + <Grid container spacing={2} mt={0}> + <Grid item xs={12} md={4}> + {status && ( + <ContentCard title="Gateway Status"> + <TwoColSmallTable + loading={false} + keys={['Mix port', 'Client WS API Port']} + values={status.map((each) => each)} + icons={status.map((elem) => !!elem)} + /> + </ContentCard> + )} + </Grid> + <Grid item xs={12} md={8}> + {uptimeStory && ( + <ContentCard title="Routing Score"> + {uptimeStory.error && <ComponentError text="There was a problem retrieving routing score." />} + <UptimeChart loading={uptimeStory.isLoading} xLabel="date" uptimeStory={uptimeStory} /> + </ContentCard> + )} + </Grid> + </Grid> + </Box> + ); +}; + +/** + * Guard component to handle loading and not found states + */ +const PageGatewayDetailGuard: React.FC = () => { + const [selectedGateway, setSelectedGateway] = React.useState<GatewayResponseItem | undefined>(); + const { gateways } = useMainContext(); + const { id } = useParams<{ id: string | undefined }>(); + + React.useEffect(() => { + if (gateways?.data) { + setSelectedGateway(gateways.data.find((gateway) => gateway.gateway.identity_key === id)); + } + }, [gateways, id]); + + if (gateways?.isLoading) { + return <CircularProgress />; + } + + if (gateways?.error) { + // eslint-disable-next-line no-console + console.error(gateways?.error); + return ( + <Alert severity="error"> + Oh no! Could not load mixnode <code>{id || ''}</code> + </Alert> + ); + } + + // loaded, but not found + if (gateways && !gateways.isLoading && !gateways.data) { + return ( + <Alert severity="warning"> + <AlertTitle>Gateway not found</AlertTitle> + Sorry, we could not find a mixnode with id <code>{id || ''}</code> + </Alert> + ); + } + + return <PageGatewayDetailsWithState selectedGateway={selectedGateway} />; +}; + +/** + * Wrapper component that adds the mixnode content based on the `id` in the address URL + */ +export const PageGatewayDetail: React.FC = () => { + const { id } = useParams<{ id: string | undefined }>(); + + if (!id) { + return <Alert severity="error">Oh no! No mixnode identity key specified</Alert>; + } + + return ( + <GatewayContextProvider gatewayIdentityKey={id}> + <PageGatewayDetailGuard /> + </GatewayContextProvider> + ); +}; diff --git a/explorer/src/pages/Gateways/index.tsx b/explorer/src/pages/Gateways/index.tsx index 39f3619308..6b557d593f 100644 --- a/explorer/src/pages/Gateways/index.tsx +++ b/explorer/src/pages/Gateways/index.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; -import { Button, Card, Grid, Typography } from '@mui/material'; +import { Link as RRDLink } from 'react-router-dom'; +import { Box, Button, Card, Grid, Typography, Link as MuiLink } from '@mui/material'; import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; import { SelectChangeEvent } from '@mui/material/Select'; import { useMainContext } from '../../context/main'; @@ -10,6 +11,7 @@ import { CustomColumnHeading } from '../../components/CustomColumnHeading'; import { Title } from '../../components/Title'; import { cellStyles, UniversalDataGrid } from '../../components/Universal-DataGrid'; import { currencyToString } from '../../utils/currency'; +import { Tooltip } from '../../components/Tooltip'; export const PageGateways: React.FC = () => { const { gateways } = useMainContext(); @@ -43,29 +45,20 @@ export const PageGateways: React.FC = () => { const columns: GridColDef[] = [ { - field: 'owner', - headerName: 'Owner', - renderHeader: () => <CustomColumnHeading headingTitle="Owner" />, - width: 380, - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => ( - <Typography sx={cellStyles} data-testid="owner"> - {params.value} - </Typography> - ), - }, - { - field: 'identity_key', + field: 'identityKey', headerName: 'Identity Key', renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />, headerClassName: 'MuiDataGrid-header-override', width: 380, headerAlign: 'left', renderCell: (params: GridRenderCellParams) => ( - <Typography sx={cellStyles} data-testid="identity-key"> + <MuiLink + sx={{ ...cellStyles }} + component={RRDLink} + to={`/network-components/gateway/${params.row.identityKey}`} + > {params.value} - </Typography> + </MuiLink> ), }, { @@ -84,7 +77,7 @@ export const PageGateways: React.FC = () => { { field: 'host', renderHeader: () => <CustomColumnHeading headingTitle="IP:Port" />, - width: 110, + width: 180, headerAlign: 'left', headerClassName: 'MuiDataGrid-header-override', renderCell: (params: GridRenderCellParams) => ( @@ -96,7 +89,7 @@ export const PageGateways: React.FC = () => { { field: 'location', renderHeader: () => <CustomColumnHeading headingTitle="Location" />, - width: 150, + width: 180, headerAlign: 'left', headerClassName: 'MuiDataGrid-header-override', renderCell: (params: GridRenderCellParams) => ( @@ -105,10 +98,33 @@ export const PageGateways: React.FC = () => { sx={{ ...cellStyles, justifyContent: 'flex-start' }} data-testid="location-button" > - {params.value} + <Tooltip text={params.value} id="gateway-location-text"> + <Box + sx={{ + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + }} + > + {params.value} + </Box> + </Tooltip> </Button> ), }, + { + field: 'owner', + headerName: 'Owner', + renderHeader: () => <CustomColumnHeading headingTitle="Owner" />, + width: 380, + headerAlign: 'left', + headerClassName: 'MuiDataGrid-header-override', + renderCell: (params: GridRenderCellParams) => ( + <Typography sx={cellStyles} data-testid="owner"> + {params.value} + </Typography> + ), + }, ]; const handlePageSize = (event: SelectChangeEvent<string>) => { @@ -133,7 +149,12 @@ export const PageGateways: React.FC = () => { pageSize={pageSize} searchTerm={searchTerm} /> - <UniversalDataGrid rows={gatewayToGridRow(filteredGateways)} columns={columns} pageSize={pageSize} /> + <UniversalDataGrid + pagination + rows={gatewayToGridRow(filteredGateways)} + columns={columns} + pageSize={pageSize} + /> </Card> </Grid> </Grid> diff --git a/explorer/src/pages/Mixnodes/index.tsx b/explorer/src/pages/Mixnodes/index.tsx index 8e4774e721..f37707c134 100644 --- a/explorer/src/pages/Mixnodes/index.tsx +++ b/explorer/src/pages/Mixnodes/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; -import { Button, Card, Grid, Link as MuiLink } from '@mui/material'; +import { Button, Card, Grid, Link as MuiLink, Box } from '@mui/material'; import { Link as RRDLink, useParams, useNavigate } from 'react-router-dom'; import { SelectChangeEvent } from '@mui/material/Select'; import { SxProps } from '@mui/system'; @@ -18,6 +18,7 @@ import { currencyToString } from '../../utils/currency'; import { splice } from '../../utils'; import { getMixNodeStatusColor } from '../../components/MixNodes/Status'; import { MixNodeStatusDropdown } from '../../components/MixNodes/StatusDropdown'; +import { Tooltip } from '../../components/Tooltip'; const getCellFontStyle = (theme: Theme, row: MixnodeRowType, textColor?: string) => { const color = textColor || getMixNodeStatusColor(theme, row.status); @@ -81,6 +82,24 @@ export const PageMixnodes: React.FC = () => { }; const columns: GridColDef[] = [ + { + field: 'mix_id', + headerName: 'Mix ID', + renderHeader: () => <CustomColumnHeading headingTitle="Mix ID" />, + headerClassName: 'MuiDataGrid-header-override', + width: 100, + headerAlign: 'left', + renderCell: (params: GridRenderCellParams) => ( + <MuiLink + sx={getCellStyles(theme, params.row)} + component={RRDLink} + to={`/network-components/mixnode/${params.value}`} + data-testid="mix-id" + > + {params.value} + </MuiLink> + ), + }, { field: 'identity_key', headerName: 'Identity Key', @@ -98,7 +117,7 @@ export const PageMixnodes: React.FC = () => { <MuiLink sx={getCellStyles(theme, params.row)} component={RRDLink} - to={`/network-components/mixnode/${params.value}`} + to={`/network-components/mixnode/${params.row.mix_id}`} data-testid="identity-link" > {splice(7, 29, params.value)} @@ -118,7 +137,7 @@ export const PageMixnodes: React.FC = () => { <MuiLink sx={getCellStyles(theme, params.row)} component={RRDLink} - to={`/network-components/mixnode/${params.row.identity_key}`} + to={`/network-components/mixnode/${params.row.mix_id}`} > {currencyToString(params.value)} </MuiLink> @@ -143,7 +162,7 @@ export const PageMixnodes: React.FC = () => { ...getCellStyles(theme, params.row, params.value > 100 ? 'theme.palette.warning.main' : undefined), }} component={RRDLink} - to={`/network-components/mixnode/${params.row.identity_key}`} + to={`/network-components/mixnode/${params.row.mix_id}`} > {`${params.value.toFixed(2)} %`} </MuiLink> @@ -161,7 +180,7 @@ export const PageMixnodes: React.FC = () => { <MuiLink sx={getCellStyles(theme, params.row)} component={RRDLink} - to={`/network-components/mixnode/${params.row.identity_key}`} + to={`/network-components/mixnode/${params.row.mix_id}`} > {currencyToString(params.value)} </MuiLink> @@ -183,7 +202,29 @@ export const PageMixnodes: React.FC = () => { <MuiLink sx={{ ...getCellStyles(theme, params.row), textAlign: 'left' }} component={RRDLink} - to={`/network-components/mixnode/${params.row.identity_key}`} + to={`/network-components/mixnode/${params.row.mix_id}`} + > + {params.value} + </MuiLink> + ), + }, + { + field: 'operating_cost', + headerName: 'Operating cost', + renderHeader: () => ( + <CustomColumnHeading + headingTitle="Operating cost" + tooltipInfo="Monthly operational cost of running this node. This cost is set by the operator and it influences how the rewards are split between the operator and delegators." + /> + ), + headerClassName: 'MuiDataGrid-header-override', + width: 170, + headerAlign: 'left', + renderCell: (params: GridRenderCellParams) => ( + <MuiLink + sx={{ ...getCellStyles(theme, params.row), textAlign: 'left' }} + component={RRDLink} + to={`/network-components/mixnode/${params.row.mix_id}`} > {params.value} </MuiLink> @@ -205,7 +246,7 @@ export const PageMixnodes: React.FC = () => { <MuiLink sx={{ ...getCellStyles(theme, params.row), textAlign: 'left' }} component={RRDLink} - to={`/network-components/mixnode/${params.row.identity_key}`} + to={`/network-components/mixnode/${params.row.mix_id}`} > {params.value} </MuiLink> @@ -244,7 +285,17 @@ export const PageMixnodes: React.FC = () => { justifyContent: 'flex-start', }} > - {params.value} + <Tooltip text={params.value} id="mixnode-location-text"> + <Box + sx={{ + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + }} + > + {params.value} + </Box> + </Tooltip> </Button> ), }, @@ -259,7 +310,7 @@ export const PageMixnodes: React.FC = () => { <MuiLink sx={getCellStyles(theme, params.row)} component={RRDLink} - to={`/network-components/mixnode/${params.row.identity_key}`} + to={`/network-components/mixnode/${params.row.mix_id}`} > {params.value} </MuiLink> diff --git a/explorer/src/routes/network-components.tsx b/explorer/src/routes/network-components.tsx index 73b17d108a..0118641b2e 100644 --- a/explorer/src/routes/network-components.tsx +++ b/explorer/src/routes/network-components.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { Routes as ReactRouterRoutes, Route, useNavigate } from 'react-router-dom'; import { BIG_DIPPER } from '../api/constants'; import { PageGateways } from '../pages/Gateways'; +import { PageGatewayDetail } from '../pages/GatewayDetail'; import { PageMixnodeDetail } from '../pages/MixnodeDetail'; import { PageMixnodes } from '../pages/Mixnodes'; @@ -18,6 +19,7 @@ export const NetworkComponentsRoutes: React.FC = () => ( <Route path="mixnodes" element={<PageMixnodes />} /> <Route path="mixnode/:id" element={<PageMixnodeDetail />} /> <Route path="gateways" element={<PageGateways />} /> + <Route path="gateway/:id" element={<PageGatewayDetail />} /> <Route path="validators" element={<ValidatorRoute />} /> <Route path="gateways/:id" element={<h1> Specific Gateways ID</h1>} /> </ReactRouterRoutes> diff --git a/explorer/src/typeDefs/explorer-api.ts b/explorer/src/typeDefs/explorer-api.ts index dd342ac149..a01b87a1a9 100644 --- a/explorer/src/typeDefs/explorer-api.ts +++ b/explorer/src/typeDefs/explorer-api.ts @@ -30,7 +30,6 @@ export interface MixNode { sphinx_key: string; identity_key: string; version: string; - profit_margin_percent: number; location: string; } @@ -70,6 +69,7 @@ export const toMixnodeStatus = (status?: MixnodeStatusWithAll): MixnodeStatus | }; export interface MixNodeResponseItem { + mix_id: number; pledge_amount: Amount; total_delegation: Amount; owner: string; @@ -85,6 +85,8 @@ export interface MixNodeResponseItem { mix_node: MixNode; avg_uptime: number; stake_saturation: number; + operating_cost: Amount; + profit_margin_percent: string; } export type MixNodeResponse = MixNodeResponseItem[]; @@ -126,12 +128,9 @@ export type GatewayResponse = GatewayResponseItem[]; export interface GatewayReportResponse { identity: string; owner: string; - most_recent_ipv4: boolean; - most_recent_ipv6: boolean; - last_hour_ipv4: number; - last_hour_ipv6: number; - last_day_ipv4: number; - last_day_ipv6: number; + most_recent: number; + last_hour: number; + last_day: number; } export type GatewayHistoryResponse = StatsResponse; diff --git a/explorer/src/utils/currency.ts b/explorer/src/utils/currency.ts index 599ff5e69f..65cdde8952 100644 --- a/explorer/src/utils/currency.ts +++ b/explorer/src/utils/currency.ts @@ -1,4 +1,5 @@ import { printableCoin } from '@nymproject/nym-validator-client'; +import Big from 'big.js'; const DENOM = process.env.CURRENCY_DENOM || 'unym'; const DENOM_STAKING = process.env.CURRENCY_STAKING_DENOM || 'unyx'; @@ -14,3 +15,38 @@ export const stakingCurrencyToString = (amount: string, denom: string = DENOM_ST amount, denom, }); + +/** + * Converts a decimal number to a pretty representation + * with fixed decimal places. + * + * @param val - a decimal number of string form + * @param dp - number of decimal places (4 by default ie. 0.0000) + * @returns A prettyfied decimal number + */ +export const toDisplay = (val: string | number | Big, dp = 4) => { + let displayValue; + try { + displayValue = Big(val).toFixed(dp); + } catch (e: any) { + console.warn(`${displayValue} not a valid decimal number: ${e}`); + } + return displayValue; +}; + +/** + * Converts a decimal number of μNYM (micro NYM) to NYM. + * + * @param unym - a decimal number of μNYM + * @param dp - number of decimal places (4 by default ie. 0.0000) + * @returns The corresponding decimal number in NYM + */ +export const unymToNym = (unym: string | number | Big, dp = 4) => { + let nym; + try { + nym = Big(unym).div(1_000_000).toFixed(dp); + } catch (e: any) { + console.warn(`${unym} not a valid decimal number: ${e}`); + } + return nym; +}; diff --git a/explorer/src/utils/index.ts b/explorer/src/utils/index.ts index a44515b10c..1065c000ae 100644 --- a/explorer/src/utils/index.ts +++ b/explorer/src/utils/index.ts @@ -45,3 +45,11 @@ export const splice = (start: number, deleteCount: number, address?: string): st } return ''; }; + +/** + * Converts a stringified percentage float (0.0-1.0) to a stringified integer (0-100). + * + * @param value - the percentage to convert + * @returns A stringified integer + */ +export const toPercentIntegerString = (value: string) => Math.round(Number(value) * 100).toString(); diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 178ab70c59..3cf8112ff9 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -56,6 +56,7 @@ credentials = { path = "../common/credentials" } config = { path = "../common/config" } crypto = { path = "../common/crypto" } completions = { path = "../common/completions" } +logging = { path = "../common/logging"} gateway-requests = { path = "gateway-requests" } mixnet-client = { path = "../common/client-libs/mixnet-client" } mixnode-common = { path = "../common/mixnode-common" } diff --git a/gateway/gateway-requests/src/types.rs b/gateway/gateway-requests/src/types.rs index 5206f444a8..bfa20a9273 100644 --- a/gateway/gateway-requests/src/types.rs +++ b/gateway/gateway-requests/src/types.rs @@ -82,8 +82,10 @@ impl fmt::Display for GatewayRequestsError { RequestOfInvalidSize(actual) => write!( f, - "received request had invalid size. (actual: {}, but expected one of: {} (ACK), {} (REGULAR), {} (EXTENDED))", - actual, PacketSize::AckPacket.size(), PacketSize::RegularPacket.size(), PacketSize::ExtendedPacket.size() + "received request had invalid size. (actual: {}, but expected one of: {} (ACK), {} (REGULAR), {}, {}, {} (EXTENDED))", + actual, PacketSize::AckPacket.size(), PacketSize::RegularPacket.size(), + PacketSize::ExtendedPacket8.size(), PacketSize::ExtendedPacket16.size(), + PacketSize::ExtendedPacket32.size() ), MalformedSphinxPacket => write!(f, "received sphinx packet was malformed"), MalformedEncryption => write!(f, "the received encrypted data was malformed"), diff --git a/gateway/src/config/mod.rs b/gateway/src/config/mod.rs index 5a332fed9a..30812e140f 100644 --- a/gateway/src/config/mod.rs +++ b/gateway/src/config/mod.rs @@ -289,6 +289,10 @@ impl Config { self.debug.maximum_connection_buffer_size } + pub fn get_use_legacy_sphinx_framing(&self) -> bool { + self.debug.use_legacy_framed_packet_version + } + pub fn get_message_retrieval_limit(&self) -> i64 { self.debug.message_retrieval_limit } @@ -456,6 +460,12 @@ struct Debug { /// Number of messages from offline client that can be pulled at once from the storage. message_retrieval_limit: i64, + + /// Specifies whether the mixnode should be using the legacy framing for the sphinx packets. + // it's set to true by default. The reason for that decision is to preserve compatibility with the + // existing nodes whilst everyone else is upgrading and getting the code for handling the new field. + // It shall be disabled in the subsequent releases. + use_legacy_framed_packet_version: bool, } impl Default for Debug { @@ -468,6 +478,8 @@ impl Default for Debug { maximum_connection_buffer_size: DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE, stored_messages_filename_length: DEFAULT_STORED_MESSAGE_FILENAME_LENGTH, message_retrieval_limit: DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + // TODO: remember to change it in one of future releases!! + use_legacy_framed_packet_version: true, } } } diff --git a/gateway/src/main.rs b/gateway/src/main.rs index 0651b37237..3a850333b3 100644 --- a/gateway/src/main.rs +++ b/gateway/src/main.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use clap::{crate_version, Parser}; +use logging::setup_logging; use network_defaults::setup_env; use once_cell::sync::OnceCell; @@ -88,27 +89,6 @@ fn long_version() -> String { ) } -fn setup_logging() { - let mut log_builder = pretty_env_logger::formatted_timed_builder(); - if let Ok(s) = ::std::env::var("RUST_LOG") { - log_builder.parse_filters(&s); - } else { - // default to 'Info' - log_builder.filter(None, log::LevelFilter::Info); - } - - log_builder - .filter_module("hyper", log::LevelFilter::Warn) - .filter_module("tokio_reactor", log::LevelFilter::Warn) - .filter_module("reqwest", log::LevelFilter::Warn) - .filter_module("mio", log::LevelFilter::Warn) - .filter_module("want", log::LevelFilter::Warn) - .filter_module("sled", log::LevelFilter::Warn) - .filter_module("tungstenite", log::LevelFilter::Warn) - .filter_module("tokio_tungstenite", log::LevelFilter::Warn) - .init(); -} - #[cfg(test)] mod tests { use super::*; diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index cb4c735c7a..fd31af1af7 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -211,6 +211,7 @@ where self.config.get_packet_forwarding_maximum_backoff(), self.config.get_initial_connection_timeout(), self.config.get_maximum_connection_buffer_size(), + self.config.get_use_legacy_sphinx_framing(), ); tokio::spawn(async move { packet_forwarder.run().await }); diff --git a/mixnode/Cargo.toml b/mixnode/Cargo.toml index a3cfe6c513..2fbe6bd5c6 100644 --- a/mixnode/Cargo.toml +++ b/mixnode/Cargo.toml @@ -41,6 +41,7 @@ url = { version = "2.2", features = ["serde"] } config = { path="../common/config" } crypto = { path="../common/crypto" } completions = { path="../common/completions" } +logging = { path="../common/logging" } mixnet-client = { path="../common/client-libs/mixnet-client" } mixnode-common = { path="../common/mixnode-common" } nonexhaustive-delayqueue = { path="../common/nonexhaustive-delayqueue" } diff --git a/mixnode/src/config/mod.rs b/mixnode/src/config/mod.rs index 51525c9b7f..23a80a5127 100644 --- a/mixnode/src/config/mod.rs +++ b/mixnode/src/config/mod.rs @@ -280,6 +280,10 @@ impl Config { self.debug.maximum_connection_buffer_size } + pub fn get_use_legacy_sphinx_framing(&self) -> bool { + self.debug.use_legacy_framed_packet_version + } + pub fn get_version(&self) -> &str { &self.mixnode.version } @@ -485,6 +489,12 @@ struct Debug { /// Maximum number of packets that can be stored waiting to get sent to a particular connection. maximum_connection_buffer_size: usize, + + /// Specifies whether the mixnode should be using the legacy framing for the sphinx packets. + // it's set to true by default. The reason for that decision is to preserve compatibility with the + // existing nodes whilst everyone else is upgrading and getting the code for handling the new field. + // It shall be disabled in the subsequent releases. + use_legacy_framed_packet_version: bool, } impl Default for Debug { @@ -496,6 +506,8 @@ impl Default for Debug { packet_forwarding_maximum_backoff: DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF, initial_connection_timeout: DEFAULT_INITIAL_CONNECTION_TIMEOUT, maximum_connection_buffer_size: DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE, + // TODO: remember to change it in one of future releases!! + use_legacy_framed_packet_version: true, } } } diff --git a/mixnode/src/main.rs b/mixnode/src/main.rs index 2e30e00976..d52be0c120 100644 --- a/mixnode/src/main.rs +++ b/mixnode/src/main.rs @@ -7,6 +7,7 @@ extern crate rocket; use ::config::defaults::setup_env; use clap::{crate_version, Parser}; use lazy_static::lazy_static; +use logging::setup_logging; mod commands; mod config; @@ -90,24 +91,6 @@ fn long_version() -> String { ) } -fn setup_logging() { - let mut log_builder = pretty_env_logger::formatted_timed_builder(); - if let Ok(s) = ::std::env::var("RUST_LOG") { - log_builder.parse_filters(&s); - } else { - // default to 'Info' - log_builder.filter(None, log::LevelFilter::Info); - } - - log_builder - .filter_module("hyper", log::LevelFilter::Warn) - .filter_module("tokio_reactor", log::LevelFilter::Warn) - .filter_module("reqwest", log::LevelFilter::Warn) - .filter_module("mio", log::LevelFilter::Warn) - .filter_module("want", log::LevelFilter::Warn) - .init(); -} - #[cfg(test)] mod tests { use super::*; diff --git a/mixnode/src/node/mod.rs b/mixnode/src/node/mod.rs index 10f70b147d..0bca9b31b0 100644 --- a/mixnode/src/node/mod.rs +++ b/mixnode/src/node/mod.rs @@ -200,6 +200,7 @@ impl MixNode { self.config.get_packet_forwarding_maximum_backoff(), self.config.get_initial_connection_timeout(), self.config.get_maximum_connection_buffer_size(), + self.config.get_use_legacy_sphinx_framing(), ); let mut packet_forwarder = DelayForwarder::new( diff --git a/nym-connect/Cargo.lock b/nym-connect/Cargo.lock index a0cd9ef1c0..350d542d96 100644 --- a/nym-connect/Cargo.lock +++ b/nym-connect/Cargo.lock @@ -752,7 +752,9 @@ name = "contracts-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "schemars", "serde", + "thiserror", ] [[package]] @@ -2868,6 +2870,14 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "logging" +version = "0.1.0" +dependencies = [ + "log", + "pretty_env_logger", +] + [[package]] name = "loom" version = "0.5.6" @@ -3214,6 +3224,7 @@ dependencies = [ "fix-path-env", "futures", "log", + "logging", "nym-socks5-client", "pretty_env_logger", "rand 0.8.5", @@ -3249,6 +3260,7 @@ dependencies = [ "gateway-client", "gateway-requests", "log", + "logging", "network-defaults", "nymsphinx", "ordered-buffer", @@ -6077,6 +6089,7 @@ dependencies = [ "coconut-interface", "colored", "config", + "contracts-common", "cosmrs", "cosmwasm-std", "cw3", @@ -6159,12 +6172,14 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "vesting-contract" version = "1.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", "cw-storage-plus", "mixnet-contract-common", "schemars", "serde", "thiserror", + "vergen", "vesting-contract-common", ] @@ -6172,7 +6187,9 @@ dependencies = [ name = "vesting-contract-common" version = "0.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", + "log", "mixnet-contract-common", "schemars", "serde", diff --git a/nym-connect/src-tauri/Cargo.toml b/nym-connect/src-tauri/Cargo.toml index fd6ef638cf..b7ee99f14c 100644 --- a/nym-connect/src-tauri/Cargo.toml +++ b/nym-connect/src-tauri/Cargo.toml @@ -40,6 +40,7 @@ url = "2.2" client-core = { path = "../../clients/client-core" } config-common = { path = "../../common/config", package = "config" } +logging = { path = "../../common/logging"} nym-socks5-client = { path = "../../clients/socks5" } topology = { path = "../../common/topology" } diff --git a/nym-connect/src-tauri/src/config/mod.rs b/nym-connect/src-tauri/src/config/mod.rs index 923e544a52..f4d260ea57 100644 --- a/nym-connect/src-tauri/src/config/mod.rs +++ b/nym-connect/src-tauri/src/config/mod.rs @@ -90,7 +90,7 @@ impl Config { pub fn config_file_location(id: &str) -> Result<PathBuf> { Socks5Config::try_default_config_file_path(Some(id)) - .ok_or(BackendError::CouldNotGetFilename) + .ok_or(BackendError::CouldNotGetConfigFilename) } } diff --git a/nym-connect/src-tauri/src/error.rs b/nym-connect/src-tauri/src/error.rs index 9ffaba93a6..b2b44ee345 100644 --- a/nym-connect/src-tauri/src/error.rs +++ b/nym-connect/src-tauri/src/error.rs @@ -36,12 +36,6 @@ pub enum BackendError { source: ClientCoreError, }, - #[error("State error")] - StateError, - #[error("Could not connect")] - CouldNotConnect, - #[error("Could not disconnect")] - CouldNotDisconnect, #[error("Could not send disconnect signal to the SOCKS5 client")] CoundNotSendDisconnectSignal, #[error("No service provider set")] diff --git a/nym-connect/src-tauri/src/main.rs b/nym-connect/src-tauri/src/main.rs index e1117c7516..4f4e8bb5fb 100644 --- a/nym-connect/src-tauri/src/main.rs +++ b/nym-connect/src-tauri/src/main.rs @@ -6,6 +6,7 @@ use std::sync::Arc; use config_common::defaults::setup_env; +use logging::setup_logging; use tauri::Menu; use tokio::sync::RwLock; @@ -55,22 +56,3 @@ fn main() { .run(tauri::generate_context!()) .expect("error while running tauri application"); } - -fn setup_logging() { - let mut log_builder = pretty_env_logger::formatted_timed_builder(); - if let Ok(s) = ::std::env::var("RUST_LOG") { - log_builder.parse_filters(&s); - } else { - // default to 'Info' - log_builder.filter(None, log::LevelFilter::Info); - } - - log_builder - .filter_module("handlebars", log::LevelFilter::Warn) - .filter_module("mio", log::LevelFilter::Warn) - .filter_module("sled", log::LevelFilter::Warn) - .filter_module("tokio_tungstenite", log::LevelFilter::Warn) - .filter_module("tungstenite", log::LevelFilter::Warn) - .filter_module("want", log::LevelFilter::Warn) - .init(); -} diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index e861e1b5fc..dfbfe63531 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -440,6 +440,9 @@ name = "cc" version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +dependencies = [ + "jobserver", +] [[package]] name = "cesu8" @@ -665,7 +668,9 @@ name = "contracts-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "schemars", "serde", + "thiserror", ] [[package]] @@ -1253,6 +1258,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum-iterator" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eeac5c5edb79e4e39fe8439ef35207780a11f69c52cbe424ce3dfad4cb78de6" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c134c37760b27a871ba422106eedbb8247da973a09e82558bf26d619c882b159" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "env_logger" version = "0.7.1" @@ -1719,6 +1744,19 @@ dependencies = [ "winapi", ] +[[package]] +name = "git2" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2994bee4a3a6a51eb90c218523be382fd7ea09b16380b9312e9dbe955ff7c7d1" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "url", +] + [[package]] name = "glib" version = "0.15.6" @@ -2321,6 +2359,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.56" @@ -2385,6 +2432,30 @@ version = "0.2.134" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb" +[[package]] +name = "libgit2-sys" +version = "0.14.0+1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a00859c70c8a4f7218e6d1cc32875c4b55f6799445b842b0d8ed5e4c3d959b" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + +[[package]] +name = "libz-sys" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "line-wrap" version = "0.1.1" @@ -2413,6 +2484,14 @@ dependencies = [ "serde", ] +[[package]] +name = "logging" +version = "0.1.0" +dependencies = [ + "log", + "pretty_env_logger", +] + [[package]] name = "loom" version = "0.5.4" @@ -2771,6 +2850,7 @@ dependencies = [ "bip39", "clap", "log", + "logging", "pretty_env_logger", "serde_json", ] @@ -5250,6 +5330,7 @@ dependencies = [ "coconut-interface", "colored 2.0.0", "config", + "contracts-common", "cosmrs", "cosmwasm-std", "cw3", @@ -5286,6 +5367,23 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vergen" +version = "5.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cf88d94e969e7956d924ba70741316796177fa0c79a2c9f4ab04998d96e966e" +dependencies = [ + "anyhow", + "cfg-if", + "chrono", + "enum-iterator", + "getset", + "git2", + "rustc_version 0.4.0", + "rustversion", + "thiserror", +] + [[package]] name = "version-compare" version = "0.0.11" @@ -5308,12 +5406,14 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "vesting-contract" version = "1.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", "cw-storage-plus", "mixnet-contract-common", "schemars", "serde", "thiserror", + "vergen", "vesting-contract-common", ] @@ -5321,7 +5421,9 @@ dependencies = [ name = "vesting-contract-common" version = "0.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", + "log", "mixnet-contract-common", "schemars", "serde", diff --git a/nym-wallet/nym-wallet-recovery-cli/Cargo.toml b/nym-wallet/nym-wallet-recovery-cli/Cargo.toml index e4ac1eb284..909467a6e0 100644 --- a/nym-wallet/nym-wallet-recovery-cli/Cargo.toml +++ b/nym-wallet/nym-wallet-recovery-cli/Cargo.toml @@ -15,3 +15,5 @@ clap = { version = "3.2", features = ["derive"] } log = "0.4" pretty_env_logger = "0.4" serde_json = "1.0.0" + +logging = { path = "../../common/logging" } \ No newline at end of file diff --git a/nym-wallet/nym-wallet-recovery-cli/src/main.rs b/nym-wallet/nym-wallet-recovery-cli/src/main.rs index e507581def..7cb2c3e0d7 100644 --- a/nym-wallet/nym-wallet-recovery-cli/src/main.rs +++ b/nym-wallet/nym-wallet-recovery-cli/src/main.rs @@ -12,6 +12,7 @@ use aes_gcm::{aead::Aead, Aes256Gcm, Key, NewAead, Nonce}; use anyhow::{anyhow, Result}; use argon2::{Algorithm, Argon2, Params, Version}; use clap::Parser; +use logging::setup_logging; use serde_json::Value; // Mostly defaults @@ -61,18 +62,6 @@ fn main() -> Result<()> { decrypt_file(file, &args.password, &parse) } -fn setup_logging() { - let mut log_builder = pretty_env_logger::formatted_timed_builder(); - if let Ok(s) = ::std::env::var("RUST_LOG") { - log_builder.parse_filters(&s); - } else { - // default to 'Info' - log_builder.filter(None, log::LevelFilter::Info); - } - - log_builder.init(); -} - fn decrypt_file(file: File, passwords: &[String], parse: &ParseMode) -> Result<()> { let json_file: Value = serde_json::from_reader(file)?; diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index db6795ff49..9bd44028c2 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -100,6 +100,7 @@ fn main() { validator_api::status::mixnode_reward_estimation, validator_api::status::mixnode_stake_saturation, validator_api::status::mixnode_status, + validator_api::status::gateway_report, vesting::rewards::vesting_claim_delegator_reward, vesting::rewards::vesting_claim_operator_reward, vesting::bond::vesting_bond_gateway, diff --git a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs index 7fa05ef0ea..dfbf46389b 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs @@ -266,6 +266,13 @@ pub async fn get_all_mix_delegations( pending_events.len() ); + let mixnode_is_unbonding = mixnode.as_ref().map(|m| m.is_unbonding()); + log::trace!( + " >>> mixnode with mix_id: {} is unbonding: {:?}", + d.mix_id, + mixnode_is_unbonding + ); + with_everything.push(DelegationWithEverything { owner: d.owner, mix_id: d.mix_id, @@ -283,6 +290,7 @@ pub async fn get_all_mix_delegations( cost_params, unclaimed_rewards: accumulated_rewards, pending_events, + mixnode_is_unbonding, }) } log::trace!("<<< {:?}", with_everything); diff --git a/nym-wallet/src-tauri/src/operations/validator_api/status.rs b/nym-wallet/src-tauri/src/operations/validator_api/status.rs index aefca433ce..16c83cfb67 100644 --- a/nym-wallet/src-tauri/src/operations/validator_api/status.rs +++ b/nym-wallet/src-tauri/src/operations/validator_api/status.rs @@ -6,8 +6,9 @@ use crate::error::BackendError; use crate::state::WalletState; use mixnet_contract_common::{IdentityKeyRef, MixId}; use validator_client::models::{ - GatewayCoreStatusResponse, InclusionProbabilityResponse, MixnodeCoreStatusResponse, - MixnodeStatusResponse, RewardEstimationResponse, StakeSaturationResponse, + GatewayCoreStatusResponse, GatewayStatusReportResponse, InclusionProbabilityResponse, + MixnodeCoreStatusResponse, MixnodeStatusResponse, RewardEstimationResponse, + StakeSaturationResponse, }; #[tauri::command] @@ -32,6 +33,14 @@ pub async fn gateway_core_node_status( .await?) } +#[tauri::command] +pub async fn gateway_report( + identity: IdentityKeyRef<'_>, + state: tauri::State<'_, WalletState>, +) -> Result<GatewayStatusReportResponse, BackendError> { + Ok(api_client!(state).get_gateway_report(identity).await?) +} + #[tauri::command] pub async fn mixnode_status( mix_id: MixId, diff --git a/nym-wallet/src/components/Accounts/MultiAccountHowTo.tsx b/nym-wallet/src/components/Accounts/MultiAccountHowTo.tsx index 5cfb4fd0b2..c23f392bbe 100644 --- a/nym-wallet/src/components/Accounts/MultiAccountHowTo.tsx +++ b/nym-wallet/src/components/Accounts/MultiAccountHowTo.tsx @@ -34,7 +34,12 @@ export const MultiAccountHowTo = ({ show, handleClose }: { show: boolean; handle <Typography>{`${step}`}</Typography> </Stack> ))} - <Link href="todo" target="_blank" text="Open Nym docs for this guide in a browser window" fontWeight={600} /> + <Link + href="https://nymtech.net/docs/stable/wallet/#importing-or-creating-accounts-when-you-have-signed-in-with-mnemonic" + target="_blank" + text="Open Nym docs for this guide in a browser window" + fontWeight={600} + /> </Stack> </SimpleModal> ); diff --git a/nym-wallet/src/components/Accounts/MultiAccountWithPwdHowTo.tsx b/nym-wallet/src/components/Accounts/MultiAccountWithPwdHowTo.tsx index cd1e6a5be5..c5cda54150 100644 --- a/nym-wallet/src/components/Accounts/MultiAccountWithPwdHowTo.tsx +++ b/nym-wallet/src/components/Accounts/MultiAccountWithPwdHowTo.tsx @@ -38,7 +38,12 @@ export const MultiAccountWithPwdHowTo = ({ show, handleClose }: { show: boolean; <Typography>{`${step}`}</Typography> </Stack> ))} - <Link href="todo" target="_blank" text="Open Nym docs for this guide in a browser window" fontWeight={600} /> + <Link + href="https://nymtech.net/docs/stable/wallet#importing-or-creating-accounts-when-you-have-signed-in-with-mnemonic-but-a-password-already-exists-on-your-machine" + target="_blank" + text="Open Nym docs for this guide in a browser window" + fontWeight={600} + /> </Stack> </SimpleModal> ); diff --git a/nym-wallet/src/components/Accounts/modals/AddAccountModal.tsx b/nym-wallet/src/components/Accounts/modals/AddAccountModal.tsx index 8a76dba709..61d6f7b9e5 100644 --- a/nym-wallet/src/components/Accounts/modals/AddAccountModal.tsx +++ b/nym-wallet/src/components/Accounts/modals/AddAccountModal.tsx @@ -122,6 +122,7 @@ const NameAccount = ({ onNext, onBack }: { onNext: (value: string) => void; onBa setError(undefined); }} fullWidth + InputLabelProps={{ shrink: true }} /> </DialogContent> <DialogActions sx={{ p: 3, pt: 0, gap: 2 }}> diff --git a/nym-wallet/src/components/Accounts/modals/EditAccountModal.tsx b/nym-wallet/src/components/Accounts/modals/EditAccountModal.tsx index 8ddbb207b3..e5dbd0464e 100644 --- a/nym-wallet/src/components/Accounts/modals/EditAccountModal.tsx +++ b/nym-wallet/src/components/Accounts/modals/EditAccountModal.tsx @@ -55,6 +55,7 @@ export const EditAccountModal = () => { value={accountName} onChange={(e) => setAccountName(e.target.value)} autoFocus + InputLabelProps={{ shrink: true }} /> </Box> </DialogContent> diff --git a/nym-wallet/src/components/Bonding/BondedGateway.tsx b/nym-wallet/src/components/Bonding/BondedGateway.tsx index 15a5d26367..8b2776f8a9 100644 --- a/nym-wallet/src/components/Bonding/BondedGateway.tsx +++ b/nym-wallet/src/components/Bonding/BondedGateway.tsx @@ -9,18 +9,27 @@ import { Cell, Header, NodeTable } from './NodeTable'; import { BondedGatewayActions, TBondedGatwayActions } from './BondedGatewayAction'; const headers: Header[] = [ - { - header: 'IP', - id: 'ip', - sx: { pl: 0 }, - }, { header: 'Bond', id: 'bond', }, + + { + header: 'Routing score', + id: 'routing-score', + tooltipText: 'Routing score', + }, + { + header: 'Average score', + id: 'average-score', + tooltipText: 'Average score', + }, + { + header: 'IP', + id: 'ip', + }, { id: 'menu-button', - sx: { width: 34, maxWidth: 34 }, }, ]; @@ -33,18 +42,26 @@ export const BondedGateway = ({ network?: Network; onActionSelect: (action: TBondedGatwayActions) => void; }) => { - const { name, bond, ip, identityKey } = gateway; + const { name, bond, ip, identityKey, routingScore } = gateway; const cells: Cell[] = [ - { - cell: ip, - id: 'ip-cell', - }, { cell: `${bond.amount} ${bond.denom}`, id: 'bond-cell', sx: { pl: 0 }, }, + { + cell: `${routingScore?.current || '- '}%`, + id: 'routing-score-cell', + }, + { + cell: `${routingScore?.average || '- '}%`, + id: 'average-score-cell', + }, + { + cell: ip, + id: 'ip-cell', + }, { cell: <BondedGatewayActions onActionSelect={onActionSelect} />, id: 'actions-cell', @@ -73,7 +90,7 @@ export const BondedGateway = ({ <NodeTable headers={headers} cells={cells} /> {network && ( <Typography sx={{ mt: 2, fontSize: 'small' }}> - Check more stats of your node on the{' '} + Check more stats of your gateway on the{' '} <Link href={`${urls(network).networkExplorer}/network-components/gateways`} target="_blank"> explorer </Link> diff --git a/nym-wallet/src/components/Bonding/BondedMixnode.tsx b/nym-wallet/src/components/Bonding/BondedMixnode.tsx index a4df67a9d3..a30e5557cb 100644 --- a/nym-wallet/src/components/Bonding/BondedMixnode.tsx +++ b/nym-wallet/src/components/Bonding/BondedMixnode.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { useNavigate } from 'react-router-dom'; -import { Box, Button, Stack, Tooltip, Typography } from '@mui/material'; +import { Box, Button, Chip, Stack, Tooltip, Typography } from '@mui/material'; import { Link } from '@nymproject/react/link/Link'; import { isMixnode, Network } from 'src/types'; import { TBondedMixnode, urls } from 'src/context'; @@ -35,7 +35,8 @@ const headers: Header[] = [ { header: 'Operating cost', id: 'operator-cost', - // tooltipText: 'TODO', // TODO + tooltipText: + 'Monthly operational costs of running your node. The cost also influences how the rewards are split between you and your delegators. ', }, { header: 'Operator rewards', @@ -106,7 +107,9 @@ export const BondedMixnode = ({ id: 'delegators-cell', }, { - cell: ( + cell: mixnode.isUnbonding ? ( + <Chip label="Pending unbond" sx={{ textTransform: 'initial' }} /> + ) : ( <BondedMixnodeActions onActionSelect={onActionSelect} disabledRedeemAndCompound={(operatorRewards && Number(operatorRewards.amount) === 0) || false} @@ -142,14 +145,19 @@ export const BondedMixnode = ({ } Action={ isMixnode(mixnode) && ( - <Button - variant="text" - color="secondary" - onClick={() => navigate('/bonding/node-settings')} - startIcon={<NodeIcon />} - > - Node Settings - </Button> + <Tooltip title={mixnode.isUnbonding ? 'You have a pending unbond event. Node settings are disabled.' : ''}> + <Box> + <Button + variant="text" + color="secondary" + onClick={() => navigate('/bonding/node-settings')} + startIcon={<NodeIcon />} + disabled={mixnode.isUnbonding} + > + Node Settings + </Button> + </Box> + </Tooltip> ) } > diff --git a/nym-wallet/src/components/Bonding/forms/BondGatewayForm.tsx b/nym-wallet/src/components/Bonding/forms/BondGatewayForm.tsx index 12403f5e98..bf0511d51a 100644 --- a/nym-wallet/src/components/Bonding/forms/BondGatewayForm.tsx +++ b/nym-wallet/src/components/Bonding/forms/BondGatewayForm.tsx @@ -47,6 +47,7 @@ const NodeFormData = ({ gatewayData, onNext }: { gatewayData: GatewayData; onNex label="Sphinx key" error={Boolean(errors.sphinxKey)} helperText={errors.sphinxKey?.message} + InputLabelProps={{ shrink: true }} /> <TextField {...register('ownerSignature')} @@ -54,6 +55,7 @@ const NodeFormData = ({ gatewayData, onNext }: { gatewayData: GatewayData; onNex label="Owner signature" error={Boolean(errors.ownerSignature)} helperText={errors.ownerSignature?.message} + InputLabelProps={{ shrink: true }} /> <TextField {...register('location')} @@ -62,6 +64,7 @@ const NodeFormData = ({ gatewayData, onNext }: { gatewayData: GatewayData; onNex error={Boolean(errors.location)} helperText={errors.location?.message} required + InputLabelProps={{ shrink: true }} sx={{ flexBasis: '50%' }} /> <Stack direction="row" gap={3}> @@ -72,6 +75,7 @@ const NodeFormData = ({ gatewayData, onNext }: { gatewayData: GatewayData; onNex error={Boolean(errors.host)} helperText={errors.host?.message} required + InputLabelProps={{ shrink: true }} sx={{ flexBasis: '50%' }} /> <TextField @@ -81,6 +85,7 @@ const NodeFormData = ({ gatewayData, onNext }: { gatewayData: GatewayData; onNex error={Boolean(errors.version)} helperText={errors.version?.message} required + InputLabelProps={{ shrink: true }} sx={{ flexBasis: '50%' }} /> </Stack> @@ -97,6 +102,7 @@ const NodeFormData = ({ gatewayData, onNext }: { gatewayData: GatewayData; onNex error={Boolean(errors.mixPort)} helperText={errors.mixPort?.message} fullWidth + InputLabelProps={{ shrink: true }} /> <TextField {...register('clientsPort')} @@ -105,6 +111,7 @@ const NodeFormData = ({ gatewayData, onNext }: { gatewayData: GatewayData; onNex error={Boolean(errors.clientsPort)} helperText={errors.clientsPort?.message} fullWidth + InputLabelProps={{ shrink: true }} /> </Stack> )} diff --git a/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx b/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx index 79cd8617e0..721d114961 100644 --- a/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx +++ b/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx @@ -47,6 +47,7 @@ const NodeFormData = ({ mixnodeData, onNext }: { mixnodeData: MixnodeData; onNex label="Sphinx key" error={Boolean(errors.sphinxKey)} helperText={errors.sphinxKey?.message} + InputLabelProps={{ shrink: true }} /> <TextField {...register('ownerSignature')} @@ -54,6 +55,7 @@ const NodeFormData = ({ mixnodeData, onNext }: { mixnodeData: MixnodeData; onNex label="Owner signature" error={Boolean(errors.ownerSignature)} helperText={errors.ownerSignature?.message} + InputLabelProps={{ shrink: true }} /> <Stack direction="row" gap={3}> <TextField @@ -63,6 +65,7 @@ const NodeFormData = ({ mixnodeData, onNext }: { mixnodeData: MixnodeData; onNex error={Boolean(errors.host)} helperText={errors.host?.message} required + InputLabelProps={{ shrink: true }} sx={{ flexBasis: '50%' }} /> <TextField @@ -72,6 +75,7 @@ const NodeFormData = ({ mixnodeData, onNext }: { mixnodeData: MixnodeData; onNex error={Boolean(errors.version)} helperText={errors.version?.message} required + InputLabelProps={{ shrink: true }} sx={{ flexBasis: '50%' }} /> </Stack> @@ -88,6 +92,7 @@ const NodeFormData = ({ mixnodeData, onNext }: { mixnodeData: MixnodeData; onNex error={Boolean(errors.mixPort)} helperText={errors.mixPort?.message} fullWidth + InputLabelProps={{ shrink: true }} /> <TextField {...register('verlocPort')} @@ -96,6 +101,7 @@ const NodeFormData = ({ mixnodeData, onNext }: { mixnodeData: MixnodeData; onNex error={Boolean(errors.verlocPort)} helperText={errors.verlocPort?.message} fullWidth + InputLabelProps={{ shrink: true }} /> <TextField {...register('httpApiPort')} @@ -104,6 +110,7 @@ const NodeFormData = ({ mixnodeData, onNext }: { mixnodeData: MixnodeData; onNex error={Boolean(errors.httpApiPort)} helperText={errors.httpApiPort?.message} fullWidth + InputLabelProps={{ shrink: true }} /> </Stack> )} diff --git a/nym-wallet/src/components/Bonding/modals/BondMoreModal.tsx b/nym-wallet/src/components/Bonding/modals/BondMoreModal.tsx index 39aeb6443c..88518abc28 100644 --- a/nym-wallet/src/components/Bonding/modals/BondMoreModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/BondMoreModal.tsx @@ -100,7 +100,13 @@ export const BondMoreModal = ({ </Box> <Box> - <TextField fullWidth label="Signature" value={signature} onChange={(e) => setSignature(e.target.value)} /> + <TextField + fullWidth + label="Signature" + value={signature} + onChange={(e) => setSignature(e.target.value)} + InputLabelProps={{ shrink: true }} + /> {errorSignature && <FormHelperText sx={{ color: 'error.main' }}>Invalid signature</FormHelperText>} </Box> diff --git a/nym-wallet/src/components/Bonding/modals/NodeSettingsModal.tsx b/nym-wallet/src/components/Bonding/modals/NodeSettingsModal.tsx index 34b35e7a3f..3a8dca5576 100644 --- a/nym-wallet/src/components/Bonding/modals/NodeSettingsModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/NodeSettingsModal.tsx @@ -114,7 +114,13 @@ export const NodeSettings = ({ Set profit margin </Typography> <Box sx={{ mb: 3 }}> - <TextField label="Profit margin" value={pm} onChange={(e) => setPm(e.target.value)} fullWidth /> + <TextField + label="Profit margin" + value={pm} + onChange={(e) => setPm(e.target.value)} + fullWidth + InputLabelProps={{ shrink: true }} + /> {error && ( <FormHelperText sx={{ color: 'error.main' }}> Profit margin should be a number between 0 and 100 diff --git a/nym-wallet/src/components/Delegation/DelegationActions.tsx b/nym-wallet/src/components/Delegation/DelegationActions.tsx index 17c45bff77..653e2a80e2 100644 --- a/nym-wallet/src/components/Delegation/DelegationActions.tsx +++ b/nym-wallet/src/components/Delegation/DelegationActions.tsx @@ -66,9 +66,9 @@ export const DelegationActions: React.FC<{ export const DelegationsActionsMenu: React.FC<{ onActionClick?: (action: DelegationListItemActions) => void; - isPending?: DelegationEventKind; disableRedeemingRewards?: boolean; -}> = ({ disableRedeemingRewards, onActionClick, isPending }) => { + disableDelegateMore?: boolean | null; +}> = ({ disableRedeemingRewards, disableDelegateMore, onActionClick }) => { const [isOpen, setIsOpen] = useState(false); const handleOpenMenu = () => setIsOpen(true); @@ -79,21 +79,15 @@ export const DelegationsActionsMenu: React.FC<{ handleOnClose(); }; - if (isPending) { - return ( - <Box py={0.5} fontSize="inherit" minWidth={MIN_WIDTH} minHeight={BUTTON_SIZE}> - <Tooltip title="There will be a new epoch roughly every hour when your changes will take effect" arrow> - <Typography fontSize="inherit" color="text.disabled"> - Pending {isPending === 'Delegate' ? 'delegation' : 'undelegation'}... - </Typography> - </Tooltip> - </Box> - ); - } - return ( <ActionsMenu open={isOpen} onOpen={handleOpenMenu} onClose={handleOnClose}> - <ActionsMenuItem title="Delegate more" Icon={<Delegate />} onClick={() => handleActionSelect('delegate')} /> + <ActionsMenuItem + title="Delegate more" + description={disableDelegateMore ? 'This node is unbonding, action disabled.' : undefined} + Icon={<Delegate />} + onClick={() => handleActionSelect('delegate')} + disabled={Boolean(disableDelegateMore)} + /> <ActionsMenuItem title="Undelegate" Icon={<Undelegate />} onClick={() => handleActionSelect('undelegate')} /> <ActionsMenuItem title="Redeem" diff --git a/nym-wallet/src/components/Delegation/DelegationItem.tsx b/nym-wallet/src/components/Delegation/DelegationItem.tsx new file mode 100644 index 0000000000..2906ed2abd --- /dev/null +++ b/nym-wallet/src/components/Delegation/DelegationItem.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import { Chip, IconButton, TableCell, TableRow, Tooltip, Typography } from '@mui/material'; +import { Link } from '@nymproject/react/link/Link'; +import { decimalToPercentage, DelegationWithEverything } from '@nymproject/types'; +import { isDelegation } from 'src/context/delegations'; +import { toPercentIntegerString } from 'src/utils'; +import { format } from 'date-fns'; +import { Undelegate } from 'src/svg-icons'; +import { DelegationListItemActions, DelegationsActionsMenu } from './DelegationActions'; + +const getStakeSaturation = (item: DelegationWithEverything) => + !item.stake_saturation ? '-' : `${decimalToPercentage(item.stake_saturation)}%`; + +const getRewardValue = (item: DelegationWithEverything) => { + const { unclaimed_rewards } = item; + return !unclaimed_rewards ? '-' : `${unclaimed_rewards.amount} ${unclaimed_rewards.denom}`; +}; + +export const DelegationItem = ({ + item, + explorerUrl, + nodeIsUnbonded, + onItemActionClick, +}: { + item: DelegationWithEverything; + explorerUrl: string; + nodeIsUnbonded: boolean; + onItemActionClick?: (item: DelegationWithEverything, action: DelegationListItemActions) => void; +}) => { + const operatingCost = isDelegation(item) && item.cost_params?.interval_operating_cost; + + return ( + <Tooltip + arrow + title={ + nodeIsUnbonded + ? 'This node has unbonded and it does not exist anymore. You need to undelegate from it to get your stake and outstanding rewards (if any) back.' + : '' + } + > + <TableRow key={item.node_identity} sx={{ color: !item.node_identity ? 'error.main' : 'inherit' }}> + <TableCell sx={{ color: 'inherit', pr: 1 }} padding="normal"> + {nodeIsUnbonded ? ( + '-' + ) : ( + <Link + target="_blank" + href={`${explorerUrl}/network-components/mixnode/${item.node_identity}`} + text={`${item.node_identity.slice(0, 6)}...${item.node_identity.slice(-6)}`} + color="text.primary" + noIcon + /> + )} + </TableCell> + <TableCell sx={{ color: 'inherit' }}> + {isDelegation(item) && (!item.avg_uptime_percent ? '-' : `${item.avg_uptime_percent}%`)} + </TableCell> + <TableCell sx={{ color: 'inherit' }}> + {isDelegation(item) && + (!item.cost_params?.profit_margin_percent + ? '-' + : `${toPercentIntegerString(item.cost_params.profit_margin_percent)}%`)} + </TableCell> + <TableCell sx={{ color: 'inherit' }}> + <Typography style={{ textTransform: 'uppercase', fontSize: 'inherit' }}> + {operatingCost ? `${operatingCost.amount} ${operatingCost.denom}` : '-'} + </Typography> + </TableCell> + <TableCell sx={{ color: 'inherit' }}>{getStakeSaturation(item)}</TableCell> + <TableCell sx={{ color: 'inherit' }}> + {format(new Date(item.delegated_on_iso_datetime), 'dd/MM/yyyy')} + </TableCell> + <TableCell sx={{ color: 'inherit' }}> + <Typography style={{ textTransform: 'uppercase', fontSize: 'inherit' }}> + {isDelegation(item) ? `${item.amount.amount} ${item.amount.denom}` : '-'} + </Typography> + </TableCell> + <TableCell sx={{ textTransform: 'uppercase', color: 'inherit' }}>{getRewardValue(item)}</TableCell> + <TableCell align="right" sx={{ color: 'inherit' }}> + {!item.pending_events.length && !nodeIsUnbonded && ( + <DelegationsActionsMenu + onActionClick={(action) => (onItemActionClick ? onItemActionClick(item, action) : undefined)} + disableRedeemingRewards={!item.unclaimed_rewards || item.unclaimed_rewards.amount === '0'} + disableDelegateMore={item.mixnode_is_unbonding} + /> + )} + {!item.pending_events.length && nodeIsUnbonded && ( + <IconButton sx={{ color: (t) => t.palette.nym.nymWallet.text.main }}> + <Undelegate onClick={() => (onItemActionClick ? onItemActionClick(item, 'undelegate') : undefined)} /> + </IconButton> + )} + {item.pending_events.length > 0 && ( + <Tooltip + title="Your changes will take effect when the new epoch starts. There is a new epoch every hour." + arrow + > + <Chip label="Pending Events" /> + </Tooltip> + )} + </TableCell> + </TableRow> + </Tooltip> + ); +}; diff --git a/nym-wallet/src/components/Delegation/DelegationList.stories.tsx b/nym-wallet/src/components/Delegation/DelegationList.stories.tsx index e7e993bcd1..57ed73c380 100644 --- a/nym-wallet/src/components/Delegation/DelegationList.stories.tsx +++ b/nym-wallet/src/components/Delegation/DelegationList.stories.tsx @@ -13,7 +13,7 @@ const explorerUrl = 'https://sandbox-explorer.nymtech.net/network-components/mix export const items: DelegationWithEverything[] = [ { - mix_id: 1234, + mix_id: 1, node_identity: 'FiojKW7oY9WQmLCiYAsCA21tpowZHS6zcUoyYm319p6Z', delegated_on_iso_datetime: new Date(2021, 1, 1).toDateString(), unclaimed_rewards: { amount: '0.05', denom: 'nym' }, @@ -33,9 +33,10 @@ export const items: DelegationWithEverything[] = [ avg_uptime_percent: 0.5, uses_vesting_contract_tokens: false, pending_events: [], + mixnode_is_unbonding: true, }, { - mix_id: 5678, + mix_id: 2, node_identity: 'DT8S942S8AQs2zKHS9SVo1GyHmuca3pfL2uLhLksJ3D8', amount: { amount: '100', denom: 'nym' }, delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(), @@ -55,6 +56,237 @@ export const items: DelegationWithEverything[] = [ avg_uptime_percent: 0.1, uses_vesting_contract_tokens: true, pending_events: [], + mixnode_is_unbonding: true, + }, + { + mix_id: 3, + node_identity: '', + amount: { amount: '100', denom: 'nym' }, + delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(), + unclaimed_rewards: { amount: '0.05', denom: 'nym' }, + cost_params: { + profit_margin_percent: '0.1122323949234', + interval_operating_cost: { + amount: '40', + denom: 'nym', + }, + }, + accumulated_by_delegates: { amount: '50', denom: 'nym' }, + accumulated_by_operator: { amount: '100', denom: 'nym' }, + owner: '', + block_height: BigInt(4000), + stake_saturation: '0.5', + avg_uptime_percent: 0.1, + uses_vesting_contract_tokens: true, + pending_events: [], + mixnode_is_unbonding: true, + }, + { + mix_id: 4, + node_identity: 'DT8S942S8AQs2zKHS9SVo1GyHmuca3pfL2uLhLksJ3D8', + amount: { amount: '100', denom: 'nym' }, + delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(), + unclaimed_rewards: { amount: '0.05', denom: 'nym' }, + cost_params: { + profit_margin_percent: '0.1122323949234', + interval_operating_cost: { + amount: '40', + denom: 'nym', + }, + }, + accumulated_by_delegates: { amount: '50', denom: 'nym' }, + accumulated_by_operator: { amount: '100', denom: 'nym' }, + owner: '', + block_height: BigInt(4000), + stake_saturation: '0.5', + avg_uptime_percent: 0.1, + uses_vesting_contract_tokens: true, + pending_events: [], + mixnode_is_unbonding: true, + }, + { + mix_id: 5, + node_identity: 'DT8S942S8AQs2zKHS9SVo1GyHmuca3pfL2uLhLksJ3D8', + amount: { amount: '100', denom: 'nym' }, + delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(), + unclaimed_rewards: { amount: '0.05', denom: 'nym' }, + cost_params: { + profit_margin_percent: '0.1122323949234', + interval_operating_cost: { + amount: '40', + denom: 'nym', + }, + }, + accumulated_by_delegates: { amount: '50', denom: 'nym' }, + accumulated_by_operator: { amount: '100', denom: 'nym' }, + owner: '', + block_height: BigInt(4000), + stake_saturation: '0.5', + avg_uptime_percent: 0.1, + uses_vesting_contract_tokens: true, + pending_events: [], + mixnode_is_unbonding: true, + }, + { + mix_id: 6, + node_identity: '', + amount: { amount: '100', denom: 'nym' }, + delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(), + unclaimed_rewards: { amount: '0.05', denom: 'nym' }, + cost_params: { + profit_margin_percent: '0.1122323949234', + interval_operating_cost: { + amount: '40', + denom: 'nym', + }, + }, + accumulated_by_delegates: { amount: '50', denom: 'nym' }, + accumulated_by_operator: { amount: '100', denom: 'nym' }, + owner: '', + block_height: BigInt(4000), + stake_saturation: '0.5', + avg_uptime_percent: 0.1, + uses_vesting_contract_tokens: true, + pending_events: [], + mixnode_is_unbonding: true, + }, + { + mix_id: 7, + node_identity: 'FiojKW7oY9WQmLCiYAsCA21tpowZHS6zcUoyYm319p6Z', + delegated_on_iso_datetime: new Date(2021, 1, 1).toDateString(), + unclaimed_rewards: { amount: '0.05', denom: 'nym' }, + amount: { amount: '10', denom: 'nym' }, + cost_params: { + profit_margin_percent: '0.1122323949234', + interval_operating_cost: { + amount: '40', + denom: 'nym', + }, + }, + accumulated_by_delegates: { amount: '50', denom: 'nym' }, + accumulated_by_operator: { amount: '100', denom: 'nym' }, + owner: '', + block_height: BigInt(100), + stake_saturation: '0.5', + avg_uptime_percent: 0.5, + uses_vesting_contract_tokens: false, + pending_events: [], + mixnode_is_unbonding: true, + }, + { + mix_id: 8, + node_identity: 'DT8S942S8AQs2zKHS9SVo1GyHmuca3pfL2uLhLksJ3D8', + amount: { amount: '100', denom: 'nym' }, + delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(), + unclaimed_rewards: { amount: '0.05', denom: 'nym' }, + cost_params: { + profit_margin_percent: '0.1122323949234', + interval_operating_cost: { + amount: '40', + denom: 'nym', + }, + }, + accumulated_by_delegates: { amount: '50', denom: 'nym' }, + accumulated_by_operator: { amount: '100', denom: 'nym' }, + owner: '', + block_height: BigInt(4000), + stake_saturation: '0.5', + avg_uptime_percent: 0.1, + uses_vesting_contract_tokens: true, + pending_events: [], + mixnode_is_unbonding: true, + }, + { + mix_id: 9, + node_identity: '', + amount: { amount: '100', denom: 'nym' }, + delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(), + unclaimed_rewards: { amount: '0.05', denom: 'nym' }, + cost_params: { + profit_margin_percent: '0.1122323949234', + interval_operating_cost: { + amount: '40', + denom: 'nym', + }, + }, + accumulated_by_delegates: { amount: '50', denom: 'nym' }, + accumulated_by_operator: { amount: '100', denom: 'nym' }, + owner: '', + block_height: BigInt(4000), + stake_saturation: '0.5', + avg_uptime_percent: 0.1, + uses_vesting_contract_tokens: true, + pending_events: [], + mixnode_is_unbonding: true, + }, + { + mix_id: 10, + node_identity: 'DT8S942S8AQs2zKHS9SVo1GyHmuca3pfL2uLhLksJ3D8', + amount: { amount: '100', denom: 'nym' }, + delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(), + unclaimed_rewards: { amount: '0.05', denom: 'nym' }, + cost_params: { + profit_margin_percent: '0.1122323949234', + interval_operating_cost: { + amount: '40', + denom: 'nym', + }, + }, + accumulated_by_delegates: { amount: '50', denom: 'nym' }, + accumulated_by_operator: { amount: '100', denom: 'nym' }, + owner: '', + block_height: BigInt(4000), + stake_saturation: '0.5', + avg_uptime_percent: 0.1, + uses_vesting_contract_tokens: true, + pending_events: [], + mixnode_is_unbonding: true, + }, + { + mix_id: 11, + node_identity: 'DT8S942S8AQs2zKHS9SVo1GyHmuca3pfL2uLhLksJ3D8', + amount: { amount: '100', denom: 'nym' }, + delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(), + unclaimed_rewards: { amount: '0.05', denom: 'nym' }, + cost_params: { + profit_margin_percent: '0.1122323949234', + interval_operating_cost: { + amount: '40', + denom: 'nym', + }, + }, + accumulated_by_delegates: { amount: '50', denom: 'nym' }, + accumulated_by_operator: { amount: '100', denom: 'nym' }, + owner: '', + block_height: BigInt(4000), + stake_saturation: '0.5', + avg_uptime_percent: 0.1, + uses_vesting_contract_tokens: true, + pending_events: [], + mixnode_is_unbonding: true, + }, + { + mix_id: 12, + node_identity: '', + amount: { amount: '100', denom: 'nym' }, + delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(), + unclaimed_rewards: { amount: '0.05', denom: 'nym' }, + cost_params: { + profit_margin_percent: '0.1122323949234', + interval_operating_cost: { + amount: '40', + denom: 'nym', + }, + }, + accumulated_by_delegates: { amount: '50', denom: 'nym' }, + accumulated_by_operator: { amount: '100', denom: 'nym' }, + owner: '', + block_height: BigInt(4000), + stake_saturation: '0.5', + avg_uptime_percent: 0.1, + uses_vesting_contract_tokens: true, + pending_events: [], + mixnode_is_unbonding: true, }, ]; @@ -64,4 +296,4 @@ export const Empty = () => <DelegationList items={[]} explorerUrl={explorerUrl} export const OneItem = () => <DelegationList items={[items[0]]} explorerUrl={explorerUrl} />; -export const Loading = () => <DelegationList isLoading explorerUrl={explorerUrl} />; +export const Loading = () => <DelegationList items={[]} isLoading explorerUrl={explorerUrl} />; diff --git a/nym-wallet/src/components/Delegation/DelegationList.tsx b/nym-wallet/src/components/Delegation/DelegationList.tsx index 436b5e027d..a6c2ac1bcf 100644 --- a/nym-wallet/src/components/Delegation/DelegationList.tsx +++ b/nym-wallet/src/components/Delegation/DelegationList.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { Box, - Chip, CircularProgress, Table, TableBody, @@ -10,17 +9,14 @@ import { TableHead, TableRow, TableSortLabel, - Tooltip, - Typography, } from '@mui/material'; import { visuallyHidden } from '@mui/utils'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; -import { decimalToPercentage, DelegationWithEverything } from '@nymproject/types'; -import { Link } from '@nymproject/react/link/Link'; -import { format } from 'date-fns'; -import { DelegationListItemActions, DelegationsActionsMenu } from './DelegationActions'; +import { DelegationWithEverything } from '@nymproject/types'; +import { DelegationListItemActions } from './DelegationActions'; import { DelegationWithEvent, isDelegation, isPendingDelegation, TDelegations } from '../../context/delegations'; -import { toPercentIntegerString } from '../../utils'; +import { DelegationItem } from './DelegationItem'; +import { PendingDelegationItem } from './PendingDelegationItem'; type Order = 'asc' | 'desc'; @@ -42,44 +38,13 @@ const headCells: HeadCell[] = [ { id: 'node_identity', label: 'Node ID', sortable: true, align: 'left' }, { id: 'avg_uptime_percent', label: 'Routing score', sortable: true, align: 'left' }, { id: 'profit_margin_percent', label: 'Profit margin', sortable: true, align: 'left' }, + { id: 'operating_cost', label: 'Operating Cost', sortable: true, align: 'left' }, { id: 'stake_saturation', label: 'Stake saturation', sortable: true, align: 'left' }, { id: 'delegated_on_iso_datetime', label: 'Delegated on', sortable: true, align: 'left' }, { id: 'amount', label: 'Delegation', sortable: true, align: 'left' }, { id: 'unclaimed_rewards', label: 'Reward', sortable: true, align: 'left' }, ]; -function descendingComparator(a: any, b: any, orderBy: string) { - if (b[orderBy] < a[orderBy]) { - return -1; - } - if (b[orderBy] > a[orderBy]) { - return 1; - } - return 0; -} -// Sorting function needs fixing - -function sortPendingDelegation(a: DelegationWithEvent, b: DelegationWithEvent) { - if (isPendingDelegation(a) && isPendingDelegation(b)) return 0; - if (isPendingDelegation(b)) return -1; - if (isPendingDelegation(a)) return 1; - return 2; -} - -function getComparator(order: Order, orderBy: string): (a: DelegationWithEvent, b: DelegationWithEvent) => number { - return order === 'desc' - ? (a, b) => { - const pendingSort = sortPendingDelegation(a, b); - if (pendingSort === 2) return descendingComparator(a, b, orderBy); - return pendingSort; - } - : (a, b) => { - const pendingSort = -sortPendingDelegation(a, b); - if (pendingSort === 2) return -descendingComparator(a, b, orderBy); - return pendingSort; - }; -} - const EnhancedTableHead: React.FC<EnhancedTableProps> = ({ order, orderBy, onRequestSort }) => { const createSortHandler = (property: string) => (event: React.MouseEvent<unknown>) => { onRequestSort(event, property); @@ -117,9 +82,14 @@ const EnhancedTableHead: React.FC<EnhancedTableProps> = ({ order, orderBy, onReq ); }; +const sortByUnbondedMixnodeFirst = (a: DelegationWithEvent) => { + if (!a.node_identity) return -1; + return 1; +}; + export const DelegationList: React.FC<{ isLoading?: boolean; - items?: TDelegations; + items: TDelegations; onItemActionClick?: (item: DelegationWithEverything, action: DelegationListItemActions) => void; explorerUrl: string; // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -133,89 +103,25 @@ export const DelegationList: React.FC<{ setOrderBy(property); }; - const getStakeSaturation = (item: DelegationWithEvent) => { - if (isDelegation(item)) { - return !item.stake_saturation ? '-' : `${decimalToPercentage(item.stake_saturation)}%`; - } - return ''; - }; - - const getRewardValue = (item: DelegationWithEvent) => { - if (isPendingDelegation(item)) { - return ''; - } - // eslint-disable-next-line @typescript-eslint/naming-convention - const { unclaimed_rewards } = item; - return !unclaimed_rewards ? '-' : `${unclaimed_rewards.amount} ${unclaimed_rewards.denom}`; - }; - return ( <TableContainer> <Table sx={{ width: '100%' }}> <EnhancedTableHead order={order} orderBy={orderBy} onRequestSort={handleRequestSort} /> <TableBody> - {items?.length ? ( - items.map((item) => ( - <TableRow key={item.node_identity}> - <TableCell> - <Link - target="_blank" - href={`${explorerUrl}/network-components/mixnode/${item.node_identity}`} - text={`${item.node_identity.slice(0, 6)}...${item.node_identity.slice(-6)}`} - color="text.primary" - noIcon + {items.length ? ( + items.sort(sortByUnbondedMixnodeFirst).map((item) => { + if (isPendingDelegation(item)) return <PendingDelegationItem item={item} explorerUrl={explorerUrl} />; + if (isDelegation(item)) + return ( + <DelegationItem + item={item} + explorerUrl={explorerUrl} + nodeIsUnbonded={Boolean(!item.node_identity)} + onItemActionClick={onItemActionClick} /> - </TableCell> - <TableCell> - {isDelegation(item) && (!item.avg_uptime_percent ? '-' : `${item.avg_uptime_percent}%`)} - </TableCell> - <TableCell> - {isDelegation(item) && - (!item.cost_params?.profit_margin_percent - ? '-' - : `${toPercentIntegerString(item.cost_params.profit_margin_percent)}%`)} - </TableCell> - <TableCell>{getStakeSaturation(item)}</TableCell> - <TableCell> - {isDelegation(item) && format(new Date(item.delegated_on_iso_datetime), 'dd/MM/yyyy')} - </TableCell> - <TableCell> - <Typography style={{ textTransform: 'uppercase' }}> - {isDelegation(item) && `${item.amount.amount} ${item.amount.denom}`} - </Typography> - </TableCell> - <TableCell sx={{ textTransform: 'uppercase' }}>{getRewardValue(item)}</TableCell> - <TableCell align="right"> - {isDelegation(item) && !item.pending_events.length && ( - <DelegationsActionsMenu - isPending={undefined} - onActionClick={(action) => (onItemActionClick ? onItemActionClick(item, action) : undefined)} - disableRedeemingRewards={!item.unclaimed_rewards || item.unclaimed_rewards.amount === '0'} - /> - )} - {isDelegation(item) && item.pending_events.length > 0 && ( - <Tooltip - title="Your changes will take effect when -the new epoch starts. There is a new -epoch every hour." - arrow - > - <Chip label="Pending Events" /> - </Tooltip> - )} - {isPendingDelegation(item) && ( - <Tooltip - title={`Your delegation of ${item.event.amount?.amount} ${item.event.amount?.denom} will take effect - when the new epoch starts. There is a new - epoch every hour.`} - arrow - > - <Chip label="Pending Events" /> - </Tooltip> - )} - </TableCell> - </TableRow> - )) + ); + return null; + }) ) : ( <TableRow> <TableCell colSpan={7}> diff --git a/nym-wallet/src/components/Delegation/Delegations.tsx b/nym-wallet/src/components/Delegation/Delegations.tsx index 35a35a22a5..0a6781e44a 100644 --- a/nym-wallet/src/components/Delegation/Delegations.tsx +++ b/nym-wallet/src/components/Delegation/Delegations.tsx @@ -7,7 +7,7 @@ import { DelegationListItemActions } from './DelegationActions'; export const Delegations: React.FC<{ isLoading?: boolean; - items?: DelegationWithEverything[]; + items: DelegationWithEverything[]; explorerUrl: string; onDelegationItemActionClick?: (item: DelegationWithEverything, action: DelegationListItemActions) => void; }> = ({ isLoading, items, explorerUrl, onDelegationItemActionClick }) => ( diff --git a/nym-wallet/src/components/Delegation/PendingDelegationItem.tsx b/nym-wallet/src/components/Delegation/PendingDelegationItem.tsx new file mode 100644 index 0000000000..ef4b71390a --- /dev/null +++ b/nym-wallet/src/components/Delegation/PendingDelegationItem.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { Chip, TableCell, TableRow, Tooltip } from '@mui/material'; +import { WrappedDelegationEvent } from '@nymproject/types'; +import { Link } from '@nymproject/react/link/Link'; + +export const PendingDelegationItem = ({ item, explorerUrl }: { item: WrappedDelegationEvent; explorerUrl: string }) => ( + <TableRow key={item.node_identity}> + <TableCell> + <Link + target="_blank" + href={`${explorerUrl}/network-components/mixnode/${item.node_identity}`} + text={`${item.node_identity.slice(0, 6)}...${item.node_identity.slice(-6)}`} + color="text.primary" + noIcon + /> + </TableCell> + <TableCell>-</TableCell> + <TableCell>-</TableCell> + <TableCell>-</TableCell> + <TableCell>-</TableCell> + <TableCell>-</TableCell> + <TableCell>-</TableCell> + <TableCell>-</TableCell> + <TableCell align="right"> + <Tooltip + title={`Your delegation of ${item.event.amount?.amount} ${item.event.amount?.denom} will take effect + when the new epoch starts. There is a new + epoch every hour.`} + arrow + > + <Chip label="Pending Events" /> + </Tooltip> + </TableCell> + </TableRow> +); diff --git a/nym-wallet/src/components/Delegation/UndelegateModal.tsx b/nym-wallet/src/components/Delegation/UndelegateModal.tsx index dc48b204c3..99715d7425 100644 --- a/nym-wallet/src/components/Delegation/UndelegateModal.tsx +++ b/nym-wallet/src/components/Delegation/UndelegateModal.tsx @@ -46,15 +46,8 @@ export const UndelegateModal: React.FC<{ sx={sx} backdropProps={backdropProps} > - <IdentityKeyFormField - readOnly - fullWidth - placeholder="Node identity key" - initialValue={identityKey} - showTickOnValid={false} - /> - <Box sx={{ mt: 3 }}> + <ModalListItem label="Node identity" value={identityKey || '-'} divider /> <ModalListItem label="Delegation amount" value={`${amount} ${currency.toUpperCase()}`} divider /> <ModalFee fee={fee} isLoading={isFeeLoading} error={feeError} divider /> <ModalListItem label=" Tokens will be transferred to account you are logged in with now" value="" divider /> diff --git a/nym-wallet/src/components/Mnemonic.tsx b/nym-wallet/src/components/Mnemonic.tsx index c41fde3d86..d445172e9a 100644 --- a/nym-wallet/src/components/Mnemonic.tsx +++ b/nym-wallet/src/components/Mnemonic.tsx @@ -30,6 +30,7 @@ export const Mnemonic = ({ height: '160px', }, }} + InputLabelProps={{ shrink: true }} sx={{ 'input::-webkit-textfield-decoration-container': { alignItems: 'start', diff --git a/nym-wallet/src/components/Send/SendInputModal.tsx b/nym-wallet/src/components/Send/SendInputModal.tsx index c27eb58781..a54541d040 100644 --- a/nym-wallet/src/components/Send/SendInputModal.tsx +++ b/nym-wallet/src/components/Send/SendInputModal.tsx @@ -62,6 +62,7 @@ export const SendInputModal = ({ fullWidth onChange={(e) => onAddressChange(e.target.value)} value={toAddress} + InputLabelProps={{ shrink: true }} /> <CurrencyFormField label="Amount" diff --git a/nym-wallet/src/components/textfields.tsx b/nym-wallet/src/components/textfields.tsx index c4373176f3..085ede1ab3 100644 --- a/nym-wallet/src/components/textfields.tsx +++ b/nym-wallet/src/components/textfields.tsx @@ -26,6 +26,7 @@ export const MnemonicInput: React.FC<{ height: '160px', }, }} + InputLabelProps={{ shrink: true }} sx={{ 'input::-webkit-textfield-decoration-container': { alignItems: 'start', @@ -71,6 +72,7 @@ export const PasswordInput: React.FC<{ </IconButton> ), }} + InputLabelProps={{ shrink: true }} /> </Box> {error && <Error message={error} />} diff --git a/nym-wallet/src/context/bonding.tsx b/nym-wallet/src/context/bonding.tsx index 6f87921db6..bf3e558e7e 100644 --- a/nym-wallet/src/context/bonding.tsx +++ b/nym-wallet/src/context/bonding.tsx @@ -22,6 +22,7 @@ import { vestingBondMixNode, vestingUnbondGateway, vestingUnbondMixnode, + getPendingEpochEvents, updateMixnodeCostParams as updateMixnodeCostParamsRequest, vestingUpdateMixnodeCostParams as updateMixnodeVestingCostParamsRequest, getNodeDescription as getNodeDescriptionRequest, @@ -32,11 +33,13 @@ import { getInclusionProbability, getMixnodeAvgUptime, getMixnodeRewardEstimation, + getGatewayReport, } from '../requests'; import { useCheckOwnership } from '../hooks/useCheckOwnership'; import { AppContext } from './main'; import { attachDefaultOperatingCost, + decCoinToDisplay, toDisplay, toPercentFloatString, toPercentIntegerString, @@ -64,6 +67,7 @@ export type TBondedMixnode = { mixPort: number; verlocPort: number; version: string; + isUnbonding: boolean; }; export interface TBondedGateway { @@ -78,6 +82,11 @@ export interface TBondedGateway { mixPort: number; verlocPort: number; version: string; + routingScore?: { + current: number; + average: number; + }; + isUnbonding: boolean; } export type TokenPool = 'locked' | 'balance'; @@ -215,6 +224,16 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod return stake; }; + const getGatewayReportDetails = async (identityKey: string) => { + try { + const report = await getGatewayReport(identityKey); + return { current: report.most_recent, average: report.last_day }; + } catch (e) { + Console.error(e); + return undefined; + } + }; + const refresh = useCallback(async () => { setIsLoading(true); @@ -252,14 +271,14 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod amount: calculateStake(rewarding_details.operator, rewarding_details.delegates), denom: bond_information.original_pledge.denom, }, - bond: bond_information.original_pledge, + bond: decCoinToDisplay(bond_information.original_pledge), profitMargin: toPercentIntegerString(rewarding_details.cost_params.profit_margin_percent), delegators: rewarding_details.unique_delegations, proxy: bond_information.proxy, operatorRewards, status, stakeSaturation, - operatorCost: rewarding_details.cost_params.interval_operating_cost, + operatorCost: decCoinToDisplay(rewarding_details.cost_params.interval_operating_cost), host: bond_information.mix_node.host.replace(/\s/g, ''), routingScore, activeSetProbability: setProbabilities?.in_active, @@ -269,6 +288,7 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod mixPort: bond_information.mix_node.mix_port, verlocPort: bond_information.mix_node.verloc_port, version: bond_information.mix_node.version, + isUnbonding: bond_information.is_unbonding, } as TBondedMixnode); } } catch (e: any) { @@ -282,14 +302,16 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod const data = await getGatewayBondDetails(); if (data) { const nodeDescription = await getNodeDescription(data.gateway.host, data.gateway.clients_port); - + const routingScore = await getGatewayReportDetails(data.gateway.identity_key); setBondedNode({ name: nodeDescription?.name, identityKey: data.gateway.identity_key, ip: data.gateway.host, location: data.gateway.location, - bond: data.pledge_amount, + bond: decCoinToDisplay(data.pledge_amount), proxy: data.proxy, + routingScore, + isUnbonding: false, } as TBondedGateway); } } catch (e: any) { diff --git a/nym-wallet/src/context/delegations.tsx b/nym-wallet/src/context/delegations.tsx index fa27a14fe1..79aeb70d94 100644 --- a/nym-wallet/src/context/delegations.tsx +++ b/nym-wallet/src/context/delegations.tsx @@ -10,6 +10,7 @@ import { import type { Network } from 'src/types'; import { delegateToMixnode, getAllPendingDelegations, vestingDelegateToMixnode } from 'src/requests'; import { TPoolOption } from 'src/components'; +import { decCoinToDisplay } from 'src/utils'; export type TDelegationContext = { isLoading: boolean; @@ -36,7 +37,8 @@ export type TDelegationTransaction = { }; export type DelegationWithEvent = DelegationWithEverything | WrappedDelegationEvent; -export type TDelegations = DelegationWithEvent[]; +export type TDelegation = DelegationWithEvent; +export type TDelegations = TDelegation[]; export const isPendingDelegation = (delegation: DelegationWithEvent): delegation is WrappedDelegationEvent => 'event' in delegation; @@ -99,9 +101,18 @@ export const DelegationContextProvider: FC<{ const some = data.delegations.some(({ node_identity }) => node_identity === event.node_identity); return !some; }); + const items = data.delegations.map((delegation) => ({ + ...delegation, + amount: decCoinToDisplay(delegation.amount), + unclaimed_rewards: delegation.unclaimed_rewards && decCoinToDisplay(delegation.unclaimed_rewards), + cost_params: delegation.cost_params && { + ...delegation.cost_params, + interval_operating_cost: decCoinToDisplay(delegation.cost_params.interval_operating_cost), + }, + })); setPendingDelegations(pending); - setDelegations([...data.delegations, ...pendingOnNewNodes]); + setDelegations([...items, ...pendingOnNewNodes]); setTotalDelegations(`${data.total_delegations.amount} ${data.total_delegations.denom}`); setTotalRewards(`${data.total_rewards.amount} ${data.total_rewards.denom}`); } catch (e) { diff --git a/nym-wallet/src/context/mocks/bonding.tsx b/nym-wallet/src/context/mocks/bonding.tsx index 6ab98ce490..5fde300d75 100644 --- a/nym-wallet/src/context/mocks/bonding.tsx +++ b/nym-wallet/src/context/mocks/bonding.tsx @@ -26,6 +26,7 @@ const bondedMixnodeMock: TBondedMixnode = { mixPort: 1789, verlocPort: 1790, version: '1.0.2', + isUnbonding: false, }; const bondedGatewayMock: TBondedGateway = { @@ -38,6 +39,11 @@ const bondedGatewayMock: TBondedGateway = { mixPort: 1789, verlocPort: 1790, version: '1.0.2', + routingScore: { + average: 100, + current: 100, + }, + isUnbonding: false, }; const TxResultMock: TransactionExecuteResult = { diff --git a/nym-wallet/src/context/mocks/delegations.tsx b/nym-wallet/src/context/mocks/delegations.tsx index 7fcd4e6dc2..3674dc1494 100644 --- a/nym-wallet/src/context/mocks/delegations.tsx +++ b/nym-wallet/src/context/mocks/delegations.tsx @@ -29,6 +29,7 @@ let mockDelegations: DelegationWithEverything[] = [ accumulated_by_operator: { amount: '0', denom: 'nym' }, uses_vesting_contract_tokens: false, pending_events: [], + mixnode_is_unbonding: false, }, { mix_id: 5678, @@ -51,6 +52,7 @@ let mockDelegations: DelegationWithEverything[] = [ accumulated_by_operator: { amount: '0', denom: 'nym' }, uses_vesting_contract_tokens: true, pending_events: [], + mixnode_is_unbonding: false, }, ]; diff --git a/nym-wallet/src/pages/Admin/index.tsx b/nym-wallet/src/pages/Admin/index.tsx index 1642570db9..b3a8c157c5 100644 --- a/nym-wallet/src/pages/Admin/index.tsx +++ b/nym-wallet/src/pages/Admin/index.tsx @@ -35,6 +35,7 @@ const AdminForm: React.FC<{ fullWidth error={!!errors.minimum_mixnode_pledge} helperText={`${errors?.minimum_mixnode_pledge?.amount?.message} ${errors?.minimum_mixnode_pledge?.denom?.message}`} + InputLabelProps={{ shrink: true }} /> </Grid> <Grid item xs={12}> @@ -48,6 +49,7 @@ const AdminForm: React.FC<{ fullWidth error={!!errors.minimum_gateway_pledge} helperText={`${errors?.minimum_gateway_pledge?.amount?.message} ${errors?.minimum_gateway_pledge?.denom?.message}`} + InputLabelProps={{ shrink: true }} /> </Grid> </Grid> diff --git a/nym-wallet/src/pages/bonding/node-settings/NodeSettings.tsx b/nym-wallet/src/pages/bonding/node-settings/NodeSettings.tsx index c9bd51687f..da50bc2d2d 100644 --- a/nym-wallet/src/pages/bonding/node-settings/NodeSettings.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/NodeSettings.tsx @@ -54,6 +54,8 @@ export const NodeSettings = () => { }); }; + if (isLoading) return <LoadingModal />; + if (!bondedNode) { return null; } @@ -75,7 +77,7 @@ export const NodeSettings = () => { <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <NodeIcon /> <Typography variant="h6" fontWeight={600}> - Node Settings + {isMixnode(bondedNode) ? 'Node' : 'Gateway'} Settings </Typography> </Box> </Box> @@ -133,7 +135,6 @@ export const NodeSettings = () => { }} /> )} - {isLoading && <LoadingModal />} </NymCard> </PageLayout> ); diff --git a/nym-wallet/src/pages/bonding/node-settings/settings-pages/NodeUnbondPage.tsx b/nym-wallet/src/pages/bonding/node-settings/settings-pages/NodeUnbondPage.tsx index eb92fc2758..4a6097b8da 100644 --- a/nym-wallet/src/pages/bonding/node-settings/settings-pages/NodeUnbondPage.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/settings-pages/NodeUnbondPage.tsx @@ -3,8 +3,10 @@ import { Box, Button, Typography, Grid, TextField } from '@mui/material'; import { TBondedMixnode, TBondedGateway } from 'src/context/bonding'; import { Error } from 'src/components/Error'; import { UnbondModal } from 'src/components/Bonding/modals/UnbondModal'; +import { isMixnode } from 'src/types'; interface Props { bondedNode: TBondedMixnode | TBondedGateway; + onConfirm: () => Promise<void>; onError: (e: string) => void; } @@ -21,15 +23,21 @@ export const NodeUnbondPage = ({ bondedNode, onConfirm, onError }: Props) => { Unbond </Typography> </Grid> - <Grid item> - <Typography variant="body2" sx={{ color: (theme) => theme.palette.nym.text.muted }}> - If you unbond your node you will loose all delegations! - </Typography> - </Grid> + {isMixnode(bondedNode) && ( + <Grid item> + <Typography variant="body2" sx={{ color: (theme) => theme.palette.nym.text.muted }}> + If you unbond you will loose all delegations! + </Typography> + </Grid> + )} </Grid> <Grid item container direction={'column'} spacing={2} width={0.5} padding={3}> <Grid item> - <Error message="Unbonding is irreversible and it won’t be possible to restore the current state of your node again" /> + <Error + message={`Unbonding is irreversible and it won’t be possible to restore the current state of your ${ + isMixnode(bondedNode) ? 'node' : 'gateway' + } again`} + /> </Grid> <Grid item> <Typography variant="body2"> @@ -41,7 +49,12 @@ export const NodeUnbondPage = ({ bondedNode, onConfirm, onError }: Props) => { </Typography> </Grid> <Grid item> - <TextField fullWidth value={confirmField} onChange={(e) => setConfirmField(e.target.value)} /> + <TextField + fullWidth + value={confirmField} + onChange={(e) => setConfirmField(e.target.value)} + InputLabelProps={{ shrink: true }} + /> </Grid> <Grid item sx={{ mt: 2 }}> <Button diff --git a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/InfoSettings.tsx b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/InfoSettings.tsx index d48f9c20bf..7b85f1c0c2 100644 --- a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/InfoSettings.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/InfoSettings.tsx @@ -87,6 +87,7 @@ export const InfoSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBon fullWidth error={!!errors.mixPort} helperText={errors.mixPort?.message} + InputLabelProps={{ shrink: true }} /> </Grid> <Grid item width={1}> @@ -97,6 +98,7 @@ export const InfoSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBon fullWidth error={!!errors.verlocPort} helperText={errors.verlocPort?.message} + InputLabelProps={{ shrink: true }} /> </Grid> <Grid item width={1}> @@ -107,6 +109,7 @@ export const InfoSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBon fullWidth error={!!errors.httpApiPort} helperText={errors.httpApiPort?.message} + InputLabelProps={{ shrink: true }} /> </Grid> </Grid> @@ -137,6 +140,7 @@ export const InfoSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBon fullWidth error={!!errors.host} helperText={errors.host?.message} + InputLabelProps={{ shrink: true }} /> </Grid> </Grid> @@ -167,6 +171,7 @@ export const InfoSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBon fullWidth error={!!errors.version} helperText={errors.version?.message} + InputLabelProps={{ shrink: true }} /> </Grid> </Grid> diff --git a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/ParametersSettings.tsx b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/ParametersSettings.tsx index ce7a3d0250..ad9b3d03af 100644 --- a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/ParametersSettings.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/ParametersSettings.tsx @@ -160,6 +160,7 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode </InputAdornment> ), }} + InputLabelProps={{ shrink: true }} /> {pendingUpdates && ( <FormHelperText> diff --git a/nym-wallet/src/pages/delegation/index.tsx b/nym-wallet/src/pages/delegation/index.tsx index 92845056b0..62418f0463 100644 --- a/nym-wallet/src/pages/delegation/index.tsx +++ b/nym-wallet/src/pages/delegation/index.tsx @@ -111,7 +111,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { const handleNewDelegation = async ( mix_id: number, - identityKey: string, + _: string, amount: DecCoin, tokenPool: TPoolOption, fee?: FeeDetails, @@ -268,19 +268,20 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { } }; - const delegationsComponent = (delegations: TDelegations | undefined) => { + const delegationsComponent = (delegationItems: TDelegations | undefined) => { if (isLoading) { return ( <Box sx={{ width: '100%', display: 'flex', justifyContent: 'center' }}> <CircularProgress /> </Box> ); - } else if (Boolean(delegations?.length)) { + } + if (delegationItems && Boolean(delegationItems?.length)) { return ( <DelegationList explorerUrl={urls(network).networkExplorer} isLoading={isLoading} - items={delegations} + items={delegationItems} onItemActionClick={handleDelegationItemActionClick} /> ); diff --git a/nym-wallet/src/pages/internal-docs/DocEntry.tsx b/nym-wallet/src/pages/internal-docs/DocEntry.tsx index 0901151b14..43c26b6491 100644 --- a/nym-wallet/src/pages/internal-docs/DocEntry.tsx +++ b/nym-wallet/src/pages/internal-docs/DocEntry.tsx @@ -69,6 +69,7 @@ export const DocEntry: React.FC<DocEntryProps> = (props) => { label={arg.name} id={argKey(props.function.name, arg.name)} key={argKey(props.function.name, arg.name)} + InputLabelProps={{ shrink: true }} /> ))} </div> diff --git a/nym-wallet/src/pages/settings/profile.tsx b/nym-wallet/src/pages/settings/profile.tsx index c9c5021ce9..f97ac9513d 100644 --- a/nym-wallet/src/pages/settings/profile.tsx +++ b/nym-wallet/src/pages/settings/profile.tsx @@ -15,9 +15,9 @@ export const Profile = () => { Node identity: {mixnodeDetails?.bond_information.mix_node.identity_key || 'n/a'} </Typography> <Divider /> - <TextField label="Mixnode name" disabled /> - <TextField multiline label="Mixnode description" rows={3} disabled /> - <TextField label="Link" disabled /> + <TextField label="Mixnode name" disabled InputLabelProps={{ shrink: true }} /> + <TextField multiline label="Mixnode description" rows={3} disabled InputLabelProps={{ shrink: true }} /> + <TextField label="Link" disabled InputLabelProps={{ shrink: true }} /> </Stack> </Box> <Box diff --git a/nym-wallet/src/pages/settings/system-variables.tsx b/nym-wallet/src/pages/settings/system-variables.tsx index 9cbfe2e1e5..015034f76e 100644 --- a/nym-wallet/src/pages/settings/system-variables.tsx +++ b/nym-wallet/src/pages/settings/system-variables.tsx @@ -131,6 +131,7 @@ export const SystemVariables = ({ }} error={!!errors.profitMarginPercent} disabled={isSubmitting} + InputLabelProps={{ shrink: true }} /> <DataField title="Estimated reward" diff --git a/nym-wallet/src/requests/index.ts b/nym-wallet/src/requests/index.ts index ea42390330..6e148116c2 100644 --- a/nym-wallet/src/requests/index.ts +++ b/nym-wallet/src/requests/index.ts @@ -10,3 +10,4 @@ export * from './signature'; export * from './simulate'; export * from './utils'; export * from './vesting'; +export * from './pendingEvents'; diff --git a/nym-wallet/src/requests/pendingEvents.ts b/nym-wallet/src/requests/pendingEvents.ts new file mode 100644 index 0000000000..c92954a237 --- /dev/null +++ b/nym-wallet/src/requests/pendingEvents.ts @@ -0,0 +1,4 @@ +import { PendingEpochEvent } from '@nymproject/types'; +import { invokeWrapper } from './wrapper'; + +export const getPendingEpochEvents = async () => invokeWrapper<PendingEpochEvent[]>('get_pending_epoch_events'); diff --git a/nym-wallet/src/requests/queries.ts b/nym-wallet/src/requests/queries.ts index 466c83efc1..5fba51beec 100644 --- a/nym-wallet/src/requests/queries.ts +++ b/nym-wallet/src/requests/queries.ts @@ -9,7 +9,7 @@ import { WrappedDelegationEvent, PendingIntervalEvent, } from '@nymproject/types'; -import { Interval, TNodeDescription } from 'src/types'; +import { Interval, TGatewayReport, TNodeDescription } from 'src/types'; import { invokeWrapper } from './wrapper'; export const getAllPendingDelegations = async () => @@ -48,3 +48,6 @@ export const getNodeDescription = async (host: string, port: number) => export const getPendingIntervalEvents = async () => invokeWrapper<PendingIntervalEvent[]>('get_pending_interval_events'); + +export const getGatewayReport = async (identity: string) => + invokeWrapper<TGatewayReport>('gateway_report', { identity }); diff --git a/nym-wallet/src/types/global.ts b/nym-wallet/src/types/global.ts index 1bba38e24f..d5bb61c26d 100644 --- a/nym-wallet/src/types/global.ts +++ b/nym-wallet/src/types/global.ts @@ -1,4 +1,12 @@ -import { Gateway, DecCoin, MixNode, PledgeData, MixNodeCostParams } from '@nymproject/types'; +import { + Gateway, + DecCoin, + MixNode, + PledgeData, + MixNodeCostParams, + PendingIntervalEventData, + PendingEpochEventData, +} from '@nymproject/types'; import { Fee } from '@nymproject/types/dist/types/rust/Fee'; import { TBondedGateway, TBondedMixnode } from 'src/context'; @@ -61,6 +69,14 @@ export type TAccount = { mnemonic: string; }; +export type TGatewayReport = { + identity: string; + owner: string; + last_day: number; + last_hour: number; + most_recent: number; +}; + export const isMixnode = (node: TBondedMixnode | TBondedGateway): node is TBondedMixnode => (node as TBondedMixnode).profitMargin !== undefined; diff --git a/nym-wallet/src/utils/index.ts b/nym-wallet/src/utils/index.ts index 8d41218392..49d56c6f72 100644 --- a/nym-wallet/src/utils/index.ts +++ b/nym-wallet/src/utils/index.ts @@ -152,7 +152,7 @@ export const toPercentIntegerString = (value: string) => Math.round(Number(value * * @param val - a decimal number of string form * @param dp - number of decimal places (4 by default ie. 0.0000) - * @returns A prettyfied decimal number + * @returns A prettified decimal number */ export const toDisplay = (val: string | number | Big, dp = 4) => { let displayValue; @@ -164,6 +164,24 @@ export const toDisplay = (val: string | number | Big, dp = 4) => { return displayValue; }; +/** + * Takes a DecCoin and prettify its amount to a representation + * with fixed decimal places. + * + * @param coin - a DecCoin + * @param dp - number of decimal places to apply to amount (4 by default ie. 0.0000) + * @returns A DecCoin with prettified amount + */ +export const decCoinToDisplay = (coin: DecCoin, dp = 4) => { + const displayCoin = { ...coin }; + try { + displayCoin.amount = Big(coin.amount).toFixed(dp); + } catch (e: any) { + Console.warn(`${coin.amount} not a valid decimal number: ${e}`); + } + return displayCoin; +}; + /** * Converts a decimal number of μNYM (micro NYM) to NYM. * diff --git a/service-providers/network-requester/Cargo.toml b/service-providers/network-requester/Cargo.toml index 727f2f4c31..b0a113e0ab 100644 --- a/service-providers/network-requester/Cargo.toml +++ b/service-providers/network-requester/Cargo.toml @@ -31,6 +31,7 @@ tokio-tungstenite = "0.17.2" network-defaults = { path = "../../common/network-defaults" } nymsphinx = { path = "../../common/nymsphinx" } completions = { path = "../../common/completions" } +logging = { path = "../../common/logging"} ordered-buffer = {path = "../../common/socks5/ordered-buffer"} proxy-helpers = { path = "../../common/socks5/proxy-helpers" } socks5-requests = { path = "../../common/socks5/requests" } diff --git a/service-providers/network-requester/src/main.rs b/service-providers/network-requester/src/main.rs index 07e0207f77..71046109da 100644 --- a/service-providers/network-requester/src/main.rs +++ b/service-providers/network-requester/src/main.rs @@ -5,6 +5,7 @@ use clap::CommandFactory; use clap::Subcommand; use clap::{Args, Parser}; use completions::{fig_generate, ArgShell}; +use logging::setup_logging; use error::NetworkRequesterError; use network_defaults::DEFAULT_WEBSOCKET_LISTENING_PORT; @@ -106,21 +107,3 @@ async fn main() -> Result<(), NetworkRequesterError> { execute(args).await } - -fn setup_logging() { - let mut log_builder = pretty_env_logger::formatted_timed_builder(); - if let Ok(s) = ::std::env::var("RUST_LOG") { - log_builder.parse_filters(&s); - } else { - // default to 'Info' - log_builder.filter(None, log::LevelFilter::Info); - } - - log_builder - .filter_module("hyper", log::LevelFilter::Warn) - .filter_module("tokio_reactor", log::LevelFilter::Warn) - .filter_module("reqwest", log::LevelFilter::Warn) - .filter_module("mio", log::LevelFilter::Warn) - .filter_module("want", log::LevelFilter::Warn) - .init(); -} diff --git a/service-providers/network-statistics/Cargo.toml b/service-providers/network-statistics/Cargo.toml index a1e4185b3c..eb97366ecb 100644 --- a/service-providers/network-statistics/Cargo.toml +++ b/service-providers/network-statistics/Cargo.toml @@ -14,7 +14,7 @@ serde = { version = "1.0", features = ["derive"] } sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "chrono"]} thiserror = "1" tokio = { version = "1.4", features = [ "net", "rt-multi-thread", "macros", "time" ] } - +logging = { path = "../../common/logging"} statistics-common = { path = "../../common/statistics" } [build-dependencies] diff --git a/service-providers/network-statistics/src/main.rs b/service-providers/network-statistics/src/main.rs index 0a32438a80..d9dab909e4 100644 --- a/service-providers/network-statistics/src/main.rs +++ b/service-providers/network-statistics/src/main.rs @@ -1,9 +1,9 @@ // Copyright 2022 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: Apache-2.0 -use std::path::PathBuf; - use api::NetworkStatisticsAPI; +use logging::setup_logging; +use std::path::PathBuf; mod api; mod storage; @@ -23,24 +23,6 @@ async fn main() { api.run().await; } -fn setup_logging() { - let mut log_builder = pretty_env_logger::formatted_timed_builder(); - if let Ok(s) = ::std::env::var("RUST_LOG") { - log_builder.parse_filters(&s); - } else { - // default to 'Info' - log_builder.filter(None, log::LevelFilter::Info); - } - - log_builder - .filter_module("hyper", log::LevelFilter::Warn) - .filter_module("tokio_reactor", log::LevelFilter::Warn) - .filter_module("reqwest", log::LevelFilter::Warn) - .filter_module("mio", log::LevelFilter::Warn) - .filter_module("want", log::LevelFilter::Warn) - .init(); -} - /// Returns the default base directory for the storefile. /// /// This is split out so we can easily inject our own base_dir for unit tests. diff --git a/tools/nym-cli/Cargo.toml b/tools/nym-cli/Cargo.toml index dcc92dfc36..b4bdb9222a 100644 --- a/tools/nym-cli/Cargo.toml +++ b/tools/nym-cli/Cargo.toml @@ -20,5 +20,6 @@ bip39 = "1.0.1" anyhow = "1" nym-cli-commands = { path = "../../common/commands" } +logging = { path = "../../common/logging"} validator-client = { path = "../../common/client-libs/validator-client", features = ["nymd-client"] } network-defaults = { path = "../../common/network-defaults" } diff --git a/tools/nym-cli/src/main.rs b/tools/nym-cli/src/main.rs index 842229b15e..10a4e9e463 100644 --- a/tools/nym-cli/src/main.rs +++ b/tools/nym-cli/src/main.rs @@ -3,7 +3,7 @@ use clap::{CommandFactory, Parser, Subcommand}; use log::{error, warn}; - +use logging::setup_logging; use nym_cli_commands::context::{get_network_details, ClientArgs}; use validator_client::nymd::AccountId; @@ -122,27 +122,6 @@ async fn execute(cli: Cli) -> anyhow::Result<()> { Ok(()) } -fn setup_logging() { - let mut log_builder = pretty_env_logger::formatted_timed_builder(); - if let Ok(s) = ::std::env::var("RUST_LOG") { - log_builder.parse_filters(&s); - } else { - // default to 'Info' - log_builder.filter(None, log::LevelFilter::Info); - } - - log_builder - .filter_module("hyper", log::LevelFilter::Warn) - .filter_module("tokio_reactor", log::LevelFilter::Warn) - .filter_module("reqwest", log::LevelFilter::Warn) - .filter_module("mio", log::LevelFilter::Warn) - .filter_module("want", log::LevelFilter::Warn) - .filter_module("sled", log::LevelFilter::Warn) - .filter_module("tungstenite", log::LevelFilter::Warn) - .filter_module("tokio_tungstenite", log::LevelFilter::Warn) - .init(); -} - async fn wait_for_interrupt() { if let Err(e) = tokio::signal::ctrl_c().await { error!( diff --git a/ts-packages/react-components/src/components/currency/CurrencyFormField.tsx b/ts-packages/react-components/src/components/currency/CurrencyFormField.tsx index 34fb118c34..d4449a8d40 100644 --- a/ts-packages/react-components/src/components/currency/CurrencyFormField.tsx +++ b/ts-packages/react-components/src/components/currency/CurrencyFormField.tsx @@ -148,6 +148,7 @@ export const CurrencyFormField: React.FC<{ placeholder={placeholder} label={label} onChange={handleChange} + InputLabelProps={{ shrink: true }} sx={sx} /> ); diff --git a/ts-packages/react-components/src/components/mixnodes/IdentityKeyFormField.tsx b/ts-packages/react-components/src/components/mixnodes/IdentityKeyFormField.tsx index f15465438a..94fa6ae382 100644 --- a/ts-packages/react-components/src/components/mixnodes/IdentityKeyFormField.tsx +++ b/ts-packages/react-components/src/components/mixnodes/IdentityKeyFormField.tsx @@ -99,6 +99,7 @@ export const IdentityKeyFormField: React.FC<{ helperText={validationError} defaultValue={initialValue} onChange={handleChange} + InputLabelProps={{ shrink: true }} /> ); }; diff --git a/ts-packages/types/src/types/global.ts b/ts-packages/types/src/types/global.ts index 3c020800e2..1ec545138c 100644 --- a/ts-packages/types/src/types/global.ts +++ b/ts-packages/types/src/types/global.ts @@ -1,4 +1,4 @@ -import { MixNode, DecCoin, CurrencyDenom } from './rust'; +import { MixNode, DecCoin } from './rust'; export type TNodeType = 'mixnode' | 'gateway'; diff --git a/ts-packages/types/src/types/rust/DelegationWithEverything.ts b/ts-packages/types/src/types/rust/DelegationWithEverything.ts index 9b0cdf95a4..0d3423a133 100644 --- a/ts-packages/types/src/types/rust/DelegationWithEverything.ts +++ b/ts-packages/types/src/types/rust/DelegationWithEverything.ts @@ -17,4 +17,5 @@ export interface DelegationWithEverything { uses_vesting_contract_tokens: boolean; unclaimed_rewards: DecCoin | null; pending_events: Array<DelegationEvent>; + mixnode_is_unbonding: boolean | null; } diff --git a/validator-api/Cargo.toml b/validator-api/Cargo.toml index 498773a970..90be5aa060 100644 --- a/validator-api/Cargo.toml +++ b/validator-api/Cargo.toml @@ -69,9 +69,11 @@ cosmwasm-std = "1.0.0" credential-storage = { path = "../common/credential-storage" } credentials = { path = "../common/credentials", optional = true } crypto = { path = "../common/crypto" } +logging = { path = "../common/logging"} gateway-client = { path = "../common/client-libs/gateway-client" } inclusion-probability = { path = "../common/inclusion-probability" } mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract" } +contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common" } multisig-contract-common = { path = "../common/cosmwasm-smart-contracts/multisig-contract" } nymcoconut = { path = "../common/nymcoconut", optional = true } nymsphinx = { path = "../common/nymsphinx" } diff --git a/validator-api/src/epoch_operations/mod.rs b/validator-api/src/epoch_operations/mod.rs index 1273ac4310..cb33962890 100644 --- a/validator-api/src/epoch_operations/mod.rs +++ b/validator-api/src/epoch_operations/mod.rs @@ -30,6 +30,7 @@ pub(crate) mod error; mod helpers; use crate::epoch_operations::helpers::stake_to_f64; +use crate::node_status_api::ONE_DAY; use error::RewardingError; use mixnet_contract_common::mixnode::MixNodeDetails; use task::ShutdownListener; @@ -50,9 +51,6 @@ impl From<MixnodeToReward> for ExecuteMsg { } } -// // Epoch has all the same semantics as interval, but has a lower set duration -// type Epoch = Interval; - pub struct RewardedSetUpdater { nymd_client: Client<SigningNymdClient>, validator_cache: ValidatorCache, @@ -268,8 +266,8 @@ impl RewardedSetUpdater { log::info!("Advanced the epoch and updated the rewarded set... SUCCESS"); } - log::info!("Puring all node statuses from the storage..."); - let cutoff = (epoch_end - Duration::from_secs(86400)).unix_timestamp(); + log::info!("Purging old node statuses from the storage..."); + let cutoff = (epoch_end - 2 * ONE_DAY).unix_timestamp(); self.storage.purge_old_statuses(cutoff).await?; Ok(()) diff --git a/validator-api/src/main.rs b/validator-api/src/main.rs index 68befdf169..ece8fab179 100644 --- a/validator-api/src/main.rs +++ b/validator-api/src/main.rs @@ -42,6 +42,7 @@ use crate::epoch_operations::RewardedSetUpdater; use coconut::{comm::QueryCommunicationChannel, InternalSignRequest}; #[cfg(feature = "coconut")] use coconut_interface::{Base58, KeyPair}; +use logging::setup_logging; pub(crate) mod config; pub(crate) mod contract_cache; @@ -248,27 +249,6 @@ async fn wait_for_signal() { } } -fn setup_logging() { - let mut log_builder = pretty_env_logger::formatted_timed_builder(); - if let Ok(s) = ::std::env::var("RUST_LOG") { - log_builder.parse_filters(&s); - } else { - // default to 'Info' - log_builder.filter(None, log::LevelFilter::Info); - } - - log_builder - .filter_module("hyper", log::LevelFilter::Warn) - .filter_module("tokio_reactor", log::LevelFilter::Warn) - .filter_module("reqwest", log::LevelFilter::Warn) - .filter_module("mio", log::LevelFilter::Warn) - .filter_module("want", log::LevelFilter::Warn) - .filter_module("sled", log::LevelFilter::Warn) - .filter_module("tungstenite", log::LevelFilter::Warn) - .filter_module("tokio_tungstenite", log::LevelFilter::Warn) - .init(); -} - fn override_config(mut config: Config, matches: &ArgMatches) -> Config { if let Some(id) = matches.value_of(ID) { fs::create_dir_all(Config::default_config_directory(Some(id))) diff --git a/validator-api/src/node_status_api/cache.rs b/validator-api/src/node_status_api/cache.rs index dd942ff6ed..ef5df963bf 100644 --- a/validator-api/src/node_status_api/cache.rs +++ b/validator-api/src/node_status_api/cache.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::contract_cache::{Cache, CacheNotification, ValidatorCache}; -use mixnet_contract_common::rewarding::helpers::truncate_decimal; +use contracts_common::truncate_decimal; use mixnet_contract_common::{MixId, MixNodeDetails, RewardingParams}; use rocket::fairing::AdHoc; use serde::Serialize; diff --git a/validator-api/src/node_status_api/helpers.rs b/validator-api/src/node_status_api/helpers.rs index 6beca7731a..c0c2a544bd 100644 --- a/validator-api/src/node_status_api/helpers.rs +++ b/validator-api/src/node_status_api/helpers.rs @@ -3,7 +3,7 @@ use crate::contract_cache::reward_estimate::compute_reward_estimate; use crate::contract_cache::Cache; -use crate::node_status_api::models::{ErrorResponse, MixnodeStatusReport, MixnodeUptimeHistory}; +use crate::node_status_api::models::ErrorResponse; use crate::storage::ValidatorApiStorage; use crate::{NodeStatusCache, ValidatorCache}; use cosmwasm_std::Decimal; @@ -13,26 +13,29 @@ use rocket::http::Status; use rocket::State; use validator_api_requests::models::{ ComputeRewardEstParam, InclusionProbabilityResponse, MixnodeCoreStatusResponse, - MixnodeStatusResponse, RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, + MixnodeStatusReportResponse, MixnodeStatusResponse, MixnodeUptimeHistoryResponse, + RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, }; pub(crate) async fn _mixnode_report( storage: &ValidatorApiStorage, mix_id: MixId, -) -> Result<MixnodeStatusReport, ErrorResponse> { +) -> Result<MixnodeStatusReportResponse, ErrorResponse> { storage .construct_mixnode_report(mix_id) .await + .map(MixnodeStatusReportResponse::from) .map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound)) } pub(crate) async fn _mixnode_uptime_history( storage: &ValidatorApiStorage, mix_id: MixId, -) -> Result<MixnodeUptimeHistory, ErrorResponse> { +) -> Result<MixnodeUptimeHistoryResponse, ErrorResponse> { storage .get_mixnode_uptime_history(mix_id) .await + .map(MixnodeUptimeHistoryResponse::from) .map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound)) } diff --git a/validator-api/src/node_status_api/models.rs b/validator-api/src/node_status_api/models.rs index 6fb54a6f22..ae9661bf23 100644 --- a/validator-api/src/node_status_api/models.rs +++ b/validator-api/src/node_status_api/models.rs @@ -6,8 +6,9 @@ use crate::storage::models::NodeStatus; use mixnet_contract_common::reward_params::Performance; use mixnet_contract_common::{IdentityKey, MixId}; use okapi::openapi3::{Responses, SchemaObject}; -use rocket::http::{ContentType, Status}; +use rocket::http::Status; use rocket::response::{self, Responder, Response}; +use rocket::serde::json::Json; use rocket::Request; use rocket_okapi::gen::OpenApiGenerator; use rocket_okapi::response::OpenApiResponderInner; @@ -19,8 +20,11 @@ use serde::{Deserialize, Serialize}; use sqlx::Error; use std::convert::TryFrom; use std::fmt::{self, Display, Formatter}; -use std::io::Cursor; use time::OffsetDateTime; +use validator_api_requests::models::{ + GatewayStatusReportResponse, GatewayUptimeHistoryResponse, HistoricalUptimeResponse, + MixnodeStatusReportResponse, MixnodeUptimeHistoryResponse, RequestError, +}; // todo: put into some error enum #[derive(Debug)] @@ -153,6 +157,19 @@ impl MixnodeStatusReport { } } +impl From<MixnodeStatusReport> for MixnodeStatusReportResponse { + fn from(status: MixnodeStatusReport) -> Self { + MixnodeStatusReportResponse { + mix_id: status.mix_id, + identity: status.identity, + owner: status.owner, + most_recent: status.most_recent.0, + last_hour: status.last_hour.0, + last_day: status.last_day.0, + } + } +} + #[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct GatewayStatusReport { pub(crate) identity: String, @@ -190,6 +207,18 @@ impl GatewayStatusReport { } } +impl From<GatewayStatusReport> for GatewayStatusReportResponse { + fn from(status: GatewayStatusReport) -> Self { + GatewayStatusReportResponse { + identity: status.identity, + owner: status.owner, + most_recent: status.most_recent.0, + last_hour: status.last_hour.0, + last_day: status.last_day.0, + } + } +} + #[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct MixnodeUptimeHistory { pub(crate) mix_id: MixId, @@ -215,6 +244,17 @@ impl MixnodeUptimeHistory { } } +impl From<MixnodeUptimeHistory> for MixnodeUptimeHistoryResponse { + fn from(history: MixnodeUptimeHistory) -> Self { + MixnodeUptimeHistoryResponse { + mix_id: history.mix_id, + identity: history.identity, + owner: history.owner, + history: history.history.into_iter().map(Into::into).collect(), + } + } +} + #[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct GatewayUptimeHistory { pub(crate) identity: String, @@ -233,6 +273,16 @@ impl GatewayUptimeHistory { } } +impl From<GatewayUptimeHistory> for GatewayUptimeHistoryResponse { + fn from(history: GatewayUptimeHistory) -> Self { + GatewayUptimeHistoryResponse { + identity: history.identity, + owner: history.owner, + history: history.history.into_iter().map(Into::into).collect(), + } + } +} + #[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct HistoricalUptime { // ISO 8601 date string @@ -242,25 +292,35 @@ pub struct HistoricalUptime { pub(crate) uptime: Uptime, } +impl From<HistoricalUptime> for HistoricalUptimeResponse { + fn from(uptime: HistoricalUptime) -> Self { + HistoricalUptimeResponse { + date: uptime.date, + uptime: uptime.uptime.0, + } + } +} + pub(crate) struct ErrorResponse { - error_message: String, + error_message: RequestError, status: Status, } impl ErrorResponse { pub(crate) fn new(error_message: impl Into<String>, status: Status) -> Self { ErrorResponse { - error_message: error_message.into(), + error_message: RequestError::new(error_message), status, } } } impl<'r, 'o: 'r> Responder<'r, 'o> for ErrorResponse { - fn respond_to(self, _: &'r Request<'_>) -> response::Result<'o> { + fn respond_to(self, req: &'r Request<'_>) -> response::Result<'o> { + // piggyback on the existing implementation + // also prefer json over plain for ease of use in frontend Response::build() - .header(ContentType::Plain) - .sized_body(self.error_message.len(), Cursor::new(self.error_message)) + .merge(Json(self.error_message).respond_to(req)?) .status(self.status) .ok() } diff --git a/validator-api/src/node_status_api/routes.rs b/validator-api/src/node_status_api/routes.rs index 72a476f841..7665f1bac1 100644 --- a/validator-api/src/node_status_api/routes.rs +++ b/validator-api/src/node_status_api/routes.rs @@ -8,10 +8,7 @@ use crate::node_status_api::helpers::{ _get_mixnode_stake_saturation, _get_mixnode_status, _mixnode_core_status_count, _mixnode_report, _mixnode_uptime_history, }; -use crate::node_status_api::models::{ - ErrorResponse, GatewayStatusReport, GatewayUptimeHistory, MixnodeStatusReport, - MixnodeUptimeHistory, -}; +use crate::node_status_api::models::ErrorResponse; use crate::storage::ValidatorApiStorage; use crate::ValidatorCache; use mixnet_contract_common::MixId; @@ -21,8 +18,10 @@ use rocket::State; use rocket_okapi::openapi; use validator_api_requests::models::{ AllInclusionProbabilitiesResponse, ComputeRewardEstParam, GatewayCoreStatusResponse, - InclusionProbabilityResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse, - RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, + GatewayStatusReportResponse, GatewayUptimeHistoryResponse, InclusionProbabilityResponse, + MixnodeCoreStatusResponse, MixnodeStatusReportResponse, MixnodeStatusResponse, + MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse, + UptimeResponse, }; #[openapi(tag = "status")] @@ -30,10 +29,11 @@ use validator_api_requests::models::{ pub(crate) async fn gateway_report( storage: &State<ValidatorApiStorage>, identity: &str, -) -> Result<Json<GatewayStatusReport>, ErrorResponse> { +) -> Result<Json<GatewayStatusReportResponse>, ErrorResponse> { storage .construct_gateway_report(identity) .await + .map(GatewayStatusReportResponse::from) .map(Json) .map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound)) } @@ -43,10 +43,11 @@ pub(crate) async fn gateway_report( pub(crate) async fn gateway_uptime_history( storage: &State<ValidatorApiStorage>, identity: &str, -) -> Result<Json<GatewayUptimeHistory>, ErrorResponse> { +) -> Result<Json<GatewayUptimeHistoryResponse>, ErrorResponse> { storage .get_gateway_uptime_history(identity) .await + .map(GatewayUptimeHistoryResponse::from) .map(Json) .map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound)) } @@ -74,7 +75,7 @@ pub(crate) async fn gateway_core_status_count( pub(crate) async fn mixnode_report( storage: &State<ValidatorApiStorage>, mix_id: MixId, -) -> Result<Json<MixnodeStatusReport>, ErrorResponse> { +) -> Result<Json<MixnodeStatusReportResponse>, ErrorResponse> { Ok(Json(_mixnode_report(storage, mix_id).await?)) } @@ -83,7 +84,7 @@ pub(crate) async fn mixnode_report( pub(crate) async fn mixnode_uptime_history( storage: &State<ValidatorApiStorage>, mix_id: MixId, -) -> Result<Json<MixnodeUptimeHistory>, ErrorResponse> { +) -> Result<Json<MixnodeUptimeHistoryResponse>, ErrorResponse> { Ok(Json(_mixnode_uptime_history(storage, mix_id).await?)) } diff --git a/validator-api/src/node_status_api/uptime_updater.rs b/validator-api/src/node_status_api/uptime_updater.rs index 0e60650f65..dcb4f6424c 100644 --- a/validator-api/src/node_status_api/uptime_updater.rs +++ b/validator-api/src/node_status_api/uptime_updater.rs @@ -7,9 +7,10 @@ use crate::node_status_api::models::{ use crate::node_status_api::ONE_DAY; use crate::storage::ValidatorApiStorage; use log::error; +use std::time::Duration; use task::ShutdownListener; -use time::OffsetDateTime; -use tokio::time::sleep; +use time::{OffsetDateTime, PrimitiveDateTime, Time}; +use tokio::time::{interval, sleep}; pub(crate) struct HistoricalUptimeUpdater { storage: ValidatorApiStorage, @@ -69,28 +70,51 @@ impl HistoricalUptimeUpdater { } pub(crate) async fn run(&self, mut shutdown: ShutdownListener) { + // update uptimes at 23:00 UTC each day so that we'd have data from the actual [almost] whole day + // and so that we would avoid the edge case of starting validator API at 23:59 and having some + // nodes update for different days + + // the unwrap is fine as 23:00:00 is a valid time + let update_time = Time::from_hms(23, 0, 0).unwrap(); + let now = OffsetDateTime::now_utc(); + // is the current time within 0:00 - 22:59:59 or 23:00 - 23:59:59 ? + let update_date = if now.hour() < 23 { + now.date() + } else { + // the unwrap is fine as (**PRESUMABLY**) we're not running this code in the year 9999 + now.date().next_day().unwrap() + }; + let update_datetime = PrimitiveDateTime::new(update_date, update_time).assume_utc(); + // the unwrap here is fine as we're certain `update_datetime` is in the future and thus the + // resultant Duration is positive + let time_left: Duration = (update_datetime - now).try_into().unwrap(); + + log::info!( + "waiting until {update_datetime} to update the historical uptimes for the first time ({} seconds left)", time_left.as_secs() + ); + + tokio::select! { + biased; + _ = shutdown.recv() => { + trace!("UpdateHandler: Received shutdown"); + } + _ = sleep(time_left) => {} + } + + let mut interval = interval(ONE_DAY); while !shutdown.is_shutdown() { tokio::select! { - _ = sleep(ONE_DAY) => { - tokio::select! { - biased; - _ = shutdown.recv() => { - trace!("UpdateHandler: Received shutdown"); - } - Err(err) = self.update_uptimes() => { - // normally that would have been a warning rather than an error, - // however, in this case it implies some underlying issues with our database - // that might affect the entire program - error!( - "We failed to update daily uptimes of active nodes - {}", - err - ); - } - } - } + biased; _ = shutdown.recv() => { trace!("UpdateHandler: Received shutdown"); } + _ = interval.tick() => { + // we don't want to have another select here; uptime update is relatively speedy + // and we don't want to exit while we're in the middle of database update + if let Err(err) = self.update_uptimes().await { + error!("We failed to update daily uptimes of active nodes - {err}"); + } + } } } } diff --git a/validator-api/validator-api-requests/src/models.rs b/validator-api/validator-api-requests/src/models.rs index 398fe65c00..046e4152b3 100644 --- a/validator-api/validator-api-requests/src/models.rs +++ b/validator-api/validator-api-requests/src/models.rs @@ -5,11 +5,30 @@ use cosmwasm_std::{Coin, Decimal}; use mixnet_contract_common::mixnode::MixNodeDetails; use mixnet_contract_common::reward_params::{Performance, RewardingParams}; use mixnet_contract_common::rewarding::RewardEstimate; -use mixnet_contract_common::{Interval, MixId, MixNode, Percent, RewardedSetNodeStatus}; +use mixnet_contract_common::{ + IdentityKey, Interval, MixId, MixNode, Percent, RewardedSetNodeStatus, +}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::{fmt, time::Duration}; +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +pub struct RequestError { + message: String, +} + +impl RequestError { + pub fn new<S: Into<String>>(msg: S) -> Self { + RequestError { + message: msg.into(), + } + } + + pub fn message(&self) -> &str { + &self.message + } +} + #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( @@ -219,3 +238,45 @@ pub struct InclusionProbability { pub in_active: f64, pub in_reserve: f64, } + +type Uptime = u8; + +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)] +pub struct MixnodeStatusReportResponse { + pub mix_id: MixId, + pub identity: IdentityKey, + pub owner: String, + pub most_recent: Uptime, + pub last_hour: Uptime, + pub last_day: Uptime, +} + +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)] +pub struct GatewayStatusReportResponse { + pub identity: String, + pub owner: String, + pub most_recent: Uptime, + pub last_hour: Uptime, + pub last_day: Uptime, +} + +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)] +pub struct HistoricalUptimeResponse { + pub date: String, + pub uptime: Uptime, +} + +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)] +pub struct MixnodeUptimeHistoryResponse { + pub mix_id: MixId, + pub identity: String, + pub owner: String, + pub history: Vec<HistoricalUptimeResponse>, +} + +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)] +pub struct GatewayUptimeHistoryResponse { + pub identity: String, + pub owner: String, + pub history: Vec<HistoricalUptimeResponse>, +}