Wallet: Avoid oversaturated node delegation or compound (#1356)

* wip checking ts-package

* adding the validation to the identity form field

* changing the error message

* changing node oversaturated error mss

* adding oversaturated modal blocker

* adding error colour to palette, and styles to basic modal

* wip

* wip

* some refactor

* dont validate field till we have api response

* fix typo

* adding line break

* catch error when node is not valid

* handle error out of the field component

* removing logs

* Adding disableCompoundRewards prop in DelegationsActionsMenu

* Adding disableCompoundRewards to DelegationsActionsMenu

* refactor validation

* Revert some not needed changes

* adding line break

* adding stories

Co-authored-by: fmtabbara <fmtabbara@hotmail.co.uk>
This commit is contained in:
Gala
2022-06-15 17:51:57 +02:00
committed by GitHub
parent 937ae22e6b
commit 1ad458b2be
7 changed files with 245 additions and 95 deletions
@@ -0,0 +1,31 @@
import React from 'react';
import { SimpleModal } from '../Modals/SimpleModal';
export const OverSaturatedBlockerModal: React.FC<{
open: boolean;
onClose?: () => void;
identityKey?: string;
header?: string;
subHeader?: string;
buttonText?: string;
}> = ({ open, onClose, header, subHeader, buttonText }) => (
<SimpleModal
open={open}
hideCloseIcon={true}
displayErrorIcon
onClose={onClose}
onOk={onClose}
header={header || 'Delegate'}
subHeader={subHeader || 'This node is over saturated, you cant compound rewards to it'}
okLabel={buttonText || 'Close'}
sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}
headerStyles={{
width: '100%',
mb: 3,
textAlign: 'center',
color: 'error.main',
fontSize: 16,
}}
subHeaderStyles={{ textAlign: 'center', color: 'text.primary', fontSize: 14, fontWeight: 400 }}
/>
);
@@ -1,13 +1,14 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { Box, Stack, Typography } from '@mui/material';
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
import { CurrencyDenom, MajorCurrencyAmount } from '@nymproject/types';
import { getGasFee } from 'src/requests';
import { Console } from 'src/utils/console';
import { SimpleModal } from '../Modals/SimpleModal';
import { ModalListItem } from './ModalListItem';
import { validateKey } from '../../utils';
import { TokenPoolSelector, TPoolOption } from '../TokenPoolSelector';
import { getMixnodeStakeSaturation } from '../../requests';
const MIN_AMOUNT_TO_DELEGATE = 10;
@@ -50,10 +51,12 @@ export const DelegateModal: React.FC<{
}) => {
const [identityKey, setIdentityKey] = useState<string | undefined>(initialIdentityKey);
const [amount, setAmount] = useState<string | undefined>(initialAmount);
const [isValidated, setValidated] = useState<boolean>(false);
const [errorAmount, setErrorAmount] = useState<string | undefined>();
const [tokenPool, setTokenPool] = useState<TPoolOption>('balance');
const [fee, setFee] = useState<string>();
const [tokenPool, setTokenPool] = useState<TPoolOption>('balance');
const [isValidated, setValidated] = useState<boolean>(false);
const [errorAmount, setErrorAmount] = useState<string>();
const [errorNodeSaturation, setErrorNodeSaturation] = useState<string>();
const [errorIdentityKey, setErrorIdentityKey] = useState<string>();
const getFee = async () => {
if (feeOverride) setFee(feeOverride);
@@ -63,18 +66,41 @@ export const DelegateModal: React.FC<{
}
};
const validate = () => {
let newValidatedValue = true;
if (!identityKey || !validateKey(identityKey, 32)) {
newValidatedValue = false;
const handleCheckStakeSaturation = async (identity: string) => {
setErrorNodeSaturation(undefined);
try {
const newSaturation = await getMixnodeStakeSaturation(identity);
if (newSaturation && newSaturation.saturation > 1) {
const saturationPercentage = Math.round(newSaturation.saturation * 100);
setErrorNodeSaturation(`This node is over saturated (${saturationPercentage}%), please select another node`);
}
} catch (e) {
Console.error('Error fetching the saturation, error:', e);
setErrorNodeSaturation(undefined);
}
if (amount && Number(amount) < MIN_AMOUNT_TO_DELEGATE) {
setErrorAmount(`Min. delegation amount: ${MIN_AMOUNT_TO_DELEGATE} ${currency}`);
newValidatedValue = false;
};
const validateIdentityKey = async (isValid: boolean) => {
if (!isValid) {
setErrorIdentityKey('Identity key is invalid');
setErrorNodeSaturation(undefined);
} else {
setErrorAmount(undefined);
setErrorIdentityKey(undefined);
await handleCheckStakeSaturation(identityKey!);
}
};
const validateAmount = (newValue: MajorCurrencyAmount) => {
setErrorAmount(undefined);
if (newValue.amount && Number(newValue.amount) < MIN_AMOUNT_TO_DELEGATE) {
setErrorAmount(`Min. delegation amount: ${MIN_AMOUNT_TO_DELEGATE} ${currency}`);
}
if (!newValue.amount) {
setErrorAmount('Amount required');
}
setValidated(newValidatedValue);
};
const handleOk = () => {
@@ -83,8 +109,9 @@ export const DelegateModal: React.FC<{
}
};
const handleIdentityKeyChanged = (newIdentityKey: string) => {
const handleIdentityKeyChanged = async (newIdentityKey: string) => {
setIdentityKey(newIdentityKey);
if (onIdentityKeyChanged) {
onIdentityKeyChanged(newIdentityKey);
}
@@ -92,19 +119,22 @@ export const DelegateModal: React.FC<{
const handleAmountChanged = (newAmount: MajorCurrencyAmount) => {
setAmount(newAmount.amount);
validateAmount(newAmount);
if (onAmountChanged) {
onAmountChanged(newAmount.amount);
}
};
React.useEffect(() => {
validate();
}, [amount, identityKey]);
React.useEffect(() => {
useEffect(() => {
getFee();
}, []);
useEffect(() => {
if (!!errorIdentityKey || !!errorAmount || errorNodeSaturation) setValidated(false);
else setValidated(true);
}, [errorIdentityKey, errorAmount, errorNodeSaturation]);
return (
<SimpleModal
open={open}
@@ -120,12 +150,21 @@ export const DelegateModal: React.FC<{
fullWidth
placeholder="Node identity key"
onChanged={handleIdentityKeyChanged}
onValidate={validateIdentityKey}
initialValue={initialIdentityKey}
readOnly={Boolean(initialIdentityKey)}
textFieldProps={{
autoFocus: !initialIdentityKey,
}}
/>
<Typography
component="div"
textAlign="left"
variant="caption"
sx={{ color: 'error.main', mx: '14px', mt: '3px' }}
>
{errorNodeSaturation}
</Typography>
<Box display="flex" gap={2} alignItems="center" sx={{ mt: 2 }}>
{hasVestingContract && <TokenPoolSelector disabled={false} onSelect={(pool) => setTokenPool(pool)} />}
<CurrencyFormField
@@ -137,7 +176,12 @@ export const DelegateModal: React.FC<{
onChanged={handleAmountChanged}
/>
</Box>
<Typography component="div" textAlign="right" variant="caption" sx={{ color: 'error.main' }}>
<Typography
component="div"
textAlign="left"
variant="caption"
sx={{ color: 'error.main', mx: '14px', mt: '3px' }}
>
{errorAmount}
</Typography>
<Stack direction="row" justifyContent="space-between" my={3}>
@@ -99,7 +99,8 @@ export const DelegationsActionsMenu: React.FC<{
isPending?: DelegationEventKind;
disableRedeemingRewards?: boolean;
disableDelegateMore?: boolean;
}> = ({ disableRedeemingRewards, disableDelegateMore, onActionClick, isPending }) => {
disableCompoundRewards?: boolean;
}> = ({ disableRedeemingRewards, disableDelegateMore, disableCompoundRewards, onActionClick, isPending }) => {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
@@ -135,7 +136,7 @@ export const DelegationsActionsMenu: React.FC<{
title="Delegate more"
Icon={<Delegate />}
onClick={() => handleActionSelect?.('delegate')}
disabled={disableDelegateMore}
disabled={disableDelegateMore || disableCompoundRewards}
/>
<DelegationActionsMenuItem
title="Undelegate"
@@ -231,7 +231,8 @@ export const DelegationList: React.FC<{
isPending={undefined}
onActionClick={(action) => (onItemActionClick ? onItemActionClick(item, action) : undefined)}
disableRedeemingRewards={!item.accumulated_rewards || item.accumulated_rewards.amount === '0'}
disableDelegateMore={(item?.stake_saturation || 0) > 100}
disableDelegateMore={(item?.stake_saturation || 0) > 1}
disableCompoundRewards={(item?.stake_saturation || 0) > 1}
/>
) : (
<Tooltip
@@ -10,40 +10,51 @@ export default {
component: SimpleModal,
} as ComponentMeta<typeof SimpleModal>;
const BasePage: React.FC<{ children: React.ReactElement<any, any>; handleClick: () => void }> = ({
children,
handleClick,
}) => (
<>
<Paper elevation={0} sx={{ px: 4, pt: 2, pb: 4 }}>
<h2>Lorem ipsum</h2>
<Button variant="contained" onClick={handleClick}>
Show modal
</Button>
<p>
Veniam dolor laborum labore sit reprehenderit enim mollit magna nulla adipisicing fugiat. Est ex irure quis sunt
velit elit do minim mollit non duis reprehenderit. Eiusmod dolore adipisicing ex nostrud consectetur culpa
exercitation do. Ad elit esse ipsum aliqua labore irure laborum qui culpa.
</p>
<p>
Occaecat commodo excepteur anim ut officia dolor laboris dolore id occaecat enim qui eiusmod occaecat aliquip ad
tempor. Labore amet laborum magna amet consequat dolor cupidatat in consequat sunt aliquip magna laboris tempor
culpa est magna. Sit tempor cillum culpa sint ipsum nostrud ullamco voluptate exercitation dolore magna elit ut
mollit.
</p>
<p>
Labore voluptate elit amet ipsum qui officia duis in et occaecat culpa ex do non labore mollit. Cillum cupidatat
duis ea dolore laboris laboris sunt duis anim consectetur cupidatat nulla ad minim sunt ea. Aliqua amet commodo
est irure sint magna sunt. Pariatur dolore commodo labore quis incididunt proident duis voluptate exercitation
in duis. Occaecat aliqua laboris reprehenderit nostrud est aute pariatur fugiat anim. Dolore sunt cillum ea
aliquip consectetur laborum ipsum qui veniam Lorem consectetur adipisicing velit magna aute. Amet tempor quis
excepteur minim culpa velit Lorem enim ad.
</p>
<p>
Mollit laborum exercitation excepteur laboris adipisicing ipsum veniam cillum mollit voluptate do. Amet et anim
Lorem mollit minim duis cupidatat non. Consectetur sit deserunt nisi nisi non excepteur dolor eiusmod aute aute
irure anim dolore ipsum et veniam.
</p>
</Paper>
{children}
</>
);
export const Default = () => {
const [open, setOpen] = React.useState<boolean>(true);
const handleClick = () => setOpen(true);
return (
<>
<Paper elevation={0} sx={{ px: 4, pt: 2, pb: 4 }}>
<h2>Lorem ipsum</h2>
<Button variant="contained" onClick={() => setOpen(true)}>
Show modal
</Button>
<p>
Veniam dolor laborum labore sit reprehenderit enim mollit magna nulla adipisicing fugiat. Est ex irure quis
sunt velit elit do minim mollit non duis reprehenderit. Eiusmod dolore adipisicing ex nostrud consectetur
culpa exercitation do. Ad elit esse ipsum aliqua labore irure laborum qui culpa.
</p>
<p>
Occaecat commodo excepteur anim ut officia dolor laboris dolore id occaecat enim qui eiusmod occaecat aliquip
ad tempor. Labore amet laborum magna amet consequat dolor cupidatat in consequat sunt aliquip magna laboris
tempor culpa est magna. Sit tempor cillum culpa sint ipsum nostrud ullamco voluptate exercitation dolore magna
elit ut mollit.
</p>
<p>
Labore voluptate elit amet ipsum qui officia duis in et occaecat culpa ex do non labore mollit. Cillum
cupidatat duis ea dolore laboris laboris sunt duis anim consectetur cupidatat nulla ad minim sunt ea. Aliqua
amet commodo est irure sint magna sunt. Pariatur dolore commodo labore quis incididunt proident duis voluptate
exercitation in duis. Occaecat aliqua laboris reprehenderit nostrud est aute pariatur fugiat anim. Dolore sunt
cillum ea aliquip consectetur laborum ipsum qui veniam Lorem consectetur adipisicing velit magna aute. Amet
tempor quis excepteur minim culpa velit Lorem enim ad.
</p>
<p>
Mollit laborum exercitation excepteur laboris adipisicing ipsum veniam cillum mollit voluptate do. Amet et
anim Lorem mollit minim duis cupidatat non. Consectetur sit deserunt nisi nisi non excepteur dolor eiusmod
aute aute irure anim dolore ipsum et veniam.
</p>
</Paper>
<BasePage handleClick={handleClick}>
<SimpleModal
open={open}
onClose={() => setOpen(false)}
@@ -63,44 +74,16 @@ export const Default = () => {
elit ut mollit.
</p>
</SimpleModal>
</>
</BasePage>
);
};
export const NoSubheader = () => {
const [open, setOpen] = React.useState<boolean>(true);
const handleClick = () => setOpen(true);
return (
<>
<Paper elevation={0} sx={{ px: 4, pt: 2, pb: 4 }}>
<h2>Lorem ipsum</h2>
<Button variant="contained" onClick={() => setOpen(true)}>
Show modal
</Button>
<p>
Veniam dolor laborum labore sit reprehenderit enim mollit magna nulla adipisicing fugiat. Est ex irure quis
sunt velit elit do minim mollit non duis reprehenderit. Eiusmod dolore adipisicing ex nostrud consectetur
culpa exercitation do. Ad elit esse ipsum aliqua labore irure laborum qui culpa.
</p>
<p>
Occaecat commodo excepteur anim ut officia dolor laboris dolore id occaecat enim qui eiusmod occaecat aliquip
ad tempor. Labore amet laborum magna amet consequat dolor cupidatat in consequat sunt aliquip magna laboris
tempor culpa est magna. Sit tempor cillum culpa sint ipsum nostrud ullamco voluptate exercitation dolore magna
elit ut mollit.
</p>
<p>
Labore voluptate elit amet ipsum qui officia duis in et occaecat culpa ex do non labore mollit. Cillum
cupidatat duis ea dolore laboris laboris sunt duis anim consectetur cupidatat nulla ad minim sunt ea. Aliqua
amet commodo est irure sint magna sunt. Pariatur dolore commodo labore quis incididunt proident duis voluptate
exercitation in duis. Occaecat aliqua laboris reprehenderit nostrud est aute pariatur fugiat anim. Dolore sunt
cillum ea aliquip consectetur laborum ipsum qui veniam Lorem consectetur adipisicing velit magna aute. Amet
tempor quis excepteur minim culpa velit Lorem enim ad.
</p>
<p>
Mollit laborum exercitation excepteur laboris adipisicing ipsum veniam cillum mollit voluptate do. Amet et
anim Lorem mollit minim duis cupidatat non. Consectetur sit deserunt nisi nisi non excepteur dolor eiusmod
aute aute irure anim dolore ipsum et veniam.
</p>
</Paper>
<BasePage handleClick={handleClick}>
<SimpleModal
open={open}
onClose={() => setOpen(false)}
@@ -117,6 +100,69 @@ export const NoSubheader = () => {
Veniam dolor laborum labore sit reprehenderit enim mollit magna nulla adipisicing fugiat. Est ex irure quis.
</p>
</SimpleModal>
</>
</BasePage>
);
};
export const hideCloseIcon = () => {
const [open, setOpen] = React.useState<boolean>(true);
const handleClick = () => setOpen(true);
return (
<BasePage handleClick={handleClick}>
<SimpleModal
open={open}
hideCloseIcon={true}
onClose={() => setOpen(false)}
onOk={() => setOpen(false)}
header="This is a modal"
okLabel="Kaplow!"
>
<p>
Tempor culpa est magna. Sit tempor cillum culpa sint ipsum nostrud ullamco voluptate exercitation dolore magna
elit ut mollit.
</p>
<ModalDivider />
<p>
Veniam dolor laborum labore sit reprehenderit enim mollit magna nulla adipisicing fugiat. Est ex irure quis.
</p>
</SimpleModal>
</BasePage>
);
};
export const hideCloseIconAndDisplayErrorIcon = () => {
const [open, setOpen] = React.useState<boolean>(true);
const handleClick = () => setOpen(true);
return (
<BasePage handleClick={handleClick}>
<SimpleModal
open={open}
hideCloseIcon={true}
displayErrorIcon={true}
onClose={() => setOpen(false)}
onOk={() => setOpen(false)}
header="This modal announces an error !"
okLabel="Kaplow!"
sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}
headerStyles={{
width: '100%',
mb: 3,
textAlign: 'center',
color: 'error.main',
}}
subHeaderStyles={{ textAlign: 'center', color: 'text.primary', fontSize: 14, fontWeight: 400 }}
>
<p>
Tempor culpa est magna. Sit tempor cillum culpa sint ipsum nostrud ullamco voluptate exercitation dolore magna
elit ut mollit.
</p>
<ModalDivider />
<p>
Veniam dolor laborum labore sit reprehenderit enim mollit magna nulla adipisicing fugiat. Est ex irure quis.
</p>
</SimpleModal>
</BasePage>
);
};
@@ -1,10 +1,15 @@
import React from 'react';
import { Box, Button, Modal, Stack, SxProps, Typography } from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import ErrorOutline from '@mui/icons-material/ErrorOutline';
import { modalStyle } from './styles';
export const SimpleModal: React.FC<{
open: boolean;
hideCloseIcon?: boolean;
displayErrorIcon?: boolean;
headerStyles?: SxProps;
subHeaderStyles?: SxProps;
onClose?: () => void;
onOk?: () => void;
header: string;
@@ -12,17 +17,38 @@ export const SimpleModal: React.FC<{
okLabel: string;
okDisabled?: boolean;
sx?: SxProps;
}> = ({ open, onClose, okDisabled, onOk, header, subHeader, okLabel, sx, children }) => (
}> = ({
open,
hideCloseIcon,
displayErrorIcon,
headerStyles,
subHeaderStyles,
onClose,
okDisabled,
onOk,
header,
subHeader,
okLabel,
sx,
children,
}) => (
<Modal open={open} onClose={onClose}>
<Box sx={{ ...modalStyle, ...sx }}>
{displayErrorIcon && <ErrorOutline color="error" sx={{ mb: 3 }} />}
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography fontSize={22} fontWeight={600}>
<Typography fontSize={22} fontWeight={600} sx={{ ...headerStyles }}>
{header}
</Typography>
<CloseIcon onClick={onClose} cursor="pointer" />
{!hideCloseIcon && <CloseIcon onClick={onClose} cursor="pointer" />}
</Stack>
{subHeader && (
<Typography mt={0.5} mb={3} fontSize="small" color={(theme) => theme.palette.text.secondary}>
<Typography
mt={0.5}
mb={3}
fontSize="small"
color={(theme) => theme.palette.text.secondary}
sx={{ ...subHeaderStyles }}
>
{subHeader}
</Typography>
)}
@@ -62,9 +62,10 @@ export const IdentityKeyFormField: React.FC<{
if (doValidation(newValue)) {
setValue(newValue);
if (onChanged) {
onChanged(newValue);
}
}
if (onChanged) {
onChanged(newValue);
}
};