Compare commits

...

13 Commits

Author SHA1 Message Date
fmtabbara d811446b6a use back button component 2022-07-05 16:03:15 +01:00
fmtabbara 65e252416c Merge branch 'develop' into feature/wallet-bonding-with-fees 2022-07-05 16:00:15 +01:00
fmtabbara bfd86d1462 fix type errors 2022-07-04 13:46:00 +01:00
fmtabbara f4215d192f lint fixes 2022-07-04 12:27:42 +01:00
fmtabbara 2524051da9 update tauri functions 2022-07-04 12:01:02 +01:00
fmtabbara 57245aaffe centralize functions 2022-07-04 11:59:46 +01:00
fmtabbara 9398a055a2 refresh state 2022-07-04 11:59:20 +01:00
fmtabbara c8bf0fb853 use confirmation modal for successful/failed unbond 2022-07-04 11:58:58 +01:00
fmtabbara 91b9e2a1a7 update to bonding confirmation modal 2022-07-04 11:58:23 +01:00
fmtabbara 426c9ae026 centralize functions for unbonding gateways and mixnodes 2022-07-04 11:57:28 +01:00
fmtabbara 65e0b69a9d rebuild unbond page to allow logic split between gateways and mixnodes 2022-07-04 11:56:57 +01:00
fmtabbara b9001458de remove dummy data 2022-06-29 15:40:24 +01:00
fmtabbara 3a8214cdb7 split out bond mixnode and gateway forms. add fees for bonding to mixnode or gateway. some small UI additions 2022-06-29 15:15:49 +01:00
26 changed files with 960 additions and 338 deletions
@@ -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;
+1 -5
View File
@@ -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>
);
};
@@ -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>
</>
);
};
@@ -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);
+13 -28
View File
@@ -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) && (
+49 -9
View File
@@ -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 = ({
+1 -1
View File
@@ -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>
);
};
+167 -63
View File
@@ -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: '',
}; };
+4 -9
View File
@@ -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);
};
+17 -9
View File
@@ -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);
+9 -13
View File
@@ -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');
+3 -2
View File
@@ -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;
+2 -5
View File
@@ -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 = {
+2 -2
View File
@@ -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 };