Merge pull request #1665 from nymtech/348-bonding-settings

Wallet: Bonded node settings
This commit is contained in:
Gala
2022-10-10 14:09:26 +02:00
committed by GitHub
21 changed files with 1062 additions and 386 deletions
@@ -1,6 +1,8 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { Box, Button, Stack, Typography } from '@mui/material';
import { Link } from '@nymproject/react/link/Link';
import { isMixnode } from 'src/types';
import { TBondedMixnode, urls } from 'src/context';
import { NymCard } from 'src/components';
import { Network } from 'src/types';
@@ -60,6 +62,7 @@ export const BondedMixnode = ({
network?: Network;
onActionSelect: (action: TBondedMixnodeActions) => void;
}) => {
const navigate = useNavigate();
const {
name,
stake,
@@ -133,14 +136,16 @@ export const BondedMixnode = ({
</Stack>
}
Action={
<Button
variant="text"
color="secondary"
onClick={() => onActionSelect('nodeSettings')}
startIcon={<NodeIcon />}
>
Node Settings
</Button>
isMixnode(mixnode) && (
<Button
variant="text"
color="secondary"
onClick={() => navigate('/bonding/node-settings')}
startIcon={<NodeIcon />}
>
Settings
</Button>
)
}
>
<NodeTable headers={headers} cells={cells} />
@@ -49,3 +49,31 @@ export const amountSchema = Yup.object().shape({
}),
profitMargin: Yup.number().required('Profit Percentage is required').min(0).max(100),
});
export const bondedInfoParametersValidationSchema = Yup.object().shape({
host: Yup.string()
.required('A host is required')
.test('valid-host', 'A valid host is required', (value) => (value ? isValidHostname(value) : false)),
version: Yup.string()
.required('A version is required')
.test('valid-version', 'A valid version is required', (value) => (value ? validateVersion(value) : false)),
mixPort: Yup.number()
.required('A mixport is required')
.test('valid-mixport', 'A valid mixport is required', (value) => (value ? validateRawPort(value) : false)),
verlocPort: Yup.number()
.required('A verloc port is required')
.test('valid-verloc', 'A valid verloc port is required', (value) => (value ? validateRawPort(value) : false)),
httpApiPort: Yup.number()
.required('A http-api port is required')
.test('valid-http', 'A valid http-api port is required', (value) => (value ? validateRawPort(value) : false)),
});
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'),
})
@@ -12,6 +12,7 @@ import { simulateUpdateMixnodeCostParams, simulateVestingUpdateMixnodeCostParams
import { LoadingModal } from 'src/components/Modals/LoadingModal';
import { FeeDetails } from '@nymproject/types';
//Now we are using the node setting page instead of this modal
export const NodeSettings = ({
currentPm,
isVesting,
@@ -19,13 +20,13 @@ export const NodeSettings = ({
onClose,
onError,
}: {
currentPm: TBondedMixnode['profitMargin'];
isVesting: boolean;
currentPm?: TBondedMixnode['profitMargin'];
isVesting?: boolean;
onConfirm: (profitMargin: string, fee?: FeeDetails) => Promise<void>;
onClose: () => void;
onError: (err: string) => void;
}) => {
const [pm, setPm] = useState(currentPm.toString());
const [pm, setPm] = useState(currentPm?.toString());
const [error, setError] = useState(false);
const { fee, getFee, resetFeeState, isFeeLoading, feeError } = useGetFee();
@@ -52,13 +53,15 @@ export const NodeSettings = ({
return;
}
// TODO: this will have to be updated with allowing users to provide their operating cost in the form
const defaultCostParams = await attachDefaultOperatingCost(toPercentFloatString(pm));
if (pm) {
// TODO: this will have to be updated with allowing users to provide their operating cost in the form
const defaultCostParams = await attachDefaultOperatingCost(toPercentFloatString(pm));
if (isVesting) {
await getFee(simulateVestingUpdateMixnodeCostParams, defaultCostParams);
} else {
await getFee(simulateUpdateMixnodeCostParams, defaultCostParams);
if (isVesting) {
await getFee(simulateVestingUpdateMixnodeCostParams, defaultCostParams);
} else {
await getFee(simulateUpdateMixnodeCostParams, defaultCostParams);
}
}
};
@@ -74,7 +77,7 @@ export const NodeSettings = ({
if (isFeeLoading) return <LoadingModal />;
if (fee)
if (fee && pm)
return (
<ConfirmTx
open
@@ -105,7 +108,7 @@ export const NodeSettings = ({
okLabel="Next"
onClose={onClose}
>
<Tabs tabs={['System variables']} selectedTab={0} disableActiveTabHighlight />
<Tabs tabs={['System variables']} selectedTab={'System variables'} disableActiveTabHighlight />
<Box sx={{ p: 3 }}>
<Typography fontWeight={600} sx={{ mb: 1 }}>
Set profit margin
@@ -1,5 +1,5 @@
import * as React from 'react';
import { Box, TextField, Typography } from '@mui/material';
import { Typography } from '@mui/material';
import { useEffect, useState } from 'react';
import { TBondedGateway, TBondedMixnode } from 'src/context';
import { useGetFee } from 'src/hooks/useGetFee';
@@ -13,8 +13,6 @@ import {
simulateVestingUnbondGateway,
simulateVestingUnbondMixnode,
} from '../../../requests';
import { ConfirmationModal } from '../../Modals/ConfirmationModal';
import { Error } from '../../Error';
interface Props {
node: TBondedMixnode | TBondedGateway;
@@ -25,9 +23,6 @@ interface Props {
export const UnbondModal = ({ node, onConfirm, onClose, onError }: Props) => {
const { fee, isFeeLoading, getFee, feeError } = useGetFee();
const [isConfirmed, setIsConfirmed] = useState(false);
const [showConfirmModal, setShowConfirmModal] = useState(true);
const [confirmField, setConfirmField] = useState('');
useEffect(() => {
if (feeError) {
@@ -53,63 +48,18 @@ export const UnbondModal = ({ node, onConfirm, onClose, onError }: Props) => {
}
}, [node]);
if (showConfirmModal) {
return (
<ConfirmationModal
title="Unbond"
confirmButton="UNBOND"
open={showConfirmModal}
onConfirm={() => {
setIsConfirmed(true);
setShowConfirmModal(false);
}}
onClose={onClose}
disabled={confirmField !== 'UNBOND'}
>
<Typography fontWeight={600} mb={2}>
If you unbond your node you will loose all your delegators!
</Typography>
<Error message="This action is irreversible and it will not be possible to restore the current state again" />
<Typography mt={2} mb={2}>
To unbond, type{' '}
<Typography display="inline" component="span" sx={{ color: (t) => t.palette.nym.highlight }}>
UNBOND
</Typography>{' '}
in the field below and click UNBOND button
</Typography>
<TextField fullWidth value={confirmField} onChange={(e) => setConfirmField(e.target.value)} />
</ConfirmationModal>
);
}
if (isConfirmed) {
return (
<SimpleModal
open
header="Unbond"
subHeader="Unbond and remove your node from the mixnet"
okLabel="Unbond"
onOk={onConfirm}
onClose={onClose}
>
<ModalListItem
label="Amount to unbond"
value={`${node.bond.amount} ${node.bond.denom.toUpperCase()}`}
divider
/>
{isMixnode(node) && (
<ModalListItem
label="Operator rewards"
value={
node.operatorRewards ? `${node.operatorRewards.amount} ${node.operatorRewards.denom.toUpperCase()}` : '-'
}
divider
/>
)}
<ModalFee isLoading={isFeeLoading} fee={fee} divider />
<Typography fontSize="small">Tokens will be transferred to the account you are logged in with now</Typography>
</SimpleModal>
);
}
return <Box />;
return (
<SimpleModal
open
header="Unbond"
subHeader="Unbond and remove your node from the mixnet"
okLabel="Unbond"
onOk={onConfirm}
onClose={onClose}
>
<ModalListItem label="Total to unbond" value={`${node.bond.amount} ${node.bond.denom.toUpperCase()}`} divider />
<ModalFee isLoading={isFeeLoading} fee={fee} divider />
<Typography fontSize="small">Tokens will be transferred to the account you are logged in with now</Typography>
</SimpleModal>
);
};
@@ -2,6 +2,7 @@ import React from 'react';
import { Box, Button, Modal, Stack, SxProps, Typography } from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import ErrorOutline from '@mui/icons-material/ErrorOutline';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import { StyledBackButton } from 'src/components/StyledBackButton';
import { modalStyle } from './styles';
@@ -9,8 +10,10 @@ export const SimpleModal: React.FC<{
open: boolean;
hideCloseIcon?: boolean;
displayErrorIcon?: boolean;
displayInfoIcon?: boolean;
headerStyles?: SxProps;
subHeaderStyles?: SxProps;
buttonFullWidth?: boolean;
onClose?: () => void;
onOk?: () => Promise<void>;
onBack?: () => void;
@@ -24,8 +27,10 @@ export const SimpleModal: React.FC<{
open,
hideCloseIcon,
displayErrorIcon,
displayInfoIcon,
headerStyles,
subHeaderStyles,
buttonFullWidth,
onClose,
okDisabled,
onOk,
@@ -40,6 +45,7 @@ export const SimpleModal: React.FC<{
<Modal open={open} onClose={onClose} BackdropProps={backdropProps}>
<Box sx={{ border: (t) => `1px solid ${t.palette.nym.nymWallet.modal.border}`, ...modalStyle, ...sx }}>
{displayErrorIcon && <ErrorOutline color="error" sx={{ mb: 3 }} />}
{displayInfoIcon && <InfoOutlinedIcon sx={{ mb: 2, color: (theme) => theme.palette.nym.nymWallet.text.blue }} />}
<Stack direction="row" justifyContent="space-between" alignItems="center">
{typeof header === 'string' ? (
<Typography fontSize={20} fontWeight={600} sx={{ color: 'text.primary', ...headerStyles }}>
@@ -64,8 +70,8 @@ export const SimpleModal: React.FC<{
{children}
{(onOk || onBack) && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 1 }}>
{onBack && <StyledBackButton onBack={onBack} sx={{ mt: 3 }} />}
<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 }}>
{okLabel}
+26 -16
View File
@@ -1,13 +1,25 @@
import React from 'react';
import { Tab, Tabs as MuiTabs } from '@mui/material';
import { Tab, Tabs as MuiTabs, SxProps } from '@mui/material';
export const Tabs: React.FC<{
tabs: string[];
selectedTab: number;
type Props = {
tabs: readonly string[];
selectedTab: string;
disabled?: boolean;
onChange?: (event: React.SyntheticEvent, tab: number) => void;
onChange?: (event: React.SyntheticEvent, tab: string) => void;
disableActiveTabHighlight?: boolean;
}> = ({ tabs, selectedTab, disabled, disableActiveTabHighlight, onChange }) => (
tabSx?: SxProps;
tabIndicatorStyles?: {};
};
export const Tabs = ({
tabs,
selectedTab,
disabled,
disableActiveTabHighlight,
onChange,
tabSx,
tabIndicatorStyles,
}: Props) => (
<MuiTabs
value={selectedTab}
onChange={onChange}
@@ -16,20 +28,18 @@ export const Tabs: React.FC<{
borderTop: '1px solid',
borderBottom: '1px solid',
borderColor: (theme) => theme.palette.nym.nymWallet.background.greyStroke,
...tabSx,
}}
textColor="inherit"
TabIndicatorProps={
disableActiveTabHighlight
? {
style: {
opacity: 0,
},
}
: {}
}
TabIndicatorProps={{
style: {
opacity: disableActiveTabHighlight ? 0 : 1,
...tabIndicatorStyles,
},
}}
>
{tabs.map((tabName) => (
<Tab key={tabName} label={tabName} sx={{ textTransform: 'capitalize' }} disabled={disabled} />
<Tab key={tabName} label={tabName} sx={{ textTransform: 'capitalize' }} value={tabName} disabled={disabled} />
))}
</MuiTabs>
);
+28 -14
View File
@@ -46,8 +46,12 @@ export type TBondedMixnode = {
delegators: number;
status: MixnodeStatus;
proxy?: string;
operatorCost?: string;
host: string;
httpApiPort: number;
mixPort: number;
verlocPort: number;
version: string;
operatorCost?: string;
};
export interface TBondedGateway {
@@ -57,6 +61,11 @@ export interface TBondedGateway {
bond: DecCoin;
location?: string; // TODO not yet available, only available in Network Explorer API
proxy?: string;
host: string;
httpApiPort: number;
mixPort: number;
verlocPort: number;
version: string;
}
export type TokenPool = 'locked' | 'balance';
@@ -159,7 +168,7 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
const refresh = useCallback(async () => {
setIsLoading(true);
if (ownership.hasOwnership && ownership.nodeType === 'mixnode' && clientDetails) {
if (ownership.hasOwnership && clientDetails) {
try {
const data = await getMixnodeBondDetails();
let operatorRewards;
@@ -169,28 +178,33 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
Console.warn(`get_operator_rewards request failed: ${e}`);
}
if (data) {
const { bond_information, rewarding_details } = data;
const { status, stakeSaturation, operatorCost } = await getAdditionalMixnodeDetails(data.bond_information.id);
const nodeDescription = await getNodeDescription(
data.bond_information.mix_node.host,
data.bond_information.mix_node.http_api_port,
bond_information.mix_node.host,
bond_information.mix_node.http_api_port,
);
setBondedNode({
name: nodeDescription?.name,
identityKey: data.bond_information.mix_node.identity_key,
ip: '',
identityKey: bond_information.mix_node.identity_key,
ip: bond_information.id,
stake: {
amount: calculateStake(data.rewarding_details.operator, data.rewarding_details.delegates).toString(),
denom: data.bond_information.original_pledge.denom,
amount: calculateStake(rewarding_details.operator, data.rewarding_details.delegates).toString(),
denom: bond_information.original_pledge.denom,
},
bond: data.bond_information.original_pledge,
profitMargin: toPercentIntegerString(data.rewarding_details.cost_params.profit_margin_percent),
delegators: data.rewarding_details.unique_delegations,
proxy: data.bond_information.proxy,
bond: bond_information.original_pledge,
profitMargin: toPercentIntegerString(rewarding_details.cost_params.profit_margin_percent),
delegators: rewarding_details.unique_delegations,
proxy: bond_information.proxy,
operatorRewards,
status,
stakeSaturation,
host: bond_information.mix_node.host.replace(/\s/g, ''),
httpApiPort: bond_information.mix_node.http_api_port,
mixPort: bond_information.mix_node.mix_port,
verlocPort: bond_information.mix_node.verloc_port,
version: bond_information.mix_node.version,
operatorCost,
host: data.bond_information.mix_node.host,
} as TBondedMixnode);
}
} catch (e: any) {
@@ -199,7 +213,7 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
}
}
if (ownership.hasOwnership && ownership.nodeType === 'gateway') {
if (ownership.hasOwnership) {
try {
const data = await getGatewayBondDetails();
if (data) {
+9
View File
@@ -18,6 +18,10 @@ const bondedMixnodeMock: TBondedMixnode = {
status: 'active',
operatorCost: '2',
host: '1.2.3.4',
httpApiPort: 8000,
mixPort: 1789,
verlocPort: 1790,
version: '1.0.2',
};
const bondedGatewayMock: TBondedGateway = {
@@ -25,6 +29,11 @@ const bondedGatewayMock: TBondedGateway = {
identityKey: 'WayM2fYbtN6kxMwp1TrmQ4VwPks3URR5pBgWPWhzT98F',
ip: '112.43.234.57',
bond: { denom: 'nym', amount: '1234' },
host: '1.2.34.5 ',
httpApiPort: 8000,
mixPort: 1789,
verlocPort: 1790,
version: '1.0.2',
};
const TxResultMock: TransactionExecuteResult = {
+183
View File
@@ -0,0 +1,183 @@
import React, { useContext, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { FeeDetails } from '@nymproject/types';
import { TPoolOption } from 'src/components';
import { Bond } from 'src/components/Bonding/Bond';
import { BondedMixnode } from 'src/components/Bonding/BondedMixnode';
import { TBondedMixnodeActions } from 'src/components/Bonding/BondedMixnodeActions';
import { BondGatewayModal } from 'src/components/Bonding/modals/BondGatewayModal';
import { BondMixnodeModal } from 'src/components/Bonding/modals/BondMixnodeModal';
import { ConfirmationDetailProps, ConfirmationDetailsModal } from 'src/components/Bonding/modals/ConfirmationModal';
import { ErrorModal } from 'src/components/Modals/ErrorModal';
import { LoadingModal } from 'src/components/Modals/LoadingModal';
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';
const Bonding = () => {
const [showModal, setShowModal] = useState<'bond-mixnode' | 'bond-gateway' | 'bond-more' | 'unbond' | 'redeem'>();
const [confirmationDetails, setConfirmationDetails] = useState<ConfirmationDetailProps>();
const {
network,
clientDetails,
userBalance: { originalVesting },
} = useContext(AppContext);
const navigate = useNavigate();
const { bondedNode, bondMixnode, bondGateway, unbond, redeemRewards, isLoading, checkOwnership } =
useBondingContext();
const handleCloseModal = async () => {
setShowModal(undefined);
await checkOwnership();
};
const handleError = (error: string) => {
setShowModal(undefined);
setConfirmationDetails({
status: 'error',
title: 'An error occurred',
subtitle: error,
});
};
const handleBondMixnode = async (data: TBondMixNodeArgs, tokenPool: TPoolOption) => {
setShowModal(undefined);
const tx = await bondMixnode(data, tokenPool);
setConfirmationDetails({
status: 'success',
title: 'Bond successful',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
return undefined;
};
const handleBondGateway = async (data: TBondGatewayArgs, tokenPool: TPoolOption) => {
setShowModal(undefined);
const tx = await bondGateway(data, tokenPool);
setConfirmationDetails({
status: 'success',
title: 'Bond successful',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
};
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);
setConfirmationDetails({
status: 'success',
title: 'Rewards redeemed successfully',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
};
const handleBondedMixnodeAction = (action: TBondedMixnodeActions) => {
switch (action) {
case 'bondMore': {
setShowModal('bond-more');
break;
}
case 'unbond': {
navigate('/bonding/node-settings', { state: 'unbond' });
break;
}
case 'redeem': {
setShowModal('redeem');
break;
}
default: {
return undefined;
}
}
return undefined;
};
return (
<Box sx={{ mt: 4 }}>
{!bondedNode && <Bond disabled={isLoading} onBond={() => setShowModal('bond-mixnode')} />}
{bondedNode && isMixnode(bondedNode) && (
<BondedMixnode
mixnode={bondedNode}
network={network}
onActionSelect={(action) => handleBondedMixnodeAction(action)}
/>
)}
{bondedNode && isGateway(bondedNode) && (
<BondedGateway gateway={bondedNode} onActionSelect={handleBondedMixnodeAction} network={network} />
)}
{showModal === 'bond-mixnode' && (
<BondMixnodeModal
denom={clientDetails?.display_mix_denom || 'nym'}
hasVestingTokens={Boolean(originalVesting)}
onBondMixnode={handleBondMixnode}
onSelectNodeType={() => setShowModal('bond-gateway')}
onClose={() => setShowModal(undefined)}
onError={handleError}
/>
)}
{showModal === 'bond-gateway' && (
<BondGatewayModal
denom={clientDetails?.display_mix_denom || 'nym'}
hasVestingTokens={Boolean(originalVesting)}
onBondGateway={handleBondGateway}
onSelectNodeType={() => setShowModal('bond-mixnode')}
onClose={() => setShowModal(undefined)}
onError={handleError}
/>
)}
{showModal === 'redeem' && bondedNode && isMixnode(bondedNode) && (
<RedeemRewardsModal
node={bondedNode}
onClose={() => setShowModal(undefined)}
onConfirm={handleRedeemReward}
onError={handleError}
/>
)}
{confirmationDetails && confirmationDetails.status === 'success' && (
<ConfirmationDetailsModal
title={confirmationDetails.title}
subtitle={confirmationDetails.subtitle || 'This operation can take up to one hour to process'}
status={confirmationDetails.status}
txUrl={confirmationDetails.txUrl}
onClose={() => {
setConfirmationDetails(undefined);
handleCloseModal();
}}
/>
)}
{confirmationDetails && confirmationDetails.status === 'error' && (
<ErrorModal open message={confirmationDetails.subtitle} onClose={() => setConfirmationDetails(undefined)} />
)}
{isLoading && <LoadingModal />}
</Box>
);
};
export const BondingPage = () => (
<BondingContextProvider>
<Bonding />
</BondingContextProvider>
);
@@ -1,5 +1,5 @@
import * as React from 'react';
import { BondingPage } from './index';
import { BondingPage } from './Bonding';
import { MockBondingContextProvider } from '../../context/mocks/bonding';
export default {
+2 -266
View File
@@ -1,266 +1,2 @@
import React, { useContext, useEffect, useState } from 'react';
import { FeeDetails, DecCoin } from '@nymproject/types';
import { TPoolOption } from 'src/components';
import { Bond } from 'src/components/Bonding/Bond';
import { BondedMixnode } from 'src/components/Bonding/BondedMixnode';
import { TBondedMixnodeActions } from 'src/components/Bonding/BondedMixnodeActions';
import { BondGatewayModal } from 'src/components/Bonding/modals/BondGatewayModal';
import { BondMixnodeModal } from 'src/components/Bonding/modals/BondMixnodeModal';
import { ConfirmationDetailProps, ConfirmationDetailsModal } from 'src/components/Bonding/modals/ConfirmationModal';
import { NodeSettings } from 'src/components/Bonding/modals/NodeSettingsModal';
import { UnbondModal } from 'src/components/Bonding/modals/UnbondModal';
import { ErrorModal } from 'src/components/Modals/ErrorModal';
import { LoadingModal } from 'src/components/Modals/LoadingModal';
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 { Box } from '@mui/material';
import { BondingContextProvider, useBondingContext } from '../../context';
import { BondMoreModal } from '../../components/Bonding/modals/BondMoreModal';
const Bonding = () => {
const [showModal, setShowModal] = useState<
'bond-mixnode' | 'bond-gateway' | 'bond-more' | 'unbond' | 'redeem' | 'node-settings'
>();
const [confirmationDetails, setConfirmationDetails] = useState<ConfirmationDetailProps>();
const {
network,
clientDetails,
userBalance: { originalVesting, balance, tokenAllocation },
} = useContext(AppContext);
const {
bondedNode,
bondMixnode,
bondGateway,
unbond,
bondMore,
updateMixnode,
redeemRewards,
isLoading,
checkOwnership,
error,
} = useBondingContext();
useEffect(() => {
if (error) {
setShowModal(undefined);
setConfirmationDetails({
status: 'error',
title: 'An error occurred',
subtitle: error,
});
}
}, [error]);
const handleCloseModal = async () => {
setShowModal(undefined);
await checkOwnership();
};
const handleError = (e: string) => {
setShowModal(undefined);
setConfirmationDetails({
status: 'error',
title: 'An error occurred',
subtitle: e,
});
};
const handleBondMixnode = async (data: TBondMixNodeArgs, tokenPool: TPoolOption) => {
setShowModal(undefined);
const tx = await bondMixnode(data, tokenPool);
setConfirmationDetails({
status: 'success',
title: 'Bond successful',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
return undefined;
};
const handleBondGateway = async (data: TBondGatewayArgs, tokenPool: TPoolOption) => {
setShowModal(undefined);
const tx = await bondGateway(data, tokenPool);
setConfirmationDetails({
status: 'success',
title: 'Bond successful',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
};
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 handleUpdateProfitMargin = async (profitMargin: string, fee?: FeeDetails) => {
setShowModal(undefined);
const tx = await updateMixnode(profitMargin, fee);
setConfirmationDetails({
status: 'success',
title: 'Profit margin update successful',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
};
const handleRedeemReward = async (fee?: FeeDetails) => {
setShowModal(undefined);
const tx = await redeemRewards(fee);
setConfirmationDetails({
status: 'success',
title: 'Rewards redeemed successfully',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
};
const handleBondedMixnodeAction = (action: TBondedMixnodeActions) => {
switch (action) {
case 'bondMore': {
setShowModal('bond-more');
break;
}
case 'unbond': {
setShowModal('unbond');
break;
}
case 'redeem': {
setShowModal('redeem');
break;
}
case 'nodeSettings': {
setShowModal('node-settings');
break;
}
default: {
return undefined;
}
}
return undefined;
};
const handleBondMore = async ({
signature,
additionalBond,
}: {
additionalBond: DecCoin;
signature: string;
tokenPool: TPoolOption;
}) => {
setShowModal(undefined);
const tx = await bondMore(signature, additionalBond);
setConfirmationDetails({
status: 'success',
title: 'Bond more successful',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
};
return (
<Box sx={{ mt: 4 }}>
{!bondedNode && <Bond disabled={isLoading} onBond={() => setShowModal('bond-mixnode')} />}
{bondedNode && isMixnode(bondedNode) && (
<BondedMixnode
mixnode={bondedNode}
network={network}
onActionSelect={(action) => handleBondedMixnodeAction(action)}
/>
)}
{bondedNode && isGateway(bondedNode) && (
<BondedGateway gateway={bondedNode} onActionSelect={handleBondedMixnodeAction} network={network} />
)}
{showModal === 'bond-mixnode' && (
<BondMixnodeModal
denom={clientDetails?.display_mix_denom || 'nym'}
hasVestingTokens={Boolean(originalVesting)}
onBondMixnode={handleBondMixnode}
onSelectNodeType={() => setShowModal('bond-gateway')}
onClose={() => setShowModal(undefined)}
onError={handleError}
/>
)}
{showModal === 'bond-gateway' && (
<BondGatewayModal
denom={clientDetails?.display_mix_denom || 'nym'}
hasVestingTokens={Boolean(originalVesting)}
onBondGateway={handleBondGateway}
onSelectNodeType={() => setShowModal('bond-mixnode')}
onClose={() => setShowModal(undefined)}
onError={handleError}
/>
)}
{showModal === 'bond-more' && bondedNode && (
<BondMoreModal
currentBond={bondedNode.bond}
userBalance={balance?.printable_balance}
hasVestingTokens={Number(tokenAllocation?.vesting) > 0}
onConfirm={handleBondMore}
onClose={() => setShowModal(undefined)}
/>
)}
{showModal === 'unbond' && bondedNode && (
<UnbondModal
node={bondedNode}
onClose={() => setShowModal(undefined)}
onConfirm={handleUnbond}
onError={handleError}
/>
)}
{showModal === 'redeem' && bondedNode && isMixnode(bondedNode) && (
<RedeemRewardsModal
node={bondedNode}
onClose={() => setShowModal(undefined)}
onConfirm={handleRedeemReward}
onError={handleError}
/>
)}
{showModal === 'node-settings' && bondedNode && isMixnode(bondedNode) && (
<NodeSettings
currentPm={bondedNode.profitMargin}
isVesting={Boolean(bondedNode.proxy)}
onConfirm={handleUpdateProfitMargin}
onClose={() => setShowModal(undefined)}
onError={handleError}
/>
)}
{confirmationDetails && confirmationDetails.status === 'success' && (
<ConfirmationDetailsModal
title={confirmationDetails.title}
subtitle={confirmationDetails.subtitle || 'This operation can take up to one hour to process'}
status={confirmationDetails.status}
txUrl={confirmationDetails.txUrl}
onClose={() => {
setConfirmationDetails(undefined);
handleCloseModal();
}}
/>
)}
{confirmationDetails && confirmationDetails.status === 'error' && (
<ErrorModal open message={confirmationDetails.subtitle} onClose={() => setConfirmationDetails(undefined)} />
)}
{isLoading && <LoadingModal />}
</Box>
);
};
export const BondingPage = () => (
<BondingContextProvider>
<Bonding />
</BondingContextProvider>
);
export * from './Bonding';
export * from './node-settings/NodeSettings';
@@ -0,0 +1,141 @@
import React, { useContext, useState, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { FeeDetails } from '@nymproject/types';
import { Box, Typography, Stack, IconButton, Divider } from '@mui/material';
import { Close } from '@mui/icons-material';
import { useTheme } from '@mui/material/styles';
import { ConfirmationDetailProps, ConfirmationDetailsModal } from 'src/components/Bonding/modals/ConfirmationModal';
import { Node as NodeIcon } from 'src/svg-icons/node';
import { LoadingModal } from 'src/components/Modals/LoadingModal';
import { NymCard } from 'src/components';
import { PageLayout } from 'src/layouts';
import { Tabs } from 'src/components/Tabs';
import { useBondingContext, BondingContextProvider } from 'src/context';
import { AppContext, urls } from 'src/context/main';
import { NodeGeneralSettings } from './settings-pages/general-settings';
import { NodeUnbondPage } from './settings-pages/NodeUnbondPage';
import { navItems, NodeSettingsNav } from './node-settings.constant';
export const NodeSettings = () => {
const theme = useTheme();
const { network } = useContext(AppContext);
const { bondedNode, unbond, isLoading } = useBondingContext();
const navigate = useNavigate();
const location = useLocation();
const [confirmationDetails, setConfirmationDetails] = useState<ConfirmationDetailProps>();
const [value, setValue] = React.useState<NodeSettingsNav>('General');
const handleChange = (event: React.SyntheticEvent, tab: string) => {
setValue(tab as NodeSettingsNav);
};
useEffect(() => {
if (location.state === 'unbond') {
setValue('Unbond');
}
}, [location]);
const handleUnbond = async (fee?: FeeDetails) => {
const tx = await unbond(fee);
setConfirmationDetails({
status: 'success',
title: 'Unbond successful',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
};
const handleError = (error: string) => {
setConfirmationDetails({
status: 'error',
title: 'An error occurred',
subtitle: error,
});
};
return (
<PageLayout>
<NymCard
borderless
noPadding
title={
<Stack gap={2} sx={{ py: 0 }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<NodeIcon />
<Typography variant="h6" fontWeight={600}>
Node Settings
</Typography>
</Box>
</Box>
<Box sx={{ width: '100%' }}>
<Tabs
tabs={navItems}
selectedTab={value}
onChange={handleChange}
tabSx={{
bgcolor: 'transparent',
borderBottom: 'none',
borderTop: 'none',
'& button': {
p: 0,
mr: 4,
minWidth: 'none',
fontSize: 16,
},
'& button:hover': {
color: theme.palette.nym.highlight,
opacity: 1,
},
}}
tabIndicatorStyles={{ height: 4, bottom: '6px', borderRadius: '2px' }}
/>
</Box>
</Stack>
}
Action={
<IconButton
size="small"
sx={{
color: 'text.primary',
}}
onClick={() => navigate('/bonding')}
>
<Close />
</IconButton>
}
>
<Divider />
{value === 'General' && bondedNode && <NodeGeneralSettings bondedNode={bondedNode} />}
{value === 'Unbond' && bondedNode && (
<NodeUnbondPage bondedNode={bondedNode} onConfirm={handleUnbond} onError={handleError} />
)}
{confirmationDetails && confirmationDetails.status === 'success' && (
<ConfirmationDetailsModal
title={confirmationDetails.title}
subtitle={confirmationDetails.subtitle || 'This operation can take up to one hour to process'}
status={confirmationDetails.status}
txUrl={confirmationDetails.txUrl}
onClose={() => {
setConfirmationDetails(undefined);
navigate('/bonding');
}}
/>
)}
{isLoading && <LoadingModal />}
</NymCard>
</PageLayout>
);
};
export const NodeSettingsPage = () => (
<BondingContextProvider>
<NodeSettings />
</BondingContextProvider>
);
@@ -0,0 +1,3 @@
export const navItems = ['General', 'Unbond'] as const;
export type NodeSettingsNav = typeof navItems[number]; // type NodeSettingsNav = 'General' | 'Unbond';
@@ -0,0 +1,66 @@
import React, { useState } from 'react';
import { Box, Button, Typography, Grid, TextField } from '@mui/material';
import { TBondedMixnode, TBondedGateway } from 'src/context/bonding';
import { Error } from 'src/components/Error';
import { UnbondModal } from 'src/components/Bonding/modals/UnbondModal';
interface Props {
bondedNode: TBondedMixnode | TBondedGateway;
onConfirm: () => Promise<void>;
onError: (e: string) => void;
}
export const NodeUnbondPage = ({ bondedNode, onConfirm, onError }: Props) => {
const [confirmField, setConfirmField] = useState('');
const [isConfirmed, setIsConfirmed] = useState(false);
//TODO: Check what happens with a gateway
return (
<Box sx={{ pl: 3, minHeight: '450px' }}>
<Grid container direction="row" alignItems="start" justifyContent={'space-between'}>
<Grid item container direction={'column'} width={0.5} spacing={1} sx={{ pt: 3 }}>
<Grid item>
<Typography variant="body1" fontWeight={600}>
Unbond
</Typography>
</Grid>
<Grid item>
<Typography variant="body2" sx={{ color: (theme) => theme.palette.nym.text.muted }}>
If you unbond your node you will loose all delegations!
</Typography>
</Grid>
</Grid>
<Grid item container direction={'column'} spacing={2} width={0.5} padding={3}>
<Grid item>
<Error message="Unbonding is irreversible and it wont be possible to restore the current state of your node again" />
</Grid>
<Grid item>
<Typography variant="body2">
To unbond, type{' '}
<Typography display="inline" component="span" sx={{ color: (t) => t.palette.nym.highlight }}>
UNBOND
</Typography>{' '}
in the field below and click continue
</Typography>
</Grid>
<Grid item>
<TextField fullWidth value={confirmField} onChange={(e) => setConfirmField(e.target.value)} />
</Grid>
<Grid item sx={{ mt: 2 }}>
<Button
size="large"
variant="contained"
fullWidth
disabled={confirmField !== 'UNBOND'}
onClick={() => {
setIsConfirmed(true);
}}
>
Continue
</Button>
</Grid>
</Grid>
</Grid>
{isConfirmed && (
<UnbondModal node={bondedNode} onConfirm={onConfirm} onClose={() => setIsConfirmed(false)} onError={onError} />
)}
</Box>
);
};
@@ -0,0 +1,247 @@
import { useEffect, 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 { 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';
export const InfoSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBondedGateway }) => {
const [open, setOpen] = useState(true);
const [openConfirmationModal, setOpenConfirmationModal] = useState<boolean>(false);
const theme = useTheme();
const {
register,
handleSubmit,
formState: { errors, isSubmitting, isDirty, isValid },
} = useForm({
resolver: yupResolver(bondedInfoParametersValidationSchema),
mode: 'onChange',
defaultValues: isMixnode(bondedNode) ? bondedNode : {},
});
const onSubmit = async (data: {
host?: string;
version?: string;
mixPort?: number;
verlocPort?: number;
httpApiPort?: number;
}) => {
const { host, version, mixPort, verlocPort, httpApiPort } = data;
if (host && version && mixPort && verlocPort && httpApiPort) {
const MixNodeConfigParams = {
host,
mix_port: mixPort,
verloc_port: verlocPort,
http_api_port: httpApiPort,
version,
};
try {
await updateMixnodeConfig(MixNodeConfigParams);
setOpenConfirmationModal(true);
} catch (error) {
Console.error(error);
}
}
};
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>
)}
<Grid container>
<Grid item container direction="row" alignItems="left" justifyContent="space-between" padding={3}>
<Grid item>
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1 }}>
Port
</Typography>
<Typography
variant="body1"
sx={{
fontSize: 14,
mb: 2,
color: (t) => (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'),
}}
>
Change profit margin of your node
</Typography>
</Grid>
<Grid spacing={3} item container alignItems="center" xs={12} md={6}>
<Grid item width={1}>
<TextField
{...register('mixPort')}
name="mixPort"
label="Mix Port"
fullWidth
error={!!errors.mixPort}
helperText={errors.mixPort?.message}
/>
</Grid>
<Grid item width={1}>
<TextField
{...register('verlocPort')}
name="verlocPort"
label="Verloc Port"
fullWidth
error={!!errors.verlocPort}
helperText={errors.verlocPort?.message}
/>
</Grid>
<Grid item width={1}>
<TextField
{...register('httpApiPort')}
name="httpApiPort"
label="HTTP port"
fullWidth
error={!!errors.httpApiPort}
helperText={errors.httpApiPort?.message}
/>
</Grid>
</Grid>
</Grid>
<Divider flexItem />
<Grid item container direction="row" alignItems="left" justifyContent="space-between" padding={3}>
<Grid item>
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1 }}>
Host
</Typography>
<Typography
variant="body1"
sx={{
fontSize: 14,
mb: 2,
color: (t) => (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'),
}}
>
Lock wallet after certain time
</Typography>
</Grid>
<Grid spacing={3} item container alignItems="center" xs={12} md={6}>
<Grid item width={1}>
<TextField
{...register('host')}
name="host"
label="host"
fullWidth
error={!!errors.host}
helperText={errors.host?.message}
/>
</Grid>
</Grid>
</Grid>
<Divider flexItem />
<Grid item container direction="row" alignItems="left" justifyContent="space-between" padding={3}>
<Grid item>
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1 }}>
Version
</Typography>
<Typography
variant="body1"
sx={{
fontSize: 14,
mb: 2,
color: (t) => (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'),
}}
>
Lock wallet after certain time
</Typography>
</Grid>
<Grid spacing={3} item container alignItems="center" xs={12} md={6}>
<Grid item width={1}>
<TextField
{...register('version')}
name="version"
label="Version"
fullWidth
error={!!errors.version}
helperText={errors.version?.message}
/>
</Grid>
</Grid>
</Grid>
<Divider flexItem />
<Grid container justifyContent="end">
<Button
size="large"
variant="contained"
disabled={isSubmitting || !isDirty || !isValid}
onClick={handleSubmit((d) => onSubmit(d))}
type="submit"
sx={{ m: 3, width: '320px' }}
endIcon={isSubmitting && <CircularProgress size={20} />}
>
Save all display changes
</Button>
</Grid>
</Grid>
<SimpleModal
open={openConfirmationModal}
header="Your changes were ONLY saved on the display"
subHeader="Remember to change the values
on your nodes config file too."
okLabel="close"
hideCloseIcon
displayInfoIcon
onOk={async () => {
await setOpenConfirmationModal(false);
}}
buttonFullWidth
sx={{
width: '450px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
headerStyles={{
width: '100%',
mb: 1,
textAlign: 'center',
color: theme.palette.nym.nymWallet.text.blue,
fontSize: 16,
textTransform: 'capitalize',
}}
subHeaderStyles={{
width: '100%',
mb: 1,
textAlign: 'center',
color: 'main',
fontSize: 14,
textTransform: 'capitalize',
}}
/>
</Grid>
);
};
@@ -0,0 +1,218 @@
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import {
Button,
Divider,
Typography,
TextField,
InputAdornment,
Grid,
Alert,
IconButton,
CircularProgress,
Box,
} 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 { 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';
export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBondedGateway }): JSX.Element => {
const [open, setOpen] = useState(true);
const [openConfirmationModal, setOpenConfirmationModal] = useState<boolean>(false);
const theme = useTheme();
const {
register,
handleSubmit,
formState: { errors, isSubmitting, isDirty, isValid },
} = useForm({
resolver: yupResolver(bondedNodeParametersValidationSchema),
mode: 'onChange',
defaultValues: isMixnode(bondedNode)
? {
operatorCost: bondedNode.bond.amount,
profitMargin: bondedNode.profitMargin,
}
: {},
});
const onSubmit = async (data: { operatorCost?: string; profitMargin?: string }) => {
if (data.operatorCost && data.profitMargin) {
const MixNodeCostParams = {
profit_margin_percent: data.profitMargin.toString(),
interval_operating_cost: {
denom: bondedNode.bond.denom,
amount: data.operatorCost.toString(),
},
};
try {
await updateMixnodeCostParams(MixNodeCostParams);
setOpenConfirmationModal(true);
} catch (error) {
Console.error(error);
}
}
};
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>
)}
<Grid container direction="column">
<Grid item container direction="row" alignItems="left" justifyContent="space-between" padding={3} spacing={1}>
<Grid item>
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1 }}>
Profit Margin
</Typography>
<Typography
variant="body1"
sx={{
fontSize: 14,
mb: 2,
color: (t) => (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'),
}}
>
Profit margin can be changed once a month
</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>
</Grid>
<Divider flexItem />
<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
</Typography>
<Typography
variant="body1"
sx={{
fontSize: 14,
mb: 2,
color: (t) => (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'),
}}
>
Lock Wallet after a certain time
</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"
fullWidth
error={!!errors.operatorCost}
helperText={errors?.operatorCost?.message}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Box>{bondedNode.bond.denom.toUpperCase()}</Box>
</InputAdornment>
),
}}
/>
</Grid>
</Grid>
</Grid>
<Divider flexItem />
<Grid container justifyContent="end">
<Button
size="large"
variant="contained"
disabled={isSubmitting || !isDirty || !isValid}
onClick={handleSubmit((d) => onSubmit(d))}
type="submit"
sx={{ m: 3, width: '320px' }}
endIcon={isSubmitting && <CircularProgress size={20} />}
>
Save all display changes
</Button>
</Grid>
</Grid>
<SimpleModal
open={openConfirmationModal}
header="Your changes will take place
in the next interval"
okLabel="close"
hideCloseIcon
displayInfoIcon
onOk={async () => {
await setOpenConfirmationModal(false);
}}
buttonFullWidth
sx={{
width: '320px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
headerStyles={{
width: '100%',
mb: 1,
textAlign: 'center',
color: theme.palette.nym.nymWallet.text.blue,
fontSize: 16,
textTransform: 'capitalize',
}}
subHeaderStyles={{
m: 0,
}}
/>
</Grid>
);
};
@@ -0,0 +1,41 @@
import React, { useContext, useEffect, useState } from 'react';
import { Box, Button, Divider, Grid } from '@mui/material';
import { TBondedMixnode, TBondedGateway } from '../../../../../context/bonding';
import { InfoSettings } from './InfoSettings';
import { ParametersSettings } from './ParametersSettings';
const nodeGeneralNav = ['Info', 'Parameters'];
export const NodeGeneralSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBondedGateway }) => {
const [settingsCard, setSettingsCard] = useState<string>(nodeGeneralNav[0]);
//TODO: Check what happens with a gateway
return (
<Box sx={{ pl: 3, pt: 3 }}>
<Grid container direction="row" spacing={3}>
<Grid item container direction="column" xs={3}>
{nodeGeneralNav.map((item) => (
<Button
size="small"
sx={{
fontSize: 14,
color: settingsCard === item ? 'primary.main' : 'inherit',
justifyContent: 'start',
':hover': {
bgcolor: 'transparent',
color: 'primary.main',
},
}}
key={item}
onClick={() => setSettingsCard(item)}
>
{item}
</Button>
))}
</Grid>
<Divider orientation="vertical" flexItem />
{settingsCard === nodeGeneralNav[0] && <InfoSettings bondedNode={bondedNode} />}
{settingsCard === nodeGeneralNav[1] && <ParametersSettings bondedNode={bondedNode} />}
</Grid>
</Box>
);
};
+2 -2
View File
@@ -110,11 +110,11 @@ const TerminalInner: React.FC = () => {
{status ? (
<Alert color="info" icon={<RefreshIcon />} sx={{ mb: 2 }}>
<strong>{status}</strong>
<Box sx={{ fontWeight: 600 }}>{status}</Box>
</Alert>
) : (
<Alert color="success" sx={{ mb: 2 }}>
<strong>Data loading complete</strong>
<Box sx={{ fontWeight: 600 }}>Data loading complete</Box>
</Alert>
)}
+2 -1
View File
@@ -4,7 +4,7 @@ import { ApplicationLayout } from 'src/layouts';
import { Terminal } from 'src/pages/terminal';
import { Send } from 'src/components/Send';
import { Receive } from '../components/Receive';
import { Balance, InternalDocs, DelegationPage, Admin, BondingPage } from '../pages';
import { Balance, InternalDocs, DelegationPage, Admin, BondingPage, NodeSettingsPage } from '../pages';
export const AppRoutes = () => (
<ApplicationLayout>
@@ -14,6 +14,7 @@ export const AppRoutes = () => (
<Routes>
<Route path="/balance" element={<Balance />} />
<Route path="/bonding" element={<BondingPage />} />
<Route path="/bonding/node-settings" element={<NodeSettingsPage />} />
<Route path="/delegation" element={<DelegationPage />} />
<Route path="/docs" element={<InternalDocs />} />
<Route path="/admin" element={<Admin />} />
+2
View File
@@ -31,6 +31,7 @@ declare module '@mui/material/styles' {
highlight: string;
success: string;
info: string;
red: string;
fee: string;
background: { light: string; dark: string };
text: {
@@ -58,6 +59,7 @@ declare module '@mui/material/styles' {
warn: string;
contrast: string;
grey: string;
blue: string;
};
topNav: {
background: string;
+13
View File
@@ -23,6 +23,7 @@ const nymPalette: NymPalette = {
highlight: '#FB6E4E',
success: '#21D073',
info: '#60D7EF',
red: '#DA465B',
fee: '#967FF0',
background: { light: '#F4F6F8', dark: '#1D2125' },
text: {
@@ -52,6 +53,7 @@ const darkMode: NymPaletteVariant = {
warn: '#FFE600',
contrast: '#1D2125',
grey: '#5B6174',
blue: '#60D7EF',
},
topNav: {
background: '#111826',
@@ -82,6 +84,7 @@ const lightMode: NymPaletteVariant = {
warn: '#FFE600',
contrast: '#FFFFFF',
grey: '#3A4053',
blue: '#514EFB',
},
topNav: {
background: '#111826',
@@ -288,6 +291,16 @@ export const getDesignTokens = (mode: PaletteMode): ThemeOptions => {
},
},
},
MuiToolbar: {
styleOverrides: {
root: {
minWidth: 0,
'@media (min-width: 0px)': {
minHeight: 'fit-content',
},
},
},
},
MuiMenu: {
styleOverrides: {
list: ({ _, theme }) => ({