From 796f5a678a74cd267d3bf1ede563b95fa320c394 Mon Sep 17 00:00:00 2001 From: Pierre Dommerc Date: Wed, 26 Apr 2023 15:29:39 +0200 Subject: [PATCH] feat(wallet): update bond amount (#3338) --- nym-wallet/src-tauri/src/error.rs | 6 + nym-wallet/src-tauri/src/main.rs | 4 + .../src-tauri/src/operations/mixnet/bond.rs | 50 ++++++ .../src/operations/simulate/mixnet.rs | 44 +++++ .../src/operations/simulate/vesting.rs | 55 ++++++ .../src-tauri/src/operations/vesting/bond.rs | 55 ++++++ nym-wallet/src-tauri/src/state.rs | 28 ++++ .../Bonding/BondedMixnodeActions.tsx | 12 +- .../Bonding/modals/BondMoreModal.tsx | 116 ------------- .../Bonding/modals/BondOversaturatedModal.tsx | 4 +- .../Bonding/modals/UpdateBondAmountModal.tsx | 157 ++++++++++++++++++ nym-wallet/src/context/bonding.tsx | 36 ++-- nym-wallet/src/context/main.tsx | 19 +++ nym-wallet/src/context/mocks/bonding.tsx | 4 +- nym-wallet/src/context/mocks/main.tsx | 1 + nym-wallet/src/pages/bonding/Bonding.tsx | 139 ++++++++-------- nym-wallet/src/requests/actions.ts | 9 +- nym-wallet/src/requests/simulate.ts | 9 +- nym-wallet/src/requests/vesting.ts | 15 +- nym-wallet/src/types/global.ts | 10 +- 20 files changed, 541 insertions(+), 232 deletions(-) delete mode 100644 nym-wallet/src/components/Bonding/modals/BondMoreModal.tsx create mode 100644 nym-wallet/src/components/Bonding/modals/UpdateBondAmountModal.tsx diff --git a/nym-wallet/src-tauri/src/error.rs b/nym-wallet/src-tauri/src/error.rs index c639a39aa5..5426698b95 100644 --- a/nym-wallet/src-tauri/src/error.rs +++ b/nym-wallet/src-tauri/src/error.rs @@ -94,6 +94,12 @@ pub enum BackendError { WalletFileAlreadyExists, #[error("The wallet file is not found")] WalletFileNotFound, + #[error("Invalid update pledge request, the new bond amount is the same as the current one")] + WalletPledgeUpdateNoOp, + #[error( + "Invalid update pledge request, the new bond is a different currency from the current one" + )] + WalletPledgeUpdateInvalidCurrency, #[error("The wallet file has a malformed name")] WalletFileMalformedFilename, #[error("Unable to archive wallet file")] diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index 76e96494f7..e0842fa096 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -58,6 +58,7 @@ fn main() { mixnet::admin::update_contract_settings, mixnet::bond::bond_gateway, mixnet::bond::bond_mixnode, + mixnet::bond::update_pledge, mixnet::bond::pledge_more, mixnet::bond::decrease_pledge, mixnet::bond::gateway_bond_details, @@ -114,6 +115,7 @@ fn main() { vesting::rewards::vesting_claim_operator_reward, vesting::bond::vesting_bond_gateway, vesting::bond::vesting_bond_mixnode, + vesting::bond::vesting_update_pledge, vesting::bond::vesting_pledge_more, vesting::bond::vesting_decrease_pledge, vesting::bond::vesting_unbond_gateway, @@ -152,6 +154,7 @@ fn main() { simulate::mixnet::simulate_bond_gateway, simulate::mixnet::simulate_unbond_gateway, simulate::mixnet::simulate_bond_mixnode, + simulate::mixnet::simulate_update_pledge, simulate::mixnet::simulate_pledge_more, simulate::mixnet::simulate_unbond_mixnode, simulate::mixnet::simulate_update_mixnode_config, @@ -164,6 +167,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_update_pledge, simulate::vesting::simulate_vesting_pledge_more, simulate::vesting::simulate_vesting_unbond_mixnode, simulate::vesting::simulate_vesting_update_mixnode_config, diff --git a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs index dd29b76610..4acd1aecfc 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs @@ -17,6 +17,7 @@ use nym_types::transaction::TransactionExecuteResult; use nym_validator_client::nyxd::traits::{MixnetQueryClient, MixnetSigningClient}; use nym_validator_client::nyxd::Fee; use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; use std::time::Duration; #[derive(Debug, Serialize, Deserialize)] @@ -133,6 +134,55 @@ pub async fn bond_mixnode( )?) } +#[tauri::command] +pub async fn update_pledge( + current_pledge: DecCoin, + new_pledge: DecCoin, + fee: Option, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.read().await; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + let dec_delta = guard.calculate_coin_delta(¤t_pledge, &new_pledge)?; + let delta = guard.attempt_convert_to_base_coin(dec_delta.clone())?; + log::info!( + ">>> Pledge update, current pledge {}, new pledge {}", + ¤t_pledge, + &new_pledge, + ); + + let res = match new_pledge.amount.cmp(¤t_pledge.amount) { + Ordering::Greater => { + log::info!( + "Pledge increase, calculated additional pledge {}, fee = {:?}", + &dec_delta, + fee, + ); + guard.current_client()?.nyxd.pledge_more(delta, fee).await? + } + Ordering::Less => { + log::info!( + "Pledge reduction, calculated reduction pledge {}, fee = {:?}", + &dec_delta, + fee, + ); + guard + .current_client()? + .nyxd + .decrease_pledge(delta, fee) + .await? + } + Ordering::Equal => return Err(BackendError::WalletPledgeUpdateNoOp), + }; + + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + + Ok(TransactionExecuteResult::from_execute_result( + res, fee_amount, + )?) +} + #[tauri::command] pub async fn pledge_more( 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 fbf0c9a225..0f12ff80c4 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs @@ -1,6 +1,8 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::cmp::Ordering; + use crate::error::BackendError; use crate::operations::simulate::FeeDetails; use crate::WalletState; @@ -93,6 +95,48 @@ pub async fn simulate_pledge_more( simulate_mixnet_operation(ExecuteMsg::PledgeMore {}, Some(additional_pledge), &state).await } +#[tauri::command] +pub async fn simulate_update_pledge( + current_pledge: DecCoin, + new_pledge: DecCoin, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.read().await; + let dec_delta = guard.calculate_coin_delta(¤t_pledge, &new_pledge)?; + log::info!( + ">>> Simulate pledge update, current pledge {}, new pledge {}", + ¤t_pledge, + &new_pledge, + ); + + match new_pledge.amount.cmp(¤t_pledge.amount) { + Ordering::Greater => { + log::info!( + "Simulate pledge increase, calculated additional pledge {}", + dec_delta, + ); + simulate_mixnet_operation(ExecuteMsg::PledgeMore {}, Some(dec_delta), &state).await + } + Ordering::Less => { + log::info!( + "Simulate pledge reduction, calculated reduction pledge {}", + dec_delta, + ); + simulate_mixnet_operation( + ExecuteMsg::DecreasePledge { + decrease_by: guard + .attempt_convert_to_base_coin(dec_delta.clone())? + .into(), + }, + Some(dec_delta), + &state, + ) + .await + } + Ordering::Equal => Err(BackendError::WalletPledgeUpdateNoOp), + } +} + #[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 b7e5cb2648..79e40ec792 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs @@ -1,6 +1,8 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::cmp::Ordering; + use crate::error::BackendError; use crate::operations::simulate::FeeDetails; use crate::WalletState; @@ -92,6 +94,59 @@ pub async fn simulate_vesting_bond_mixnode( .await } +#[tauri::command] +pub async fn simulate_vesting_update_pledge( + current_pledge: DecCoin, + new_pledge: DecCoin, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.read().await; + + match new_pledge.amount.cmp(¤t_pledge.amount) { + Ordering::Greater => { + let additional_pledge = guard + .attempt_convert_to_base_coin(DecCoin { + amount: new_pledge.amount - current_pledge.amount, + denom: current_pledge.denom, + })? + .into(); + log::info!( + ">>> Simulate pledge more, calculated additional pledge {}", + additional_pledge, + ); + simulate_vesting_operation( + ExecuteMsg::PledgeMore { + amount: additional_pledge, + }, + None, + &state, + ) + .await + } + Ordering::Less => { + let decrease_pledge = guard + .attempt_convert_to_base_coin(DecCoin { + amount: current_pledge.amount - new_pledge.amount, + denom: current_pledge.denom, + })? + .into(); + log::info!( + ">>> Simulate decrease pledge, calculated decrease pledge {}", + decrease_pledge, + ); + simulate_vesting_operation( + ExecuteMsg::DecreasePledge { + amount: decrease_pledge, + }, + None, + &state, + ) + .await + } + Ordering::Equal => Err(BackendError::WalletPledgeUpdateNoOp), + } +} + #[tauri::command] pub async fn simulate_vesting_pledge_more( additional_pledge: DecCoin, diff --git a/nym-wallet/src-tauri/src/operations/vesting/bond.rs b/nym-wallet/src-tauri/src/operations/vesting/bond.rs index 53571f91f8..38583a877b 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/bond.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/bond.rs @@ -1,3 +1,5 @@ +use std::cmp::Ordering; + use crate::error::BackendError; use crate::nyxd_client; use crate::state::WalletState; @@ -124,6 +126,59 @@ pub async fn vesting_bond_mixnode( )?) } +#[tauri::command] +pub async fn vesting_update_pledge( + current_pledge: DecCoin, + new_pledge: DecCoin, + fee: Option, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.read().await; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + let dec_delta = guard.calculate_coin_delta(¤t_pledge, &new_pledge)?; + let delta = guard.attempt_convert_to_base_coin(dec_delta.clone())?; + log::info!( + ">>> Pledge update, current pledge {}, new pledge {}", + ¤t_pledge, + &new_pledge, + ); + + let res = match new_pledge.amount.cmp(¤t_pledge.amount) { + Ordering::Greater => { + log::info!( + "Pledge increase with locked tokens, calculated additional pledge {}, fee = {:?}", + dec_delta, + fee, + ); + guard + .current_client()? + .nyxd + .vesting_pledge_more(delta, fee) + .await? + } + Ordering::Less => { + log::info!( + "Pledge reduction with locked tokens, calculated reduction pledge {}, fee = {:?}", + dec_delta, + fee, + ); + guard + .current_client()? + .nyxd + .vesting_decrease_pledge(delta, fee) + .await? + } + Ordering::Equal => return Err(BackendError::WalletPledgeUpdateNoOp), + }; + + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + + Ok(TransactionExecuteResult::from_execute_result( + res, fee_amount, + )?) +} + #[tauri::command] pub async fn vesting_pledge_more( fee: Option, diff --git a/nym-wallet/src-tauri/src/state.rs b/nym-wallet/src-tauri/src/state.rs index d942356d0a..c10bbf486f 100644 --- a/nym-wallet/src-tauri/src/state.rs +++ b/nym-wallet/src-tauri/src/state.rs @@ -460,6 +460,34 @@ impl WalletStateInner { pub fn remove_validator_url(&mut self, url: config::ValidatorConfigEntry, network: Network) { self.config.remove_validator_url(url, network) } + + pub fn calculate_coin_delta( + &self, + coin1: &DecCoin, + coin2: &DecCoin, + ) -> Result { + if coin1.denom != coin2.denom { + return Err(BackendError::WalletPledgeUpdateInvalidCurrency); + } + + match coin1.amount.cmp(&coin2.amount) { + std::cmp::Ordering::Greater => { + let delta = DecCoin { + amount: coin1.amount - coin2.amount, + denom: coin1.denom.clone(), + }; + Ok(delta) + } + std::cmp::Ordering::Less => { + let delta = DecCoin { + amount: coin2.amount - coin1.amount, + denom: coin1.denom.clone(), + }; + Ok(delta) + } + std::cmp::Ordering::Equal => Ok(coin1.to_owned()), + } + } } async fn fetch_status_for_urls( diff --git a/nym-wallet/src/components/Bonding/BondedMixnodeActions.tsx b/nym-wallet/src/components/Bonding/BondedMixnodeActions.tsx index 2a7d6e6bf4..1991e29cdf 100644 --- a/nym-wallet/src/components/Bonding/BondedMixnodeActions.tsx +++ b/nym-wallet/src/components/Bonding/BondedMixnodeActions.tsx @@ -3,16 +3,16 @@ import { Typography } from '@mui/material'; import { ActionsMenu, ActionsMenuItem } from 'src/components/ActionsMenu'; import { Unbond as UnbondIcon, Bond as BondIcon } from '../../svg-icons'; -export type TBondedMixnodeActions = 'nodeSettings' | 'bondMore' | 'unbond' | 'redeem'; +export type TBondedMixnodeActions = 'nodeSettings' | 'updateBond' | 'unbond' | 'redeem'; export const BondedMixnodeActions = ({ onActionSelect, disabledRedeemAndCompound, - disabledBondMore, + disabledUpdateBond, }: { onActionSelect: (action: TBondedMixnodeActions) => void; disabledRedeemAndCompound: boolean; - disabledBondMore?: boolean; + disabledUpdateBond?: boolean; }) => { const [isOpen, setIsOpen] = useState(false); @@ -26,11 +26,11 @@ export const BondedMixnodeActions = ({ return ( - {!disabledBondMore && ( + {!disabledUpdateBond && ( } - onClick={() => handleActionClick('bondMore')} + onClick={() => handleActionClick('updateBond')} /> )} Promise; - onClose: () => void; - onError: (e: string) => void; -}) => { - const { bond: currentBond, proxy } = node; - const { fee, getFee, resetFeeState, feeError } = useGetFee(); - const [additionalBond, setAdditionalBond] = useState({ amount: '0', denom: currentBond.denom }); - const [errorAmount, setErrorAmount] = useState(false); - - useEffect(() => { - if (feeError) { - onError(feeError); - } - }, [feeError]); - - const handleConfirm = async () => { - const data = { additionalPledge: additionalBond }; - const tokenPool = proxy ? 'locked' : 'balance'; - await onBondMore(data, tokenPool); - }; - - const handleAmountChanged = async (value: DecCoin) => { - setAdditionalBond(value); - const { amount } = value; - - if (!amount) { - setErrorAmount(true); - } else { - const validAmount = await validateAmount(amount, '1'); - if (!validAmount) { - setErrorAmount(true); - return; - } - setErrorAmount(false); - } - }; - - const handleOnOk = async () => { - if (!proxy) { - await getFee(simulateBondMore, { additionalPledge: additionalBond }); - } else { - await getFee(simulateVestingBondMore, { additionalPledge: additionalBond }); - } - }; - - if (fee) - return ( - - - - - ); - - return ( - - - - { - handleAmountChanged(value); - }} - fullWidth - validationError={errorAmount ? 'Please enter a valid amount' : undefined} - /> - - - - - - - - - - ); -}; diff --git a/nym-wallet/src/components/Bonding/modals/BondOversaturatedModal.tsx b/nym-wallet/src/components/Bonding/modals/BondOversaturatedModal.tsx index 86d6b990b9..9a20e0afaa 100644 --- a/nym-wallet/src/components/Bonding/modals/BondOversaturatedModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/BondOversaturatedModal.tsx @@ -12,8 +12,8 @@ export const BondOversaturatedModal: FCWithChildren<{ open={open} onClose={onClose} onOk={async () => onContinue?.()} - header="Bond More" - okLabel="Bond More" + header="Change bond amount" + okLabel="Change bond" buttonFullWidth > diff --git a/nym-wallet/src/components/Bonding/modals/UpdateBondAmountModal.tsx b/nym-wallet/src/components/Bonding/modals/UpdateBondAmountModal.tsx new file mode 100644 index 0000000000..552f2b1f4c --- /dev/null +++ b/nym-wallet/src/components/Bonding/modals/UpdateBondAmountModal.tsx @@ -0,0 +1,157 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { Box, Stack } from '@mui/material'; +import Big from 'big.js'; +import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; +import { ModalListItem } from 'src/components/Modals/ModalListItem'; +import { SimpleModal } from 'src/components/Modals/SimpleModal'; +import { DecCoin } from '@nymproject/types'; +import { ConfirmTx } from 'src/components/ConfirmTX'; +import { useGetFee } from 'src/hooks/useGetFee'; +import { decCoinToDisplay, validateAmount } from 'src/utils'; +import { simulateUpdateBond, simulateVestingUpdateBond } from 'src/requests'; +import { TSimulateUpdateBondArgs, TUpdateBondArgs } from 'src/types'; +import { AppContext, TBondedMixnode } from 'src/context'; +import { TPoolOption } from '../../TokenPoolSelector'; + +export const UpdateBondAmountModal = ({ + node, + onUpdateBond, + onClose, + onError, +}: { + node: TBondedMixnode; + onUpdateBond: (data: TUpdateBondArgs, tokenPool: TPoolOption) => Promise; + onClose: () => void; + onError: (e: string) => void; +}) => { + const { bond: currentBond, proxy, stakeSaturation, uncappedStakeSaturation } = node; + + const { fee, getFee, resetFeeState, feeError } = useGetFee(); + const [newBond, setNewBond] = useState(); + const [errorAmount, setErrorAmount] = useState(false); + + const { printBalance, printVestedBalance } = useContext(AppContext); + + useEffect(() => { + if (feeError) { + onError(feeError); + } + }, [feeError]); + + const handleConfirm = async () => { + if (!newBond) { + return; + } + const tokenPool = proxy ? 'locked' : 'balance'; + await onUpdateBond( + { + currentPledge: currentBond, + newPledge: newBond, + fee: fee?.fee, + }, + tokenPool, + ); + }; + + const handleAmountChanged = async (value: DecCoin) => { + const { amount } = value; + setNewBond(value); + if (!amount || !Number(amount)) { + setErrorAmount(true); + } else if (Big(amount).eq(currentBond.amount)) { + setErrorAmount(true); + } else { + const validAmount = await validateAmount(amount, '1'); + if (!validAmount) { + setErrorAmount(true); + return; + } + setErrorAmount(false); + } + }; + + const handleOnOk = async () => { + if (!newBond) { + return; + } + if (!proxy) { + await getFee(simulateUpdateBond, { + currentPledge: currentBond, + newPledge: newBond, + }); + } else { + await getFee(simulateVestingUpdateBond, { + currentPledge: currentBond, + newPledge: newBond, + }); + } + }; + + const newBondToDisplay = () => { + const coin = decCoinToDisplay(newBond as DecCoin); + return `${coin.amount} ${coin.denom}`; + }; + + if (fee) + return ( + + + + + ); + + return ( + + + + { + handleAmountChanged(value); + }} + fullWidth + validationError={errorAmount ? 'Please enter a valid amount' : undefined} + /> + + + + + + {uncappedStakeSaturation ? ( + + ) : ( + + )} + + + + + ); +}; diff --git a/nym-wallet/src/context/bonding.tsx b/nym-wallet/src/context/bonding.tsx index bf2ee2597a..1b7bd23dbc 100644 --- a/nym-wallet/src/context/bonding.tsx +++ b/nym-wallet/src/context/bonding.tsx @@ -6,6 +6,7 @@ import { TransactionExecuteResult, decimalToPercentage, SelectionChance, + decimalToFloatApproximation, } from '@nymproject/types'; import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import Big from 'big.js'; @@ -17,7 +18,7 @@ import { TBondGatewaySignatureArgs, TBondMixNodeArgs, TBondMixnodeSignatureArgs, - TBondMoreArgs, + TUpdateBondArgs, } from 'src/types'; import { Console } from 'src/utils/console'; import { @@ -28,8 +29,6 @@ import { getMixnodeBondDetails, unbondGateway as unbondGatewayRequest, unbondMixNode as unbondMixnodeRequest, - bondMore as bondMoreRequest, - vestingBondMore, vestingBondGateway, vestingBondMixNode, vestingUnbondGateway, @@ -50,6 +49,8 @@ import { generateMixnodeMsgPayload as generateMixnodeMsgPayloadReq, vestingGenerateGatewayMsgPayload as vestingGenerateGatewayMsgPayloadReq, generateGatewayMsgPayload as generateGatewayMsgPayloadReq, + updateBond as updateBondReq, + vestingUpdateBond as vestingUpdateBondReq, } from '../requests'; import { useCheckOwnership } from '../hooks/useCheckOwnership'; import { AppContext } from './main'; @@ -69,6 +70,7 @@ export type TBondedMixnode = { stake: DecCoin; bond: DecCoin; stakeSaturation: string; + uncappedStakeSaturation?: number; profitMargin: string; operatorRewards?: DecCoin; delegators: number; @@ -117,7 +119,7 @@ export type TBondingContext = { bondMixnode: (data: TBondMixNodeArgs, tokenPool: TokenPool) => Promise; bondGateway: (data: TBondGatewayArgs, tokenPool: TokenPool) => Promise; unbond: (fee?: FeeDetails) => Promise; - bondMore: (data: TBondMoreArgs, tokenPool: TokenPool) => Promise; + updateBondAmount: (data: TUpdateBondArgs, tokenPool: TokenPool) => Promise; redeemRewards: (fee?: FeeDetails) => Promise; updateMixnode: (pm: string, fee?: FeeDetails) => Promise; checkOwnership: () => Promise; @@ -138,7 +140,7 @@ export const BondingContext = createContext({ unbond: async () => { throw new Error('Not implemented'); }, - bondMore: async () => { + updateBondAmount: async () => { throw new Error('Not implemented'); }, redeemRewards: async () => { @@ -189,6 +191,7 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen stakeSaturation: string; estimatedRewards?: DecCoin; uptime: number; + uncappedSaturation?: number; } = { status: 'not_found', stakeSaturation: '0', @@ -207,6 +210,11 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen try { const stakeSaturationResponse = await getMixnodeStakeSaturation(mixId); additionalDetails.stakeSaturation = decimalToPercentage(stakeSaturationResponse.saturation); + + const rawUncappedSaturation = decimalToFloatApproximation(stakeSaturationResponse.uncapped_saturation); + if (rawUncappedSaturation && rawUncappedSaturation > 1) { + additionalDetails.uncappedSaturation = Math.round(rawUncappedSaturation * 100); + } } catch (e) { Console.log('getMixnodeStakeSaturation fails', e); } @@ -277,6 +285,7 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen const refresh = useCallback(async () => { setIsLoading(true); + setError(undefined); if (ownership.hasOwnership && ownership.nodeType === EnumNodeType.mixnode && clientDetails) { try { @@ -298,7 +307,13 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen bond_information: { mix_id }, } = data; - const { status, stakeSaturation, estimatedRewards, uptime } = await getAdditionalMixnodeDetails(mix_id); + const { + status, + stakeSaturation, + uncappedSaturation: uncappedStakeSaturation, + estimatedRewards, + uptime, + } = await getAdditionalMixnodeDetails(mix_id); const setProbabilities = await getSetProbabilities(mix_id); const nodeDescription = await getNodeDescription( bond_information.mix_node.host, @@ -322,6 +337,7 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen uptime, status, stakeSaturation, + uncappedStakeSaturation, operatorCost: decCoinToDisplay(rewarding_details.cost_params.interval_operating_cost), host: bond_information.mix_node.host.replace(/\s/g, ''), routingScore, @@ -477,16 +493,16 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen return tx; }; - const bondMore = async (data: TBondMoreArgs, tokenPool: TokenPool) => { + const updateBondAmount = async (data: TUpdateBondArgs, tokenPool: TokenPool) => { let tx: TransactionExecuteResult | undefined; setIsLoading(true); try { if (tokenPool === 'balance') { - tx = await bondMoreRequest(data); + tx = await updateBondReq(data); await userBalance.fetchBalance(); } if (tokenPool === 'locked') { - tx = await vestingBondMore(data); + tx = await vestingUpdateBondReq(data); await userBalance.fetchTokenAllocation(); } @@ -547,7 +563,7 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen updateMixnode, refresh, redeemRewards, - bondMore, + updateBondAmount, checkOwnership, generateMixnodeMsgPayload, generateGatewayMsgPayload, diff --git a/nym-wallet/src/context/main.tsx b/nym-wallet/src/context/main.tsx index 59d389988c..af965b79a0 100644 --- a/nym-wallet/src/context/main.tsx +++ b/nym-wallet/src/context/main.tsx @@ -18,6 +18,7 @@ import { } from '../requests'; import { Console } from '../utils/console'; import { createSignInWindow, getReactState, setReactState } from '../requests/app'; +import { toDisplay } from '../utils'; export const urls = (networkName?: Network) => networkName === 'MAINNET' @@ -64,6 +65,8 @@ export type TAppContext = { signInWithPassword: (password: string) => void; logOut: () => void; keepState: () => Promise; + printBalance: string; + printVestedBalance?: string; // spendable vested token }; interface RustState { @@ -89,6 +92,8 @@ export const AppProvider: FCWithChildren = ({ children }) => { const [isAdminAddress, setIsAdminAddress] = useState(false); const [showSendModal, setShowSendModal] = useState(false); const [showReceiveModal, setShowReceiveModal] = useState(false); + const [printBalance, setPrintBalance] = useState('-'); + const [printVestedBalance, setPrintVestedBalance] = useState(); const userBalance = useGetBalance(clientDetails); const navigate = useNavigate(); @@ -192,6 +197,18 @@ export const AppProvider: FCWithChildren = ({ children }) => { } }, [network]); + useEffect(() => { + const currency = clientDetails?.display_mix_denom.toUpperCase() || 'NYM'; + if (userBalance.originalVesting) { + setPrintVestedBalance(`${toDisplay(userBalance.tokenAllocation?.spendableVestedCoins || 0)} ${currency}`); + } + if (userBalance?.balance?.amount) { + setPrintBalance(`${toDisplay(userBalance.balance.amount.amount)} ${currency}`); + } else { + setPrintBalance(`${toDisplay(0)} ${currency}`); + } + }, [userBalance, clientDetails]); + useEffect(() => { let newValue = false; if (network && appEnv?.ADMIN_ADDRESS && clientDetails?.client_address) { @@ -310,6 +327,8 @@ export const AppProvider: FCWithChildren = ({ children }) => { handleShowSendModal, handleShowReceiveModal, handleSwitchMode, + printBalance, + printVestedBalance, }), [ appVersion, diff --git a/nym-wallet/src/context/mocks/bonding.tsx b/nym-wallet/src/context/mocks/bonding.tsx index 7e72672399..6218e9aa50 100644 --- a/nym-wallet/src/context/mocks/bonding.tsx +++ b/nym-wallet/src/context/mocks/bonding.tsx @@ -154,7 +154,7 @@ export const MockBondingContextProvider = ({ return TxResultMock; }; - const bondMore = async (): Promise => { + const updateBondAmount = async (): Promise => { setIsLoading(true); await mockSleep(SLEEP_MS); triggerStateUpdate(); @@ -202,7 +202,7 @@ export const MockBondingContextProvider = ({ getFee, resetFeeState, updateMixnode, - bondMore, + updateBondAmount, checkOwnership, generateMixnodeMsgPayload, generateGatewayMsgPayload, diff --git a/nym-wallet/src/context/mocks/main.tsx b/nym-wallet/src/context/mocks/main.tsx index 7b0eba0b8a..cb68ead589 100644 --- a/nym-wallet/src/context/mocks/main.tsx +++ b/nym-wallet/src/context/mocks/main.tsx @@ -57,6 +57,7 @@ export const MockMainContextProvider: FCWithChildren = ({ children }) => { handleShowSendModal: () => undefined, handleShowReceiveModal: () => undefined, keepState: async () => undefined, + printBalance: '100.0000 NYMT', }), [], ); diff --git a/nym-wallet/src/pages/bonding/Bonding.tsx b/nym-wallet/src/pages/bonding/Bonding.tsx index 957f603d0e..e21c19c3ce 100644 --- a/nym-wallet/src/pages/bonding/Bonding.tsx +++ b/nym-wallet/src/pages/bonding/Bonding.tsx @@ -1,6 +1,6 @@ -import React, { useContext, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { FeeDetails, decimalToFloatApproximation } from '@nymproject/types'; +import { FeeDetails } from '@nymproject/types'; import { Box } from '@mui/material'; import { TPoolOption } from 'src/components'; import { Bond } from 'src/components/Bonding/Bond'; @@ -8,122 +8,122 @@ import { BondedMixnode } from 'src/components/Bonding/BondedMixnode'; import { TBondedMixnodeActions } from 'src/components/Bonding/BondedMixnodeActions'; import { BondGatewayModal } from 'src/components/Bonding/modals/BondGatewayModal'; import { BondMixnodeModal } from 'src/components/Bonding/modals/BondMixnodeModal'; -import { BondMoreModal } from 'src/components/Bonding/modals/BondMoreModal'; +import { UpdateBondAmountModal } from 'src/components/Bonding/modals/UpdateBondAmountModal'; import { BondOversaturatedModal } from 'src/components/Bonding/modals/BondOversaturatedModal'; import { ConfirmationDetailProps, ConfirmationDetailsModal } from 'src/components/Bonding/modals/ConfirmationModal'; import { ErrorModal } from 'src/components/Modals/ErrorModal'; import { LoadingModal } from 'src/components/Modals/LoadingModal'; import { AppContext, urls } from 'src/context/main'; -import { isGateway, isMixnode, TBondGatewayArgs, TBondMixNodeArgs, TBondMoreArgs } from 'src/types'; +import { isGateway, isMixnode, TBondGatewayArgs, TBondMixNodeArgs, TUpdateBondArgs } from 'src/types'; import { BondedGateway } from 'src/components/Bonding/BondedGateway'; import { RedeemRewardsModal } from 'src/components/Bonding/modals/RedeemRewardsModal'; -import { Console } from 'src/utils/console'; import { BondingContextProvider, useBondingContext } from '../../context'; -import { getMixnodeStakeSaturation } from '../../requests'; const Bonding = () => { const [showModal, setShowModal] = useState< - 'bond-mixnode' | 'bond-gateway' | 'bond-more' | 'bond-more-oversaturated' | 'unbond' | 'redeem' + 'bond-mixnode' | 'bond-gateway' | 'update-bond' | 'update-bond-oversaturated' | 'unbond' | 'redeem' >(); const [confirmationDetails, setConfirmationDetails] = useState(); - const [saturationPercentage, setSaturationPercentage] = useState(); + const [uncappedSaturation, setUncappedSaturation] = useState(); const { network, clientDetails, - userBalance: { originalVesting, balance }, + userBalance: { originalVesting }, } = useContext(AppContext); const navigate = useNavigate(); - const { bondedNode, bondMixnode, bondGateway, redeemRewards, isLoading, checkOwnership, bondMore } = - useBondingContext(); + const { + bondedNode, + bondMixnode, + bondGateway, + redeemRewards, + isLoading, + checkOwnership, + updateBondAmount, + error, + refresh, + } = useBondingContext(); + + useEffect(() => { + if (bondedNode && isMixnode(bondedNode) && bondedNode.uncappedStakeSaturation) { + setUncappedSaturation(bondedNode.uncappedStakeSaturation); + } + }, [bondedNode]); const handleCloseModal = async () => { setShowModal(undefined); await checkOwnership(); }; - const handleError = (error: string) => { + const handleError = (err: string) => { setShowModal(undefined); setConfirmationDetails({ status: 'error', title: 'An error occurred', - subtitle: error, + subtitle: err, }); }; const handleBondMixnode = async (data: TBondMixNodeArgs, tokenPool: TPoolOption) => { setShowModal(undefined); const tx = await bondMixnode(data, tokenPool); - setConfirmationDetails({ - status: 'success', - title: 'Bond successful', - txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`, - }); + if (tx) { + setConfirmationDetails({ + status: 'success', + title: 'Bond successful', + txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`, + }); + } return undefined; }; const handleBondGateway = async (data: TBondGatewayArgs, tokenPool: TPoolOption) => { setShowModal(undefined); const tx = await bondGateway(data, tokenPool); - setConfirmationDetails({ - status: 'success', - title: 'Bond successful', - txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`, - }); + if (tx) { + setConfirmationDetails({ + status: 'success', + title: 'Bond successful', + txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`, + }); + } }; - const handleBondMore = async (data: TBondMoreArgs, tokenPool: TPoolOption) => { + const handleUpdateBond = async (data: TUpdateBondArgs, tokenPool: TPoolOption) => { setShowModal(undefined); - const tx = await bondMore(data, tokenPool); - setConfirmationDetails({ - status: 'success', - title: 'Bond More successful', - txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`, - }); + + const tx = await updateBondAmount(data, tokenPool); + if (tx) { + setConfirmationDetails({ + status: 'success', + title: 'Bond amount changed successfully', + txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`, + }); + } }; const handleRedeemReward = async (fee?: FeeDetails) => { setShowModal(undefined); const tx = await redeemRewards(fee); - setConfirmationDetails({ - status: 'success', - title: 'Rewards redeemed successfully', - txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`, - }); - }; - - const handleCheckStakeSaturation = async (newMixId: number) => { - try { - const newSaturation = decimalToFloatApproximation( - (await getMixnodeStakeSaturation(newMixId)).uncapped_saturation, - ); - if (newSaturation && newSaturation > 1) { - const newSaturationPercentage = Math.round(newSaturation * 100); - return { isOverSaturated: true, saturationPercentage: newSaturationPercentage }; - } - return { isOverSaturated: false, saturationPercentage: undefined }; - } catch (e) { - Console.error('Error fetching the saturation, error:', e); - return { isOverSaturated: false, saturationPercentage: undefined }; + if (tx) { + setConfirmationDetails({ + status: 'success', + title: 'Rewards redeemed successfully', + txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`, + }); } }; const handleBondedMixnodeAction = async (action: TBondedMixnodeActions) => { switch (action) { - case 'bondMore': { - if (bondedNode && isMixnode(bondedNode)) { - const { isOverSaturated, saturationPercentage: newSaturationPercentage } = await handleCheckStakeSaturation( - bondedNode.mixId, - ); - if (isOverSaturated && newSaturationPercentage) { - setShowModal('bond-more-oversaturated'); - setSaturationPercentage(newSaturationPercentage.toString()); - break; - } + case 'updateBond': { + if (uncappedSaturation) { + setShowModal('update-bond-oversaturated'); + } else { + setShowModal('update-bond'); } - setShowModal('bond-more'); break; } case 'unbond': { @@ -141,6 +141,10 @@ const Bonding = () => { return undefined; }; + if (error) { + return refresh()} />; + } + return ( {!bondedNode && setShowModal('bond-mixnode')} />} @@ -179,20 +183,19 @@ const Bonding = () => { /> )} - {showModal === 'bond-more-oversaturated' && saturationPercentage && ( + {showModal === 'update-bond-oversaturated' && uncappedSaturation && ( setShowModal(undefined)} - onContinue={() => setShowModal('bond-more')} - saturationPercentage={saturationPercentage} + onContinue={() => setShowModal('update-bond')} + saturationPercentage={uncappedSaturation.toString()} /> )} - {showModal === 'bond-more' && bondedNode && isMixnode(bondedNode) && ( - setShowModal(undefined)} onError={handleError} /> diff --git a/nym-wallet/src/requests/actions.ts b/nym-wallet/src/requests/actions.ts index 949b62d8a5..cf09085d5f 100644 --- a/nym-wallet/src/requests/actions.ts +++ b/nym-wallet/src/requests/actions.ts @@ -13,8 +13,7 @@ import { TBondGatewaySignatureArgs, TBondMixNodeArgs, TBondMixnodeSignatureArgs, - TBondMoreArgs, - TDecreaseBondArgs, + TUpdateBondArgs, } from '../types'; import { invokeWrapper } from './wrapper'; @@ -51,7 +50,5 @@ export const unbond = async (type: EnumNodeType) => { return unbondGateway(); }; -export const bondMore = async (args: TBondMoreArgs) => invokeWrapper('pledge_more', args); - -export const decreaseBond = async (args: TDecreaseBondArgs) => - invokeWrapper('decrease_pledge', args); +export const updateBond = async (args: TUpdateBondArgs) => + invokeWrapper('update_pledge', args); diff --git a/nym-wallet/src/requests/simulate.ts b/nym-wallet/src/requests/simulate.ts index d31544e83e..158df8e5be 100644 --- a/nym-wallet/src/requests/simulate.ts +++ b/nym-wallet/src/requests/simulate.ts @@ -6,7 +6,7 @@ import { MixNodeConfigUpdate, GatewayConfigUpdate, } from '@nymproject/types'; -import { TBondGatewayArgs, TBondMixNodeArgs } from 'src/types'; +import { TBondGatewayArgs, TBondMixNodeArgs, TSimulateUpdateBondArgs } from 'src/types'; import { invokeWrapper } from './wrapper'; export const simulateBondGateway = async (args: TBondGatewayArgs) => @@ -80,7 +80,8 @@ export const simulateClaimOperatorReward = async () => invokeWrapper export const simulateVestingClaimOperatorReward = async () => invokeWrapper('simulate_vesting_claim_operator_reward'); -export const simulateBondMore = async (args: any) => invokeWrapper('simulate_pledge_more', args); +export const simulateUpdateBond = async (args: TSimulateUpdateBondArgs) => + invokeWrapper('simulate_update_pledge', args); -export const simulateVestingBondMore = async (args: any) => - invokeWrapper('simulate_vesting_pledge_more', args); +export const simulateVestingUpdateBond = async (args: TSimulateUpdateBondArgs) => + invokeWrapper('simulate_vesting_update_pledge', args); diff --git a/nym-wallet/src/requests/vesting.ts b/nym-wallet/src/requests/vesting.ts index 3e330ff9fa..2d13cb4448 100644 --- a/nym-wallet/src/requests/vesting.ts +++ b/nym-wallet/src/requests/vesting.ts @@ -14,7 +14,7 @@ import { } from '@nymproject/types'; import { Fee } from '@nymproject/types/dist/types/rust/Fee'; import { invokeWrapper } from './wrapper'; -import { TBondGatewaySignatureArgs, TBondMixnodeSignatureArgs } from '../types'; +import { TBondGatewaySignatureArgs, TBondMixnodeSignatureArgs, TUpdateBondArgs } from '../types'; export const getLockedCoins = async (): Promise => invokeWrapper('locked_coins'); @@ -121,14 +121,5 @@ export const vestingClaimOperatorReward = async (fee?: Fee) => export const vestingClaimDelegatorRewards = async (mixId: number) => invokeWrapper('vesting_claim_delegator_reward', { mixId }); -export const vestingBondMore = async ({ fee, additionalPledge }: { fee?: Fee; additionalPledge: DecCoin }) => - invokeWrapper('vesting_pledge_more', { - fee, - additionalPledge, - }); - -export const vestingDecreaseBond = async ({ fee, decreaseBy }: { fee?: Fee; decreaseBy: DecCoin }) => - invokeWrapper('vesting_decrease_pledge', { - fee, - decreaseBy, - }); +export const vestingUpdateBond = async (args: TUpdateBondArgs) => + invokeWrapper('vesting_update_pledge', args); diff --git a/nym-wallet/src/types/global.ts b/nym-wallet/src/types/global.ts index 3cbf5bfc93..db7864a56c 100644 --- a/nym-wallet/src/types/global.ts +++ b/nym-wallet/src/types/global.ts @@ -54,15 +54,13 @@ export type TBondGatewaySignatureArgs = { tokenPool: 'balance' | 'locked'; }; -export type TBondMoreArgs = { - additionalPledge: DecCoin; +export type TUpdateBondArgs = { + currentPledge: DecCoin; + newPledge: DecCoin; fee?: Fee; }; -export type TDecreaseBondArgs = { - decreaseBy: DecCoin; - fee?: Fee; -}; +export type TSimulateUpdateBondArgs = Omit; export type TNodeDescription = { name: string;