Migrate Legacy Node (Frontend) (#4826)

* refactor bonding requests

* use migrate node modal

* disable node settings for legacy nodes

* refine bonded node types

* start migration and bonding work

* update types and requests

* clean up bonding context

* move old forms to legacy directory

* create nymnode bonding flow

---------

Co-authored-by: fmtabbara <fmtabbara@hotmail.co.uk>
This commit is contained in:
Gala
2024-10-17 11:08:05 +02:00
committed by GitHub
parent 20bc305fb9
commit f455b7c720
71 changed files with 2206 additions and 1229 deletions
@@ -26,7 +26,7 @@ export const VestingTimeline: FCWithChildren<{ percentageComplete: number }> = (
const nextPeriod =
typeof currentVestingPeriod === 'object' && !!vestingAccountInfo?.periods
? Number(vestingAccountInfo?.periods[currentVestingPeriod.in + 1]?.start_time)
? Number(vestingAccountInfo?.periods[currentVestingPeriod.In + 1]?.start_time)
: undefined;
return (
@@ -23,7 +23,7 @@ const columnsHeaders: Array<{ title: string; align: TableCellProps['align'] }> =
const vestingPeriod = (current?: Period, original?: number) => {
if (current === 'After') return 'Complete';
if (typeof current === 'object' && typeof original === 'number') return `${current.in + 1}/${original}`;
if (typeof current === 'object' && typeof original === 'number') return `${current.In + 1}/${original}`;
return 'N/A';
};
+1 -1
View File
@@ -20,7 +20,7 @@ export const Bond = ({
}}
>
<Typography variant="body2">
Bond a mix node or a gateway. Learn how to set up and run a node{' '}
Bond a nym node. Learn how to set up and run a Nym node{' '}
<Link href="https://nymtech.net/operators/nodes/mix-node-setup.html" target="_blank">
here
</Link>
@@ -0,0 +1,44 @@
import React from 'react';
import { Box, Button, Stack, Tooltip, Typography } from '@mui/material';
import { NymCard } from 'src/components';
export const BondUpdateCard = ({ setSuccesfullUpdate }: { setSuccesfullUpdate: (staus: boolean) => void }) => (
<Stack gap={2}>
<NymCard
borderless
title={
<Typography variant="h5" fontWeight={600} marginBottom={3}>
Upgrade your node!
</Typography>
}
subheader={
<Stack gap={1}>
<Typography variant="subtitle2" fontWeight={600} sx={{ color: 'nym.text.dark' }}>
It seems like your node is running outdated binaries.
</Typography>
<Typography variant="body2">Update to the latest stable Nym node binary now*</Typography>
<Typography variant="body2">The update takes less than a minute!</Typography>
<Typography variant="caption">
*Without updating, legacy node settings can be changed in the Nym CLI.
</Typography>
</Stack>
}
Action={
<Box display="flex" flexDirection="column" alignItems="flex-end" justifyContent="space-between" height={70}>
<Tooltip title="Update to the latest stable Nym node binary now">
<Box>
<Button
variant="contained"
color="primary"
// TODO wallet-smoosh: update when we have the actual endpoint
onClick={() => setSuccesfullUpdate(true)}
>
Upgrade to Nym Node
</Button>
</Box>
</Tooltip>
</Box>
}
/>
</Stack>
);
@@ -1,11 +1,13 @@
import React from 'react';
import { Box, Button, Stack, Typography } from '@mui/material';
import { Box, Button, Stack, Tooltip, Typography } from '@mui/material';
import { Link } from '@nymproject/react/link/Link';
import { TBondedGateway, urls } from 'src/context';
import { urls } from 'src/context';
import { NymCard } from 'src/components';
import { Network } from 'src/types';
import { IdentityKey } from 'src/components/IdentityKey';
import { useNavigate } from 'react-router-dom';
import { UpgradeRounded } from '@mui/icons-material';
import { TBondedGateway } from 'src/requests/gatewayDetails';
import { Node as NodeIcon } from '../../svg-icons/node';
import { Cell, Header, NodeTable } from './NodeTable';
import { BondedGatewayActions, TBondedGatwayActions } from './BondedGatewayAction';
@@ -39,10 +41,12 @@ const headers: Header[] = [
export const BondedGateway = ({
gateway,
network,
onShowMigrateToNymNodeModal,
onActionSelect,
}: {
gateway: TBondedGateway;
network?: Network;
onShowMigrateToNymNodeModal: () => void;
onActionSelect: (action: TBondedGatwayActions) => void;
}) => {
const { name, bond, ip, identityKey, routingScore } = gateway;
@@ -91,16 +95,29 @@ export const BondedGateway = ({
</Stack>
}
Action={
<Box>
<Stack direction="row" gap={1}>
<Tooltip title="Gateway settings are disabled for legacy Gateways. Please migrate your node in order to access your Gateway settings.">
<Box>
<Button
variant="text"
color="secondary"
onClick={() => navigate('/bonding/node-settings')}
startIcon={<NodeIcon />}
disabled
>
Gateway Settings
</Button>
</Box>
</Tooltip>
<Button
variant="text"
color="secondary"
onClick={() => navigate('/bonding/node-settings')}
startIcon={<NodeIcon />}
startIcon={<UpgradeRounded />}
variant="contained"
disableElevation
onClick={onShowMigrateToNymNodeModal}
>
Gateway Settings
Migrate to Nym Node
</Button>
</Box>
</Stack>
}
>
<NodeTable headers={headers} cells={cells} />
@@ -2,12 +2,14 @@ import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Box, Button, Chip, Stack, Tooltip, Typography } from '@mui/material';
import { Link } from '@nymproject/react/link/Link';
import { isMixnode, Network } from 'src/types';
import { TBondedMixnode, urls } from 'src/context';
import { Network } from 'src/types';
import { urls } from 'src/context';
import { NymCard } from 'src/components';
import { IdentityKey } from 'src/components/IdentityKey';
import { NodeStatus } from 'src/components/NodeStatus';
import { getIntervalAsDate } from 'src/utils';
import { UpgradeRounded } from '@mui/icons-material';
import { TBondedMixnode } from 'src/requests/mixnodeDetails';
import { Node as NodeIcon } from '../../svg-icons/node';
import { Cell, Header, NodeTable } from './NodeTable';
import { BondedMixnodeActions, TBondedMixnodeActions } from './BondedMixnodeActions';
@@ -60,10 +62,12 @@ const headers: Header[] = [
export const BondedMixnode = ({
mixnode,
network,
onShowMigrateToNymNodeModal,
onActionSelect,
}: {
mixnode: TBondedMixnode;
network?: Network;
onShowMigrateToNymNodeModal: () => void;
onActionSelect: (action: TBondedMixnodeActions) => void;
}) => {
const [nextEpoch, setNextEpoch] = useState<string | Error>();
@@ -81,6 +85,7 @@ export const BondedMixnode = ({
status,
identityKey,
host,
isUnbonding,
} = mixnode;
const getNextInterval = async () => {
@@ -165,9 +170,13 @@ export const BondedMixnode = ({
}
Action={
<Box display="flex" flexDirection="column" alignItems="flex-end" justifyContent="space-between" height={70}>
{isMixnode(mixnode) && (
<Stack direction="row" gap={1}>
<Tooltip
title={mixnode.isUnbonding ? 'You have a pending unbond event. Node settings are disabled.' : ''}
title={
mixnode.isUnbonding
? 'You have a pending unbond event. Node settings are disabled.'
: 'Node settings are disabled for legacy nodes. Please migrate your node in order to access your node settings.'
}
>
<Box>
<Button
@@ -175,13 +184,23 @@ export const BondedMixnode = ({
color="secondary"
onClick={() => navigate('/bonding/node-settings')}
startIcon={<NodeIcon />}
disabled={mixnode.isUnbonding}
disabled
>
Node Settings
</Button>
</Box>
</Tooltip>
)}
<Button
startIcon={<UpgradeRounded />}
variant="contained"
disableElevation
onClick={onShowMigrateToNymNodeModal}
disabled={isUnbonding}
>
Migrate to Nym Node
</Button>
</Stack>
{nextEpoch instanceof Error ? null : (
<Typography fontSize={14} marginRight={1}>
Next epoch starts at <b>{nextEpoch}</b>
@@ -0,0 +1,206 @@
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Box, Button, Chip, Stack, Tooltip, Typography } from '@mui/material';
import { Link } from '@nymproject/react/link/Link';
import { Network } from 'src/types';
import { urls } from 'src/context';
import { NymCard } from 'src/components';
import { IdentityKey } from 'src/components/IdentityKey';
import { getIntervalAsDate } from 'src/utils';
import { TBondedNymNode } from 'src/requests/nymNodeDetails';
import { Node as NodeIcon } from '../../svg-icons/node';
import { Cell, Header, NodeTable } from './NodeTable';
import { BondedNymNodeActions, TBondedNymNodeActions } from './BondedNymNodeActions';
const textWhenNotName = 'This node has not yet set a name';
const headers: Header[] = [
{
header: 'Stake',
id: 'stake',
sx: { pl: 0 },
},
{
header: 'Bond',
id: 'bond',
},
{
header: 'Stake saturation',
id: 'stake-saturation',
},
{
header: 'PM',
id: 'profit-margin',
tooltipText:
'The percentage of the node rewards that you as the node operator will take before the rest of the reward is shared between you and the delegators.',
},
{
header: 'Operating cost',
id: 'operator-cost',
tooltipText:
'Monthly operational costs of running your node. The cost also influences how the rewards are split between you and your delegators. ',
},
{
header: 'Operator rewards',
id: 'operator-rewards',
tooltipText:
'This is your (operator) rewards including the PM and cost. Rewards are automatically compounded every epoch. You can redeem your rewards at any time.',
},
{
header: 'No. delegators',
id: 'delegators',
},
{
id: 'menu-button',
sx: { width: 34, maxWidth: 34 },
},
];
export const BondedNymNode = ({
nymnode,
network,
onActionSelect,
}: {
nymnode: TBondedNymNode;
network?: Network;
onActionSelect: (action: TBondedNymNodeActions) => void;
}) => {
const [nextEpoch, setNextEpoch] = useState<string | Error>();
const navigate = useNavigate();
const {
name,
nodeId,
stake,
bond,
stakeSaturation,
profitMargin,
operatorRewards,
operatorCost,
delegators,
identityKey,
host,
} = nymnode;
const getNextInterval = async () => {
try {
const { nextEpoch: newNextEpoch } = await getIntervalAsDate();
setNextEpoch(newNextEpoch);
} catch {
setNextEpoch(Error());
}
};
const cells: Cell[] = [
{
cell: `${stake.amount} ${stake.denom}`,
id: 'stake-cell',
},
{
cell: `${bond.amount} ${bond.denom}`,
id: 'bond-cell',
},
{
cell: `${stakeSaturation}%`,
id: 'stake-saturation-cell',
},
{
cell: `${profitMargin}%`,
id: 'pm-cell',
},
{
cell: operatorCost ? `${operatorCost.amount} ${operatorCost.denom}` : '-',
id: 'operator-cost-cell',
},
{
cell: operatorRewards ? `${operatorRewards.amount} ${operatorRewards.denom}` : '-',
id: 'operator-rewards-cell',
},
{
cell: delegators,
id: 'delegators-cell',
},
{
cell: nymnode.isUnbonding ? (
<Chip label="Pending unbond" sx={{ textTransform: 'initial' }} />
) : (
<BondedNymNodeActions
onActionSelect={onActionSelect}
disabledRedeemAndCompound={(operatorRewards && Number(operatorRewards.amount) === 0) || false}
/>
),
id: 'actions-cell',
align: 'right',
},
];
useEffect(() => {
getNextInterval();
}, []);
return (
<Stack gap={2}>
<NymCard
borderless
title={
<Stack gap={3}>
<Box display="flex" alignItems="center" gap={2}>
<Typography variant="h5" fontWeight={600}>
Nym node
</Typography>
</Box>
{name?.includes(textWhenNotName) ? null : (
<Typography fontWeight="regular" variant="h6" width="fit-content">
{name}
</Typography>
)}
<Tooltip title={host} placement="top" arrow>
<Box width="fit-content">
<IdentityKey identityKey={identityKey} />
</Box>
</Tooltip>
</Stack>
}
Action={
<Box display="flex" flexDirection="column" alignItems="flex-end" justifyContent="space-between" height={70}>
<Stack direction="row" gap={1}>
<Tooltip
title={
nymnode.isUnbonding
? 'You have a pending unbond event. Node settings are disabled.'
: 'Node settings are disabled for legacy nodes. Please migrate your node in order to access your node settings.'
}
>
<Box>
<Button
variant="text"
color="secondary"
onClick={() => navigate('/bonding/node-settings')}
startIcon={<NodeIcon />}
>
Node Settings
</Button>
</Box>
</Tooltip>
</Stack>
{nextEpoch instanceof Error ? null : (
<Typography fontSize={14} marginRight={1}>
Next epoch starts at <b>{nextEpoch}</b>
</Typography>
)}
</Box>
}
>
<NodeTable headers={headers} cells={cells} />
{network && (
<Typography sx={{ mt: 2, fontSize: 'small' }}>
Check more stats of your node on the{' '}
<Link href={`${urls(network).networkExplorer}/network-components/mixnode/${nodeId}`} target="_blank">
explorer
</Link>
</Typography>
)}
</NymCard>
{/* <NodeStats bondedNode={nymnode} /> */}
</Stack>
);
};
@@ -0,0 +1,49 @@
import React, { useState } from 'react';
import { Typography } from '@mui/material';
import { ActionsMenu, ActionsMenuItem } from 'src/components/ActionsMenu';
import { Unbond as UnbondIcon, Bond as BondIcon } from '../../svg-icons';
export type TBondedNymNodeActions = 'nodeSettings' | 'updateBond' | 'unbond' | 'redeem';
export const BondedNymNodeActions = ({
onActionSelect,
disabledRedeemAndCompound,
disabledUpdateBond,
}: {
onActionSelect: (action: TBondedNymNodeActions) => void;
disabledRedeemAndCompound: boolean;
disabledUpdateBond?: boolean;
}) => {
const [isOpen, setIsOpen] = useState(false);
const handleOpen = () => setIsOpen(true);
const handleClose = () => setIsOpen(false);
const handleActionClick = (action: TBondedNymNodeActions) => {
onActionSelect(action);
handleClose();
};
return (
<ActionsMenu open={isOpen} onOpen={handleOpen} onClose={handleClose}>
{!disabledUpdateBond && (
<ActionsMenuItem
title="Change bond amount"
Icon={<BondIcon fontSize="inherit" />}
onClick={() => handleActionClick('updateBond')}
/>
)}
<ActionsMenuItem
title="Redeem rewards"
Icon={<Typography sx={{ pl: 0.5, fontWeight: 700 }}>R</Typography>}
onClick={() => handleActionClick('redeem')}
disabled={disabledRedeemAndCompound}
/>
<ActionsMenuItem
title="Unbond"
Icon={<UnbondIcon fontSize="inherit" />}
onClick={() => handleActionClick('unbond')}
/>
</ActionsMenu>
);
};
@@ -1,9 +1,8 @@
import React from 'react';
import { Stack, Typography, Box, useTheme, Grid, LinearProgress, LinearProgressProps, Button } from '@mui/material';
import { useNavigate } from 'react-router-dom';
import { TBondedMixnode } from 'src/context';
import { Stack, Typography, Box, useTheme, Grid, LinearProgress, LinearProgressProps } from '@mui/material';
import { Cell, Pie, PieChart, Legend, ResponsiveContainer } from 'recharts';
import { SelectionChance } from '@nymproject/types';
import { TBondedMixnode } from 'src/requests/mixnodeDetails';
import { NymCard } from '../NymCard';
import { InfoTooltip } from '../InfoToolTip';
@@ -50,10 +49,9 @@ const StatRow = ({
export const NodeStats = ({ mixnode }: { mixnode: TBondedMixnode }) => {
const { activeSetProbability, routingScore } = mixnode;
const theme = useTheme();
const navigate = useNavigate();
// clamp routing score to [0-100]
const score = Math.min(Math.max(routingScore, 0), 100);
const score = Math.min(Math.max(routingScore || 0, 0), 100);
const data = [
{ key: 'routingScore', value: score },
@@ -74,10 +72,6 @@ export const NodeStats = ({ mixnode }: { mixnode: TBondedMixnode }) => {
}
};
const handleGoToTestNode = () => {
navigate('/bonding/node-settings', { state: 'test-node' });
};
const renderLegend = () => (
<Stack
alignItems="center"
@@ -105,11 +99,6 @@ export const NodeStats = ({ mixnode }: { mixnode: TBondedMixnode }) => {
Node stats
</Typography>
}
Action={
<Button size="small" variant="contained" disableElevation onClick={handleGoToTestNode}>
Test node
</Button>
}
>
<Stack justifyContent="center" alignItems="center" mb={2}>
<ResponsiveContainer width="100%" height={100}>
@@ -1,50 +0,0 @@
import React from 'react';
import { Box } from '@mui/material';
import { CurrencyDenom, TNodeType } from '@nymproject/types';
import { NodeTypeSelector } from 'src/components';
import { MixnodeAmount, MixnodeData, Signature } from 'src/pages/bonding/types';
import MixnodeInitForm from './MixnodeInitForm';
import MixnodeAmountForm from './MixnodeAmountForm';
import MixnodeSignatureForm from './MixnodeSignatureForm';
export const BondMixnodeForm = ({
step,
denom,
mixnodeData,
amountData,
hasVestingTokens,
onSelectNodeType,
onValidateMixnodeData,
onValidateAmountData,
onValidateSignature,
}: {
step: 1 | 2 | 3 | 4;
mixnodeData: MixnodeData;
amountData: MixnodeAmount;
denom: CurrencyDenom;
hasVestingTokens: boolean;
onSelectNodeType: (nodeType: TNodeType) => void;
onValidateMixnodeData: (data: MixnodeData) => void;
onValidateAmountData: (data: MixnodeAmount) => Promise<void>;
onValidateSignature: (signature: Signature) => void;
}) => (
<>
{step === 1 && (
<>
<Box sx={{ mb: 3 }}>
<NodeTypeSelector disabled={false} setNodeType={onSelectNodeType} nodeType="mixnode" />
</Box>
<MixnodeInitForm onNext={onValidateMixnodeData} mixnodeData={mixnodeData} />
</>
)}
{step === 2 && (
<MixnodeAmountForm
denom={denom}
amountData={amountData}
hasVestingTokens={hasVestingTokens}
onNext={onValidateAmountData}
/>
)}
{step === 3 && <MixnodeSignatureForm mixnode={mixnodeData} amount={amountData} onNext={onValidateSignature} />}
</>
);
@@ -1,94 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Stack, TextField, Typography } from '@mui/material';
import { useForm } from 'react-hook-form';
import { gatewayToTauri } from '../utils';
import { CopyToClipboard } from '../../CopyToClipboard';
import { useBondingContext } from '../../../context';
import { Console } from '../../../utils/console';
import { ErrorModal } from '../../Modals/ErrorModal';
import { GatewayData, GatewayAmount, Signature } from '../../../pages/bonding/types';
const GatewaySignatureForm = ({
gateway,
amount,
onNext,
}: {
gateway: GatewayData;
amount: GatewayAmount;
onNext: (data: Signature) => void;
}) => {
const [message, setMessage] = useState<string>();
const [error, setError] = useState<string>();
const { generateGatewayMsgPayload } = useBondingContext();
const { register, handleSubmit } = useForm<Signature>();
const handleOnNext = (event: { detail: { step: number } }) => {
if (event.detail.step === 3) {
handleSubmit(onNext)();
}
};
useEffect(() => {
window.addEventListener('validate_bond_gateway_step' as any, handleOnNext);
return () => window.removeEventListener('validate_bond_gateway_step' as any, handleOnNext);
}, []);
const generateMessage = async () => {
try {
setMessage(
await generateGatewayMsgPayload({
pledge: amount.amount,
gateway: gatewayToTauri(gateway),
tokenPool: amount.tokenPool as 'balance' | 'locked',
}),
);
} catch (e) {
Console.error(e);
setError('Something went wrong while generating the payload signature');
}
};
useEffect(() => {
generateMessage();
}, [gateway, amount]);
if (error) {
return <ErrorModal open message={error} onClose={() => {}} />;
}
return (
<Stack gap={3} mb={3}>
<Typography variant="body1">
Copy the message below and sign it:
<br />
If you are using a nym-gateway:
<br />
<code>nym-gateway sign --id &lt;your-node-id&gt; --contract-msg &lt;payload-generated-by-the-wallet&gt;</code>
<br />
If you are using a nym-node:
<br />
<code>nym-node sign --id &lt;your-node-id&gt; --contract-msg &lt;payload-generated-by-the-wallet&gt;</code>
<br />
Then paste the signature in the next field.
</Typography>
<TextField id="outlined-multiline-static" multiline rows={7} value={message} fullWidth disabled />
<Stack direction="row" alignItems="center" gap={1} justifyContent="end">
<Typography fontWeight={600}>Copy Message</Typography>
{message && <CopyToClipboard text={message} iconButton />}
</Stack>
<TextField
{...register('signature')}
id="outlined-multiline-static"
name="signature"
rows={3}
placeholder="Paste Signature"
multiline
fullWidth
required
/>
</Stack>
);
};
export default GatewaySignatureForm;
@@ -1,95 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Stack, TextField, Typography } from '@mui/material';
import { useForm } from 'react-hook-form';
import { costParamsToTauri, mixnodeToTauri } from '../utils';
import { CopyToClipboard } from '../../CopyToClipboard';
import { useBondingContext } from '../../../context';
import { Console } from '../../../utils/console';
import { ErrorModal } from '../../Modals/ErrorModal';
import { MixnodeAmount, MixnodeData, Signature } from '../../../pages/bonding/types';
const MixnodeSignatureForm = ({
mixnode,
amount,
onNext,
}: {
mixnode: MixnodeData;
amount: MixnodeAmount;
onNext: (data: Signature) => void;
}) => {
const [message, setMessage] = useState<string>('');
const [error, setError] = useState<string>();
const { generateMixnodeMsgPayload } = useBondingContext();
const { register, handleSubmit } = useForm<Signature>();
const handleOnNext = (event: { detail: { step: number } }) => {
if (event.detail.step === 3) {
handleSubmit(onNext)();
}
};
useEffect(() => {
window.addEventListener('validate_bond_mixnode_step' as any, handleOnNext);
return () => window.removeEventListener('validate_bond_mixnode_step' as any, handleOnNext);
}, []);
const generateMessage = async () => {
try {
setMessage(
(await generateMixnodeMsgPayload({
pledge: amount.amount,
mixnode: mixnodeToTauri(mixnode),
costParams: costParamsToTauri(amount),
tokenPool: amount.tokenPool as 'balance' | 'locked',
})) as string,
);
} catch (e) {
Console.error(e);
setError('Something went wrong while generating the payload signature');
}
};
useEffect(() => {
generateMessage();
}, [mixnode, amount]);
if (error) {
return <ErrorModal open message={error} onClose={() => {}} />;
}
return (
<Stack gap={3} mb={3}>
<Typography variant="body1">
Copy the message below and sign it:
<br />
If you are using a nym-mixnode:
<br />
<code>nym-mixnode sign --id &lt;your-node-id&gt; --contract-msg &lt;payload-generated-by-the-wallet&gt;</code>
<br />
If you are using a nym-node:
<br />
<code>nym-node sign --id &lt;your-node-id&gt; --contract-msg &lt;payload-generated-by-the-wallet&gt;</code>
<br />
Then paste the signature in the next field.
</Typography>
<TextField id="outlined-multiline-static" multiline rows={7} value={message} fullWidth disabled />
<Stack direction="row" alignItems="center" gap={1} justifyContent="end">
<Typography fontWeight={600}>Copy Message</Typography>
{message && <CopyToClipboard text={message} iconButton />}
</Stack>
<TextField
{...register('signature')}
id="outlined-multiline-static"
name="signature"
rows={3}
placeholder="Paste Signature"
multiline
fullWidth
required
/>
</Stack>
);
};
export default MixnodeSignatureForm;
@@ -2,10 +2,9 @@ import React from 'react';
import { Box } from '@mui/material';
import { NodeTypeSelector } from 'src/components';
import { CurrencyDenom, TNodeType } from '@nymproject/types';
import { GatewayAmount, GatewayData, Signature } from 'src/pages/bonding/types';
import { GatewayAmount, GatewayData } from 'src/pages/bonding/types';
import GatewayInitForm from './GatewayInitForm';
import GatewayAmountForm from './GatewayAmountForm';
import GatewaySignatureForm from './GatewaySignatureForm';
export const BondGatewayForm = ({
step,
@@ -16,7 +15,6 @@ export const BondGatewayForm = ({
onSelectNodeType,
onValidateGatewayData,
onValidateAmountData,
onValidateSignature,
}: {
step: 1 | 2 | 3 | 4;
gatewayData: GatewayData;
@@ -26,7 +24,6 @@ export const BondGatewayForm = ({
onSelectNodeType: (nodeType: TNodeType) => void;
onValidateGatewayData: (data: GatewayData) => void;
onValidateAmountData: (data: GatewayAmount) => Promise<void>;
onValidateSignature: (signature: Signature) => void;
}) => (
<>
{step === 1 && (
@@ -45,6 +42,5 @@ export const BondGatewayForm = ({
onNext={onValidateAmountData}
/>
)}
{step === 3 && <GatewaySignatureForm gateway={gatewayData} amount={amountData} onNext={onValidateSignature} />}
</>
);
@@ -5,9 +5,9 @@ import { yupResolver } from '@hookform/resolvers/yup/dist/yup';
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
import { Box, Stack } from '@mui/material';
import { amountSchema } from './gatewayValidationSchema';
import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from '../../../utils';
import { GatewayAmount } from '../../../pages/bonding/types';
import { TokenPoolSelector } from '../../TokenPoolSelector';
import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from '../../../../utils';
import { GatewayAmount } from '../../../../pages/bonding/types';
import { TokenPoolSelector } from '../../../TokenPoolSelector';
const GatewayAmountForm = ({
denom,
@@ -4,7 +4,7 @@ import { clean } from 'semver';
import { Checkbox, FormControlLabel, Stack, TextField } from '@mui/material';
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
import { yupResolver } from '@hookform/resolvers/yup/dist/yup';
import { GatewayData } from '../../../pages/bonding/types';
import { GatewayData } from '../../../../pages/bonding/types';
import { gatewayValidationSchema } from './gatewayValidationSchema';
const GatewayInitForm = ({
@@ -5,11 +5,11 @@ import { yupResolver } from '@hookform/resolvers/yup/dist/yup';
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
import { CurrencyDenom } from '@nymproject/types';
import { amountSchema } from './mixnodeValidationSchema';
import { MixnodeAmount } from '../../../pages/bonding/types';
import { AppContext } from '../../../context';
import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from '../../../utils';
import { TokenPoolSelector } from '../../TokenPoolSelector';
import { ModalListItem } from '../../Modals/ModalListItem';
import { MixnodeAmount } from '../../../../pages/bonding/types';
import { AppContext } from '../../../../context';
import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from '../../../../utils';
import { TokenPoolSelector } from '../../../TokenPoolSelector';
import { ModalListItem } from '../../../Modals/ModalListItem';
const MixnodeAmountForm = ({
amountData,
@@ -5,7 +5,7 @@ import { Checkbox, FormControlLabel, Stack, TextField } from '@mui/material';
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
import { yupResolver } from '@hookform/resolvers/yup/dist/yup';
import { mixnodeValidationSchema } from './mixnodeValidationSchema';
import { MixnodeData } from '../../../pages/bonding/types';
import { MixnodeData } from '../../../../pages/bonding/types';
const MixnodeInitForm = ({ mixnodeData, onNext }: { mixnodeData: MixnodeData; onNext: (data: any) => void }) => {
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
@@ -8,7 +8,7 @@ import {
validateRawPort,
validateVersion,
} from 'src/utils';
import { TauriContractStateParams } from '../../../types';
import { TauriContractStateParams } from '../../../../types';
export const mixnodeValidationSchema = Yup.object().shape({
identityKey: Yup.string()
@@ -0,0 +1,98 @@
import React, { createContext, useContext, useMemo, useState } from 'react';
import { CurrencyDenom } from '@nymproject/types';
import { TBondNymNodeArgs, TBondMixNodeArgs } from 'src/types';
const defaultNymNodeValues: TBondNymNodeArgs['nymNode'] = {
identity_key: 'H6rXWgsW89QsVyaNSS3qBe9zZFLhBS6Gn3YRkGFSoFW9',
custom_http_port: 1,
host: '1.1.1.1',
};
const defaultCostParams = (denom: CurrencyDenom): TBondNymNodeArgs['costParams'] => ({
interval_operating_cost: { amount: '40', denom },
profit_margin_percent: '10',
});
const defaultAmount = (denom: CurrencyDenom): TBondMixNodeArgs['pledge'] => ({
amount: '100',
denom,
});
interface FormContextType {
step: 1 | 2 | 3 | 4;
setStep: React.Dispatch<React.SetStateAction<1 | 2 | 3 | 4>>;
nymNodeData: TBondNymNodeArgs['nymNode'];
setNymNodeData: React.Dispatch<React.SetStateAction<TBondNymNodeArgs['nymNode']>>;
costParams: TBondNymNodeArgs['costParams'];
setCostParams: React.Dispatch<React.SetStateAction<TBondNymNodeArgs['costParams']>>;
amountData: TBondMixNodeArgs['pledge'];
setAmountData: React.Dispatch<React.SetStateAction<TBondMixNodeArgs['pledge']>>;
signature?: string;
setSignature: React.Dispatch<React.SetStateAction<string | undefined>>;
onError: (e: string) => void;
}
const FormContext = createContext<FormContextType>({
step: 1,
setStep: () => {},
nymNodeData: defaultNymNodeValues,
setNymNodeData: () => {},
costParams: defaultCostParams('nym'),
setCostParams: () => {},
amountData: defaultAmount('nym'),
setAmountData: () => {},
signature: undefined,
setSignature: () => {},
onError: () => {},
});
const FormContextProvider = ({ children }: { children: React.ReactNode }) => {
// TODO - Make denom dynamic
const denom = 'nym';
const [step, setStep] = useState<1 | 2 | 3 | 4>(1);
const [nymNodeData, setNymNodeData] = useState<TBondNymNodeArgs['nymNode']>(defaultNymNodeValues);
const [costParams, setCostParams] = useState<TBondNymNodeArgs['costParams']>(defaultCostParams(denom));
const [amountData, setAmountData] = useState<TBondNymNodeArgs['pledge']>(defaultAmount(denom));
const [signature, setSignature] = useState<string>();
const onError = (e: string) => {
console.error(e);
};
const value = useMemo(
() => ({
step,
setStep,
nymNodeData,
setNymNodeData,
costParams,
setCostParams,
amountData,
setAmountData,
signature,
setSignature,
onError,
}),
[
step,
setStep,
nymNodeData,
setNymNodeData,
costParams,
setCostParams,
amountData,
setAmountData,
signature,
setSignature,
onError,
],
);
return <FormContext.Provider value={value}>{children}</FormContext.Provider>;
};
export const useFormContext = () => useContext(FormContext);
export default FormContextProvider;
@@ -0,0 +1,120 @@
import React from 'react';
import { Stack, TextField, Box, FormHelperText } from '@mui/material';
import { useForm } from 'react-hook-form';
import { TBondNymNodeArgs } from 'src/types';
import { yupResolver } from '@hookform/resolvers/yup';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
import { checkHasEnoughFunds } from 'src/utils';
import { nymNodeAmountSchema } from './amountValidationSchema';
const defaultNymNodeCostParamValues: TBondNymNodeArgs['costParams'] = {
profit_margin_percent: '10',
interval_operating_cost: { amount: '40', denom: 'nym' },
};
const defaultNymNodePledgeValue: TBondNymNodeArgs['pledge'] = {
amount: '100',
denom: 'nym',
};
type NymNodeDataProps = {
onClose: () => void;
onBack: () => void;
onNext: () => Promise<void>;
step: number;
};
const NymNodeAmount = ({ onClose, onBack, onNext, step }: NymNodeDataProps) => {
const {
formState: { errors },
register,
getValues,
setValue,
setError,
handleSubmit,
} = useForm({
mode: 'all',
defaultValues: {
pledge: defaultNymNodePledgeValue,
...defaultNymNodeCostParamValues,
},
resolver: yupResolver(nymNodeAmountSchema()),
});
console.log(errors, 'errors');
const handleRequestValidation = async () => {
const values = getValues();
const hasSufficientTokens = await checkHasEnoughFunds(values.pledge.amount);
if (hasSufficientTokens) {
handleSubmit(onNext)();
} else {
setError('pledge.amount', { message: 'Not enough tokens' });
}
};
return (
<SimpleModal
open
onOk={handleRequestValidation}
onClose={onClose}
header="Bond Nym Node"
subHeader={`Step ${step}/3`}
okLabel="Next"
onBack={onBack}
okDisabled={Object.keys(errors).length > 0}
>
<Stack gap={3}>
<CurrencyFormField
required
fullWidth
label="Amount"
autoFocus
onChanged={(newValue) => {
setValue('pledge.amount', newValue.amount, { shouldValidate: true });
}}
validationError={errors.pledge?.amount?.message}
denom={defaultNymNodePledgeValue.denom}
initialValue={defaultNymNodePledgeValue.amount}
/>
<Box>
<CurrencyFormField
required
fullWidth
label="Operating cost"
onChanged={(newValue) => {
setValue('interval_operating_cost', newValue, { shouldValidate: true });
}}
validationError={errors.interval_operating_cost?.amount?.message}
denom={defaultNymNodeCostParamValues.interval_operating_cost.denom}
initialValue={defaultNymNodeCostParamValues.interval_operating_cost.amount}
/>
<FormHelperText>
Monthly operational costs of running your node. If your node is in the active set the amount will be paid
back to you from the rewards.
</FormHelperText>
</Box>
<Box>
<TextField
{...register('profit_margin_percent')}
name="profit_margin_percent"
label="Profit margin"
error={Boolean(errors.profit_margin_percent)}
helperText={errors.profit_margin_percent?.message}
fullWidth
/>
<FormHelperText>
The percentage of node rewards that you as the node operator take before rewards are distributed to operator
and delegators.
</FormHelperText>
</Box>
</Stack>
</SimpleModal>
);
};
export default NymNodeAmount;
@@ -0,0 +1,110 @@
import React from 'react';
import { Stack, TextField, FormControlLabel, Checkbox } from '@mui/material';
import { useForm } from 'react-hook-form';
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
import { TBondNymNodeArgs } from 'src/types';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
import { isValidHostname, validateRawPort } from 'src/utils';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
const defaultNymNodeValues: TBondNymNodeArgs['nymNode'] = {
identity_key: 'H6rXWgsW89QsVyaNSS3qBe9zZFLhBS6Gn3YRkGFSoFW9',
custom_http_port: 1,
host: '1.1.1.1',
};
const yupValidationSchema = yup.object().shape({
identity_key: yup.string().required('Identity key is required'),
host: yup
.string()
.required('A host is required')
.test('no-whitespace', 'Host cannot contain whitespace', (value) => !/\s/.test(value || ''))
.test('valid-host', 'A valid host is required', (value) => (value ? isValidHostname(value) : false)),
custom_http_port: yup
.number()
.required('A custom http port is required')
.test('valid-http', 'A valid http port is required', (value) => (value ? validateRawPort(value) : false)),
});
type NymNodeDataProps = {
onClose: () => void;
onBack: () => void;
onNext: () => Promise<void>;
step: number;
};
const NymNodeData = ({ onClose, onNext, step }: NymNodeDataProps) => {
const {
formState: { errors },
register,
setValue,
handleSubmit,
} = useForm({
mode: 'all',
defaultValues: defaultNymNodeValues,
resolver: yupResolver(yupValidationSchema),
});
const [showAdvancedOptions, setShowAdvancedOptions] = React.useState(false);
const handleNext = async () => {
handleSubmit(onNext)();
};
return (
<SimpleModal
open
onOk={handleNext}
onClose={onClose}
header="Bond Nym Node"
subHeader={`Step ${step}/3`}
okLabel="Next"
okDisabled={Object.keys(errors).length > 0}
>
<Stack gap={3}>
<IdentityKeyFormField
autoFocus
required
fullWidth
label="Identity Key"
initialValue={defaultNymNodeValues.identity_key}
errorText={errors.identity_key?.message?.toString()}
onChanged={(value) => setValue('identity_key', value, { shouldValidate: true })}
showTickOnValid={false}
/>
<TextField
{...register('host')}
name="host"
label="Host"
error={Boolean(errors.host)}
helperText={errors.host?.message}
required
InputLabelProps={{ shrink: true }}
/>
<FormControlLabel
control={<Checkbox onChange={() => setShowAdvancedOptions((show) => !show)} checked={showAdvancedOptions} />}
label="Show advanced options"
/>
{showAdvancedOptions && (
<Stack direction="row" gap={3} sx={{ mb: 2 }}>
<TextField
{...register('custom_http_port')}
name="custom_http_port"
label="Custom HTTP port"
error={Boolean(errors.custom_http_port)}
helperText={errors.custom_http_port?.message}
fullWidth
InputLabelProps={{ shrink: true }}
/>
</Stack>
)}
</Stack>
</SimpleModal>
);
};
export default NymNodeData;
@@ -0,0 +1,111 @@
import React, { useEffect, useState } from 'react';
import { Stack, TextField, Typography } from '@mui/material';
import { useForm } from 'react-hook-form';
import { CopyToClipboard } from 'src/components/CopyToClipboard';
import { ErrorModal } from 'src/components/Modals/ErrorModal';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { useBondingContext } from 'src/context';
import { TBondNymNodeArgs } from 'src/types';
import { Signature } from 'src/pages/bonding/types';
const NymNodeSignature = ({
nymNode,
pledge,
costParams,
step,
onNext,
onClose,
onBack,
}: {
nymNode: TBondNymNodeArgs['nymNode'];
pledge: TBondNymNodeArgs['pledge'];
costParams: TBondNymNodeArgs['costParams'];
step: number;
onNext: (data: Signature) => void;
onClose: () => void;
onBack: () => void;
}) => {
const [message, setMessage] = useState<string>('');
const [error, setError] = useState<string>();
const { generateNymNodeMsgPayload } = useBondingContext();
const {
register,
formState: { errors },
} = useForm<Signature>();
const generateMessage = async () => {
try {
const msg = await generateNymNodeMsgPayload({
nymNode,
pledge,
costParams,
});
if (msg) {
setMessage(msg);
}
} catch (e) {
console.error(e);
setError('Something went wrong while generating the payload signature');
}
};
useEffect(() => {
generateMessage();
}, [nymNode, pledge, costParams]);
const onSubmit = async (data: Signature) => {
onNext(data);
};
if (error) {
return <ErrorModal open message={error} onClose={() => {}} />;
}
return (
<SimpleModal
open
onOk={() =>
onSubmit({
signature: 'signature',
})
}
onClose={onClose}
header="Bond Nym Node"
subHeader={`Step ${step}/3`}
okLabel="Next"
onBack={onBack}
okDisabled={Object.keys(errors).length > 0}
>
<Stack gap={3} mb={3}>
<Typography variant="body1">
Copy the message below and sign it:
<br />
If you are using a nym-node:
<br />
<code>nym-node sign --id &lt;your-node-id&gt; --contract-msg &lt;payload-generated-by-the-wallet&gt;</code>
<br />
Then paste the signature in the next field.
</Typography>
<TextField id="outlined-multiline-static" multiline rows={7} value={message} fullWidth disabled />
<Stack direction="row" alignItems="center" gap={1} justifyContent="end">
<Typography fontWeight={600}>Copy Message</Typography>
{message && <CopyToClipboard text={message} iconButton />}
</Stack>
<TextField
{...register('signature')}
id="outlined-multiline-static"
name="signature"
rows={3}
placeholder="Paste Signature"
multiline
fullWidth
required
/>
</Stack>
</SimpleModal>
);
};
export default NymNodeSignature;
@@ -0,0 +1,58 @@
import * as Yup from 'yup';
import { TauriContractStateParams } from 'src/types';
import { isLessThan, isGreaterThan, validateAmount } from 'src/utils';
const operatingCostAndPmValidation = (params?: TauriContractStateParams) => {
const defaultParams = {
profit_margin_percent: {
minimum: parseFloat(params?.profit_margin.minimum || '0%'),
maximum: parseFloat(params?.profit_margin.maximum || '100%'),
},
interval_operating_cost: {
minimum: parseFloat(params?.operating_cost.minimum.amount || '0'),
maximum: parseFloat(params?.operating_cost.maximum.amount || '1000000000'),
},
};
return {
profit_margin_percent: Yup.number()
.required('Profit Percentage is required')
.min(defaultParams.profit_margin_percent.minimum)
.max(defaultParams.profit_margin_percent.maximum),
interval_operating_cost: Yup.object().shape({
amount: Yup.string()
.required('An operating cost is required')
// eslint-disable-next-line prefer-arrow-callback
.test('valid-operating-cost', 'A valid amount is required', async function isValidAmount(this, value) {
if (
value &&
(!Number(value) ||
isLessThan(+value, defaultParams.interval_operating_cost.minimum) ||
isGreaterThan(+value, Number(defaultParams.interval_operating_cost.maximum)))
) {
return this.createError({
message: `A valid amount is required (min ${defaultParams?.interval_operating_cost.minimum} - max ${defaultParams?.interval_operating_cost.maximum})`,
});
}
return true;
}),
}),
};
};
export const nymNodeAmountSchema = (params?: TauriContractStateParams) =>
Yup.object().shape({
pledge: Yup.object().shape({
amount: Yup.string()
.required('An amount is required')
.test('valid-amount', 'Pledge error', async function isValidAmount(this, value) {
const isValid = await validateAmount(value || '', '100');
if (!isValid) {
return this.createError({ message: 'A valid amount is required (min 100)' });
}
return true;
}),
}),
...operatingCostAndPmValidation(params),
});
@@ -4,15 +4,11 @@ import { CurrencyDenom, TNodeType } from '@nymproject/types';
import { ConfirmTx } from 'src/components/ConfirmTX';
import { ModalListItem } from 'src/components/Modals/ModalListItem';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { TPoolOption } from 'src/components/TokenPoolSelector';
import { useGetFee } from 'src/hooks/useGetFee';
import { GatewayAmount, GatewayData, Signature } from 'src/pages/bonding/types';
import { simulateBondGateway, simulateVestingBondGateway } from 'src/requests';
import { TBondGatewayArgs } from 'src/types';
import { GatewayAmount, GatewayData } from 'src/pages/bonding/types';
import { BalanceWarning } from 'src/components/FeeWarning';
import { AppContext } from 'src/context';
import { BondGatewayForm } from '../forms/BondGatewayForm';
import { gatewayToTauri } from '../utils';
import { BondGatewayForm } from '../forms/legacyForms/BondGatewayForm';
const defaultGatewayValues: GatewayData = {
identityKey: '',
@@ -34,14 +30,12 @@ const defaultAmountValues = (denom: CurrencyDenom) => ({
export const BondGatewayModal = ({
denom,
hasVestingTokens,
onBondGateway,
onSelectNodeType,
onClose,
onError,
}: {
denom: CurrencyDenom;
hasVestingTokens: boolean;
onBondGateway: (data: TBondGatewayArgs, tokenPool: TPoolOption) => void;
onSelectNodeType: (type: TNodeType) => void;
onClose: () => void;
onError: (e: string) => void;
@@ -49,9 +43,8 @@ export const BondGatewayModal = ({
const [step, setStep] = useState<1 | 2 | 3>(1);
const [gatewayData, setGatewayData] = useState<GatewayData>(defaultGatewayValues);
const [amountData, setAmountData] = useState<GatewayAmount>(defaultAmountValues(denom));
const [signature, setSignature] = useState<string>();
const { fee, getFee, resetFeeState, feeError } = useGetFee();
const { fee, resetFeeState, feeError } = useGetFee();
const { userBalance } = useContext(AppContext);
useEffect(() => {
@@ -83,32 +76,7 @@ export const BondGatewayModal = ({
setStep(3);
};
const handleUpdateSignature = async (data: Signature) => {
setSignature(data.signature);
const payload = {
pledge: amountData.amount,
msgSignature: data.signature,
gateway: gatewayToTauri(gatewayData),
};
if (amountData.tokenPool === 'balance') {
await getFee<TBondGatewayArgs>(simulateBondGateway, payload);
} else {
await getFee<TBondGatewayArgs>(simulateVestingBondGateway, payload);
}
};
const handleConfirm = async () => {
await onBondGateway(
{
pledge: amountData.amount,
msgSignature: signature as string,
gateway: gatewayToTauri(gatewayData),
},
amountData.tokenPool as TPoolOption,
);
};
const handleConfirm = async () => {};
if (fee) {
return (
@@ -154,7 +122,6 @@ export const BondGatewayModal = ({
hasVestingTokens={hasVestingTokens}
onValidateGatewayData={handleUpdateGatwayData}
onValidateAmountData={handleUpdateAmountData}
onValidateSignature={handleUpdateSignature}
onSelectNodeType={onSelectNodeType}
/>
</Box>
@@ -1,163 +0,0 @@
import React, { useContext, useEffect, useState } from 'react';
import { CurrencyDenom, TNodeType } from '@nymproject/types';
import { ConfirmTx } from 'src/components/ConfirmTX';
import { ModalListItem } from 'src/components/Modals/ModalListItem';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { TPoolOption } from 'src/components/TokenPoolSelector';
import { useGetFee } from 'src/hooks/useGetFee';
import { MixnodeAmount, MixnodeData, Signature } from 'src/pages/bonding/types';
import { simulateBondMixnode, simulateVestingBondMixnode } from 'src/requests';
import { TBondMixNodeArgs } from 'src/types';
import { BalanceWarning } from 'src/components/FeeWarning';
import { AppContext } from 'src/context';
import { BondMixnodeForm } from '../forms/BondMixnodeForm';
import { costParamsToTauri, mixnodeToTauri } from '../utils';
const defaultMixnodeValues: MixnodeData = {
identityKey: '',
sphinxKey: '',
ownerSignature: '',
host: '',
version: '',
mixPort: 1789,
verlocPort: 1790,
httpApiPort: 8000,
};
const defaultAmountValues = (denom: CurrencyDenom) => ({
amount: { amount: '100', denom },
operatorCost: { amount: '40', denom },
profitMargin: '10',
tokenPool: 'balance',
});
export const BondMixnodeModal = ({
denom,
hasVestingTokens,
onBondMixnode,
onSelectNodeType,
onClose,
onError,
}: {
denom: CurrencyDenom;
hasVestingTokens: boolean;
onBondMixnode: (data: TBondMixNodeArgs, tokenPool: TPoolOption) => void;
onSelectNodeType: (type: TNodeType) => void;
onClose: () => void;
onError: (e: string) => void;
}) => {
const [step, setStep] = useState<1 | 2 | 3 | 4>(1);
const [mixnodeData, setMixnodeData] = useState<MixnodeData>(defaultMixnodeValues);
const [amountData, setAmountData] = useState<MixnodeAmount>(defaultAmountValues(denom));
const [signature, setSignature] = useState<string>();
const { fee, getFee, resetFeeState, feeError } = useGetFee();
const { userBalance } = useContext(AppContext);
useEffect(() => {
if (feeError) {
onError(feeError);
}
}, [feeError]);
const validateStep = async (s: number) => {
const event = new CustomEvent('validate_bond_mixnode_step', { detail: { step: s } });
window.dispatchEvent(event);
};
const handleBack = () => {
if (step === 2) {
setStep(1);
} else if (step === 3) {
setStep(2);
}
};
const handleUpdateMixnodeData = (data: MixnodeData) => {
setMixnodeData(data);
setStep(2);
};
const handleUpdateAmountData = async (data: MixnodeAmount) => {
setAmountData({ ...data });
setStep(3);
};
const handleUpdateSignature = async (data: Signature) => {
setSignature(data.signature);
const payload = {
pledge: amountData.amount,
msgSignature: data.signature,
mixnode: mixnodeToTauri(mixnodeData),
costParams: costParamsToTauri(amountData),
};
if (amountData.tokenPool === 'balance') {
await getFee<TBondMixNodeArgs>(simulateBondMixnode, payload);
} else {
await getFee<TBondMixNodeArgs>(simulateVestingBondMixnode, payload);
}
};
const handleConfirm = async () => {
await onBondMixnode(
{
pledge: amountData.amount,
msgSignature: signature as string,
mixnode: mixnodeToTauri(mixnodeData),
costParams: costParamsToTauri(amountData),
},
amountData.tokenPool as TPoolOption,
);
};
if (fee) {
return (
<ConfirmTx
open
header="Bond details"
fee={fee}
onClose={onClose}
onPrev={resetFeeState}
onConfirm={handleConfirm}
>
<ModalListItem label="Node identity key" value={mixnodeData.identityKey} divider />
<ModalListItem
label="Amount"
value={`${amountData.amount.amount} ${amountData.amount.denom.toUpperCase()}`}
divider
/>
{fee.amount?.amount && userBalance.balance && (
<BalanceWarning fee={fee.amount?.amount} tx={amountData.amount.amount} />
)}
</ConfirmTx>
);
}
return (
<SimpleModal
open
onOk={async () => {
await validateStep(step);
}}
onBack={step === 2 || step === 3 ? handleBack : undefined}
onClose={onClose}
header="Bond mixnode"
subHeader={`Step ${step}/3`}
okLabel="Next"
>
<BondMixnodeForm
step={step}
denom={denom}
mixnodeData={mixnodeData}
amountData={amountData}
hasVestingTokens={hasVestingTokens}
onValidateMixnodeData={handleUpdateMixnodeData}
onValidateAmountData={handleUpdateAmountData}
onValidateSignature={handleUpdateSignature}
onSelectNodeType={onSelectNodeType}
/>
</SimpleModal>
);
};
@@ -0,0 +1,88 @@
import React, { useContext, useEffect } from 'react';
import { ConfirmTx } from 'src/components/ConfirmTX';
import { ModalListItem } from 'src/components/Modals/ModalListItem';
import { useGetFee } from 'src/hooks/useGetFee';
import { Signature } from 'src/pages/bonding/types';
import { BalanceWarning } from 'src/components/FeeWarning';
import { AppContext } from 'src/context';
import FormContextProvider, { useFormContext } from '../forms/nym-node/FormContext';
import NymNodeData from '../forms/nym-node/NymNodeData';
import NymNodeAmount from '../forms/nym-node/NymNodeAmount';
import NymNodeSignature from '../forms/nym-node/NymNodeSignature';
export const BondNymNodeModal = ({ onClose }: { onClose: () => void }) => {
const { fee, resetFeeState, feeError } = useGetFee();
const { userBalance } = useContext(AppContext);
const { setStep, step, onError, setSignature, amountData, costParams, nymNodeData } = useFormContext();
useEffect(() => {
if (feeError) {
onError(feeError);
}
}, [feeError]);
const handleUpdateMixnodeData = async () => {
setStep(2);
};
const handleUpdateSignature = async (data: Signature) => {
setSignature(data.signature);
};
const handleConfirm = async () => {};
if (fee) {
return (
<ConfirmTx
open
header="Bond details"
fee={fee}
onClose={onClose}
onPrev={resetFeeState}
onConfirm={handleConfirm}
>
<ModalListItem label="Node identity key" value={nymNodeData.identity_key} divider />
<ModalListItem label="Amount" value={`${amountData.amount} ${amountData.denom.toUpperCase()}`} divider />
{fee.amount?.amount && userBalance.balance && (
<BalanceWarning fee={fee.amount?.amount} tx={amountData.amount} />
)}
</ConfirmTx>
);
}
if (step === 1) {
return <NymNodeData onClose={onClose} onBack={onClose} onNext={handleUpdateMixnodeData} step={step} />;
}
if (step === 2) {
return <NymNodeAmount onClose={onClose} onBack={() => setStep(1)} onNext={async () => setStep(3)} step={step} />;
}
if (step === 3) {
return (
<NymNodeSignature
nymNode={nymNodeData}
pledge={amountData}
costParams={costParams}
onNext={handleUpdateSignature}
onClose={onClose}
onBack={() => setStep(2)}
step={step}
/>
);
}
return null;
};
export const BondNymNodeModalWithState = ({ open, onClose }: { open: boolean; onClose: () => void }) => {
if (!open) {
return null;
}
return (
<FormContextProvider>
<BondNymNodeModal onClose={onClose} />
</FormContextProvider>
);
};
@@ -0,0 +1,28 @@
import React from 'react';
import { Box, Typography } from '@mui/material';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
const MigrateLegacyNode = ({
open,
onClose,
handleMigrate,
}: {
open: boolean;
onClose: () => void;
handleMigrate: () => Promise<void>;
}) => (
<SimpleModal
open={open}
header={<Typography sx={{ fontWeight: 700 }}>Migrate Legacy Node</Typography>}
onOk={handleMigrate}
okLabel="Migrate"
onClose={onClose}
sx={{ maxWidth: 500 }}
>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<Typography>You have a legacy node that needs to be migrated to the new system.</Typography>
</Box>
</SimpleModal>
);
export default MigrateLegacyNode;
@@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react';
import { Box, Button, FormHelperText, TextField, Typography } from '@mui/material';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { Node as NodeIcon } from 'src/svg-icons/node';
import { TBondedMixnode } from 'src/context';
import { Tabs } from 'src/components/Tabs';
import { ModalListItem } from 'src/components/Modals/ModalListItem';
import { attachDefaultOperatingCost, isDecimal, toPercentFloatString } from 'src/utils';
@@ -11,6 +10,7 @@ import { ConfirmTx } from 'src/components/ConfirmTX';
import { simulateUpdateMixnodeCostParams, simulateVestingUpdateMixnodeCostParams } from 'src/requests';
import { LoadingModal } from 'src/components/Modals/LoadingModal';
import { FeeDetails } from '@nymproject/types';
import { TBondedMixnode } from 'src/requests/mixnodeDetails';
// Now we are using the node setting page instead of this modal
export const NodeSettings = ({
@@ -5,9 +5,10 @@ import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { ModalFee } from 'src/components/Modals/ModalFee';
import { useGetFee } from 'src/hooks/useGetFee';
import { simulateClaimOperatorReward, simulateVestingClaimOperatorReward } from 'src/requests';
import { AppContext, TBondedMixnode } from 'src/context';
import { AppContext } from 'src/context';
import { BalanceWarning } from 'src/components/FeeWarning';
import { Box } from '@mui/material';
import { TBondedMixnode } from 'src/requests/mixnodeDetails';
export const RedeemRewardsModal = ({
node,
@@ -1,9 +1,10 @@
import * as React from 'react';
import { useEffect } from 'react';
import { Typography } from '@mui/material';
import { TBondedGateway, TBondedMixnode } from 'src/context';
import { useGetFee } from 'src/hooks/useGetFee';
import { isGateway, isMixnode } from 'src/types';
import { TBondedGateway } from 'src/requests/gatewayDetails';
import { TBondedMixnode } from 'src/requests/mixnodeDetails';
import { ModalFee } from '../../Modals/ModalFee';
import { ModalListItem } from '../../Modals/ModalListItem';
import { SimpleModal } from '../../Modals/SimpleModal';
@@ -10,8 +10,9 @@ import { useGetFee } from 'src/hooks/useGetFee';
import { decCoinToDisplay, validateAmount } from 'src/utils';
import { simulateUpdateBond, simulateVestingUpdateBond } from 'src/requests';
import { TSimulateUpdateBondArgs, TUpdateBondArgs } from 'src/types';
import { AppContext, TBondedMixnode } from 'src/context';
import { AppContext } from 'src/context';
import { BalanceWarning } from 'src/components/FeeWarning';
import { TBondedMixnode } from 'src/requests/mixnodeDetails';
import { TPoolOption } from '../../TokenPoolSelector';
export const UpdateBondAmountModal = ({
@@ -0,0 +1,153 @@
import React, { useContext, useEffect, useState } from 'react';
import { Box, Stack } from '@mui/material';
import Big from 'big.js';
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
import { ModalListItem } from 'src/components/Modals/ModalListItem';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { DecCoin } from '@nymproject/types';
import { ConfirmTx } from 'src/components/ConfirmTX';
import { useGetFee } from 'src/hooks/useGetFee';
import { decCoinToDisplay, validateAmount } from 'src/utils';
import { simulateUpdateBond } from 'src/requests';
import { TSimulateUpdateBondArgs, TUpdateBondArgs } from 'src/types';
import { AppContext } from 'src/context';
import { BalanceWarning } from 'src/components/FeeWarning';
import { TBondedNymNode } from 'src/requests/nymNodeDetails';
import { TPoolOption } from '../../TokenPoolSelector';
export const UpdateBondAmountNymNode = ({
node,
onUpdateBond,
onClose,
onError,
}: {
node: TBondedNymNode;
onUpdateBond: (data: TUpdateBondArgs, tokenPool: TPoolOption) => Promise<void>;
onClose: () => void;
onError: (e: string) => void;
}) => {
const { bond: currentBond, stakeSaturation, uncappedStakeSaturation } = node;
const { fee, getFee, resetFeeState, feeError } = useGetFee();
const [newBond, setNewBond] = useState<DecCoin | undefined>();
const [errorAmount, setErrorAmount] = useState(false);
const { printBalance, userBalance } = useContext(AppContext);
useEffect(() => {
if (feeError) {
onError(feeError);
}
}, [feeError]);
const handleConfirm = async () => {
if (!newBond) {
return;
}
await onUpdateBond(
{
currentPledge: currentBond,
newPledge: newBond,
fee: fee?.fee,
},
'balance',
);
};
const handleAmountChanged = async (value: DecCoin) => {
const { amount } = value;
setNewBond(value);
if (!amount || !Number(amount)) {
setErrorAmount(true);
} else if (Big(amount).eq(currentBond.amount)) {
setErrorAmount(true);
} else {
const validAmount = await validateAmount(amount, '1');
if (!validAmount) {
setErrorAmount(true);
return;
}
setErrorAmount(false);
}
};
const handleOnOk = async () => {
if (!newBond) {
return;
}
await getFee<TSimulateUpdateBondArgs>(simulateUpdateBond, {
currentPledge: currentBond,
newPledge: newBond,
});
};
const newBondToDisplay = () => {
const coin = decCoinToDisplay(newBond as DecCoin);
return `${coin.amount} ${coin.denom}`;
};
if (fee)
return (
<ConfirmTx
open
header="Change bond details"
fee={fee}
onClose={onClose}
onPrev={resetFeeState}
onConfirm={handleConfirm}
>
<ModalListItem label="New bond details" value={newBondToDisplay()} divider />
<ModalListItem label="Change bond details" value={`${currentBond.amount} ${currentBond.denom}`} divider />
{userBalance.balance?.amount.amount && fee?.amount?.amount && (
<Box sx={{ my: 2 }}>
<BalanceWarning fee={fee?.amount?.amount} tx={newBond?.amount} />
</Box>
)}
</ConfirmTx>
);
return (
<SimpleModal
open
header="Change bond amount"
subHeader="Add or reduce amount of tokens on your node"
okLabel="Next"
onOk={handleOnOk}
okDisabled={errorAmount || !newBond}
onClose={onClose}
>
<Stack gap={3}>
<Box display="flex" gap={1}>
<CurrencyFormField
autoFocus
label="New bond amount"
denom={currentBond.denom}
onChanged={(value) => {
handleAmountChanged(value);
}}
fullWidth
validationError={errorAmount ? 'Please enter a valid amount' : undefined}
/>
</Box>
<Box>
<ModalListItem fontWeight={600} label="Account balance" value={printBalance} divider />
<ModalListItem label="Current bond amount" value={`${currentBond.amount} ${currentBond.denom}`} divider />
{uncappedStakeSaturation ? (
<ModalListItem
label="Node saturation"
value={`${uncappedStakeSaturation}%`}
sxValue={{ color: 'error.main' }}
divider
/>
) : (
<ModalListItem label="Node saturation" value={`${stakeSaturation}%`} divider />
)}
<ModalListItem label="Est. fee for this operation will be calculated in the next page" value="" divider />
</Box>
</Stack>
</SimpleModal>
);
};
+2 -2
View File
@@ -1,4 +1,4 @@
import { Gateway, MixNode, MixNodeCostParams } from '@nymproject/types';
import { Gateway, MixNode, NodeCostParams } from '@nymproject/types';
import { GatewayData, MixnodeAmount, MixnodeData } from '../../pages/bonding/types';
import { toPercentFloatString } from '../../utils';
@@ -14,7 +14,7 @@ export function mixnodeToTauri(data: MixnodeData): MixNode {
};
}
export function costParamsToTauri(data: MixnodeAmount): MixNodeCostParams {
export function costParamsToTauri(data: MixnodeAmount): NodeCostParams {
return {
profit_margin_percent: toPercentFloatString(data.profitMargin),
interval_operating_cost: {
-1
View File
@@ -32,5 +32,4 @@ export const NodeStatus = ({ status }: { status: MixnodeStatus }) => {
default:
return null;
}
return null;
};
+1 -1
View File
@@ -20,7 +20,7 @@ export const NymCard: FCWithChildren<{
dataTestid?: string;
sx?: SxProps;
sxTitle?: SxProps;
children: React.ReactNode;
children?: React.ReactNode;
}> = ({ title, subheader, Action, Icon, noPadding, borderless, children, dataTestid, sx, sxTitle }) => (
<Card variant="outlined" sx={{ overflow: 'auto', ...(borderless && { border: 'none', dropShadow: 'none' }), ...sx }}>
<CardHeader
+72 -504
View File
@@ -1,146 +1,46 @@
/* eslint-disable @typescript-eslint/naming-convention */
import {
FeeDetails,
DecCoin,
MixnodeStatus,
TransactionExecuteResult,
decimalToPercentage,
SelectionChance,
InclusionProbabilityResponse,
decimalToFloatApproximation,
} from '@nymproject/types';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import Big from 'big.js';
import {
EnumNodeType,
isGateway,
isMixnode,
TBondGatewayArgs,
TBondGatewaySignatureArgs,
TBondMixNodeArgs,
TBondMixnodeSignatureArgs,
TUpdateBondArgs,
TNodeDescription,
} from 'src/types';
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { FeeDetails, TransactionExecuteResult } from '@nymproject/types';
import { isGateway, isMixnode, TUpdateBondArgs, isNymNode, TNymNodeSignatureArgs } from 'src/types';
import { Console } from 'src/utils/console';
import {
bondGateway as bondGatewayRequest,
bondMixNode as bondMixNodeRequest,
claimOperatorReward,
getGatewayBondDetails,
getMixnodeBondDetails,
unbondGateway as unbondGatewayRequest,
unbondMixNode as unbondMixnodeRequest,
vestingBondGateway,
vestingBondMixNode,
vestingUnbondGateway,
vestingUnbondMixnode,
updateMixnodeCostParams as updateMixnodeCostParamsRequest,
vestingUpdateMixnodeCostParams as updateMixnodeVestingCostParamsRequest,
getNodeDescription as getNodeDescriptionRequest,
getMixnodeStatus,
getPendingOperatorRewards,
getMixnodeStakeSaturation,
vestingClaimOperatorReward,
getInclusionProbability,
getMixnodeAvgUptime,
getMixnodeRewardEstimation,
getGatewayReport,
getMixnodeUptime,
vestingGenerateMixnodeMsgPayload as vestingGenerateMixnodeMsgPayloadReq,
generateMixnodeMsgPayload as generateMixnodeMsgPayloadReq,
vestingGenerateGatewayMsgPayload as vestingGenerateGatewayMsgPayloadReq,
generateGatewayMsgPayload as generateGatewayMsgPayloadReq,
updateBond as updateBondReq,
vestingUpdateBond as vestingUpdateBondReq,
migrateVestedMixnode as tauriMigrateVestedMixnode,
} from '../requests';
import { useCheckOwnership } from '../hooks/useCheckOwnership';
import useGetNodeDetails from 'src/hooks/useGetNodeDetails';
import { TBondedNymNode } from 'src/requests/nymNodeDetails';
import { TBondedMixnode } from 'src/requests/mixnodeDetails';
import { TBondedGateway } from 'src/requests/gatewayDetails';
import { toPercentFloatString } from 'src/utils';
import { AppContext } from './main';
import {
fireRequests,
TauriReq,
attachDefaultOperatingCost,
decCoinToDisplay,
toPercentFloatString,
toPercentIntegerString,
unymToNym,
} from '../utils';
claimOperatorReward,
unbondGateway as unbondGatewayRequest,
unbondMixNode as unbondMixnodeRequest,
unbondNymNode as unbondNymNodeRequest,
vestingClaimOperatorReward,
generateNymNodeMsgPayload as generateNymNodeMsgPayloadReq,
updateBond as updateBondReq,
migrateVestedMixnode as tauriMigrateVestedMixnode,
migrateLegacyMixnode as migrateLegacyMixnodeReq,
migrateLegacyGateway as migrateLegacyGatewayReq,
} from '../requests';
export type TBondedMixnode = {
name?: string;
mixId: number;
identityKey: string;
stake: DecCoin;
bond: DecCoin;
stakeSaturation: string;
uncappedStakeSaturation?: number;
profitMargin: string;
operatorRewards?: DecCoin;
delegators: number;
status: MixnodeStatus;
proxy?: string;
operatorCost: DecCoin;
host: string;
estimatedRewards?: DecCoin;
activeSetProbability?: SelectionChance;
standbySetProbability?: SelectionChance;
routingScore: number;
httpApiPort: number;
mixPort: number;
verlocPort: number;
version: string;
isUnbonding: boolean;
uptime: number;
};
export interface TBondedGateway {
name?: string;
id: number;
identityKey: string;
ip: string;
bond: DecCoin;
location?: string;
proxy?: string;
host: string;
httpApiPort: number;
mixPort: number;
verlocPort: number;
version: string;
routingScore?: {
current: number;
average: number;
};
}
export type TokenPool = 'locked' | 'balance';
export type TBondedNode = TBondedMixnode | TBondedGateway | TBondedNymNode;
export type TBondingContext = {
isLoading: boolean;
error?: string;
bondedNode?: TBondedMixnode | TBondedGateway;
refresh: () => Promise<void>;
bondMixnode: (data: TBondMixNodeArgs, tokenPool: TokenPool) => Promise<TransactionExecuteResult | undefined>;
bondGateway: (data: TBondGatewayArgs, tokenPool: TokenPool) => Promise<TransactionExecuteResult | undefined>;
unbond: (fee?: FeeDetails) => Promise<TransactionExecuteResult | undefined>;
updateBondAmount: (data: TUpdateBondArgs, tokenPool: TokenPool) => Promise<TransactionExecuteResult | undefined>;
redeemRewards: (fee?: FeeDetails) => Promise<TransactionExecuteResult | undefined>;
updateMixnode: (pm: string, fee?: FeeDetails) => Promise<TransactionExecuteResult | undefined>;
generateMixnodeMsgPayload: (data: TBondMixnodeSignatureArgs) => Promise<string | undefined>;
generateGatewayMsgPayload: (data: TBondGatewaySignatureArgs) => Promise<string | undefined>;
bondedNode?: TBondedNode | null;
isVestingAccount: boolean;
refresh: () => void;
unbond: (fee?: FeeDetails) => Promise<TransactionExecuteResult | undefined>;
updateBondAmount: (data: TUpdateBondArgs) => Promise<TransactionExecuteResult | undefined>;
redeemRewards: (fee?: FeeDetails) => Promise<TransactionExecuteResult | undefined>;
generateNymNodeMsgPayload: (data: TNymNodeSignatureArgs) => Promise<string | undefined>;
migrateVestedMixnode: () => Promise<TransactionExecuteResult | undefined>;
migrateLegacyNode: () => Promise<TransactionExecuteResult | undefined>;
};
export const BondingContext = createContext<TBondingContext>({
isLoading: true,
refresh: async () => undefined,
bondMixnode: async () => {
throw new Error('Not implemented');
},
bondGateway: async () => {
throw new Error('Not implemented');
},
unbond: async () => {
throw new Error('Not implemented');
},
@@ -150,29 +50,27 @@ export const BondingContext = createContext<TBondingContext>({
redeemRewards: async () => {
throw new Error('Not implemented');
},
updateMixnode: async () => {
throw new Error('Not implemented');
},
generateMixnodeMsgPayload: async () => {
throw new Error('Not implemented');
},
generateGatewayMsgPayload: async () => {
generateNymNodeMsgPayload: async () => {
throw new Error('Not implemented');
},
migrateVestedMixnode: async () => {
throw new Error('Not implemented');
},
migrateLegacyNode: async () => {
throw new Error('Not implemented');
},
isVestingAccount: false,
});
export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Element => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string>();
const [bondedNode, setBondedNode] = useState<TBondedMixnode | TBondedGateway>();
const [isVestingAccount, setIsVestingAccount] = useState(false);
const { userBalance, clientDetails } = useContext(AppContext);
const { ownership, isLoading: isOwnershipLoading } = useCheckOwnership();
const { userBalance, clientDetails, network } = useContext(AppContext);
const { bondedNode, isLoading: isBondedNodeLoading } = useGetNodeDetails(clientDetails?.client_address, network);
useEffect(() => {
userBalance.fetchBalance();
@@ -186,317 +84,19 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen
const resetState = () => {
setError(undefined);
setBondedNode(undefined);
};
/**
* Fetch mixnode **optional** data.
* ⚠ The underlying queries are allowed to fail.
*/
const fetchMixnodeDetails = async (mixId: number, host: string, port: number) => {
const details: {
status: MixnodeStatus;
stakeSaturation: string;
estimatedRewards?: DecCoin;
uptime: number;
averageUptime?: number;
setProbability?: InclusionProbabilityResponse;
nodeDescription?: TNodeDescription | undefined;
operatorRewards?: DecCoin;
uncappedSaturation?: number;
} = {
status: 'not_found',
stakeSaturation: '0',
uptime: 0,
};
const statusReq: TauriReq<typeof getMixnodeStatus> = {
name: 'getMixnodeStatus',
request: () => getMixnodeStatus(mixId),
onFulfilled: (value) => {
details.status = value.status;
},
};
const uptimeReq: TauriReq<typeof getMixnodeUptime> = {
name: 'getMixnodeUptime',
request: () => getMixnodeUptime(mixId),
onFulfilled: (value) => {
details.uptime = value;
},
};
const stakeSaturationReq: TauriReq<typeof getMixnodeStakeSaturation> = {
name: 'getMixnodeStakeSaturation',
request: () => getMixnodeStakeSaturation(mixId),
onFulfilled: (value) => {
details.stakeSaturation = decimalToPercentage(value.saturation);
const rawUncappedSaturation = decimalToFloatApproximation(value.uncapped_saturation);
if (rawUncappedSaturation && rawUncappedSaturation > 1) {
details.uncappedSaturation = Math.round(rawUncappedSaturation * 100);
}
},
};
const rewardReq: TauriReq<typeof getMixnodeRewardEstimation> = {
name: 'getMixnodeRewardEstimation',
request: () => getMixnodeRewardEstimation(mixId),
onFulfilled: (value) => {
const estimatedRewards = unymToNym(value.estimation.total_node_reward);
if (estimatedRewards) {
details.estimatedRewards = {
amount: estimatedRewards,
denom: 'nym',
};
}
},
};
const inclusionReq: TauriReq<typeof getInclusionProbability> = {
name: 'getInclusionProbability',
request: () => getInclusionProbability(mixId),
onFulfilled: (value) => {
details.setProbability = value;
},
};
const avgUptimeReq: TauriReq<typeof getMixnodeAvgUptime> = {
name: 'getMixnodeAvgUptime',
request: () => getMixnodeAvgUptime(),
onFulfilled: (value) => {
details.averageUptime = value as number | undefined;
},
};
const nodeDescReq: TauriReq<typeof getNodeDescriptionRequest> = {
name: 'getNodeDescription',
request: () => getNodeDescriptionRequest(host, port),
onFulfilled: (value) => {
details.nodeDescription = value;
},
};
const operatorRewardsReq: TauriReq<typeof getPendingOperatorRewards> = {
name: 'getPendingOperatorRewards',
request: () => getPendingOperatorRewards(clientDetails?.client_address || ''),
onFulfilled: (value) => {
details.operatorRewards = decCoinToDisplay(value);
},
};
await fireRequests([
statusReq,
uptimeReq,
stakeSaturationReq,
rewardReq,
inclusionReq,
avgUptimeReq,
nodeDescReq,
operatorRewardsReq,
]);
return details;
};
/**
* Fetch gateway **optional** data.
* ⚠ The underlying queries are allowed to fail.
*/
const fetchGatewayDetails = async (identityKey: string, host: string, port: number) => {
const details: {
routingScore?: { current: number; average: number } | undefined;
nodeDescription?: TNodeDescription | undefined;
} = {};
const reportReq: TauriReq<typeof getGatewayReport> = {
name: 'getGatewayReport',
request: () => getGatewayReport(identityKey),
onFulfilled: (value) => {
details.routingScore = { current: value.most_recent, average: value.last_day };
},
};
const nodeDescReq: TauriReq<typeof getNodeDescriptionRequest> = {
name: 'getNodeDescription',
request: () => getNodeDescriptionRequest(host, port),
onFulfilled: (value) => {
details.nodeDescription = value;
},
};
await fireRequests([reportReq, nodeDescReq]);
return details;
};
const calculateStake = (pledge: string, delegations: string) => {
let stake;
try {
stake = unymToNym(Big(pledge).plus(delegations));
} catch (e: any) {
Console.warn(`not a valid decimal number: ${e}`);
}
return stake;
};
const refresh = useCallback(async () => {
setIsLoading(true);
setError(undefined);
if (ownership.hasOwnership && ownership.nodeType === EnumNodeType.mixnode && clientDetails) {
try {
const data = await getMixnodeBondDetails();
if (data) {
const {
bond_information,
rewarding_details,
bond_information: { mix_id },
} = data;
const {
status,
stakeSaturation,
uncappedSaturation: uncappedStakeSaturation,
estimatedRewards,
uptime,
operatorRewards,
averageUptime,
nodeDescription,
setProbability,
} = await fetchMixnodeDetails(
mix_id,
bond_information.mix_node.host,
bond_information.mix_node.http_api_port,
);
setBondedNode({
id: data.bond_information.mix_id,
name: nodeDescription?.name,
mixId: mix_id,
identityKey: bond_information.mix_node.identity_key,
stake: {
amount: calculateStake(rewarding_details.operator, rewarding_details.delegates),
denom: bond_information.original_pledge.denom,
},
bond: decCoinToDisplay(bond_information.original_pledge),
profitMargin: toPercentIntegerString(rewarding_details.cost_params.profit_margin_percent),
delegators: rewarding_details.unique_delegations,
proxy: bond_information.proxy,
operatorRewards,
uptime,
status,
stakeSaturation,
uncappedStakeSaturation,
operatorCost: decCoinToDisplay(rewarding_details.cost_params.interval_operating_cost),
host: bond_information.mix_node.host.replace(/\s/g, ''),
routingScore: averageUptime,
activeSetProbability: setProbability?.in_active,
standbySetProbability: setProbability?.in_reserve,
estimatedRewards,
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,
isUnbonding: bond_information.is_unbonding,
} as TBondedMixnode);
}
} catch (e: any) {
Console.warn(e);
setError(`While fetching current bond state, an error occurred: ${e}`);
}
}
if (ownership.hasOwnership && ownership.nodeType === EnumNodeType.gateway) {
try {
const data = await getGatewayBondDetails();
if (data) {
const { gateway, proxy } = data;
const { nodeDescription, routingScore } = await fetchGatewayDetails(
gateway.identity_key,
data.gateway.host,
data.gateway.clients_port,
);
setBondedNode({
name: nodeDescription?.name,
identityKey: gateway.identity_key,
mixPort: gateway.mix_port,
httpApiPort: gateway.clients_port,
host: gateway.host,
ip: gateway.host,
location: gateway.location,
bond: decCoinToDisplay(data.pledge_amount),
proxy,
routingScore,
version: gateway.version,
} as TBondedGateway);
}
} catch (e: any) {
Console.warn(e);
setError(`While fetching current bond state, an error occurred: ${e}`);
}
}
if (!ownership.hasOwnership) {
resetState();
}
setIsLoading(false);
}, [ownership]);
useEffect(() => {
refresh();
}, [ownership, refresh]);
const bondMixnode = async (data: TBondMixNodeArgs, tokenPool: TokenPool) => {
let tx: TransactionExecuteResult | undefined;
setIsLoading(true);
try {
if (tokenPool === 'balance') {
tx = await bondMixNodeRequest(data);
await userBalance.fetchBalance();
}
if (tokenPool === 'locked') {
tx = await vestingBondMixNode(data);
await userBalance.fetchTokenAllocation();
}
return tx;
} catch (e: any) {
Console.warn(e);
setError(`an error occurred: ${e}`);
} finally {
setIsLoading(false);
}
return undefined;
};
const bondGateway = async (data: TBondGatewayArgs, tokenPool: TokenPool) => {
let tx: TransactionExecuteResult | undefined;
setIsLoading(true);
try {
if (tokenPool === 'balance') {
tx = await bondGatewayRequest(data);
await userBalance.fetchBalance();
}
if (tokenPool === 'locked') {
tx = await vestingBondGateway(data);
await userBalance.fetchTokenAllocation();
}
return tx;
} catch (e: any) {
Console.warn(e);
setError(`an error occurred: ${e}`);
} finally {
setIsLoading(false);
}
return undefined;
const refresh = () => {
resetState();
};
const unbond = async (fee?: FeeDetails) => {
let tx;
setIsLoading(true);
try {
if (bondedNode && isMixnode(bondedNode) && bondedNode.proxy) tx = await vestingUnbondMixnode(fee?.fee);
if (bondedNode && isNymNode(bondedNode)) tx = await unbondNymNodeRequest(fee?.fee);
if (bondedNode && isMixnode(bondedNode) && !bondedNode.proxy) tx = await unbondMixnodeRequest(fee?.fee);
if (bondedNode && isGateway(bondedNode) && bondedNode.proxy) tx = await vestingUnbondGateway(fee?.fee);
if (bondedNode && isGateway(bondedNode) && !bondedNode.proxy) tx = await unbondGatewayRequest(fee?.fee);
} catch (e) {
Console.warn(e);
@@ -507,35 +107,11 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen
return tx;
};
const updateMixnode = async (pm: string, fee?: FeeDetails) => {
let tx;
setIsLoading(true);
// TODO: this will have to be updated with allowing users to provide their operating cost in the form
const defaultCostParams = await attachDefaultOperatingCost(toPercentFloatString(pm));
try {
// JS: this check is not entirely valid. you can have proxy field set whilst not using the vesting contract,
// you have to check if proxy exists AND if it matches the known vesting contract address!
if (bondedNode?.proxy) {
tx = await updateMixnodeVestingCostParamsRequest(defaultCostParams, fee?.fee);
} else {
tx = await updateMixnodeCostParamsRequest(defaultCostParams, fee?.fee);
}
} catch (e: any) {
Console.warn(e);
setError(`an error occurred: ${e}`);
} finally {
setIsLoading(false);
}
return tx;
};
const redeemRewards = async (fee?: FeeDetails) => {
let tx;
setIsLoading(true);
try {
if (bondedNode?.proxy) tx = await vestingClaimOperatorReward(fee?.fee);
if (bondedNode && !isNymNode(bondedNode)) tx = await vestingClaimOperatorReward(fee?.fee);
else tx = await claimOperatorReward(fee?.fee);
} catch (e: any) {
setError(`an error occurred: ${e}`);
@@ -545,18 +121,12 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen
return tx;
};
const updateBondAmount = async (data: TUpdateBondArgs, tokenPool: TokenPool) => {
const updateBondAmount = async (data: TUpdateBondArgs) => {
let tx: TransactionExecuteResult | undefined;
setIsLoading(true);
try {
if (tokenPool === 'balance') {
tx = await updateBondReq(data);
await userBalance.fetchBalance();
}
if (tokenPool === 'locked') {
tx = await vestingUpdateBondReq(data);
await userBalance.fetchTokenAllocation();
}
tx = await updateBondReq(data);
await userBalance.fetchBalance();
return tx;
} catch (e: any) {
@@ -568,40 +138,26 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen
return undefined;
};
const generateMixnodeMsgPayload = async (data: TBondMixnodeSignatureArgs) => {
let message;
const generateNymNodeMsgPayload = async (data: TNymNodeSignatureArgs) => {
setIsLoading(true);
try {
if (data.tokenPool === 'locked') {
message = await vestingGenerateMixnodeMsgPayloadReq(data);
} else {
message = await generateMixnodeMsgPayloadReq(data);
}
} catch (e) {
Console.warn(e);
setError(`an error occurred: ${e}`);
} finally {
setIsLoading(false);
}
return message;
};
const generateGatewayMsgPayload = async (data: TBondGatewaySignatureArgs) => {
let message;
setIsLoading(true);
try {
if (data.tokenPool === 'locked') {
message = await vestingGenerateGatewayMsgPayloadReq(data);
} else {
message = await generateGatewayMsgPayloadReq(data);
}
const message = await generateNymNodeMsgPayloadReq({
nymNode: data.nymNode,
pledge: data.pledge,
costParams: {
...data.costParams,
profit_margin_percent: toPercentFloatString(data.costParams.profit_margin_percent),
},
});
return message;
} catch (e) {
Console.warn(e);
setError(`an error occurred: ${e}`);
} finally {
setIsLoading(false);
}
return message;
return undefined;
};
const migrateVestedMixnode = async () => {
@@ -611,24 +167,36 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen
return tx;
};
const migrateLegacyNode = async () => {
setIsLoading(true);
let tx: TransactionExecuteResult | undefined;
if (bondedNode && isMixnode(bondedNode)) {
tx = await migrateLegacyMixnodeReq();
}
if (bondedNode && isGateway(bondedNode)) {
tx = await migrateLegacyGatewayReq();
}
setIsLoading(false);
return tx;
};
const memoizedValue = useMemo(
() => ({
isLoading: isLoading || isOwnershipLoading,
isLoading: isLoading || isBondedNodeLoading,
error,
bondMixnode,
bondedNode,
bondGateway,
unbond,
updateMixnode,
refresh,
redeemRewards,
updateBondAmount,
generateMixnodeMsgPayload,
generateGatewayMsgPayload,
generateNymNodeMsgPayload,
migrateVestedMixnode,
migrateLegacyNode,
isVestingAccount,
}),
[isLoading, isOwnershipLoading, error, bondedNode, isVestingAccount],
[isLoading, error, bondedNode, isVestingAccount, isBondedNodeLoading],
);
return <BondingContext.Provider value={memoizedValue}>{children}</BondingContext.Provider>;
+12 -17
View File
@@ -1,9 +1,10 @@
import { FeeDetails, TransactionExecuteResult } from '@nymproject/types';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import type { Network } from 'src/types';
import { BondingContext, TBondedGateway, TBondedMixnode } from '../bonding';
import type { Network, TNymNodeSignatureArgs } from 'src/types';
import { TBondedMixnode } from 'src/requests/mixnodeDetails';
import { TBondedGateway } from 'src/requests/gatewayDetails';
import { BondingContext } from '../bonding';
import { mockSleep } from './utils';
import { TBondGatewaySignatureArgs, TBondMixnodeSignatureArgs } from '../../types';
const SLEEP_MS = 1000;
@@ -30,10 +31,11 @@ const bondedMixnodeMock: TBondedMixnode = {
version: '1.0.2',
isUnbonding: false,
uptime: 1,
proxy: null,
uncappedStakeSaturation: 100,
};
const bondedGatewayMock: TBondedGateway = {
id: 1,
name: 'Monster node',
identityKey: 'WayM2fYbtN6kxMwp1TrmQ4VwPks3URR5pBgWPWhzT98F',
ip: '112.43.234.57',
@@ -41,17 +43,18 @@ const bondedGatewayMock: TBondedGateway = {
host: '1.2.34.5 ',
httpApiPort: 8000,
mixPort: 1789,
verlocPort: 1790,
version: '1.0.2',
routingScore: {
average: 100,
current: 100,
},
location: 'Germany',
proxy: null,
};
const TxResultMock: TransactionExecuteResult = {
logs_json: '',
data_json: '',
msg_responses_json: '',
transaction_hash: '55303CD4B91FAC4C2715E40EBB52BB3B92829D9431B3A279D37B5CC58432E354',
gas_info: {
gas_wanted: { gas_units: BigInt(1) },
@@ -170,15 +173,7 @@ export const MockBondingContextProvider = ({
return feeMock;
};
const generateMixnodeMsgPayload = async (_data: TBondMixnodeSignatureArgs) => {
setIsLoading(true);
await mockSleep(SLEEP_MS);
triggerStateUpdate();
setIsLoading(false);
return '77dcaba7f41409984f4ebce4a386f59b10f1e65ed5514d1acdccae30174bd84b';
};
const generateGatewayMsgPayload = async (_data: TBondGatewaySignatureArgs) => {
const generateNymNodeMsgPayload = async (_data: TNymNodeSignatureArgs) => {
setIsLoading(true);
await mockSleep(SLEEP_MS);
triggerStateUpdate();
@@ -204,10 +199,10 @@ export const MockBondingContextProvider = ({
updateMixnode,
updateBondAmount,
checkOwnership,
generateMixnodeMsgPayload,
generateGatewayMsgPayload,
generateNymNodeMsgPayload,
isVestingAccount: false,
migrateVestedMixnode: async () => undefined,
migrateLegacyNode: async () => undefined,
}),
[isLoading, error, bondedMixnode, bondedGateway, trigger, fee],
);
+4 -3
View File
@@ -108,7 +108,7 @@ export const MockDelegationContextProvider: FCWithChildren = ({ children }) => {
return {
logs_json: '',
data_json: '',
msg_responses_json: '',
gas_info: {
gas_wanted: { gas_units: BigInt(1) },
gas_used: { gas_units: BigInt(1) },
@@ -183,7 +183,7 @@ export const MockDelegationContextProvider: FCWithChildren = ({ children }) => {
return {
logs_json: '',
data_json: '',
msg_responses_json: '',
transaction_hash: '',
gas_info: {
gas_wanted: { gas_units: BigInt(1) },
@@ -195,8 +195,9 @@ export const MockDelegationContextProvider: FCWithChildren = ({ children }) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const undelegateVesting = async (mix_id: number, _fee?: FeeDetails) => ({
logs_json: '',
msg_responses_json: '',
data_json: '',
logs_json: '',
transaction_hash: '',
gas_info: {
gas_wanted: { gas_units: BigInt(1) },
+2 -2
View File
@@ -65,8 +65,8 @@ export const MockRewardsContextProvider: FCWithChildren = ({ children }) => {
amount: '1',
denom: 'nym',
},
data_json: '[]',
logs_json: '[]',
msg_responses_json: '[]',
gas_info: {
gas_wanted: { gas_units: BigInt(1) },
gas_used: { gas_units: BigInt(1) },
@@ -78,7 +78,7 @@ export const MockRewardsContextProvider: FCWithChildren = ({ children }) => {
amount: '1',
denom: 'nym',
},
data_json: '[]',
msg_responses_json: '[]',
logs_json: '[]',
gas_info: {
gas_wanted: { gas_units: BigInt(1) },
+69
View File
@@ -0,0 +1,69 @@
import { useEffect, useState } from 'react';
import { TBondedNode } from 'src/context';
import { getGatewayDetails } from 'src/requests/gatewayDetails';
import { getMixnodeDetails } from 'src/requests/mixnodeDetails';
import { getNymNodeDetails } from 'src/requests/nymNodeDetails';
import { fireRequests, TauriReq } from 'src/utils';
const useGetNodeDetails = (clientAddress?: string, network?: string) => {
const [bondedNode, setBondedNode] = useState<TBondedNode | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const getNodeDetails = async (address: string) => {
setIsError(false);
setBondedNode(null);
setIsLoading(true);
// Check if the address has a Nym node bonded
const nymnode: TauriReq<typeof getNymNodeDetails> = {
name: 'getNymNodeBondDetails',
request: () => getNymNodeDetails(address),
onFulfilled: (value) => {
if (value) {
setBondedNode(value);
}
},
};
// Check if the address has a Mix node bonded
const mixnode: TauriReq<typeof getMixnodeDetails> = {
name: 'getMixnodeDetails',
request: () => getMixnodeDetails(address),
onFulfilled: (value) => {
if (value) {
setBondedNode(value);
}
},
};
// Check if the address has a Gateway bonded
const gateway: TauriReq<typeof getGatewayDetails> = {
name: 'getGatewayDetails',
request: () => getGatewayDetails(),
onFulfilled: (value) => {
if (value) {
setBondedNode(value);
}
},
};
await fireRequests([nymnode, mixnode, gateway]);
setIsLoading(false);
};
useEffect(() => {
if (clientAddress) {
getNodeDetails(clientAddress);
}
}, [clientAddress, network]);
return {
bondedNode,
isLoading,
isError,
};
};
export default useGetNodeDetails;
+3 -17
View File
@@ -26,29 +26,15 @@ const AdminForm: FCWithChildren<{
<Grid container spacing={3}>
<Grid item xs={12}>
<TextField
{...register('minimum_mixnode_pledge')}
{...register('minimum_pledge')}
required
variant="outlined"
id="minimum_mixnode_bond"
name="minimum_mixnode_bond"
label="Minumum mixnode bond"
fullWidth
error={!!errors.minimum_mixnode_pledge}
helperText={`${errors?.minimum_mixnode_pledge?.amount?.message} ${errors?.minimum_mixnode_pledge?.denom?.message}`}
InputLabelProps={{ shrink: true }}
/>
</Grid>
<Grid item xs={12}>
<TextField
{...register('minimum_gateway_pledge')}
required
variant="outlined"
id="minimum_gateway_bond"
name="minimum_gateway_bond"
label="Minumum gateway bond"
fullWidth
error={!!errors.minimum_gateway_pledge}
helperText={`${errors?.minimum_gateway_pledge?.amount?.message} ${errors?.minimum_gateway_pledge?.denom?.message}`}
error={!!errors.minimum_pledge}
helperText={`${errors?.minimum_pledge?.amount?.message} ${errors?.minimum_pledge?.denom?.message}`}
InputLabelProps={{ shrink: true }}
/>
</Grid>
+105 -67
View File
@@ -2,55 +2,76 @@ import React, { useContext, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { FeeDetails } from '@nymproject/types';
import { Alert, AlertTitle, Box, Button, Typography } from '@mui/material';
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 { UpdateBondAmountModal } from 'src/components/Bonding/modals/UpdateBondAmountModal';
import { BondOversaturatedModal } from 'src/components/Bonding/modals/BondOversaturatedModal';
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, TUpdateBondArgs } from 'src/types';
import { isGateway, isMixnode, isNymNode, TUpdateBondArgs } from 'src/types';
import { BondedGateway } from 'src/components/Bonding/BondedGateway';
import { RedeemRewardsModal } from 'src/components/Bonding/modals/RedeemRewardsModal';
import { VestingWarningModal } from 'src/components/VestingWarningModal';
import MigrateLegacyNode from 'src/components/Bonding/modals/MigrateLegacyNode';
import { BondedNymNode } from 'src/components/Bonding/BondedNymNode';
import { UpdateBondAmountNymNode } from 'src/components/Bonding/modals/UpdateBondAmountNymNode';
import { BondNymNodeModalWithState } from 'src/components/Bonding/modals/BondNymNodeModal';
import { BondingContextProvider, useBondingContext } from '../../context';
export const Bonding = () => {
const [showModal, setShowModal] = useState<
'bond-mixnode' | 'bond-gateway' | 'update-bond' | 'update-bond-oversaturated' | 'unbond' | 'redeem'
| 'bond-mixnode'
| 'bond-nymnode'
| 'bond-gateway'
| 'update-bond'
| 'update-bond-oversaturated'
| 'unbond'
| 'redeem'
| 'update-bond-nymnode'
>();
const [confirmationDetails, setConfirmationDetails] = useState<ConfirmationDetailProps>();
const [uncappedSaturation, setUncappedSaturation] = useState<number | undefined>();
const [showMigrationModal, setShowMigrationModal] = useState(false);
const {
network,
clientDetails,
userBalance: { originalVesting },
} = useContext(AppContext);
const { network } = useContext(AppContext);
const navigate = useNavigate();
const {
bondedNode,
bondMixnode,
bondGateway,
redeemRewards,
isLoading,
updateBondAmount,
error,
redeemRewards,
updateBondAmount,
refresh,
migrateVestedMixnode,
migrateLegacyNode,
} = useBondingContext();
const shouldShowMigrateLegacyNodeModal = () => {
if (!bondedNode) {
return false;
}
if (isMixnode(bondedNode) && !bondedNode.isUnbonding) {
return true;
}
if (isGateway(bondedNode)) {
return true;
}
return false;
};
const [confirmationDetails, setConfirmationDetails] = useState<ConfirmationDetailProps>();
const [uncappedSaturation, setUncappedSaturation] = useState<number | undefined>();
const [showMigrationModal, setShowMigrationModal] = useState(false);
const [showMigrateLegacyNodeModal, setShowMigrateLegacyNodeModal] = useState(false);
useEffect(() => {
if (bondedNode && isMixnode(bondedNode) && bondedNode.uncappedStakeSaturation) {
setUncappedSaturation(bondedNode.uncappedStakeSaturation);
}
setShowMigrateLegacyNodeModal(shouldShowMigrateLegacyNodeModal());
}, [bondedNode]);
const handleMigrateVestedMixnode = async () => {
@@ -65,6 +86,18 @@ export const Bonding = () => {
}
};
const handleMigrateLegacyNode = async () => {
setShowMigrateLegacyNodeModal(false);
const tx = await migrateLegacyNode();
if (tx) {
setConfirmationDetails({
status: 'success',
title: 'Migration successful',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
}
};
const handleCloseModal = async () => {
setShowModal(undefined);
refresh();
@@ -79,35 +112,10 @@ export const Bonding = () => {
});
};
const handleBondMixnode = async (data: TBondMixNodeArgs, tokenPool: TPoolOption) => {
setShowModal(undefined);
const tx = await bondMixnode(data, tokenPool);
if (tx) {
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);
if (tx) {
setConfirmationDetails({
status: 'success',
title: 'Bond successful',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
}
};
const handleUpdateBond = async (data: TUpdateBondArgs, tokenPool: TPoolOption) => {
const handleUpdateBond = async (data: TUpdateBondArgs) => {
setShowModal(undefined);
const tx = await updateBondAmount(data, tokenPool);
const tx = await updateBondAmount(data);
if (tx) {
setConfirmationDetails({
status: 'success',
@@ -154,13 +162,34 @@ export const Bonding = () => {
return undefined;
};
const handleBondedNymNodeAction = async (action: TBondedMixnodeActions) => {
switch (action) {
case 'unbond': {
navigate('/bonding/node-settings', { state: 'unbond' });
break;
}
case 'updateBond': {
setShowModal('update-bond-nymnode');
break;
}
case 'redeem': {
setShowModal('redeem');
break;
}
default: {
return undefined;
}
}
return undefined;
};
if (error) {
return <ErrorModal open message="An error occured, please check logs for details" onClose={() => refresh()} />;
}
return (
<Box sx={{ mt: 4 }}>
{bondedNode?.proxy && (
{bondedNode && !isNymNode(bondedNode) && bondedNode?.proxy && (
<Alert severity="warning" sx={{ mb: 3 }}>
<AlertTitle sx={{ fontWeight: 600 }}>Your bonded node is using tokens from the vesting contract!</AlertTitle>
<Typography>
@@ -187,41 +216,41 @@ export const Bonding = () => {
}}
/>
{!bondedNode && <Bond disabled={isLoading} onBond={() => setShowModal('bond-mixnode')} />}
<MigrateLegacyNode
open={showMigrateLegacyNodeModal}
onClose={() => setShowMigrateLegacyNodeModal(false)}
handleMigrate={handleMigrateLegacyNode}
/>
{!bondedNode && <Bond disabled={isLoading} onBond={() => setShowModal('bond-nymnode')} />}
{bondedNode && isNymNode(bondedNode) && (
<BondedNymNode
nymnode={bondedNode}
network={network}
onActionSelect={(action) => handleBondedNymNodeAction(action)}
/>
)}
{bondedNode && isMixnode(bondedNode) && (
<BondedMixnode
mixnode={bondedNode}
network={network}
onShowMigrateToNymNodeModal={() => setShowMigrateLegacyNodeModal(true)}
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}
<BondedGateway
gateway={bondedNode}
network={network}
onShowMigrateToNymNodeModal={() => setShowMigrateLegacyNodeModal(true)}
onActionSelect={handleBondedMixnodeAction}
/>
)}
{showModal === 'bond-gateway' && (
<BondGatewayModal
denom={clientDetails?.display_mix_denom || 'nym'}
hasVestingTokens={Boolean(originalVesting)}
onBondGateway={handleBondGateway}
onSelectNodeType={() => setShowModal('bond-mixnode')}
onClose={() => setShowModal(undefined)}
onError={handleError}
/>
)}
<BondNymNodeModalWithState open={showModal === 'bond-nymnode'} onClose={handleCloseModal} />
{showModal === 'update-bond-oversaturated' && uncappedSaturation && (
<BondOversaturatedModal
@@ -241,6 +270,15 @@ export const Bonding = () => {
/>
)}
{showModal === 'update-bond-nymnode' && bondedNode && isNymNode(bondedNode) && (
<UpdateBondAmountNymNode
node={bondedNode}
onUpdateBond={handleUpdateBond}
onClose={() => setShowModal(undefined)}
onError={handleError}
/>
)}
{showModal === 'redeem' && bondedNode && isMixnode(bondedNode) && (
<RedeemRewardsModal
node={bondedNode}
@@ -10,11 +10,11 @@ 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, TBondedMixnode } from 'src/context';
import { useBondingContext, BondingContextProvider } from 'src/context';
import { AppContext, urls } from 'src/context/main';
import { isMixnode } from 'src/types';
import { getIntervalAsDate } from 'src/utils';
import { TBondedMixnode } from 'src/requests/mixnodeDetails';
import { NodeGeneralSettings } from './settings-pages/general-settings';
import { NodeUnbondPage } from './settings-pages/NodeUnbondPage';
import { NavItems, makeNavItems } from './node-settings.constant';
@@ -86,13 +86,13 @@ export const NodeSettings = () => {
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<NodeIcon />
<Typography variant="h6" fontWeight={600}>
{isMixnode(bondedNode) ? 'Node' : 'Gateway'} Settings
Nym Node Settings
</Typography>
</Box>
</Box>
<Box sx={{ width: '100%' }}>
<Tabs
tabs={makeNavItems(isMixnode(bondedNode))}
tabs={makeNavItems(bondedNode)}
selectedTab={value}
onChange={handleChange}
tabSx={{
@@ -146,8 +146,7 @@ export const NodeSettings = () => {
}}
>
<Typography fontWeight="bold">
You should NOT shutdown your {isMixnode(bondedNode) ? 'mix node' : 'gateway'} until the unbond process is
complete
You should NOT shutdown your node until the unbond process is complete
</Typography>
</ConfirmationDetailsModal>
)}
@@ -4,10 +4,10 @@ import { ResultsTable } from 'src/components/RewardsPlayground/ResultsTable';
import { getDelegationSummary } from 'src/requests';
import { NodeDetails } from 'src/components/RewardsPlayground/NodeDetail';
import { CalculateArgs, Inputs } from 'src/components/RewardsPlayground/Inputs';
import { TBondedMixnode } from 'src/context';
import { useSnackbar } from 'notistack';
import { LoadingModal } from 'src/components/Modals/LoadingModal';
import { Console } from 'src/utils/console';
import { TBondedMixnode } from 'src/requests/mixnodeDetails';
import { computeEstimate, computeStakeSaturation, handleCalculatePeriodRewards } from './utils';
export type DefaultInputValues = {
@@ -1,7 +1,13 @@
export const makeNavItems = (isMixnode: boolean) => {
const navItems: NavItems[] = ['General', 'Unbond'];
import { TBondedNode } from 'src/context';
import { isNymNode } from 'src/types';
if (isMixnode) navItems.splice(1, 0, 'Test my node', 'Playground');
export const makeNavItems = (bondedNode: TBondedNode) => {
const navItems: NavItems[] = ['Unbond'];
if (isNymNode(bondedNode)) {
// add these items to the beginning of the array "General", "Test my node", "Playground"
navItems.unshift('General', 'Test my node', 'Playground');
}
return navItems;
};
@@ -10,6 +10,7 @@ import { ErrorModal } from 'src/components/Modals/ErrorModal';
import { PrintResults } from 'src/components/TestNode/PrintResults';
import { MAINNET_VALIDATOR_URL, QA_VALIDATOR_URL } from 'src/constants';
import { TestStatus } from 'src/components/TestNode/types';
import { isMixnode } from 'src/types';
export const NodeTestPage = () => {
const [nodeTestClient, setNodeTestClient] = useState<NodeTester>();
@@ -37,7 +38,7 @@ export const NodeTestPage = () => {
};
const handleTestNode = async () => {
if (nodeTestClient && bondedNode) {
if (nodeTestClient && bondedNode && isMixnode(bondedNode)) {
setResults(undefined);
setTestDate(format(new Date(), 'dd/MM/yyyy HH:mm'));
setIsLoading(true);
@@ -87,7 +88,7 @@ export const NodeTestPage = () => {
<Box p={4}>
{isLoading && <LoadingModal text="Testing mixnode, please wait.." />}
{error && <ErrorModal open title="Node test failed" message={error} onClose={() => setError(undefined)} />}
{printResults && results && (
{printResults && results && bondedNode && isMixnode(bondedNode) && (
<PrintResults
mixnodeId={bondedNode?.identityKey || '-'}
mixnodeName={bondedNode?.name || '-'}
@@ -1,12 +1,12 @@
import React, { useState } from 'react';
import { Box, Button, Typography, Grid, TextField, Stack } from '@mui/material';
import { TBondedMixnode, TBondedGateway } from 'src/context/bonding';
import { TBondedNode } from 'src/context/bonding';
import { Error } from 'src/components/Error';
import { UnbondModal } from 'src/components/Bonding/modals/UnbondModal';
import { isMixnode } from 'src/types';
import { isMixnode, isNymNode } from 'src/types';
interface Props {
bondedNode: TBondedMixnode | TBondedGateway;
bondedNode: TBondedNode;
onConfirm: () => Promise<void>;
onError: (e: string) => void;
@@ -15,6 +15,9 @@ export const NodeUnbondPage = ({ bondedNode, onConfirm, onError }: Props) => {
const [confirmField, setConfirmField] = useState('');
const [isConfirmed, setIsConfirmed] = useState(false);
// TODO: Check what happens with a gateway
const shouldDisplayWarning = isMixnode(bondedNode) || isNymNode(bondedNode);
return (
<Box sx={{ p: 3, minHeight: '450px' }}>
<Grid container justifyContent="space-between">
@@ -24,7 +27,7 @@ export const NodeUnbondPage = ({ bondedNode, onConfirm, onError }: Props) => {
Unbond
</Typography>
{isMixnode(bondedNode) && (
{shouldDisplayWarning && (
<Grid item>
<Typography variant="body2" sx={{ color: (theme) => theme.palette.nym.text.muted }}>
Remember you should only unbond if you want to remove your node from the network for good.
@@ -35,7 +38,7 @@ export const NodeUnbondPage = ({ bondedNode, onConfirm, onError }: Props) => {
</Grid>
<Grid item xs={12} lg={6}>
<Stack gap={3}>
{isMixnode(bondedNode) && (
{shouldDisplayWarning && (
<Error message="Unbonding is irreversible. You will lose all your delegations. It wont be possible to restore the current state of your node again." />
)}
@@ -68,7 +71,7 @@ export const NodeUnbondPage = ({ bondedNode, onConfirm, onError }: Props) => {
</Stack>
</Grid>
</Grid>
{isConfirmed && (
{isConfirmed && !isNymNode(bondedNode) && (
<UnbondModal
node={bondedNode}
onConfirm={async () => {
@@ -10,16 +10,17 @@ import {
updateGatewayConfig,
vestingUpdateGatewayConfig,
} from 'src/requests';
import { TBondedGateway, useBondingContext } from 'src/context/bonding';
import { useBondingContext } from 'src/context/bonding';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { Console } from 'src/utils/console';
import { Alert } from 'src/components/Alert';
import { ConfirmTx } from 'src/components/ConfirmTX';
import { useGetFee } from 'src/hooks/useGetFee';
import { LoadingModal } from 'src/components/Modals/LoadingModal';
import { updateGatewayValidationSchema } from 'src/components/Bonding/forms/gatewayValidationSchema';
import { updateGatewayValidationSchema } from 'src/components/Bonding/forms/legacyForms/gatewayValidationSchema';
import { BalanceWarning } from 'src/components/FeeWarning';
import { AppContext } from 'src/context';
import { TBondedGateway } from 'src/requests/gatewayDetails';
export const GeneralGatewaySettings = ({ bondedNode }: { bondedNode: TBondedGateway }) => {
const [openConfirmationModal, setOpenConfirmationModal] = useState<boolean>(false);
@@ -56,7 +57,6 @@ export const GeneralGatewaySettings = ({ bondedNode }: { bondedNode: TBondedGate
location,
version: clean(version) as string,
clients_port: httpApiPort,
verloc_port: bondedNode.verlocPort,
};
if (bondedNode.proxy) {
@@ -206,7 +206,6 @@ export const GeneralGatewaySettings = ({ bondedNode }: { bondedNode: TBondedGate
clients_port: data.httpApiPort,
location: bondedNode.location!,
version: data.version,
verloc_port: bondedNode.verlocPort,
}),
)}
sx={{ m: 3 }}
@@ -6,9 +6,8 @@ import { Box, Button, Divider, Grid, Stack, TextField, Typography } from '@mui/m
import { useTheme } from '@mui/material/styles';
import { isMixnode } from 'src/types';
import { simulateUpdateMixnodeConfig, simulateVestingUpdateMixnodeConfig, updateMixnodeConfig } from 'src/requests';
import { TBondedGateway, TBondedMixnode } from 'src/context/bonding';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { bondedInfoParametersValidationSchema } from 'src/components/Bonding/forms/mixnodeValidationSchema';
import { bondedInfoParametersValidationSchema } from 'src/components/Bonding/forms/legacyForms/mixnodeValidationSchema';
import { Console } from 'src/utils/console';
import { Alert } from 'src/components/Alert';
import { vestingUpdateMixnodeConfig } from 'src/requests/vesting';
@@ -17,8 +16,9 @@ import { useGetFee } from 'src/hooks/useGetFee';
import { LoadingModal } from 'src/components/Modals/LoadingModal';
import { BalanceWarning } from 'src/components/FeeWarning';
import { AppContext } from 'src/context';
import { TBondedMixnode } from 'src/requests/mixnodeDetails';
export const GeneralMixnodeSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBondedGateway }) => {
export const GeneralMixnodeSettings = ({ bondedNode }: { bondedNode: TBondedMixnode }) => {
const [openConfirmationModal, setOpenConfirmationModal] = useState<boolean>(false);
const { getFee, fee, resetFeeState } = useGetFee();
const { userBalance } = useContext(AppContext);
@@ -0,0 +1,182 @@
import React, { useContext, useState } from 'react';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import { Box, Button, Divider, Grid, Stack, TextField, Typography } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { updateNymNodeConfig } from 'src/requests';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { bondedInfoParametersValidationSchema } from 'src/components/Bonding/forms/legacyForms/mixnodeValidationSchema';
import { Console } from 'src/utils/console';
import { Alert } from 'src/components/Alert';
import { ConfirmTx } from 'src/components/ConfirmTX';
import { useGetFee } from 'src/hooks/useGetFee';
import { LoadingModal } from 'src/components/Modals/LoadingModal';
import { BalanceWarning } from 'src/components/FeeWarning';
import { AppContext } from 'src/context';
import { TBondedNymNode } from 'src/requests/nymNodeDetails';
export const GeneralNymNodeSettings = ({ bondedNode }: { bondedNode: TBondedNymNode }) => {
const [openConfirmationModal, setOpenConfirmationModal] = useState<boolean>(false);
const { fee, resetFeeState } = useGetFee();
const { userBalance } = useContext(AppContext);
const theme = useTheme();
const {
register,
handleSubmit,
formState: { errors, isSubmitting, isDirty, isValid },
} = useForm({
resolver: yupResolver(bondedInfoParametersValidationSchema),
mode: 'onChange',
defaultValues: {
host: bondedNode.host,
customHttpPort: bondedNode.customHttpPort,
},
});
const onSubmit = async (data: { host?: string; customHttpPort?: number | null }) => {
resetFeeState();
const { host, customHttpPort } = data;
if (host && customHttpPort) {
const NymNodeConfigParams = {
host,
custom_http_port: customHttpPort,
};
try {
await updateNymNodeConfig(NymNodeConfigParams);
setOpenConfirmationModal(true);
} catch (error) {
Console.error(error);
}
}
};
return (
<Grid container xs>
{fee && (
<ConfirmTx
open
header="Update node settings"
fee={fee}
onConfirm={handleSubmit((d) => onSubmit(d))}
onPrev={resetFeeState}
onClose={resetFeeState}
>
{fee.amount?.amount && userBalance?.balance?.amount.amount && (
<Box sx={{ mb: 2 }}>
<BalanceWarning fee={fee.amount.amount} />
</Box>
)}
</ConfirmTx>
)}
{isSubmitting && <LoadingModal />}
<Alert
title={
<Stack>
<Typography fontWeight={600}>
Changing these values will ONLY change the data about your node on the blockchain.
</Typography>
<Typography>Remember to change your nodes config file with the same values too.</Typography>
</Stack>
}
bgColor={`${theme.palette.nym.nymWallet.text.blue}0D !important`}
dismissable
/>
<Grid container mt={2}>
<Grid item container direction="row" alignItems="left" justifyContent="space-between" padding={3}>
<Grid item>
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1 }}>
Port
</Typography>
</Grid>
<Grid spacing={3} item container alignItems="center" xs={12} md={6}>
<Grid item width={1}>
<TextField
{...register('customHttpPort')}
name="customHttpPort"
label="Custom HTTP port"
fullWidth
error={!!errors.customHttpPort}
helperText={errors.customHttpPort?.message}
InputLabelProps={{ shrink: true }}
/>
</Grid>
</Grid>
</Grid>
<Divider sx={{ width: '100%' }} />
<Grid item container direction="row" alignItems="left" justifyContent="space-between" padding={3}>
<Grid item>
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1 }}>
Host
</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}
InputLabelProps={{ shrink: true }}
/>
</Grid>
</Grid>
</Grid>
<Divider sx={{ width: '100%' }} />
<Grid item container direction="row" justifyContent="space-between" padding={3}>
<Grid item />
<Grid spacing={3} item container alignItems="center" xs={12} md={6}>
<Button
size="large"
variant="contained"
disabled={isSubmitting || !isDirty || !isValid}
onClick={handleSubmit(() => undefined)}
sx={{ m: 3, mr: 0 }}
fullWidth
>
Submit changes to the blockchain
</Button>
</Grid>
</Grid>
</Grid>
<SimpleModal
open={openConfirmationModal}
header="Your changes are submitted to the blockchain"
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,
}}
subHeaderStyles={{
width: '100%',
mb: 1,
textAlign: 'center',
color: 'main',
fontSize: 14,
}}
/>
</Grid>
);
};
@@ -14,7 +14,7 @@ import {
Typography,
} from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { CurrencyDenom, MixNodeCostParams } from '@nymproject/types';
import { CurrencyDenom, NodeCostParams } from '@nymproject/types';
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
import { isMixnode } from 'src/types';
import {
@@ -22,11 +22,9 @@ import {
simulateUpdateMixnodeCostParams,
simulateVestingUpdateMixnodeCostParams,
updateMixnodeCostParams,
vestingUpdateMixnodeCostParams,
} from 'src/requests';
import { TBondedMixnode } from 'src/context/bonding';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { bondedNodeParametersValidationSchema } from 'src/components/Bonding/forms/mixnodeValidationSchema';
import { bondedNodeParametersValidationSchema } from 'src/components/Bonding/forms/legacyForms/mixnodeValidationSchema';
import { Console } from 'src/utils/console';
import { getIntervalAsDate } from 'src/utils';
import { Alert } from 'src/components/Alert';
@@ -36,6 +34,7 @@ import { useGetFee } from 'src/hooks/useGetFee';
import { ConfirmTx } from 'src/components/ConfirmTX';
import { LoadingModal } from 'src/components/Modals/LoadingModal';
import { InfoOutlined } from '@mui/icons-material';
import { TBondedMixnode } from 'src/requests/mixnodeDetails';
const operatorCostHint = `This is your (operator) rewards including the PM and cost. Rewards are automatically compounded every epoch.You can redeem your rewards at any time.
`;
@@ -45,7 +44,7 @@ const profitMarginHint =
export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode }): JSX.Element => {
const [openConfirmationModal, setOpenConfirmationModal] = useState<boolean>(false);
const [intervalTime, setIntervalTime] = useState<string>();
const [pendingUpdates, setPendingUpdates] = useState<MixNodeCostParams>();
const [pendingUpdates, setPendingUpdates] = useState<NodeCostParams>();
const { clientDetails } = useContext(AppContext);
const theme = useTheme();
@@ -115,11 +114,8 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode
},
};
try {
if (bondedNode.proxy) {
await vestingUpdateMixnodeCostParams(mixNodeCostParams);
} else {
await updateMixnodeCostParams(mixNodeCostParams);
}
await updateMixnodeCostParams(mixNodeCostParams);
await getPendingEvents();
reset();
setOpenConfirmationModal(true);
@@ -1,12 +1,13 @@
import React, { useState } from 'react';
import { Box, Button, Divider, Grid } from '@mui/material';
import { isGateway, isMixnode } from 'src/types';
import { TBondedMixnode, TBondedGateway } from 'src/context/bonding';
import { isGateway, isMixnode, isNymNode } from 'src/types';
import { TBondedNode } from 'src/context/bonding';
import { GeneralMixnodeSettings } from './GeneralMixnodeSettings';
import { ParametersSettings } from './ParametersSettings';
import { GeneralGatewaySettings } from './GeneralGatewaySettings';
import { GeneralNymNodeSettings } from './GeneralNymNodeSettings';
const makeGeneralNav = (bondedNode: TBondedMixnode | TBondedGateway) => {
const makeGeneralNav = (bondedNode: TBondedNode) => {
const navItems = ['Info'];
if (isMixnode(bondedNode)) {
navItems.push('Parameters');
@@ -15,7 +16,7 @@ const makeGeneralNav = (bondedNode: TBondedMixnode | TBondedGateway) => {
return navItems;
};
export const NodeGeneralSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBondedGateway }) => {
export const NodeGeneralSettings = ({ bondedNode }: { bondedNode: TBondedNode }) => {
const [navSelection, setNavSelection] = useState<number>(0);
const getSettings = () => {
@@ -23,10 +24,12 @@ export const NodeGeneralSettings = ({ bondedNode }: { bondedNode: TBondedMixnode
case 0: {
if (isMixnode(bondedNode)) return <GeneralMixnodeSettings bondedNode={bondedNode} />;
if (isGateway(bondedNode)) return <GeneralGatewaySettings bondedNode={bondedNode} />;
if (isNymNode(bondedNode)) return <GeneralNymNodeSettings bondedNode={bondedNode} />;
break;
}
case 1: {
if (isMixnode(bondedNode)) return <ParametersSettings bondedNode={bondedNode} />;
if (isNymNode(bondedNode)) return null;
break;
}
default:
+2 -2
View File
@@ -1,4 +1,4 @@
import { DecCoin, MixNodeCostParams, TNodeType, TransactionExecuteResult } from '@nymproject/types';
import { DecCoin, NodeCostParams, TNodeType, TransactionExecuteResult } from '@nymproject/types';
import { TPoolOption } from 'src/components';
export type FormStep = 1 | 2 | 3 | 4;
@@ -61,5 +61,5 @@ export interface BondState {
export interface ChangeMixCostParams {
mix_id: number;
new_costs: MixNodeCostParams;
new_costs: NodeCostParams;
}
@@ -7,8 +7,7 @@ import { yupResolver } from '@hookform/resolvers/yup';
import { InclusionProbabilityResponse, SelectionChance } from '@nymproject/types';
import { validationSchema } from './validationSchema';
import { InfoTooltip } from '../../components';
import { useCheckOwnership } from '../../hooks/useCheckOwnership';
import { updateMixnodeCostParams, vestingUpdateMixnodeCostParams } from '../../requests';
import { updateMixnodeCostParams } from '../../requests';
import { AppContext } from '../../context';
import { Console } from '../../utils/console';
import { attachDefaultOperatingCost, toPercentFloatString } from '../../utils';
@@ -74,7 +73,6 @@ export const SystemVariables = ({
}) => {
const [nodeUpdateResponse, setNodeUpdateResponse] = useState<'success' | 'failed'>();
const { mixnodeDetails } = useContext(AppContext);
const { ownership } = useCheckOwnership();
const {
register,
@@ -106,12 +104,6 @@ export const SystemVariables = ({
await updateMixnodeCostParams(defaultCostParams);
};
const vestingUpdateMixnodeProfitMargin = async (profitMarginPercent: string) => {
// TODO: this will have to be updated with allowing users to provide their operating cost in the form
const defaultCostParams = await attachDefaultOperatingCost(toPercentFloatString(profitMarginPercent));
await vestingUpdateMixnodeCostParams(defaultCostParams);
};
if (!mixnodeDetails) return null;
return (
@@ -175,12 +167,7 @@ export const SystemVariables = ({
<Button
variant="contained"
color="primary"
onClick={handleSubmit((data) =>
onSubmit(
data.profitMarginPercent,
ownership.vestingPledge ? updateMixnodeProfitMargin : vestingUpdateMixnodeProfitMargin,
),
)}
onClick={handleSubmit((data) => onSubmit(data.profitMarginPercent, updateMixnodeProfitMargin))}
disableElevation
endIcon={isSubmitting && <CircularProgress size={20} />}
disabled={Object.keys(errors).length > 0 || isSubmitting}
+4 -30
View File
@@ -4,17 +4,9 @@ import {
SendTxResult,
TransactionExecuteResult,
MixNodeConfigUpdate,
MixNodeCostParams,
GatewayConfigUpdate,
} from '@nymproject/types';
import {
EnumNodeType,
TBondGatewayArgs,
TBondGatewaySignatureArgs,
TBondMixNodeArgs,
TBondMixnodeSignatureArgs,
TUpdateBondArgs,
} from '../types';
import { TBondGatewayArgs, TBondGatewaySignatureArgs, TNodeConfigUpdateArgs } from '../types';
import { invokeWrapper } from './wrapper';
export const bondGateway = async (args: TBondGatewayArgs) =>
@@ -23,34 +15,16 @@ export const bondGateway = async (args: TBondGatewayArgs) =>
export const generateGatewayMsgPayload = async (args: Omit<TBondGatewaySignatureArgs, 'tokenPool'>) =>
invokeWrapper<string>('generate_gateway_bonding_msg_payload', args);
export const unbondGateway = async (fee?: Fee) => invokeWrapper<TransactionExecuteResult>('unbond_gateway', { fee });
export const bondMixNode = async (args: TBondMixNodeArgs) =>
invokeWrapper<TransactionExecuteResult>('bond_mixnode', args);
export const generateMixnodeMsgPayload = async (args: Omit<TBondMixnodeSignatureArgs, 'tokenPool'>) =>
invokeWrapper<string>('generate_mixnode_bonding_msg_payload', args);
export const unbondMixNode = async (fee?: Fee) => invokeWrapper<TransactionExecuteResult>('unbond_mixnode', { fee });
export const updateMixnodeCostParams = async (newCosts: MixNodeCostParams, fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('update_mixnode_cost_params', { newCosts, fee });
export const updateMixnodeConfig = async (update: MixNodeConfigUpdate, fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('update_mixnode_config', { update, fee });
export const updateNymNodeConfig = async (update: TNodeConfigUpdateArgs, fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('update_nymnode_config', { update, fee });
export const updateGatewayConfig = async (update: GatewayConfigUpdate, fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('update_gateway_config', { update, fee });
export const send = async (args: { amount: DecCoin; address: string; memo: string; fee?: Fee }) =>
invokeWrapper<SendTxResult>('send', args);
export const unbond = async (type: EnumNodeType) => {
if (type === EnumNodeType.mixnode) return unbondMixNode();
return unbondGateway();
};
export const updateBond = async (args: TUpdateBondArgs) =>
invokeWrapper<TransactionExecuteResult>('update_pledge', args);
export const migrateVestedMixnode = async () => invokeWrapper<TransactionExecuteResult>('migrate_vested_mixnode');
+56
View File
@@ -0,0 +1,56 @@
import {
Fee,
TransactionExecuteResult,
NodeCostParams,
GatewayBond,
NymNodeDetails,
MixNodeDetails,
} from '@nymproject/types';
import {
TBondMixNodeArgs,
TBondMixnodeSignatureArgs,
EnumNodeType,
TUpdateBondArgs,
TBondNymNodeArgs,
TNymNodeSignatureArgs,
} from 'src/types';
import { invokeWrapper } from './wrapper';
export const unbondGateway = async (fee?: Fee) => invokeWrapper<TransactionExecuteResult>('unbond_gateway', { fee });
export const bondMixNode = async (args: TBondMixNodeArgs) =>
invokeWrapper<TransactionExecuteResult>('bond_mixnode', args);
export const bondNymNode = async (args: TBondNymNodeArgs) =>
invokeWrapper<TransactionExecuteResult>('bond_nymnode', args);
export const unbondNymNode = async (fee?: Fee) => invokeWrapper<TransactionExecuteResult>('unbond_nymnode', { fee });
export const generateMixnodeMsgPayload = async (args: Omit<TBondMixnodeSignatureArgs, 'tokenPool'>) =>
invokeWrapper<string>('generate_mixnode_bonding_msg_payload', args);
export const generateNymNodeMsgPayload = async (args: TNymNodeSignatureArgs) =>
invokeWrapper<string>('generate_nym_node_bonding_msg_payload', args);
export const unbondMixNode = async (fee?: Fee) => invokeWrapper<TransactionExecuteResult>('unbond_mixnode', { fee });
export const updateMixnodeCostParams = async (newCosts: NodeCostParams, fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('update_mixnode_cost_params', { newCosts, fee });
export const unbond = async (type: EnumNodeType) => {
if (type === EnumNodeType.mixnode) return unbondMixNode();
return unbondGateway();
};
export const updateBond = async (args: TUpdateBondArgs) =>
invokeWrapper<TransactionExecuteResult>('update_pledge', args);
export const getNymNodeBondDetails = async () => invokeWrapper<NymNodeDetails | null>('nym_node_bond_details');
export const getMixnodeBondDetails = async () => invokeWrapper<MixNodeDetails | null>('mixnode_bond_details');
export const getGatewayBondDetails = async () => invokeWrapper<GatewayBond | null>('gateway_bond_details');
export const migrateLegacyMixnode = async () => invokeWrapper<TransactionExecuteResult>('migrate_legacy_mixnode');
export const migrateLegacyGateway = async () => invokeWrapper<TransactionExecuteResult>('migrate_legacy_gateway');
+71
View File
@@ -0,0 +1,71 @@
import { TNodeDescription } from 'src/types';
import { TauriReq, decCoinToDisplay, fireRequests } from 'src/utils';
import { getGatewayReport, getMixNodeDescription as getNodeDescriptionRequest } from './queries';
import { getGatewayBondDetails } from './bond';
async function getAdditionalGatewayDetails(identityKey: string, host: string, port: number) {
const details: {
routingScore?: { current: number; average: number } | undefined;
nodeDescription?: TNodeDescription | undefined;
} = {};
const reportReq: TauriReq<typeof getGatewayReport> = {
name: 'getGatewayReport',
request: () => getGatewayReport(identityKey),
onFulfilled: (value) => {
details.routingScore = { current: value.most_recent, average: value.last_day };
},
};
const nodeDescReq: TauriReq<typeof getNodeDescriptionRequest> = {
name: 'getNodeDescription',
request: () => getNodeDescriptionRequest(host, port),
onFulfilled: (value) => {
details.nodeDescription = value;
},
};
await fireRequests([reportReq, nodeDescReq]);
return details;
}
async function getGatewayDetails() {
try {
const data = await getGatewayBondDetails();
if (!data) {
return null;
}
const { gateway, proxy } = data;
const { nodeDescription, routingScore } = await getAdditionalGatewayDetails(
gateway.identity_key,
gateway.host,
gateway.clients_port,
);
return {
name: nodeDescription?.name,
identityKey: gateway.identity_key,
mixPort: gateway.mix_port,
httpApiPort: gateway.clients_port,
host: gateway.host,
ip: gateway.host,
location: gateway.location,
bond: decCoinToDisplay(data.pledge_amount),
proxy,
routingScore,
version: gateway.version,
};
} catch (error) {
console.error(error);
return null;
}
}
type TBondedGatewayResponse = Awaited<ReturnType<typeof getGatewayDetails>>;
type TBondedGateway = NonNullable<TBondedGatewayResponse>;
export { getGatewayDetails };
export type { TBondedGatewayResponse, TBondedGateway };
+1
View File
@@ -1,5 +1,6 @@
export * from './app';
export * from './account';
export * from './bond';
export * from './actions';
export * from './contract';
export * from './delegation';
+199
View File
@@ -0,0 +1,199 @@
/* eslint-disable @typescript-eslint/naming-convention */
import {
DecCoin,
decimalToFloatApproximation,
decimalToPercentage,
InclusionProbabilityResponse,
MixnodeStatus,
} from '@nymproject/types';
import { Console } from 'src/utils/console';
import { TNodeDescription } from 'src/types';
import { TauriReq, unymToNym, decCoinToDisplay, fireRequests, toPercentIntegerString, calculateStake } from 'src/utils';
import {
getMixnodeStatus,
getMixnodeUptime,
getMixnodeStakeSaturation,
getMixnodeRewardEstimation,
getInclusionProbability,
getMixnodeAvgUptime,
getMixNodeDescription as getNodeDescriptionRequest,
getPendingOperatorRewards,
} from './queries';
import { getMixnodeBondDetails } from './bond';
async function getAdditionalMixnodeDetails(mixId: number, host: string, port: number, client_address: string) {
const details: {
status: MixnodeStatus;
stakeSaturation: string;
estimatedRewards?: DecCoin;
uptime: number;
averageUptime?: number;
setProbability?: InclusionProbabilityResponse;
nodeDescription?: TNodeDescription | undefined;
operatorRewards?: DecCoin;
uncappedSaturation?: number;
} = {
status: 'not_found',
stakeSaturation: '0',
uptime: 0,
};
const statusReq: TauriReq<typeof getMixnodeStatus> = {
name: 'getMixnodeStatus',
request: () => getMixnodeStatus(mixId),
onFulfilled: (value) => {
details.status = value.status;
},
};
const uptimeReq: TauriReq<typeof getMixnodeUptime> = {
name: 'getMixnodeUptime',
request: () => getMixnodeUptime(mixId),
onFulfilled: (value) => {
details.uptime = value;
},
};
const stakeSaturationReq: TauriReq<typeof getMixnodeStakeSaturation> = {
name: 'getMixnodeStakeSaturation',
request: () => getMixnodeStakeSaturation(mixId),
onFulfilled: (value) => {
details.stakeSaturation = decimalToPercentage(value.saturation);
const rawUncappedSaturation = decimalToFloatApproximation(value.uncapped_saturation);
if (rawUncappedSaturation && rawUncappedSaturation > 1) {
details.uncappedSaturation = Math.round(rawUncappedSaturation * 100);
}
},
};
const rewardReq: TauriReq<typeof getMixnodeRewardEstimation> = {
name: 'getMixnodeRewardEstimation',
request: () => getMixnodeRewardEstimation(mixId),
onFulfilled: (value) => {
const estimatedRewards = unymToNym(value.estimation.total_node_reward);
if (estimatedRewards) {
details.estimatedRewards = {
amount: estimatedRewards,
denom: 'nym',
};
}
},
};
const inclusionReq: TauriReq<typeof getInclusionProbability> = {
name: 'getInclusionProbability',
request: () => getInclusionProbability(mixId),
onFulfilled: (value) => {
details.setProbability = value;
},
};
const avgUptimeReq: TauriReq<typeof getMixnodeAvgUptime> = {
name: 'getMixnodeAvgUptime',
request: () => getMixnodeAvgUptime(),
onFulfilled: (value) => {
details.averageUptime = value as number | undefined;
},
};
const nodeDescReq: TauriReq<typeof getNodeDescriptionRequest> = {
name: 'getNodeDescription',
request: () => getNodeDescriptionRequest(host, port),
onFulfilled: (value) => {
details.nodeDescription = value;
},
};
const operatorRewardsReq: TauriReq<typeof getPendingOperatorRewards> = {
name: 'getPendingOperatorRewards',
request: () => getPendingOperatorRewards(client_address),
onFulfilled: (value) => {
details.operatorRewards = decCoinToDisplay(value);
},
};
await fireRequests([
statusReq,
uptimeReq,
stakeSaturationReq,
rewardReq,
inclusionReq,
avgUptimeReq,
nodeDescReq,
operatorRewardsReq,
]);
return details;
}
async function getMixnodeDetails(client_address: string) {
try {
const data = await getMixnodeBondDetails();
if (!data) {
return null;
}
const {
bond_information,
rewarding_details,
bond_information: { mix_id },
} = data;
const {
status,
stakeSaturation,
uncappedSaturation: uncappedStakeSaturation,
estimatedRewards,
uptime,
operatorRewards,
averageUptime,
nodeDescription,
setProbability,
} = await getAdditionalMixnodeDetails(
mix_id,
bond_information.mix_node.host,
bond_information.mix_node.http_api_port,
client_address,
);
return {
name: nodeDescription?.name,
mixId: mix_id,
identityKey: bond_information.mix_node.identity_key,
stake: {
amount: calculateStake(rewarding_details.operator, rewarding_details.delegates) || '0',
denom: bond_information.original_pledge.denom,
},
bond: decCoinToDisplay(bond_information.original_pledge),
profitMargin: toPercentIntegerString(rewarding_details.cost_params.profit_margin_percent),
delegators: rewarding_details.unique_delegations,
proxy: bond_information.proxy,
operatorRewards,
uptime,
status,
stakeSaturation,
uncappedStakeSaturation,
operatorCost: decCoinToDisplay(rewarding_details.cost_params.interval_operating_cost),
host: bond_information.mix_node.host.replace(/\s/g, ''),
routingScore: averageUptime,
activeSetProbability: setProbability?.in_active,
standbySetProbability: setProbability?.in_reserve,
estimatedRewards,
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,
isUnbonding: bond_information.is_unbonding,
};
} catch (e: any) {
Console.warn(e);
throw new Error(`While fetching current bond state, an error occurred: ${e}`);
}
}
type TBondedMixnodeResponse = Awaited<ReturnType<typeof getMixnodeDetails>>;
type TBondedMixnode = NonNullable<TBondedMixnodeResponse>;
export { getMixnodeDetails };
export type { TBondedMixnodeResponse, TBondedMixnode };
+145
View File
@@ -0,0 +1,145 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { calculateStake, Console, decCoinToDisplay, fireRequests, TauriReq, toPercentIntegerString } from 'src/utils';
import { DecCoin, decimalToFloatApproximation, decimalToPercentage } from '@nymproject/types';
import { TNodeRole } from 'src/types';
import { getNymNodeBondDetails } from './bond';
import {
getNymNodeDescription,
getNymNodeRole,
getNymNodeStakeSaturation,
getNymNodeUptime,
getPendingOperatorRewards,
} from './queries';
async function getNymNodeDetails(clientAddress: string) {
try {
const data = await getNymNodeBondDetails();
if (!data) {
return null;
}
const {
bond_information,
rewarding_details,
bond_information: { node_id },
} = data;
const { name, operatorRewards, uptime, stakeSaturation, uncappedSaturation, role } =
await getAdditionalNymNodeDetails(
node_id,
bond_information.host,
bond_information.custom_http_port,
clientAddress,
);
return {
name,
nodeId: node_id,
identityKey: bond_information.identity_key,
stake: {
amount: calculateStake(rewarding_details.operator, rewarding_details.delegates) || '0',
denom: bond_information.original_pledge.denom,
},
bond: decCoinToDisplay(bond_information.original_pledge),
profitMargin: toPercentIntegerString(rewarding_details.cost_params.profit_margin_percent),
delegators: rewarding_details.unique_delegations,
operatorCost: decCoinToDisplay(rewarding_details.cost_params.interval_operating_cost),
host: bond_information.host.replace(/\s/g, ''),
customHttpPort: bond_information.custom_http_port,
isUnbonding: bond_information.is_unbonding,
operatorRewards,
uptime,
stakeSaturation,
uncappedStakeSaturation: uncappedSaturation,
role,
};
} catch (e: any) {
Console.warn(e);
throw new Error(`While fetching current bond state, an error occurred: ${e}`);
}
}
async function getAdditionalNymNodeDetails(nodeId: number, host: string, port: number | null, clientAddress: string) {
const details: {
name: string;
uptime: number;
operatorRewards?: DecCoin;
stakeSaturation: string;
uncappedSaturation?: number;
role?: TNodeRole;
} = {
name: 'Name has not been set',
uptime: 0,
stakeSaturation: '0',
};
if (port) {
try {
const nodeDescription = await getNymNodeDescription(host, port);
details.name = nodeDescription.name;
} catch (e) {
Console.warn(`Failed to get node description for ${host}:${port}`);
}
}
const nodeDescription: TauriReq<typeof getNymNodeDescription> = {
name: 'getNymNodeDescription',
request: () => {
if (port) {
return getNymNodeDescription(host, port);
}
return Promise.resolve({ name: 'Name has not been set', description: '', link: '', location: '' });
},
onFulfilled: (value) => {
details.name = value.name;
},
};
const uptimeReq: TauriReq<typeof getNymNodeUptime> = {
name: 'getMixnodeAvgUptime',
request: () => getNymNodeUptime(nodeId),
onFulfilled: (value) => {
details.uptime = value;
},
};
const stakeSaturationReq: TauriReq<typeof getNymNodeStakeSaturation> = {
name: 'getMixnodeStakeSaturation',
request: () => getNymNodeStakeSaturation(nodeId),
onFulfilled: (value) => {
details.stakeSaturation = decimalToPercentage(value.uncapped_saturation);
const rawUncappedSaturation = decimalToFloatApproximation(value.uncapped_saturation);
if (rawUncappedSaturation && rawUncappedSaturation > 1) {
details.uncappedSaturation = Math.round(rawUncappedSaturation * 100);
}
},
};
const operatorRewardsReq: TauriReq<typeof getPendingOperatorRewards> = {
name: 'getPendingOperatorRewards',
request: () => getPendingOperatorRewards(clientAddress),
onFulfilled: (value) => {
details.operatorRewards = decCoinToDisplay(value);
},
};
const getNymNodeRoleReq: TauriReq<typeof getNymNodeRole> = {
name: 'getNymNodeRole',
request: () => getNymNodeRole(nodeId),
onFulfilled: (value) => {
details.role = value;
},
};
await fireRequests([operatorRewardsReq, uptimeReq, stakeSaturationReq, getNymNodeRoleReq, nodeDescription]);
return details;
}
type TBondedNymNodeResponse = Awaited<ReturnType<typeof getNymNodeDetails>>;
type TBondedNymNode = NonNullable<TBondedNymNodeResponse>;
export { getNymNodeDetails };
export type { TBondedNymNodeResponse, TBondedNymNode };
+17 -9
View File
@@ -1,31 +1,25 @@
import {
DecCoin,
GatewayBond,
InclusionProbabilityResponse,
MixNodeDetails,
MixnodeStatusResponse,
PendingIntervalEvent,
RewardEstimationResponse,
StakeSaturationResponse,
WrappedDelegationEvent,
NymNodeDetails,
} from '@nymproject/types';
import { Interval, TGatewayReport, TNodeDescription } from 'src/types';
import { Interval, MixnodeSaturationResponse, TGatewayReport, TNodeDescription, TNodeRole } from 'src/types';
import { invokeWrapper } from './wrapper';
export const getAllPendingDelegations = async () =>
invokeWrapper<WrappedDelegationEvent[]>('get_pending_delegation_events');
export const getMixnodeBondDetails = async () => invokeWrapper<MixNodeDetails | null>('mixnode_bond_details');
export const getGatewayBondDetails = async () => invokeWrapper<GatewayBond | null>('gateway_bond_details');
export const getNymNodeBondDetails = async () => invokeWrapper<NymNodeDetails | null>('nym_node_bond_details');
export const getMixnodeAvgUptime = async () => invokeWrapper<number | null>('get_mixnode_avg_uptime');
export const getPendingOperatorRewards = async (address: string) =>
invokeWrapper<DecCoin>('get_pending_operator_rewards', { address });
export const getMixnodeStakeSaturation = async (mixId: number) =>
invokeWrapper<StakeSaturationResponse>('mixnode_stake_saturation', { mixId });
invokeWrapper<MixnodeSaturationResponse>('mixnode_stake_saturation', { mixId });
export const getMixnodeRewardEstimation = async (mixId: number) =>
invokeWrapper<RewardEstimationResponse>('mixnode_reward_estimation', { mixId });
@@ -37,6 +31,8 @@ export const checkMixnodeOwnership = async () => invokeWrapper<boolean>('owns_mi
export const checkGatewayOwnership = async () => invokeWrapper<boolean>('owns_gateway');
export const checkNymNodeOwnership = async () => invokeWrapper<boolean>('owns_nym_node');
export const getInclusionProbability = async (mixId: number) =>
invokeWrapper<InclusionProbabilityResponse>('mixnode_inclusion_probability', { mixId });
@@ -45,9 +41,12 @@ export const getCurrentInterval = async () => invokeWrapper<Interval>('get_curre
export const getNumberOfMixnodeDelegators = async (mixId: number) =>
invokeWrapper<number>('get_number_of_mixnode_delegators', { mixId });
export const getNodeDescription = async (host: string, port: number) =>
export const getMixNodeDescription = async (host: string, port: number) =>
invokeWrapper<TNodeDescription>('get_mix_node_description', { host, port });
export const getNymNodeDescription = async (host: string, port: number) =>
invokeWrapper<TNodeDescription>('get_nym_node_description', { host, port });
export const getPendingIntervalEvents = async () =>
invokeWrapper<PendingIntervalEvent[]>('get_pending_interval_events');
@@ -63,3 +62,12 @@ export const computeMixnodeRewardEstimation = async (args: {
intervalOperatingCost: { denom: 'unym'; amount: string };
}) => invokeWrapper<RewardEstimationResponse>('compute_mixnode_reward_estimation', args);
export const getMixnodeUptime = async (mixId: number) => invokeWrapper<number>('get_mixnode_uptime', { mixId });
export const getNymNodePerformance = async () => invokeWrapper<number>('get_nymnode_performance');
export const getNymNodeUptime = async (nodeId: number) => invokeWrapper<number>('get_nymnode_uptime', { nodeId });
export const getNymNodeStakeSaturation = async (nodeId: number) =>
invokeWrapper<StakeSaturationResponse>('get_nymnode_stake_saturation', { nodeId });
export const getNymNodeRole = async (nodeId: number) => invokeWrapper<TNodeRole>('get_nymnode_role', { nodeId });
+3 -3
View File
@@ -2,7 +2,7 @@ import {
FeeDetails,
DecCoin,
Gateway,
MixNodeCostParams,
NodeCostParams,
MixNodeConfigUpdate,
GatewayConfigUpdate,
} from '@nymproject/types';
@@ -19,7 +19,7 @@ export const simulateBondMixnode = async (args: TBondMixNodeArgs) =>
export const simulateUnbondMixnode = async (args: any) => invokeWrapper<FeeDetails>('simulate_unbond_mixnode', args);
export const simulateUpdateMixnodeCostParams = async (newCosts: MixNodeCostParams) =>
export const simulateUpdateMixnodeCostParams = async (newCosts: NodeCostParams) =>
invokeWrapper<FeeDetails>('simulate_update_mixnode_cost_params', { newCosts });
export const simulateUpdateMixnodeConfig = async (update: MixNodeConfigUpdate) =>
@@ -57,7 +57,7 @@ export const simulateVestingBondMixnode = async (args: TBondMixNodeArgs) =>
export const simulateVestingUnbondMixnode = async () => invokeWrapper<FeeDetails>('simulate_vesting_unbond_mixnode');
export const simulateVestingUpdateMixnodeCostParams = async (newCosts: MixNodeCostParams) =>
export const simulateVestingUpdateMixnodeCostParams = async (newCosts: NodeCostParams) =>
invokeWrapper<FeeDetails>('simulate_vesting_update_mixnode_cost_params', { newCosts });
export const simulateVestingUpdateMixnodeConfig = async (update: MixNodeConfigUpdate) =>
+3 -3
View File
@@ -1,5 +1,5 @@
import { AppEnv } from 'src/types';
import { MixNodeCostParams } from '@nymproject/types';
import { NodeCostParams } from '@nymproject/types';
import { invokeWrapper } from './wrapper';
export const getEnv = async () => invokeWrapper<AppEnv>('get_env');
@@ -7,5 +7,5 @@ export const getEnv = async () => invokeWrapper<AppEnv>('get_env');
export const tryConvertIdentityToMixId = async (mixIdentity: string) =>
invokeWrapper<number | null>('try_convert_pubkey_to_mix_id', { mixIdentity });
export const getDefaultMixnodeCostParams = async (profitMarginPercent: string) =>
invokeWrapper<MixNodeCostParams>('default_mixnode_cost_params', { profitMarginPercent });
export const getDefaultNodeCostParams = async (profitMarginPercent: string) =>
invokeWrapper<NodeCostParams>('default_mixnode_cost_params', { profitMarginPercent });
+3 -3
View File
@@ -8,7 +8,7 @@ import {
PledgeData,
TransactionExecuteResult,
VestingAccountInfo,
MixNodeCostParams,
NodeCostParams,
MixNodeConfigUpdate,
GatewayConfigUpdate,
} from '@nymproject/types';
@@ -61,7 +61,7 @@ export const vestingBondMixNode = async ({
msgSignature,
}: {
mixnode: MixNode;
costParams: MixNodeCostParams;
costParams: NodeCostParams;
pledge: DecCoin;
msgSignature: string;
}) => invokeWrapper<TransactionExecuteResult>('vesting_bond_mixnode', { mixnode, costParams, msgSignature, pledge });
@@ -75,7 +75,7 @@ export const vestingUnbondMixnode = async (fee?: Fee) =>
export const withdrawVestedCoins = async (amount: DecCoin, fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('withdraw_vested_coins', { amount, fee });
export const vestingUpdateMixnodeCostParams = async (newCosts: MixNodeCostParams, fee?: Fee) =>
export const vestingUpdateNodeCostParams = async (newCosts: NodeCostParams, fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('vesting_update_mixnode_cost_params', { newCosts, fee });
export const vestingUpdateMixnodeConfig = async (update: MixNodeConfigUpdate, fee?: Fee) =>
+35 -7
View File
@@ -1,10 +1,14 @@
import { DecCoin, Gateway, MixNode, MixNodeCostParams, PledgeData } from '@nymproject/types';
import { DecCoin, Gateway, MixNode, NodeCostParams, NymNode, PledgeData } from '@nymproject/types';
import { Fee } from '@nymproject/types/dist/types/rust/Fee';
import { TBondedGateway, TBondedMixnode } from 'src/context';
import { TBondedNode } from 'src/context';
import { TBondedGateway } from 'src/requests/gatewayDetails';
import { TBondedMixnode } from 'src/requests/mixnodeDetails';
import { TBondedNymNode } from 'src/requests/nymNodeDetails';
export enum EnumNodeType {
mixnode = 'mixnode',
gateway = 'gateway',
nymnode = 'nymnode',
}
export type TNodeOwnership = {
@@ -26,6 +30,17 @@ export type TDelegation = {
pending?: TPendingDelegation;
};
export type TBondNymNodeArgs = TNymNodeSignatureArgs & {
msgSignature: string;
fee?: Fee;
};
export type TNymNodeSignatureArgs = {
nymNode: NymNode;
costParams: NodeCostParams;
pledge: DecCoin;
};
export type TBondGatewayArgs = {
gateway: Gateway;
pledge: DecCoin;
@@ -35,7 +50,7 @@ export type TBondGatewayArgs = {
export type TBondMixNodeArgs = {
mixnode: MixNode;
costParams: MixNodeCostParams;
costParams: NodeCostParams;
pledge: DecCoin;
msgSignature: string;
fee?: Fee;
@@ -43,7 +58,7 @@ export type TBondMixNodeArgs = {
export type TBondMixnodeSignatureArgs = {
mixnode: MixNode;
costParams: MixNodeCostParams;
costParams: NodeCostParams;
pledge: DecCoin;
tokenPool: 'balance' | 'locked';
};
@@ -69,6 +84,11 @@ export type TNodeDescription = {
location: string;
};
export type TNodeConfigUpdateArgs = {
host: string;
custom_http_port: number;
};
export type TDelegateArgs = {
identity: string;
amount: DecCoin;
@@ -90,7 +110,15 @@ export type TGatewayReport = {
most_recent: number;
};
export const isMixnode = (node: TBondedMixnode | TBondedGateway): node is TBondedMixnode =>
(node as TBondedMixnode).profitMargin !== undefined;
export type TNodeRole = 'entry' | 'exit' | 'layer1' | 'layer2' | 'layer3' | 'standby';
export const isGateway = (node: TBondedMixnode | TBondedGateway): node is TBondedGateway => !isMixnode(node);
export type MixnodeSaturationResponse = {
saturation: string;
uncapped_saturation: string;
};
export const isMixnode = (node: TBondedNode): node is TBondedMixnode => (node as TBondedMixnode).mixId !== undefined;
export const isGateway = (node: TBondedNode): node is TBondedGateway => (node as TBondedGateway).location !== undefined;
export const isNymNode = (node: TBondedNode): node is TBondedNymNode => !isMixnode(node) && !isGateway(node);
+14 -4
View File
@@ -3,12 +3,12 @@ import bs58 from 'bs58';
import Big from 'big.js';
import { valid } from 'semver';
import { add, format, fromUnixTime } from 'date-fns';
import { DecCoin, isValidRawCoin, MixNodeCostParams } from '@nymproject/types';
import { DecCoin, isValidRawCoin, NodeCostParams } from '@nymproject/types';
import { TPoolOption } from 'src/components';
import Joi from 'joi';
import {
getCurrentInterval,
getDefaultMixnodeCostParams,
getDefaultNodeCostParams,
getLockedCoins,
getSpendableCoins,
userBalance,
@@ -138,8 +138,8 @@ export const checkTokenBalance = async (tokenPool: TPoolOption, amount: string)
export const isDecimal = (value: number) => value - Math.floor(value) !== 0;
export const attachDefaultOperatingCost = async (profitMarginPercent: string): Promise<MixNodeCostParams> =>
getDefaultMixnodeCostParams(profitMarginPercent);
export const attachDefaultOperatingCost = async (profitMarginPercent: string): Promise<NodeCostParams> =>
getDefaultNodeCostParams(profitMarginPercent);
/**
* Converts a stringified percentage integer (0-100) to a stringified float (0.0-1.0).
@@ -251,3 +251,13 @@ export const getIntervalAsDate = async () => {
return { nextEpoch, nextInterval };
};
export const calculateStake = (pledge: string, delegations: string) => {
let stake;
try {
stake = unymToNym(Big(pledge).plus(delegations));
} catch (e: any) {
Console.warn(`not a valid decimal number: ${e}`);
}
return stake;
};
@@ -18,7 +18,7 @@ import {
MixnetContractVersion,
MixNode,
MixNodeBond,
MixNodeCostParams,
NodeCostParams,
MixNodeDetails,
MixNodeRewarding,
MixOwnershipResponse,
@@ -451,7 +451,7 @@ export default class ValidatorClient implements INymClient {
public async bondMixNode(
mixNode: MixNode,
ownerSignature: string,
costParams: MixNodeCostParams,
costParams: NodeCostParams,
pledge: Coin,
fee?: StdFee | 'auto' | number,
memo?: string,
@@ -25,7 +25,7 @@ import {
MixnetContractVersion,
MixNode,
MixNodeBond,
MixNodeCostParams,
NodeCostParams,
MixNodeDetails,
MixNodeRewarding,
MixOwnershipResponse,
@@ -161,7 +161,7 @@ export interface ISigningClient extends IQueryClient, ICosmWasmSigning, INymSign
bondMixNode(
mixnetContractAddress: string,
mixNode: MixNode,
costParams: MixNodeCostParams,
costParams: NodeCostParams,
ownerSignature: string,
pledge: Coin,
fee?: StdFee | 'auto' | number,
@@ -372,7 +372,7 @@ export default class SigningClient extends SigningCosmWasmClient implements ISig
bondMixNode(
mixnetContractAddress: string,
mixNode: MixNode,
costParams: MixNodeCostParams,
costParams: NodeCostParams,
ownerSignature: string,
pledge: Coin,
fee: StdFee | 'auto' | number = 'auto',
@@ -1,8 +1,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { MixNodeCostParams } from './MixNodeCostParams';
import type { NodeCostParams } from './MixNodeCostParams';
export interface MixNodeRewarding {
cost_params: MixNodeCostParams;
cost_params: NodeCostParams;
operator: string;
delegates: string;
total_unit_reward: string;
+1 -2
View File
@@ -23,7 +23,6 @@ export * from './GatewayConfigUpdate';
export * from './GatewayCoreStatusResponse';
export * from './InclusionProbabilityResponse';
export * from './IntervalRewardingParamsUpdate';
export * from './IntervalRewardParams';
export * from './Mixnode';
export * from './MixNodeBond';
export * from './MixNodeConfigUpdate';
@@ -33,6 +32,7 @@ export * from './MixNodeDetails';
export * from './MixNodeRewarding';
export * from './MixnodeStatus';
export * from './MixnodeStatusResponse';
export * from './NymNodeDetails';
export * from './OriginalVestingResponse';
export * from './PendingEpochEvent';
export * from './PendingEpochEventData';
@@ -56,5 +56,4 @@ export * from './VestingPeriod';
export * from './WrappedDelegationEvent';
export * from './NymNode';
export * from './NymNodeBond';
export * from './NymNodeDetails';
export * from './NodeRewarding';