Merge pull request #1357 from nymtech/feature/wallet-delegation-fees

Wallet: simulated delegation fees
This commit is contained in:
Fouad
2022-06-28 16:13:50 +01:00
committed by GitHub
29 changed files with 478 additions and 188 deletions
+11 -8
View File
@@ -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 = {};
+2 -2
View File
@@ -4,5 +4,5 @@
*/
module.exports = {
getVersion: () => undefined
}
getVersion: () => undefined,
};
+2
View File
@@ -130,6 +130,8 @@ fn main() {
simulate::mixnet::simulate_update_mixnode,
simulate::mixnet::simulate_delegate_to_mixnode,
simulate::mixnet::simulate_undelegate_from_mixnode,
simulate::vesting::simulate_vesting_delegate_to_mixnode,
simulate::vesting::simulate_vesting_undelegate_from_mixnode,
simulate::vesting::simulate_vesting_bond_gateway,
simulate::vesting::simulate_vesting_unbond_gateway,
simulate::vesting::simulate_vesting_bond_mixnode,
@@ -150,6 +150,7 @@ pub async fn simulate_undelegate_from_mixnode(
identity: &str,
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<FeeDetails, BackendError> {
println!("Called");
let guard = state.read().await;
let client = guard.current_client()?;
let mixnet_contract = client.nymd.mixnet_contract_address();
@@ -126,6 +126,53 @@ pub async fn simulate_vesting_update_mixnode(
Ok(detailed_fee(client, result))
}
#[tauri::command]
pub async fn simulate_vesting_delegate_to_mixnode(
identity: &str,
amount: MajorCurrencyAmount,
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<FeeDetails, BackendError> {
let guard = state.read().await;
let amount = amount.into();
let client = guard.current_client()?;
let vesting_contract = client.nymd.vesting_contract_address();
let msg = client.nymd.wrap_contract_execute_message(
vesting_contract,
&ExecuteMsg::DelegateToMixnode {
mix_identity: identity.to_string(),
amount,
},
vec![],
)?;
let result = client.nymd.simulate(vec![msg]).await?;
Ok(detailed_fee(client, result))
}
#[tauri::command]
pub async fn simulate_vesting_undelegate_from_mixnode(
identity: &str,
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<FeeDetails, BackendError> {
let guard = state.read().await;
let client = guard.current_client()?;
let vesting_contract = client.nymd.vesting_contract_address();
let msg = client.nymd.wrap_contract_execute_message(
vesting_contract,
&ExecuteMsg::UndelegateFromMixnode {
mix_identity: identity.to_string(),
},
vec![],
)?;
let result = client.nymd.simulate(vec![msg]).await?;
Ok(detailed_fee(client, result))
}
#[tauri::command]
pub async fn simulate_withdraw_vested_coins(
amount: MajorCurrencyAmount,
@@ -0,0 +1,28 @@
import * as React from 'react';
import { ComponentMeta, ComponentStory } from '@storybook/react';
import { ConfirmTx } from './ConfirmTX';
import { ModalListItem } from './Modals/ModalListItem';
export default {
title: 'Wallet / Confirm Transaction',
component: ConfirmTx,
} as ComponentMeta<typeof ConfirmTx>;
const Template: ComponentStory<typeof ConfirmTx> = (args) => (
<ConfirmTx {...args}>
<ModalListItem label="Transaction type" value="Bond" divider />
<ModalListItem label="Current bond" value="100 NYM" divider />
<ModalListItem label="Additional bond" value="50 NYM" divider />
</ConfirmTx>
);
export const Default = Template.bind({});
Default.args = {
open: true,
header: 'Confirm transaction',
subheader: 'Confirm and proceed or cancel transaction',
fee: { amount: { amount: '0.001', denom: 'NYM' }, fee: { Auto: null } },
onClose: () => {},
onConfirm: async () => {},
onPrev: () => {},
};
+34
View File
@@ -0,0 +1,34 @@
import React from 'react';
import { FeeDetails } from '@nymproject/types';
import { Box, Button } from '@mui/material';
import { SimpleModal } from './Modals/SimpleModal';
import { ModalFee } from './Modals/ModalFee';
export const ConfirmTx: React.FC<{
open: boolean;
header: string;
subheader?: string;
fee: FeeDetails;
onConfirm: () => Promise<void>;
onClose?: () => void;
onPrev: () => void;
}> = ({ open, fee, onConfirm, onClose, header, subheader, onPrev, children }) => (
<SimpleModal
open={open}
header={header}
subHeader={subheader}
okLabel="Confirm"
onOk={onConfirm}
onClose={onClose}
SecondaryAction={
<Button fullWidth sx={{ mt: 1 }} size="large" onClick={onPrev}>
Cancel
</Button>
}
>
<Box sx={{ mt: 3 }}>
{children}
<ModalFee fee={fee} isLoading={false} />
</Box>
</SimpleModal>
);
@@ -13,7 +13,7 @@ export const OverSaturatedBlockerModal: React.FC<{
hideCloseIcon
displayErrorIcon
onClose={onClose}
onOk={onClose}
onOk={async () => onClose?.()}
header={header || 'Delegate'}
subHeader={subHeader || "This node is over saturated, you can't delegate more stake to it"}
okLabel={buttonText || 'Close'}
@@ -1,13 +1,17 @@
import React, { useEffect, useState } from 'react';
import { Box, Stack, Typography } from '@mui/material';
import React, { useState } from 'react';
import { Box, 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 { Console } from 'src/utils/console';
import { useGetFee } from 'src/hooks/useGetFee';
import { simulateDelegateToMixnode, simulateVestingDelegateToMixnode } from 'src/requests';
import { SimpleModal } from '../Modals/SimpleModal';
import { ModalListItem } from './ModalListItem';
import { ModalListItem } from '../Modals/ModalListItem';
import { checkTokenBalance, validateAmount, validateKey } from '../../utils';
import { TokenPoolSelector, TPoolOption } from '../TokenPoolSelector';
import { ConfirmTx } from '../ConfirmTX';
import { getMixnodeStakeSaturation } from '../../requests';
const MIN_AMOUNT_TO_DELEGATE = 10;
@@ -15,7 +19,7 @@ const MIN_AMOUNT_TO_DELEGATE = 10;
export const DelegateModal: React.FC<{
open: boolean;
onClose?: () => void;
onOk?: (identityKey: string, amount: MajorCurrencyAmount, tokenPool: TPoolOption) => Promise<void>;
onOk?: (identityKey: string, amount: MajorCurrencyAmount, tokenPool: TPoolOption, fee?: FeeDetails) => Promise<void>;
identityKey?: string;
onIdentityKeyChanged?: (identityKey: string) => void;
onAmountChanged?: (amount: string) => void;
@@ -26,7 +30,6 @@ export const DelegateModal: React.FC<{
estimatedReward?: number;
profitMarginPercentage?: number | null;
nodeUptimePercentage?: number | null;
feeOverride?: string;
currency: CurrencyDenom;
initialAmount?: string;
hasVestingContract: boolean;
@@ -41,7 +44,6 @@ export const DelegateModal: React.FC<{
identityKey: initialIdentityKey,
rewardInterval,
accountBalance,
feeOverride,
estimatedReward,
currency,
profitMarginPercentage,
@@ -51,66 +53,89 @@ export const DelegateModal: React.FC<{
}) => {
const [identityKey, setIdentityKey] = useState<string | undefined>(initialIdentityKey);
const [amount, setAmount] = useState<string | undefined>(initialAmount);
const [fee, setFee] = useState<string>();
const [tokenPool, setTokenPool] = useState<TPoolOption>('balance');
const [isValidated, setValidated] = useState<boolean>(false);
const [errorAmount, setErrorAmount] = useState<string>();
const [errorNodeSaturation, setErrorNodeSaturation] = useState<string>();
const [errorAmount, setErrorAmount] = useState<string | undefined>();
const [tokenPool, setTokenPool] = useState<TPoolOption>('balance');
const [errorIdentityKey, setErrorIdentityKey] = useState<string>();
const getFee = async () => {
if (feeOverride) setFee(feeOverride);
else {
const res = await getGasFee('BondMixnode');
setFee(res.amount);
}
};
const { fee, getFee, resetFeeState } = useGetFee();
const handleCheckStakeSaturation = async (identity: string) => {
setErrorNodeSaturation(undefined);
try {
const newSaturation = await getMixnodeStakeSaturation(identity);
if (newSaturation && newSaturation.saturation > 1) {
const saturationPercentage = Math.round(newSaturation.saturation * 100);
setErrorNodeSaturation(`This node is over saturated (${saturationPercentage}%), please select another node`);
return { isOverSaturated: true, saturationPercentage };
}
return { isOverSaturated: false, saturationPercentage: undefined };
} catch (e) {
Console.error('Error fetching the saturation, error:', e);
setErrorNodeSaturation(undefined);
return { isOverSaturated: false, saturationPercentage: undefined };
}
};
const validateIdentityKey = (isValid: boolean) => {
if (!isValid) {
setErrorIdentityKey('Identity key is invalid');
setErrorNodeSaturation(undefined);
} else {
setErrorIdentityKey(undefined);
const validate = async () => {
let newValidatedValue = true;
let errorAmountMessage;
let errorIdentityKeyMessage;
if (!identityKey || !validateKey(identityKey, 32)) {
newValidatedValue = false;
errorIdentityKeyMessage = undefined;
}
if (identityKey && validateKey(identityKey, 32)) {
const { isOverSaturated, saturationPercentage } = await handleCheckStakeSaturation(identityKey);
if (isOverSaturated) {
newValidatedValue = false;
errorIdentityKeyMessage = `This node is over saturated (${saturationPercentage}%), please select another node`;
}
}
if (amount && !(await validateAmount(amount, '0'))) {
newValidatedValue = false;
errorAmountMessage = 'Please enter a valid amount';
}
if (amount && Number(amount) < MIN_AMOUNT_TO_DELEGATE) {
errorAmountMessage = `Min. delegation amount: ${MIN_AMOUNT_TO_DELEGATE} ${currency}`;
newValidatedValue = false;
}
if (!amount?.length) {
newValidatedValue = false;
}
setErrorIdentityKey(errorIdentityKeyMessage);
setErrorAmount(errorAmountMessage);
setValidated(newValidatedValue);
};
const validateAmount = (newValue: MajorCurrencyAmount) => {
setErrorAmount(undefined);
if (newValue.amount && Number(newValue.amount) < MIN_AMOUNT_TO_DELEGATE) {
setErrorAmount(`Min. delegation amount: ${MIN_AMOUNT_TO_DELEGATE} ${currency}`);
}
if (!newValue.amount) {
setErrorAmount('Amount required');
}
};
const handleOk = () => {
const handleOk = async () => {
if (onOk && amount && identityKey) {
onOk(identityKey, { amount, denom: currency }, tokenPool);
onOk(identityKey, { amount, denom: currency }, tokenPool, fee);
}
};
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 === 'balance') {
getFee(simulateDelegateToMixnode, { identity, amount: value });
}
if (tokenPool === 'locked') {
getFee(simulateVestingDelegateToMixnode, { identity, amount: value });
}
};
const handleIdentityKeyChanged = (newIdentityKey: string) => {
setIdentityKey(newIdentityKey);
handleCheckStakeSaturation(newIdentityKey);
if (onIdentityKeyChanged) {
onIdentityKeyChanged(newIdentityKey);
@@ -119,27 +144,41 @@ export const DelegateModal: React.FC<{
const handleAmountChanged = (newAmount: MajorCurrencyAmount) => {
setAmount(newAmount.amount);
validateAmount(newAmount);
if (onAmountChanged) {
onAmountChanged(newAmount.amount);
}
};
useEffect(() => {
getFee();
}, []);
React.useEffect(() => {
validate();
}, [amount, identityKey]);
useEffect(() => {
if (!!errorIdentityKey || !!errorAmount || errorNodeSaturation) setValidated(false);
else setValidated(true);
}, [errorIdentityKey, errorAmount, errorNodeSaturation]);
if (fee) {
return (
<ConfirmTx
open
header="Delegation details"
fee={fee}
onClose={onClose}
onPrev={resetFeeState}
onConfirm={handleOk}
>
<ModalListItem label="Node identity key" value={identityKey} divider />
<ModalListItem label="Amount" value={`${amount} ${currency}`} divider />
</ConfirmTx>
);
}
return (
<SimpleModal
open={open}
onClose={onClose}
onOk={handleOk}
onOk={async () => {
if (identityKey && amount) {
handleConfirm({ identity: identityKey, value: { amount, denom: currency } });
}
}}
header={header || 'Delegate'}
subHeader="Delegate to mixnode"
okLabel={buttonText || 'Delegate stake'}
@@ -150,8 +189,7 @@ export const DelegateModal: React.FC<{
fullWidth
placeholder="Node identity key"
onChanged={handleIdentityKeyChanged}
onValidate={validateIdentityKey}
initialValue={initialIdentityKey}
initialValue={identityKey}
readOnly={Boolean(initialIdentityKey)}
textFieldProps={{
autoFocus: !initialIdentityKey,
@@ -163,7 +201,7 @@ export const DelegateModal: React.FC<{
variant="caption"
sx={{ color: 'error.main', mx: '14px', mt: '3px' }}
>
{errorNodeSaturation}
{errorIdentityKey}
</Typography>
<Box display="flex" gap={2} alignItems="center" sx={{ mt: 2 }}>
{hasVestingContract && <TokenPoolSelector disabled={false} onSelect={(pool) => setTokenPool(pool)} />}
@@ -171,7 +209,7 @@ export const DelegateModal: React.FC<{
required
fullWidth
placeholder="Amount"
initialValue={initialAmount}
initialValue={amount}
autoFocus={Boolean(initialIdentityKey)}
onChanged={handleAmountChanged}
/>
@@ -184,10 +222,10 @@ export const DelegateModal: React.FC<{
>
{errorAmount}
</Typography>
<Stack direction="row" justifyContent="space-between" my={3}>
<Typography fontWeight={600}>Account balance</Typography>
<Typography fontWeight={600}>{accountBalance}</Typography>
</Stack>
<Box sx={{ mt: 3 }}>
<ModalListItem label="Account balance" value={accountBalance} divider />
</Box>
<ModalListItem label="Rewards payout interval" value={rewardInterval} hidden divider />
<ModalListItem
label="Node profit margin"
@@ -203,14 +241,6 @@ export const DelegateModal: React.FC<{
/>
<ModalListItem label="Node est. reward per epoch" value={`${estimatedReward} ${currency}`} hidden divider />
<Stack direction="row" justifyContent="space-between" mt={4}>
<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>
</SimpleModal>
);
};
@@ -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}
@@ -117,9 +114,8 @@ export const Undelegate = () => {
<UndelegateModal
open={open}
onClose={() => setOpen(false)}
onOk={() => setOpen(false)}
onOk={async () => setOpen(false)}
currency="NYM"
fee={0.004375}
amount={150}
identityKey="AA6RfeY8DttMD3CQKoayV6mss5a5FC3RoH75Kmcujyxx"
usesVestingContractTokens={false}
@@ -1,23 +1,37 @@
import React from 'react';
import { Stack, Typography } from '@mui/material';
import { Box, Typography } from '@mui/material';
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
import React, { useEffect } from 'react';
import { FeeDetails } from '@nymproject/types';
import { useGetFee } from 'src/hooks/useGetFee';
import { simulateUndelegateFromMixnode, simulateVestingUndelegateFromMixnode } from 'src/requests';
import { ModalFee } from '../Modals/ModalFee';
import { ModalListItem } from '../Modals/ModalListItem';
import { SimpleModal } from '../Modals/SimpleModal';
export const UndelegateModal: React.FC<{
open: boolean;
onClose?: () => void;
onOk?: (identityKey: string, usesVestingContractTokens: boolean) => void;
onOk?: (identityKey: string, usesVestingContractTokens: boolean, fee?: FeeDetails) => void;
identityKey: string;
amount: number;
fee: number;
currency: string;
usesVestingContractTokens: boolean;
}> = ({ identityKey, open, onClose, onOk, amount, fee, currency, usesVestingContractTokens }) => {
const handleOk = () => {
}> = ({ identityKey, open, onClose, onOk, amount, currency, usesVestingContractTokens }) => {
const { fee, isFeeLoading, feeError, getFee } = useGetFee();
useEffect(() => {
if (usesVestingContractTokens) getFee(simulateVestingUndelegateFromMixnode, { identity: identityKey });
else {
getFee(simulateUndelegateFromMixnode, identityKey);
}
}, []);
const handleOk = async () => {
if (onOk) {
onOk(identityKey, usesVestingContractTokens);
onOk(identityKey, usesVestingContractTokens, fee);
}
};
return (
<SimpleModal
open={open}
@@ -26,6 +40,7 @@ export const UndelegateModal: React.FC<{
header="Undelegate"
subHeader="Undelegate from mixnode"
okLabel="Undelegate stake"
okDisabled={!fee}
>
<IdentityKeyFormField
readOnly
@@ -35,25 +50,15 @@ export const UndelegateModal: React.FC<{
showTickOnValid={false}
/>
<Stack direction="row" justifyContent="space-between" my={3}>
<Typography fontWeight={600}>Delegation amount:</Typography>
<Typography fontWeight={600}>
{amount} {currency}
</Typography>
</Stack>
<Box sx={{ mt: 3 }}>
<ModalListItem label="Delegation amount" value={`${amount} ${currency}`} divider />
</Box>
<Typography mb={5} fontSize="smaller">
Tokens will be transferred to account you are logged in with now
</Typography>
<Stack direction="row" justifyContent="space-between" mt={3}>
<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>
<ModalFee fee={fee} isLoading={isFeeLoading} error={feeError} />
</SimpleModal>
);
};
@@ -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 <CircularProgress size={15} />;
if (error && !isLoading) return 'n/a';
if (fee) return `${fee.amount?.amount} ${fee.amount?.denom}`;
return '-';
};
export const ModalFee = ({ fee, isLoading, error }: TFeeProps) => (
<ModalListItem label="Estimated fee for this operation" value={getValue({ fee, isLoading, error })} />
);
@@ -1,12 +1,12 @@
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;
divider?: boolean;
hidden?: boolean;
value: React.ReactNode;
value: string | React.ReactNode;
}> = ({ label, value, hidden, divider }) => (
<Box sx={{ display: hidden ? 'none' : 'block' }}>
<Stack direction="row" justifyContent="space-between">
@@ -58,7 +58,7 @@ export const Default = () => {
<SimpleModal
open={open}
onClose={() => setOpen(false)}
onOk={() => setOpen(false)}
onOk={async () => setOpen(false)}
header="This is a modal"
subHeader="This is a sub header"
okLabel="Click to continue"
@@ -87,7 +87,7 @@ export const NoSubheader = () => {
<SimpleModal
open={open}
onClose={() => setOpen(false)}
onOk={() => setOpen(false)}
onOk={async () => setOpen(false)}
header="This is a modal"
okLabel="Kaplow!"
>
@@ -114,7 +114,7 @@ export const hideCloseIcon = () => {
open={open}
hideCloseIcon
onClose={() => setOpen(false)}
onOk={() => setOpen(false)}
onOk={async () => setOpen(false)}
header="This is a modal"
okLabel="Kaplow!"
>
@@ -142,7 +142,7 @@ export const hideCloseIconAndDisplayErrorIcon = () => {
hideCloseIcon
displayErrorIcon
onClose={() => setOpen(false)}
onOk={() => setOpen(false)}
onOk={async () => setOpen(false)}
header="This modal announces an error !"
okLabel="Kaplow!"
sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}
@@ -11,12 +11,13 @@ export const SimpleModal: React.FC<{
headerStyles?: SxProps;
subHeaderStyles?: SxProps;
onClose?: () => void;
onOk?: () => void;
onOk?: () => Promise<void>;
header: string;
subHeader?: string;
okLabel: string;
okDisabled?: boolean;
sx?: SxProps;
SecondaryAction?: React.ReactNode;
}> = ({
open,
hideCloseIcon,
@@ -30,6 +31,7 @@ export const SimpleModal: React.FC<{
subHeader,
okLabel,
sx,
SecondaryAction,
children,
}) => (
<Modal open={open} onClose={onClose}>
@@ -55,9 +57,11 @@ export const SimpleModal: React.FC<{
{children}
<Button variant="contained" fullWidth sx={{ mt: 3 }} size="large" onClick={onOk} disabled={okDisabled}>
<Button variant="contained" fullWidth size="large" onClick={onOk} disabled={okDisabled} sx={{ mt: 2 }}>
{okLabel}
</Button>
{SecondaryAction}
</Box>
</Modal>
);
+1 -1
View File
@@ -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',
@@ -1,24 +1,40 @@
import React from 'react';
import React, { useEffect } 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 { 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';
export const CompoundModal: React.FC<{
open: boolean;
onClose?: () => void;
onOk?: (identityKey: string) => void;
onOk?: (identityKey: string, fee?: FeeDetails) => void;
identityKey: string;
amount: number;
fee: number;
minimum?: number;
currency: string;
message: string;
}> = ({ open, onClose, onOk, identityKey, amount, fee, currency, message }) => {
const handleOk = () => {
usesVestingTokens: boolean;
}> = ({ open, onClose, onOk, identityKey, amount, currency, message, usesVestingTokens }) => {
const { fee, isFeeLoading, feeError, getFee } = useGetFee();
const handleOk = async () => {
if (onOk) {
onOk(identityKey);
onOk(identityKey, fee);
}
};
useEffect(() => {
if (usesVestingTokens) getFee(simulateVestingCompoundDelgatorReward, identityKey);
else {
getFee(simulateCompoundDelgatorReward, identityKey);
}
}, []);
return (
<SimpleModal
open={open}
@@ -41,16 +57,8 @@ 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>
{amount < 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?
@@ -52,12 +52,12 @@ export const RedeemAllRewards = () => {
<RedeemModal
open={open}
onClose={() => setOpen(false)}
onOk={() => setOpen(false)}
onOk={async () => setOpen(false)}
message="Redeem all rewards"
currency="NYM"
identityKey="D88RfeY8DttMD3CQKoayV6mss5a5FC3RoH75Kmcujaaa"
fee={0.004375}
amount={425.65843}
usesVestingTokens={false}
/>
</>
);
@@ -71,12 +71,12 @@ export const RedeemRewardForMixnode = () => {
<RedeemModal
open={open}
onClose={() => setOpen(false)}
onOk={() => setOpen(false)}
onOk={async () => setOpen(false)}
message="Redeem rewards"
currency="NYM"
identityKey="D88RfeY8DttMD3CQKoayV6mss5a5FC3RoH75Kmcujaaa"
fee={0.004375}
amount={425.65843}
usesVestingTokens={false}
/>
</>
);
@@ -94,8 +94,8 @@ export const FeeIsMoreThanAllRewards = () => {
message="Redeem all rewards"
currency="NYM"
identityKey="D88RfeY8DttMD3CQKoayV6mss5a5FC3RoH75Kmcujaaa"
fee={0.004375}
amount={0.001}
usesVestingTokens={false}
/>
</>
);
@@ -109,12 +109,12 @@ export const FeeIsMoreThanMixnodeReward = () => {
<RedeemModal
open={open}
onClose={() => setOpen(false)}
onOk={() => setOpen(false)}
onOk={async () => setOpen(false)}
identityKey="D88RfeY8DttMD3CQKoayV6mss5a5FC3RoH75Kmcujaaa"
message="Redeem rewards"
currency="NYM"
fee={0.004375}
amount={0.001}
usesVestingTokens={false}
/>
</>
);
@@ -1,24 +1,41 @@
import React from 'react';
import React, { useEffect } 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 { 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';
export const RedeemModal: React.FC<{
open: boolean;
onClose?: () => void;
onOk?: (identityKey: string) => void;
onOk?: (identityKey: string, fee?: FeeDetails) => void;
identityKey: string;
amount: number;
fee: number;
minimum?: number;
currency: string;
message: string;
}> = ({ open, onClose, onOk, identityKey, amount, fee, currency, message }) => {
const handleOk = () => {
usesVestingTokens: boolean;
}> = ({ open, onClose, onOk, identityKey, amount, currency, message, usesVestingTokens }) => {
const { fee, isFeeLoading, feeError, getFee } = useGetFee();
const handleOk = async () => {
if (onOk) {
onOk(identityKey);
onOk(identityKey, fee);
}
};
useEffect(() => {
if (usesVestingTokens) {
getFee(simulateVestingClaimDelgatorReward, identityKey);
} else {
getFee(simulateClaimDelgatorReward, identityKey);
}
}, []);
return (
<SimpleModal
open={open}
@@ -41,16 +58,8 @@ 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>
{amount < 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?
+12 -2
View File
@@ -3,6 +3,7 @@ import { getDelegationSummary, undelegateAllFromMixnode } from 'src/requests/del
import {
DelegationEvent,
DelegationWithEverything,
FeeDetails,
MajorCurrencyAmount,
TransactionExecuteResult,
} from '@nymproject/types';
@@ -21,8 +22,13 @@ export type TDelegationContext = {
addDelegation: (
data: { identity: string; amount: MajorCurrencyAmount },
tokenPool: TPoolOption,
fee?: FeeDetails,
) => Promise<TransactionExecuteResult>;
undelegate: (identity: string, usesVestingContractTokens: boolean) => Promise<TransactionExecuteResult[]>;
undelegate: (
identity: string,
usesVestingContractTokens: boolean,
fee?: FeeDetails,
) => Promise<TransactionExecuteResult[]>;
};
export type TDelegationTransaction = {
@@ -50,7 +56,11 @@ export const DelegationContextProvider: FC<{
const [totalRewards, setTotalRewards] = useState<undefined | string>();
const [pendingDelegations, setPendingDelegations] = useState<DelegationEvent[]>();
const addDelegation = async (data: { identity: string; amount: MajorCurrencyAmount }, tokenPool: TPoolOption) => {
const addDelegation = async (
data: { identity: string; amount: MajorCurrencyAmount },
tokenPool: TPoolOption,
fee?: FeeDetails,
) => {
try {
let tx;
+3 -3
View File
@@ -1,6 +1,6 @@
import React, { createContext, FC, useContext, useEffect, useMemo, useState } from 'react';
import { Network } from 'src/types';
import { TransactionExecuteResult } from '@nymproject/types';
import { FeeDetails, TransactionExecuteResult } from '@nymproject/types';
import { useDelegationContext } from './delegations';
import { claimDelegatorRewards, compoundDelegatorRewards } from '../requests';
@@ -9,8 +9,8 @@ type TRewardsContext = {
error?: string;
totalRewards?: string;
refresh: () => Promise<void>;
claimRewards: (identity: string) => Promise<TransactionExecuteResult[]>;
compoundRewards: (identity: string) => Promise<TransactionExecuteResult[]>;
claimRewards: (identity: string, fee?: FeeDetails) => Promise<TransactionExecuteResult[]>;
compoundRewards: (identity: string, fee?: FeeDetails) => Promise<TransactionExecuteResult[]>;
};
export type TRewardsTransaction = {
+35
View File
@@ -0,0 +1,35 @@
import { FeeDetails } from '@nymproject/types';
import { useState } from 'react';
import { Console } from 'src/utils/console';
export function useGetFee() {
const [fee, setFee] = useState<FeeDetails>();
const [isFeeLoading, setIsFeeLoading] = useState(false);
const [feeError, setFeeError] = useState<string>();
async function getFee<T>(operation: (args: T) => Promise<FeeDetails>, args: T) {
try {
setIsFeeLoading(true);
const simulatedFee = await operation(args);
setFee(simulatedFee);
} catch (e) {
Console.error(e);
setFeeError(e as string);
}
setIsFeeLoading(false);
}
const resetFeeState = () => {
setFee(undefined);
setIsFeeLoading(false);
setFeeError(undefined);
};
return {
fee,
isFeeLoading,
feeError,
getFee,
resetFeeState,
};
}
+26 -16
View File
@@ -1,11 +1,12 @@
import React, { FC, useContext, useEffect, useState } from 'react';
import { Box, Button, Paper, Stack, Typography } from '@mui/material';
import { DelegationWithEverything, MajorCurrencyAmount } from '@nymproject/types';
import { DelegationWithEverything, FeeDetails, MajorCurrencyAmount } from '@nymproject/types';
import { Link } from '@nymproject/react/link/Link';
import { AppContext, urls } from 'src/context/main';
import { DelegationList } from 'src/components/Delegation/DelegationList';
import { PendingEvents } from 'src/components/Delegation/PendingEvents';
import { TPoolOption } from 'src/components';
import { Console } from 'src/utils/console';
import { CompoundModal } from 'src/components/Rewards/CompoundModal';
import { OverSaturatedBlockerModal } from 'src/components/Delegation/DelegateBlocker';
import { getSpendableCoins, userBalance } from 'src/requests';
@@ -17,7 +18,6 @@ import { UndelegateModal } from '../../components/Delegation/UndelegateModal';
import { DelegationListItemActions } from '../../components/Delegation/DelegationActions';
import { RedeemModal } from '../../components/Rewards/RedeemModal';
import { DelegationModal, DelegationModalProps } from '../../components/Delegation/DelegationModal';
import { Console } from '../../utils/console';
export const Delegation: FC = () => {
const [showNewDelegationModal, setShowNewDelegationModal] = useState<boolean>(false);
@@ -99,7 +99,12 @@ export const Delegation: FC = () => {
}
};
const handleNewDelegation = async (identityKey: string, amount: MajorCurrencyAmount, tokenPool: TPoolOption) => {
const handleNewDelegation = async (
identityKey: string,
amount: MajorCurrencyAmount,
tokenPool: TPoolOption,
fee?: FeeDetails,
) => {
setConfirmationModalProps({
status: 'loading',
action: 'delegate',
@@ -113,6 +118,7 @@ export const Delegation: FC = () => {
amount,
},
tokenPool,
fee,
);
const balances = await getAllBalances();
@@ -136,7 +142,12 @@ export const Delegation: FC = () => {
}
};
const handleDelegateMore = async (identityKey: string, amount: MajorCurrencyAmount, tokenPool: TPoolOption) => {
const handleDelegateMore = async (
identityKey: string,
amount: MajorCurrencyAmount,
tokenPool: TPoolOption,
fee?: FeeDetails,
) => {
if (currentDelegationListActionItem?.node_identity !== identityKey || !clientDetails) {
setConfirmationModalProps({
status: 'error',
@@ -159,6 +170,7 @@ export const Delegation: FC = () => {
amount,
},
tokenPool,
fee,
);
const balances = await getAllBalances();
@@ -180,7 +192,7 @@ export const Delegation: FC = () => {
}
};
const handleUndelegate = async (identityKey: string, usesVestingContractTokens: boolean) => {
const handleUndelegate = async (identityKey: string, usesVestingContractTokens: boolean, fee?: FeeDetails) => {
setConfirmationModalProps({
status: 'loading',
action: 'undelegate',
@@ -189,7 +201,7 @@ export const Delegation: FC = () => {
setCurrentDelegationListActionItem(undefined);
try {
const txs = await undelegate(identityKey, usesVestingContractTokens);
const txs = await undelegate(identityKey, usesVestingContractTokens, fee);
const balances = await getAllBalances();
setConfirmationModalProps({
@@ -211,7 +223,7 @@ export const Delegation: FC = () => {
}
};
const handleRedeem = async (identityKey: string) => {
const handleRedeem = async (identityKey: string, fee?: FeeDetails) => {
setConfirmationModalProps({
status: 'loading',
action: 'redeem',
@@ -220,7 +232,7 @@ export const Delegation: FC = () => {
setCurrentDelegationListActionItem(undefined);
try {
const txs = await claimRewards(identityKey);
const txs = await claimRewards(identityKey, fee);
const bal = await userBalance();
setConfirmationModalProps({
status: 'success',
@@ -241,7 +253,7 @@ export const Delegation: FC = () => {
}
};
const handleCompound = async (identityKey: string) => {
const handleCompound = async (identityKey: string, fee?: FeeDetails) => {
setConfirmationModalProps({
status: 'loading',
action: 'compound',
@@ -250,7 +262,7 @@ export const Delegation: FC = () => {
setCurrentDelegationListActionItem(undefined);
try {
const txs = await compoundRewards(identityKey);
const txs = await compoundRewards(identityKey, fee);
const bal = await userBalance();
setConfirmationModalProps({
status: 'success',
@@ -336,7 +348,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}
@@ -352,7 +363,6 @@ export const Delegation: FC = () => {
onOk={handleUndelegate}
usesVestingContractTokens={currentDelegationListActionItem.uses_vesting_contract_tokens}
currency={currentDelegationListActionItem.amount.denom}
fee={0.1}
amount={+currentDelegationListActionItem.amount.amount}
identityKey={currentDelegationListActionItem.node_identity}
/>
@@ -362,12 +372,12 @@ export const Delegation: FC = () => {
<RedeemModal
open={showRedeemRewardsModal}
onClose={() => setShowRedeemRewardsModal(false)}
onOk={(identity) => handleRedeem(identity)}
onOk={(identity, fee) => handleRedeem(identity, fee)}
message="Redeem rewards"
currency={clientDetails!.denom}
identityKey={currentDelegationListActionItem?.node_identity}
fee={0.004375}
amount={+currentDelegationListActionItem.accumulated_rewards.amount}
usesVestingTokens={currentDelegationListActionItem.uses_vesting_contract_tokens}
/>
)}
@@ -375,12 +385,12 @@ export const Delegation: FC = () => {
<CompoundModal
open={showCompoundRewardsModal}
onClose={() => setShowCompoundRewardsModal(false)}
onOk={(identity) => handleCompound(identity)}
onOk={(identity, fee) => handleCompound(identity, fee)}
message="Compound rewards"
currency={clientDetails!.denom}
identityKey={currentDelegationListActionItem?.node_identity}
fee={0.004375}
amount={+currentDelegationListActionItem.accumulated_rewards.amount}
usesVestingTokens={currentDelegationListActionItem.uses_vesting_contract_tokens}
/>
)}
+11 -2
View File
@@ -3,6 +3,7 @@ import {
DelegationsSummaryResponse,
TransactionExecuteResult,
MajorCurrencyAmount,
FeeDetails,
} from '@nymproject/types';
import { invokeWrapper } from './wrapper';
@@ -14,8 +15,16 @@ export const getDelegationSummary = async () => invokeWrapper<DelegationsSummary
export const undelegateFromMixnode = async (identity: string) =>
invokeWrapper<TransactionExecuteResult>('undelegate_from_mixnode', { identity });
export const undelegateAllFromMixnode = async (identity: string, usesVestingContractTokens: boolean) =>
invokeWrapper<TransactionExecuteResult[]>('undelegate_all_from_mixnode', { identity, usesVestingContractTokens });
export const undelegateAllFromMixnode = async (
identity: string,
usesVestingContractTokens: boolean,
fee?: FeeDetails,
) =>
invokeWrapper<TransactionExecuteResult[]>('undelegate_all_from_mixnode', {
identity,
usesVestingContractTokens,
fee: fee?.fee,
});
export const delegateToMixnode = async ({ identity, amount }: { identity: string; amount: MajorCurrencyAmount }) =>
invokeWrapper<TransactionExecuteResult>('delegate_to_mixnode', { identity, amount });
+1
View File
@@ -7,3 +7,4 @@ export * from './queries';
export * from './utils';
export * from './delegation';
export * from './rewards';
export * from './simulate';
+11 -5
View File
@@ -1,4 +1,4 @@
import { TransactionExecuteResult } from '@nymproject/types';
import { FeeDetails, TransactionExecuteResult } from '@nymproject/types';
import { invokeWrapper } from './wrapper';
export const claimOperatorRewards = async () => invokeWrapper<TransactionExecuteResult[]>('claim_operator_reward');
@@ -6,8 +6,14 @@ export const claimOperatorRewards = async () => invokeWrapper<TransactionExecute
export const compoundOperatorRewards = async () =>
invokeWrapper<TransactionExecuteResult[]>('compound_operator_reward');
export const claimDelegatorRewards = async (mixIdentity: string) =>
invokeWrapper<TransactionExecuteResult[]>('claim_locked_and_unlocked_delegator_reward', { mixIdentity });
export const claimDelegatorRewards = async (mixIdentity: string, fee?: FeeDetails) =>
invokeWrapper<TransactionExecuteResult[]>('claim_locked_and_unlocked_delegator_reward', {
mixIdentity,
fee: fee?.fee,
});
export const compoundDelegatorRewards = async (mixIdentity: String) =>
invokeWrapper<TransactionExecuteResult[]>('compound_locked_and_unlocked_delegator_reward', { mixIdentity });
export const compoundDelegatorRewards = async (mixIdentity: String, fee?: FeeDetails) =>
invokeWrapper<TransactionExecuteResult[]>('compound_locked_and_unlocked_delegator_reward', {
mixIdentity,
fee: fee?.fee,
});
+22 -4
View File
@@ -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<FeeDetails>('simulate_bond_gateway', args);
@@ -11,11 +11,26 @@ export const simulateUnbondMixnode = async (args: any) => invokeWrapper<FeeDetai
export const simulateUpdateMixnode = async (args: any) => invokeWrapper<FeeDetails>('simulate_update_mixnode', args);
export const simulateDelegateToMixnode = async (args: any) =>
export const simulateDelegateToMixnode = async (args: { identity: string; amount: MajorCurrencyAmount }) =>
invokeWrapper<FeeDetails>('simulate_delegate_to_mixnode', args);
export const simulateUndelegateFromMixnode = async (args: any) =>
invokeWrapper<FeeDetails>('simulate_undelegate_from_mixnode,', args);
export const simulateUndelegateFromMixnode = async (identity: string) =>
invokeWrapper<FeeDetails>('simulate_undelegate_from_mixnode', { identity });
export const simulateCompoundDelgatorReward = async (identity: string) =>
invokeWrapper<FeeDetails>('simulate_compound_delegator_reward', { mixIdentity: identity });
export const simulateClaimDelgatorReward = async (identity: string) =>
invokeWrapper<FeeDetails>('simulate_claim_delegator_reward', { mixIdentity: identity });
export const simulateVestingClaimDelgatorReward = async (identity: string) =>
invokeWrapper<FeeDetails>('simulate_vesting_claim_delegator_reward', { mixIdentity: identity });
export const simulateVestingCompoundDelgatorReward = async (identity: string) =>
invokeWrapper<FeeDetails>('simulate_vesting_compound_delegator_reward', { mixIdentity: identity });
export const simulateVestingUndelegateFromMixnode = async (args: any) =>
invokeWrapper<FeeDetails>('simulate_vesting_undelegate_from_mixnode', args);
export const simulateVestingBondGateway = async (args: any) =>
invokeWrapper<FeeDetails>('simulate_vesting_bond_gateway', args);
@@ -23,6 +38,9 @@ export const simulateVestingBondGateway = async (args: any) =>
export const simulateVestingUnbondGateway = async (args: any) =>
invokeWrapper<FeeDetails>('simulate_vesting_unbond_gateway', args);
export const simulateVestingDelegateToMixnode = async (args: { identity: string }) =>
invokeWrapper<FeeDetails>('simulate_vesting_delegate_to_mixnode', args);
export const simulateVestingBondMixnode = async (args: any) =>
invokeWrapper<FeeDetails>('simulate_vesting_bond_mixnode', args);
+4 -1
View File
@@ -1,5 +1,6 @@
import {
EnumNodeType,
FeeDetails,
Gateway,
MajorCurrencyAmount,
MixNode,
@@ -65,10 +66,12 @@ export const vestingUpdateMixnode = async (profitMarginPercent: number) =>
export const vestingDelegateToMixnode = async ({
identity,
amount,
fee,
}: {
identity: string;
amount: MajorCurrencyAmount;
}) => invokeWrapper<TransactionExecuteResult>('vesting_delegate_to_mixnode', { identity, amount });
fee?: FeeDetails;
}) => invokeWrapper<TransactionExecuteResult>('vesting_delegate_to_mixnode', { identity, amount, fee: fee?.fee });
export const vestingUndelegateFromMixnode = async (identity: string) =>
invokeWrapper<TransactionExecuteResult>('vesting_undelegate_from_mixnode', { identity });
+14
View File
@@ -2,6 +2,7 @@ import { appWindow } from '@tauri-apps/api/window';
import bs58 from 'bs58';
import { valid } from 'semver';
import { isValidRawCoin, MajorAmountString } from '@nymproject/types';
import { TPoolOption } from 'src/components';
import { getLockedCoins, getSpendableCoins, userBalance } from '../requests';
import { Console } from './console';
@@ -107,3 +108,16 @@ export const maximizeWindow = async () => {
export function removeObjectDuplicates<T extends object, K extends keyof T>(arr: T[], id: K) {
return arr.filter((v, i, a) => a.findIndex((v2) => v2[id] === v[id]) === i);
}
export const checkTokenBalance = async (tokenPool: TPoolOption, amount: string) => {
let hasEnoughFunds = false;
if (tokenPool === 'locked') {
hasEnoughFunds = await checkHasEnoughLockedTokens(amount);
}
if (tokenPool === 'balance') {
hasEnoughFunds = await checkHasEnoughFunds(amount);
}
return hasEnoughFunds;
};