diff --git a/CHANGELOG.md b/CHANGELOG.md index e395038495..b8334cd702 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,17 +9,27 @@ - wallet: added support for multiple accounts ([#1265]) - wallet: the wallet backend learned how to keep track of validator name, either hardcoded or by querying the status endpoint. - mixnet-contract: Replace all naked `-` with `saturating_sub`. +- mixnet-contract: Added ClaimOperatorReward and ClaimDelegatorReward messages ([#1292]) +- vesting-contract: Added ClaimOperatorReward and ClaimDelegatorReward messages ([#1292]) - validator-api: add Swagger to document the REST API ([#1249]). +- validator-api: add `estimated_node_profit` and `estimated_operator_cost` to `reward-estimate` endpoint ([#1284]) - all: added network compilation target to `--help` (or `--version`) commands ([#1256]). - network-requester: send traffic statistics from all network requesters and receive it in a special network-requester that aggregates the data and exposes it via a rest API ([#1267], [#1278]). ### Fixed +- mixnet-contract: replaced integer division with fixed for performance calculations ([#1284]) +- mixnet-contract: delegator and operator rewards use lambda and sigma instead of lambda_ticked and sigma_ticked ([#1284]) +- mixnet-contract: `estimated_delegator_reward` calculation ([#1284]) - vesting-contract: replaced `checked_sub` with `saturating_sub` to fix the underflow in `get_vesting_tokens` ([#1275]) - mixnet-contract: removed `expect` in `query_delegator_reward` and queries containing invalid proxy address should now return a more human-readable error ([#1257]) - mixnet-contract: Under certain circumstances nodes could not be unbonded ([#1255](https://github.com/nymtech/nym/issues/1255)) ([#1258]) - mixnode, gateway: attempting to determine reconnection backoff to persistently failing mixnode could result in a crash ([#1260]) +### Changed + +- validator-client: created internal `Coin` type that replaces coins from `cosmrs` and `cosmwasm` for API entrypoints [[#1295]] + [#1258]: https://github.com/nymtech/nym/pull/1258 [#1249]: https://github.com/nymtech/nym/pull/1249 [#1256]: https://github.com/nymtech/nym/pull/1256 @@ -29,6 +39,9 @@ [#1267]: https://github.com/nymtech/nym/pull/1267 [#1275]: https://github.com/nymtech/nym/pull/1275 [#1278]: https://github.com/nymtech/nym/pull/1278 +[#1284]: https://github.com/nymtech/nym/pull/1284 +[#1292]: https://github.com/nymtech/nym/pull/1292 +[#1295]: https://github.com/nymtech/nym/pull/1295 ## [nym-wallet-v1.0.4](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.4) (2022-05-04) diff --git a/clients/credential/src/client.rs b/clients/credential/src/client.rs index 678a4bdd1d..40f035c6b9 100644 --- a/clients/credential/src/client.rs +++ b/clients/credential/src/client.rs @@ -1,19 +1,15 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use bip39::Mnemonic; -use coconut_bandwidth_contract_common::deposit::DepositData; -use std::str::FromStr; -use url::Url; - use crate::error::Result; use crate::{CONTRACT_ADDRESS, MNEMONIC, NYMD_URL}; - +use bip39::Mnemonic; +use coconut_bandwidth_contract_common::deposit::DepositData; use coconut_bandwidth_contract_common::msg::ExecuteMsg; use network_defaults::DEFAULT_NETWORK; -use validator_client::nymd::{ - AccountId, CosmosCoin, Decimal, Denom, NymdClient, SigningNymdClient, -}; +use std::str::FromStr; +use url::Url; +use validator_client::nymd::{AccountId, Coin, Denom, NymdClient, SigningNymdClient}; pub(crate) struct Client { nymd_client: NymdClient, @@ -55,10 +51,7 @@ impl Client { let req = ExecuteMsg::DepositFunds { data: DepositData::new(info.to_string(), verification_key, encryption_key), }; - let funds = vec![CosmosCoin { - denom: self.denom.clone(), - amount: Decimal::from(amount), - }]; + let funds = vec![Coin::new(amount as u128, self.denom.to_string())]; Ok(self .nymd_client .execute(&self.contract_address, &req, Default::default(), "", funds) diff --git a/common/client-libs/validator-client/src/nymd/coin.rs b/common/client-libs/validator-client/src/nymd/coin.rs new file mode 100644 index 0000000000..77451032ac --- /dev/null +++ b/common/client-libs/validator-client/src/nymd/coin.rs @@ -0,0 +1,130 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 +use serde::{Deserialize, Serialize}; +use std::fmt; + +pub use cosmrs::Coin as CosmosCoin; +pub use cosmwasm_std::Coin as CosmWasmCoin; + +#[derive(Serialize, Deserialize, Clone, Copy, Default, Debug, PartialEq)] +pub struct MismatchedDenoms; + +// the reason the coin is created here as opposed to different place in the codebase is that +// eventually we want to either publish the cosmwasm client separately or commit it to +// some other project, like cosmrs. Either way, in that case we can't really have +// a dependency on an internal type +#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq)] +pub struct Coin { + pub amount: u128, + pub denom: String, +} + +impl Coin { + pub fn new>(amount: u128, denom: S) -> Self { + Coin { + amount, + denom: denom.into(), + } + } + + pub fn try_add(&self, other: &Self) -> Result { + if self.denom != other.denom { + Err(MismatchedDenoms) + } else { + Ok(Coin { + amount: self.amount + other.amount, + denom: self.denom.clone(), + }) + } + } +} + +impl fmt::Display for Coin { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}{}", self.amount, self.denom) + } +} + +impl From for CosmosCoin { + fn from(coin: Coin) -> Self { + assert!( + coin.amount <= u64::MAX as u128, + "the coin amount is higher than the maximum supported by cosmrs" + ); + + CosmosCoin { + denom: coin + .denom + .parse() + .expect("the coin should have had a valid denom!"), + amount: (coin.amount as u64).into(), + } + } +} + +impl From for Coin { + fn from(coin: CosmosCoin) -> Self { + Coin { + amount: coin + .amount + .to_string() + .parse() + .expect("somehow failed to parse string representation of u64"), + denom: coin.denom.to_string(), + } + } +} + +impl From for CosmWasmCoin { + fn from(coin: Coin) -> Self { + CosmWasmCoin::new(coin.amount, coin.denom) + } +} + +impl From for Coin { + fn from(coin: CosmWasmCoin) -> Self { + Coin { + amount: coin.amount.u128(), + denom: coin.denom, + } + } +} + +pub trait CoinConverter { + type Target; + + fn convert_coin(&self) -> Self::Target; +} + +impl CoinConverter for CosmosCoin { + type Target = CosmWasmCoin; + + fn convert_coin(&self) -> Self::Target { + CosmWasmCoin::new( + self.amount + .to_string() + .parse() + .expect("cosmos coin had an invalid amount assigned"), + self.denom.to_string(), + ) + } +} + +impl CoinConverter for CosmWasmCoin { + type Target = CosmosCoin; + + fn convert_coin(&self) -> Self::Target { + assert!( + self.amount.u128() <= u64::MAX as u128, + "the coin amount is higher than the maximum supported by cosmrs" + ); + + CosmosCoin { + denom: self + .denom + .parse() + .expect("cosmwasm coin had an invalid amount assigned"), + amount: (self.amount.u128() as u64).into(), + } + } +} diff --git a/common/client-libs/validator-client/src/nymd/cosmwasm_client/client.rs b/common/client-libs/validator-client/src/nymd/cosmwasm_client/client.rs index 594092bc86..08a3ce22a2 100644 --- a/common/client-libs/validator-client/src/nymd/cosmwasm_client/client.rs +++ b/common/client-libs/validator-client/src/nymd/cosmwasm_client/client.rs @@ -1,6 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::nymd::coin::Coin; use crate::nymd::cosmwasm_client::helpers::{create_pagination, next_page_key}; use crate::nymd::cosmwasm_client::types::{ Account, Code, CodeDetails, Contract, ContractCodeHistoryEntry, ContractCodeId, @@ -25,8 +26,7 @@ use cosmrs::rpc::{self, HttpClient, Order}; use cosmrs::tendermint::abci::Code as AbciCode; use cosmrs::tendermint::abci::Transaction; use cosmrs::tendermint::{abci, block, chain}; -use cosmrs::{tx, AccountId, Coin, Denom, Tx}; -use cosmwasm_std::Coin as CosmWasmCoin; +use cosmrs::{tx, AccountId, Coin as CosmosCoin, Denom, Tx}; use prost::Message; use serde::{Deserialize, Serialize}; use std::convert::{TryFrom, TryInto}; @@ -135,7 +135,7 @@ pub trait CosmWasmClient: rpc::Client { .await?; res.balance - .map(TryFrom::try_from) + .map(|proto| CosmosCoin::try_from(proto).map(Into::into)) .transpose() .map_err(|_| NymdError::SerializationError("Coin".to_owned())) } @@ -166,16 +166,12 @@ pub trait CosmWasmClient: rpc::Client { raw_balances .into_iter() - .map(TryFrom::try_from) + .map(|proto| CosmosCoin::try_from(proto).map(Into::into)) .collect::>() .map_err(|_| NymdError::SerializationError("Coins".to_owned())) } - // this is annoyingly and inconsistently returning `Vec` rather than - // Vec, since cosmrs::Coin can't deal with IBC denoms. - // Presumably after https://github.com/cosmos/cosmos-rust/issues/173 is resolved, - // the code could be adjusted - async fn get_total_supply(&self) -> Result, NymdError> { + async fn get_total_supply(&self) -> Result, NymdError> { let path = Some("/cosmos.bank.v1beta1.Query/TotalSupply".parse().unwrap()); let mut supply = Vec::new(); @@ -198,12 +194,7 @@ pub trait CosmWasmClient: rpc::Client { supply .into_iter() - .map(|coin| { - coin.amount.parse().map(|amount| CosmWasmCoin { - denom: coin.denom, - amount, - }) - }) + .map(|proto| CosmosCoin::try_from(proto).map(Into::into)) .collect::>() .map_err(|_| NymdError::SerializationError("Coins".to_owned())) } diff --git a/common/client-libs/validator-client/src/nymd/cosmwasm_client/signing_client.rs b/common/client-libs/validator-client/src/nymd/cosmwasm_client/signing_client.rs index 58e10c8655..1f690d6812 100644 --- a/common/client-libs/validator-client/src/nymd/cosmwasm_client/signing_client.rs +++ b/common/client-libs/validator-client/src/nymd/cosmwasm_client/signing_client.rs @@ -8,7 +8,7 @@ use crate::nymd::cosmwasm_client::types::*; use crate::nymd::error::NymdError; use crate::nymd::fee::{Fee, DEFAULT_SIMULATED_GAS_MULTIPLIER}; use crate::nymd::wallet::DirectSecp256k1HdWallet; -use crate::nymd::{GasPrice, TxResponse}; +use crate::nymd::{Coin, GasPrice, TxResponse}; use async_trait::async_trait; use cosmrs::bank::MsgSend; use cosmrs::distribution::MsgWithdrawDelegatorReward; @@ -17,7 +17,7 @@ use cosmrs::rpc::endpoint::broadcast; use cosmrs::rpc::{Error as TendermintRpcError, HttpClient, HttpClientUrl, SimpleRequest}; use cosmrs::staking::{MsgDelegate, MsgUndelegate}; use cosmrs::tx::{self, Msg, SignDoc, SignerInfo}; -use cosmrs::{cosmwasm, rpc, AccountId, Any, Coin, Tx}; +use cosmrs::{cosmwasm, rpc, AccountId, Any, Tx}; use log::debug; use serde::Serialize; use sha2::Digest; @@ -310,7 +310,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient { sender: sender_address.clone(), contract: contract_address.clone(), msg: serde_json::to_vec(msg)?, - funds, + funds: funds.into_iter().map(Into::into).collect(), } .to_any() .map_err(|_| NymdError::SerializationError("MsgExecuteContract".to_owned()))?; @@ -349,7 +349,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient { sender: sender_address.clone(), contract: contract_address.clone(), msg: serde_json::to_vec(&msg)?, - funds, + funds: funds.into_iter().map(Into::into).collect(), } .to_any() .map_err(|_| NymdError::SerializationError("MsgExecuteContract".to_owned())) @@ -382,7 +382,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient { let send_msg = MsgSend { from_address: sender_address.clone(), to_address: recipient_address.clone(), - amount, + amount: amount.into_iter().map(Into::into).collect(), } .to_any() .map_err(|_| NymdError::SerializationError("MsgSend".to_owned()))?; @@ -408,7 +408,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient { MsgSend { from_address: sender_address.clone(), to_address, - amount, + amount: amount.into_iter().map(Into::into).collect(), } .to_any() .map_err(|_| NymdError::SerializationError("MsgExecuteContract".to_owned())) @@ -431,7 +431,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient { let delegate_msg = MsgDelegate { delegator_address: delegator_address.to_owned(), validator_address: validator_address.to_owned(), - amount, + amount: amount.into(), } .to_any() .map_err(|_| NymdError::SerializationError("MsgDelegate".to_owned()))?; @@ -452,7 +452,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient { let undelegate_msg = MsgUndelegate { delegator_address: delegator_address.to_owned(), validator_address: validator_address.to_owned(), - amount, + amount: amount.into(), } .to_any() .map_err(|_| NymdError::SerializationError("MsgUndelegate".to_owned()))?; diff --git a/common/client-libs/validator-client/src/nymd/cosmwasm_client/types.rs b/common/client-libs/validator-client/src/nymd/cosmwasm_client/types.rs index 5d3994bd15..3c28d8991f 100644 --- a/common/client-libs/validator-client/src/nymd/cosmwasm_client/types.rs +++ b/common/client-libs/validator-client/src/nymd/cosmwasm_client/types.rs @@ -28,7 +28,7 @@ use cosmrs::proto::cosmwasm::wasm::v1::{ use cosmrs::tendermint::abci::Data; use cosmrs::tendermint::{abci, chain}; use cosmrs::tx::{AccountNumber, Gas, SequenceNumber}; -use cosmrs::{tx, AccountId, Any, Coin}; +use cosmrs::{tx, AccountId, Any, Coin as CosmosCoin}; use prost::Message; use serde::Serialize; use std::convert::{TryFrom, TryInto}; @@ -107,9 +107,9 @@ impl TryFrom for ModuleAccount { #[derive(Debug)] pub struct BaseVestingAccount { pub base_account: Option, - pub original_vesting: Vec, - pub delegated_free: Vec, - pub delegated_vesting: Vec, + pub original_vesting: Vec, + pub delegated_free: Vec, + pub delegated_vesting: Vec, pub end_time: i64, } @@ -184,7 +184,7 @@ impl TryFrom for DelayedVestingAccount { #[derive(Debug)] pub struct Period { pub length: i64, - pub amount: Vec, + pub amount: Vec, } impl TryFrom for Period { @@ -628,7 +628,7 @@ pub struct InstantiateOptions { /// created and before the instantiation message is executed by the contract. /// /// Only native tokens are supported. - pub funds: Vec, + pub funds: Vec, /// A bech32 encoded address of an admin account. /// Caution: an admin has the privilege to upgrade a contract. @@ -636,6 +636,15 @@ pub struct InstantiateOptions { pub admin: Option, } +impl InstantiateOptions { + pub fn new>(funds: Vec, admin: Option) -> Self { + InstantiateOptions { + funds: funds.into_iter().map(Into::into).collect(), + admin, + } + } +} + #[derive(Debug)] pub struct InstantiateResult { /// The address of the newly instantiated contract diff --git a/common/client-libs/validator-client/src/nymd/fee/gas_price.rs b/common/client-libs/validator-client/src/nymd/fee/gas_price.rs index e7e69a32a5..c79015ff22 100644 --- a/common/client-libs/validator-client/src/nymd/fee/gas_price.rs +++ b/common/client-libs/validator-client/src/nymd/fee/gas_price.rs @@ -4,7 +4,7 @@ use crate::nymd::error::NymdError; use config::defaults; use cosmrs::tx::Gas; -use cosmrs::{Coin, Denom}; +use cosmrs::Coin; use cosmwasm_std::{Decimal, Fraction, Uint128}; use std::ops::Mul; use std::str::FromStr; @@ -13,11 +13,12 @@ use std::str::FromStr; /// the smallest fee token unit, such as 0.012utoken. #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] pub struct GasPrice { - // I really hate the combination of cosmwasm Decimal with cosmos-sdk Denom, - // but cosmos-sdk's Decimal is too basic for our needs + // I really dislike the usage of cosmwasm Decimal, but I didn't feel like implementing + // our own maths subcrate just for the purposes of calculating gas requirements + // this should definitely be rectified later on pub amount: Decimal, - pub denom: Denom, + pub denom: String, } impl<'a> Mul for &'a GasPrice { @@ -44,7 +45,10 @@ impl<'a> Mul for &'a GasPrice { assert!(amount.u128() <= u64::MAX as u128); Coin { - denom: self.denom.clone(), + denom: self + .denom + .parse() + .expect("the gas price has been created with invalid denom"), amount: (amount.u128() as u64).into(), } } @@ -63,9 +67,7 @@ impl FromStr for GasPrice { .parse() .map_err(|_| NymdError::MalformedGasPrice)?; let possible_denom = s.chars().skip(amount_len).collect::(); - let denom = possible_denom - .parse() - .map_err(|_| NymdError::MalformedGasPrice)?; + let denom = possible_denom.trim().to_string(); Ok(GasPrice { amount, denom }) } @@ -106,7 +108,14 @@ mod tests { ); assert!(".25upunk".parse::().is_err()); - assert!("0.025 upunk".parse::().is_err()); + + assert_eq!( + "0.025upunk".parse::().unwrap(), + "0.025 upunk".parse().unwrap() + ); + + let gas: GasPrice = "0.025 upunk ".parse().unwrap(); + assert_eq!("upunk", gas.denom); } #[test] diff --git a/common/client-libs/validator-client/src/nymd/mod.rs b/common/client-libs/validator-client/src/nymd/mod.rs index 33591c41b8..536cc69d89 100644 --- a/common/client-libs/validator-client/src/nymd/mod.rs +++ b/common/client-libs/validator-client/src/nymd/mod.rs @@ -9,10 +9,11 @@ use crate::nymd::cosmwasm_client::types::{ use crate::nymd::error::NymdError; use crate::nymd::fee::DEFAULT_SIMULATED_GAS_MULTIPLIER; use crate::nymd::wallet::DirectSecp256k1HdWallet; +use cosmrs::cosmwasm; use cosmrs::rpc::Error as TendermintRpcError; use cosmrs::rpc::HttpClientUrl; use cosmrs::tx::Msg; -use cosmwasm_std::{Coin, Uint128}; +use cosmwasm_std::Uint128; pub use fee::gas_price::GasPrice; use mixnet_contract_common::mixnode::DelegationEvent; use mixnet_contract_common::{ @@ -28,8 +29,8 @@ use std::convert::TryInto; pub use crate::nymd::cosmwasm_client::client::CosmWasmClient; pub use crate::nymd::cosmwasm_client::signing_client::SigningCosmWasmClient; pub use crate::nymd::fee::Fee; +pub use coin::Coin; pub use cosmrs::bank::MsgSend; -use cosmrs::cosmwasm; pub use cosmrs::rpc::endpoint::tx::Response as TxResponse; pub use cosmrs::rpc::endpoint::validators::Response as ValidatorResponse; pub use cosmrs::rpc::HttpClient as QueryNymdClient; @@ -43,9 +44,11 @@ pub use cosmrs::tendermint::Time as TendermintTime; pub use cosmrs::tx::{self, Gas}; pub use cosmrs::Coin as CosmosCoin; pub use cosmrs::{bip32, AccountId, Decimal, Denom}; +pub use cosmwasm_std::Coin as CosmWasmCoin; pub use signing_client::Client as SigningNymdClient; pub use traits::{VestingQueryClient, VestingSigningClient}; +pub mod coin; pub mod cosmwasm_client; pub mod error; pub mod fee; @@ -175,7 +178,7 @@ impl NymdClient { &self, contract_address: &AccountId, msg: &M, - funds: Vec, + funds: Vec, ) -> Result where C: SigningCosmWasmClient, @@ -185,7 +188,7 @@ impl NymdClient { sender: self.address().clone(), contract: contract_address.clone(), msg: serde_json::to_vec(msg)?, - funds, + funds: funds.into_iter().map(Into::into).collect(), }) } @@ -275,7 +278,7 @@ impl NymdClient { &self, address: &AccountId, denom: Denom, - ) -> Result, NymdError> + ) -> Result, NymdError> where C: CosmWasmClient + Sync, { @@ -650,7 +653,7 @@ impl NymdClient { pub async fn send( &self, recipient: &AccountId, - amount: Vec, + amount: Vec, memo: impl Into + Send + 'static, fee: Option, ) -> Result @@ -666,7 +669,7 @@ impl NymdClient { /// Send funds from one address to multiple others pub async fn send_multiple( &self, - msgs: Vec<(AccountId, Vec)>, + msgs: Vec<(AccountId, Vec)>, memo: impl Into + Send + 'static, fee: Option, ) -> Result @@ -685,7 +688,7 @@ impl NymdClient { msg: &M, fee: Fee, memo: impl Into + Send + 'static, - funds: Vec, + funds: Vec, ) -> Result where C: SigningCosmWasmClient + Sync, @@ -705,7 +708,7 @@ impl NymdClient { ) -> Result where C: SigningCosmWasmClient + Sync, - I: IntoIterator)> + Send, + I: IntoIterator)> + Send, M: Serialize, { self.client @@ -796,6 +799,89 @@ impl NymdClient { .await } + pub async fn compound_operator_reward( + &self, + fee: Option, + ) -> Result + where + C: SigningCosmWasmClient + Sync, + { + let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); + let req = ExecuteMsg::CompoundOperatorReward {}; + self.client + .execute( + self.address(), + self.mixnet_contract_address()?, + &req, + fee, + "MixnetContract::CompoundOperatorReward", + vec![], + ) + .await + } + + pub async fn claim_operator_reward(&self, fee: Option) -> Result + where + C: SigningCosmWasmClient + Sync, + { + let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); + let req = ExecuteMsg::ClaimOperatorReward {}; + self.client + .execute( + self.address(), + self.mixnet_contract_address()?, + &req, + fee, + "MixnetContract::ClaimOperatorReward", + vec![], + ) + .await + } + + pub async fn compound_delegator_reward( + &self, + mix_identity: IdentityKey, + fee: Option, + ) -> Result + where + C: SigningCosmWasmClient + Sync, + { + let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); + let req = ExecuteMsg::CompoundDelegatorReward { mix_identity }; + self.client + .execute( + self.address(), + self.mixnet_contract_address()?, + &req, + fee, + "MixnetContract::CompoundDelegatorReward", + vec![], + ) + .await + } + + pub async fn claim_delegator_reward( + &self, + mix_identity: IdentityKey, + fee: Option, + ) -> Result + where + C: SigningCosmWasmClient + Sync, + { + let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); + let req = ExecuteMsg::ClaimDelegatorReward { mix_identity }; + self.client + .execute( + self.address(), + self.mixnet_contract_address()?, + &req, + fee, + "MixnetContract::ClaimDelegatorReward", + vec![], + ) + .await + } + /// Announce a mixnode, paying a fee. pub async fn bond_mixnode( &self, @@ -820,7 +906,7 @@ impl NymdClient { &req, fee, "Bonding mixnode from rust!", - vec![cosmwasm_coin_to_cosmos_coin(pledge)], + vec![pledge], ) .await } @@ -851,7 +937,7 @@ impl NymdClient { &req, fee, "Bonding mixnode on behalf from rust!", - vec![cosmwasm_coin_to_cosmos_coin(pledge)], + vec![pledge], ) .await } @@ -867,7 +953,7 @@ impl NymdClient { { let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - let reqs: Vec<(ExecuteMsg, Vec)> = mixnode_bonds_with_sigs + let reqs: Vec<(ExecuteMsg, Vec)> = mixnode_bonds_with_sigs .into_iter() .map(|(bond, owner_signature)| { ( @@ -876,7 +962,7 @@ impl NymdClient { owner: bond.owner.to_string(), owner_signature, }, - vec![cosmwasm_coin_to_cosmos_coin(bond.pledge_amount)], + vec![bond.pledge_amount.into()], ) }) .collect(); @@ -907,7 +993,7 @@ impl NymdClient { &req, fee, "Unbonding mixnode from rust!", - Vec::new(), + vec![], ) .await } @@ -931,7 +1017,7 @@ impl NymdClient { &req, fee, "Unbonding mixnode on behalf from rust!", - Vec::new(), + vec![], ) .await } @@ -957,7 +1043,7 @@ impl NymdClient { &req, fee, "Updating mixnode configuration from rust!", - Vec::new(), + vec![], ) .await } @@ -966,7 +1052,7 @@ impl NymdClient { pub async fn delegate_to_mixnode( &self, mix_identity: &str, - amount: &Coin, + amount: Coin, fee: Option, ) -> Result where @@ -984,7 +1070,7 @@ impl NymdClient { &req, fee, "Delegating to mixnode from rust!", - vec![cosmwasm_coin_ptr_to_cosmos_coin(amount)], + vec![amount], ) .await } @@ -995,7 +1081,7 @@ impl NymdClient { &self, mix_identity: &str, delegate: &str, - amount: &Coin, + amount: Coin, fee: Option, ) -> Result where @@ -1014,7 +1100,7 @@ impl NymdClient { &req, fee, "Delegating to mixnode on behalf from rust!", - vec![cosmwasm_coin_ptr_to_cosmos_coin(amount)], + vec![amount], ) .await } @@ -1030,7 +1116,7 @@ impl NymdClient { { let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - let reqs: Vec<(ExecuteMsg, Vec)> = mixnode_delegations + let reqs: Vec<(ExecuteMsg, Vec)> = mixnode_delegations .into_iter() .map(|delegation| { ( @@ -1038,7 +1124,7 @@ impl NymdClient { mix_identity: delegation.node_identity(), delegate: delegation.owner().to_string(), }, - vec![cosmwasm_coin_to_cosmos_coin(delegation.amount().clone())], + vec![delegation.amount().clone().into()], ) }) .collect(); @@ -1075,7 +1161,7 @@ impl NymdClient { &req, fee, "Removing mixnode delegation from rust!", - Vec::new(), + vec![], ) .await } @@ -1103,7 +1189,7 @@ impl NymdClient { &req, fee, "Removing mixnode delegation on behalf from rust!", - Vec::new(), + vec![], ) .await } @@ -1132,7 +1218,7 @@ impl NymdClient { &req, fee, "Bonding gateway from rust!", - vec![cosmwasm_coin_to_cosmos_coin(pledge)], + vec![pledge], ) .await } @@ -1163,7 +1249,7 @@ impl NymdClient { &req, fee, "Bonding gateway on behalf from rust!", - vec![cosmwasm_coin_to_cosmos_coin(pledge)], + vec![pledge], ) .await } @@ -1179,7 +1265,7 @@ impl NymdClient { { let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - let reqs: Vec<(ExecuteMsg, Vec)> = gateway_bonds_with_sigs + let reqs: Vec<(ExecuteMsg, Vec)> = gateway_bonds_with_sigs .into_iter() .map(|(bond, owner_signature)| { ( @@ -1188,7 +1274,7 @@ impl NymdClient { owner: bond.owner.to_string(), owner_signature, }, - vec![cosmwasm_coin_to_cosmos_coin(bond.pledge_amount)], + vec![bond.pledge_amount.into()], ) }) .collect(); @@ -1219,7 +1305,7 @@ impl NymdClient { &req, fee, "Unbonding gateway from rust!", - Vec::new(), + vec![], ) .await } @@ -1244,7 +1330,7 @@ impl NymdClient { &req, fee, "Unbonding gateway on behalf from rust!", - Vec::new(), + vec![], ) .await } @@ -1267,7 +1353,7 @@ impl NymdClient { &req, fee, "Updating contract state from rust!", - Vec::new(), + vec![], ) .await } @@ -1286,7 +1372,7 @@ impl NymdClient { &req, fee, "Advance current epoch", - Vec::new(), + vec![], ) .await } @@ -1305,7 +1391,7 @@ impl NymdClient { &req, fee, "Reconciling delegation events", - Vec::new(), + vec![], ) .await } @@ -1324,7 +1410,7 @@ impl NymdClient { &req, fee, "Snapshotting mixnodes", - Vec::new(), + vec![], ) .await } @@ -1351,24 +1437,8 @@ impl NymdClient { &req, fee, "Writing rewarded set", - Vec::new(), + vec![], ) .await } } - -fn cosmwasm_coin_to_cosmos_coin(coin: Coin) -> CosmosCoin { - CosmosCoin { - denom: coin.denom.parse().unwrap(), - // this might be a bit iffy, cosmwasm coin stores value as u128, while cosmos does it as u64 - amount: (coin.amount.u128() as u64).into(), - } -} - -fn cosmwasm_coin_ptr_to_cosmos_coin(coin: &Coin) -> CosmosCoin { - CosmosCoin { - denom: coin.denom.parse().unwrap(), - // this might be a bit iffy, cosmwasm coin stores value as u128, while cosmos does it as u64 - amount: (coin.amount.u128() as u64).into(), - } -} 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 4bdcb03101..2aef2294ac 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 @@ -1,11 +1,12 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::nymd::coin::Coin; pub use crate::nymd::cosmwasm_client::client::CosmWasmClient; use crate::nymd::error::NymdError; use crate::nymd::NymdClient; use async_trait::async_trait; -use cosmwasm_std::{Coin, Timestamp}; +use cosmwasm_std::{Coin as CosmWasmCoin, Timestamp}; use vesting_contract::vesting::Account; use vesting_contract_common::{ messages::QueryMsg as VestingQueryMsg, OriginalVestingResponse, Period, PledgeData, @@ -83,8 +84,9 @@ impl VestingQueryClient for NymdClient { block_time, }; self.client - .query_contract_smart(self.vesting_contract_address()?, &request) + .query_contract_smart::<_, CosmWasmCoin>(self.vesting_contract_address()?, &request) .await + .map(Into::into) } async fn spendable_coins( @@ -97,8 +99,9 @@ impl VestingQueryClient for NymdClient { block_time, }; self.client - .query_contract_smart(self.vesting_contract_address()?, &request) + .query_contract_smart::<_, CosmWasmCoin>(self.vesting_contract_address()?, &request) .await + .map(Into::into) } async fn vested_coins( &self, @@ -110,8 +113,9 @@ impl VestingQueryClient for NymdClient { block_time, }; self.client - .query_contract_smart(self.vesting_contract_address()?, &request) + .query_contract_smart::<_, CosmWasmCoin>(self.vesting_contract_address()?, &request) .await + .map(Into::into) } async fn vesting_coins( &self, @@ -123,8 +127,9 @@ impl VestingQueryClient for NymdClient { block_time, }; self.client - .query_contract_smart(self.vesting_contract_address()?, &request) + .query_contract_smart::<_, CosmWasmCoin>(self.vesting_contract_address()?, &request) .await + .map(Into::into) } async fn vesting_start_time( @@ -173,8 +178,9 @@ impl VestingQueryClient for NymdClient { block_time, }; self.client - .query_contract_smart(self.vesting_contract_address()?, &request) + .query_contract_smart::<_, CosmWasmCoin>(self.vesting_contract_address()?, &request) .await + .map(Into::into) } /// Returns the total amount of delegated tokens that have vested @@ -188,8 +194,9 @@ impl VestingQueryClient for NymdClient { block_time, }; self.client - .query_contract_smart(self.vesting_contract_address()?, &request) + .query_contract_smart::<_, CosmWasmCoin>(self.vesting_contract_address()?, &request) .await + .map(Into::into) } async fn get_account(&self, address: &str) -> Result { 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 073cfb7052..163b6957db 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 @@ -4,14 +4,35 @@ pub use crate::nymd::cosmwasm_client::signing_client::SigningCosmWasmClient; use crate::nymd::cosmwasm_client::types::ExecuteResult; use crate::nymd::error::NymdError; -use crate::nymd::{cosmwasm_coin_to_cosmos_coin, Fee, NymdClient}; +use crate::nymd::{Coin, Fee, NymdClient}; use async_trait::async_trait; -use cosmwasm_std::Coin; use mixnet_contract_common::{Gateway, IdentityKey, IdentityKeyRef, MixNode}; use vesting_contract_common::messages::{ExecuteMsg as VestingExecuteMsg, VestingSpecification}; #[async_trait] pub trait VestingSigningClient { + async fn vesting_claim_operator_reward( + &self, + fee: Option, + ) -> Result; + + async fn vesting_claim_delegator_reward( + &self, + mix_identity: IdentityKey, + fee: Option, + ) -> Result; + + async fn vesting_compound_operator_reward( + &self, + fee: Option, + ) -> Result; + + async fn vesting_compound_delegator_reward( + &self, + mix_identity: IdentityKey, + fee: Option, + ) -> Result; + async fn vesting_update_mixnode_config( &self, profix_margin_percent: u8, @@ -74,7 +95,7 @@ pub trait VestingSigningClient { async fn vesting_delegate_to_mixnode<'a>( &self, mix_identity: IdentityKeyRef<'a>, - amount: &Coin, + amount: Coin, fee: Option, ) -> Result; @@ -149,7 +170,7 @@ impl VestingSigningClient for NymdClient let req = VestingExecuteMsg::BondGateway { gateway, owner_signature: owner_signature.to_string(), - amount: pledge, + amount: pledge.into(), }; self.client .execute( @@ -187,7 +208,7 @@ impl VestingSigningClient for NymdClient let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); let req = VestingExecuteMsg::TrackUnbondGateway { owner: owner.to_string(), - amount, + amount: amount.into(), }; self.client .execute( @@ -212,7 +233,7 @@ impl VestingSigningClient for NymdClient let req = VestingExecuteMsg::BondMixnode { mix_node, owner_signature: owner_signature.to_string(), - amount: pledge, + amount: pledge.into(), }; self.client .execute( @@ -250,7 +271,7 @@ impl VestingSigningClient for NymdClient let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); let req = VestingExecuteMsg::TrackUnbondMixnode { owner: owner.to_string(), - amount, + amount: amount.into(), }; self.client .execute( @@ -269,7 +290,9 @@ impl VestingSigningClient for NymdClient fee: Option, ) -> Result { let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - let req = VestingExecuteMsg::WithdrawVestedCoins { amount }; + let req = VestingExecuteMsg::WithdrawVestedCoins { + amount: amount.into(), + }; self.client .execute( self.address(), @@ -292,7 +315,7 @@ impl VestingSigningClient for NymdClient let req = VestingExecuteMsg::TrackUndelegation { owner: address.to_string(), mix_identity, - amount, + amount: amount.into(), }; self.client .execute( @@ -308,13 +331,13 @@ impl VestingSigningClient for NymdClient async fn vesting_delegate_to_mixnode<'a>( &self, mix_identity: IdentityKeyRef<'a>, - amount: &Coin, + amount: Coin, fee: Option, ) -> Result { let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); let req = VestingExecuteMsg::DelegateToMixnode { mix_identity: mix_identity.into(), - amount: amount.clone(), + amount: amount.into(), }; self.client .execute( @@ -370,7 +393,81 @@ impl VestingSigningClient for NymdClient &req, fee, "VestingContract::CreatePeriodicVestingAccount", - vec![cosmwasm_coin_to_cosmos_coin(amount)], + vec![amount], + ) + .await + } + + async fn vesting_claim_operator_reward( + &self, + fee: Option, + ) -> Result { + let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); + let req = VestingExecuteMsg::ClaimOperatorReward {}; + self.client + .execute( + self.address(), + self.vesting_contract_address()?, + &req, + fee, + "VestingContract::ClaimOperatorReward", + vec![], + ) + .await + } + + async fn vesting_compound_operator_reward( + &self, + fee: Option, + ) -> Result { + let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); + let req = VestingExecuteMsg::CompoundOperatorReward {}; + self.client + .execute( + self.address(), + self.vesting_contract_address()?, + &req, + fee, + "VestingContract::CompoundOperatorReward", + vec![], + ) + .await + } + + async fn vesting_claim_delegator_reward( + &self, + mix_identity: IdentityKey, + fee: Option, + ) -> Result { + let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); + let req = VestingExecuteMsg::ClaimDelegatorReward { mix_identity }; + self.client + .execute( + self.address(), + self.vesting_contract_address()?, + &req, + fee, + "VestingContract::ClaimDelegatorReward", + vec![], + ) + .await + } + + async fn vesting_compound_delegator_reward( + &self, + mix_identity: IdentityKey, + fee: Option, + ) -> Result { + let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); + let req = VestingExecuteMsg::CompoundDelegatorReward { mix_identity }; + self.client + .execute( + self.address(), + self.vesting_contract_address()?, + &req, + fee, + "VestingContract::CompoundDelegatorReward", + vec![], ) .await } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs index 8464fc6d4c..a2ed8ffe35 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs @@ -23,7 +23,9 @@ pub const CHANGE_REWARDED_SET_EVENT_TYPE: &str = "change_rewarded_set"; pub const ADVANCE_INTERVAL_EVENT_TYPE: &str = "advance_interval"; pub const ADVANCE_EPOCH_EVENT_TYPE: &str = "advance_epoch"; pub const COMPOUND_DELEGATOR_REWARD_EVENT_TYPE: &str = "compound_delegator_reward"; +pub const CLAIM_DELEGATOR_REWARD_EVENT_TYPE: &str = "claim_delegator_reward"; pub const COMPOUND_OPERATOR_REWARD_EVENT_TYPE: &str = "compound_operator_reward"; +pub const CLAIM_OPERATOR_REWARD_EVENT_TYPE: &str = "claim_operator_reward"; pub const SNAPSHOT_MIXNODES_EVENT: &str = "snapshot_mixnodes"; // attributes that are used in multiple places @@ -151,6 +153,11 @@ pub fn new_compound_operator_reward_event(owner: &Addr, amount: Uint128) -> Even event.add_attribute(AMOUNT_KEY, amount.to_string()) } +pub fn new_claim_operator_reward_event(owner: &Addr, amount: Uint128) -> Event { + let event = Event::new(CLAIM_OPERATOR_REWARD_EVENT_TYPE).add_attribute(OWNER_KEY, owner); + event.add_attribute(AMOUNT_KEY, amount.to_string()) +} + pub fn new_compound_delegator_reward_event( delegator: &Addr, proxy: &Option, @@ -171,6 +178,26 @@ pub fn new_compound_delegator_reward_event( .add_attribute(DELEGATOR_KEY, delegator) } +pub fn new_claim_delegator_reward_event( + delegator: &Addr, + proxy: &Option, + amount: Uint128, + mix_identity: IdentityKeyRef<'_>, +) -> Event { + let mut event = + Event::new(CLAIM_DELEGATOR_REWARD_EVENT_TYPE).add_attribute(DELEGATOR_KEY, delegator); + + if let Some(proxy) = proxy { + event = event.add_attribute(PROXY_KEY, proxy) + } + + // coin implements Display trait and we use that implementation here + event + .add_attribute(AMOUNT_KEY, amount.to_string()) + .add_attribute(DELEGATION_TARGET_KEY, mix_identity) + .add_attribute(DELEGATOR_KEY, delegator) +} + pub fn new_undelegation_event( delegator: &Addr, proxy: &Option, diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs index 4b0bfcfbd7..92406b3c2a 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs @@ -310,6 +310,14 @@ impl NodeRewardResult { } } +pub struct RewardEstimate { + pub total_node_reward: u64, + pub operator_reward: u64, + pub delegators_reward: u64, + pub node_profit: u64, + pub operator_cost: u64, +} + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct MixNodeBond { pub pledge_amount: Coin, @@ -402,63 +410,80 @@ impl MixNodeBond { / U128::from_num(circulating_supply) } + pub fn lambda_ticked(&self, params: &RewardParams) -> U128 { + // Ratio of a bond to the token circulating supply + self.lambda(params).min(params.one_over_k()) + } + pub fn lambda(&self, params: &RewardParams) -> U128 { // Ratio of a bond to the token circulating supply - let pledge_to_circulating_supply_ratio = - self.pledge_to_circulating_supply(params.circulating_supply()); - pledge_to_circulating_supply_ratio.min(params.one_over_k()) + self.pledge_to_circulating_supply(params.circulating_supply()) + } + + pub fn sigma_ticked(&self, params: &RewardParams) -> U128 { + // Ratio of a delegation to the the token circulating supply + self.sigma(params).min(params.one_over_k()) } pub fn sigma(&self, params: &RewardParams) -> U128 { // Ratio of a delegation to the the token circulating supply - let total_bond_to_circulating_supply_ratio = - self.total_bond_to_circulating_supply(params.circulating_supply()); - total_bond_to_circulating_supply_ratio.min(params.one_over_k()) + self.total_bond_to_circulating_supply(params.circulating_supply()) } pub fn estimate_reward( &self, params: &RewardParams, - ) -> Result<(u64, u64, u64), MixnetContractError> { + ) -> Result { let total_node_reward = self .reward(params) .reward() .checked_to_num::() .unwrap_or_default(); + let node_profit = self + .node_profit(params) + .checked_to_num::() + .unwrap_or_default(); + let operator_cost = params + .node + .operator_cost() + .checked_to_num::() + .unwrap_or_default(); let operator_reward = self.operator_reward(params); // Total reward has to be the sum of operator and delegator rewards - let delegators_reward = total_node_reward - operator_reward; + let delegators_reward = node_profit - operator_reward; - Ok(( - total_node_reward.try_into()?, - operator_reward.try_into()?, - delegators_reward.try_into()?, - )) + Ok(RewardEstimate { + total_node_reward: total_node_reward.try_into()?, + operator_reward: operator_reward.try_into()?, + delegators_reward: delegators_reward.try_into()?, + node_profit: node_profit.try_into()?, + operator_cost: operator_cost.try_into()?, + }) } + // keybase://chat/nymtech#dev-core/14473 pub fn reward(&self, params: &RewardParams) -> NodeRewardResult { - let lambda = self.lambda(params); - let sigma = self.sigma(params); + let lambda_ticked = self.lambda_ticked(params); + let sigma_ticked = self.sigma_ticked(params); let reward = params.performance() * params.epoch_reward_pool() - * (sigma * params.omega() - + params.alpha() * lambda * sigma * params.rewarded_set_size()) + * (sigma_ticked * params.omega() + + params.alpha() * lambda_ticked * sigma_ticked * params.rewarded_set_size()) / (ONE + params.alpha()); + // we only need regular lambda and sigma to calculate operator and delegator rewards NodeRewardResult { reward, - lambda, - sigma, + lambda: self.lambda(params), + sigma: self.sigma(params), } } pub fn node_profit(&self, params: &RewardParams) -> U128 { - if self.reward(params).reward() < params.node.operator_cost() { - U128::from_num(0u128) - } else { - self.reward(params).reward() - params.node.operator_cost() - } + self.reward(params) + .reward() + .saturating_sub(params.node.operator_cost()) } pub fn operator_reward(&self, params: &RewardParams) -> u128 { @@ -466,11 +491,9 @@ impl MixNodeBond { if reward.sigma == 0 { return 0; } - let profit = if reward.reward < params.node.operator_cost() { - U128::from_num(0u128) - } else { - reward.reward - params.node.operator_cost() - }; + + let profit = reward.reward.saturating_sub(params.node.operator_cost()); + let operator_base_reward = reward.reward.min(params.node.operator_cost()); // Div by zero checked above let operator_reward = (self.profit_margin() diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index 8728df83d1..5c49215d9c 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -99,6 +99,17 @@ pub enum ExecuteMsg { }, // AdvanceCurrentInterval {}, AdvanceCurrentEpoch {}, + ClaimOperatorReward {}, + ClaimOperatorRewardOnBehalf { + owner: String, + }, + ClaimDelegatorReward { + mix_identity: IdentityKey, + }, + ClaimDelegatorRewardOnBehalf { + mix_identity: IdentityKey, + owner: String, + }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] 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 ec911e7e1d..b033e06c60 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs @@ -42,7 +42,7 @@ impl NodeEpochRewards { } pub fn operator_cost(&self) -> U128 { - U128::from_num(self.params.uptime.u128() / 100u128 * DEFAULT_OPERATOR_INTERVAL_COST as u128) + self.params.operator_cost() } pub fn node_profit(&self) -> U128 { @@ -178,11 +178,15 @@ impl NodeRewardParams { } pub fn operator_cost(&self) -> U128 { - U128::from_num(self.uptime.u128() / 100u128 * DEFAULT_OPERATOR_INTERVAL_COST as u128) + self.performance() * U128::from_num(DEFAULT_OPERATOR_INTERVAL_COST) } - pub fn uptime(&self) -> u128 { - self.uptime.u128() + pub fn uptime(&self) -> Uint128 { + self.uptime + } + + pub fn performance(&self) -> U128 { + U128::from_num(self.uptime.u128()) / U128::from_num(100) } pub fn set_reward_blockstamp(&mut self, blockstamp: u64) { @@ -233,7 +237,7 @@ impl RewardParams { } pub fn performance(&self) -> U128 { - U128::from_num(self.node.uptime.u128()) / U128::from_num(100) + self.node.performance() } pub fn set_reward_blockstamp(&mut self, blockstamp: u64) { @@ -256,8 +260,8 @@ impl RewardParams { self.node.reward_blockstamp } - pub fn uptime(&self) -> u128 { - self.node.uptime.u128() + pub fn uptime(&self) -> Uint128 { + self.node.uptime() } pub fn one_over_k(&self) -> U128 { diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs index 85eb009ab2..93ca6814dd 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs @@ -20,6 +20,7 @@ pub const VESTING_UPDATE_MIXNODE_CONFIG_EVENT_TYPE: &str = "vesting_update_mixno pub const TRACK_MIXNODE_UNBOND_EVENT_TYPE: &str = "track_mixnode_unbond"; pub const TRACK_GATEWAY_UNBOND_EVENT_TYPE: &str = "track_gateway_unbond"; pub const TRACK_UNDELEGATION_EVENT_TYPE: &str = "track_undelegation"; +pub const TRACK_REWARD_EVENT_TYPE: &str = "track_reaward"; // attributes that are used in multiple places pub const OWNER_KEY: &str = "owner"; @@ -136,3 +137,7 @@ pub fn new_track_gateway_unbond_event() -> Event { pub fn new_track_undelegation_event() -> Event { Event::new(TRACK_UNDELEGATION_EVENT_TYPE) } + +pub fn new_track_reward_event() -> Event { + Event::new(TRACK_REWARD_EVENT_TYPE) +} diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs index 4ed3114b4d..e763e164f3 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs @@ -49,6 +49,14 @@ impl VestingSpecification { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum ExecuteMsg { + TrackReward { + amount: Coin, + address: String, + }, + ClaimOperatorReward {}, + ClaimDelegatorReward { + mix_identity: String, + }, CompoundDelegatorReward { mix_identity: String, }, diff --git a/common/types/src/currency.rs b/common/types/src/currency.rs index 411548133b..51f57c254f 100644 --- a/common/types/src/currency.rs +++ b/common/types/src/currency.rs @@ -4,6 +4,8 @@ use cosmrs::Denom as CosmosDenom; use cosmwasm_std::Coin as CosmWasmCoin; use cosmwasm_std::{Decimal, Uint128}; use itertools::Itertools; +use schemars::gen::SchemaGenerator; +use schemars::schema::Schema; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; @@ -11,7 +13,7 @@ use std::fmt::{Display, Formatter}; use std::ops::Add; use std::str::FromStr; use strum::{Display, EnumString, EnumVariantNames}; -use validator_client::nymd::{CosmosCoin, GasPrice}; +use validator_client::nymd::{Coin, CosmosCoin, GasPrice}; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( @@ -77,540 +79,518 @@ pub struct MajorAmountString(String); // see https://github.com/Aleph-Alpha/ts-r feature = "generate-ts", ts(export_to = "ts-packages/types/src/types/rust/Currency.ts") )] -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +// #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct MajorCurrencyAmount { - pub amount: MajorAmountString, - pub denom: CurrencyDenom, + // temporary... + #[cfg_attr(feature = "generate-ts", ts(skip))] + pub coin: Coin, +} + +impl JsonSchema for MajorCurrencyAmount { + fn schema_name() -> String { + todo!() + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + todo!() + } } impl MajorCurrencyAmount { - pub fn new(amount: &str, denom: CurrencyDenom) -> MajorCurrencyAmount { - MajorCurrencyAmount { - amount: MajorAmountString(amount.to_string()), - denom, - } + // pub fn new(amount: &str, denom: CurrencyDenom) -> MajorCurrencyAmount { + // MajorCurrencyAmount { + // amount: MajorAmountString(amount.to_string()), + // denom, + // } + // } + // + // pub fn zero(denom: &CurrencyDenom) -> MajorCurrencyAmount { + // MajorCurrencyAmount::new("0", denom.clone()) + // } + // + // pub fn from_cosmrs_coin(coin: &CosmosCoin) -> Result { + // MajorCurrencyAmount::from_cosmrs_decimal_and_denom(coin.amount, coin.denom.to_string()) + // } + // + // pub fn from_minor_uint128_and_denom( + // amount_minor: Uint128, + // denom_minor: &str, + // ) -> Result { + // MajorCurrencyAmount::from_minor_decimal_and_denom( + // Decimal::from_atomics(amount_minor, 0)?, + // denom_minor, + // ) + // } + // + // pub fn from_minor_decimal_and_denom( + // amount_minor: Decimal, + // denom_minor: &str, + // ) -> Result { + // if !(denom_minor.starts_with('u') || denom_minor.starts_with('U')) { + // return Err(TypesError::InvalidDenom(denom_minor.to_string())); + // } + // let major = amount_minor / Uint128::from(1_000_000u64); + // if let Ok(denom) = CurrencyDenom::from_str(&denom_minor[1..].to_string()) { + // return Ok(MajorCurrencyAmount { + // amount: MajorAmountString(major.to_string()), + // denom, + // }); + // } + // Err(TypesError::InvalidDenom(denom_minor.to_string())) + // } + // pub fn from_decimal_and_denom( + // amount: Decimal, + // denom: String, + // ) -> Result { + // if denom.starts_with('u') || denom.starts_with('U') { + // return MajorCurrencyAmount::from_minor_decimal_and_denom(amount, &denom); + // } + // if let Ok(denom) = CurrencyDenom::from_str(denom.as_str()) { + // return Ok(MajorCurrencyAmount { + // amount: MajorAmountString(amount.to_string()), + // denom, + // }); + // } + // Err(TypesError::InvalidDenom(denom)) + // } + // pub fn from_cosmrs_decimal_and_denom( + // amount: CosmosDecimal, + // denom: String, + // ) -> Result { + // if denom.starts_with('u') || denom.starts_with('U') { + // return match Decimal::from_str(&amount.to_string()) { + // Ok(amount) => MajorCurrencyAmount::from_minor_decimal_and_denom(amount, &denom), + // Err(_e) => Err(TypesError::InvalidAmount(amount.to_string())), + // }; + // } + // + // if let Ok(denom) = CurrencyDenom::from_str(denom.as_str()) { + // return Ok(MajorCurrencyAmount { + // amount: MajorAmountString(amount.to_string()), + // denom, + // }); + // } + // Err(TypesError::InvalidDenom(denom)) + // } + + pub fn into_cosmos_coin(self) -> CosmosCoin { + self.coin.into() } - pub fn zero(denom: &CurrencyDenom) -> MajorCurrencyAmount { - MajorCurrencyAmount::new("0", denom.clone()) - } + // pub fn to_minor_uint128(&self) -> Result { + // if self.amount.0.contains('.') { + // // has a decimal point (Cosmos assumes "." is the decimal separator) + // let parts = self.amount.0.split('.'); + // let str = parts.collect_vec(); + // if str.is_empty() || str.len() > 2 { + // return Err(TypesError::InvalidAmount("Amount is invalid".to_string())); + // } + // if str.len() == 2 { + // // has a decimal, so check decimal places first + // if str[1].len() > 6 { + // return Err(TypesError::InvalidDenom( + // "Amount is invalid, only 6 decimal places of precision are allowed" + // .to_string(), + // )); + // } + // + // // so multiple whole part by 1e6 and add decimal part + // let whole_part = Uint128::from_str(str[0])? * Uint128::from(1_000_000u64); + // + // // TODO: has Rust got anything that deals with fixed point values, or parsing from format strings? Leading zeroes are causing issues + // return match format!("0.{}", str[1]).parse::() { + // Ok(decimal_part_float) => { + // // this makes an assumption that 6 decimal places of f64 can never lose precision + // let truncated = (decimal_part_float * 1_000_000.).trunc() as u32; + // let decimal_part = Uint128::from(truncated); + // let sum = whole_part + decimal_part; + // Ok(sum) + // } + // Err(_e) => Err(TypesError::InvalidAmount( + // "Amount decimal part is invalid".to_string(), + // )), + // }; + // } + // } + // + // let major = Uint128::from_str(&self.amount.0)?; + // let scaled = major * Uint128::new(1_000_000u128); + // Ok(scaled) + // } - pub fn from_cosmrs_coin(coin: &CosmosCoin) -> Result { - MajorCurrencyAmount::from_cosmrs_decimal_and_denom(coin.amount, coin.denom.to_string()) - } - - pub fn from_minor_uint128_and_denom( - amount_minor: Uint128, - denom_minor: &str, - ) -> Result { - MajorCurrencyAmount::from_minor_decimal_and_denom( - Decimal::from_atomics(amount_minor, 0)?, - denom_minor, - ) - } - - pub fn from_minor_decimal_and_denom( - amount_minor: Decimal, - denom_minor: &str, - ) -> Result { - if !(denom_minor.starts_with('u') || denom_minor.starts_with('U')) { - return Err(TypesError::InvalidDenom(denom_minor.to_string())); - } - let major = amount_minor / Uint128::from(1_000_000u64); - if let Ok(denom) = CurrencyDenom::from_str(&denom_minor[1..].to_string()) { - return Ok(MajorCurrencyAmount { - amount: MajorAmountString(major.to_string()), - denom, - }); - } - Err(TypesError::InvalidDenom(denom_minor.to_string())) - } - pub fn from_decimal_and_denom( - amount: Decimal, - denom: String, - ) -> Result { - if denom.starts_with('u') || denom.starts_with('U') { - return MajorCurrencyAmount::from_minor_decimal_and_denom(amount, &denom); - } - if let Ok(denom) = CurrencyDenom::from_str(denom.as_str()) { - return Ok(MajorCurrencyAmount { - amount: MajorAmountString(amount.to_string()), - denom, - }); - } - Err(TypesError::InvalidDenom(denom)) - } - pub fn from_cosmrs_decimal_and_denom( - amount: CosmosDecimal, - denom: String, - ) -> Result { - if denom.starts_with('u') || denom.starts_with('U') { - return match Decimal::from_str(&amount.to_string()) { - Ok(amount) => MajorCurrencyAmount::from_minor_decimal_and_denom(amount, &denom), - Err(_e) => Err(TypesError::InvalidAmount(amount.to_string())), - }; - } - - if let Ok(denom) = CurrencyDenom::from_str(denom.as_str()) { - return Ok(MajorCurrencyAmount { - amount: MajorAmountString(amount.to_string()), - denom, - }); - } - Err(TypesError::InvalidDenom(denom)) - } - - pub fn into_cosmos_coin(self) -> Result { - match CosmosDecimal::from_str(&self.amount.0) { - Ok(amount) => Ok(CosmosCoin { - amount, - denom: CosmosDenom::from_str(&self.denom.to_string().to_lowercase())?, - }), - Err(e) => Err(e.into()), - } - } - - pub fn to_minor_uint128(&self) -> Result { - if self.amount.0.contains('.') { - // has a decimal point (Cosmos assumes "." is the decimal separator) - let parts = self.amount.0.split('.'); - let str = parts.collect_vec(); - if str.is_empty() || str.len() > 2 { - return Err(TypesError::InvalidAmount("Amount is invalid".to_string())); - } - if str.len() == 2 { - // has a decimal, so check decimal places first - if str[1].len() > 6 { - return Err(TypesError::InvalidDenom( - "Amount is invalid, only 6 decimal places of precision are allowed" - .to_string(), - )); - } - - // so multiple whole part by 1e6 and add decimal part - let whole_part = Uint128::from_str(str[0])? * Uint128::from(1_000_000u64); - - // TODO: has Rust got anything that deals with fixed point values, or parsing from format strings? Leading zeroes are causing issues - return match format!("0.{}", str[1]).parse::() { - Ok(decimal_part_float) => { - // this makes an assumption that 6 decimal places of f64 can never lose precision - let truncated = (decimal_part_float * 1_000_000.).trunc() as u32; - let decimal_part = Uint128::from(truncated); - let sum = whole_part + decimal_part; - Ok(sum) - } - Err(_e) => Err(TypesError::InvalidAmount( - "Amount decimal part is invalid".to_string(), - )), - }; - } - } - - let major = Uint128::from_str(&self.amount.0)?; - let scaled = major * Uint128::new(1_000_000u128); - Ok(scaled) - } - - pub fn denom_to_string(&self) -> String { - self.denom.to_string() - } - - pub fn into_minor_cosmos_coin(self) -> Result { - let denom = format!("u{}", self.denom_to_string().to_lowercase()); - let minor = self.to_minor_uint128()?; - let amount = minor.to_string(); - Ok(CosmosCoin { - amount: CosmosDecimal::from_str(&amount)?, - denom: CosmosDenom::from_str(&denom)?, - }) - } - - pub fn into_cosmwasm_coin(self) -> Result { - Ok(CosmWasmCoin { - denom: self.denom.to_string().to_lowercase(), - amount: Uint128::try_from(self.amount.0.as_str())?, - }) - } - - pub fn into_minor_cosmwasm_coin(self) -> Result { - let denom = format!("u{}", self.denom_to_string().to_lowercase()); - let amount = self.to_minor_uint128()?; - let amount = Uint128::from_str(&amount.to_string())?; - Ok(CosmWasmCoin { denom, amount }) - } + // pub fn denom_to_string(&self) -> String { + // self.denom.to_string() + // } } impl Display for MajorCurrencyAmount { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{} {}", self.amount.0, self.denom) + write!(f, "{}", self.coin) } } -impl TryFrom for MajorCurrencyAmount { - type Error = TypesError; +// TODO: cleanup after merge - fn try_from(g: GasPrice) -> Result { - MajorCurrencyAmount::from_decimal_and_denom(g.amount, g.denom.to_string()) +impl From for MajorCurrencyAmount { + fn from(c: CosmosCoin) -> Self { + MajorCurrencyAmount { coin: c.into() } } } -impl TryFrom for MajorCurrencyAmount { - type Error = TypesError; - - fn try_from(c: CosmosCoin) -> Result { - MajorCurrencyAmount::from_cosmrs_decimal_and_denom(c.amount, c.denom.to_string()) +impl From for MajorCurrencyAmount { + fn from(c: CosmWasmCoin) -> Self { + MajorCurrencyAmount { coin: c.into() } } } -impl TryFrom for MajorCurrencyAmount { - type Error = TypesError; - - fn try_from(c: CosmWasmCoin) -> Result { - // note: there are always 0 decimals of precision because: - // - for major values, only whole values can be represented, e.g. 1 NYM, 1000 NYM - // - for minor values, again only whole values are represented, e.g. 1 UNYM, 1000 UNYM - match Decimal::from_atomics(c.amount, 0) { - Ok(amount) => Ok(MajorCurrencyAmount::from_decimal_and_denom( - amount, c.denom, - )?), - Err(_) => Err(TypesError::InvalidAmount(c.to_string())), - } +impl From for MajorCurrencyAmount { + fn from(coin: Coin) -> Self { + MajorCurrencyAmount { coin } } } -impl Add for MajorCurrencyAmount { - type Output = Self; - - fn add(self, rhs: Self) -> Self::Output { - // TODO: fix up error checking - let arg1 = Decimal::from_str(&self.amount.0).unwrap_or(Decimal::zero()); - let arg2 = Decimal::from_str(&rhs.amount.0).unwrap_or(Decimal::zero()); - MajorCurrencyAmount::from_decimal_and_denom(arg1 + arg2, self.denom_to_string()) - .unwrap_or_else(|_| MajorCurrencyAmount::zero(&self.denom)) +// temporary... +impl From for CosmosCoin { + fn from(c: MajorCurrencyAmount) -> CosmosCoin { + c.coin.into() } } -#[cfg(test)] -mod test { - use super::*; - use cosmrs::Coin as CosmosCoin; - use cosmrs::Decimal; - use cosmrs::Denom as CosmosDenom; - use cosmwasm_std::Coin as CosmWasmCoin; - use cosmwasm_std::Decimal as CosmWasmDecimal; - use serde_json::json; - use std::convert::TryFrom; - use std::str::FromStr; - use std::string::ToString; - - #[test] - fn json_to_major_currency_amount() { - let nym = json!({ - "amount": "1", - "denom": "NYM" - }); - let nymt = json!({ - "amount": "1", - "denom": "NYMT" - }); - - let test_nym_amount = MajorCurrencyAmount::new("1", CurrencyDenom::Nym); - let test_nymt_amount = MajorCurrencyAmount::new("1", CurrencyDenom::Nymt); - - let nym_amount = serde_json::from_value::(nym).unwrap(); - let nymt_amount = serde_json::from_value::(nymt).unwrap(); - - assert_eq!(nym_amount, test_nym_amount); - assert_eq!(nymt_amount, test_nymt_amount); - } - - #[test] - fn minor_amount_json_to_major_currency_amount() { - let one_micro_nym = json!({ - "amount": "0.000001", - "denom": "NYM" - }); - - let expected_nym_amount = MajorCurrencyAmount::new("0.000001", CurrencyDenom::Nym); - let actual_nym_amount = - serde_json::from_value::(one_micro_nym).unwrap(); - - assert_eq!(expected_nym_amount, actual_nym_amount); - } - - #[test] - fn denom_from_str() { - assert_eq!(CurrencyDenom::from_str("nym").unwrap(), CurrencyDenom::Nym); - assert_eq!( - CurrencyDenom::from_str("nymt").unwrap(), - CurrencyDenom::Nymt - ); - assert_eq!(CurrencyDenom::from_str("NYM").unwrap(), CurrencyDenom::Nym); - assert_eq!( - CurrencyDenom::from_str("NYMT").unwrap(), - CurrencyDenom::Nymt - ); - assert_eq!(CurrencyDenom::from_str("NyM").unwrap(), CurrencyDenom::Nym); - assert_eq!( - CurrencyDenom::from_str("NYmt").unwrap(), - CurrencyDenom::Nymt - ); - - assert!(matches!( - CurrencyDenom::from_str("foo").unwrap_err(), - strum::ParseError::VariantNotFound, - )); - - // denominations must all be major - assert!(matches!( - CurrencyDenom::from_str("unym").unwrap_err(), - strum::ParseError::VariantNotFound, - )); - assert!(matches!( - CurrencyDenom::from_str("unymt").unwrap_err(), - strum::ParseError::VariantNotFound, - )); - } - - #[test] - fn to_string() { - assert_eq!( - MajorCurrencyAmount::new("1", CurrencyDenom::Nym).to_string(), - "1 NYM" - ); - assert_eq!( - MajorCurrencyAmount::new("1", CurrencyDenom::Nymt).to_string(), - "1 NYMT" - ); - assert_eq!( - MajorCurrencyAmount::new("1000000000000", CurrencyDenom::Nym).to_string(), - "1000000000000 NYM" - ); - } - - #[test] - fn minor_cosmos_coin_to_major_currency() { - let cosmos_coin = CosmosCoin { - amount: CosmosDecimal::from(1u64), - denom: CosmosDenom::from_str("unym").unwrap(), - }; - let c = MajorCurrencyAmount::from_cosmrs_coin(&cosmos_coin).unwrap(); - assert_eq!(c, MajorCurrencyAmount::new("0.000001", CurrencyDenom::Nym)); - } - - #[test] - fn minor_cosmwasm_coin_to_major_currency() { - let coin = CosmWasmCoin { - amount: Uint128::from(1u64), - denom: "unym".to_string(), - }; - println!( - "from_atomics = {}", - CosmWasmDecimal::from_atomics(coin.amount.clone(), 6) - .unwrap() - .to_string() - ); - let c: MajorCurrencyAmount = coin.try_into().unwrap(); - assert_eq!(c, MajorCurrencyAmount::new("0.000001", CurrencyDenom::Nym)); - } - - #[test] - fn minor_cosmwasm_coin_to_major_currency_2() { - let coin = CosmWasmCoin { - amount: Uint128::from(1_000_000u64), - denom: "unym".to_string(), - }; - println!( - "from_atomics = {:?}", - CosmWasmDecimal::from_atomics(coin.amount.clone(), 6) - .unwrap() - .to_string() - ); - let c: MajorCurrencyAmount = coin.try_into().unwrap(); - assert_eq!(c, MajorCurrencyAmount::new("1", CurrencyDenom::Nym)); - } - - #[test] - fn major_cosmwasm_coin_to_major_currency() { - let coin = CosmWasmCoin { - amount: Uint128::from(1u64), - denom: "nym".to_string(), - }; - println!( - "from_atomics = {:?}", - CosmWasmDecimal::from_atomics(coin.amount.clone(), 6) - .unwrap() - .to_string() - ); - let c: MajorCurrencyAmount = coin.try_into().unwrap(); - assert_eq!(c, MajorCurrencyAmount::new("1", CurrencyDenom::Nym)); - } - - #[test] - fn major_currency_to_minor_cosmos_coin() { - let expected_cosmos_coin = CosmosCoin { - amount: CosmosDecimal::from(1u64), - denom: CosmosDenom::from_str("unym").unwrap(), - }; - let c = MajorCurrencyAmount::new("0.000001", CurrencyDenom::Nym); - let minor_cosmos_coin = c.into_minor_cosmos_coin().unwrap(); - assert_eq!(expected_cosmos_coin, minor_cosmos_coin); - assert_eq!("unym", minor_cosmos_coin.denom.to_string()); - } - - #[test] - fn major_currency_to_minor_cosmos_coin_2() { - let expected_cosmos_coin = CosmosCoin { - amount: CosmosDecimal::from(1000000u64), - denom: CosmosDenom::from_str("unym").unwrap(), - }; - let c = MajorCurrencyAmount::new("1", CurrencyDenom::Nym); - let minor_cosmos_coin = c.into_minor_cosmos_coin().unwrap(); - assert_eq!(expected_cosmos_coin, minor_cosmos_coin); - assert_eq!("unym", minor_cosmos_coin.denom.to_string()); - } - - #[test] - fn minor_cosmos_coin_to_major_currency_string() { - // check minor cosmos coin is converted to major value - let cosmos_coin = CosmosCoin { - amount: CosmosDecimal::from(1u64), - denom: CosmosDenom::from_str("unym").unwrap(), - }; - let c = MajorCurrencyAmount::from_cosmrs_coin(&cosmos_coin).unwrap(); - assert_eq!(c.to_string(), "0.000001 NYM"); - } - - #[test] - fn denom_to_string() { - let c = MajorCurrencyAmount::new("1", CurrencyDenom::Nym); - let denom = c.denom_to_string(); - assert_eq!(denom, "NYM".to_string()); - } - - #[test] - fn to_minor_one_unym() { - let c = MajorCurrencyAmount::new("1", CurrencyDenom::Nym); - let minor = c.to_minor_uint128().unwrap(); - assert_eq!("1000000", minor.to_string()); - } - - #[test] - fn to_minor() { - let amounts = vec![ - ("1000000", "1000000000000"), - ("1", "1000000"), - ("0.000001", "1"), - ]; - - for amount in amounts { - let c = MajorCurrencyAmount::new(amount.0, CurrencyDenom::Nym); - let minor = c.to_minor_uint128().unwrap(); - assert_eq!(amount.1, minor.to_string()); - } - } - - #[test] - fn to_minor_errors_expected() { - let bad_amounts = vec![ - "0.0000001", // because there are more than 6 decimals, it gets truncated - "0.0000009999999999999999999999999999999999", // would overflow - ]; - - for bad_amount in bad_amounts { - let c = MajorCurrencyAmount::new(bad_amount, CurrencyDenom::Nym); - assert!(matches!( - c.to_minor_uint128().unwrap_err(), - TypesError::InvalidDenom { .. } - )); - } - } - - fn amounts() -> Vec<&'static str> { - vec![ - "1", - "10", - "100", - "1000", - "10000", - "100000", - "10000000", - "100000000", - "1000000000", - "10000000000", - "100000000000", - "1000000000000", - "10000000000000", - "100000000000000", - "1000000000000000", - "10000000000000000", - "100000000000000000", - "1000000000000000000", - ] - } - - #[test] - fn major_currency_amount_into_cosmos_coin() { - for amount in amounts() { - let c = MajorCurrencyAmount::new(amount, CurrencyDenom::Nym); - let coin: CosmosCoin = c.into_cosmos_coin().unwrap(); - assert_eq!( - coin, - CosmosCoin { - amount: Decimal::from_str(amount).unwrap(), - denom: CosmosDenom::from_str("nym").unwrap() - } - ); - } - } - - #[test] - fn major_currency_amount_into_cosmwasm_coin() { - for amount in amounts() { - let c = MajorCurrencyAmount::new(amount, CurrencyDenom::Nym); - let coin: CosmWasmCoin = c.into_cosmwasm_coin().unwrap(); - assert_eq!( - coin, - CosmWasmCoin { - amount: Uint128::try_from(amount).unwrap(), - denom: "nym".to_string(), - } - ); - } - } - - #[test] - fn major_currency_amount_from_gas_price() { - assert_eq!( - MajorCurrencyAmount::try_from(GasPrice::from_str("42unym").unwrap()).unwrap(), - MajorCurrencyAmount { - amount: MajorAmountString("0.000042".to_string()), - denom: CurrencyDenom::Nym, - } - ); - - assert_eq!( - MajorCurrencyAmount::try_from(GasPrice::from_str("42nym").unwrap()).unwrap(), - MajorCurrencyAmount { - amount: MajorAmountString("42".to_string()), - denom: CurrencyDenom::Nym, - } - ); - - assert_eq!( - MajorCurrencyAmount::try_from(GasPrice::from_str("42unymt").unwrap()).unwrap(), - MajorCurrencyAmount { - amount: MajorAmountString("0.000042".to_string()), - denom: CurrencyDenom::Nymt, - } - ); - - assert_eq!( - MajorCurrencyAmount::try_from(GasPrice::from_str("42nymt").unwrap()).unwrap(), - MajorCurrencyAmount { - amount: MajorAmountString("42".to_string()), - denom: CurrencyDenom::Nymt, - } - ); +impl From for CosmWasmCoin { + fn from(c: MajorCurrencyAmount) -> CosmWasmCoin { + c.coin.into() } } + +impl From for Coin { + fn from(c: MajorCurrencyAmount) -> Coin { + c.coin + } +} + +// +// #[cfg(test)] +// mod test { +// use super::*; +// use cosmrs::Coin as CosmosCoin; +// use cosmrs::Decimal; +// use cosmrs::Denom as CosmosDenom; +// use cosmwasm_std::Coin as CosmWasmCoin; +// use cosmwasm_std::Decimal as CosmWasmDecimal; +// use serde_json::json; +// use std::convert::TryFrom; +// use std::str::FromStr; +// use std::string::ToString; +// +// #[test] +// fn json_to_major_currency_amount() { +// let nym = json!({ +// "amount": "1", +// "denom": "NYM" +// }); +// let nymt = json!({ +// "amount": "1", +// "denom": "NYMT" +// }); +// +// let test_nym_amount = MajorCurrencyAmount::new("1", CurrencyDenom::Nym); +// let test_nymt_amount = MajorCurrencyAmount::new("1", CurrencyDenom::Nymt); +// +// let nym_amount = serde_json::from_value::(nym).unwrap(); +// let nymt_amount = serde_json::from_value::(nymt).unwrap(); +// +// assert_eq!(nym_amount, test_nym_amount); +// assert_eq!(nymt_amount, test_nymt_amount); +// } +// +// #[test] +// fn minor_amount_json_to_major_currency_amount() { +// let one_micro_nym = json!({ +// "amount": "0.000001", +// "denom": "NYM" +// }); +// +// let expected_nym_amount = MajorCurrencyAmount::new("0.000001", CurrencyDenom::Nym); +// let actual_nym_amount = +// serde_json::from_value::(one_micro_nym).unwrap(); +// +// assert_eq!(expected_nym_amount, actual_nym_amount); +// } +// +// #[test] +// fn denom_from_str() { +// assert_eq!(CurrencyDenom::from_str("nym").unwrap(), CurrencyDenom::Nym); +// assert_eq!( +// CurrencyDenom::from_str("nymt").unwrap(), +// CurrencyDenom::Nymt +// ); +// assert_eq!(CurrencyDenom::from_str("NYM").unwrap(), CurrencyDenom::Nym); +// assert_eq!( +// CurrencyDenom::from_str("NYMT").unwrap(), +// CurrencyDenom::Nymt +// ); +// assert_eq!(CurrencyDenom::from_str("NyM").unwrap(), CurrencyDenom::Nym); +// assert_eq!( +// CurrencyDenom::from_str("NYmt").unwrap(), +// CurrencyDenom::Nymt +// ); +// +// assert!(matches!( +// CurrencyDenom::from_str("foo").unwrap_err(), +// strum::ParseError::VariantNotFound, +// )); +// +// // denominations must all be major +// assert!(matches!( +// CurrencyDenom::from_str("unym").unwrap_err(), +// strum::ParseError::VariantNotFound, +// )); +// assert!(matches!( +// CurrencyDenom::from_str("unymt").unwrap_err(), +// strum::ParseError::VariantNotFound, +// )); +// } +// +// #[test] +// fn to_string() { +// assert_eq!( +// MajorCurrencyAmount::new("1", CurrencyDenom::Nym).to_string(), +// "1 NYM" +// ); +// assert_eq!( +// MajorCurrencyAmount::new("1", CurrencyDenom::Nymt).to_string(), +// "1 NYMT" +// ); +// assert_eq!( +// MajorCurrencyAmount::new("1000000000000", CurrencyDenom::Nym).to_string(), +// "1000000000000 NYM" +// ); +// } +// +// #[test] +// fn minor_cosmos_coin_to_major_currency() { +// let cosmos_coin = CosmosCoin { +// amount: CosmosDecimal::from(1u64), +// denom: CosmosDenom::from_str("unym").unwrap(), +// }; +// let c = MajorCurrencyAmount::from_cosmrs_coin(&cosmos_coin).unwrap(); +// assert_eq!(c, MajorCurrencyAmount::new("0.000001", CurrencyDenom::Nym)); +// } +// +// #[test] +// fn minor_cosmwasm_coin_to_major_currency() { +// let coin = CosmWasmCoin { +// amount: Uint128::from(1u64), +// denom: "unym".to_string(), +// }; +// println!( +// "from_atomics = {}", +// CosmWasmDecimal::from_atomics(coin.amount.clone(), 6) +// .unwrap() +// .to_string() +// ); +// let c: MajorCurrencyAmount = coin.try_into().unwrap(); +// assert_eq!(c, MajorCurrencyAmount::new("0.000001", CurrencyDenom::Nym)); +// } +// +// #[test] +// fn minor_cosmwasm_coin_to_major_currency_2() { +// let coin = CosmWasmCoin { +// amount: Uint128::from(1_000_000u64), +// denom: "unym".to_string(), +// }; +// println!( +// "from_atomics = {:?}", +// CosmWasmDecimal::from_atomics(coin.amount.clone(), 6) +// .unwrap() +// .to_string() +// ); +// let c: MajorCurrencyAmount = coin.try_into().unwrap(); +// assert_eq!(c, MajorCurrencyAmount::new("1", CurrencyDenom::Nym)); +// } +// +// #[test] +// fn major_cosmwasm_coin_to_major_currency() { +// let coin = CosmWasmCoin { +// amount: Uint128::from(1u64), +// denom: "nym".to_string(), +// }; +// println!( +// "from_atomics = {:?}", +// CosmWasmDecimal::from_atomics(coin.amount.clone(), 6) +// .unwrap() +// .to_string() +// ); +// let c: MajorCurrencyAmount = coin.try_into().unwrap(); +// assert_eq!(c, MajorCurrencyAmount::new("1", CurrencyDenom::Nym)); +// } +// +// #[test] +// fn major_currency_to_minor_cosmos_coin() { +// let expected_cosmos_coin = CosmosCoin { +// amount: CosmosDecimal::from(1u64), +// denom: CosmosDenom::from_str("unym").unwrap(), +// }; +// let c = MajorCurrencyAmount::new("0.000001", CurrencyDenom::Nym); +// let minor_cosmos_coin = c.into_minor_cosmos_coin().unwrap(); +// assert_eq!(expected_cosmos_coin, minor_cosmos_coin); +// assert_eq!("unym", minor_cosmos_coin.denom.to_string()); +// } +// +// #[test] +// fn major_currency_to_minor_cosmos_coin_2() { +// let expected_cosmos_coin = CosmosCoin { +// amount: CosmosDecimal::from(1000000u64), +// denom: CosmosDenom::from_str("unym").unwrap(), +// }; +// let c = MajorCurrencyAmount::new("1", CurrencyDenom::Nym); +// let minor_cosmos_coin = c.into_minor_cosmos_coin().unwrap(); +// assert_eq!(expected_cosmos_coin, minor_cosmos_coin); +// assert_eq!("unym", minor_cosmos_coin.denom.to_string()); +// } +// +// #[test] +// fn minor_cosmos_coin_to_major_currency_string() { +// // check minor cosmos coin is converted to major value +// let cosmos_coin = CosmosCoin { +// amount: CosmosDecimal::from(1u64), +// denom: CosmosDenom::from_str("unym").unwrap(), +// }; +// let c = MajorCurrencyAmount::from_cosmrs_coin(&cosmos_coin).unwrap(); +// assert_eq!(c.to_string(), "0.000001 NYM"); +// } +// +// #[test] +// fn denom_to_string() { +// let c = MajorCurrencyAmount::new("1", CurrencyDenom::Nym); +// let denom = c.denom_to_string(); +// assert_eq!(denom, "NYM".to_string()); +// } +// +// #[test] +// fn to_minor_one_unym() { +// let c = MajorCurrencyAmount::new("1", CurrencyDenom::Nym); +// let minor = c.to_minor_uint128().unwrap(); +// assert_eq!("1000000", minor.to_string()); +// } +// +// #[test] +// fn to_minor() { +// let amounts = vec![ +// ("1000000", "1000000000000"), +// ("1", "1000000"), +// ("0.000001", "1"), +// ]; +// +// for amount in amounts { +// let c = MajorCurrencyAmount::new(amount.0, CurrencyDenom::Nym); +// let minor = c.to_minor_uint128().unwrap(); +// assert_eq!(amount.1, minor.to_string()); +// } +// } +// +// #[test] +// fn to_minor_errors_expected() { +// let bad_amounts = vec![ +// "0.0000001", // because there are more than 6 decimals, it gets truncated +// "0.0000009999999999999999999999999999999999", // would overflow +// ]; +// +// for bad_amount in bad_amounts { +// let c = MajorCurrencyAmount::new(bad_amount, CurrencyDenom::Nym); +// assert!(matches!( +// c.to_minor_uint128().unwrap_err(), +// TypesError::InvalidDenom { .. } +// )); +// } +// } +// +// fn amounts() -> Vec<&'static str> { +// vec![ +// "1", +// "10", +// "100", +// "1000", +// "10000", +// "100000", +// "10000000", +// "100000000", +// "1000000000", +// "10000000000", +// "100000000000", +// "1000000000000", +// "10000000000000", +// "100000000000000", +// "1000000000000000", +// "10000000000000000", +// "100000000000000000", +// "1000000000000000000", +// ] +// } +// +// #[test] +// fn major_currency_amount_into_cosmos_coin() { +// for amount in amounts() { +// let c = MajorCurrencyAmount::new(amount, CurrencyDenom::Nym); +// let coin: CosmosCoin = c.into_cosmos_coin().unwrap(); +// assert_eq!( +// coin, +// CosmosCoin { +// amount: Decimal::from_str(amount).unwrap(), +// denom: CosmosDenom::from_str("nym").unwrap() +// } +// ); +// } +// } +// +// #[test] +// fn major_currency_amount_into_cosmwasm_coin() { +// for amount in amounts() { +// let c = MajorCurrencyAmount::new(amount, CurrencyDenom::Nym); +// let coin: CosmWasmCoin = c.into_cosmwasm_coin().unwrap(); +// assert_eq!( +// coin, +// CosmWasmCoin { +// amount: Uint128::try_from(amount).unwrap(), +// denom: "nym".to_string(), +// } +// ); +// } +// } +// +// #[test] +// fn major_currency_amount_from_gas_price() { +// assert_eq!( +// MajorCurrencyAmount::try_from(GasPrice::from_str("42unym").unwrap()).unwrap(), +// MajorCurrencyAmount { +// amount: MajorAmountString("0.000042".to_string()), +// denom: CurrencyDenom::Nym, +// } +// ); +// +// assert_eq!( +// MajorCurrencyAmount::try_from(GasPrice::from_str("42nym").unwrap()).unwrap(), +// MajorCurrencyAmount { +// amount: MajorAmountString("42".to_string()), +// denom: CurrencyDenom::Nym, +// } +// ); +// +// assert_eq!( +// MajorCurrencyAmount::try_from(GasPrice::from_str("42unymt").unwrap()).unwrap(), +// MajorCurrencyAmount { +// amount: MajorAmountString("0.000042".to_string()), +// denom: CurrencyDenom::Nymt, +// } +// ); +// +// assert_eq!( +// MajorCurrencyAmount::try_from(GasPrice::from_str("42nymt").unwrap()).unwrap(), +// MajorCurrencyAmount { +// amount: MajorAmountString("42".to_string()), +// denom: CurrencyDenom::Nymt, +// } +// ); +// } +// } diff --git a/common/types/src/delegation.rs b/common/types/src/delegation.rs index 6468ee1574..8b273529dd 100644 --- a/common/types/src/delegation.rs +++ b/common/types/src/delegation.rs @@ -35,7 +35,7 @@ impl TryFrom for Delegation { proxy, } = value; - let amount: MajorCurrencyAmount = amount.try_into()?; + let amount: MajorCurrencyAmount = amount.into(); Ok(Delegation { owner: owner.into_string(), @@ -112,7 +112,7 @@ impl TryFrom for DelegationResult { type Error = TypesError; fn try_from(delegation: MixnetContractDelegation) -> Result { - let amount: MajorCurrencyAmount = delegation.amount.clone().try_into()?; + let amount: MajorCurrencyAmount = delegation.amount.clone().into(); Ok(DelegationResult { source_address: delegation.owner().to_string(), target_address: delegation.node_identity(), @@ -152,7 +152,7 @@ impl TryFrom for DelegationEvent { fn try_from(event: ContractDelegationEvent) -> Result { match event { ContractDelegationEvent::Delegate(delegation) => { - let amount: MajorCurrencyAmount = delegation.amount.try_into()?; + let amount: MajorCurrencyAmount = delegation.amount.into(); Ok(DelegationEvent { kind: DelegationEventKind::Delegate, block_height: delegation.block_height, diff --git a/common/types/src/gas.rs b/common/types/src/gas.rs index cd4e7c8d29..49a9756714 100644 --- a/common/types/src/gas.rs +++ b/common/types/src/gas.rs @@ -26,35 +26,39 @@ pub struct Gas { impl Gas { pub fn from_cosmrs_gas(value: CosmrsGas, denom_minor: &str) -> Result { - // TODO: use simulator struct to do conversion to fee - let value_u128 = Uint128::from(value.value()); - let amount = Decimal::new(value_u128) * Decimal::from_str("0.0025")?; - Ok(Gas { - gas_units: value.value(), - amount: MajorCurrencyAmount::from_minor_decimal_and_denom(amount, denom_minor)?, - }) + todo!() + + // // TODO: use simulator struct to do conversion to fee + // let value_u128 = Uint128::from(value.value()); + // let amount = Decimal::new(value_u128) * Decimal::from_str("0.0025")?; + // Ok(Gas { + // gas_units: value.value(), + // amount: MajorCurrencyAmount::from_minor_decimal_and_denom(amount, denom_minor)?, + // }) } pub fn from_u64(value: u64, denom_minor: &str) -> Result { - // TODO: use simulator struct to do conversion to fee - let value_u128 = Uint128::from(value); - let amount = Decimal::new(value_u128) * Decimal::from_str("0.0025")?; - Ok(Gas { - gas_units: value, - amount: MajorCurrencyAmount::from_minor_decimal_and_denom(amount, denom_minor)?, - }) + todo!() + // // TODO: use simulator struct to do conversion to fee + // let value_u128 = Uint128::from(value); + // let amount = Decimal::new(value_u128) * Decimal::from_str("0.0025")?; + // Ok(Gas { + // gas_units: value, + // amount: MajorCurrencyAmount::from_minor_decimal_and_denom(amount, denom_minor)?, + // }) } pub fn from_gas_price(value: ValidatorClientGasPrice) -> Result { - // TODO: use simulator struct to do conversion to fee - let gas_units_str = (value.amount / Uint128::from_str("0.0025")?).to_string(); - let decimal_seperator_pos = gas_units_str.find('.').unwrap_or(gas_units_str.len()); - let gas_units = gas_units_str[..decimal_seperator_pos] - .parse() - .unwrap_or(0_u64); - let ValidatorClientGasPrice { amount, denom } = value; - Ok(Gas { - gas_units, - amount: MajorCurrencyAmount::from_minor_decimal_and_denom(amount, denom.as_ref())?, - }) + todo!() + // // TODO: use simulator struct to do conversion to fee + // let gas_units_str = (value.amount / Uint128::from_str("0.0025")?).to_string(); + // let decimal_seperator_pos = gas_units_str.find('.').unwrap_or(gas_units_str.len()); + // let gas_units = gas_units_str[..decimal_seperator_pos] + // .parse() + // .unwrap_or(0_u64); + // let ValidatorClientGasPrice { amount, denom } = value; + // Ok(Gas { + // gas_units, + // amount: MajorCurrencyAmount::from_minor_decimal_and_denom(amount, denom.as_ref())?, + // }) } } diff --git a/common/types/src/gateway.rs b/common/types/src/gateway.rs index 4860fb6cd8..d45d0f8274 100644 --- a/common/types/src/gateway.rs +++ b/common/types/src/gateway.rs @@ -87,7 +87,7 @@ impl TryFrom for GatewayBond { proxy, } = value; - let pledge_amount: MajorCurrencyAmount = pledge_amount.try_into()?; + let pledge_amount: MajorCurrencyAmount = pledge_amount.into(); Ok(GatewayBond { pledge_amount, diff --git a/common/types/src/mixnode.rs b/common/types/src/mixnode.rs index e1c3ef52fb..85c65be18d 100644 --- a/common/types/src/mixnode.rs +++ b/common/types/src/mixnode.rs @@ -1,7 +1,8 @@ use crate::currency::MajorCurrencyAmount; use crate::error::TypesError; use mixnet_contract_common::{ - MixNode as MixnetContractMixNode, MixNodeBond as MixnetContractMixNodeBond, + Coin as CosmWasmCoin, MixNode as MixnetContractMixNode, + MixNodeBond as MixnetContractMixNodeBond, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -102,12 +103,12 @@ impl TryFrom for MixNodeBond { )); } - let pledge_amount: MajorCurrencyAmount = pledge_amount.try_into()?; - let total_delegation: MajorCurrencyAmount = total_delegation.try_into()?; - let accumulated_rewards: Option = accumulated_rewards.and_then(|r| { - MajorCurrencyAmount::from_minor_uint128_and_denom(r, &pledge_amount.denom.to_string()) - .ok() - }); + let denom = total_delegation.denom.clone(); + + let pledge_amount: MajorCurrencyAmount = pledge_amount.into(); + let total_delegation: MajorCurrencyAmount = total_delegation.into(); + let accumulated_rewards: Option = + accumulated_rewards.map(|r| CosmWasmCoin::new(r.u128(), denom).into()); Ok(MixNodeBond { pledge_amount, diff --git a/common/types/src/transaction.rs b/common/types/src/transaction.rs index a1e5685113..982ae4cd94 100644 --- a/common/types/src/transaction.rs +++ b/common/types/src/transaction.rs @@ -20,8 +20,8 @@ pub struct SendTxResult { pub details: TransactionDetails, pub gas_used: u64, pub gas_wanted: u64, - pub fee: MajorCurrencyAmount, pub tx_hash: String, + // pub fee: MajorCurrencyAmount, } impl SendTxResult { @@ -37,10 +37,11 @@ impl SendTxResult { gas_used: t.tx_result.gas_used.value(), gas_wanted: t.tx_result.gas_wanted.value(), tx_hash: t.hash.to_string(), - fee: MajorCurrencyAmount::from_decimal_and_denom( - Decimal::new(Uint128::from(t.tx_result.gas_used.value())), - denom_minor.to_string(), - )?, + // that is completely wrong: fee is what you told the validator to use beforehandn + // fee: MajorCurrencyAmount::from_decimal_and_denom( + // Decimal::new(Uint128::from(t.tx_result.gas_used.value())), + // denom_minor.to_string(), + // )?, }) } } @@ -100,7 +101,7 @@ pub struct RpcTransactionResponse { pub block_height: u64, pub transaction_hash: String, pub gas_info: GasInfo, - pub fee: MajorCurrencyAmount, + // pub fee: MajorCurrencyAmount, } impl RpcTransactionResponse { @@ -118,10 +119,11 @@ impl RpcTransactionResponse { transaction_hash: value.hash.to_string(), tx_result_json: ::serde_json::to_string_pretty(&value.tx_result)?, block_height: value.height.value(), - fee: MajorCurrencyAmount::from_decimal_and_denom( - Decimal::new(Uint128::from(value.tx_result.gas_used.value())), - denom_minor.to_string(), - )?, + // wrong + // fee: MajorCurrencyAmount::from_decimal_and_denom( + // Decimal::new(Uint128::from(value.tx_result.gas_used.value())), + // denom_minor.to_string(), + // )?, }) } } diff --git a/common/types/src/vesting.rs b/common/types/src/vesting.rs index d834ca666b..c20b0845e0 100644 --- a/common/types/src/vesting.rs +++ b/common/types/src/vesting.rs @@ -21,7 +21,7 @@ impl TryFrom for PledgeData { type Error = TypesError; fn try_from(data: VestingPledgeData) -> Result { - let amount: MajorCurrencyAmount = data.amount().try_into()?; + let amount: MajorCurrencyAmount = data.amount().into(); Ok(Self { amount, block_time: data.block_time().seconds(), @@ -51,7 +51,7 @@ impl TryFrom for OriginalVestingResponse { type Error = TypesError; fn try_from(data: VestingOriginalVestingResponse) -> Result { - let amount = data.amount().try_into()?; + let amount = data.amount().into(); Ok(Self { amount, number_of_periods: data.number_of_periods(), @@ -82,7 +82,7 @@ impl TryFrom for VestingAccountInfo { for period in account.periods() { periods.push(period.into()); } - let amount: MajorCurrencyAmount = account.coin().try_into()?; + let amount: MajorCurrencyAmount = account.coin().into(); Ok(Self { owner_address: account.owner_address().to_string(), staking_address: account.staking_address().map(|a| a.to_string()), diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 0ace19f985..3c8368c8e8 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + [[package]] name = "aes" version = "0.7.5" @@ -1040,6 +1046,7 @@ dependencies = [ "serde_repr", "thiserror", "time 0.3.6", + "ts-rs", ] [[package]] @@ -1573,6 +1580,15 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.30" @@ -1645,6 +1661,29 @@ dependencies = [ "serde", ] +[[package]] +name = "ts-rs" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc59f479df54269b400dd95bc3b7e81623b3e4b9c70c8ca7125ab8341eafa64e" +dependencies = [ + "thiserror", + "ts-rs-macros", +] + +[[package]] +name = "ts-rs-macros" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f807fdb3151fee75df7485b901a89624358cd07a67a8fb1a5831bf5a07681ff" +dependencies = [ + "Inflector", + "proc-macro2", + "quote", + "syn", + "termcolor", +] + [[package]] name = "typenum" version = "1.15.0" @@ -1755,6 +1794,7 @@ dependencies = [ "mixnet-contract-common", "schemars", "serde", + "ts-rs", ] [[package]] @@ -1839,6 +1879,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index 85a86bcfc3..240055062c 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -283,6 +283,32 @@ pub fn execute( info, ) } + ExecuteMsg::ClaimOperatorReward {} => { + crate::rewards::transactions::try_claim_operator_reward(deps, &env, &info) + } + ExecuteMsg::ClaimOperatorRewardOnBehalf { owner } => { + crate::rewards::transactions::try_claim_operator_reward_on_behalf( + deps, &env, &info, owner, + ) + } + ExecuteMsg::ClaimDelegatorReward { mix_identity } => { + crate::rewards::transactions::try_claim_delegator_reward( + deps, + &env, + &info, + &mix_identity, + ) + } + ExecuteMsg::ClaimDelegatorRewardOnBehalf { + mix_identity, + owner, + } => crate::rewards::transactions::try_claim_delegator_reward_on_behalf( + deps, + &env, + &info, + owner, + &mix_identity, + ), } } diff --git a/contracts/mixnet/src/error.rs b/contracts/mixnet/src/error.rs index b8af736b26..15e082f845 100644 --- a/contracts/mixnet/src/error.rs +++ b/contracts/mixnet/src/error.rs @@ -153,8 +153,8 @@ pub enum ContractError { #[from] source: MixnetContractError, }, - #[error("No rewards to claim for mixnode {identity} for delegate {delegate}")] - NoRewardsToClaim { identity: String, delegate: String }, + #[error("No rewards to claim for mixnode {identity} for {address}")] + NoRewardsToClaim { identity: String, address: String }, #[error("Epoch not initialized yet!")] EpochNotInitialized, diff --git a/contracts/mixnet/src/rewards/transactions.rs b/contracts/mixnet/src/rewards/transactions.rs index 495f3a78d5..239b992fc9 100644 --- a/contracts/mixnet/src/rewards/transactions.rs +++ b/contracts/mixnet/src/rewards/transactions.rs @@ -15,9 +15,13 @@ use crate::mixnodes::storage::{self as mixnodes_storage, StoredMixnodeBond}; use crate::rewards::helpers; use crate::support::helpers::is_authorized; use config::defaults::DENOM; -use cosmwasm_std::{Addr, Api, Coin, DepsMut, Env, MessageInfo, Order, Response, Storage, Uint128}; +use cosmwasm_std::{ + coins, wasm_execute, Addr, Api, BankMsg, Coin, DepsMut, Env, MessageInfo, Order, Response, + Storage, Uint128, +}; use cw_storage_plus::Bound; use mixnet_contract_common::events::{ + new_claim_delegator_reward_event, new_claim_operator_reward_event, new_compound_delegator_reward_event, new_compound_operator_reward_event, new_mix_operator_rewarding_event, new_not_found_mix_operator_rewarding_event, new_too_fresh_bond_mix_operator_rewarding_event, new_zero_uptime_mix_operator_rewarding_event, @@ -27,6 +31,186 @@ use mixnet_contract_common::reward_params::{NodeEpochRewards, NodeRewardParams, use mixnet_contract_common::{Delegation, IdentityKey, RewardingStatus}; use mixnet_contract_common::RewardingResult; +use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; +use vesting_contract_common::one_ucoin; + +// All four of the below methods need to do the following things: +// 1. Calculate currently available rewards +// 2. Send the rewards back to whoever claimed them +// 3. Set the LAST_CLAIMED_HEIGHT to the current height +pub fn try_claim_operator_reward( + deps: DepsMut<'_>, + env: &Env, + info: &MessageInfo, +) -> Result { + _try_claim_operator_reward(deps.storage, deps.api, env, &info.sender.to_string(), None) +} + +pub fn try_claim_operator_reward_on_behalf( + deps: DepsMut<'_>, + env: &Env, + info: &MessageInfo, + owner: String, +) -> Result { + _try_claim_operator_reward( + deps.storage, + deps.api, + env, + &owner, + Some(info.sender.clone()), + ) +} + +fn _try_claim_operator_reward( + storage: &mut dyn Storage, + api: &dyn Api, + env: &Env, + owner: &str, + proxy: Option, +) -> Result { + let owner = api.addr_validate(owner)?; + + let bond = match crate::mixnodes::storage::mixnodes() + .idx + .owner + .item(storage, owner.clone())? + { + Some(record) => record.1, + None => return Err(ContractError::NoAssociatedMixNodeBond { owner }), + }; + + if proxy != bond.proxy { + return Err(ContractError::ProxyMismatch { + existing: bond + .proxy + .map_or_else(|| "None".to_string(), |a| a.as_str().to_string()), + incoming: proxy.map_or_else(|| "None".to_string(), |a| a.as_str().to_string()), + }); + } + + let reward = calculate_operator_reward(storage, api, &owner, &bond)?; + + OPERATOR_REWARD_CLAIMED_HEIGHT.save( + storage, + (owner.to_string(), bond.identity().to_string()), + &env.block.height, + )?; + + if reward.is_zero() { + return Err(ContractError::NoRewardsToClaim { + identity: bond.identity().to_string(), + address: owner.to_string(), + }); + } + + let return_tokens = BankMsg::Send { + to_address: proxy.as_ref().unwrap_or(&owner).to_string(), + amount: coins(reward.u128(), DENOM), + }; + + let mut response = Response::default() + .add_message(return_tokens) + .add_event(new_claim_operator_reward_event(&owner, reward)); + + if let Some(proxy) = proxy { + let msg = Some(VestingContractExecuteMsg::TrackReward { + address: owner.to_string(), + amount: Coin::new(reward.u128(), DENOM), + }); + + let wasm_msg = wasm_execute(proxy, &msg, vec![one_ucoin()])?; + response = response.add_message(wasm_msg); + } + + Ok(response) +} + +pub fn _try_claim_delegator_reward( + storage: &mut dyn Storage, + api: &dyn Api, + env: &Env, + owner: &str, + mix_identity: &str, + proxy: Option, +) -> Result { + let owner = api.addr_validate(owner)?; + + let key = mixnet_contract_common::delegation::generate_storage_key(&owner, proxy.as_ref()); + let reward = calculate_delegator_reward(storage, api, key.clone(), mix_identity)?; + + DELEGATOR_REWARD_CLAIMED_HEIGHT.save( + storage, + (key, mix_identity.to_string()), + &env.block.height, + )?; + + if reward.is_zero() { + return Err(ContractError::NoRewardsToClaim { + identity: mix_identity.to_string(), + address: owner.to_string(), + }); + } + + let return_tokens = BankMsg::Send { + to_address: proxy.as_ref().unwrap_or(&owner).to_string(), + amount: coins(reward.u128(), DENOM), + }; + + let mut response = + Response::default() + .add_message(return_tokens) + .add_event(new_claim_delegator_reward_event( + &owner, + &proxy, + reward, + mix_identity, + )); + + if let Some(proxy) = proxy { + let msg = Some(VestingContractExecuteMsg::TrackReward { + address: owner.to_string(), + amount: Coin::new(reward.u128(), DENOM), + }); + + let wasm_msg = wasm_execute(proxy, &msg, vec![one_ucoin()])?; + response = response.add_message(wasm_msg); + } + + Ok(response) +} + +pub fn try_claim_delegator_reward_on_behalf( + deps: DepsMut<'_>, + env: &Env, + info: &MessageInfo, + owner: String, + mix_identity: &str, +) -> Result { + _try_claim_delegator_reward( + deps.storage, + deps.api, + env, + &owner, + mix_identity, + Some(info.sender.clone()), + ) +} + +pub fn try_claim_delegator_reward( + deps: DepsMut<'_>, + env: &Env, + info: &MessageInfo, + mix_identity: &str, +) -> Result { + _try_claim_delegator_reward( + deps.storage, + deps.api, + env, + &info.sender.to_string(), + mix_identity, + None, + ) +} pub fn try_compound_operator_reward_on_behalf( deps: DepsMut, @@ -449,7 +633,7 @@ pub(crate) fn try_reward_mixnode( let node_delegation = current_bond.total_delegation.amount; // check if it has non-zero uptime - if params.uptime() == 0 { + if params.uptime() == Uint128::zero() { storage::REWARDING_STATUS.save( deps.storage, (epoch.id(), mix_identity.clone()), @@ -1381,7 +1565,7 @@ pub mod tests { let mix_1 = mixnodes_storage::read_full_mixnode_bond(&deps.storage, &node_identity) .unwrap() .unwrap(); - let mix_1_uptime = 100; + let mix_1_uptime = 90; let epoch = Interval::init_epoch(env.clone()); save_epoch(&mut deps.storage, &epoch).unwrap(); @@ -1395,7 +1579,7 @@ pub mod tests { params.set_reward_blockstamp(env.block.height); - assert_eq!(params.performance(), U128::from_num(1u32)); + assert_eq!(params.performance(), U128::from_num(0.8999999999999999)); let mix_1_reward_result = mix_1.reward(¶ms); @@ -1407,9 +1591,14 @@ pub mod tests { mix_1_reward_result.lambda(), U128::from_num(0.0000133333333333f64) ); - assert_eq!(mix_1_reward_result.reward().int(), 259114u128); + assert_eq!(mix_1_reward_result.reward().int(), 233202u128); - assert_eq!(mix_1.node_profit(¶ms).int(), 203558u128); + assert_eq!(mix_1.node_profit(¶ms).int(), 183202u128); + + assert_ne!( + mix_1_reward_result.reward(), + mix_1.node_profit(¶ms).int() + ); let mix1_operator_reward = mix_1.operator_reward(¶ms); @@ -1417,9 +1606,9 @@ pub mod tests { let mix1_delegator2_reward = mix_1.reward_delegation(Uint128::new(2000_000000), ¶ms); - assert_eq!(mix1_operator_reward, 167513); - assert_eq!(mix1_delegator1_reward, 73280); - assert_eq!(mix1_delegator2_reward, 18320); + assert_eq!(mix1_operator_reward, 150761); + assert_eq!(mix1_delegator1_reward, 65952); + assert_eq!(mix1_delegator2_reward, 16488); assert_eq!( mix_1_reward_result.reward().int(), diff --git a/contracts/vesting/src/contract.rs b/contracts/vesting/src/contract.rs index e60fd0e2ef..e40feb27dd 100644 --- a/contracts/vesting/src/contract.rs +++ b/contracts/vesting/src/contract.rs @@ -13,7 +13,8 @@ use mixnet_contract_common::{Gateway, IdentityKey, MixNode}; use vesting_contract_common::events::{ new_ownership_transfer_event, new_periodic_vesting_account_event, new_staking_address_update_event, new_track_gateway_unbond_event, - new_track_mixnode_unbond_event, new_track_undelegation_event, new_vested_coins_withdraw_event, + new_track_mixnode_unbond_event, new_track_reward_event, new_track_undelegation_event, + new_vested_coins_withdraw_event, }; use vesting_contract_common::messages::{ ExecuteMsg, InitMsg, MigrateMsg, QueryMsg, VestingSpecification, @@ -46,6 +47,13 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { + ExecuteMsg::TrackReward { amount, address } => { + try_track_reward(deps, info, amount, &address) + } + ExecuteMsg::ClaimOperatorReward {} => try_claim_operator_reward(deps, info), + ExecuteMsg::ClaimDelegatorReward { mix_identity } => { + try_claim_delegator_reward(deps, info, mix_identity) + } ExecuteMsg::CompoundDelegatorReward { mix_identity } => { try_compound_delegator_reward(mix_identity, info, deps) } @@ -278,6 +286,20 @@ pub fn try_track_unbond_mixnode( Ok(Response::new().add_event(new_track_mixnode_unbond_event())) } +fn try_track_reward( + deps: DepsMut<'_>, + info: MessageInfo, + amount: Coin, + address: &str, +) -> Result { + if info.sender != MIXNET_CONTRACT_ADDRESS.load(deps.storage)? { + return Err(ContractError::NotMixnetContract(info.sender)); + } + let account = account_from_address(address, deps.storage, deps.api)?; + account.track_reward(amount, deps.storage)?; + Ok(Response::new().add_event(new_track_reward_event())) +} + fn try_track_undelegation( address: &str, mix_identity: IdentityKey, @@ -314,6 +336,23 @@ fn try_compound_delegator_reward( account.try_compound_delegator_reward(mix_identity, deps.storage) } +fn try_claim_operator_reward( + deps: DepsMut<'_>, + info: MessageInfo, +) -> Result { + let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; + account.try_claim_operator_reward(deps.storage) +} + +fn try_claim_delegator_reward( + deps: DepsMut<'_>, + info: MessageInfo, + mix_identity: String, +) -> Result { + let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; + account.try_claim_delegator_reward(mix_identity, deps.storage) +} + fn try_undelegate_from_mixnode( mix_identity: IdentityKey, info: MessageInfo, diff --git a/contracts/vesting/src/traits/bonding_account.rs b/contracts/vesting/src/traits/bonding_account.rs index ba9a7e2e81..1ce2690df9 100644 --- a/contracts/vesting/src/traits/bonding_account.rs +++ b/contracts/vesting/src/traits/bonding_account.rs @@ -8,6 +8,8 @@ pub trait MixnodeBondingAccount { storage: &dyn Storage, ) -> Result; + fn try_claim_operator_reward(&self, storage: &dyn Storage) -> Result; + fn try_bond_mixnode( &self, mix_node: MixNode, diff --git a/contracts/vesting/src/traits/delegating_account.rs b/contracts/vesting/src/traits/delegating_account.rs index c3b188a99a..174a9202ea 100644 --- a/contracts/vesting/src/traits/delegating_account.rs +++ b/contracts/vesting/src/traits/delegating_account.rs @@ -3,6 +3,12 @@ use cosmwasm_std::{Coin, Env, Response, Storage, Uint128}; use mixnet_contract_common::IdentityKey; pub trait DelegatingAccount { + fn try_claim_delegator_reward( + &self, + mix_identity: IdentityKey, + storage: &dyn Storage, + ) -> Result; + fn try_compound_delegator_reward( &self, mix_identity: IdentityKey, diff --git a/contracts/vesting/src/traits/vesting_account.rs b/contracts/vesting/src/traits/vesting_account.rs index b1d6687e47..bfb16c6aa9 100644 --- a/contracts/vesting/src/traits/vesting_account.rs +++ b/contracts/vesting/src/traits/vesting_account.rs @@ -73,4 +73,5 @@ pub trait VestingAccount { to_address: Option, storage: &mut dyn Storage, ) -> Result<(), ContractError>; + fn track_reward(&self, amount: Coin, storage: &mut dyn Storage) -> Result<(), ContractError>; } diff --git a/contracts/vesting/src/vesting/account/delegating_account.rs b/contracts/vesting/src/vesting/account/delegating_account.rs index 5ba3b16a5b..ccbacefdcc 100644 --- a/contracts/vesting/src/vesting/account/delegating_account.rs +++ b/contracts/vesting/src/vesting/account/delegating_account.rs @@ -13,6 +13,25 @@ use vesting_contract_common::one_ucoin; use super::Account; impl DelegatingAccount for Account { + fn try_claim_delegator_reward( + &self, + mix_identity: IdentityKey, + storage: &dyn Storage, + ) -> Result { + let msg = MixnetExecuteMsg::ClaimDelegatorRewardOnBehalf { + owner: self.owner_address().to_string(), + mix_identity, + }; + + let compound_delegator_reward_msg = wasm_execute( + MIXNET_CONTRACT_ADDRESS.load(storage)?, + &msg, + vec![one_ucoin()], + )?; + + Ok(Response::new().add_message(compound_delegator_reward_msg)) + } + fn try_compound_delegator_reward( &self, mix_identity: IdentityKey, diff --git a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs index 63d05dff6b..abbfe835ae 100644 --- a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs +++ b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs @@ -14,6 +14,20 @@ use vesting_contract_common::PledgeData; use super::Account; impl MixnodeBondingAccount for Account { + fn try_claim_operator_reward(&self, storage: &dyn Storage) -> Result { + let msg = MixnetExecuteMsg::ClaimOperatorRewardOnBehalf { + owner: self.owner_address().into_string(), + }; + + let compound_operator_reward_msg = wasm_execute( + MIXNET_CONTRACT_ADDRESS.load(storage)?, + &msg, + vec![one_ucoin()], + )?; + + Ok(Response::new().add_message(compound_operator_reward_msg)) + } + fn try_compound_operator_reward( &self, storage: &dyn Storage, diff --git a/contracts/vesting/src/vesting/account/vesting_account.rs b/contracts/vesting/src/vesting/account/vesting_account.rs index f4528aded9..46da32e66d 100644 --- a/contracts/vesting/src/vesting/account/vesting_account.rs +++ b/contracts/vesting/src/vesting/account/vesting_account.rs @@ -8,6 +8,13 @@ use vesting_contract_common::{OriginalVestingResponse, Period}; use super::Account; impl VestingAccount for Account { + fn track_reward(&self, amount: Coin, storage: &mut dyn Storage) -> Result<(), ContractError> { + let current_balance = self.load_balance(storage)?; + let new_balance = current_balance + amount.amount; + self.save_balance(new_balance, storage)?; + Ok(()) + } + fn locked_coins( &self, block_time: Option, diff --git a/explorer-api/src/mix_node/econ_stats.rs b/explorer-api/src/mix_node/econ_stats.rs index 25765cf8c6..3c526097d4 100644 --- a/explorer-api/src/mix_node/econ_stats.rs +++ b/explorer-api/src/mix_node/econ_stats.rs @@ -36,6 +36,6 @@ pub(crate) async fn retrieve_mixnode_econ_stats( estimated_total_node_reward: reward_estimation.estimated_total_node_reward, estimated_operator_reward: reward_estimation.estimated_operator_reward, estimated_delegators_reward: reward_estimation.estimated_delegators_reward, - current_interval_uptime: reward_estimation.reward_params.node.uptime() as u8, + current_interval_uptime: reward_estimation.reward_params.node.uptime().u128() as u8, }) } diff --git a/nym-wallet/src-tauri/rustfmt.toml b/nym-wallet/src-tauri/rustfmt.toml deleted file mode 100644 index 45642c1904..0000000000 --- a/nym-wallet/src-tauri/rustfmt.toml +++ /dev/null @@ -1,13 +0,0 @@ -max_width = 100 -hard_tabs = false -tab_spaces = 2 -newline_style = "Auto" -use_small_heuristics = "Default" -reorder_imports = true -reorder_modules = true -remove_nested_parens = true -edition = "2018" -merge_derives = true -use_try_shorthand = false -use_field_init_shorthand = false -force_explicit_abi = true diff --git a/nym-wallet/src-tauri/src/build.rs b/nym-wallet/src-tauri/src/build.rs index 795b9b7c83..d860e1e6a7 100644 --- a/nym-wallet/src-tauri/src/build.rs +++ b/nym-wallet/src-tauri/src/build.rs @@ -1,3 +1,3 @@ fn main() { - tauri_build::build() + tauri_build::build() } diff --git a/nym-wallet/src-tauri/src/config/mod.rs b/nym-wallet/src-tauri/src/config/mod.rs index dd664a00dd..ad7674eff0 100644 --- a/nym-wallet/src-tauri/src/config/mod.rs +++ b/nym-wallet/src-tauri/src/config/mod.rs @@ -22,7 +22,7 @@ use crate::error::BackendError; use crate::platform_constants::{CONFIG_DIR_NAME, CONFIG_FILENAME}; pub const REMOTE_SOURCE_OF_VALIDATOR_URLS: &str = - "https://nymtech.net/.wellknown/wallet/validators.json"; + "https://nymtech.net/.wellknown/wallet/validators.json"; const CURRENT_GLOBAL_CONFIG_VERSION: u32 = 1; const CURRENT_NETWORK_CONFIG_VERSION: u32 = 1; @@ -30,448 +30,445 @@ pub(crate) const CUSTOM_SIMULATED_GAS_MULTIPLIER: f32 = 1.4; #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct Config { - // Base configuration is not part of the configuration file as it's not intended to be changed. - base: Base, + // Base configuration is not part of the configuration file as it's not intended to be changed. + base: Base, - // Global configuration file - global: Option, + // Global configuration file + global: Option, - // One configuration file per network - networks: HashMap, + // One configuration file per network + networks: HashMap, } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] struct Base { - /// Information on all the networks that the wallet connects to. - networks: SupportedNetworks, + /// Information on all the networks that the wallet connects to. + networks: SupportedNetworks, } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] pub struct GlobalConfig { - version: Option, - // TODO: there are no global settings (yet) + version: Option, + // TODO: there are no global settings (yet) } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] pub struct NetworkConfig { - version: Option, + version: Option, - // User selected urls - selected_nymd_url: Option, - selected_api_url: Option, + // User selected urls + selected_nymd_url: Option, + selected_api_url: Option, - // Additional user provided validators. - // It is an option for the purpuse of file serialization. - validator_urls: Option>, + // Additional user provided validators. + // It is an option for the purpuse of file serialization. + validator_urls: Option>, } impl Default for Base { - fn default() -> Self { - let networks = WalletNetwork::iter().map(Into::into).collect(); - Base { - networks: SupportedNetworks::new(networks), + fn default() -> Self { + let networks = WalletNetwork::iter().map(Into::into).collect(); + Base { + networks: SupportedNetworks::new(networks), + } } - } } impl Default for GlobalConfig { - fn default() -> Self { - Self { - version: Some(CURRENT_GLOBAL_CONFIG_VERSION), + fn default() -> Self { + Self { + version: Some(CURRENT_GLOBAL_CONFIG_VERSION), + } } - } } impl Default for NetworkConfig { - fn default() -> Self { - Self { - version: Some(CURRENT_NETWORK_CONFIG_VERSION), - selected_nymd_url: None, - selected_api_url: None, - validator_urls: None, + fn default() -> Self { + Self { + version: Some(CURRENT_NETWORK_CONFIG_VERSION), + selected_nymd_url: None, + selected_api_url: None, + validator_urls: None, + } } - } } impl NetworkConfig { - fn validators(&self) -> impl Iterator { - self.validator_urls.iter().flat_map(|v| v.iter()) - } + fn validators(&self) -> impl Iterator { + self.validator_urls.iter().flat_map(|v| v.iter()) + } } impl Config { - fn root_directory() -> PathBuf { - tauri::api::path::config_dir().expect("Failed to get config directory") - } - - fn config_directory() -> PathBuf { - Self::root_directory().join(CONFIG_DIR_NAME) - } - - fn config_file_path(network: Option) -> PathBuf { - if let Some(network) = network { - let network_filename = format!("{}.toml", network.as_key()); - Self::config_directory().join(network_filename) - } else { - Self::config_directory().join(CONFIG_FILENAME) - } - } - - pub fn save_to_files(&self) -> io::Result<()> { - log::trace!("Config::save_to_file"); - - // Make sure the whole directory structure actually exists - fs::create_dir_all(Self::config_directory())?; - - // Global config - if let Some(global) = &self.global { - let location = Self::config_file_path(None); - - match toml::to_string_pretty(&global) - .map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err)) - .map(|toml| fs::write(location.clone(), toml)) - { - Ok(_) => log::debug!("Writing to: {:#?}", location), - Err(err) => log::warn!("Failed to write to {:#?}: {err}", location), - } + fn root_directory() -> PathBuf { + tauri::api::path::config_dir().expect("Failed to get config directory") } - // One file per network - for (network, config) in &self.networks { - let network = match Network::from_str(network).map(Into::into) { - Ok(network) => network, - Err(err) => { - log::warn!("Unexpected name for network configuration, not saving: {err}"); - break; + fn config_directory() -> PathBuf { + Self::root_directory().join(CONFIG_DIR_NAME) + } + + fn config_file_path(network: Option) -> PathBuf { + if let Some(network) = network { + let network_filename = format!("{}.toml", network.as_key()); + Self::config_directory().join(network_filename) + } else { + Self::config_directory().join(CONFIG_FILENAME) } - }; - - let location = Self::config_file_path(Some(network)); - match toml::to_string_pretty(config) - .map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err)) - .map(|toml| fs::write(location.clone(), toml)) - { - Ok(_) => log::debug!("Writing to: {:#?}", location), - Err(err) => log::warn!("Failed to write to {:#?}: {err}", location), - } } - Ok(()) - } - pub fn load_from_files() -> Self { - // Global - let global = { - let file = Self::config_file_path(None); - match load_from_file::(file.clone()) { - Ok(global) => { - log::debug!("Loaded from file {:#?}", file); - Some(global) + pub fn save_to_files(&self) -> io::Result<()> { + log::trace!("Config::save_to_file"); + + // Make sure the whole directory structure actually exists + fs::create_dir_all(Self::config_directory())?; + + // Global config + if let Some(global) = &self.global { + let location = Self::config_file_path(None); + + match toml::to_string_pretty(&global) + .map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err)) + .map(|toml| fs::write(location.clone(), toml)) + { + Ok(_) => log::debug!("Writing to: {:#?}", location), + Err(err) => log::warn!("Failed to write to {:#?}: {err}", location), + } } - Err(err) => { - log::trace!("Not loading {:#?}: {}", file, err); - None + + // One file per network + for (network, config) in &self.networks { + let network = match Network::from_str(network).map(Into::into) { + Ok(network) => network, + Err(err) => { + log::warn!("Unexpected name for network configuration, not saving: {err}"); + break; + } + }; + + let location = Self::config_file_path(Some(network)); + match toml::to_string_pretty(config) + .map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err)) + .map(|toml| fs::write(location.clone(), toml)) + { + Ok(_) => log::debug!("Writing to: {:#?}", location), + Err(err) => log::warn!("Failed to write to {:#?}: {err}", location), + } } - } - }; + Ok(()) + } - // One file per network - let mut networks = HashMap::new(); - for network in WalletNetwork::iter() { - let file = Self::config_file_path(Some(network)); - match load_from_file::(file.clone()) { - Ok(config) => { - log::trace!("Loaded from file {:#?}", file); - networks.insert(network.as_key(), config); + pub fn load_from_files() -> Self { + // Global + let global = { + let file = Self::config_file_path(None); + match load_from_file::(file.clone()) { + Ok(global) => { + log::debug!("Loaded from file {:#?}", file); + Some(global) + } + Err(err) => { + log::trace!("Not loading {:#?}: {}", file, err); + None + } + } + }; + + // One file per network + let mut networks = HashMap::new(); + for network in WalletNetwork::iter() { + let file = Self::config_file_path(Some(network)); + match load_from_file::(file.clone()) { + Ok(config) => { + log::trace!("Loaded from file {:#?}", file); + networks.insert(network.as_key(), config); + } + Err(err) => log::trace!("Not loading {:#?}: {}", file, err), + }; + } + + Self { + base: Base::default(), + global, + networks, } - Err(err) => log::trace!("Not loading {:#?}: {}", file, err), - }; } - Self { - base: Base::default(), - global, - networks, + pub fn get_base_validators( + &self, + network: WalletNetwork, + ) -> impl Iterator + '_ { + self.base.networks.validators(network.into()).map(|v| { + v.clone() + .try_into() + .expect("The hardcoded validators are assumed to be valid urls") + }) } - } - pub fn get_base_validators( - &self, - network: WalletNetwork, - ) -> impl Iterator + '_ { - self.base.networks.validators(network.into()).map(|v| { - v.clone() - .try_into() - .expect("The hardcoded validators are assumed to be valid urls") - }) - } - - pub fn get_configured_validators( - &self, - network: WalletNetwork, - ) -> impl Iterator + '_ { - self - .networks - .get(&network.as_key()) - .into_iter() - .flat_map(|c| c.validators().cloned()) - } - - pub fn get_mixnet_contract_address(&self, network: WalletNetwork) -> Option { - self - .base - .networks - .mixnet_contract_address(network.into()) - .expect("No mixnet contract address found in config") - .parse() - .ok() - } - - pub fn get_vesting_contract_address(&self, network: WalletNetwork) -> Option { - self - .base - .networks - .vesting_contract_address(network.into()) - .expect("No vesting contract address found in config") - .parse() - .ok() - } - - pub fn get_bandwidth_claim_contract_address( - &self, - network: WalletNetwork, - ) -> Option { - self - .base - .networks - .bandwidth_claim_contract_address(network.into()) - .expect("No bandwidth claim contract address found in config") - .parse() - .ok() - } - - pub fn select_validator_nymd_url(&mut self, nymd_url: Url, network: WalletNetwork) { - if let Some(net) = self.networks.get_mut(&network.as_key()) { - net.selected_nymd_url = Some(nymd_url); - } else { - self.networks.insert( - network.as_key(), - NetworkConfig { - selected_nymd_url: Some(nymd_url), - ..NetworkConfig::default() - }, - ); + pub fn get_configured_validators( + &self, + network: WalletNetwork, + ) -> impl Iterator + '_ { + self.networks + .get(&network.as_key()) + .into_iter() + .flat_map(|c| c.validators().cloned()) } - } - pub fn select_validator_api_url(&mut self, api_url: Url, network: WalletNetwork) { - if let Some(net) = self.networks.get_mut(&network.as_key()) { - net.selected_api_url = Some(api_url); - } else { - self.networks.insert( - network.as_key(), - NetworkConfig { - selected_nymd_url: Some(api_url), - ..NetworkConfig::default() - }, - ); + pub fn get_mixnet_contract_address(&self, network: WalletNetwork) -> Option { + self.base + .networks + .mixnet_contract_address(network.into()) + .expect("No mixnet contract address found in config") + .parse() + .ok() } - } - pub fn get_selected_validator_nymd_url(&self, network: WalletNetwork) -> Option { - self - .networks - .get(&network.as_key()) - .and_then(|config| config.selected_nymd_url.clone()) - } - - pub fn get_selected_validator_api_url(&self, network: &WalletNetwork) -> Option { - self - .networks - .get(&network.as_key()) - .and_then(|config| config.selected_api_url.clone()) - } - - pub fn add_validator_url(&mut self, url: ValidatorConfigEntry, network: WalletNetwork) { - if let Some(network_config) = self.networks.get_mut(&network.as_key()) { - if let Some(ref mut urls) = network_config.validator_urls { - urls.push(url); - } else { - network_config.validator_urls = Some(vec![url]); - } - } else { - self.networks.insert( - network.as_key(), - NetworkConfig { - validator_urls: Some(vec![url]), - ..NetworkConfig::default() - }, - ); + pub fn get_vesting_contract_address(&self, network: WalletNetwork) -> Option { + self.base + .networks + .vesting_contract_address(network.into()) + .expect("No vesting contract address found in config") + .parse() + .ok() } - } - pub fn remove_validator_url(&mut self, url: ValidatorConfigEntry, network: WalletNetwork) { - if let Some(network_config) = self.networks.get_mut(&network.as_key()) { - if let Some(ref mut urls) = network_config.validator_urls { - // Removes duplicates too if there are any - urls.retain(|existing_url| existing_url != &url); - } + pub fn get_bandwidth_claim_contract_address( + &self, + network: WalletNetwork, + ) -> Option { + self.base + .networks + .bandwidth_claim_contract_address(network.into()) + .expect("No bandwidth claim contract address found in config") + .parse() + .ok() + } + + pub fn select_validator_nymd_url(&mut self, nymd_url: Url, network: WalletNetwork) { + if let Some(net) = self.networks.get_mut(&network.as_key()) { + net.selected_nymd_url = Some(nymd_url); + } else { + self.networks.insert( + network.as_key(), + NetworkConfig { + selected_nymd_url: Some(nymd_url), + ..NetworkConfig::default() + }, + ); + } + } + + pub fn select_validator_api_url(&mut self, api_url: Url, network: WalletNetwork) { + if let Some(net) = self.networks.get_mut(&network.as_key()) { + net.selected_api_url = Some(api_url); + } else { + self.networks.insert( + network.as_key(), + NetworkConfig { + selected_nymd_url: Some(api_url), + ..NetworkConfig::default() + }, + ); + } + } + + pub fn get_selected_validator_nymd_url(&self, network: WalletNetwork) -> Option { + self.networks + .get(&network.as_key()) + .and_then(|config| config.selected_nymd_url.clone()) + } + + pub fn get_selected_validator_api_url(&self, network: &WalletNetwork) -> Option { + self.networks + .get(&network.as_key()) + .and_then(|config| config.selected_api_url.clone()) + } + + pub fn add_validator_url(&mut self, url: ValidatorConfigEntry, network: WalletNetwork) { + if let Some(network_config) = self.networks.get_mut(&network.as_key()) { + if let Some(ref mut urls) = network_config.validator_urls { + urls.push(url); + } else { + network_config.validator_urls = Some(vec![url]); + } + } else { + self.networks.insert( + network.as_key(), + NetworkConfig { + validator_urls: Some(vec![url]), + ..NetworkConfig::default() + }, + ); + } + } + + pub fn remove_validator_url(&mut self, url: ValidatorConfigEntry, network: WalletNetwork) { + if let Some(network_config) = self.networks.get_mut(&network.as_key()) { + if let Some(ref mut urls) = network_config.validator_urls { + // Removes duplicates too if there are any + urls.retain(|existing_url| existing_url != &url); + } + } } - } } fn load_from_file(file: PathBuf) -> Result where - T: DeserializeOwned, + T: DeserializeOwned, { - fs::read_to_string(file).and_then(|contents| { - toml::from_str::(&contents) - .map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err)) - }) + fs::read_to_string(file).and_then(|contents| { + toml::from_str::(&contents) + .map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err)) + }) } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] pub struct ValidatorConfigEntry { - pub nymd_url: Url, - pub nymd_name: Option, - pub api_url: Option, + pub nymd_url: Url, + pub nymd_name: Option, + pub api_url: Option, } impl TryFrom for ValidatorConfigEntry { - type Error = BackendError; + type Error = BackendError; - fn try_from(validator: ValidatorDetails) -> Result { - Ok(ValidatorConfigEntry { - nymd_url: validator.nymd_url.parse()?, - nymd_name: None, - api_url: match &validator.api_url { - Some(url) => Some(url.parse()?), - None => None, - }, - }) - } + fn try_from(validator: ValidatorDetails) -> Result { + Ok(ValidatorConfigEntry { + nymd_url: validator.nymd_url.parse()?, + nymd_name: None, + api_url: match &validator.api_url { + Some(url) => Some(url.parse()?), + None => None, + }, + }) + } } impl TryFrom for ValidatorConfigEntry { - type Error = BackendError; + type Error = BackendError; - fn try_from(validator: network_config::Validator) -> Result { - Ok(ValidatorConfigEntry { - nymd_url: validator.nymd_url.parse()?, - nymd_name: validator.nymd_name, - api_url: match &validator.api_url { - Some(url) => Some(url.parse()?), - None => None, - }, - }) - } + fn try_from(validator: network_config::Validator) -> Result { + Ok(ValidatorConfigEntry { + nymd_url: validator.nymd_url.parse()?, + nymd_name: validator.nymd_name, + api_url: match &validator.api_url { + Some(url) => Some(url.parse()?), + None => None, + }, + }) + } } impl fmt::Display for ValidatorConfigEntry { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s1 = format!("nymd_url: {}", self.nymd_url); - let name = self.nymd_name.as_ref().map(|name| format!(" ({})", name)); - let s2 = self - .api_url - .as_ref() - .map(|url| format!(", api_url: {}", url)); - write!( - f, - " {}{}{},", - s1, - name.unwrap_or_default(), - s2.unwrap_or_default() - ) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s1 = format!("nymd_url: {}", self.nymd_url); + let name = self.nymd_name.as_ref().map(|name| format!(" ({})", name)); + let s2 = self + .api_url + .as_ref() + .map(|url| format!(", api_url: {}", url)); + write!( + f, + " {}{}{},", + s1, + name.unwrap_or_default(), + s2.unwrap_or_default() + ) + } } #[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct OptionalValidators { - // User supplied additional validator urls in addition to the hardcoded ones. - // These are separate fields, rather than a map, to force the serialization order. - mainnet: Option>, - sandbox: Option>, - qa: Option>, + // User supplied additional validator urls in addition to the hardcoded ones. + // These are separate fields, rather than a map, to force the serialization order. + mainnet: Option>, + sandbox: Option>, + qa: Option>, } impl OptionalValidators { - pub fn validators(&self, network: WalletNetwork) -> impl Iterator { - match network { - WalletNetwork::MAINNET => self.mainnet.as_ref(), - WalletNetwork::SANDBOX => self.sandbox.as_ref(), - WalletNetwork::QA => self.qa.as_ref(), + pub fn validators( + &self, + network: WalletNetwork, + ) -> impl Iterator { + match network { + WalletNetwork::MAINNET => self.mainnet.as_ref(), + WalletNetwork::SANDBOX => self.sandbox.as_ref(), + WalletNetwork::QA => self.qa.as_ref(), + } + .into_iter() + .flatten() } - .into_iter() - .flatten() - } } impl fmt::Display for OptionalValidators { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s1 = self - .mainnet - .as_ref() - .map(|validators| format!("mainnet: [\n{}\n]", validators.iter().format("\n"))) - .unwrap_or_default(); - let s2 = self - .sandbox - .as_ref() - .map(|validators| format!(",\nsandbox: [\n{}\n]", validators.iter().format("\n"))) - .unwrap_or_default(); - let s3 = self - .qa - .as_ref() - .map(|validators| format!(",\nqa: [\n{}\n]", validators.iter().format("\n"))) - .unwrap_or_default(); - write!(f, "{}{}{}", s1, s2, s3) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s1 = self + .mainnet + .as_ref() + .map(|validators| format!("mainnet: [\n{}\n]", validators.iter().format("\n"))) + .unwrap_or_default(); + let s2 = self + .sandbox + .as_ref() + .map(|validators| format!(",\nsandbox: [\n{}\n]", validators.iter().format("\n"))) + .unwrap_or_default(); + let s3 = self + .qa + .as_ref() + .map(|validators| format!(",\nqa: [\n{}\n]", validators.iter().format("\n"))) + .unwrap_or_default(); + write!(f, "{}{}{}", s1, s2, s3) + } } #[cfg(test)] mod tests { - use super::*; + use super::*; - fn test_config() -> Config { - let netconfig = NetworkConfig { - selected_nymd_url: None, - selected_api_url: Some("https://my_api_url.com".parse().unwrap()), + fn test_config() -> Config { + let netconfig = NetworkConfig { + selected_nymd_url: None, + selected_api_url: Some("https://my_api_url.com".parse().unwrap()), - validator_urls: Some(vec![ - ValidatorConfigEntry { - nymd_url: "https://foo".parse().unwrap(), - nymd_name: Some("FooName".to_string()), - api_url: None, - }, - ValidatorConfigEntry { - nymd_url: "https://bar".parse().unwrap(), - nymd_name: None, - api_url: Some("https://bar/api".parse().unwrap()), - }, - ValidatorConfigEntry { - nymd_url: "https://baz".parse().unwrap(), - nymd_name: None, - api_url: Some("https://baz/api".parse().unwrap()), - }, - ]), - ..NetworkConfig::default() - }; + validator_urls: Some(vec![ + ValidatorConfigEntry { + nymd_url: "https://foo".parse().unwrap(), + nymd_name: Some("FooName".to_string()), + api_url: None, + }, + ValidatorConfigEntry { + nymd_url: "https://bar".parse().unwrap(), + nymd_name: None, + api_url: Some("https://bar/api".parse().unwrap()), + }, + ValidatorConfigEntry { + nymd_url: "https://baz".parse().unwrap(), + nymd_name: None, + api_url: Some("https://baz/api".parse().unwrap()), + }, + ]), + ..NetworkConfig::default() + }; - Config { - base: Base::default(), - global: Some(GlobalConfig::default()), - networks: [(WalletNetwork::MAINNET.as_key(), netconfig)] - .into_iter() - .collect(), + Config { + base: Base::default(), + global: Some(GlobalConfig::default()), + networks: [(WalletNetwork::MAINNET.as_key(), netconfig)] + .into_iter() + .collect(), + } } - } - #[test] - fn serialize_to_toml() { - let config = test_config(); - let netconfig = &config.networks[&WalletNetwork::MAINNET.as_key()]; - assert_eq!( - toml::to_string_pretty(netconfig).unwrap(), - r#"version = 1 + #[test] + fn serialize_to_toml() { + let config = test_config(); + let netconfig = &config.networks[&WalletNetwork::MAINNET.as_key()]; + assert_eq!( + toml::to_string_pretty(netconfig).unwrap(), + r#"version = 1 selected_api_url = 'https://my_api_url.com/' [[validator_urls]] @@ -486,17 +483,17 @@ api_url = 'https://bar/api' nymd_url = 'https://baz/' api_url = 'https://baz/api' "# - ); - } + ); + } - #[test] - fn serialize_to_json() { - let config = test_config(); - let netconfig = &config.networks[&WalletNetwork::MAINNET.as_key()]; - println!("{}", serde_json::to_string_pretty(netconfig).unwrap()); - assert_eq!( - serde_json::to_string_pretty(netconfig).unwrap(), - r#"{ + #[test] + fn serialize_to_json() { + let config = test_config(); + let netconfig = &config.networks[&WalletNetwork::MAINNET.as_key()]; + println!("{}", serde_json::to_string_pretty(netconfig).unwrap()); + assert_eq!( + serde_json::to_string_pretty(netconfig).unwrap(), + r#"{ "version": 1, "selected_nymd_url": null, "selected_api_url": "https://my_api_url.com/", @@ -518,53 +515,53 @@ api_url = 'https://baz/api' } ] }"# - ); - } + ); + } - #[test] - fn serialize_and_deserialize_to_toml() { - let config = test_config(); - let netconfig = &config.networks[&WalletNetwork::MAINNET.as_key()]; - let config_str = toml::to_string_pretty(netconfig).unwrap(); - let config_from_toml: NetworkConfig = toml::from_str(&config_str).unwrap(); - assert_eq!(netconfig, &config_from_toml); - } + #[test] + fn serialize_and_deserialize_to_toml() { + let config = test_config(); + let netconfig = &config.networks[&WalletNetwork::MAINNET.as_key()]; + let config_str = toml::to_string_pretty(netconfig).unwrap(); + let config_from_toml: NetworkConfig = toml::from_str(&config_str).unwrap(); + assert_eq!(netconfig, &config_from_toml); + } - #[test] - fn get_urls_parsed_from_config() { - let config = test_config(); + #[test] + fn get_urls_parsed_from_config() { + let config = test_config(); - let nymd_url = config - .get_configured_validators(WalletNetwork::MAINNET) - .next() - .map(|v| v.nymd_url) - .unwrap(); - assert_eq!(nymd_url.as_ref(), "https://foo/"); + let nymd_url = config + .get_configured_validators(WalletNetwork::MAINNET) + .next() + .map(|v| v.nymd_url) + .unwrap(); + assert_eq!(nymd_url.as_ref(), "https://foo/"); - // The first entry is missing an API URL - let api_url = config - .get_configured_validators(WalletNetwork::MAINNET) - .next() - .and_then(|v| v.api_url); - assert_eq!(api_url, None); - } + // The first entry is missing an API URL + let api_url = config + .get_configured_validators(WalletNetwork::MAINNET) + .next() + .and_then(|v| v.api_url); + assert_eq!(api_url, None); + } - #[test] - fn get_urls_from_defaults() { - let config = Config::default(); + #[test] + fn get_urls_from_defaults() { + let config = Config::default(); - let nymd_url = config - .get_base_validators(WalletNetwork::MAINNET) - .next() - .map(|v| v.nymd_url) - .unwrap(); - assert_eq!(nymd_url.as_ref(), "https://rpc.nyx.nodes.guru/"); + let nymd_url = config + .get_base_validators(WalletNetwork::MAINNET) + .next() + .map(|v| v.nymd_url) + .unwrap(); + assert_eq!(nymd_url.as_ref(), "https://rpc.nyx.nodes.guru/"); - let api_url = config - .get_base_validators(WalletNetwork::MAINNET) - .next() - .and_then(|v| v.api_url) - .unwrap(); - assert_eq!(api_url.as_ref(), "https://validator.nymtech.net/api/",); - } + let api_url = config + .get_base_validators(WalletNetwork::MAINNET) + .next() + .and_then(|v| v.api_url) + .unwrap(); + assert_eq!(api_url.as_ref(), "https://validator.nymtech.net/api/",); + } } diff --git a/nym-wallet/src-tauri/src/error.rs b/nym-wallet/src-tauri/src/error.rs index 15f8cbd497..cd97eb4c05 100644 --- a/nym-wallet/src-tauri/src/error.rs +++ b/nym-wallet/src-tauri/src/error.rs @@ -7,117 +7,117 @@ use validator_client::{nymd::error::NymdError, ValidatorClientError}; #[derive(Error, Debug)] pub enum BackendError { - #[error("{source}")] - TypesError { - #[from] - source: TypesError, - }, - #[error("{source}")] - Bip39Error { - #[from] - source: bip39::Error, - }, - #[error("{source}")] - TendermintError { - #[from] - source: tendermint_rpc::Error, - }, - #[error("{source}")] - NymdError { - #[from] - source: NymdError, - }, - #[error("{source}")] - CosmwasmStd { - #[from] - source: cosmwasm_std::StdError, - }, - #[error("{source}")] - ErrorReport { - #[from] - source: eyre::Report, - }, - #[error("{source}")] - ValidatorApiError { - #[from] - source: ValidatorAPIError, - }, - #[error("{source}")] - KeyDerivationError { - #[from] - source: argon2::Error, - }, - #[error("{source}")] - IOError { - #[from] - source: io::Error, - }, - #[error("{source}")] - SerdeJsonError { - #[from] - source: serde_json::Error, - }, - #[error("{source}")] - MalformedUrlProvided { - #[from] - source: url::ParseError, - }, - #[error("{source}")] - ReqwestError { - #[from] - source: reqwest::Error, - }, - #[error("failed to encrypt the given data with the provided password")] - EncryptionError, - #[error("failed to decrypt the given data with the provided password")] - DecryptionError, - #[error("Client has not been initialized yet, connect with mnemonic to initialize")] - ClientNotInitialized, - #[error("No balance available for address {0}")] - NoBalance(String), - #[error("The provided network is not supported (yet)")] - NetworkNotSupported(config::defaults::all::Network), - #[error("Could not access the local data storage directory")] - UnknownStorageDirectory, - #[error("The wallet file already exists")] - WalletFileAlreadyExists, - #[error("The wallet file is not found")] - WalletFileNotFound, - #[error("Login ID not found in wallet")] - WalletNoSuchLoginId, - #[error("Account ID not found in wallet login")] - WalletNoSuchAccountIdInWalletLogin, - #[error("Login ID already found in wallet")] - WalletLoginIdAlreadyExists, - #[error("Account ID already found in wallet login")] - WalletAccountIdAlreadyExistsInWalletLogin, - #[error("Mnemonic already found in wallet login, was it already imported?")] - WalletMnemonicAlreadyExistsInWalletLogin, - #[error("Adding a different password to the wallet not currently supported")] - WalletDifferentPasswordDetected, - #[error("Unexpected mnemonic account for login")] - WalletUnexpectedMnemonicAccount, - // #[error("Unexpected multiple account entry for login")] - // WalletUnexpectedMultipleAccounts, - #[error("Failed to derive address from mnemonic")] - FailedToDeriveAddress, + #[error("{source}")] + TypesError { + #[from] + source: TypesError, + }, + #[error("{source}")] + Bip39Error { + #[from] + source: bip39::Error, + }, + #[error("{source}")] + TendermintError { + #[from] + source: tendermint_rpc::Error, + }, + #[error("{source}")] + NymdError { + #[from] + source: NymdError, + }, + #[error("{source}")] + CosmwasmStd { + #[from] + source: cosmwasm_std::StdError, + }, + #[error("{source}")] + ErrorReport { + #[from] + source: eyre::Report, + }, + #[error("{source}")] + ValidatorApiError { + #[from] + source: ValidatorAPIError, + }, + #[error("{source}")] + KeyDerivationError { + #[from] + source: argon2::Error, + }, + #[error("{source}")] + IOError { + #[from] + source: io::Error, + }, + #[error("{source}")] + SerdeJsonError { + #[from] + source: serde_json::Error, + }, + #[error("{source}")] + MalformedUrlProvided { + #[from] + source: url::ParseError, + }, + #[error("{source}")] + ReqwestError { + #[from] + source: reqwest::Error, + }, + #[error("failed to encrypt the given data with the provided password")] + EncryptionError, + #[error("failed to decrypt the given data with the provided password")] + DecryptionError, + #[error("Client has not been initialized yet, connect with mnemonic to initialize")] + ClientNotInitialized, + #[error("No balance available for address {0}")] + NoBalance(String), + #[error("The provided network is not supported (yet)")] + NetworkNotSupported(config::defaults::all::Network), + #[error("Could not access the local data storage directory")] + UnknownStorageDirectory, + #[error("The wallet file already exists")] + WalletFileAlreadyExists, + #[error("The wallet file is not found")] + WalletFileNotFound, + #[error("Login ID not found in wallet")] + WalletNoSuchLoginId, + #[error("Account ID not found in wallet login")] + WalletNoSuchAccountIdInWalletLogin, + #[error("Login ID already found in wallet")] + WalletLoginIdAlreadyExists, + #[error("Account ID already found in wallet login")] + WalletAccountIdAlreadyExistsInWalletLogin, + #[error("Mnemonic already found in wallet login, was it already imported?")] + WalletMnemonicAlreadyExistsInWalletLogin, + #[error("Adding a different password to the wallet not currently supported")] + WalletDifferentPasswordDetected, + #[error("Unexpected mnemonic account for login")] + WalletUnexpectedMnemonicAccount, + // #[error("Unexpected multiple account entry for login")] + // WalletUnexpectedMultipleAccounts, + #[error("Failed to derive address from mnemonic")] + FailedToDeriveAddress, } impl Serialize for BackendError { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.collect_str(self) - } + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.collect_str(self) + } } impl From for BackendError { - fn from(e: ValidatorClientError) -> Self { - match e { - ValidatorClientError::ValidatorAPIError { source } => source.into(), - ValidatorClientError::MalformedUrlProvided(e) => e.into(), - ValidatorClientError::NymdError(e) => e.into(), + fn from(e: ValidatorClientError) -> Self { + match e { + ValidatorClientError::ValidatorAPIError { source } => source.into(), + ValidatorClientError::MalformedUrlProvided(e) => e.into(), + ValidatorClientError::NymdError(e) => e.into(), + } } - } } diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index e5f336ddad..2b5a97e472 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -1,6 +1,6 @@ #![cfg_attr( - all(not(debug_assertions), target_os = "windows"), - windows_subsystem = "windows" + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" )] use mixnet_contract_common::{Gateway, MixNode}; @@ -27,130 +27,130 @@ use crate::operations::vesting; use crate::state::State; fn main() { - dotenv::dotenv().ok(); - setup_logging(); + dotenv::dotenv().ok(); + setup_logging(); - tauri::Builder::default() - .manage(Arc::new(RwLock::new(State::default()))) - .invoke_handler(tauri::generate_handler![ - mixnet::account::add_account_for_password, - mixnet::account::connect_with_mnemonic, - mixnet::account::create_new_mnemonic, - mixnet::account::create_password, - mixnet::account::does_password_file_exist, - mixnet::account::get_balance, - mixnet::account::list_accounts, - mixnet::account::logout, - mixnet::account::remove_account_for_password, - mixnet::account::remove_password, - mixnet::account::show_mnemonic_for_account_in_password, - mixnet::account::sign_in_with_password, - mixnet::account::sign_in_with_password_and_account_id, - mixnet::account::switch_network, - mixnet::account::validate_mnemonic, - mixnet::admin::get_contract_settings, - mixnet::admin::update_contract_settings, - mixnet::bond::bond_gateway, - mixnet::bond::bond_mixnode, - mixnet::bond::gateway_bond_details, - mixnet::bond::get_operator_rewards, - mixnet::bond::mixnode_bond_details, - mixnet::bond::unbond_gateway, - mixnet::bond::unbond_mixnode, - mixnet::bond::update_mixnode, - mixnet::delegate::delegate_to_mixnode, - mixnet::delegate::get_delegator_rewards, - mixnet::delegate::get_pending_delegation_events, - mixnet::delegate::get_delegation_summary, - mixnet::delegate::get_all_pending_delegation_events, - mixnet::delegate::get_all_mix_delegations, - mixnet::delegate::undelegate_from_mixnode, - mixnet::epoch::get_current_epoch, - mixnet::send::send, - network_config::add_validator, - network_config::get_validator_api_urls, - network_config::get_validator_nymd_urls, - network_config::remove_validator, - network_config::select_validator_api_url, - network_config::select_validator_nymd_url, - network_config::update_validator_urls, - state::load_config_from_files, - state::save_config_to_files, - utils::owns_gateway, - utils::owns_mixnode, - utils::get_env, - validator_api::status::gateway_core_node_status, - validator_api::status::mixnode_core_node_status, - validator_api::status::mixnode_inclusion_probability, - validator_api::status::mixnode_reward_estimation, - validator_api::status::mixnode_stake_saturation, - validator_api::status::mixnode_status, - vesting::bond::vesting_bond_gateway, - vesting::bond::vesting_bond_mixnode, - vesting::bond::vesting_unbond_gateway, - vesting::bond::vesting_unbond_mixnode, - vesting::bond::vesting_update_mixnode, - vesting::bond::withdraw_vested_coins, - vesting::delegate::get_pending_vesting_delegation_events, - vesting::delegate::vesting_delegate_to_mixnode, - vesting::delegate::vesting_undelegate_from_mixnode, - vesting::queries::delegated_free, - vesting::queries::delegated_vesting, - vesting::queries::get_account_info, - vesting::queries::get_current_vesting_period, - vesting::queries::locked_coins, - vesting::queries::original_vesting, - vesting::queries::spendable_coins, - vesting::queries::vested_coins, - vesting::queries::vesting_coins, - vesting::queries::vesting_end_time, - vesting::queries::vesting_get_gateway_pledge, - vesting::queries::vesting_get_mixnode_pledge, - vesting::queries::vesting_start_time, - simulate::admin::simulate_update_contract_settings, - simulate::cosmos::simulate_send, - simulate::mixnet::simulate_bond_gateway, - simulate::mixnet::simulate_unbond_gateway, - simulate::mixnet::simulate_bond_mixnode, - simulate::mixnet::simulate_unbond_mixnode, - simulate::mixnet::simulate_update_mixnode, - simulate::mixnet::simulate_delegate_to_mixnode, - simulate::mixnet::simulate_undelegate_from_mixnode, - simulate::vesting::simulate_vesting_bond_gateway, - simulate::vesting::simulate_vesting_unbond_gateway, - simulate::vesting::simulate_vesting_bond_mixnode, - simulate::vesting::simulate_vesting_unbond_mixnode, - simulate::vesting::simulate_vesting_update_mixnode, - simulate::vesting::simulate_withdraw_vested_coins, - ]) - .menu(Menu::new().add_default_app_submenu_if_macos()) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); + tauri::Builder::default() + .manage(Arc::new(RwLock::new(State::default()))) + .invoke_handler(tauri::generate_handler![ + mixnet::account::add_account_for_password, + mixnet::account::connect_with_mnemonic, + mixnet::account::create_new_mnemonic, + mixnet::account::create_password, + mixnet::account::does_password_file_exist, + mixnet::account::get_balance, + mixnet::account::list_accounts, + mixnet::account::logout, + mixnet::account::remove_account_for_password, + mixnet::account::remove_password, + mixnet::account::show_mnemonic_for_account_in_password, + mixnet::account::sign_in_with_password, + mixnet::account::sign_in_with_password_and_account_id, + mixnet::account::switch_network, + mixnet::account::validate_mnemonic, + mixnet::admin::get_contract_settings, + mixnet::admin::update_contract_settings, + mixnet::bond::bond_gateway, + mixnet::bond::bond_mixnode, + mixnet::bond::gateway_bond_details, + mixnet::bond::get_operator_rewards, + mixnet::bond::mixnode_bond_details, + mixnet::bond::unbond_gateway, + mixnet::bond::unbond_mixnode, + mixnet::bond::update_mixnode, + mixnet::delegate::delegate_to_mixnode, + mixnet::delegate::get_delegator_rewards, + mixnet::delegate::get_pending_delegation_events, + mixnet::delegate::get_delegation_summary, + mixnet::delegate::get_all_pending_delegation_events, + mixnet::delegate::get_all_mix_delegations, + mixnet::delegate::undelegate_from_mixnode, + mixnet::epoch::get_current_epoch, + mixnet::send::send, + network_config::add_validator, + network_config::get_validator_api_urls, + network_config::get_validator_nymd_urls, + network_config::remove_validator, + network_config::select_validator_api_url, + network_config::select_validator_nymd_url, + network_config::update_validator_urls, + state::load_config_from_files, + state::save_config_to_files, + utils::owns_gateway, + utils::owns_mixnode, + utils::get_env, + validator_api::status::gateway_core_node_status, + validator_api::status::mixnode_core_node_status, + validator_api::status::mixnode_inclusion_probability, + validator_api::status::mixnode_reward_estimation, + validator_api::status::mixnode_stake_saturation, + validator_api::status::mixnode_status, + vesting::bond::vesting_bond_gateway, + vesting::bond::vesting_bond_mixnode, + vesting::bond::vesting_unbond_gateway, + vesting::bond::vesting_unbond_mixnode, + vesting::bond::vesting_update_mixnode, + vesting::bond::withdraw_vested_coins, + vesting::delegate::get_pending_vesting_delegation_events, + vesting::delegate::vesting_delegate_to_mixnode, + vesting::delegate::vesting_undelegate_from_mixnode, + vesting::queries::delegated_free, + vesting::queries::delegated_vesting, + vesting::queries::get_account_info, + vesting::queries::get_current_vesting_period, + vesting::queries::locked_coins, + vesting::queries::original_vesting, + vesting::queries::spendable_coins, + vesting::queries::vested_coins, + vesting::queries::vesting_coins, + vesting::queries::vesting_end_time, + vesting::queries::vesting_get_gateway_pledge, + vesting::queries::vesting_get_mixnode_pledge, + vesting::queries::vesting_start_time, + simulate::admin::simulate_update_contract_settings, + simulate::cosmos::simulate_send, + simulate::mixnet::simulate_bond_gateway, + simulate::mixnet::simulate_unbond_gateway, + simulate::mixnet::simulate_bond_mixnode, + simulate::mixnet::simulate_unbond_mixnode, + simulate::mixnet::simulate_update_mixnode, + simulate::mixnet::simulate_delegate_to_mixnode, + simulate::mixnet::simulate_undelegate_from_mixnode, + simulate::vesting::simulate_vesting_bond_gateway, + simulate::vesting::simulate_vesting_unbond_gateway, + simulate::vesting::simulate_vesting_bond_mixnode, + simulate::vesting::simulate_vesting_unbond_mixnode, + simulate::vesting::simulate_vesting_update_mixnode, + simulate::vesting::simulate_withdraw_vested_coins, + ]) + .menu(Menu::new().add_default_app_submenu_if_macos()) + .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); - } + 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); + } - if ::std::env::var("RUST_TRACE_OPERATIONS").is_ok() { - log_builder.filter_module("nym_wallet::operations", log::LevelFilter::Trace); - } + if ::std::env::var("RUST_TRACE_OPERATIONS").is_ok() { + log_builder.filter_module("nym_wallet::operations", log::LevelFilter::Trace); + } - 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) - .filter_module("rustls", log::LevelFilter::Warn) - .filter_module("tokio_util", log::LevelFilter::Warn) - .init(); + 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) + .filter_module("rustls", log::LevelFilter::Warn) + .filter_module("tokio_util", log::LevelFilter::Warn) + .init(); } diff --git a/nym-wallet/src-tauri/src/menu.rs b/nym-wallet/src-tauri/src/menu.rs index 493ed6ceb9..6c7a8d4bd7 100644 --- a/nym-wallet/src-tauri/src/menu.rs +++ b/nym-wallet/src-tauri/src/menu.rs @@ -3,25 +3,25 @@ use tauri::Menu; use tauri::{MenuItem, Submenu}; pub trait AddDefaultSubmenus { - fn add_default_app_submenu_if_macos(self) -> Self; + fn add_default_app_submenu_if_macos(self) -> Self; } impl AddDefaultSubmenus for Menu { - fn add_default_app_submenu_if_macos(self) -> Menu { - #[cfg(target_os = "macos")] - return self.add_submenu(Submenu::new( - "Menu", - Menu::new() - .add_native_item(MenuItem::Copy) - .add_native_item(MenuItem::Cut) - .add_native_item(MenuItem::Paste) - .add_native_item(MenuItem::Hide) - .add_native_item(MenuItem::HideOthers) - .add_native_item(MenuItem::SelectAll) - .add_native_item(MenuItem::ShowAll) - .add_native_item(MenuItem::Quit), - )); - #[cfg(not(target_os = "macos"))] - return self; - } + fn add_default_app_submenu_if_macos(self) -> Menu { + #[cfg(target_os = "macos")] + return self.add_submenu(Submenu::new( + "Menu", + Menu::new() + .add_native_item(MenuItem::Copy) + .add_native_item(MenuItem::Cut) + .add_native_item(MenuItem::Paste) + .add_native_item(MenuItem::Hide) + .add_native_item(MenuItem::HideOthers) + .add_native_item(MenuItem::SelectAll) + .add_native_item(MenuItem::ShowAll) + .add_native_item(MenuItem::Quit), + )); + #[cfg(not(target_os = "macos"))] + return self; + } } diff --git a/nym-wallet/src-tauri/src/network_config.rs b/nym-wallet/src-tauri/src/network_config.rs index 744f6f4196..2a621922d5 100644 --- a/nym-wallet/src-tauri/src/network_config.rs +++ b/nym-wallet/src-tauri/src/network_config.rs @@ -13,79 +13,79 @@ use crate::state::State; #[tauri::command] pub async fn get_validator_nymd_urls( - network: WalletNetwork, - state: tauri::State<'_, Arc>>, + network: WalletNetwork, + state: tauri::State<'_, Arc>>, ) -> Result { - let state = state.read().await; - let urls: Vec = state.get_nymd_urls(network).collect(); - Ok(ValidatorUrls { urls }) + let state = state.read().await; + let urls: Vec = state.get_nymd_urls(network).collect(); + Ok(ValidatorUrls { urls }) } #[tauri::command] pub async fn get_validator_api_urls( - network: WalletNetwork, - state: tauri::State<'_, Arc>>, + network: WalletNetwork, + state: tauri::State<'_, Arc>>, ) -> Result { - let state = state.read().await; - let urls: Vec = state.get_api_urls(network).collect(); - Ok(ValidatorUrls { urls }) + let state = state.read().await; + let urls: Vec = state.get_api_urls(network).collect(); + Ok(ValidatorUrls { urls }) } #[tauri::command] pub async fn select_validator_nymd_url( - url: &str, - network: WalletNetwork, - state: tauri::State<'_, Arc>>, + url: &str, + network: WalletNetwork, + state: tauri::State<'_, Arc>>, ) -> Result<(), BackendError> { - log::debug!("Selecting new validator nymd_url for {network}: {url}"); - state - .write() - .await - .select_validator_nymd_url(url, network)?; - Ok(()) + log::debug!("Selecting new validator nymd_url for {network}: {url}"); + state + .write() + .await + .select_validator_nymd_url(url, network)?; + Ok(()) } #[tauri::command] pub async fn select_validator_api_url( - url: &str, - network: WalletNetwork, - state: tauri::State<'_, Arc>>, + url: &str, + network: WalletNetwork, + state: tauri::State<'_, Arc>>, ) -> Result<(), BackendError> { - log::debug!("Selecting new validator api_url for {network}: {url}"); - state.write().await.select_validator_api_url(url, network)?; - Ok(()) + log::debug!("Selecting new validator api_url for {network}: {url}"); + state.write().await.select_validator_api_url(url, network)?; + Ok(()) } #[tauri::command] pub async fn add_validator( - validator: Validator, - network: WalletNetwork, - state: tauri::State<'_, Arc>>, + validator: Validator, + network: WalletNetwork, + state: tauri::State<'_, Arc>>, ) -> Result<(), BackendError> { - log::debug!("Add validator for {network}: {validator}"); - let url = validator.try_into()?; - state.write().await.add_validator_url(url, network); - Ok(()) + log::debug!("Add validator for {network}: {validator}"); + let url = validator.try_into()?; + state.write().await.add_validator_url(url, network); + Ok(()) } #[tauri::command] pub async fn remove_validator( - validator: Validator, - network: WalletNetwork, - state: tauri::State<'_, Arc>>, + validator: Validator, + network: WalletNetwork, + state: tauri::State<'_, Arc>>, ) -> Result<(), BackendError> { - log::debug!("Remove validator for {network}: {validator}"); - let url = validator.try_into()?; - state.write().await.remove_validator_url(url, network); - Ok(()) + log::debug!("Remove validator for {network}: {validator}"); + let url = validator.try_into()?; + state.write().await.remove_validator_url(url, network); + Ok(()) } // Update the list of validators by fecthing additional ones remotely. If it fails, just ignore. #[tauri::command] pub async fn update_validator_urls( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, Arc>>, ) -> Result<(), BackendError> { - let mut w_state = state.write().await; - let _r = w_state.fetch_updated_validator_urls().await; - Ok(()) + let mut w_state = state.write().await; + let _r = w_state.fetch_updated_validator_urls().await; + Ok(()) } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/account.rs b/nym-wallet/src-tauri/src/operations/mixnet/account.rs index 037d7622af..0beb789c4d 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/account.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/account.rs @@ -1,631 +1,632 @@ -use std::collections::HashMap; -use std::convert::TryInto; -use std::str::FromStr; -use std::sync::Arc; - -use bip39::{Language, Mnemonic}; -use cosmrs::bip32::DerivationPath; -use itertools::Itertools; -use rand::seq::SliceRandom; -use strum::IntoEnumIterator; -use tokio::sync::RwLock; -use url::Url; - -use config::defaults::all::Network; -use config::defaults::COSMOS_DERIVATION_PATH; -use nym_types::account::{Account, AccountEntry, Balance}; -use nym_types::currency::MajorCurrencyAmount; -use nym_wallet_types::network::Network as WalletNetwork; -use validator_client::nymd::wallet::{AccountData, DirectSecp256k1HdWallet}; -use validator_client::{nymd::SigningNymdClient, Client}; - use crate::config::{Config, CUSTOM_SIMULATED_GAS_MULTIPLIER}; use crate::error::BackendError; use crate::network_config; use crate::nymd_client; use crate::state::{State, WalletAccountIds}; use crate::wallet_storage::{self, DEFAULT_LOGIN_ID}; +use bip39::{Language, Mnemonic}; +use config::defaults::all::Network; +use config::defaults::COSMOS_DERIVATION_PATH; +use cosmrs::bip32::DerivationPath; +use itertools::Itertools; +use nym_types::account::{Account, AccountEntry, Balance}; +use nym_types::currency::MajorCurrencyAmount; +use nym_wallet_types::network::Network as WalletNetwork; +use rand::seq::SliceRandom; +use std::collections::HashMap; +use std::convert::TryInto; +use std::str::FromStr; +use std::sync::Arc; +use strum::IntoEnumIterator; +use tokio::sync::RwLock; +use url::Url; +use validator_client::nymd::wallet::{AccountData, DirectSecp256k1HdWallet}; +use validator_client::{nymd::SigningNymdClient, Client}; #[tauri::command] pub async fn connect_with_mnemonic( - mnemonic: String, - state: tauri::State<'_, Arc>>, + mnemonic: String, + state: tauri::State<'_, Arc>>, ) -> Result { - let mnemonic = Mnemonic::from_str(&mnemonic)?; - _connect_with_mnemonic(mnemonic, state).await + let mnemonic = Mnemonic::from_str(&mnemonic)?; + _connect_with_mnemonic(mnemonic, state).await } #[tauri::command] pub async fn get_balance( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, Arc>>, ) -> Result { - let denom = state.read().await.current_network().denom(); - match nymd_client!(state) - .get_balance(nymd_client!(state).address(), denom) - .await - { - Ok(Some(coin)) => { - let amount = MajorCurrencyAmount::from_cosmrs_coin(&coin)?; - let printable_balance = amount.to_string(); - Ok(Balance { - amount, - printable_balance, - }) + let denom = state.read().await.current_network().denom(); + match nymd_client!(state) + .get_balance(nymd_client!(state).address(), denom) + .await + { + Ok(Some(coin)) => { + let amount = MajorCurrencyAmount::from(coin); + let printable_balance = amount.to_string(); + Ok(Balance { + amount, + printable_balance, + }) + } + Ok(None) => Err(BackendError::NoBalance( + nymd_client!(state).address().to_string(), + )), + Err(e) => Err(BackendError::from(e)), } - Ok(None) => Err(BackendError::NoBalance( - nymd_client!(state).address().to_string(), - )), - Err(e) => Err(BackendError::from(e)), - } } #[tauri::command] pub fn create_new_mnemonic() -> String { - random_mnemonic().to_string() + random_mnemonic().to_string() } #[tauri::command] pub fn validate_mnemonic(mnemonic: &str) -> bool { - Mnemonic::from_str(mnemonic).is_ok() + Mnemonic::from_str(mnemonic).is_ok() } #[tauri::command] pub async fn switch_network( - state: tauri::State<'_, Arc>>, - network: WalletNetwork, + state: tauri::State<'_, Arc>>, + network: WalletNetwork, ) -> Result { - let account = { - let r_state = state.read().await; - let client = r_state.client(network)?; - let denom = network.denom(); + let account = { + let r_state = state.read().await; + let client = r_state.client(network)?; + let denom = network.denom(); - Account::new( - client.nymd.mixnet_contract_address()?.to_string(), - client.nymd.address().to_string(), - denom.try_into()?, - ) - }; + Account::new( + client.nymd.mixnet_contract_address()?.to_string(), + client.nymd.address().to_string(), + denom.try_into()?, + ) + }; - let mut w_state = state.write().await; - w_state.set_network(network); + let mut w_state = state.write().await; + w_state.set_network(network); - Ok(account) + Ok(account) } #[tauri::command] pub async fn logout(state: tauri::State<'_, Arc>>) -> Result<(), BackendError> { - state.write().await.logout(); - Ok(()) + state.write().await.logout(); + Ok(()) } fn random_mnemonic() -> Mnemonic { - let mut rng = rand::thread_rng(); - Mnemonic::generate_in_with(&mut rng, Language::English, 24).unwrap() + let mut rng = rand::thread_rng(); + Mnemonic::generate_in_with(&mut rng, Language::English, 24).unwrap() } async fn _connect_with_mnemonic( - mnemonic: Mnemonic, - state: tauri::State<'_, Arc>>, + mnemonic: Mnemonic, + state: tauri::State<'_, Arc>>, ) -> Result { - { - let mut w_state = state.write().await; - w_state.load_config_files(); - } - - network_config::update_validator_urls(state.clone()).await?; - - let config = { - let state = state.read().await; - - // Take the oppertunity to list all the known validators while we have the state. - for network in WalletNetwork::iter() { - log::debug!( - "List of validators for {network}: [\n{}\n]", - state.get_config_validator_entries(network).format(",\n") - ); + { + let mut w_state = state.write().await; + w_state.load_config_files(); } - state.config().clone() - }; + network_config::update_validator_urls(state.clone()).await?; - // Get all the urls needed for the connection test - let (untested_nymd_urls, untested_api_urls) = { - let state = state.read().await; - (state.get_all_nymd_urls(), state.get_all_api_urls()) - }; - let default_nymd_urls: HashMap = untested_nymd_urls - .iter() - .map(|(network, urls)| (*network, urls.iter().next().unwrap().clone())) - .collect(); - let default_api_urls: HashMap = untested_api_urls - .iter() - .map(|(network, urls)| (*network, urls.iter().next().unwrap().clone())) - .collect(); + let config = { + let state = state.read().await; - // Run connection tests on all nymd and validator-api endpoints - let (nymd_urls, api_urls) = - run_connection_test(untested_nymd_urls, untested_api_urls, &config).await; + // Take the oppertunity to list all the known validators while we have the state. + for network in WalletNetwork::iter() { + log::debug!( + "List of validators for {network}: [\n{}\n]", + state.get_config_validator_entries(network).format(",\n") + ); + } - // Create clients for all networks - let clients = create_clients( - &nymd_urls, - &api_urls, - &default_nymd_urls, - &default_api_urls, - &config, - &mnemonic, - )?; + state.config().clone() + }; - // Set the default account - let default_network: WalletNetwork = config::defaults::DEFAULT_NETWORK.into(); - let client_for_default_network = clients - .iter() - .find(|client| WalletNetwork::from(client.network) == default_network); - let account_for_default_network = match client_for_default_network { - Some(client) => Ok(Account::new( - client.nymd.mixnet_contract_address()?.to_string(), - client.nymd.address().to_string(), - default_network.denom().try_into()?, - )), - None => Err(BackendError::NetworkNotSupported( - config::defaults::DEFAULT_NETWORK, - )), - }; + // Get all the urls needed for the connection test + let (untested_nymd_urls, untested_api_urls) = { + let state = state.read().await; + (state.get_all_nymd_urls(), state.get_all_api_urls()) + }; + let default_nymd_urls: HashMap = untested_nymd_urls + .iter() + .map(|(network, urls)| (*network, urls.iter().next().unwrap().clone())) + .collect(); + let default_api_urls: HashMap = untested_api_urls + .iter() + .map(|(network, urls)| (*network, urls.iter().next().unwrap().clone())) + .collect(); - // Register all the clients - { - let mut w_state = state.write().await; - w_state.logout(); - } - for client in clients { - let network: WalletNetwork = client.network.into(); - let mut w_state = state.write().await; - w_state.add_client(network, client); - } + // Run connection tests on all nymd and validator-api endpoints + let (nymd_urls, api_urls) = + run_connection_test(untested_nymd_urls, untested_api_urls, &config).await; - account_for_default_network + // Create clients for all networks + let clients = create_clients( + &nymd_urls, + &api_urls, + &default_nymd_urls, + &default_api_urls, + &config, + &mnemonic, + )?; + + // Set the default account + let default_network: WalletNetwork = config::defaults::DEFAULT_NETWORK.into(); + let client_for_default_network = clients + .iter() + .find(|client| WalletNetwork::from(client.network) == default_network); + let account_for_default_network = match client_for_default_network { + Some(client) => Ok(Account::new( + client.nymd.mixnet_contract_address()?.to_string(), + client.nymd.address().to_string(), + default_network.denom().try_into()?, + )), + None => Err(BackendError::NetworkNotSupported( + config::defaults::DEFAULT_NETWORK, + )), + }; + + // Register all the clients + { + let mut w_state = state.write().await; + w_state.logout(); + } + for client in clients { + let network: WalletNetwork = client.network.into(); + let mut w_state = state.write().await; + w_state.add_client(network, client); + } + + account_for_default_network } async fn run_connection_test( - untested_nymd_urls: HashMap>, - untested_api_urls: HashMap>, - config: &Config, + untested_nymd_urls: HashMap>, + untested_api_urls: HashMap>, + config: &Config, ) -> ( - HashMap>, - HashMap>, + HashMap>, + HashMap>, ) { - let mixnet_contract_address = WalletNetwork::iter() - .map(|network| (network.into(), config.get_mixnet_contract_address(network))) - .collect::>(); + let mixnet_contract_address = WalletNetwork::iter() + .map(|network| (network.into(), config.get_mixnet_contract_address(network))) + .collect::>(); - let untested_nymd_urls = untested_nymd_urls - .into_iter() - .flat_map(|(net, urls)| urls.into_iter().map(move |url| (net.into(), url))); + let untested_nymd_urls = untested_nymd_urls + .into_iter() + .flat_map(|(net, urls)| urls.into_iter().map(move |url| (net.into(), url))); - let untested_api_urls = untested_api_urls - .into_iter() - .flat_map(|(net, urls)| urls.into_iter().map(move |url| (net.into(), url))); + let untested_api_urls = untested_api_urls + .into_iter() + .flat_map(|(net, urls)| urls.into_iter().map(move |url| (net.into(), url))); - validator_client::connection_tester::run_validator_connection_test( - untested_nymd_urls, - untested_api_urls, - mixnet_contract_address, - ) - .await + validator_client::connection_tester::run_validator_connection_test( + untested_nymd_urls, + untested_api_urls, + mixnet_contract_address, + ) + .await } fn create_clients( - nymd_urls: &HashMap>, - api_urls: &HashMap>, - default_nymd_urls: &HashMap, - default_api_urls: &HashMap, - config: &Config, - mnemonic: &Mnemonic, + nymd_urls: &HashMap>, + api_urls: &HashMap>, + default_nymd_urls: &HashMap, + default_api_urls: &HashMap, + config: &Config, + mnemonic: &Mnemonic, ) -> Result>, BackendError> { - let mut clients = Vec::new(); - for network in WalletNetwork::iter() { - let nymd_url = if let Some(url) = config.get_selected_validator_nymd_url(network) { - log::debug!("Using selected nymd_url for {network}: {url}"); - url.clone() - } else { - let default_nymd_url = default_nymd_urls - .get(&network) - .expect("Expected at least one nymd_url"); - select_random_responding_url(nymd_urls, network).unwrap_or_else(|| { - log::debug!("No successful nymd_urls for {network}: using default: {default_nymd_url}"); - default_nymd_url.clone() - }) - }; + let mut clients = Vec::new(); + for network in WalletNetwork::iter() { + let nymd_url = if let Some(url) = config.get_selected_validator_nymd_url(network) { + log::debug!("Using selected nymd_url for {network}: {url}"); + url.clone() + } else { + let default_nymd_url = default_nymd_urls + .get(&network) + .expect("Expected at least one nymd_url"); + select_random_responding_url(nymd_urls, network).unwrap_or_else(|| { + log::debug!( + "No successful nymd_urls for {network}: using default: {default_nymd_url}" + ); + default_nymd_url.clone() + }) + }; - let api_url = if let Some(url) = config.get_selected_validator_api_url(&network) { - log::debug!("Using selected api_url for {network}: {url}"); - url.clone() - } else { - let default_api_url = default_api_urls - .get(&network) - .expect("Expected at least one api url"); - select_first_responding_url(api_urls, network).unwrap_or_else(|| { - log::debug!("No passing api_urls for {network}: using default: {default_api_url}"); - default_api_url.clone() - }) - }; + let api_url = if let Some(url) = config.get_selected_validator_api_url(&network) { + log::debug!("Using selected api_url for {network}: {url}"); + url.clone() + } else { + let default_api_url = default_api_urls + .get(&network) + .expect("Expected at least one api url"); + select_first_responding_url(api_urls, network).unwrap_or_else(|| { + log::debug!("No passing api_urls for {network}: using default: {default_api_url}"); + default_api_url.clone() + }) + }; - log::info!("Connecting to: nymd_url: {nymd_url} for {network}"); - log::info!("Connecting to: api_url: {api_url} for {network}"); + log::info!("Connecting to: nymd_url: {nymd_url} for {network}"); + log::info!("Connecting to: api_url: {api_url} for {network}"); - let mut client = validator_client::Client::new_signing( - validator_client::Config::new( - network.into(), - nymd_url, - api_url, - config.get_mixnet_contract_address(network), - config.get_vesting_contract_address(network), - config.get_bandwidth_claim_contract_address(network), - ), - mnemonic.clone(), - )?; - client.set_nymd_simulated_gas_multiplier(CUSTOM_SIMULATED_GAS_MULTIPLIER); - clients.push(client); - } - Ok(clients) + let mut client = validator_client::Client::new_signing( + validator_client::Config::new( + network.into(), + nymd_url, + api_url, + config.get_mixnet_contract_address(network), + config.get_vesting_contract_address(network), + config.get_bandwidth_claim_contract_address(network), + ), + mnemonic.clone(), + )?; + client.set_nymd_simulated_gas_multiplier(CUSTOM_SIMULATED_GAS_MULTIPLIER); + clients.push(client); + } + Ok(clients) } fn select_random_responding_url( - urls: &HashMap>, - network: WalletNetwork, + urls: &HashMap>, + network: WalletNetwork, ) -> Option { - urls.get(&network.into()).and_then(|urls| { - let urls: Vec<_> = urls - .iter() - .filter_map(|(url, result)| if *result { Some(url.clone()) } else { None }) - .collect(); - urls.choose(&mut rand::thread_rng()).cloned() - }) + urls.get(&network.into()).and_then(|urls| { + let urls: Vec<_> = urls + .iter() + .filter_map(|(url, result)| if *result { Some(url.clone()) } else { None }) + .collect(); + urls.choose(&mut rand::thread_rng()).cloned() + }) } fn select_first_responding_url( - urls: &HashMap>, - network: WalletNetwork, - //config: &Config, + urls: &HashMap>, + network: WalletNetwork, + //config: &Config, ) -> Option { - urls.get(&network.into()).and_then(|urls| { - urls - .iter() - .find_map(|(url, result)| if *result { Some(url.clone()) } else { None }) - }) + urls.get(&network.into()).and_then(|urls| { + urls.iter() + .find_map(|(url, result)| if *result { Some(url.clone()) } else { None }) + }) } #[tauri::command] pub fn does_password_file_exist() -> Result { - log::info!("Checking wallet file"); - let file = wallet_storage::wallet_login_filepath()?; - if file.exists() { - log::info!("Exists: {}", file.to_string_lossy()); - Ok(true) - } else { - log::info!("Does not exist: {}", file.to_string_lossy()); - Ok(false) - } + log::info!("Checking wallet file"); + let file = wallet_storage::wallet_login_filepath()?; + if file.exists() { + log::info!("Exists: {}", file.to_string_lossy()); + Ok(true) + } else { + log::info!("Does not exist: {}", file.to_string_lossy()); + Ok(false) + } } #[tauri::command] pub fn create_password(mnemonic: &str, password: String) -> Result<(), BackendError> { - if does_password_file_exist()? { - return Err(BackendError::WalletFileAlreadyExists); - } - log::info!("Creating password"); + if does_password_file_exist()? { + return Err(BackendError::WalletFileAlreadyExists); + } + log::info!("Creating password"); - let mnemonic = Mnemonic::from_str(mnemonic)?; - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - // Currently we only support a single, default, login id in the wallet - let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string()); - let password = wallet_storage::UserPassword::new(password); - wallet_storage::store_login_with_multiple_accounts(mnemonic, hd_path, login_id, &password) + let mnemonic = Mnemonic::from_str(mnemonic)?; + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + // Currently we only support a single, default, login id in the wallet + let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string()); + let password = wallet_storage::UserPassword::new(password); + wallet_storage::store_login_with_multiple_accounts(mnemonic, hd_path, login_id, &password) } #[tauri::command] pub async fn sign_in_with_password( - password: String, - state: tauri::State<'_, Arc>>, + password: String, + state: tauri::State<'_, Arc>>, ) -> Result { - log::info!("Signing in with password"); + log::info!("Signing in with password"); - // Currently we only support a single, default, id in the wallet - let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string()); - let password = wallet_storage::UserPassword::new(password); - let stored_login = wallet_storage::load_existing_login(&login_id, &password)?; + // Currently we only support a single, default, id in the wallet + let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string()); + let password = wallet_storage::UserPassword::new(password); + let stored_login = wallet_storage::load_existing_login(&login_id, &password)?; - let mnemonic = extract_first_mnemonic(&stored_login)?; - let first_login_id_when_converting = login_id.into(); - set_state_with_all_accounts(stored_login, first_login_id_when_converting, state.clone()).await?; + let mnemonic = extract_first_mnemonic(&stored_login)?; + let first_login_id_when_converting = login_id.into(); + set_state_with_all_accounts(stored_login, first_login_id_when_converting, state.clone()) + .await?; - _connect_with_mnemonic(mnemonic, state).await + _connect_with_mnemonic(mnemonic, state).await } fn extract_first_mnemonic( - stored_login: &wallet_storage::StoredLogin, + stored_login: &wallet_storage::StoredLogin, ) -> Result { - let mnemonic = match stored_login { - wallet_storage::StoredLogin::Mnemonic(ref account) => account.mnemonic().clone(), - wallet_storage::StoredLogin::Multiple(ref accounts) => { - // Login using the first account in the list - accounts - .get_accounts() - .next() - .ok_or(BackendError::WalletNoSuchAccountIdInWalletLogin)? - .mnemonic() - .clone() - } - }; + let mnemonic = match stored_login { + wallet_storage::StoredLogin::Mnemonic(ref account) => account.mnemonic().clone(), + wallet_storage::StoredLogin::Multiple(ref accounts) => { + // Login using the first account in the list + accounts + .get_accounts() + .next() + .ok_or(BackendError::WalletNoSuchAccountIdInWalletLogin)? + .mnemonic() + .clone() + } + }; - Ok(mnemonic) + Ok(mnemonic) } #[tauri::command] pub async fn sign_in_with_password_and_account_id( - account_id: &str, - password: &str, - state: tauri::State<'_, Arc>>, + account_id: &str, + password: &str, + state: tauri::State<'_, Arc>>, ) -> Result { - log::info!("Signing in with password"); + log::info!("Signing in with password"); - // Currently we only support a single, default, id in the wallet - let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string()); - let account_id = wallet_storage::AccountId::new(account_id.to_string()); - let password = wallet_storage::UserPassword::new(password.to_string()); - let stored_login = wallet_storage::load_existing_login(&login_id, &password)?; + // Currently we only support a single, default, id in the wallet + let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string()); + let account_id = wallet_storage::AccountId::new(account_id.to_string()); + let password = wallet_storage::UserPassword::new(password.to_string()); + let stored_login = wallet_storage::load_existing_login(&login_id, &password)?; - let mnemonic = extract_mnemonic(&stored_login, &account_id)?; - let first_login_id_when_converting = login_id.into(); - set_state_with_all_accounts(stored_login, first_login_id_when_converting, state.clone()).await?; + let mnemonic = extract_mnemonic(&stored_login, &account_id)?; + let first_login_id_when_converting = login_id.into(); + set_state_with_all_accounts(stored_login, first_login_id_when_converting, state.clone()) + .await?; - _connect_with_mnemonic(mnemonic, state).await + _connect_with_mnemonic(mnemonic, state).await } fn extract_mnemonic( - stored_login: &wallet_storage::StoredLogin, - account_id: &wallet_storage::AccountId, + stored_login: &wallet_storage::StoredLogin, + account_id: &wallet_storage::AccountId, ) -> Result { - let mnemonic = match stored_login { - wallet_storage::StoredLogin::Mnemonic(_) => { - return Err(BackendError::WalletNoSuchAccountIdInWalletLogin); - } - wallet_storage::StoredLogin::Multiple(ref accounts) => accounts - .get_account(account_id) - .ok_or(BackendError::WalletNoSuchAccountIdInWalletLogin)? - .mnemonic() - .clone(), - }; - Ok(mnemonic) + let mnemonic = match stored_login { + wallet_storage::StoredLogin::Mnemonic(_) => { + return Err(BackendError::WalletNoSuchAccountIdInWalletLogin); + } + wallet_storage::StoredLogin::Multiple(ref accounts) => accounts + .get_account(account_id) + .ok_or(BackendError::WalletNoSuchAccountIdInWalletLogin)? + .mnemonic() + .clone(), + }; + Ok(mnemonic) } #[tauri::command] pub fn remove_password() -> Result<(), BackendError> { - log::info!("Removing password"); - let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string()); - wallet_storage::remove_login(&login_id) + log::info!("Removing password"); + let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string()); + wallet_storage::remove_login(&login_id) } #[tauri::command] pub async fn add_account_for_password( - mnemonic: &str, - password: &str, - account_id: &str, - state: tauri::State<'_, Arc>>, + mnemonic: &str, + password: &str, + account_id: &str, + state: tauri::State<'_, Arc>>, ) -> Result { - log::info!("Adding account for the current password: {account_id}"); - let mnemonic = Mnemonic::from_str(mnemonic)?; - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - // Currently we only support a single, default, login id in the wallet - let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string()); - let account_id = wallet_storage::AccountId::new(account_id.to_string()); - let password = wallet_storage::UserPassword::new(password.to_string()); + log::info!("Adding account for the current password: {account_id}"); + let mnemonic = Mnemonic::from_str(mnemonic)?; + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + // Currently we only support a single, default, login id in the wallet + let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string()); + let account_id = wallet_storage::AccountId::new(account_id.to_string()); + let password = wallet_storage::UserPassword::new(password.to_string()); - wallet_storage::append_account_to_login( - mnemonic.clone(), - hd_path, - login_id.clone(), - account_id.clone(), - &password, - )?; + wallet_storage::append_account_to_login( + mnemonic.clone(), + hd_path, + login_id.clone(), + account_id.clone(), + &password, + )?; - let address = { - let state = state.read().await; - let network: Network = state.current_network().into(); - derive_address(mnemonic, network.bech32_prefix())?.to_string() - }; + let address = { + let state = state.read().await; + let network: Network = state.current_network().into(); + derive_address(mnemonic, network.bech32_prefix())?.to_string() + }; - // Re-read all the acccounts from the wallet to reset the state, rather than updating it - // incrementally - let stored_login = wallet_storage::load_existing_login(&login_id, &password)?; - // NOTE: since we are appending, this id shouldn't be needed, but setting the state is supposed - // to be a general function - let first_id_when_converting = login_id.into(); - set_state_with_all_accounts(stored_login, first_id_when_converting, state).await?; + // Re-read all the acccounts from the wallet to reset the state, rather than updating it + // incrementally + let stored_login = wallet_storage::load_existing_login(&login_id, &password)?; + // NOTE: since we are appending, this id shouldn't be needed, but setting the state is supposed + // to be a general function + let first_id_when_converting = login_id.into(); + set_state_with_all_accounts(stored_login, first_id_when_converting, state).await?; - Ok(AccountEntry { - id: account_id.to_string(), - address, - }) + Ok(AccountEntry { + id: account_id.to_string(), + address, + }) } // The first `AccoundId` when converting is the `LoginId` for the entry that was loaded. async fn set_state_with_all_accounts( - stored_login: wallet_storage::StoredLogin, - first_id_when_converting: wallet_storage::AccountId, - state: tauri::State<'_, Arc>>, + stored_login: wallet_storage::StoredLogin, + first_id_when_converting: wallet_storage::AccountId, + state: tauri::State<'_, Arc>>, ) -> Result<(), BackendError> { - log::trace!("Set state with accounts:"); - let all_accounts: Vec<_> = stored_login - .unwrap_into_multiple_accounts(first_id_when_converting) - .into_accounts() - .collect(); + log::trace!("Set state with accounts:"); + let all_accounts: Vec<_> = stored_login + .unwrap_into_multiple_accounts(first_id_when_converting) + .into_accounts() + .collect(); - for account in &all_accounts { - log::trace!("account: {:?}", account.id()); - } + for account in &all_accounts { + log::trace!("account: {:?}", account.id()); + } - let all_account_ids: Vec = all_accounts - .iter() - .map(|account| { - let mnemonic = account.mnemonic(); - let addresses: HashMap = WalletNetwork::iter() - .map(|network| { - let config_network: Network = network.into(); - ( - network, - derive_address(mnemonic.clone(), config_network.bech32_prefix()).unwrap(), - ) + let all_account_ids: Vec = all_accounts + .iter() + .map(|account| { + let mnemonic = account.mnemonic(); + let addresses: HashMap = WalletNetwork::iter() + .map(|network| { + let config_network: Network = network.into(); + ( + network, + derive_address(mnemonic.clone(), config_network.bech32_prefix()).unwrap(), + ) + }) + .collect(); + WalletAccountIds { + id: account.id().clone(), + addresses, + } }) .collect(); - WalletAccountIds { - id: account.id().clone(), - addresses, - } - }) - .collect(); - let mut w_state = state.write().await; - w_state.set_all_accounts(all_account_ids); - Ok(()) + let mut w_state = state.write().await; + w_state.set_all_accounts(all_account_ids); + Ok(()) } #[tauri::command] pub async fn remove_account_for_password( - password: &str, - account_id: &str, - state: tauri::State<'_, Arc>>, + password: &str, + account_id: &str, + state: tauri::State<'_, Arc>>, ) -> Result<(), BackendError> { - log::info!("Removing account: {account_id}"); - // Currently we only support a single, default, id in the wallet - let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string()); - let account_id = wallet_storage::AccountId::new(account_id.to_string()); - let password = wallet_storage::UserPassword::new(password.to_string()); - wallet_storage::remove_account_from_login(&login_id, &account_id, &password)?; + log::info!("Removing account: {account_id}"); + // Currently we only support a single, default, id in the wallet + let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string()); + let account_id = wallet_storage::AccountId::new(account_id.to_string()); + let password = wallet_storage::UserPassword::new(password.to_string()); + wallet_storage::remove_account_from_login(&login_id, &account_id, &password)?; - // Load to reset the internal state - let stored_login = wallet_storage::load_existing_login(&login_id, &password)?; - // NOTE: Since we removed from a multi-account login, this id shouldn't be needed, but setting - // the state is supposed to be a general function - let first_account_id_when_converting = login_id.into(); - set_state_with_all_accounts(stored_login, first_account_id_when_converting, state).await + // Load to reset the internal state + let stored_login = wallet_storage::load_existing_login(&login_id, &password)?; + // NOTE: Since we removed from a multi-account login, this id shouldn't be needed, but setting + // the state is supposed to be a general function + let first_account_id_when_converting = login_id.into(); + set_state_with_all_accounts(stored_login, first_account_id_when_converting, state).await } fn derive_address( - mnemonic: bip39::Mnemonic, - prefix: &str, + mnemonic: bip39::Mnemonic, + prefix: &str, ) -> Result { - DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic)? - .try_derive_accounts()? - .first() - .map(AccountData::address) - .cloned() - .ok_or(BackendError::FailedToDeriveAddress) + DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic)? + .try_derive_accounts()? + .first() + .map(AccountData::address) + .cloned() + .ok_or(BackendError::FailedToDeriveAddress) } #[tauri::command] pub async fn list_accounts( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, Arc>>, ) -> Result, BackendError> { - log::trace!("Listing accounts"); - let state = state.read().await; - let network = state.current_network(); + log::trace!("Listing accounts"); + let state = state.read().await; + let network = state.current_network(); - let all_accounts = state - .get_all_accounts() - .map(|account| AccountEntry { - id: account.id.to_string(), - address: account.addresses[&network].to_string(), - }) - .map(|account| { - log::trace!("{:?}", account); - account - }) - .collect(); + let all_accounts = state + .get_all_accounts() + .map(|account| AccountEntry { + id: account.id.to_string(), + address: account.addresses[&network].to_string(), + }) + .map(|account| { + log::trace!("{:?}", account); + account + }) + .collect(); - Ok(all_accounts) + Ok(all_accounts) } #[tauri::command] pub fn show_mnemonic_for_account_in_password( - account_id: String, - password: String, + account_id: String, + password: String, ) -> Result { - log::info!("Getting mnemonic for: {account_id}"); - let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string()); - let account_id = wallet_storage::AccountId::new(account_id); - let password = wallet_storage::UserPassword::new(password); - let mnemonic = _show_mnemonic_for_account_in_password(&login_id, &account_id, &password)?; - Ok(mnemonic.to_string()) + log::info!("Getting mnemonic for: {account_id}"); + let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string()); + let account_id = wallet_storage::AccountId::new(account_id); + let password = wallet_storage::UserPassword::new(password); + let mnemonic = _show_mnemonic_for_account_in_password(&login_id, &account_id, &password)?; + Ok(mnemonic.to_string()) } fn _show_mnemonic_for_account_in_password( - login_id: &wallet_storage::LoginId, - account_id: &wallet_storage::AccountId, - password: &wallet_storage::UserPassword, + login_id: &wallet_storage::LoginId, + account_id: &wallet_storage::AccountId, + password: &wallet_storage::UserPassword, ) -> Result { - let stored_account = wallet_storage::load_existing_login(login_id, password)?; - let mnemonic = match stored_account { - wallet_storage::StoredLogin::Mnemonic(ref account) => account.mnemonic().clone(), - wallet_storage::StoredLogin::Multiple(ref accounts) => { - for account in accounts.get_accounts() { - log::debug!("{:?}", account); - } - accounts - .get_account(account_id) - .ok_or(BackendError::WalletNoSuchAccountIdInWalletLogin)? - .mnemonic() - .clone() - } - }; - Ok(mnemonic) + let stored_account = wallet_storage::load_existing_login(login_id, password)?; + let mnemonic = match stored_account { + wallet_storage::StoredLogin::Mnemonic(ref account) => account.mnemonic().clone(), + wallet_storage::StoredLogin::Multiple(ref accounts) => { + for account in accounts.get_accounts() { + log::debug!("{:?}", account); + } + accounts + .get_account(account_id) + .ok_or(BackendError::WalletNoSuchAccountIdInWalletLogin)? + .mnemonic() + .clone() + } + }; + Ok(mnemonic) } #[cfg(test)] mod tests { - use std::path::PathBuf; + use std::path::PathBuf; - use crate::wallet_storage::{ - self, - account_data::{MnemonicAccount, WalletAccount}, - }; + use crate::wallet_storage::{ + self, + account_data::{MnemonicAccount, WalletAccount}, + }; - use super::*; + use super::*; - // This decryptes a stored wallet file using the same procedure as when signing in. Most tests - // related to the encryped wallet storage is in `wallet_storage`. - #[test] - fn decrypt_stored_wallet_for_sign_in() { - const SAVED_WALLET: &str = "src/wallet_storage/test-data/saved-wallet.json"; - let wallet_file = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(SAVED_WALLET); - let login_id = wallet_storage::LoginId::new("first".to_string()); - let account_id = wallet_storage::AccountId::new("first".to_string()); - let password = wallet_storage::UserPassword::new("password".to_string()); - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + // This decryptes a stored wallet file using the same procedure as when signing in. Most tests + // related to the encryped wallet storage is in `wallet_storage`. + #[test] + fn decrypt_stored_wallet_for_sign_in() { + const SAVED_WALLET: &str = "src/wallet_storage/test-data/saved-wallet.json"; + let wallet_file = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(SAVED_WALLET); + let login_id = wallet_storage::LoginId::new("first".to_string()); + let account_id = wallet_storage::AccountId::new("first".to_string()); + let password = wallet_storage::UserPassword::new("password".to_string()); + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let stored_login = - wallet_storage::load_existing_login_at_file(&wallet_file, &login_id, &password).unwrap(); - let mnemonic = extract_first_mnemonic(&stored_login).unwrap(); + let stored_login = + wallet_storage::load_existing_login_at_file(&wallet_file, &login_id, &password) + .unwrap(); + let mnemonic = extract_first_mnemonic(&stored_login).unwrap(); - let expected_mnemonic = bip39::Mnemonic::from_str("country mean universe text phone begin deputy reject result good cram illness common cluster proud swamp digital patrol spread bar face december base kick").unwrap(); - assert_eq!(mnemonic, expected_mnemonic); + let expected_mnemonic = bip39::Mnemonic::from_str("country mean universe text phone begin deputy reject result good cram illness common cluster proud swamp digital patrol spread bar face december base kick").unwrap(); + assert_eq!(mnemonic, expected_mnemonic); - let all_accounts: Vec<_> = stored_login - .unwrap_into_multiple_accounts(account_id.clone()) - .into_accounts() - .collect(); + let all_accounts: Vec<_> = stored_login + .unwrap_into_multiple_accounts(account_id.clone()) + .into_accounts() + .collect(); - assert_eq!( - all_accounts, - vec![WalletAccount::new( - account_id, - MnemonicAccount::new(expected_mnemonic, hd_path), - )] - ); - } + assert_eq!( + all_accounts, + vec![WalletAccount::new( + account_id, + MnemonicAccount::new(expected_mnemonic, hd_path), + )] + ); + } - #[test] - fn decrypt_stored_wallet_multiple_for_sign_in() { - // WIP(JON): same as above but with file containing multiple accounts - } + #[test] + fn decrypt_stored_wallet_multiple_for_sign_in() { + // WIP(JON): same as above but with file containing multiple accounts + } } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/admin.rs b/nym-wallet/src-tauri/src/operations/mixnet/admin.rs index f1edec9653..abfa3a48b5 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/admin.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/admin.rs @@ -13,29 +13,29 @@ use crate::state::State; #[tauri::command] pub async fn get_contract_settings( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, Arc>>, ) -> Result { - log::info!(">>> Getting contract settings"); - let res = nymd_client!(state).get_contract_settings().await?.into(); - log::trace!("<<< {:?}", res); - Ok(res) + log::info!(">>> Getting contract settings"); + let res = nymd_client!(state).get_contract_settings().await?.into(); + log::trace!("<<< {:?}", res); + Ok(res) } #[tauri::command] pub async fn update_contract_settings( - params: TauriContractStateParams, - fee: Option, - state: tauri::State<'_, Arc>>, + params: TauriContractStateParams, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - let mixnet_contract_settings_params: ContractStateParams = params.try_into()?; - log::info!( - ">>> Updating contract settings: {:?}", - mixnet_contract_settings_params - ); - nymd_client!(state) - .update_contract_settings(mixnet_contract_settings_params.clone(), fee) - .await?; - let res = mixnet_contract_settings_params.into(); - log::trace!("<<< {:?}", res); - Ok(res) + let mixnet_contract_settings_params: ContractStateParams = params.try_into()?; + log::info!( + ">>> Updating contract settings: {:?}", + mixnet_contract_settings_params + ); + nymd_client!(state) + .update_contract_settings(mixnet_contract_settings_params.clone(), fee) + .await?; + let res = mixnet_contract_settings_params.into(); + log::trace!("<<< {:?}", res); + Ok(res) } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs index bf31054e2f..7c20d1bab6 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs @@ -8,167 +8,167 @@ use nym_types::mixnode::MixNodeBond; use nym_types::transaction::TransactionExecuteResult; use std::sync::Arc; use tokio::sync::RwLock; -use validator_client::nymd::Fee; +use validator_client::nymd::{CosmWasmCoin, Fee}; #[tauri::command] pub async fn bond_gateway( - gateway: Gateway, - pledge: MajorCurrencyAmount, - owner_signature: String, - fee: Option, - state: tauri::State<'_, Arc>>, + gateway: Gateway, + pledge: MajorCurrencyAmount, + owner_signature: String, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - let denom_minor = state.read().await.current_network().denom(); - let pledge_minor = pledge.clone().into_minor_cosmwasm_coin()?; - log::info!( - ">>> Bond gateway: identity_key = {}, pledge = {}, pledge_minor = {}, fee = {:?}", - &gateway.identity_key, - pledge, - &pledge_minor, - fee, - ); - let res = nymd_client!(state) - .bond_gateway(gateway, owner_signature, pledge_minor, fee) - .await?; - log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); - Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), - )?) + let denom_minor = state.read().await.current_network().denom(); + let pledge_minor = pledge.clone().into(); + log::info!( + ">>> Bond gateway: identity_key = {}, pledge = {}, pledge_minor = {}, fee = {:?}", + &gateway.identity_key, + pledge, + &pledge_minor, + fee, + ); + let res = nymd_client!(state) + .bond_gateway(gateway, owner_signature, pledge_minor, fee) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) } #[tauri::command] pub async fn unbond_gateway( - fee: Option, - state: tauri::State<'_, Arc>>, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - let denom_minor = state.read().await.current_network().denom(); - log::info!(">>> Unbond gateway, fee = {:?}", fee); - let res = nymd_client!(state).unbond_gateway(fee).await?; - log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); - Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), - )?) + let denom_minor = state.read().await.current_network().denom(); + log::info!(">>> Unbond gateway, fee = {:?}", fee); + let res = nymd_client!(state).unbond_gateway(fee).await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) } #[tauri::command] pub async fn bond_mixnode( - mixnode: MixNode, - owner_signature: String, - pledge: MajorCurrencyAmount, - fee: Option, - state: tauri::State<'_, Arc>>, + mixnode: MixNode, + owner_signature: String, + pledge: MajorCurrencyAmount, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - let denom_minor = state.read().await.current_network().denom(); - let pledge_minor = pledge.clone().into_minor_cosmwasm_coin()?; - log::info!( - ">>> Bond mixnode: identity_key = {}, pledge = {}, pledge_minor = {}, fee = {:?}", - mixnode.identity_key, - pledge, - pledge_minor, - fee, - ); - let res = nymd_client!(state) - .bond_mixnode(mixnode, owner_signature, pledge_minor, fee) - .await?; - log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); - Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), - )?) + let denom_minor = state.read().await.current_network().denom(); + let pledge_minor = pledge.clone().into(); + log::info!( + ">>> Bond mixnode: identity_key = {}, pledge = {}, pledge_minor = {}, fee = {:?}", + mixnode.identity_key, + pledge, + pledge_minor, + fee, + ); + let res = nymd_client!(state) + .bond_mixnode(mixnode, owner_signature, pledge_minor, fee) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) } #[tauri::command] pub async fn unbond_mixnode( - fee: Option, - state: tauri::State<'_, Arc>>, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - let denom_minor = state.read().await.current_network().denom(); - log::info!(">>> Unbond mixnode, fee = {:?}", fee); - let res = nymd_client!(state).unbond_mixnode(fee).await?; - log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); - Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), - )?) + let denom_minor = state.read().await.current_network().denom(); + log::info!(">>> Unbond mixnode, fee = {:?}", fee); + let res = nymd_client!(state).unbond_mixnode(fee).await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) } #[tauri::command] pub async fn update_mixnode( - profit_margin_percent: u8, - fee: Option, - state: tauri::State<'_, Arc>>, + profit_margin_percent: u8, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - let denom_minor = state.read().await.current_network().denom(); - log::info!( - ">>> Update mixnode: profit_margin_percent = {}, fee {:?}", - profit_margin_percent, - fee, - ); - let res = nymd_client!(state) - .update_mixnode_config(profit_margin_percent, fee) - .await?; - log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); - Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), - )?) + let denom_minor = state.read().await.current_network().denom(); + log::info!( + ">>> Update mixnode: profit_margin_percent = {}, fee {:?}", + profit_margin_percent, + fee, + ); + let res = nymd_client!(state) + .update_mixnode_config(profit_margin_percent, fee) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) } #[tauri::command] pub async fn mixnode_bond_details( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, Arc>>, ) -> Result, BackendError> { - log::info!(">>> Get mixnode bond details"); - let guard = state.read().await; - let client = guard.current_client()?; - let bond = client.nymd.owns_mixnode(client.nymd.address()).await?; - let res = MixNodeBond::from_mixnet_contract_mixnode_bond(bond)?; - log::info!( - "<<< identity_key = {:?}", - res.as_ref().map(|r| r.mix_node.identity_key.to_string()) - ); - log::trace!("<<< {:?}", res); - Ok(res) + log::info!(">>> Get mixnode bond details"); + let guard = state.read().await; + let client = guard.current_client()?; + let bond = client.nymd.owns_mixnode(client.nymd.address()).await?; + let res = MixNodeBond::from_mixnet_contract_mixnode_bond(bond)?; + log::info!( + "<<< identity_key = {:?}", + res.as_ref().map(|r| r.mix_node.identity_key.to_string()) + ); + log::trace!("<<< {:?}", res); + Ok(res) } #[tauri::command] pub async fn gateway_bond_details( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, Arc>>, ) -> Result, BackendError> { - log::info!(">>> Get gateway bond details"); - let guard = state.read().await; - let client = guard.current_client()?; - let bond = client.nymd.owns_gateway(client.nymd.address()).await?; - let res = GatewayBond::from_mixnet_contract_gateway_bond(bond)?; - log::info!( - "<<< identity_key = {:?}", - res.as_ref().map(|r| r.gateway.identity_key.to_string()) - ); - log::trace!("<<< {:?}", res); - Ok(res) + log::info!(">>> Get gateway bond details"); + let guard = state.read().await; + let client = guard.current_client()?; + let bond = client.nymd.owns_gateway(client.nymd.address()).await?; + let res = GatewayBond::from_mixnet_contract_gateway_bond(bond)?; + log::info!( + "<<< identity_key = {:?}", + res.as_ref().map(|r| r.gateway.identity_key.to_string()) + ); + log::trace!("<<< {:?}", res); + Ok(res) } #[tauri::command] pub async fn get_operator_rewards( - address: String, - state: tauri::State<'_, Arc>>, + address: String, + state: tauri::State<'_, Arc>>, ) -> Result { - log::info!(">>> Get operator rewards for {}", address); - let denom = state.read().await.current_network().denom(); - let rewards_as_minor = nymd_client!(state).get_operator_rewards(address).await?; - let amount: MajorCurrencyAmount = - MajorCurrencyAmount::from_minor_uint128_and_denom(rewards_as_minor, &denom.to_string())?; - log::info!( - "<<< rewards_as_minor = {}, amount = {}", - rewards_as_minor, - amount - ); - Ok(amount) + log::info!(">>> Get operator rewards for {}", address); + let denom = state.read().await.current_network().denom(); + let rewards_as_minor = nymd_client!(state).get_operator_rewards(address).await?; + let coin = CosmWasmCoin::new(rewards_as_minor.u128(), denom.as_ref()); + let amount: MajorCurrencyAmount = coin.into(); + log::info!( + "<<< rewards_as_minor = {}, amount = {}", + rewards_as_minor, + amount + ); + Ok(amount) } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs index 5c89ec2f74..8b02326e61 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs @@ -1,373 +1,378 @@ -use std::collections::HashMap; -use std::sync::Arc; - -use cosmwasm_std::Coin as CosmWasmCoin; -use tokio::sync::RwLock; - -use mixnet_contract_common::IdentityKey; -use nym_types::currency::{CurrencyDenom, MajorCurrencyAmount}; -use nym_types::delegation::{ - from_contract_delegation_events, Delegation, DelegationEvent, DelegationRecord, - DelegationWithEverything, DelegationsSummaryResponse, -}; -use nym_types::transaction::TransactionExecuteResult; -use validator_client::nymd::Fee; - use crate::error::BackendError; use crate::state::State; use crate::vesting::delegate::get_pending_vesting_delegation_events; use crate::{api_client, nymd_client}; +use cosmwasm_std::Coin as CosmWasmCoin; +use mixnet_contract_common::IdentityKey; +use nym_types::currency::{CurrencyDenom, MajorCurrencyAmount}; +use nym_types::delegation::{ + from_contract_delegation_events, Delegation, DelegationEvent, DelegationRecord, + DelegationWithEverything, DelegationsSummaryResponse, +}; +use nym_types::transaction::TransactionExecuteResult; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; +use validator_client::nymd::Fee; #[tauri::command] pub async fn get_pending_delegation_events( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, Arc>>, ) -> Result, BackendError> { - log::info!(">>> Get pending delegation events"); - let events = nymd_client!(state) - .get_pending_delegation_events(nymd_client!(state).address().to_string(), None) - .await?; - log::info!("<<< {} pending delegation events", events.len()); - log::trace!("<<< pending delegation events = {:?}", events); + log::info!(">>> Get pending delegation events"); + let events = nymd_client!(state) + .get_pending_delegation_events(nymd_client!(state).address().to_string(), None) + .await?; + log::info!("<<< {} pending delegation events", events.len()); + log::trace!("<<< pending delegation events = {:?}", events); - match from_contract_delegation_events(events) { - Ok(res) => Ok(res), - Err(e) => Err(e.into()), - } + match from_contract_delegation_events(events) { + Ok(res) => Ok(res), + Err(e) => Err(e.into()), + } } #[tauri::command] pub async fn delegate_to_mixnode( - identity: &str, - amount: MajorCurrencyAmount, - fee: Option, - state: tauri::State<'_, Arc>>, + identity: &str, + amount: MajorCurrencyAmount, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - let denom_minor = state.read().await.current_network().denom(); - let delegation: CosmWasmCoin = amount.clone().into_minor_cosmwasm_coin()?; - log::info!( - ">>> Delegate to mixnode: identity_key = {}, amount = {}, minor_amount = {}, fee = {:?}", - identity, - amount, - delegation, - fee, - ); - let res = nymd_client!(state) - .delegate_to_mixnode(identity, &delegation, fee) - .await?; - log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); - Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), - )?) + let denom_minor = state.read().await.current_network().denom(); + let delegation = amount.clone().into(); + log::info!( + ">>> Delegate to mixnode: identity_key = {}, amount = {}, minor_amount = {}, fee = {:?}", + identity, + amount, + delegation, + fee, + ); + let res = nymd_client!(state) + .delegate_to_mixnode(identity, delegation, fee) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) } #[tauri::command] pub async fn undelegate_from_mixnode( - identity: &str, - fee: Option, - state: tauri::State<'_, Arc>>, + identity: &str, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - let denom_minor = state.read().await.current_network().denom(); - log::info!( - ">>> Undelegate from mixnode: identity_key = {}, fee = {:?}", - identity, - fee - ); - let res = nymd_client!(state) - .remove_mixnode_delegation(identity, fee) - .await?; - log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); - Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), - )?) + let denom_minor = state.read().await.current_network().denom(); + log::info!( + ">>> Undelegate from mixnode: identity_key = {}, fee = {:?}", + identity, + fee + ); + let res = nymd_client!(state) + .remove_mixnode_delegation(identity, fee) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) } struct DelegationWithHistory { - pub delegation: Delegation, - pub amount_sum: MajorCurrencyAmount, - pub history: Vec, + pub delegation: Delegation, + pub amount_sum: MajorCurrencyAmount, + pub history: Vec, } #[tauri::command] pub async fn get_all_mix_delegations( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, Arc>>, ) -> Result, BackendError> { - log::info!(">>> Get all mixnode delegations"); + todo!("figure out this one after merging") - // TODO: add endpoint to validator API to get a single mix node bond - let mixnodes = api_client!(state).get_mixnodes().await?; - - let address = nymd_client!(state).address().to_string(); - - let denom_minor = state.read().await.current_network().denom(); - let denom: CurrencyDenom = denom_minor.clone().try_into()?; - - log::info!(" >>> Get delegations"); - let delegations = nymd_client!(state) - .get_delegator_delegations_paged(address.clone(), None, None) // get all delegations, ignoring paging - .await? - .delegations; - log::info!(" <<< {} delegations", delegations.len()); - - // first get pending events from the mixnet contract (operations made with unlocked tokens) - let mut pending_events_for_account = get_pending_delegation_events(state.clone()).await?; - - // then get pending events from the vesting contract (operations made with locked tokens) - let pending_vesting_events = get_pending_vesting_delegation_events(state.clone()).await?; - for event in pending_vesting_events { - pending_events_for_account.push(event); - } - - log::info!( - " <<< {} pending delegation events for account", - pending_events_for_account.len() - ); - - let mut map: HashMap = HashMap::new(); - - for pending_event in pending_events_for_account.clone() { - if delegations - .iter() - .any(|d| d.node_identity == pending_event.node_identity) - { - let amount = pending_event - .amount - .unwrap_or_else(|| MajorCurrencyAmount::zero(&denom)); - let delegation = DelegationWithHistory { - delegation: Delegation { - amount: amount.clone(), - node_identity: pending_event.node_identity, - proxy: None, - owner: pending_event.address, - block_height: pending_event.block_height, - }, - amount_sum: amount, - history: vec![], - }; - map.insert(delegation.delegation.node_identity.clone(), delegation); - } - } - - for d in delegations { - // create record of delegation - let delegated_on_iso_datetime = nymd_client!(state) - .get_block_timestamp(Some(d.block_height as u32)) - .await? - .to_rfc3339(); - let amount: MajorCurrencyAmount = d.amount.clone().try_into()?; - let record = DelegationRecord { - amount: amount.clone(), - block_height: d.block_height, - delegated_on_iso_datetime, - }; - - let entry = map - .entry(d.node_identity.clone()) - .or_insert(DelegationWithHistory { - delegation: d.try_into()?, - history: vec![], - amount_sum: MajorCurrencyAmount::zero(&amount.denom), - }); - - entry.history.push(record); - entry.amount_sum = entry.amount_sum.clone() + amount; - } - - let mut with_everything: Vec = vec![]; - - for item in map { - let d = item.1.delegation; - let history = item.1.history; - let Delegation { - owner, - node_identity, - amount, - block_height, - proxy, - } = d; - - log::trace!( - " --- Delegation: node_identity = {}, amount = {}", - node_identity, - amount - ); - - let mixnode = mixnodes - .iter() - .find(|m| m.mix_node.identity_key == node_identity); - - let pledge_amount: Option = - mixnode.and_then(|m| m.pledge_amount.clone().try_into().ok()); - - let total_delegation: Option = - mixnode.and_then(|m| m.total_delegation.clone().try_into().ok()); - - let profit_margin_percent: Option = mixnode.map(|m| m.mix_node.profit_margin_percent); - - log::trace!(" >>> Get accumulated rewards: address = {}", address); - let accumulated_rewards = match nymd_client!(state) - .get_delegator_rewards(address.clone(), node_identity.clone(), proxy.clone()) - .await - { - Ok(rewards) => { - let amount = - MajorCurrencyAmount::from_minor_uint128_and_denom(rewards, denom_minor.as_ref())?; - log::trace!(" <<< rewards = {}, amount = {}", rewards, amount); - Some(amount) - } - Err(_) => { - log::trace!(" <<< no rewards waiting"); - None - } - }; - - let pending_events = filter_pending_events(&node_identity, pending_events_for_account.clone()); - log::trace!( - " --- pending events for mixnode = {}", - pending_events.len() - ); - - log::trace!( - " >>> Get stake saturation: node_identity = {}", - node_identity - ); - let stake_saturation = api_client!(state) - .get_mixnode_stake_saturation(&node_identity) - .await - .ok() - .map(|r| r.saturation); - log::trace!(" <<< {:?}", stake_saturation); - - log::trace!( - " >>> Get average uptime percentage: node_identity = {}", - node_identity - ); - let avg_uptime_percent = api_client!(state) - .get_mixnode_avg_uptime(&node_identity) - .await - .ok() - .map(|r| r.avg_uptime); - log::trace!(" <<< {:?}", avg_uptime_percent); - - log::trace!( - " >>> Convert delegated on block height to timestamp: block_height = {}", - d.block_height - ); - let timestamp = nymd_client!(state) - .get_block_timestamp(Some(d.block_height as u32)) - .await?; - let delegated_on_iso_datetime = timestamp.to_rfc3339(); - log::trace!( - " <<< timestamp = {:?}, delegated_on_iso_datetime = {:?}", - timestamp, - delegated_on_iso_datetime - ); - - with_everything.push(DelegationWithEverything { - owner: owner.to_string(), - node_identity: node_identity.to_string(), - amount: item.1.amount_sum, - block_height, - proxy: proxy.clone(), - delegated_on_iso_datetime, - stake_saturation, - accumulated_rewards, - profit_margin_percent, - pledge_amount, - avg_uptime_percent, - total_delegation, - pending_events, - history, - }) - } - log::trace!("<<< {:?}", with_everything); - - Ok(with_everything) + // log::info!(">>> Get all mixnode delegations"); + // + // // TODO: add endpoint to validator API to get a single mix node bond + // let mixnodes = api_client!(state).get_mixnodes().await?; + // + // let address = nymd_client!(state).address().to_string(); + // + // let denom_minor = state.read().await.current_network().denom(); + // let denom: CurrencyDenom = denom_minor.clone().try_into()?; + // + // log::info!(" >>> Get delegations"); + // let delegations = nymd_client!(state) + // .get_delegator_delegations_paged(address.clone(), None, None) // get all delegations, ignoring paging + // .await? + // .delegations; + // log::info!(" <<< {} delegations", delegations.len()); + // + // // first get pending events from the mixnet contract (operations made with unlocked tokens) + // let mut pending_events_for_account = get_pending_delegation_events(state.clone()).await?; + // + // // then get pending events from the vesting contract (operations made with locked tokens) + // let pending_vesting_events = get_pending_vesting_delegation_events(state.clone()).await?; + // for event in pending_vesting_events { + // pending_events_for_account.push(event); + // } + // + // log::info!( + // " <<< {} pending delegation events for account", + // pending_events_for_account.len() + // ); + // + // let mut map: HashMap = HashMap::new(); + // + // for pending_event in pending_events_for_account.clone() { + // if delegations + // .iter() + // .any(|d| d.node_identity == pending_event.node_identity) + // { + // let amount = pending_event + // .amount + // .unwrap_or_else(|| MajorCurrencyAmount::zero(&denom)); + // let delegation = DelegationWithHistory { + // delegation: Delegation { + // amount: amount.clone(), + // node_identity: pending_event.node_identity, + // proxy: None, + // owner: pending_event.address, + // block_height: pending_event.block_height, + // }, + // amount_sum: amount, + // history: vec![], + // }; + // map.insert(delegation.delegation.node_identity.clone(), delegation); + // } + // } + // + // for d in delegations { + // // create record of delegation + // let delegated_on_iso_datetime = nymd_client!(state) + // .get_block_timestamp(Some(d.block_height as u32)) + // .await? + // .to_rfc3339(); + // let amount: MajorCurrencyAmount = d.amount.clone().try_into()?; + // let record = DelegationRecord { + // amount: amount.clone(), + // block_height: d.block_height, + // delegated_on_iso_datetime, + // }; + // + // let entry = map + // .entry(d.node_identity.clone()) + // .or_insert(DelegationWithHistory { + // delegation: d.try_into()?, + // history: vec![], + // amount_sum: MajorCurrencyAmount::zero(&amount.denom), + // }); + // + // entry.history.push(record); + // entry.amount_sum = entry.amount_sum.clone() + amount; + // } + // + // let mut with_everything: Vec = vec![]; + // + // for item in map { + // let d = item.1.delegation; + // let history = item.1.history; + // let Delegation { + // owner, + // node_identity, + // amount, + // block_height, + // proxy, + // } = d; + // + // log::trace!( + // " --- Delegation: node_identity = {}, amount = {}", + // node_identity, + // amount + // ); + // + // let mixnode = mixnodes + // .iter() + // .find(|m| m.mix_node.identity_key == node_identity); + // + // let pledge_amount: Option = + // mixnode.and_then(|m| m.pledge_amount.clone().try_into().ok()); + // + // let total_delegation: Option = + // mixnode.and_then(|m| m.total_delegation.clone().try_into().ok()); + // + // let profit_margin_percent: Option = mixnode.map(|m| m.mix_node.profit_margin_percent); + // + // log::trace!(" >>> Get accumulated rewards: address = {}", address); + // let accumulated_rewards = match nymd_client!(state) + // .get_delegator_rewards(address.clone(), node_identity.clone(), proxy.clone()) + // .await + // { + // Ok(rewards) => { + // let amount = MajorCurrencyAmount::from_minor_uint128_and_denom( + // rewards, + // denom_minor.as_ref(), + // )?; + // log::trace!(" <<< rewards = {}, amount = {}", rewards, amount); + // Some(amount) + // } + // Err(_) => { + // log::trace!(" <<< no rewards waiting"); + // None + // } + // }; + // + // let pending_events = + // filter_pending_events(&node_identity, pending_events_for_account.clone()); + // log::trace!( + // " --- pending events for mixnode = {}", + // pending_events.len() + // ); + // + // log::trace!( + // " >>> Get stake saturation: node_identity = {}", + // node_identity + // ); + // let stake_saturation = api_client!(state) + // .get_mixnode_stake_saturation(&node_identity) + // .await + // .ok() + // .map(|r| r.saturation); + // log::trace!(" <<< {:?}", stake_saturation); + // + // log::trace!( + // " >>> Get average uptime percentage: node_identity = {}", + // node_identity + // ); + // let avg_uptime_percent = api_client!(state) + // .get_mixnode_avg_uptime(&node_identity) + // .await + // .ok() + // .map(|r| r.avg_uptime); + // log::trace!(" <<< {:?}", avg_uptime_percent); + // + // log::trace!( + // " >>> Convert delegated on block height to timestamp: block_height = {}", + // d.block_height + // ); + // let timestamp = nymd_client!(state) + // .get_block_timestamp(Some(d.block_height as u32)) + // .await?; + // let delegated_on_iso_datetime = timestamp.to_rfc3339(); + // log::trace!( + // " <<< timestamp = {:?}, delegated_on_iso_datetime = {:?}", + // timestamp, + // delegated_on_iso_datetime + // ); + // + // with_everything.push(DelegationWithEverything { + // owner: owner.to_string(), + // node_identity: node_identity.to_string(), + // amount: item.1.amount_sum, + // block_height, + // proxy: proxy.clone(), + // delegated_on_iso_datetime, + // stake_saturation, + // accumulated_rewards, + // profit_margin_percent, + // pledge_amount, + // avg_uptime_percent, + // total_delegation, + // pending_events, + // history, + // }) + // } + // log::trace!("<<< {:?}", with_everything); + // + // Ok(with_everything) } fn filter_pending_events( - node_identity: &str, - pending_events: Vec, + node_identity: &str, + pending_events: Vec, ) -> Vec { - pending_events - .iter() - .filter(|e| e.node_identity == node_identity) - .cloned() - .collect::>() + pending_events + .iter() + .filter(|e| e.node_identity == node_identity) + .cloned() + .collect::>() } #[tauri::command] pub async fn get_delegator_rewards( - address: String, - mix_identity: IdentityKey, - proxy: Option, - state: tauri::State<'_, Arc>>, + address: String, + mix_identity: IdentityKey, + proxy: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - let denom_minor = state.read().await.current_network().denom(); - log::info!( - ">>> Get delegator rewards: mix_identity = {}, proxy = {:?}", - mix_identity, - proxy - ); - let res = nymd_client!(state) - .get_delegator_rewards(address, mix_identity, proxy) - .await?; - let amount = MajorCurrencyAmount::from_minor_uint128_and_denom(res, denom_minor.as_ref())?; - log::info!(">>> res = {}, amount = {}", res, amount); - Ok(amount) + let denom_minor = state.read().await.current_network().denom(); + log::info!( + ">>> Get delegator rewards: mix_identity = {}, proxy = {:?}", + mix_identity, + proxy + ); + let res = nymd_client!(state) + .get_delegator_rewards(address, mix_identity, proxy) + .await?; + let coin = CosmWasmCoin::new(res.u128(), denom_minor.as_ref()); + let amount = coin.into(); + log::info!(">>> res = {}, amount = {}", res, amount); + Ok(amount) } #[tauri::command] pub async fn get_delegation_summary( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, Arc>>, ) -> Result { - log::info!(">>> Get delegation summary"); - - let denom_minor = state.read().await.current_network().denom(); - let denom: CurrencyDenom = denom_minor.clone().try_into()?; - - let delegations = get_all_mix_delegations(state.clone()).await?; - let mut total_delegations = MajorCurrencyAmount::zero(&denom); - let mut total_rewards = MajorCurrencyAmount::zero(&denom); - - for d in delegations.clone() { - total_delegations = total_delegations + d.amount; - if let Some(rewards) = d.accumulated_rewards { - total_rewards = total_rewards + rewards; - } - } - log::info!( - "<<< {} delegations, total_delegations = {}, total_rewards = {}", - delegations.len(), - total_delegations, - total_rewards - ); - log::trace!("<<< {:?}", delegations); - - Ok(DelegationsSummaryResponse { - delegations, - total_delegations, - total_rewards, - }) + todo!("figure out this one after merging") + // + // log::info!(">>> Get delegation summary"); + // + // let denom_minor = state.read().await.current_network().denom(); + // let denom: CurrencyDenom = denom_minor.clone().try_into()?; + // + // let delegations = get_all_mix_delegations(state.clone()).await?; + // let mut total_delegations = MajorCurrencyAmount::zero(&denom); + // let mut total_rewards = MajorCurrencyAmount::zero(&denom); + // + // for d in delegations.clone() { + // total_delegations = total_delegations + d.amount; + // if let Some(rewards) = d.accumulated_rewards { + // total_rewards = total_rewards + rewards; + // } + // } + // log::info!( + // "<<< {} delegations, total_delegations = {}, total_rewards = {}", + // delegations.len(), + // total_delegations, + // total_rewards + // ); + // log::trace!("<<< {:?}", delegations); + // + // Ok(DelegationsSummaryResponse { + // delegations, + // total_delegations, + // total_rewards, + // }) } #[tauri::command] pub async fn get_all_pending_delegation_events( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, Arc>>, ) -> Result, BackendError> { - log::info!(">>> Get all pending delegation events"); + log::info!(">>> Get all pending delegation events"); - // get pending events from mixnet and vesting contract - let mut pending_events_for_account = get_pending_delegation_events(state.clone()).await?; - let pending_vesting_events = get_pending_vesting_delegation_events(state.clone()).await?; + // get pending events from mixnet and vesting contract + let mut pending_events_for_account = get_pending_delegation_events(state.clone()).await?; + let pending_vesting_events = get_pending_vesting_delegation_events(state.clone()).await?; - // combine them - for event in pending_vesting_events { - pending_events_for_account.push(event); - } + // combine them + for event in pending_vesting_events { + pending_events_for_account.push(event); + } - Ok(pending_events_for_account) + Ok(pending_events_for_account) } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/epoch.rs b/nym-wallet/src-tauri/src/operations/mixnet/epoch.rs index 6cfbb4d539..5203e5bc62 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/epoch.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/epoch.rs @@ -7,10 +7,10 @@ use tokio::sync::RwLock; #[tauri::command] pub async fn get_current_epoch( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, Arc>>, ) -> Result { - log::info!(">>> Get curren epoch"); - let interval = nymd_client!(state).get_current_epoch().await?; - log::info!("<<< curren epoch = {}", interval); - Ok(interval.into()) + log::info!(">>> Get curren epoch"); + let interval = nymd_client!(state).get_current_epoch().await?; + log::info!("<<< curren epoch = {}", interval); + Ok(interval.into()) } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/send.rs b/nym-wallet/src-tauri/src/operations/mixnet/send.rs index d6a178ddc6..30efde8566 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/send.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/send.rs @@ -6,41 +6,41 @@ use nym_types::transaction::{SendTxResult, TransactionDetails}; use std::str::FromStr; use std::sync::Arc; use tokio::sync::RwLock; -use validator_client::nymd::{AccountId, CosmosCoin, Fee}; +use validator_client::nymd::{AccountId, Fee}; #[tauri::command] pub async fn send( - address: &str, - amount: MajorCurrencyAmount, - memo: String, - fee: Option, - state: tauri::State<'_, Arc>>, + address: &str, + amount: MajorCurrencyAmount, + memo: String, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - let denom_minor = state.read().await.current_network().denom(); - let address = AccountId::from_str(address)?; - let from_address = nymd_client!(state).address().to_string(); - let cosmos_amount: CosmosCoin = amount.clone().into_minor_cosmos_coin()?; - log::info!( - ">>> Send: amount = {}, minor_amount = {:?}, from = {}, to = {}, fee = {:?}", - amount, - cosmos_amount, - from_address, - address.as_ref(), - fee, - ); - let raw_res = nymd_client!(state) - .send(&address, vec![cosmos_amount], memo, fee) - .await?; - log::info!("<<< tx hash = {}", raw_res.hash.to_string()); - let res = SendTxResult::new( - raw_res, - TransactionDetails { - from_address, - to_address: address.to_string(), - amount, - }, - denom_minor.as_ref(), - )?; - log::trace!("<<< {:?}", res); - Ok(res) + let denom_minor = state.read().await.current_network().denom(); + let address = AccountId::from_str(address)?; + let from_address = nymd_client!(state).address().to_string(); + let amount2 = amount.clone().into(); + log::info!( + ">>> Send: amount = {}, minor_amount = {:?}, from = {}, to = {}, fee = {:?}", + amount, + amount2, + from_address, + address.as_ref(), + fee, + ); + let raw_res = nymd_client!(state) + .send(&address, vec![amount2], memo, fee) + .await?; + log::info!("<<< tx hash = {}", raw_res.hash.to_string()); + let res = SendTxResult::new( + raw_res, + TransactionDetails { + from_address, + to_address: address.to_string(), + amount, + }, + denom_minor.as_ref(), + )?; + log::trace!("<<< {:?}", res); + Ok(res) } diff --git a/nym-wallet/src-tauri/src/operations/simulate/admin.rs b/nym-wallet/src-tauri/src/operations/simulate/admin.rs index 9eae191d73..2d42d0c31a 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/admin.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/admin.rs @@ -11,22 +11,22 @@ use tokio::sync::RwLock; #[tauri::command] pub async fn simulate_update_contract_settings( - params: TauriContractStateParams, - state: tauri::State<'_, Arc>>, + params: TauriContractStateParams, + state: tauri::State<'_, Arc>>, ) -> Result { - let guard = state.read().await; - let mixnet_contract_settings_params: ContractStateParams = params.try_into()?; + let guard = state.read().await; + let mixnet_contract_settings_params: ContractStateParams = params.try_into()?; - let client = guard.current_client()?; - let mixnet_contract = client.nymd.mixnet_contract_address()?; - let gas_price = client.nymd.gas_price().clone(); + let client = guard.current_client()?; + let mixnet_contract = client.nymd.mixnet_contract_address()?; + let gas_price = client.nymd.gas_price().clone(); - let msg = client.nymd.wrap_contract_execute_message( - mixnet_contract, - &ExecuteMsg::UpdateContractStateParams(mixnet_contract_settings_params), - vec![], - )?; + let msg = client.nymd.wrap_contract_execute_message( + mixnet_contract, + &ExecuteMsg::UpdateContractStateParams(mixnet_contract_settings_params), + vec![], + )?; - let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) + let result = client.nymd.simulate(vec![msg]).await?; + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } diff --git a/nym-wallet/src-tauri/src/operations/simulate/cosmos.rs b/nym-wallet/src-tauri/src/operations/simulate/cosmos.rs index 12c41bd576..4b90e96ac2 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/cosmos.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/cosmos.rs @@ -12,26 +12,26 @@ use validator_client::nymd::{AccountId, MsgSend}; #[tauri::command] pub async fn simulate_send( - address: &str, - amount: MajorCurrencyAmount, - state: tauri::State<'_, Arc>>, + address: &str, + amount: MajorCurrencyAmount, + state: tauri::State<'_, Arc>>, ) -> Result { - let guard = state.read().await; + let guard = state.read().await; - let to_address = AccountId::from_str(address)?; - let amount = vec![amount.into_cosmos_coin()?]; + let to_address = AccountId::from_str(address)?; + let amount = vec![amount.into()]; - let client = guard.current_client()?; - let from_address = client.nymd.address().clone(); - let gas_price = client.nymd.gas_price().clone(); + let client = guard.current_client()?; + let from_address = client.nymd.address().clone(); + let gas_price = client.nymd.gas_price().clone(); - // TODO: I'm still not 100% convinced whether this should be exposed here or handled somewhere else in the client code - let msg = MsgSend { - from_address, - to_address, - amount, - }; + // TODO: I'm still not 100% convinced whether this should be exposed here or handled somewhere else in the client code + let msg = MsgSend { + from_address, + to_address, + amount, + }; - let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) + let result = client.nymd.simulate(vec![msg]).await?; + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } diff --git a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs index ac988bcf2b..8c78c862d7 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs @@ -11,166 +11,166 @@ use tokio::sync::RwLock; #[tauri::command] pub async fn simulate_bond_gateway( - gateway: Gateway, - pledge: MajorCurrencyAmount, - owner_signature: String, - state: tauri::State<'_, Arc>>, + gateway: Gateway, + pledge: MajorCurrencyAmount, + owner_signature: String, + state: tauri::State<'_, Arc>>, ) -> Result { - let guard = state.read().await; - let pledge = pledge.into_cosmos_coin()?; + let guard = state.read().await; + let pledge = pledge.into(); - let client = guard.current_client()?; - let mixnet_contract = client.nymd.mixnet_contract_address()?; - let gas_price = client.nymd.gas_price().clone(); + let client = guard.current_client()?; + let mixnet_contract = client.nymd.mixnet_contract_address()?; + let gas_price = client.nymd.gas_price().clone(); - // TODO: I'm still not 100% convinced whether this should be exposed here or handled somewhere else in the client code - let msg = client.nymd.wrap_contract_execute_message( - mixnet_contract, - &ExecuteMsg::BondGateway { - gateway, - owner_signature, - }, - vec![pledge], - )?; + // TODO: I'm still not 100% convinced whether this should be exposed here or handled somewhere else in the client code + let msg = client.nymd.wrap_contract_execute_message( + mixnet_contract, + &ExecuteMsg::BondGateway { + gateway, + owner_signature, + }, + vec![pledge], + )?; - let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) + let result = client.nymd.simulate(vec![msg]).await?; + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] pub async fn simulate_unbond_gateway( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, Arc>>, ) -> Result { - let guard = state.read().await; + let guard = state.read().await; - let client = guard.current_client()?; - let mixnet_contract = client.nymd.mixnet_contract_address()?; - let gas_price = client.nymd.gas_price().clone(); + let client = guard.current_client()?; + let mixnet_contract = client.nymd.mixnet_contract_address()?; + let gas_price = client.nymd.gas_price().clone(); - let msg = client.nymd.wrap_contract_execute_message( - mixnet_contract, - &ExecuteMsg::UnbondGateway {}, - vec![], - )?; + let msg = client.nymd.wrap_contract_execute_message( + mixnet_contract, + &ExecuteMsg::UnbondGateway {}, + vec![], + )?; - let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) + let result = client.nymd.simulate(vec![msg]).await?; + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] pub async fn simulate_bond_mixnode( - mixnode: MixNode, - owner_signature: String, - pledge: MajorCurrencyAmount, - state: tauri::State<'_, Arc>>, + mixnode: MixNode, + owner_signature: String, + pledge: MajorCurrencyAmount, + state: tauri::State<'_, Arc>>, ) -> Result { - let guard = state.read().await; - let pledge = pledge.into_cosmos_coin()?; + let guard = state.read().await; + let pledge = pledge.into(); - let client = guard.current_client()?; - let mixnet_contract = client.nymd.mixnet_contract_address()?; - let gas_price = client.nymd.gas_price().clone(); + let client = guard.current_client()?; + let mixnet_contract = client.nymd.mixnet_contract_address()?; + let gas_price = client.nymd.gas_price().clone(); - let msg = client.nymd.wrap_contract_execute_message( - mixnet_contract, - &ExecuteMsg::BondMixnode { - mix_node: mixnode, - owner_signature, - }, - vec![pledge], - )?; + let msg = client.nymd.wrap_contract_execute_message( + mixnet_contract, + &ExecuteMsg::BondMixnode { + mix_node: mixnode, + owner_signature, + }, + vec![pledge], + )?; - let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) + let result = client.nymd.simulate(vec![msg]).await?; + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] pub async fn simulate_unbond_mixnode( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, Arc>>, ) -> Result { - let guard = state.read().await; + let guard = state.read().await; - let client = guard.current_client()?; - let mixnet_contract = client.nymd.mixnet_contract_address()?; - let gas_price = client.nymd.gas_price().clone(); + let client = guard.current_client()?; + let mixnet_contract = client.nymd.mixnet_contract_address()?; + let gas_price = client.nymd.gas_price().clone(); - let msg = client.nymd.wrap_contract_execute_message( - mixnet_contract, - &ExecuteMsg::UnbondMixnode {}, - vec![], - )?; + let msg = client.nymd.wrap_contract_execute_message( + mixnet_contract, + &ExecuteMsg::UnbondMixnode {}, + vec![], + )?; - let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) + let result = client.nymd.simulate(vec![msg]).await?; + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] pub async fn simulate_update_mixnode( - profit_margin_percent: u8, - state: tauri::State<'_, Arc>>, + profit_margin_percent: u8, + state: tauri::State<'_, Arc>>, ) -> Result { - let guard = state.read().await; + let guard = state.read().await; - let client = guard.current_client()?; - let mixnet_contract = client.nymd.mixnet_contract_address()?; - let gas_price = client.nymd.gas_price().clone(); + let client = guard.current_client()?; + let mixnet_contract = client.nymd.mixnet_contract_address()?; + let gas_price = client.nymd.gas_price().clone(); - let msg = client.nymd.wrap_contract_execute_message( - mixnet_contract, - &ExecuteMsg::UpdateMixnodeConfig { - profit_margin_percent, - }, - vec![], - )?; + let msg = client.nymd.wrap_contract_execute_message( + mixnet_contract, + &ExecuteMsg::UpdateMixnodeConfig { + profit_margin_percent, + }, + vec![], + )?; - let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) + let result = client.nymd.simulate(vec![msg]).await?; + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] pub async fn simulate_delegate_to_mixnode( - identity: &str, - amount: MajorCurrencyAmount, - state: tauri::State<'_, Arc>>, + identity: &str, + amount: MajorCurrencyAmount, + state: tauri::State<'_, Arc>>, ) -> Result { - let guard = state.read().await; - let delegation = amount.into_cosmos_coin()?; + let guard = state.read().await; + let delegation = amount.into(); - let client = guard.current_client()?; - let mixnet_contract = client.nymd.mixnet_contract_address()?; - let gas_price = client.nymd.gas_price().clone(); + let client = guard.current_client()?; + let mixnet_contract = client.nymd.mixnet_contract_address()?; + let gas_price = client.nymd.gas_price().clone(); - let msg = client.nymd.wrap_contract_execute_message( - mixnet_contract, - &ExecuteMsg::DelegateToMixnode { - mix_identity: identity.to_string(), - }, - vec![delegation], - )?; + let msg = client.nymd.wrap_contract_execute_message( + mixnet_contract, + &ExecuteMsg::DelegateToMixnode { + mix_identity: identity.to_string(), + }, + vec![delegation], + )?; - let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) + let result = client.nymd.simulate(vec![msg]).await?; + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] pub async fn simulate_undelegate_from_mixnode( - identity: &str, - state: tauri::State<'_, Arc>>, + identity: &str, + state: tauri::State<'_, Arc>>, ) -> Result { - let guard = state.read().await; + let guard = state.read().await; - let client = guard.current_client()?; - let mixnet_contract = client.nymd.mixnet_contract_address()?; - let gas_price = client.nymd.gas_price().clone(); + let client = guard.current_client()?; + let mixnet_contract = client.nymd.mixnet_contract_address()?; + let gas_price = client.nymd.gas_price().clone(); - let msg = client.nymd.wrap_contract_execute_message( - mixnet_contract, - &ExecuteMsg::UndelegateFromMixnode { - mix_identity: identity.to_string(), - }, - vec![], - )?; + let msg = client.nymd.wrap_contract_execute_message( + mixnet_contract, + &ExecuteMsg::UndelegateFromMixnode { + mix_identity: identity.to_string(), + }, + vec![], + )?; - let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) + let result = client.nymd.simulate(vec![msg]).await?; + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } diff --git a/nym-wallet/src-tauri/src/operations/simulate/mod.rs b/nym-wallet/src-tauri/src/operations/simulate/mod.rs index 55103a1f92..f9fb86abcf 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/mod.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/mod.rs @@ -15,47 +15,45 @@ pub mod vesting; // technically we could have also exposed a result: Option field from the SimulateResponse, // but in the context of the wallet it's really irrelevant and useless for the time being pub(crate) struct SimulateResult { - // As I mentioned somewhere before, from what I've seen in manual testing, - // gas estimation does not exist if transaction itself fails to get executed. - // for example if you attempt to send a 'BondMixnode' with invalid signature - pub gas_info: Option, - pub gas_price: GasPrice, + // As I mentioned somewhere before, from what I've seen in manual testing, + // gas estimation does not exist if transaction itself fails to get executed. + // for example if you attempt to send a 'BondMixnode' with invalid signature + pub gas_info: Option, + pub gas_price: GasPrice, } impl SimulateResult { - pub fn new(gas_info: Option, gas_price: GasPrice) -> Self { - SimulateResult { - gas_info, - gas_price, + pub fn new(gas_info: Option, gas_price: GasPrice) -> Self { + SimulateResult { + gas_info, + gas_price, + } } - } - pub fn detailed_fee(&self) -> Result { - let amount: Option = match self.to_fee_amount() { - Some(a) => Some(a.try_into()?), - None => None, - }; - Ok(FeeDetails { - amount, - fee: self.to_fee(), - }) - } - - fn to_fee_amount(&self) -> Option { - self - .gas_info - .map(|gas_info| &self.gas_price * gas_info.gas_used) - } - - fn to_fee(&self) -> Fee { - self - .to_fee_amount() - .and_then(|fee_amount| { - self.gas_info.map(|gas_info| { - let gas_limit = gas_info.gas_used; - tx::Fee::from_amount_and_gas(fee_amount, gas_limit).into() + pub fn detailed_fee(&self) -> Result { + let amount: Option = match self.to_fee_amount() { + Some(a) => Some(a.into()), + None => None, + }; + Ok(FeeDetails { + amount, + fee: self.to_fee(), }) - }) - .unwrap_or_default() - } + } + + fn to_fee_amount(&self) -> Option { + self.gas_info + .map(|gas_info| &self.gas_price * gas_info.gas_used) + } + + fn to_fee(&self) -> Fee { + self.to_fee_amount() + .and_then(|fee_amount| { + self.gas_info.map(|gas_info| { + let gas_limit = gas_info.gas_used; + tx::Fee::from_amount_and_gas(fee_amount, gas_limit).into() + }) + }) + .unwrap_or_default() + } } diff --git a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs index f2de5277ac..f576df2240 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs @@ -12,141 +12,141 @@ use vesting_contract_common::ExecuteMsg; #[tauri::command] pub async fn simulate_vesting_bond_gateway( - gateway: Gateway, - pledge: MajorCurrencyAmount, - owner_signature: String, - state: tauri::State<'_, Arc>>, + gateway: Gateway, + pledge: MajorCurrencyAmount, + owner_signature: String, + state: tauri::State<'_, Arc>>, ) -> Result { - let guard = state.read().await; - let pledge = pledge.into_cosmwasm_coin()?; + let guard = state.read().await; + let pledge = pledge.into(); - let client = guard.current_client()?; - let vesting_contract = client.nymd.vesting_contract_address()?; - let gas_price = client.nymd.gas_price().clone(); + let client = guard.current_client()?; + let vesting_contract = client.nymd.vesting_contract_address()?; + let gas_price = client.nymd.gas_price().clone(); - let msg = client.nymd.wrap_contract_execute_message( - vesting_contract, - &ExecuteMsg::BondGateway { - gateway, - owner_signature, - amount: pledge, - }, - vec![], - )?; + let msg = client.nymd.wrap_contract_execute_message( + vesting_contract, + &ExecuteMsg::BondGateway { + gateway, + owner_signature, + amount: pledge, + }, + vec![], + )?; - let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) + let result = client.nymd.simulate(vec![msg]).await?; + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] pub async fn simulate_vesting_unbond_gateway( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, Arc>>, ) -> Result { - let guard = state.read().await; + let guard = state.read().await; - let client = guard.current_client()?; - let vesting_contract = client.nymd.vesting_contract_address()?; - let gas_price = client.nymd.gas_price().clone(); + let client = guard.current_client()?; + let vesting_contract = client.nymd.vesting_contract_address()?; + let gas_price = client.nymd.gas_price().clone(); - let msg = client.nymd.wrap_contract_execute_message( - vesting_contract, - &ExecuteMsg::UnbondGateway {}, - vec![], - )?; + let msg = client.nymd.wrap_contract_execute_message( + vesting_contract, + &ExecuteMsg::UnbondGateway {}, + vec![], + )?; - let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) + let result = client.nymd.simulate(vec![msg]).await?; + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] pub async fn simulate_vesting_bond_mixnode( - mixnode: MixNode, - owner_signature: String, - pledge: MajorCurrencyAmount, - state: tauri::State<'_, Arc>>, + mixnode: MixNode, + owner_signature: String, + pledge: MajorCurrencyAmount, + state: tauri::State<'_, Arc>>, ) -> Result { - let guard = state.read().await; - let pledge = pledge.into_cosmwasm_coin()?; + let guard = state.read().await; + let pledge = pledge.into(); - let client = guard.current_client()?; - let vesting_contract = client.nymd.vesting_contract_address()?; - let gas_price = client.nymd.gas_price().clone(); + let client = guard.current_client()?; + let vesting_contract = client.nymd.vesting_contract_address()?; + let gas_price = client.nymd.gas_price().clone(); - let msg = client.nymd.wrap_contract_execute_message( - vesting_contract, - &ExecuteMsg::BondMixnode { - mix_node: mixnode, - owner_signature, - amount: pledge, - }, - vec![], - )?; + let msg = client.nymd.wrap_contract_execute_message( + vesting_contract, + &ExecuteMsg::BondMixnode { + mix_node: mixnode, + owner_signature, + amount: pledge, + }, + vec![], + )?; - let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) + let result = client.nymd.simulate(vec![msg]).await?; + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] pub async fn simulate_vesting_unbond_mixnode( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, Arc>>, ) -> Result { - let guard = state.read().await; + let guard = state.read().await; - let client = guard.current_client()?; - let vesting_contract = client.nymd.vesting_contract_address()?; - let gas_price = client.nymd.gas_price().clone(); + let client = guard.current_client()?; + let vesting_contract = client.nymd.vesting_contract_address()?; + let gas_price = client.nymd.gas_price().clone(); - let msg = client.nymd.wrap_contract_execute_message( - vesting_contract, - &ExecuteMsg::UnbondMixnode {}, - vec![], - )?; + let msg = client.nymd.wrap_contract_execute_message( + vesting_contract, + &ExecuteMsg::UnbondMixnode {}, + vec![], + )?; - let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) + let result = client.nymd.simulate(vec![msg]).await?; + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] pub async fn simulate_vesting_update_mixnode( - profit_margin_percent: u8, - state: tauri::State<'_, Arc>>, + profit_margin_percent: u8, + state: tauri::State<'_, Arc>>, ) -> Result { - let guard = state.read().await; + let guard = state.read().await; - let client = guard.current_client()?; - let vesting_contract = client.nymd.vesting_contract_address()?; - let gas_price = client.nymd.gas_price().clone(); + let client = guard.current_client()?; + let vesting_contract = client.nymd.vesting_contract_address()?; + let gas_price = client.nymd.gas_price().clone(); - let msg = client.nymd.wrap_contract_execute_message( - vesting_contract, - &ExecuteMsg::UpdateMixnodeConfig { - profit_margin_percent, - }, - vec![], - )?; + let msg = client.nymd.wrap_contract_execute_message( + vesting_contract, + &ExecuteMsg::UpdateMixnodeConfig { + profit_margin_percent, + }, + vec![], + )?; - let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) + let result = client.nymd.simulate(vec![msg]).await?; + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] pub async fn simulate_withdraw_vested_coins( - amount: MajorCurrencyAmount, - state: tauri::State<'_, Arc>>, + amount: MajorCurrencyAmount, + state: tauri::State<'_, Arc>>, ) -> Result { - let guard = state.read().await; - let amount = amount.into_cosmwasm_coin()?; + let guard = state.read().await; + let amount = amount.into(); - let client = guard.current_client()?; - let vesting_contract = client.nymd.vesting_contract_address()?; - let gas_price = client.nymd.gas_price().clone(); + let client = guard.current_client()?; + let vesting_contract = client.nymd.vesting_contract_address()?; + let gas_price = client.nymd.gas_price().clone(); - let msg = client.nymd.wrap_contract_execute_message( - vesting_contract, - &ExecuteMsg::WithdrawVestedCoins { amount }, - vec![], - )?; + let msg = client.nymd.wrap_contract_execute_message( + vesting_contract, + &ExecuteMsg::WithdrawVestedCoins { amount }, + vec![], + )?; - let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) + let result = client.nymd.simulate(vec![msg]).await?; + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } 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 6b559b4acd..721c39bf82 100644 --- a/nym-wallet/src-tauri/src/operations/validator_api/status.rs +++ b/nym-wallet/src-tauri/src/operations/validator_api/status.rs @@ -7,76 +7,66 @@ use crate::state::State; use std::sync::Arc; use tokio::sync::RwLock; use validator_client::models::{ - CoreNodeStatusResponse, InclusionProbabilityResponse, MixnodeStatusResponse, - RewardEstimationResponse, StakeSaturationResponse, + CoreNodeStatusResponse, InclusionProbabilityResponse, MixnodeStatusResponse, + RewardEstimationResponse, StakeSaturationResponse, }; #[tauri::command] pub async fn mixnode_core_node_status( - identity: &str, - since: Option, - state: tauri::State<'_, Arc>>, + identity: &str, + since: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - Ok( - api_client!(state) - .get_mixnode_core_status_count(identity, since) - .await?, - ) + Ok(api_client!(state) + .get_mixnode_core_status_count(identity, since) + .await?) } #[tauri::command] pub async fn gateway_core_node_status( - identity: &str, - since: Option, - state: tauri::State<'_, Arc>>, + identity: &str, + since: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - Ok( - api_client!(state) - .get_gateway_core_status_count(identity, since) - .await?, - ) + Ok(api_client!(state) + .get_gateway_core_status_count(identity, since) + .await?) } #[tauri::command] pub async fn mixnode_status( - identity: &str, - state: tauri::State<'_, Arc>>, + identity: &str, + state: tauri::State<'_, Arc>>, ) -> Result { - Ok(api_client!(state).get_mixnode_status(identity).await?) + Ok(api_client!(state).get_mixnode_status(identity).await?) } #[tauri::command] pub async fn mixnode_reward_estimation( - identity: &str, - state: tauri::State<'_, Arc>>, + identity: &str, + state: tauri::State<'_, Arc>>, ) -> Result { - Ok( - api_client!(state) - .get_mixnode_reward_estimation(identity) - .await?, - ) + Ok(api_client!(state) + .get_mixnode_reward_estimation(identity) + .await?) } #[tauri::command] pub async fn mixnode_stake_saturation( - identity: &str, - state: tauri::State<'_, Arc>>, + identity: &str, + state: tauri::State<'_, Arc>>, ) -> Result { - Ok( - api_client!(state) - .get_mixnode_stake_saturation(identity) - .await?, - ) + Ok(api_client!(state) + .get_mixnode_stake_saturation(identity) + .await?) } #[tauri::command] pub async fn mixnode_inclusion_probability( - identity: &str, - state: tauri::State<'_, Arc>>, + identity: &str, + state: tauri::State<'_, Arc>>, ) -> Result { - Ok( - api_client!(state) - .get_mixnode_inclusion_probability(identity) - .await?, - ) + Ok(api_client!(state) + .get_mixnode_inclusion_probability(identity) + .await?) } diff --git a/nym-wallet/src-tauri/src/operations/vesting/bond.rs b/nym-wallet/src-tauri/src/operations/vesting/bond.rs index 9c27bdaa35..67df1a2e6d 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/bond.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/bond.rs @@ -11,142 +11,142 @@ use validator_client::nymd::{Fee, VestingSigningClient}; #[tauri::command] pub async fn vesting_bond_gateway( - gateway: Gateway, - pledge: MajorCurrencyAmount, - owner_signature: String, - fee: Option, - state: tauri::State<'_, Arc>>, + gateway: Gateway, + pledge: MajorCurrencyAmount, + owner_signature: String, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - let denom_minor = state.read().await.current_network().denom(); - let pledge_minor = pledge.clone().into_minor_cosmwasm_coin()?; - log::info!( + let denom_minor = state.read().await.current_network().denom(); + let pledge_minor = pledge.clone().into(); + log::info!( ">>> Bond gateway with locked tokens: identity_key = {}, pledge = {}, pledge_minor = {}, fee = {:?}", gateway.identity_key, pledge, pledge_minor, fee, ); - let res = nymd_client!(state) - .vesting_bond_gateway(gateway, &owner_signature, pledge_minor, fee) - .await?; - log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); - Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), - )?) + let res = nymd_client!(state) + .vesting_bond_gateway(gateway, &owner_signature, pledge_minor, fee) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) } #[tauri::command] pub async fn vesting_unbond_gateway( - fee: Option, - state: tauri::State<'_, Arc>>, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - let denom_minor = state.read().await.current_network().denom(); - log::info!( - ">>> Unbond gateway bonded with locked tokens, fee = {:?}", - fee - ); - let res = nymd_client!(state).vesting_unbond_gateway(fee).await?; - log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); - Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), - )?) + let denom_minor = state.read().await.current_network().denom(); + log::info!( + ">>> Unbond gateway bonded with locked tokens, fee = {:?}", + fee + ); + let res = nymd_client!(state).vesting_unbond_gateway(fee).await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) } #[tauri::command] pub async fn vesting_bond_mixnode( - mixnode: MixNode, - owner_signature: String, - pledge: MajorCurrencyAmount, - fee: Option, - state: tauri::State<'_, Arc>>, + mixnode: MixNode, + owner_signature: String, + pledge: MajorCurrencyAmount, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - let denom_minor = state.read().await.current_network().denom(); - let pledge_minor = pledge.clone().into_minor_cosmwasm_coin()?; - log::info!( + let denom_minor = state.read().await.current_network().denom(); + let pledge_minor = pledge.clone().into(); + log::info!( ">>> Bond mixnode with locked tokens: identity_key = {}, pledge = {}, pledge_minor = {}, fee = {:?}", mixnode.identity_key, pledge, pledge_minor, fee ); - let res = nymd_client!(state) - .vesting_bond_mixnode(mixnode, &owner_signature, pledge_minor, fee) - .await?; - log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); - Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), - )?) + let res = nymd_client!(state) + .vesting_bond_mixnode(mixnode, &owner_signature, pledge_minor, fee) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) } #[tauri::command] pub async fn vesting_unbond_mixnode( - fee: Option, - state: tauri::State<'_, Arc>>, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - let denom_minor = state.read().await.current_network().denom(); - log::info!( - ">>> Unbond mixnode bonded with locked tokens, fee = {:?}", - fee - ); - let res = nymd_client!(state).vesting_unbond_mixnode(fee).await?; - log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); - Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), - )?) + let denom_minor = state.read().await.current_network().denom(); + log::info!( + ">>> Unbond mixnode bonded with locked tokens, fee = {:?}", + fee + ); + let res = nymd_client!(state).vesting_unbond_mixnode(fee).await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) } #[tauri::command] pub async fn withdraw_vested_coins( - amount: MajorCurrencyAmount, - fee: Option, - state: tauri::State<'_, Arc>>, + amount: MajorCurrencyAmount, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - let denom_minor = state.read().await.current_network().denom(); - let amount_minor = amount.clone().into_minor_cosmwasm_coin()?; - log::info!( - ">>> Withdraw vested liquid coins: amount = {}, amount_minor = {}, fee = {:?}", - amount, - amount_minor, - fee - ); - let res = nymd_client!(state) - .withdraw_vested_coins(amount_minor, fee) - .await?; - log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); - Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), - )?) + let denom_minor = state.read().await.current_network().denom(); + let amount_minor = amount.clone().into(); + log::info!( + ">>> Withdraw vested liquid coins: amount = {}, amount_minor = {}, fee = {:?}", + amount, + amount_minor, + fee + ); + let res = nymd_client!(state) + .withdraw_vested_coins(amount_minor, fee) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) } #[tauri::command] pub async fn vesting_update_mixnode( - profit_margin_percent: u8, - fee: Option, - state: tauri::State<'_, Arc>>, + profit_margin_percent: u8, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - let denom_minor = state.read().await.current_network().denom(); - log::info!( - ">>> Update mixnode bonded with locked tokens: profit_margin_percent = {}, fee = {:?}", - profit_margin_percent, - fee, - ); - let res = nymd_client!(state) - .vesting_update_mixnode_config(profit_margin_percent, fee) - .await?; - log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); - Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), - )?) + let denom_minor = state.read().await.current_network().denom(); + log::info!( + ">>> Update mixnode bonded with locked tokens: profit_margin_percent = {}, fee = {:?}", + profit_margin_percent, + fee, + ); + let res = nymd_client!(state) + .vesting_update_mixnode_config(profit_margin_percent, fee) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) } diff --git a/nym-wallet/src-tauri/src/operations/vesting/delegate.rs b/nym-wallet/src-tauri/src/operations/vesting/delegate.rs index 3b34bc05f2..bf6eca6838 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/delegate.rs @@ -13,76 +13,76 @@ use crate::state::State; #[tauri::command] pub async fn get_pending_vesting_delegation_events( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, Arc>>, ) -> Result, BackendError> { - log::info!(">>> Get pending delegations from vesting contract"); + log::info!(">>> Get pending delegations from vesting contract"); - let guard = state.read().await; - let client = &guard.current_client()?.nymd; - let vesting_contract = client.vesting_contract_address()?; + let guard = state.read().await; + let client = &guard.current_client()?.nymd; + let vesting_contract = client.vesting_contract_address()?; - let events = client - .get_pending_delegation_events( - client.address().to_string(), - Some(vesting_contract.to_string()), - ) - .await?; + let events = client + .get_pending_delegation_events( + client.address().to_string(), + Some(vesting_contract.to_string()), + ) + .await?; - log::info!("<<< {} events", events.len()); - log::trace!("<<< {:?}", events); + log::info!("<<< {} events", events.len()); + log::trace!("<<< {:?}", events); - match from_contract_delegation_events(events) { - Ok(res) => Ok(res), - Err(e) => Err(e.into()), - } + match from_contract_delegation_events(events) { + Ok(res) => Ok(res), + Err(e) => Err(e.into()), + } } #[tauri::command] pub async fn vesting_delegate_to_mixnode( - identity: &str, - amount: MajorCurrencyAmount, - fee: Option, - state: tauri::State<'_, Arc>>, + identity: &str, + amount: MajorCurrencyAmount, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - let denom_minor = state.read().await.current_network().denom(); - let delegation = amount.clone().into_minor_cosmwasm_coin()?; - log::info!( + let denom_minor = state.read().await.current_network().denom(); + let delegation = amount.clone().into(); + log::info!( ">>> Delegate to mixnode with locked tokens: identity_key = {}, amount = {}, minor_amount = {}, fee = {:?}", identity, amount, delegation, fee ); - let res = nymd_client!(state) - .vesting_delegate_to_mixnode(identity, &delegation, fee) - .await?; - log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); - Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), - )?) + let res = nymd_client!(state) + .vesting_delegate_to_mixnode(identity, delegation, fee) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) } #[tauri::command] pub async fn vesting_undelegate_from_mixnode( - identity: &str, - fee: Option, - state: tauri::State<'_, Arc>>, + identity: &str, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - let denom_minor = state.read().await.current_network().denom(); - log::info!( - ">>> Undelegate from mixnode delegated with locked tokens: identity_key = {}, fee = {:?}", - identity, - fee, - ); - let res = nymd_client!(state) - .vesting_undelegate_from_mixnode(identity, fee) - .await?; - log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); - Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), - )?) + let denom_minor = state.read().await.current_network().denom(); + log::info!( + ">>> Undelegate from mixnode delegated with locked tokens: identity_key = {}, fee = {:?}", + identity, + fee, + ); + let res = nymd_client!(state) + .vesting_undelegate_from_mixnode(identity, fee) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) } diff --git a/nym-wallet/src-tauri/src/operations/vesting/queries.rs b/nym-wallet/src-tauri/src/operations/vesting/queries.rs index 3ce316cdcd..72c85f69d7 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/queries.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/queries.rs @@ -15,201 +15,201 @@ use crate::state::State; #[tauri::command] pub async fn locked_coins( - block_time: Option, - state: tauri::State<'_, Arc>>, + block_time: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - log::info!(">>> Query locked coins"); - let res = nymd_client!(state) - .locked_coins( - nymd_client!(state).address().as_ref(), - block_time.map(Timestamp::from_seconds), - ) - .await? - .try_into()?; - log::info!("<<< locked coins = {}", res); - Ok(res) + log::info!(">>> Query locked coins"); + let res = nymd_client!(state) + .locked_coins( + nymd_client!(state).address().as_ref(), + block_time.map(Timestamp::from_seconds), + ) + .await? + .into(); + log::info!("<<< locked coins = {}", res); + Ok(res) } #[tauri::command] pub async fn spendable_coins( - block_time: Option, - state: tauri::State<'_, Arc>>, + block_time: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - log::info!(">>> Query spendable coins"); - let res = nymd_client!(state) - .spendable_coins( - nymd_client!(state).address().as_ref(), - block_time.map(Timestamp::from_seconds), - ) - .await? - .try_into()?; - log::info!("<<< spendable coins = {}", res); - Ok(res) + log::info!(">>> Query spendable coins"); + let res = nymd_client!(state) + .spendable_coins( + nymd_client!(state).address().as_ref(), + block_time.map(Timestamp::from_seconds), + ) + .await? + .into(); + log::info!("<<< spendable coins = {}", res); + Ok(res) } #[tauri::command] pub async fn vested_coins( - vesting_account_address: &str, - block_time: Option, - state: tauri::State<'_, Arc>>, + vesting_account_address: &str, + block_time: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - log::info!(">>> Query vested coins"); - let res = nymd_client!(state) - .vested_coins( - vesting_account_address, - block_time.map(Timestamp::from_seconds), - ) - .await? - .try_into()?; - log::info!("<<< vested coins = {}", res); - Ok(res) + log::info!(">>> Query vested coins"); + let res = nymd_client!(state) + .vested_coins( + vesting_account_address, + block_time.map(Timestamp::from_seconds), + ) + .await? + .into(); + log::info!("<<< vested coins = {}", res); + Ok(res) } #[tauri::command] pub async fn vesting_coins( - vesting_account_address: &str, - block_time: Option, - state: tauri::State<'_, Arc>>, + vesting_account_address: &str, + block_time: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - log::info!(">>> Query vesting coins"); - let res = nymd_client!(state) - .vesting_coins( - vesting_account_address, - block_time.map(Timestamp::from_seconds), - ) - .await? - .try_into()?; - log::info!("<<< vesting coins = {}", res); - Ok(res) + log::info!(">>> Query vesting coins"); + let res = nymd_client!(state) + .vesting_coins( + vesting_account_address, + block_time.map(Timestamp::from_seconds), + ) + .await? + .into(); + log::info!("<<< vesting coins = {}", res); + Ok(res) } #[tauri::command] pub async fn vesting_start_time( - vesting_account_address: &str, - state: tauri::State<'_, Arc>>, + vesting_account_address: &str, + state: tauri::State<'_, Arc>>, ) -> Result { - log::info!(">>> Query vesting start time"); - let res = nymd_client!(state) - .vesting_start_time(vesting_account_address) - .await? - .seconds(); - log::info!("<<< vesting start time = {}", res); - Ok(res) + log::info!(">>> Query vesting start time"); + let res = nymd_client!(state) + .vesting_start_time(vesting_account_address) + .await? + .seconds(); + log::info!("<<< vesting start time = {}", res); + Ok(res) } #[tauri::command] pub async fn vesting_end_time( - vesting_account_address: &str, - state: tauri::State<'_, Arc>>, + vesting_account_address: &str, + state: tauri::State<'_, Arc>>, ) -> Result { - log::info!(">>> Query vesting end time"); - let res = nymd_client!(state) - .vesting_end_time(vesting_account_address) - .await? - .seconds(); - log::info!("<<< vesting end time = {}", res); - Ok(res) + log::info!(">>> Query vesting end time"); + let res = nymd_client!(state) + .vesting_end_time(vesting_account_address) + .await? + .seconds(); + log::info!("<<< vesting end time = {}", res); + Ok(res) } #[tauri::command] pub async fn original_vesting( - vesting_account_address: &str, - state: tauri::State<'_, Arc>>, + vesting_account_address: &str, + state: tauri::State<'_, Arc>>, ) -> Result { - log::info!(">>> Query original vesting"); - let res = nymd_client!(state) - .original_vesting(vesting_account_address) - .await? - .try_into()?; - log::info!("<<< {:?}", res); - Ok(res) + log::info!(">>> Query original vesting"); + let res = nymd_client!(state) + .original_vesting(vesting_account_address) + .await? + .try_into()?; + log::info!("<<< {:?}", res); + Ok(res) } #[tauri::command] pub async fn delegated_free( - vesting_account_address: &str, - block_time: Option, - state: tauri::State<'_, Arc>>, + vesting_account_address: &str, + block_time: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - log::info!(">>> Query delegated free"); - let res = nymd_client!(state) - .delegated_free( - vesting_account_address, - block_time.map(Timestamp::from_seconds), - ) - .await? - .try_into()?; - log::info!("<<< delegated free = {}", res); - Ok(res) + log::info!(">>> Query delegated free"); + let res = nymd_client!(state) + .delegated_free( + vesting_account_address, + block_time.map(Timestamp::from_seconds), + ) + .await? + .into(); + log::info!("<<< delegated free = {}", res); + Ok(res) } /// Returns the total amount of delegated tokens that have vested #[tauri::command] pub async fn delegated_vesting( - block_time: Option, - vesting_account_address: &str, - state: tauri::State<'_, Arc>>, + block_time: Option, + vesting_account_address: &str, + state: tauri::State<'_, Arc>>, ) -> Result { - log::info!(">>> Query delegated vesting"); - let res = nymd_client!(state) - .delegated_vesting( - vesting_account_address, - block_time.map(Timestamp::from_seconds), - ) - .await? - .try_into()?; - log::info!("<<< delegated_vesting = {}", res); - Ok(res) + log::info!(">>> Query delegated vesting"); + let res = nymd_client!(state) + .delegated_vesting( + vesting_account_address, + block_time.map(Timestamp::from_seconds), + ) + .await? + .into(); + log::info!("<<< delegated_vesting = {}", res); + Ok(res) } #[tauri::command] pub async fn vesting_get_mixnode_pledge( - address: &str, - state: tauri::State<'_, Arc>>, + address: &str, + state: tauri::State<'_, Arc>>, ) -> Result, BackendError> { - log::info!(">>> Query vesting get mixnode pledge"); - let res = nymd_client!(state) - .get_mixnode_pledge(address) - .await? - .and_then(PledgeData::and_then); - log::info!("<<< {:?}", res); - Ok(res) + log::info!(">>> Query vesting get mixnode pledge"); + let res = nymd_client!(state) + .get_mixnode_pledge(address) + .await? + .and_then(PledgeData::and_then); + log::info!("<<< {:?}", res); + Ok(res) } #[tauri::command] pub async fn vesting_get_gateway_pledge( - address: &str, - state: tauri::State<'_, Arc>>, + address: &str, + state: tauri::State<'_, Arc>>, ) -> Result, BackendError> { - log::info!(">>> Query vesting get gateway pledge"); - let res = nymd_client!(state) - .get_gateway_pledge(address) - .await? - .and_then(PledgeData::and_then); - log::info!("<<< {:?}", res); - Ok(res) + log::info!(">>> Query vesting get gateway pledge"); + let res = nymd_client!(state) + .get_gateway_pledge(address) + .await? + .and_then(PledgeData::and_then); + log::info!("<<< {:?}", res); + Ok(res) } #[tauri::command] pub async fn get_current_vesting_period( - address: &str, - state: tauri::State<'_, Arc>>, + address: &str, + state: tauri::State<'_, Arc>>, ) -> Result { - log::info!(">>> Query current vesting period"); - let res = nymd_client!(state) - .get_current_vesting_period(address) - .await?; - log::info!("<<< {:?}", res); - Ok(res) + log::info!(">>> Query current vesting period"); + let res = nymd_client!(state) + .get_current_vesting_period(address) + .await?; + log::info!("<<< {:?}", res); + Ok(res) } #[tauri::command] pub async fn get_account_info( - address: &str, - state: tauri::State<'_, Arc>>, + address: &str, + state: tauri::State<'_, Arc>>, ) -> Result { - log::info!(">>> Query account info"); - let res = nymd_client!(state).get_account(address).await?.try_into()?; - log::info!("<<< {:?}", res); - Ok(res) + log::info!(">>> Query account info"); + let res = nymd_client!(state).get_account(address).await?.try_into()?; + log::info!("<<< {:?}", res); + Ok(res) } diff --git a/nym-wallet/src-tauri/src/state.rs b/nym-wallet/src-tauri/src/state.rs index 77dfe6b5ff..1095d872b6 100644 --- a/nym-wallet/src-tauri/src/state.rs +++ b/nym-wallet/src-tauri/src/state.rs @@ -18,450 +18,440 @@ use std::time::Duration; // Some hardcoded metadata overrides static METADATA_OVERRIDES: Lazy> = Lazy::new(|| { - vec![( - "https://rpc.nyx.nodes.guru/".parse().unwrap(), - ValidatorMetadata { - name: Some("Nodes.Guru".to_string()), - }, - )] + vec![( + "https://rpc.nyx.nodes.guru/".parse().unwrap(), + ValidatorMetadata { + name: Some("Nodes.Guru".to_string()), + }, + )] }); #[tauri::command] pub async fn load_config_from_files( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, Arc>>, ) -> Result<(), BackendError> { - state.write().await.load_config_files(); - Ok(()) + state.write().await.load_config_files(); + Ok(()) } #[tauri::command] pub async fn save_config_to_files( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, Arc>>, ) -> Result<(), BackendError> { - state.read().await.save_config_files() + state.read().await.save_config_files() } #[derive(Default)] pub struct State { - config: config::Config, - signing_clients: HashMap>, - current_network: Network, + config: config::Config, + signing_clients: HashMap>, + current_network: Network, - // All the accounts the we get from decrypting the wallet. We hold on to these for being able to - // switch accounts on-the-fly - all_accounts: Vec, + // All the accounts the we get from decrypting the wallet. We hold on to these for being able to + // switch accounts on-the-fly + all_accounts: Vec, - /// Validators that have been fetched dynamically, probably during startup. - fetched_validators: config::OptionalValidators, + /// Validators that have been fetched dynamically, probably during startup. + fetched_validators: config::OptionalValidators, - /// We fetch (and cache) some metadata, such as names, when available - validator_metadata: HashMap, + /// We fetch (and cache) some metadata, such as names, when available + validator_metadata: HashMap, } pub(crate) struct WalletAccountIds { - // The wallet account id - pub id: crate::wallet_storage::AccountId, - // The set of corresponding network identities derived from the mnemonic - pub addresses: HashMap, + // The wallet account id + pub id: crate::wallet_storage::AccountId, + // The set of corresponding network identities derived from the mnemonic + pub addresses: HashMap, } impl State { - pub fn client(&self, network: Network) -> Result<&Client, BackendError> { - self - .signing_clients - .get(&network) - .ok_or(BackendError::ClientNotInitialized) - } + pub fn client(&self, network: Network) -> Result<&Client, BackendError> { + self.signing_clients + .get(&network) + .ok_or(BackendError::ClientNotInitialized) + } - pub fn client_mut( - &mut self, - network: Network, - ) -> Result<&mut Client, BackendError> { - self - .signing_clients - .get_mut(&network) - .ok_or(BackendError::ClientNotInitialized) - } + pub fn client_mut( + &mut self, + network: Network, + ) -> Result<&mut Client, BackendError> { + self.signing_clients + .get_mut(&network) + .ok_or(BackendError::ClientNotInitialized) + } - pub fn current_client(&self) -> Result<&Client, BackendError> { - self - .signing_clients - .get(&self.current_network) - .ok_or(BackendError::ClientNotInitialized) - } + pub fn current_client(&self) -> Result<&Client, BackendError> { + self.signing_clients + .get(&self.current_network) + .ok_or(BackendError::ClientNotInitialized) + } - #[allow(unused)] - pub fn current_client_mut(&mut self) -> Result<&mut Client, BackendError> { - self - .signing_clients - .get_mut(&self.current_network) - .ok_or(BackendError::ClientNotInitialized) - } + #[allow(unused)] + pub fn current_client_mut(&mut self) -> Result<&mut Client, BackendError> { + self.signing_clients + .get_mut(&self.current_network) + .ok_or(BackendError::ClientNotInitialized) + } - pub fn config(&self) -> &config::Config { - &self.config - } + pub fn config(&self) -> &config::Config { + &self.config + } - /// Load configuration from files. If unsuccessful we just log it and move on. - pub fn load_config_files(&mut self) { - self.config = config::Config::load_from_files(); - } + /// Load configuration from files. If unsuccessful we just log it and move on. + pub fn load_config_files(&mut self) { + self.config = config::Config::load_from_files(); + } - #[allow(unused)] - pub fn save_config_files(&self) -> Result<(), BackendError> { - Ok(self.config.save_to_files()?) - } + #[allow(unused)] + pub fn save_config_files(&self) -> Result<(), BackendError> { + Ok(self.config.save_to_files()?) + } - pub fn add_client(&mut self, network: Network, client: Client) { - self.signing_clients.insert(network, client); - } + pub fn add_client(&mut self, network: Network, client: Client) { + self.signing_clients.insert(network, client); + } - pub fn set_network(&mut self, network: Network) { - self.current_network = network; - } + pub fn set_network(&mut self, network: Network) { + self.current_network = network; + } - pub fn current_network(&self) -> Network { - self.current_network - } + pub fn current_network(&self) -> Network { + self.current_network + } - pub(crate) fn set_all_accounts(&mut self, all_accounts: Vec) { - self.all_accounts = all_accounts - } + pub(crate) fn set_all_accounts(&mut self, all_accounts: Vec) { + self.all_accounts = all_accounts + } - pub(crate) fn get_all_accounts(&self) -> impl Iterator { - self.all_accounts.iter() - } + pub(crate) fn get_all_accounts(&self) -> impl Iterator { + self.all_accounts.iter() + } - pub fn logout(&mut self) { - self.signing_clients = HashMap::new(); - } + pub fn logout(&mut self) { + self.signing_clients = HashMap::new(); + } - /// Get the available validators in the order - /// 1. from the configuration file - /// 2. provided remotely - /// 3. hardcoded fallback - /// The format is the config backend format, which is flat due to serialization preference. - pub fn get_config_validator_entries( - &self, - network: Network, - ) -> impl Iterator + '_ { - let validators_in_config = self.config.get_configured_validators(network); - let fetched_validators = self.fetched_validators.validators(network).cloned(); - let default_validators = self.config.get_base_validators(network); + /// Get the available validators in the order + /// 1. from the configuration file + /// 2. provided remotely + /// 3. hardcoded fallback + /// The format is the config backend format, which is flat due to serialization preference. + pub fn get_config_validator_entries( + &self, + network: Network, + ) -> impl Iterator + '_ { + let validators_in_config = self.config.get_configured_validators(network); + let fetched_validators = self.fetched_validators.validators(network).cloned(); + let default_validators = self.config.get_base_validators(network); - // All the validators, in decending list of priority - let validators = validators_in_config - .chain(fetched_validators) - .chain(default_validators) - .unique_by(|v| (v.nymd_url.clone(), v.api_url.clone())); + // All the validators, in decending list of priority + let validators = validators_in_config + .chain(fetched_validators) + .chain(default_validators) + .unique_by(|v| (v.nymd_url.clone(), v.api_url.clone())); - // Annotate with dynamic metadata - validators.map(|v| { - let metadata = self.validator_metadata.get(&v.nymd_url); - let name = v - .nymd_name - .or_else(|| metadata.and_then(|m| m.name.clone())); - config::ValidatorConfigEntry { - nymd_url: v.nymd_url, - nymd_name: name, - api_url: v.api_url, - } - }) - } - - pub fn get_nymd_urls_only(&self, network: Network) -> impl Iterator + '_ { - self - .get_config_validator_entries(network) - .into_iter() - .map(|v| v.nymd_url) - } - - pub fn get_api_urls_only(&self, network: Network) -> impl Iterator + '_ { - self - .get_config_validator_entries(network) - .into_iter() - .filter_map(|v| v.api_url) - } - - /// Get the list of validator nymd urls in the network config format, suitable for passing on to - /// the UI - pub fn get_nymd_urls( - &self, - network: Network, - ) -> impl Iterator + '_ { - self - .get_config_validator_entries(network) - .into_iter() - .map(|v| network_config::ValidatorUrl { - url: v.nymd_url.to_string(), - name: v.nymd_name, - }) - } - - /// Get the list of validator-api urls in the network config format, suitable for passing on to - /// the UI - pub fn get_api_urls( - &self, - network: Network, - ) -> impl Iterator + '_ { - self - .get_config_validator_entries(network) - .into_iter() - .filter_map(|v| { - v.api_url.map(|u| network_config::ValidatorUrl { - url: u.to_string(), - name: None, + // Annotate with dynamic metadata + validators.map(|v| { + let metadata = self.validator_metadata.get(&v.nymd_url); + let name = v + .nymd_name + .or_else(|| metadata.and_then(|m| m.name.clone())); + config::ValidatorConfigEntry { + nymd_url: v.nymd_url, + nymd_name: name, + api_url: v.api_url, + } }) - }) - } + } - pub fn get_all_nymd_urls(&self) -> HashMap> { - Network::iter() - .flat_map(|network| { - self - .get_nymd_urls_only(network) - .map(move |url| (network, url)) - }) - .into_group_map() - } + pub fn get_nymd_urls_only(&self, network: Network) -> impl Iterator + '_ { + self.get_config_validator_entries(network) + .into_iter() + .map(|v| v.nymd_url) + } - pub fn get_all_api_urls(&self) -> HashMap> { - Network::iter() - .flat_map(|network| { - self - .get_api_urls_only(network) - .map(move |url| (network, url)) - }) - .into_group_map() - } + pub fn get_api_urls_only(&self, network: Network) -> impl Iterator + '_ { + self.get_config_validator_entries(network) + .into_iter() + .filter_map(|v| v.api_url) + } - /// Fetch validator urls remotely. These are used to in addition to the base ones, and the user - /// configured ones. - pub async fn fetch_updated_validator_urls(&mut self) -> Result<(), BackendError> { - let client = reqwest::Client::builder() - .timeout(Duration::from_secs(3)) - .build()?; - log::debug!( - "Fetching validator urls from: {}", - crate::config::REMOTE_SOURCE_OF_VALIDATOR_URLS - ); - let response = client - .get(crate::config::REMOTE_SOURCE_OF_VALIDATOR_URLS.to_string()) - .send() - .await?; + /// Get the list of validator nymd urls in the network config format, suitable for passing on to + /// the UI + pub fn get_nymd_urls( + &self, + network: Network, + ) -> impl Iterator + '_ { + self.get_config_validator_entries(network) + .into_iter() + .map(|v| network_config::ValidatorUrl { + url: v.nymd_url.to_string(), + name: v.nymd_name, + }) + } - self.fetched_validators = serde_json::from_str(&response.text().await?)?; - log::debug!("Received validator urls: \n{}", self.fetched_validators); + /// Get the list of validator-api urls in the network config format, suitable for passing on to + /// the UI + pub fn get_api_urls( + &self, + network: Network, + ) -> impl Iterator + '_ { + self.get_config_validator_entries(network) + .into_iter() + .filter_map(|v| { + v.api_url.map(|u| network_config::ValidatorUrl { + url: u.to_string(), + name: None, + }) + }) + } - self.refresh_validator_status().await?; + pub fn get_all_nymd_urls(&self) -> HashMap> { + Network::iter() + .flat_map(|network| { + self.get_nymd_urls_only(network) + .map(move |url| (network, url)) + }) + .into_group_map() + } - Ok(()) - } + pub fn get_all_api_urls(&self) -> HashMap> { + Network::iter() + .flat_map(|network| { + self.get_api_urls_only(network) + .map(move |url| (network, url)) + }) + .into_group_map() + } - pub async fn refresh_validator_status(&mut self) -> Result<(), BackendError> { - log::debug!("Refreshing validator status"); - - // All urls for all networks - let nymd_urls = self - .get_all_nymd_urls() - .into_iter() - .flat_map(|(_, urls)| urls.into_iter()); - - // Fetch status for all urls - let responses = fetch_status_for_urls(nymd_urls).await?; - - // Update the stored metadata - self.apply_responses(responses)?; - - // Override some overrides for usability - self.apply_metadata_override(METADATA_OVERRIDES.to_vec()); - - Ok(()) - } - - fn apply_responses( - &mut self, - responses: Vec>, - ) -> Result<(), BackendError> { - for response in responses.into_iter().flatten() { - let json: serde_json::Value = serde_json::from_str(&response.1)?; - let moniker = &json["result"]["node_info"]["moniker"]; - log::debug!("Fetched moniker for: {}: {}", response.0, moniker); - - // Insert into metadata map - if let Some(ref mut m) = self.validator_metadata.get_mut(&response.0) { - m.name = Some(moniker.to_string()); - } else { - self.validator_metadata.insert( - response.0, - ValidatorMetadata { - name: Some(moniker.to_string()), - }, + /// Fetch validator urls remotely. These are used to in addition to the base ones, and the user + /// configured ones. + pub async fn fetch_updated_validator_urls(&mut self) -> Result<(), BackendError> { + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(3)) + .build()?; + log::debug!( + "Fetching validator urls from: {}", + crate::config::REMOTE_SOURCE_OF_VALIDATOR_URLS ); - } + let response = client + .get(crate::config::REMOTE_SOURCE_OF_VALIDATOR_URLS.to_string()) + .send() + .await?; + + self.fetched_validators = serde_json::from_str(&response.text().await?)?; + log::debug!("Received validator urls: \n{}", self.fetched_validators); + + self.refresh_validator_status().await?; + + Ok(()) } - Ok(()) - } - fn apply_metadata_override(&mut self, metadata_overrides: Vec<(Url, ValidatorMetadata)>) { - for (url, metadata) in metadata_overrides { - log::debug!("Overriding (some) metadata for: {url}"); - if let Some(m) = self.validator_metadata.get_mut(&url) { - m.name = metadata.name; - } else { - self.validator_metadata.insert(url, metadata); - } + pub async fn refresh_validator_status(&mut self) -> Result<(), BackendError> { + log::debug!("Refreshing validator status"); + + // All urls for all networks + let nymd_urls = self + .get_all_nymd_urls() + .into_iter() + .flat_map(|(_, urls)| urls.into_iter()); + + // Fetch status for all urls + let responses = fetch_status_for_urls(nymd_urls).await?; + + // Update the stored metadata + self.apply_responses(responses)?; + + // Override some overrides for usability + self.apply_metadata_override(METADATA_OVERRIDES.to_vec()); + + Ok(()) } - } - pub fn select_validator_nymd_url( - &mut self, - url: &str, - network: Network, - ) -> Result<(), BackendError> { - self.config.select_validator_nymd_url(url.parse()?, network); - if let Ok(client) = self.client_mut(network) { - client.change_nymd(url.parse()?)?; + fn apply_responses( + &mut self, + responses: Vec>, + ) -> Result<(), BackendError> { + for response in responses.into_iter().flatten() { + let json: serde_json::Value = serde_json::from_str(&response.1)?; + let moniker = &json["result"]["node_info"]["moniker"]; + log::debug!("Fetched moniker for: {}: {}", response.0, moniker); + + // Insert into metadata map + if let Some(ref mut m) = self.validator_metadata.get_mut(&response.0) { + m.name = Some(moniker.to_string()); + } else { + self.validator_metadata.insert( + response.0, + ValidatorMetadata { + name: Some(moniker.to_string()), + }, + ); + } + } + Ok(()) } - Ok(()) - } - pub fn select_validator_api_url( - &mut self, - url: &str, - network: Network, - ) -> Result<(), BackendError> { - self.config.select_validator_api_url(url.parse()?, network); - if let Ok(client) = self.client_mut(network) { - client.change_validator_api(url.parse()?); + fn apply_metadata_override(&mut self, metadata_overrides: Vec<(Url, ValidatorMetadata)>) { + for (url, metadata) in metadata_overrides { + log::debug!("Overriding (some) metadata for: {url}"); + if let Some(m) = self.validator_metadata.get_mut(&url) { + m.name = metadata.name; + } else { + self.validator_metadata.insert(url, metadata); + } + } } - Ok(()) - } - pub fn add_validator_url(&mut self, url: config::ValidatorConfigEntry, network: Network) { - self.config.add_validator_url(url, network); - } + pub fn select_validator_nymd_url( + &mut self, + url: &str, + network: Network, + ) -> Result<(), BackendError> { + self.config.select_validator_nymd_url(url.parse()?, network); + if let Ok(client) = self.client_mut(network) { + client.change_nymd(url.parse()?)?; + } + Ok(()) + } - pub fn remove_validator_url(&mut self, url: config::ValidatorConfigEntry, network: Network) { - self.config.remove_validator_url(url, network) - } + pub fn select_validator_api_url( + &mut self, + url: &str, + network: Network, + ) -> Result<(), BackendError> { + self.config.select_validator_api_url(url.parse()?, network); + if let Ok(client) = self.client_mut(network) { + client.change_validator_api(url.parse()?); + } + Ok(()) + } + + pub fn add_validator_url(&mut self, url: config::ValidatorConfigEntry, network: Network) { + self.config.add_validator_url(url, network); + } + + pub fn remove_validator_url(&mut self, url: config::ValidatorConfigEntry, network: Network) { + self.config.remove_validator_url(url, network) + } } async fn fetch_status_for_urls( - nymd_urls: impl Iterator, + nymd_urls: impl Iterator, ) -> Result>, BackendError> { - let client = reqwest::Client::builder() - .timeout(Duration::from_secs(3)) - .build()?; + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(3)) + .build()?; - let responses = futures::future::join_all(nymd_urls.into_iter().map(|url| { - let client = &client; - let status_url = url.join("status").unwrap_or_else(|_| url.clone()); - async move { - let resp = client.get(status_url).send().await?; - resp.text().await.map(|text| (url, text)) - } - })) - .await; + let responses = futures::future::join_all(nymd_urls.into_iter().map(|url| { + let client = &client; + let status_url = url.join("status").unwrap_or_else(|_| url.clone()); + async move { + let resp = client.get(status_url).send().await?; + resp.text().await.map(|text| (url, text)) + } + })) + .await; - Ok(responses) + Ok(responses) } // Validator metadata that can by dynamically populated #[derive(Clone, Debug)] pub struct ValidatorMetadata { - pub name: Option, + pub name: Option, } #[macro_export] macro_rules! client { - ($state:ident) => { - $state.read().await.current_client()? - }; + ($state:ident) => { + $state.read().await.current_client()? + }; } #[macro_export] macro_rules! nymd_client { - ($state:ident) => { - $state.read().await.current_client()?.nymd - }; + ($state:ident) => { + $state.read().await.current_client()?.nymd + }; } #[macro_export] macro_rules! api_client { - ($state:ident) => { - $state.read().await.current_client()?.validator_api - }; + ($state:ident) => { + $state.read().await.current_client()?.validator_api + }; } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn adding_validators_urls_prepends() { - let mut state = State::default(); - let _api_urls = state.get_api_urls(Network::MAINNET).collect::>(); + #[test] + fn adding_validators_urls_prepends() { + let mut state = State::default(); + let _api_urls = state.get_api_urls(Network::MAINNET).collect::>(); - state.add_validator_url( - config::ValidatorConfigEntry { - nymd_url: "http://nymd_url.com".parse().unwrap(), - nymd_name: Some("NymdUrl".to_string()), - api_url: Some("http://nymd_url.com/api".parse().unwrap()), - }, - Network::MAINNET, - ); + state.add_validator_url( + config::ValidatorConfigEntry { + nymd_url: "http://nymd_url.com".parse().unwrap(), + nymd_name: Some("NymdUrl".to_string()), + api_url: Some("http://nymd_url.com/api".parse().unwrap()), + }, + Network::MAINNET, + ); - state.add_validator_url( - config::ValidatorConfigEntry { - nymd_url: "http://foo.com".parse().unwrap(), - nymd_name: None, - api_url: None, - }, - Network::MAINNET, - ); + state.add_validator_url( + config::ValidatorConfigEntry { + nymd_url: "http://foo.com".parse().unwrap(), + nymd_name: None, + api_url: None, + }, + Network::MAINNET, + ); - state.add_validator_url( - config::ValidatorConfigEntry { - nymd_url: "http://bar.com".parse().unwrap(), - nymd_name: None, - api_url: None, - }, - Network::MAINNET, - ); + state.add_validator_url( + config::ValidatorConfigEntry { + nymd_url: "http://bar.com".parse().unwrap(), + nymd_name: None, + api_url: None, + }, + Network::MAINNET, + ); - assert_eq!( - state - .get_nymd_urls_only(Network::MAINNET) - .collect::>(), - vec![ - "http://nymd_url.com/".parse().unwrap(), - "http://foo.com".parse().unwrap(), - "http://bar.com".parse().unwrap(), - "https://rpc.nyx.nodes.guru".parse().unwrap(), - ], - ); - assert_eq!( - state - .get_api_urls_only(Network::MAINNET) - .collect::>(), - vec![ - "http://nymd_url.com/api".parse().unwrap(), - "https://validator.nymtech.net/api/".parse().unwrap(), - ], - ); - assert_eq!( - state - .get_all_nymd_urls() - .get(&Network::MAINNET) - .unwrap() - .clone(), - vec![ - "http://nymd_url.com/".parse().unwrap(), - "http://foo.com".parse().unwrap(), - "http://bar.com".parse().unwrap(), - "https://rpc.nyx.nodes.guru".parse().unwrap(), - ], - ) - } + assert_eq!( + state + .get_nymd_urls_only(Network::MAINNET) + .collect::>(), + vec![ + "http://nymd_url.com/".parse().unwrap(), + "http://foo.com".parse().unwrap(), + "http://bar.com".parse().unwrap(), + "https://rpc.nyx.nodes.guru".parse().unwrap(), + ], + ); + assert_eq!( + state + .get_api_urls_only(Network::MAINNET) + .collect::>(), + vec![ + "http://nymd_url.com/api".parse().unwrap(), + "https://validator.nymtech.net/api/".parse().unwrap(), + ], + ); + assert_eq!( + state + .get_all_nymd_urls() + .get(&Network::MAINNET) + .unwrap() + .clone(), + vec![ + "http://nymd_url.com/".parse().unwrap(), + "http://foo.com".parse().unwrap(), + "http://bar.com".parse().unwrap(), + "https://rpc.nyx.nodes.guru".parse().unwrap(), + ], + ) + } } diff --git a/nym-wallet/src-tauri/src/utils.rs b/nym-wallet/src-tauri/src/utils.rs index 00e26ccc7d..5204a858d4 100644 --- a/nym-wallet/src-tauri/src/utils.rs +++ b/nym-wallet/src-tauri/src/utils.rs @@ -6,40 +6,36 @@ use std::sync::Arc; use tokio::sync::RwLock; fn get_env_as_option(key: &str) -> Option { - match ::std::env::var(key) { - Ok(res) => Some(res), - Err(_e) => None, - } + match ::std::env::var(key) { + Ok(res) => Some(res), + Err(_e) => None, + } } #[tauri::command] pub fn get_env() -> AppEnv { - AppEnv { - ADMIN_ADDRESS: get_env_as_option("ADMIN_ADDRESS"), - SHOW_TERMINAL: get_env_as_option("SHOW_TERMINAL"), - } + AppEnv { + ADMIN_ADDRESS: get_env_as_option("ADMIN_ADDRESS"), + SHOW_TERMINAL: get_env_as_option("SHOW_TERMINAL"), + } } #[tauri::command] pub async fn owns_mixnode( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, Arc>>, ) -> Result { - Ok( - nymd_client!(state) - .owns_mixnode(nymd_client!(state).address()) - .await? - .is_some(), - ) + Ok(nymd_client!(state) + .owns_mixnode(nymd_client!(state).address()) + .await? + .is_some()) } #[tauri::command] pub async fn owns_gateway( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, Arc>>, ) -> Result { - Ok( - nymd_client!(state) - .owns_gateway(nymd_client!(state).address()) - .await? - .is_some(), - ) + Ok(nymd_client!(state) + .owns_gateway(nymd_client!(state).address()) + .await? + .is_some()) } diff --git a/nym-wallet/src-tauri/src/wallet_storage/account_data.rs b/nym-wallet/src-tauri/src/wallet_storage/account_data.rs index 10149948e9..d0027592d9 100644 --- a/nym-wallet/src-tauri/src/wallet_storage/account_data.rs +++ b/nym-wallet/src-tauri/src/wallet_storage/account_data.rs @@ -29,119 +29,125 @@ const CURRENT_WALLET_FILE_VERSION: u32 = 1; /// The wallet, stored as a serialized json file. #[derive(Serialize, Deserialize, Debug)] pub(crate) struct StoredWallet { - version: u32, - accounts: Vec, + version: u32, + accounts: Vec, } impl StoredWallet { - #[allow(unused)] - pub fn version(&self) -> u32 { - self.version - } - - #[allow(unused)] - pub fn len(&self) -> usize { - self.accounts.len() - } - - pub fn is_empty(&self) -> bool { - self.accounts.is_empty() - } - - pub fn add_encrypted_login(&mut self, new_login: EncryptedLogin) -> Result<(), BackendError> { - if self.get_encrypted_login(&new_login.id).is_ok() { - return Err(BackendError::WalletLoginIdAlreadyExists); + #[allow(unused)] + pub fn version(&self) -> u32 { + self.version } - self.accounts.push(new_login); - Ok(()) - } - fn get_encrypted_login(&self, id: &LoginId) -> Result<&EncryptedData, BackendError> { - self - .accounts - .iter() - .find(|account| &account.id == id) - .map(|account| &account.account) - .ok_or(BackendError::WalletNoSuchLoginId) - } - - fn get_encrypted_login_mut(&mut self, id: &LoginId) -> Result<&mut EncryptedLogin, BackendError> { - self - .accounts - .iter_mut() - .find(|account| &account.id == id) - .ok_or(BackendError::WalletNoSuchLoginId) - } - - #[cfg(test)] - pub fn get_encrypted_login_by_index(&self, index: usize) -> Option<&EncryptedLogin> { - self.accounts.get(index) - } - - pub fn replace_encrypted_login(&mut self, new_login: EncryptedLogin) -> Result<(), BackendError> { - let login = self.get_encrypted_login_mut(&new_login.id)?; - *login = new_login; - Ok(()) - } - - pub fn remove_encrypted_login(&mut self, id: &LoginId) -> Option { - if let Some(index) = self.accounts.iter().position(|account| &account.id == id) { - log::info!("Removing from wallet file: {id}"); - Some(self.accounts.remove(index)) - } else { - log::debug!("Tried to remove non-existent id from wallet: {id}"); - None + #[allow(unused)] + pub fn len(&self) -> usize { + self.accounts.len() } - } - pub fn decrypt_login( - &self, - id: &LoginId, - password: &UserPassword, - ) -> Result { - self.get_encrypted_login(id)?.decrypt_struct(password) - } + pub fn is_empty(&self) -> bool { + self.accounts.is_empty() + } - pub fn decrypt_all(&self, password: &UserPassword) -> Result, BackendError> { - self - .accounts - .iter() - .map(|account| account.account.decrypt_struct(password)) - .collect::, _>>() - } + pub fn add_encrypted_login(&mut self, new_login: EncryptedLogin) -> Result<(), BackendError> { + if self.get_encrypted_login(&new_login.id).is_ok() { + return Err(BackendError::WalletLoginIdAlreadyExists); + } + self.accounts.push(new_login); + Ok(()) + } - pub fn password_can_decrypt_all(&self, password: &UserPassword) -> bool { - self.decrypt_all(password).is_ok() - } + fn get_encrypted_login( + &self, + id: &LoginId, + ) -> Result<&EncryptedData, BackendError> { + self.accounts + .iter() + .find(|account| &account.id == id) + .map(|account| &account.account) + .ok_or(BackendError::WalletNoSuchLoginId) + } + + fn get_encrypted_login_mut( + &mut self, + id: &LoginId, + ) -> Result<&mut EncryptedLogin, BackendError> { + self.accounts + .iter_mut() + .find(|account| &account.id == id) + .ok_or(BackendError::WalletNoSuchLoginId) + } + + #[cfg(test)] + pub fn get_encrypted_login_by_index(&self, index: usize) -> Option<&EncryptedLogin> { + self.accounts.get(index) + } + + pub fn replace_encrypted_login( + &mut self, + new_login: EncryptedLogin, + ) -> Result<(), BackendError> { + let login = self.get_encrypted_login_mut(&new_login.id)?; + *login = new_login; + Ok(()) + } + + pub fn remove_encrypted_login(&mut self, id: &LoginId) -> Option { + if let Some(index) = self.accounts.iter().position(|account| &account.id == id) { + log::info!("Removing from wallet file: {id}"); + Some(self.accounts.remove(index)) + } else { + log::debug!("Tried to remove non-existent id from wallet: {id}"); + None + } + } + + pub fn decrypt_login( + &self, + id: &LoginId, + password: &UserPassword, + ) -> Result { + self.get_encrypted_login(id)?.decrypt_struct(password) + } + + pub fn decrypt_all(&self, password: &UserPassword) -> Result, BackendError> { + self.accounts + .iter() + .map(|account| account.account.decrypt_struct(password)) + .collect::, _>>() + } + + pub fn password_can_decrypt_all(&self, password: &UserPassword) -> bool { + self.decrypt_all(password).is_ok() + } } impl Default for StoredWallet { - fn default() -> Self { - StoredWallet { - version: CURRENT_WALLET_FILE_VERSION, - accounts: Vec::new(), + fn default() -> Self { + StoredWallet { + version: CURRENT_WALLET_FILE_VERSION, + accounts: Vec::new(), + } } - } } /// Each entry in the stored wallet file. An id field in plaintext and an encrypted stored login. #[derive(Serialize, Deserialize, Debug)] pub(crate) struct EncryptedLogin { - pub id: LoginId, - pub account: EncryptedData, + pub id: LoginId, + pub account: EncryptedData, } impl EncryptedLogin { - pub(crate) fn encrypt( - id: LoginId, - login: &StoredLogin, - password: &UserPassword, - ) -> Result { - Ok(EncryptedLogin { - id, - account: super::encryption::encrypt_struct(login, password)?, - }) - } + pub(crate) fn encrypt( + id: LoginId, + login: &StoredLogin, + password: &UserPassword, + ) -> Result { + Ok(EncryptedLogin { + id, + account: super::encryption::encrypt_struct(login, password)?, + }) + } } /// A stored login is either a account, such as a mnemonic, or a list of multiple accounts where @@ -150,147 +156,148 @@ impl EncryptedLogin { #[serde(untagged)] #[zeroize(drop)] pub(crate) enum StoredLogin { - Mnemonic(MnemonicAccount), - // PrivateKey(PrivateKeyAccount) - Multiple(MultipleAccounts), + Mnemonic(MnemonicAccount), + // PrivateKey(PrivateKeyAccount) + Multiple(MultipleAccounts), } impl StoredLogin { - #[cfg(test)] - pub(crate) fn as_mnemonic_account(&self) -> Option<&MnemonicAccount> { - match self { - StoredLogin::Mnemonic(mn) => Some(mn), - StoredLogin::Multiple(_) => None, + #[cfg(test)] + pub(crate) fn as_mnemonic_account(&self) -> Option<&MnemonicAccount> { + match self { + StoredLogin::Mnemonic(mn) => Some(mn), + StoredLogin::Multiple(_) => None, + } } - } - #[cfg(test)] - pub(crate) fn as_multiple_accounts(&self) -> Option<&MultipleAccounts> { - match self { - StoredLogin::Mnemonic(_) => None, - StoredLogin::Multiple(accounts) => Some(accounts), + #[cfg(test)] + pub(crate) fn as_multiple_accounts(&self) -> Option<&MultipleAccounts> { + match self { + StoredLogin::Mnemonic(_) => None, + StoredLogin::Multiple(accounts) => Some(accounts), + } } - } - // Return the login as multiple accounts, and if there is only a single mnemonic backed account, - // return a set containing only the single account paired with the account id passed as function - // argument. - pub(crate) fn unwrap_into_multiple_accounts(self, id: AccountId) -> MultipleAccounts { - match self { - StoredLogin::Mnemonic(ref account) => vec![WalletAccount::new(id, account.clone())].into(), - StoredLogin::Multiple(ref accounts) => accounts.clone(), + // Return the login as multiple accounts, and if there is only a single mnemonic backed account, + // return a set containing only the single account paired with the account id passed as function + // argument. + pub(crate) fn unwrap_into_multiple_accounts(self, id: AccountId) -> MultipleAccounts { + match self { + StoredLogin::Mnemonic(ref account) => { + vec![WalletAccount::new(id, account.clone())].into() + } + StoredLogin::Multiple(ref accounts) => accounts.clone(), + } } - } } /// Multiple stored accounts, each entry having an id and a data field. #[derive(Serialize, Deserialize, Clone, Debug, Zeroize, PartialEq, Eq)] pub(crate) struct MultipleAccounts { - accounts: Vec, + accounts: Vec, } impl MultipleAccounts { - pub(crate) fn new() -> Self { - MultipleAccounts { - accounts: Vec::new(), + pub(crate) fn new() -> Self { + MultipleAccounts { + accounts: Vec::new(), + } } - } - pub(crate) fn get_accounts(&self) -> impl Iterator { - self.accounts.iter() - } - - pub(crate) fn get_account(&self, id: &AccountId) -> Option<&WalletAccount> { - self.accounts.iter().find(|account| &account.id == id) - } - - pub(crate) fn get_account_with_mnemonic( - &self, - mnemonic: &bip39::Mnemonic, - ) -> Option<&WalletAccount> { - self - .get_accounts() - .find(|account| account.mnemonic() == mnemonic) - } - - pub(crate) fn into_accounts(self) -> impl Iterator { - self.accounts.into_iter() - } - - #[allow(unused)] - pub(crate) fn len(&self) -> usize { - self.accounts.len() - } - - pub(crate) fn is_empty(&self) -> bool { - self.accounts.is_empty() - } - - pub(crate) fn add( - &mut self, - id: AccountId, - mnemonic: bip39::Mnemonic, - hd_path: DerivationPath, - ) -> Result<(), BackendError> { - if self.get_account(&id).is_some() { - Err(BackendError::WalletAccountIdAlreadyExistsInWalletLogin) - } else if self.get_account_with_mnemonic(&mnemonic).is_some() { - Err(BackendError::WalletMnemonicAlreadyExistsInWalletLogin) - } else { - self.accounts.push(WalletAccount::new( - id, - MnemonicAccount::new(mnemonic, hd_path), - )); - Ok(()) + pub(crate) fn get_accounts(&self) -> impl Iterator { + self.accounts.iter() } - } - pub(crate) fn remove(&mut self, id: &AccountId) -> Result<(), BackendError> { - if self.get_account(id).is_none() { - return Err(BackendError::WalletNoSuchAccountIdInWalletLogin); + pub(crate) fn get_account(&self, id: &AccountId) -> Option<&WalletAccount> { + self.accounts.iter().find(|account| &account.id == id) + } + + pub(crate) fn get_account_with_mnemonic( + &self, + mnemonic: &bip39::Mnemonic, + ) -> Option<&WalletAccount> { + self.get_accounts() + .find(|account| account.mnemonic() == mnemonic) + } + + pub(crate) fn into_accounts(self) -> impl Iterator { + self.accounts.into_iter() + } + + #[allow(unused)] + pub(crate) fn len(&self) -> usize { + self.accounts.len() + } + + pub(crate) fn is_empty(&self) -> bool { + self.accounts.is_empty() + } + + pub(crate) fn add( + &mut self, + id: AccountId, + mnemonic: bip39::Mnemonic, + hd_path: DerivationPath, + ) -> Result<(), BackendError> { + if self.get_account(&id).is_some() { + Err(BackendError::WalletAccountIdAlreadyExistsInWalletLogin) + } else if self.get_account_with_mnemonic(&mnemonic).is_some() { + Err(BackendError::WalletMnemonicAlreadyExistsInWalletLogin) + } else { + self.accounts.push(WalletAccount::new( + id, + MnemonicAccount::new(mnemonic, hd_path), + )); + Ok(()) + } + } + + pub(crate) fn remove(&mut self, id: &AccountId) -> Result<(), BackendError> { + if self.get_account(id).is_none() { + return Err(BackendError::WalletNoSuchAccountIdInWalletLogin); + } + self.accounts.retain(|accounts| &accounts.id != id); + Ok(()) } - self.accounts.retain(|accounts| &accounts.id != id); - Ok(()) - } } impl From> for MultipleAccounts { - fn from(accounts: Vec) -> MultipleAccounts { - Self { accounts } - } + fn from(accounts: Vec) -> MultipleAccounts { + Self { accounts } + } } /// An entry in the list of stored accounts #[derive(Serialize, Deserialize, Clone, Debug, Zeroize, PartialEq, Eq)] pub(crate) struct WalletAccount { - id: AccountId, - account: AccountData, + id: AccountId, + account: AccountData, } impl WalletAccount { - pub(crate) fn new(id: AccountId, mnemonic_account: MnemonicAccount) -> Self { - Self { - id, - account: AccountData::Mnemonic(mnemonic_account), + pub(crate) fn new(id: AccountId, mnemonic_account: MnemonicAccount) -> Self { + Self { + id, + account: AccountData::Mnemonic(mnemonic_account), + } } - } - pub(crate) fn id(&self) -> &AccountId { - &self.id - } - - pub(crate) fn mnemonic(&self) -> &bip39::Mnemonic { - match self.account { - AccountData::Mnemonic(ref account) => account.mnemonic(), + pub(crate) fn id(&self) -> &AccountId { + &self.id } - } - #[cfg(test)] - pub(crate) fn hd_path(&self) -> &DerivationPath { - match self.account { - AccountData::Mnemonic(ref account) => account.hd_path(), + pub(crate) fn mnemonic(&self) -> &bip39::Mnemonic { + match self.account { + AccountData::Mnemonic(ref account) => account.mnemonic(), + } + } + + #[cfg(test)] + pub(crate) fn hd_path(&self) -> &DerivationPath { + match self.account { + AccountData::Mnemonic(ref account) => account.hd_path(), + } } - } } /// An account usually is a mnemonic account, but in the future it might be backed by a private @@ -299,69 +306,69 @@ impl WalletAccount { #[serde(untagged)] #[zeroize(drop)] enum AccountData { - Mnemonic(MnemonicAccount), - // PrivateKey(PrivateKeyAccount) + Mnemonic(MnemonicAccount), + // PrivateKey(PrivateKeyAccount) } /// An account backed by a unique mnemonic. #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub(crate) struct MnemonicAccount { - mnemonic: bip39::Mnemonic, - #[serde(with = "display_hd_path")] - hd_path: DerivationPath, + mnemonic: bip39::Mnemonic, + #[serde(with = "display_hd_path")] + hd_path: DerivationPath, } impl MnemonicAccount { - pub(crate) fn new(mnemonic: bip39::Mnemonic, hd_path: DerivationPath) -> Self { - Self { mnemonic, hd_path } - } + pub(crate) fn new(mnemonic: bip39::Mnemonic, hd_path: DerivationPath) -> Self { + Self { mnemonic, hd_path } + } - pub(crate) fn mnemonic(&self) -> &bip39::Mnemonic { - &self.mnemonic - } + pub(crate) fn mnemonic(&self) -> &bip39::Mnemonic { + &self.mnemonic + } - #[cfg(test)] - pub(crate) fn hd_path(&self) -> &DerivationPath { - &self.hd_path - } + #[cfg(test)] + pub(crate) fn hd_path(&self) -> &DerivationPath { + &self.hd_path + } } impl Zeroize for MnemonicAccount { - fn zeroize(&mut self) { - // in ideal world, Mnemonic would have had zeroize defined on it (there's an almost year old PR that introduces it) - // and the memory would have been filled with zeroes. - // - // we really don't want to keep our real mnemonic in memory, so let's do the semi-nasty thing - // of overwriting it with a fresh mnemonic that was never used before - // - // note: this function can only fail on an invalid word count, which clearly is not the case here - self.mnemonic = bip39::Mnemonic::generate(self.mnemonic.word_count()).unwrap(); + fn zeroize(&mut self) { + // in ideal world, Mnemonic would have had zeroize defined on it (there's an almost year old PR that introduces it) + // and the memory would have been filled with zeroes. + // + // we really don't want to keep our real mnemonic in memory, so let's do the semi-nasty thing + // of overwriting it with a fresh mnemonic that was never used before + // + // note: this function can only fail on an invalid word count, which clearly is not the case here + self.mnemonic = bip39::Mnemonic::generate(self.mnemonic.word_count()).unwrap(); - // further note: we don't really care about the hd_path, there's nothing secret about it. - } + // further note: we don't really care about the hd_path, there's nothing secret about it. + } } impl Drop for MnemonicAccount { - fn drop(&mut self) { - self.zeroize() - } + fn drop(&mut self) { + self.zeroize() + } } mod display_hd_path { - use serde::{Deserialize, Deserializer, Serializer}; - use validator_client::nymd::bip32::DerivationPath; + use serde::{Deserialize, Deserializer, Serializer}; + use validator_client::nymd::bip32::DerivationPath; - pub fn serialize( - hd_path: &DerivationPath, - serializer: S, - ) -> Result { - serializer.collect_str(hd_path) - } + pub fn serialize( + hd_path: &DerivationPath, + serializer: S, + ) -> Result { + serializer.collect_str(hd_path) + } - pub fn deserialize<'de, D: Deserializer<'de>>( - deserializer: D, - ) -> Result { - let s = <&str>::deserialize(deserializer)?; - s.parse().map_err(serde::de::Error::custom) - } + pub fn deserialize<'de, D: Deserializer<'de>>( + deserializer: D, + ) -> Result { + let s = <&str>::deserialize(deserializer)?; + s.parse().map_err(serde::de::Error::custom) + } } diff --git a/nym-wallet/src-tauri/src/wallet_storage/encryption.rs b/nym-wallet/src-tauri/src/wallet_storage/encryption.rs index 101dde0417..bfaea8582e 100644 --- a/nym-wallet/src-tauri/src/wallet_storage/encryption.rs +++ b/nym-wallet/src-tauri/src/wallet_storage/encryption.rs @@ -7,8 +7,8 @@ use aes_gcm::aead::generic_array::ArrayLength; use aes_gcm::aead::{Aead, NewAead}; use aes_gcm::{Aes256Gcm, Key, Nonce}; use argon2::{ - password_hash::rand_core::{OsRng, RngCore}, - Algorithm, Argon2, Params, Version, + password_hash::rand_core::{OsRng, RngCore}, + Algorithm, Argon2, Params, Version, }; use serde::{Deserialize, Serialize}; use std::marker::PhantomData; @@ -27,249 +27,249 @@ const IV_LEN: usize = 12; #[derive(Debug, Serialize, Deserialize, Zeroize)] pub(crate) struct EncryptedData { - #[serde(with = "base64")] - ciphertext: Vec, - #[serde(with = "base64")] - salt: Vec, - #[serde(with = "base64")] - iv: Vec, + #[serde(with = "base64")] + ciphertext: Vec, + #[serde(with = "base64")] + salt: Vec, + #[serde(with = "base64")] + iv: Vec, - #[serde(skip)] - #[zeroize(skip)] - _marker: PhantomData, + #[serde(skip)] + #[zeroize(skip)] + _marker: PhantomData, } impl Drop for EncryptedData { - fn drop(&mut self) { - self.zeroize(); - } + fn drop(&mut self) { + self.zeroize(); + } } // we only ever want to expose those getters in the test code #[cfg(test)] impl EncryptedData { - pub(crate) fn ciphertext(&self) -> &[u8] { - &self.ciphertext - } + pub(crate) fn ciphertext(&self) -> &[u8] { + &self.ciphertext + } - pub(crate) fn salt(&self) -> &[u8] { - &self.salt - } + pub(crate) fn salt(&self) -> &[u8] { + &self.salt + } - pub(crate) fn iv(&self) -> &[u8] { - &self.iv - } + pub(crate) fn iv(&self) -> &[u8] { + &self.iv + } } // helper to make Vec serialization use base64 representation to make it human readable // so that it would be easier for users to copy contents from the disk if they wanted to use it elsewhere mod base64 { - use serde::{Deserialize, Deserializer, Serializer}; + use serde::{Deserialize, Deserializer, Serializer}; - pub fn serialize(bytes: &[u8], serializer: S) -> Result { - serializer.serialize_str(&base64::encode(bytes)) - } + pub fn serialize(bytes: &[u8], serializer: S) -> Result { + serializer.serialize_str(&base64::encode(bytes)) + } - pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result, D::Error> { - let s = ::deserialize(deserializer)?; - base64::decode(&s).map_err(serde::de::Error::custom) - } + pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result, D::Error> { + let s = ::deserialize(deserializer)?; + base64::decode(&s).map_err(serde::de::Error::custom) + } } impl EncryptedData { - #[allow(unused)] - pub(crate) fn encrypt_struct(data: &T, password: &UserPassword) -> Result - where - T: Serialize, - { - encrypt_struct(data, password) - } + #[allow(unused)] + pub(crate) fn encrypt_struct(data: &T, password: &UserPassword) -> Result + where + T: Serialize, + { + encrypt_struct(data, password) + } - pub(crate) fn decrypt_struct(&self, password: &UserPassword) -> Result - where - T: for<'a> Deserialize<'a>, - { - decrypt_struct(self, password) - } + pub(crate) fn decrypt_struct(&self, password: &UserPassword) -> Result + where + T: for<'a> Deserialize<'a>, + { + decrypt_struct(self, password) + } } impl EncryptedData> { - #[allow(unused)] - pub(crate) fn encrypt_data(data: &[u8], password: &UserPassword) -> Result { - encrypt_data(data, password) - } + #[allow(unused)] + pub(crate) fn encrypt_data(data: &[u8], password: &UserPassword) -> Result { + encrypt_data(data, password) + } - #[allow(unused)] - pub(crate) fn decrypt_data(&self, password: &UserPassword) -> Result, BackendError> { - decrypt_data(self, password) - } + #[allow(unused)] + pub(crate) fn decrypt_data(&self, password: &UserPassword) -> Result, BackendError> { + decrypt_data(self, password) + } } fn derive_cipher_key( - password: &UserPassword, - salt: &[u8], + password: &UserPassword, + salt: &[u8], ) -> Result, BackendError> where - KeySize: ArrayLength, + KeySize: ArrayLength, { - // this can only fail if output length is either smaller than 4 or larger than 2^32 - 1 which is not the case here - let params = Params::new(MEMORY_COST, ITERATIONS, PARALLELISM, Some(OUTPUT_LENGTH)).unwrap(); + // this can only fail if output length is either smaller than 4 or larger than 2^32 - 1 which is not the case here + let params = Params::new(MEMORY_COST, ITERATIONS, PARALLELISM, Some(OUTPUT_LENGTH)).unwrap(); - let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params); + let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params); - let mut key = Key::default(); - argon2.hash_password_into(password.as_bytes(), salt, &mut key)?; + let mut key = Key::default(); + argon2.hash_password_into(password.as_bytes(), salt, &mut key)?; - Ok(key) + Ok(key) } fn random_salt_and_iv() -> (Vec, Vec) { - let mut rng = OsRng; + let mut rng = OsRng; - let mut salt = vec![0u8; SALT_LEN]; - rng.fill_bytes(&mut salt); + let mut salt = vec![0u8; SALT_LEN]; + rng.fill_bytes(&mut salt); - let mut iv = vec![0u8; IV_LEN]; - rng.fill_bytes(&mut iv); + let mut iv = vec![0u8; IV_LEN]; + rng.fill_bytes(&mut iv); - (salt, iv) + (salt, iv) } fn encrypt( - data: &[u8], - password: &UserPassword, - salt: &[u8], - iv: &[u8], + data: &[u8], + password: &UserPassword, + salt: &[u8], + iv: &[u8], ) -> Result, BackendError> { - let key = derive_cipher_key(password, salt)?; - let cipher = Aes256Gcm::new(&key); - cipher - .encrypt(Nonce::from_slice(iv), data) - .map_err(|_| BackendError::EncryptionError) + let key = derive_cipher_key(password, salt)?; + let cipher = Aes256Gcm::new(&key); + cipher + .encrypt(Nonce::from_slice(iv), data) + .map_err(|_| BackendError::EncryptionError) } fn decrypt( - ciphertext: &[u8], - password: &UserPassword, - salt: &[u8], - iv: &[u8], + ciphertext: &[u8], + password: &UserPassword, + salt: &[u8], + iv: &[u8], ) -> Result, BackendError> { - let key = derive_cipher_key(password, salt)?; - let cipher = Aes256Gcm::new(&key); - cipher - .decrypt(Nonce::from_slice(iv), ciphertext) - .map_err(|_| BackendError::DecryptionError) + let key = derive_cipher_key(password, salt)?; + let cipher = Aes256Gcm::new(&key); + cipher + .decrypt(Nonce::from_slice(iv), ciphertext) + .map_err(|_| BackendError::DecryptionError) } #[allow(unused)] pub(crate) fn encrypt_data( - data: &[u8], - password: &UserPassword, + data: &[u8], + password: &UserPassword, ) -> Result>, BackendError> { - let (salt, iv) = random_salt_and_iv(); - let ciphertext = encrypt(data, password, &salt, &iv)?; + let (salt, iv) = random_salt_and_iv(); + let ciphertext = encrypt(data, password, &salt, &iv)?; - Ok(EncryptedData { - ciphertext, - salt, - iv, - _marker: Default::default(), - }) + Ok(EncryptedData { + ciphertext, + salt, + iv, + _marker: Default::default(), + }) } pub(crate) fn encrypt_struct( - data: &T, - password: &UserPassword, + data: &T, + password: &UserPassword, ) -> Result, BackendError> where - T: Serialize, + T: Serialize, { - let bytes = serde_json::to_vec(data).map_err(|_| BackendError::EncryptionError)?; + let bytes = serde_json::to_vec(data).map_err(|_| BackendError::EncryptionError)?; - let (salt, iv) = random_salt_and_iv(); - let ciphertext = encrypt(&bytes, password, &salt, &iv)?; + let (salt, iv) = random_salt_and_iv(); + let ciphertext = encrypt(&bytes, password, &salt, &iv)?; - Ok(EncryptedData { - ciphertext, - salt, - iv, - _marker: Default::default(), - }) + Ok(EncryptedData { + ciphertext, + salt, + iv, + _marker: Default::default(), + }) } #[allow(unused)] pub(crate) fn decrypt_data( - encrypted_data: &EncryptedData>, - password: &UserPassword, + encrypted_data: &EncryptedData>, + password: &UserPassword, ) -> Result, BackendError> { - decrypt( - &encrypted_data.ciphertext, - password, - &encrypted_data.salt, - &encrypted_data.iv, - ) + decrypt( + &encrypted_data.ciphertext, + password, + &encrypted_data.salt, + &encrypted_data.iv, + ) } pub(crate) fn decrypt_struct( - encrypted_data: &EncryptedData, - password: &UserPassword, + encrypted_data: &EncryptedData, + password: &UserPassword, ) -> Result where - T: for<'a> Deserialize<'a>, + T: for<'a> Deserialize<'a>, { - let bytes = decrypt( - &encrypted_data.ciphertext, - password, - &encrypted_data.salt, - &encrypted_data.iv, - )?; + let bytes = decrypt( + &encrypted_data.ciphertext, + password, + &encrypted_data.salt, + &encrypted_data.iv, + )?; - serde_json::from_slice(&bytes).map_err(|_| BackendError::DecryptionError) + serde_json::from_slice(&bytes).map_err(|_| BackendError::DecryptionError) } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[derive(Serialize, Deserialize, PartialEq, Debug)] - struct DummyData { - foo: String, - bar: String, - } + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct DummyData { + foo: String, + bar: String, + } - #[test] - fn struct_encryption() { - let password = UserPassword::new("my-super-secret-password".to_string()); - let data = DummyData { - foo: "my secret mnemonic".to_string(), - bar: "totally-valid-hd-path".to_string(), - }; + #[test] + fn struct_encryption() { + let password = UserPassword::new("my-super-secret-password".to_string()); + let data = DummyData { + foo: "my secret mnemonic".to_string(), + bar: "totally-valid-hd-path".to_string(), + }; - let wrong_password = UserPassword::new("brute-force-attempt-1".to_string()); + let wrong_password = UserPassword::new("brute-force-attempt-1".to_string()); - let mut encrypted_data = encrypt_struct(&data, &password).unwrap(); - let recovered = decrypt_struct(&encrypted_data, &password).unwrap(); - assert_eq!(data, recovered); + let mut encrypted_data = encrypt_struct(&data, &password).unwrap(); + let recovered = decrypt_struct(&encrypted_data, &password).unwrap(); + assert_eq!(data, recovered); - // decryption with wrong password fails - assert!(decrypt_struct(&encrypted_data, &wrong_password).is_err()); + // decryption with wrong password fails + assert!(decrypt_struct(&encrypted_data, &wrong_password).is_err()); - // decryption fails if ciphertext got malformed - encrypted_data.ciphertext[3] ^= 123; - assert!(decrypt_struct(&encrypted_data, &wrong_password).is_err()); + // decryption fails if ciphertext got malformed + encrypted_data.ciphertext[3] ^= 123; + assert!(decrypt_struct(&encrypted_data, &wrong_password).is_err()); - // restore the ciphertext (for test purposes) - encrypted_data.ciphertext[3] ^= 123; + // restore the ciphertext (for test purposes) + encrypted_data.ciphertext[3] ^= 123; - // decryption fails if salt got malformed (it would result in incorrect key being derived) - encrypted_data.salt[3] ^= 123; - assert!(decrypt_struct(&encrypted_data, &password).is_err()); + // decryption fails if salt got malformed (it would result in incorrect key being derived) + encrypted_data.salt[3] ^= 123; + assert!(decrypt_struct(&encrypted_data, &password).is_err()); - // restore the salt (for test purposes) - encrypted_data.salt[3] ^= 123; + // restore the salt (for test purposes) + encrypted_data.salt[3] ^= 123; - // decryption fails if iv got malformed - encrypted_data.iv[3] ^= 123; - assert!(decrypt_struct(&encrypted_data, &password).is_err()); - } + // decryption fails if iv got malformed + encrypted_data.iv[3] ^= 123; + assert!(decrypt_struct(&encrypted_data, &password).is_err()); + } } diff --git a/nym-wallet/src-tauri/src/wallet_storage/mod.rs b/nym-wallet/src-tauri/src/wallet_storage/mod.rs index 9427471ff9..5fcc56bc1e 100644 --- a/nym-wallet/src-tauri/src/wallet_storage/mod.rs +++ b/nym-wallet/src-tauri/src/wallet_storage/mod.rs @@ -35,146 +35,146 @@ pub(crate) const DEFAULT_LOGIN_ID: &str = "default"; pub(crate) const DEFAULT_FIRST_ACCOUNT_NAME: &str = "Account 1"; fn get_storage_directory() -> Result { - tauri::api::path::local_data_dir() - .map(|dir| dir.join(STORAGE_DIR_NAME)) - .ok_or(BackendError::UnknownStorageDirectory) + tauri::api::path::local_data_dir() + .map(|dir| dir.join(STORAGE_DIR_NAME)) + .ok_or(BackendError::UnknownStorageDirectory) } pub(crate) fn wallet_login_filepath() -> Result { - get_storage_directory().map(|dir| dir.join(WALLET_INFO_FILENAME)) + get_storage_directory().map(|dir| dir.join(WALLET_INFO_FILENAME)) } fn write_to_file(filepath: &Path, wallet: &StoredWallet) -> Result<(), BackendError> { - let file = OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(filepath)?; + let file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(filepath)?; - Ok(serde_json::to_writer_pretty(file, &wallet)?) + Ok(serde_json::to_writer_pretty(file, &wallet)?) } /// Load stored wallet file #[allow(unused)] pub(crate) fn load_existing_wallet() -> Result { - let store_dir = get_storage_directory()?; - let filepath = store_dir.join(WALLET_INFO_FILENAME); - load_existing_wallet_at_file(&filepath) + let store_dir = get_storage_directory()?; + let filepath = store_dir.join(WALLET_INFO_FILENAME); + load_existing_wallet_at_file(&filepath) } fn load_existing_wallet_at_file(filepath: &Path) -> Result { - if !filepath.exists() { - return Err(BackendError::WalletFileNotFound); - } - let file = OpenOptions::new().read(true).open(filepath)?; - let wallet: StoredWallet = serde_json::from_reader(file)?; - Ok(wallet) + if !filepath.exists() { + return Err(BackendError::WalletFileNotFound); + } + let file = OpenOptions::new().read(true).open(filepath)?; + let wallet: StoredWallet = serde_json::from_reader(file)?; + Ok(wallet) } /// Load the stored wallet file and return the stored login for the given id. /// The returned login is either an account or list of (inner id, account) pairs. pub(crate) fn load_existing_login( - id: &LoginId, - password: &UserPassword, + id: &LoginId, + password: &UserPassword, ) -> Result { - let store_dir = get_storage_directory()?; - let filepath = store_dir.join(WALLET_INFO_FILENAME); - load_existing_login_at_file(&filepath, id, password) + let store_dir = get_storage_directory()?; + let filepath = store_dir.join(WALLET_INFO_FILENAME); + load_existing_login_at_file(&filepath, id, password) } pub(crate) fn load_existing_login_at_file( - filepath: &Path, - id: &LoginId, - password: &UserPassword, + filepath: &Path, + id: &LoginId, + password: &UserPassword, ) -> Result { - load_existing_wallet_at_file(filepath)?.decrypt_login(id, password) + load_existing_wallet_at_file(filepath)?.decrypt_login(id, password) } // DEPRECATED: only used in tests, where it's used to test supporting older wallet formats #[allow(unused)] #[cfg(test)] pub(crate) fn store_login( - mnemonic: bip39::Mnemonic, - hd_path: DerivationPath, - id: LoginId, - password: &UserPassword, + mnemonic: bip39::Mnemonic, + hd_path: DerivationPath, + id: LoginId, + password: &UserPassword, ) -> Result<(), BackendError> { - // make sure the entire directory structure exists - let store_dir = get_storage_directory()?; - create_dir_all(&store_dir)?; - let filepath = store_dir.join(WALLET_INFO_FILENAME); + // make sure the entire directory structure exists + let store_dir = get_storage_directory()?; + create_dir_all(&store_dir)?; + let filepath = store_dir.join(WALLET_INFO_FILENAME); - store_login_at_file(&filepath, mnemonic, hd_path, id, password) + store_login_at_file(&filepath, mnemonic, hd_path, id, password) } // DEPRECATED: only used in tests, where it's used to test supporting older wallet formats #[cfg(test)] fn store_login_at_file( - filepath: &Path, - mnemonic: bip39::Mnemonic, - hd_path: DerivationPath, - id: LoginId, - password: &UserPassword, + filepath: &Path, + mnemonic: bip39::Mnemonic, + hd_path: DerivationPath, + id: LoginId, + password: &UserPassword, ) -> Result<(), BackendError> { - let mut stored_wallet = match load_existing_wallet_at_file(filepath) { - Err(BackendError::WalletFileNotFound) => StoredWallet::default(), - result => result?, - }; + let mut stored_wallet = match load_existing_wallet_at_file(filepath) { + Err(BackendError::WalletFileNotFound) => StoredWallet::default(), + result => result?, + }; - // Confirm that the given password also can unlock the other entries. - // This is restriction we can relax in the future, but for now it's a sanity check. - if !stored_wallet.password_can_decrypt_all(password) { - return Err(BackendError::WalletDifferentPasswordDetected); - } + // Confirm that the given password also can unlock the other entries. + // This is restriction we can relax in the future, but for now it's a sanity check. + if !stored_wallet.password_can_decrypt_all(password) { + return Err(BackendError::WalletDifferentPasswordDetected); + } - let new_account = MnemonicAccount::new(mnemonic, hd_path); - let new_login = StoredLogin::Mnemonic(new_account); - let new_encrypted_account = EncryptedLogin::encrypt(id, &new_login, password)?; - stored_wallet.add_encrypted_login(new_encrypted_account)?; + let new_account = MnemonicAccount::new(mnemonic, hd_path); + let new_login = StoredLogin::Mnemonic(new_account); + let new_encrypted_account = EncryptedLogin::encrypt(id, &new_login, password)?; + stored_wallet.add_encrypted_login(new_encrypted_account)?; - write_to_file(filepath, &stored_wallet) + write_to_file(filepath, &stored_wallet) } pub(crate) fn store_login_with_multiple_accounts( - mnemonic: bip39::Mnemonic, - hd_path: DerivationPath, - id: LoginId, - password: &UserPassword, + mnemonic: bip39::Mnemonic, + hd_path: DerivationPath, + id: LoginId, + password: &UserPassword, ) -> Result<(), BackendError> { - // make sure the entire directory structure exists - let store_dir = get_storage_directory()?; - create_dir_all(&store_dir)?; - let filepath = store_dir.join(WALLET_INFO_FILENAME); + // make sure the entire directory structure exists + let store_dir = get_storage_directory()?; + create_dir_all(&store_dir)?; + let filepath = store_dir.join(WALLET_INFO_FILENAME); - store_login_with_multiple_accounts_at_file(&filepath, mnemonic, hd_path, id, password) + store_login_with_multiple_accounts_at_file(&filepath, mnemonic, hd_path, id, password) } fn store_login_with_multiple_accounts_at_file( - filepath: &Path, - mnemonic: bip39::Mnemonic, - hd_path: DerivationPath, - id: LoginId, - password: &UserPassword, + filepath: &Path, + mnemonic: bip39::Mnemonic, + hd_path: DerivationPath, + id: LoginId, + password: &UserPassword, ) -> Result<(), BackendError> { - let mut stored_wallet = match load_existing_wallet_at_file(filepath) { - Err(BackendError::WalletFileNotFound) => StoredWallet::default(), - result => result?, - }; + let mut stored_wallet = match load_existing_wallet_at_file(filepath) { + Err(BackendError::WalletFileNotFound) => StoredWallet::default(), + result => result?, + }; - // Confirm that the given password also can unlock the other entries. - // This is restriction we can relax in the future, but for now it's a sanity check. - if !stored_wallet.password_can_decrypt_all(password) { - return Err(BackendError::WalletDifferentPasswordDetected); - } + // Confirm that the given password also can unlock the other entries. + // This is restriction we can relax in the future, but for now it's a sanity check. + if !stored_wallet.password_can_decrypt_all(password) { + return Err(BackendError::WalletDifferentPasswordDetected); + } - let mut new_accounts = MultipleAccounts::new(); - new_accounts.add(DEFAULT_FIRST_ACCOUNT_NAME.into(), mnemonic, hd_path)?; - let new_login = StoredLogin::Multiple(new_accounts); - let new_encrypted_login = EncryptedLogin::encrypt(id, &new_login, password)?; + let mut new_accounts = MultipleAccounts::new(); + new_accounts.add(DEFAULT_FIRST_ACCOUNT_NAME.into(), mnemonic, hd_path)?; + let new_login = StoredLogin::Multiple(new_accounts); + let new_encrypted_login = EncryptedLogin::encrypt(id, &new_login, password)?; - stored_wallet.add_encrypted_login(new_encrypted_login)?; + stored_wallet.add_encrypted_login(new_encrypted_login)?; - write_to_file(filepath, &stored_wallet) + write_to_file(filepath, &stored_wallet) } /// Append an account to an already existing top-level encrypted account entry. @@ -182,74 +182,75 @@ fn store_login_with_multiple_accounts_at_file( /// account in the list of accounts associated with the encrypted entry. The inner id for this /// entry will be set to the same as the outer, unencrypted, id. pub(crate) fn append_account_to_login( - mnemonic: bip39::Mnemonic, - hd_path: DerivationPath, - id: LoginId, - inner_id: AccountId, - password: &UserPassword, + mnemonic: bip39::Mnemonic, + hd_path: DerivationPath, + id: LoginId, + inner_id: AccountId, + password: &UserPassword, ) -> Result<(), BackendError> { - // make sure the entire directory structure exists - let store_dir = get_storage_directory()?; - create_dir_all(&store_dir)?; - let filepath = store_dir.join(WALLET_INFO_FILENAME); + // make sure the entire directory structure exists + let store_dir = get_storage_directory()?; + create_dir_all(&store_dir)?; + let filepath = store_dir.join(WALLET_INFO_FILENAME); - append_account_to_login_at_file(&filepath, mnemonic, hd_path, id, inner_id, password) + append_account_to_login_at_file(&filepath, mnemonic, hd_path, id, inner_id, password) } fn append_account_to_login_at_file( - filepath: &Path, - mnemonic: bip39::Mnemonic, - hd_path: DerivationPath, - id: LoginId, - inner_id: AccountId, - password: &UserPassword, + filepath: &Path, + mnemonic: bip39::Mnemonic, + hd_path: DerivationPath, + id: LoginId, + inner_id: AccountId, + password: &UserPassword, ) -> Result<(), BackendError> { - let mut stored_wallet = load_existing_wallet_at_file(filepath)?; + let mut stored_wallet = load_existing_wallet_at_file(filepath)?; - let decrypted_login = stored_wallet.decrypt_login(&id, password)?; + let decrypted_login = stored_wallet.decrypt_login(&id, password)?; - // Add accounts to the inner structure. - // Note that in case we only have single account entry, without an inner_id, we convert to - // multiple accounts and we set the first inner_id to id. - let first_id_when_converting = id.clone().into(); - let mut accounts = decrypted_login.unwrap_into_multiple_accounts(first_id_when_converting); - accounts.add(inner_id, mnemonic, hd_path)?; + // Add accounts to the inner structure. + // Note that in case we only have single account entry, without an inner_id, we convert to + // multiple accounts and we set the first inner_id to id. + let first_id_when_converting = id.clone().into(); + let mut accounts = decrypted_login.unwrap_into_multiple_accounts(first_id_when_converting); + accounts.add(inner_id, mnemonic, hd_path)?; - let encrypted_accounts = EncryptedLogin::encrypt(id, &StoredLogin::Multiple(accounts), password)?; + let encrypted_accounts = + EncryptedLogin::encrypt(id, &StoredLogin::Multiple(accounts), password)?; - stored_wallet.replace_encrypted_login(encrypted_accounts)?; + stored_wallet.replace_encrypted_login(encrypted_accounts)?; - write_to_file(filepath, &stored_wallet) + write_to_file(filepath, &stored_wallet) } /// Remove the entire encrypted login entry for the given `id`. This means potentially removing all /// associated accounts! /// If this was the last entry in the file, the file is removed. pub(crate) fn remove_login(id: &LoginId) -> Result<(), BackendError> { - let store_dir = get_storage_directory()?; - let filepath = store_dir.join(WALLET_INFO_FILENAME); - remove_login_at_file(&filepath, id) + let store_dir = get_storage_directory()?; + let filepath = store_dir.join(WALLET_INFO_FILENAME); + remove_login_at_file(&filepath, id) } fn remove_login_at_file(filepath: &Path, id: &LoginId) -> Result<(), BackendError> { - log::warn!("Removing wallet account with id: {id}. This includes all associated accounts!"); - let mut stored_wallet = load_existing_wallet_at_file(filepath)?; + log::warn!("Removing wallet account with id: {id}. This includes all associated accounts!"); + let mut stored_wallet = load_existing_wallet_at_file(filepath)?; - if stored_wallet.is_empty() { - log::info!("Removing file: {:#?}", filepath); - return Ok(fs::remove_file(filepath)?); - } + if stored_wallet.is_empty() { + log::info!("Removing file: {:#?}", filepath); + return Ok(fs::remove_file(filepath)?); + } - stored_wallet - .remove_encrypted_login(id) - .ok_or(BackendError::WalletNoSuchLoginId)?; + stored_wallet + .remove_encrypted_login(id) + .ok_or(BackendError::WalletNoSuchLoginId)?; - if stored_wallet.is_empty() { - log::info!("Removing file: {:#?}", filepath); - Ok(fs::remove_file(filepath)?) - } else { - write_to_file(filepath, &stored_wallet) - } + if stored_wallet.is_empty() { + log::info!("Removing file: {:#?}", filepath); + Ok(fs::remove_file(filepath)?) + } else { + write_to_file(filepath, &stored_wallet) + } } /// Remove an account from inside the encrypted login. @@ -257,1267 +258,1283 @@ fn remove_login_at_file(filepath: &Path, id: &LoginId) -> Result<(), BackendErro /// - If it is the last associated account with that login, the encrypted login will be removed. /// - If this was the last encrypted login in the file, it will be removed. pub(crate) fn remove_account_from_login( - id: &LoginId, - inner_id: &AccountId, - password: &UserPassword, + id: &LoginId, + inner_id: &AccountId, + password: &UserPassword, ) -> Result<(), BackendError> { - let store_dir = get_storage_directory()?; - let filepath = store_dir.join(WALLET_INFO_FILENAME); - remove_account_from_login_at_file(&filepath, id, inner_id, password) + let store_dir = get_storage_directory()?; + let filepath = store_dir.join(WALLET_INFO_FILENAME); + remove_account_from_login_at_file(&filepath, id, inner_id, password) } fn remove_account_from_login_at_file( - filepath: &Path, - id: &LoginId, - inner_id: &AccountId, - password: &UserPassword, + filepath: &Path, + id: &LoginId, + inner_id: &AccountId, + password: &UserPassword, ) -> Result<(), BackendError> { - log::info!("Removing associated account from login account: {id}"); - let mut stored_wallet = load_existing_wallet_at_file(filepath)?; + log::info!("Removing associated account from login account: {id}"); + let mut stored_wallet = load_existing_wallet_at_file(filepath)?; - let mut decrypted_login = stored_wallet.decrypt_login(id, password)?; + let mut decrypted_login = stored_wallet.decrypt_login(id, password)?; - // Remove the account - let is_empty = match decrypted_login { - StoredLogin::Mnemonic(_) => { - log::warn!("Encountered mnemonic login instead of list of accounts, aborting"); - return Err(BackendError::WalletUnexpectedMnemonicAccount); + // Remove the account + let is_empty = match decrypted_login { + StoredLogin::Mnemonic(_) => { + log::warn!("Encountered mnemonic login instead of list of accounts, aborting"); + return Err(BackendError::WalletUnexpectedMnemonicAccount); + } + StoredLogin::Multiple(ref mut accounts) => { + accounts.remove(inner_id)?; + accounts.is_empty() + } + }; + + // Remove the login, or encrypt the new updated login + if is_empty { + stored_wallet + .remove_encrypted_login(id) + .ok_or(BackendError::WalletNoSuchLoginId)?; + } else { + let encrypted_accounts = EncryptedLogin::encrypt(id.clone(), &decrypted_login, password)?; + stored_wallet.replace_encrypted_login(encrypted_accounts)?; } - StoredLogin::Multiple(ref mut accounts) => { - accounts.remove(inner_id)?; - accounts.is_empty() + + // Remove the file, or write the new file + if stored_wallet.is_empty() { + log::info!("Removing file: {:#?}", filepath); + Ok(fs::remove_file(filepath)?) + } else { + write_to_file(filepath, &stored_wallet) } - }; - - // Remove the login, or encrypt the new updated login - if is_empty { - stored_wallet - .remove_encrypted_login(id) - .ok_or(BackendError::WalletNoSuchLoginId)?; - } else { - let encrypted_accounts = EncryptedLogin::encrypt(id.clone(), &decrypted_login, password)?; - stored_wallet.replace_encrypted_login(encrypted_accounts)?; - } - - // Remove the file, or write the new file - if stored_wallet.is_empty() { - log::info!("Removing file: {:#?}", filepath); - Ok(fs::remove_file(filepath)?) - } else { - write_to_file(filepath, &stored_wallet) - } } #[cfg(test)] mod tests { - use crate::wallet_storage::account_data::{MnemonicAccount, WalletAccount}; - - use super::*; - use config::defaults::COSMOS_DERIVATION_PATH; - use std::str::FromStr; - use tempfile::tempdir; - - #[test] - fn trying_to_load_nonexistant_wallet_fails() { - let store_dir = tempdir().unwrap(); - let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); - let id1 = LoginId::new("first".to_string()); - let password = UserPassword::new("password".to_string()); - - assert!(matches!( - load_existing_wallet_at_file(&wallet_file), - Err(BackendError::WalletFileNotFound), - )); - assert!(matches!( - load_existing_login_at_file(&wallet_file, &id1, &password), - Err(BackendError::WalletFileNotFound), - )); - remove_login_at_file(&wallet_file, &id1).unwrap_err(); - } - - #[test] - fn store_single_login() { - let store_dir = tempdir().unwrap(); - let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); - let account1 = bip39::Mnemonic::generate(24).unwrap(); - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let password = UserPassword::new("password".to_string()); - let id1 = LoginId::new("first".to_string()); - - store_login_at_file(&wallet_file, account1, hd_path, id1.clone(), &password).unwrap(); - - let stored_wallet = load_existing_wallet_at_file(&wallet_file).unwrap(); - assert_eq!(stored_wallet.len(), 1); - - let login = stored_wallet.get_encrypted_login_by_index(0).unwrap(); - assert_eq!(login.id, id1); - - // some actual ciphertext was saved - assert!(!login.account.ciphertext().is_empty()); - } - - #[test] - fn store_single_login_with_multi() { - let store_dir = tempdir().unwrap(); - let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); - let account1 = bip39::Mnemonic::generate(24).unwrap(); - let cosmos_hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let password = UserPassword::new("password".to_string()); - let id1 = LoginId::new("first".to_string()); - - store_login_with_multiple_accounts_at_file( - &wallet_file, - account1, - cosmos_hd_path, - id1.clone(), - &password, - ) - .unwrap(); - - let stored_wallet = load_existing_wallet_at_file(&wallet_file).unwrap(); - assert_eq!(stored_wallet.len(), 1); - - let login = stored_wallet.get_encrypted_login_by_index(0).unwrap(); - assert_eq!(login.id, id1); - - // some actual ciphertext was saved - assert!(!login.account.ciphertext().is_empty()); - } - - #[test] - fn store_twice_for_the_same_id_fails() { - let store_dir = tempdir().unwrap(); - let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); - let account1 = bip39::Mnemonic::generate(24).unwrap(); - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let password = UserPassword::new("password".to_string()); - let id1 = LoginId::new("first".to_string()); - - // Store the first login - store_login_at_file( - &wallet_file, - account1.clone(), - hd_path.clone(), - id1.clone(), - &password, - ) - .unwrap(); - - // and storing the same id again fails - assert!(matches!( - store_login_at_file(&wallet_file, account1, hd_path, id1, &password,), - Err(BackendError::WalletLoginIdAlreadyExists), - )); - } - - #[test] - fn store_twice_for_the_same_id_fails_with_multiple() { - let store_dir = tempdir().unwrap(); - let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); - let account1 = bip39::Mnemonic::generate(24).unwrap(); - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let password = UserPassword::new("password".to_string()); - let id1 = LoginId::new("first".to_string()); - - // Store the first login - store_login_with_multiple_accounts_at_file( - &wallet_file, - account1.clone(), - hd_path.clone(), - id1.clone(), - &password, - ) - .unwrap(); - - // and storing the same id again fails - assert!(matches!( - store_login_with_multiple_accounts_at_file(&wallet_file, account1, hd_path, id1, &password,), - Err(BackendError::WalletLoginIdAlreadyExists), - )); - } - - #[test] - fn load_with_wrong_password_fails() { - let store_dir = tempdir().unwrap(); - let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); - let account1 = bip39::Mnemonic::generate(24).unwrap(); - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let password = UserPassword::new("password".to_string()); - let bad_password = UserPassword::new("bad-password".to_string()); - let id1 = LoginId::new("first".to_string()); - - store_login_at_file(&wallet_file, account1, hd_path, id1.clone(), &password).unwrap(); - - // Trying to load it with wrong password now fails - assert!(matches!( - load_existing_login_at_file(&wallet_file, &id1, &bad_password), - Err(BackendError::DecryptionError), - )); - } - - #[test] - fn load_with_wrong_password_fails_with_multi() { - let store_dir = tempdir().unwrap(); - let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); - let account1 = bip39::Mnemonic::generate(24).unwrap(); - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let password = UserPassword::new("password".to_string()); - let bad_password = UserPassword::new("bad-password".to_string()); - let id1 = LoginId::new("first".to_string()); - - store_login_with_multiple_accounts_at_file( - &wallet_file, - account1, - hd_path, - id1.clone(), - &password, - ) - .unwrap(); - - // Trying to load it with wrong password now fails - assert!(matches!( - load_existing_login_at_file(&wallet_file, &id1, &bad_password), - Err(BackendError::DecryptionError), - )); - } - - #[test] - fn load_with_wrong_id_fails() { - let store_dir = tempdir().unwrap(); - let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); - let account1 = bip39::Mnemonic::generate(24).unwrap(); - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let password = UserPassword::new("password".to_string()); - let id1 = LoginId::new("first".to_string()); - let id2 = LoginId::new("second".to_string()); - - store_login_at_file(&wallet_file, account1, hd_path, id1, &password).unwrap(); - - // Trying to load with the wrong id - assert!(matches!( - load_existing_login_at_file(&wallet_file, &id2, &password), - Err(BackendError::WalletNoSuchLoginId), - )); - } - - #[test] - fn load_with_wrong_id_fails_with_multi() { - let store_dir = tempdir().unwrap(); - let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); - let account1 = bip39::Mnemonic::generate(24).unwrap(); - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let password = UserPassword::new("password".to_string()); - let id1 = LoginId::new("first".to_string()); - let id2 = LoginId::new("second".to_string()); - - store_login_with_multiple_accounts_at_file(&wallet_file, account1, hd_path, id1, &password) - .unwrap(); - - // Trying to load with the wrong id - assert!(matches!( - load_existing_login_at_file(&wallet_file, &id2, &password), - Err(BackendError::WalletNoSuchLoginId), - )); - } - - #[test] - fn store_and_load_a_single_login() { - let store_dir = tempdir().unwrap(); - let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); - let account1 = bip39::Mnemonic::generate(24).unwrap(); - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let password = UserPassword::new("password".to_string()); - let id1 = LoginId::new("first".to_string()); - - store_login_at_file( - &wallet_file, - account1.clone(), - hd_path.clone(), - id1.clone(), - &password, - ) - .unwrap(); - - let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap(); - let acc = loaded_login.as_mnemonic_account().unwrap(); - assert_eq!(&account1, acc.mnemonic()); - assert_eq!(&hd_path, acc.hd_path()); - } - - #[test] - fn store_and_load_a_single_login_with_multi() { - let store_dir = tempdir().unwrap(); - let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); - let acc1 = bip39::Mnemonic::generate(24).unwrap(); - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let password = UserPassword::new("password".to_string()); - let id1 = LoginId::new("first".to_string()); - - store_login_with_multiple_accounts_at_file( - &wallet_file, - acc1.clone(), - hd_path.clone(), - id1.clone(), - &password, - ) - .unwrap(); - - let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap(); - let accounts = loaded_login.as_multiple_accounts().unwrap(); - assert_eq!(accounts.len(), 1); - let account = accounts - .get_account(&DEFAULT_FIRST_ACCOUNT_NAME.into()) - .unwrap(); - assert_eq!(account.id().as_ref(), DEFAULT_FIRST_ACCOUNT_NAME); - assert_eq!(account.mnemonic(), &acc1); - assert_eq!(account.hd_path(), &hd_path); - } - - #[test] - fn store_a_second_login_with_a_different_password_fails() { - let store_dir = tempdir().unwrap(); - let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); - let account1 = bip39::Mnemonic::generate(24).unwrap(); - let account2 = bip39::Mnemonic::generate(24).unwrap(); - let cosmos_hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let password = UserPassword::new("password".to_string()); - let bad_password = UserPassword::new("bad-password".to_string()); - let id1 = LoginId::new("first".to_string()); - let id2 = LoginId::new("second".to_string()); - - store_login_at_file( - &wallet_file, - account1, - cosmos_hd_path.clone(), - id1, - &password, - ) - .unwrap(); - - // Can't store a second login if you use different password - assert!(matches!( - store_login_at_file(&wallet_file, account2, cosmos_hd_path, id2, &bad_password), - Err(BackendError::WalletDifferentPasswordDetected), - )); - } - - #[test] - fn store_a_second_login_with_a_different_password_fails_with_multi() { - let store_dir = tempdir().unwrap(); - let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); - let account1 = bip39::Mnemonic::generate(24).unwrap(); - let account2 = bip39::Mnemonic::generate(24).unwrap(); - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let password = UserPassword::new("password".to_string()); - let bad_password = UserPassword::new("bad-password".to_string()); - let id1 = LoginId::new("first".to_string()); - let id2 = LoginId::new("second".to_string()); - - store_login_with_multiple_accounts_at_file( - &wallet_file, - account1, - hd_path.clone(), - id1, - &password, - ) - .unwrap(); - - // Can't store a second login if you use different password - assert!(matches!( - store_login_with_multiple_accounts_at_file( - &wallet_file, - account2, - hd_path, - id2, - &bad_password - ), - Err(BackendError::WalletDifferentPasswordDetected), - )); - } - - #[test] - fn store_two_mnemonic_accounts_gives_different_salts_and_iv() { - let store_dir = tempdir().unwrap(); - let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); - let account1 = bip39::Mnemonic::generate(24).unwrap(); - let account2 = bip39::Mnemonic::generate(24).unwrap(); - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let different_hd_path: DerivationPath = "m".parse().unwrap(); - let password = UserPassword::new("password".to_string()); - let id1 = LoginId::new("first".to_string()); - let id2 = LoginId::new("second".to_string()); - - // Store the first account - store_login_at_file(&wallet_file, account1, hd_path, id1, &password).unwrap(); - - let stored_wallet = load_existing_wallet_at_file(&wallet_file).unwrap(); - let encrypted_blob = &stored_wallet - .get_encrypted_login_by_index(0) - .unwrap() - .account; - - // keep track of salt and iv for future assertion - let original_iv = encrypted_blob.iv().to_vec(); - let original_salt = encrypted_blob.salt().to_vec(); - - // Add an extra account - store_login_at_file(&wallet_file, account2, different_hd_path, id2, &password).unwrap(); - - let loaded_accounts = load_existing_wallet_at_file(&wallet_file).unwrap(); - assert_eq!(loaded_accounts.len(), 2); - let encrypted_blob = &loaded_accounts - .get_encrypted_login_by_index(1) - .unwrap() - .account; - - // fresh IV and salt are used - assert_ne!(original_iv, encrypted_blob.iv()); - assert_ne!(original_salt, encrypted_blob.salt()); - } - - #[test] - fn store_two_mnemonic_accounts_using_two_logins() { - let store_dir = tempdir().unwrap(); - let wallet = store_dir.path().join(WALLET_INFO_FILENAME); - let account1 = bip39::Mnemonic::generate(24).unwrap(); - let account2 = bip39::Mnemonic::generate(24).unwrap(); - let cosmos_hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let different_hd_path: DerivationPath = "m".parse().unwrap(); - let password = UserPassword::new("password".to_string()); - let id1 = LoginId::new("first".to_string()); - let id2 = LoginId::new("second".to_string()); - - // Store the first account - store_login_at_file( - &wallet, - account1.clone(), - cosmos_hd_path.clone(), - id1.clone(), - &password, - ) - .unwrap(); - - let login = load_existing_login_at_file(&wallet, &id1, &password).unwrap(); - let acc = login.as_mnemonic_account().unwrap(); - assert_eq!(&account1, acc.mnemonic()); - assert_eq!(&cosmos_hd_path, acc.hd_path()); - - // Add an extra account - store_login_at_file( - &wallet, - account2.clone(), - different_hd_path.clone(), - id2.clone(), - &password, - ) - .unwrap(); - - // first account should be unchanged - let loaded_login = load_existing_login_at_file(&wallet, &id1, &password).unwrap(); - let acc1 = loaded_login.as_mnemonic_account().unwrap(); - assert_eq!(&account1, acc1.mnemonic()); - assert_eq!(&cosmos_hd_path, acc1.hd_path()); - - let loaded_login = load_existing_login_at_file(&wallet, &id2, &password).unwrap(); - let acc2 = loaded_login.as_mnemonic_account().unwrap(); - assert_eq!(&account2, acc2.mnemonic()); - assert_eq!(&different_hd_path, acc2.hd_path()); - } - - #[test] - fn store_one_mnemonic_account_and_one_multi_account() { - let store_dir = tempdir().unwrap(); - let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); - let account1 = bip39::Mnemonic::generate(24).unwrap(); - let account2 = bip39::Mnemonic::generate(24).unwrap(); - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let different_hd_path: DerivationPath = "m".parse().unwrap(); - let password = UserPassword::new("password".to_string()); - let id1 = LoginId::new("first".to_string()); - let id2 = LoginId::new("second".to_string()); - - // Store the first account - store_login_at_file( - &wallet_file, - account1.clone(), - hd_path.clone(), - id1.clone(), - &password, - ) - .unwrap(); - - let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap(); - let acc = loaded_login.as_mnemonic_account().unwrap(); - assert_eq!(&account1, acc.mnemonic()); - assert_eq!(&hd_path, acc.hd_path()); - - // Add an extra account - store_login_with_multiple_accounts_at_file( - &wallet_file, - account2.clone(), - different_hd_path.clone(), - id2.clone(), - &password, - ) - .unwrap(); - - // first account should be unchanged - let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap(); - let acc1 = loaded_login.as_mnemonic_account().unwrap(); - assert_eq!(&account1, acc1.mnemonic()); - assert_eq!(&hd_path, acc1.hd_path()); - - let loaded_login = load_existing_login_at_file(&wallet_file, &id2, &password).unwrap(); - let acc2 = loaded_login.as_multiple_accounts().unwrap(); - assert_eq!(acc2.len(), 1); - let account = acc2 - .get_account(&DEFAULT_FIRST_ACCOUNT_NAME.into()) - .unwrap(); - assert_eq!(account.id().as_ref(), DEFAULT_FIRST_ACCOUNT_NAME); - assert_eq!(account.mnemonic(), &account2); - assert_eq!(account.hd_path(), &different_hd_path); - } - - #[test] - fn remove_non_existent_id_fails() { - let store_dir = tempdir().unwrap(); - let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); - let account1 = bip39::Mnemonic::generate(24).unwrap(); - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let password = UserPassword::new("password".to_string()); - let id1 = LoginId::new("first".to_string()); - let id2 = LoginId::new("second".to_string()); - - store_login_with_multiple_accounts_at_file(&wallet_file, account1, hd_path, id1, &password) - .unwrap(); - - // Fails to delete non-existent id in the wallet - assert!(matches!( - remove_login_at_file(&wallet_file, &id2), - Err(BackendError::WalletNoSuchLoginId), - )); - } - - #[test] - fn store_and_remove_wallet_login_information() { - let store_dir = tempdir().unwrap(); - let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); - let account1 = bip39::Mnemonic::generate(24).unwrap(); - let account2 = bip39::Mnemonic::generate(24).unwrap(); - let cosmos_hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let different_hd_path: DerivationPath = "m".parse().unwrap(); - let password = UserPassword::new("password".to_string()); - let id1 = LoginId::new("first".to_string()); - let id2 = LoginId::new("second".to_string()); - - // Store two accounts with two different passwords - store_login_at_file( - &wallet_file, - account1.clone(), - cosmos_hd_path.clone(), - id1.clone(), - &password, - ) - .unwrap(); - store_login_at_file( - &wallet_file, - account2.clone(), - different_hd_path.clone(), - id2.clone(), - &password, - ) - .unwrap(); - - // Load and compare - let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap(); - let acc1 = loaded_login.as_mnemonic_account().unwrap(); - assert_eq!(&account1, acc1.mnemonic()); - assert_eq!(&cosmos_hd_path, acc1.hd_path()); - - let loaded_login = load_existing_login_at_file(&wallet_file, &id2, &password).unwrap(); - let acc2 = loaded_login.as_mnemonic_account().unwrap(); - assert_eq!(&account2, acc2.mnemonic()); - assert_eq!(&different_hd_path, acc2.hd_path()); - - // Delete the second account - remove_login_at_file(&wallet_file, &id2).unwrap(); - - // The first account should be unchanged - let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap(); - let acc1 = loaded_login.as_mnemonic_account().unwrap(); - assert_eq!(&account1, acc1.mnemonic()); - assert_eq!(&cosmos_hd_path, acc1.hd_path()); - - // And we can't load the second one anymore - assert!(matches!( - load_existing_login_at_file(&wallet_file, &id2, &password), - Err(BackendError::WalletNoSuchLoginId), - )); - - // Delete the first account - assert!(wallet_file.exists()); - remove_login_at_file(&wallet_file, &id1).unwrap(); - - // The file should now be removed - assert!(!wallet_file.exists()); - - // And trying to load when the file is gone fails - assert!(matches!( - load_existing_login_at_file(&wallet_file, &id1, &password), - Err(BackendError::WalletFileNotFound), - )); - } - - #[test] - fn append_account_converts_the_type() { - let store_dir = tempdir().unwrap(); - let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); - let account1 = bip39::Mnemonic::generate(24).unwrap(); - let account2 = bip39::Mnemonic::generate(24).unwrap(); - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let password = UserPassword::new("password".to_string()); - let id1 = LoginId::new("first".to_string()); - let id2 = AccountId::new("second".to_string()); - - store_login_at_file( - &wallet_file, - account1.clone(), - hd_path.clone(), - id1.clone(), - &password, - ) - .unwrap(); - - // Check that it's there as the correct non-multiple type - let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap(); - let acc = loaded_login.as_mnemonic_account().unwrap(); - assert_eq!(acc.mnemonic(), &account1); - assert_eq!(acc.hd_path(), &hd_path); - - append_account_to_login_at_file( - &wallet_file, - account2.clone(), - hd_path.clone(), - id1.clone(), - id2.clone(), - &password, - ) - .unwrap(); - - // Check that it is now multiple mnemonic type - let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap(); - let loaded_accounts = loaded_login.as_multiple_accounts().unwrap(); - let expected = vec![ - WalletAccount::new(id1.into(), MnemonicAccount::new(account1, hd_path.clone())), - WalletAccount::new(id2, MnemonicAccount::new(account2, hd_path)), - ] - .into(); - assert_eq!(loaded_accounts, &expected); - } - - #[test] - fn append_accounts_to_existing_login() { - let store_dir = tempdir().unwrap(); - let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); - let account1 = bip39::Mnemonic::generate(24).unwrap(); - let account2 = bip39::Mnemonic::generate(24).unwrap(); - let account3 = bip39::Mnemonic::generate(24).unwrap(); - let account4 = bip39::Mnemonic::generate(24).unwrap(); - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let password = UserPassword::new("password".to_string()); - let id1 = LoginId::new("first".to_string()); - let id2 = LoginId::new("second".to_string()); - let id3 = AccountId::new("third".to_string()); - let id4 = AccountId::new("fourth".to_string()); - - store_login_at_file( - &wallet_file, - account1.clone(), - hd_path.clone(), - id1.clone(), - &password, - ) - .unwrap(); - - store_login_at_file( - &wallet_file, - account2.clone(), - hd_path.clone(), - id2.clone(), - &password, - ) - .unwrap(); - - // Check that it's there as the correct non-multiple type - let loaded_login = load_existing_login_at_file(&wallet_file, &id2, &password).unwrap(); - let acc2 = loaded_login.as_mnemonic_account().unwrap(); - assert_eq!(acc2.mnemonic(), &account2); - assert_eq!(acc2.hd_path(), &hd_path); - - // Add a third and fourth mnenonic grouped together with the second one - append_account_to_login_at_file( - &wallet_file, - account3.clone(), - hd_path.clone(), - id2.clone(), - id3.clone(), - &password, - ) - .unwrap(); - append_account_to_login_at_file( - &wallet_file, - account4.clone(), - hd_path.clone(), - id2.clone(), - id4.clone(), - &password, - ) - .unwrap(); - - // Check that we can load all four - let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap(); - let acc1 = loaded_login.as_mnemonic_account().unwrap(); - assert_eq!(acc1.mnemonic(), &account1); - assert_eq!(acc1.hd_path(), &hd_path); - - let loaded_login = load_existing_login_at_file(&wallet_file, &id2, &password).unwrap(); - let loaded_accounts = loaded_login.as_multiple_accounts().unwrap(); - let expected = vec![ - WalletAccount::new(id2.into(), MnemonicAccount::new(account2, hd_path.clone())), - WalletAccount::new(id3, MnemonicAccount::new(account3, hd_path.clone())), - WalletAccount::new(id4, MnemonicAccount::new(account4, hd_path)), - ] - .into(); - assert_eq!(loaded_accounts, &expected); - } - - #[test] - fn append_accounts_to_existing_login_with_multi() { - let store_dir = tempdir().unwrap(); - let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); - let account1 = bip39::Mnemonic::generate(24).unwrap(); - let account2 = bip39::Mnemonic::generate(24).unwrap(); - let account3 = bip39::Mnemonic::generate(24).unwrap(); - let account4 = bip39::Mnemonic::generate(24).unwrap(); - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let password = UserPassword::new("password".to_string()); - let id1 = LoginId::new("first".to_string()); - let id2 = LoginId::new("second".to_string()); - let id3 = AccountId::new("third".to_string()); - let id4 = AccountId::new("fourth".to_string()); - - store_login_with_multiple_accounts_at_file( - &wallet_file, - account1.clone(), - hd_path.clone(), - id1.clone(), - &password, - ) - .unwrap(); - - store_login_with_multiple_accounts_at_file( - &wallet_file, - account2.clone(), - hd_path.clone(), - id2.clone(), - &password, - ) - .unwrap(); - - // Add a third and fourth mnenonic grouped together with the second one - append_account_to_login_at_file( - &wallet_file, - account3.clone(), - hd_path.clone(), - id2.clone(), - id3.clone(), - &password, - ) - .unwrap(); - append_account_to_login_at_file( - &wallet_file, - account4.clone(), - hd_path.clone(), - id2.clone(), - id4.clone(), - &password, - ) - .unwrap(); - - // Check that we can load all four - let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap(); - let loaded_accounts = loaded_login.as_multiple_accounts().unwrap(); - let expected = vec![WalletAccount::new( - DEFAULT_FIRST_ACCOUNT_NAME.into(), - MnemonicAccount::new(account1, hd_path.clone()), - )] - .into(); - assert_eq!(loaded_accounts, &expected); - - let loaded_login = load_existing_login_at_file(&wallet_file, &id2, &password).unwrap(); - let loaded_accounts = loaded_login.as_multiple_accounts().unwrap(); - let expected = vec![ - WalletAccount::new( - DEFAULT_FIRST_ACCOUNT_NAME.into(), - MnemonicAccount::new(account2, hd_path.clone()), - ), - WalletAccount::new(id3, MnemonicAccount::new(account3, hd_path.clone())), - WalletAccount::new(id4, MnemonicAccount::new(account4, hd_path)), - ] - .into(); - assert_eq!(loaded_accounts, &expected); - } - - #[test] - fn append_the_same_mnemonic_twice_fails() { - let store_dir = tempdir().unwrap(); - let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); - let account1 = bip39::Mnemonic::generate(24).unwrap(); - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let password = UserPassword::new("password".to_string()); - let id1 = LoginId::new("first".to_string()); - let id2 = AccountId::new("second".to_string()); - - store_login_with_multiple_accounts_at_file( - &wallet_file, - account1.clone(), - hd_path.clone(), - id1.clone(), - &password, - ) - .unwrap(); - - assert!(matches!( - append_account_to_login_at_file(&wallet_file, account1, hd_path, id1, id2, &password,), - Err(BackendError::WalletMnemonicAlreadyExistsInWalletLogin), - )) - } - - #[test] - fn delete_the_same_account_twice_for_a_login_fails() { - let store_dir = tempdir().unwrap(); - let wallet = store_dir.path().join(WALLET_INFO_FILENAME); - let account1 = bip39::Mnemonic::generate(24).unwrap(); - let account2 = bip39::Mnemonic::generate(24).unwrap(); - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let password = UserPassword::new("password".to_string()); - let id1 = LoginId::new("first".to_string()); - let id2 = AccountId::new("second".to_string()); - - store_login_at_file(&wallet, account1, hd_path.clone(), id1.clone(), &password).unwrap(); - - append_account_to_login_at_file( - &wallet, - account2, - hd_path, - id1.clone(), - id2.clone(), - &password, - ) - .unwrap(); - - remove_account_from_login_at_file(&wallet, &id1, &id2, &password).unwrap(); - - assert!(matches!( - remove_account_from_login_at_file(&wallet, &id1, &id2, &password), - Err(BackendError::WalletNoSuchAccountIdInWalletLogin), - )); - } - - #[test] - fn delete_the_same_account_twice_for_a_login_fails_with_multi() { - let store_dir = tempdir().unwrap(); - let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); - let account1 = bip39::Mnemonic::generate(24).unwrap(); - let account2 = bip39::Mnemonic::generate(24).unwrap(); - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let password = UserPassword::new("password".to_string()); - let id1 = LoginId::new("first".to_string()); - let id2 = AccountId::new("second".to_string()); - - store_login_with_multiple_accounts_at_file( - &wallet_file, - account1, - hd_path.clone(), - id1.clone(), - &password, - ) - .unwrap(); - - append_account_to_login_at_file( - &wallet_file, - account2, - hd_path, - id1.clone(), - id2.clone(), - &password, - ) - .unwrap(); - - remove_account_from_login_at_file(&wallet_file, &id1, &id2, &password).unwrap(); - - assert!(matches!( - remove_account_from_login_at_file(&wallet_file, &id1, &id2, &password), - Err(BackendError::WalletNoSuchAccountIdInWalletLogin), - )); - } - - #[test] - fn delete_appended_account_doesnt_affect_others() { - let store_dir = tempdir().unwrap(); - let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); - let account1 = bip39::Mnemonic::generate(24).unwrap(); - let account2 = bip39::Mnemonic::generate(24).unwrap(); - let account3 = bip39::Mnemonic::generate(24).unwrap(); - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let password = UserPassword::new("password".to_string()); - let id1 = LoginId::new("first".to_string()); - let id2 = LoginId::new("second".to_string()); - let id3 = AccountId::new("third".to_string()); - - store_login_at_file( - &wallet_file, - account1, - hd_path.clone(), - id1.clone(), - &password, - ) - .unwrap(); - - store_login_at_file( - &wallet_file, - account2.clone(), - hd_path.clone(), - id2.clone(), - &password, - ) - .unwrap(); - - append_account_to_login_at_file( - &wallet_file, - account3.clone(), - hd_path.clone(), - id2.clone(), - id3.clone(), - &password, - ) - .unwrap(); - - remove_login_at_file(&wallet_file, &id1).unwrap(); - - // The second login one is still there - let loaded_login = load_existing_login_at_file(&wallet_file, &id2, &password).unwrap(); - let loaded_accounts = loaded_login.as_multiple_accounts().unwrap(); - let expected = vec![ - WalletAccount::new(id2.into(), MnemonicAccount::new(account2, hd_path.clone())), - WalletAccount::new(id3, MnemonicAccount::new(account3, hd_path)), - ] - .into(); - assert_eq!(loaded_accounts, &expected); - } - - #[test] - fn remove_all_accounts_for_a_login_removes_the_file_when_empty() { - let store_dir = tempdir().unwrap(); - let wallet = store_dir.path().join(WALLET_INFO_FILENAME); - let account1 = bip39::Mnemonic::generate(24).unwrap(); - let account2 = bip39::Mnemonic::generate(24).unwrap(); - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let password = UserPassword::new("password".to_string()); - let id1 = LoginId::new("first".to_string()); - let id2 = AccountId::new("second".to_string()); - - store_login_with_multiple_accounts_at_file( - &wallet, - account1, - hd_path.clone(), - id1.clone(), - &password, - ) - .unwrap(); - - append_account_to_login_at_file( - &wallet, - account2, - hd_path, - id1.clone(), - id2.clone(), - &password, - ) - .unwrap(); - - remove_account_from_login_at_file(&wallet, &id1, &DEFAULT_FIRST_ACCOUNT_NAME.into(), &password) - .unwrap(); - remove_account_from_login_at_file(&wallet, &id1, &id2, &password).unwrap(); - - // The file should now be removed - assert!(!wallet.exists()); - - // And trying to load when the file is gone fails - assert!(matches!( - load_existing_login_at_file(&wallet, &id1, &password), - Err(BackendError::WalletFileNotFound), - )); - } - - #[test] - fn remove_all_accounts_for_a_login_removes_that_login() { - let store_dir = tempdir().unwrap(); - let wallet = store_dir.path().join(WALLET_INFO_FILENAME); - let account1 = bip39::Mnemonic::generate(24).unwrap(); - let account2 = bip39::Mnemonic::generate(24).unwrap(); - let account3 = bip39::Mnemonic::generate(24).unwrap(); - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let password = UserPassword::new("password".to_string()); - let id1 = LoginId::new("first".to_string()); - let id2 = AccountId::new("second".to_string()); - let id3 = LoginId::new("third".to_string()); - - store_login_with_multiple_accounts_at_file( - &wallet, - account1, - hd_path.clone(), - id1.clone(), - &password, - ) - .unwrap(); - - append_account_to_login_at_file( - &wallet, - account2, - hd_path.clone(), - id1.clone(), - id2.clone(), - &password, - ) - .unwrap(); - - store_login_with_multiple_accounts_at_file( - &wallet, - account3.clone(), - hd_path.clone(), - id3.clone(), - &password, - ) - .unwrap(); - - remove_account_from_login_at_file(&wallet, &id1, &DEFAULT_FIRST_ACCOUNT_NAME.into(), &password) - .unwrap(); - remove_account_from_login_at_file(&wallet, &id1, &id2, &password).unwrap(); - - // And trying to load when the file is gone fails - assert!(matches!( - load_existing_login_at_file(&wallet, &id1, &password), - Err(BackendError::WalletNoSuchLoginId), - )); - - // The other login is still there - let loaded_login = load_existing_login_at_file(&wallet, &id3, &password).unwrap(); - let acc3 = loaded_login.as_multiple_accounts().unwrap(); - let expected = vec![WalletAccount::new( - DEFAULT_FIRST_ACCOUNT_NAME.into(), - MnemonicAccount::new(account3, hd_path), - )] - .into(); - assert_eq!(acc3, &expected); - } - - #[test] - fn append_accounts_and_remove_appended_accounts() { - let store_dir = tempdir().unwrap(); - let wallet = store_dir.path().join(WALLET_INFO_FILENAME); - let acc1 = bip39::Mnemonic::generate(24).unwrap(); - let acc2 = bip39::Mnemonic::generate(24).unwrap(); - let acc3 = bip39::Mnemonic::generate(24).unwrap(); - let acc4 = bip39::Mnemonic::generate(24).unwrap(); - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let password = UserPassword::new("password".to_string()); - let id1 = LoginId::new("first".to_string()); - let id2 = LoginId::new("second".to_string()); - let id3 = AccountId::new("third".to_string()); - let id4 = AccountId::new("fourth".to_string()); - - store_login_at_file( - &wallet, - acc1.clone(), - hd_path.clone(), - id1.clone(), - &password, - ) - .unwrap(); - - store_login_at_file( - &wallet, - acc2.clone(), - hd_path.clone(), - id2.clone(), - &password, - ) - .unwrap(); - - // Add a third and fourth mnenonic grouped together with the second one - append_account_to_login_at_file( - &wallet, - acc3, - hd_path.clone(), - id2.clone(), - id3.clone(), - &password, - ) - .unwrap(); - append_account_to_login_at_file( - &wallet, - acc4.clone(), - hd_path.clone(), - id2.clone(), - id4.clone(), - &password, - ) - .unwrap(); - - // Delete the third mnemonic, from the second login entry - remove_account_from_login_at_file(&wallet, &id2, &id3, &password).unwrap(); - - // Check that we can still load the other accounts - let loaded_login = load_existing_login_at_file(&wallet, &id2, &password).unwrap(); - let loaded_accounts = loaded_login.as_multiple_accounts().unwrap(); - let expected = vec![ - WalletAccount::new( - id2.clone().into(), - MnemonicAccount::new(acc2, hd_path.clone()), - ), - WalletAccount::new(id4.clone(), MnemonicAccount::new(acc4, hd_path.clone())), - ] - .into(); - assert_eq!(loaded_accounts, &expected); - - // Delete the second and fourth mnemonic from the second login entry removes the login entry - remove_account_from_login_at_file(&wallet, &id2, &id2.clone().into(), &password).unwrap(); - remove_account_from_login_at_file(&wallet, &id2, &id4, &password).unwrap(); - assert!(matches!( - load_existing_login_at_file(&wallet, &id2, &password), - Err(BackendError::WalletNoSuchLoginId), - )); - - // The first login is still available - let loaded_login = load_existing_login_at_file(&wallet, &id1, &password).unwrap(); - let account = loaded_login.as_mnemonic_account().unwrap(); - assert_eq!(account.mnemonic(), &acc1); - assert_eq!(account.hd_path(), &hd_path); - } - - // Test to that decrypts a stored file from the repo, to make sure we are able to decrypt stored - // wallets created with older versions. - #[test] - fn decrypt_stored_wallet() { - const SAVED_WALLET: &str = "src/wallet_storage/test-data/saved-wallet.json"; - let wallet_file = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(SAVED_WALLET); - - let wallet = load_existing_wallet_at_file(&wallet_file).unwrap(); - - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let password = UserPassword::new("password".to_string()); - let bad_password = UserPassword::new("bad-password".to_string()); - let id1 = LoginId::new("first".to_string()); - let id2 = LoginId::new("second".to_string()); - - assert!(!wallet.password_can_decrypt_all(&bad_password)); - assert!(wallet.password_can_decrypt_all(&password)); - - let acc1 = wallet.decrypt_login(&id1, &password).unwrap(); - let acc2 = wallet.decrypt_login(&id2, &password).unwrap(); - - assert!(matches!(acc1, StoredLogin::Mnemonic(_))); - assert!(matches!(acc2, StoredLogin::Mnemonic(_))); - - let expected_acc1 = bip39::Mnemonic::from_str("country mean universe text phone begin deputy reject result good cram illness common cluster proud swamp digital patrol spread bar face december base kick").unwrap(); - let expected_acc2 = bip39::Mnemonic::from_str("home mansion start quiz dress decide hint second dragon sunny juice always steak real minimum art rival skin draw total pulp foot goddess agent").unwrap(); - - assert_eq!( - acc1.as_mnemonic_account().unwrap().mnemonic(), - &expected_acc1 - ); - assert_eq!(acc1.as_mnemonic_account().unwrap().hd_path(), &hd_path,); - - assert_eq!( - acc2.as_mnemonic_account().unwrap().mnemonic(), - &expected_acc2 - ); - assert_eq!(acc2.as_mnemonic_account().unwrap().hd_path(), &hd_path,); - } - - #[test] - fn decrypt_stored_wallet_1_0_4() { - const SAVED_WALLET: &str = "src/wallet_storage/test-data/saved-wallet-1.0.4.json"; - let wallet_file = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(SAVED_WALLET); - - let wallet = load_existing_wallet_at_file(&wallet_file).unwrap(); - - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let password = UserPassword::new("password11!".to_string()); - let bad_password = UserPassword::new("bad-password".to_string()); - let login_id = LoginId::new("default".to_string()); - - assert!(!wallet.password_can_decrypt_all(&bad_password)); - assert!(wallet.password_can_decrypt_all(&password)); - - let acc1 = wallet.decrypt_login(&login_id, &password).unwrap(); - - assert!(matches!(acc1, StoredLogin::Mnemonic(_))); - - let expected_acc1 = bip39::Mnemonic::from_str("arrow capable abstract industry elevator nominee december piece hotel feed lounge web faint sword veteran bundle hour page actual laptop horror gold test warrior").unwrap(); - - assert_eq!( - acc1.as_mnemonic_account().unwrap().mnemonic(), - &expected_acc1 - ); - assert_eq!(acc1.as_mnemonic_account().unwrap().hd_path(), &hd_path,); - } - - #[test] - fn decrypt_stored_wallet_1_0_5_with_multiple_accounts() { - const SAVED_WALLET: &str = "src/wallet_storage/test-data/saved-wallet-1.0.5.json"; - let wallet_file = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(SAVED_WALLET); - - let wallet = load_existing_wallet_at_file(&wallet_file).unwrap(); - - let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); - let password = UserPassword::new("password11!".to_string()); - let bad_password = UserPassword::new("bad-password".to_string()); - let login_id = LoginId::new("default".to_string()); - - assert!(!wallet.password_can_decrypt_all(&bad_password)); - assert!(wallet.password_can_decrypt_all(&password)); - - let login = wallet.decrypt_login(&login_id, &password).unwrap(); - - assert!(matches!(login, StoredLogin::Multiple(_))); - - let login = login.as_multiple_accounts().unwrap(); - assert_eq!(login.len(), 4); - - let expected_mn1 = bip39::Mnemonic::from_str("arrow capable abstract industry elevator nominee december piece hotel feed lounge web faint sword veteran bundle hour page actual laptop horror gold test warrior").unwrap(); - let expected_mn2 = bip39::Mnemonic::from_str("border hurt skull lunar goddess second danger game dismiss exhaust oven thumb dog drama onion false orchard spice tent next predict invite cherry green").unwrap(); - let expected_mn3 = bip39::Mnemonic::from_str("gentle crowd rule snap girl urge flat jump winner cluster night sand museum stock grunt quick tree acquire traffic major awake tag rack peasant").unwrap(); - let expected_mn4 = bip39::Mnemonic::from_str("debris blue skin annual inhale text border rigid spatial lesson coconut yard horn crystal control survey version vote hawk neck frame arrive oblige width").unwrap(); - - let expected = vec![ - WalletAccount::new( - "default".into(), - MnemonicAccount::new(expected_mn1, hd_path.clone()), - ), - WalletAccount::new( - "account2".into(), - MnemonicAccount::new(expected_mn2, hd_path.clone()), - ), - WalletAccount::new( - "foobar".into(), - MnemonicAccount::new(expected_mn3, hd_path.clone()), - ), - WalletAccount::new("42".into(), MnemonicAccount::new(expected_mn4, hd_path)), - ] - .into(); - - assert_eq!(login, &expected); - } + use crate::wallet_storage::account_data::{MnemonicAccount, WalletAccount}; + + use super::*; + use config::defaults::COSMOS_DERIVATION_PATH; + use std::str::FromStr; + use tempfile::tempdir; + + #[test] + fn trying_to_load_nonexistant_wallet_fails() { + let store_dir = tempdir().unwrap(); + let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); + let id1 = LoginId::new("first".to_string()); + let password = UserPassword::new("password".to_string()); + + assert!(matches!( + load_existing_wallet_at_file(&wallet_file), + Err(BackendError::WalletFileNotFound), + )); + assert!(matches!( + load_existing_login_at_file(&wallet_file, &id1, &password), + Err(BackendError::WalletFileNotFound), + )); + remove_login_at_file(&wallet_file, &id1).unwrap_err(); + } + + #[test] + fn store_single_login() { + let store_dir = tempdir().unwrap(); + let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); + let account1 = bip39::Mnemonic::generate(24).unwrap(); + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let password = UserPassword::new("password".to_string()); + let id1 = LoginId::new("first".to_string()); + + store_login_at_file(&wallet_file, account1, hd_path, id1.clone(), &password).unwrap(); + + let stored_wallet = load_existing_wallet_at_file(&wallet_file).unwrap(); + assert_eq!(stored_wallet.len(), 1); + + let login = stored_wallet.get_encrypted_login_by_index(0).unwrap(); + assert_eq!(login.id, id1); + + // some actual ciphertext was saved + assert!(!login.account.ciphertext().is_empty()); + } + + #[test] + fn store_single_login_with_multi() { + let store_dir = tempdir().unwrap(); + let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); + let account1 = bip39::Mnemonic::generate(24).unwrap(); + let cosmos_hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let password = UserPassword::new("password".to_string()); + let id1 = LoginId::new("first".to_string()); + + store_login_with_multiple_accounts_at_file( + &wallet_file, + account1, + cosmos_hd_path, + id1.clone(), + &password, + ) + .unwrap(); + + let stored_wallet = load_existing_wallet_at_file(&wallet_file).unwrap(); + assert_eq!(stored_wallet.len(), 1); + + let login = stored_wallet.get_encrypted_login_by_index(0).unwrap(); + assert_eq!(login.id, id1); + + // some actual ciphertext was saved + assert!(!login.account.ciphertext().is_empty()); + } + + #[test] + fn store_twice_for_the_same_id_fails() { + let store_dir = tempdir().unwrap(); + let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); + let account1 = bip39::Mnemonic::generate(24).unwrap(); + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let password = UserPassword::new("password".to_string()); + let id1 = LoginId::new("first".to_string()); + + // Store the first login + store_login_at_file( + &wallet_file, + account1.clone(), + hd_path.clone(), + id1.clone(), + &password, + ) + .unwrap(); + + // and storing the same id again fails + assert!(matches!( + store_login_at_file(&wallet_file, account1, hd_path, id1, &password,), + Err(BackendError::WalletLoginIdAlreadyExists), + )); + } + + #[test] + fn store_twice_for_the_same_id_fails_with_multiple() { + let store_dir = tempdir().unwrap(); + let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); + let account1 = bip39::Mnemonic::generate(24).unwrap(); + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let password = UserPassword::new("password".to_string()); + let id1 = LoginId::new("first".to_string()); + + // Store the first login + store_login_with_multiple_accounts_at_file( + &wallet_file, + account1.clone(), + hd_path.clone(), + id1.clone(), + &password, + ) + .unwrap(); + + // and storing the same id again fails + assert!(matches!( + store_login_with_multiple_accounts_at_file( + &wallet_file, + account1, + hd_path, + id1, + &password, + ), + Err(BackendError::WalletLoginIdAlreadyExists), + )); + } + + #[test] + fn load_with_wrong_password_fails() { + let store_dir = tempdir().unwrap(); + let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); + let account1 = bip39::Mnemonic::generate(24).unwrap(); + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let password = UserPassword::new("password".to_string()); + let bad_password = UserPassword::new("bad-password".to_string()); + let id1 = LoginId::new("first".to_string()); + + store_login_at_file(&wallet_file, account1, hd_path, id1.clone(), &password).unwrap(); + + // Trying to load it with wrong password now fails + assert!(matches!( + load_existing_login_at_file(&wallet_file, &id1, &bad_password), + Err(BackendError::DecryptionError), + )); + } + + #[test] + fn load_with_wrong_password_fails_with_multi() { + let store_dir = tempdir().unwrap(); + let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); + let account1 = bip39::Mnemonic::generate(24).unwrap(); + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let password = UserPassword::new("password".to_string()); + let bad_password = UserPassword::new("bad-password".to_string()); + let id1 = LoginId::new("first".to_string()); + + store_login_with_multiple_accounts_at_file( + &wallet_file, + account1, + hd_path, + id1.clone(), + &password, + ) + .unwrap(); + + // Trying to load it with wrong password now fails + assert!(matches!( + load_existing_login_at_file(&wallet_file, &id1, &bad_password), + Err(BackendError::DecryptionError), + )); + } + + #[test] + fn load_with_wrong_id_fails() { + let store_dir = tempdir().unwrap(); + let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); + let account1 = bip39::Mnemonic::generate(24).unwrap(); + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let password = UserPassword::new("password".to_string()); + let id1 = LoginId::new("first".to_string()); + let id2 = LoginId::new("second".to_string()); + + store_login_at_file(&wallet_file, account1, hd_path, id1, &password).unwrap(); + + // Trying to load with the wrong id + assert!(matches!( + load_existing_login_at_file(&wallet_file, &id2, &password), + Err(BackendError::WalletNoSuchLoginId), + )); + } + + #[test] + fn load_with_wrong_id_fails_with_multi() { + let store_dir = tempdir().unwrap(); + let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); + let account1 = bip39::Mnemonic::generate(24).unwrap(); + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let password = UserPassword::new("password".to_string()); + let id1 = LoginId::new("first".to_string()); + let id2 = LoginId::new("second".to_string()); + + store_login_with_multiple_accounts_at_file(&wallet_file, account1, hd_path, id1, &password) + .unwrap(); + + // Trying to load with the wrong id + assert!(matches!( + load_existing_login_at_file(&wallet_file, &id2, &password), + Err(BackendError::WalletNoSuchLoginId), + )); + } + + #[test] + fn store_and_load_a_single_login() { + let store_dir = tempdir().unwrap(); + let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); + let account1 = bip39::Mnemonic::generate(24).unwrap(); + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let password = UserPassword::new("password".to_string()); + let id1 = LoginId::new("first".to_string()); + + store_login_at_file( + &wallet_file, + account1.clone(), + hd_path.clone(), + id1.clone(), + &password, + ) + .unwrap(); + + let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap(); + let acc = loaded_login.as_mnemonic_account().unwrap(); + assert_eq!(&account1, acc.mnemonic()); + assert_eq!(&hd_path, acc.hd_path()); + } + + #[test] + fn store_and_load_a_single_login_with_multi() { + let store_dir = tempdir().unwrap(); + let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); + let acc1 = bip39::Mnemonic::generate(24).unwrap(); + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let password = UserPassword::new("password".to_string()); + let id1 = LoginId::new("first".to_string()); + + store_login_with_multiple_accounts_at_file( + &wallet_file, + acc1.clone(), + hd_path.clone(), + id1.clone(), + &password, + ) + .unwrap(); + + let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap(); + let accounts = loaded_login.as_multiple_accounts().unwrap(); + assert_eq!(accounts.len(), 1); + let account = accounts + .get_account(&DEFAULT_FIRST_ACCOUNT_NAME.into()) + .unwrap(); + assert_eq!(account.id().as_ref(), DEFAULT_FIRST_ACCOUNT_NAME); + assert_eq!(account.mnemonic(), &acc1); + assert_eq!(account.hd_path(), &hd_path); + } + + #[test] + fn store_a_second_login_with_a_different_password_fails() { + let store_dir = tempdir().unwrap(); + let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); + let account1 = bip39::Mnemonic::generate(24).unwrap(); + let account2 = bip39::Mnemonic::generate(24).unwrap(); + let cosmos_hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let password = UserPassword::new("password".to_string()); + let bad_password = UserPassword::new("bad-password".to_string()); + let id1 = LoginId::new("first".to_string()); + let id2 = LoginId::new("second".to_string()); + + store_login_at_file( + &wallet_file, + account1, + cosmos_hd_path.clone(), + id1, + &password, + ) + .unwrap(); + + // Can't store a second login if you use different password + assert!(matches!( + store_login_at_file(&wallet_file, account2, cosmos_hd_path, id2, &bad_password), + Err(BackendError::WalletDifferentPasswordDetected), + )); + } + + #[test] + fn store_a_second_login_with_a_different_password_fails_with_multi() { + let store_dir = tempdir().unwrap(); + let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); + let account1 = bip39::Mnemonic::generate(24).unwrap(); + let account2 = bip39::Mnemonic::generate(24).unwrap(); + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let password = UserPassword::new("password".to_string()); + let bad_password = UserPassword::new("bad-password".to_string()); + let id1 = LoginId::new("first".to_string()); + let id2 = LoginId::new("second".to_string()); + + store_login_with_multiple_accounts_at_file( + &wallet_file, + account1, + hd_path.clone(), + id1, + &password, + ) + .unwrap(); + + // Can't store a second login if you use different password + assert!(matches!( + store_login_with_multiple_accounts_at_file( + &wallet_file, + account2, + hd_path, + id2, + &bad_password + ), + Err(BackendError::WalletDifferentPasswordDetected), + )); + } + + #[test] + fn store_two_mnemonic_accounts_gives_different_salts_and_iv() { + let store_dir = tempdir().unwrap(); + let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); + let account1 = bip39::Mnemonic::generate(24).unwrap(); + let account2 = bip39::Mnemonic::generate(24).unwrap(); + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let different_hd_path: DerivationPath = "m".parse().unwrap(); + let password = UserPassword::new("password".to_string()); + let id1 = LoginId::new("first".to_string()); + let id2 = LoginId::new("second".to_string()); + + // Store the first account + store_login_at_file(&wallet_file, account1, hd_path, id1, &password).unwrap(); + + let stored_wallet = load_existing_wallet_at_file(&wallet_file).unwrap(); + let encrypted_blob = &stored_wallet + .get_encrypted_login_by_index(0) + .unwrap() + .account; + + // keep track of salt and iv for future assertion + let original_iv = encrypted_blob.iv().to_vec(); + let original_salt = encrypted_blob.salt().to_vec(); + + // Add an extra account + store_login_at_file(&wallet_file, account2, different_hd_path, id2, &password).unwrap(); + + let loaded_accounts = load_existing_wallet_at_file(&wallet_file).unwrap(); + assert_eq!(loaded_accounts.len(), 2); + let encrypted_blob = &loaded_accounts + .get_encrypted_login_by_index(1) + .unwrap() + .account; + + // fresh IV and salt are used + assert_ne!(original_iv, encrypted_blob.iv()); + assert_ne!(original_salt, encrypted_blob.salt()); + } + + #[test] + fn store_two_mnemonic_accounts_using_two_logins() { + let store_dir = tempdir().unwrap(); + let wallet = store_dir.path().join(WALLET_INFO_FILENAME); + let account1 = bip39::Mnemonic::generate(24).unwrap(); + let account2 = bip39::Mnemonic::generate(24).unwrap(); + let cosmos_hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let different_hd_path: DerivationPath = "m".parse().unwrap(); + let password = UserPassword::new("password".to_string()); + let id1 = LoginId::new("first".to_string()); + let id2 = LoginId::new("second".to_string()); + + // Store the first account + store_login_at_file( + &wallet, + account1.clone(), + cosmos_hd_path.clone(), + id1.clone(), + &password, + ) + .unwrap(); + + let login = load_existing_login_at_file(&wallet, &id1, &password).unwrap(); + let acc = login.as_mnemonic_account().unwrap(); + assert_eq!(&account1, acc.mnemonic()); + assert_eq!(&cosmos_hd_path, acc.hd_path()); + + // Add an extra account + store_login_at_file( + &wallet, + account2.clone(), + different_hd_path.clone(), + id2.clone(), + &password, + ) + .unwrap(); + + // first account should be unchanged + let loaded_login = load_existing_login_at_file(&wallet, &id1, &password).unwrap(); + let acc1 = loaded_login.as_mnemonic_account().unwrap(); + assert_eq!(&account1, acc1.mnemonic()); + assert_eq!(&cosmos_hd_path, acc1.hd_path()); + + let loaded_login = load_existing_login_at_file(&wallet, &id2, &password).unwrap(); + let acc2 = loaded_login.as_mnemonic_account().unwrap(); + assert_eq!(&account2, acc2.mnemonic()); + assert_eq!(&different_hd_path, acc2.hd_path()); + } + + #[test] + fn store_one_mnemonic_account_and_one_multi_account() { + let store_dir = tempdir().unwrap(); + let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); + let account1 = bip39::Mnemonic::generate(24).unwrap(); + let account2 = bip39::Mnemonic::generate(24).unwrap(); + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let different_hd_path: DerivationPath = "m".parse().unwrap(); + let password = UserPassword::new("password".to_string()); + let id1 = LoginId::new("first".to_string()); + let id2 = LoginId::new("second".to_string()); + + // Store the first account + store_login_at_file( + &wallet_file, + account1.clone(), + hd_path.clone(), + id1.clone(), + &password, + ) + .unwrap(); + + let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap(); + let acc = loaded_login.as_mnemonic_account().unwrap(); + assert_eq!(&account1, acc.mnemonic()); + assert_eq!(&hd_path, acc.hd_path()); + + // Add an extra account + store_login_with_multiple_accounts_at_file( + &wallet_file, + account2.clone(), + different_hd_path.clone(), + id2.clone(), + &password, + ) + .unwrap(); + + // first account should be unchanged + let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap(); + let acc1 = loaded_login.as_mnemonic_account().unwrap(); + assert_eq!(&account1, acc1.mnemonic()); + assert_eq!(&hd_path, acc1.hd_path()); + + let loaded_login = load_existing_login_at_file(&wallet_file, &id2, &password).unwrap(); + let acc2 = loaded_login.as_multiple_accounts().unwrap(); + assert_eq!(acc2.len(), 1); + let account = acc2 + .get_account(&DEFAULT_FIRST_ACCOUNT_NAME.into()) + .unwrap(); + assert_eq!(account.id().as_ref(), DEFAULT_FIRST_ACCOUNT_NAME); + assert_eq!(account.mnemonic(), &account2); + assert_eq!(account.hd_path(), &different_hd_path); + } + + #[test] + fn remove_non_existent_id_fails() { + let store_dir = tempdir().unwrap(); + let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); + let account1 = bip39::Mnemonic::generate(24).unwrap(); + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let password = UserPassword::new("password".to_string()); + let id1 = LoginId::new("first".to_string()); + let id2 = LoginId::new("second".to_string()); + + store_login_with_multiple_accounts_at_file(&wallet_file, account1, hd_path, id1, &password) + .unwrap(); + + // Fails to delete non-existent id in the wallet + assert!(matches!( + remove_login_at_file(&wallet_file, &id2), + Err(BackendError::WalletNoSuchLoginId), + )); + } + + #[test] + fn store_and_remove_wallet_login_information() { + let store_dir = tempdir().unwrap(); + let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); + let account1 = bip39::Mnemonic::generate(24).unwrap(); + let account2 = bip39::Mnemonic::generate(24).unwrap(); + let cosmos_hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let different_hd_path: DerivationPath = "m".parse().unwrap(); + let password = UserPassword::new("password".to_string()); + let id1 = LoginId::new("first".to_string()); + let id2 = LoginId::new("second".to_string()); + + // Store two accounts with two different passwords + store_login_at_file( + &wallet_file, + account1.clone(), + cosmos_hd_path.clone(), + id1.clone(), + &password, + ) + .unwrap(); + store_login_at_file( + &wallet_file, + account2.clone(), + different_hd_path.clone(), + id2.clone(), + &password, + ) + .unwrap(); + + // Load and compare + let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap(); + let acc1 = loaded_login.as_mnemonic_account().unwrap(); + assert_eq!(&account1, acc1.mnemonic()); + assert_eq!(&cosmos_hd_path, acc1.hd_path()); + + let loaded_login = load_existing_login_at_file(&wallet_file, &id2, &password).unwrap(); + let acc2 = loaded_login.as_mnemonic_account().unwrap(); + assert_eq!(&account2, acc2.mnemonic()); + assert_eq!(&different_hd_path, acc2.hd_path()); + + // Delete the second account + remove_login_at_file(&wallet_file, &id2).unwrap(); + + // The first account should be unchanged + let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap(); + let acc1 = loaded_login.as_mnemonic_account().unwrap(); + assert_eq!(&account1, acc1.mnemonic()); + assert_eq!(&cosmos_hd_path, acc1.hd_path()); + + // And we can't load the second one anymore + assert!(matches!( + load_existing_login_at_file(&wallet_file, &id2, &password), + Err(BackendError::WalletNoSuchLoginId), + )); + + // Delete the first account + assert!(wallet_file.exists()); + remove_login_at_file(&wallet_file, &id1).unwrap(); + + // The file should now be removed + assert!(!wallet_file.exists()); + + // And trying to load when the file is gone fails + assert!(matches!( + load_existing_login_at_file(&wallet_file, &id1, &password), + Err(BackendError::WalletFileNotFound), + )); + } + + #[test] + fn append_account_converts_the_type() { + let store_dir = tempdir().unwrap(); + let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); + let account1 = bip39::Mnemonic::generate(24).unwrap(); + let account2 = bip39::Mnemonic::generate(24).unwrap(); + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let password = UserPassword::new("password".to_string()); + let id1 = LoginId::new("first".to_string()); + let id2 = AccountId::new("second".to_string()); + + store_login_at_file( + &wallet_file, + account1.clone(), + hd_path.clone(), + id1.clone(), + &password, + ) + .unwrap(); + + // Check that it's there as the correct non-multiple type + let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap(); + let acc = loaded_login.as_mnemonic_account().unwrap(); + assert_eq!(acc.mnemonic(), &account1); + assert_eq!(acc.hd_path(), &hd_path); + + append_account_to_login_at_file( + &wallet_file, + account2.clone(), + hd_path.clone(), + id1.clone(), + id2.clone(), + &password, + ) + .unwrap(); + + // Check that it is now multiple mnemonic type + let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap(); + let loaded_accounts = loaded_login.as_multiple_accounts().unwrap(); + let expected = vec![ + WalletAccount::new(id1.into(), MnemonicAccount::new(account1, hd_path.clone())), + WalletAccount::new(id2, MnemonicAccount::new(account2, hd_path)), + ] + .into(); + assert_eq!(loaded_accounts, &expected); + } + + #[test] + fn append_accounts_to_existing_login() { + let store_dir = tempdir().unwrap(); + let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); + let account1 = bip39::Mnemonic::generate(24).unwrap(); + let account2 = bip39::Mnemonic::generate(24).unwrap(); + let account3 = bip39::Mnemonic::generate(24).unwrap(); + let account4 = bip39::Mnemonic::generate(24).unwrap(); + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let password = UserPassword::new("password".to_string()); + let id1 = LoginId::new("first".to_string()); + let id2 = LoginId::new("second".to_string()); + let id3 = AccountId::new("third".to_string()); + let id4 = AccountId::new("fourth".to_string()); + + store_login_at_file( + &wallet_file, + account1.clone(), + hd_path.clone(), + id1.clone(), + &password, + ) + .unwrap(); + + store_login_at_file( + &wallet_file, + account2.clone(), + hd_path.clone(), + id2.clone(), + &password, + ) + .unwrap(); + + // Check that it's there as the correct non-multiple type + let loaded_login = load_existing_login_at_file(&wallet_file, &id2, &password).unwrap(); + let acc2 = loaded_login.as_mnemonic_account().unwrap(); + assert_eq!(acc2.mnemonic(), &account2); + assert_eq!(acc2.hd_path(), &hd_path); + + // Add a third and fourth mnenonic grouped together with the second one + append_account_to_login_at_file( + &wallet_file, + account3.clone(), + hd_path.clone(), + id2.clone(), + id3.clone(), + &password, + ) + .unwrap(); + append_account_to_login_at_file( + &wallet_file, + account4.clone(), + hd_path.clone(), + id2.clone(), + id4.clone(), + &password, + ) + .unwrap(); + + // Check that we can load all four + let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap(); + let acc1 = loaded_login.as_mnemonic_account().unwrap(); + assert_eq!(acc1.mnemonic(), &account1); + assert_eq!(acc1.hd_path(), &hd_path); + + let loaded_login = load_existing_login_at_file(&wallet_file, &id2, &password).unwrap(); + let loaded_accounts = loaded_login.as_multiple_accounts().unwrap(); + let expected = vec![ + WalletAccount::new(id2.into(), MnemonicAccount::new(account2, hd_path.clone())), + WalletAccount::new(id3, MnemonicAccount::new(account3, hd_path.clone())), + WalletAccount::new(id4, MnemonicAccount::new(account4, hd_path)), + ] + .into(); + assert_eq!(loaded_accounts, &expected); + } + + #[test] + fn append_accounts_to_existing_login_with_multi() { + let store_dir = tempdir().unwrap(); + let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); + let account1 = bip39::Mnemonic::generate(24).unwrap(); + let account2 = bip39::Mnemonic::generate(24).unwrap(); + let account3 = bip39::Mnemonic::generate(24).unwrap(); + let account4 = bip39::Mnemonic::generate(24).unwrap(); + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let password = UserPassword::new("password".to_string()); + let id1 = LoginId::new("first".to_string()); + let id2 = LoginId::new("second".to_string()); + let id3 = AccountId::new("third".to_string()); + let id4 = AccountId::new("fourth".to_string()); + + store_login_with_multiple_accounts_at_file( + &wallet_file, + account1.clone(), + hd_path.clone(), + id1.clone(), + &password, + ) + .unwrap(); + + store_login_with_multiple_accounts_at_file( + &wallet_file, + account2.clone(), + hd_path.clone(), + id2.clone(), + &password, + ) + .unwrap(); + + // Add a third and fourth mnenonic grouped together with the second one + append_account_to_login_at_file( + &wallet_file, + account3.clone(), + hd_path.clone(), + id2.clone(), + id3.clone(), + &password, + ) + .unwrap(); + append_account_to_login_at_file( + &wallet_file, + account4.clone(), + hd_path.clone(), + id2.clone(), + id4.clone(), + &password, + ) + .unwrap(); + + // Check that we can load all four + let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap(); + let loaded_accounts = loaded_login.as_multiple_accounts().unwrap(); + let expected = vec![WalletAccount::new( + DEFAULT_FIRST_ACCOUNT_NAME.into(), + MnemonicAccount::new(account1, hd_path.clone()), + )] + .into(); + assert_eq!(loaded_accounts, &expected); + + let loaded_login = load_existing_login_at_file(&wallet_file, &id2, &password).unwrap(); + let loaded_accounts = loaded_login.as_multiple_accounts().unwrap(); + let expected = vec![ + WalletAccount::new( + DEFAULT_FIRST_ACCOUNT_NAME.into(), + MnemonicAccount::new(account2, hd_path.clone()), + ), + WalletAccount::new(id3, MnemonicAccount::new(account3, hd_path.clone())), + WalletAccount::new(id4, MnemonicAccount::new(account4, hd_path)), + ] + .into(); + assert_eq!(loaded_accounts, &expected); + } + + #[test] + fn append_the_same_mnemonic_twice_fails() { + let store_dir = tempdir().unwrap(); + let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); + let account1 = bip39::Mnemonic::generate(24).unwrap(); + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let password = UserPassword::new("password".to_string()); + let id1 = LoginId::new("first".to_string()); + let id2 = AccountId::new("second".to_string()); + + store_login_with_multiple_accounts_at_file( + &wallet_file, + account1.clone(), + hd_path.clone(), + id1.clone(), + &password, + ) + .unwrap(); + + assert!(matches!( + append_account_to_login_at_file(&wallet_file, account1, hd_path, id1, id2, &password,), + Err(BackendError::WalletMnemonicAlreadyExistsInWalletLogin), + )) + } + + #[test] + fn delete_the_same_account_twice_for_a_login_fails() { + let store_dir = tempdir().unwrap(); + let wallet = store_dir.path().join(WALLET_INFO_FILENAME); + let account1 = bip39::Mnemonic::generate(24).unwrap(); + let account2 = bip39::Mnemonic::generate(24).unwrap(); + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let password = UserPassword::new("password".to_string()); + let id1 = LoginId::new("first".to_string()); + let id2 = AccountId::new("second".to_string()); + + store_login_at_file(&wallet, account1, hd_path.clone(), id1.clone(), &password).unwrap(); + + append_account_to_login_at_file( + &wallet, + account2, + hd_path, + id1.clone(), + id2.clone(), + &password, + ) + .unwrap(); + + remove_account_from_login_at_file(&wallet, &id1, &id2, &password).unwrap(); + + assert!(matches!( + remove_account_from_login_at_file(&wallet, &id1, &id2, &password), + Err(BackendError::WalletNoSuchAccountIdInWalletLogin), + )); + } + + #[test] + fn delete_the_same_account_twice_for_a_login_fails_with_multi() { + let store_dir = tempdir().unwrap(); + let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); + let account1 = bip39::Mnemonic::generate(24).unwrap(); + let account2 = bip39::Mnemonic::generate(24).unwrap(); + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let password = UserPassword::new("password".to_string()); + let id1 = LoginId::new("first".to_string()); + let id2 = AccountId::new("second".to_string()); + + store_login_with_multiple_accounts_at_file( + &wallet_file, + account1, + hd_path.clone(), + id1.clone(), + &password, + ) + .unwrap(); + + append_account_to_login_at_file( + &wallet_file, + account2, + hd_path, + id1.clone(), + id2.clone(), + &password, + ) + .unwrap(); + + remove_account_from_login_at_file(&wallet_file, &id1, &id2, &password).unwrap(); + + assert!(matches!( + remove_account_from_login_at_file(&wallet_file, &id1, &id2, &password), + Err(BackendError::WalletNoSuchAccountIdInWalletLogin), + )); + } + + #[test] + fn delete_appended_account_doesnt_affect_others() { + let store_dir = tempdir().unwrap(); + let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME); + let account1 = bip39::Mnemonic::generate(24).unwrap(); + let account2 = bip39::Mnemonic::generate(24).unwrap(); + let account3 = bip39::Mnemonic::generate(24).unwrap(); + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let password = UserPassword::new("password".to_string()); + let id1 = LoginId::new("first".to_string()); + let id2 = LoginId::new("second".to_string()); + let id3 = AccountId::new("third".to_string()); + + store_login_at_file( + &wallet_file, + account1, + hd_path.clone(), + id1.clone(), + &password, + ) + .unwrap(); + + store_login_at_file( + &wallet_file, + account2.clone(), + hd_path.clone(), + id2.clone(), + &password, + ) + .unwrap(); + + append_account_to_login_at_file( + &wallet_file, + account3.clone(), + hd_path.clone(), + id2.clone(), + id3.clone(), + &password, + ) + .unwrap(); + + remove_login_at_file(&wallet_file, &id1).unwrap(); + + // The second login one is still there + let loaded_login = load_existing_login_at_file(&wallet_file, &id2, &password).unwrap(); + let loaded_accounts = loaded_login.as_multiple_accounts().unwrap(); + let expected = vec![ + WalletAccount::new(id2.into(), MnemonicAccount::new(account2, hd_path.clone())), + WalletAccount::new(id3, MnemonicAccount::new(account3, hd_path)), + ] + .into(); + assert_eq!(loaded_accounts, &expected); + } + + #[test] + fn remove_all_accounts_for_a_login_removes_the_file_when_empty() { + let store_dir = tempdir().unwrap(); + let wallet = store_dir.path().join(WALLET_INFO_FILENAME); + let account1 = bip39::Mnemonic::generate(24).unwrap(); + let account2 = bip39::Mnemonic::generate(24).unwrap(); + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let password = UserPassword::new("password".to_string()); + let id1 = LoginId::new("first".to_string()); + let id2 = AccountId::new("second".to_string()); + + store_login_with_multiple_accounts_at_file( + &wallet, + account1, + hd_path.clone(), + id1.clone(), + &password, + ) + .unwrap(); + + append_account_to_login_at_file( + &wallet, + account2, + hd_path, + id1.clone(), + id2.clone(), + &password, + ) + .unwrap(); + + remove_account_from_login_at_file( + &wallet, + &id1, + &DEFAULT_FIRST_ACCOUNT_NAME.into(), + &password, + ) + .unwrap(); + remove_account_from_login_at_file(&wallet, &id1, &id2, &password).unwrap(); + + // The file should now be removed + assert!(!wallet.exists()); + + // And trying to load when the file is gone fails + assert!(matches!( + load_existing_login_at_file(&wallet, &id1, &password), + Err(BackendError::WalletFileNotFound), + )); + } + + #[test] + fn remove_all_accounts_for_a_login_removes_that_login() { + let store_dir = tempdir().unwrap(); + let wallet = store_dir.path().join(WALLET_INFO_FILENAME); + let account1 = bip39::Mnemonic::generate(24).unwrap(); + let account2 = bip39::Mnemonic::generate(24).unwrap(); + let account3 = bip39::Mnemonic::generate(24).unwrap(); + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let password = UserPassword::new("password".to_string()); + let id1 = LoginId::new("first".to_string()); + let id2 = AccountId::new("second".to_string()); + let id3 = LoginId::new("third".to_string()); + + store_login_with_multiple_accounts_at_file( + &wallet, + account1, + hd_path.clone(), + id1.clone(), + &password, + ) + .unwrap(); + + append_account_to_login_at_file( + &wallet, + account2, + hd_path.clone(), + id1.clone(), + id2.clone(), + &password, + ) + .unwrap(); + + store_login_with_multiple_accounts_at_file( + &wallet, + account3.clone(), + hd_path.clone(), + id3.clone(), + &password, + ) + .unwrap(); + + remove_account_from_login_at_file( + &wallet, + &id1, + &DEFAULT_FIRST_ACCOUNT_NAME.into(), + &password, + ) + .unwrap(); + remove_account_from_login_at_file(&wallet, &id1, &id2, &password).unwrap(); + + // And trying to load when the file is gone fails + assert!(matches!( + load_existing_login_at_file(&wallet, &id1, &password), + Err(BackendError::WalletNoSuchLoginId), + )); + + // The other login is still there + let loaded_login = load_existing_login_at_file(&wallet, &id3, &password).unwrap(); + let acc3 = loaded_login.as_multiple_accounts().unwrap(); + let expected = vec![WalletAccount::new( + DEFAULT_FIRST_ACCOUNT_NAME.into(), + MnemonicAccount::new(account3, hd_path), + )] + .into(); + assert_eq!(acc3, &expected); + } + + #[test] + fn append_accounts_and_remove_appended_accounts() { + let store_dir = tempdir().unwrap(); + let wallet = store_dir.path().join(WALLET_INFO_FILENAME); + let acc1 = bip39::Mnemonic::generate(24).unwrap(); + let acc2 = bip39::Mnemonic::generate(24).unwrap(); + let acc3 = bip39::Mnemonic::generate(24).unwrap(); + let acc4 = bip39::Mnemonic::generate(24).unwrap(); + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let password = UserPassword::new("password".to_string()); + let id1 = LoginId::new("first".to_string()); + let id2 = LoginId::new("second".to_string()); + let id3 = AccountId::new("third".to_string()); + let id4 = AccountId::new("fourth".to_string()); + + store_login_at_file( + &wallet, + acc1.clone(), + hd_path.clone(), + id1.clone(), + &password, + ) + .unwrap(); + + store_login_at_file( + &wallet, + acc2.clone(), + hd_path.clone(), + id2.clone(), + &password, + ) + .unwrap(); + + // Add a third and fourth mnenonic grouped together with the second one + append_account_to_login_at_file( + &wallet, + acc3, + hd_path.clone(), + id2.clone(), + id3.clone(), + &password, + ) + .unwrap(); + append_account_to_login_at_file( + &wallet, + acc4.clone(), + hd_path.clone(), + id2.clone(), + id4.clone(), + &password, + ) + .unwrap(); + + // Delete the third mnemonic, from the second login entry + remove_account_from_login_at_file(&wallet, &id2, &id3, &password).unwrap(); + + // Check that we can still load the other accounts + let loaded_login = load_existing_login_at_file(&wallet, &id2, &password).unwrap(); + let loaded_accounts = loaded_login.as_multiple_accounts().unwrap(); + let expected = vec![ + WalletAccount::new( + id2.clone().into(), + MnemonicAccount::new(acc2, hd_path.clone()), + ), + WalletAccount::new(id4.clone(), MnemonicAccount::new(acc4, hd_path.clone())), + ] + .into(); + assert_eq!(loaded_accounts, &expected); + + // Delete the second and fourth mnemonic from the second login entry removes the login entry + remove_account_from_login_at_file(&wallet, &id2, &id2.clone().into(), &password).unwrap(); + remove_account_from_login_at_file(&wallet, &id2, &id4, &password).unwrap(); + assert!(matches!( + load_existing_login_at_file(&wallet, &id2, &password), + Err(BackendError::WalletNoSuchLoginId), + )); + + // The first login is still available + let loaded_login = load_existing_login_at_file(&wallet, &id1, &password).unwrap(); + let account = loaded_login.as_mnemonic_account().unwrap(); + assert_eq!(account.mnemonic(), &acc1); + assert_eq!(account.hd_path(), &hd_path); + } + + // Test to that decrypts a stored file from the repo, to make sure we are able to decrypt stored + // wallets created with older versions. + #[test] + fn decrypt_stored_wallet() { + const SAVED_WALLET: &str = "src/wallet_storage/test-data/saved-wallet.json"; + let wallet_file = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(SAVED_WALLET); + + let wallet = load_existing_wallet_at_file(&wallet_file).unwrap(); + + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let password = UserPassword::new("password".to_string()); + let bad_password = UserPassword::new("bad-password".to_string()); + let id1 = LoginId::new("first".to_string()); + let id2 = LoginId::new("second".to_string()); + + assert!(!wallet.password_can_decrypt_all(&bad_password)); + assert!(wallet.password_can_decrypt_all(&password)); + + let acc1 = wallet.decrypt_login(&id1, &password).unwrap(); + let acc2 = wallet.decrypt_login(&id2, &password).unwrap(); + + assert!(matches!(acc1, StoredLogin::Mnemonic(_))); + assert!(matches!(acc2, StoredLogin::Mnemonic(_))); + + let expected_acc1 = bip39::Mnemonic::from_str("country mean universe text phone begin deputy reject result good cram illness common cluster proud swamp digital patrol spread bar face december base kick").unwrap(); + let expected_acc2 = bip39::Mnemonic::from_str("home mansion start quiz dress decide hint second dragon sunny juice always steak real minimum art rival skin draw total pulp foot goddess agent").unwrap(); + + assert_eq!( + acc1.as_mnemonic_account().unwrap().mnemonic(), + &expected_acc1 + ); + assert_eq!(acc1.as_mnemonic_account().unwrap().hd_path(), &hd_path,); + + assert_eq!( + acc2.as_mnemonic_account().unwrap().mnemonic(), + &expected_acc2 + ); + assert_eq!(acc2.as_mnemonic_account().unwrap().hd_path(), &hd_path,); + } + + #[test] + fn decrypt_stored_wallet_1_0_4() { + const SAVED_WALLET: &str = "src/wallet_storage/test-data/saved-wallet-1.0.4.json"; + let wallet_file = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(SAVED_WALLET); + + let wallet = load_existing_wallet_at_file(&wallet_file).unwrap(); + + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let password = UserPassword::new("password11!".to_string()); + let bad_password = UserPassword::new("bad-password".to_string()); + let login_id = LoginId::new("default".to_string()); + + assert!(!wallet.password_can_decrypt_all(&bad_password)); + assert!(wallet.password_can_decrypt_all(&password)); + + let acc1 = wallet.decrypt_login(&login_id, &password).unwrap(); + + assert!(matches!(acc1, StoredLogin::Mnemonic(_))); + + let expected_acc1 = bip39::Mnemonic::from_str("arrow capable abstract industry elevator nominee december piece hotel feed lounge web faint sword veteran bundle hour page actual laptop horror gold test warrior").unwrap(); + + assert_eq!( + acc1.as_mnemonic_account().unwrap().mnemonic(), + &expected_acc1 + ); + assert_eq!(acc1.as_mnemonic_account().unwrap().hd_path(), &hd_path,); + } + + #[test] + fn decrypt_stored_wallet_1_0_5_with_multiple_accounts() { + const SAVED_WALLET: &str = "src/wallet_storage/test-data/saved-wallet-1.0.5.json"; + let wallet_file = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(SAVED_WALLET); + + let wallet = load_existing_wallet_at_file(&wallet_file).unwrap(); + + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let password = UserPassword::new("password11!".to_string()); + let bad_password = UserPassword::new("bad-password".to_string()); + let login_id = LoginId::new("default".to_string()); + + assert!(!wallet.password_can_decrypt_all(&bad_password)); + assert!(wallet.password_can_decrypt_all(&password)); + + let login = wallet.decrypt_login(&login_id, &password).unwrap(); + + assert!(matches!(login, StoredLogin::Multiple(_))); + + let login = login.as_multiple_accounts().unwrap(); + assert_eq!(login.len(), 4); + + let expected_mn1 = bip39::Mnemonic::from_str("arrow capable abstract industry elevator nominee december piece hotel feed lounge web faint sword veteran bundle hour page actual laptop horror gold test warrior").unwrap(); + let expected_mn2 = bip39::Mnemonic::from_str("border hurt skull lunar goddess second danger game dismiss exhaust oven thumb dog drama onion false orchard spice tent next predict invite cherry green").unwrap(); + let expected_mn3 = bip39::Mnemonic::from_str("gentle crowd rule snap girl urge flat jump winner cluster night sand museum stock grunt quick tree acquire traffic major awake tag rack peasant").unwrap(); + let expected_mn4 = bip39::Mnemonic::from_str("debris blue skin annual inhale text border rigid spatial lesson coconut yard horn crystal control survey version vote hawk neck frame arrive oblige width").unwrap(); + + let expected = vec![ + WalletAccount::new( + "default".into(), + MnemonicAccount::new(expected_mn1, hd_path.clone()), + ), + WalletAccount::new( + "account2".into(), + MnemonicAccount::new(expected_mn2, hd_path.clone()), + ), + WalletAccount::new( + "foobar".into(), + MnemonicAccount::new(expected_mn3, hd_path.clone()), + ), + WalletAccount::new("42".into(), MnemonicAccount::new(expected_mn4, hd_path)), + ] + .into(); + + assert_eq!(login, &expected); + } } diff --git a/nym-wallet/src-tauri/src/wallet_storage/password.rs b/nym-wallet/src-tauri/src/wallet_storage/password.rs index 8701f8994d..dfeacf33a8 100644 --- a/nym-wallet/src-tauri/src/wallet_storage/password.rs +++ b/nym-wallet/src-tauri/src/wallet_storage/password.rs @@ -11,33 +11,33 @@ use zeroize::Zeroize; pub(crate) struct LoginId(String); impl LoginId { - pub(crate) fn new(id: String) -> LoginId { - LoginId(id) - } + pub(crate) fn new(id: String) -> LoginId { + LoginId(id) + } } impl AsRef for LoginId { - fn as_ref(&self) -> &str { - self.0.as_ref() - } + fn as_ref(&self) -> &str { + self.0.as_ref() + } } impl From for LoginId { - fn from(id: String) -> Self { - Self::new(id) - } + fn from(id: String) -> Self { + Self::new(id) + } } impl From<&str> for LoginId { - fn from(id: &str) -> Self { - Self::new(id.to_string()) - } + fn from(id: &str) -> Self { + Self::new(id.to_string()) + } } impl fmt::Display for LoginId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } } // For each encrypted login, we can have multiple encrypted accounts. @@ -45,39 +45,39 @@ impl fmt::Display for LoginId { pub(crate) struct AccountId(String); impl AccountId { - pub(crate) fn new(id: String) -> AccountId { - AccountId(id) - } + pub(crate) fn new(id: String) -> AccountId { + AccountId(id) + } } impl AsRef for AccountId { - fn as_ref(&self) -> &str { - self.0.as_ref() - } + fn as_ref(&self) -> &str { + self.0.as_ref() + } } impl From for AccountId { - fn from(id: String) -> Self { - Self::new(id) - } + fn from(id: String) -> Self { + Self::new(id) + } } impl From<&str> for AccountId { - fn from(id: &str) -> Self { - Self::new(id.to_string()) - } + fn from(id: &str) -> Self { + Self::new(id.to_string()) + } } impl From for AccountId { - fn from(login_id: LoginId) -> Self { - Self::new(login_id.0) - } + fn from(login_id: LoginId) -> Self { + Self::new(login_id.0) + } } impl fmt::Display for AccountId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } } // simple wrapper for String that will get zeroized on drop @@ -86,17 +86,17 @@ impl fmt::Display for AccountId { pub(crate) struct UserPassword(String); impl UserPassword { - pub(crate) fn new(pass: String) -> UserPassword { - UserPassword(pass) - } + pub(crate) fn new(pass: String) -> UserPassword { + UserPassword(pass) + } - pub(crate) fn as_bytes(&self) -> &[u8] { - self.0.as_bytes() - } + pub(crate) fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } } impl AsRef for UserPassword { - fn as_ref(&self) -> &str { - self.0.as_ref() - } + fn as_ref(&self) -> &str { + self.0.as_ref() + } } diff --git a/validator-api/src/node_status_api/routes.rs b/validator-api/src/node_status_api/routes.rs index 116bb9b190..a03f59661d 100644 --- a/validator-api/src/node_status_api/routes.rs +++ b/validator-api/src/node_status_api/routes.rs @@ -148,15 +148,13 @@ pub(crate) async fn get_mixnode_reward_estimation( let reward_params = RewardParams::new(reward_params, node_reward_params); match bond.estimate_reward(&reward_params) { - Ok(( - estimated_total_node_reward, - estimated_operator_reward, - estimated_delegators_reward, - )) => { + Ok(reward_estimate) => { let reponse = RewardEstimationResponse { - estimated_total_node_reward, - estimated_operator_reward, - estimated_delegators_reward, + estimated_total_node_reward: reward_estimate.total_node_reward, + estimated_operator_reward: reward_estimate.operator_reward, + estimated_delegators_reward: reward_estimate.delegators_reward, + estimated_node_profit: reward_estimate.node_profit, + estimated_operator_cost: reward_estimate.operator_cost, reward_params, as_at, }; diff --git a/validator-api/src/nymd_client.rs b/validator-api/src/nymd_client.rs index 7de1c49be6..3e32d40bef 100644 --- a/validator-api/src/nymd_client.rs +++ b/validator-api/src/nymd_client.rs @@ -18,7 +18,7 @@ use tokio::sync::RwLock; use tokio::time::sleep; use validator_client::nymd::{ hash::{Hash, SHA256_HASH_SIZE}, - CosmWasmClient, CosmosCoin, Fee, QueryNymdClient, SigningCosmWasmClient, SigningNymdClient, + Coin, CosmWasmClient, Fee, QueryNymdClient, SigningCosmWasmClient, SigningNymdClient, TendermintTime, }; use validator_client::ValidatorClientError; @@ -326,7 +326,7 @@ impl Client { &self, rewarded_set: Vec, expected_active_set_size: u32, - reward_msgs: Vec<(ExecuteMsg, Vec)>, + reward_msgs: Vec<(ExecuteMsg, Vec)>, ) -> Result<(), RewardingError> where C: SigningCosmWasmClient + Sync, @@ -358,7 +358,7 @@ impl Client { async fn execute_multiple_with_retry( &self, - msgs: Vec<(M, Vec)>, + msgs: Vec<(M, Vec)>, fee: Fee, memo: String, ) -> Result<(), RewardingError> diff --git a/validator-api/src/rewarded_set_updater/mod.rs b/validator-api/src/rewarded_set_updater/mod.rs index 8de7d0c891..9f008348d0 100644 --- a/validator-api/src/rewarded_set_updater/mod.rs +++ b/validator-api/src/rewarded_set_updater/mod.rs @@ -25,7 +25,7 @@ use std::collections::HashSet; use std::time::Duration; use time::OffsetDateTime; use tokio::time::sleep; -use validator_client::nymd::{CosmosCoin, SigningNymdClient}; +use validator_client::nymd::{Coin, SigningNymdClient}; pub(crate) mod error; @@ -120,7 +120,7 @@ impl RewardedSetUpdater { async fn reward_current_rewarded_set( &self, - ) -> Result)>, RewardingError> { + ) -> Result)>, RewardingError> { let to_reward = self.nodes_to_reward().await?; let epoch = self.epoch().await?; @@ -143,7 +143,7 @@ impl RewardedSetUpdater { async fn generate_reward_messages( &self, eligible_mixnodes: &[MixnodeToReward], - ) -> Result)>, RewardingError> { + ) -> Result)>, RewardingError> { cfg_if::cfg_if! { if #[cfg(feature = "no-reward")] { Ok(vec![]) diff --git a/validator-api/validator-api-requests/src/models.rs b/validator-api/validator-api-requests/src/models.rs index b1f01761f2..0abe5affd0 100644 --- a/validator-api/validator-api-requests/src/models.rs +++ b/validator-api/validator-api-requests/src/models.rs @@ -52,6 +52,8 @@ pub struct RewardEstimationResponse { pub estimated_total_node_reward: u64, pub estimated_operator_reward: u64, pub estimated_delegators_reward: u64, + pub estimated_node_profit: u64, + pub estimated_operator_cost: u64, pub reward_params: RewardParams, pub as_at: i64,