feat(wallet): update bond amount (#3338)

This commit is contained in:
Pierre Dommerc
2023-04-26 15:29:39 +02:00
committed by pierre
parent 91c20af893
commit 796f5a678a
20 changed files with 541 additions and 232 deletions
+6
View File
@@ -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")]
+4
View File
@@ -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,
@@ -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<Fee>,
state: tauri::State<'_, WalletState>,
) -> Result<TransactionExecuteResult, BackendError> {
let guard = state.read().await;
let fee_amount = guard.convert_tx_fee(fee.as_ref());
let dec_delta = guard.calculate_coin_delta(&current_pledge, &new_pledge)?;
let delta = guard.attempt_convert_to_base_coin(dec_delta.clone())?;
log::info!(
">>> Pledge update, current pledge {}, new pledge {}",
&current_pledge,
&new_pledge,
);
let res = match new_pledge.amount.cmp(&current_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<Fee>,
@@ -1,6 +1,8 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// 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<FeeDetails, BackendError> {
let guard = state.read().await;
let dec_delta = guard.calculate_coin_delta(&current_pledge, &new_pledge)?;
log::info!(
">>> Simulate pledge update, current pledge {}, new pledge {}",
&current_pledge,
&new_pledge,
);
match new_pledge.amount.cmp(&current_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>,
@@ -1,6 +1,8 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// 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<FeeDetails, BackendError> {
let guard = state.read().await;
match new_pledge.amount.cmp(&current_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,
@@ -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<Fee>,
state: tauri::State<'_, WalletState>,
) -> Result<TransactionExecuteResult, BackendError> {
let guard = state.read().await;
let fee_amount = guard.convert_tx_fee(fee.as_ref());
let dec_delta = guard.calculate_coin_delta(&current_pledge, &new_pledge)?;
let delta = guard.attempt_convert_to_base_coin(dec_delta.clone())?;
log::info!(
">>> Pledge update, current pledge {}, new pledge {}",
&current_pledge,
&new_pledge,
);
let res = match new_pledge.amount.cmp(&current_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<Fee>,
+28
View File
@@ -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<DecCoin, BackendError> {
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(
@@ -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 (
<ActionsMenu open={isOpen} onOpen={handleOpen} onClose={handleClose}>
{!disabledBondMore && (
{!disabledUpdateBond && (
<ActionsMenuItem
title="Bond More"
title="Change bond amount"
Icon={<BondIcon fontSize="inherit" />}
onClick={() => handleActionClick('bondMore')}
onClick={() => handleActionClick('updateBond')}
/>
)}
<ActionsMenuItem
@@ -1,116 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Box, Stack } from '@mui/material';
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 { TPoolOption } from 'src/components/TokenPoolSelector';
import { ConfirmTx } from 'src/components/ConfirmTX';
import { useGetFee } from 'src/hooks/useGetFee';
import { validateAmount } from 'src/utils';
import { simulateBondMore, simulateVestingBondMore } from 'src/requests';
import { TBondMoreArgs } from 'src/types';
import { TBondedMixnode } from 'src/context';
export const BondMoreModal = ({
node,
userBalance,
onBondMore,
onClose,
onError,
}: {
node: TBondedMixnode;
userBalance?: string;
onBondMore: (data: TBondMoreArgs, tokenPool: TPoolOption) => Promise<void>;
onClose: () => void;
onError: (e: string) => void;
}) => {
const { bond: currentBond, proxy } = node;
const { fee, getFee, resetFeeState, feeError } = useGetFee();
const [additionalBond, setAdditionalBond] = useState<DecCoin>({ 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<TBondMoreArgs>(simulateBondMore, { additionalPledge: additionalBond });
} else {
await getFee<TBondMoreArgs>(simulateVestingBondMore, { additionalPledge: additionalBond });
}
};
if (fee)
return (
<ConfirmTx
open
header="Bond more details"
fee={fee}
onClose={onClose}
onPrev={resetFeeState}
onConfirm={handleConfirm}
>
<ModalListItem label="Current bond" value={`${currentBond.amount} ${currentBond.denom}`} divider />
<ModalListItem label="Additional bond" value={`${additionalBond?.amount} ${additionalBond?.denom}`} divider />
</ConfirmTx>
);
return (
<SimpleModal
open
header="Bond more"
subHeader="Bond more tokens on your node and receive more rewards"
okLabel="Next"
onOk={handleOnOk}
okDisabled={errorAmount}
onClose={onClose}
>
<Stack gap={3}>
<Box display="flex" gap={1}>
<CurrencyFormField
autoFocus
label="Bond amount"
denom={currentBond.denom}
onChanged={(value) => {
handleAmountChanged(value);
}}
fullWidth
validationError={errorAmount ? 'Please enter a valid amount' : undefined}
/>
</Box>
<Box>
<ModalListItem label="Account balance" value={userBalance?.toUpperCase() || '-'} divider />
<ModalListItem label="Current bond" value={`${currentBond.amount} ${currentBond.denom}`} divider />
<ModalListItem label="Est. fee for this operation will be calculated in the next page" value="" divider />
</Box>
</Stack>
</SimpleModal>
);
};
@@ -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
>
<Stack spacing={3} marginBottom={3}>
@@ -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<void>;
onClose: () => void;
onError: (e: string) => void;
}) => {
const { bond: currentBond, proxy, stakeSaturation, uncappedStakeSaturation } = node;
const { fee, getFee, resetFeeState, feeError } = useGetFee();
const [newBond, setNewBond] = useState<DecCoin | undefined>();
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<TSimulateUpdateBondArgs>(simulateUpdateBond, {
currentPledge: currentBond,
newPledge: newBond,
});
} else {
await getFee<TSimulateUpdateBondArgs>(simulateVestingUpdateBond, {
currentPledge: currentBond,
newPledge: newBond,
});
}
};
const newBondToDisplay = () => {
const coin = decCoinToDisplay(newBond as DecCoin);
return `${coin.amount} ${coin.denom}`;
};
if (fee)
return (
<ConfirmTx
open
header="Change bond details"
fee={fee}
onClose={onClose}
onPrev={resetFeeState}
onConfirm={handleConfirm}
>
<ModalListItem label="New bond details" value={newBondToDisplay()} divider />
<ModalListItem label="Change bond details" value={`${currentBond.amount} ${currentBond.denom}`} divider />
</ConfirmTx>
);
return (
<SimpleModal
open
header="Change bond amount"
subHeader="Add or reduce amount of tokens on your node"
okLabel="Next"
onOk={handleOnOk}
okDisabled={errorAmount || !newBond}
onClose={onClose}
>
<Stack gap={3}>
<Box display="flex" gap={1}>
<CurrencyFormField
autoFocus
label="New bond amount"
denom={currentBond.denom}
onChanged={(value) => {
handleAmountChanged(value);
}}
fullWidth
validationError={errorAmount ? 'Please enter a valid amount' : undefined}
/>
</Box>
<Box>
<ModalListItem
fontWeight={600}
label={proxy ? 'Locked account balance' : 'Account balance'}
value={proxy ? printVestedBalance || '-' : printBalance}
divider
/>
<ModalListItem label="Current bond amount" value={`${currentBond.amount} ${currentBond.denom}`} divider />
{uncappedStakeSaturation ? (
<ModalListItem
label="Node saturation"
value={`${uncappedStakeSaturation}%`}
sxValue={{ color: 'error.main' }}
divider
/>
) : (
<ModalListItem label="Node saturation" value={`${stakeSaturation}%`} divider />
)}
<ModalListItem label="Est. fee for this operation will be calculated in the next page" value="" divider />
</Box>
</Stack>
</SimpleModal>
);
};
+26 -10
View File
@@ -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<TransactionExecuteResult | undefined>;
bondGateway: (data: TBondGatewayArgs, tokenPool: TokenPool) => Promise<TransactionExecuteResult | undefined>;
unbond: (fee?: FeeDetails) => Promise<TransactionExecuteResult | undefined>;
bondMore: (data: TBondMoreArgs, tokenPool: TokenPool) => Promise<TransactionExecuteResult | undefined>;
updateBondAmount: (data: TUpdateBondArgs, tokenPool: TokenPool) => Promise<TransactionExecuteResult | undefined>;
redeemRewards: (fee?: FeeDetails) => Promise<TransactionExecuteResult | undefined>;
updateMixnode: (pm: string, fee?: FeeDetails) => Promise<TransactionExecuteResult | undefined>;
checkOwnership: () => Promise<void>;
@@ -138,7 +140,7 @@ export const BondingContext = createContext<TBondingContext>({
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,
+19
View File
@@ -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<void>;
printBalance: string;
printVestedBalance?: string; // spendable vested token
};
interface RustState {
@@ -89,6 +92,8 @@ export const AppProvider: FCWithChildren = ({ children }) => {
const [isAdminAddress, setIsAdminAddress] = useState<boolean>(false);
const [showSendModal, setShowSendModal] = useState(false);
const [showReceiveModal, setShowReceiveModal] = useState(false);
const [printBalance, setPrintBalance] = useState<string>('-');
const [printVestedBalance, setPrintVestedBalance] = useState<string | undefined>();
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,
+2 -2
View File
@@ -154,7 +154,7 @@ export const MockBondingContextProvider = ({
return TxResultMock;
};
const bondMore = async (): Promise<TransactionExecuteResult> => {
const updateBondAmount = async (): Promise<TransactionExecuteResult> => {
setIsLoading(true);
await mockSleep(SLEEP_MS);
triggerStateUpdate();
@@ -202,7 +202,7 @@ export const MockBondingContextProvider = ({
getFee,
resetFeeState,
updateMixnode,
bondMore,
updateBondAmount,
checkOwnership,
generateMixnodeMsgPayload,
generateGatewayMsgPayload,
+1
View File
@@ -57,6 +57,7 @@ export const MockMainContextProvider: FCWithChildren = ({ children }) => {
handleShowSendModal: () => undefined,
handleShowReceiveModal: () => undefined,
keepState: async () => undefined,
printBalance: '100.0000 NYMT',
}),
[],
);
+71 -68
View File
@@ -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<ConfirmationDetailProps>();
const [saturationPercentage, setSaturationPercentage] = useState<string | undefined>();
const [uncappedSaturation, setUncappedSaturation] = useState<number | undefined>();
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 <ErrorModal open message="An error occured, please check logs for details" onClose={() => refresh()} />;
}
return (
<Box sx={{ mt: 4 }}>
{!bondedNode && <Bond disabled={isLoading} onBond={() => setShowModal('bond-mixnode')} />}
@@ -179,20 +183,19 @@ const Bonding = () => {
/>
)}
{showModal === 'bond-more-oversaturated' && saturationPercentage && (
{showModal === 'update-bond-oversaturated' && uncappedSaturation && (
<BondOversaturatedModal
open
onClose={() => setShowModal(undefined)}
onContinue={() => setShowModal('bond-more')}
saturationPercentage={saturationPercentage}
onContinue={() => setShowModal('update-bond')}
saturationPercentage={uncappedSaturation.toString()}
/>
)}
{showModal === 'bond-more' && bondedNode && isMixnode(bondedNode) && (
<BondMoreModal
{showModal === 'update-bond' && bondedNode && isMixnode(bondedNode) && (
<UpdateBondAmountModal
node={bondedNode}
userBalance={balance?.printable_balance}
onBondMore={handleBondMore}
onUpdateBond={handleUpdateBond}
onClose={() => setShowModal(undefined)}
onError={handleError}
/>
+3 -6
View File
@@ -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<TransactionExecuteResult>('pledge_more', args);
export const decreaseBond = async (args: TDecreaseBondArgs) =>
invokeWrapper<TransactionExecuteResult>('decrease_pledge', args);
export const updateBond = async (args: TUpdateBondArgs) =>
invokeWrapper<TransactionExecuteResult>('update_pledge', args);
+5 -4
View File
@@ -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<FeeDetails>
export const simulateVestingClaimOperatorReward = async () =>
invokeWrapper<FeeDetails>('simulate_vesting_claim_operator_reward');
export const simulateBondMore = async (args: any) => invokeWrapper<FeeDetails>('simulate_pledge_more', args);
export const simulateUpdateBond = async (args: TSimulateUpdateBondArgs) =>
invokeWrapper<FeeDetails>('simulate_update_pledge', args);
export const simulateVestingBondMore = async (args: any) =>
invokeWrapper<FeeDetails>('simulate_vesting_pledge_more', args);
export const simulateVestingUpdateBond = async (args: TSimulateUpdateBondArgs) =>
invokeWrapper<FeeDetails>('simulate_vesting_update_pledge', args);
+3 -12
View File
@@ -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<DecCoin> => invokeWrapper<DecCoin>('locked_coins');
@@ -121,14 +121,5 @@ export const vestingClaimOperatorReward = async (fee?: Fee) =>
export const vestingClaimDelegatorRewards = async (mixId: number) =>
invokeWrapper<TransactionExecuteResult>('vesting_claim_delegator_reward', { mixId });
export const vestingBondMore = async ({ fee, additionalPledge }: { fee?: Fee; additionalPledge: DecCoin }) =>
invokeWrapper<TransactionExecuteResult>('vesting_pledge_more', {
fee,
additionalPledge,
});
export const vestingDecreaseBond = async ({ fee, decreaseBy }: { fee?: Fee; decreaseBy: DecCoin }) =>
invokeWrapper<TransactionExecuteResult>('vesting_decrease_pledge', {
fee,
decreaseBy,
});
export const vestingUpdateBond = async (args: TUpdateBondArgs) =>
invokeWrapper<TransactionExecuteResult>('vesting_update_pledge', args);
+4 -6
View File
@@ -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<TUpdateBondArgs, 'fee'>;
export type TNodeDescription = {
name: string;