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:
Fouad
2022-10-19 09:56:22 +01:00
committed by GitHub
parent dbb674f042
commit ba979c2e60
17 changed files with 121 additions and 51 deletions
@@ -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>
)}
+1 -1
View File
@@ -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';
+3 -14
View File
@@ -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>
);
@@ -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}
+1
View File
@@ -35,6 +35,7 @@ export type MixnodeData = NodeIdentity & {
export type Amount = {
amount: DecCoin;
operatorCost: DecCoin;
tokenPool: string;
};
+3
View File
@@ -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, }
+2
View File
@@ -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;