From 61fcd4ac69a7467683cafca5e3748f52bd2baf78 Mon Sep 17 00:00:00 2001 From: Mark Sinclair Date: Thu, 18 Jul 2024 17:19:19 +0100 Subject: [PATCH] Dialog and mock for migrating vesting contract delegations --- nym-wallet/.storybook/mocks/tauri/index.js | 72 ++++++++++++++++++ .../components/VestingWarningModal/index.tsx | 31 ++++++++ nym-wallet/src/pages/delegation/index.tsx | 75 ++++++++++++++++--- nym-wallet/src/requests/delegation.ts | 3 + 4 files changed, 172 insertions(+), 9 deletions(-) create mode 100644 nym-wallet/src/components/VestingWarningModal/index.tsx diff --git a/nym-wallet/.storybook/mocks/tauri/index.js b/nym-wallet/.storybook/mocks/tauri/index.js index e2bf50b00d..3a40c8158f 100644 --- a/nym-wallet/.storybook/mocks/tauri/index.js +++ b/nym-wallet/.storybook/mocks/tauri/index.js @@ -1,3 +1,55 @@ +const delegations = [ + { + mix_id: 1234, + node_identity: 'FiojKW7oY9WQmLCiYAsCA21tpowZHS6zcUoyYm319p6Z', + delegated_on_iso_datetime: new Date(2021, 1, 1).toDateString(), + unclaimed_rewards: { amount: '0.05', denom: 'nym' }, + amount: { amount: '10', denom: 'nym' }, + owner: '', + block_height: BigInt(100), + cost_params: { + profit_margin_percent: '0.04', + interval_operating_cost: { + amount: '20', + denom: 'nym', + }, + }, + stake_saturation: '0.2', + avg_uptime_percent: 0.5, + accumulated_by_delegates: { amount: '0', denom: 'nym' }, + accumulated_by_operator: { amount: '0', denom: 'nym' }, + uses_vesting_contract_tokens: false, + pending_events: [], + mixnode_is_unbonding: false, + errors: null, + }, + { + mix_id: 5678, + node_identity: 'DT8S942S8AQs2zKHS9SVo1GyHmuca3pfL2uLhLksJ3D8', + unclaimed_rewards: { amount: '0.1', denom: 'nym' }, + amount: { amount: '100', denom: 'nym' }, + delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(), + owner: '', + block_height: BigInt(4000), + stake_saturation: '0.5', + avg_uptime_percent: 0.1, + cost_params: { + profit_margin_percent: '0.04', + interval_operating_cost: { + amount: '60', + denom: 'nym', + }, + }, + accumulated_by_delegates: { amount: '0', denom: 'nym' }, + accumulated_by_operator: { amount: '0', denom: 'nym' }, + uses_vesting_contract_tokens: true, + pending_events: [], + mixnode_is_unbonding: false, + errors: null, + }, +]; + + /** * This is a mock for Tauri's API package (@tauri-apps/api), to prevent stories from being excluded, because they either use * or import dependencies that use Tauri. @@ -29,6 +81,26 @@ module.exports = { }, }; } + case 'get_delegation_summary': { + return { + delegations, + total_delegations: { + amount: '1000', + denom: 'nymt', + }, + total_rewards: { + amount: '42', + denom: 'nymt', + }, + }; + } + case 'get_pending_delegation_events' : { + return []; + } + case 'migrate_vested_delegations': { + delegations[1].uses_vesting_contract_tokens = false; + return {}; + } } console.error( diff --git a/nym-wallet/src/components/VestingWarningModal/index.tsx b/nym-wallet/src/components/VestingWarningModal/index.tsx new file mode 100644 index 0000000000..08ec056853 --- /dev/null +++ b/nym-wallet/src/components/VestingWarningModal/index.tsx @@ -0,0 +1,31 @@ +import React, { FC } from 'react'; +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; + +export const VestingWarningModal: FC<{ isVisible: boolean; handleClose: (result: 'yes' | 'no') => void }> = ({ + isVisible, + handleClose, +}) => ( + + Migrate all of your delegations? + + + By clicking yes we will migrate your delegations to the mixnet contract. + + + The operation will be instant, you will keep your rewards and they will continue to accumulate. Once migrated, + you will be able to withdraw you rewards. + + + + + + + +); diff --git a/nym-wallet/src/pages/delegation/index.tsx b/nym-wallet/src/pages/delegation/index.tsx index 3e985a3526..6d6c32d204 100644 --- a/nym-wallet/src/pages/delegation/index.tsx +++ b/nym-wallet/src/pages/delegation/index.tsx @@ -1,5 +1,5 @@ import React, { FC, useContext, useEffect, useState } from 'react'; -import { Box, Button, Paper, Stack, Typography } from '@mui/material'; +import { Alert, AlertTitle, Box, Button, Paper, Stack, Typography } from '@mui/material'; import { Theme, useTheme } from '@mui/material/styles'; import { DecCoin, decimalToFloatApproximation, DelegationWithEverything, FeeDetails } from '@nymproject/types'; import { Link } from '@nymproject/react/link/Link'; @@ -8,11 +8,11 @@ import { DelegationList } from 'src/components/Delegation/DelegationList'; import { TPoolOption } from 'src/components'; import { Console } from 'src/utils/console'; import { OverSaturatedBlockerModal } from 'src/components/Delegation/DelegateBlocker'; -import { getSpendableCoins, userBalance } from 'src/requests'; +import { getSpendableCoins, migrateVestedDelegations, userBalance } from 'src/requests'; import { LoadingModal } from 'src/components/Modals/LoadingModal'; import { getIntervalAsDate, toPercentIntegerString } from 'src/utils'; import { RewardsSummary } from '../../components/Rewards/RewardsSummary'; -import { DelegationContextProvider, TDelegations, useDelegationContext } from '../../context/delegations'; +import { DelegationContextProvider, isDelegation, TDelegations, useDelegationContext } from '../../context/delegations'; import { RewardsContextProvider, useRewardsContext } from '../../context/rewards'; import { DelegateModal } from '../../components/Delegation/DelegateModal'; import { UndelegateModal } from '../../components/Delegation/UndelegateModal'; @@ -20,6 +20,7 @@ import { DelegationListItemActions } from '../../components/Delegation/Delegatio import { RedeemModal } from '../../components/Rewards/RedeemModal'; import { DelegationModal, DelegationModalProps } from '../../components/Delegation/DelegationModal'; import { backDropStyles, modalStyles } from '../../../.storybook/storiesStyles'; +import { VestingWarningModal } from '../../components/VestingWarningModal'; const storybookStyles = (theme: Theme, isStorybook?: boolean, backdropProps?: object) => isStorybook @@ -38,6 +39,8 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { const [currentDelegationListActionItem, setCurrentDelegationListActionItem] = useState(); const [saturationError, setSaturationError] = useState<{ action: 'compound' | 'delegate'; saturation: string }>(); const [nextEpoch, setNextEpoch] = useState(); + const [showVestingWarningModal, setShowVestingWarningModal] = useState(false); + const [showVestingMigrationProgressModal, setShowVestingMigrationProgressModal] = useState(false); const theme = useTheme(); const { @@ -57,6 +60,11 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { refresh: refreshDelegations, } = useDelegationContext(); + const delegationsUseVestingTokens: boolean = React.useMemo( + () => Boolean(delegations?.filter((d) => isDelegation(d) && d.uses_vesting_contract_tokens).length), + [delegations], + ); + const { refresh: refreshRewards, claimRewards } = useRewardsContext(); const refresh = async () => Promise.all([refreshDelegations(), refreshRewards()]); @@ -105,6 +113,13 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { return () => clearInterval(timer); }, []); + const doMigrateNow = async () => { + setShowVestingMigrationProgressModal(true); + await migrateVestedDelegations(); + await refresh(); + setShowVestingMigrationProgressModal(false); + }; + useEffect(() => { refreshWithIntervalUpdate(); }, [clientDetails, confirmationModalProps]); @@ -119,6 +134,11 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { return; } + if (item.uses_vesting_contract_tokens) { + setShowVestingWarningModal(true); + return; + } + setCurrentDelegationListActionItem(item); // eslint-disable-next-line default-case switch (action) { @@ -305,12 +325,49 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { const delegationsComponent = (delegationItems: TDelegations | undefined) => { if (delegationItems && Boolean(delegationItems?.length)) { return ( - + <> + {delegationsUseVestingTokens && ( + <> + + + Some of your delegations are using tokens from the vesting contract! + + + In order to claim your rewards, you will need to migrate them out of the vesting contract.{' '} + + + Never fear, if you do not migrate them,{' '} + you will continue to get rewards. However, please migrate your delegations as soon as + possible. + + + + { + setShowVestingWarningModal(false); + if (result === 'yes') { + doMigrateNow(); + } + }} + /> + {showVestingMigrationProgressModal && } + + )} + + ); } diff --git a/nym-wallet/src/requests/delegation.ts b/nym-wallet/src/requests/delegation.ts index d68d8a9084..1ab73ba655 100644 --- a/nym-wallet/src/requests/delegation.ts +++ b/nym-wallet/src/requests/delegation.ts @@ -31,3 +31,6 @@ export const undelegateAllFromMixnode = async ( export const delegateToMixnode = async (mixId: number, amount: DecCoin, fee?: Fee) => invokeWrapper('delegate_to_mixnode', { mixId, amount, fee }); + +export const migrateVestedDelegations = async () => + invokeWrapper('migrate_vested_delegations');