Display interval timings (#1690)

* make reusable Alert component

* get current interval

* display next interval and epoch times

* display pending events
This commit is contained in:
Fouad
2022-10-20 17:11:02 +01:00
committed by GitHub
parent 862178c9c5
commit 8547e770da
10 changed files with 188 additions and 116 deletions
+5 -5
View File
@@ -160,9 +160,9 @@ mod qa {
pub(crate) const STAKE_DENOM: DenomDetails = DenomDetails::new("unyx", "nyx", 6);
pub(crate) const MIXNET_CONTRACT_ADDRESS: &str =
"n1rjzps6qrmdqmf0xz4cn4x4rcmqeqzq6hnzqg4wcvd0r2lyasdq5sepn5s8";
"n1frq2hzkjtatsupc6jtyaz67ytydk9nya437q92qg76ny3y8fcnjsw806vg";
pub(crate) const VESTING_CONTRACT_ADDRESS: &str =
"n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav";
"n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g";
pub(crate) const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str =
"n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0";
pub(crate) const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str =
@@ -172,13 +172,13 @@ mod qa {
hex_literal::hex!("0000000000000000000000000000000000000000");
pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] =
hex_literal::hex!("0000000000000000000000000000000000000000");
//pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "n1tfzd4qz3a45u8p4mr5zmzv66457uwjgcl05jdq";
//pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "n17dcjwykjj9ydd8p9hk65uf2flrhhlfklju8flp";
//pub(crate) const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "http://0.0.0.0";
pub(crate) fn validators() -> Vec<ValidatorDetails> {
vec![ValidatorDetails::new(
"https://qa-validator.nymtech.net",
Some("https://qa-validator-api.nymtech.net/api"),
"https://adv-epoch-qa-validator.qa.nymte.ch/",
Some("https://adv-epoch-qa-val-api.qa.nymte.ch/api"),
)]
}
+32
View File
@@ -0,0 +1,32 @@
import React, { useState } from 'react';
import { Alert as MuiAlert, IconButton } from '@mui/material';
import { Close } from '@mui/icons-material';
export const Alert = ({ title, dismissable }: { title: string | React.ReactNode; dismissable?: boolean }) => {
const [displayAlert, setDisplayAlert] = useState(true);
const handleDismiss = () => setDisplayAlert(false);
if (!displayAlert) return null;
return (
<MuiAlert
severity="info"
sx={{
width: '100%',
borderRadius: 0,
bgcolor: 'background.default',
color: (theme) => theme.palette.nym.nymWallet.text.blue,
'& .MuiAlert-icon': { color: 'nym.nymWallet.text.blue', mr: 1 },
}}
action={
dismissable && (
<IconButton aria-label="close" color="inherit" size="small" onClick={handleDismiss}>
<Close fontSize="inherit" />
</IconButton>
)
}
>
{title}
</MuiAlert>
);
};
@@ -37,17 +37,15 @@ export const mixnodeValidationSchema = Yup.object().shape({
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;
}
operatorCost: 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;
}),
}),
return true;
}),
};
export const amountSchema = Yup.object().shape({
@@ -1,18 +1,17 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import { Button, Divider, Typography, TextField, Grid, Alert, IconButton, CircularProgress, Box } from '@mui/material';
import { Button, Divider, Typography, TextField, Grid, CircularProgress, Box } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import CloseIcon from '@mui/icons-material/Close';
import { isMixnode } from 'src/types';
import { updateMixnodeConfig } from 'src/requests';
import { TBondedMixnode, TBondedGateway } from 'src/context/bonding';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { bondedInfoParametersValidationSchema } from 'src/components/Bonding/forms/mixnodeValidationSchema';
import { Console } from 'src/utils/console';
import { Alert } from 'src/components/Alert';
export const InfoSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBondedGateway }) => {
const [open, setOpen] = useState(true);
const [openConfirmationModal, setOpenConfirmationModal] = useState<boolean>(false);
const theme = useTheme();
@@ -54,33 +53,14 @@ export const InfoSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBon
return (
<Grid container xs item>
{open && (
<Alert
severity="info"
action={
<IconButton
aria-label="close"
color="inherit"
size="small"
onClick={() => {
setOpen(false);
}}
>
<CloseIcon fontSize="inherit" />
</IconButton>
}
sx={{
px: 2,
borderRadius: 0,
bgcolor: 'background.default',
color: (theme) => theme.palette.nym.nymWallet.text.blue,
'& .MuiAlert-icon': { color: (theme) => theme.palette.nym.nymWallet.text.blue, mr: 1 },
}}
>
<Box sx={{ fontWeight: 600 }}>Your changes will be ONLY saved on the display.</Box> Remember to change the
values on your nodes config file too.
</Alert>
)}
<Alert
title={
<Box sx={{ fontWeight: 600 }}>
Your changes will be ONLY saved on the display. Remember to change the values on your nodes config file too
</Box>
}
dismissable
/>
<Grid container>
<Grid item container direction="row" alignItems="left" justifyContent="space-between" padding={3}>
<Grid item>
@@ -1,4 +1,4 @@
import { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import {
@@ -8,30 +8,33 @@ import {
TextField,
InputAdornment,
Grid,
Alert,
IconButton,
CircularProgress,
Box,
FormHelperText,
} from '@mui/material';
import { useTheme } from '@mui/material/styles';
import CloseIcon from '@mui/icons-material/Close';
import { isMixnode } from 'src/types';
import { updateMixnodeCostParams } from 'src/requests';
import { getCurrentInterval, getPendingIntervalEvents, updateMixnodeCostParams } from 'src/requests';
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';
import { add, format, fromUnixTime } from 'date-fns';
import { Alert } from 'src/components/Alert';
import { ChangeMixCostParams } from 'src/pages/bonding/types';
import { MixNodeCostParams } from '@nymproject/types';
export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBondedGateway }): JSX.Element => {
const [open, setOpen] = useState(true);
const [openConfirmationModal, setOpenConfirmationModal] = useState<boolean>(false);
const [intervalTime, setIntervalTime] = useState<string>();
const [nextEpoch, setNextEpoch] = useState<string>();
const [pendingUpdates, setPendingUpdates] = useState<MixNodeCostParams>();
const theme = useTheme();
const {
register,
handleSubmit,
reset,
formState: { errors, isSubmitting, isDirty, isValid },
} = useForm({
resolver: yupResolver(bondedNodeParametersValidationSchema),
@@ -44,6 +47,50 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode
: {},
});
const getIntervalAsDate = async () => {
const interval = await getCurrentInterval();
const secondsToNextInterval =
Number(interval.epochs_in_interval - interval.current_epoch_id) * Number(interval.epoch_length_seconds);
setIntervalTime(
format(
add(new Date(), {
seconds: secondsToNextInterval,
}),
'MM/dd/yyyy HH:mm',
),
);
setNextEpoch(
format(
add(fromUnixTime(Number(interval.current_epoch_start_unix)), {
seconds: Number(interval.epoch_length_seconds),
}),
'HH:mm',
),
);
};
const getPendingEvents = async () => {
const events = await getPendingIntervalEvents();
const latestEvent = events.reverse().find((evt) => 'ChangeMixCostParams' in evt.event) as unknown as
| {
id: number;
event: {
ChangeMixCostParams: ChangeMixCostParams;
};
}
| undefined;
if (latestEvent) {
setPendingUpdates(latestEvent.event.ChangeMixCostParams.new_costs);
}
};
useEffect(() => {
getIntervalAsDate();
getPendingEvents();
}, []);
const onSubmit = async (data: { operatorCost?: string; profitMargin?: string }) => {
if (data.operatorCost && data.profitMargin) {
const MixNodeCostParams = {
@@ -55,6 +102,8 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode
};
try {
await updateMixnodeCostParams(MixNodeCostParams);
await getPendingEvents();
reset();
setOpenConfirmationModal(true);
} catch (error) {
Console.error(error);
@@ -64,37 +113,18 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode
return (
<Grid container xs item>
{open && (
<Alert
severity="info"
action={
<IconButton
aria-label="close"
color="inherit"
size="small"
onClick={() => {
setOpen(false);
}}
>
<CloseIcon fontSize="inherit" />
</IconButton>
}
sx={{
width: 1,
px: 2,
borderRadius: 0,
bgcolor: 'background.default',
color: (theme) => theme.palette.nym.nymWallet.text.blue,
'& .MuiAlert-icon': { color: (theme) => theme.palette.nym.nymWallet.text.blue, mr: 1 },
}}
>
<Box sx={{ fontWeight: 600 }}>
Profit margin can be changed once a month, your changes will be applied in the next interval
</Box>
</Alert>
)}
<Alert
title={
<>
<Box component="span" sx={{ fontWeight: 600, mr: 2 }}>
{`Next epoch ${nextEpoch}`}
</Box>
<Box component="span" sx={{ fontWeight: 600 }}>{`Next interval: ${intervalTime}`}</Box>
</>
}
/>
<Grid container direction="column">
<Grid item container direction="row" alignItems="left" justifyContent="space-between" padding={3} spacing={1}>
<Grid item container alignItems="left" justifyContent="space-between" padding={3} spacing={1}>
<Grid item>
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1 }}>
Profit Margin
@@ -107,30 +137,37 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode
color: (t) => (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'),
}}
>
Changes to PM will be applied in the next interval.
Changes to PM will be applied in the next interval.
</Typography>
</Grid>
<Grid spacing={3} container item alignItems="center" sm={12} md={6}>
{isMixnode(bondedNode) && (
<Grid item width={1}>
<TextField
{...register('profitMargin')}
name="profitMargin"
label="Profit margin"
fullWidth
error={!!errors.profitMargin}
helperText={errors.profitMargin?.message}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Box>%</Box>
</InputAdornment>
),
}}
/>
</Grid>
)}
</Grid>
{isMixnode(bondedNode) && (
<Grid item xs={12} xl={6}>
<TextField
{...register('profitMargin')}
name="profitMargin"
label="Profit margin"
fullWidth
error={!!errors.profitMargin}
helperText={errors.profitMargin?.message}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Box>%</Box>
</InputAdornment>
),
}}
/>
{pendingUpdates && (
<FormHelperText>
Your last change to{' '}
<Typography variant="caption" fontWeight="bold">
{(+pendingUpdates.profit_margin_percent * 100).toFixed(2)}%{' '}
</Typography>
will be applied in the next interval
</FormHelperText>
)}
</Grid>
)}
</Grid>
<Divider flexItem />
<Grid item container direction="row" alignItems="left" justifyContent="space-between" padding={3} spacing={1}>
@@ -146,8 +183,8 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode
color: (t) => (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'),
}}
>
Changes to cost will be applied in the next interval.
</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}>
@@ -166,6 +203,16 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode
),
}}
/>
{pendingUpdates && (
<FormHelperText>
Your last change to{' '}
<Typography variant="caption" fontWeight="bold">
{pendingUpdates.interval_operating_cost.amount}{' '}
{pendingUpdates?.interval_operating_cost.denom.toUpperCase()}{' '}
</Typography>
will be applied in the next interval
</FormHelperText>
)}
</Grid>
</Grid>
</Grid>
@@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react';
import React, { useState } from 'react';
import { Box, Button, Divider, Grid } from '@mui/material';
import { TBondedMixnode, TBondedGateway } from '../../../../../context/bonding';
import { InfoSettings } from './InfoSettings';
+6 -1
View File
@@ -1,4 +1,4 @@
import { DecCoin, TNodeType, TransactionExecuteResult } from '@nymproject/types';
import { DecCoin, MixNodeCostParams, TNodeType, TransactionExecuteResult } from '@nymproject/types';
import { TPoolOption } from 'src/components';
export type FormStep = 1 | 2 | 3 | 4;
@@ -69,3 +69,8 @@ export interface BondState {
bondStatus: BondStatus;
error?: string | null;
}
export interface ChangeMixCostParams {
mix_id: number;
new_costs: MixNodeCostParams;
}
+4
View File
@@ -7,6 +7,7 @@ import {
GatewayBond,
RewardEstimationResponse,
WrappedDelegationEvent,
PendingIntervalEvent,
} from '@nymproject/types';
import { Interval, TNodeDescription } from 'src/types';
import { invokeWrapper } from './wrapper';
@@ -44,3 +45,6 @@ export const getNumberOfMixnodeDelegators = async (mixId: number) =>
export const getNodeDescription = async (host: string, port: number) =>
invokeWrapper<TNodeDescription>('get_mix_node_description', { host, port });
export const getPendingIntervalEvents = async () =>
invokeWrapper<PendingIntervalEvent[]>('get_pending_interval_events');
+8 -2
View File
@@ -1,2 +1,8 @@
export interface Interval { id: number, epochs_in_interval: number, current_epoch_start_unix: bigint, current_epoch_id: number, epoch_length_seconds: bigint, total_elapsed_epochs: number, }
export interface Interval {
id: number;
epochs_in_interval: number;
current_epoch_start_unix: bigint;
current_epoch_id: number;
epoch_length_seconds: bigint;
total_elapsed_epochs: number;
}
+4 -4
View File
@@ -147,17 +147,17 @@ export const toPercentFloatString = (value: string) => (Number(value) / 100).toS
export const toPercentIntegerString = (value: string) => Math.round(Number(value) * 100).toString();
/**
* Converts a decimal number of NYM to a pretty representation
* Converts a decimal number to a pretty representation
* with fixed decimal places.
*
* @param nym - a decimal number of NYM
* @param val - a decimal number of string form
* @param dp - number of decimal places (4 by default ie. 0.0000)
* @returns A prettyfied decimal number
*/
export const toDisplay = (nym: string | number | Big, dp = 4) => {
export const toDisplay = (val: string | number | Big, dp = 4) => {
let displayValue;
try {
displayValue = Big(nym).toFixed(dp);
displayValue = Big(val).toFixed(dp);
} catch (e: any) {
Console.warn(`${displayValue} not a valid decimal number: ${e}`);
}