diff --git a/nym-wallet/src/components/Bonding/forms/BondGatewayForm.tsx b/nym-wallet/src/components/Bonding/forms/BondGatewayForm.tsx index b16afa7efe..12403f5e98 100644 --- a/nym-wallet/src/components/Bonding/forms/BondGatewayForm.tsx +++ b/nym-wallet/src/components/Bonding/forms/BondGatewayForm.tsx @@ -169,6 +169,16 @@ const AmountFormData = ({ denom={denom} initialValue={amountData.amount.amount} /> + setValue('operatorCost', newValue, { shouldValidate: true })} + validationError={errors.operatorCost?.amount?.message} + denom={denom} + initialValue={amountData.operatorCost.amount} + /> ); diff --git a/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx b/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx index fe74699dcf..79cd8617e0 100644 --- a/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx +++ b/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; -import { Box, Checkbox, FormControlLabel, Stack, TextField } from '@mui/material'; +import { Box, Checkbox, FormControlLabel, FormHelperText, Stack, TextField } from '@mui/material'; import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; import { CurrencyDenom, TNodeType } from '@nymproject/types'; @@ -172,13 +172,37 @@ const AmountFormData = ({ initialValue={amountData.amount.amount} /> - + + { + setValue('operatorCost', newValue, { shouldValidate: true }); + }} + validationError={errors.operatorCost?.amount?.message} + denom={denom} + initialValue={amountData.operatorCost.amount} + /> + + Monthly operational costs of running your node. If your node is in the active set the amount will be paid back + to you from the rewards. + + + + + + The percentage of node rewards that you as the node operator take before rewards are distributed to operator + and delegators. + + ); }; diff --git a/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts b/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts index 01ff72bdff..aa5752f4aa 100644 --- a/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts +++ b/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts @@ -1,5 +1,5 @@ import * as Yup from 'yup'; -import { isValidHostname, validateAmount, validateKey, validateRawPort, validateVersion } from 'src/utils'; +import { isLessThan, isValidHostname, validateAmount, validateKey, validateRawPort, validateVersion } from 'src/utils'; export const mixnodeValidationSchema = Yup.object().shape({ identityKey: Yup.string() @@ -35,6 +35,21 @@ export const mixnodeValidationSchema = Yup.object().shape({ .test('valid-http', 'A valid http-api port is required', (value) => (value ? validateRawPort(value) : false)), }); +const operatingCostAndPmValidation = { + profitMargin: Yup.number().required('Profit Percentage is required').min(0).max(100), + operatorCost: Yup.object().shape({ + amount: Yup.string() + .required('An operating cost is required') + .test('valid-operating-cost', 'A valid amount is required (min 40)', async function isValidAmount(this, value) { + if (value && (!Number(value) || isLessThan(+value, 40))) { + return false; + } + + return true; + }), + }), +}; + export const amountSchema = Yup.object().shape({ amount: Yup.object().shape({ amount: Yup.string() @@ -47,7 +62,7 @@ export const amountSchema = Yup.object().shape({ return true; }), }), - profitMargin: Yup.number().required('Profit Percentage is required').min(0).max(100), + ...operatingCostAndPmValidation, }); export const bondedInfoParametersValidationSchema = Yup.object().shape({ @@ -73,7 +88,5 @@ export const bondedInfoParametersValidationSchema = Yup.object().shape({ }); export const bondedNodeParametersValidationSchema = Yup.object().shape({ - profitMargin: Yup.number().required('Profit Percentage is required').min(0).max(100), - - operatorCost: Yup.number().required('Operator cost is required'), -}) + ...operatingCostAndPmValidation, +}); diff --git a/nym-wallet/src/components/Bonding/modals/BondGatewayModal.tsx b/nym-wallet/src/components/Bonding/modals/BondGatewayModal.tsx index 779eb25449..f6bc04f6c7 100644 --- a/nym-wallet/src/components/Bonding/modals/BondGatewayModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/BondGatewayModal.tsx @@ -24,6 +24,7 @@ const defaultGatewayValues: GatewayData = { const defaultAmountValues = (denom: CurrencyDenom) => ({ amount: { amount: '100', denom }, + operatorCost: { amount: '0', denom }, tokenPool: 'balance', }); diff --git a/nym-wallet/src/components/Bonding/modals/BondMixnodeModal.tsx b/nym-wallet/src/components/Bonding/modals/BondMixnodeModal.tsx index 3151ffad5d..fcc7685f87 100644 --- a/nym-wallet/src/components/Bonding/modals/BondMixnodeModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/BondMixnodeModal.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useState } from 'react'; -import { Box } from '@mui/material'; import { CurrencyDenom, TNodeType } from '@nymproject/types'; import { ConfirmTx } from 'src/components/ConfirmTX'; import { ModalListItem } from 'src/components/Modals/ModalListItem'; @@ -10,7 +9,7 @@ import { MixnodeAmount, MixnodeData } from 'src/pages/bonding/types'; import { simulateBondMixnode, simulateVestingBondMixnode } from 'src/requests'; import { TBondMixNodeArgs } from 'src/types'; import { BondMixnodeForm } from '../forms/BondMixnodeForm'; -import { attachDefaultOperatingCost, toPercentFloatString } from '../../../utils'; +import { toPercentFloatString } from '../../../utils'; const defaultMixnodeValues: MixnodeData = { identityKey: '', @@ -25,6 +24,7 @@ const defaultMixnodeValues: MixnodeData = { const defaultAmountValues = (denom: CurrencyDenom) => ({ amount: { amount: '100', denom }, + operatorCost: { amount: '40', denom }, profitMargin: '10', tokenPool: 'balance', }); @@ -71,11 +71,7 @@ export const BondMixnodeModal = ({ }; const handleUpdateAmountData = async (data: MixnodeAmount) => { - const pm = toPercentFloatString(data.profitMargin); - setAmountData({ ...data, profitMargin: pm }); - - // TODO: this will have to be updated with allowing users to provide their operating cost in the form - const defaultCostParams = await attachDefaultOperatingCost(pm); + setAmountData({ ...data }); const payload = { pledge: data.amount, @@ -88,7 +84,13 @@ export const BondMixnodeModal = ({ sphinx_key: mixnodeData.sphinxKey, identity_key: mixnodeData.identityKey, }, - costParams: defaultCostParams, + costParams: { + profit_margin_percent: toPercentFloatString(data.profitMargin), + interval_operating_cost: { + amount: data.operatorCost.amount.toString(), + denom: data.operatorCost.denom, + }, + }, }; if (data.tokenPool === 'balance') { @@ -99,12 +101,8 @@ export const BondMixnodeModal = ({ }; const handleConfirm = async () => { - // TODO: this will have to be updated with allowing users to provide their operating cost in the form - const defaultCostParams = await attachDefaultOperatingCost(amountData.profitMargin); - await onBondMixnode( { - costParams: defaultCostParams, pledge: amountData.amount, ownerSignature: mixnodeData.ownerSignature, mixnode: { @@ -115,6 +113,13 @@ export const BondMixnodeModal = ({ sphinx_key: mixnodeData.sphinxKey, identity_key: mixnodeData.identityKey, }, + costParams: { + profit_margin_percent: toPercentFloatString(amountData.profitMargin), + interval_operating_cost: { + amount: amountData.operatorCost.amount, + denom: amountData.operatorCost.denom, + }, + }, }, amountData.tokenPool as TPoolOption, ); diff --git a/nym-wallet/src/components/Delegation/DelegationList.tsx b/nym-wallet/src/components/Delegation/DelegationList.tsx index 29349c2612..91fccfdf0a 100644 --- a/nym-wallet/src/components/Delegation/DelegationList.tsx +++ b/nym-wallet/src/components/Delegation/DelegationList.tsx @@ -40,7 +40,7 @@ interface HeadCell { const headCells: HeadCell[] = [ { id: 'node_identity', label: 'Node ID', sortable: true, align: 'left' }, - { id: 'avg_uptime_percent', label: 'Uptime', sortable: true, align: 'left' }, + { id: 'avg_uptime_percent', label: 'Routing score', sortable: true, align: 'left' }, { id: 'profit_margin_percent', label: 'Profit margin', sortable: true, align: 'left' }, { id: 'stake_saturation', label: 'Stake saturation', sortable: true, align: 'left' }, { id: 'delegated_on_iso_datetime', label: 'Delegated on', sortable: true, align: 'left' }, diff --git a/nym-wallet/src/components/Modals/SimpleModal.tsx b/nym-wallet/src/components/Modals/SimpleModal.tsx index 782a83ecc1..85c6480619 100644 --- a/nym-wallet/src/components/Modals/SimpleModal.tsx +++ b/nym-wallet/src/components/Modals/SimpleModal.tsx @@ -73,7 +73,7 @@ export const SimpleModal: React.FC<{ {onBack && } {onOk && ( - )} diff --git a/nym-wallet/src/context/bonding.tsx b/nym-wallet/src/context/bonding.tsx index e9f4118c0f..fcacae9f9b 100644 --- a/nym-wallet/src/context/bonding.tsx +++ b/nym-wallet/src/context/bonding.tsx @@ -16,7 +16,6 @@ import { claimOperatorReward, getGatewayBondDetails, getMixnodeBondDetails, - getMixnodeRewardEstimation, unbondGateway as unbondGatewayRequest, unbondMixNode as unbondMixnodeRequest, vestingBondGateway, @@ -32,6 +31,7 @@ import { vestingClaimOperatorReward, getInclusionProbability, getMixnodeAvgUptime, + getMixnodeRewardEstimation, } from '../requests'; import { useCheckOwnership } from '../hooks/useCheckOwnership'; import { AppContext } from './main'; diff --git a/nym-wallet/src/pages/bonding/Bonding.tsx b/nym-wallet/src/pages/bonding/Bonding.tsx index 4e8415a2eb..d1ec197b0f 100644 --- a/nym-wallet/src/pages/bonding/Bonding.tsx +++ b/nym-wallet/src/pages/bonding/Bonding.tsx @@ -1,6 +1,7 @@ import React, { useContext, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { FeeDetails } from '@nymproject/types'; +import { Box } from '@mui/material'; import { TPoolOption } from 'src/components'; import { Bond } from 'src/components/Bonding/Bond'; import { BondedMixnode } from 'src/components/Bonding/BondedMixnode'; @@ -14,8 +15,7 @@ import { AppContext, urls } from 'src/context/main'; import { isGateway, isMixnode, TBondGatewayArgs, TBondMixNodeArgs } from 'src/types'; import { BondedGateway } from 'src/components/Bonding/BondedGateway'; import { RedeemRewardsModal } from 'src/components/Bonding/modals/RedeemRewardsModal'; -import { BondingContextProvider, useBondingContext, TBondedMixnode } from '../../context'; -import { Box } from '@mui/material'; +import { BondingContextProvider, useBondingContext } from '../../context'; const Bonding = () => { const [showModal, setShowModal] = useState<'bond-mixnode' | 'bond-gateway' | 'bond-more' | 'unbond' | 'redeem'>(); @@ -29,8 +29,7 @@ const Bonding = () => { const navigate = useNavigate(); - const { bondedNode, bondMixnode, bondGateway, unbond, redeemRewards, isLoading, checkOwnership } = - useBondingContext(); + const { bondedNode, bondMixnode, bondGateway, redeemRewards, isLoading, checkOwnership } = useBondingContext(); const handleCloseModal = async () => { setShowModal(undefined); @@ -67,16 +66,6 @@ const Bonding = () => { }); }; - const handleUnbond = async (fee?: FeeDetails) => { - setShowModal(undefined); - const tx = await unbond(fee); - setConfirmationDetails({ - status: 'success', - title: 'Unbond successful', - txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`, - }); - }; - const handleRedeemReward = async (fee?: FeeDetails) => { setShowModal(undefined); const tx = await redeemRewards(fee); diff --git a/nym-wallet/src/pages/bonding/node-settings/settings-pages/NodeUnbondPage.tsx b/nym-wallet/src/pages/bonding/node-settings/settings-pages/NodeUnbondPage.tsx index 451930342f..eb92fc2758 100644 --- a/nym-wallet/src/pages/bonding/node-settings/settings-pages/NodeUnbondPage.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/settings-pages/NodeUnbondPage.tsx @@ -59,7 +59,15 @@ export const NodeUnbondPage = ({ bondedNode, onConfirm, onError }: Props) => { {isConfirmed && ( - setIsConfirmed(false)} onError={onError} /> + { + setIsConfirmed(false); + onConfirm(); + }} + onClose={() => setIsConfirmed(false)} + onError={onError} + /> )} ); diff --git a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/ParametersSettings.tsx b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/ParametersSettings.tsx index c0c7ebff6c..d2bceafd1f 100644 --- a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/ParametersSettings.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/ParametersSettings.tsx @@ -21,6 +21,7 @@ import { TBondedMixnode, TBondedGateway } from 'src/context/bonding'; import { SimpleModal } from 'src/components/Modals/SimpleModal'; import { bondedNodeParametersValidationSchema } from 'src/components/Bonding/forms/mixnodeValidationSchema'; import { Console } from 'src/utils/console'; +import { decimalToFloatApproximation, decimalToPercentage } from '@nymproject/types'; export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBondedGateway }): JSX.Element => { const [open, setOpen] = useState(true); @@ -37,7 +38,7 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode mode: 'onChange', defaultValues: isMixnode(bondedNode) ? { - operatorCost: bondedNode.bond.amount, + operatorCost: bondedNode.operatorCost, profitMargin: bondedNode.profitMargin, } : {}, @@ -46,7 +47,7 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode const onSubmit = async (data: { operatorCost?: string; profitMargin?: string }) => { if (data.operatorCost && data.profitMargin) { const MixNodeCostParams = { - profit_margin_percent: data.profitMargin.toString(), + profit_margin_percent: (+data.profitMargin / 100).toString(), interval_operating_cost: { denom: bondedNode.bond.denom, amount: data.operatorCost.toString(), @@ -106,7 +107,7 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode color: (t) => (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'), }} > - Profit margin can be changed once a month + Changes to PM will be applied in the next interval. @@ -135,7 +136,7 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode - Operator cost + Operating cost (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'), }} > - Lock Wallet after a certain time - + Changes to cost will be applied in the next interval. + `${text.substring(0, tri export const isGreaterThan = (a: number, b: number) => a > b; +export const isLessThan = (a: number, b: number) => a < b; + export const checkHasEnoughFunds = async (allocationValue: string): Promise => { try { const walletValue = await userBalance(); diff --git a/ts-packages/types/src/types/rust/CoreNodeStatusResponse.ts b/ts-packages/types/src/types/rust/CoreNodeStatusResponse.ts index 44ac51adbe..9814be68fc 100644 --- a/ts-packages/types/src/types/rust/CoreNodeStatusResponse.ts +++ b/ts-packages/types/src/types/rust/CoreNodeStatusResponse.ts @@ -1,3 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + export interface CoreNodeStatusResponse { identity: string; count: number; diff --git a/ts-packages/types/src/types/rust/DelegationRecord.ts b/ts-packages/types/src/types/rust/DelegationRecord.ts new file mode 100644 index 0000000000..3d485d8b68 --- /dev/null +++ b/ts-packages/types/src/types/rust/DelegationRecord.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DecCoin } from './DecCoin'; + +export interface DelegationRecord { + amount: DecCoin; + block_height: bigint; + delegated_on_iso_datetime: string; + uses_vesting_contract_tokens: boolean; +} diff --git a/ts-packages/types/src/types/rust/PendingUndelegate.ts b/ts-packages/types/src/types/rust/PendingUndelegate.ts index 70855568cf..93c132d301 100644 --- a/ts-packages/types/src/types/rust/PendingUndelegate.ts +++ b/ts-packages/types/src/types/rust/PendingUndelegate.ts @@ -1,3 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + export interface PendingUndelegate { mix_identity: string; delegate: string;