Feature/operator costs (#1680)
* add new operator cost field * change uptime label to routing score * update validation for operator cost * fix profit margin type
This commit is contained in:
@@ -169,6 +169,16 @@ const AmountFormData = ({
|
||||
denom={denom}
|
||||
initialValue={amountData.amount.amount}
|
||||
/>
|
||||
<CurrencyFormField
|
||||
required
|
||||
fullWidth
|
||||
label="Operator cost"
|
||||
autoFocus
|
||||
onChanged={(newValue) => setValue('operatorCost', newValue, { shouldValidate: true })}
|
||||
validationError={errors.operatorCost?.amount?.message}
|
||||
denom={denom}
|
||||
initialValue={amountData.operatorCost.amount}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</Box>
|
||||
<TextField
|
||||
{...register('profitMargin')}
|
||||
name="profitMargin"
|
||||
label="Profit margin"
|
||||
error={Boolean(errors.profitMargin)}
|
||||
helperText={errors.profitMargin?.message}
|
||||
/>
|
||||
<Box>
|
||||
<CurrencyFormField
|
||||
required
|
||||
fullWidth
|
||||
label="Operating cost"
|
||||
onChanged={(newValue) => {
|
||||
setValue('operatorCost', newValue, { shouldValidate: true });
|
||||
}}
|
||||
validationError={errors.operatorCost?.amount?.message}
|
||||
denom={denom}
|
||||
initialValue={amountData.operatorCost.amount}
|
||||
/>
|
||||
<FormHelperText>
|
||||
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.
|
||||
</FormHelperText>
|
||||
</Box>
|
||||
<Box>
|
||||
<TextField
|
||||
{...register('profitMargin')}
|
||||
name="profitMargin"
|
||||
label="Profit margin"
|
||||
error={Boolean(errors.profitMargin)}
|
||||
helperText={errors.profitMargin?.message}
|
||||
fullWidth
|
||||
/>
|
||||
<FormHelperText>
|
||||
The percentage of node rewards that you as the node operator take before rewards are distributed to operator
|
||||
and delegators.
|
||||
</FormHelperText>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ const defaultGatewayValues: GatewayData = {
|
||||
|
||||
const defaultAmountValues = (denom: CurrencyDenom) => ({
|
||||
amount: { amount: '100', denom },
|
||||
operatorCost: { amount: '0', denom },
|
||||
tokenPool: 'balance',
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -73,7 +73,7 @@ export const SimpleModal: React.FC<{
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 2, width: buttonFullWidth ? '100%' : null }}>
|
||||
{onBack && <StyledBackButton onBack={onBack} />}
|
||||
{onOk && (
|
||||
<Button variant="contained" fullWidth size="large" onClick={onOk} disabled={okDisabled} sx={{ mt: 3 }}>
|
||||
<Button variant="contained" fullWidth size="large" onClick={onOk} disabled={okDisabled}>
|
||||
{okLabel}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -59,7 +59,15 @@ export const NodeUnbondPage = ({ bondedNode, onConfirm, onError }: Props) => {
|
||||
</Grid>
|
||||
</Grid>
|
||||
{isConfirmed && (
|
||||
<UnbondModal node={bondedNode} onConfirm={onConfirm} onClose={() => setIsConfirmed(false)} onError={onError} />
|
||||
<UnbondModal
|
||||
node={bondedNode}
|
||||
onConfirm={async () => {
|
||||
setIsConfirmed(false);
|
||||
onConfirm();
|
||||
}}
|
||||
onClose={() => setIsConfirmed(false)}
|
||||
onError={onError}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
+8
-7
@@ -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.
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid spacing={3} container item alignItems="center" sm={12} md={6}>
|
||||
@@ -135,7 +136,7 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode
|
||||
<Grid item container direction="row" alignItems="left" justifyContent="space-between" padding={3} spacing={1}>
|
||||
<Grid item>
|
||||
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
Operator cost
|
||||
Operating cost
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body1"
|
||||
@@ -145,15 +146,15 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode
|
||||
color: (t) => (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'),
|
||||
}}
|
||||
>
|
||||
Lock Wallet after a certain time
|
||||
</Typography>
|
||||
Changes to cost will be applied in the next interval.
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid spacing={3} container item alignItems="center" xs={12} md={6}>
|
||||
<Grid item width={1}>
|
||||
<TextField
|
||||
{...register('operatorCost')}
|
||||
name="operatorCost"
|
||||
label="Operator cost"
|
||||
label="Operating cost"
|
||||
fullWidth
|
||||
error={!!errors.operatorCost}
|
||||
helperText={errors?.operatorCost?.message}
|
||||
|
||||
@@ -35,6 +35,7 @@ export type MixnodeData = NodeIdentity & {
|
||||
|
||||
export type Amount = {
|
||||
amount: DecCoin;
|
||||
operatorCost: DecCoin;
|
||||
tokenPool: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export interface Epoch { id: number, start: bigint, end: bigint, duration_seconds: bigint, }
|
||||
@@ -65,6 +65,8 @@ export const truncate = (text: string, trim: number) => `${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<boolean> => {
|
||||
try {
|
||||
const walletValue = await userBalance();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user