diff --git a/nym-wallet/nym-wallet-types/src/network.rs b/nym-wallet/nym-wallet-types/src/network.rs index 2a07812fa1..1be3c80732 100644 --- a/nym-wallet/nym-wallet-types/src/network.rs +++ b/nym-wallet/nym-wallet-types/src/network.rs @@ -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 { 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"), )] } diff --git a/nym-wallet/src/components/Alert.tsx b/nym-wallet/src/components/Alert.tsx new file mode 100644 index 0000000000..38bd2cb989 --- /dev/null +++ b/nym-wallet/src/components/Alert.tsx @@ -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 ( + theme.palette.nym.nymWallet.text.blue, + '& .MuiAlert-icon': { color: 'nym.nymWallet.text.blue', mr: 1 }, + }} + action={ + dismissable && ( + + + + ) + } + > + {title} + + ); +}; diff --git a/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts b/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts index aa5752f4aa..3d6e407301 100644 --- a/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts +++ b/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts @@ -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({ diff --git a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/InfoSettings.tsx b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/InfoSettings.tsx index fedc986d8c..d48f9c20bf 100644 --- a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/InfoSettings.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/InfoSettings.tsx @@ -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(false); const theme = useTheme(); @@ -54,33 +53,14 @@ export const InfoSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBon return ( - {open && ( - { - setOpen(false); - }} - > - - - } - 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 }, - }} - > - Your changes will be ONLY saved on the display. Remember to change the - values on your node’s config file too. - - )} + + Your changes will be ONLY saved on the display. Remember to change the values on your node’s config file too + + } + dismissable + /> 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 d2bceafd1f..0e30dc63a7 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 @@ -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(false); - + const [intervalTime, setIntervalTime] = useState(); + const [nextEpoch, setNextEpoch] = useState(); + const [pendingUpdates, setPendingUpdates] = useState(); 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 ( - {open && ( - { - setOpen(false); - }} - > - - - } - 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 }, - }} - > - - Profit margin can be changed once a month, your changes will be applied in the next interval - - - )} + + + {`Next epoch ${nextEpoch}`} + + {`Next interval: ${intervalTime}`} + + } + /> - + 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. - - {isMixnode(bondedNode) && ( - - - % - - ), - }} - /> - - )} - + {isMixnode(bondedNode) && ( + + + % + + ), + }} + /> + {pendingUpdates && ( + + Your last change to{' '} + + {(+pendingUpdates.profit_margin_percent * 100).toFixed(2)}%{' '} + + will be applied in the next interval + + )} + + )} @@ -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. - + Changes to cost will be applied in the next interval. + @@ -166,6 +203,16 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode ), }} /> + {pendingUpdates && ( + + Your last change to{' '} + + {pendingUpdates.interval_operating_cost.amount}{' '} + {pendingUpdates?.interval_operating_cost.denom.toUpperCase()}{' '} + + will be applied in the next interval + + )} diff --git a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/index.tsx b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/index.tsx index 05f8193447..1e36b195f2 100644 --- a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/index.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/index.tsx @@ -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'; diff --git a/nym-wallet/src/pages/bonding/types.ts b/nym-wallet/src/pages/bonding/types.ts index d7ed865f49..a15ac8b522 100644 --- a/nym-wallet/src/pages/bonding/types.ts +++ b/nym-wallet/src/pages/bonding/types.ts @@ -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; +} diff --git a/nym-wallet/src/requests/queries.ts b/nym-wallet/src/requests/queries.ts index 00a28aa03b..466c83efc1 100644 --- a/nym-wallet/src/requests/queries.ts +++ b/nym-wallet/src/requests/queries.ts @@ -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('get_mix_node_description', { host, port }); + +export const getPendingIntervalEvents = async () => + invokeWrapper('get_pending_interval_events'); diff --git a/nym-wallet/src/types/rust/Interval.ts b/nym-wallet/src/types/rust/Interval.ts index 2fb85edb76..c1526be658 100644 --- a/nym-wallet/src/types/rust/Interval.ts +++ b/nym-wallet/src/types/rust/Interval.ts @@ -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, } \ No newline at end of file +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; +} diff --git a/nym-wallet/src/utils/index.ts b/nym-wallet/src/utils/index.ts index 16d7bcb85e..8d41218392 100644 --- a/nym-wallet/src/utils/index.ts +++ b/nym-wallet/src/utils/index.ts @@ -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}`); }