From 4cbbead359e9bf6152fd58e52869099b9c8d0839 Mon Sep 17 00:00:00 2001 From: fmtabbara Date: Fri, 10 Jun 2022 21:38:53 +0100 Subject: [PATCH 01/17] create new tx confirmation modal --- nym-wallet/.storybook/main.js | 19 ++++++----- nym-wallet/.storybook/mocks/tauri/app.js | 8 +++++ .../src/components/ConfirmTX.stories.tsx | 30 +++++++++++++++++ nym-wallet/src/components/ConfirmTX.tsx | 33 +++++++++++++++++++ 4 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 nym-wallet/.storybook/mocks/tauri/app.js create mode 100644 nym-wallet/src/components/ConfirmTX.stories.tsx create mode 100644 nym-wallet/src/components/ConfirmTX.tsx diff --git a/nym-wallet/.storybook/main.js b/nym-wallet/.storybook/main.js index db6de0998f..c4a255d616 100644 --- a/nym-wallet/.storybook/main.js +++ b/nym-wallet/.storybook/main.js @@ -9,6 +9,7 @@ module.exports = { core: { builder: 'webpack5', }, + typescript: { reactDocgen: false }, // webpackFinal: async (config, { configType }) => { // // `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION' // // You can change the configuration based on that. @@ -32,15 +33,17 @@ module.exports = { config.resolve.extensions = ['.tsx', '.ts', '.js']; config.resolve.plugins = [new TsconfigPathsPlugin()]; - config.plugins.push(new ForkTsCheckerWebpackPlugin({ - typescript: { - mode: 'write-references', - diagnosticOptions: { - semantic: true, - syntactic: true, + config.plugins.push( + new ForkTsCheckerWebpackPlugin({ + typescript: { + mode: 'write-references', + diagnosticOptions: { + semantic: true, + syntactic: true, + }, }, - }, - })); + }), + ); if (!config.resolve.alias) { config.resolve.alias = {}; diff --git a/nym-wallet/.storybook/mocks/tauri/app.js b/nym-wallet/.storybook/mocks/tauri/app.js new file mode 100644 index 0000000000..9ef3db093f --- /dev/null +++ b/nym-wallet/.storybook/mocks/tauri/app.js @@ -0,0 +1,8 @@ +/** + * This is a mock for Tauri's API package (@tauri-apps/api/window), to prevent stories from being excluded, because they either use + * or import dependencies that use Tauri. + */ + +module.exports = { + getVersion: () => undefined, +}; diff --git a/nym-wallet/src/components/ConfirmTX.stories.tsx b/nym-wallet/src/components/ConfirmTX.stories.tsx new file mode 100644 index 0000000000..83f42d7000 --- /dev/null +++ b/nym-wallet/src/components/ConfirmTX.stories.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Box } from '@mui/material'; +import { ConfirmTx } from './ConfirmTX'; +import { ModalListItem } from './Modals/ModalListItem'; + +export default { + title: 'Wallet / Confirm Transaction', + component: ConfirmTx, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + + + + + +); + +export const Default = Template.bind({}); +Default.args = { + open: true, + header: 'Confirm transaction', + subheader: 'Confirm and proceed or cancel transaction', + fee: { amount: '1', denom: 'NYM' }, + currency: 'NYM', + onClose: () => {}, + onConfirm: async () => {}, + onPrev: () => {}, +}; diff --git a/nym-wallet/src/components/ConfirmTX.tsx b/nym-wallet/src/components/ConfirmTX.tsx new file mode 100644 index 0000000000..65ccee4839 --- /dev/null +++ b/nym-wallet/src/components/ConfirmTX.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { SimpleModal } from './Modals/SimpleModal'; +import { MajorCurrencyAmount, MajorAmountString } from '@nymproject/types'; +import { ModalListItem } from './Modals/ModalListItem'; +import { Button } from '@mui/material'; + +export const ConfirmTx: React.FC<{ + open: boolean; + header: string; + subheader: string; + fee: MajorCurrencyAmount; + currency: MajorAmountString; + onConfirm: () => Promise; + onClose?: () => void; + onPrev: () => void; +}> = ({ open, fee, onConfirm, onClose, header, subheader, onPrev, children }) => ( + + Cancel + + } + > + {children} + + +); From 70a9bd0f6d3791e26d6cecd004c2af32c18d825d Mon Sep 17 00:00:00 2001 From: fmtabbara Date: Fri, 10 Jun 2022 21:39:44 +0100 Subject: [PATCH 02/17] fix heading bug --- nym-wallet/src/components/Delegation/DelegationModal.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nym-wallet/src/components/Delegation/DelegationModal.tsx b/nym-wallet/src/components/Delegation/DelegationModal.tsx index 4a37c05250..06fe1db5b7 100644 --- a/nym-wallet/src/components/Delegation/DelegationModal.tsx +++ b/nym-wallet/src/components/Delegation/DelegationModal.tsx @@ -18,8 +18,9 @@ const actionToHeader = (action: ActionType): string => { return 'Undelegation complete'; case 'compound': return 'Undelegation complete'; + default: + return 'Oh no! Something went wrong!'; } - return 'Oh no! Something went wrong!'; }; export type DelegationModalProps = { From a7a39526b4b470065baf02a858cd8a2ee5bf5db9 Mon Sep 17 00:00:00 2001 From: fmtabbara Date: Fri, 10 Jun 2022 21:40:28 +0100 Subject: [PATCH 03/17] move modal list item to shared dir --- .../src/components/{Delegation => Modals}/ModalListItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename nym-wallet/src/components/{Delegation => Modals}/ModalListItem.tsx (90%) diff --git a/nym-wallet/src/components/Delegation/ModalListItem.tsx b/nym-wallet/src/components/Modals/ModalListItem.tsx similarity index 90% rename from nym-wallet/src/components/Delegation/ModalListItem.tsx rename to nym-wallet/src/components/Modals/ModalListItem.tsx index 24da1f7506..4f467d03f8 100644 --- a/nym-wallet/src/components/Delegation/ModalListItem.tsx +++ b/nym-wallet/src/components/Modals/ModalListItem.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Box, Stack, Typography } from '@mui/material'; -import { ModalDivider } from '../Modals/ModalDivider'; +import { ModalDivider } from './ModalDivider'; export const ModalListItem: React.FC<{ label: string; From cfc7e6df77c8e2b51c8f36f574439d30d20879e7 Mon Sep 17 00:00:00 2001 From: fmtabbara Date: Fri, 10 Jun 2022 21:40:49 +0100 Subject: [PATCH 04/17] widen delegation modals --- nym-wallet/src/components/Modals/styles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nym-wallet/src/components/Modals/styles.ts b/nym-wallet/src/components/Modals/styles.ts index de4621a2a8..1f3c92cb09 100644 --- a/nym-wallet/src/components/Modals/styles.ts +++ b/nym-wallet/src/components/Modals/styles.ts @@ -3,7 +3,7 @@ export const modalStyle = { top: '50%', left: '50%', transform: 'translate(-50%, -50%)', - width: 500, + width: 600, bgcolor: 'background.paper', boxShadow: 24, borderRadius: '16px', From 4ea9bb7dc6e26943c26ffbc073593ff522e52934 Mon Sep 17 00:00:00 2001 From: fmtabbara Date: Fri, 10 Jun 2022 21:41:09 +0100 Subject: [PATCH 05/17] fix spelling error --- nym-wallet/src/types/rust/StateParams.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/nym-wallet/src/types/rust/StateParams.ts b/nym-wallet/src/types/rust/StateParams.ts index 22f6653c0e..68c3e018e9 100644 --- a/nym-wallet/src/types/rust/StateParams.ts +++ b/nym-wallet/src/types/rust/StateParams.ts @@ -1,2 +1,6 @@ - -export interface TauriContractStateParams { minimum_mixnode_pledge: string, minimum_gateway_pledge: string, mixnode_rewarded_set_size: number, mixnode_active_set_size: number, } \ No newline at end of file +export interface TauriContractStateParams { + minimum_mixnode_pledge: string; + minimum_gateway_pledge: string; + mixnode_rewarded_set_size: number; + mixnode_active_set_size: number; +} From 89d4910e6ff92675496d50b926d285a6c9efef79 Mon Sep 17 00:00:00 2001 From: fmtabbara Date: Fri, 10 Jun 2022 21:47:17 +0100 Subject: [PATCH 06/17] update simulation functions --- nym-wallet/src/requests/index.ts | 1 + nym-wallet/src/requests/simulate.ts | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/nym-wallet/src/requests/index.ts b/nym-wallet/src/requests/index.ts index dfd4c57f11..aec40a78b4 100644 --- a/nym-wallet/src/requests/index.ts +++ b/nym-wallet/src/requests/index.ts @@ -7,3 +7,4 @@ export * from './queries'; export * from './utils'; export * from './delegation'; export * from './rewards'; +export * from './simulate'; diff --git a/nym-wallet/src/requests/simulate.ts b/nym-wallet/src/requests/simulate.ts index 96ce7e4472..d21b9c0ec1 100644 --- a/nym-wallet/src/requests/simulate.ts +++ b/nym-wallet/src/requests/simulate.ts @@ -1,4 +1,4 @@ -import { FeeDetails } from '@nymproject/types'; +import { FeeDetails, MajorCurrencyAmount } from '@nymproject/types'; import { invokeWrapper } from './wrapper'; export const simulateBondGateway = async (args: any) => invokeWrapper('simulate_bond_gateway', args); @@ -11,11 +11,11 @@ export const simulateUnbondMixnode = async (args: any) => invokeWrapper invokeWrapper('simulate_update_mixnode', args); -export const simulateDelegateToMixnode = async (args: any) => +export const simulateDelegateToMixnode = async (args: { identity: string; amount: MajorCurrencyAmount }) => invokeWrapper('simulate_delegate_to_mixnode', args); -export const simulateUndelegateFromMixnode = async (args: any) => - invokeWrapper('simulate_undelegate_from_mixnode,', args); +export const simulateUndelegateFromMixnode = async (identity: string) => + invokeWrapper('simulate_undelegate_from_mixnode,', { identity }); export const simulateVestingBondGateway = async (args: any) => invokeWrapper('simulate_vesting_bond_gateway', args); @@ -23,6 +23,9 @@ export const simulateVestingBondGateway = async (args: any) => export const simulateVestingUnbondGateway = async (args: any) => invokeWrapper('simulate_vesting_unbond_gateway', args); +export const simulateVestingDelegateToMixnode = async (args: { identity: string; amount: MajorCurrencyAmount }) => + invokeWrapper('simulate_delegate_to_mixnode', args); + export const simulateVestingBondMixnode = async (args: any) => invokeWrapper('simulate_vesting_bond_mixnode', args); From b731aa0bcfad86ee0000b3f1941249eed53ba7b0 Mon Sep 17 00:00:00 2001 From: fmtabbara Date: Fri, 10 Jun 2022 21:48:43 +0100 Subject: [PATCH 07/17] update modals --- nym-wallet/src/components/Delegation/Modals.stories.tsx | 3 +-- nym-wallet/src/components/Modals/SimpleModal.stories.tsx | 4 ++-- nym-wallet/src/components/Rewards/CompoundModal.tsx | 3 ++- nym-wallet/src/components/Rewards/RedeemModal.stories.tsx | 6 +++--- nym-wallet/src/components/Rewards/RedeemModal.tsx | 2 +- nym-wallet/src/pages/delegation/index.tsx | 3 +-- 6 files changed, 10 insertions(+), 11 deletions(-) diff --git a/nym-wallet/src/components/Delegation/Modals.stories.tsx b/nym-wallet/src/components/Delegation/Modals.stories.tsx index 025013e818..83fb390db9 100644 --- a/nym-wallet/src/components/Delegation/Modals.stories.tsx +++ b/nym-wallet/src/components/Delegation/Modals.stories.tsx @@ -117,9 +117,8 @@ export const Undelegate = () => { setOpen(false)} - onOk={() => setOpen(false)} + onOk={async () => setOpen(false)} currency="NYM" - fee={0.004375} amount={150} identityKey="AA6RfeY8DttMD3CQKoayV6mss5a5FC3RoH75Kmcujyxx" proxy={null} diff --git a/nym-wallet/src/components/Modals/SimpleModal.stories.tsx b/nym-wallet/src/components/Modals/SimpleModal.stories.tsx index 7cb71929ed..a249ab4f9b 100644 --- a/nym-wallet/src/components/Modals/SimpleModal.stories.tsx +++ b/nym-wallet/src/components/Modals/SimpleModal.stories.tsx @@ -47,7 +47,7 @@ export const Default = () => { setOpen(false)} - onOk={() => setOpen(false)} + onOk={async () => setOpen(false)} header="This is a modal" subHeader="This is a sub header" okLabel="Click to continue" @@ -104,7 +104,7 @@ export const NoSubheader = () => { setOpen(false)} - onOk={() => setOpen(false)} + onOk={async () => setOpen(false)} header="This is a modal" okLabel="Kaplow!" > diff --git a/nym-wallet/src/components/Rewards/CompoundModal.tsx b/nym-wallet/src/components/Rewards/CompoundModal.tsx index d8d23dfc7a..76c56df9e0 100644 --- a/nym-wallet/src/components/Rewards/CompoundModal.tsx +++ b/nym-wallet/src/components/Rewards/CompoundModal.tsx @@ -15,11 +15,12 @@ export const CompoundModal: React.FC<{ currency: string; message: string; }> = ({ open, onClose, onOk, identityKey, amount, fee, currency, message }) => { - const handleOk = () => { + const handleOk = async () => { if (onOk) { onOk(identityKey); } }; + return ( { setOpen(false)} - onOk={() => setOpen(false)} + onOk={async () => setOpen(false)} message="Redeem all rewards" currency="NYM" identityKey="D88RfeY8DttMD3CQKoayV6mss5a5FC3RoH75Kmcujaaa" @@ -71,7 +71,7 @@ export const RedeemRewardForMixnode = () => { setOpen(false)} - onOk={() => setOpen(false)} + onOk={async () => setOpen(false)} message="Redeem rewards" currency="NYM" identityKey="D88RfeY8DttMD3CQKoayV6mss5a5FC3RoH75Kmcujaaa" @@ -109,7 +109,7 @@ export const FeeIsMoreThanMixnodeReward = () => { setOpen(false)} - onOk={() => setOpen(false)} + onOk={async () => setOpen(false)} identityKey="D88RfeY8DttMD3CQKoayV6mss5a5FC3RoH75Kmcujaaa" message="Redeem rewards" currency="NYM" diff --git a/nym-wallet/src/components/Rewards/RedeemModal.tsx b/nym-wallet/src/components/Rewards/RedeemModal.tsx index dc12068f2b..e6228ac9ff 100644 --- a/nym-wallet/src/components/Rewards/RedeemModal.tsx +++ b/nym-wallet/src/components/Rewards/RedeemModal.tsx @@ -15,7 +15,7 @@ export const RedeemModal: React.FC<{ currency: string; message: string; }> = ({ open, onClose, onOk, identityKey, amount, fee, currency, message }) => { - const handleOk = () => { + const handleOk = async () => { if (onOk) { onOk(identityKey); } diff --git a/nym-wallet/src/pages/delegation/index.tsx b/nym-wallet/src/pages/delegation/index.tsx index 078e95b3cb..4845ed43da 100644 --- a/nym-wallet/src/pages/delegation/index.tsx +++ b/nym-wallet/src/pages/delegation/index.tsx @@ -106,6 +106,7 @@ export const Delegation: FC = () => { tokenPool, }); } catch (e) { + console.log(e); setConfirmationModalProps({ status: 'error', action: 'delegate', @@ -307,7 +308,6 @@ export const Delegation: FC = () => { buttonText="Delegate more" identityKey={currentDelegationListActionItem.node_identity} currency={clientDetails!.denom} - estimatedReward={0} accountBalance={balance?.printable_balance} nodeUptimePercentage={currentDelegationListActionItem.avg_uptime_percent} profitMarginPercentage={currentDelegationListActionItem.profit_margin_percent} @@ -323,7 +323,6 @@ export const Delegation: FC = () => { onOk={handleUndelegate} proxy={currentDelegationListActionItem.proxy} currency={currentDelegationListActionItem.amount.denom} - fee={0.1} amount={+currentDelegationListActionItem.amount.amount} identityKey={currentDelegationListActionItem.node_identity} /> From 7d4f6c0bbd6f82ec5a76796f001c2ae6dcf33d73 Mon Sep 17 00:00:00 2001 From: fmtabbara Date: Fri, 10 Jun 2022 21:49:26 +0100 Subject: [PATCH 08/17] give SimpleModal component a secondary action --- nym-wallet/src/components/Modals/SimpleModal.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/nym-wallet/src/components/Modals/SimpleModal.tsx b/nym-wallet/src/components/Modals/SimpleModal.tsx index 53e7c79d5a..f9438db18e 100644 --- a/nym-wallet/src/components/Modals/SimpleModal.tsx +++ b/nym-wallet/src/components/Modals/SimpleModal.tsx @@ -6,13 +6,14 @@ import { modalStyle } from './styles'; export const SimpleModal: React.FC<{ open: boolean; onClose?: () => void; - onOk?: () => void; + onOk?: () => Promise; header: string; subHeader?: string; okLabel: string; okDisabled?: boolean; sx?: SxProps; -}> = ({ open, onClose, okDisabled, onOk, header, subHeader, okLabel, sx, children }) => ( + SecondaryAction?: React.ReactNode; +}> = ({ open, onClose, okDisabled, onOk, header, subHeader, okLabel, sx, SecondaryAction, children }) => ( @@ -29,9 +30,11 @@ export const SimpleModal: React.FC<{ {children} - + + {SecondaryAction} ); From a9ff98418c907815ce787d8f3f1b3f53bfec3536 Mon Sep 17 00:00:00 2001 From: fmtabbara Date: Sat, 11 Jun 2022 00:19:00 +0100 Subject: [PATCH 09/17] update modals --- .../components/Delegation/DelegateModal.tsx | 166 ++++++++++++++---- .../components/Delegation/UndelegateModal.tsx | 51 ++++-- 2 files changed, 160 insertions(+), 57 deletions(-) diff --git a/nym-wallet/src/components/Delegation/DelegateModal.tsx b/nym-wallet/src/components/Delegation/DelegateModal.tsx index 1da4668c64..374b3a650f 100644 --- a/nym-wallet/src/components/Delegation/DelegateModal.tsx +++ b/nym-wallet/src/components/Delegation/DelegateModal.tsx @@ -2,15 +2,74 @@ import React, { 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 { CurrencyDenom, FeeDetails, MajorCurrencyAmount } from '@nymproject/types'; +import { getGasFee, simulateDelegateToMixnode } from 'src/requests'; import { SimpleModal } from '../Modals/SimpleModal'; -import { ModalListItem } from './ModalListItem'; -import { validateKey } from '../../utils'; +import { ModalListItem } from '../Modals/ModalListItem'; +import { checkHasEnoughFunds, checkHasEnoughLockedTokens, validateAmount, validateKey } from '../../utils'; import { TokenPoolSelector, TPoolOption } from '../TokenPoolSelector'; +import { ConfirmTx } from '../ConfirmTX'; +import { Console } from 'src/utils/console'; const MIN_AMOUNT_TO_DELEGATE = 10; +type confirmationResponseType = { + error?: string; + fees?: FeeDetails; +}; + +const handleConfirmWithBalance = async ({ + identityKey, + amount, +}: { + identityKey: string; + amount: MajorCurrencyAmount; +}) => { + const response: confirmationResponseType = {}; + const hasEnoughTokens = await checkHasEnoughFunds(amount.amount); + + try { + if (!hasEnoughTokens) { + response.error = 'Not enough funds'; + } else { + const fees = await simulateDelegateToMixnode({ identity: identityKey, amount }); + response.fees = fees; + } + return response; + } catch (e) { + Console.error(e); + response.fees = undefined; + response.error = 'An error occurred. Please check the address and amount are correct'; + return response; + } +}; + +const handleConfirmWithLocked = async ({ + identityKey, + amount, +}: { + identityKey: string; + amount: MajorCurrencyAmount; +}) => { + const response: confirmationResponseType = {}; + const hasEnoughTokens = await checkHasEnoughLockedTokens(amount.amount); + + try { + if (!hasEnoughTokens) { + response.error = 'Not enough funds'; + } else { + const fees = await simulateDelegateToMixnode({ identity: identityKey, amount }); + response.fees = fees; + } + return response; + } catch (e) { + Console.error(e); + response.fees = undefined; + response.error = 'An error occurred. Please check the address and amount are correct'; + return response; + } +}; + export const DelegateModal: React.FC<{ open: boolean; onClose?: () => void; @@ -40,7 +99,6 @@ export const DelegateModal: React.FC<{ identityKey: initialIdentityKey, rewardInterval, accountBalance, - feeOverride, estimatedReward, currency, profitMarginPercentage, @@ -53,36 +111,59 @@ export const DelegateModal: React.FC<{ const [isValidated, setValidated] = useState(false); const [errorAmount, setErrorAmount] = useState(); const [tokenPool, setTokenPool] = useState('balance'); - const [fee, setFee] = useState(); + const [fee, setFee] = useState(); - const getFee = async () => { - if (feeOverride) setFee(feeOverride); - else { - const res = await getGasFee('BondMixnode'); - setFee(res.amount); - } - }; - - const validate = () => { + const validate = async () => { let newValidatedValue = true; + let errorMessage; + if (!identityKey || !validateKey(identityKey, 32)) { newValidatedValue = false; } - if (amount && Number(amount) < MIN_AMOUNT_TO_DELEGATE) { - setErrorAmount(`Min. delegation amount: ${MIN_AMOUNT_TO_DELEGATE} ${currency}`); + + if (amount && !(await validateAmount(amount, '0'))) { newValidatedValue = false; - } else { - setErrorAmount(undefined); + errorMessage = 'Please enter a valid amount'; } + + if (amount && Number(amount) < MIN_AMOUNT_TO_DELEGATE) { + errorMessage = `Min. delegation amount: ${MIN_AMOUNT_TO_DELEGATE} ${currency}`; + newValidatedValue = false; + } + + if (!amount?.length) { + newValidatedValue = false; + } + + setErrorAmount(errorMessage); setValidated(newValidatedValue); }; - const handleOk = () => { + const handleOk = async () => { if (onOk && amount && identityKey) { onOk(identityKey, { amount, denom: currency }, tokenPool); } }; + const handleConfirm = async ({ identityKey, amount }: { identityKey: string; amount: MajorCurrencyAmount }) => { + let response: confirmationResponseType = {}; + + if (tokenPool === 'locked') { + response = await handleConfirmWithLocked({ identityKey, amount }); + } else { + response = await handleConfirmWithBalance({ identityKey, amount }); + } + + if (response.error) { + setErrorAmount(response.error); + } + + if (!response.error && response.fees) { + setFee(response.fees); + setErrorAmount(undefined); + } + }; + const handleIdentityKeyChanged = (newIdentityKey: string) => { setIdentityKey(newIdentityKey); if (onIdentityKeyChanged) { @@ -101,15 +182,32 @@ export const DelegateModal: React.FC<{ validate(); }, [amount, identityKey]); - React.useEffect(() => { - getFee(); - }, []); + if (fee?.amount) { + return ( + setFee(undefined)} + onConfirm={handleOk} + currency={currency} + > + + + + ); + } return ( { + if (identityKey && amount) { + handleConfirm({ identityKey, amount: { amount, denom: currency } }); + } + }} header={header || 'Delegate'} subHeader="Delegate to mixnode" okLabel={buttonText || 'Delegate stake'} @@ -120,7 +218,7 @@ export const DelegateModal: React.FC<{ fullWidth placeholder="Node identity key" onChanged={handleIdentityKeyChanged} - initialValue={initialIdentityKey} + initialValue={identityKey} readOnly={Boolean(initialIdentityKey)} textFieldProps={{ autoFocus: !initialIdentityKey, @@ -132,7 +230,7 @@ export const DelegateModal: React.FC<{ required fullWidth placeholder="Amount" - initialValue={initialAmount} + initialValue={amount} autoFocus={Boolean(initialIdentityKey)} onChanged={handleAmountChanged} /> @@ -140,10 +238,10 @@ export const DelegateModal: React.FC<{ {errorAmount} - - Account balance - {accountBalance} - + + + + ); }; diff --git a/nym-wallet/src/components/Delegation/UndelegateModal.tsx b/nym-wallet/src/components/Delegation/UndelegateModal.tsx index 350fb739a8..2787a64515 100644 --- a/nym-wallet/src/components/Delegation/UndelegateModal.tsx +++ b/nym-wallet/src/components/Delegation/UndelegateModal.tsx @@ -1,7 +1,10 @@ -import React from 'react'; -import { Stack, Typography } from '@mui/material'; +import React, { useEffect, useState } from 'react'; +import { Box, Stack, Typography } from '@mui/material'; import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; import { SimpleModal } from '../Modals/SimpleModal'; +import { simulateUndelegateFromMixnode } from 'src/requests'; +import { ModalListItem } from '../Modals/ModalListItem'; +import { FeeDetails } from '@nymproject/types'; export const UndelegateModal: React.FC<{ open: boolean; @@ -9,15 +12,31 @@ export const UndelegateModal: React.FC<{ onOk?: (identityKey: string, proxy: string | null) => void; identityKey: string; amount: number; - fee: number; currency: string; proxy: string | null; -}> = ({ identityKey, open, onClose, onOk, amount, fee, currency, proxy }) => { - const handleOk = () => { +}> = ({ identityKey, open, onClose, onOk, amount, currency, proxy }) => { + const [fee, setFee] = useState(); + const [error, setError] = useState(); + + const getFee = async () => { + try { + const simulatedFee = await simulateUndelegateFromMixnode(identityKey); + setFee(simulatedFee); + } catch (e) { + setError('Unable to determine fee estimate'); + } + }; + + useEffect(() => { + getFee(); + }, []); + + const handleOk = async () => { if (onOk) { onOk(identityKey, proxy); } }; + return ( - - Delegation amount: - - {amount} {currency} - - + + + Tokens will be transferred to account you are logged in with now - - theme.palette.nym.fee}> - Est. fee for this transaction: - - theme.palette.nym.fee}> - {fee} {currency} - - + ); }; From 0a632599cd8484c9efa401eadc73ac07efa1e139 Mon Sep 17 00:00:00 2001 From: fmtabbara Date: Sat, 11 Jun 2022 00:22:50 +0100 Subject: [PATCH 10/17] make subheader optional --- nym-wallet/src/components/ConfirmTX.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nym-wallet/src/components/ConfirmTX.tsx b/nym-wallet/src/components/ConfirmTX.tsx index 65ccee4839..e021a2f411 100644 --- a/nym-wallet/src/components/ConfirmTX.tsx +++ b/nym-wallet/src/components/ConfirmTX.tsx @@ -7,7 +7,7 @@ import { Button } from '@mui/material'; export const ConfirmTx: React.FC<{ open: boolean; header: string; - subheader: string; + subheader?: string; fee: MajorCurrencyAmount; currency: MajorAmountString; onConfirm: () => Promise; From 70011d05926da8ab4203269f19c084de16a3d338 Mon Sep 17 00:00:00 2001 From: fmtabbara Date: Mon, 13 Jun 2022 11:35:34 +0100 Subject: [PATCH 11/17] add fees for claim and compound --- .../src/components/Rewards/CompoundModal.tsx | 36 ++++++++++++------ .../src/components/Rewards/RedeemModal.tsx | 38 +++++++++++++------ nym-wallet/src/requests/simulate.ts | 12 ++++++ 3 files changed, 64 insertions(+), 22 deletions(-) diff --git a/nym-wallet/src/components/Rewards/CompoundModal.tsx b/nym-wallet/src/components/Rewards/CompoundModal.tsx index 76c56df9e0..a3eff3fc22 100644 --- a/nym-wallet/src/components/Rewards/CompoundModal.tsx +++ b/nym-wallet/src/components/Rewards/CompoundModal.tsx @@ -1,8 +1,12 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Alert, AlertTitle, Stack, Typography } from '@mui/material'; import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; import WarningIcon from '@mui/icons-material/Warning'; +import { simulateCompoundDelgatorReward } from 'src/requests'; import { SimpleModal } from '../Modals/SimpleModal'; +import { FeeDetails } from '@nymproject/types'; +import { Console } from 'src/utils/console'; +import { ModalListItem } from '../Modals/ModalListItem'; export const CompoundModal: React.FC<{ open: boolean; @@ -14,13 +18,27 @@ export const CompoundModal: React.FC<{ minimum?: number; currency: string; message: string; -}> = ({ open, onClose, onOk, identityKey, amount, fee, currency, message }) => { +}> = ({ open, onClose, onOk, identityKey, amount, currency, message }) => { + const [fee, setFee] = useState(); const handleOk = async () => { if (onOk) { onOk(identityKey); } }; + const getFee = async () => { + try { + const simulatedfee = await simulateCompoundDelgatorReward(identityKey); + setFee(simulatedfee); + } catch (e) { + Console.log(`Unable to get fee estimate for compounding reward: ${e}`); + } + }; + + useEffect(() => { + getFee(); + }, []); + return ( - - theme.palette.nym.fee}> - Est. fee for this transaction: - - theme.palette.nym.fee}> - {fee} {currency} - - + - {amount < fee && ( + {fee?.amount && amount < +fee.amount?.amount && ( }> Warning: fees are greater than the reward The fees for redeeming rewards will cost more than the rewards. Are you sure you want to continue? diff --git a/nym-wallet/src/components/Rewards/RedeemModal.tsx b/nym-wallet/src/components/Rewards/RedeemModal.tsx index e6228ac9ff..56e4615bf0 100644 --- a/nym-wallet/src/components/Rewards/RedeemModal.tsx +++ b/nym-wallet/src/components/Rewards/RedeemModal.tsx @@ -1,8 +1,12 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Alert, AlertTitle, Stack, Typography } from '@mui/material'; import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; import WarningIcon from '@mui/icons-material/Warning'; +import { FeeDetails } from '@nymproject/types'; import { SimpleModal } from '../Modals/SimpleModal'; +import { Console } from 'src/utils/console'; +import { simulateRedeemDelgatorReward } from 'src/requests'; +import { ModalListItem } from '../Modals/ModalListItem'; export const RedeemModal: React.FC<{ open: boolean; @@ -14,12 +18,28 @@ export const RedeemModal: React.FC<{ minimum?: number; currency: string; message: string; -}> = ({ open, onClose, onOk, identityKey, amount, fee, currency, message }) => { +}> = ({ open, onClose, onOk, identityKey, amount, currency, message }) => { + const [fee, setFee] = useState(); + const handleOk = async () => { if (onOk) { onOk(identityKey); } }; + + const getFee = async () => { + try { + const simulatedfee = await simulateRedeemDelgatorReward(identityKey); + setFee(simulatedfee); + } catch (e) { + Console.error(`Unable to get fee estimate for compounding reward: ${e}`); + } + }; + + useEffect(() => { + getFee(); + }, []); + return ( - - theme.palette.nym.fee}> - Est. fee for this transaction: - - theme.palette.nym.fee}> - {fee} {currency} - - + - {amount < fee && ( + {fee?.amount && amount < +fee.amount?.amount && ( }> Warning: fees are greater than the reward The fees for redeeming rewards will cost more than the rewards. Are you sure you want to continue? diff --git a/nym-wallet/src/requests/simulate.ts b/nym-wallet/src/requests/simulate.ts index d21b9c0ec1..bfe878e243 100644 --- a/nym-wallet/src/requests/simulate.ts +++ b/nym-wallet/src/requests/simulate.ts @@ -17,6 +17,18 @@ export const simulateDelegateToMixnode = async (args: { identity: string; amount export const simulateUndelegateFromMixnode = async (identity: string) => invokeWrapper('simulate_undelegate_from_mixnode,', { identity }); +export const simulateCompoundDelgatorReward = async (identity: string) => + invokeWrapper('simulate_compound_delegator_reward', { mixIdentity: identity }); + +export const simulateRedeemDelgatorReward = async (identity: string) => + invokeWrapper('simulate_claim_delegator_reward', { mixIdentity: identity }); + +export const simulateVestingRedeemDelgatorReward = async (identity: string) => + invokeWrapper('simulate_vesting_claim_delegator_reward', { mixIdentity: identity }); + +export const simulateVestingCompoundDelgatorReward = async (identity: string) => + invokeWrapper('simulate_vesting_compound_delegator_reward', { mixIdentity: identity }); + export const simulateVestingBondGateway = async (args: any) => invokeWrapper('simulate_vesting_bond_gateway', args); From ce7d02220f04024f4b0406620b34e96330bb6de8 Mon Sep 17 00:00:00 2001 From: fmtabbara Date: Mon, 13 Jun 2022 17:17:26 +0100 Subject: [PATCH 12/17] refactor --- .../src/components/ConfirmTX.stories.tsx | 8 +- nym-wallet/src/components/ConfirmTX.tsx | 15 +-- .../components/Delegation/DelegateModal.tsx | 103 +++++------------- .../components/Delegation/Modals.stories.tsx | 3 - .../components/Delegation/UndelegateModal.tsx | 31 ++---- nym-wallet/src/components/Modals/ModalFee.tsx | 17 +++ .../src/components/Modals/ModalListItem.tsx | 2 +- .../src/components/Rewards/CompoundModal.tsx | 41 +++---- .../Rewards/RedeemModal.stories.tsx | 8 +- .../src/components/Rewards/RedeemModal.tsx | 39 +++---- nym-wallet/src/hooks/useGetFee.ts | 33 ++++++ nym-wallet/src/pages/delegation/index.tsx | 7 +- nym-wallet/src/requests/simulate.ts | 8 +- 13 files changed, 143 insertions(+), 172 deletions(-) create mode 100644 nym-wallet/src/components/Modals/ModalFee.tsx create mode 100644 nym-wallet/src/hooks/useGetFee.ts diff --git a/nym-wallet/src/components/ConfirmTX.stories.tsx b/nym-wallet/src/components/ConfirmTX.stories.tsx index 83f42d7000..e3498e29a3 100644 --- a/nym-wallet/src/components/ConfirmTX.stories.tsx +++ b/nym-wallet/src/components/ConfirmTX.stories.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { Box } from '@mui/material'; import { ConfirmTx } from './ConfirmTX'; import { ModalListItem } from './Modals/ModalListItem'; @@ -12,8 +11,8 @@ export default { const Template: ComponentStory = (args) => ( - - + + ); @@ -22,8 +21,7 @@ Default.args = { open: true, header: 'Confirm transaction', subheader: 'Confirm and proceed or cancel transaction', - fee: { amount: '1', denom: 'NYM' }, - currency: 'NYM', + fee: { amount: { amount: '0.001', denom: 'NYM' } }, onClose: () => {}, onConfirm: async () => {}, onPrev: () => {}, diff --git a/nym-wallet/src/components/ConfirmTX.tsx b/nym-wallet/src/components/ConfirmTX.tsx index e021a2f411..c398e2e879 100644 --- a/nym-wallet/src/components/ConfirmTX.tsx +++ b/nym-wallet/src/components/ConfirmTX.tsx @@ -1,15 +1,14 @@ import React from 'react'; +import { FeeDetails } from '@nymproject/types'; +import { Box, Button } from '@mui/material'; import { SimpleModal } from './Modals/SimpleModal'; -import { MajorCurrencyAmount, MajorAmountString } from '@nymproject/types'; -import { ModalListItem } from './Modals/ModalListItem'; -import { Button } from '@mui/material'; +import { ModalFee } from './Modals/ModalFee'; export const ConfirmTx: React.FC<{ open: boolean; header: string; subheader?: string; - fee: MajorCurrencyAmount; - currency: MajorAmountString; + fee: FeeDetails; onConfirm: () => Promise; onClose?: () => void; onPrev: () => void; @@ -27,7 +26,9 @@ export const ConfirmTx: React.FC<{ } > - {children} - + + {children} + + ); diff --git a/nym-wallet/src/components/Delegation/DelegateModal.tsx b/nym-wallet/src/components/Delegation/DelegateModal.tsx index 374b3a650f..aa20ce688c 100644 --- a/nym-wallet/src/components/Delegation/DelegateModal.tsx +++ b/nym-wallet/src/components/Delegation/DelegateModal.tsx @@ -1,73 +1,29 @@ import React, { useState } from 'react'; -import { Box, Stack, Typography } from '@mui/material'; +import { Box, Typography } from '@mui/material'; import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; -import { CurrencyDenom, FeeDetails, MajorCurrencyAmount } from '@nymproject/types'; -import { getGasFee, simulateDelegateToMixnode } from 'src/requests'; +import { CurrencyDenom, MajorCurrencyAmount } from '@nymproject/types'; +import { useGetFee } from 'src/hooks/useGetFee'; +import { simulateDelegateToMixnode } from 'src/requests'; import { SimpleModal } from '../Modals/SimpleModal'; import { ModalListItem } from '../Modals/ModalListItem'; import { checkHasEnoughFunds, checkHasEnoughLockedTokens, validateAmount, validateKey } from '../../utils'; import { TokenPoolSelector, TPoolOption } from '../TokenPoolSelector'; import { ConfirmTx } from '../ConfirmTX'; -import { Console } from 'src/utils/console'; const MIN_AMOUNT_TO_DELEGATE = 10; -type confirmationResponseType = { - error?: string; - fees?: FeeDetails; -}; - -const handleConfirmWithBalance = async ({ - identityKey, - amount, -}: { - identityKey: string; - amount: MajorCurrencyAmount; -}) => { - const response: confirmationResponseType = {}; - const hasEnoughTokens = await checkHasEnoughFunds(amount.amount); - - try { - if (!hasEnoughTokens) { - response.error = 'Not enough funds'; - } else { - const fees = await simulateDelegateToMixnode({ identity: identityKey, amount }); - response.fees = fees; - } - return response; - } catch (e) { - Console.error(e); - response.fees = undefined; - response.error = 'An error occurred. Please check the address and amount are correct'; - return response; +const checkTokenBalance = async (tokenPool: TPoolOption, amount: string) => { + let hasEnoughFunds = false; + if (tokenPool === 'locked') { + hasEnoughFunds = await checkHasEnoughLockedTokens(amount); } -}; -const handleConfirmWithLocked = async ({ - identityKey, - amount, -}: { - identityKey: string; - amount: MajorCurrencyAmount; -}) => { - const response: confirmationResponseType = {}; - const hasEnoughTokens = await checkHasEnoughLockedTokens(amount.amount); - - try { - if (!hasEnoughTokens) { - response.error = 'Not enough funds'; - } else { - const fees = await simulateDelegateToMixnode({ identity: identityKey, amount }); - response.fees = fees; - } - return response; - } catch (e) { - Console.error(e); - response.fees = undefined; - response.error = 'An error occurred. Please check the address and amount are correct'; - return response; + if (tokenPool === 'balance') { + hasEnoughFunds = await checkHasEnoughFunds(amount); } + + return hasEnoughFunds; }; export const DelegateModal: React.FC<{ @@ -84,7 +40,6 @@ export const DelegateModal: React.FC<{ estimatedReward?: number; profitMarginPercentage?: number | null; nodeUptimePercentage?: number | null; - feeOverride?: string; currency: CurrencyDenom; initialAmount?: string; hasVestingContract: boolean; @@ -111,7 +66,8 @@ export const DelegateModal: React.FC<{ const [isValidated, setValidated] = useState(false); const [errorAmount, setErrorAmount] = useState(); const [tokenPool, setTokenPool] = useState('balance'); - const [fee, setFee] = useState(); + + const { fee, getFee, resetFeeState } = useGetFee(); const validate = async () => { let newValidatedValue = true; @@ -145,22 +101,20 @@ export const DelegateModal: React.FC<{ } }; - const handleConfirm = async ({ identityKey, amount }: { identityKey: string; amount: MajorCurrencyAmount }) => { - let response: confirmationResponseType = {}; + const handleConfirm = async ({ identity, value }: { identity: string; value: MajorCurrencyAmount }) => { + const hasEnoughTokens = await checkTokenBalance(tokenPool, value.amount); + + if (!hasEnoughTokens) { + setErrorAmount('Not enough funds'); + return; + } if (tokenPool === 'locked') { - response = await handleConfirmWithLocked({ identityKey, amount }); - } else { - response = await handleConfirmWithBalance({ identityKey, amount }); + getFee(simulateDelegateToMixnode, { identity, amount: value }); } - if (response.error) { - setErrorAmount(response.error); - } - - if (!response.error && response.fees) { - setFee(response.fees); - setErrorAmount(undefined); + if (tokenPool === 'balance') { + getFee(simulateDelegateToMixnode, { identity, amount: value }); } }; @@ -182,16 +136,15 @@ export const DelegateModal: React.FC<{ validate(); }, [amount, identityKey]); - if (fee?.amount) { + if (fee) { return ( setFee(undefined)} + onPrev={resetFeeState} onConfirm={handleOk} - currency={currency} > @@ -205,7 +158,7 @@ export const DelegateModal: React.FC<{ onClose={onClose} onOk={async () => { if (identityKey && amount) { - handleConfirm({ identityKey, amount: { amount, denom: currency } }); + handleConfirm({ identity: identityKey, value: { amount, denom: currency } }); } }} header={header || 'Delegate'} diff --git a/nym-wallet/src/components/Delegation/Modals.stories.tsx b/nym-wallet/src/components/Delegation/Modals.stories.tsx index 83fb390db9..29c1eea04f 100644 --- a/nym-wallet/src/components/Delegation/Modals.stories.tsx +++ b/nym-wallet/src/components/Delegation/Modals.stories.tsx @@ -51,7 +51,6 @@ export const Delegate = () => { onClose={() => setOpen(false)} onOk={async () => setOpen(false)} currency="NYM" - feeOverride="0.004375" estimatedReward={50.423} accountBalance="425.2345053" nodeUptimePercentage={99.28394} @@ -73,7 +72,6 @@ export const DelegateBelowMinimum = () => { onClose={() => setOpen(false)} onOk={async () => setOpen(false)} currency="NYM" - feeOverride="0.004375" estimatedReward={425.2345053} nodeUptimePercentage={99.28394} profitMarginPercentage={11.12334234} @@ -97,7 +95,6 @@ export const DelegateMore = () => { header="Delegate more" buttonText="Delegate more" currency="NYM" - feeOverride="0.004375" estimatedReward={50.423} accountBalance="425.2345053" nodeUptimePercentage={99.28394} diff --git a/nym-wallet/src/components/Delegation/UndelegateModal.tsx b/nym-wallet/src/components/Delegation/UndelegateModal.tsx index 2787a64515..8c7c3eeff7 100644 --- a/nym-wallet/src/components/Delegation/UndelegateModal.tsx +++ b/nym-wallet/src/components/Delegation/UndelegateModal.tsx @@ -1,10 +1,11 @@ -import React, { useEffect, useState } from 'react'; -import { Box, Stack, Typography } from '@mui/material'; +import React, { useEffect } from 'react'; +import { Box, Typography } from '@mui/material'; import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; -import { SimpleModal } from '../Modals/SimpleModal'; +import { useGetFee } from 'src/hooks/useGetFee'; import { simulateUndelegateFromMixnode } from 'src/requests'; +import { SimpleModal } from '../Modals/SimpleModal'; import { ModalListItem } from '../Modals/ModalListItem'; -import { FeeDetails } from '@nymproject/types'; +import { ModalFee } from '../Modals/ModalFee'; export const UndelegateModal: React.FC<{ open: boolean; @@ -15,20 +16,11 @@ export const UndelegateModal: React.FC<{ currency: string; proxy: string | null; }> = ({ identityKey, open, onClose, onOk, amount, currency, proxy }) => { - const [fee, setFee] = useState(); - const [error, setError] = useState(); - - const getFee = async () => { - try { - const simulatedFee = await simulateUndelegateFromMixnode(identityKey); - setFee(simulatedFee); - } catch (e) { - setError('Unable to determine fee estimate'); - } - }; + const { fee, isFeeLoading, feeError, getFee } = useGetFee(); useEffect(() => { - getFee(); + // Need simulateVestingUndelegateFromMixnode + getFee(simulateUndelegateFromMixnode, identityKey); }, []); const handleOk = async () => { @@ -45,7 +37,7 @@ export const UndelegateModal: React.FC<{ header="Undelegate" subHeader="Undelegate from mixnode" okLabel="Undelegate stake" - okDisabled={!fee && !error} + okDisabled={!fee} > - + ); }; diff --git a/nym-wallet/src/components/Modals/ModalFee.tsx b/nym-wallet/src/components/Modals/ModalFee.tsx new file mode 100644 index 0000000000..c64fa0286e --- /dev/null +++ b/nym-wallet/src/components/Modals/ModalFee.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { FeeDetails } from '@nymproject/types'; +import { CircularProgress } from '@mui/material'; +import { ModalListItem } from './ModalListItem'; + +type TFeeProps = { fee?: FeeDetails; isLoading: boolean; error?: string }; + +const getValue = ({ fee, isLoading, error }: TFeeProps) => { + if (isLoading) return ; + if (error && !isLoading) return 'n/a'; + if (fee) return `${fee.amount?.amount} ${fee.amount?.denom}`; + return '-'; +}; + +export const ModalFee = ({ fee, isLoading, error }: TFeeProps) => ( + +); diff --git a/nym-wallet/src/components/Modals/ModalListItem.tsx b/nym-wallet/src/components/Modals/ModalListItem.tsx index 4f467d03f8..a416375200 100644 --- a/nym-wallet/src/components/Modals/ModalListItem.tsx +++ b/nym-wallet/src/components/Modals/ModalListItem.tsx @@ -6,7 +6,7 @@ export const ModalListItem: React.FC<{ label: string; divider?: boolean; hidden?: boolean; - value: React.ReactNode; + value: string | React.ReactNode; }> = ({ label, value, hidden, divider }) => (