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:
@@ -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';
|
||||
};
|
||||
|
||||
@@ -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 <your-node-id> --contract-msg <payload-generated-by-the-wallet></code>
|
||||
<br />
|
||||
If you are using a nym-node:
|
||||
<br />
|
||||
<code>nym-node sign --id <your-node-id> --contract-msg <payload-generated-by-the-wallet></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 <your-node-id> --contract-msg <payload-generated-by-the-wallet></code>
|
||||
<br />
|
||||
If you are using a nym-node:
|
||||
<br />
|
||||
<code>nym-node sign --id <your-node-id> --contract-msg <payload-generated-by-the-wallet></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;
|
||||
+1
-5
@@ -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} />}
|
||||
</>
|
||||
);
|
||||
+3
-3
@@ -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,
|
||||
+1
-1
@@ -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
-5
@@ -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,
|
||||
+1
-1
@@ -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);
|
||||
+1
-1
@@ -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 <your-node-id> --contract-msg <payload-generated-by-the-wallet></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>
|
||||
);
|
||||
};
|
||||
@@ -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: {
|
||||
|
||||
@@ -32,5 +32,4 @@ export const NodeStatus = ({ status }: { status: MixnodeStatus }) => {
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
|
||||
@@ -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) },
|
||||
|
||||
@@ -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) },
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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 won’t 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 () => {
|
||||
|
||||
+3
-4
@@ -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 }}
|
||||
|
||||
+3
-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);
|
||||
|
||||
+182
@@ -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 node’s 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 node’s 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>
|
||||
);
|
||||
};
|
||||
+6
-10
@@ -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);
|
||||
|
||||
+7
-4
@@ -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:
|
||||
|
||||
@@ -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,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');
|
||||
|
||||
@@ -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');
|
||||
@@ -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,5 +1,6 @@
|
||||
export * from './app';
|
||||
export * from './account';
|
||||
export * from './bond';
|
||||
export * from './actions';
|
||||
export * from './contract';
|
||||
export * from './delegation';
|
||||
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 });
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user