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:
@@ -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"),
|
||||
)]
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
+11
-31
@@ -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 node’s 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 node’s config file too
|
||||
</Box>
|
||||
}
|
||||
dismissable
|
||||
/>
|
||||
<Grid container>
|
||||
<Grid item container direction="row" alignItems="left" justifyContent="space-between" padding={3}>
|
||||
<Grid item>
|
||||
|
||||
+109
-62
@@ -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
-1
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user