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:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -32,6 +32,7 @@ export const MockMainContextProvider: FC<{}> = ({ children }) => {
|
||||
clearBalance: () => undefined,
|
||||
fetchBalance: async () => undefined,
|
||||
fetchTokenAllocation: async () => undefined,
|
||||
refreshBalances: async () => {},
|
||||
},
|
||||
showAdmin: false,
|
||||
showTerminal: false,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export type TResponseState = 'loading' | 'success' | 'fail';
|
||||
export type TTransactionDetails = { amount: string; url: string };
|
||||
@@ -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>
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user