Wallet: Transfer token component with fee included (#1385)

* set up transfer token component with fee included

* open transfer modal after balance refresh

* create fee warning component

* use confirmation modal
This commit is contained in:
Fouad
2022-07-06 16:19:15 +01:00
committed by GitHub
parent 2f4e505aac
commit a11e674967
11 changed files with 210 additions and 80 deletions
+16
View File
@@ -0,0 +1,16 @@
import React from 'react';
import { Warning } from '@mui/icons-material';
import { FeeDetails } from '@nymproject/types';
import { Alert, AlertTitle } from '@mui/material';
export const FeeWarning = ({ fee, amount }: { fee: FeeDetails; amount: number }) => {
if (fee.amount && +fee.amount.amount > amount) {
return (
<Alert color="warning" sx={{ mt: 3 }} icon={<Warning />}>
<AlertTitle>Warning: fees are greater than the reward</AlertTitle>
The fees for redeeming rewards will cost more than the rewards. Are you sure you want to continue?
</Alert>
);
}
return null;
};
@@ -1,13 +1,12 @@
import React, { useEffect } from 'react';
import { Alert, AlertTitle, Stack, Typography } from '@mui/material';
import WarningIcon from '@mui/icons-material/Warning';
import { Stack, Typography } from '@mui/material';
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
import { simulateCompoundDelgatorReward, simulateVestingCompoundDelgatorReward } from 'src/requests';
import { isGreaterThan } from 'src/utils';
import { useGetFee } from 'src/hooks/useGetFee';
import { SimpleModal } from '../Modals/SimpleModal';
import { ModalFee } from '../Modals/ModalFee';
import { FeeDetails } from '@nymproject/types';
import { FeeWarning } from '../FeeWarning';
export const CompoundModal: React.FC<{
open: boolean;
@@ -57,13 +56,16 @@ export const CompoundModal: React.FC<{
Rewards will be transferred to account you are logged in with now
</Typography>
<Stack direction="row" justifyContent="space-between">
<Typography fontSize="smaller" color={(theme) => theme.palette.nym.fee}>
Est. fee for this transaction:
</Typography>
<Typography fontSize="smaller" color={(theme) => theme.palette.nym.fee}>
{fee} {currency}
</Typography>
</Stack>
{fee && <FeeWarning amount={amount} fee={fee} />}
<ModalFee fee={fee} isLoading={isFeeLoading} error={feeError} />
{fee?.amount && isGreaterThan(+fee.amount.amount, amount) && (
<Alert color="warning" sx={{ mt: 3 }} icon={<WarningIcon />}>
<AlertTitle>Warning: fees are greater than the reward</AlertTitle>
The fees for redeeming rewards will cost more than the rewards. Are you sure you want to continue?
</Alert>
)}
</SimpleModal>
);
};
@@ -1,13 +1,12 @@
import React, { useEffect } from 'react';
import { Alert, AlertTitle, Stack, Typography } from '@mui/material';
import { Stack, Typography } from '@mui/material';
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
import WarningIcon from '@mui/icons-material/Warning';
import { simulateClaimDelgatorReward, simulateVestingClaimDelgatorReward } from 'src/requests';
import { isGreaterThan } from 'src/utils';
import { useGetFee } from 'src/hooks/useGetFee';
import { SimpleModal } from '../Modals/SimpleModal';
import { ModalFee } from '../Modals/ModalFee';
import { FeeDetails } from '@nymproject/types';
import { useGetFee } from 'src/hooks/useGetFee';
import { simulateClaimDelgatorReward, simulateVestingClaimDelgatorReward } from 'src/requests';
import { ModalFee } from '../Modals/ModalFee';
import { SimpleModal } from '../Modals/SimpleModal';
import { FeeWarning } from '../FeeWarning';
export const RedeemModal: React.FC<{
open: boolean;
@@ -58,13 +57,16 @@ export const RedeemModal: React.FC<{
Rewards will be transferred to account you are logged in with now
</Typography>
<Stack direction="row" justifyContent="space-between">
<Typography fontSize="smaller" color={(theme) => theme.palette.nym.fee}>
Est. fee for this transaction:
</Typography>
<Typography fontSize="smaller" color={(theme) => theme.palette.nym.fee}>
{fee} {currency}
</Typography>
</Stack>
{fee && <FeeWarning amount={amount} fee={fee} />}
<ModalFee fee={fee} isLoading={isFeeLoading} error={feeError} />
{fee?.amount && isGreaterThan(+fee.amount.amount, amount) && (
<Alert color="warning" sx={{ mt: 3 }} icon={<WarningIcon />}>
<AlertTitle>Warning: fees are greater than the reward</AlertTitle>
The fees for redeeming rewards will cost more than the rewards. Are you sure you want to continue?
</Alert>
)}
</SimpleModal>
);
};
+1
View File
@@ -32,6 +32,7 @@ export const MockMainContextProvider: FC<{}> = ({ children }) => {
clearBalance: () => undefined,
fetchBalance: async () => undefined,
fetchTokenAllocation: async () => undefined,
refreshBalances: async () => {},
},
showAdmin: false,
showTerminal: false,
+5 -3
View File
@@ -35,6 +35,7 @@ export type TUseuserBalance = {
fetchTokenAllocation: () => Promise<void>;
clearBalance: () => void;
clearAll: () => void;
refreshBalances: () => Promise<void>;
};
export const useGetBalance = (clientDetails?: Account): TUseuserBalance => {
@@ -108,8 +109,8 @@ export const useGetBalance = (clientDetails?: Account): TUseuserBalance => {
clearOriginalVesting();
};
const handleRefresh = async (addr?: string) => {
if (addr) {
const refreshBalances = async () => {
if (clientDetails?.client_address) {
await fetchBalance();
await fetchTokenAllocation();
} else {
@@ -118,7 +119,7 @@ export const useGetBalance = (clientDetails?: Account): TUseuserBalance => {
};
useEffect(() => {
handleRefresh(clientDetails?.client_address);
refreshBalances();
}, [clientDetails]);
return {
@@ -133,5 +134,6 @@ export const useGetBalance = (clientDetails?: Account): TUseuserBalance => {
clearBalance,
clearAll,
fetchTokenAllocation,
refreshBalances,
};
};
@@ -0,0 +1,101 @@
import React, { useContext, useEffect, useState } from 'react';
import { Alert, Box, CircularProgress } from '@mui/material';
import { FeeDetails } from '@nymproject/types';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { ModalListItem } from 'src/components/Modals/ModalListItem';
import { AppContext, urls } from 'src/context';
import { FeeWarning } from 'src/components/FeeWarning';
import { withdrawVestedCoins } from 'src/requests';
import { Console } from 'src/utils/console';
import { simulateWithdrawVestedCoins } from 'src/requests/simulate';
import { SuccessModal } from './TransferModalSuccess';
import { TResponseState, TTransactionDetails } from '../types';
export const TransferModal = ({ onClose }: { onClose: () => void }) => {
const [state, setState] = useState<TResponseState>();
const [fee, setFee] = useState<FeeDetails>();
const [tx, setTx] = useState<TTransactionDetails>();
const { userBalance, clientDetails, network } = useContext(AppContext);
const getFee = async () => {
if (userBalance.tokenAllocation?.spendable && clientDetails?.denom) {
try {
const simulatedFee = await simulateWithdrawVestedCoins({
amount: { amount: userBalance.tokenAllocation?.spendable, denom: clientDetails?.denom },
});
setFee(simulatedFee);
} catch (e) {
setFee({ amount: { amount: 'n/a', denom: clientDetails.denom }, fee: { Auto: null } });
Console.error(e);
}
}
};
useEffect(() => {
getFee();
}, []);
const handleTransfer = async () => {
if (userBalance.tokenAllocation?.spendable && clientDetails?.denom) {
setState('loading');
try {
const txResponse = await withdrawVestedCoins({
amount: userBalance.tokenAllocation?.spendable,
denom: clientDetails.denom,
});
setState('success');
setTx({
amount: `${userBalance.tokenAllocation?.spendable} ${clientDetails?.denom}`,
url: `${urls(network).blockExplorer}/transaction/${txResponse.transaction_hash}`,
});
await userBalance.refreshBalances();
} catch (e) {
Console.error(e as string);
setState('fail');
}
}
};
if (state === 'success') {
return <SuccessModal onClose={onClose} tx={tx} />;
}
return (
<SimpleModal
open
okLabel={state === 'loading' ? 'Transferring..' : 'Transfer'}
header="Transfer locked tokens"
subHeader="Transfer locked tokens to balance"
sx={{ width: 600 }}
onOk={handleTransfer}
okDisabled={state === 'loading' || !fee || userBalance.tokenAllocation?.spendable === '0'}
onClose={onClose}
>
<Box sx={{ mt: 3 }}>
{state === 'loading' ? (
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<CircularProgress />
</Box>
) : (
<>
<ModalListItem
label="Unlocked transferrable tokens"
value={`${userBalance.tokenAllocation?.spendable} ${clientDetails?.denom}`}
divider
/>
<ModalListItem
label="Est. fee for this transaction"
value={fee ? `${fee.amount?.amount} ${fee.amount?.denom}` : <CircularProgress size={15} />}
divider
/>
{userBalance.tokenAllocation?.spendable && fee && (
<FeeWarning fee={fee} amount={+userBalance.tokenAllocation.spendable} />
)}
</>
)}
</Box>
{state === 'fail' && <Alert severity="error">Transfer failed please try again in a few minutes</Alert>}
</SimpleModal>
);
};
@@ -0,0 +1,26 @@
import React from 'react';
import { Stack, Typography } from '@mui/material';
import { Link } from '@nymproject/react/link/Link';
import { ConfirmationModal } from 'src/components';
import { TTransactionDetails } from '../types';
export const SuccessModal = ({ tx, onClose }: { tx?: TTransactionDetails; onClose: () => void }) => (
<ConfirmationModal
open
title={<Typography>Transfer to balance successful</Typography>}
onClose={onClose}
onConfirm={onClose}
maxWidth="xs"
fullWidth
confirmButton="Done"
>
<Stack alignItems="center" spacing={2}>
{tx && (
<>
<Typography variant="h5">{tx.amount}</Typography>
<Link href={tx.url} target="_blank" sx={{ ml: 1 }} text="View on blockchain" />
</>
)}
</Stack>
</ConfirmationModal>
);
+24 -10
View File
@@ -1,15 +1,29 @@
import React from 'react';
import React, { useContext, useState } from 'react';
import { Box } from '@mui/material';
import { AppContext } from '../../context/main';
import { BalanceCard } from './balance';
import { VestingCard } from './vesting';
import { PageLayout } from '../../layouts';
import { TransferModal } from './components/TransferModal';
export const Balance = () => (
<PageLayout>
<Box display="flex" flexDirection="column" gap={2}>
<BalanceCard />
<VestingCard />
</Box>
</PageLayout>
);
export const Balance = () => {
const [showTransferModal, setShowTransferModal] = useState(false);
const { userBalance } = useContext(AppContext);
const handleShowTransferModal = async () => {
await userBalance.refreshBalances();
setShowTransferModal(true);
};
return (
<PageLayout>
<Box display="flex" flexDirection="column" gap={2}>
<BalanceCard />
<VestingCard onTransfer={handleShowTransferModal} />
{showTransferModal && <TransferModal onClose={() => setShowTransferModal(false)} />}
</Box>
</PageLayout>
);
};
+2
View File
@@ -0,0 +1,2 @@
export type TResponseState = 'loading' | 'success' | 'fail';
export type TTransactionDetails = { amount: string; url: string };
+7 -43
View File
@@ -2,7 +2,6 @@ import React, { useCallback, useContext, useEffect, useState } from 'react';
import {
Box,
Button,
CircularProgress,
Grid,
IconButton,
Table,
@@ -15,12 +14,10 @@ import {
} from '@mui/material';
import { InfoOutlined, Refresh } from '@mui/icons-material';
import { useSnackbar } from 'notistack';
import { Fee, InfoTooltip, NymCard, Title } from '../../components';
import { AppContext } from '../../context/main';
import { withdrawVestedCoins } from '../../requests';
import { Period } from '../../types';
import { InfoTooltip, NymCard, Title } from 'src/components';
import { AppContext } from 'src/context/main';
import { Period } from 'src/types';
import { VestingTimeline } from './components/vesting-timeline';
import { Console } from '../../utils/console';
const columnsHeaders: Array<{ title: string; align: TableCellProps['align'] }> = [
{ title: 'Locked', align: 'left' },
@@ -120,10 +117,8 @@ const TokenTransfer = () => {
);
};
export const VestingCard = () => {
const [isLoading, setIsLoading] = useState(false);
const { userBalance, clientDetails } = useContext(AppContext);
export const VestingCard = ({ onTransfer }: { onTransfer: () => Promise<void> }) => {
const { userBalance } = useContext(AppContext);
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
const refreshBalances = async () => {
@@ -156,39 +151,8 @@ export const VestingCard = () => {
>
<VestingSchedule />
<TokenTransfer />
<Box display="flex" justifyContent="space-between" alignItems="center">
{userBalance.tokenAllocation?.spendable !== '0' ? <Fee feeType="Send" /> : <div />}
<Button
size="large"
variant="contained"
onClick={async () => {
if (userBalance.tokenAllocation?.spendable && clientDetails?.denom) {
setIsLoading(true);
try {
await withdrawVestedCoins({
amount: userBalance.tokenAllocation?.spendable,
denom: clientDetails.denom,
});
await refreshBalances();
enqueueSnackbar('Token transfer succeeded', {
variant: 'success',
preventDuplicate: true,
});
} catch (e) {
Console.error(e as string);
enqueueSnackbar('Token transfer failed. You may not have any transferable tokens at this time', {
variant: 'error',
preventDuplicate: true,
});
} finally {
setIsLoading(false);
}
}
}}
endIcon={isLoading && <CircularProgress size={16} color="inherit" />}
disabled={isLoading || userBalance.tokenAllocation?.spendable === '0'}
disableElevation
>
<Box display="flex" justifyContent="end" alignItems="center">
<Button size="large" variant="contained" onClick={onTransfer} disableElevation>
Transfer
</Button>
</Box>
+2 -2
View File
@@ -50,5 +50,5 @@ export const simulateVestingUnbondMixnode = async (args: any) =>
export const simulateVestingUpdateMixnode = async (args: any) =>
invokeWrapper<FeeDetails>('simulate_vesting_update_mixnode', args);
export const simulateWithdrawVestedCoins = async (args: any) =>
invokeWrapper<FeeDetails>('simulate_withdraw_vested_coins', args);
export const simulateWithdrawVestedCoins = async ({ amount }: { amount: MajorCurrencyAmount }) =>
invokeWrapper<FeeDetails>('simulate_withdraw_vested_coins', { amount });