Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d811446b6a | |||
| 65e252416c | |||
| bfd86d1462 | |||
| f4215d192f | |||
| 2524051da9 | |||
| 57245aaffe | |||
| 9398a055a2 | |||
| c8bf0fb853 | |||
| 91b9e2a1a7 | |||
| 426c9ae026 | |||
| 65e0b69a9d | |||
| b9001458de | |||
| 3a8214cdb7 |
@@ -98,9 +98,8 @@ export const DelegationsActionsMenu: React.FC<{
|
|||||||
onActionClick?: (action: DelegationListItemActions) => void;
|
onActionClick?: (action: DelegationListItemActions) => void;
|
||||||
isPending?: DelegationEventKind;
|
isPending?: DelegationEventKind;
|
||||||
disableRedeemingRewards?: boolean;
|
disableRedeemingRewards?: boolean;
|
||||||
disableDelegateMore?: boolean;
|
|
||||||
disableCompoundRewards?: boolean;
|
disableCompoundRewards?: boolean;
|
||||||
}> = ({ disableRedeemingRewards, disableDelegateMore, disableCompoundRewards, onActionClick, isPending }) => {
|
}> = ({ disableRedeemingRewards, disableCompoundRewards, onActionClick, isPending }) => {
|
||||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||||
const open = Boolean(anchorEl);
|
const open = Boolean(anchorEl);
|
||||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, CircularProgress, Modal, Stack, Typography } from '@mui/material';
|
||||||
|
|
||||||
|
const modalStyle = {
|
||||||
|
position: 'absolute' as 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
width: 500,
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
boxShadow: 24,
|
||||||
|
borderRadius: '16px',
|
||||||
|
p: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LoadingModal = () => (
|
||||||
|
<Modal open>
|
||||||
|
<Box sx={modalStyle} textAlign="center">
|
||||||
|
<Stack spacing={4} direction="row" alignItems="center">
|
||||||
|
<CircularProgress />
|
||||||
|
<Typography>Please wait...</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormControl, FormControlLabel, FormLabel, Radio, RadioGroup } from '@mui/material';
|
import { FormControl, FormControlLabel, FormLabel, Radio, RadioGroup } from '@mui/material';
|
||||||
import { EnumNodeType } from '@nymproject/types';
|
import { TNodeType } from '@nymproject/types';
|
||||||
|
|
||||||
export const NodeTypeSelector = ({
|
export const NodeTypeSelector = ({
|
||||||
disabled,
|
disabled,
|
||||||
@@ -8,10 +8,10 @@ export const NodeTypeSelector = ({
|
|||||||
setNodeType,
|
setNodeType,
|
||||||
}: {
|
}: {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
nodeType: EnumNodeType;
|
nodeType: TNodeType;
|
||||||
setNodeType: (nodeType: EnumNodeType) => void;
|
setNodeType: (nodeType: TNodeType) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const handleNodeTypeChange = (e: React.ChangeEvent<HTMLInputElement>) => setNodeType(e.target.value as EnumNodeType);
|
const handleNodeTypeChange = (e: React.ChangeEvent<HTMLInputElement>) => setNodeType(e.target.value as TNodeType);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControl component="fieldset">
|
<FormControl component="fieldset">
|
||||||
@@ -24,14 +24,14 @@ export const NodeTypeSelector = ({
|
|||||||
style={{ display: 'block' }}
|
style={{ display: 'block' }}
|
||||||
>
|
>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
value={EnumNodeType.mixnode}
|
value="mixnode"
|
||||||
control={<Radio color="default" />}
|
control={<Radio color="default" />}
|
||||||
label="Mixnode"
|
label="Mixnode"
|
||||||
data-testid="mix-node"
|
data-testid="mix-node"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
value={EnumNodeType.gateway}
|
value="gateway"
|
||||||
control={<Radio color="default" />}
|
control={<Radio color="default" />}
|
||||||
data-testid="gate-way"
|
data-testid="gate-way"
|
||||||
label="Gateway"
|
label="Gateway"
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Alert, AlertTitle, Stack, Typography } from '@mui/material';
|
|
||||||
import WarningIcon from '@mui/icons-material/Warning';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
|
import { Alert, AlertTitle, Stack, Typography } from '@mui/material';
|
||||||
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
|
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
|
||||||
|
import { FeeDetails } from '@nymproject/types';
|
||||||
|
import { useGetFee } from 'src/hooks/useGetFee';
|
||||||
import { simulateCompoundDelgatorReward, simulateVestingCompoundDelgatorReward } from 'src/requests';
|
import { simulateCompoundDelgatorReward, simulateVestingCompoundDelgatorReward } from 'src/requests';
|
||||||
import { isGreaterThan } from 'src/utils';
|
import { isGreaterThan } from 'src/utils';
|
||||||
import { useGetFee } from 'src/hooks/useGetFee';
|
|
||||||
import { SimpleModal } from '../Modals/SimpleModal';
|
|
||||||
import { ModalFee } from '../Modals/ModalFee';
|
import { ModalFee } from '../Modals/ModalFee';
|
||||||
import { FeeDetails } from '@nymproject/types';
|
import { SimpleModal } from '../Modals/SimpleModal';
|
||||||
|
|
||||||
export const CompoundModal: React.FC<{
|
export const CompoundModal: React.FC<{
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -15,7 +15,6 @@ export const CompoundModal: React.FC<{
|
|||||||
onOk?: (identityKey: string, fee?: FeeDetails) => void;
|
onOk?: (identityKey: string, fee?: FeeDetails) => void;
|
||||||
identityKey: string;
|
identityKey: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
minimum?: number;
|
|
||||||
currency: string;
|
currency: string;
|
||||||
message: string;
|
message: string;
|
||||||
usesVestingTokens: boolean;
|
usesVestingTokens: boolean;
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Alert, AlertTitle, Stack, Typography } from '@mui/material';
|
import { Alert, AlertTitle, Stack, Typography } from '@mui/material';
|
||||||
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
|
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
|
||||||
|
import { FeeDetails } from '@nymproject/types';
|
||||||
import WarningIcon from '@mui/icons-material/Warning';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
import { simulateClaimDelgatorReward, simulateVestingClaimDelgatorReward } from 'src/requests';
|
import { simulateClaimDelgatorReward, simulateVestingClaimDelgatorReward } from 'src/requests';
|
||||||
import { isGreaterThan } from 'src/utils';
|
import { isGreaterThan } from 'src/utils';
|
||||||
import { useGetFee } from 'src/hooks/useGetFee';
|
import { useGetFee } from 'src/hooks/useGetFee';
|
||||||
import { SimpleModal } from '../Modals/SimpleModal';
|
import { SimpleModal } from '../Modals/SimpleModal';
|
||||||
import { ModalFee } from '../Modals/ModalFee';
|
import { ModalFee } from '../Modals/ModalFee';
|
||||||
import { FeeDetails } from '@nymproject/types';
|
|
||||||
|
|
||||||
export const RedeemModal: React.FC<{
|
export const RedeemModal: React.FC<{
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -15,7 +15,6 @@ export const RedeemModal: React.FC<{
|
|||||||
onOk?: (identityKey: string, fee?: FeeDetails) => void;
|
onOk?: (identityKey: string, fee?: FeeDetails) => void;
|
||||||
identityKey: string;
|
identityKey: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
minimum?: number;
|
|
||||||
currency: string;
|
currency: string;
|
||||||
message: string;
|
message: string;
|
||||||
usesVestingTokens: boolean;
|
usesVestingTokens: boolean;
|
||||||
|
|||||||
@@ -56,11 +56,7 @@ export const DelegationContextProvider: FC<{
|
|||||||
const [totalRewards, setTotalRewards] = useState<undefined | string>();
|
const [totalRewards, setTotalRewards] = useState<undefined | string>();
|
||||||
const [pendingDelegations, setPendingDelegations] = useState<DelegationEvent[]>();
|
const [pendingDelegations, setPendingDelegations] = useState<DelegationEvent[]>();
|
||||||
|
|
||||||
const addDelegation = async (
|
const addDelegation = async (data: { identity: string; amount: MajorCurrencyAmount }, tokenPool: TPoolOption) => {
|
||||||
data: { identity: string; amount: MajorCurrencyAmount },
|
|
||||||
tokenPool: TPoolOption,
|
|
||||||
fee?: FeeDetails,
|
|
||||||
) => {
|
|
||||||
try {
|
try {
|
||||||
let tx;
|
let tx;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, Button } from '@mui/material';
|
||||||
|
import { FeeDetails, MajorCurrencyAmount } from '@nymproject/types';
|
||||||
|
import { SimpleModal } from 'src/components/Modals/SimpleModal';
|
||||||
|
import { ModalFee } from 'src/components/Modals/ModalFee';
|
||||||
|
import { ModalListItem } from 'src/components/Modals/ModalListItem';
|
||||||
|
|
||||||
|
export const ConfirmationModal = ({
|
||||||
|
identity,
|
||||||
|
amount,
|
||||||
|
fee,
|
||||||
|
onPrev,
|
||||||
|
onConfirm,
|
||||||
|
}: {
|
||||||
|
identity: string;
|
||||||
|
amount: MajorCurrencyAmount;
|
||||||
|
fee: FeeDetails;
|
||||||
|
onPrev: () => void;
|
||||||
|
onConfirm: () => Promise<void>;
|
||||||
|
}) => (
|
||||||
|
<SimpleModal header="Bond confirmation" open onOk={onConfirm} okLabel="Confirm" hideCloseIcon onBack={onPrev}>
|
||||||
|
<Box sx={{ mt: 3 }}>
|
||||||
|
<ModalListItem label="Mixnode identity" value={identity} />
|
||||||
|
<ModalListItem label="Amount" value={`${amount.amount} ${amount.denom}`} />
|
||||||
|
<ModalFee fee={fee} isLoading={false} />
|
||||||
|
</Box>
|
||||||
|
</SimpleModal>
|
||||||
|
);
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Box } from '@mui/material';
|
||||||
|
import { TNodeType } from '@nymproject/types';
|
||||||
|
import { NodeTypeSelector } from 'src/components';
|
||||||
|
import { GatewayForm } from './GatewayForm';
|
||||||
|
import { MixnodeForm } from './MixnodeForm';
|
||||||
|
|
||||||
|
export const FormHandler = ({
|
||||||
|
onSuccess,
|
||||||
|
onError,
|
||||||
|
}: {
|
||||||
|
onSuccess: (details: { address: string; amount: string }) => void;
|
||||||
|
onError: (msg?: string) => void;
|
||||||
|
}) => {
|
||||||
|
const [nodeType, setNodeType] = useState<TNodeType>('mixnode');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 3 }}>
|
||||||
|
<NodeTypeSelector disabled={false} nodeType={nodeType} setNodeType={setNodeType} />
|
||||||
|
{nodeType === 'mixnode' && <MixnodeForm disabled={false} onError={onError} onSuccess={onSuccess} />}
|
||||||
|
{nodeType === 'gateway' && <GatewayForm disabled={false} onError={onError} onSuccess={onSuccess} />}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
+108
-168
@@ -1,45 +1,34 @@
|
|||||||
import React, { useContext, useEffect } from 'react';
|
import React, { useContext, useEffect } from 'react';
|
||||||
import { Box, Button, Checkbox, CircularProgress, FormControl, FormControlLabel, Grid, TextField } from '@mui/material';
|
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { useForm } from 'react-hook-form';
|
import { Box, Button, Checkbox, CircularProgress, FormControl, FormControlLabel, Grid, TextField } from '@mui/material';
|
||||||
import {
|
|
||||||
Gateway,
|
|
||||||
MixNode,
|
|
||||||
EnumNodeType,
|
|
||||||
MajorCurrencyAmount,
|
|
||||||
CurrencyDenom,
|
|
||||||
TransactionExecuteResult,
|
|
||||||
} from '@nymproject/types';
|
|
||||||
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
|
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
|
||||||
import { TBondArgs } from 'src/types';
|
import { CurrencyDenom, MajorCurrencyAmount } from '@nymproject/types';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { LoadingModal } from 'src/components/Modals/LoadingModal';
|
||||||
|
import { useGetFee } from 'src/hooks/useGetFee';
|
||||||
|
import { bondGateway, simulateBondGateway, simulateVestingBondGateway, vestingBondGateway } from 'src/requests';
|
||||||
import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from 'src/utils';
|
import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from 'src/utils';
|
||||||
import { NodeTypeSelector } from '../../components/NodeTypeSelector';
|
import { Fee, TokenPoolSelector } from '../../../components';
|
||||||
import { bond, vestingBond } from '../../requests';
|
import { AppContext } from '../../../context/main';
|
||||||
import { validationSchema } from './validationSchema';
|
import { gatewayValidationSchema } from '../validationSchema';
|
||||||
import { AppContext } from '../../context/main';
|
import { ConfirmationModal } from './ConfirmationModal';
|
||||||
import { Fee, TokenPoolSelector } from '../../components';
|
|
||||||
|
|
||||||
type TBondFormFields = {
|
type TBondFormFields = {
|
||||||
withAdvancedOptions: boolean;
|
withAdvancedOptions: boolean;
|
||||||
nodeType: EnumNodeType;
|
|
||||||
tokenPool: string;
|
tokenPool: string;
|
||||||
ownerSignature: string;
|
ownerSignature: string;
|
||||||
identityKey: string;
|
identityKey: string;
|
||||||
sphinxKey: string;
|
sphinxKey: string;
|
||||||
profitMarginPercent: number;
|
|
||||||
amount: MajorCurrencyAmount;
|
amount: MajorCurrencyAmount;
|
||||||
host: string;
|
host: string;
|
||||||
version: string;
|
version: string;
|
||||||
location?: string;
|
location: string;
|
||||||
mixPort: number;
|
mixPort: number;
|
||||||
verlocPort: number;
|
|
||||||
clientsPort: number;
|
clientsPort: number;
|
||||||
httpApiPort: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValues = {
|
const defaultValues = {
|
||||||
withAdvancedOptions: false,
|
withAdvancedOptions: false,
|
||||||
nodeType: EnumNodeType.mixnode,
|
|
||||||
tokenPool: 'balance',
|
tokenPool: 'balance',
|
||||||
identityKey: '',
|
identityKey: '',
|
||||||
sphinxKey: '',
|
sphinxKey: '',
|
||||||
@@ -47,35 +36,12 @@ const defaultValues = {
|
|||||||
amount: { amount: '', denom: 'NYM' as CurrencyDenom },
|
amount: { amount: '', denom: 'NYM' as CurrencyDenom },
|
||||||
host: '',
|
host: '',
|
||||||
version: '',
|
version: '',
|
||||||
profitMarginPercent: 10,
|
location: '',
|
||||||
location: undefined,
|
|
||||||
mixPort: 1789,
|
mixPort: 1789,
|
||||||
verlocPort: 1790,
|
|
||||||
httpApiPort: 8000,
|
|
||||||
clientsPort: 9000,
|
clientsPort: 9000,
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatData = (data: TBondFormFields): MixNode | Gateway => {
|
export const GatewayForm = ({
|
||||||
const payload: { [key: string]: any } = {
|
|
||||||
identity_key: data.identityKey,
|
|
||||||
sphinx_key: data.sphinxKey,
|
|
||||||
host: data.host,
|
|
||||||
version: data.version,
|
|
||||||
mix_port: data.mixPort,
|
|
||||||
profit_margin_percent: data.profitMarginPercent,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (data.nodeType === EnumNodeType.mixnode) {
|
|
||||||
payload.verloc_port = data.verlocPort;
|
|
||||||
payload.http_api_port = data.httpApiPort;
|
|
||||||
return payload as MixNode;
|
|
||||||
}
|
|
||||||
payload.clients_port = data.clientsPort;
|
|
||||||
payload.location = data.location;
|
|
||||||
return payload as Gateway;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const BondForm = ({
|
|
||||||
disabled,
|
disabled,
|
||||||
onError,
|
onError,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
@@ -91,22 +57,23 @@ export const BondForm = ({
|
|||||||
watch,
|
watch,
|
||||||
reset,
|
reset,
|
||||||
setError,
|
setError,
|
||||||
|
getValues,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
} = useForm<TBondFormFields>({
|
} = useForm<TBondFormFields>({
|
||||||
resolver: yupResolver(validationSchema),
|
resolver: yupResolver(gatewayValidationSchema),
|
||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { userBalance, clientDetails } = useContext(AppContext);
|
const { userBalance, clientDetails } = useContext(AppContext);
|
||||||
|
|
||||||
|
const { fee, getFee, resetFeeState } = useGetFee();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset();
|
reset();
|
||||||
}, [clientDetails]);
|
}, [clientDetails]);
|
||||||
|
|
||||||
const watchNodeType = watch('nodeType', defaultValues.nodeType);
|
|
||||||
const watchAdvancedOptions = watch('withAdvancedOptions', defaultValues.withAdvancedOptions);
|
const watchAdvancedOptions = watch('withAdvancedOptions', defaultValues.withAdvancedOptions);
|
||||||
|
|
||||||
const onSubmit = async (data: TBondFormFields, cb: (data: TBondArgs) => Promise<TransactionExecuteResult>) => {
|
const handleValidateAndGetFee = async (data: TBondFormFields) => {
|
||||||
if (data.tokenPool === 'balance' && !(await checkHasEnoughFunds(data.amount.amount || ''))) {
|
if (data.tokenPool === 'balance' && !(await checkHasEnoughFunds(data.amount.amount || ''))) {
|
||||||
return setError('amount.amount', { message: 'Not enough funds in wallet' });
|
return setError('amount.amount', { message: 'Not enough funds in wallet' });
|
||||||
}
|
}
|
||||||
@@ -115,43 +82,73 @@ export const BondForm = ({
|
|||||||
return setError('amount.amount', { message: 'Not enough locked tokens' });
|
return setError('amount.amount', { message: 'Not enough locked tokens' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const formattedData = formatData(data);
|
try {
|
||||||
|
await getFee(data.tokenPool === 'locked' ? simulateVestingBondGateway : simulateBondGateway, {
|
||||||
return cb({
|
ownerSignature: data.ownerSignature,
|
||||||
type: data.nodeType,
|
gateway: {
|
||||||
ownerSignature: data.ownerSignature,
|
identity_key: data.identityKey,
|
||||||
[data.nodeType]: formattedData,
|
sphinx_key: data.sphinxKey,
|
||||||
pledge: data.amount,
|
host: data.host,
|
||||||
} as TBondArgs)
|
version: data.version,
|
||||||
.then(async () => {
|
mix_port: data.mixPort,
|
||||||
if (data.tokenPool === 'balance') {
|
location: data.location,
|
||||||
await userBalance.fetchBalance();
|
clients_port: data.clientsPort,
|
||||||
} else {
|
},
|
||||||
await userBalance.fetchTokenAllocation();
|
pledge: data.amount,
|
||||||
}
|
|
||||||
onSuccess({ address: data.identityKey, amount: data.amount.amount });
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
onError(e);
|
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
onError(e as string);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (data: TBondFormFields) => {
|
||||||
|
const payload = {
|
||||||
|
ownerSignature: data.ownerSignature,
|
||||||
|
gateway: {
|
||||||
|
identity_key: data.identityKey,
|
||||||
|
sphinx_key: data.sphinxKey,
|
||||||
|
host: data.host,
|
||||||
|
version: data.version,
|
||||||
|
mix_port: data.mixPort,
|
||||||
|
location: data.location,
|
||||||
|
clients_port: data.clientsPort,
|
||||||
|
},
|
||||||
|
pledge: data.amount,
|
||||||
|
fee: fee?.fee,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
if (data.tokenPool === 'balance') {
|
||||||
|
await bondGateway(payload);
|
||||||
|
await userBalance.fetchBalance();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.tokenPool === 'locked') {
|
||||||
|
await vestingBondGateway(payload);
|
||||||
|
await userBalance.fetchTokenAllocation();
|
||||||
|
}
|
||||||
|
|
||||||
|
onSuccess({ address: payload.gateway.identity_key, amount: payload.pledge.amount });
|
||||||
|
} catch (e) {
|
||||||
|
onError(e as string);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControl fullWidth>
|
<FormControl fullWidth>
|
||||||
<Box sx={{ p: 3 }}>
|
{isSubmitting && <LoadingModal />}
|
||||||
|
|
||||||
|
{fee && !isSubmitting && (
|
||||||
|
<ConfirmationModal
|
||||||
|
identity={getValues('identityKey')}
|
||||||
|
amount={getValues('amount')}
|
||||||
|
fee={fee}
|
||||||
|
onPrev={resetFeeState}
|
||||||
|
onConfirm={handleSubmit(onSubmit)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Box>
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
<Grid container item justifyContent="space-between">
|
|
||||||
<Grid item>
|
|
||||||
<NodeTypeSelector
|
|
||||||
nodeType={watchNodeType}
|
|
||||||
setNodeType={(nodeType) => {
|
|
||||||
setValue('nodeType', nodeType);
|
|
||||||
if (nodeType === EnumNodeType.mixnode) setValue('location', undefined);
|
|
||||||
}}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<TextField
|
<TextField
|
||||||
{...register('identityKey')}
|
{...register('identityKey')}
|
||||||
@@ -213,40 +210,20 @@ export const BondForm = ({
|
|||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{watchNodeType === EnumNodeType.mixnode && (
|
<Grid item xs={6}>
|
||||||
<Grid item xs={12} sm={6}>
|
<TextField
|
||||||
<TextField
|
{...register('location')}
|
||||||
{...register('profitMarginPercent')}
|
variant="outlined"
|
||||||
variant="outlined"
|
required
|
||||||
required
|
id="location"
|
||||||
id="profitMarginPercent"
|
name="location"
|
||||||
name="profitMarginPercent"
|
label="Location"
|
||||||
label="Profit percentage"
|
fullWidth
|
||||||
fullWidth
|
error={!!errors.location}
|
||||||
error={!!errors.profitMarginPercent}
|
helperText={errors.location?.message}
|
||||||
helperText={errors.profitMarginPercent ? errors.profitMarginPercent.message : 'Default is 10%'}
|
disabled={disabled}
|
||||||
disabled={disabled}
|
/>
|
||||||
/>
|
</Grid>
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* if it's a gateway - get location */}
|
|
||||||
{watchNodeType === EnumNodeType.gateway && (
|
|
||||||
<Grid item xs={6}>
|
|
||||||
<TextField
|
|
||||||
{...register('location')}
|
|
||||||
variant="outlined"
|
|
||||||
required
|
|
||||||
id="location"
|
|
||||||
name="location"
|
|
||||||
label="Location"
|
|
||||||
fullWidth
|
|
||||||
error={!!errors.location}
|
|
||||||
helperText={errors.location?.message}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -291,12 +268,6 @@ export const BondForm = ({
|
|||||||
setValue('clientsPort', defaultValues.clientsPort, {
|
setValue('clientsPort', defaultValues.clientsPort, {
|
||||||
shouldValidate: true,
|
shouldValidate: true,
|
||||||
});
|
});
|
||||||
setValue('verlocPort', defaultValues.verlocPort, {
|
|
||||||
shouldValidate: true,
|
|
||||||
});
|
|
||||||
setValue('httpApiPort', defaultValues.httpApiPort, {
|
|
||||||
shouldValidate: true,
|
|
||||||
});
|
|
||||||
setValue('withAdvancedOptions', false);
|
setValue('withAdvancedOptions', false);
|
||||||
} else {
|
} else {
|
||||||
setValue('withAdvancedOptions', true);
|
setValue('withAdvancedOptions', true);
|
||||||
@@ -322,55 +293,24 @@ export const BondForm = ({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
{watchNodeType === EnumNodeType.mixnode ? (
|
|
||||||
<>
|
|
||||||
<Grid item xs={12} sm={4}>
|
|
||||||
<TextField
|
|
||||||
{...register('verlocPort', { valueAsNumber: true })}
|
|
||||||
variant="outlined"
|
|
||||||
id="verlocPort"
|
|
||||||
name="verlocPort"
|
|
||||||
label="Verloc Port"
|
|
||||||
fullWidth
|
|
||||||
error={!!errors.verlocPort}
|
|
||||||
helperText={errors.verlocPort?.message && 'A valid port value is required'}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid item xs={12} sm={4}>
|
<Grid item xs={12} sm={4}>
|
||||||
<TextField
|
<TextField
|
||||||
{...register('httpApiPort', { valueAsNumber: true })}
|
{...register('clientsPort', { valueAsNumber: true })}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
id="httpApiPort"
|
id="clientsPort"
|
||||||
name="httpApiPort"
|
name="clientsPort"
|
||||||
label="HTTP API Port"
|
label="client WS API Port"
|
||||||
fullWidth
|
fullWidth
|
||||||
error={!!errors.httpApiPort}
|
error={!!errors.clientsPort}
|
||||||
helperText={errors.httpApiPort?.message && 'A valid port value is required'}
|
helperText={errors.clientsPort?.message && 'A valid port value is required'}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Grid item xs={12} sm={4}>
|
|
||||||
<TextField
|
|
||||||
{...register('clientsPort', { valueAsNumber: true })}
|
|
||||||
variant="outlined"
|
|
||||||
id="clientsPort"
|
|
||||||
name="clientsPort"
|
|
||||||
label="client WS API Port"
|
|
||||||
fullWidth
|
|
||||||
error={!!errors.clientsPort}
|
|
||||||
helperText={errors.clientsPort?.message && 'A valid port value is required'}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
{!disabled ? <Fee feeType={EnumNodeType.mixnode ? 'BondMixnode' : 'BondGateway'} /> : <div />}
|
{!disabled ? <Fee feeType="BondGateway" /> : <div />}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -390,7 +330,7 @@ export const BondForm = ({
|
|||||||
type="submit"
|
type="submit"
|
||||||
data-testid="submit-button"
|
data-testid="submit-button"
|
||||||
disableElevation
|
disableElevation
|
||||||
onClick={handleSubmit((data) => onSubmit(data, data.tokenPool === 'balance' ? bond : vestingBond))}
|
onClick={handleSubmit(handleValidateAndGetFee)}
|
||||||
endIcon={isSubmitting && <CircularProgress size={20} />}
|
endIcon={isSubmitting && <CircularProgress size={20} />}
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
@@ -0,0 +1,361 @@
|
|||||||
|
import React, { useContext, useEffect } from 'react';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import { Box, Button, Checkbox, CircularProgress, FormControl, FormControlLabel, Grid, TextField } from '@mui/material';
|
||||||
|
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
|
||||||
|
import { CurrencyDenom, MajorCurrencyAmount } from '@nymproject/types';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { LoadingModal } from 'src/components/Modals/LoadingModal';
|
||||||
|
import { useGetFee } from 'src/hooks/useGetFee';
|
||||||
|
import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from 'src/utils';
|
||||||
|
import { TokenPoolSelector } from '../../../components';
|
||||||
|
import { AppContext } from '../../../context/main';
|
||||||
|
import { bondMixNode, simulateBondMixnode, simulateVestingBondMixnode, vestingBondMixNode } from '../../../requests';
|
||||||
|
import { mixnodeValidationSchema } from '../validationSchema';
|
||||||
|
import { ConfirmationModal } from './ConfirmationModal';
|
||||||
|
|
||||||
|
type TBondFormFields = {
|
||||||
|
withAdvancedOptions: boolean;
|
||||||
|
tokenPool: string;
|
||||||
|
ownerSignature: string;
|
||||||
|
identityKey: string;
|
||||||
|
sphinxKey: string;
|
||||||
|
profitMarginPercent: number;
|
||||||
|
amount: MajorCurrencyAmount;
|
||||||
|
host: string;
|
||||||
|
version: string;
|
||||||
|
mixPort: number;
|
||||||
|
verlocPort: number;
|
||||||
|
httpApiPort: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultValues = {
|
||||||
|
withAdvancedOptions: false,
|
||||||
|
tokenPool: 'balance',
|
||||||
|
identityKey: '',
|
||||||
|
sphinxKey: '',
|
||||||
|
ownerSignature: '',
|
||||||
|
amount: { amount: '', denom: 'NYM' as CurrencyDenom },
|
||||||
|
host: '',
|
||||||
|
version: '',
|
||||||
|
profitMarginPercent: 10,
|
||||||
|
mixPort: 1789,
|
||||||
|
verlocPort: 1790,
|
||||||
|
httpApiPort: 8000,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MixnodeForm = ({
|
||||||
|
disabled,
|
||||||
|
onError,
|
||||||
|
onSuccess,
|
||||||
|
}: {
|
||||||
|
disabled: boolean;
|
||||||
|
onError: (message?: string) => void;
|
||||||
|
onSuccess: (details: { address: string; amount: string }) => void;
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
|
getValues,
|
||||||
|
watch,
|
||||||
|
reset,
|
||||||
|
setError,
|
||||||
|
formState: { errors, isSubmitting },
|
||||||
|
} = useForm<TBondFormFields>({
|
||||||
|
resolver: yupResolver(mixnodeValidationSchema),
|
||||||
|
defaultValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { userBalance, clientDetails } = useContext(AppContext);
|
||||||
|
|
||||||
|
const { fee, getFee, resetFeeState } = useGetFee();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset();
|
||||||
|
}, [clientDetails]);
|
||||||
|
|
||||||
|
const watchAdvancedOptions = watch('withAdvancedOptions', defaultValues.withAdvancedOptions);
|
||||||
|
|
||||||
|
const handleValidateAndGetFee = async (data: TBondFormFields) => {
|
||||||
|
if (data.tokenPool === 'balance' && !(await checkHasEnoughFunds(data.amount.amount || ''))) {
|
||||||
|
return setError('amount.amount', { message: 'Not enough funds in wallet' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.tokenPool === 'locked' && !(await checkHasEnoughLockedTokens(data.amount.amount || ''))) {
|
||||||
|
return setError('amount.amount', { message: 'Not enough locked tokens' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await getFee(data.tokenPool === 'locked' ? simulateVestingBondMixnode : simulateBondMixnode, {
|
||||||
|
ownerSignature: data.ownerSignature,
|
||||||
|
mixnode: {
|
||||||
|
identity_key: data.identityKey,
|
||||||
|
sphinx_key: data.sphinxKey,
|
||||||
|
host: data.host,
|
||||||
|
version: data.version,
|
||||||
|
mix_port: data.mixPort,
|
||||||
|
profit_margin_percent: data.profitMarginPercent,
|
||||||
|
verloc_port: data.verlocPort,
|
||||||
|
http_api_port: data.httpApiPort,
|
||||||
|
},
|
||||||
|
pledge: data.amount,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
onError(e as string);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (data: TBondFormFields) => {
|
||||||
|
const payload = {
|
||||||
|
ownerSignature: data.ownerSignature,
|
||||||
|
mixnode: {
|
||||||
|
identity_key: data.identityKey,
|
||||||
|
sphinx_key: data.sphinxKey,
|
||||||
|
host: data.host,
|
||||||
|
version: data.version,
|
||||||
|
mix_port: data.mixPort,
|
||||||
|
profit_margin_percent: data.profitMarginPercent,
|
||||||
|
verloc_port: data.verlocPort,
|
||||||
|
http_api_port: data.httpApiPort,
|
||||||
|
},
|
||||||
|
pledge: data.amount,
|
||||||
|
fee: fee?.fee,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
if (data.tokenPool === 'balance') {
|
||||||
|
await bondMixNode(payload);
|
||||||
|
await userBalance.fetchBalance();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.tokenPool === 'locked') {
|
||||||
|
await vestingBondMixNode(payload);
|
||||||
|
await userBalance.fetchTokenAllocation();
|
||||||
|
}
|
||||||
|
|
||||||
|
onSuccess({ address: payload.mixnode.identity_key, amount: payload.pledge.amount });
|
||||||
|
} catch (e) {
|
||||||
|
onError(e as string);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isSubmitting && <LoadingModal />}
|
||||||
|
|
||||||
|
{fee && !isSubmitting && (
|
||||||
|
<ConfirmationModal
|
||||||
|
identity={getValues('identityKey')}
|
||||||
|
amount={getValues('amount')}
|
||||||
|
fee={fee}
|
||||||
|
onPrev={resetFeeState}
|
||||||
|
onConfirm={handleSubmit(onSubmit)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<Box>
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TextField
|
||||||
|
{...register('identityKey')}
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
id="identityKey"
|
||||||
|
name="identityKey"
|
||||||
|
label="Identity key"
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.identityKey}
|
||||||
|
helperText={errors.identityKey?.message}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TextField
|
||||||
|
{...register('sphinxKey')}
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
id="sphinxKey"
|
||||||
|
name="sphinxKey"
|
||||||
|
label="Sphinx key"
|
||||||
|
error={!!errors.sphinxKey}
|
||||||
|
helperText={errors.sphinxKey?.message}
|
||||||
|
fullWidth
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sm={12}>
|
||||||
|
<TextField
|
||||||
|
{...register('ownerSignature')}
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
id="ownerSignature"
|
||||||
|
name="ownerSignature"
|
||||||
|
label="Owner signature"
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.ownerSignature}
|
||||||
|
helperText={errors.ownerSignature?.message}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{userBalance.originalVesting && (
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<TokenPoolSelector onSelect={(pool) => setValue('tokenPool', pool)} disabled={disabled} />
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<CurrencyFormField
|
||||||
|
showCoinMark
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
label="Amount"
|
||||||
|
onChanged={(val) => setValue('amount', val, { shouldValidate: true })}
|
||||||
|
denom={clientDetails?.denom}
|
||||||
|
validationError={errors.amount?.amount?.message}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<TextField
|
||||||
|
{...register('profitMarginPercent')}
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
id="profitMarginPercent"
|
||||||
|
name="profitMarginPercent"
|
||||||
|
label="Profit percentage"
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.profitMarginPercent}
|
||||||
|
helperText={errors.profitMarginPercent ? errors.profitMarginPercent.message : 'Default is 10%'}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<TextField
|
||||||
|
{...register('host')}
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
id="host"
|
||||||
|
name="host"
|
||||||
|
label="Host"
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.host}
|
||||||
|
helperText={errors.host?.message}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<TextField
|
||||||
|
{...register('version')}
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
id="version"
|
||||||
|
name="version"
|
||||||
|
label="Version"
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.version}
|
||||||
|
helperText={errors.version?.message}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={watchAdvancedOptions}
|
||||||
|
onChange={() => {
|
||||||
|
if (watchAdvancedOptions) {
|
||||||
|
setValue('mixPort', defaultValues.mixPort, {
|
||||||
|
shouldValidate: true,
|
||||||
|
});
|
||||||
|
setValue('httpApiPort', defaultValues.httpApiPort, {
|
||||||
|
shouldValidate: true,
|
||||||
|
});
|
||||||
|
setValue('withAdvancedOptions', false);
|
||||||
|
} else {
|
||||||
|
setValue('withAdvancedOptions', true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Use advanced options"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
{watchAdvancedOptions && (
|
||||||
|
<>
|
||||||
|
<Grid item xs={12} sm={4}>
|
||||||
|
<TextField
|
||||||
|
{...register('mixPort', { valueAsNumber: true })}
|
||||||
|
variant="outlined"
|
||||||
|
id="mixPort"
|
||||||
|
name="mixPort"
|
||||||
|
label="Mix Port"
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.mixPort}
|
||||||
|
helperText={errors.mixPort?.message && 'A valid port value is required'}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sm={4}>
|
||||||
|
<TextField
|
||||||
|
{...register('verlocPort', { valueAsNumber: true })}
|
||||||
|
variant="outlined"
|
||||||
|
id="verlocPort"
|
||||||
|
name="verlocPort"
|
||||||
|
label="Verloc Port"
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.verlocPort}
|
||||||
|
helperText={errors.verlocPort?.message && 'A valid port value is required'}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sm={4}>
|
||||||
|
<TextField
|
||||||
|
{...register('httpApiPort', { valueAsNumber: true })}
|
||||||
|
variant="outlined"
|
||||||
|
id="httpApiPort"
|
||||||
|
name="httpApiPort"
|
||||||
|
label="HTTP API Port"
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.httpApiPort}
|
||||||
|
helperText={errors.httpApiPort?.message && 'A valid port value is required'}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
padding: 3,
|
||||||
|
pt: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
disabled={isSubmitting || disabled}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
data-testid="submit-button"
|
||||||
|
disableElevation
|
||||||
|
onClick={handleSubmit(handleValidateAndGetFee)}
|
||||||
|
endIcon={isSubmitting && <CircularProgress size={20} />}
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
Bond
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</FormControl>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+3
-3
@@ -1,8 +1,8 @@
|
|||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import { SuccessReponse, TransactionDetails } from '../../components';
|
import { SuccessReponse, TransactionDetails } from '../../../components';
|
||||||
import { AppContext } from '../../context/main';
|
import { AppContext } from '../../../context/main';
|
||||||
import { useCheckOwnership } from '../../hooks/useCheckOwnership';
|
import { useCheckOwnership } from '../../../hooks/useCheckOwnership';
|
||||||
|
|
||||||
export const SuccessView: React.FC<{ details?: { amount: string; address: string } }> = ({ details }) => {
|
export const SuccessView: React.FC<{ details?: { amount: string; address: string } }> = ({ details }) => {
|
||||||
const { userBalance, clientDetails } = useContext(AppContext);
|
const { userBalance, clientDetails } = useContext(AppContext);
|
||||||
@@ -1,21 +1,20 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Alert, Box, Button, CircularProgress } from '@mui/material';
|
import { Alert, Box, Button, CircularProgress } from '@mui/material';
|
||||||
import { useSnackbar } from 'notistack';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { BondForm } from './BondForm';
|
|
||||||
import { SuccessView } from './SuccessView';
|
|
||||||
import { NymCard } from '../../components';
|
import { NymCard } from '../../components';
|
||||||
import { EnumRequestStatus, RequestStatus } from '../../components/RequestStatus';
|
import { EnumRequestStatus, RequestStatus } from '../../components/RequestStatus';
|
||||||
import { unbond, vestingUnbond } from '../../requests';
|
|
||||||
import { useCheckOwnership } from '../../hooks/useCheckOwnership';
|
import { useCheckOwnership } from '../../hooks/useCheckOwnership';
|
||||||
import { PageLayout } from '../../layouts';
|
import { PageLayout } from '../../layouts';
|
||||||
|
import { FormHandler } from './components/FormHandler';
|
||||||
|
import { SuccessView } from './components/SuccessView';
|
||||||
|
|
||||||
export const Bond = () => {
|
export const Bond = () => {
|
||||||
const [status, setStatus] = useState(EnumRequestStatus.initial);
|
const [status, setStatus] = useState<EnumRequestStatus>(EnumRequestStatus.initial);
|
||||||
const [error, setError] = useState<string>();
|
const [error, setError] = useState<string>();
|
||||||
const [successDetails, setSuccessDetails] = useState<{ amount: string; address: string }>();
|
const [successDetails, setSuccessDetails] = useState<{ amount: string; address: string }>();
|
||||||
|
|
||||||
const { checkOwnership, ownership, isLoading } = useCheckOwnership();
|
const { checkOwnership, ownership, isLoading } = useCheckOwnership();
|
||||||
const { enqueueSnackbar } = useSnackbar();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === EnumRequestStatus.initial) {
|
if (status === EnumRequestStatus.initial) {
|
||||||
@@ -28,7 +27,7 @@ export const Bond = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<NymCard title="Bond" subheader="Bond a node or gateway" noPadding>
|
<NymCard title="Bond" subheader="Bond a mixnode or gateway" noPadding>
|
||||||
{status === EnumRequestStatus.initial && (
|
{status === EnumRequestStatus.initial && (
|
||||||
<Box sx={{ px: 3, mb: 1 }}>
|
<Box sx={{ px: 3, mb: 1 }}>
|
||||||
<Alert severity="warning">Always ensure you leave yourself enough funds to UNBOND</Alert>
|
<Alert severity="warning">Always ensure you leave yourself enough funds to UNBOND</Alert>
|
||||||
@@ -41,20 +40,7 @@ export const Bond = () => {
|
|||||||
action={
|
action={
|
||||||
<Button
|
<Button
|
||||||
disabled={status === EnumRequestStatus.loading}
|
disabled={status === EnumRequestStatus.loading}
|
||||||
onClick={async () => {
|
onClick={() => navigate('/unbond')}
|
||||||
setStatus(EnumRequestStatus.loading);
|
|
||||||
try {
|
|
||||||
if (ownership.vestingPledge) {
|
|
||||||
await vestingUnbond(ownership.nodeType!);
|
|
||||||
} else {
|
|
||||||
await unbond(ownership.nodeType!);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
enqueueSnackbar(`Failed to unbond ${ownership.nodeType}}`, { variant: 'error' });
|
|
||||||
} finally {
|
|
||||||
setStatus(EnumRequestStatus.initial);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
data-testid="unBond"
|
data-testid="unBond"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
>
|
>
|
||||||
@@ -78,16 +64,15 @@ export const Bond = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{status === EnumRequestStatus.initial && !ownership.hasOwnership && !isLoading && (
|
{status === EnumRequestStatus.initial && !ownership.hasOwnership && !isLoading && (
|
||||||
<BondForm
|
<FormHandler
|
||||||
onError={(e?: string) => {
|
|
||||||
setError(e);
|
|
||||||
setStatus(EnumRequestStatus.error);
|
|
||||||
}}
|
|
||||||
onSuccess={(details) => {
|
onSuccess={(details) => {
|
||||||
setSuccessDetails(details);
|
|
||||||
setStatus(EnumRequestStatus.success);
|
setStatus(EnumRequestStatus.success);
|
||||||
|
setSuccessDetails(details);
|
||||||
|
}}
|
||||||
|
onError={(err) => {
|
||||||
|
setStatus(EnumRequestStatus.error);
|
||||||
|
setError(err);
|
||||||
}}
|
}}
|
||||||
disabled={ownership?.hasOwnership}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(status === EnumRequestStatus.error || status === EnumRequestStatus.success) && (
|
{(status === EnumRequestStatus.error || status === EnumRequestStatus.success) && (
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
validateVersion,
|
validateVersion,
|
||||||
} from '../../utils';
|
} from '../../utils';
|
||||||
|
|
||||||
export const validationSchema = Yup.object().shape({
|
export const mixnodeValidationSchema = Yup.object().shape({
|
||||||
identityKey: Yup.string()
|
identityKey: Yup.string()
|
||||||
.required('An indentity key is required')
|
.required('An indentity key is required')
|
||||||
.test('valid-id-key', 'A valid identity key is required', (value) => validateKey(value || '', 32)),
|
.test('valid-id-key', 'A valid identity key is required', (value) => validateKey(value || '', 32)),
|
||||||
@@ -37,6 +37,54 @@ export const validationSchema = Yup.object().shape({
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
host: Yup.string()
|
||||||
|
.required('A host is required')
|
||||||
|
.test('valid-host', 'A valid host is required', (value) => (value ? isValidHostname(value) : false)),
|
||||||
|
|
||||||
|
version: Yup.string()
|
||||||
|
.required('A version is required')
|
||||||
|
.test('valid-version', 'A valid version is required', (value) => (value ? validateVersion(value) : false)),
|
||||||
|
|
||||||
|
mixPort: Yup.number()
|
||||||
|
.required('A mixport is required')
|
||||||
|
.test('valid-mixport', 'A valid mixport is required', (value) => (value ? validateRawPort(value) : false)),
|
||||||
|
|
||||||
|
verlocPort: Yup.number()
|
||||||
|
.required('A verloc port is required')
|
||||||
|
.test('valid-verloc', 'A valid verloc port is required', (value) => (value ? validateRawPort(value) : false)),
|
||||||
|
|
||||||
|
httpApiPort: Yup.number()
|
||||||
|
.required('A http-api port is required')
|
||||||
|
.test('valid-http', 'A valid http-api port is required', (value) => (value ? validateRawPort(value) : false)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const gatewayValidationSchema = Yup.object().shape({
|
||||||
|
identityKey: Yup.string()
|
||||||
|
.required('An indentity key is required')
|
||||||
|
.test('valid-id-key', 'A valid identity key is required', (value) => validateKey(value || '', 32)),
|
||||||
|
|
||||||
|
sphinxKey: Yup.string()
|
||||||
|
.required('A sphinx key is required')
|
||||||
|
.test('valid-sphinx-key', 'A valid sphinx key is required', (value) => validateKey(value || '', 32)),
|
||||||
|
|
||||||
|
ownerSignature: Yup.string()
|
||||||
|
.required('Signature is required')
|
||||||
|
.test('valid-signature', 'A valid signature is required', (value) => validateKey(value || '', 64)),
|
||||||
|
|
||||||
|
amount: 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;
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
host: Yup.string()
|
host: Yup.string()
|
||||||
.required('A host is required')
|
.required('A host is required')
|
||||||
.test('valid-host', 'A valid host is required', (value) => (value ? isValidHostname(value) : false)),
|
.test('valid-host', 'A valid host is required', (value) => (value ? isValidHostname(value) : false)),
|
||||||
@@ -60,14 +108,6 @@ export const validationSchema = Yup.object().shape({
|
|||||||
.required('A mixport is required')
|
.required('A mixport is required')
|
||||||
.test('valid-mixport', 'A valid mixport is required', (value) => (value ? validateRawPort(value) : false)),
|
.test('valid-mixport', 'A valid mixport is required', (value) => (value ? validateRawPort(value) : false)),
|
||||||
|
|
||||||
verlocPort: Yup.number()
|
|
||||||
.required('A verloc port is required')
|
|
||||||
.test('valid-verloc', 'A valid verloc port is required', (value) => (value ? validateRawPort(value) : false)),
|
|
||||||
|
|
||||||
httpApiPort: Yup.number()
|
|
||||||
.required('A http-api port is required')
|
|
||||||
.test('valid-http', 'A valid http-api port is required', (value) => (value ? validateRawPort(value) : false)),
|
|
||||||
|
|
||||||
clientsPort: Yup.number()
|
clientsPort: Yup.number()
|
||||||
.required('A clients port is required')
|
.required('A clients port is required')
|
||||||
.test('valid-clients', 'A valid clients port is required', (value) => (value ? validateRawPort(value) : false)),
|
.test('valid-clients', 'A valid clients port is required', (value) => (value ? validateRawPort(value) : false)),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useContext, useEffect } from 'react';
|
|||||||
import { Box, Button, CircularProgress, FormControl, Grid, InputAdornment, TextField } from '@mui/material';
|
import { Box, Button, CircularProgress, FormControl, Grid, InputAdornment, TextField } from '@mui/material';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { EnumNodeType, MajorCurrencyAmount, TransactionExecuteResult } from '@nymproject/types';
|
import { MajorCurrencyAmount, TNodeType, TransactionExecuteResult } from '@nymproject/types';
|
||||||
import { TDelegateArgs } from '../../types';
|
import { TDelegateArgs } from '../../types';
|
||||||
import { validationSchema } from './validationSchema';
|
import { validationSchema } from './validationSchema';
|
||||||
import { AppContext } from '../../context/main';
|
import { AppContext } from '../../context/main';
|
||||||
@@ -14,14 +14,14 @@ type TDelegateForm = {
|
|||||||
identity: string;
|
identity: string;
|
||||||
amount: MajorCurrencyAmount;
|
amount: MajorCurrencyAmount;
|
||||||
tokenPool: string;
|
tokenPool: string;
|
||||||
type: EnumNodeType;
|
type: TNodeType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValues: TDelegateForm = {
|
const defaultValues: TDelegateForm = {
|
||||||
identity: '',
|
identity: '',
|
||||||
amount: { amount: '', denom: 'NYM' },
|
amount: { amount: '', denom: 'NYM' },
|
||||||
tokenPool: 'balance',
|
tokenPool: 'balance',
|
||||||
type: EnumNodeType.mixnode,
|
type: 'mixnode',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DelegateForm = ({
|
export const DelegateForm = ({
|
||||||
|
|||||||
@@ -410,7 +410,7 @@ export const Delegation: FC = () => {
|
|||||||
open={Boolean(saturationError)}
|
open={Boolean(saturationError)}
|
||||||
onClose={() => setSaturationError(undefined)}
|
onClose={() => setSaturationError(undefined)}
|
||||||
header={`Node saturation: ${Math.round(saturationError.saturation * 100000) / 1000}%`}
|
header={`Node saturation: ${Math.round(saturationError.saturation * 100000) / 1000}%`}
|
||||||
subHeader={'This node is over saturated. Choose a new mix node to delegate to and start compounding rewards.'}
|
subHeader="This node is over saturated. Choose a new mix node to delegate to and start compounding rewards."
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Typography } from '@mui/material';
|
||||||
|
import { Link } from '@nymproject/react/link/Link';
|
||||||
|
import { SimpleModal } from 'src/components/Modals/SimpleModal';
|
||||||
|
|
||||||
|
export const ConfirmationModal = ({
|
||||||
|
txUrl,
|
||||||
|
message,
|
||||||
|
success,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
txUrl?: string;
|
||||||
|
message?: string;
|
||||||
|
success: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}) => (
|
||||||
|
<SimpleModal
|
||||||
|
open
|
||||||
|
header={success ? 'Successfully Unbonded' : 'Unbonding failed'}
|
||||||
|
displayErrorIcon={!success}
|
||||||
|
onOk={async () => onClose()}
|
||||||
|
okLabel="Done"
|
||||||
|
hideCloseIcon
|
||||||
|
sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1 }}
|
||||||
|
>
|
||||||
|
{message && <Typography variant="caption">Error: {message}</Typography>}
|
||||||
|
{txUrl && <Link href={txUrl} target="_blank" sx={{ ml: 1 }} text="View on blockchain" />}
|
||||||
|
</SimpleModal>
|
||||||
|
);
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, Button } from '@mui/material';
|
||||||
|
import { FeeDetails } from '@nymproject/types';
|
||||||
|
import { useGetFee } from 'src/hooks/useGetFee';
|
||||||
|
import { simulateUnbondGateway, simulateVestingUnbondGateway } from 'src/requests';
|
||||||
|
import { ConfirmTx } from 'src/components/ConfirmTX';
|
||||||
|
|
||||||
|
export const UnbondGateway = ({
|
||||||
|
isWithVestingTokens,
|
||||||
|
onConfirm,
|
||||||
|
onError,
|
||||||
|
}: {
|
||||||
|
isWithVestingTokens: boolean;
|
||||||
|
onConfirm: (isWithVestingTokens: boolean, fee: FeeDetails) => Promise<void>;
|
||||||
|
onError: (err?: string) => void;
|
||||||
|
}) => {
|
||||||
|
const { fee, getFee, resetFeeState } = useGetFee();
|
||||||
|
|
||||||
|
const handleGetFee = async () => {
|
||||||
|
try {
|
||||||
|
if (isWithVestingTokens) await getFee(simulateVestingUnbondGateway, {});
|
||||||
|
if (!isWithVestingTokens) await getFee(simulateUnbondGateway, {});
|
||||||
|
} catch (e) {
|
||||||
|
onError(e as string);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 3, display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
|
{fee && (
|
||||||
|
<ConfirmTx
|
||||||
|
open
|
||||||
|
fee={fee}
|
||||||
|
header="Unbond gateway details"
|
||||||
|
onPrev={resetFeeState}
|
||||||
|
onClose={resetFeeState}
|
||||||
|
onConfirm={async () => {
|
||||||
|
onConfirm(isWithVestingTokens, fee);
|
||||||
|
resetFeeState();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button size="large" variant="contained" disableElevation onClick={handleGetFee}>
|
||||||
|
Unbond gateway
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, Button } from '@mui/material';
|
||||||
|
import { ConfirmTx } from 'src/components/ConfirmTX';
|
||||||
|
import { useGetFee } from 'src/hooks/useGetFee';
|
||||||
|
import { simulateUnbondMixnode, simulateVestingUnbondMixnode } from 'src/requests';
|
||||||
|
import { FeeDetails } from '@nymproject/types';
|
||||||
|
|
||||||
|
export const UnbondMixnode = ({
|
||||||
|
isWithVestingTokens,
|
||||||
|
onConfirm,
|
||||||
|
onError,
|
||||||
|
}: {
|
||||||
|
isWithVestingTokens: boolean;
|
||||||
|
onConfirm: (isWithVestingTokens: boolean, fee: FeeDetails) => Promise<void>;
|
||||||
|
onError: (err?: string) => void;
|
||||||
|
}) => {
|
||||||
|
const { fee, getFee, resetFeeState } = useGetFee();
|
||||||
|
|
||||||
|
const handleGetFee = async () => {
|
||||||
|
try {
|
||||||
|
if (isWithVestingTokens) await getFee(simulateVestingUnbondMixnode, {});
|
||||||
|
if (!isWithVestingTokens) await getFee(simulateUnbondMixnode, {});
|
||||||
|
} catch (e) {
|
||||||
|
onError(e as string);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 3, display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
|
{fee && (
|
||||||
|
<ConfirmTx
|
||||||
|
open
|
||||||
|
fee={fee}
|
||||||
|
header="Unbond mixnode details"
|
||||||
|
onPrev={resetFeeState}
|
||||||
|
onClose={resetFeeState}
|
||||||
|
onConfirm={async () => {
|
||||||
|
onConfirm(isWithVestingTokens, fee);
|
||||||
|
resetFeeState();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button size="large" variant="contained" disableElevation onClick={handleGetFee}>
|
||||||
|
Unbond mixnode
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,87 +1,191 @@
|
|||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { Alert, Box, Button, CircularProgress } from '@mui/material';
|
import { Alert, Button } from '@mui/material';
|
||||||
import { useSnackbar } from 'notistack';
|
import { FeeDetails } from '@nymproject/types';
|
||||||
import { Fee, NymCard } from '../../components';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { LoadingModal } from 'src/components/Modals/LoadingModal';
|
||||||
|
import { AppContext, urls } from 'src/context';
|
||||||
|
import { unbondGateway, unbondMixNode, vestingUnbondGateway, vestingUnbondMixnode } from 'src/requests';
|
||||||
|
import { EnumNodeType } from 'src/types';
|
||||||
|
import { NymCard } from '../../components';
|
||||||
import { useCheckOwnership } from '../../hooks/useCheckOwnership';
|
import { useCheckOwnership } from '../../hooks/useCheckOwnership';
|
||||||
import { AppContext } from '../../context/main';
|
|
||||||
import { unbond, vestingUnbond } from '../../requests';
|
|
||||||
import { PageLayout } from '../../layouts';
|
import { PageLayout } from '../../layouts';
|
||||||
|
import { ConfirmationModal } from './ConfirmationModal';
|
||||||
|
import { UnbondGateway } from './UnbondGateway';
|
||||||
|
import { UnbondMixnode } from './UnbondMixnode';
|
||||||
|
|
||||||
export const Unbond = () => {
|
export const Unbond = () => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const { network } = useContext(AppContext);
|
||||||
const { checkOwnership, ownership } = useCheckOwnership();
|
const { checkOwnership, ownership } = useCheckOwnership();
|
||||||
const { userBalance, getBondDetails } = useContext(AppContext);
|
|
||||||
|
|
||||||
const { enqueueSnackbar } = useSnackbar();
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [confirmationDetails, setConfirmationDetails] = useState<
|
||||||
|
{ success: boolean; txUrl?: string; message?: string } | undefined
|
||||||
|
>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initialiseForm = async () => {
|
const initialiseForm = async () => {
|
||||||
await checkOwnership();
|
await checkOwnership();
|
||||||
};
|
};
|
||||||
initialiseForm();
|
initialiseForm();
|
||||||
}, [ownership.hasOwnership, checkOwnership]);
|
}, [checkOwnership]);
|
||||||
|
|
||||||
|
const handleUnbondMixnode = async (isWithVestingTokens: boolean, fee: FeeDetails) => {
|
||||||
|
let tx;
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
if (isWithVestingTokens) tx = await vestingUnbondMixnode(fee?.fee);
|
||||||
|
if (!isWithVestingTokens) tx = await unbondMixNode(fee?.fee);
|
||||||
|
setConfirmationDetails({
|
||||||
|
success: true,
|
||||||
|
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
setConfirmationDetails({
|
||||||
|
success: false,
|
||||||
|
message: err as string,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await checkOwnership();
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUnbondGateway = async (isWithVestingTokens: boolean, fee: FeeDetails) => {
|
||||||
|
let tx;
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
if (isWithVestingTokens) tx = await vestingUnbondGateway(fee?.fee);
|
||||||
|
if (!isWithVestingTokens) tx = await unbondGateway(fee?.fee);
|
||||||
|
setConfirmationDetails({
|
||||||
|
success: true,
|
||||||
|
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
setConfirmationDetails({
|
||||||
|
success: false,
|
||||||
|
message: err as string,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await checkOwnership();
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<NymCard title="Unbond" subheader="Unbond a mixnode or gateway" noPadding>
|
<NymCard title="Unbond" subheader="Unbond a mixnode or gateway" noPadding>
|
||||||
{ownership?.hasOwnership ? (
|
{!ownership.hasOwnership && (
|
||||||
<>
|
<Alert
|
||||||
<Alert
|
severity="info"
|
||||||
severity="info"
|
sx={{ m: 3 }}
|
||||||
data-testid="bond-noded"
|
data-testid="no-bond"
|
||||||
action={
|
action={
|
||||||
<Button
|
<Button color="inherit" onClick={() => navigate('/bond')}>
|
||||||
data-testid="un-bond"
|
Bond
|
||||||
disabled={isLoading}
|
</Button>
|
||||||
onClick={async () => {
|
}
|
||||||
setIsLoading(true);
|
>
|
||||||
try {
|
You do not currently have a bonded mixnode or gateway
|
||||||
if (ownership.vestingPledge) {
|
|
||||||
await vestingUnbond(ownership.nodeType!);
|
|
||||||
} else {
|
|
||||||
await unbond(ownership.nodeType!);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
enqueueSnackbar(`Failed to unbond ${ownership.nodeType}}`, { variant: 'error' });
|
|
||||||
} finally {
|
|
||||||
await getBondDetails();
|
|
||||||
await checkOwnership();
|
|
||||||
await userBalance.fetchBalance();
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
color="inherit"
|
|
||||||
>
|
|
||||||
Unbond
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
sx={{ m: 2 }}
|
|
||||||
>
|
|
||||||
{`Looks like you already have a ${ownership.nodeType} bonded.`}
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Box sx={{ p: 3 }}>
|
|
||||||
<Fee feeType="UnbondMixnode" />
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Alert severity="info" sx={{ m: 3 }} data-testid="no-bond">
|
|
||||||
You do not currently have a bonded node
|
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
{isLoading && (
|
{ownership.hasOwnership && ownership?.nodeType === EnumNodeType.mixnode && (
|
||||||
<Box
|
<UnbondMixnode
|
||||||
sx={{
|
isWithVestingTokens={!!ownership.vestingPledge}
|
||||||
display: 'flex',
|
onConfirm={handleUnbondMixnode}
|
||||||
justifyContent: 'center',
|
onError={(err) =>
|
||||||
p: 3,
|
setConfirmationDetails({
|
||||||
pt: 0,
|
success: false,
|
||||||
}}
|
message: err as string,
|
||||||
>
|
})
|
||||||
<CircularProgress size={48} />
|
}
|
||||||
</Box>
|
/>
|
||||||
|
)}
|
||||||
|
{ownership.hasOwnership && ownership?.nodeType === EnumNodeType.gateway && (
|
||||||
|
<UnbondGateway
|
||||||
|
isWithVestingTokens={!!ownership.vestingPledge}
|
||||||
|
onConfirm={handleUnbondGateway}
|
||||||
|
onError={(err) =>
|
||||||
|
setConfirmationDetails({
|
||||||
|
success: false,
|
||||||
|
message: err as string,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isLoading && <LoadingModal />}
|
||||||
|
{confirmationDetails && (
|
||||||
|
<ConfirmationModal {...confirmationDetails} onClose={() => setConfirmationDetails(undefined)} />
|
||||||
)}
|
)}
|
||||||
</NymCard>
|
</NymCard>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// {fee && <ConfirmationModal fee={fee} nodeType={ownership.nodeType} onPrev={resetFeeState} onConfirm={async () => {
|
||||||
|
// setIsLoading(true);
|
||||||
|
// try {
|
||||||
|
// if (ownership.vestingPledge) {
|
||||||
|
// await vestingUnbond(ownership.nodeType!);
|
||||||
|
// } else {
|
||||||
|
// await unbond(ownership.nodeType!);
|
||||||
|
// }
|
||||||
|
// } catch (e) {
|
||||||
|
// enqueueSnackbar(`Failed to unbond ${ownership.nodeType}}`, { variant: 'error' });
|
||||||
|
// } finally {
|
||||||
|
// await getBondDetails();
|
||||||
|
// await checkOwnership();
|
||||||
|
// await userBalance.fetchBalance();
|
||||||
|
// setIsLoading(false);
|
||||||
|
// }
|
||||||
|
// }}/>}
|
||||||
|
// <NymCard title="Unbond" subheader="Unbond a mixnode or gateway" noPadding>
|
||||||
|
// {ownership?.hasOwnership ? (
|
||||||
|
// <>
|
||||||
|
// <Alert
|
||||||
|
// severity="info"
|
||||||
|
// data-testid="bond-noded"
|
||||||
|
// action={
|
||||||
|
// <Button
|
||||||
|
// data-testid="un-bond"
|
||||||
|
// disabled={isLoading}
|
||||||
|
// onClick={async () => {
|
||||||
|
// setIsLoading(true)
|
||||||
|
// if (ownership.vestingPledge) {
|
||||||
|
// await getFee(simulateVestingUnbondMixnode);
|
||||||
|
// } else {
|
||||||
|
// await getFee(unbond, {type: ownership.nodeType})
|
||||||
|
// }
|
||||||
|
// }}
|
||||||
|
// color="inherit"
|
||||||
|
// >
|
||||||
|
// Unbond
|
||||||
|
// </Button>
|
||||||
|
// }
|
||||||
|
// sx={{ m: 2 }}
|
||||||
|
// >
|
||||||
|
// {`Looks like you already have a ${ownership.nodeType} bonded.`}
|
||||||
|
// </Alert>
|
||||||
|
|
||||||
|
// <Box sx={{ p: 3 }}>
|
||||||
|
// <Fee feeType="UnbondMixnode" />
|
||||||
|
// </Box>
|
||||||
|
// </>
|
||||||
|
// ) : (
|
||||||
|
// <Alert severity="info" sx={{ m: 3 }} data-testid="no-bond">
|
||||||
|
// You do not currently have a bonded node
|
||||||
|
// </Alert>
|
||||||
|
// )}
|
||||||
|
// {isLoading && (
|
||||||
|
// <Box
|
||||||
|
// sx={{
|
||||||
|
// display: 'flex',
|
||||||
|
// justifyContent: 'center',
|
||||||
|
// p: 3,
|
||||||
|
// pt: 0,
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// <CircularProgress size={48} />
|
||||||
|
// </Box>
|
||||||
|
// )}
|
||||||
|
// </NymCard>
|
||||||
|
|||||||
@@ -13,18 +13,18 @@ import {
|
|||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { EnumNodeType, PendingUndelegate, TDelegation } from '@nymproject/types';
|
import { TNodeType, PendingUndelegate, TDelegation } from '@nymproject/types';
|
||||||
import { validationSchema } from './validationSchema';
|
import { validationSchema } from './validationSchema';
|
||||||
import { undelegateFromMixnode, vestingUndelegateFromMixnode } from '../../requests';
|
import { undelegateFromMixnode, vestingUndelegateFromMixnode } from '../../requests';
|
||||||
import { Fee } from '../../components';
|
import { Fee } from '../../components';
|
||||||
|
|
||||||
type TFormData = {
|
type TFormData = {
|
||||||
nodeType: EnumNodeType;
|
nodeType: TNodeType;
|
||||||
identity: string;
|
identity: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValues = {
|
const defaultValues = {
|
||||||
nodeType: EnumNodeType.mixnode,
|
nodeType: 'Mixnode' as TNodeType,
|
||||||
identity: '',
|
identity: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import { MajorCurrencyAmount, SendTxResult, TransactionExecuteResult } from '@nymproject/types';
|
import { MajorCurrencyAmount, SendTxResult, TransactionExecuteResult } from '@nymproject/types';
|
||||||
import { EnumNodeType, TBondArgs, TBondGatewayArgs, TBondMixNodeArgs } from '../types';
|
import { Fee } from '@nymproject/types/dist/types/rust/Fee';
|
||||||
|
import { EnumNodeType, TBondGatewayArgs, TBondMixNodeArgs } from '../types';
|
||||||
import { invokeWrapper } from './wrapper';
|
import { invokeWrapper } from './wrapper';
|
||||||
|
|
||||||
export const bondGateway = async (args: TBondGatewayArgs) =>
|
export const bondGateway = async (args: TBondGatewayArgs) =>
|
||||||
invokeWrapper<TransactionExecuteResult>('bond_gateway', args);
|
invokeWrapper<TransactionExecuteResult>('bond_gateway', args);
|
||||||
|
|
||||||
export const unbondGateway = async () => invokeWrapper<TransactionExecuteResult>('unbond_gateway');
|
export const unbondGateway = async (fee?: Fee) => invokeWrapper<TransactionExecuteResult>('unbond_gateway', { fee });
|
||||||
|
|
||||||
export const bondMixNode = async (args: TBondMixNodeArgs) =>
|
export const bondMixNode = async (args: TBondMixNodeArgs) =>
|
||||||
invokeWrapper<TransactionExecuteResult>('bond_mixnode', args);
|
invokeWrapper<TransactionExecuteResult>('bond_mixnode', args);
|
||||||
|
|
||||||
export const unbondMixNode = async () => invokeWrapper<TransactionExecuteResult>('unbond_mixnode');
|
export const unbondMixNode = async (fee?: Fee) => invokeWrapper<TransactionExecuteResult>('unbond_mixnode', { fee });
|
||||||
|
|
||||||
export const updateMixnode = async (profitMarginPercent: number) =>
|
export const updateMixnode = async (profitMarginPercent: number) =>
|
||||||
invokeWrapper<TransactionExecuteResult>('update_mixnode', { profitMarginPercent });
|
invokeWrapper<TransactionExecuteResult>('update_mixnode', { profitMarginPercent });
|
||||||
@@ -22,9 +23,3 @@ export const unbond = async (type: EnumNodeType) => {
|
|||||||
if (type === EnumNodeType.mixnode) return unbondMixNode();
|
if (type === EnumNodeType.mixnode) return unbondMixNode();
|
||||||
return unbondGateway();
|
return unbondGateway();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const bond = async (args: TBondArgs) => {
|
|
||||||
const { type, ...other } = args;
|
|
||||||
if (type === EnumNodeType.mixnode) return bondMixNode(other as TBondMixNodeArgs);
|
|
||||||
return bondGateway(other as TBondGatewayArgs);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { FeeDetails, MajorCurrencyAmount } from '@nymproject/types';
|
import { FeeDetails, MajorCurrencyAmount, Gateway, MixNode } from '@nymproject/types';
|
||||||
|
import { TBondGatewayArgs, TBondMixNodeArgs } from 'src/types';
|
||||||
import { invokeWrapper } from './wrapper';
|
import { invokeWrapper } from './wrapper';
|
||||||
|
|
||||||
export const simulateBondGateway = async (args: any) => invokeWrapper<FeeDetails>('simulate_bond_gateway', args);
|
export const simulateBondGateway = async (args: TBondGatewayArgs) =>
|
||||||
|
invokeWrapper<FeeDetails>('simulate_bond_gateway', args);
|
||||||
|
|
||||||
export const simulateUnbondGateway = async (args: any) => invokeWrapper<FeeDetails>('simulate_unbond_gateway', args);
|
export const simulateUnbondGateway = async (args: any) => invokeWrapper<FeeDetails>('simulate_unbond_gateway', args);
|
||||||
|
|
||||||
export const simulateBondMixnode = async (args: any) => invokeWrapper<FeeDetails>('simulate_bond_mixnode', args);
|
export const simulateBondMixnode = async (args: TBondMixNodeArgs) =>
|
||||||
|
invokeWrapper<FeeDetails>('simulate_bond_mixnode', args);
|
||||||
|
|
||||||
export const simulateUnbondMixnode = async (args: any) => invokeWrapper<FeeDetails>('simulate_unbond_mixnode', args);
|
export const simulateUnbondMixnode = async (args: any) => invokeWrapper<FeeDetails>('simulate_unbond_mixnode', args);
|
||||||
|
|
||||||
@@ -32,8 +35,11 @@ export const simulateVestingCompoundDelgatorReward = async (identity: string) =>
|
|||||||
export const simulateVestingUndelegateFromMixnode = async (args: any) =>
|
export const simulateVestingUndelegateFromMixnode = async (args: any) =>
|
||||||
invokeWrapper<FeeDetails>('simulate_vesting_undelegate_from_mixnode', args);
|
invokeWrapper<FeeDetails>('simulate_vesting_undelegate_from_mixnode', args);
|
||||||
|
|
||||||
export const simulateVestingBondGateway = async (args: any) =>
|
export const simulateVestingBondGateway = async (args: {
|
||||||
invokeWrapper<FeeDetails>('simulate_vesting_bond_gateway', args);
|
gateway: Gateway;
|
||||||
|
pledge: MajorCurrencyAmount;
|
||||||
|
ownerSignature: string;
|
||||||
|
}) => invokeWrapper<FeeDetails>('simulate_vesting_bond_gateway', args);
|
||||||
|
|
||||||
export const simulateVestingUnbondGateway = async (args: any) =>
|
export const simulateVestingUnbondGateway = async (args: any) =>
|
||||||
invokeWrapper<FeeDetails>('simulate_vesting_unbond_gateway', args);
|
invokeWrapper<FeeDetails>('simulate_vesting_unbond_gateway', args);
|
||||||
@@ -41,11 +47,13 @@ export const simulateVestingUnbondGateway = async (args: any) =>
|
|||||||
export const simulateVestingDelegateToMixnode = async (args: { identity: string }) =>
|
export const simulateVestingDelegateToMixnode = async (args: { identity: string }) =>
|
||||||
invokeWrapper<FeeDetails>('simulate_vesting_delegate_to_mixnode', args);
|
invokeWrapper<FeeDetails>('simulate_vesting_delegate_to_mixnode', args);
|
||||||
|
|
||||||
export const simulateVestingBondMixnode = async (args: any) =>
|
export const simulateVestingBondMixnode = async (args: {
|
||||||
invokeWrapper<FeeDetails>('simulate_vesting_bond_mixnode', args);
|
mixnode: MixNode;
|
||||||
|
pledge: MajorCurrencyAmount;
|
||||||
|
ownerSignature: string;
|
||||||
|
}) => invokeWrapper<FeeDetails>('simulate_vesting_bond_mixnode', args);
|
||||||
|
|
||||||
export const simulateVestingUnbondMixnode = async (args: any) =>
|
export const simulateVestingUnbondMixnode = async () => invokeWrapper<FeeDetails>('simulate_vesting_unbond_mixnode');
|
||||||
invokeWrapper<FeeDetails>('simulate_vesting_unbond_mixnode', args);
|
|
||||||
|
|
||||||
export const simulateVestingUpdateMixnode = async (args: any) =>
|
export const simulateVestingUpdateMixnode = async (args: any) =>
|
||||||
invokeWrapper<FeeDetails>('simulate_vesting_update_mixnode', args);
|
invokeWrapper<FeeDetails>('simulate_vesting_update_mixnode', args);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
EnumNodeType,
|
TNodeType,
|
||||||
FeeDetails,
|
FeeDetails,
|
||||||
Gateway,
|
Gateway,
|
||||||
MajorCurrencyAmount,
|
MajorCurrencyAmount,
|
||||||
@@ -10,8 +10,8 @@ import {
|
|||||||
TransactionExecuteResult,
|
TransactionExecuteResult,
|
||||||
VestingAccountInfo,
|
VestingAccountInfo,
|
||||||
} from '@nymproject/types';
|
} from '@nymproject/types';
|
||||||
|
import { Fee } from '@nymproject/types/dist/types/rust/Fee';
|
||||||
import { invokeWrapper } from './wrapper';
|
import { invokeWrapper } from './wrapper';
|
||||||
import { TBondGatewayArgs, TBondMixNodeArgs } from '../types';
|
|
||||||
|
|
||||||
export const getLockedCoins = async (): Promise<MajorCurrencyAmount> =>
|
export const getLockedCoins = async (): Promise<MajorCurrencyAmount> =>
|
||||||
invokeWrapper<MajorCurrencyAmount>('locked_coins');
|
invokeWrapper<MajorCurrencyAmount>('locked_coins');
|
||||||
@@ -43,7 +43,8 @@ export const vestingBondGateway = async ({
|
|||||||
ownerSignature: string;
|
ownerSignature: string;
|
||||||
}) => invokeWrapper<TransactionExecuteResult>('vesting_bond_gateway', { gateway, ownerSignature, pledge });
|
}) => invokeWrapper<TransactionExecuteResult>('vesting_bond_gateway', { gateway, ownerSignature, pledge });
|
||||||
|
|
||||||
export const vestingUnbondGateway = async () => invokeWrapper<TransactionExecuteResult>('vesting_unbond_gateway');
|
export const vestingUnbondGateway = async (fee?: Fee) =>
|
||||||
|
invokeWrapper<TransactionExecuteResult>('vesting_unbond_gateway', { fee });
|
||||||
|
|
||||||
export const vestingBondMixNode = async ({
|
export const vestingBondMixNode = async ({
|
||||||
mixnode,
|
mixnode,
|
||||||
@@ -55,7 +56,8 @@ export const vestingBondMixNode = async ({
|
|||||||
ownerSignature: string;
|
ownerSignature: string;
|
||||||
}) => invokeWrapper<TransactionExecuteResult>('vesting_bond_mixnode', { mixnode, ownerSignature, pledge });
|
}) => invokeWrapper<TransactionExecuteResult>('vesting_bond_mixnode', { mixnode, ownerSignature, pledge });
|
||||||
|
|
||||||
export const vestingUnbondMixnode = async () => invokeWrapper<TransactionExecuteResult>('vesting_unbond_mixnode');
|
export const vestingUnbondMixnode = async (fee?: Fee) =>
|
||||||
|
invokeWrapper<TransactionExecuteResult>('vesting_unbond_mixnode', { fee });
|
||||||
|
|
||||||
export const withdrawVestedCoins = async (amount: MajorCurrencyAmount) =>
|
export const withdrawVestedCoins = async (amount: MajorCurrencyAmount) =>
|
||||||
invokeWrapper<TransactionExecuteResult>('withdraw_vested_coins', { amount });
|
invokeWrapper<TransactionExecuteResult>('withdraw_vested_coins', { amount });
|
||||||
@@ -84,7 +86,7 @@ export const getVestingPledgeInfo = async ({
|
|||||||
type,
|
type,
|
||||||
}: {
|
}: {
|
||||||
address?: string;
|
address?: string;
|
||||||
type: EnumNodeType;
|
type: TNodeType;
|
||||||
}): Promise<PledgeData | undefined> => {
|
}): Promise<PledgeData | undefined> => {
|
||||||
try {
|
try {
|
||||||
return await invokeWrapper<PledgeData>(`vesting_get_${type}_pledge`, { address });
|
return await invokeWrapper<PledgeData>(`vesting_get_${type}_pledge`, { address });
|
||||||
@@ -96,17 +98,11 @@ export const getVestingPledgeInfo = async ({
|
|||||||
export const vestingDelegatedFree = async (vestingAccountAddress: string) =>
|
export const vestingDelegatedFree = async (vestingAccountAddress: string) =>
|
||||||
invokeWrapper<MajorCurrencyAmount>('delegated_free', { vestingAccountAddress });
|
invokeWrapper<MajorCurrencyAmount>('delegated_free', { vestingAccountAddress });
|
||||||
|
|
||||||
export const vestingUnbond = async (type: EnumNodeType) => {
|
export const vestingUnbond = async (type: TNodeType) => {
|
||||||
if (type === EnumNodeType.mixnode) return vestingUnbondMixnode();
|
if (type === 'mixnode') return vestingUnbondMixnode();
|
||||||
return vestingUnbondGateway();
|
return vestingUnbondGateway();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const vestingBond = async (args: { type: EnumNodeType } & (TBondMixNodeArgs | TBondGatewayArgs)) => {
|
|
||||||
const { type, ...other } = args;
|
|
||||||
if (type === EnumNodeType.mixnode) return vestingBondMixNode(other as TBondMixNodeArgs);
|
|
||||||
return vestingBondGateway(other as TBondGatewayArgs);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const vestingClaimOperatorRewards = async () =>
|
export const vestingClaimOperatorRewards = async () =>
|
||||||
invokeWrapper<TransactionExecuteResult>('vesting_claim_operator_reward');
|
invokeWrapper<TransactionExecuteResult>('vesting_claim_operator_reward');
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Gateway, MajorCurrencyAmount, MixNode, PledgeData } from '@nymproject/types';
|
import { Gateway, MajorCurrencyAmount, MixNode, PledgeData } from '@nymproject/types';
|
||||||
|
import { Fee } from '@nymproject/types/dist/types/rust/Fee';
|
||||||
|
|
||||||
export enum EnumNodeType {
|
export enum EnumNodeType {
|
||||||
mixnode = 'mixnode',
|
mixnode = 'mixnode',
|
||||||
@@ -28,16 +29,16 @@ export type TBondGatewayArgs = {
|
|||||||
gateway: Gateway;
|
gateway: Gateway;
|
||||||
pledge: MajorCurrencyAmount;
|
pledge: MajorCurrencyAmount;
|
||||||
ownerSignature: string;
|
ownerSignature: string;
|
||||||
|
fee?: Fee;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TBondMixNodeArgs = {
|
export type TBondMixNodeArgs = {
|
||||||
mixnode: MixNode;
|
mixnode: MixNode;
|
||||||
pledge: MajorCurrencyAmount;
|
pledge: MajorCurrencyAmount;
|
||||||
ownerSignature: string;
|
ownerSignature: string;
|
||||||
|
fee?: Fee;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TBondArgs = { type: EnumNodeType } & (TBondGatewayArgs | TBondMixNodeArgs);
|
|
||||||
|
|
||||||
export type TDelegateArgs = {
|
export type TDelegateArgs = {
|
||||||
identity: string;
|
identity: string;
|
||||||
amount: MajorCurrencyAmount;
|
amount: MajorCurrencyAmount;
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import { MixNode, MajorCurrencyAmount } from './rust';
|
import { MixNode, MajorCurrencyAmount } from './rust';
|
||||||
|
|
||||||
export enum EnumNodeType {
|
export type TNodeType = 'mixnode' | 'gateway';
|
||||||
mixnode = 'mixnode',
|
|
||||||
gateway = 'gateway',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TNodeOwnership = {
|
export type TNodeOwnership = {
|
||||||
hasOwnership: boolean;
|
hasOwnership: boolean;
|
||||||
nodeType?: EnumNodeType;
|
nodeType?: TNodeType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDelegation = {
|
export type TDelegation = {
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
import type { CosmosFee } from "./CosmosFee";
|
import type { CosmosFee } from './CosmosFee';
|
||||||
|
|
||||||
export type Fee = { Manual: CosmosFee } | { Auto: number | null };
|
export type Fee = { Manual: CosmosFee } | { Auto: number | null };
|
||||||
|
|||||||
Reference in New Issue
Block a user