From db08a177c49c73d694b969edae9312938f069fac Mon Sep 17 00:00:00 2001 From: Art Date: Wed, 24 Aug 2022 00:09:08 +0200 Subject: [PATCH] fix(#806): dissasociation without vega wallet (#1075) * fix: dissasociation without vega wallet (806) * fix: removed unused import * chore: removed redundant func prepend0xIfNeeded * Update apps/token/src/routes/staking/disassociate/disassociate-page.tsx Co-authored-by: candida-d <62548908+candida-d@users.noreply.github.com> * Update apps/token/src/routes/staking/disassociate/disassociate-page.tsx Co-authored-by: candida-d <62548908+candida-d@users.noreply.github.com> Co-authored-by: candida-d <62548908+candida-d@users.noreply.github.com> --- .../components/staking-method-radio/index.tsx | 1 + .../components/wallet-card/wallet-card.tsx | 21 +-- apps/token/src/lib/format-number.spec.ts | 33 ++++ apps/token/src/lib/format-number.ts | 30 +++- apps/token/src/lib/truncate-middle.spec.ts | 15 ++ apps/token/src/lib/truncate-middle.ts | 1 + .../disassociate/contract-disassociate.tsx | 41 ----- .../disassociate-page-container.tsx | 11 +- .../disassociate-page-no-vega.tsx | 53 ------ .../disassociate/disassociate-page.tsx | 170 ++++++++++++------ .../src/routes/staking/disassociate/hooks.ts | 42 +++-- .../disassociate/wallet-disassociate.tsx | 41 ----- .../src/utils/prepend-0x.test.ts | 11 +- libs/smart-contracts/src/utils/prepend-0x.ts | 2 +- 14 files changed, 238 insertions(+), 234 deletions(-) create mode 100644 apps/token/src/lib/format-number.spec.ts create mode 100644 apps/token/src/lib/truncate-middle.spec.ts delete mode 100644 apps/token/src/routes/staking/disassociate/contract-disassociate.tsx delete mode 100644 apps/token/src/routes/staking/disassociate/disassociate-page-no-vega.tsx delete mode 100644 apps/token/src/routes/staking/disassociate/wallet-disassociate.tsx diff --git a/apps/token/src/components/staking-method-radio/index.tsx b/apps/token/src/components/staking-method-radio/index.tsx index 279547a93..2b0e7e38f 100644 --- a/apps/token/src/components/staking-method-radio/index.tsx +++ b/apps/token/src/components/staking-method-radio/index.tsx @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'; export enum StakingMethod { Contract = 'Contract', Wallet = 'Wallet', + Unknown = 'Unknown', } export const StakingMethodRadio = ({ diff --git a/apps/token/src/components/wallet-card/wallet-card.tsx b/apps/token/src/components/wallet-card/wallet-card.tsx index 44c369496..b56a00de0 100644 --- a/apps/token/src/components/wallet-card/wallet-card.tsx +++ b/apps/token/src/components/wallet-card/wallet-card.tsx @@ -3,25 +3,8 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { useAnimateValue } from '../../hooks/use-animate-value'; -import { BigNumber } from '../../lib/bignumber'; -import { formatNumber } from '../../lib/format-number'; - -const useNumberParts = ( - value: BigNumber | null | undefined, - decimals: number -) => { - return React.useMemo(() => { - if (!value) { - return ['0', '0'.repeat(decimals)]; - } - // @ts-ignore confident not undefined - const separator = BigNumber.config().FORMAT.decimalSeparator as string; - const [integers, decimalsPlaces] = formatNumber(value, 18) - .toString() - .split(separator); - return [integers, decimalsPlaces]; - }, [decimals, value]); -}; +import type { BigNumber } from '../../lib/bignumber'; +import { useNumberParts } from '../../lib/format-number'; interface WalletCardProps { children: React.ReactNode; diff --git a/apps/token/src/lib/format-number.spec.ts b/apps/token/src/lib/format-number.spec.ts new file mode 100644 index 000000000..a39f27409 --- /dev/null +++ b/apps/token/src/lib/format-number.spec.ts @@ -0,0 +1,33 @@ +import { BigNumber } from './bignumber'; +import { + formatNumber, + formatNumberPercentage, + toNumberParts, +} from './format-number'; + +describe('formatNumber and formatNumberPercentage', () => { + it.each([ + { v: new BigNumber(123), d: 3, o: '123.000' }, + { v: new BigNumber(123.123), d: 3, o: '123.123' }, + { v: new BigNumber(123.123), d: 6, o: '123.123000' }, + { v: new BigNumber(123.123), d: 0, o: '123' }, + { v: new BigNumber(123), d: undefined, o: '123.00' }, // it default to 2 decimal places + ])('formats given number correctly', ({ v, d, o }) => { + expect(formatNumber(v, d)).toStrictEqual(o); + expect(formatNumberPercentage(v, d)).toStrictEqual(`${o}%`); + }); +}); + +describe('toNumberParts', () => { + it.each([ + { v: null, d: 3, o: ['0', '000'] }, + { v: undefined, d: 3, o: ['0', '000'] }, + { v: new BigNumber(123), d: 3, o: ['123', '000'] }, + { v: new BigNumber(123.123), d: 3, o: ['123', '123'] }, + { v: new BigNumber(123.123), d: 6, o: ['123', '123000'] }, + { v: new BigNumber(123.123), d: 0, o: ['123', ''] }, + { v: new BigNumber(123), d: undefined, o: ['123', '000000000000000000'] }, + ])('returns correct tuple given the different arguments', ({ v, d, o }) => { + expect(toNumberParts(v, d)).toStrictEqual(o); + }); +}); diff --git a/apps/token/src/lib/format-number.ts b/apps/token/src/lib/format-number.ts index 01270e06a..e2c79fb3c 100644 --- a/apps/token/src/lib/format-number.ts +++ b/apps/token/src/lib/format-number.ts @@ -1,4 +1,5 @@ -import type { BigNumber } from './bignumber'; +import React from 'react'; +import { BigNumber } from './bignumber'; export const formatNumber = (value: BigNumber, decimals?: number) => { const decimalPlaces = @@ -6,8 +7,27 @@ export const formatNumber = (value: BigNumber, decimals?: number) => { return value.dp(decimalPlaces).toFormat(decimalPlaces); }; -export const formatNumberPercentage = (value: BigNumber, decimals?: number) => { - const decimalPlaces = - typeof decimals === 'undefined' ? Math.max(value.dp(), 2) : decimals; - return `${value.dp(decimalPlaces).toFormat(decimalPlaces)}%`; +export const formatNumberPercentage = (value: BigNumber, decimals?: number) => + `${formatNumber(value, decimals)}%`; + +export const toNumberParts = ( + value: BigNumber | null | undefined, + decimals = 18 +): [integers: string, decimalPlaces: string] => { + if (!value) { + return ['0', '0'.repeat(decimals)]; + } + // @ts-ignore confident not undefined + const separator = BigNumber.config().FORMAT.decimalSeparator as string; + const [integers, decimalsPlaces] = formatNumber(value, decimals) + .toString() + .split(separator); + return [integers, decimalsPlaces || '']; +}; + +export const useNumberParts = ( + value: BigNumber | null | undefined, + decimals: number +): [integers: string, decimalPlaces: string] => { + return React.useMemo(() => toNumberParts(value, decimals), [decimals, value]); }; diff --git a/apps/token/src/lib/truncate-middle.spec.ts b/apps/token/src/lib/truncate-middle.spec.ts new file mode 100644 index 000000000..453625d5c --- /dev/null +++ b/apps/token/src/lib/truncate-middle.spec.ts @@ -0,0 +1,15 @@ +import { truncateMiddle } from './truncate-middle'; + +describe('truncateMiddle', () => { + it.each([ + { i: '1234567890134567890', o: '123456\u20267890' }, + { i: '12345678901', o: '123456\u20268901' }, + { i: '1234567890', o: '1234567890' }, + { i: '123456', o: '123456' }, + ])( + 'truncates the middle section of any long string (address)', + ({ i, o }) => { + expect(truncateMiddle(i)).toStrictEqual(o); + } + ); +}); diff --git a/apps/token/src/lib/truncate-middle.ts b/apps/token/src/lib/truncate-middle.ts index 240e97b54..10b2c6798 100644 --- a/apps/token/src/lib/truncate-middle.ts +++ b/apps/token/src/lib/truncate-middle.ts @@ -1,4 +1,5 @@ export function truncateMiddle(address: string) { + if (address.length < 11) return address; return ( address.slice(0, 6) + '\u2026' + diff --git a/apps/token/src/routes/staking/disassociate/contract-disassociate.tsx b/apps/token/src/routes/staking/disassociate/contract-disassociate.tsx deleted file mode 100644 index 14d1fff1d..000000000 --- a/apps/token/src/routes/staking/disassociate/contract-disassociate.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; - -import { TokenInput } from '../../../components/token-input'; -import { useAppState } from '../../../contexts/app-state/app-state-context'; - -export const ContractDisassociate = ({ - perform, - amount, - setAmount, -}: { - perform: () => void; - amount: string; - setAmount: React.Dispatch>; -}) => { - const { - appState: { lien }, - } = useAppState(); - const { t } = useTranslation(); - - if (lien.isEqualTo('0')) { - return ( -
- {t( - 'You have no VEGA tokens currently staked through your connected Eth wallet.' - )} -
- ); - } - - return ( - - ); -}; diff --git a/apps/token/src/routes/staking/disassociate/disassociate-page-container.tsx b/apps/token/src/routes/staking/disassociate/disassociate-page-container.tsx index 4edc0e4b1..74ab4ceef 100644 --- a/apps/token/src/routes/staking/disassociate/disassociate-page-container.tsx +++ b/apps/token/src/routes/staking/disassociate/disassociate-page-container.tsx @@ -1,17 +1,12 @@ import { StakingWalletsContainer } from '../staking-wallets-container'; import { DisassociatePage } from './disassociate-page'; -import { DisassociatePageNoVega } from './disassociate-page-no-vega'; export const DisassociateContainer = () => { return ( - {({ address, currVegaKey = null }) => - currVegaKey ? ( - - ) : ( - - ) - } + {({ address, currVegaKey = null }) => ( + + )} ); }; diff --git a/apps/token/src/routes/staking/disassociate/disassociate-page-no-vega.tsx b/apps/token/src/routes/staking/disassociate/disassociate-page-no-vega.tsx deleted file mode 100644 index c9986bf46..000000000 --- a/apps/token/src/routes/staking/disassociate/disassociate-page-no-vega.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; - -import { - StakingMethod, - StakingMethodRadio, -} from '../../../components/staking-method-radio'; -import { useSearchParams } from '../../../hooks/use-search-params'; -import { ConnectToVega } from '../connect-to-vega'; -import { ContractDisassociate } from './contract-disassociate'; - -export const DisassociatePageNoVega = () => { - const { t } = useTranslation(); - const params = useSearchParams(); - const [amount, setAmount] = React.useState(''); - const [selectedStakingMethod, setSelectedStakingMethod] = - React.useState( - (params.method as StakingMethod) || null - ); - - return ( -
-

- {t( - 'Use this form to disassociate VEGA tokens with a Vega key. This returns them to either the Ethereum wallet that used the Staking bridge or the vesting contract.' - )} -

-

- {t('Warning')}:{' '} - {t( - 'Any Tokens that have been nominated to a node will sacrifice any Rewards they are due for the current epoch. If you do not wish to sacrifices fees you should remove stake from a node at the end of an epoch before disassocation.' - )} -

-

{t('What Vega wallet are you removing Tokens from?')}

- -

{t('What tokens would you like to return?')}

- - {selectedStakingMethod && - (selectedStakingMethod === StakingMethod.Wallet ? ( - - ) : ( - undefined} - /> - ))} -
- ); -}; diff --git a/apps/token/src/routes/staking/disassociate/disassociate-page.tsx b/apps/token/src/routes/staking/disassociate/disassociate-page.tsx index f7f2d60c4..17c90ba30 100644 --- a/apps/token/src/routes/staking/disassociate/disassociate-page.tsx +++ b/apps/token/src/routes/staking/disassociate/disassociate-page.tsx @@ -1,65 +1,147 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; - -import { ConnectedVegaKey } from '../../../components/connected-vega-key'; -import { - StakingMethod, - StakingMethodRadio, -} from '../../../components/staking-method-radio'; -import { TxState } from '../../../hooks/transaction-reducer'; -import { useSearchParams } from '../../../hooks/use-search-params'; -import { useRefreshAssociatedBalances } from '../../../hooks/use-refresh-associated-balances'; -import { ContractDisassociate } from './contract-disassociate'; import { DisassociateTransaction } from './disassociate-transaction'; +import { formatNumber } from '../../../lib/format-number'; +import { remove0x, toBigNum } from '@vegaprotocol/react-helpers'; +import { Select } from '@vegaprotocol/ui-toolkit'; +import { StakingMethod } from '../../../components/staking-method-radio'; +import { TokenInput } from '../../../components/token-input'; +import { TxState } from '../../../hooks/transaction-reducer'; +import { useAppState } from '../../../contexts/app-state/app-state-context'; +import { useRefreshAssociatedBalances } from '../../../hooks/use-refresh-associated-balances'; import { useRemoveStake } from './hooks'; -import { WalletDisassociate } from './wallet-disassociate'; -import type { VegaKeyExtended } from '@vegaprotocol/wallet'; +import type { RemoveStakePayload } from './hooks'; +import { useState, useEffect, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import type { BigNumber } from '../../../lib/bignumber'; + +type Association = { + key: string; + value: BigNumber; + stakingMethod: StakingMethod; +}; + +const toListOfAssociations = ( + obj: { [vegaKey: string]: BigNumber }, + stakingMethod: StakingMethod +): Association[] => + Object.keys(obj) + .map((k) => ({ + key: remove0x(k), + value: obj[k], + stakingMethod, + })) + .filter((k) => k.value.isGreaterThan(0)); export const DisassociatePage = ({ address, vegaKey, }: { address: string; - vegaKey: VegaKeyExtended; + vegaKey: string; }) => { const { t } = useTranslation(); - const params = useSearchParams(); - const [amount, setAmount] = React.useState(''); - const [selectedStakingMethod, setSelectedStakingMethod] = - React.useState( - (params.method as StakingMethod) || null - ); + + const { + appState: { + associationBreakdown: { stakingAssociations, vestingAssociations }, + }, + } = useAppState(); + + const associations = useMemo( + () => [ + ...toListOfAssociations(stakingAssociations, StakingMethod.Wallet), + ...toListOfAssociations(vestingAssociations, StakingMethod.Contract), + ], + [stakingAssociations, vestingAssociations] + ); + + useEffect(() => { + setChosen(associations.find((k) => k.key === vegaKey) || associations[0]); + }, [associations, vegaKey]); + + const [chosen, setChosen] = useState(); + + const maximum = chosen?.value || toBigNum(0, 0); + const [amount, setAmount] = useState(''); + const refreshBalances = useRefreshAssociatedBalances(); - // Clear the amount when the staking method changes - React.useEffect(() => { - setAmount(''); - }, [selectedStakingMethod]); + const payload: RemoveStakePayload = { + amount, + vegaKey: chosen?.key || '', + stakingMethod: chosen?.stakingMethod || StakingMethod.Unknown, + }; const { state: txState, dispatch: txDispatch, perform: txPerform, - } = useRemoveStake(address, amount, vegaKey.pub, selectedStakingMethod); + } = useRemoveStake(address, payload); - React.useEffect(() => { + useEffect(() => { if (txState.txState === TxState.Complete) { - refreshBalances(address, vegaKey.pub); + refreshBalances(address, chosen?.key || ''); } - }, [txState, refreshBalances, address, vegaKey.pub]); + }, [txState, refreshBalances, address, chosen]); - if (txState.txState !== TxState.Default) { + if (txState.txState !== TxState.Default && payload) { return ( ); } + const noKeysMessage = ( +
+ {t( + 'You have no VEGA tokens currently associated through your connected Ethereum wallet.' + )} +
+ ); + + const disassociate = ( + <> +
+ +
+ + + ); + return (

@@ -70,30 +152,12 @@ export const DisassociatePage = ({

{t('Warning')}:{' '} {t( - 'Any Tokens that have been nominated to a node will sacrifice any Rewards they are due for the current epoch. If you do not wish to sacrifices fees you should remove stake from a node at the end of an epoch before disassocation.' + 'Any tokens that have been nominated to a node will sacrifice rewards they are due for the current epoch. If you do not wish to sacrifice these, you should remove stake from a node at the end of an epoch before disassociation.' )}

-

{t('What Vega wallet are you removing Tokens from?')}

- +

{t('What tokens would you like to return?')}

- - {selectedStakingMethod && - (selectedStakingMethod === StakingMethod.Wallet ? ( - - ) : ( - - ))} + {associations.length === 0 ? noKeysMessage : disassociate}
); }; diff --git a/apps/token/src/routes/staking/disassociate/hooks.ts b/apps/token/src/routes/staking/disassociate/hooks.ts index 59f3f1c15..8f29a18ab 100644 --- a/apps/token/src/routes/staking/disassociate/hooks.ts +++ b/apps/token/src/routes/staking/disassociate/hooks.ts @@ -8,12 +8,24 @@ import { TxState } from '../../../hooks/transaction-reducer'; import { useGetAssociationBreakdown } from '../../../hooks/use-get-association-breakdown'; import { useRefreshBalances } from '../../../hooks/use-refresh-balances'; import { useTransaction } from '../../../hooks/use-transaction'; +import { initialState } from '../../../hooks/transaction-reducer'; + +export type RemoveStakePayload = { + amount: string; + vegaKey: string; + stakingMethod: StakingMethod; +}; + +const EMPTY_REMOVE = { + state: initialState, + dispatch: () => undefined, + perform: () => undefined as void, + reset: () => undefined as void, +}; export const useRemoveStake = ( address: string, - amount: string, - vegaKey: string, - stakingMethod: StakingMethod | null + payload: RemoveStakePayload ) => { const { appState } = useAppState(); const { staking, vesting } = useContracts(); @@ -21,11 +33,18 @@ export const useRemoveStake = ( // which if staked > wallet balance means you cannot unstaked // even worse if you stake everything then you can't unstake anything! const contractRemove = useTransaction(() => - vesting.remove_stake(removeDecimal(amount, appState.decimals), vegaKey) + vesting.remove_stake( + removeDecimal(payload.amount, appState.decimals), + payload.vegaKey + ) ); const walletRemove = useTransaction(() => - staking.remove_stake(removeDecimal(amount, appState.decimals), vegaKey) + staking.remove_stake( + removeDecimal(payload.amount, appState.decimals), + payload.vegaKey + ) ); + const refreshBalances = useRefreshBalances(address); const getAssociationBreakdown = useGetAssociationBreakdown( address, @@ -49,10 +68,13 @@ export const useRemoveStake = ( ]); return React.useMemo(() => { - if (stakingMethod === StakingMethod.Contract) { - return contractRemove; - } else { - return walletRemove; + switch (payload.stakingMethod) { + case StakingMethod.Contract: + return contractRemove; + case StakingMethod.Wallet: + return walletRemove; + default: + return EMPTY_REMOVE; } - }, [contractRemove, stakingMethod, walletRemove]); + }, [contractRemove, payload, walletRemove]); }; diff --git a/apps/token/src/routes/staking/disassociate/wallet-disassociate.tsx b/apps/token/src/routes/staking/disassociate/wallet-disassociate.tsx deleted file mode 100644 index 6d08be851..000000000 --- a/apps/token/src/routes/staking/disassociate/wallet-disassociate.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; - -import { TokenInput } from '../../../components/token-input'; -import { useAppState } from '../../../contexts/app-state/app-state-context'; - -export const WalletDisassociate = ({ - perform, - amount, - setAmount, -}: { - perform: () => void; - amount: string; - setAmount: React.Dispatch>; -}) => { - const { - appState: { walletAssociatedBalance }, - } = useAppState(); - const { t } = useTranslation(); - - if (!walletAssociatedBalance || walletAssociatedBalance.isEqualTo('0')) { - return ( -
- {t( - 'You have no VEGA tokens currently staked through your connected Vega wallet.' - )} -
- ); - } - - return ( - - ); -}; diff --git a/libs/smart-contracts/src/utils/prepend-0x.test.ts b/libs/smart-contracts/src/utils/prepend-0x.test.ts index d3bf2116e..dcc556895 100644 --- a/libs/smart-contracts/src/utils/prepend-0x.test.ts +++ b/libs/smart-contracts/src/utils/prepend-0x.test.ts @@ -1,6 +1,11 @@ import { prepend0x } from './prepend-0x'; -test('Prepends strings with 0x', () => { - expect(prepend0x('abc')).toEqual('0xabc'); - expect(prepend0x('123456789')).toEqual('0x123456789'); +describe('prepend0x', () => { + it.each([ + { input: 'ABC123', output: '0xABC123' }, + { input: '0XABC123', output: '0x0XABC123' }, + { input: '0xABC123', output: '0xABC123' }, + ])('prepends strings with 0x only if needed', ({ input, output }) => { + expect(prepend0x(input)).toBe(output); + }); }); diff --git a/libs/smart-contracts/src/utils/prepend-0x.ts b/libs/smart-contracts/src/utils/prepend-0x.ts index f8f6aee19..dd3f28f98 100644 --- a/libs/smart-contracts/src/utils/prepend-0x.ts +++ b/libs/smart-contracts/src/utils/prepend-0x.ts @@ -1,3 +1,3 @@ export function prepend0x(str: string) { - return `0x${str}`; + return !str || str.indexOf('0x') === 0 ? str : `0x${str}`; }