Feature/send with simulated fee (#1402)
* add simulate send function * extract loading modal into its own component * move appbar component into folder * add additional prop for making modal list item text bold * create new send UI and stories * remove old send page * use simulated fee in send request * use new confirmation modal component
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
module.exports = {
|
||||
invoke: (operation, args) => {
|
||||
switch(operation) {
|
||||
switch (operation) {
|
||||
case 'get_balance': {
|
||||
return {
|
||||
amount: {
|
||||
@@ -12,20 +12,30 @@ module.exports = {
|
||||
denom: 'NYMT',
|
||||
},
|
||||
printable_balance: '100 NYMT',
|
||||
}
|
||||
};
|
||||
}
|
||||
case 'delegate_to_mixnode': {
|
||||
return ({
|
||||
return {
|
||||
logs_json: '[]',
|
||||
data_json: '{}',
|
||||
transaction_hash: '12345',
|
||||
});
|
||||
};
|
||||
}
|
||||
case 'simulate_send': {
|
||||
return {
|
||||
amount: {
|
||||
amount: '0.01',
|
||||
denom: 'NYM',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
console.error(`Tauri cannot be used in Storybook. The operation requested was "${operation}". You can add mock responses to "nym_wallet/.storybook/mocks/tauri.js" if you need. The default response is "void".`);
|
||||
console.error(
|
||||
`Tauri cannot be used in Storybook. The operation requested was "${operation}". You can add mock responses to "nym_wallet/.storybook/mocks/tauri.js" if you need. The default response is "void".`,
|
||||
);
|
||||
return new Promise((resolve, reject) => {
|
||||
reject(new Error(`Tauri operation ${operation} not available in storybook.`));
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
+11
-19
@@ -1,17 +1,16 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { AppBar as MuiAppBar, Grid, IconButton, Toolbar } from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Logout } from '@mui/icons-material';
|
||||
import TerminalIcon from '@mui/icons-material/Terminal';
|
||||
import { AppContext } from '../context/main';
|
||||
import { NetworkSelector } from './NetworkSelector';
|
||||
import { Node as NodeIcon } from '../svg-icons/node';
|
||||
import { MultiAccounts } from './Accounts';
|
||||
import { config } from '../config';
|
||||
import { AppBar as MuiAppBar, Grid, IconButton, Toolbar } from '@mui/material';
|
||||
import { Node } from 'src/svg-icons/node';
|
||||
import { config } from '../../config';
|
||||
import { AppContext } from '../../context/main';
|
||||
import { MultiAccounts } from '../Accounts';
|
||||
import { NetworkSelector } from '../NetworkSelector';
|
||||
|
||||
export const AppBar = () => {
|
||||
const { logOut, handleShowTerminal, appEnv, handleShowSettings, showSettings } = useContext(AppContext);
|
||||
const navigate = useNavigate();
|
||||
const { showSettings, handleShowTerminal, appEnv, handleShowSettings, logOut } = useContext(AppContext);
|
||||
|
||||
return (
|
||||
<MuiAppBar position="sticky" sx={{ boxShadow: 'none', bgcolor: 'transparent' }}>
|
||||
<Toolbar disableGutters>
|
||||
@@ -24,7 +23,7 @@ export const AppBar = () => {
|
||||
<NetworkSelector />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item container justifyContent="flex-end" md={12} lg={5} spacing={2}>
|
||||
<Grid item container justifyContent="flex-end" alignItems="center" md={12} lg={5} spacing={2}>
|
||||
{(appEnv?.SHOW_TERMINAL || config.IS_DEV_MODE) && (
|
||||
<Grid item>
|
||||
<IconButton size="small" onClick={handleShowTerminal} sx={{ color: 'nym.background.dark' }}>
|
||||
@@ -38,18 +37,11 @@ export const AppBar = () => {
|
||||
sx={{ color: showSettings ? 'primary.main' : 'nym.background.dark' }}
|
||||
size="small"
|
||||
>
|
||||
<NodeIcon fontSize="small" />
|
||||
<Node fontSize="small" />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={async () => {
|
||||
await logOut();
|
||||
navigate('/');
|
||||
}}
|
||||
sx={{ color: 'nym.background.dark' }}
|
||||
>
|
||||
<IconButton size="small" onClick={logOut} sx={{ color: 'nym.background.dark' }}>
|
||||
<Logout fontSize="small" />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
@@ -0,0 +1 @@
|
||||
export * from './AppBar';
|
||||
@@ -98,9 +98,8 @@ export const DelegationsActionsMenu: React.FC<{
|
||||
onActionClick?: (action: DelegationListItemActions) => void;
|
||||
isPending?: DelegationEventKind;
|
||||
disableRedeemingRewards?: boolean;
|
||||
disableDelegateMore?: boolean;
|
||||
disableCompoundRewards?: boolean;
|
||||
}> = ({ disableRedeemingRewards, disableDelegateMore, disableCompoundRewards, onActionClick, isPending }) => {
|
||||
}> = ({ disableRedeemingRewards, disableCompoundRewards, onActionClick, isPending }) => {
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Box, Button, CircularProgress, Modal, Stack, Typography } from '@mui/material';
|
||||
import { Box, Button, Modal, Typography } from '@mui/material';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { modalStyle } from '../Modals/styles';
|
||||
import { LoadingModal } from '../Modals/LoadingModal';
|
||||
|
||||
export type ActionType = 'delegate' | 'undelegate' | 'redeem' | 'redeem-all' | 'compound';
|
||||
|
||||
@@ -42,18 +43,7 @@ export const DelegationModal: React.FC<
|
||||
onClose?: () => void;
|
||||
}
|
||||
> = ({ status, action, message, recipient, balance, balanceVested, transactions, open, onClose, children }) => {
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
<Modal open>
|
||||
<Box sx={modalStyle} textAlign="center">
|
||||
<Stack spacing={4} direction="row" alignItems="center">
|
||||
<CircularProgress />
|
||||
<Typography>Please wait...</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
if (status === 'loading') return <LoadingModal />;
|
||||
|
||||
if (status === 'error') {
|
||||
return (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
@@ -6,12 +6,17 @@ export const ModalListItem: React.FC<{
|
||||
label: string;
|
||||
divider?: boolean;
|
||||
hidden?: boolean;
|
||||
value: string | React.ReactNode;
|
||||
}> = ({ label, value, hidden, divider }) => (
|
||||
strong?: boolean;
|
||||
value: React.ReactNode;
|
||||
}> = ({ label, value, hidden, divider, strong }) => (
|
||||
<Box sx={{ display: hidden ? 'none' : 'block' }}>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography fontSize="smaller">{label}:</Typography>
|
||||
<Typography fontSize="smaller">{value}</Typography>
|
||||
<Typography fontSize="smaller" fontWeight={strong ? 600 : undefined}>
|
||||
{label}:
|
||||
</Typography>
|
||||
<Typography fontSize="smaller" fontWeight={strong ? 600 : undefined}>
|
||||
{value}
|
||||
</Typography>
|
||||
</Stack>
|
||||
{divider && <ModalDivider />}
|
||||
</Box>
|
||||
|
||||
@@ -1,58 +1,67 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import React, { useState, useContext } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { List, ListItem, ListItemIcon, ListItemText } from '@mui/material';
|
||||
import { AccountBalanceWalletOutlined, ArrowBack, ArrowForward, Description, Settings } from '@mui/icons-material';
|
||||
import { AppContext } from '../context/main';
|
||||
import { Bond, Delegate, Unbond } from '../svg-icons';
|
||||
|
||||
const routesSchema = [
|
||||
{
|
||||
label: 'Balance',
|
||||
route: '/balance',
|
||||
Icon: AccountBalanceWalletOutlined,
|
||||
},
|
||||
{
|
||||
label: 'Send',
|
||||
route: '/send',
|
||||
Icon: ArrowForward,
|
||||
},
|
||||
{
|
||||
label: 'Receive',
|
||||
route: '/receive',
|
||||
Icon: ArrowBack,
|
||||
},
|
||||
{
|
||||
label: 'Bond',
|
||||
route: '/bond',
|
||||
Icon: Bond,
|
||||
},
|
||||
{
|
||||
label: 'Unbond',
|
||||
route: '/unbond',
|
||||
Icon: Unbond,
|
||||
},
|
||||
{
|
||||
label: 'Delegation',
|
||||
route: '/delegation',
|
||||
Icon: Delegate,
|
||||
},
|
||||
{
|
||||
label: 'Docs',
|
||||
route: '/docs',
|
||||
Icon: Description,
|
||||
mode: 'dev',
|
||||
},
|
||||
{
|
||||
label: 'Admin',
|
||||
route: '/admin',
|
||||
Icon: Settings,
|
||||
mode: 'admin',
|
||||
},
|
||||
];
|
||||
|
||||
export const Nav = () => {
|
||||
const location = useLocation();
|
||||
const { isAdminAddress } = useContext(AppContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { isAdminAddress, handleShowSendModal } = useContext(AppContext);
|
||||
|
||||
const [routesSchema] = useState([
|
||||
{
|
||||
label: 'Balance',
|
||||
route: '/balance',
|
||||
Icon: AccountBalanceWalletOutlined,
|
||||
onClick: () => navigate('/balance'),
|
||||
},
|
||||
{
|
||||
label: 'Send',
|
||||
Icon: ArrowForward,
|
||||
onClick: handleShowSendModal,
|
||||
},
|
||||
{
|
||||
label: 'Receive',
|
||||
route: '/receive',
|
||||
Icon: ArrowBack,
|
||||
onClick: () => navigate('/receive'),
|
||||
},
|
||||
{
|
||||
label: 'Bond',
|
||||
route: '/bond',
|
||||
Icon: Bond,
|
||||
onClick: () => navigate('/bond'),
|
||||
},
|
||||
{
|
||||
label: 'Unbond',
|
||||
route: '/unbond',
|
||||
Icon: Unbond,
|
||||
onClick: () => navigate('/unbond'),
|
||||
},
|
||||
{
|
||||
label: 'Delegation',
|
||||
route: '/delegation',
|
||||
Icon: Delegate,
|
||||
onClick: () => navigate('/delegation'),
|
||||
},
|
||||
{
|
||||
label: 'Docs',
|
||||
route: '/admin',
|
||||
Icon: Description,
|
||||
mode: 'dev',
|
||||
onClick: () => navigate('/docs'),
|
||||
},
|
||||
{
|
||||
label: 'Admin',
|
||||
route: '/admin',
|
||||
Icon: Settings,
|
||||
mode: 'admin',
|
||||
onClick: () => navigate('/admin'),
|
||||
},
|
||||
]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -77,8 +86,8 @@ export const Nav = () => {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.map(({ Icon, route, label }) => (
|
||||
<ListItem disableGutters component={Link} to={route} key={label}>
|
||||
.map(({ Icon, onClick, label, route }) => (
|
||||
<ListItem disableGutters key={label} onClick={onClick} sx={{ cursor: 'pointer' }}>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
minWidth: 30,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Stack, Typography } from '@mui/material';
|
||||
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
|
||||
import { FeeDetails } from '@nymproject/types';
|
||||
import { simulateCompoundDelgatorReward, simulateVestingCompoundDelgatorReward } from 'src/requests';
|
||||
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<{
|
||||
@@ -14,7 +14,6 @@ export const CompoundModal: React.FC<{
|
||||
onOk?: (identityKey: string, fee?: FeeDetails) => void;
|
||||
identityKey: string;
|
||||
amount: number;
|
||||
minimum?: number;
|
||||
currency: string;
|
||||
message: string;
|
||||
usesVestingTokens: boolean;
|
||||
|
||||
@@ -14,7 +14,6 @@ export const RedeemModal: React.FC<{
|
||||
onOk?: (identityKey: string, fee?: FeeDetails) => void;
|
||||
identityKey: string;
|
||||
amount: number;
|
||||
minimum?: number;
|
||||
currency: string;
|
||||
message: string;
|
||||
usesVestingTokens: boolean;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { ComponentMeta } from '@storybook/react';
|
||||
import { MockMainContextProvider } from 'src/context/mocks/main';
|
||||
import { SendDetailsModal } from './SendDetailsModal';
|
||||
import { SendSuccessModal } from './SendSuccessModal';
|
||||
import { SendErrorModal } from './SendErrorModal';
|
||||
import { SendInputModal } from './SendInputModal';
|
||||
import { Send } from '.';
|
||||
|
||||
export default {
|
||||
title: 'Send/Components',
|
||||
component: SendDetailsModal,
|
||||
} as ComponentMeta<typeof SendDetailsModal>;
|
||||
|
||||
export const SendInput = () => (
|
||||
<SendInputModal
|
||||
toAddress=""
|
||||
fromAddress="nymt1w8qp7zsxggvtxhpqpt6e329j42wtv07dm5ts8u"
|
||||
onNext={() => {}}
|
||||
onClose={() => {}}
|
||||
onAddressChange={() => {}}
|
||||
onAmountChange={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const SendDetails = () => (
|
||||
<SendDetailsModal
|
||||
fromAddress="nymt1w8qp7zsxggvtxhpqpt6e329j42wtv07dm5ts8u"
|
||||
toAddress="nymt1w8qp7zsxggvtxhpqpt6e329j42wtv07dm5ts8u"
|
||||
fee={{ amount: { amount: '0.01', denom: 'NYM' }, fee: { Auto: null } }}
|
||||
amount={{ amount: '100', denom: 'NYM' }}
|
||||
onPrev={() => {}}
|
||||
onSend={() => {}}
|
||||
onClose={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const SendSuccess = () => (
|
||||
<SendSuccessModal txDetails={{ amount: '100 NYM', txUrl: 'dummtUrl.com' }} onClose={() => {}} />
|
||||
);
|
||||
|
||||
export const SendError = () => <SendErrorModal onClose={() => {}} />;
|
||||
|
||||
export const SendFlow = () => (
|
||||
<MockMainContextProvider>
|
||||
<Send />
|
||||
</MockMainContextProvider>
|
||||
);
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { Stack } from '@mui/material';
|
||||
import { FeeDetails, MajorCurrencyAmount } from '@nymproject/types';
|
||||
import { SimpleModal } from '../Modals/SimpleModal';
|
||||
import { ModalListItem } from '../Modals/ModalListItem';
|
||||
|
||||
export const SendDetailsModal = ({
|
||||
amount,
|
||||
toAddress,
|
||||
fromAddress,
|
||||
fee,
|
||||
onClose,
|
||||
onPrev,
|
||||
onSend,
|
||||
}: {
|
||||
fromAddress?: string;
|
||||
toAddress: string;
|
||||
fee?: FeeDetails;
|
||||
amount?: MajorCurrencyAmount;
|
||||
onClose: () => void;
|
||||
onPrev: () => void;
|
||||
onSend: (data: { val: MajorCurrencyAmount; to: string }) => void;
|
||||
}) => (
|
||||
<SimpleModal
|
||||
header="Send details"
|
||||
open
|
||||
onClose={onClose}
|
||||
okLabel="Confirm"
|
||||
onOk={async () => amount && onSend({ val: amount, to: toAddress })}
|
||||
onBack={onPrev}
|
||||
>
|
||||
<Stack gap={0.5} sx={{ mt: 4 }}>
|
||||
<ModalListItem label="From" value={fromAddress} divider />
|
||||
<ModalListItem label="To" value={toAddress} divider />
|
||||
<ModalListItem label="Amount" value={`${amount?.amount} ${amount?.denom}`} divider />
|
||||
<ModalListItem
|
||||
label="Fee for this transaction"
|
||||
value={!fee ? 'n/a' : `${fee.amount?.amount} ${fee.amount?.denom}`}
|
||||
divider
|
||||
/>
|
||||
</Stack>
|
||||
</SimpleModal>
|
||||
);
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { SimpleModal } from '../Modals/SimpleModal';
|
||||
|
||||
export const SendErrorModal = ({ onClose }: { onClose: () => void }) => (
|
||||
<SimpleModal
|
||||
open
|
||||
hideCloseIcon
|
||||
displayErrorIcon
|
||||
onOk={async () => onClose()}
|
||||
header="Send"
|
||||
subHeader="An error occurred while sending. Please try again"
|
||||
okLabel="Close"
|
||||
sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}
|
||||
headerStyles={{
|
||||
width: '100%',
|
||||
mb: 3,
|
||||
textAlign: 'center',
|
||||
color: 'error.main',
|
||||
fontSize: 16,
|
||||
textTransform: 'capitalize',
|
||||
}}
|
||||
subHeaderStyles={{ textAlign: 'center', color: 'text.primary', fontSize: 14, fontWeight: 400 }}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,70 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Stack, TextField, Typography } from '@mui/material';
|
||||
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
|
||||
import { MajorCurrencyAmount } from '@nymproject/types';
|
||||
import { validateAmount } from 'src/utils';
|
||||
import { SimpleModal } from '../Modals/SimpleModal';
|
||||
import { ModalListItem } from '../Modals/ModalListItem';
|
||||
|
||||
export const SendInputModal = ({
|
||||
fromAddress,
|
||||
toAddress,
|
||||
amount,
|
||||
balance,
|
||||
error,
|
||||
onNext,
|
||||
onClose,
|
||||
onAmountChange,
|
||||
onAddressChange,
|
||||
}: {
|
||||
fromAddress?: string;
|
||||
toAddress: string;
|
||||
amount?: MajorCurrencyAmount;
|
||||
balance?: string;
|
||||
error?: string;
|
||||
onNext: () => void;
|
||||
onClose: () => void;
|
||||
onAmountChange: (value: MajorCurrencyAmount) => void;
|
||||
onAddressChange: (value: string) => void;
|
||||
}) => {
|
||||
const [isValid, setIsValid] = useState(false);
|
||||
|
||||
const validate = async (value: MajorCurrencyAmount) => {
|
||||
const isValidAmount = await validateAmount(value.amount, '0');
|
||||
setIsValid(isValidAmount);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (amount) validate(amount);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SimpleModal header="Send" open onClose={onClose} okLabel="Next" onOk={async () => onNext()} okDisabled={!isValid}>
|
||||
<Stack gap={2} sx={{ mt: 2 }}>
|
||||
<TextField
|
||||
placeholder="Recipient address"
|
||||
fullWidth
|
||||
onChange={(e) => onAddressChange(e.target.value)}
|
||||
value={toAddress}
|
||||
/>
|
||||
<CurrencyFormField
|
||||
placeholder="Amount"
|
||||
fullWidth
|
||||
onChanged={(value) => {
|
||||
onAmountChange(value);
|
||||
validate(value);
|
||||
}}
|
||||
initialValue={amount?.amount}
|
||||
/>
|
||||
<Typography fontSize="smaller" sx={{ color: 'error.main' }}>
|
||||
{error}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack gap={0.5} sx={{ mt: 2 }}>
|
||||
<ModalListItem label="Account balance" value={balance} divider strong />
|
||||
<ModalListItem label="Your address" value={fromAddress} divider />
|
||||
<Typography fontSize="smaller">Est. fee for this transaction will be show on the next page</Typography>
|
||||
</Stack>
|
||||
</SimpleModal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { MajorCurrencyAmount } from '@nymproject/types';
|
||||
import { AppContext, urls } from 'src/context';
|
||||
import { useGetFee } from 'src/hooks/useGetFee';
|
||||
import { send } from 'src/requests';
|
||||
import { Console } from 'src/utils/console';
|
||||
import { simulateSend } from 'src/requests/simulate';
|
||||
import { LoadingModal } from '../Modals/LoadingModal';
|
||||
import { SendDetailsModal } from './SendDetailsModal';
|
||||
import { SendErrorModal } from './SendErrorModal';
|
||||
import { SendInputModal } from './SendInputModal';
|
||||
import { SendSuccessModal } from './SendSuccessModal';
|
||||
import { TTransactionDetails } from './types';
|
||||
|
||||
export const SendModal = ({ onClose }: { onClose: () => void }) => {
|
||||
const [toAddress, setToAddress] = useState<string>('');
|
||||
const [amount, setAmount] = useState<MajorCurrencyAmount>();
|
||||
const [modal, setModal] = useState<'send' | 'send details'>('send');
|
||||
const [error, setError] = useState<string>();
|
||||
const [sendError, setSendError] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [txDetails, setTxDetails] = useState<TTransactionDetails>();
|
||||
|
||||
const { clientDetails, userBalance, network } = useContext(AppContext);
|
||||
const { fee, getFee } = useGetFee();
|
||||
|
||||
const handleOnNext = async () => {
|
||||
if (amount) {
|
||||
setIsLoading(true);
|
||||
setError(undefined);
|
||||
try {
|
||||
await getFee(simulateSend, { address: toAddress, amount });
|
||||
setModal('send details');
|
||||
} catch (e) {
|
||||
setError(e as string);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
} else {
|
||||
setError('An amount is required');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSend = async ({ val, to }: { val: MajorCurrencyAmount; to: string }) => {
|
||||
setIsLoading(true);
|
||||
setError(undefined);
|
||||
try {
|
||||
const txResponse = await send({ amount: val, address: to, memo: '', fee: fee?.fee });
|
||||
setTxDetails({
|
||||
amount: `${amount?.amount} ${clientDetails?.denom}`,
|
||||
txUrl: `${urls(network).blockExplorer}/transaction/${txResponse.tx_hash}`,
|
||||
});
|
||||
} catch (e) {
|
||||
Console.error(e as string);
|
||||
setSendError(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) return <LoadingModal />;
|
||||
|
||||
if (sendError) return <SendErrorModal onClose={onClose} />;
|
||||
|
||||
if (txDetails) return <SendSuccessModal txDetails={txDetails} onClose={onClose} />;
|
||||
|
||||
if (modal === 'send details')
|
||||
return (
|
||||
<SendDetailsModal
|
||||
fromAddress={clientDetails?.client_address}
|
||||
toAddress={toAddress}
|
||||
amount={amount}
|
||||
fee={fee}
|
||||
onClose={onClose}
|
||||
onPrev={() => setModal('send')}
|
||||
onSend={handleSend}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<SendInputModal
|
||||
fromAddress={clientDetails?.client_address}
|
||||
toAddress={toAddress}
|
||||
amount={amount}
|
||||
balance={userBalance.balance?.printable_balance}
|
||||
onClose={onClose}
|
||||
onNext={handleOnNext}
|
||||
error={error}
|
||||
onAmountChange={(value) => setAmount(value)}
|
||||
onAddressChange={(value) => setToAddress(value)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Stack, Typography } from '@mui/material';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { TTransactionDetails } from './types';
|
||||
import { ConfirmationModal } from '../Modals/ConfirmationModal';
|
||||
|
||||
export const SendSuccessModal = ({ txDetails, onClose }: { txDetails: TTransactionDetails; onClose: () => void }) => (
|
||||
<ConfirmationModal open onConfirm={onClose} onClose={onClose} title="" confirmButton="Done" maxWidth="xs" fullWidth>
|
||||
<Stack alignItems="center" spacing={2}>
|
||||
<Typography>You sent</Typography>
|
||||
{txDetails && (
|
||||
<>
|
||||
<Typography variant="h5">{txDetails.amount}</Typography>
|
||||
<Link href={txDetails.txUrl} target="_blank" sx={{ ml: 1 }} text="View on blockchain" />
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</ConfirmationModal>
|
||||
);
|
||||
@@ -0,0 +1,11 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { AppContext } from 'src/context';
|
||||
import { SendModal } from './SendModal';
|
||||
|
||||
export const Send = () => {
|
||||
const { showSendModal, handleShowSendModal } = useContext(AppContext);
|
||||
|
||||
if (showSendModal) return <SendModal onClose={handleShowSendModal} />;
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export type TTransactionDetails = {
|
||||
amount: string;
|
||||
txUrl: string;
|
||||
};
|
||||
@@ -64,7 +64,7 @@ export const DelegationContextProvider: FC<{
|
||||
try {
|
||||
let tx;
|
||||
|
||||
if (tokenPool === 'locked') tx = await vestingDelegateToMixnode(data);
|
||||
if (tokenPool === 'locked') tx = await vestingDelegateToMixnode({ ...data, fee });
|
||||
else tx = await delegateToMixnode(data);
|
||||
|
||||
return tx;
|
||||
|
||||
@@ -46,7 +46,9 @@ export type TAppContext = {
|
||||
error?: string;
|
||||
loginType?: TLoginType;
|
||||
showSettings: boolean;
|
||||
handleShowSettings?: () => void;
|
||||
showSendModal: boolean;
|
||||
handleShowSettings: () => void;
|
||||
handleShowSendModal: () => void;
|
||||
setIsLoading: (isLoading: boolean) => void;
|
||||
setError: (value?: string) => void;
|
||||
switchNetwork: (network: Network) => void;
|
||||
@@ -76,6 +78,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [appVersion, setAppVersion] = useState<string>();
|
||||
const [isAdminAddress, setIsAdminAddress] = useState<boolean>(false);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [showSendModal, setShowSendModal] = useState(false);
|
||||
|
||||
const userBalance = useGetBalance(clientDetails);
|
||||
const navigate = useNavigate();
|
||||
@@ -211,6 +214,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const handleShowTerminal = () => setShowTerminal((show) => !show);
|
||||
const switchNetwork = (_network: Network) => setNetwork(_network);
|
||||
const handleShowSettings = () => setShowSettings((show) => !show);
|
||||
const handleShowSendModal = () => setShowSendModal((show) => !show);
|
||||
|
||||
const memoizedValue = useMemo(
|
||||
() => ({
|
||||
@@ -240,6 +244,8 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
logOut,
|
||||
onAccountChange,
|
||||
handleShowSettings,
|
||||
showSendModal,
|
||||
handleShowSendModal,
|
||||
}),
|
||||
[
|
||||
appVersion,
|
||||
@@ -257,6 +263,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
storedAccounts,
|
||||
showTerminal,
|
||||
showSettings,
|
||||
showSendModal,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ export const MockMainContextProvider: FC<{}> = ({ children }) => {
|
||||
showAdmin: false,
|
||||
showTerminal: false,
|
||||
showSettings: false,
|
||||
showSendModal: true,
|
||||
network: 'SANDBOX',
|
||||
loginType: 'mnemonic',
|
||||
setIsLoading: () => undefined,
|
||||
@@ -50,6 +51,7 @@ export const MockMainContextProvider: FC<{}> = ({ children }) => {
|
||||
logOut: () => undefined,
|
||||
onAccountChange: () => undefined,
|
||||
handleShowSettings: () => undefined,
|
||||
handleShowSendModal: () => undefined,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
@@ -410,7 +410,7 @@ export const Delegation: FC = () => {
|
||||
open={Boolean(saturationError)}
|
||||
onClose={() => setSaturationError(undefined)}
|
||||
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."
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MajorCurrencyAmount, SendTxResult, TransactionExecuteResult } from '@nymproject/types';
|
||||
import { Fee, MajorCurrencyAmount, SendTxResult, TransactionExecuteResult } from '@nymproject/types';
|
||||
import { EnumNodeType, TBondArgs, TBondGatewayArgs, TBondMixNodeArgs } from '../types';
|
||||
import { invokeWrapper } from './wrapper';
|
||||
|
||||
@@ -15,7 +15,7 @@ export const unbondMixNode = async () => invokeWrapper<TransactionExecuteResult>
|
||||
export const updateMixnode = async (profitMarginPercent: number) =>
|
||||
invokeWrapper<TransactionExecuteResult>('update_mixnode', { profitMarginPercent });
|
||||
|
||||
export const send = async (args: { amount: MajorCurrencyAmount; address: string; memo: string }) =>
|
||||
export const send = async (args: { amount: MajorCurrencyAmount; address: string; memo: string; fee?: Fee }) =>
|
||||
invokeWrapper<SendTxResult>('send', args);
|
||||
|
||||
export const unbond = async (type: EnumNodeType) => {
|
||||
|
||||
@@ -50,5 +50,8 @@ export const simulateVestingUnbondMixnode = async (args: any) =>
|
||||
export const simulateVestingUpdateMixnode = async (args: any) =>
|
||||
invokeWrapper<FeeDetails>('simulate_vesting_update_mixnode', args);
|
||||
|
||||
export const simulateWithdrawVestedCoins = async ({ amount }: { amount: MajorCurrencyAmount }) =>
|
||||
invokeWrapper<FeeDetails>('simulate_withdraw_vested_coins', { amount });
|
||||
export const simulateWithdrawVestedCoins = async (args: any) =>
|
||||
invokeWrapper<FeeDetails>('simulate_withdraw_vested_coins', args);
|
||||
|
||||
export const simulateSend = async ({ address, amount }: { address: string; amount: MajorCurrencyAmount }) =>
|
||||
invokeWrapper<FeeDetails>('simulate_send', { address, amount });
|
||||
|
||||
@@ -2,15 +2,16 @@ import React from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import { ApplicationLayout } from 'src/layouts';
|
||||
import { Terminal } from 'src/pages/terminal';
|
||||
import { Bond, Balance, InternalDocs, Receive, Send, Unbond, DelegationPage, Admin, Settings } from '../pages';
|
||||
import { Send } from 'src/components/Send';
|
||||
import { Bond, Balance, InternalDocs, Receive, Unbond, DelegationPage, Admin, Settings } from '../pages';
|
||||
|
||||
export const AppRoutes = () => (
|
||||
<ApplicationLayout>
|
||||
<Terminal />
|
||||
<Settings />
|
||||
<Send />
|
||||
<Routes>
|
||||
<Route path="/balance" element={<Balance />} />
|
||||
<Route path="/send" element={<Send />} />
|
||||
<Route path="/receive" element={<Receive />} />
|
||||
<Route path="/bond" element={<Bond />} />
|
||||
<Route path="/unbond" element={<Unbond />} />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Fee } from "./Fee";
|
||||
import type { MajorCurrencyAmount } from "./Currency";
|
||||
import type { Fee } from './Fee';
|
||||
import type { MajorCurrencyAmount } from './Currency';
|
||||
|
||||
export type FeeDetails = { amount: MajorCurrencyAmount | null, fee: Fee };
|
||||
export type FeeDetails = { amount: MajorCurrencyAmount | null; fee: Fee };
|
||||
|
||||
@@ -13,6 +13,7 @@ export * from './DelegationRecord';
|
||||
export * from './DelegationResult';
|
||||
export * from './DelegationSummaryResponse';
|
||||
export * from './DelegationWithEverything';
|
||||
export * from './Fee';
|
||||
export * from './FeeDetails';
|
||||
export * from './Gas';
|
||||
export * from './GasInfo';
|
||||
|
||||
Reference in New Issue
Block a user