From 033333bb520f90a9a228998525bfc0e9db785356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 1 Dec 2022 12:44:20 +0000 Subject: [PATCH] Feature/pledge more (#1679) * New transactions for increasing amount of pledged tokens * unit tests * Added an option to pledge extra tokens through the vesting contract * Introduced wallet endpoints for new operations * Using updated pledge cap in the vesting contract * Changelog update --- .../src/nymd/traits/mixnet_signing_client.rs | 29 ++ .../src/nymd/traits/vesting_signing_client.rs | 15 + .../mixnet-contract/src/events.rs | 17 ++ .../mixnet-contract/src/mixnode.rs | 8 + .../mixnet-contract/src/msg.rs | 6 + .../mixnet-contract/src/pending_events.rs | 5 +- .../vesting-contract/src/events.rs | 5 + .../vesting-contract/src/messages.rs | 4 + common/types/src/pending_events.rs | 10 + contracts/CHANGELOG.md | 2 + contracts/mixnet/src/contract.rs | 6 + .../mixnet/src/interval/pending_events.rs | 264 +++++++++++++++++- .../src/mixnet_contract_settings/storage.rs | 1 - contracts/mixnet/src/mixnodes/helpers.rs | 12 +- contracts/mixnet/src/mixnodes/transactions.rs | 239 +++++++++++++++- contracts/mixnet/src/support/tests/mod.rs | 38 ++- contracts/vesting/src/contract.rs | 14 + .../vesting/src/traits/bonding_account.rs | 7 + .../account/mixnode_bonding_account.rs | 61 +++- nym-wallet/src-tauri/src/main.rs | 4 + .../src-tauri/src/operations/mixnet/bond.rs | 27 ++ .../src/operations/simulate/mixnet.rs | 8 + .../src/operations/simulate/vesting.rs | 13 + .../src-tauri/src/operations/vesting/bond.rs | 27 ++ 24 files changed, 808 insertions(+), 14 deletions(-) diff --git a/common/client-libs/validator-client/src/nymd/traits/mixnet_signing_client.rs b/common/client-libs/validator-client/src/nymd/traits/mixnet_signing_client.rs index 41fa7818a8..ad7773305a 100644 --- a/common/client-libs/validator-client/src/nymd/traits/mixnet_signing_client.rs +++ b/common/client-libs/validator-client/src/nymd/traits/mixnet_signing_client.rs @@ -180,6 +180,35 @@ pub trait MixnetSigningClient { .await } + async fn pledge_more( + &self, + additional_pledge: Coin, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::PledgeMore {}, + vec![additional_pledge], + ) + .await + } + + async fn pledge_more_on_behalf( + &self, + owner: AccountId, + additional_pledge: Coin, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::PledgeMoreOnBehalf { + owner: owner.to_string(), + }, + vec![additional_pledge], + ) + .await + } + async fn unbond_mixnode(&self, fee: Option) -> Result { self.execute_mixnet_contract(fee, MixnetExecuteMsg::UnbondMixnode {}, vec![]) .await 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 eebf0adfa3..120e9ca2e0 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 @@ -64,6 +64,21 @@ pub trait VestingSigningClient { fee: Option, ) -> Result; + async fn vesting_pledge_more( + &self, + additional_pledge: Coin, + fee: Option, + ) -> Result { + self.execute_vesting_contract( + fee, + VestingExecuteMsg::PledgeMore { + amount: additional_pledge.into(), + }, + vec![], + ) + .await + } + async fn vesting_unbond_mixnode(&self, fee: Option) -> Result; async fn vesting_track_unbond_mixnode( diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs index bba85e3ccd..f9f5de1802 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs @@ -12,6 +12,8 @@ pub const EVENT_VERSION_PREFIX: &str = "v2_"; pub enum MixnetEventType { MixnodeBonding, + PendingPledgeIncrease, + PledgeIncrease, GatewayBonding, GatewayUnbonding, PendingMixnodeUnbonding, @@ -51,6 +53,8 @@ impl ToString for MixnetEventType { fn to_string(&self) -> String { let event_name = match self { MixnetEventType::MixnodeBonding => "mixnode_bonding", + MixnetEventType::PendingPledgeIncrease => "pending_pledge_increase", + MixnetEventType::PledgeIncrease => "pledge_increase", MixnetEventType::GatewayBonding => "gateway_bonding", MixnetEventType::GatewayUnbonding => "gateway_unbonding", MixnetEventType::PendingMixnodeUnbonding => "pending_mixnode_unbonding", @@ -330,6 +334,19 @@ pub fn new_mixnode_bonding_event( .add_attribute(AMOUNT_KEY, amount.to_string()) } +pub fn new_pending_pledge_increase_event(mix_id: MixId, amount: &Coin) -> Event { + Event::new(MixnetEventType::PendingPledgeIncrease) + .add_attribute(MIX_ID_KEY, mix_id.to_string()) + .add_attribute(AMOUNT_KEY, amount.to_string()) +} + +pub fn new_pledge_increase_event(created_at: BlockHeight, mix_id: MixId, amount: &Coin) -> Event { + Event::new(MixnetEventType::PledgeIncrease) + .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) + .add_attribute(MIX_ID_KEY, mix_id.to_string()) + .add_attribute(AMOUNT_KEY, amount.to_string()) +} + pub fn new_mixnode_unbonding_event(created_at: BlockHeight, mix_id: MixId) -> Event { Event::new(MixnetEventType::MixnodeUnbonding) .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs index 9bccf2d1ff..f8568e2c6e 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs @@ -325,6 +325,14 @@ impl MixNodeRewarding { Ok(()) } + pub fn increase_operator_uint128( + &mut self, + amount: Uint128, + ) -> Result<(), MixnetContractError> { + self.operator += amount.into_base_decimal()?; + Ok(()) + } + pub fn increase_delegates_uint128( &mut self, amount: Uint128, diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index 1c00c64a9c..7bb3dd766c 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -113,6 +113,10 @@ pub enum ExecuteMsg { owner_signature: String, owner: String, }, + PledgeMore {}, + PledgeMoreOnBehalf { + owner: String, + }, UnbondMixnode {}, UnbondMixnodeOnBehalf { owner: String, @@ -223,6 +227,8 @@ impl ExecuteMsg { ExecuteMsg::BondMixnodeOnBehalf { mix_node, .. } => { format!("bonding mixnode {} on behalf", mix_node.identity_key) } + ExecuteMsg::PledgeMore {} => "pledging additional tokens".into(), + ExecuteMsg::PledgeMoreOnBehalf { .. } => "pledging additional tokens on behalf".into(), ExecuteMsg::UnbondMixnode { .. } => "unbonding mixnode".into(), ExecuteMsg::UnbondMixnodeOnBehalf { .. } => "unbonding mixnode on behalf".into(), ExecuteMsg::UpdateMixnodeCostParams { .. } => "updating mixnode cost parameters".into(), diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs index fce8bd8cf0..3fe817fcb6 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs @@ -34,6 +34,10 @@ pub enum PendingEpochEventKind { mix_id: MixId, proxy: Option, }, + PledgeMore { + mix_id: MixId, + amount: Coin, + }, UnbondMixnode { mix_id: MixId, }, @@ -78,7 +82,6 @@ pub enum PendingIntervalEventKind { mix_id: MixId, new_costs: MixNodeCostParams, }, - UpdateRewardingParams { update: IntervalRewardingParamsUpdate, }, diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs index 896ead2c9e..4e5809beee 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs @@ -14,6 +14,7 @@ pub const VESTING_UNDELEGATION_EVENT_TYPE: &str = "vesting_undelegation"; pub const VESTING_GATEWAY_BONDING_EVENT_TYPE: &str = "vesting_gateway_bonding"; pub const VESTING_GATEWAY_UNBONDING_EVENT_TYPE: &str = "vesting_gateway_unbonding"; pub const VESTING_MIXNODE_BONDING_EVENT_TYPE: &str = "vesting_mixnode_bonding"; +pub const VESTING_PLEDGE_MORE_EVENT_TYPE: &str = "vesting_pledge_more"; pub const VESTING_MIXNODE_UNBONDING_EVENT_TYPE: &str = "vesting_mixnode_unbonding"; pub const VESTING_UPDATE_MIXNODE_CONFIG_EVENT_TYPE: &str = "vesting_update_mixnode_config"; pub const VESTING_UPDATE_MIXNODE_COST_PARAMS_EVENT_TYPE: &str = @@ -112,6 +113,10 @@ pub fn new_vesting_mixnode_bonding_event() -> Event { Event::new(VESTING_MIXNODE_BONDING_EVENT_TYPE) } +pub fn new_vesting_pledge_more_event() -> Event { + Event::new(VESTING_PLEDGE_MORE_EVENT_TYPE) +} + pub fn new_vesting_update_mixnode_config_event() -> Event { Event::new(VESTING_UPDATE_MIXNODE_CONFIG_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 696df628aa..69eb323156 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs @@ -101,6 +101,9 @@ pub enum ExecuteMsg { owner_signature: String, amount: Coin, }, + PledgeMore { + amount: Coin, + }, UnbondMixnode {}, TrackUnbondMixnode { owner: String, @@ -145,6 +148,7 @@ impl ExecuteMsg { ExecuteMsg::WithdrawVestedCoins { .. } => "VestingExecuteMsg::WithdrawVestedCoins", ExecuteMsg::TrackUndelegation { .. } => "VestingExecuteMsg::TrackUndelegation", ExecuteMsg::BondMixnode { .. } => "VestingExecuteMsg::BondMixnode", + ExecuteMsg::PledgeMore { .. } => "VestingExecuteMsg::PledgeMore", ExecuteMsg::UnbondMixnode { .. } => "VestingExecuteMsg::UnbondMixnode", ExecuteMsg::TrackUnbondMixnode { .. } => "VestingExecuteMsg::TrackUnbondMixnode", ExecuteMsg::BondGateway { .. } => "VestingExecuteMsg::BondGateway", diff --git a/common/types/src/pending_events.rs b/common/types/src/pending_events.rs index afe5a54d9e..e9d578dc12 100644 --- a/common/types/src/pending_events.rs +++ b/common/types/src/pending_events.rs @@ -57,6 +57,10 @@ pub enum PendingEpochEventData { mix_id: MixId, proxy: Option, }, + PledgeMore { + mix_id: MixId, + amount: DecCoin, + }, UnbondMixnode { mix_id: MixId, }, @@ -91,6 +95,12 @@ impl PendingEpochEventData { mix_id, proxy: proxy.map(|p| p.into_string()), }), + MixnetContractPendingEpochEventKind::PledgeMore { mix_id, amount } => { + Ok(PendingEpochEventData::PledgeMore { + mix_id, + amount: reg.attempt_convert_to_display_dec_coin(amount.into())?, + }) + } MixnetContractPendingEpochEventKind::UnbondMixnode { mix_id } => { Ok(PendingEpochEventData::UnbondMixnode { mix_id }) } diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md index f57a633956..c192eab3ae 100644 --- a/contracts/CHANGELOG.md +++ b/contracts/CHANGELOG.md @@ -3,7 +3,9 @@ ## Added - Added migration code to the mixnet contract to allow updating stored vesting contract address to make it easier to deploy any future environments ([#1759],[#1769]) +- Added an option to pledge additional tokens without the need to rebond minxode ([#1679]) +[#1679]: https://github.com/nymtech/nym/pull/1679 [#1759]: https://github.com/nymtech/nym/pull/1759 [#1769]: https://github.com/nymtech/nym/pull/1769 diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index e07b7ba32a..63bf3f1a72 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -168,6 +168,12 @@ pub fn execute( owner, owner_signature, ), + ExecuteMsg::PledgeMore {} => { + crate::mixnodes::transactions::try_increase_pledge(deps, env, info) + } + ExecuteMsg::PledgeMoreOnBehalf { owner } => { + crate::mixnodes::transactions::try_increase_pledge_on_behalf(deps, env, info, owner) + } ExecuteMsg::UnbondMixnode {} => { crate::mixnodes::transactions::try_remove_mixnode(deps, env, info) } diff --git a/contracts/mixnet/src/interval/pending_events.rs b/contracts/mixnet/src/interval/pending_events.rs index 8cb09566f2..b242944988 100644 --- a/contracts/mixnet/src/interval/pending_events.rs +++ b/contracts/mixnet/src/interval/pending_events.rs @@ -7,13 +7,14 @@ use crate::interval::helpers::change_interval_config; use crate::interval::storage; use crate::mixnet_contract_settings::storage as mixnet_params_storage; use crate::mixnodes::helpers::{cleanup_post_unbond_mixnode_storage, get_mixnode_details_by_id}; +use crate::mixnodes::storage as mixnodes_storage; use crate::rewards::storage as rewards_storage; use crate::support::helpers::send_to_proxy_or_owner; use cosmwasm_std::{wasm_execute, Addr, Coin, DepsMut, Env, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ new_active_set_update_event, new_delegation_event, new_delegation_on_unbonded_node_event, - new_mixnode_cost_params_update_event, new_mixnode_unbonding_event, + new_mixnode_cost_params_update_event, new_mixnode_unbonding_event, new_pledge_increase_event, new_rewarding_params_update_event, new_undelegation_event, }; use mixnet_contract_common::mixnode::MixNodeCostParams; @@ -258,6 +259,42 @@ pub(crate) fn update_active_set_size( Ok(Response::new().add_event(new_active_set_update_event(created_at, active_set_size))) } +pub(crate) fn increase_pledge( + deps: DepsMut<'_>, + created_at: BlockHeight, + mix_id: MixId, + increase: Coin, +) -> Result { + // note: we have already validated the amount to know it has the correct denomination + + // the target node MUST exist - we have checked it at the time of putting this event onto the queue + // we have also verified there were no preceding unbond events + let mix_details = get_mixnode_details_by_id(deps.storage, mix_id)?.ok_or( + MixnetContractError::InconsistentState { + comment: + "mixnode getting processed to increase its pledge doesn't exist in the storage" + .into(), + }, + )?; + + let mut updated_bond = mix_details.bond_information.clone(); + let mut updated_rewarding = mix_details.rewarding_details; + + updated_bond.original_pledge.amount += increase.amount; + updated_rewarding.increase_operator_uint128(increase.amount)?; + + // update both, bond information and rewarding details + mixnodes_storage::mixnode_bonds().replace( + deps.storage, + mix_id, + Some(&updated_bond), + Some(&mix_details.bond_information), + )?; + rewards_storage::MIXNODE_REWARDING.save(deps.storage, mix_id, &updated_rewarding)?; + + Ok(Response::new().add_event(new_pledge_increase_event(created_at, mix_id, &increase))) +} + impl ContractExecutableEvent for PendingEpochEventData { fn execute(self, deps: DepsMut<'_>, env: &Env) -> Result { // note that the basic validation on all those events was already performed before @@ -274,6 +311,9 @@ impl ContractExecutableEvent for PendingEpochEventData { mix_id, proxy, } => undelegate(deps, self.created_at, owner, mix_id, proxy), + PendingEpochEventKind::PledgeMore { mix_id, amount } => { + increase_pledge(deps, self.created_at, mix_id, amount) + } PendingEpochEventKind::UnbondMixnode { mix_id } => { unbond_mixnode(deps, env, self.created_at, mix_id) } @@ -1135,6 +1175,228 @@ mod tests { } } + #[cfg(test)] + mod increasing_pledge { + use super::*; + use cosmwasm_std::Uint128; + use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; + + #[test] + fn returns_hard_error_if_mixnode_doesnt_exist() { + // this should have never happened so hard error MUST be thrown here + let mut test = TestSetup::new(); + + let amount = test.coin(123); + let res = increase_pledge(test.deps_mut(), 123, 1, amount); + assert!(matches!( + res, + Err(MixnetContractError::InconsistentState { .. }) + )); + } + + #[test] + fn updates_stored_bond_information_and_rewarding_details() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let old_details = get_mixnode_details_by_id(test.deps().storage, mix_id) + .unwrap() + .unwrap(); + + let amount = test.coin(12345); + increase_pledge(test.deps_mut(), 123, mix_id, amount.clone()).unwrap(); + + let updated_details = get_mixnode_details_by_id(test.deps().storage, mix_id) + .unwrap() + .unwrap(); + + assert_eq!( + updated_details.bond_information.original_pledge.amount, + old_details.bond_information.original_pledge.amount + amount.amount + ); + + assert_eq!( + updated_details.rewarding_details.operator, + old_details.rewarding_details.operator + + Decimal::from_atomics(amount.amount, 0).unwrap() + ); + } + + #[test] + fn without_any_events_in_between_is_equivalent_to_pledging_the_same_amount_immediately() { + let mut test = TestSetup::new(); + let pledge1 = Uint128::new(150_000_000); + let pledge2 = Uint128::new(50_000_000); + let pledge3 = Uint128::new(200_000_000); + + let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1)); + let increase = test.coin(pledge2.u128()); + increase_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap(); + + let mix_id_full_pledge = test.add_dummy_mixnode("mix-owner2", Some(pledge3)); + + test.add_immediate_delegation("alice", 123_456_789u128, mix_id_repledge); + test.add_immediate_delegation("bob", 500_000_000u128, mix_id_repledge); + test.add_immediate_delegation("carol", 111_111_111u128, mix_id_repledge); + + test.add_immediate_delegation("alice", 123_456_789u128, mix_id_full_pledge); + test.add_immediate_delegation("bob", 500_000_000u128, mix_id_full_pledge); + test.add_immediate_delegation("carol", 111_111_111u128, mix_id_full_pledge); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); + + let dist1 = + test.reward_with_distribution(mix_id_repledge, test_helpers::performance(100.0)); + let dist2 = + test.reward_with_distribution(mix_id_full_pledge, test_helpers::performance(100.0)); + + assert_eq!(dist1, dist2) + } + + #[test] + fn correctly_increases_future_rewards() { + let mut test = TestSetup::new(); + let pledge1 = Uint128::new(150_000_000_000); + let pledge2 = Uint128::new(50_000_000_000); + + let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1)); + + test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_repledge); + test.add_immediate_delegation("bob", 500_000_000_000u128, mix_id_repledge); + test.add_immediate_delegation("carol", 111_111_111_000u128, mix_id_repledge); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id_repledge]); + + let dist = + test.reward_with_distribution(mix_id_repledge, test_helpers::performance(100.0)); + + let increase = test.coin(pledge2.u128()); + increase_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap(); + + let pledge3 = Uint128::new(200_000_000_000) + truncate_reward_amount(dist.operator); + let mix_id_full_pledge = test.add_dummy_mixnode("mix-owner2", Some(pledge3)); + + test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_full_pledge); + test.add_immediate_delegation("bob", 500_000_000_000u128, mix_id_full_pledge); + test.add_immediate_delegation("carol", 111_111_111_000u128, mix_id_full_pledge); + + let lost_operator = dist.operator + - Decimal::from_atomics(truncate_reward_amount(dist.operator), 0).unwrap(); + let lost_delegates = dist.delegates + - Decimal::from_atomics(truncate_reward_amount(dist.delegates), 0).unwrap(); + + // add the tiny bit of lost precision manually + let mut mix_rewarding_full = test.mix_rewarding(mix_id_full_pledge); + mix_rewarding_full.delegates += lost_delegates; + mix_rewarding_full.operator += lost_operator; + rewards_storage::MIXNODE_REWARDING + .save( + test.deps_mut().storage, + mix_id_full_pledge, + &mix_rewarding_full, + ) + .unwrap(); + + test.add_immediate_delegation( + "dave", + truncate_reward_amount(dist.delegates).u128(), + mix_id_full_pledge, + ); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); + + // go through few epochs of rewarding + for _ in 0..500 { + test.skip_to_next_epoch_end(); + let dist1 = test + .reward_with_distribution(mix_id_repledge, test_helpers::performance(100.0)); + let dist2 = test + .reward_with_distribution(mix_id_full_pledge, test_helpers::performance(100.0)); + + assert_eq!(dist1, dist2) + } + } + + #[test] + fn correctly_increases_future_rewards_with_more_passed_epochs() { + let mut test = TestSetup::new(); + let pledge1 = Uint128::new(150_000_000_000); + let pledge2 = Uint128::new(50_000_000_000); + + let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1)); + + test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_repledge); + test.add_immediate_delegation("bob", 500_000_000_000u128, mix_id_repledge); + test.add_immediate_delegation("carol", 111_111_111_000u128, mix_id_repledge); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id_repledge]); + + let mut cumulative_op_reward = Decimal::zero(); + let mut cumulative_del_reward = Decimal::zero(); + + // go few epochs of rewarding before adding more pledge + for _ in 0..500 { + test.skip_to_next_epoch_end(); + let dist = test + .reward_with_distribution(mix_id_repledge, test_helpers::performance(100.0)); + cumulative_op_reward += dist.operator; + cumulative_del_reward += dist.delegates; + } + + let increase = test.coin(pledge2.u128()); + increase_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap(); + + let pledge3 = + Uint128::new(200_000_000_000) + truncate_reward_amount(cumulative_op_reward); + let mix_id_full_pledge = test.add_dummy_mixnode("mix-owner2", Some(pledge3)); + + test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_full_pledge); + test.add_immediate_delegation("bob", 500_000_000_000u128, mix_id_full_pledge); + test.add_immediate_delegation("carol", 111_111_111_000u128, mix_id_full_pledge); + + let lost_operator = cumulative_op_reward + - Decimal::from_atomics(truncate_reward_amount(cumulative_op_reward), 0).unwrap(); + let lost_delegates = cumulative_del_reward + - Decimal::from_atomics(truncate_reward_amount(cumulative_del_reward), 0).unwrap(); + + // add the tiny bit of lost precision manually + let mut mix_rewarding_full = test.mix_rewarding(mix_id_full_pledge); + mix_rewarding_full.delegates += lost_delegates; + mix_rewarding_full.operator += lost_operator; + rewards_storage::MIXNODE_REWARDING + .save( + test.deps_mut().storage, + mix_id_full_pledge, + &mix_rewarding_full, + ) + .unwrap(); + + test.add_immediate_delegation( + "dave", + truncate_reward_amount(cumulative_del_reward).u128(), + mix_id_full_pledge, + ); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); + + // go through few more epochs of rewarding + for _ in 0..500 { + test.skip_to_next_epoch_end(); + let dist1 = test + .reward_with_distribution(mix_id_repledge, test_helpers::performance(100.0)); + let dist2 = test + .reward_with_distribution(mix_id_full_pledge, test_helpers::performance(100.0)); + + assert_eq!(dist1, dist2) + } + } + } + #[test] fn updating_active_set_updates_rewarding_params() { let mut test = TestSetup::new(); diff --git a/contracts/mixnet/src/mixnet_contract_settings/storage.rs b/contracts/mixnet/src/mixnet_contract_settings/storage.rs index 990c5e604d..45037a2ed5 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/storage.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/storage.rs @@ -37,7 +37,6 @@ pub(crate) fn minimum_delegation_stake( .map(|state| state.params.minimum_mixnode_delegation)?) } -#[allow(unused)] pub(crate) fn rewarding_denom(storage: &dyn Storage) -> Result { Ok(CONTRACT_STATE .load(storage) diff --git a/contracts/mixnet/src/mixnodes/helpers.rs b/contracts/mixnet/src/mixnodes/helpers.rs index 8244f67b13..383dbc6224 100644 --- a/contracts/mixnet/src/mixnodes/helpers.rs +++ b/contracts/mixnet/src/mixnodes/helpers.rs @@ -147,7 +147,7 @@ pub(crate) fn cleanup_post_unbond_mixnode_storage( } #[cfg(test)] -mod tests { +pub(crate) mod tests { use super::*; use crate::support::tests::fixtures::{ mix_node_cost_params_fixture, mix_node_fixture, TEST_COIN_DENOM, @@ -155,13 +155,13 @@ mod tests { use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::coin; - const OWNER_EXISTS: &str = "mix-owner-existing"; - const OWNER_UNBONDING: &str = "mix-owner-unbonding"; - const OWNER_UNBONDED: &str = "mix-owner-unbonded"; - const OWNER_UNBONDED_LEFTOVER: &str = "mix-owner-unbonded-leftover"; + pub(crate) const OWNER_EXISTS: &str = "mix-owner-existing"; + pub(crate) const OWNER_UNBONDING: &str = "mix-owner-unbonding"; + pub(crate) const OWNER_UNBONDED: &str = "mix-owner-unbonded"; + pub(crate) const OWNER_UNBONDED_LEFTOVER: &str = "mix-owner-unbonded-leftover"; // create a mixnode that is bonded, unbonded, in the process of unbonding and unbonded with leftover mix rewarding details - fn setup_mix_combinations(test: &mut TestSetup) -> Vec { + pub(crate) fn setup_mix_combinations(test: &mut TestSetup) -> Vec { let mix_id_exists = test.add_dummy_mixnode(OWNER_EXISTS, None); let mix_id_unbonding = test.add_dummy_mixnode(OWNER_UNBONDING, None); let mix_id_unbonded = test.add_dummy_mixnode(OWNER_UNBONDED, None); diff --git a/contracts/mixnet/src/mixnodes/transactions.rs b/contracts/mixnet/src/mixnodes/transactions.rs index 0a1e816ffd..92fa131a9e 100644 --- a/contracts/mixnet/src/mixnodes/transactions.rs +++ b/contracts/mixnet/src/mixnodes/transactions.rs @@ -5,16 +5,20 @@ use super::storage; use crate::interval::storage as interval_storage; use crate::interval::storage::push_new_interval_event; use crate::mixnet_contract_settings::storage as mixnet_params_storage; -use crate::mixnodes::helpers::{must_get_mixnode_bond_by_owner, save_new_mixnode}; +use crate::mixnet_contract_settings::storage::rewarding_denom; +use crate::mixnodes::helpers::{ + get_mixnode_details_by_owner, must_get_mixnode_bond_by_owner, save_new_mixnode, +}; use crate::support::helpers::{ ensure_bonded, ensure_no_existing_bond, ensure_proxy_match, validate_node_identity_signature, validate_pledge, }; -use cosmwasm_std::{Addr, Coin, DepsMut, Env, MessageInfo, Response}; +use cosmwasm_std::{coin, Addr, Coin, DepsMut, Env, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ new_mixnode_bonding_event, new_mixnode_config_update_event, new_mixnode_pending_cost_params_update_event, new_pending_mixnode_unbonding_event, + new_pending_pledge_increase_event, }; use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; use mixnet_contract_common::pending_events::{PendingEpochEventKind, PendingIntervalEventKind}; @@ -117,6 +121,54 @@ fn _try_add_mixnode( ))) } +pub fn try_increase_pledge( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, +) -> Result { + _try_increase_pledge(deps, env, info.funds, info.sender, None) +} + +pub fn try_increase_pledge_on_behalf( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + owner: String, +) -> Result { + let proxy = info.sender; + let owner = deps.api.addr_validate(&owner)?; + _try_increase_pledge(deps, env, info.funds, owner, Some(proxy)) +} + +pub fn _try_increase_pledge( + deps: DepsMut<'_>, + env: Env, + increase: Vec, + owner: Addr, + proxy: Option, +) -> Result { + let mix_details = get_mixnode_details_by_owner(deps.storage, owner.clone())? + .ok_or(MixnetContractError::NoAssociatedMixNodeBond { owner })?; + let mix_id = mix_details.mix_id(); + + ensure_proxy_match(&proxy, &mix_details.bond_information.proxy)?; + ensure_bonded(&mix_details.bond_information)?; + + let rewarding_denom = rewarding_denom(deps.storage)?; + let pledge_increase = validate_pledge(increase, coin(1, rewarding_denom))?; + + let cosmos_event = new_pending_pledge_increase_event(mix_id, &pledge_increase); + + // push the event to execute it at the end of the epoch + let epoch_event = PendingEpochEventKind::PledgeMore { + mix_id, + amount: pledge_increase, + }; + interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?; + + Ok(Response::new().add_event(cosmos_event)) +} + pub fn try_remove_mixnode_on_behalf( deps: DepsMut<'_>, env: Env, @@ -680,4 +732,187 @@ pub mod tests { ) .is_err()); } + + #[cfg(test)] + mod increasing_mixnode_pledge { + use super::*; + use crate::mixnodes::helpers::tests::{ + setup_mix_combinations, OWNER_UNBONDED, OWNER_UNBONDED_LEFTOVER, OWNER_UNBONDING, + }; + use crate::support::tests::test_helpers::TestSetup; + + #[test] + fn is_not_allowed_if_account_doesnt_own_mixnode() { + let mut test = TestSetup::new(); + let env = test.env(); + let sender = mock_info("not-mix-owner", &[]); + + let res = try_increase_pledge(test.deps_mut(), env, sender); + assert_eq!( + res, + Err(MixnetContractError::NoAssociatedMixNodeBond { + owner: Addr::unchecked("not-mix-owner") + }) + ) + } + + #[test] + fn is_not_allowed_if_theres_proxy_mismatch() { + let mut test = TestSetup::new(); + let env = test.env(); + + let owner_without_proxy = Addr::unchecked("no-proxy"); + let owner_with_proxy = Addr::unchecked("with-proxy"); + let proxy = Addr::unchecked("proxy"); + let wrong_proxy = Addr::unchecked("unrelated-proxy"); + + test.add_dummy_mixnode(owner_without_proxy.as_str(), None); + test.add_dummy_mixnode_with_proxy(owner_with_proxy.as_str(), None, proxy.clone()); + + let res = _try_increase_pledge( + test.deps_mut(), + env.clone(), + Vec::new(), + owner_without_proxy.clone(), + Some(proxy), + ); + assert_eq!( + res, + Err(MixnetContractError::ProxyMismatch { + existing: "None".to_string(), + incoming: "proxy".to_string() + }) + ); + + let res = _try_increase_pledge( + test.deps_mut(), + env.clone(), + Vec::new(), + owner_with_proxy.clone(), + None, + ); + assert_eq!( + res, + Err(MixnetContractError::ProxyMismatch { + existing: "proxy".to_string(), + incoming: "None".to_string() + }) + ); + + let res = _try_increase_pledge( + test.deps_mut(), + env, + Vec::new(), + owner_with_proxy.clone(), + Some(wrong_proxy), + ); + assert_eq!( + res, + Err(MixnetContractError::ProxyMismatch { + existing: "proxy".to_string(), + incoming: "unrelated-proxy".to_string() + }) + ) + } + + #[test] + fn is_not_allowed_if_mixnode_has_unbonded_or_is_unbonding() { + let mut test = TestSetup::new(); + let env = test.env(); + + // TODO: I dislike this cross-test access, but it provides us with exactly what we need + // perhaps it should be refactored a bit? + let owner_unbonding = Addr::unchecked(OWNER_UNBONDING); + let owner_unbonded = Addr::unchecked(OWNER_UNBONDED); + let owner_unbonded_leftover = Addr::unchecked(OWNER_UNBONDED_LEFTOVER); + + let ids = setup_mix_combinations(&mut test); + let mix_id_unbonding = ids[1]; + + let res = try_increase_pledge( + test.deps_mut(), + env.clone(), + mock_info(owner_unbonding.as_str(), &[]), + ); + assert_eq!( + res, + Err(MixnetContractError::MixnodeIsUnbonding { + mix_id: mix_id_unbonding + }) + ); + + // if the nodes are gone we treat them as tey never existed in the first place + // (regardless of if there's some leftover data) + let res = try_increase_pledge( + test.deps_mut(), + env.clone(), + mock_info(owner_unbonded_leftover.as_str(), &[]), + ); + assert_eq!( + res, + Err(MixnetContractError::NoAssociatedMixNodeBond { + owner: owner_unbonded_leftover + }) + ); + + let res = try_increase_pledge( + test.deps_mut(), + env, + mock_info(owner_unbonded.as_str(), &[]), + ); + assert_eq!( + res, + Err(MixnetContractError::NoAssociatedMixNodeBond { + owner: owner_unbonded + }) + ) + } + + #[test] + fn is_not_allowed_if_no_tokens_were_sent() { + let mut test = TestSetup::new(); + let env = test.env(); + let owner = "mix-owner"; + + test.add_dummy_mixnode(owner, None); + + let sender_empty = mock_info(owner, &[]); + let res = try_increase_pledge(test.deps_mut(), env.clone(), sender_empty); + assert_eq!(res, Err(MixnetContractError::NoBondFound)); + + let sender_zero = mock_info(owner, &[test.coin(0)]); + let res = try_increase_pledge(test.deps_mut(), env, sender_zero); + assert_eq!( + res, + Err(MixnetContractError::InsufficientPledge { + received: test.coin(0), + minimum: test.coin(1) + }) + ) + } + + #[test] + fn with_valid_information_creates_pending_event() { + let mut test = TestSetup::new(); + let env = test.env(); + let owner = "mix-owner"; + let mix_id = test.add_dummy_mixnode(owner, None); + + let events = test.pending_epoch_events(); + assert!(events.is_empty()); + + let sender = mock_info(owner, &[test.coin(1000)]); + try_increase_pledge(test.deps_mut(), env, sender).unwrap(); + + let events = test.pending_epoch_events(); + + assert_eq!( + events[0].kind, + PendingEpochEventKind::PledgeMore { + mix_id, + amount: test.coin(1000) + } + ); + } + } } diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index a7cf4a3875..3d17b324df 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -28,6 +28,9 @@ pub mod test_helpers { use crate::mixnodes::transactions::{ try_add_mixnode, try_add_mixnode_on_behalf, try_remove_mixnode, }; + use crate::rewards::queries::{ + query_pending_delegator_reward, query_pending_mixnode_operator_reward, + }; use crate::rewards::storage as rewards_storage; use crate::rewards::transactions::try_reward_mixnode; use crate::support::tests; @@ -37,7 +40,7 @@ pub mod test_helpers { use cosmwasm_std::testing::mock_info; use cosmwasm_std::testing::MockApi; use cosmwasm_std::testing::MockQuerier; - use cosmwasm_std::{Addr, BankMsg, CosmosMsg, Storage}; + use cosmwasm_std::{coin, Addr, BankMsg, CosmosMsg, Storage}; use cosmwasm_std::{Coin, Order}; use cosmwasm_std::{Decimal, Empty, MemoryStorage}; use cosmwasm_std::{Deps, OwnedDeps}; @@ -138,6 +141,10 @@ pub mod test_helpers { .vesting_contract_address } + pub fn coin(&self, amount: u128) -> Coin { + coin(amount, rewarding_denom(self.deps().storage).unwrap()) + } + pub fn current_interval(&self) -> Interval { interval_storage::current_interval(self.deps().storage).unwrap() } @@ -308,6 +315,22 @@ pub mod test_helpers { .unwrap(); } + #[allow(unused)] + pub fn pending_operator_reward(&mut self, mix: MixId) -> Decimal { + query_pending_mixnode_operator_reward(self.deps(), mix) + .unwrap() + .amount_earned_detailed + .expect("no reward!") + } + + #[allow(unused)] + pub fn pending_delegator_reward(&mut self, delegator: &str, target: MixId) -> Decimal { + query_pending_delegator_reward(self.deps(), delegator.into(), target, None) + .unwrap() + .amount_earned_detailed + .expect("no reward!") + } + pub fn skip_to_next_epoch_end(&mut self) { self.skip_to_next_epoch(); self.skip_to_current_epoch_end(); @@ -337,8 +360,19 @@ pub mod test_helpers { self.env.block.time = Timestamp::from_seconds(epoch_end as u64 + 1); let advanced = interval.advance_epoch(); - if interval.current_epoch_id() != interval.epochs_in_interval() { + assert_eq!( + interval.current_epoch_absolute_id() + 1, + advanced.current_epoch_absolute_id() + ); + + if interval.current_epoch_id() != interval.epochs_in_interval() - 1 { assert_eq!(interval.current_epoch_id() + 1, advanced.current_epoch_id()) + } else { + assert_eq!(advanced.current_epoch_id(), 0); + assert_eq!( + interval.current_interval_id() + 1, + advanced.current_interval_id() + ) } interval_storage::save_interval(self.deps_mut().storage, &advanced).unwrap() diff --git a/contracts/vesting/src/contract.rs b/contracts/vesting/src/contract.rs index 5efb3d750a..90a72d2ae8 100644 --- a/contracts/vesting/src/contract.rs +++ b/contracts/vesting/src/contract.rs @@ -121,6 +121,7 @@ pub fn execute( env, deps, ), + ExecuteMsg::PledgeMore { amount } => try_pledge_more(deps, env, info, amount), ExecuteMsg::UnbondMixnode {} => try_unbond_mixnode(info, deps), ExecuteMsg::TrackUnbondMixnode { owner, amount } => { try_track_unbond_mixnode(&owner, amount, info, deps) @@ -331,6 +332,19 @@ pub fn try_bond_mixnode( ) } +pub fn try_pledge_more( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + amount: Coin, +) -> Result { + let mix_denom = MIX_DENOM.load(deps.storage)?; + let additional_pledge = validate_funds(&[amount], mix_denom)?; + + let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; + account.try_pledge_additional_tokens(additional_pledge, &env, deps.storage) +} + /// Unbond a mixnode, sends [mixnet_contract_common::ExecuteMsg::UnbondMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. pub fn try_unbond_mixnode(info: MessageInfo, deps: DepsMut<'_>) -> Result { let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; diff --git a/contracts/vesting/src/traits/bonding_account.rs b/contracts/vesting/src/traits/bonding_account.rs index 36c248e393..491378d630 100644 --- a/contracts/vesting/src/traits/bonding_account.rs +++ b/contracts/vesting/src/traits/bonding_account.rs @@ -18,6 +18,13 @@ pub trait MixnodeBondingAccount { storage: &mut dyn Storage, ) -> Result; + fn try_pledge_additional_tokens( + &self, + additional_pledge: Coin, + env: &Env, + storage: &mut dyn Storage, + ) -> Result; + fn try_unbond_mixnode(&self, storage: &dyn Storage) -> Result; fn try_track_unbond_mixnode( diff --git a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs index e3aec82584..74917a16c7 100644 --- a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs +++ b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs @@ -12,7 +12,8 @@ use mixnet_contract_common::mixnode::MixNodeCostParams; use mixnet_contract_common::{ExecuteMsg as MixnetExecuteMsg, MixNode}; use vesting_contract_common::events::{ new_vesting_mixnode_bonding_event, new_vesting_mixnode_unbonding_event, - new_vesting_update_mixnode_config_event, new_vesting_update_mixnode_cost_params_event, + new_vesting_pledge_more_event, new_vesting_update_mixnode_config_event, + new_vesting_update_mixnode_cost_params_event, }; use vesting_contract_common::PledgeData; @@ -84,6 +85,64 @@ impl MixnodeBondingAccount for Account { .add_event(new_vesting_mixnode_bonding_event())) } + fn try_pledge_additional_tokens( + &self, + additional_pledge: Coin, + env: &Env, + storage: &mut dyn Storage, + ) -> Result { + let current_balance = self.load_balance(storage)?; + let total_pledged_after = + self.total_pledged_locked(storage, env)? + additional_pledge.amount; + let locked_pledge_cap = self.absolute_pledge_cap()?; + + if locked_pledge_cap < total_pledged_after { + return Err(ContractError::LockedPledgeCapReached { + current: total_pledged_after, + cap: locked_pledge_cap, + }); + } + + if current_balance < additional_pledge.amount { + return Err(ContractError::InsufficientBalance( + self.owner_address().as_str().to_string(), + current_balance.u128(), + )); + } + + let mut pledge_data = if let Some(pledge_data) = self.load_mixnode_pledge(storage)? { + pledge_data + } else { + return Err(ContractError::NoBondFound( + self.owner_address().as_str().to_string(), + )); + }; + + // need a second pair of eyes here to make sure updating existing timestamp on pledge data + // is not going to have some unexpected consequences + pledge_data.amount.amount += additional_pledge.amount; + pledge_data.block_time = env.block.time; + + let msg = MixnetExecuteMsg::PledgeMoreOnBehalf { + owner: self.owner_address().into_string(), + }; + + let new_balance = Uint128::new(current_balance.u128() - additional_pledge.amount.u128()); + + let pledge_more_mag = wasm_execute( + MIXNET_CONTRACT_ADDRESS.load(storage)?, + &msg, + vec![additional_pledge], + )?; + + self.save_balance(new_balance, storage)?; + self.save_mixnode_pledge(pledge_data, storage)?; + + Ok(Response::new() + .add_message(pledge_more_mag) + .add_event(new_vesting_pledge_more_event())) + } + fn try_unbond_mixnode(&self, storage: &dyn Storage) -> Result { let msg = MixnetExecuteMsg::UnbondMixnodeOnBehalf { owner: self.owner_address().into_string(), diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index 9bd44028c2..125be1cb8c 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -55,6 +55,7 @@ fn main() { mixnet::admin::update_contract_settings, mixnet::bond::bond_gateway, mixnet::bond::bond_mixnode, + mixnet::bond::pledge_more, mixnet::bond::gateway_bond_details, mixnet::bond::get_pending_operator_rewards, mixnet::bond::mixnode_bond_details, @@ -105,6 +106,7 @@ fn main() { vesting::rewards::vesting_claim_operator_reward, vesting::bond::vesting_bond_gateway, vesting::bond::vesting_bond_mixnode, + vesting::bond::vesting_pledge_more, vesting::bond::vesting_unbond_gateway, vesting::bond::vesting_unbond_mixnode, vesting::bond::vesting_update_mixnode_cost_params, @@ -130,6 +132,7 @@ fn main() { simulate::mixnet::simulate_bond_gateway, simulate::mixnet::simulate_unbond_gateway, simulate::mixnet::simulate_bond_mixnode, + simulate::mixnet::simulate_pledge_more, simulate::mixnet::simulate_unbond_mixnode, simulate::mixnet::simulate_update_mixnode_config, simulate::mixnet::simulate_update_mixnode_cost_params, @@ -140,6 +143,7 @@ fn main() { simulate::vesting::simulate_vesting_bond_gateway, simulate::vesting::simulate_vesting_unbond_gateway, simulate::vesting::simulate_vesting_bond_mixnode, + simulate::vesting::simulate_vesting_pledge_more, simulate::vesting::simulate_vesting_unbond_mixnode, simulate::vesting::simulate_vesting_update_mixnode_config, simulate::vesting::simulate_vesting_update_mixnode_cost_params, diff --git a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs index 7098577545..567dc84290 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs @@ -102,6 +102,33 @@ pub async fn bond_mixnode( )?) } +#[tauri::command] +pub async fn pledge_more( + fee: Option, + additional_pledge: DecCoin, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.read().await; + let additional_pledge_base = guard.attempt_convert_to_base_coin(additional_pledge.clone())?; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + log::info!( + ">>> Pledge more, additional_pledge_display = {}, additional_pledge_base = {}, fee = {:?}", + additional_pledge, + additional_pledge_base, + fee, + ); + let res = guard + .current_client()? + .nymd + .pledge_more(additional_pledge_base, fee) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, fee_amount, + )?) +} + #[tauri::command] pub async fn unbond_mixnode( fee: Option, diff --git a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs index 51912e1332..33e649690b 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs @@ -84,6 +84,14 @@ pub async fn simulate_bond_mixnode( .await } +#[tauri::command] +pub async fn simulate_pledge_more( + additional_pledge: DecCoin, + state: tauri::State<'_, WalletState>, +) -> Result { + simulate_mixnet_operation(ExecuteMsg::PledgeMore {}, Some(additional_pledge), &state).await +} + #[tauri::command] pub async fn simulate_unbond_mixnode( state: tauri::State<'_, WalletState>, diff --git a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs index c7cebf2ce5..213c8d3fd8 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs @@ -91,6 +91,19 @@ pub async fn simulate_vesting_bond_mixnode( .await } +#[tauri::command] +pub async fn simulate_vesting_pledge_more( + additional_pledge: DecCoin, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.read().await; + let amount = guard + .attempt_convert_to_base_coin(additional_pledge)? + .into(); + + simulate_vesting_operation(ExecuteMsg::PledgeMore { amount }, None, &state).await +} + #[tauri::command] pub async fn simulate_vesting_unbond_mixnode( state: tauri::State<'_, WalletState>, diff --git a/nym-wallet/src-tauri/src/operations/vesting/bond.rs b/nym-wallet/src-tauri/src/operations/vesting/bond.rs index 99f0086d9c..da5a71eee1 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/bond.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/bond.rs @@ -93,6 +93,33 @@ pub async fn vesting_bond_mixnode( )?) } +#[tauri::command] +pub async fn vesting_pledge_more( + fee: Option, + additional_pledge: DecCoin, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.read().await; + let additional_pledge_base = guard.attempt_convert_to_base_coin(additional_pledge.clone())?; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + log::info!( + ">>> Pledge more with locked tokens, additional_pledge_display = {}, additional_pledge_base = {}, fee = {:?}", + additional_pledge, + additional_pledge_base, + fee, + ); + let res = guard + .current_client()? + .nymd + .vesting_pledge_more(additional_pledge_base, fee) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, fee_amount, + )?) +} + #[tauri::command] pub async fn vesting_unbond_mixnode( fee: Option,