diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index 6c5337afaf..7723ab89a8 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -11,7 +11,7 @@ use nym_api_requests::ecash::models::{ }; use nym_api_requests::ecash::VerificationKeyResponse; use nym_api_requests::legacy::LegacyGatewayBondWithId; -use nym_api_requests::models::LegacyDescribedMixNode; +use nym_api_requests::models::{LegacyDescribedMixNode, NodePerformanceResponse}; pub use nym_api_requests::{ ecash::{ models::{ @@ -427,6 +427,14 @@ pub trait NymApiClientExt: ApiClient { .await } + async fn get_current_node_performance( + &self, + node_id: NodeId, + ) -> Result { + self.get_json_from(format!("/v1/nym-nodes/performance/{node_id}")) + .await + } + async fn get_mixnode_avg_uptime(&self, mix_id: NodeId) -> Result { self.get_json( &[ diff --git a/common/client-libs/validator-client/src/nym_api/routes.rs b/common/client-libs/validator-client/src/nym_api/routes.rs index dc87026701..e2a1540dda 100644 --- a/common/client-libs/validator-client/src/nym_api/routes.rs +++ b/common/client-libs/validator-client/src/nym_api/routes.rs @@ -38,6 +38,7 @@ pub mod ecash { pub const STATUS_ROUTES: &str = "status"; pub const MIXNODE: &str = "mixnode"; pub const GATEWAY: &str = "gateway"; +pub const NYM_NODES: &str = "nym-nodes"; pub const CORE_STATUS_COUNT: &str = "core-status-count"; pub const SINCE_ARG: &str = "since"; @@ -52,5 +53,6 @@ pub const STAKE_SATURATION: &str = "stake-saturation"; pub const INCLUSION_CHANCE: &str = "inclusion-probability"; pub const SUBMIT_GATEWAY: &str = "submit-gateway-monitoring-results"; pub const SUBMIT_NODE: &str = "submit-node-monitoring-results"; +pub const PERFORMANCE: &str = "performance"; pub const SERVICE_PROVIDERS: &str = "services"; diff --git a/nym-node/nym-node-requests/src/api/client.rs b/nym-node/nym-node-requests/src/api/client.rs index 6255a5644a..11ec66a4b9 100644 --- a/nym-node/nym-node-requests/src/api/client.rs +++ b/nym-node/nym-node-requests/src/api/client.rs @@ -2,7 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use crate::api::v1::gateway::models::WebSockets; -use crate::api::v1::node::models::{AuxiliaryDetails, NodeRoles, SignedHostInformation}; +use crate::api::v1::node::models::{ + AuxiliaryDetails, NodeDescription, NodeRoles, SignedHostInformation, +}; use crate::api::ErrorResponse; use crate::routes; use async_trait::async_trait; @@ -32,6 +34,11 @@ pub trait NymNodeApiClientExt: ApiClient { .await } + async fn get_description(&self) -> Result { + self.get_json_from(routes::api::v1::description_absolute()) + .await + } + async fn get_build_information( &self, ) -> Result { diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index ba8aff6fbe..9ca5e637a1 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -3335,6 +3335,7 @@ dependencies = [ name = "nym-node-requests" version = "0.1.0" dependencies = [ + "async-trait", "base64 0.22.1", "celes", "humantime 2.1.0", @@ -3342,6 +3343,7 @@ dependencies = [ "nym-bin-common", "nym-crypto", "nym-exit-policy", + "nym-http-api-client", "nym-wireguard-types", "schemars", "serde", @@ -3552,6 +3554,7 @@ dependencies = [ "nym-contracts-common", "nym-crypto", "nym-mixnet-contract-common", + "nym-node-requests", "nym-store-cipher", "nym-types", "nym-validator-client", diff --git a/nym-wallet/src-tauri/Cargo.toml b/nym-wallet/src-tauri/Cargo.toml index 8e995a784f..61eee10d85 100644 --- a/nym-wallet/src-tauri/Cargo.toml +++ b/nym-wallet/src-tauri/Cargo.toml @@ -52,6 +52,7 @@ zeroize = { version = "1.5", features = ["zeroize_derive", "serde"] } cosmwasm-std = "1.3.0" cosmrs = { git = "https://github.com/cosmos/cosmos-rust", rev = "4b1332e6d8258ac845cef71589c8d362a669675a" } +nym-node-requests = { path = "../../nym-node/nym-node-requests" } nym-validator-client = { path = "../../common/client-libs/validator-client" } nym-crypto = { path = "../../common/crypto", features = ["asymmetric"] } nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" } diff --git a/nym-wallet/src-tauri/src/error.rs b/nym-wallet/src-tauri/src/error.rs index cefac100e7..573e931b6b 100644 --- a/nym-wallet/src-tauri/src/error.rs +++ b/nym-wallet/src-tauri/src/error.rs @@ -1,5 +1,6 @@ use nym_contracts_common::signing::SigningAlgorithm; use nym_crypto::asymmetric::identity::Ed25519RecoveryError; +use nym_node_requests::api::client::NymNodeApiClientError; use nym_types::error::TypesError; use nym_validator_client::nym_api::error::NymAPIError; use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWalletError; @@ -12,17 +13,17 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum BackendError { - #[error("{source}")] + #[error(transparent)] TypesError { #[from] source: TypesError, }, - #[error("{source}")] + #[error(transparent)] Bip39Error { #[from] source: bip39::Error, }, - #[error("{source}")] + #[error(transparent)] TendermintError { #[from] source: cosmrs::rpc::Error, @@ -33,42 +34,47 @@ pub enum BackendError { #[source] source: NyxdError, }, - #[error("{source}")] + #[error(transparent)] CosmwasmStd { #[from] source: cosmwasm_std::StdError, }, - #[error("{source}")] + #[error(transparent)] ErrorReport { #[from] source: eyre::Report, }, - #[error("{source}")] + #[error(transparent)] NymApiError { #[from] source: NymAPIError, }, - #[error("{source}")] + #[error(transparent)] + NymNodeApiError { + #[from] + source: NymNodeApiClientError, + }, + #[error(transparent)] IOError { #[from] source: io::Error, }, - #[error("{source}")] + #[error(transparent)] SerdeJsonError { #[from] source: serde_json::Error, }, - #[error("{source}")] + #[error(transparent)] MalformedUrlProvided { #[from] source: url::ParseError, }, - #[error("{source}")] + #[error(transparent)] ReqwestError { #[from] source: reqwest::Error, }, - #[error("{source}")] + #[error(transparent)] K256Error { #[from] source: k256::ecdsa::Error, diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index 623ab63e45..a570bfb00b 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -73,6 +73,14 @@ fn main() { mixnet::bond::get_number_of_mixnode_delegators, mixnet::bond::get_mix_node_description, mixnet::bond::get_mixnode_avg_uptime, + mixnet::bond::bond_nymnode, + mixnet::bond::unbond_nymnode, + mixnet::bond::nym_node_bond_details, + mixnet::bond::get_nym_node_description, + mixnet::bond::migrate_legacy_mixnode, + mixnet::bond::migrate_legacy_gateway, + mixnet::bond::update_nymnode_config, + mixnet::bond::get_nymnode_performance, mixnet::delegate::delegate_to_mixnode, mixnet::delegate::get_pending_delegator_rewards, mixnet::delegate::get_pending_delegation_events, @@ -103,6 +111,7 @@ fn main() { state::save_config_to_files, utils::owns_gateway, utils::owns_mixnode, + utils::owns_nym_node, utils::get_env, utils::try_convert_pubkey_to_mix_id, utils::default_mixnode_cost_params, @@ -188,6 +197,7 @@ fn main() { signatures::ed25519_signing_payload::generate_mixnode_bonding_msg_payload, signatures::ed25519_signing_payload::vesting_generate_mixnode_bonding_msg_payload, signatures::ed25519_signing_payload::generate_gateway_bonding_msg_payload, + signatures::ed25519_signing_payload::generate_nym_node_bonding_msg_payload, signatures::ed25519_signing_payload::vesting_generate_gateway_bonding_msg_payload, help::log::help_log_toggle_window, app::window::create_main_window, diff --git a/nym-wallet/src-tauri/src/operations/helpers.rs b/nym-wallet/src-tauri/src/operations/helpers.rs index fc8212d48e..c2267ccb20 100644 --- a/nym-wallet/src-tauri/src/operations/helpers.rs +++ b/nym-wallet/src-tauri/src/operations/helpers.rs @@ -10,7 +10,8 @@ use nym_contracts_common::signing::{ use nym_crypto::asymmetric::identity; use nym_mixnet_contract_common::{ construct_legacy_mixnode_bonding_sign_payload, Gateway, GatewayBondingPayload, MixNode, - NodeCostParams, SignableGatewayBondingMsg, SignableLegacyMixNodeBondingMsg, + NodeCostParams, NymNode, NymNodeBondingPayload, SignableGatewayBondingMsg, + SignableLegacyMixNodeBondingMsg, SignableNymNodeBondingMsg, }; use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; use nym_validator_client::nyxd::error::NyxdError; @@ -143,6 +144,52 @@ pub(crate) async fn verify_gateway_bonding_sign_payload( + client: &P, + nym_node: NymNode, + cost_params: NodeCostParams, + pledge: Coin, +) -> Result { + let payload = NymNodeBondingPayload::new(nym_node, cost_params); + let sender = client.cw_address(); + let content = ContractMessageContent::new(sender, vec![pledge.into()], payload); + let nonce = client.get_signing_nonce().await?; + + Ok(SignableMessage::new(nonce, content)) +} + +pub(crate) async fn verify_nym_node_bonding_sign_payload( + client: &P, + nym_node: &NymNode, + cost_params: &NodeCostParams, + pledge: &Coin, + msg_signature: &MessageSignature, +) -> Result<(), BackendError> { + let identity_key = identity::PublicKey::from_base58_string(&nym_node.identity_key)?; + let signature = identity::Signature::from_bytes(msg_signature.as_ref())?; + + // recreate the plaintext + let msg = create_nym_node_bonding_sign_payload( + client, + nym_node.clone(), + cost_params.clone(), + pledge.clone(), + ) + .await?; + let plaintext = msg.to_plaintext()?; + + if !msg.algorithm.is_ed25519() { + return Err(BackendError::UnexpectedSigningAlgorithm { + received: msg.algorithm, + expected: SigningAlgorithm::Ed25519, + }); + } + + // TODO: possibly provide better error message if this check fails + identity_key.verify(plaintext, &signature)?; + Ok(()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs index a942321f9b..ba3d03b6ba 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs @@ -4,12 +4,18 @@ use crate::error::BackendError; use crate::operations::helpers::{ verify_gateway_bonding_sign_payload, verify_mixnode_bonding_sign_payload, + verify_nym_node_bonding_sign_payload, }; use crate::state::WalletState; use crate::{nyxd_client, Gateway, MixNode}; +use log::info; use nym_contracts_common::signing::MessageSignature; use nym_mixnet_contract_common::gateway::GatewayConfigUpdate; -use nym_mixnet_contract_common::{MixNodeConfigUpdate, NodeId}; +use nym_mixnet_contract_common::nym_node::NodeConfigUpdate; +use nym_mixnet_contract_common::{MixNodeConfigUpdate, NodeId, NymNode}; +use nym_node_requests::api::client::NymNodeApiClientExt; +use nym_node_requests::api::v1::node::models::NodeDescription; +use nym_node_requests::api::ErrorResponse; use nym_types::currency::DecCoin; use nym_types::gateway::GatewayBond; use nym_types::mixnode::{MixNodeDetails, NodeCostParams}; @@ -23,7 +29,7 @@ use std::cmp::Ordering; use std::time::Duration; #[derive(Debug, Serialize, Deserialize)] -pub struct NodeDescription { +pub struct LegacyNodeDescription { name: String, description: String, link: String, @@ -136,6 +142,54 @@ pub async fn bond_mixnode( )?) } +#[tauri::command] +pub async fn bond_nymnode( + nymnode: NymNode, + cost_params: NodeCostParams, + msg_signature: MessageSignature, + pledge: DecCoin, + fee: Option, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.write().await; + let reg = guard.registered_coins()?; + let pledge_base = guard.attempt_convert_to_base_coin(pledge.clone())?; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + let cost_params = cost_params.try_convert_to_mixnet_contract_cost_params(reg)?; + log::info!( + ">>> Bond NymNode: identity_key = {}, pledge_display = {}, pledge_base = {}, fee = {:?}", + nymnode.identity_key, + pledge, + pledge_base, + fee, + ); + + let client = guard.current_client()?; + // check the signature to make sure the user copied it correctly + if let Err(err) = verify_nym_node_bonding_sign_payload( + client, + &nymnode, + &cost_params, + &pledge_base, + &msg_signature, + ) + .await + { + log::warn!("failed to verify provided nymnode bonding signature: {err}"); + return Err(err); + } + + let res = client + .nyxd + .bond_nymnode(nymnode, cost_params, msg_signature, pledge_base, fee) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, fee_amount, + )?) +} + #[tauri::command] pub async fn update_pledge( current_pledge: DecCoin, @@ -255,6 +309,23 @@ pub async fn unbond_mixnode( )?) } +#[tauri::command] +pub async fn unbond_nymnode( + fee: Option, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.write().await; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + log::info!(">>> Unbond NymNode, fee = {fee:?}"); + let res = guard.current_client()?.nyxd.unbond_nymnode(fee).await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + + Ok(TransactionExecuteResult::from_execute_result( + res, fee_amount, + )?) +} + #[tauri::command] pub async fn update_mixnode_cost_params( new_costs: NodeCostParams, @@ -506,7 +577,7 @@ pub async fn get_number_of_mixnode_delegators( pub async fn get_mix_node_description( host: &str, port: u16, -) -> Result { +) -> Result { Ok(reqwest::Client::builder() .timeout(Duration::from_millis(1000)) .build()? @@ -517,6 +588,23 @@ pub async fn get_mix_node_description( .await?) } +#[tauri::command] +pub async fn get_nym_node_description( + host: &str, + port: u16, +) -> Result { + Ok( + nym_node_requests::api::Client::builder::<_, ErrorResponse>(format!( + "http://{host}:{port}" + ))? + .with_timeout(Duration::from_millis(1000)) + .with_user_agent(format!("nym-wallet/{}", env!("CARGO_PKG_VERSION"))) + .build::()? + .get_description() + .await?, + ) +} + #[tauri::command] pub async fn get_mixnode_uptime( mix_id: NodeId, @@ -531,3 +619,84 @@ pub async fn get_mixnode_uptime( log::info!(">>> Uptime response: {}", uptime.avg_uptime); Ok(uptime.avg_uptime) } + +#[tauri::command] +pub async fn migrate_legacy_mixnode( + fee: Option, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.write().await; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + + info!(">>> migrate to NymNode, fee = {fee:?}"); + let client = guard.current_client()?; + + let res = client.nyxd.migrate_legacy_mixnode(fee).await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, fee_amount, + )?) +} + +#[tauri::command] +pub async fn migrate_legacy_gateway( + fee: Option, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.write().await; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + + info!(">>> migrate to NymNode, fee = {fee:?}"); + let client = guard.current_client()?; + + let res = client.nyxd.migrate_legacy_gateway(None, fee).await?; + + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, fee_amount, + )?) +} + +#[tauri::command] +pub async fn update_nymnode_config( + update: NodeConfigUpdate, + fee: Option, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.write().await; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + log::info!(">>> update nym node config: update = {update:?}, fee {fee:?}",); + let res = guard + .current_client()? + .nyxd + .update_nymnode_config(update, fee) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, fee_amount, + )?) +} + +#[tauri::command] +pub async fn get_nymnode_performance( + state: tauri::State<'_, WalletState>, +) -> Result, BackendError> { + let Some(details) = nym_node_bond_details(state.clone()).await? else { + return Ok(None); + }; + let node_id = details.bond_information.node_id; + + log::trace!(" >>> Get node performance: node_id = {node_id}"); + let guard = state.read().await; + let res = guard + .current_client()? + .nym_api + .get_current_node_performance(node_id) + .await?; + log::trace!(" <<< {res:?}"); + + Ok(res.performance) +} diff --git a/nym-wallet/src-tauri/src/operations/signatures/ed25519_signing_payload.rs b/nym-wallet/src-tauri/src/operations/signatures/ed25519_signing_payload.rs index c0c3dd373a..53658067aa 100644 --- a/nym-wallet/src-tauri/src/operations/signatures/ed25519_signing_payload.rs +++ b/nym-wallet/src-tauri/src/operations/signatures/ed25519_signing_payload.rs @@ -4,9 +4,10 @@ use crate::error::BackendError; use crate::operations::helpers::{ create_gateway_bonding_sign_payload, create_mixnode_bonding_sign_payload, + create_nym_node_bonding_sign_payload, }; use crate::state::WalletState; -use nym_mixnet_contract_common::{Gateway, MixNode}; +use nym_mixnet_contract_common::{Gateway, MixNode, NymNode}; use nym_types::currency::DecCoin; use nym_types::mixnode::NodeCostParams; @@ -61,6 +62,29 @@ async fn gateway_bonding_msg_payload( Ok(msg.to_base58_string()?) } +async fn nym_node_bonding_msg_payload( + nym_node: NymNode, + cost_params: NodeCostParams, + pledge: DecCoin, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.read().await; + let reg = guard.registered_coins()?; + + let pledge_base = guard.attempt_convert_to_base_coin(pledge.clone())?; + let cost_params = cost_params.try_convert_to_mixnet_contract_cost_params(reg)?; + log::info!( + ">>> Bond nym_node bonding signature: identity_key = {}, pledge_display = {pledge}, pledge_base = {pledge_base}", + nym_node.identity_key, + ); + + let client = guard.current_client()?; + + let msg = + create_nym_node_bonding_sign_payload(client, nym_node, cost_params, pledge_base).await?; + Ok(msg.to_base58_string()?) +} + #[tauri::command] pub async fn generate_mixnode_bonding_msg_payload( mixnode: MixNode, @@ -90,6 +114,16 @@ pub async fn generate_gateway_bonding_msg_payload( gateway_bonding_msg_payload(gateway, pledge, false, state).await } +#[tauri::command] +pub async fn generate_nym_node_bonding_msg_payload( + nym_node: NymNode, + cost_params: NodeCostParams, + pledge: DecCoin, + state: tauri::State<'_, WalletState>, +) -> Result { + nym_node_bonding_msg_payload(nym_node, cost_params, pledge, state).await +} + #[tauri::command] pub async fn vesting_generate_gateway_bonding_msg_payload( gateway: Gateway, diff --git a/nym-wallet/src-tauri/src/operations/vesting/migrate.rs b/nym-wallet/src-tauri/src/operations/vesting/migrate.rs index 6d5fbadf16..00257464b1 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/migrate.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/migrate.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::BackendError; -use crate::nyxd_client; use crate::state::WalletState; use nym_mixnet_contract_common::ExecuteMsg; use nym_types::transaction::TransactionExecuteResult; @@ -20,7 +19,11 @@ pub async fn migrate_vested_mixnode( let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!(">>> migrate vested mixnode, fee = {fee:?}"); - let res = nyxd_client!(state).migrate_vested_mixnode(fee).await?; + let res = guard + .current_client()? + .nyxd + .migrate_vested_mixnode(fee) + .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); Ok(TransactionExecuteResult::from_execute_result( diff --git a/nym-wallet/src-tauri/src/utils.rs b/nym-wallet/src-tauri/src/utils.rs index f294d4c09e..9a90861059 100644 --- a/nym-wallet/src-tauri/src/utils.rs +++ b/nym-wallet/src-tauri/src/utils.rs @@ -48,7 +48,7 @@ pub async fn owns_gateway(state: tauri::State<'_, WalletState>) -> Result) -> Result { Ok(nyxd_client!(state) - .get_owned_nym_node(&nyxd_client!(state).address()) + .get_owned_nymnode(&nyxd_client!(state).address()) .await? .details .is_some())