From f4f98027a03c4213ee48b1236b08914295e7f2ee Mon Sep 17 00:00:00 2001 From: Drazen Urch Date: Wed, 26 Oct 2022 10:51:40 +0200 Subject: [PATCH 01/31] Add per account pledge caps (#1687) * Add per account pledge caps * Address PR comments * Update CHANGELOG * No cap if no locked * Fail account creation if taking account already exists * Delegated free should be counted from vesting period start --- CHANGELOG.md | 3 + Cargo.lock | 7 ++ .../validator-client/src/nymd/mod.rs | 21 ++-- .../src/nymd/traits/vesting_signing_client.rs | 4 + .../vesting/create_vesting_schedule.rs | 8 ++ .../contracts-common/Cargo.toml | 2 + .../contracts-common/src/types.rs | 115 +++++++++++++++++- .../mixnet-contract/src/error.rs | 3 - .../mixnet-contract/src/rewarding/helpers.rs | 5 +- .../mixnet-contract/src/types.rs | 94 +------------- .../vesting-contract/Cargo.toml | 2 + .../vesting-contract/src/lib.rs | 73 +++++++++++ .../vesting-contract/src/messages.rs | 9 +- contracts/Cargo.lock | 5 + contracts/vesting/Cargo.toml | 1 + contracts/vesting/src/contract.rs | 36 +++--- contracts/vesting/src/errors.rs | 2 + contracts/vesting/src/storage.rs | 17 +-- contracts/vesting/src/support/tests.rs | 27 ++++ .../src/vesting/account/delegating_account.rs | 6 +- .../account/gateway_bonding_account.rs | 6 +- .../account/mixnode_bonding_account.rs | 6 +- contracts/vesting/src/vesting/account/mod.rs | 18 ++- .../src/vesting/account/vesting_account.rs | 12 +- contracts/vesting/src/vesting/mod.rs | 38 +++++- explorer-api/Cargo.toml | 1 + explorer-api/src/mix_node/econ_stats.rs | 2 +- nym-connect/Cargo.lock | 5 + nym-wallet/Cargo.lock | 5 + validator-api/Cargo.toml | 1 + validator-api/src/node_status_api/cache.rs | 2 +- 31 files changed, 369 insertions(+), 167 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48321f57c6..4b4e5d91ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - native-client/socks5-client: `use_extended_packet_size` Debug config option to make the client use 'ExtendedPacketSize' for its traffic (32kB as opposed to 2kB in 1.0.2) ([#1671]) - 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 +- vesting-contract: optional locked token pledge cap per account ([#1687]), defaults to 100_000 NYM ### Fixed @@ -26,6 +27,7 @@ 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]) +- moved `Percent` struct to to `contracts-common`, change affects explorer-api [#1541]: https://github.com/nymtech/nym/pull/1541 [#1558]: https://github.com/nymtech/nym/pull/1558 @@ -39,6 +41,7 @@ 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 +[#1687]: https://github.com/nymtech/nym/pull/1687 ## [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 627cb1f2b4..bfe183e51c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -737,7 +737,9 @@ name = "contracts-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "schemars", "serde", + "thiserror", ] [[package]] @@ -1584,6 +1586,7 @@ dependencies = [ "chrono", "clap 3.2.8", "dotenv", + "contracts-common", "humantime-serde", "isocountry", "itertools", @@ -3349,6 +3352,7 @@ dependencies = [ "coconut-interface", "config", "console-subscriber", + "contracts-common", "cosmwasm-std", "credential-storage", "credentials", @@ -6474,6 +6478,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "vesting-contract" version = "1.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", "cw-storage-plus", "mixnet-contract-common", @@ -6487,7 +6492,9 @@ dependencies = [ name = "vesting-contract-common" version = "0.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", + "log", "mixnet-contract-common", "schemars", "serde", 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_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/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..1c1ec8ebee 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml +++ b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml @@ -9,3 +9,5 @@ edition = "2021" [dependencies] cosmwasm-std = "1.0.0" serde = { version = "1.0", features = ["derive"] } +schemars = "0.8" +thiserror = "1" diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs index 6c25d95552..320212143d 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs @@ -1,7 +1,120 @@ // 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 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_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 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/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/types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs index 7d6e8f2d0d..f4af0362db 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_atomics(100u32, 0).unwrap() * 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, @@ -209,7 +125,7 @@ pub struct PagedRewardedSetResponse { #[cfg(test)] mod tests { - use super::*; + use contracts_common::Percent; #[test] fn percent_serde() { 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 1d7b4d0ab1..4d494510b5 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs @@ -1,6 +1,10 @@ +use std::str::FromStr; + // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +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}; @@ -42,6 +46,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, @@ -98,3 +142,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..98f8024dbc 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, }, } @@ -201,7 +205,6 @@ pub enum QueryMsg { GetCurrentVestingPeriod { address: String, }, - GetLockedPledgeCap {}, GetDelegationTimes { address: String, mix_id: MixId, diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 3490bbf2fd..a62c529faf 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,6 +1608,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "vesting-contract" version = "1.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", "cw-storage-plus", "mixnet-contract-common", @@ -1619,7 +1622,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/vesting/Cargo.toml b/contracts/vesting/Cargo.toml index 3c03d92c91..f55bd649f1 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 "} diff --git a/contracts/vesting/src/contract.rs b/contracts/vesting/src/contract.rs index 99a2acb9f4..9d1cee6711 100644 --- a/contracts/vesting/src/contract.rs +++ b/contracts/vesting/src/contract.rs @@ -1,8 +1,8 @@ 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, @@ -25,8 +25,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 +59,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 +88,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 +146,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 +436,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 +444,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 +460,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 +485,7 @@ fn try_create_periodic_vesting_account( coin.clone(), start_time, periods, + cap, deps.storage, )?; @@ -486,7 +500,6 @@ 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::LockedCoins { vesting_account_address, block_time, @@ -567,11 +580,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, diff --git a/contracts/vesting/src/errors.rs b/contracts/vesting/src/errors.rs index 92b8524292..bc992e7ca4 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 203ab3feb2..8520dad85e 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 d7eeeb0946..53f39e2891 100644 --- a/contracts/vesting/src/vesting/account/vesting_account.rs +++ b/contracts/vesting/src/vesting/account/vesting_account.rs @@ -123,12 +123,13 @@ impl VestingAccount for Account { storage: &dyn Storage, ) -> Result { let block_time = block_time.unwrap_or(env.block.time); - let period = self.get_current_vesting_period(block_time); let withdrawn = self.load_withdrawn(storage)?; let max_available = self .get_vested_coins(Some(block_time), env, storage)? .amount .saturating_sub(withdrawn); + + let period = self.get_current_vesting_period(block_time); let start_time = match period { Period::Before => 0, Period::After => u64::MAX, @@ -155,15 +156,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 as usize].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 3e46c46496..d678d2aab8 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()); @@ -389,12 +392,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( @@ -434,7 +463,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 { @@ -449,6 +478,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) @@ -813,6 +846,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..8aae21b2ef 100644 --- a/explorer-api/Cargo.toml +++ b/explorer-api/Cargo.toml @@ -27,6 +27,7 @@ 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" } task = { path = "../common/task" } validator-client = { path = "../common/client-libs/validator-client", features=["nymd-client"] } 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/nym-connect/Cargo.lock b/nym-connect/Cargo.lock index 01a52ecf52..5a102a8d6e 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]] @@ -6158,6 +6160,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "vesting-contract" version = "1.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", "cw-storage-plus", "mixnet-contract-common", @@ -6171,7 +6174,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/Cargo.lock b/nym-wallet/Cargo.lock index e861e1b5fc..1c44d275a5 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -665,7 +665,9 @@ name = "contracts-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "schemars", "serde", + "thiserror", ] [[package]] @@ -5308,6 +5310,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "vesting-contract" version = "1.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", "cw-storage-plus", "mixnet-contract-common", @@ -5321,7 +5324,9 @@ dependencies = [ name = "vesting-contract-common" version = "0.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", + "log", "mixnet-contract-common", "schemars", "serde", diff --git a/validator-api/Cargo.toml b/validator-api/Cargo.toml index 498773a970..b4c82a5748 100644 --- a/validator-api/Cargo.toml +++ b/validator-api/Cargo.toml @@ -72,6 +72,7 @@ crypto = { path = "../common/crypto" } 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/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; From cb1e93e58d6692aae95ebf07783eaa425afcd2a8 Mon Sep 17 00:00:00 2001 From: pierre Date: Wed, 26 Oct 2022 12:06:47 +0200 Subject: [PATCH 02/31] fix(explorer-api): geoip lookup --- Cargo.lock | 2 +- explorer-api/src/geo_ip/location.rs | 27 +++++++++++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bfe183e51c..c7367f201e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1585,8 +1585,8 @@ version = "1.0.1" dependencies = [ "chrono", "clap 3.2.8", - "dotenv", "contracts-common", + "dotenv", "humantime-serde", "isocountry", "itertools", diff --git a/explorer-api/src/geo_ip/location.rs b/explorer-api/src/geo_ip/location.rs index 643de3e76d..65f7edc120 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 { @@ -53,9 +58,23 @@ impl GeoIp { } 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 + let ip: IpAddr = FromStr::from_str(address).or_else(|_| { + warn!( + "Fail to create IpAddr from {}. Trying using internal lookup...", + &address + ); + let socket_addr = (address, FAKE_PORT) + .to_socket_addrs() + .map_err(|e| { + error!("Fail to resolve IP address from {}: {}", &address, e); + GeoIpError::NoValidIP + })? + .next() + .ok_or_else(|| { + error!("Fail to resolve IP address from {}", &address); + GeoIpError::NoValidIP + })?; + Ok(socket_addr.ip()) })?; let result = self .db From cf10bb12efd3dcc3d3a905cdfabbced38d730176 Mon Sep 17 00:00:00 2001 From: pierre Date: Wed, 26 Oct 2022 12:23:33 +0200 Subject: [PATCH 03/31] feat(explorer-api): use mixnode port in ip lookup --- explorer-api/src/country_statistics/geolocate.rs | 5 ++++- explorer-api/src/geo_ip/location.rs | 9 +++++---- explorer-api/src/guards/location.rs | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) 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 65f7edc120..ba75d4e52c 100644 --- a/explorer-api/src/geo_ip/location.rs +++ b/explorer-api/src/geo_ip/location.rs @@ -57,21 +57,22 @@ impl GeoIp { GeoIp { db: reader } } - pub fn query(&self, address: &str) -> Result, GeoIpError> { + pub fn query(&self, address: &str, port: Option) -> Result, GeoIpError> { let ip: IpAddr = FromStr::from_str(address).or_else(|_| { warn!( "Fail to create IpAddr from {}. Trying using internal lookup...", &address ); - let socket_addr = (address, FAKE_PORT) + 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, 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); + error!("Fail to resolve IP address from {}:{}", &address, p); GeoIpError::NoValidIP })?; Ok(socket_addr.ip()) 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 => { From 3f4373eb98bd6508a238f7fd3737f65a53fb3903 Mon Sep 17 00:00:00 2001 From: pierre Date: Wed, 26 Oct 2022 12:29:01 +0200 Subject: [PATCH 04/31] feat(explorer-api): add debug logs --- explorer-api/src/geo_ip/location.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/explorer-api/src/geo_ip/location.rs b/explorer-api/src/geo_ip/location.rs index ba75d4e52c..a1afec55d6 100644 --- a/explorer-api/src/geo_ip/location.rs +++ b/explorer-api/src/geo_ip/location.rs @@ -59,7 +59,7 @@ impl GeoIp { pub fn query(&self, address: &str, port: Option) -> Result, GeoIpError> { let ip: IpAddr = FromStr::from_str(address).or_else(|_| { - warn!( + debug!( "Fail to create IpAddr from {}. Trying using internal lookup...", &address ); @@ -75,7 +75,9 @@ impl GeoIp { error!("Fail to resolve IP address from {}:{}", &address, p); GeoIpError::NoValidIP })?; - Ok(socket_addr.ip()) + let ip = socket_addr.ip(); + debug!("Internal lookup succeed, resolved ip: {}", ip); + Ok(ip) })?; let result = self .db From 4df927cc3d89b8559643aa9a744e6fd82a06e47b Mon Sep 17 00:00:00 2001 From: pierre Date: Wed, 26 Oct 2022 16:51:25 +0200 Subject: [PATCH 05/31] fix(explorer): mixnode location overflow --- explorer/src/pages/Mixnodes/index.tsx | 31 +++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/explorer/src/pages/Mixnodes/index.tsx b/explorer/src/pages/Mixnodes/index.tsx index 8e4774e721..93eb35b3e6 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, Tooltip, 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'; @@ -244,7 +244,34 @@ export const PageMixnodes: React.FC = () => { justifyContent: 'flex-start', }} > - {params.value} + + + {params.value} + + ), }, From 56cf1817708471f71f91685c85513292ba1c609a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 26 Oct 2022 17:26:17 +0200 Subject: [PATCH 06/31] validator-api: use response type for history and report endpoints (#1711) --- Cargo.lock | 2 +- .../validator-client/src/validator_api/mod.rs | 75 ++++++++++++++++++- .../src/validator_api/routes.rs | 2 + validator-api/src/node_status_api/helpers.rs | 11 ++- validator-api/src/node_status_api/models.rs | 59 +++++++++++++++ validator-api/src/node_status_api/routes.rs | 21 +++--- .../validator-api-requests/src/models.rs | 46 +++++++++++- 7 files changed, 197 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bfe183e51c..c7367f201e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1585,8 +1585,8 @@ version = "1.0.1" dependencies = [ "chrono", "clap 3.2.8", - "dotenv", "contracts-common", + "dotenv", "humantime-serde", "isocountry", "itertools", 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..faed34b4dc 100644 --- a/common/client-libs/validator-client/src/validator_api/mod.rs +++ b/common/client-libs/validator-client/src/validator_api/mod.rs @@ -12,9 +12,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, + RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, }; pub mod error; @@ -135,6 +136,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> { 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..5100f59e05 100644 --- a/common/client-libs/validator-client/src/validator_api/routes.rs +++ b/common/client-libs/validator-client/src/validator_api/routes.rs @@ -28,6 +28,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/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 { +) -> Result { 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 { +) -> Result { 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..b793db5901 100644 --- a/validator-api/src/node_status_api/models.rs +++ b/validator-api/src/node_status_api/models.rs @@ -21,6 +21,10 @@ 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, +}; // todo: put into some error enum #[derive(Debug)] @@ -153,6 +157,19 @@ impl MixnodeStatusReport { } } +impl From 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 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 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 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,6 +292,15 @@ pub struct HistoricalUptime { pub(crate) uptime: Uptime, } +impl From for HistoricalUptimeResponse { + fn from(uptime: HistoricalUptime) -> Self { + HistoricalUptimeResponse { + date: uptime.date, + uptime: uptime.uptime.0, + } + } +} + pub(crate) struct ErrorResponse { error_message: String, status: Status, 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, identity: &str, -) -> Result, ErrorResponse> { +) -> Result, 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, identity: &str, -) -> Result, ErrorResponse> { +) -> Result, 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, mix_id: MixId, -) -> Result, ErrorResponse> { +) -> Result, 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, mix_id: MixId, -) -> Result, ErrorResponse> { +) -> Result, ErrorResponse> { Ok(Json(_mixnode_uptime_history(storage, mix_id).await?)) } diff --git a/validator-api/validator-api-requests/src/models.rs b/validator-api/validator-api-requests/src/models.rs index 398fe65c00..9435749fb0 100644 --- a/validator-api/validator-api-requests/src/models.rs +++ b/validator-api/validator-api-requests/src/models.rs @@ -5,7 +5,9 @@ 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}; @@ -219,3 +221,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, +} + +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)] +pub struct GatewayUptimeHistoryResponse { + pub identity: String, + pub owner: String, + pub history: Vec, +} From 0d399f7d707f183bfbe88671b0e324aaa36f67d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 26 Oct 2022 17:26:31 +0200 Subject: [PATCH 07/31] connect: tidy error enum (#1712) --- nym-connect/src-tauri/src/config/mod.rs | 2 +- nym-connect/src-tauri/src/error.rs | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) 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 { 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")] From 4990a4745fddd29d9e22e681793b7ef827b1be98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Thu, 27 Oct 2022 10:52:57 +0200 Subject: [PATCH 08/31] Add two more sphinx extended packet sizes (8kb and 16kb) (#1694) * Rename to ExtendedPacketSize32 * Add two more extended packet sizes * Update config handling for new packet sizes * Update wasm-client * Changelog: update * wasm-client: fix ref * Switch use enum instead of string for config --- CHANGELOG.md | 1 + clients/client-core/src/config/mod.rs | 29 +++++++-- clients/native/src/client/mod.rs | 11 ++-- clients/socks5/src/client/mod.rs | 11 ++-- clients/webassembly/src/client/config.rs | 11 +++- clients/webassembly/src/client/mod.rs | 9 ++- .../gateway-client/src/packet_router.rs | 14 +++- .../src/packet_processor/processor.rs | 5 +- common/nymsphinx/forwarding/src/packet.rs | 6 +- common/nymsphinx/framing/src/codec.rs | 8 ++- common/nymsphinx/framing/src/packet.rs | 4 +- common/nymsphinx/params/src/packet_sizes.rs | 64 +++++++++++++++++-- gateway/gateway-requests/src/types.rs | 6 +- 13 files changed, 140 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b4e5d91ba..cb862a7f77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ 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 - 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 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/src/client/mod.rs b/clients/native/src/client/mod.rs index 38f3d083a0..2b9e843695 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -31,7 +31,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/socks5/src/client/mod.rs b/clients/socks5/src/client/mod.rs index 2e45d59281..7892382246 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/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 544447e9dc..8f71155563 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/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..7998c11d0c 100644 --- a/common/nymsphinx/framing/src/codec.rs +++ b/common/nymsphinx/framing/src/codec.rs @@ -217,7 +217,9 @@ mod packet_encoding { 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 { @@ -255,7 +257,9 @@ mod packet_encoding { let packet_sizes = vec![ PacketSize::AckPacket, PacketSize::RegularPacket, - PacketSize::ExtendedPacket, + PacketSize::ExtendedPacket8, + PacketSize::ExtendedPacket16, + PacketSize::ExtendedPacket32, ]; for packet_size in packet_sizes { diff --git a/common/nymsphinx/framing/src/packet.rs b/common/nymsphinx/framing/src/packet.rs index 7c1881f318..860ddf7ec2 100644 --- a/common/nymsphinx/framing/src/packet.rs +++ b/common/nymsphinx/framing/src/packet.rs @@ -140,7 +140,9 @@ mod header_encoding { 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 { 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/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"), From d36e349cc67d610ce47bfb72adebd6dc26460e08 Mon Sep 17 00:00:00 2001 From: Fouad Date: Thu, 27 Oct 2022 10:34:11 +0100 Subject: [PATCH 09/31] Display routing scores for bonded gateway (#1693) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * access gateway report from node status api * Create 4 response types for gateway and mixnode uptime and status * Add the three remaining validator-client functions * display gatways routing scores * handle undefined gateway report Co-authored-by: Jon Häggblad --- .../src/validator_api/routes.rs | 1 - nym-wallet/src-tauri/src/main.rs | 1 + .../src/operations/validator_api/status.rs | 13 +++++- .../src/components/Bonding/BondedGateway.tsx | 45 +++++++++++++------ nym-wallet/src/context/bonding.tsx | 18 +++++++- nym-wallet/src/context/mocks/bonding.tsx | 4 ++ .../bonding/node-settings/NodeSettings.tsx | 2 +- .../settings-pages/NodeUnbondPage.tsx | 20 ++++++--- nym-wallet/src/requests/queries.ts | 5 ++- nym-wallet/src/types/global.ts | 8 ++++ 10 files changed, 92 insertions(+), 25 deletions(-) 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 5100f59e05..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"; 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/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 { + 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/Bonding/BondedGateway.tsx b/nym-wallet/src/components/Bonding/BondedGateway.tsx index 15a5d26367..305d261cca 100644 --- a/nym-wallet/src/components/Bonding/BondedGateway.tsx +++ b/nym-wallet/src/components/Bonding/BondedGateway.tsx @@ -1,26 +1,37 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { Stack, Typography } from '@mui/material'; import { Link } from '@nymproject/react/link/Link'; import { TBondedGateway, urls } from 'src/context'; import { NymCard } from 'src/components'; import { Network } from 'src/types'; import { IdentityKey } from 'src/components/IdentityKey'; +import { getGatewayReport } from 'src/requests'; import { Cell, Header, NodeTable } from './NodeTable'; import { BondedGatewayActions, TBondedGatwayActions } from './BondedGatewayAction'; +import { Console } from 'src/utils/console'; 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 +44,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: , id: 'actions-cell', @@ -73,7 +92,7 @@ export const BondedGateway = ({ {network && ( - Check more stats of your node on the{' '} + Check more stats of your gateway on the{' '} explorer diff --git a/nym-wallet/src/context/bonding.tsx b/nym-wallet/src/context/bonding.tsx index 6f87921db6..dcecd5bac2 100644 --- a/nym-wallet/src/context/bonding.tsx +++ b/nym-wallet/src/context/bonding.tsx @@ -32,6 +32,7 @@ import { getInclusionProbability, getMixnodeAvgUptime, getMixnodeRewardEstimation, + getGatewayReport, } from '../requests'; import { useCheckOwnership } from '../hooks/useCheckOwnership'; import { AppContext } from './main'; @@ -78,6 +79,10 @@ export interface TBondedGateway { mixPort: number; verlocPort: number; version: string; + routingScore?: { + current: number; + average: number; + }; } export type TokenPool = 'locked' | 'balance'; @@ -215,6 +220,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); @@ -282,7 +297,7 @@ 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, @@ -290,6 +305,7 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod location: data.gateway.location, bond: data.pledge_amount, proxy: data.proxy, + routingScore, } as TBondedGateway); } } catch (e: any) { diff --git a/nym-wallet/src/context/mocks/bonding.tsx b/nym-wallet/src/context/mocks/bonding.tsx index 6ab98ce490..a70501dbae 100644 --- a/nym-wallet/src/context/mocks/bonding.tsx +++ b/nym-wallet/src/context/mocks/bonding.tsx @@ -38,6 +38,10 @@ const bondedGatewayMock: TBondedGateway = { mixPort: 1789, verlocPort: 1790, version: '1.0.2', + routingScore: { + average: 100, + current: 100, + }, }; const TxResultMock: TransactionExecuteResult = { diff --git a/nym-wallet/src/pages/bonding/node-settings/NodeSettings.tsx b/nym-wallet/src/pages/bonding/node-settings/NodeSettings.tsx index c9bd51687f..0c6fc495f3 100644 --- a/nym-wallet/src/pages/bonding/node-settings/NodeSettings.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/NodeSettings.tsx @@ -75,7 +75,7 @@ export const NodeSettings = () => { - Node Settings + {isMixnode(bondedNode) ? 'Node' : 'Gateway'} Settings 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..89efefb0a8 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; onError: (e: string) => void; } @@ -21,15 +23,21 @@ export const NodeUnbondPage = ({ bondedNode, onConfirm, onError }: Props) => { Unbond - - theme.palette.nym.text.muted }}> - If you unbond your node you will loose all delegations! - - + {isMixnode(bondedNode) && ( + + theme.palette.nym.text.muted }}> + If you unbond you will loose all delegations! + + + )} - + 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('get_pending_interval_events'); + +export const getGatewayReport = async (identity: string) => + invokeWrapper('gateway_report', { identity }); diff --git a/nym-wallet/src/types/global.ts b/nym-wallet/src/types/global.ts index 1bba38e24f..e2dc6a2af8 100644 --- a/nym-wallet/src/types/global.ts +++ b/nym-wallet/src/types/global.ts @@ -61,6 +61,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; From 306e9b9dc2e7bf63656afc1bf9d142dc4545d372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 27 Oct 2022 10:51:09 +0100 Subject: [PATCH 10/31] Bugfix/correct staking supply accounting (#1706) * Added staking_supply_scale_factor field on RewardingParams * Scaling the amount of tokens released to the circulating/staking supplies --- .../contracts-common/src/types.rs | 8 ++++++++ .../mixnet-contract/src/msg.rs | 2 ++ .../mixnet-contract/src/reward_params.rs | 5 +++++ .../mixnet-contract/src/rewarding/simulator/mod.rs | 10 +++++++++- contracts/mixnet/src/contract.rs | 2 ++ contracts/mixnet/src/rewards/helpers.rs | 3 ++- contracts/mixnet/src/support/tests/mod.rs | 1 + 7 files changed, 29 insertions(+), 2 deletions(-) diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs index 320212143d..e9b399c02e 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs @@ -47,6 +47,14 @@ impl Percent { 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)) } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index ee172a4c20..c2046af9b0 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -31,6 +31,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, @@ -51,6 +52,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 fc543a3c02..73459f42d9 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs @@ -26,6 +26,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. 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 bd4fc8d8d3..5fe1c46175 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 @@ -40,7 +40,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 / Decimal::from_atomics(self.interval.epochs_in_interval(), 0).unwrap() * old.interval_pool_emission.value(); @@ -51,6 +56,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, @@ -209,6 +215,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(), @@ -537,6 +544,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/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index 1e91323c6c..af41f50b83 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/rewards/helpers.rs b/contracts/mixnet/src/rewards/helpers.rs index 02cdf83e68..894c260e6f 100644 --- a/contracts/mixnet/src/rewards/helpers.rs +++ b/contracts/mixnet/src/rewards/helpers.rs @@ -20,7 +20,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 / Decimal::from_atomics(interval.epochs_in_interval(), 0).unwrap() * rewarding_params.interval.interval_pool_emission; 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(), From 8bcec241a207682a8fe9b03bb4a794dd2c8de235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 27 Oct 2022 11:33:33 +0100 Subject: [PATCH 11/31] Moves Percent tests to the crate with the type definition (#1714) --- Cargo.lock | 1 + .../contracts-common/Cargo.toml | 3 ++ .../contracts-common/src/types.rs | 44 ++++++++++++++++++ .../mixnet-contract/src/types.rs | 45 ------------------- 4 files changed, 48 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c7367f201e..d88659c295 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -739,6 +739,7 @@ dependencies = [ "cosmwasm-std", "schemars", "serde", + "serde_json", "thiserror", ] diff --git a/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml index 1c1ec8ebee..4f98451100 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml +++ b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml @@ -11,3 +11,6 @@ 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 e9b399c02e..a4dda07148 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs @@ -152,3 +152,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/types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs index f4af0362db..41914e62fb 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs @@ -122,48 +122,3 @@ pub struct PagedRewardedSetResponse { pub nodes: Vec<(MixId, RewardedSetNodeStatus)>, pub start_next_after: Option, } - -#[cfg(test)] -mod tests { - use contracts_common::Percent; - - #[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); - } -} From 8656abcbde951bb58c0005ceaf69ff697aca6055 Mon Sep 17 00:00:00 2001 From: Pierre Dommerc Date: Thu, 27 Oct 2022 17:16:30 +0200 Subject: [PATCH 12/31] fix(explorer): minoxde details page (#1715) * fix(explorer): minoxde details page * feat(explorer): add mix_id column in mixnodes list --- explorer-api/src/mix_node/models.rs | 1 + explorer-api/src/mix_nodes/models.rs | 1 + explorer/src/components/MixNodes/index.ts | 2 ++ explorer/src/pages/Mixnodes/index.tsx | 32 ++++++++++++++++++----- explorer/src/typeDefs/explorer-api.ts | 1 + 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/explorer-api/src/mix_node/models.rs b/explorer-api/src/mix_node/models.rs index 0332aeaa17..225aab1f4c 100644 --- a/explorer-api/src/mix_node/models.rs +++ b/explorer-api/src/mix_node/models.rs @@ -24,6 +24,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, diff --git a/explorer-api/src/mix_nodes/models.rs b/explorer-api/src/mix_nodes/models.rs index 11d91a6eb1..4909ebdf6e 100644 --- a/explorer-api/src/mix_nodes/models.rs +++ b/explorer-api/src/mix_nodes/models.rs @@ -154,6 +154,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), diff --git a/explorer/src/components/MixNodes/index.ts b/explorer/src/components/MixNodes/index.ts index 112e55163a..6718e341bd 100644 --- a/explorer/src/components/MixNodes/index.ts +++ b/explorer/src/components/MixNodes/index.ts @@ -2,6 +2,7 @@ import { MixNodeResponse, MixNodeResponseItem, MixnodeStatus } from '../../typeDefs/explorer-api'; export type MixnodeRowType = { + mix_id: number; id: string; status: MixnodeStatus; owner: string; @@ -29,6 +30,7 @@ export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem): const profitPercentage = item.mix_node.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, diff --git a/explorer/src/pages/Mixnodes/index.tsx b/explorer/src/pages/Mixnodes/index.tsx index 93eb35b3e6..3cede1e92f 100644 --- a/explorer/src/pages/Mixnodes/index.tsx +++ b/explorer/src/pages/Mixnodes/index.tsx @@ -81,6 +81,24 @@ export const PageMixnodes: React.FC = () => { }; const columns: GridColDef[] = [ + { + field: 'mix_id', + headerName: 'Mix ID', + renderHeader: () => , + headerClassName: 'MuiDataGrid-header-override', + width: 100, + headerAlign: 'left', + renderCell: (params: GridRenderCellParams) => ( + + {params.value} + + ), + }, { field: 'identity_key', headerName: 'Identity Key', @@ -98,7 +116,7 @@ export const PageMixnodes: React.FC = () => { {splice(7, 29, params.value)} @@ -118,7 +136,7 @@ export const PageMixnodes: React.FC = () => { {currencyToString(params.value)} @@ -143,7 +161,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)} %`} @@ -161,7 +179,7 @@ export const PageMixnodes: React.FC = () => { {currencyToString(params.value)} @@ -183,7 +201,7 @@ export const PageMixnodes: React.FC = () => { {params.value} @@ -205,7 +223,7 @@ export const PageMixnodes: React.FC = () => { {params.value} @@ -286,7 +304,7 @@ export const PageMixnodes: React.FC = () => { {params.value} diff --git a/explorer/src/typeDefs/explorer-api.ts b/explorer/src/typeDefs/explorer-api.ts index dd342ac149..dcd7554c89 100644 --- a/explorer/src/typeDefs/explorer-api.ts +++ b/explorer/src/typeDefs/explorer-api.ts @@ -70,6 +70,7 @@ export const toMixnodeStatus = (status?: MixnodeStatusWithAll): MixnodeStatus | }; export interface MixNodeResponseItem { + mix_id: number; pledge_amount: Amount; total_delegation: Amount; owner: string; From b21ca41e16224cb7a3cb95e3365906ae18127521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 27 Oct 2022 16:18:55 +0100 Subject: [PATCH 13/31] Adding staking supply scale factor in rewarding params update (#1716) --- .../mixnet-contract/src/reward_params.rs | 8 ++++++++ contracts/mixnet/src/interval/pending_events.rs | 1 + contracts/mixnet/src/rewards/transactions.rs | 6 ++++++ 3 files changed, 15 insertions(+) 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 73459f42d9..c8e38fbe5b 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs @@ -169,6 +169,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; } @@ -240,6 +244,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, @@ -257,6 +264,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/contracts/mixnet/src/interval/pending_events.rs b/contracts/mixnet/src/interval/pending_events.rs index 84a5be1163..6383169f97 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/transactions.rs b/contracts/mixnet/src/rewards/transactions.rs index b56686651f..342522ac9e 100644 --- a/contracts/mixnet/src/rewards/transactions.rs +++ b/contracts/mixnet/src/rewards/transactions.rs @@ -1710,6 +1710,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, @@ -1742,6 +1743,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, @@ -1761,6 +1763,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, @@ -1795,6 +1798,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, @@ -1819,6 +1823,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, @@ -1859,6 +1864,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, From fb1649bab5a5080d50e2d1e3ac171fab28532700 Mon Sep 17 00:00:00 2001 From: Pierre Dommerc Date: Fri, 28 Oct 2022 10:50:49 +0200 Subject: [PATCH 14/31] fix(explorer): gateway list location column (#1718) * fix(explorer): gateway list location column * fix(explorer): gateway list columns width --- explorer/src/components/Tooltip.tsx | 36 +++++++++++++++++++++++++++ explorer/src/pages/Gateways/index.tsx | 19 +++++++++++--- explorer/src/pages/Mixnodes/index.tsx | 22 +++------------- 3 files changed, 54 insertions(+), 23 deletions(-) create mode 100644 explorer/src/components/Tooltip.tsx 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/pages/Gateways/index.tsx b/explorer/src/pages/Gateways/index.tsx index 39f3619308..76827566ca 100644 --- a/explorer/src/pages/Gateways/index.tsx +++ b/explorer/src/pages/Gateways/index.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Button, Card, Grid, Typography } from '@mui/material'; +import { Box, Button, Card, Grid, Typography } from '@mui/material'; import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; import { SelectChangeEvent } from '@mui/material/Select'; import { useMainContext } from '../../context/main'; @@ -10,6 +10,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(); @@ -84,7 +85,7 @@ export const PageGateways: React.FC = () => { { field: 'host', renderHeader: () => , - width: 110, + width: 180, headerAlign: 'left', headerClassName: 'MuiDataGrid-header-override', renderCell: (params: GridRenderCellParams) => ( @@ -96,7 +97,7 @@ export const PageGateways: React.FC = () => { { field: 'location', renderHeader: () => , - width: 150, + width: 180, headerAlign: 'left', headerClassName: 'MuiDataGrid-header-override', renderCell: (params: GridRenderCellParams) => ( @@ -105,7 +106,17 @@ export const PageGateways: React.FC = () => { sx={{ ...cellStyles, justifyContent: 'flex-start' }} data-testid="location-button" > - {params.value} + + + {params.value} + + ), }, diff --git a/explorer/src/pages/Mixnodes/index.tsx b/explorer/src/pages/Mixnodes/index.tsx index 3cede1e92f..94fda7867a 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, Tooltip, Box } 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); @@ -262,24 +263,7 @@ export const PageMixnodes: React.FC = () => { justifyContent: 'flex-start', }} > - + Date: Fri, 28 Oct 2022 14:00:54 +0100 Subject: [PATCH 15/31] comment regarding ts-rs compilation warning --- .../cosmwasm-smart-contracts/mixnet-contract/src/interval.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs index 560f1582b7..efedb88b8a 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; }"))] From 90cc23999945b779f59b1c88b0c7932a45371f5f Mon Sep 17 00:00:00 2001 From: Pierre Dommerc Date: Fri, 28 Oct 2022 15:12:37 +0200 Subject: [PATCH 16/31] Feature/ne gateway details (#1722) * create gateway details page * adding uptime chart * adding loading state for gateways * adding link style * fixing gateways pagination * remove gateway name and desc * adding correct toolpit text and cleaning * fix build * PR requested changes * fix build * requested changes * fix build a rever console utility addition Co-authored-by: Gala --- explorer/src/api/constants.ts | 1 + explorer/src/api/index.ts | 10 +- explorer/src/components/DetailTable.tsx | 93 ++++++----- explorer/src/components/Gateways.ts | 31 +++- explorer/src/context/gateway.tsx | 59 +++++++ explorer/src/context/hooks.ts | 47 ++++-- explorer/src/context/main.tsx | 1 + explorer/src/context/mixnode.tsx | 45 +----- explorer/src/pages/GatewayDetail/index.tsx | 179 +++++++++++++++++++++ explorer/src/pages/Gateways/index.tsx | 46 +++--- explorer/src/routes/network-components.tsx | 2 + explorer/src/typeDefs/explorer-api.ts | 9 +- 12 files changed, 400 insertions(+), 123 deletions(-) create mode 100644 explorer/src/context/gateway.tsx create mode 100644 explorer/src/pages/GatewayDetail/index.tsx 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/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..a8db9e270e 100644 --- a/explorer/src/context/main.tsx +++ b/explorer/src/context/main.tsx @@ -109,6 +109,7 @@ export const MainContextProvider: React.FC = ({ children }) => { }; 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 76827566ca..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 { Box, 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'; @@ -44,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> ), }, { @@ -120,6 +112,19 @@ export const PageGateways: React.FC = () => { </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>) => { @@ -144,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/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 dcd7554c89..22ea762a29 100644 --- a/explorer/src/typeDefs/explorer-api.ts +++ b/explorer/src/typeDefs/explorer-api.ts @@ -127,12 +127,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; From 19f3c76f721cba55283b456fe1f436c893078cce Mon Sep 17 00:00:00 2001 From: Pierre Dommerc <dommerc.pierre@gmail.com> Date: Fri, 28 Oct 2022 16:24:52 +0200 Subject: [PATCH 17/31] Feature/explorer operating cost (#1719) * feat(explorer): operating cost --- explorer-api/src/mix_node/models.rs | 1 + explorer-api/src/mix_nodes/models.rs | 1 + explorer/package.json | 3 +- .../components/MixNodes/Economics/Columns.ts | 8 +++++ .../Economics/MixNodeEconomics.stories.tsx | 6 ++++ .../src/components/MixNodes/Economics/Rows.ts | 7 +++- .../components/MixNodes/Economics/types.ts | 1 + explorer/src/components/MixNodes/index.ts | 4 +++ explorer/src/pages/Mixnodes/index.tsx | 22 ++++++++++++ explorer/src/typeDefs/explorer-api.ts | 1 + explorer/src/utils/currency.ts | 36 +++++++++++++++++++ 11 files changed, 88 insertions(+), 2 deletions(-) diff --git a/explorer-api/src/mix_node/models.rs b/explorer-api/src/mix_node/models.rs index 225aab1f4c..2ecb907f6a 100644 --- a/explorer-api/src/mix_node/models.rs +++ b/explorer-api/src/mix_node/models.rs @@ -36,6 +36,7 @@ pub(crate) struct PrettyDetailedMixNodeBond { pub avg_uptime: u8, pub estimated_operator_apy: f64, pub estimated_delegators_apy: f64, + pub operating_cost: Coin, } #[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 4909ebdf6e..d117bec721 100644 --- a/explorer-api/src/mix_nodes/models.rs +++ b/explorer-api/src/mix_nodes/models.rs @@ -166,6 +166,7 @@ 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(), } } 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/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..bcc43914a5 100644 --- a/explorer/src/components/MixNodes/Economics/Rows.ts +++ b/explorer/src/components/MixNodes/Economics/Rows.ts @@ -1,4 +1,4 @@ -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'; @@ -32,6 +32,8 @@ export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => { const profitMargin = mixNode?.data?.mix_node.profit_margin_percent || '-'; const avgUptime = economicDynamicsStats?.data?.current_interval_uptime; + const opCost = mixNode?.data?.operating_cost; + return { id: 1, estimatedTotalReward: { @@ -50,6 +52,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 6718e341bd..bcf5b09efd 100644 --- a/explorer/src/components/MixNodes/index.ts +++ b/explorer/src/components/MixNodes/index.ts @@ -1,5 +1,6 @@ /* eslint-disable camelcase */ import { MixNodeResponse, MixNodeResponseItem, MixnodeStatus } from '../../typeDefs/explorer-api'; +import { unymToNym } from '../../utils/currency'; export type MixnodeRowType = { mix_id: number; @@ -16,6 +17,7 @@ export type MixnodeRowType = { profit_percentage: string; avg_uptime: string; stake_saturation: number; + operating_cost: string; }; export function mixnodeToGridRow(arrayOfMixnodes?: MixNodeResponse): MixnodeRowType[] { @@ -29,6 +31,7 @@ export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem): const selfPercentage = ((pledge * 100) / totalBond).toFixed(2); const profitPercentage = item.mix_node.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, @@ -44,5 +47,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/pages/Mixnodes/index.tsx b/explorer/src/pages/Mixnodes/index.tsx index 94fda7867a..f37707c134 100644 --- a/explorer/src/pages/Mixnodes/index.tsx +++ b/explorer/src/pages/Mixnodes/index.tsx @@ -208,6 +208,28 @@ export const PageMixnodes: React.FC = () => { </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> + ), + }, { field: 'avg_uptime', headerName: 'Routing Score', diff --git a/explorer/src/typeDefs/explorer-api.ts b/explorer/src/typeDefs/explorer-api.ts index 22ea762a29..68563ee82a 100644 --- a/explorer/src/typeDefs/explorer-api.ts +++ b/explorer/src/typeDefs/explorer-api.ts @@ -86,6 +86,7 @@ export interface MixNodeResponseItem { mix_node: MixNode; avg_uptime: number; stake_saturation: number; + operating_cost: Amount; } export type MixNodeResponse = MixNodeResponseItem[]; 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; +}; From ec3a6b3e27ad93ee262aa64de89c73e013b6d512 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= <jon.haggblad@gmail.com> Date: Mon, 31 Oct 2022 11:56:31 +0100 Subject: [PATCH 18/31] socks5: wait to close buffer (v1.1 branch) (#1724) * socks5: wait to close buffer This is the fix proposed by @simonwicky in https://github.com/nymtech/nym/issues/1701 * socks5: fix typo in patch * socks5: fix tests * socks5: add type for returned data and index * socks5: make closed_at_index an Option * changelog: add note * changelog: update --- CHANGELOG.md | 3 ++ common/socks5/ordered-buffer/src/buffer.rs | 33 +++++++++++++------ common/socks5/ordered-buffer/src/lib.rs | 2 +- .../src/connection_controller.rs | 23 ++++++++----- 4 files changed, 42 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb862a7f77..6f649d977c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// ### 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],[#1724]) ### Changed @@ -43,6 +44,8 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// [#1671]: https://github.com/nymtech/nym/pull/1671 [#1673]: https://github.com/nymtech/nym/pull/1673 [#1687]: https://github.com/nymtech/nym/pull/1687 +[#1702]: https://github.com/nymtech/nym/pull/1702 +[#1724]: https://github.com/nymtech/nym/pull/1724 ## [nym-binaries-1.0.2](https://github.com/nymtech/nym/tree/nym-binaries-1.0.2) diff --git a/common/socks5/ordered-buffer/src/buffer.rs b/common/socks5/ordered-buffer/src/buffer.rs index 19d946f8d6..33c4d69142 100644 --- a/common/socks5/ordered-buffer/src/buffer.rs +++ b/common/socks5/ordered-buffer/src/buffer.rs @@ -13,6 +13,13 @@ pub struct OrderedMessageBuffer { messages: HashMap<u64, OrderedMessage>, } +/// Data returned from `OrderedMessageBuffer` on a successful read of gapless ordered data. +#[derive(Debug, PartialEq, Eq)] +pub struct ReadContiguousData { + pub data: Vec<u8>, + pub last_index: u64, +} + impl OrderedMessageBuffer { pub fn new() -> OrderedMessageBuffer { OrderedMessageBuffer { @@ -42,7 +49,7 @@ impl OrderedMessageBuffer { /// a read will return the bytes of messages 0, 1, 2. Subsequent reads will /// return `None` until message 3 comes in, at which point 3, 4, and any /// further contiguous messages which have arrived will be returned. - pub fn read(&mut self) -> Option<Vec<u8>> { + pub fn read(&mut self) -> Option<ReadContiguousData> { if !self.messages.contains_key(&self.next_index) { return None; } @@ -66,7 +73,10 @@ impl OrderedMessageBuffer { .collect(); trace!("Returning {} bytes from ordered message buffer", data.len()); - Some(data) + Some(ReadContiguousData { + data, + last_index: index, + }) } } @@ -102,11 +112,11 @@ mod test_chunking_and_reassembling { }; buffer.write(first_message); - let first_read = buffer.read().unwrap(); + let first_read = buffer.read().unwrap().data; assert_eq!(vec![1, 2, 3, 4], first_read); buffer.write(second_message); - let second_read = buffer.read().unwrap(); + let second_read = buffer.read().unwrap().data; assert_eq!(vec![5, 6, 7, 8], second_read); assert_eq!(None, buffer.read()); // second read on fully ordered result set is empty @@ -128,7 +138,7 @@ mod test_chunking_and_reassembling { buffer.write(first_message); buffer.write(second_message); let second_read = buffer.read(); - assert_eq!(vec![1, 2, 3, 4, 5, 6, 7, 8], second_read.unwrap()); + assert_eq!(vec![1, 2, 3, 4, 5, 6, 7, 8], second_read.unwrap().data); assert_eq!(None, buffer.read()); // second read on fully ordered result set is empty } @@ -147,8 +157,8 @@ mod test_chunking_and_reassembling { buffer.write(second_message); buffer.write(first_message); - let read = buffer.read(); - assert_eq!(vec![1, 2, 3, 4, 5, 6, 7, 8], read.unwrap()); + let read = buffer.read().unwrap().data; + assert_eq!(vec![1, 2, 3, 4, 5, 6, 7, 8], read); assert_eq!(None, buffer.read()); // second read on fully ordered result set is empty } } @@ -182,7 +192,7 @@ mod test_chunking_and_reassembling { #[test] fn everything_up_to_the_indexing_gap_is_returned() { let mut buffer = setup(); - let ordered_bytes = buffer.read().unwrap(); + let ordered_bytes = buffer.read().unwrap().data; assert_eq!([0, 0, 0, 0, 1, 1, 1, 1].to_vec(), ordered_bytes); // we shouldn't get any more from a second attempt if nothing is added @@ -208,7 +218,7 @@ mod test_chunking_and_reassembling { }; buffer.write(two_message); - let more_ordered_bytes = buffer.read().unwrap(); + let more_ordered_bytes = buffer.read().unwrap().data; assert_eq!([2, 2, 2, 2, 3, 3, 3, 3].to_vec(), more_ordered_bytes); // let's add another message @@ -227,7 +237,10 @@ mod test_chunking_and_reassembling { }; buffer.write(four_message); - assert_eq!([4, 4, 4, 4, 5, 5, 5, 5].to_vec(), buffer.read().unwrap()); + assert_eq!( + [4, 4, 4, 4, 5, 5, 5, 5].to_vec(), + buffer.read().unwrap().data + ); // at this point we should again get back nothing if we try a read assert_eq!(None, buffer.read()); diff --git a/common/socks5/ordered-buffer/src/lib.rs b/common/socks5/ordered-buffer/src/lib.rs index 7a3c9e6501..9ed9d9c867 100644 --- a/common/socks5/ordered-buffer/src/lib.rs +++ b/common/socks5/ordered-buffer/src/lib.rs @@ -2,7 +2,7 @@ mod buffer; mod message; mod sender; -pub use buffer::OrderedMessageBuffer; +pub use buffer::{OrderedMessageBuffer, ReadContiguousData}; pub use message::MessageError; pub use message::OrderedMessage; pub use sender::OrderedMessageSender; diff --git a/common/socks5/proxy-helpers/src/connection_controller.rs b/common/socks5/proxy-helpers/src/connection_controller.rs index 0eebf8d994..1a9b13cc29 100644 --- a/common/socks5/proxy-helpers/src/connection_controller.rs +++ b/common/socks5/proxy-helpers/src/connection_controller.rs @@ -4,7 +4,7 @@ use futures::channel::mpsc; use futures::StreamExt; use log::*; -use ordered_buffer::{OrderedMessage, OrderedMessageBuffer}; +use ordered_buffer::{OrderedMessage, OrderedMessageBuffer, ReadContiguousData}; use socks5_requests::ConnectionId; use std::collections::{HashMap, HashSet}; use task::ShutdownListener; @@ -38,12 +38,13 @@ pub enum ControllerCommand { struct ActiveConnection { is_closed: bool, + closed_at_index: Option<u64>, connection_sender: Option<ConnectionSender>, ordered_buffer: OrderedMessageBuffer, } impl ActiveConnection { - fn write_to_buf(&mut self, payload: Vec<u8>) { + fn write_to_buf(&mut self, payload: Vec<u8>, is_closed: bool) { let ordered_message = match OrderedMessage::try_from_bytes(payload) { Ok(msg) => msg, Err(err) => { @@ -51,10 +52,13 @@ impl ActiveConnection { return; } }; + if is_closed { + self.closed_at_index = Some(ordered_message.index); + } self.ordered_buffer.write(ordered_message); } - fn read_from_buf(&mut self) -> Option<Vec<u8>> { + fn read_from_buf(&mut self) -> Option<ReadContiguousData> { self.ordered_buffer.read() } } @@ -99,6 +103,7 @@ impl Controller { is_closed: false, connection_sender: Some(connection_sender), ordered_buffer: OrderedMessageBuffer::new(), + closed_at_index: None, }; if let Some(_active_conn) = self.active_connections.insert(conn_id, active_connection) { error!("Received a duplicate 'Connect'!") @@ -127,21 +132,23 @@ impl Controller { fn send_to_connection(&mut self, conn_id: ConnectionId, payload: Vec<u8>, is_closed: bool) { if let Some(active_connection) = self.active_connections.get_mut(&conn_id) { if !payload.is_empty() { - active_connection.write_to_buf(payload); + active_connection.write_to_buf(payload, is_closed); } else if !is_closed { error!("Tried to write an empty message to a not-closing connection. Please let us know if you see this message"); } - // if messages get unordered, make sure we don't lose information about - // remote socket getting closed! - active_connection.is_closed |= is_closed; if let Some(payload) = active_connection.read_from_buf() { + if let Some(closed_at_index) = active_connection.closed_at_index { + if payload.last_index > closed_at_index { + active_connection.is_closed = true; + } + } if let Err(err) = active_connection .connection_sender .as_mut() .unwrap() .unbounded_send(ConnectionMessage { - payload, + payload: payload.data, socket_closed: active_connection.is_closed, }) { From ebc13c43279caeb393708fe7a9293427237b5ae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= <jon.haggblad@gmail.com> Date: Mon, 31 Oct 2022 12:21:02 +0100 Subject: [PATCH 19/31] client: make channel to mix traffic controller bounded and add backpressure handling v1.1 (#1725) * clients: change mix traffic channel to bounded * clients: dynamically adjust sending delay in steps * rustfmt * wasm-client: update channel * client: introduce SendingDelayController * client-core: downgrade two debug statements to trace * sending delay controller: tweak parameters * wasm-client: add tokio dependency * client-core: rework delay controller * Revert "client-core: downgrade two debug statements to trace" This reverts commit e0a7772fafac7bff0e4a2c50ba25e94b52b794e6. * Remove outdated comment * Remove WIP comments * changelog: add note * out queue controller: simplify with just send * client-core: document constants * client: move creating mix msg channel to mix traffic controller * client-core: downgrade a warning log msg to debug * changelog: update --- CHANGELOG.md | 3 + .../src/client/cover_traffic_stream.rs | 18 +- clients/client-core/src/client/mix_traffic.rs | 32 ++-- .../real_traffic_stream.rs | 169 ++++++++++++++++-- clients/native/src/client/mod.rs | 29 ++- clients/socks5/src/client/mod.rs | 29 ++- clients/webassembly/src/client/mod.rs | 24 ++- 7 files changed, 227 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f649d977c..d867b8bb6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - 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]) - 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 @@ -45,7 +46,9 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// [#1673]: https://github.com/nymtech/nym/pull/1673 [#1687]: https://github.com/nymtech/nym/pull/1687 [#1702]: https://github.com/nymtech/nym/pull/1702 +[#1703]: https://github.com/nymtech/nym/pull/1703 [#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/clients/client-core/src/client/cover_traffic_stream.rs b/clients/client-core/src/client/cover_traffic_stream.rs index 0d362491ee..84fd277634 100644 --- a/clients/client-core/src/client/cover_traffic_stream.rs +++ b/clients/client-core/src/client/cover_traffic_stream.rs @@ -16,6 +16,7 @@ use rand::{rngs::OsRng, CryptoRng, Rng}; use std::pin::Pin; use std::sync::Arc; use std::time::Duration; +use tokio::sync::mpsc::error::TrySendError; #[cfg(not(target_arch = "wasm32"))] use tokio::time; @@ -171,11 +172,18 @@ impl LoopCoverTrafficStream<OsRng> { ) .expect("Somehow failed to generate a loop cover message with a valid topology"); - // if this one fails, there's no retrying because it means that either: - // - we run out of memory - // - the receiver channel is closed - // in either case there's no recovery and we can only panic - self.mix_tx.unbounded_send(vec![cover_message]).unwrap(); + if let Err(err) = self.mix_tx.try_send(vec![cover_message]) { + match err { + TrySendError::Full(_) => { + // This isn't a problem, if the channel is full means we're already sending the + // max amount of messages downstream can handle. + log::debug!("Failed to send cover message - channel full"); + } + TrySendError::Closed(_) => { + log::warn!("Failed to send cover message - channel closed"); + } + } + } // TODO: I'm not entirely sure whether this is really required, because I'm not 100% // sure how `yield_now()` works - whether it just notifies the scheduler or whether it diff --git a/clients/client-core/src/client/mix_traffic.rs b/clients/client-core/src/client/mix_traffic.rs index cc1d0619ae..d5702cc0a8 100644 --- a/clients/client-core/src/client/mix_traffic.rs +++ b/clients/client-core/src/client/mix_traffic.rs @@ -2,15 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 use crate::spawn_future; -use futures::channel::mpsc; -use futures::StreamExt; use gateway_client::GatewayClient; use log::*; use nymsphinx::forwarding::packet::MixPacket; -pub type BatchMixMessageSender = mpsc::UnboundedSender<Vec<MixPacket>>; -pub type BatchMixMessageReceiver = mpsc::UnboundedReceiver<Vec<MixPacket>>; +pub type BatchMixMessageSender = tokio::sync::mpsc::Sender<Vec<MixPacket>>; +pub type BatchMixMessageReceiver = tokio::sync::mpsc::Receiver<Vec<MixPacket>>; +// We remind ourselves that 32 x 32kb = 1024kb, a reasonable size for a network buffer. +pub const MIX_MESSAGE_RECEIVER_BUFFER_SIZE: usize = 32; const MAX_FAILURE_COUNT: usize = 100; pub struct MixTrafficController { @@ -25,15 +25,17 @@ pub struct MixTrafficController { } impl MixTrafficController { - pub fn new( - mix_rx: BatchMixMessageReceiver, - gateway_client: GatewayClient, - ) -> MixTrafficController { - MixTrafficController { - gateway_client, - mix_rx, - consecutive_gateway_failure_count: 0, - } + pub fn new(gateway_client: GatewayClient) -> (MixTrafficController, BatchMixMessageSender) { + let (sphinx_message_sender, sphinx_message_receiver) = + tokio::sync::mpsc::channel(MIX_MESSAGE_RECEIVER_BUFFER_SIZE); + ( + MixTrafficController { + gateway_client, + mix_rx: sphinx_message_receiver, + consecutive_gateway_failure_count: 0, + }, + sphinx_message_sender, + ) } async fn on_messages(&mut self, mut mix_packets: Vec<MixPacket>) { @@ -72,7 +74,7 @@ impl MixTrafficController { while !shutdown.is_shutdown() { tokio::select! { - mix_packets = self.mix_rx.next() => match mix_packets { + mix_packets = self.mix_rx.recv() => match mix_packets { Some(mix_packets) => { self.on_messages(mix_packets).await; }, @@ -96,7 +98,7 @@ impl MixTrafficController { spawn_future(async move { debug!("Started MixTrafficController without graceful shutdown support"); - while let Some(mix_packets) = self.mix_rx.next().await { + while let Some(mix_packets) = self.mix_rx.recv().await { self.on_messages(mix_packets).await; } }) diff --git a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs index ceb93764db..284211824b 100644 --- a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs +++ b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs @@ -27,6 +27,23 @@ use tokio::time; #[cfg(target_arch = "wasm32")] use wasm_timer; +// The minimum time between increasing the average delay between packets. If we hit the ceiling in +// the available buffer space we want to take somewhat swift action, but we still need to give a +// short time to give the channel a chance reduce pressure. +const INCREASE_DELAY_MIN_CHANGE_INTERVAL_SECS: u64 = 1; +// The minimum time between decreasing the average delay between packets. We don't want to change +// to quickly to keep things somewhat stable. Also there are buffers downstreams meaning we need to +// wait a little to see the effect before we decrease further. +const DECREASE_DELAY_MIN_CHANGE_INTERVAL_SECS: u64 = 30; +// If we enough time passes without any sign of backpressure in the channel, we can consider +// lowering the average delay. The goal is to keep somewhat stable, rather than maxing out +// bandwidth at all times. +const ACCEPTABLE_TIME_WITHOUT_BACKPRESSURE_SECS: u64 = 30; +// The maximum multiplier we apply to the base average Poisson delay. +const MAX_DELAY_MULTIPLIER: u32 = 6; +// The minium multiplier we apply to the base average Poisson delay. +const MIN_DELAY_MULTIPLIER: u32 = 1; + /// Configurable parameters of the `OutQueueControl` pub(crate) struct Config { /// Average delay an acknowledgement packet is going to get delay at a single mixnode. @@ -68,6 +85,101 @@ impl Config { } } +struct SendingDelayController { + /// Multiply the average sending delay. + /// This is normally set to unity, but if we detect backpressure we increase this + /// multiplier. We use discrete steps. + current_multiplier: u32, + + /// Maximum delay multiplier + upper_bound: u32, + + /// Minimum delay multiplier + lower_bound: u32, + + /// To make sure we don't change the multiplier to fast, we limit a change to some duration + #[cfg(not(target_arch = "wasm32"))] + time_when_changed: time::Instant, + + #[cfg(target_arch = "wasm32")] + time_when_changed: wasm_timer::Instant, + + /// If we have a long enough time without any backpressure detected we try reducing the sending + /// delay multiplier + #[cfg(not(target_arch = "wasm32"))] + time_when_backpressure_detected: time::Instant, + + #[cfg(target_arch = "wasm32")] + time_when_backpressure_detected: wasm_timer::Instant, +} + +#[cfg(not(target_arch = "wasm32"))] +fn get_time_now() -> time::Instant { + time::Instant::now() +} + +#[cfg(target_arch = "wasm32")] +fn get_time_now() -> wasm_timer::Instant { + wasm_timer::Instant::now() +} + +impl SendingDelayController { + fn new(lower_bound: u32, upper_bound: u32) -> Self { + assert!(lower_bound <= upper_bound); + let now = get_time_now(); + SendingDelayController { + current_multiplier: MIN_DELAY_MULTIPLIER, + upper_bound, + lower_bound, + time_when_changed: now, + time_when_backpressure_detected: now, + } + } + + fn current_multiplier(&self) -> u32 { + self.current_multiplier + } + + fn increase_delay_multiplier(&mut self) { + self.current_multiplier = + (self.current_multiplier + 1).clamp(self.lower_bound, self.upper_bound); + self.time_when_changed = get_time_now(); + log::debug!( + "Increasing sending delay multiplier to: {}", + self.current_multiplier + ); + } + + fn decrease_delay_multiplier(&mut self) { + self.current_multiplier = + (self.current_multiplier - 1).clamp(self.lower_bound, self.upper_bound); + self.time_when_changed = get_time_now(); + log::debug!( + "Decreasing sending delay multiplier to: {}", + self.current_multiplier + ); + } + + fn record_backpressure_detected(&mut self) { + self.time_when_backpressure_detected = get_time_now(); + } + + fn not_increased_delay_recently(&self) -> bool { + get_time_now() + > self.time_when_changed + Duration::from_secs(INCREASE_DELAY_MIN_CHANGE_INTERVAL_SECS) + } + + fn is_sending_reliable(&self) -> bool { + let now = get_time_now(); + let delay_change_interval = Duration::from_secs(DECREASE_DELAY_MIN_CHANGE_INTERVAL_SECS); + let acceptable_time_without_backpressure = + Duration::from_secs(ACCEPTABLE_TIME_WITHOUT_BACKPRESSURE_SECS); + + now > self.time_when_backpressure_detected + acceptable_time_without_backpressure + && now > self.time_when_changed + delay_change_interval + } +} + pub(crate) struct OutQueueControl<R> where R: CryptoRng + Rng, @@ -89,6 +201,10 @@ where #[cfg(target_arch = "wasm32")] next_delay: Option<Pin<Box<wasm_timer::Delay>>>, + // To make sure we don't overload the mix_tx channel, we limit the rate we are pushing + // messages. + sending_rate_controller: SendingDelayController, + /// Channel used for sending prepared sphinx packets to `MixTrafficController` that sends them /// out to the network without any further delays. mix_tx: BatchMixMessageSender, @@ -156,6 +272,10 @@ where ack_key, sent_notifier, next_delay: None, + sending_rate_controller: SendingDelayController::new( + MIN_DELAY_MULTIPLIER, + MAX_DELAY_MULTIPLIER, + ), mix_tx, real_receiver, our_full_destination, @@ -212,15 +332,8 @@ where } }; - // if this one fails, there's no retrying because it means that either: - // - we run out of memory - // - the receiver channel is closed - // in either case there's no recovery and we can only panic - if let Err(err) = self.mix_tx.unbounded_send(vec![next_message]) { - log::warn!( - "Failed to send {} packets (possible process shutdown?)", - err.into_inner().len() - ); + if let Err(err) = self.mix_tx.send(vec![next_message]).await { + log::error!("Failed to send - channel closed: {}", err); } // JS: Not entirely sure why or how it fixes stuff, but without the yield call, @@ -234,7 +347,44 @@ where tokio::task::yield_now().await; } + fn current_average_message_sending_delay(&self) -> Duration { + self.config.average_message_sending_delay + * self.sending_rate_controller.current_multiplier() + } + + fn adjust_current_average_message_sending_delay(&mut self) { + let used_slots = self.mix_tx.max_capacity() - self.mix_tx.capacity(); + log::trace!( + "used_slots: {used_slots}, current_multiplier: {}", + self.sending_rate_controller.current_multiplier() + ); + + // Even just a single used slot is enough to signal backpressure + if used_slots > 0 { + log::trace!("Backpressure detected"); + self.sending_rate_controller.record_backpressure_detected(); + } + + // If the buffer is running out, slow down the sending rate + if self.mix_tx.capacity() == 0 + && self.sending_rate_controller.not_increased_delay_recently() + { + self.sending_rate_controller.increase_delay_multiplier(); + } + + // Very carefully step up the sending rate in case it seems like we can solidly handle the + // current rate. + if self.sending_rate_controller.is_sending_reliable() { + self.sending_rate_controller.decrease_delay_multiplier(); + } + } + fn poll_poisson(&mut self, cx: &mut Context<'_>) -> Poll<Option<StreamMessage>> { + // The average delay could change depending on if backpressure in the downstream channel + // (mix_tx) was detected. + self.adjust_current_average_message_sending_delay(); + let avg_delay = self.current_average_message_sending_delay(); + if let Some(ref mut next_delay) = &mut self.next_delay { // it is not yet time to return a message if next_delay.as_mut().poll(cx).is_pending() { @@ -243,7 +393,6 @@ where // we know it's time to send a message, so let's prepare delay for the next one // Get the `now` by looking at the current `delay` deadline - let avg_delay = self.config.average_message_sending_delay; let next_poisson_delay = sample_poisson_duration(&mut self.rng, avg_delay); // The next interval value is `next_poisson_delay` after the one that just diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index 2b9e843695..9e61d5f0e0 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -6,9 +6,7 @@ use client_core::client::inbound_messages::{ InputMessage, InputMessageReceiver, InputMessageSender, }; use client_core::client::key_manager::KeyManager; -use client_core::client::mix_traffic::{ - BatchMixMessageReceiver, BatchMixMessageSender, MixTrafficController, -}; +use client_core::client::mix_traffic::{BatchMixMessageSender, MixTrafficController}; use client_core::client::real_messages_control; use client_core::client::real_messages_control::RealMessagesController; use client_core::client::received_buffer::{ @@ -264,13 +262,13 @@ impl NymClient { // over it. Perhaps GatewayClient needs to be thread-shareable or have some channel for // requests? fn start_mix_traffic_controller( - &mut self, - mix_rx: BatchMixMessageReceiver, gateway_client: GatewayClient, shutdown: ShutdownListener, - ) { + ) -> BatchMixMessageSender { info!("Starting mix traffic controller..."); - MixTrafficController::new(mix_rx, gateway_client).start_with_shutdown(shutdown); + let (mix_traffic_controller, mix_tx) = MixTrafficController::new(gateway_client); + mix_traffic_controller.start_with_shutdown(shutdown); + mix_tx } fn start_websocket_listener( @@ -357,11 +355,6 @@ impl NymClient { // rather than creating them here, so say for example the buffer controller would create the request channels // and would allow anyone to clone the sender channel - // sphinx_message_sender is the transmitter for any component generating sphinx packets that are to be sent to the mixnet - // they are used by cover traffic stream and real traffic stream - // sphinx_message_receiver is the receiver used by MixTrafficController that sends the actual traffic - let (sphinx_message_sender, sphinx_message_receiver) = mpsc::unbounded(); - // unwrapped_sphinx_sender is the transmitter of mixnet messages received from the gateway // unwrapped_sphinx_receiver is the receiver for said messages - used by ReceivedMessagesBuffer let (mixnet_messages_sender, mixnet_messages_receiver) = mpsc::unbounded(); @@ -398,11 +391,13 @@ impl NymClient { .start_gateway_client(mixnet_messages_sender, ack_sender, shutdown.subscribe()) .await; - self.start_mix_traffic_controller( - sphinx_message_receiver, - gateway_client, - shutdown.subscribe(), - ); + // The sphinx_message_sender is the transmitter for any component generating sphinx packets + // that are to be sent to the mixnet. They are used by cover traffic stream and real + // traffic stream. + // The MixTrafficController then sends the actual traffic + let sphinx_message_sender = + Self::start_mix_traffic_controller(gateway_client, shutdown.subscribe()); + self.start_real_traffic_controller( shared_topology_accessor.clone(), reply_key_storage, diff --git a/clients/socks5/src/client/mod.rs b/clients/socks5/src/client/mod.rs index 7892382246..59762ab749 100644 --- a/clients/socks5/src/client/mod.rs +++ b/clients/socks5/src/client/mod.rs @@ -13,9 +13,7 @@ use client_core::client::inbound_messages::{ InputMessage, InputMessageReceiver, InputMessageSender, }; use client_core::client::key_manager::KeyManager; -use client_core::client::mix_traffic::{ - BatchMixMessageReceiver, BatchMixMessageSender, MixTrafficController, -}; +use client_core::client::mix_traffic::{BatchMixMessageSender, MixTrafficController}; use client_core::client::real_messages_control::RealMessagesController; use client_core::client::received_buffer::{ ReceivedBufferRequestReceiver, ReceivedBufferRequestSender, ReceivedMessagesBufferController, @@ -264,13 +262,13 @@ impl NymClient { // over it. Perhaps GatewayClient needs to be thread-shareable or have some channel for // requests? fn start_mix_traffic_controller( - &mut self, - mix_rx: BatchMixMessageReceiver, gateway_client: GatewayClient, shutdown: ShutdownListener, - ) { + ) -> BatchMixMessageSender { info!("Starting mix traffic controller..."); - MixTrafficController::new(mix_rx, gateway_client).start_with_shutdown(shutdown); + let (mix_traffic_controller, mix_tx) = MixTrafficController::new(gateway_client); + mix_traffic_controller.start_with_shutdown(shutdown); + mix_tx } fn start_socks5_listener( @@ -346,11 +344,6 @@ impl NymClient { // rather than creating them here, so say for example the buffer controller would create the request channels // and would allow anyone to clone the sender channel - // sphinx_message_sender is the transmitter for any component generating sphinx packets that are to be sent to the mixnet - // they are used by cover traffic stream and real traffic stream - // sphinx_message_receiver is the receiver used by MixTrafficController that sends the actual traffic - let (sphinx_message_sender, sphinx_message_receiver) = mpsc::unbounded(); - // unwrapped_sphinx_sender is the transmitter of mixnet messages received from the gateway // unwrapped_sphinx_receiver is the receiver for said messages - used by ReceivedMessagesBuffer let (mixnet_messages_sender, mixnet_messages_receiver) = mpsc::unbounded(); @@ -387,11 +380,13 @@ impl NymClient { .start_gateway_client(mixnet_messages_sender, ack_sender, shutdown.subscribe()) .await; - self.start_mix_traffic_controller( - sphinx_message_receiver, - gateway_client, - shutdown.subscribe(), - ); + // The sphinx_message_sender is the transmitter for any component generating sphinx packets + // that are to be sent to the mixnet. They are used by cover traffic stream and real + // traffic stream. + // The MixTrafficController then sends the actual traffic + let sphinx_message_sender = + Self::start_mix_traffic_controller(gateway_client, shutdown.subscribe()); + self.start_real_traffic_controller( shared_topology_accessor.clone(), reply_key_storage, diff --git a/clients/webassembly/src/client/mod.rs b/clients/webassembly/src/client/mod.rs index 8f71155563..1fb324dc39 100644 --- a/clients/webassembly/src/client/mod.rs +++ b/clients/webassembly/src/client/mod.rs @@ -6,7 +6,7 @@ use client_core::client::{ cover_traffic_stream::LoopCoverTrafficStream, inbound_messages::{InputMessage, InputMessageReceiver, InputMessageSender}, key_manager::KeyManager, - mix_traffic::{BatchMixMessageReceiver, BatchMixMessageSender, MixTrafficController}, + mix_traffic::{BatchMixMessageSender, MixTrafficController}, real_messages_control::{self, RealMessagesController}, received_buffer::{ ReceivedBufferMessage, ReceivedBufferRequestReceiver, ReceivedBufferRequestSender, @@ -252,13 +252,11 @@ impl NymClient { // TODO: if we want to send control messages to gateway_client, this CAN'T take the ownership // over it. Perhaps GatewayClient needs to be thread-shareable or have some channel for // requests? - fn start_mix_traffic_controller( - &mut self, - mix_rx: BatchMixMessageReceiver, - gateway_client: GatewayClient, - ) { + fn start_mix_traffic_controller(gateway_client: GatewayClient) -> BatchMixMessageSender { console_log!("Starting mix traffic controller..."); - MixTrafficController::new(mix_rx, gateway_client).start(); + let (mix_traffic_controller, mix_tx) = MixTrafficController::new(gateway_client); + mix_traffic_controller.start(); + mix_tx } // TODO: this procedure is extremely overcomplicated, because it's based off native client's behaviour @@ -306,11 +304,6 @@ impl NymClient { // rather than creating them here, so say for example the buffer controller would create the request channels // and would allow anyone to clone the sender channel - // sphinx_message_sender is the transmitter for any component generating sphinx packets that are to be sent to the mixnet - // they are used by cover traffic stream and real traffic stream - // sphinx_message_receiver is the receiver used by MixTrafficController that sends the actual traffic - let (sphinx_message_sender, sphinx_message_receiver) = mpsc::unbounded(); - // unwrapped_sphinx_sender is the transmitter of mixnet messages received from the gateway // unwrapped_sphinx_receiver is the receiver for said messages - used by ReceivedMessagesBuffer let (mixnet_messages_sender, mixnet_messages_receiver) = mpsc::unbounded(); @@ -338,7 +331,12 @@ impl NymClient { .start_gateway_client(mixnet_messages_sender, ack_sender) .await; - self.start_mix_traffic_controller(sphinx_message_receiver, gateway_client); + // The sphinx_message_sender is the transmitter for any component generating sphinx packets + // that are to be sent to the mixnet. They are used by cover traffic stream and real + // traffic stream. + // The MixTrafficController then sends the actual traffic + let sphinx_message_sender = Self::start_mix_traffic_controller(gateway_client); + self.start_real_traffic_controller( shared_topology_accessor.clone(), ack_receiver, From b3272097f9dd2cd5d1038997a8f47d57b8562bc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= <jedrzej.stuczynski@gmail.com> Date: Mon, 31 Oct 2022 12:19:14 +0000 Subject: [PATCH 20/31] Jedrzej/bugfix/historical uptimes recording (#1721) * Removed commented out type alias * typos * Using the same underlying timer for uptime updater * Updating uptimes at 23:00 UTC each day * Changelog --- CHANGELOG.md | 2 + validator-api/src/epoch_operations/mod.rs | 8 +-- .../src/node_status_api/uptime_updater.rs | 62 +++++++++++++------ 3 files changed, 48 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d867b8bb6e..4e843a33f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// ### Fixed - validator-api, mixnode, gateway should now prefer values in config.toml over mainnet defaults ([#1645]) +- 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 @@ -47,6 +48,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// [#1687]: https://github.com/nymtech/nym/pull/1687 [#1702]: https://github.com/nymtech/nym/pull/1702 [#1703]: https://github.com/nymtech/nym/pull/1703 +[#1721]: https://github.com/nymtech/nym/pull/1721 [#1724]: https://github.com/nymtech/nym/pull/1724 [#1725]: https://github.com/nymtech/nym/pull/1725 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/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}"); + } + } } } } From 3727370b9ee224a78944ab31a68727310a50bc3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= <jedrzej.stuczynski@gmail.com> Date: Mon, 31 Oct 2022 15:28:54 +0000 Subject: [PATCH 21/31] Improved error propagation for fallible validator api queries (#1681) * Improved error propagation for fallible validator api queries * Updated changelog --- CHANGELOG.md | 2 + .../src/validator_api/error.rs | 4 ++ .../validator-client/src/validator_api/mod.rs | 56 ++++++++++++++++--- validator-api/src/node_status_api/models.rs | 17 +++--- .../validator-api-requests/src/models.rs | 17 ++++++ 5 files changed, 81 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e843a33f0..8ed0ebe379 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ 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]) +- 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]) @@ -45,6 +46,7 @@ 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 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 faed34b4dc..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::{ @@ -14,7 +15,7 @@ use validator_api_requests::coconut::{ use validator_api_requests::models::{ GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse, InclusionProbabilityResponse, MixNodeBondAnnotated, MixnodeCoreStatusResponse, - MixnodeStatusReportResponse, MixnodeStatusResponse, MixnodeUptimeHistoryResponse, + MixnodeStatusReportResponse, MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RequestError, RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, }; @@ -48,6 +49,19 @@ impl Client { &self.url } + async fn send_get_request<K, V>( + &self, + path: PathSegments<'_>, + params: Params<'_, K, V>, + ) -> Result<Response, ValidatorAPIError> + where + K: AsRef<str>, + V: AsRef<str>, + { + let url = create_api_url(&self.url, path, params); + Ok(self.reqwest_client.get(url).send().await?) + } + async fn query_validator_api<T, K, V>( &self, path: PathSegments<'_>, @@ -58,8 +72,36 @@ impl Client { K: AsRef<str>, V: AsRef<str>, { - 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<Json<T>, ErrorResponse> + async fn query_validator_api_fallible<T, K, V>( + &self, + path: PathSegments<'_>, + params: Params<'_, K, V>, + ) -> Result<T, ValidatorAPIError> + where + for<'a> T: Deserialize<'a>, + K: AsRef<str>, + V: AsRef<str>, + { + 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<B, T, K, V>( @@ -303,7 +345,7 @@ impl Client { &self, mix_id: MixId, ) -> Result<RewardEstimationResponse, ValidatorAPIError> { - self.query_validator_api( + self.query_validator_api_fallible( &[ routes::API_VERSION, routes::STATUS_ROUTES, @@ -320,7 +362,7 @@ impl Client { &self, mix_id: MixId, ) -> Result<StakeSaturationResponse, ValidatorAPIError> { - self.query_validator_api( + self.query_validator_api_fallible( &[ routes::API_VERSION, routes::STATUS_ROUTES, @@ -337,7 +379,7 @@ impl Client { &self, mix_id: MixId, ) -> Result<InclusionProbabilityResponse, ValidatorAPIError> { - self.query_validator_api( + self.query_validator_api_fallible( &[ routes::API_VERSION, routes::STATUS_ROUTES, @@ -354,7 +396,7 @@ impl Client { &self, mix_id: MixId, ) -> Result<UptimeResponse, ValidatorAPIError> { - self.query_validator_api( + self.query_validator_api_fallible( &[ routes::API_VERSION, routes::STATUS_ROUTES, diff --git a/validator-api/src/node_status_api/models.rs b/validator-api/src/node_status_api/models.rs index b793db5901..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,11 +20,10 @@ 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, + MixnodeStatusReportResponse, MixnodeUptimeHistoryResponse, RequestError, }; // todo: put into some error enum @@ -302,24 +302,25 @@ impl From<HistoricalUptime> for HistoricalUptimeResponse { } 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/validator-api-requests/src/models.rs b/validator-api/validator-api-requests/src/models.rs index 9435749fb0..046e4152b3 100644 --- a/validator-api/validator-api-requests/src/models.rs +++ b/validator-api/validator-api-requests/src/models.rs @@ -12,6 +12,23 @@ 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( From 4ab6f4c3a99152544ec80e96b5457b5911a61b68 Mon Sep 17 00:00:00 2001 From: Pierre Dommerc <dommerc.pierre@gmail.com> Date: Mon, 31 Oct 2022 16:58:30 +0100 Subject: [PATCH 22/31] refactor(explorer-api): route ping use mix_id as param (#1728) --- explorer-api/src/mix_nodes/models.rs | 13 ------------- explorer-api/src/ping/http.rs | 24 +++++++++++------------- explorer-api/src/ping/models.rs | 15 ++++++++------- explorer-api/src/state.rs | 9 +-------- 4 files changed, 20 insertions(+), 41 deletions(-) diff --git a/explorer-api/src/mix_nodes/models.rs b/explorer-api/src/mix_nodes/models.rs index d117bec721..69f05b84a0 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<MixNodeBondAnnotated> { - 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<HashMap<MixId, MixNodeBondAnnotated>> { self.mixnodes.read().await.get_mixnodes() } 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 <contact@nymtech.net> // 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<Route>, 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("/<pubkey>")] +#[get("/<mix_id>")] pub(crate) async fn index( - pubkey: &str, + mix_id: MixId, state: &State<ExplorerApiStateContext>, ) -> Option<Json<PingResponse>> { - 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<String, PingCacheItem>; +pub(crate) type PingCache = HashMap<MixId, PingCacheItem>; 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<PingResponse> { + pub(crate) async fn get(&self, mix_id: MixId) -> Option<PingResponse> { 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<MixNodeBondAnnotated> { self.mixnodes.get_mixnode(mix_id).await } - - pub(crate) async fn get_mix_node_by_pubkey( - &self, - pubkey: IdentityKeyRef<'_>, - ) -> Option<MixNodeBondAnnotated> { - self.mixnodes.get_mixnode_by_identity(pubkey).await - } } #[derive(Debug, Serialize, Deserialize)] From 49ce56c367b03d874843c2850044755f33105197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= <jedrzej.stuczynski@gmail.com> Date: Mon, 31 Oct 2022 16:56:37 +0000 Subject: [PATCH 23/31] Jedrzej/feature/version field in framed sphinx packets (#1723) * introduced PacketVersion into FramedSphinxPacket * Using legacy mode by default in mixnodes and gateways * fixed unit tests --- .../client-libs/mixnet-client/src/client.rs | 7 +- .../mixnet-client/src/forwarder.rs | 2 + common/nymsphinx/framing/src/codec.rs | 127 +++++++++++++++--- common/nymsphinx/framing/src/packet.rs | 91 +++++++++++-- common/nymsphinx/params/src/lib.rs | 12 ++ common/nymsphinx/params/src/packet_version.rs | 59 ++++++++ gateway/src/config/mod.rs | 12 ++ gateway/src/node/mod.rs | 1 + mixnode/src/config/mod.rs | 12 ++ mixnode/src/node/mod.rs | 1 + 10 files changed, 291 insertions(+), 33 deletions(-) create mode 100644 common/nymsphinx/params/src/packet_version.rs 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/nymsphinx/framing/src/codec.rs b/common/nymsphinx/framing/src/codec.rs index 7998c11d0c..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,12 +214,12 @@ 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, @@ -223,6 +230,7 @@ mod packet_encoding { ]; for packet_size in packet_sizes { let header = Header { + packet_version: PacketVersion::Legacy, packet_size, packet_mode: Default::default(), }; @@ -230,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(), @@ -247,13 +303,44 @@ 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::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, @@ -270,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 860ddf7ec2..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<Option<Self>, 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,16 +170,16 @@ 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, @@ -146,6 +189,28 @@ mod header_encoding { ]; 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_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 <contact@nymtech.net> +// 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<u8> { + match self { + PacketVersion::Legacy => None, + PacketVersion::Versioned(version) => Some(*version), + } + } +} + +impl From<u8> 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/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/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/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/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( From 1f0d5f8ad04a0425202c688b9c5909130a52c506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= <jedrzej.stuczynski@gmail.com> Date: Mon, 31 Oct 2022 17:37:16 +0000 Subject: [PATCH 24/31] Feature/vesting contract version query (#1726) * Introduced vesting contract query for build information * Fixed import paths * Changelog --- Cargo.lock | 2 + .../client-libs/validator-client/Cargo.toml | 1 + .../src/nymd/traits/vesting_query_client.rs | 20 +++++ .../vesting-contract/src/messages.rs | 1 + contracts/CHANGELOG.md | 8 ++ contracts/Cargo.lock | 1 + contracts/vesting/Cargo.toml | 5 +- contracts/vesting/build.rs | 8 ++ contracts/vesting/src/contract.rs | 17 ++++ nym-wallet/Cargo.lock | 88 +++++++++++++++++++ 10 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 contracts/vesting/build.rs diff --git a/Cargo.lock b/Cargo.lock index d88659c295..e304efc1c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6391,6 +6391,7 @@ dependencies = [ "coconut-interface", "colored", "config", + "contracts-common", "cosmrs", "cosmwasm-std", "cw3", @@ -6486,6 +6487,7 @@ dependencies = [ "schemars", "serde", "thiserror", + "vergen 5.1.17", "vesting-contract-common", ] 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/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<T>(&self, query: VestingQueryMsg) -> Result<T, NymdError> + where + for<'a> T: Deserialize<'a>; + + async fn get_vesting_contract_version(&self) -> Result<ContractBuildInformation, NymdError> { + self.query_vesting_contract(VestingQueryMsg::GetContractVersion {}) + .await + } + async fn locked_coins( &self, address: &str, @@ -107,6 +118,15 @@ pub trait VestingQueryClient { #[async_trait] impl<C: CosmWasmClient + Sync + Send> VestingQueryClient for NymdClient<C> { + async fn query_vesting_contract<T>(&self, query: VestingQueryMsg) -> Result<T, NymdError> + 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/cosmwasm-smart-contracts/vesting-contract/src/messages.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs index 98f8024dbc..696df628aa 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs @@ -160,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<Timestamp>, 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 a62c529faf..706a7c5e36 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -1615,6 +1615,7 @@ dependencies = [ "schemars", "serde", "thiserror", + "vergen", "vesting-contract-common", ] diff --git a/contracts/vesting/Cargo.toml b/contracts/vesting/Cargo.toml index f55bd649f1..e13491a4f6 100644 --- a/contracts/vesting/Cargo.toml +++ b/contracts/vesting/Cargo.toml @@ -23,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 <contact@nymtech.net> +// 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 9d1cee6711..da293349bf 100644 --- a/contracts/vesting/src/contract.rs +++ b/contracts/vesting/src/contract.rs @@ -8,6 +8,7 @@ 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, @@ -500,6 +501,7 @@ fn try_create_periodic_vesting_account( #[entry_point] pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result<QueryResponse, ContractError> { let query_res = match msg { + QueryMsg::GetContractVersion {} => to_binary(&get_contract_version()), QueryMsg::LockedCoins { vesting_account_address, block_time, @@ -606,6 +608,21 @@ pub fn try_get_account(address: &str, deps: Deps<'_>) -> Result<Account, Contrac account_from_address(address, deps.storage, deps.api) } +/// Gets build information of this contract. +pub fn get_contract_version() -> 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/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 1c44d275a5..06183c3c01 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" @@ -1255,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" @@ -1721,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" @@ -2323,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" @@ -2387,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" @@ -5252,6 +5321,7 @@ dependencies = [ "coconut-interface", "colored 2.0.0", "config", + "contracts-common", "cosmrs", "cosmwasm-std", "cw3", @@ -5288,6 +5358,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" @@ -5317,6 +5404,7 @@ dependencies = [ "schemars", "serde", "thiserror", + "vergen", "vesting-contract-common", ] From 80c21b3ed9ca8313862591535f2e528150a365af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= <jedrzej.stuczynski@gmail.com> Date: Tue, 1 Nov 2022 11:46:47 +0000 Subject: [PATCH 25/31] (chore) setting up a common/logging crate (#1730) --- Cargo.lock | 17 +++++++++++++ Cargo.toml | 1 + clients/native/Cargo.toml | 1 + clients/native/src/main.rs | 23 +---------------- clients/socks5/Cargo.toml | 1 + clients/socks5/src/main.rs | 21 +--------------- common/logging/Cargo.toml | 10 ++++++++ common/logging/src/lib.rs | 25 +++++++++++++++++++ explorer-api/Cargo.toml | 1 + explorer-api/src/main.rs | 16 +----------- gateway/Cargo.toml | 1 + gateway/src/main.rs | 22 +--------------- mixnode/Cargo.toml | 1 + mixnode/src/main.rs | 19 +------------- nym-connect/Cargo.lock | 12 +++++++++ nym-connect/src-tauri/Cargo.toml | 1 + nym-connect/src-tauri/src/main.rs | 20 +-------------- nym-wallet/Cargo.lock | 9 +++++++ nym-wallet/nym-wallet-recovery-cli/Cargo.toml | 2 ++ .../nym-wallet-recovery-cli/src/main.rs | 13 +--------- .../network-requester/Cargo.toml | 1 + .../network-requester/src/main.rs | 19 +------------- .../network-statistics/Cargo.toml | 2 +- .../network-statistics/src/main.rs | 22 ++-------------- tools/nym-cli/Cargo.toml | 1 + tools/nym-cli/src/main.rs | 23 +---------------- validator-api/Cargo.toml | 1 + validator-api/src/main.rs | 22 +--------------- 28 files changed, 98 insertions(+), 209 deletions(-) create mode 100644 common/logging/Cargo.toml create mode 100644 common/logging/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index e304efc1c7..207872c863 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1592,6 +1592,7 @@ dependencies = [ "isocountry", "itertools", "log", + "logging", "maxminddb", "mixnet-contract-common", "network-defaults", @@ -2730,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" @@ -3072,6 +3081,7 @@ dependencies = [ "clap_complete_fig", "dotenv", "log", + "logging", "network-defaults", "nym-cli-commands", "pretty_env_logger", @@ -3127,6 +3137,7 @@ dependencies = [ "gateway-client", "gateway-requests", "log", + "logging", "network-defaults", "nymsphinx", "pemstore", @@ -3168,6 +3179,7 @@ dependencies = [ "gateway-requests", "humantime-serde", "log", + "logging", "mixnet-client", "mixnode-common", "network-defaults", @@ -3210,6 +3222,7 @@ dependencies = [ "humantime-serde", "lazy_static", "log", + "logging", "mixnet-client", "mixnode-common", "nonexhaustive-delayqueue", @@ -3244,6 +3257,7 @@ dependencies = [ "futures", "ipnetwork 0.20.0", "log", + "logging", "network-defaults", "nymsphinx", "ordered-buffer", @@ -3269,6 +3283,7 @@ version = "1.0.2" dependencies = [ "dirs", "log", + "logging", "pretty_env_logger", "rocket", "serde", @@ -3295,6 +3310,7 @@ dependencies = [ "gateway-client", "gateway-requests", "log", + "logging", "network-defaults", "nymsphinx", "ordered-buffer", @@ -3368,6 +3384,7 @@ dependencies = [ "humantime-serde", "inclusion-probability", "log", + "logging", "mixnet-contract-common", "multisig-contract-common", "nymcoconut", 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/native/Cargo.toml b/clients/native/Cargo.toml index f585de4e24..476ffec29a 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -38,6 +38,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/main.rs b/clients/native/src/main.rs index 13e82ec660..cc78423353 100644 --- a/clients/native/src/main.rs +++ b/clients/native/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; pub mod client; @@ -34,25 +35,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("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/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index c5729951c4..6aa35d14eb 100644 --- a/clients/socks5/Cargo.toml +++ b/clients/socks5/Cargo.toml @@ -31,6 +31,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/main.rs b/clients/socks5/src/main.rs index fb7a8bb607..21ab3f33c5 100644 --- a/clients/socks5/src/main.rs +++ b/clients/socks5/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; pub mod client; @@ -34,23 +35,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("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) - .init(); -} 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 <contact@nymtech.net> +// 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/explorer-api/Cargo.toml b/explorer-api/Cargo.toml index 8aae21b2ef..217433d4e5 100644 --- a/explorer-api/Cargo.toml +++ b/explorer-api/Cargo.toml @@ -29,5 +29,6 @@ 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/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/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/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/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/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/nym-connect/Cargo.lock b/nym-connect/Cargo.lock index 5a102a8d6e..2674eae1fa 100644 --- a/nym-connect/Cargo.lock +++ b/nym-connect/Cargo.lock @@ -2870,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" @@ -3216,6 +3224,7 @@ dependencies = [ "fix-path-env", "futures", "log", + "logging", "nym-socks5-client", "pretty_env_logger", "rand 0.8.5", @@ -3251,6 +3260,7 @@ dependencies = [ "gateway-client", "gateway-requests", "log", + "logging", "network-defaults", "nymsphinx", "ordered-buffer", @@ -6078,6 +6088,7 @@ dependencies = [ "coconut-interface", "colored", "config", + "contracts-common", "cosmrs", "cosmwasm-std", "cw3", @@ -6167,6 +6178,7 @@ dependencies = [ "schemars", "serde", "thiserror", + "vergen", "vesting-contract-common", ] 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/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 06183c3c01..dfbfe63531 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -2484,6 +2484,14 @@ dependencies = [ "serde", ] +[[package]] +name = "logging" +version = "0.1.0" +dependencies = [ + "log", + "pretty_env_logger", +] + [[package]] name = "loom" version = "0.5.4" @@ -2842,6 +2850,7 @@ dependencies = [ "bip39", "clap", "log", + "logging", "pretty_env_logger", "serde_json", ] 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/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 ce1762f95b..07b7eb0f1b 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 network_defaults::DEFAULT_WEBSOCKET_LISTENING_PORT; use nymsphinx::addressing::clients::Recipient; @@ -103,21 +104,3 @@ async fn main() { 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/validator-api/Cargo.toml b/validator-api/Cargo.toml index b4c82a5748..90be5aa060 100644 --- a/validator-api/Cargo.toml +++ b/validator-api/Cargo.toml @@ -69,6 +69,7 @@ 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" } 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))) From 2952144d32399bf375d3a56c2f530183b3e6abdf Mon Sep 17 00:00:00 2001 From: Fouad <fmtabbara@hotmail.co.uk> Date: Tue, 1 Nov 2022 13:02:34 +0000 Subject: [PATCH 26/31] add profit margin percent to response (#1729) * add profit margin percent to response * use display percentage function * fix profit margin display * fix up filters --- explorer-api/src/mix_node/models.rs | 2 ++ explorer-api/src/mix_nodes/models.rs | 1 + explorer/src/components/MixNodes/Economics/Rows.ts | 5 ++++- explorer/src/components/MixNodes/index.ts | 3 ++- explorer/src/context/main.tsx | 6 ++++-- explorer/src/typeDefs/explorer-api.ts | 2 +- explorer/src/utils/index.ts | 8 ++++++++ 7 files changed, 22 insertions(+), 5 deletions(-) diff --git a/explorer-api/src/mix_node/models.rs b/explorer-api/src/mix_node/models.rs index 2ecb907f6a..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; @@ -37,6 +38,7 @@ pub(crate) struct PrettyDetailedMixNodeBond { 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 69f05b84a0..07899742c9 100644 --- a/explorer-api/src/mix_nodes/models.rs +++ b/explorer-api/src/mix_nodes/models.rs @@ -154,6 +154,7 @@ impl ThreadsafeMixNodesCache { 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/src/components/MixNodes/Economics/Rows.ts b/explorer/src/components/MixNodes/Economics/Rows.ts index bcc43914a5..a4f998a1f0 100644 --- a/explorer/src/components/MixNodes/Economics/Rows.ts +++ b/explorer/src/components/MixNodes/Economics/Rows.ts @@ -2,6 +2,7 @@ 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<MixNodeEconomicDynamicsStatsResponse> | undefined) => { const inclusionProbability = economicDynamicsStats?.data?.active_set_inclusion_probability; @@ -29,7 +30,9 @@ 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; diff --git a/explorer/src/components/MixNodes/index.ts b/explorer/src/components/MixNodes/index.ts index bcf5b09efd..3435c3bf3f 100644 --- a/explorer/src/components/MixNodes/index.ts +++ b/explorer/src/components/MixNodes/index.ts @@ -1,5 +1,6 @@ /* eslint-disable camelcase */ import { MixNodeResponse, MixNodeResponseItem, MixnodeStatus } from '../../typeDefs/explorer-api'; +import { toPercentIntegerString } from '../../utils'; import { unymToNym } from '../../utils/currency'; export type MixnodeRowType = { @@ -29,7 +30,7 @@ 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 { diff --git a/explorer/src/context/main.tsx b/explorer/src/context/main.tsx index a8db9e270e..42e89b32ee 100644 --- a/explorer/src/context/main.tsx +++ b/explorer/src/context/main.tsx @@ -96,15 +96,17 @@ 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 }); }; diff --git a/explorer/src/typeDefs/explorer-api.ts b/explorer/src/typeDefs/explorer-api.ts index 68563ee82a..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; } @@ -87,6 +86,7 @@ export interface MixNodeResponseItem { avg_uptime: number; stake_saturation: number; operating_cost: Amount; + profit_margin_percent: string; } export type MixNodeResponse = MixNodeResponseItem[]; 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(); From 4967bbb5bd8eb2d225fc1a9bc9391b9df1e3c78f Mon Sep 17 00:00:00 2001 From: Fouad <fmtabbara@hotmail.co.uk> Date: Wed, 2 Nov 2022 10:46:45 +0000 Subject: [PATCH 27/31] Feature/delegations without bonded node (#1727) * refactor delegations list to include separate delegation and pending delegation item * show tooltip on delegation with unbonded node * feat(wallet): add operating cost in delegations list * add additional state to check for unbonding event * disable actions when pending unbond event * add request and type guard for pending unbond event * add mixnode_is_unbonding to delegation item type Co-authored-by: pierre <dommerc.pierre@gmail.com> --- common/types/src/delegation.rs | 1 + .../src/operations/mixnet/delegate.rs | 8 + .../src/components/Bonding/BondedGateway.tsx | 4 +- .../src/components/Bonding/BondedMixnode.tsx | 30 ++-- .../Delegation/DelegationActions.tsx | 24 ++- .../components/Delegation/DelegationItem.tsx | 104 +++++++++++++ .../Delegation/DelegationList.stories.tsx | 4 +- .../components/Delegation/DelegationList.tsx | 142 +++--------------- .../src/components/Delegation/Delegations.tsx | 2 +- .../Delegation/PendingDelegationItem.tsx | 37 +++++ .../components/Delegation/UndelegateModal.tsx | 9 +- nym-wallet/src/context/bonding.tsx | 5 + nym-wallet/src/context/delegations.tsx | 3 +- nym-wallet/src/context/mocks/bonding.tsx | 2 + nym-wallet/src/context/mocks/delegations.tsx | 2 + .../bonding/node-settings/NodeSettings.tsx | 3 +- nym-wallet/src/pages/delegation/index.tsx | 5 +- nym-wallet/src/requests/index.ts | 1 + nym-wallet/src/requests/pendingEvents.ts | 4 + nym-wallet/src/types/global.ts | 10 +- ts-packages/types/src/types/global.ts | 2 +- .../types/rust/DelegationWithEverything.ts | 1 + 22 files changed, 240 insertions(+), 163 deletions(-) create mode 100644 nym-wallet/src/components/Delegation/DelegationItem.tsx create mode 100644 nym-wallet/src/components/Delegation/PendingDelegationItem.tsx create mode 100644 nym-wallet/src/requests/pendingEvents.ts 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<DelegationEvent>, + pub mixnode_is_unbonding: Option<bool>, } #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] 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/components/Bonding/BondedGateway.tsx b/nym-wallet/src/components/Bonding/BondedGateway.tsx index 305d261cca..8b2776f8a9 100644 --- a/nym-wallet/src/components/Bonding/BondedGateway.tsx +++ b/nym-wallet/src/components/Bonding/BondedGateway.tsx @@ -1,14 +1,12 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { Stack, Typography } from '@mui/material'; import { Link } from '@nymproject/react/link/Link'; import { TBondedGateway, urls } from 'src/context'; import { NymCard } from 'src/components'; import { Network } from 'src/types'; import { IdentityKey } from 'src/components/IdentityKey'; -import { getGatewayReport } from 'src/requests'; import { Cell, Header, NodeTable } from './NodeTable'; import { BondedGatewayActions, TBondedGatwayActions } from './BondedGatewayAction'; -import { Console } from 'src/utils/console'; const headers: Header[] = [ { 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/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..2297711368 --- /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' }}> + {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' }}> + {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..57c4f3671c 100644 --- a/nym-wallet/src/components/Delegation/DelegationList.stories.tsx +++ b/nym-wallet/src/components/Delegation/DelegationList.stories.tsx @@ -33,6 +33,7 @@ export const items: DelegationWithEverything[] = [ avg_uptime_percent: 0.5, uses_vesting_contract_tokens: false, pending_events: [], + mixnode_is_unbonding: true, }, { mix_id: 5678, @@ -55,6 +56,7 @@ export const items: DelegationWithEverything[] = [ avg_uptime_percent: 0.1, uses_vesting_contract_tokens: true, pending_events: [], + mixnode_is_unbonding: true, }, ]; @@ -64,4 +66,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..3aad9785e0 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.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..c83686f399 --- /dev/null +++ b/nym-wallet/src/components/Delegation/PendingDelegationItem.tsx @@ -0,0 +1,37 @@ +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 }) => { + return ( + <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/context/bonding.tsx b/nym-wallet/src/context/bonding.tsx index dcecd5bac2..94fa983bc1 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, @@ -65,6 +66,7 @@ export type TBondedMixnode = { mixPort: number; verlocPort: number; version: string; + isUnbonding: boolean; }; export interface TBondedGateway { @@ -83,6 +85,7 @@ export interface TBondedGateway { current: number; average: number; }; + isUnbonding: boolean; } export type TokenPool = 'locked' | 'balance'; @@ -284,6 +287,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) { @@ -306,6 +310,7 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod bond: 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..c246928b8c 100644 --- a/nym-wallet/src/context/delegations.tsx +++ b/nym-wallet/src/context/delegations.tsx @@ -36,7 +36,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; diff --git a/nym-wallet/src/context/mocks/bonding.tsx b/nym-wallet/src/context/mocks/bonding.tsx index a70501dbae..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 = { @@ -42,6 +43,7 @@ const bondedGatewayMock: TBondedGateway = { 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/bonding/node-settings/NodeSettings.tsx b/nym-wallet/src/pages/bonding/node-settings/NodeSettings.tsx index 0c6fc495f3..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; } @@ -133,7 +135,6 @@ export const NodeSettings = () => { }} /> )} - {isLoading && <LoadingModal />} </NymCard> </PageLayout> ); diff --git a/nym-wallet/src/pages/delegation/index.tsx b/nym-wallet/src/pages/delegation/index.tsx index 92845056b0..76f36cd0a8 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, @@ -275,7 +275,8 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { <CircularProgress /> </Box> ); - } else if (Boolean(delegations?.length)) { + } + if (delegations && Boolean(delegations?.length)) { return ( <DelegationList explorerUrl={urls(network).networkExplorer} 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/types/global.ts b/nym-wallet/src/types/global.ts index e2dc6a2af8..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'; 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; } From 95f98016ded2453d181fb5237533b5fe40689ece Mon Sep 17 00:00:00 2001 From: Gala <calero.vg@gmail.com> Date: Wed, 2 Nov 2022 13:49:29 +0100 Subject: [PATCH 28/31] make label always shrink --- .../src/components/Accounts/modals/AddAccountModal.tsx | 1 + .../src/components/Accounts/modals/EditAccountModal.tsx | 1 + .../src/components/Bonding/forms/BondGatewayForm.tsx | 7 +++++++ .../src/components/Bonding/forms/BondMixnodeForm.tsx | 7 +++++++ .../src/components/Bonding/modals/BondMoreModal.tsx | 8 +++++++- .../src/components/Bonding/modals/NodeSettingsModal.tsx | 8 +++++++- nym-wallet/src/components/Mnemonic.tsx | 1 + nym-wallet/src/components/Send/SendInputModal.tsx | 1 + nym-wallet/src/components/textfields.tsx | 2 ++ nym-wallet/src/pages/Admin/index.tsx | 2 ++ .../node-settings/settings-pages/NodeUnbondPage.tsx | 7 ++++++- .../settings-pages/general-settings/InfoSettings.tsx | 5 +++++ .../general-settings/ParametersSettings.tsx | 1 + nym-wallet/src/pages/internal-docs/DocEntry.tsx | 1 + nym-wallet/src/pages/settings/profile.tsx | 6 +++--- nym-wallet/src/pages/settings/system-variables.tsx | 1 + .../src/components/currency/CurrencyFormField.tsx | 1 + .../src/components/mixnodes/IdentityKeyFormField.tsx | 1 + 18 files changed, 55 insertions(+), 6 deletions(-) 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/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/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/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/settings-pages/NodeUnbondPage.tsx b/nym-wallet/src/pages/bonding/node-settings/settings-pages/NodeUnbondPage.tsx index 89efefb0a8..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 @@ -49,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/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/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 }} /> ); }; From 0529e84a31d4266881783f4a5f6cf64029a47892 Mon Sep 17 00:00:00 2001 From: Fouad <fmtabbara@hotmail.co.uk> Date: Wed, 2 Nov 2022 13:15:48 +0000 Subject: [PATCH 29/31] add account how to links (#1732) --- nym-wallet/src/components/Accounts/MultiAccountHowTo.tsx | 7 ++++++- .../src/components/Accounts/MultiAccountWithPwdHowTo.tsx | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) 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> ); From e32601ab86c339a0d4c9160365512648fcd51fd0 Mon Sep 17 00:00:00 2001 From: Pierre Dommerc <dommerc.pierre@gmail.com> Date: Wed, 2 Nov 2022 15:48:49 +0100 Subject: [PATCH 30/31] feat(wallet): normalize decimal places in ui (#1731) --- .../components/Delegation/DelegationItem.tsx | 4 +- .../Delegation/PendingDelegationItem.tsx | 58 +++++++++---------- nym-wallet/src/context/bonding.tsx | 7 ++- nym-wallet/src/context/delegations.tsx | 12 +++- nym-wallet/src/pages/delegation/index.tsx | 6 +- nym-wallet/src/utils/index.ts | 20 ++++++- 6 files changed, 67 insertions(+), 40 deletions(-) diff --git a/nym-wallet/src/components/Delegation/DelegationItem.tsx b/nym-wallet/src/components/Delegation/DelegationItem.tsx index 2297711368..2906ed2abd 100644 --- a/nym-wallet/src/components/Delegation/DelegationItem.tsx +++ b/nym-wallet/src/components/Delegation/DelegationItem.tsx @@ -62,7 +62,7 @@ export const DelegationItem = ({ : `${toPercentIntegerString(item.cost_params.profit_margin_percent)}%`)} </TableCell> <TableCell sx={{ color: 'inherit' }}> - <Typography style={{ textTransform: 'uppercase' }}> + <Typography style={{ textTransform: 'uppercase', fontSize: 'inherit' }}> {operatingCost ? `${operatingCost.amount} ${operatingCost.denom}` : '-'} </Typography> </TableCell> @@ -71,7 +71,7 @@ export const DelegationItem = ({ {format(new Date(item.delegated_on_iso_datetime), 'dd/MM/yyyy')} </TableCell> <TableCell sx={{ color: 'inherit' }}> - <Typography style={{ textTransform: 'uppercase' }}> + <Typography style={{ textTransform: 'uppercase', fontSize: 'inherit' }}> {isDelegation(item) ? `${item.amount.amount} ${item.amount.denom}` : '-'} </Typography> </TableCell> diff --git a/nym-wallet/src/components/Delegation/PendingDelegationItem.tsx b/nym-wallet/src/components/Delegation/PendingDelegationItem.tsx index c83686f399..ef4b71390a 100644 --- a/nym-wallet/src/components/Delegation/PendingDelegationItem.tsx +++ b/nym-wallet/src/components/Delegation/PendingDelegationItem.tsx @@ -3,35 +3,33 @@ 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 }) => { - return ( - <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 +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> - ); -}; + arrow + > + <Chip label="Pending Events" /> + </Tooltip> + </TableCell> + </TableRow> +); diff --git a/nym-wallet/src/context/bonding.tsx b/nym-wallet/src/context/bonding.tsx index 94fa983bc1..bf3e558e7e 100644 --- a/nym-wallet/src/context/bonding.tsx +++ b/nym-wallet/src/context/bonding.tsx @@ -39,6 +39,7 @@ import { useCheckOwnership } from '../hooks/useCheckOwnership'; import { AppContext } from './main'; import { attachDefaultOperatingCost, + decCoinToDisplay, toDisplay, toPercentFloatString, toPercentIntegerString, @@ -270,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, @@ -307,7 +308,7 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod 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, diff --git a/nym-wallet/src/context/delegations.tsx b/nym-wallet/src/context/delegations.tsx index c246928b8c..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; @@ -100,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/pages/delegation/index.tsx b/nym-wallet/src/pages/delegation/index.tsx index 76f36cd0a8..62418f0463 100644 --- a/nym-wallet/src/pages/delegation/index.tsx +++ b/nym-wallet/src/pages/delegation/index.tsx @@ -268,7 +268,7 @@ 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' }}> @@ -276,12 +276,12 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { </Box> ); } - if (delegations && 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/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. * From a7cd7a58f2aef47edc5766f97d543d2953201314 Mon Sep 17 00:00:00 2001 From: Fouad <fmtabbara@hotmail.co.uk> Date: Thu, 3 Nov 2022 13:44:09 +0000 Subject: [PATCH 31/31] Bugfix/delegations sort by no bonded node (#1737) * use sorting function * create hardcoded examples in storybook --- .../Delegation/DelegationList.stories.tsx | 234 +++++++++++++++++- .../components/Delegation/DelegationList.tsx | 2 +- 2 files changed, 233 insertions(+), 3 deletions(-) diff --git a/nym-wallet/src/components/Delegation/DelegationList.stories.tsx b/nym-wallet/src/components/Delegation/DelegationList.stories.tsx index 57c4f3671c..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' }, @@ -36,7 +36,7 @@ export const items: DelegationWithEverything[] = [ 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(), @@ -58,6 +58,236 @@ export const items: DelegationWithEverything[] = [ 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, + }, ]; export const WithData = () => <DelegationList items={items} explorerUrl={explorerUrl} />; diff --git a/nym-wallet/src/components/Delegation/DelegationList.tsx b/nym-wallet/src/components/Delegation/DelegationList.tsx index 3aad9785e0..a6c2ac1bcf 100644 --- a/nym-wallet/src/components/Delegation/DelegationList.tsx +++ b/nym-wallet/src/components/Delegation/DelegationList.tsx @@ -109,7 +109,7 @@ export const DelegationList: React.FC<{ <EnhancedTableHead order={order} orderBy={orderBy} onRequestSort={handleRequestSort} /> <TableBody> {items.length ? ( - items.map((item) => { + items.sort(sortByUnbondedMixnodeFirst).map((item) => { if (isPendingDelegation(item)) return <PendingDelegationItem item={item} explorerUrl={explorerUrl} />; if (isDelegation(item)) return (