diff --git a/CHANGELOG.md b/CHANGELOG.md index e395038495..927eb0fa5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,18 @@ - 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]) @@ -29,6 +35,8 @@ [#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 ## [nym-wallet-v1.0.4](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.4) (2022-05-04) diff --git a/common/client-libs/validator-client/src/nymd/mod.rs b/common/client-libs/validator-client/src/nymd/mod.rs index fe038c0406..5de66f7c25 100644 --- a/common/client-libs/validator-client/src/nymd/mod.rs +++ b/common/client-libs/validator-client/src/nymd/mod.rs @@ -803,6 +803,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, 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 58f4d18a00..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 @@ -11,6 +11,28 @@ use vesting_contract_common::messages::{ExecuteMsg as VestingExecuteMsg, Vesting #[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, @@ -375,4 +397,78 @@ impl VestingSigningClient for NymdClient ) .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 7447dde7bd..4325727195 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs @@ -318,6 +318,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, @@ -410,63 +418,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 { @@ -474,11 +499,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/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/coin.rs b/nym-wallet/src-tauri/src/coin.rs index 3ecb85cf01..1903e869f9 100644 --- a/nym-wallet/src-tauri/src/coin.rs +++ b/nym-wallet/src-tauri/src/coin.rs @@ -17,328 +17,328 @@ const MINOR_IN_MAJOR: f64 = 1_000_000.; #[cfg_attr(test, ts(export, export, export_to = "../src/types/rust/denom.ts"))] #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] pub enum Denom { - Major, - Minor, + Major, + Minor, } impl FromStr for Denom { - type Err = BackendError; + type Err = BackendError; - fn from_str(s: &str) -> Result { - let s = s.to_lowercase(); - for network in Network::iter() { - let denom = network.denom(); - if s == denom.to_lowercase() || s == "minor" { - return Ok(Denom::Minor); - } else if s == denom[1..].to_lowercase() || s == "major" { - return Ok(Denom::Major); - } + fn from_str(s: &str) -> Result { + let s = s.to_lowercase(); + for network in Network::iter() { + let denom = network.denom(); + if s == denom.to_lowercase() || s == "minor" { + return Ok(Denom::Minor); + } else if s == denom[1..].to_lowercase() || s == "major" { + return Ok(Denom::Major); + } + } + Err(BackendError::InvalidDenom(s)) } - Err(BackendError::InvalidDenom(s)) - } } impl TryFrom for Denom { - type Error = BackendError; + type Error = BackendError; - fn try_from(value: CosmosDenom) -> Result { - Denom::from_str(&value.to_string()) - } + fn try_from(value: CosmosDenom) -> Result { + Denom::from_str(&value.to_string()) + } } #[cfg_attr(test, derive(ts_rs::TS))] #[cfg_attr(test, ts(export, export_to = "../src/types/rust/coin.ts"))] #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] pub struct Coin { - amount: String, - denom: Denom, + amount: String, + denom: Denom, } // Allows adding minor and major denominations, output will have the LHS denom. impl Add for Coin { - type Output = Self; + type Output = Self; - fn add(self, rhs: Self) -> Self { - let denom = self.denom.clone(); - let lhs = self.to_minor(); - let rhs = rhs.to_minor(); - let lhs_amount = lhs.amount.parse::().unwrap(); - let rhs_amount = rhs.amount.parse::().unwrap(); - let amount = lhs_amount + rhs_amount; - let coin = Coin { - amount: amount.to_string(), - denom: Denom::Minor, - }; - match denom { - Denom::Major => coin.to_major(), - Denom::Minor => coin, + fn add(self, rhs: Self) -> Self { + let denom = self.denom.clone(); + let lhs = self.to_minor(); + let rhs = rhs.to_minor(); + let lhs_amount = lhs.amount.parse::().unwrap(); + let rhs_amount = rhs.amount.parse::().unwrap(); + let amount = lhs_amount + rhs_amount; + let coin = Coin { + amount: amount.to_string(), + denom: Denom::Minor, + }; + match denom { + Denom::Major => coin.to_major(), + Denom::Minor => coin, + } } - } } // Allows adding minor and major denominations, output will have the LHS denom. impl Sub for Coin { - type Output = Self; + type Output = Self; - fn sub(self, rhs: Self) -> Self { - let denom = self.denom.clone(); - let lhs = self.to_minor(); - let rhs = rhs.to_minor(); - let lhs_amount = lhs.amount.parse::().unwrap(); - let rhs_amount = rhs.amount.parse::().unwrap(); - let amount = lhs_amount - rhs_amount; - let coin = Coin { - amount: amount.to_string(), - denom: Denom::Minor, - }; - match denom { - Denom::Major => coin.to_major(), - Denom::Minor => coin, + fn sub(self, rhs: Self) -> Self { + let denom = self.denom.clone(); + let lhs = self.to_minor(); + let rhs = rhs.to_minor(); + let lhs_amount = lhs.amount.parse::().unwrap(); + let rhs_amount = rhs.amount.parse::().unwrap(); + let amount = lhs_amount - rhs_amount; + let coin = Coin { + amount: amount.to_string(), + denom: Denom::Minor, + }; + match denom { + Denom::Major => coin.to_major(), + Denom::Minor => coin, + } } - } } impl Coin { - #[allow(unused)] - pub fn major(amount: T) -> Coin { - Coin { - amount: amount.to_string(), - denom: Denom::Major, - } - } - - #[allow(unused)] - pub fn minor(amount: T) -> Coin { - Coin { - amount: amount.to_string(), - denom: Denom::Minor, - } - } - - pub fn new(amount: T, denom: &Denom) -> Coin { - Coin { - amount: amount.to_string(), - denom: denom.clone(), - } - } - - pub fn to_major(&self) -> Coin { - match self.denom { - Denom::Major => self.clone(), - Denom::Minor => Coin { - amount: (self.amount.parse::().unwrap() / MINOR_IN_MAJOR).to_string(), - denom: Denom::Major, - }, - } - } - - pub fn to_minor(&self) -> Coin { - match self.denom { - Denom::Minor => self.clone(), - Denom::Major => Coin { - amount: (self.amount.parse::().unwrap() * MINOR_IN_MAJOR).to_string(), - denom: Denom::Minor, - }, - } - } - - pub fn amount(&self) -> String { - self.amount.clone() - } - - #[allow(unused)] - pub fn denom(&self) -> Denom { - self.denom.clone() - } - - // Helper function that returns the local denom in terms of the specified network denom. - fn denom_as_string(&self, network_denom: &str) -> Result { - // Currently there is the widespread assumption that network denomination is always in - // `Denom::Minor`, and starts with 'u'. - let network_denom = network_denom.to_owned(); - if !network_denom.starts_with('u') { - return Err(BackendError::InvalidNetworkDenom(network_denom)); + #[allow(unused)] + pub fn major(amount: T) -> Coin { + Coin { + amount: amount.to_string(), + denom: Denom::Major, + } } - Ok(match &self.denom { - Denom::Minor => network_denom, - Denom::Major => network_denom[1..].to_string(), - }) - } + #[allow(unused)] + pub fn minor(amount: T) -> Coin { + Coin { + amount: amount.to_string(), + denom: Denom::Minor, + } + } - pub fn into_backend_coin(self, network_denom: &str) -> Result { - Ok(BackendCoin::new( - self.amount.parse()?, - self.denom_as_string(network_denom)?, - )) - } + pub fn new(amount: T, denom: &Denom) -> Coin { + Coin { + amount: amount.to_string(), + denom: denom.clone(), + } + } + + pub fn to_major(&self) -> Coin { + match self.denom { + Denom::Major => self.clone(), + Denom::Minor => Coin { + amount: (self.amount.parse::().unwrap() / MINOR_IN_MAJOR).to_string(), + denom: Denom::Major, + }, + } + } + + pub fn to_minor(&self) -> Coin { + match self.denom { + Denom::Minor => self.clone(), + Denom::Major => Coin { + amount: (self.amount.parse::().unwrap() * MINOR_IN_MAJOR).to_string(), + denom: Denom::Minor, + }, + } + } + + pub fn amount(&self) -> String { + self.amount.clone() + } + + #[allow(unused)] + pub fn denom(&self) -> Denom { + self.denom.clone() + } + + // Helper function that returns the local denom in terms of the specified network denom. + fn denom_as_string(&self, network_denom: &str) -> Result { + // Currently there is the widespread assumption that network denomination is always in + // `Denom::Minor`, and starts with 'u'. + let network_denom = network_denom.to_owned(); + if !network_denom.starts_with('u') { + return Err(BackendError::InvalidNetworkDenom(network_denom)); + } + + Ok(match &self.denom { + Denom::Minor => network_denom, + Denom::Major => network_denom[1..].to_string(), + }) + } + + pub fn into_backend_coin(self, network_denom: &str) -> Result { + Ok(BackendCoin::new( + self.amount.parse()?, + self.denom_as_string(network_denom)?, + )) + } } impl From for Coin { - fn from(c: BackendCoin) -> Self { - Coin { - amount: c.amount.to_string(), - denom: Denom::from_str(c.denom.as_ref()).unwrap(), + fn from(c: BackendCoin) -> Self { + Coin { + amount: c.amount.to_string(), + denom: Denom::from_str(c.denom.as_ref()).unwrap(), + } } - } } impl From for Coin { - fn from(c: CosmosCoin) -> Coin { - Coin { - amount: c.amount.to_string(), - denom: Denom::from_str(c.denom.as_ref()).unwrap(), + fn from(c: CosmosCoin) -> Coin { + Coin { + amount: c.amount.to_string(), + denom: Denom::from_str(c.denom.as_ref()).unwrap(), + } } - } } impl From for Coin { - fn from(c: CosmWasmCoin) -> Coin { - Coin { - amount: c.amount.to_string(), - denom: Denom::from_str(&c.denom).unwrap(), + fn from(c: CosmWasmCoin) -> Coin { + Coin { + amount: c.amount.to_string(), + denom: Denom::from_str(&c.denom).unwrap(), + } } - } } #[cfg(test)] mod test { - use super::*; - use crate::error::BackendError; - use serde_json::json; - use std::convert::TryFrom; - use std::str::FromStr; + use super::*; + use crate::error::BackendError; + use serde_json::json; + use std::convert::TryFrom; + use std::str::FromStr; - #[test] - fn json_to_coin() { - let minor = json!({ - "amount": "1", - "denom": "Minor" - }); + #[test] + fn json_to_coin() { + let minor = json!({ + "amount": "1", + "denom": "Minor" + }); - let major = json!({ - "amount": "1", - "denom": "Major" - }); + let major = json!({ + "amount": "1", + "denom": "Major" + }); - let test_minor_coin = Coin::minor("1"); - let test_major_coin = Coin::major("1"); + let test_minor_coin = Coin::minor("1"); + let test_major_coin = Coin::major("1"); - let minor_coin = serde_json::from_value::(minor).unwrap(); - let major_coin = serde_json::from_value::(major).unwrap(); + let minor_coin = serde_json::from_value::(minor).unwrap(); + let major_coin = serde_json::from_value::(major).unwrap(); - assert_eq!(minor_coin, test_minor_coin); - assert_eq!(major_coin, test_major_coin); - } - - #[test] - fn denom_from_str() { - assert_eq!(Denom::from_str("unym").unwrap(), Denom::Minor); - assert_eq!(Denom::from_str("nym").unwrap(), Denom::Major); - assert_eq!(Denom::from_str("minor").unwrap(), Denom::Minor); - assert_eq!(Denom::from_str("major").unwrap(), Denom::Major); - - assert!(matches!( - Denom::from_str("foo").unwrap_err(), - BackendError::InvalidDenom { .. }, - )); - } - - #[test] - fn denom_conversions() { - let minor = Coin::minor("1"); - let major = minor.to_major(); - - assert_eq!(major, Coin::major("0.000001")); - - let minor = major.to_minor(); - assert_eq!(minor, Coin::minor("1")); - } - - #[test] - fn network_denom_is_assumed_to_be_in_minor_denom() { - let network_denom = "nym"; - assert!(matches!( - Coin::minor("42") - .denom_as_string(network_denom) - .unwrap_err(), - BackendError::InvalidNetworkDenom { .. } - )); - } - - #[test] - fn local_denom_to_interpreted_using_network_denom() { - let network_denom = "unym"; - assert_eq!( - Coin::minor("42").denom_as_string(network_denom).unwrap(), - "unym", - ); - assert_eq!( - Coin::major("42").denom_as_string(network_denom).unwrap(), - "nym", - ); - } - - #[test] - fn coin_to_coin_minor() { - let network_denom = "unym"; - let coin = Coin::minor("42"); - - let backend_coin = coin.clone().into_backend_coin(&network_denom).unwrap(); - assert_eq!(backend_coin, BackendCoin::new(42, "unym")); - } - - #[test] - fn coin_to_coin_major() { - let network_denom = "unym"; - let coin = Coin::major("52"); - - let backend_coin = coin.clone().into_backend_coin(network_denom).unwrap(); - assert_eq!(backend_coin, BackendCoin::new(52, "nym")); - } - - 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 coin_to_backend() { - let network_denom = "unym"; - for amount in amounts() { - let coin = Coin::minor(amount); - let backend_coin = coin.into_backend_coin(network_denom).unwrap(); - assert_eq!( - backend_coin, - BackendCoin::new(amount.parse::().unwrap(), "unym") - ); - assert_eq!(Coin::try_from(backend_coin).unwrap(), Coin::minor(amount)); - - let coin = Coin::major(amount); - let backend_coin = coin.into_backend_coin(network_denom).unwrap(); - assert_eq!( - backend_coin, - BackendCoin::new(amount.parse::().unwrap(), "nym") - ); - assert_eq!(Coin::try_from(backend_coin).unwrap(), Coin::major(amount)); + assert_eq!(minor_coin, test_minor_coin); + assert_eq!(major_coin, test_major_coin); + } + + #[test] + fn denom_from_str() { + assert_eq!(Denom::from_str("unym").unwrap(), Denom::Minor); + assert_eq!(Denom::from_str("nym").unwrap(), Denom::Major); + assert_eq!(Denom::from_str("minor").unwrap(), Denom::Minor); + assert_eq!(Denom::from_str("major").unwrap(), Denom::Major); + + assert!(matches!( + Denom::from_str("foo").unwrap_err(), + BackendError::InvalidDenom { .. }, + )); + } + + #[test] + fn denom_conversions() { + let minor = Coin::minor("1"); + let major = minor.to_major(); + + assert_eq!(major, Coin::major("0.000001")); + + let minor = major.to_minor(); + assert_eq!(minor, Coin::minor("1")); + } + + #[test] + fn network_denom_is_assumed_to_be_in_minor_denom() { + let network_denom = "nym"; + assert!(matches!( + Coin::minor("42") + .denom_as_string(network_denom) + .unwrap_err(), + BackendError::InvalidNetworkDenom { .. } + )); + } + + #[test] + fn local_denom_to_interpreted_using_network_denom() { + let network_denom = "unym"; + assert_eq!( + Coin::minor("42").denom_as_string(network_denom).unwrap(), + "unym", + ); + assert_eq!( + Coin::major("42").denom_as_string(network_denom).unwrap(), + "nym", + ); + } + + #[test] + fn coin_to_coin_minor() { + let network_denom = "unym"; + let coin = Coin::minor("42"); + + let backend_coin = coin.clone().into_backend_coin(&network_denom).unwrap(); + assert_eq!(backend_coin, BackendCoin::new(42, "unym")); + } + + #[test] + fn coin_to_coin_major() { + let network_denom = "unym"; + let coin = Coin::major("52"); + + let backend_coin = coin.clone().into_backend_coin(network_denom).unwrap(); + assert_eq!(backend_coin, BackendCoin::new(52, "nym")); + } + + 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 coin_to_backend() { + let network_denom = "unym"; + for amount in amounts() { + let coin = Coin::minor(amount); + let backend_coin = coin.into_backend_coin(network_denom).unwrap(); + assert_eq!( + backend_coin, + BackendCoin::new(amount.parse::().unwrap(), "unym") + ); + assert_eq!(Coin::try_from(backend_coin).unwrap(), Coin::minor(amount)); + + let coin = Coin::major(amount); + let backend_coin = coin.into_backend_coin(network_denom).unwrap(); + assert_eq!( + backend_coin, + BackendCoin::new(amount.parse::().unwrap(), "nym") + ); + assert_eq!(Coin::try_from(backend_coin).unwrap(), Coin::major(amount)); + } } - } } diff --git a/nym-wallet/src-tauri/src/config/mod.rs b/nym-wallet/src-tauri/src/config/mod.rs index f4dc16a9c5..cf8cde54fe 100644 --- a/nym-wallet/src-tauri/src/config/mod.rs +++ b/nym-wallet/src-tauri/src/config/mod.rs @@ -18,7 +18,7 @@ use url::Url; use validator_client::nymd::AccountId as CosmosAccountId; 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; @@ -26,448 +26,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]] @@ -482,17 +479,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/", @@ -514,53 +511,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 bac7c09d78..e0fb1e565b 100644 --- a/nym-wallet/src-tauri/src/error.rs +++ b/nym-wallet/src-tauri/src/error.rs @@ -7,120 +7,120 @@ use validator_client::{nymd::error::NymdError, ValidatorClientError}; #[derive(Error, Debug)] pub enum BackendError { - #[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("{0} is not a valid denomination string")] - InvalidDenom(String), - #[error("{0} is not a valid network denomination string")] - InvalidNetworkDenom(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("No validator API URL configured")] - NoValidatorApiUrlConfigured, - #[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("Unexpted mnemonic account for login")] - WalletUnexpectedMnemonicAccount, - #[error("Unexpted multiple account entry for login")] - WalletUnexpectedMultipleAccounts, - #[error("Failed to derive address from mnemonic")] - FailedToDeriveAddress, - #[error("{0}")] - ValueParseError(#[from] ParseIntError), + #[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("{0} is not a valid denomination string")] + InvalidDenom(String), + #[error("{0} is not a valid network denomination string")] + InvalidNetworkDenom(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("No validator API URL configured")] + NoValidatorApiUrlConfigured, + #[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("Unexpted mnemonic account for login")] + WalletUnexpectedMnemonicAccount, + #[error("Unexpted multiple account entry for login")] + WalletUnexpectedMultipleAccounts, + #[error("Failed to derive address from mnemonic")] + FailedToDeriveAddress, + #[error("{0}")] + ValueParseError(#[from] ParseIntError), } 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 2b59a437fa..91e3734cdf 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 crate::menu::AddDefaultSubmenus; @@ -24,128 +24,128 @@ mod utils; mod wallet_storage; 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_account, - 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_reverse_mix_delegations_paged, - 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::major_to_minor, - utils::minor_to_major, - 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 endpoints - simulate::admin::simulate_update_contract_settings, - simulate::mixnet::simulate_bond_gateway, - simulate::mixnet::simulate_bond_mixnode, - simulate::mixnet::simulate_unbond_gateway, - simulate::mixnet::simulate_unbond_mixnode, - simulate::mixnet::simulate_update_mixnode, - simulate::mixnet::simulate_delegate_to_mixnode, - simulate::mixnet::simulate_undelegate_from_mixnode, - simulate::cosmos::simulate_send, - simulate::vesting::simulate_vesting_bond_gateway, - simulate::vesting::simulate_vesting_bond_mixnode, - simulate::vesting::simulate_vesting_unbond_gateway, - 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_account, + 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_reverse_mix_delegations_paged, + 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::major_to_minor, + utils::minor_to_major, + 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 endpoints + simulate::admin::simulate_update_contract_settings, + simulate::mixnet::simulate_bond_gateway, + simulate::mixnet::simulate_bond_mixnode, + simulate::mixnet::simulate_unbond_gateway, + simulate::mixnet::simulate_unbond_mixnode, + simulate::mixnet::simulate_update_mixnode, + simulate::mixnet::simulate_delegate_to_mixnode, + simulate::mixnet::simulate_undelegate_from_mixnode, + simulate::cosmos::simulate_send, + simulate::vesting::simulate_vesting_bond_gateway, + simulate::vesting::simulate_vesting_bond_mixnode, + simulate::vesting::simulate_vesting_unbond_gateway, + 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); + } - 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.rs b/nym-wallet/src-tauri/src/network.rs index b2fc442e20..9f0e21f990 100644 --- a/nym-wallet/src-tauri/src/network.rs +++ b/nym-wallet/src-tauri/src/network.rs @@ -12,54 +12,54 @@ use strum::EnumIter; #[cfg_attr(test, ts(export, export_to = "../src/types/rust/network.ts"))] #[derive(Copy, Clone, Debug, Deserialize, EnumIter, Eq, Hash, PartialEq, Serialize)] pub enum Network { - QA, - SANDBOX, - MAINNET, + QA, + SANDBOX, + MAINNET, } impl Network { - pub fn as_key(&self) -> String { - self.to_string().to_lowercase() - } - - pub fn denom(&self) -> &str { - match self { - // network defaults should be correctly formatted - Network::QA => qa::DENOM, - Network::SANDBOX => sandbox::DENOM, - Network::MAINNET => mainnet::DENOM, + pub fn as_key(&self) -> String { + self.to_string().to_lowercase() + } + + pub fn denom(&self) -> &str { + match self { + // network defaults should be correctly formatted + Network::QA => qa::DENOM, + Network::SANDBOX => sandbox::DENOM, + Network::MAINNET => mainnet::DENOM, + } } - } } impl Default for Network { - fn default() -> Self { - Network::MAINNET - } + fn default() -> Self { + Network::MAINNET + } } impl fmt::Display for Network { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } } impl From for Network { - fn from(network: ConfigNetwork) -> Self { - match network { - ConfigNetwork::QA => Network::QA, - ConfigNetwork::SANDBOX => Network::SANDBOX, - ConfigNetwork::MAINNET => Network::MAINNET, + fn from(network: ConfigNetwork) -> Self { + match network { + ConfigNetwork::QA => Network::QA, + ConfigNetwork::SANDBOX => Network::SANDBOX, + ConfigNetwork::MAINNET => Network::MAINNET, + } } - } } impl From for ConfigNetwork { - fn from(network: Network) -> Self { - match network { - Network::QA => ConfigNetwork::QA, - Network::SANDBOX => ConfigNetwork::SANDBOX, - Network::MAINNET => ConfigNetwork::MAINNET, + fn from(network: Network) -> Self { + match network { + Network::QA => ConfigNetwork::QA, + Network::SANDBOX => ConfigNetwork::SANDBOX, + Network::MAINNET => ConfigNetwork::MAINNET, + } } - } } diff --git a/nym-wallet/src-tauri/src/network_config.rs b/nym-wallet/src-tauri/src/network_config.rs index 91ee051749..75a8e38eec 100644 --- a/nym-wallet/src-tauri/src/network_config.rs +++ b/nym-wallet/src-tauri/src/network_config.rs @@ -14,15 +14,15 @@ use tokio::sync::RwLock; #[cfg_attr(test, ts(export, export_to = "../src/types/rust/validatorurls.ts"))] #[derive(Debug, Serialize, Deserialize)] pub struct ValidatorUrls { - pub urls: Vec, + pub urls: Vec, } #[cfg_attr(test, derive(ts_rs::TS))] #[cfg_attr(test, ts(export, export_to = "../src/types/rust/validatorurl.ts"))] #[derive(Debug, Serialize, Deserialize)] pub struct ValidatorUrl { - pub url: String, - pub name: Option, + pub url: String, + pub name: Option, } // The type used when adding or removing validators, effectively the input. @@ -31,98 +31,98 @@ pub struct ValidatorUrl { #[cfg_attr(test, ts(export, export_to = "../src/types/rust/validatorurls.ts"))] #[derive(Debug, Serialize, Deserialize)] pub struct Validator { - pub nymd_url: String, - pub nymd_name: Option, - pub api_url: Option, + pub nymd_url: String, + pub nymd_name: Option, + pub api_url: Option, } impl fmt::Display for Validator { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let nymd_url = format!("nymd_url: {}", self.nymd_url); - let api_url = self - .api_url - .as_ref() - .map(|api_url| format!(", api_url: {}", api_url)) - .unwrap_or_default(); - write!(f, "{nymd_url}{api_url}") - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let nymd_url = format!("nymd_url: {}", self.nymd_url); + let api_url = self + .api_url + .as_ref() + .map(|api_url| format!(", api_url: {}", api_url)) + .unwrap_or_default(); + write!(f, "{nymd_url}{api_url}") + } } #[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 a84331873b..f48cffe515 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/account.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/account.rs @@ -28,657 +28,661 @@ use validator_client::Client; #[cfg_attr(test, ts(export, export_to = "../src/types/rust/account.ts"))] #[derive(Serialize, Deserialize)] pub struct Account { - contract_address: String, - client_address: String, - denom: Denom, + contract_address: String, + client_address: String, + denom: Denom, } impl Account { - pub fn new(contract_address: String, client_address: String, denom: Denom) -> Self { - Account { - contract_address, - client_address, - denom, + pub fn new(contract_address: String, client_address: String, denom: Denom) -> Self { + Account { + contract_address, + client_address, + denom, + } } - } } #[cfg_attr(test, derive(ts_rs::TS))] #[cfg_attr(test, ts(export, export_to = "../src/types/rust/createdaccount.ts"))] #[derive(Serialize, Deserialize)] pub struct CreatedAccount { - account: Account, - mnemonic: String, + account: Account, + mnemonic: String, } #[cfg_attr(test, derive(ts_rs::TS))] #[cfg_attr(test, ts(export, export_to = "../src/types/rust/createdaccount.ts"))] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AccountEntry { - id: String, - address: String, + id: String, + address: String, } #[cfg_attr(test, derive(ts_rs::TS))] #[cfg_attr(test, ts(export, export_to = "../src/types/rust/balance.ts"))] #[derive(Serialize, Deserialize)] pub struct Balance { - coin: Coin, - printable_balance: String, + coin: Coin, + printable_balance: String, } #[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().to_owned(); - match nymd_client!(state) - .get_balance(nymd_client!(state).address(), denom.parse()?) - .await - { - Ok(Some(coin)) => { - let coin = Coin::new(&coin.amount.to_string(), &Denom::from_str(&coin.denom)?); - Ok(Balance { - coin: coin.clone(), - // haha, that's so junky : ) - printable_balance: format!("{} {}", coin.to_major().amount(), &denom[1..]), - }) + let denom = state.read().await.current_network().denom().to_owned(); + match nymd_client!(state) + .get_balance(nymd_client!(state).address(), denom.parse()?) + .await + { + Ok(Some(coin)) => { + let coin = Coin::new(&coin.amount.to_string(), &Denom::from_str(&coin.denom)?); + Ok(Balance { + coin: coin.clone(), + // haha, that's so junky : ) + printable_balance: format!("{} {}", coin.to_major().amount(), &denom[1..]), + }) + } + 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 async fn create_new_account( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, Arc>>, ) -> Result { - let rand_mnemonic = random_mnemonic(); - let account = connect_with_mnemonic(rand_mnemonic.to_string(), state).await?; - Ok(CreatedAccount { - account, - mnemonic: rand_mnemonic.to_string(), - }) + let rand_mnemonic = random_mnemonic(); + let account = connect_with_mnemonic(rand_mnemonic.to_string(), state).await?; + Ok(CreatedAccount { + account, + mnemonic: rand_mnemonic.to_string(), + }) } #[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.parse()?, - ) - }; + Account::new( + client.nymd.mixnet_contract_address()?.to_string(), + client.nymd.address().to_string(), + denom.parse()?, + ) + }; - 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().parse()?, - )), - 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().parse()?, + )), + 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: {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 super::*; + use super::*; - 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}, + }; - // 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 34f792b08e..598e108d97 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/admin.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/admin.rs @@ -13,52 +13,52 @@ use validator_client::nymd::Fee; #[cfg_attr(test, ts(export, export_to = "../src/types/rust/stateparams.ts"))] #[derive(Serialize, Deserialize)] pub struct TauriContractStateParams { - minimum_mixnode_pledge: String, - minimum_gateway_pledge: String, - mixnode_rewarded_set_size: u32, - mixnode_active_set_size: u32, + minimum_mixnode_pledge: String, + minimum_gateway_pledge: String, + mixnode_rewarded_set_size: u32, + mixnode_active_set_size: u32, } impl From for TauriContractStateParams { - fn from(p: ContractStateParams) -> TauriContractStateParams { - TauriContractStateParams { - minimum_mixnode_pledge: p.minimum_mixnode_pledge.to_string(), - minimum_gateway_pledge: p.minimum_gateway_pledge.to_string(), - mixnode_rewarded_set_size: p.mixnode_rewarded_set_size, - mixnode_active_set_size: p.mixnode_active_set_size, + fn from(p: ContractStateParams) -> TauriContractStateParams { + TauriContractStateParams { + minimum_mixnode_pledge: p.minimum_mixnode_pledge.to_string(), + minimum_gateway_pledge: p.minimum_gateway_pledge.to_string(), + mixnode_rewarded_set_size: p.mixnode_rewarded_set_size, + mixnode_active_set_size: p.mixnode_active_set_size, + } } - } } impl TryFrom for ContractStateParams { - type Error = BackendError; + type Error = BackendError; - fn try_from(p: TauriContractStateParams) -> Result { - Ok(ContractStateParams { - minimum_mixnode_pledge: Uint128::try_from(p.minimum_mixnode_pledge.as_str())?, - minimum_gateway_pledge: Uint128::try_from(p.minimum_gateway_pledge.as_str())?, - mixnode_rewarded_set_size: p.mixnode_rewarded_set_size, - mixnode_active_set_size: p.mixnode_active_set_size, - }) - } + fn try_from(p: TauriContractStateParams) -> Result { + Ok(ContractStateParams { + minimum_mixnode_pledge: Uint128::try_from(p.minimum_mixnode_pledge.as_str())?, + minimum_gateway_pledge: Uint128::try_from(p.minimum_gateway_pledge.as_str())?, + mixnode_rewarded_set_size: p.mixnode_rewarded_set_size, + mixnode_active_set_size: p.mixnode_active_set_size, + }) + } } #[tauri::command] pub async fn get_contract_settings( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, Arc>>, ) -> Result { - Ok(nymd_client!(state).get_contract_settings().await?.into()) + Ok(nymd_client!(state).get_contract_settings().await?.into()) } #[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()?; - nymd_client!(state) - .update_contract_settings(mixnet_contract_settings_params.clone(), fee) - .await?; - Ok(mixnet_contract_settings_params.into()) + let mixnet_contract_settings_params: ContractStateParams = params.try_into()?; + nymd_client!(state) + .update_contract_settings(mixnet_contract_settings_params.clone(), fee) + .await?; + Ok(mixnet_contract_settings_params.into()) } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs index ee3c634be9..e940ff60ac 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs @@ -11,88 +11,88 @@ use validator_client::nymd::Fee; #[tauri::command] pub async fn bond_gateway( - gateway: Gateway, - pledge: Coin, - owner_signature: String, - fee: Option, - state: tauri::State<'_, Arc>>, + gateway: Gateway, + pledge: Coin, + owner_signature: String, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result<(), BackendError> { - let pledge = pledge.into_backend_coin(state.read().await.current_network().denom())?; - nymd_client!(state) - .bond_gateway(gateway, owner_signature, pledge, fee) - .await?; - Ok(()) + let pledge = pledge.into_backend_coin(state.read().await.current_network().denom())?; + nymd_client!(state) + .bond_gateway(gateway, owner_signature, pledge, fee) + .await?; + Ok(()) } #[tauri::command] pub async fn unbond_gateway( - fee: Option, - state: tauri::State<'_, Arc>>, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result<(), BackendError> { - nymd_client!(state).unbond_gateway(fee).await?; - Ok(()) + nymd_client!(state).unbond_gateway(fee).await?; + Ok(()) } #[tauri::command] pub async fn unbond_mixnode( - fee: Option, - state: tauri::State<'_, Arc>>, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result<(), BackendError> { - nymd_client!(state).unbond_mixnode(fee).await?; - Ok(()) + nymd_client!(state).unbond_mixnode(fee).await?; + Ok(()) } #[tauri::command] pub async fn bond_mixnode( - mixnode: MixNode, - owner_signature: String, - pledge: Coin, - fee: Option, - state: tauri::State<'_, Arc>>, + mixnode: MixNode, + owner_signature: String, + pledge: Coin, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result<(), BackendError> { - let pledge = pledge.into_backend_coin(state.read().await.current_network().denom())?; - nymd_client!(state) - .bond_mixnode(mixnode, owner_signature, pledge, fee) - .await?; - Ok(()) + let pledge = pledge.into_backend_coin(state.read().await.current_network().denom())?; + nymd_client!(state) + .bond_mixnode(mixnode, owner_signature, pledge, fee) + .await?; + Ok(()) } #[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<(), BackendError> { - nymd_client!(state) - .update_mixnode_config(profit_margin_percent, fee) - .await?; - Ok(()) + nymd_client!(state) + .update_mixnode_config(profit_margin_percent, fee) + .await?; + Ok(()) } #[tauri::command] pub async fn mixnode_bond_details( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, Arc>>, ) -> Result, BackendError> { - let guard = state.read().await; - let client = guard.current_client()?; - let bond = client.nymd.owns_mixnode(client.nymd.address()).await?; - Ok(bond) + let guard = state.read().await; + let client = guard.current_client()?; + let bond = client.nymd.owns_mixnode(client.nymd.address()).await?; + Ok(bond) } #[tauri::command] pub async fn gateway_bond_details( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, Arc>>, ) -> Result, BackendError> { - let guard = state.read().await; - let client = guard.current_client()?; - let bond = client.nymd.owns_gateway(client.nymd.address()).await?; - Ok(bond) + let guard = state.read().await; + let client = guard.current_client()?; + let bond = client.nymd.owns_gateway(client.nymd.address()).await?; + Ok(bond) } #[tauri::command] pub async fn get_operator_rewards( - address: String, - state: tauri::State<'_, Arc>>, + address: String, + state: tauri::State<'_, Arc>>, ) -> Result { - Ok(nymd_client!(state).get_operator_rewards(address).await?) + Ok(nymd_client!(state).get_operator_rewards(address).await?) } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs index cedc6cf085..c559b18e43 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs @@ -12,73 +12,67 @@ use validator_client::nymd::Fee; #[tauri::command] pub async fn get_pending_delegation_events( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, Arc>>, ) -> Result, BackendError> { - Ok( - nymd_client!(state) - .get_pending_delegation_events(nymd_client!(state).address().to_string(), None) - .await? - .into_iter() - .map(|delegation_event| delegation_event.into()) - .collect::>(), - ) + Ok(nymd_client!(state) + .get_pending_delegation_events(nymd_client!(state).address().to_string(), None) + .await? + .into_iter() + .map(|delegation_event| delegation_event.into()) + .collect::>()) } #[tauri::command] pub async fn delegate_to_mixnode( - identity: &str, - amount: Coin, - fee: Option, - state: tauri::State<'_, Arc>>, + identity: &str, + amount: Coin, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - let delegation = amount.into_backend_coin(state.read().await.current_network().denom())?; - nymd_client!(state) - .delegate_to_mixnode(identity, delegation.clone(), fee) - .await?; - Ok(DelegationResult::new( - nymd_client!(state).address().as_ref(), - identity, - Some(delegation.into()), - )) + let delegation = amount.into_backend_coin(state.read().await.current_network().denom())?; + nymd_client!(state) + .delegate_to_mixnode(identity, delegation.clone(), fee) + .await?; + Ok(DelegationResult::new( + nymd_client!(state).address().as_ref(), + identity, + Some(delegation.into()), + )) } #[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 { - nymd_client!(state) - .remove_mixnode_delegation(identity, fee) - .await?; - Ok(DelegationResult::new( - nymd_client!(state).address().as_ref(), - identity, - None, - )) + nymd_client!(state) + .remove_mixnode_delegation(identity, fee) + .await?; + Ok(DelegationResult::new( + nymd_client!(state).address().as_ref(), + identity, + None, + )) } #[tauri::command] pub async fn get_reverse_mix_delegations_paged( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, Arc>>, ) -> Result { - Ok( - nymd_client!(state) - .get_delegator_delegations_paged(nymd_client!(state).address().to_string(), None, None) - .await?, - ) + Ok(nymd_client!(state) + .get_delegator_delegations_paged(nymd_client!(state).address().to_string(), None, None) + .await?) } #[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 { - Ok( - nymd_client!(state) - .get_delegator_rewards(address, mix_identity, proxy) - .await?, - ) + Ok(nymd_client!(state) + .get_delegator_rewards(address, mix_identity, proxy) + .await?) } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/epoch.rs b/nym-wallet/src-tauri/src/operations/mixnet/epoch.rs index 085564ca06..263e0fcf6d 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/epoch.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/epoch.rs @@ -10,27 +10,27 @@ use tokio::sync::RwLock; #[cfg_attr(test, ts(export, export_to = "../src/types/rust/epoch.ts"))] #[derive(Clone, Copy, Debug, Deserialize, PartialEq, PartialOrd, Serialize)] pub struct Epoch { - id: u32, - start: i64, - end: i64, - duration_seconds: u64, + id: u32, + start: i64, + end: i64, + duration_seconds: u64, } impl From for Epoch { - fn from(interval: Interval) -> Self { - Self { - id: interval.id(), - start: interval.start_unix_timestamp(), - end: interval.end_unix_timestamp(), - duration_seconds: interval.length().as_secs(), + fn from(interval: Interval) -> Self { + Self { + id: interval.id(), + start: interval.start_unix_timestamp(), + end: interval.end_unix_timestamp(), + duration_seconds: interval.length().as_secs(), + } } - } } #[tauri::command] pub async fn get_current_epoch( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, Arc>>, ) -> Result { - let interval = nymd_client!(state).get_current_epoch().await?; - Ok(interval.into()) + let interval = nymd_client!(state).get_current_epoch().await?; + 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 ea73967301..52a7258dfc 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/send.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/send.rs @@ -12,58 +12,58 @@ use validator_client::nymd::{AccountId, Fee, TxResponse}; #[cfg_attr(test, ts(export, export_to = "../src/types/rust/tauritxresult.ts"))] #[derive(Deserialize, Serialize)] pub struct TauriTxResult { - block_height: u64, - code: u32, - details: TransactionDetails, - gas_used: u64, - gas_wanted: u64, - tx_hash: String, + block_height: u64, + code: u32, + details: TransactionDetails, + gas_used: u64, + gas_wanted: u64, + tx_hash: String, } #[cfg_attr(test, derive(ts_rs::TS))] #[cfg_attr( - test, - ts(export, export_to = "../src/types/rust/transactiondetails.ts") + test, + ts(export, export_to = "../src/types/rust/transactiondetails.ts") )] #[derive(Deserialize, Serialize)] pub struct TransactionDetails { - amount: Coin, - from_address: String, - to_address: String, + amount: Coin, + from_address: String, + to_address: String, } impl TauriTxResult { - fn new(t: TxResponse, details: TransactionDetails) -> TauriTxResult { - TauriTxResult { - block_height: t.height.value(), - code: t.tx_result.code.value(), - details, - gas_used: t.tx_result.gas_used.value(), - gas_wanted: t.tx_result.gas_wanted.value(), - tx_hash: t.hash.to_string(), + fn new(t: TxResponse, details: TransactionDetails) -> TauriTxResult { + TauriTxResult { + block_height: t.height.value(), + code: t.tx_result.code.value(), + details, + gas_used: t.tx_result.gas_used.value(), + gas_wanted: t.tx_result.gas_wanted.value(), + tx_hash: t.hash.to_string(), + } } - } } #[tauri::command] pub async fn send( - address: &str, - amount: Coin, - memo: String, - fee: Option, - state: tauri::State<'_, Arc>>, + address: &str, + amount: Coin, + memo: String, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - let address = AccountId::from_str(address)?; - let amount = amount.into_backend_coin(state.read().await.current_network().denom())?; - let result = nymd_client!(state) - .send(&address, vec![amount.clone()], memo, fee) - .await?; - Ok(TauriTxResult::new( - result, - TransactionDetails { - from_address: nymd_client!(state).address().to_string(), - to_address: address.to_string(), - amount: amount.into(), - }, - )) + let address = AccountId::from_str(address)?; + let amount = amount.into_backend_coin(state.read().await.current_network().denom())?; + let result = nymd_client!(state) + .send(&address, vec![amount.clone()], memo, fee) + .await?; + Ok(TauriTxResult::new( + result, + TransactionDetails { + from_address: nymd_client!(state).address().to_string(), + to_address: address.to_string(), + amount: amount.into(), + }, + )) } diff --git a/nym-wallet/src-tauri/src/operations/simulate/admin.rs b/nym-wallet/src-tauri/src/operations/simulate/admin.rs index ead6a3fca6..c7006fcf46 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/admin.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/admin.rs @@ -11,21 +11,21 @@ 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_fundless_contract_execute_message( - mixnet_contract, - &ExecuteMsg::UpdateContractStateParams(mixnet_contract_settings_params), - )?; + let msg = client.nymd.wrap_fundless_contract_execute_message( + mixnet_contract, + &ExecuteMsg::UpdateContractStateParams(mixnet_contract_settings_params), + )?; - 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 cc6dce3a6e..4f2078713a 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: Coin, - state: tauri::State<'_, Arc>>, + address: &str, + amount: Coin, + state: tauri::State<'_, Arc>>, ) -> Result { - let guard = state.read().await; + let guard = state.read().await; - let to_address = AccountId::from_str(address)?; - let amount = amount.into_backend_coin(guard.current_network().denom())?; + let to_address = AccountId::from_str(address)?; + let amount = amount.into_backend_coin(guard.current_network().denom())?; - 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: vec![amount.into()], - }; + // 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: vec![amount.into()], + }; - 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 55d160043e..8b62a1ec74 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs @@ -11,160 +11,160 @@ use tokio::sync::RwLock; #[tauri::command] pub async fn simulate_bond_gateway( - gateway: Gateway, - pledge: Coin, - owner_signature: String, - state: tauri::State<'_, Arc>>, + gateway: Gateway, + pledge: Coin, + owner_signature: String, + state: tauri::State<'_, Arc>>, ) -> Result { - let guard = state.read().await; - let pledge = pledge.into_backend_coin(guard.current_network().denom())?; + let guard = state.read().await; + let pledge = pledge.into_backend_coin(guard.current_network().denom())?; - 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_fundless_contract_execute_message(mixnet_contract, &ExecuteMsg::UnbondGateway {})?; + let msg = client + .nymd + .wrap_fundless_contract_execute_message(mixnet_contract, &ExecuteMsg::UnbondGateway {})?; - 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: Coin, - state: tauri::State<'_, Arc>>, + mixnode: MixNode, + owner_signature: String, + pledge: Coin, + state: tauri::State<'_, Arc>>, ) -> Result { - let guard = state.read().await; - let pledge = pledge.into_backend_coin(guard.current_network().denom())?; + let guard = state.read().await; + let pledge = pledge.into_backend_coin(guard.current_network().denom())?; - 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_fundless_contract_execute_message(mixnet_contract, &ExecuteMsg::UnbondMixnode {})?; + let msg = client + .nymd + .wrap_fundless_contract_execute_message(mixnet_contract, &ExecuteMsg::UnbondMixnode {})?; - 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_fundless_contract_execute_message( - mixnet_contract, - &ExecuteMsg::UpdateMixnodeConfig { - profit_margin_percent, - }, - )?; + let msg = client.nymd.wrap_fundless_contract_execute_message( + mixnet_contract, + &ExecuteMsg::UpdateMixnodeConfig { + profit_margin_percent, + }, + )?; - 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: Coin, - state: tauri::State<'_, Arc>>, + identity: &str, + amount: Coin, + state: tauri::State<'_, Arc>>, ) -> Result { - let guard = state.read().await; - let delegation = amount.into_backend_coin(guard.current_network().denom())?; + let guard = state.read().await; + let delegation = amount.into_backend_coin(guard.current_network().denom())?; - 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_fundless_contract_execute_message( - mixnet_contract, - &ExecuteMsg::UndelegateFromMixnode { - mix_identity: identity.to_string(), - }, - )?; + let msg = client.nymd.wrap_fundless_contract_execute_message( + mixnet_contract, + &ExecuteMsg::UndelegateFromMixnode { + mix_identity: identity.to_string(), + }, + )?; - 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 d0f333ca4e..273f3d05ab 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/mod.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/mod.rs @@ -14,52 +14,50 @@ 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, + } } - } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FeeDetails { - // expected to be used by the wallet in order to display detailed fee information to the user - pub amount: Option, - pub fee: Fee, + // expected to be used by the wallet in order to display detailed fee information to the user + pub amount: Option, + pub fee: Fee, } impl SimulateResult { - pub fn detailed_fee(&self) -> FeeDetails { - FeeDetails { - amount: self.to_fee_amount().map(Into::into), - fee: self.to_fee(), + pub fn detailed_fee(&self) -> FeeDetails { + FeeDetails { + amount: self.to_fee_amount().map(Into::into), + 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_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() - } + 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 b852a4bf78..bb9823ea84 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs @@ -12,135 +12,135 @@ use vesting_contract_common::ExecuteMsg; #[tauri::command] pub async fn simulate_vesting_bond_gateway( - gateway: Gateway, - pledge: Coin, - owner_signature: String, - state: tauri::State<'_, Arc>>, + gateway: Gateway, + pledge: Coin, + owner_signature: String, + state: tauri::State<'_, Arc>>, ) -> Result { - let guard = state.read().await; - let pledge = pledge.into_backend_coin(guard.current_network().denom())?; + let guard = state.read().await; + let pledge = pledge.into_backend_coin(guard.current_network().denom())?; - 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_fundless_contract_execute_message( - vesting_contract, - &ExecuteMsg::BondGateway { - gateway, - owner_signature, - amount: pledge.into(), - }, - )?; + let msg = client.nymd.wrap_fundless_contract_execute_message( + vesting_contract, + &ExecuteMsg::BondGateway { + gateway, + owner_signature, + amount: pledge.into(), + }, + )?; - 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_fundless_contract_execute_message(vesting_contract, &ExecuteMsg::UnbondGateway {})?; + let msg = client + .nymd + .wrap_fundless_contract_execute_message(vesting_contract, &ExecuteMsg::UnbondGateway {})?; - 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: Coin, - state: tauri::State<'_, Arc>>, + mixnode: MixNode, + owner_signature: String, + pledge: Coin, + state: tauri::State<'_, Arc>>, ) -> Result { - let guard = state.read().await; - let pledge = pledge.into_backend_coin(guard.current_network().denom())?; + let guard = state.read().await; + let pledge = pledge.into_backend_coin(guard.current_network().denom())?; - 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_fundless_contract_execute_message( - vesting_contract, - &ExecuteMsg::BondMixnode { - mix_node: mixnode, - owner_signature, - amount: pledge.into(), - }, - )?; + let msg = client.nymd.wrap_fundless_contract_execute_message( + vesting_contract, + &ExecuteMsg::BondMixnode { + mix_node: mixnode, + owner_signature, + amount: pledge.into(), + }, + )?; - 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_fundless_contract_execute_message(vesting_contract, &ExecuteMsg::UnbondMixnode {})?; + let msg = client + .nymd + .wrap_fundless_contract_execute_message(vesting_contract, &ExecuteMsg::UnbondMixnode {})?; - 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_fundless_contract_execute_message( - vesting_contract, - &ExecuteMsg::UpdateMixnodeConfig { - profit_margin_percent, - }, - )?; + let msg = client.nymd.wrap_fundless_contract_execute_message( + vesting_contract, + &ExecuteMsg::UpdateMixnodeConfig { + profit_margin_percent, + }, + )?; - 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: Coin, - state: tauri::State<'_, Arc>>, + amount: Coin, + state: tauri::State<'_, Arc>>, ) -> Result { - let guard = state.read().await; - let amount = amount.into_backend_coin(guard.current_network().denom())?; + let guard = state.read().await; + let amount = amount.into_backend_coin(guard.current_network().denom())?; - 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_fundless_contract_execute_message( - vesting_contract, - &ExecuteMsg::WithdrawVestedCoins { - amount: amount.into(), - }, - )?; + let msg = client.nymd.wrap_fundless_contract_execute_message( + vesting_contract, + &ExecuteMsg::WithdrawVestedCoins { + amount: amount.into(), + }, + )?; - 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 a70b568171..d06842f067 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/bond.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/bond.rs @@ -9,73 +9,73 @@ use validator_client::nymd::{Fee, VestingSigningClient}; #[tauri::command] pub async fn vesting_bond_gateway( - gateway: Gateway, - pledge: Coin, - owner_signature: String, - fee: Option, - state: tauri::State<'_, Arc>>, + gateway: Gateway, + pledge: Coin, + owner_signature: String, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result<(), BackendError> { - let pledge = pledge.into_backend_coin(state.read().await.current_network().denom())?; - nymd_client!(state) - .vesting_bond_gateway(gateway, &owner_signature, pledge, fee) - .await?; - Ok(()) + let pledge = pledge.into_backend_coin(state.read().await.current_network().denom())?; + nymd_client!(state) + .vesting_bond_gateway(gateway, &owner_signature, pledge, fee) + .await?; + Ok(()) } #[tauri::command] pub async fn vesting_unbond_gateway( - fee: Option, - state: tauri::State<'_, Arc>>, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result<(), BackendError> { - nymd_client!(state).vesting_unbond_gateway(fee).await?; - Ok(()) + nymd_client!(state).vesting_unbond_gateway(fee).await?; + Ok(()) } #[tauri::command] pub async fn vesting_unbond_mixnode( - fee: Option, - state: tauri::State<'_, Arc>>, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result<(), BackendError> { - nymd_client!(state).vesting_unbond_mixnode(fee).await?; - Ok(()) + nymd_client!(state).vesting_unbond_mixnode(fee).await?; + Ok(()) } #[tauri::command] pub async fn vesting_bond_mixnode( - mixnode: MixNode, - owner_signature: String, - pledge: Coin, - fee: Option, - state: tauri::State<'_, Arc>>, + mixnode: MixNode, + owner_signature: String, + pledge: Coin, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result<(), BackendError> { - let pledge = pledge.into_backend_coin(state.read().await.current_network().denom())?; - nymd_client!(state) - .vesting_bond_mixnode(mixnode, &owner_signature, pledge, fee) - .await?; - Ok(()) + let pledge = pledge.into_backend_coin(state.read().await.current_network().denom())?; + nymd_client!(state) + .vesting_bond_mixnode(mixnode, &owner_signature, pledge, fee) + .await?; + Ok(()) } #[tauri::command] pub async fn withdraw_vested_coins( - amount: Coin, - fee: Option, - state: tauri::State<'_, Arc>>, + amount: Coin, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result<(), BackendError> { - let amount = amount.into_backend_coin(state.read().await.current_network().denom())?; - nymd_client!(state) - .withdraw_vested_coins(amount, fee) - .await?; - Ok(()) + let amount = amount.into_backend_coin(state.read().await.current_network().denom())?; + nymd_client!(state) + .withdraw_vested_coins(amount, fee) + .await?; + Ok(()) } #[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<(), BackendError> { - nymd_client!(state) - .vesting_update_mixnode_config(profit_margin_percent, fee) - .await?; - Ok(()) + nymd_client!(state) + .vesting_update_mixnode_config(profit_margin_percent, fee) + .await?; + Ok(()) } diff --git a/nym-wallet/src-tauri/src/operations/vesting/delegate.rs b/nym-wallet/src-tauri/src/operations/vesting/delegate.rs index 59e669d45f..a212afe7f1 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/delegate.rs @@ -10,55 +10,53 @@ use validator_client::nymd::{Fee, VestingSigningClient}; #[tauri::command] pub async fn get_pending_vesting_delegation_events( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, Arc>>, ) -> Result, BackendError> { - 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()?; - Ok( - client - .get_pending_delegation_events( - client.address().to_string(), - Some(vesting_contract.to_string()), - ) - .await? - .into_iter() - .map(|delegation_event| delegation_event.into()) - .collect::>(), - ) + Ok(client + .get_pending_delegation_events( + client.address().to_string(), + Some(vesting_contract.to_string()), + ) + .await? + .into_iter() + .map(|delegation_event| delegation_event.into()) + .collect::>()) } #[tauri::command] pub async fn vesting_delegate_to_mixnode( - identity: &str, - amount: Coin, - fee: Option, - state: tauri::State<'_, Arc>>, + identity: &str, + amount: Coin, + fee: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - let delegation = amount.into_backend_coin(state.read().await.current_network().denom())?; - nymd_client!(state) - .vesting_delegate_to_mixnode(identity, delegation.clone(), fee) - .await?; - Ok(DelegationResult::new( - nymd_client!(state).address().as_ref(), - identity, - Some(delegation.into()), - )) + let delegation = amount.into_backend_coin(state.read().await.current_network().denom())?; + nymd_client!(state) + .vesting_delegate_to_mixnode(identity, delegation.clone(), fee) + .await?; + Ok(DelegationResult::new( + nymd_client!(state).address().as_ref(), + identity, + Some(delegation.into()), + )) } #[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 { - nymd_client!(state) - .vesting_undelegate_from_mixnode(identity, fee) - .await?; - Ok(DelegationResult::new( - nymd_client!(state).address().as_ref(), - identity, - None, - )) + nymd_client!(state) + .vesting_undelegate_from_mixnode(identity, fee) + .await?; + Ok(DelegationResult::new( + nymd_client!(state).address().as_ref(), + identity, + None, + )) } diff --git a/nym-wallet/src-tauri/src/operations/vesting/mod.rs b/nym-wallet/src-tauri/src/operations/vesting/mod.rs index 9c4829e200..3542fd8d5c 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/mod.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/mod.rs @@ -13,90 +13,90 @@ pub mod queries; #[cfg_attr(test, ts(export, export_to = "../src/types/rust/pledgedata.ts"))] #[derive(Serialize, Deserialize, Debug)] pub struct PledgeData { - pub amount: Coin, - pub block_time: u64, + pub amount: Coin, + pub block_time: u64, } impl From for PledgeData { - fn from(data: VestingPledgeData) -> Self { - Self { - amount: data.amount().into(), - block_time: data.block_time().seconds(), + fn from(data: VestingPledgeData) -> Self { + Self { + amount: data.amount().into(), + block_time: data.block_time().seconds(), + } } - } } impl PledgeData { - fn and_then(data: VestingPledgeData) -> Option { - Some(data.into()) - } + fn and_then(data: VestingPledgeData) -> Option { + Some(data.into()) + } } #[cfg_attr(test, derive(ts_rs::TS))] #[cfg_attr( - test, - ts(export, export_to = "../src/types/rust/originalvestingresponse.ts") + test, + ts(export, export_to = "../src/types/rust/originalvestingresponse.ts") )] #[derive(Serialize, Deserialize, Debug)] pub struct OriginalVestingResponse { - amount: Coin, - number_of_periods: usize, - period_duration: u64, + amount: Coin, + number_of_periods: usize, + period_duration: u64, } impl From for OriginalVestingResponse { - fn from(data: VestingOriginalVestingResponse) -> Self { - Self { - amount: data.amount().into(), - number_of_periods: data.number_of_periods(), - period_duration: data.period_duration(), + fn from(data: VestingOriginalVestingResponse) -> Self { + Self { + amount: data.amount().into(), + number_of_periods: data.number_of_periods(), + period_duration: data.period_duration(), + } } - } } #[cfg_attr(test, derive(ts_rs::TS))] #[cfg_attr( - test, - ts(export, export_to = "../src/types/rust/vestingaccountinfo.ts") + test, + ts(export, export_to = "../src/types/rust/vestingaccountinfo.ts") )] #[derive(Serialize, Deserialize, Debug)] pub struct VestingAccountInfo { - owner_address: String, - staking_address: Option, - start_time: u64, - periods: Vec, - coin: Coin, + owner_address: String, + staking_address: Option, + start_time: u64, + periods: Vec, + coin: Coin, } impl From for VestingAccountInfo { - fn from(account: VestingAccount) -> Self { - let mut periods = Vec::new(); - for period in account.periods() { - periods.push(period.into()); + fn from(account: VestingAccount) -> Self { + let mut periods = Vec::new(); + for period in account.periods() { + periods.push(period.into()); + } + Self { + owner_address: account.owner_address().to_string(), + staking_address: account.staking_address().map(|a| a.to_string()), + start_time: account.start_time().seconds(), + periods, + coin: account.coin().into(), + } } - Self { - owner_address: account.owner_address().to_string(), - staking_address: account.staking_address().map(|a| a.to_string()), - start_time: account.start_time().seconds(), - periods, - coin: account.coin().into(), - } - } } #[cfg_attr(test, derive(ts_rs::TS))] #[cfg_attr(test, ts(export, export_to = "../src/types/rust/vestingperiod.ts"))] #[derive(Serialize, Deserialize, Debug)] pub struct VestingPeriod { - start_time: u64, - period_seconds: u64, + start_time: u64, + period_seconds: u64, } impl From for VestingPeriod { - fn from(period: VestingVestingPeriod) -> Self { - Self { - start_time: period.start_time, - period_seconds: period.period_seconds, + fn from(period: VestingVestingPeriod) -> Self { + Self { + start_time: period.start_time, + period_seconds: period.period_seconds, + } } - } } diff --git a/nym-wallet/src-tauri/src/operations/vesting/queries.rs b/nym-wallet/src-tauri/src/operations/vesting/queries.rs index e88f80a7c2..1e1b38f5cc 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/queries.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/queries.rs @@ -13,185 +13,161 @@ use super::{OriginalVestingResponse, PledgeData}; #[tauri::command] pub async fn locked_coins( - block_time: Option, - state: tauri::State<'_, Arc>>, + block_time: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - Ok( - nymd_client!(state) - .locked_coins( - nymd_client!(state).address().as_ref(), - block_time.map(Timestamp::from_seconds), - ) - .await? - .into(), - ) + Ok(nymd_client!(state) + .locked_coins( + nymd_client!(state).address().as_ref(), + block_time.map(Timestamp::from_seconds), + ) + .await? + .into()) } #[tauri::command] pub async fn spendable_coins( - block_time: Option, - state: tauri::State<'_, Arc>>, + block_time: Option, + state: tauri::State<'_, Arc>>, ) -> Result { - Ok( - nymd_client!(state) - .spendable_coins( - nymd_client!(state).address().as_ref(), - block_time.map(Timestamp::from_seconds), - ) - .await? - .into(), - ) + Ok(nymd_client!(state) + .spendable_coins( + nymd_client!(state).address().as_ref(), + block_time.map(Timestamp::from_seconds), + ) + .await? + .into()) } #[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 { - Ok( - nymd_client!(state) - .vested_coins( - vesting_account_address, - block_time.map(Timestamp::from_seconds), - ) - .await? - .into(), - ) + Ok(nymd_client!(state) + .vested_coins( + vesting_account_address, + block_time.map(Timestamp::from_seconds), + ) + .await? + .into()) } #[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 { - Ok( - nymd_client!(state) - .vesting_coins( - vesting_account_address, - block_time.map(Timestamp::from_seconds), - ) - .await? - .into(), - ) + Ok(nymd_client!(state) + .vesting_coins( + vesting_account_address, + block_time.map(Timestamp::from_seconds), + ) + .await? + .into()) } #[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 { - Ok( - nymd_client!(state) - .vesting_start_time(vesting_account_address) - .await? - .seconds(), - ) + Ok(nymd_client!(state) + .vesting_start_time(vesting_account_address) + .await? + .seconds()) } #[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 { - Ok( - nymd_client!(state) - .vesting_end_time(vesting_account_address) - .await? - .seconds(), - ) + Ok(nymd_client!(state) + .vesting_end_time(vesting_account_address) + .await? + .seconds()) } #[tauri::command] pub async fn original_vesting( - vesting_account_address: &str, - state: tauri::State<'_, Arc>>, + vesting_account_address: &str, + state: tauri::State<'_, Arc>>, ) -> Result { - Ok( - nymd_client!(state) - .original_vesting(vesting_account_address) - .await? - .into(), - ) + Ok(nymd_client!(state) + .original_vesting(vesting_account_address) + .await? + .into()) } #[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 { - Ok( - nymd_client!(state) - .delegated_free( - vesting_account_address, - block_time.map(Timestamp::from_seconds), - ) - .await? - .into(), - ) + Ok(nymd_client!(state) + .delegated_free( + vesting_account_address, + block_time.map(Timestamp::from_seconds), + ) + .await? + .into()) } #[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 { - Ok( - nymd_client!(state) - .delegated_vesting( - vesting_account_address, - block_time.map(Timestamp::from_seconds), - ) - .await? - .into(), - ) + Ok(nymd_client!(state) + .delegated_vesting( + vesting_account_address, + block_time.map(Timestamp::from_seconds), + ) + .await? + .into()) } #[tauri::command] pub async fn vesting_get_mixnode_pledge( - address: &str, - state: tauri::State<'_, Arc>>, + address: &str, + state: tauri::State<'_, Arc>>, ) -> Result, BackendError> { - Ok( - nymd_client!(state) - .get_mixnode_pledge(address) - .await? - .and_then(PledgeData::and_then), - ) + Ok(nymd_client!(state) + .get_mixnode_pledge(address) + .await? + .and_then(PledgeData::and_then)) } #[tauri::command] pub async fn vesting_get_gateway_pledge( - address: &str, - state: tauri::State<'_, Arc>>, + address: &str, + state: tauri::State<'_, Arc>>, ) -> Result, BackendError> { - Ok( - nymd_client!(state) - .get_gateway_pledge(address) - .await? - .and_then(PledgeData::and_then), - ) + Ok(nymd_client!(state) + .get_gateway_pledge(address) + .await? + .and_then(PledgeData::and_then)) } #[tauri::command] pub async fn get_current_vesting_period( - address: &str, - state: tauri::State<'_, Arc>>, + address: &str, + state: tauri::State<'_, Arc>>, ) -> Result { - Ok( - nymd_client!(state) - .get_current_vesting_period(address) - .await?, - ) + Ok(nymd_client!(state) + .get_current_vesting_period(address) + .await?) } #[tauri::command] pub async fn get_account_info( - address: &str, - state: tauri::State<'_, Arc>>, + address: &str, + state: tauri::State<'_, Arc>>, ) -> Result { - Ok(nymd_client!(state).get_account(address).await?.into()) + Ok(nymd_client!(state).get_account(address).await?.into()) } diff --git a/nym-wallet/src-tauri/src/state.rs b/nym-wallet/src-tauri/src/state.rs index cf889718e8..2fee2b09f0 100644 --- a/nym-wallet/src-tauri/src/state.rs +++ b/nym-wallet/src-tauri/src/state.rs @@ -17,450 +17,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 9a28277e3e..405da42ee3 100644 --- a/nym-wallet/src-tauri/src/utils.rs +++ b/nym-wallet/src-tauri/src/utils.rs @@ -14,126 +14,128 @@ use tokio::sync::RwLock; #[cfg_attr(test, ts(export, export_to = "../src/types/rust/appEnv.ts"))] #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] pub struct AppEnv { - pub ADMIN_ADDRESS: Option, - pub SHOW_TERMINAL: Option, + pub ADMIN_ADDRESS: Option, + pub SHOW_TERMINAL: Option, } 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 fn major_to_minor(amount: &str) -> Coin { - let coin = Coin::new(amount, &Denom::Major); - coin.to_minor() + let coin = Coin::new(amount, &Denom::Major); + coin.to_minor() } #[tauri::command] pub fn minor_to_major(amount: &str) -> Coin { - let coin = Coin::new(amount, &Denom::Minor); - coin.to_major() + let coin = Coin::new(amount, &Denom::Minor); + coin.to_major() } #[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()) } #[cfg_attr(test, derive(ts_rs::TS))] #[cfg_attr(test, ts(export, export_to = "../src/types/rust/delegationresult.ts"))] #[derive(Serialize, Deserialize)] pub struct DelegationResult { - source_address: String, - target_address: String, - amount: Option, + source_address: String, + target_address: String, + amount: Option, } impl DelegationResult { - pub fn new(source_address: &str, target_address: &str, amount: Option) -> DelegationResult { - DelegationResult { - source_address: source_address.to_string(), - target_address: target_address.to_string(), - amount, + pub fn new( + source_address: &str, + target_address: &str, + amount: Option, + ) -> DelegationResult { + DelegationResult { + source_address: source_address.to_string(), + target_address: target_address.to_string(), + amount, + } } - } } impl From for DelegationResult { - fn from(delegation: Delegation) -> Self { - DelegationResult { - source_address: delegation.owner().to_string(), - target_address: delegation.node_identity(), - amount: Some(delegation.amount.into()), + fn from(delegation: Delegation) -> Self { + DelegationResult { + source_address: delegation.owner().to_string(), + target_address: delegation.node_identity(), + amount: Some(delegation.amount.into()), + } } - } } #[cfg_attr(test, derive(ts_rs::TS))] #[cfg_attr(test, ts(export, export_to = "../src/types/rust/delegationevent.ts"))] #[derive(Deserialize, Serialize)] pub enum DelegationEvent { - Delegate(DelegationResult), - Undelegate(PendingUndelegate), + Delegate(DelegationResult), + Undelegate(PendingUndelegate), } impl From for DelegationEvent { - fn from(event: ContractDelegationEvent) -> Self { - match event { - ContractDelegationEvent::Delegate(delegation) => DelegationEvent::Delegate(delegation.into()), - ContractDelegationEvent::Undelegate(pending_undelegate) => { - DelegationEvent::Undelegate(pending_undelegate.into()) - } + fn from(event: ContractDelegationEvent) -> Self { + match event { + ContractDelegationEvent::Delegate(delegation) => { + DelegationEvent::Delegate(delegation.into()) + } + ContractDelegationEvent::Undelegate(pending_undelegate) => { + DelegationEvent::Undelegate(pending_undelegate.into()) + } + } } - } } #[cfg_attr(test, derive(ts_rs::TS))] #[cfg_attr(test, ts(export, export_to = "../src/types/rust/pendingundelegate.ts"))] #[derive(Deserialize, Serialize)] pub struct PendingUndelegate { - mix_identity: String, - delegate: String, - proxy: Option, - block_height: u64, + mix_identity: String, + delegate: String, + proxy: Option, + block_height: u64, } impl From for PendingUndelegate { - fn from(pending_undelegate: ContractPendingUndelegate) -> Self { - PendingUndelegate { - mix_identity: pending_undelegate.mix_identity(), - delegate: pending_undelegate.delegate().to_string(), - proxy: pending_undelegate.proxy().map(|p| p.to_string()), - block_height: pending_undelegate.block_height(), + fn from(pending_undelegate: ContractPendingUndelegate) -> Self { + PendingUndelegate { + mix_identity: pending_undelegate.mix_identity(), + delegate: pending_undelegate.delegate().to_string(), + proxy: pending_undelegate.proxy().map(|p| p.to_string()), + block_height: pending_undelegate.block_height(), + } } - } } 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/validator-api-requests/src/models.rs b/validator-api/validator-api-requests/src/models.rs index 46e849f816..d98b5f798d 100644 --- a/validator-api/validator-api-requests/src/models.rs +++ b/validator-api/validator-api-requests/src/models.rs @@ -58,6 +58,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,