From e0135173486d0dd8510b11dc2701a4d12854da29 Mon Sep 17 00:00:00 2001 From: Drazen Urch Date: Wed, 5 Jan 2022 15:28:17 +0100 Subject: [PATCH 01/26] Use serial integer instead of random (#1009) * Use serial integer instead of random * [ci skip] Generate TS types * cargo fmt * Return u32 --- Cargo.lock | 2 - contracts/Cargo.lock | 51 +++----------------- contracts/vesting/Cargo.toml | 4 +- contracts/vesting/src/storage.rs | 3 +- contracts/vesting/src/vesting/account/mod.rs | 21 +++----- contracts/vesting/src/vesting/mod.rs | 5 +- nym-wallet/Cargo.lock | 2 - 7 files changed, 18 insertions(+), 70 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 99aa0cac83..6824e1e4e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7275,9 +7275,7 @@ dependencies = [ "config", "cosmwasm-std", "cw-storage-plus", - "getrandom 0.2.3", "mixnet-contract", - "rand 0.8.4", "schemars", "serde", "thiserror", diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 5874784f8e..d1e0a14153 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -326,7 +326,7 @@ dependencies = [ "log", "nymsphinx-types", "pemstore", - "rand 0.7.3", + "rand", "subtle-encoding", "x25519-dalek", ] @@ -458,7 +458,7 @@ checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" dependencies = [ "curve25519-dalek", "ed25519", - "rand 0.7.3", + "rand", "serde", "sha2", "zeroize", @@ -592,10 +592,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi 0.10.2+wasi-snapshot-preview1", - "wasm-bindgen", ] [[package]] @@ -858,8 +856,8 @@ dependencies = [ "cw-storage-plus", "fixed", "mixnet-contract", - "rand 0.7.3", - "rand_chacha 0.2.2", + "rand", + "rand_chacha", "schemars", "serde", "thiserror", @@ -1067,21 +1065,9 @@ checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ "getrandom 0.1.16", "libc", - "rand_chacha 0.2.2", + "rand_chacha", "rand_core 0.5.1", - "rand_hc 0.2.0", -] - -[[package]] -name = "rand" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.3", - "rand_hc 0.3.1", + "rand_hc", ] [[package]] @@ -1094,16 +1080,6 @@ dependencies = [ "rand_core 0.5.1", ] -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.3", -] - [[package]] name = "rand_core" version = "0.5.1" @@ -1129,7 +1105,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9e9532ada3929fb8b2e9dbe28d1e06c9b2cc65813f074fcb6bd5fbefeff9d56" dependencies = [ "num-traits", - "rand 0.7.3", + "rand", ] [[package]] @@ -1141,15 +1117,6 @@ dependencies = [ "rand_core 0.5.1", ] -[[package]] -name = "rand_hc" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" -dependencies = [ - "rand_core 0.6.3", -] - [[package]] name = "regex" version = "1.5.4" @@ -1330,7 +1297,7 @@ dependencies = [ "hmac", "lioness", "log", - "rand 0.7.3", + "rand", "rand_distr", "sha2", "subtle 2.4.1", @@ -1558,9 +1525,7 @@ dependencies = [ "config", "cosmwasm-std", "cw-storage-plus", - "getrandom 0.2.3", "mixnet-contract", - "rand 0.8.4", "schemars", "serde", "thiserror", diff --git a/contracts/vesting/Cargo.toml b/contracts/vesting/Cargo.toml index 931d7c2d75..55e731443c 100644 --- a/contracts/vesting/Cargo.toml +++ b/contracts/vesting/Cargo.toml @@ -22,6 +22,4 @@ cw-storage-plus = { version = "0.10.3", features = ["iterator"] } schemars = "0.8" serde = { version = "1.0.103", default-features = false, features = ["derive"] } -thiserror = { version = "1.0.23" } -rand = {version = "0.8.4", features = ["std_rng"]} -getrandom = { version = "0.2.3", features = ["js"]} \ No newline at end of file +thiserror = { version = "1.0.23" } \ No newline at end of file diff --git a/contracts/vesting/src/storage.rs b/contracts/vesting/src/storage.rs index f8227d8581..eb24ba5409 100644 --- a/contracts/vesting/src/storage.rs +++ b/contracts/vesting/src/storage.rs @@ -1,8 +1,9 @@ use crate::errors::ContractError; use crate::vesting::Account; use cosmwasm_std::{Addr, Api, Storage}; -use cw_storage_plus::Map; +use cw_storage_plus::{Item, Map}; +pub const KEY: Item = Item::new("key"); const ACCOUNTS: Map = Map::new("acc"); pub fn delete_account(address: &Addr, storage: &mut dyn Storage) -> Result<(), ContractError> { diff --git a/contracts/vesting/src/vesting/account/mod.rs b/contracts/vesting/src/vesting/account/mod.rs index 424a6ca27f..4156630147 100644 --- a/contracts/vesting/src/vesting/account/mod.rs +++ b/contracts/vesting/src/vesting/account/mod.rs @@ -1,12 +1,10 @@ use super::{PledgeData, VestingPeriod}; use crate::contract::NUM_VESTING_PERIODS; use crate::errors::ContractError; -use crate::storage::save_account; +use crate::storage::{save_account, KEY}; use cosmwasm_std::{Addr, Coin, Order, Storage, Timestamp, Uint128}; use cw_storage_plus::{Item, Map}; use mixnet_contract::IdentityKey; -use rand::rngs::StdRng; -use rand::{RngCore, SeedableRng}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -20,17 +18,10 @@ const BALANCE_SUFFIX: &str = "ba"; const PLEDGE_SUFFIX: &str = "bo"; const GATEWAY_SUFFIX: &str = "ga"; -fn generate_storage_key(b: &[u8], storage: &dyn Storage) -> Result { - let mut rng = StdRng::seed_from_u64(b.iter().fold(0, |acc, x| acc + *x as u64)); - // Be paranoid and check for collisions - loop { - let key = rng.next_u64().to_string(); - let balance_key = format!("{}{}", key, BALANCE_SUFFIX); - let balance: Item = Item::new(&balance_key); - if balance.may_load(storage)?.is_none() { - return Ok(key); - } - } +fn generate_storage_key(storage: &mut dyn Storage) -> Result { + let key = KEY.may_load(storage)?.unwrap_or(0) + 1; + KEY.save(storage, &key)?; + Ok(key) } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] @@ -52,7 +43,7 @@ impl Account { periods: Vec, storage: &mut dyn Storage, ) -> Result { - let storage_key = generate_storage_key(owner_address.as_bytes(), storage)?; + let storage_key = generate_storage_key(storage)?.to_string(); let amount = coin.amount; let account = Account { owner_address, diff --git a/contracts/vesting/src/vesting/mod.rs b/contracts/vesting/src/vesting/mod.rs index 35d6d03d08..ef4551c11f 100644 --- a/contracts/vesting/src/vesting/mod.rs +++ b/contracts/vesting/src/vesting/mod.rs @@ -78,10 +78,7 @@ mod tests { // Test key collision avoidance let account_again = vesting_account_fixture(&mut deps.storage, &env); - assert_eq!( - created_account.balance_key(), - "5032709489228919411ba".to_string() - ); + assert_eq!(created_account.balance_key(), "1ba".to_string()); assert_ne!(created_account.balance_key(), account_again.balance_key()); } diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 7ab0c36296..1ef0c5f80d 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -5188,9 +5188,7 @@ dependencies = [ "config", "cosmwasm-std", "cw-storage-plus", - "getrandom 0.2.3", "mixnet-contract", - "rand 0.8.4", "schemars", "serde", "thiserror", From ec4955f81444eda658685c0929c05791847b7325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 6 Jan 2022 10:38:14 +0000 Subject: [PATCH 02/26] Feature/explorer node status (#1010) * Restored mixnode refresh rate to a more sane value * Moved PrettyMixNodeBondWithLocation to models.rs * Renaming * Exposed ability to query for rewarded mixnodes in the validator client * Reorganised mix_nodes module * Determining mixnode status (active/standby/inactive) * Moved LocationCache to separate lock * Minor cleanup * Changed serialization case of status enum * Made clippy happier * Slightly better grammar --- .../validator-client/src/client.rs | 6 + .../validator-client/src/validator_api/mod.rs | 5 + .../src/validator_api/routes.rs | 1 + .../src/country_statistics/geolocate.rs | 18 +- explorer-api/src/main.rs | 8 +- explorer-api/src/mix_node/http.rs | 24 +- explorer-api/src/mix_node/models.rs | 34 ++- explorer-api/src/mix_nodes/delegations.rs | 31 +++ explorer-api/src/mix_nodes/http.rs | 9 +- explorer-api/src/mix_nodes/location.rs | 62 +++++ explorer-api/src/mix_nodes/mod.rs | 249 +----------------- explorer-api/src/mix_nodes/models.rs | 150 +++++++++++ explorer-api/src/mix_nodes/tasks.rs | 77 +++++- explorer-api/src/mix_nodes/utils.rs | 24 +- explorer-api/src/state.rs | 5 +- 15 files changed, 421 insertions(+), 282 deletions(-) create mode 100644 explorer-api/src/mix_nodes/delegations.rs create mode 100644 explorer-api/src/mix_nodes/location.rs create mode 100644 explorer-api/src/mix_nodes/models.rs diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 8ad6e843d7..afacd3260c 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -420,6 +420,12 @@ impl ApiClient { Ok(self.validator_api.get_active_mixnodes().await?) } + pub async fn get_cached_rewarded_mixnodes( + &self, + ) -> Result, ValidatorClientError> { + Ok(self.validator_api.get_rewarded_mixnodes().await?) + } + pub async fn get_cached_mixnodes(&self) -> Result, ValidatorClientError> { Ok(self.validator_api.get_mixnodes().await?) } 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 8b993a5b7e..841e59b69e 100644 --- a/common/client-libs/validator-client/src/validator_api/mod.rs +++ b/common/client-libs/validator-client/src/validator_api/mod.rs @@ -73,6 +73,11 @@ impl Client { .await } + pub async fn get_rewarded_mixnodes(&self) -> Result, ValidatorAPIError> { + self.query_validator_api(&[routes::API_VERSION, routes::MIXNODES, routes::REWARDED]) + .await + } + pub async fn blind_sign( &self, request_body: &BlindSignRequestBody, 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 4a22b48150..ac171b01e6 100644 --- a/common/client-libs/validator-client/src/validator_api/routes.rs +++ b/common/client-libs/validator-client/src/validator_api/routes.rs @@ -8,6 +8,7 @@ pub const MIXNODES: &str = "mixnodes"; pub const GATEWAYS: &str = "gateways"; pub const ACTIVE: &str = "active"; +pub const REWARDED: &str = "rewarded"; pub const COCONUT_BLIND_SIGN: &str = "blind_sign"; pub const COCONUT_VERIFICATION_KEY: &str = "verification_key"; diff --git a/explorer-api/src/country_statistics/geolocate.rs b/explorer-api/src/country_statistics/geolocate.rs index a09231ceae..55cd428798 100644 --- a/explorer-api/src/country_statistics/geolocate.rs +++ b/explorer-api/src/country_statistics/geolocate.rs @@ -1,10 +1,12 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::mix_nodes::location::{GeoLocation, Location}; +use crate::state::ExplorerApiStateContext; use log::{info, warn}; use reqwest::Error as ReqwestError; use thiserror::Error; -use crate::mix_nodes::{GeoLocation, Location}; -use crate::state::ExplorerApiStateContext; - pub(crate) struct GeoLocateTask { state: ExplorerApiStateContext, } @@ -34,7 +36,15 @@ impl GeoLocateTask { } async fn locate_mix_nodes(&mut self) { - let mixnode_bonds = self.state.inner.mix_nodes.get().await.value; + // I'm unwrapping to the default value to get rid of an extra indentation level from the `if let Some(...) = ...` + // If the value is None, we'll unwrap to an empty hashmap and the `values()` loop won't do any work anyway + let mixnode_bonds = self + .state + .inner + .mix_nodes + .get_mixnodes() + .await + .unwrap_or_default(); for (i, cache_item) in mixnode_bonds.values().enumerate() { if self diff --git a/explorer-api/src/main.rs b/explorer-api/src/main.rs index 7312962d77..999e9f3fc7 100644 --- a/explorer-api/src/main.rs +++ b/explorer-api/src/main.rs @@ -36,13 +36,11 @@ impl ExplorerApi { async fn run(&mut self) { info!("Explorer API starting up..."); - info!( - "Using validator API - {}", - network_defaults::default_api_endpoints()[0].clone() - ); + let validator_api_url = network_defaults::default_api_endpoints()[0].clone(); + info!("Using validator API - {}", validator_api_url); // spawn concurrent tasks - mix_nodes::tasks::MixNodesTasks::new(self.state.clone()).start(); + mix_nodes::tasks::MixNodesTasks::new(self.state.clone(), validator_api_url).start(); country_statistics::distribution::CountryStatisticsDistributionTask::new( self.state.clone(), ) diff --git a/explorer-api/src/mix_node/http.rs b/explorer-api/src/mix_node/http.rs index 79b2b5750e..a663f594c5 100644 --- a/explorer-api/src/mix_node/http.rs +++ b/explorer-api/src/mix_node/http.rs @@ -1,16 +1,16 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::mix_node::models::{NodeDescription, NodeStats}; +use crate::mix_nodes::delegations::{get_mixnode_delegations, get_single_mixnode_delegations}; +use crate::state::ExplorerApiStateContext; +use mixnet_contract::Delegation; use reqwest::Error as ReqwestError; use rocket::serde::json::Json; use rocket::{Route, State}; use rocket_okapi::okapi::openapi3::OpenApi; use rocket_okapi::openapi_get_routes_spec; use rocket_okapi::settings::OpenApiSettings; -use serde::Serialize; - -use mixnet_contract::{Addr, Coin, Delegation, Layer, MixNode}; - -use crate::mix_node::models::{NodeDescription, NodeStats}; -use crate::mix_nodes::{get_mixnode_delegations, get_single_mixnode_delegations, Location}; -use crate::state::ExplorerApiStateContext; pub fn mix_node_make_default_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { openapi_get_routes_spec![ @@ -21,16 +21,6 @@ pub fn mix_node_make_default_routes(settings: &OpenApiSettings) -> (Vec, ] } -#[derive(Clone, Debug, Serialize, JsonSchema)] -pub(crate) struct PrettyMixNodeBondWithLocation { - pub location: Option, - pub pledge_amount: Coin, - pub total_delegation: Coin, - pub owner: Addr, - pub layer: Layer, - pub mix_node: MixNode, -} - #[openapi(tag = "mix_node")] #[get("//delegations")] pub(crate) async fn get_delegations(pubkey: &str) -> Json> { diff --git a/explorer-api/src/mix_node/models.rs b/explorer-api/src/mix_node/models.rs index 8aca0dc33e..323946a47f 100644 --- a/explorer-api/src/mix_node/models.rs +++ b/explorer-api/src/mix_node/models.rs @@ -1,11 +1,33 @@ -use std::sync::Arc; -use std::time::SystemTime; - -use serde::Deserialize; -use serde::Serialize; -use tokio::sync::RwLock; +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 use crate::mix_node::cache::Cache; +use crate::mix_nodes::location::Location; +use mixnet_contract::{Addr, Coin, Layer, MixNode}; +use serde::Deserialize; +use serde::Serialize; +use std::sync::Arc; +use std::time::SystemTime; +use tokio::sync::RwLock; + +#[derive(Clone, Debug, Serialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub(crate) enum MixnodeStatus { + Active, // in both the active set and the rewarded set + Standby, // only in the rewarded set + Inactive, // in neither the rewarded set nor the active set +} + +#[derive(Clone, Debug, Serialize, JsonSchema)] +pub(crate) struct PrettyDetailedMixNodeBond { + pub location: Option, + pub status: MixnodeStatus, + pub pledge_amount: Coin, + pub total_delegation: Coin, + pub owner: Addr, + pub layer: Layer, + pub mix_node: MixNode, +} pub(crate) struct MixNodeCache { pub(crate) descriptions: Cache, diff --git a/explorer-api/src/mix_nodes/delegations.rs b/explorer-api/src/mix_nodes/delegations.rs new file mode 100644 index 0000000000..17f15989a7 --- /dev/null +++ b/explorer-api/src/mix_nodes/delegations.rs @@ -0,0 +1,31 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use mixnet_contract::Delegation; + +pub(crate) async fn get_single_mixnode_delegations(pubkey: &str) -> Vec { + let client = super::utils::new_nymd_client(); + let delegates = match client + .get_all_nymd_single_mixnode_delegations(pubkey.to_string()) + .await + { + Ok(result) => result, + Err(e) => { + error!("Could not get delegations for mix node {}: {:?}", pubkey, e); + vec![] + } + }; + delegates +} + +pub(crate) async fn get_mixnode_delegations() -> Vec { + let client = super::utils::new_nymd_client(); + let delegates = match client.get_all_network_delegations().await { + Ok(result) => result, + Err(e) => { + error!("Could not get all mix delegations: {:?}", e); + vec![] + } + }; + delegates +} diff --git a/explorer-api/src/mix_nodes/http.rs b/explorer-api/src/mix_nodes/http.rs index efe75f6221..a2b8b0eaba 100644 --- a/explorer-api/src/mix_nodes/http.rs +++ b/explorer-api/src/mix_nodes/http.rs @@ -1,12 +1,11 @@ +use crate::mix_node::models::PrettyDetailedMixNodeBond; +use crate::state::ExplorerApiStateContext; use rocket::serde::json::Json; use rocket::{Route, State}; use rocket_okapi::okapi::openapi3::OpenApi; use rocket_okapi::openapi_get_routes_spec; use rocket_okapi::settings::OpenApiSettings; -use crate::mix_node::http::PrettyMixNodeBondWithLocation; -use crate::state::ExplorerApiStateContext; - pub fn mix_nodes_make_default_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { openapi_get_routes_spec![settings: list] } @@ -15,6 +14,6 @@ pub fn mix_nodes_make_default_routes(settings: &OpenApiSettings) -> (Vec, #[get("/")] pub(crate) async fn list( state: &State, -) -> Json> { - Json(state.inner.mix_nodes.get_mixnodes_with_location().await) +) -> Json> { + Json(state.inner.mix_nodes.get_detailed_mixnodes().await) } diff --git a/explorer-api/src/mix_nodes/location.rs b/explorer-api/src/mix_nodes/location.rs new file mode 100644 index 0000000000..80f24f6635 --- /dev/null +++ b/explorer-api/src/mix_nodes/location.rs @@ -0,0 +1,62 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::mix_nodes::utils::map_2_letter_to_3_letter_country_code; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::time::{Duration, SystemTime}; + +pub(crate) type LocationCache = HashMap; + +#[allow(dead_code)] +#[derive(Debug, Deserialize)] +pub(crate) struct GeoLocation { + pub(crate) ip: String, + pub(crate) country_code: String, + pub(crate) country_name: String, + pub(crate) region_code: String, + pub(crate) region_name: String, + pub(crate) city: String, + pub(crate) zip_code: String, + pub(crate) time_zone: String, + pub(crate) latitude: f32, + pub(crate) longitude: f32, + pub(crate) metro_code: u32, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(crate) struct LocationCacheItem { + pub(crate) location: Option, + pub(crate) valid_until: SystemTime, +} + +#[derive(Clone, Debug, JsonSchema, Serialize, Deserialize)] +pub(crate) struct Location { + pub(crate) two_letter_iso_country_code: String, + pub(crate) three_letter_iso_country_code: String, + pub(crate) country_name: String, + pub(crate) lat: f32, + pub(crate) lng: f32, +} + +impl Location { + pub(crate) fn new(geo_location: GeoLocation) -> Self { + let three_letter_iso_country_code = map_2_letter_to_3_letter_country_code(&geo_location); + Location { + country_name: geo_location.country_name, + two_letter_iso_country_code: geo_location.country_code, + three_letter_iso_country_code, + lat: geo_location.latitude, + lng: geo_location.longitude, + } + } +} + +impl LocationCacheItem { + pub(crate) fn new_from_location(location: Option) -> Self { + LocationCacheItem { + location, + valid_until: SystemTime::now() + Duration::from_secs(60 * 60 * 24), // valid for 1 day + } + } +} diff --git a/explorer-api/src/mix_nodes/mod.rs b/explorer-api/src/mix_nodes/mod.rs index dcbd5638e2..dacff38e32 100644 --- a/explorer-api/src/mix_nodes/mod.rs +++ b/explorer-api/src/mix_nodes/mod.rs @@ -1,241 +1,14 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::time::Duration; + +pub(crate) mod delegations; pub(crate) mod http; +pub(crate) mod location; +pub(crate) mod models; pub(crate) mod tasks; -mod utils; +pub(crate) mod utils; -use std::collections::HashMap; -use std::sync::Arc; -use std::time::{Duration, SystemTime}; - -use rocket::tokio::sync::RwLock; -use serde::{Deserialize, Serialize}; - -use crate::mix_node::http::PrettyMixNodeBondWithLocation; -use crate::mix_nodes::utils::map_2_letter_to_3_letter_country_code; -use mixnet_contract::{Delegation, MixNodeBond}; -use network_defaults::{ - default_api_endpoints, default_nymd_endpoints, DEFAULT_MIXNET_CONTRACT_ADDRESS, -}; -use validator_client::nymd::QueryNymdClient; - -pub(crate) type LocationCache = HashMap; - -#[allow(dead_code)] -#[derive(Debug, Deserialize)] -pub(crate) struct GeoLocation { - pub(crate) ip: String, - pub(crate) country_code: String, - pub(crate) country_name: String, - pub(crate) region_code: String, - pub(crate) region_name: String, - pub(crate) city: String, - pub(crate) zip_code: String, - pub(crate) time_zone: String, - pub(crate) latitude: f32, - pub(crate) longitude: f32, - pub(crate) metro_code: u32, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub(crate) struct LocationCacheItem { - pub(crate) location: Option, - pub(crate) valid_until: SystemTime, -} - -#[derive(Clone, Debug, JsonSchema, Serialize, Deserialize)] -pub(crate) struct Location { - pub(crate) two_letter_iso_country_code: String, - pub(crate) three_letter_iso_country_code: String, - pub(crate) country_name: String, - pub(crate) lat: f32, - pub(crate) lng: f32, -} - -impl Location { - pub(crate) fn new(geo_location: GeoLocation) -> Self { - let three_letter_iso_country_code = map_2_letter_to_3_letter_country_code(&geo_location); - Location { - country_name: geo_location.country_name, - two_letter_iso_country_code: geo_location.country_code, - three_letter_iso_country_code, - lat: geo_location.latitude, - lng: geo_location.longitude, - } - } -} - -impl LocationCacheItem { - pub(crate) fn new_from_location(location: Option) -> Self { - LocationCacheItem { - location, - valid_until: SystemTime::now() + Duration::from_secs(60 * 60 * 24), // valid for 1 day - } - } -} - -#[derive(Clone, Debug)] -pub(crate) struct MixNodesResult { - pub(crate) valid_until: SystemTime, - pub(crate) value: HashMap, - location_cache: LocationCache, -} - -#[derive(Clone)] -pub(crate) struct ThreadsafeMixNodesResult { - inner: Arc>, -} - -impl ThreadsafeMixNodesResult { - pub(crate) fn new() -> Self { - ThreadsafeMixNodesResult { - inner: Arc::new(RwLock::new(MixNodesResult { - value: HashMap::new(), - valid_until: SystemTime::now() - Duration::from_secs(60), // in the past - location_cache: LocationCache::new(), - })), - } - } - - pub(crate) fn new_with_location_cache(location_cache: LocationCache) -> Self { - ThreadsafeMixNodesResult { - inner: Arc::new(RwLock::new(MixNodesResult { - value: HashMap::new(), - valid_until: SystemTime::now() - Duration::from_secs(60), // in the past - location_cache, - })), - } - } - - pub(crate) async fn is_location_valid(&self, identity_key: &str) -> bool { - self.inner - .read() - .await - .location_cache - .get(identity_key) - .map(|cache_item| cache_item.valid_until > SystemTime::now()) - .unwrap_or(false) - } - - pub(crate) async fn get_location_cache(&self) -> LocationCache { - self.inner.read().await.location_cache.clone() - } - - pub(crate) async fn set_location(&self, identity_key: &str, location: Option) { - let mut guard = self.inner.write().await; - - // cache the location for this mix node so that it can be used when the mix node list is refreshed - guard.location_cache.insert( - identity_key.to_string(), - LocationCacheItem::new_from_location(location), - ); - } - - pub(crate) async fn get(&self) -> MixNodesResult { - // check ttl - let valid_until = self.inner.read().await.valid_until; - - if valid_until < SystemTime::now() { - // force reload - self.refresh().await; - } - - // return in-memory cache - self.inner.read().await.clone() - } - - pub(crate) async fn get_mixnodes_with_location(&self) -> Vec { - let guard = self.inner.read().await; - guard - .value - .values() - .map(|bond| { - let location = guard.location_cache.get(&bond.mix_node.identity_key); - let copy = bond.clone(); - PrettyMixNodeBondWithLocation { - location: location.and_then(|l| l.location.clone()), - pledge_amount: copy.pledge_amount, - total_delegation: copy.total_delegation, - owner: copy.owner, - layer: copy.layer, - mix_node: copy.mix_node, - } - }) - .collect() - } - - pub(crate) async fn refresh(&self) { - // get mixnodes and cache the new value - let value = retrieve_mixnodes().await; - let location_cache = self.inner.read().await.location_cache.clone(); - *self.inner.write().await = MixNodesResult { - value: value - .into_iter() - .map(|bond| (bond.mix_node.identity_key.to_string(), bond)) - .collect(), - valid_until: SystemTime::now() + Duration::from_secs(30), - location_cache, - }; - } -} - -pub(crate) async fn retrieve_mixnodes() -> Vec { - let client = new_validator_client(); - - info!("About to retrieve mixnode bonds..."); - - let bonds: Vec = match client.get_cached_mixnodes().await { - Ok(result) => result, - Err(e) => { - error!("Unable to retrieve mixnode bonds: {:?}", e); - vec![] - } - }; - info!("Fetched {} mixnode bonds", bonds.len()); - bonds -} - -pub(crate) async fn get_single_mixnode_delegations(pubkey: &str) -> Vec { - let client = new_nymd_client(); - let delegates = match client - .get_all_nymd_single_mixnode_delegations(pubkey.to_string()) - .await - { - Ok(result) => result, - Err(e) => { - error!("Could not get delegations for mix node {}: {:?}", pubkey, e); - vec![] - } - }; - delegates -} - -pub(crate) async fn get_mixnode_delegations() -> Vec { - let client = new_nymd_client(); - let delegates = match client.get_all_network_delegations().await { - Ok(result) => result, - Err(e) => { - error!("Could not get all mix delegations: {:?}", e); - vec![] - } - }; - delegates -} - -fn new_nymd_client() -> validator_client::Client { - let mixnet_contract = DEFAULT_MIXNET_CONTRACT_ADDRESS.to_string(); - let nymd_url = default_nymd_endpoints()[0].clone(); - let api_url = default_api_endpoints()[0].clone(); - - let client_config = validator_client::Config::new( - nymd_url, - api_url, - Some(mixnet_contract.parse().unwrap()), - None, - ); - - validator_client::Client::new_query(client_config).expect("Failed to connect to nymd!") -} - -// TODO: inject constants -fn new_validator_client() -> validator_client::ApiClient { - validator_client::ApiClient::new(default_api_endpoints()[0].clone()) -} +pub(crate) const MIXNODES_CACHE_REFRESH_RATE: Duration = Duration::from_secs(30); +pub(crate) const MIXNODES_CACHE_ENTRY_TTL: Duration = Duration::from_secs(60); diff --git a/explorer-api/src/mix_nodes/models.rs b/explorer-api/src/mix_nodes/models.rs new file mode 100644 index 0000000000..7ccd179f86 --- /dev/null +++ b/explorer-api/src/mix_nodes/models.rs @@ -0,0 +1,150 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::mix_node::models::{MixnodeStatus, PrettyDetailedMixNodeBond}; +use crate::mix_nodes::location::{Location, LocationCache, LocationCacheItem}; +use crate::mix_nodes::MIXNODES_CACHE_ENTRY_TTL; +use mixnet_contract::MixNodeBond; +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; +use std::time::{Duration, SystemTime}; +use tokio::sync::RwLock; + +#[derive(Clone, Debug)] +pub(crate) struct MixNodesResult { + pub(crate) valid_until: SystemTime, + pub(crate) all_mixnodes: HashMap, + active_mixnodes: HashSet, + rewarded_mixnodes: HashSet, +} + +impl MixNodesResult { + fn new() -> Self { + MixNodesResult { + valid_until: SystemTime::now() - Duration::from_secs(60), // in the past + all_mixnodes: HashMap::new(), + active_mixnodes: HashSet::new(), + rewarded_mixnodes: HashSet::new(), + } + } + + fn determine_node_status(&self, public_key: &str) -> MixnodeStatus { + if self.active_mixnodes.contains(public_key) { + MixnodeStatus::Active + } else if self.rewarded_mixnodes.contains(public_key) { + MixnodeStatus::Standby + } else { + MixnodeStatus::Inactive + } + } + + fn is_valid(&self) -> bool { + self.valid_until >= SystemTime::now() + } + + fn get_mixnode(&self, pubkey: &str) -> Option { + if self.is_valid() { + self.all_mixnodes.get(pubkey).cloned() + } else { + None + } + } + + fn get_mixnodes(&self) -> Option> { + if self.is_valid() { + Some(self.all_mixnodes.clone()) + } else { + None + } + } +} + +#[derive(Clone)] +pub(crate) struct ThreadsafeMixNodesResult { + mixnode_results: Arc>, + location_cache: Arc>, +} + +impl ThreadsafeMixNodesResult { + pub(crate) fn new() -> Self { + ThreadsafeMixNodesResult { + mixnode_results: Arc::new(RwLock::new(MixNodesResult::new())), + location_cache: Arc::new(RwLock::new(LocationCache::new())), + } + } + + pub(crate) fn new_with_location_cache(location_cache: LocationCache) -> Self { + ThreadsafeMixNodesResult { + mixnode_results: Arc::new(RwLock::new(MixNodesResult::new())), + location_cache: Arc::new(RwLock::new(location_cache)), + } + } + + pub(crate) async fn is_location_valid(&self, identity_key: &str) -> bool { + self.location_cache + .read() + .await + .get(identity_key) + .map(|cache_item| cache_item.valid_until > SystemTime::now()) + .unwrap_or(false) + } + + pub(crate) async fn get_location_cache(&self) -> LocationCache { + self.location_cache.read().await.clone() + } + + pub(crate) async fn set_location(&self, identity_key: &str, location: Option) { + // cache the location for this mix node so that it can be used when the mix node list is refreshed + self.location_cache.write().await.insert( + identity_key.to_string(), + LocationCacheItem::new_from_location(location), + ); + } + + pub(crate) async fn get_mixnode(&self, pubkey: &str) -> Option { + self.mixnode_results.read().await.get_mixnode(pubkey) + } + + pub(crate) async fn get_mixnodes(&self) -> Option> { + self.mixnode_results.read().await.get_mixnodes() + } + + pub(crate) async fn get_detailed_mixnodes(&self) -> Vec { + let mixnodes_guard = self.mixnode_results.read().await; + let location_guard = self.location_cache.read().await; + + mixnodes_guard + .all_mixnodes + .values() + .map(|bond| { + let location = location_guard.get(&bond.mix_node.identity_key); + let copy = bond.clone(); + PrettyDetailedMixNodeBond { + location: location.and_then(|l| l.location.clone()), + status: mixnodes_guard.determine_node_status(&bond.mix_node.identity_key), + pledge_amount: copy.pledge_amount, + total_delegation: copy.total_delegation, + owner: copy.owner, + layer: copy.layer, + mix_node: copy.mix_node, + } + }) + .collect() + } + + pub(crate) async fn update_cache( + &self, + all_bonds: Vec, + rewarded_nodes: HashSet, + active_nodes: HashSet, + ) { + let mut guard = self.mixnode_results.write().await; + guard.all_mixnodes = all_bonds + .into_iter() + .map(|bond| (bond.mix_node.identity_key.to_string(), bond)) + .collect(); + guard.rewarded_mixnodes = rewarded_nodes; + guard.active_mixnodes = active_nodes; + guard.valid_until = SystemTime::now() + MIXNODES_CACHE_ENTRY_TTL; + } +} diff --git a/explorer-api/src/mix_nodes/tasks.rs b/explorer-api/src/mix_nodes/tasks.rs index f89cd61217..d1580a832c 100644 --- a/explorer-api/src/mix_nodes/tasks.rs +++ b/explorer-api/src/mix_nodes/tasks.rs @@ -1,24 +1,93 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::mix_nodes::MIXNODES_CACHE_REFRESH_RATE; use crate::state::ExplorerApiStateContext; +use mixnet_contract::MixNodeBond; +use reqwest::Url; +use std::future::Future; +use validator_client::ValidatorClientError; pub(crate) struct MixNodesTasks { state: ExplorerApiStateContext, + validator_api_client: validator_client::ApiClient, } impl MixNodesTasks { - pub(crate) fn new(state: ExplorerApiStateContext) -> Self { - MixNodesTasks { state } + pub(crate) fn new(state: ExplorerApiStateContext, validator_api_endpoint: Url) -> Self { + MixNodesTasks { + state, + validator_api_client: validator_client::ApiClient::new(validator_api_endpoint), + } + } + + // a helper to remove duplicate code when grabbing active/rewarded/all mixnodes + async fn retrieve_mixnodes<'a, F, Fut>(&'a self, f: F) -> Vec + where + F: FnOnce(&'a validator_client::ApiClient) -> Fut, + Fut: Future, ValidatorClientError>>, + { + let bonds = match f(&self.validator_api_client).await { + Ok(result) => result, + Err(e) => { + error!("Unable to retrieve mixnode bonds: {:?}", e); + vec![] + } + }; + + info!("Fetched {} mixnode bonds", bonds.len()); + bonds + } + + async fn retrieve_all_mixnodes(&self) -> Vec { + info!("About to retrieve all mixnode bonds..."); + self.retrieve_mixnodes(validator_client::ApiClient::get_cached_mixnodes) + .await + } + + async fn retrieve_rewarded_mixnodes(&self) -> Vec { + info!("About to retrieve rewarded mixnode bonds..."); + self.retrieve_mixnodes(validator_client::ApiClient::get_cached_rewarded_mixnodes) + .await + } + + async fn retrieve_active_mixnodes(&self) -> Vec { + info!("About to retrieve active mixnode bonds..."); + self.retrieve_mixnodes(validator_client::ApiClient::get_cached_active_mixnodes) + .await + } + + async fn update_mixnode_cache(&self) { + let all_bonds = self.retrieve_all_mixnodes().await; + let rewarded_nodes = self + .retrieve_rewarded_mixnodes() + .await + .into_iter() + .map(|bond| bond.mix_node.identity_key) + .collect(); + let active_nodes = self + .retrieve_active_mixnodes() + .await + .into_iter() + .map(|bond| bond.mix_node.identity_key) + .collect(); + self.state + .inner + .mix_nodes + .update_cache(all_bonds, rewarded_nodes, active_nodes) + .await; } pub(crate) fn start(self) { info!("Spawning mix nodes task runner..."); tokio::spawn(async move { - let mut interval_timer = tokio::time::interval(std::time::Duration::from_secs(2)); + let mut interval_timer = tokio::time::interval(MIXNODES_CACHE_REFRESH_RATE); loop { // wait for the next interval tick interval_timer.tick().await; info!("Updating mix node cache..."); - self.state.inner.mix_nodes.refresh().await; + self.update_mixnode_cache().await; info!("Done"); } }); diff --git a/explorer-api/src/mix_nodes/utils.rs b/explorer-api/src/mix_nodes/utils.rs index 7e46ce7afd..542bfe8660 100644 --- a/explorer-api/src/mix_nodes/utils.rs +++ b/explorer-api/src/mix_nodes/utils.rs @@ -1,5 +1,12 @@ -use crate::mix_nodes::GeoLocation; +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::mix_nodes::location::GeoLocation; use isocountry::CountryCode; +use network_defaults::{ + default_api_endpoints, default_nymd_endpoints, DEFAULT_MIXNET_CONTRACT_ADDRESS, +}; +use validator_client::nymd::QueryNymdClient; pub(crate) fn map_2_letter_to_3_letter_country_code(geo: &GeoLocation) -> String { match CountryCode::for_alpha2(&geo.country_code) { @@ -13,3 +20,18 @@ pub(crate) fn map_2_letter_to_3_letter_country_code(geo: &GeoLocation) -> String } } } + +pub(crate) fn new_nymd_client() -> validator_client::Client { + let mixnet_contract = DEFAULT_MIXNET_CONTRACT_ADDRESS.to_string(); + let nymd_url = default_nymd_endpoints()[0].clone(); + let api_url = default_api_endpoints()[0].clone(); + + let client_config = validator_client::Config::new( + nymd_url, + api_url, + Some(mixnet_contract.parse().unwrap()), + None, + ); + + validator_client::Client::new_query(client_config).expect("Failed to connect to nymd!") +} diff --git a/explorer-api/src/state.rs b/explorer-api/src/state.rs index 3202abc9ea..d6be0e3f70 100644 --- a/explorer-api/src/state.rs +++ b/explorer-api/src/state.rs @@ -11,7 +11,8 @@ use crate::country_statistics::country_nodes_distribution::{ ConcurrentCountryNodesDistribution, CountryNodesDistribution, }; use crate::mix_node::models::ThreadsafeMixNodeCache; -use crate::mix_nodes::{LocationCache, ThreadsafeMixNodesResult}; +use crate::mix_nodes::location::LocationCache; +use crate::mix_nodes::models::ThreadsafeMixNodesResult; use crate::ping::models::ThreadsafePingCache; // TODO: change to an environment variable with a default value @@ -27,7 +28,7 @@ pub struct ExplorerApiState { impl ExplorerApiState { pub(crate) async fn get_mix_node(&self, pubkey: &str) -> Option { - self.mix_nodes.get().await.value.get(pubkey).cloned() + self.mix_nodes.get_mixnode(pubkey).await } } From 30e93c33bba54d768d97cf5e3f2e16223f7ebb25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bogdan-=C8=98tefan=20Neac=C5=9Fu?= Date: Thu, 6 Jan 2022 13:03:14 +0100 Subject: [PATCH 03/26] Feature/configure profit (#1008) * Introduce a method to update mixnode configuration Right now, only for profit_margin_percent * Check that the new profit margin is valid * Extend a bit the test coverage of mixnode update * Create validator client function * [ci skip] Generate TS types * Update wallet * Update the bond height as well, as if a rebond was made Co-authored-by: neacsu --- .../validator-client/src/nymd/fee/helpers.rs | 3 + .../validator-client/src/nymd/mod.rs | 25 ++++ common/mixnet-contract/src/msg.rs | 3 + contracts/mixnet/src/contract.rs | 8 ++ contracts/mixnet/src/mixnodes/transactions.rs | 107 ++++++++++++++++++ nym-wallet/src-tauri/src/main.rs | 1 + .../src-tauri/src/operations/mixnet/bond.rs | 11 ++ nym-wallet/src/types/rust/operation.ts | 1 + 8 files changed, 159 insertions(+) diff --git a/common/client-libs/validator-client/src/nymd/fee/helpers.rs b/common/client-libs/validator-client/src/nymd/fee/helpers.rs index cc16e21cb5..0ddbf15356 100644 --- a/common/client-libs/validator-client/src/nymd/fee/helpers.rs +++ b/common/client-libs/validator-client/src/nymd/fee/helpers.rs @@ -20,6 +20,7 @@ pub enum Operation { BondMixnodeOnBehalf, UnbondMixnode, UnbondMixnodeOnBehalf, + UpdateMixnodeConfig, DelegateToMixnode, DelegateToMixnodeOnBehalf, UndelegateFromMixnode, @@ -57,6 +58,7 @@ impl fmt::Display for Operation { Operation::BondMixnode => f.write_str("BondMixnode"), Operation::BondMixnodeOnBehalf => f.write_str("BondMixnodeOnBehalf"), Operation::UnbondMixnode => f.write_str("UnbondMixnode"), + Operation::UpdateMixnodeConfig => f.write_str("UpdateMixnodeConfig"), Operation::UnbondMixnodeOnBehalf => f.write_str("UnbondMixnodeOnBehalf"), Operation::BondGateway => f.write_str("BondGateway"), Operation::BondGatewayOnBehalf => f.write_str("BondGatewayOnBehalf"), @@ -94,6 +96,7 @@ impl Operation { Operation::BondMixnodeOnBehalf => 200_000u64.into(), Operation::UnbondMixnode => 175_000u64.into(), Operation::UnbondMixnodeOnBehalf => 175_000u64.into(), + Operation::UpdateMixnodeConfig => 175_000u64.into(), Operation::DelegateToMixnode => 175_000u64.into(), Operation::DelegateToMixnodeOnBehalf => 175_000u64.into(), Operation::UndelegateFromMixnode => 175_000u64.into(), diff --git a/common/client-libs/validator-client/src/nymd/mod.rs b/common/client-libs/validator-client/src/nymd/mod.rs index 72f72a220f..71c8766cf7 100644 --- a/common/client-libs/validator-client/src/nymd/mod.rs +++ b/common/client-libs/validator-client/src/nymd/mod.rs @@ -773,6 +773,31 @@ impl NymdClient { .await } + /// Update the configuration of a mixnode. Right now, only possible for profit margin. + pub async fn update_mixnode_config( + &self, + profit_margin_percent: u8, + ) -> Result + where + C: SigningCosmWasmClient + Sync, + { + let fee = self.operation_fee(Operation::UpdateMixnodeConfig); + + let req = ExecuteMsg::UpdateMixnodeConfig { + profit_margin_percent, + }; + self.client + .execute( + self.address(), + self.mixnet_contract_address()?, + &req, + fee, + "Updating mixnode configuration from rust!", + Vec::new(), + ) + .await + } + /// Delegates specified amount of stake to particular mixnode. pub async fn delegate_to_mixnode( &self, diff --git a/common/mixnet-contract/src/msg.rs b/common/mixnet-contract/src/msg.rs index c744a4f56e..8f74824ec0 100644 --- a/common/mixnet-contract/src/msg.rs +++ b/common/mixnet-contract/src/msg.rs @@ -20,6 +20,9 @@ pub enum ExecuteMsg { owner_signature: String, }, UnbondMixnode {}, + UpdateMixnodeConfig { + profit_margin_percent: u8, + }, BondGateway { gateway: Gateway, owner_signature: String, diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index d46c7aa3c7..2cf4d65686 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -104,6 +104,14 @@ pub fn execute( ExecuteMsg::UnbondMixnode {} => { crate::mixnodes::transactions::try_remove_mixnode(deps, info) } + ExecuteMsg::UpdateMixnodeConfig { + profit_margin_percent, + } => crate::mixnodes::transactions::try_update_mixnode_config( + deps, + env, + info, + profit_margin_percent, + ), ExecuteMsg::BondGateway { gateway, owner_signature, diff --git a/contracts/mixnet/src/mixnodes/transactions.rs b/contracts/mixnet/src/mixnodes/transactions.rs index 3f7c3d40e3..5ec7cdc33a 100644 --- a/contracts/mixnet/src/mixnodes/transactions.rs +++ b/contracts/mixnet/src/mixnodes/transactions.rs @@ -204,6 +204,42 @@ pub(crate) fn _try_remove_mixnode( Ok(response) } +pub(crate) fn try_update_mixnode_config( + deps: DepsMut, + env: Env, + info: MessageInfo, + profit_margin_percent: u8, +) -> Result { + let owner = deps.api.addr_validate(info.sender.as_ref())?; + let mix_identity = storage::mixnodes() + .idx + .owner + .item(deps.storage, owner.clone())? + .ok_or(ContractError::NoAssociatedMixNodeBond { owner })? + .1 + .identity() + .clone(); + + // We don't have to check lower bound as its an u8 + if profit_margin_percent > 100 { + return Err(ContractError::InvalidProfitMarginPercent( + profit_margin_percent, + )); + } + + storage::mixnodes().update(deps.storage, &mix_identity, |mixnode_bond_opt| { + mixnode_bond_opt + .map(|mut mixnode_bond| { + mixnode_bond.mix_node.profit_margin_percent = profit_margin_percent; + mixnode_bond.block_height = env.block.height; + mixnode_bond + }) + .ok_or(ContractError::NoBondFound) + })?; + + Ok(Response::new()) +} + fn validate_mixnode_pledge( mut pledge: Vec, minimum_pledge: Uint128, @@ -587,6 +623,77 @@ pub mod tests { ); } + #[test] + fn updating_mixnode_config() { + let sender = "bob"; + let mut deps = test_helpers::init_contract(); + let info = mock_info(sender, &[]); + + // try updating a non existing mixnode bond + let msg = ExecuteMsg::UpdateMixnodeConfig { + profit_margin_percent: 10, + }; + let ret = execute(deps.as_mut(), mock_env(), info.clone(), msg); + assert_eq!( + ret, + Err(ContractError::NoAssociatedMixNodeBond { + owner: Addr::unchecked(sender) + }) + ); + + test_helpers::add_mixnode( + sender, + tests::fixtures::good_mixnode_pledge(), + deps.as_mut(), + ); + + // check the initial profit margin is set to the fixture value + let fixture_profit_margin = tests::fixtures::mix_node_fixture().profit_margin_percent; + assert_eq!( + fixture_profit_margin, + storage::mixnodes() + .idx + .owner + .item(deps.as_ref().storage, Addr::unchecked("bob")) + .unwrap() + .unwrap() + .1 + .mix_node + .profit_margin_percent + ); + + // try updating with an invalid value + let profit_margin_percent = 101; + let msg = ExecuteMsg::UpdateMixnodeConfig { + profit_margin_percent, + }; + let ret = execute(deps.as_mut(), mock_env(), info.clone(), msg); + assert_eq!( + ret, + Err(ContractError::InvalidProfitMarginPercent( + profit_margin_percent + )) + ); + + let profit_margin_percent = fixture_profit_margin + 10; + let msg = ExecuteMsg::UpdateMixnodeConfig { + profit_margin_percent, + }; + execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + assert_eq!( + profit_margin_percent, + storage::mixnodes() + .idx + .owner + .item(deps.as_ref().storage, Addr::unchecked("bob")) + .unwrap() + .unwrap() + .1 + .mix_node + .profit_margin_percent + ); + } + #[test] fn validating_mixnode_bond() { // you must send SOME funds diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index e402137e39..a52b9a4a91 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -36,6 +36,7 @@ fn main() { mixnet::bond::bond_mixnode, mixnet::bond::unbond_gateway, mixnet::bond::unbond_mixnode, + mixnet::bond::update_mixnode, mixnet::bond::mixnode_bond_details, mixnet::bond::gateway_bond_details, mixnet::delegate::delegate_to_mixnode, diff --git a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs index b1801b576f..fce6c4219a 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs @@ -50,6 +50,17 @@ pub async fn bond_mixnode( Ok(()) } +#[tauri::command] +pub async fn update_mixnode( + profit_margin_percent: u8, + state: tauri::State<'_, Arc>>, +) -> Result<(), BackendError> { + client!(state) + .update_mixnode_config(profit_margin_percent) + .await?; + Ok(()) +} + #[tauri::command] pub async fn mixnode_bond_details( state: tauri::State<'_, Arc>>, diff --git a/nym-wallet/src/types/rust/operation.ts b/nym-wallet/src/types/rust/operation.ts index 545c7bbe81..6c8d38c221 100644 --- a/nym-wallet/src/types/rust/operation.ts +++ b/nym-wallet/src/types/rust/operation.ts @@ -8,6 +8,7 @@ export type Operation = | "BondMixnodeOnBehalf" | "UnbondMixnode" | "UnbondMixnodeOnBehalf" + | "UpdateMixnodeConfig" | "DelegateToMixnode" | "DelegateToMixnodeOnBehalf" | "UndelegateFromMixnode" From 70138ff54a5ad402a0a1758d000985ecad7b52ba Mon Sep 17 00:00:00 2001 From: fmtabbara Date: Thu, 6 Jan 2022 13:00:50 +0000 Subject: [PATCH 04/26] update frontend to use new profit update api --- nym-wallet/src/pages/settings/index.tsx | 6 +-- .../src/pages/settings/system-variables.tsx | 41 +++++-------------- .../src/pages/settings/validationSchema.ts | 1 - nym-wallet/src/requests/index.ts | 3 ++ 4 files changed, 15 insertions(+), 36 deletions(-) diff --git a/nym-wallet/src/pages/settings/index.tsx b/nym-wallet/src/pages/settings/index.tsx index fca3b126dc..b25859d88a 100644 --- a/nym-wallet/src/pages/settings/index.tsx +++ b/nym-wallet/src/pages/settings/index.tsx @@ -24,7 +24,7 @@ export const Settings = () => { setMixnodeDetails(details) } if (showSettings) getBondDetails() - }, [showSettings]) + }, [showSettings, selectedTab]) const handleTabChange = (event: React.SyntheticEvent, newTab: number) => setSelectedTab(newTab) @@ -50,9 +50,7 @@ export const Settings = () => { )} {selectedTab === 0 && mixnodeDetails && } - {selectedTab === 1 && mixnodeDetails && ( - - )} + {selectedTab === 1 && mixnodeDetails && } {selectedTab === 2 && mixnodeDetails && } diff --git a/nym-wallet/src/pages/settings/system-variables.tsx b/nym-wallet/src/pages/settings/system-variables.tsx index befdbd72bc..1f131c957e 100644 --- a/nym-wallet/src/pages/settings/system-variables.tsx +++ b/nym-wallet/src/pages/settings/system-variables.tsx @@ -15,23 +15,16 @@ import { AccessTimeOutlined, PercentOutlined } from '@mui/icons-material' import { yupResolver } from '@hookform/resolvers/yup' import { useForm } from 'react-hook-form' import { InfoTooltip } from '../../components/InfoToolTip' -import { EnumNodeType, TMixnodeBondDetails } from '../../types' +import { TMixnodeBondDetails } from '../../types' import { validationSchema } from './validationSchema' -import { bond, unbond } from '../../requests' +import { updateMixnode } from '../../requests' import { ClientContext } from '../../context/main' type TFormData = { profitMarginPercent: number - signature: string } -export const SystemVariables = ({ - mixnodeDetails, - pledge, -}: { - mixnodeDetails: TMixnodeBondDetails['mix_node'] - pledge: TMixnodeBondDetails['pledge_amount'] -}) => { +export const SystemVariables = ({ mixnodeDetails }: { mixnodeDetails: TMixnodeBondDetails['mix_node'] }) => { const [nodeUpdateResponse, setNodeUpdateResponse] = useState<'success' | 'failed'>() const { @@ -40,28 +33,21 @@ export const SystemVariables = ({ formState: { errors, isSubmitting }, } = useForm({ resolver: yupResolver(validationSchema), - defaultValues: { profitMarginPercent: mixnodeDetails.profit_margin_percent.toString(), signature: '' }, + defaultValues: { profitMarginPercent: mixnodeDetails.profit_margin_percent.toString() }, }) const { userBalance } = useContext(ClientContext) const onSubmit = async (data: TFormData) => { - await unbond(EnumNodeType.mixnode) - await bond({ - type: EnumNodeType.mixnode, - data: { ...mixnodeDetails, profit_margin_percent: data.profitMarginPercent }, - pledge: { denom: 'Minor', amount: pledge.amount }, - //hardcoded for the moment as required in bonding but not necessary - ownerSignature: data.signature, - }) - .then(() => { + try { + await updateMixnode({ profitMarginPercent: data.profitMarginPercent }).then(() => { userBalance.fetchBalance() setNodeUpdateResponse('success') }) - .catch((e) => { - setNodeUpdateResponse('failed') - console.log(e) - }) + } catch (e) { + setNodeUpdateResponse('failed') + console.log(e) + } } return ( @@ -80,13 +66,6 @@ export const SystemVariables = ({ error={!!errors.profitMarginPercent} disabled={isSubmitting} /> - export const getMixnodeBondDetails = async (): Promise => await invoke('mixnode_bond_details') + +export const updateMixnode = async ({ profitMarginPercent }: { profitMarginPercent: number }) => + await invoke('update_mixnode', { profitMarginPercent }) From db2ce8070c9e834238df4363c9a400b86c4aae51 Mon Sep 17 00:00:00 2001 From: fmtabbara Date: Thu, 6 Jan 2022 16:16:32 +0000 Subject: [PATCH 05/26] display mixnode update fee --- .../src/pages/settings/system-variables.tsx | 18 ++++++++++++++---- nym-wallet/src/requests/index.ts | 2 +- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/nym-wallet/src/pages/settings/system-variables.tsx b/nym-wallet/src/pages/settings/system-variables.tsx index 1f131c957e..0ad6b50e25 100644 --- a/nym-wallet/src/pages/settings/system-variables.tsx +++ b/nym-wallet/src/pages/settings/system-variables.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useState } from 'react' +import React, { useContext, useEffect, useState } from 'react' import { Box, Button, @@ -17,8 +17,8 @@ import { useForm } from 'react-hook-form' import { InfoTooltip } from '../../components/InfoToolTip' import { TMixnodeBondDetails } from '../../types' import { validationSchema } from './validationSchema' -import { updateMixnode } from '../../requests' -import { ClientContext } from '../../context/main' +import { getGasFee, updateMixnode } from '../../requests' +import { ClientContext, MAJOR_CURRENCY } from '../../context/main' type TFormData = { profitMarginPercent: number @@ -26,6 +26,7 @@ type TFormData = { export const SystemVariables = ({ mixnodeDetails }: { mixnodeDetails: TMixnodeBondDetails['mix_node'] }) => { const [nodeUpdateResponse, setNodeUpdateResponse] = useState<'success' | 'failed'>() + const [configFee, setConfigFee] = useState() const { register, @@ -36,6 +37,13 @@ export const SystemVariables = ({ mixnodeDetails }: { mixnodeDetails: TMixnodeBo defaultValues: { profitMarginPercent: mixnodeDetails.profit_margin_percent.toString() }, }) + useEffect(() => { + ;(async () => { + const fee = await getGasFee('UpdateMixnodeConfig') + setConfigFee(fee.amount) + })() + }, []) + const { userBalance } = useContext(ClientContext) const onSubmit = async (data: TFormData) => { @@ -107,7 +115,9 @@ export const SystemVariables = ({ mixnodeDetails }: { mixnodeDetails: TMixnodeBo ) : nodeUpdateResponse === 'failed' ? ( Node updated failed ) : ( - + + Fee for this transaction: {`${configFee} ${MAJOR_CURRENCY}`}{' '} + )} diff --git a/nym-wallet/src/pages/delegate/DelegateForm.tsx b/nym-wallet/src/pages/delegate/DelegateForm.tsx index dbcff4c4b0..e8c671daa8 100644 --- a/nym-wallet/src/pages/delegate/DelegateForm.tsx +++ b/nym-wallet/src/pages/delegate/DelegateForm.tsx @@ -115,9 +115,7 @@ export const DelegateForm = ({ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', - borderTop: (theme) => `1px solid ${theme.palette.grey[200]}`, - bgcolor: 'grey.100', - padding: 2, + padding: 3, }} > diff --git a/nym-wallet/src/pages/receive/index.tsx b/nym-wallet/src/pages/receive/index.tsx index 66a41cd792..afab517dc5 100644 --- a/nym-wallet/src/pages/receive/index.tsx +++ b/nym-wallet/src/pages/receive/index.tsx @@ -1,65 +1,37 @@ import React, { useContext } from 'react' import QRCode from 'qrcode.react' -import { Alert, Box, Card, Grid, Typography, useMediaQuery } from '@mui/material' +import { Alert, Box, Stack, Typography } from '@mui/material' import { CopyToClipboard, NymCard } from '../../components' import { Layout } from '../../layouts' import { ClientContext } from '../../context/main' export const Receive = () => { const { clientDetails } = useContext(ClientContext) - const matches = useMediaQuery('(min-width:769px)') return ( - - - - You can receive tokens by providing this address to the sender - - - - + + You can receive tokens by providing this address to the sender + + + - - - - {clientDetails?.client_address} - - - - - - {clientDetails && } - - - - - - + Your address: {clientDetails?.client_address} + + + + + {clientDetails && } + ) diff --git a/nym-wallet/src/pages/send/SendWizard.tsx b/nym-wallet/src/pages/send/SendWizard.tsx index f3965b3d0d..12abd78f24 100644 --- a/nym-wallet/src/pages/send/SendWizard.tsx +++ b/nym-wallet/src/pages/send/SendWizard.tsx @@ -138,9 +138,7 @@ export const SendWizard = () => { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', - borderTop: (theme) => `1px solid ${theme.palette.grey[200]}`, - bgcolor: 'grey.100', - p: 2, + p: 3, }} > {activeStep === 1 && ( @@ -155,6 +153,7 @@ export const SendWizard = () => { data-testid="button" onClick={activeStep === 0 ? handleNextStep : activeStep === 1 ? handleSend : handleFinish} disabled={!!(methods.formState.errors.amount || methods.formState.errors.to || isLoading)} + size="large" > {activeStep === 0 ? 'Next' : activeStep === 1 ? 'Send' : 'Finish'} diff --git a/nym-wallet/src/pages/settings/profile.tsx b/nym-wallet/src/pages/settings/profile.tsx index 228d63e8ad..e9e4c6b0fb 100644 --- a/nym-wallet/src/pages/settings/profile.tsx +++ b/nym-wallet/src/pages/settings/profile.tsx @@ -18,9 +18,7 @@ export const Profile = () => { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', - borderTop: (theme) => `1px solid ${theme.palette.grey[300]}`, - bgcolor: 'grey.200', - padding: 2, + padding: 3, }} > diff --git a/nym-wallet/src/pages/undelegate/UndelegateForm.tsx b/nym-wallet/src/pages/undelegate/UndelegateForm.tsx index aedb9884a9..6f1a7731ab 100644 --- a/nym-wallet/src/pages/undelegate/UndelegateForm.tsx +++ b/nym-wallet/src/pages/undelegate/UndelegateForm.tsx @@ -100,9 +100,7 @@ export const UndelegateForm = ({ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', - borderTop: (theme) => `1px solid ${theme.palette.grey[200]}`, - bgcolor: 'grey.100', - p: 2, + p: 3, }} > diff --git a/nym-wallet/src/pages/undelegate/index.tsx b/nym-wallet/src/pages/undelegate/index.tsx index 4e90d739f9..3e7d57558f 100644 --- a/nym-wallet/src/pages/undelegate/index.tsx +++ b/nym-wallet/src/pages/undelegate/index.tsx @@ -91,9 +91,7 @@ export const Undelegate = () => { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', - borderTop: '1px solid grey[200]', - bgcolor: 'grey.100', - p: 2, + p: 3, }} > diff --git a/nym-wallet/src/theme/theme.tsx b/nym-wallet/src/theme/theme.tsx index 02e4f92b9f..db13d2a16a 100644 --- a/nym-wallet/src/theme/theme.tsx +++ b/nym-wallet/src/theme/theme.tsx @@ -201,6 +201,7 @@ export const getDesignTokens = (mode: PaletteMode): ThemeOptions => { styleOverrides: { sizeLarge: { height: 55, + padding: '13px 24px', }, }, }, From 1cc06ef34921a0c745d2582dbf5ee46ec228f6e4 Mon Sep 17 00:00:00 2001 From: fmtabbara Date: Mon, 10 Jan 2022 17:31:43 +0000 Subject: [PATCH 11/26] update get_approx_fee to new function name _outdated_get_approx_fee --- nym-wallet/src/pages/internal-docs/ApiList.tsx | 2 +- nym-wallet/src/requests/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nym-wallet/src/pages/internal-docs/ApiList.tsx b/nym-wallet/src/pages/internal-docs/ApiList.tsx index d55197cff6..997b177a00 100644 --- a/nym-wallet/src/pages/internal-docs/ApiList.tsx +++ b/nym-wallet/src/pages/internal-docs/ApiList.tsx @@ -139,7 +139,7 @@ export const ApiList = () => { diff --git a/nym-wallet/src/requests/index.ts b/nym-wallet/src/requests/index.ts index f07d219540..c1630bc974 100644 --- a/nym-wallet/src/requests/index.ts +++ b/nym-wallet/src/requests/index.ts @@ -28,7 +28,7 @@ export const majorToMinor = async (amount: string): Promise => await invok // NOTE: this uses OUTDATED defaults that might have no resemblance with the reality // as for the actual transaction, the gas cost is being simulated beforehand export const getGasFee = async (operation: Operation): Promise => - await invoke('get_approximate_fee', { operation }) + await invoke('outdated_get_approximate_fee', { operation }) export const delegate = async ({ type, From 5c3c0ac39ed3c22809f5a0bcdb5195f3c273e36a Mon Sep 17 00:00:00 2001 From: fmtabbara Date: Mon, 10 Jan 2022 21:17:47 +0000 Subject: [PATCH 12/26] remove border lines and grey bg for card component --- nym-wallet/src/pages/bond/BondForm.tsx | 1 + nym-wallet/src/pages/bond/index.tsx | 5 ++--- nym-wallet/src/pages/delegate/DelegateForm.tsx | 3 ++- nym-wallet/src/pages/delegate/index.tsx | 3 ++- nym-wallet/src/pages/settings/system-variables.tsx | 3 ++- nym-wallet/src/pages/unbond/index.tsx | 1 + nym-wallet/src/pages/undelegate/index.tsx | 1 + 7 files changed, 11 insertions(+), 6 deletions(-) diff --git a/nym-wallet/src/pages/bond/BondForm.tsx b/nym-wallet/src/pages/bond/BondForm.tsx index 306239eeb8..a17ce3510f 100644 --- a/nym-wallet/src/pages/bond/BondForm.tsx +++ b/nym-wallet/src/pages/bond/BondForm.tsx @@ -376,6 +376,7 @@ export const BondForm = ({ alignItems: 'center', justifyContent: 'flex-end', padding: 3, + pt: 0, }} > )} diff --git a/nym-wallet/src/pages/settings/system-variables.tsx b/nym-wallet/src/pages/settings/system-variables.tsx index 4ffb412e7c..5c2514b870 100644 --- a/nym-wallet/src/pages/settings/system-variables.tsx +++ b/nym-wallet/src/pages/settings/system-variables.tsx @@ -66,8 +66,8 @@ export const SystemVariables = ({ return ( <> - - + + - - + } />} /> - + } />} /> - { const [isLoading, setIsLoading] = useState(false) @@ -20,7 +21,7 @@ export const Unbond = () => { return ( - + {ownership?.hasOwnership && ( - +