diff --git a/packages/extension-polkagate/src/fullscreen/accountDetails/components/LockedInReferendaFS.tsx b/packages/extension-polkagate/src/fullscreen/accountDetails/components/LockedInReferendaFS.tsx index 9faf11af1..85a5779b9 100644 --- a/packages/extension-polkagate/src/fullscreen/accountDetails/components/LockedInReferendaFS.tsx +++ b/packages/extension-polkagate/src/fullscreen/accountDetails/components/LockedInReferendaFS.tsx @@ -3,19 +3,15 @@ /* eslint-disable react/jsx-max-props-per-line */ -// @ts-ignore -import type { PalletBalancesBalanceLock } from '@polkadot/types/lookup'; import type { UnlockInformationType } from '..'; -import { faUnlockAlt } from '@fortawesome/free-solid-svg-icons'; +import { faLock, faUnlockAlt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Divider, Grid, IconButton, Typography, useTheme } from '@mui/material'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; - -import { BN_MAX_INTEGER } from '@polkadot/util'; +import React, { useCallback } from 'react'; import { FormatPrice, ShowBalance } from '../../../components'; -import { useAccountLocks, useCurrentBlockNumber, useHasDelegated, useInfo, useTimeToUnlock, useTranslation } from '../../../hooks'; +import { useAnimateOnce, useInfo, useLockedInReferenda, useTranslation } from '../../../hooks'; import { TIME_TO_SHAKE_ICON } from '../../../util/constants'; import { popupNumbers } from '..'; @@ -32,26 +28,9 @@ export default function LockedInReferendaFS ({ address, price, refreshNeeded, se const theme = useTheme(); const { api, decimal, token } = useInfo(address); - const delegatedBalance = useHasDelegated(address, refreshNeeded); - const referendaLocks = useAccountLocks(address, 'referenda', 'convictionVoting', false, refreshNeeded); - const currentBlock = useCurrentBlockNumber(address); - const { timeToUnlock, totalLocked, unlockableAmount } = useTimeToUnlock(address, referendaLocks, refreshNeeded); - - const [shake, setShake] = useState(); - - const classToUnlock = currentBlock ? referendaLocks?.filter((ref) => ref.endBlock.ltn(currentBlock) && ref.classId.lt(BN_MAX_INTEGER)) : undefined; - const isDisable = useMemo(() => !unlockableAmount || unlockableAmount.isZero() || !classToUnlock || !totalLocked, [classToUnlock, totalLocked, unlockableAmount]); - const hasDescription = useMemo(() => - (unlockableAmount && !unlockableAmount.isZero()) || (delegatedBalance && !delegatedBalance.isZero()) || timeToUnlock - , [delegatedBalance, timeToUnlock, unlockableAmount]); - - useEffect(() => { - if (unlockableAmount && !unlockableAmount.isZero()) { - setShake(true); - setTimeout(() => setShake(false), TIME_TO_SHAKE_ICON); - } - }, [unlockableAmount]); + const { classToUnlock, delegatedBalance, hasDescription, isDisable, timeToUnlock, totalLocked, unlockableAmount } = useLockedInReferenda(address, refreshNeeded); + const shake = useAnimateOnce(unlockableAmount && !unlockableAmount.isZero(), { duration: TIME_TO_SHAKE_ICON }); const onUnlock = useCallback(() => { if (isDisable) { @@ -110,7 +89,7 @@ export default function LockedInReferendaFS ({ address, price, refreshNeeded, se > diff --git a/packages/extension-polkagate/src/hooks/index.ts b/packages/extension-polkagate/src/hooks/index.ts index 99ac45803..881262c00 100644 --- a/packages/extension-polkagate/src/hooks/index.ts +++ b/packages/extension-polkagate/src/hooks/index.ts @@ -61,6 +61,7 @@ export { default as useIsRecoverableTooltipText } from './useIsRecoverableToolti export { default as useIsTestnetEnabled } from './useIsTestnetEnabled'; export { default as useIsValidator } from './useIsValidator'; export { useLedger } from './useLedger'; +export { default as useLockedInReferenda } from './useLockedInReferenda'; export { default as useLostAccountInformation } from './useLostAccountInformation'; export { default as useManifest } from './useManifest'; export { useMapEntries } from './useMapEntries'; diff --git a/packages/extension-polkagate/src/hooks/useAccountLocks.ts b/packages/extension-polkagate/src/hooks/useAccountLocks.ts index ff018736e..7f6e93673 100644 --- a/packages/extension-polkagate/src/hooks/useAccountLocks.ts +++ b/packages/extension-polkagate/src/hooks/useAccountLocks.ts @@ -7,6 +7,7 @@ import type { ApiPromise } from '@polkadot/api'; import type { Option, u32 } from '@polkadot/types'; // @ts-ignore import type { PalletConvictionVotingVoteAccountVote, PalletConvictionVotingVoteCasting, PalletConvictionVotingVoteVoting, PalletReferendaReferendumInfoConvictionVotingTally } from '@polkadot/types/lookup'; +import type { ITuple } from '@polkadot/types-codec/types'; import type { BN } from '@polkadot/util'; import { useEffect, useMemo, useState } from 'react'; @@ -16,7 +17,6 @@ import { BN_MAX_INTEGER, BN_ZERO } from '@polkadot/util'; import { CONVICTIONS } from '../fullscreen/governance/utils/consts'; import useCurrentBlockNumber from './useCurrentBlockNumber'; import { useInfo } from '.'; -import type { ITuple } from '@polkadot/types-codec/types'; export interface Lock { classId: BN; @@ -124,6 +124,8 @@ export default function useAccountLocks (address: string | undefined, palletRefe return undefined; } + setInfo(undefined); + const locks = await api.query[palletVote]?.['classLocksFor'](formatted) as unknown as [BN, BN][]; const lockClasses = locks?.length ? locks.map((l) => l[0]) @@ -203,8 +205,7 @@ export default function useAccountLocks (address: string | undefined, palletRefe }); } - // eslint-disable-next-line @typescript-eslint/no-floating-promises - getLockClass(); + getLockClass().catch(console.error); }, [api, chain?.genesisHash, formatted, palletReferenda, palletVote, refresh]); return useMemo(() => { diff --git a/packages/extension-polkagate/src/hooks/useCurrentBlockNumber.ts b/packages/extension-polkagate/src/hooks/useCurrentBlockNumber.ts index 67bf177bb..e2608dc41 100644 --- a/packages/extension-polkagate/src/hooks/useCurrentBlockNumber.ts +++ b/packages/extension-polkagate/src/hooks/useCurrentBlockNumber.ts @@ -1,20 +1,21 @@ // Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors // SPDX-License-Identifier: Apache-2.0 -// @ts-nocheck - -import { useEffect, useState } from 'react'; import type { AccountId } from '@polkadot/types/interfaces/runtime'; +import { useEffect, useState } from 'react'; + import { useApi } from '.'; -export default function useCurrentBlockNumber(address: AccountId | string | undefined): number | undefined { +export default function useCurrentBlockNumber (address: AccountId | string | undefined): number | undefined { const api = useApi(address); const [blockNumber, setCurrentBlockNumber] = useState(); useEffect(() => { - api && api.rpc.chain.getHeader().then((b) => setCurrentBlockNumber(b.number.unwrap().toNumber())); + api?.rpc.chain.getHeader() + .then((b) => setCurrentBlockNumber(b.number.unwrap().toNumber())) + .catch(console.error); }, [api]); return blockNumber; diff --git a/packages/extension-polkagate/src/hooks/useHasDelegated.ts b/packages/extension-polkagate/src/hooks/useHasDelegated.ts index 668515678..17e66c917 100644 --- a/packages/extension-polkagate/src/hooks/useHasDelegated.ts +++ b/packages/extension-polkagate/src/hooks/useHasDelegated.ts @@ -1,31 +1,31 @@ // Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors // SPDX-License-Identifier: Apache-2.0 -// @ts-nocheck +import type { ApiPromise } from '@polkadot/api'; +//@ts-ignore import type { PalletConvictionVotingVoteVoting } from '@polkadot/types/lookup'; +import type { BN } from '@polkadot/util'; import { useEffect, useState } from 'react'; -import { BN, BN_ZERO } from '@polkadot/util'; +import { BN_ZERO } from '@polkadot/util'; -import useApi from './useApi'; -import useFormatted from './useFormatted'; -import { useChain, useTracks } from '.'; +import { useInfo, useTracks } from '.'; -export default function useHasDelegated(address: string | undefined, refresh?: boolean): BN | null | undefined { - const api = useApi(address); - const formatted = useFormatted(address); - const chain = useChain(address); +export default function useHasDelegated (address: string | undefined, refresh?: boolean): BN | null | undefined { + const { api, chain, formatted } = useInfo(address); const { tracks } = useTracks(address); const [hasDelegated, setHasDelegated] = useState(); + const [fetchedFor, setFetchedFor] = useState(undefined); useEffect(() => { if (refresh) { setHasDelegated(undefined); + setFetchedFor(undefined); } - if (!api || !formatted || !tracks || !tracks?.length || !api?.query?.convictionVoting) { + if (!api || !formatted || !tracks?.length || !api?.query?.['convictionVoting'] || fetchedFor === address) { return; } @@ -33,21 +33,34 @@ export default function useHasDelegated(address: string | undefined, refresh?: b return; } - const params: [string, BN][] = tracks.map((t) => [String(formatted), t[0]]); + const fetchDelegationData = async ( + api: ApiPromise, + formatted: string, + tracks: [BN, unknown][] + ): Promise => { + try { + setFetchedFor(address); - // eslint-disable-next-line no-void - void api.query.convictionVoting.votingFor.multi(params).then((votingFor: PalletConvictionVotingVoteVoting[]) => { - let maxDelegated = BN_ZERO; + const params: [string, BN][] = tracks.map((t) => [formatted, t[0]]); + const votingFor: PalletConvictionVotingVoteVoting[] = await api.query['convictionVoting']['votingFor'].multi(params); - votingFor?.filter((v) => v.isDelegating).forEach((v) => { - if (v.asDelegating.balance.gt(maxDelegated)) { - maxDelegated = v.asDelegating.balance; - } - }); + const maxDelegated = votingFor + .filter((v) => v.isDelegating) + .reduce((max, v) => { + const balance = v.asDelegating.balance; - maxDelegated.isZero() ? setHasDelegated(null) : setHasDelegated(maxDelegated); - }); - }, [api, chain?.genesisHash, formatted, tracks, refresh]); + return balance.gt(max) ? balance : max; + }, BN_ZERO); + + setHasDelegated(maxDelegated.isZero() ? null : maxDelegated); + } catch (error) { + console.error('Error fetching delegation data:', error); + setFetchedFor(undefined); + } + }; + + fetchDelegationData(api, formatted, tracks).catch(console.error); + }, [api, chain?.genesisHash, formatted, tracks, refresh, fetchedFor, address]); return hasDelegated; } diff --git a/packages/extension-polkagate/src/hooks/useLockedInReferenda.ts b/packages/extension-polkagate/src/hooks/useLockedInReferenda.ts new file mode 100644 index 000000000..e2a1d0ff4 --- /dev/null +++ b/packages/extension-polkagate/src/hooks/useLockedInReferenda.ts @@ -0,0 +1,54 @@ +// Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { BN } from '@polkadot/util'; + +import { useMemo } from 'react'; + +import { BN_MAX_INTEGER } from '@polkadot/util'; + +import { useAccountLocks, useCurrentBlockNumber, useHasDelegated, useTimeToUnlock } from '.'; + +interface Lock { + classId: BN; + endBlock: BN; + locked: string; + refId: BN | 'N/A'; + total: BN; +} + +interface OutputType { + classToUnlock: Lock[] | undefined; + delegatedBalance: BN | null | undefined; + hasDescription: boolean; + isDisable: boolean; + lockedInRef: BN | undefined + timeToUnlock: string | null | undefined; + totalLocked: BN | null | undefined; + unlockableAmount: BN | undefined; +} + +export default function useLockedInReferenda (address: string | undefined, refreshNeeded: boolean | undefined): OutputType { + const delegatedBalance = useHasDelegated(address, refreshNeeded); + const referendaLocks = useAccountLocks(address, 'referenda', 'convictionVoting', false, refreshNeeded); + const currentBlock = useCurrentBlockNumber(address); + const { lockedInRef, timeToUnlock, totalLocked, unlockableAmount } = useTimeToUnlock(address, delegatedBalance, referendaLocks, refreshNeeded); + + const classToUnlock = currentBlock ? referendaLocks?.filter((ref) => ref.endBlock.ltn(currentBlock) && ref.classId.lt(BN_MAX_INTEGER)) : undefined; + const isDisable = useMemo(() => !unlockableAmount || unlockableAmount.isZero() || !classToUnlock || !totalLocked, [classToUnlock, totalLocked, unlockableAmount]); + + const hasDescription = useMemo(() => + Boolean((unlockableAmount && !unlockableAmount.isZero()) || (delegatedBalance && !delegatedBalance.isZero()) || timeToUnlock) + , [delegatedBalance, timeToUnlock, unlockableAmount]); + + return { + classToUnlock, + delegatedBalance, + hasDescription, + isDisable, + lockedInRef, + timeToUnlock, + totalLocked, + unlockableAmount + }; +} diff --git a/packages/extension-polkagate/src/hooks/useTimeToUnlock.tsx b/packages/extension-polkagate/src/hooks/useTimeToUnlock.tsx index be8d9a792..e67583dc0 100644 --- a/packages/extension-polkagate/src/hooks/useTimeToUnlock.tsx +++ b/packages/extension-polkagate/src/hooks/useTimeToUnlock.tsx @@ -11,12 +11,11 @@ import { BN_MAX_INTEGER, BN_ZERO } from '@polkadot/util'; import blockToDate from '../popup/crowdloans/partials/blockToDate'; import { type Lock } from './useAccountLocks'; -import { useCurrentBlockNumber, useHasDelegated, useInfo, useTranslation } from '.'; +import { useCurrentBlockNumber, useInfo, useTranslation } from '.'; -export default function useTimeToUnlock (address: string | undefined, referendaLocks: Lock[] | null | undefined, refresh?: boolean) { +export default function useTimeToUnlock (address: string | undefined, delegatedBalance: BN | null | undefined, referendaLocks: Lock[] | null | undefined, refresh?: boolean) { const { t } = useTranslation(); const { api, chain, formatted } = useInfo(address); - const delegatedBalance = useHasDelegated(address, refresh); const currentBlock = useCurrentBlockNumber(address); const [unlockableAmount, setUnlockableAmount] = useState(); @@ -31,6 +30,15 @@ export default function useTimeToUnlock (address: string | undefined, referendaL return maybeFound ? maybeFound.total : BN_ZERO; }, []); + // Reset states when address changes + useEffect(() => { + setUnlockableAmount(undefined); + setLockedInReferenda(undefined); + setTotalLocked(undefined); + setTimeToUnlock(undefined); + setMiscRefLock(undefined); + }, [address]); + useEffect(() => { if (refresh) { setLockedInReferenda(undefined); // TODO: needs double check @@ -43,6 +51,8 @@ export default function useTimeToUnlock (address: string | undefined, referendaL useEffect(() => { if (referendaLocks === null) { setLockedInReferenda(BN_ZERO); + setUnlockableAmount(BN_ZERO); + setTotalLocked(BN_ZERO); setTimeToUnlock(null); return; @@ -50,6 +60,8 @@ export default function useTimeToUnlock (address: string | undefined, referendaL if (!referendaLocks || !currentBlock) { setLockedInReferenda(undefined); + setUnlockableAmount(undefined); + setTotalLocked(undefined); setTimeToUnlock(undefined); return; @@ -116,16 +128,17 @@ export default function useTimeToUnlock (address: string | undefined, referendaL return setMiscRefLock(undefined); } - // eslint-disable-next-line no-void - void api.query['balances']['locks'](formatted).then((locks) => { - const _locks = locks as unknown as PalletBalancesBalanceLock[]; + api.query['balances']['locks'](formatted) + .then((locks) => { + const _locks = locks as unknown as PalletBalancesBalanceLock[]; - if (_locks?.length) { - const foundRefLock = _locks.find((l) => l.id.toHuman() === 'pyconvot'); + if (_locks?.length) { + const foundRefLock = _locks.find((l) => l.id.toHuman() === 'pyconvot'); - setMiscRefLock(foundRefLock?.amount); - } - }); + setMiscRefLock(foundRefLock?.amount); + } + }) + .catch(console.error); }, [api, chain?.genesisHash, formatted, refresh]); useEffect(() => { diff --git a/packages/extension-polkagate/src/popup/account/unlock/LockedInReferenda.tsx b/packages/extension-polkagate/src/popup/account/unlock/LockedInReferenda.tsx index 547cb126c..f89f2086a 100644 --- a/packages/extension-polkagate/src/popup/account/unlock/LockedInReferenda.tsx +++ b/packages/extension-polkagate/src/popup/account/unlock/LockedInReferenda.tsx @@ -1,7 +1,6 @@ // Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors // SPDX-License-Identifier: Apache-2.0 -/* eslint-disable header/header */ /* eslint-disable react/jsx-max-props-per-line */ /** @@ -9,16 +8,15 @@ * this component shows an account locked tokens information * */ -import { faUnlockAlt } from '@fortawesome/free-solid-svg-icons'; +import { faLock, faUnlockAlt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Divider, Grid, useTheme } from '@mui/material'; import React, { useCallback, useEffect, useState } from 'react'; import { noop } from '@polkadot/extension-polkagate/src/util/utils'; -import { BN_MAX_INTEGER } from '@polkadot/util'; import { FormatPrice, ShowBalance, ShowValue } from '../../../components'; -import { useAccountLocks, useCurrentBlockNumber, useHasDelegated, useInfo, useTimeToUnlock, useTokenPrice, useTranslation } from '../../../hooks'; +import { useInfo, useLockedInReferenda, useTokenPrice, useTranslation } from '../../../hooks'; import { TIME_TO_SHAKE_ICON } from '../../../util/constants'; import Review from './Review'; @@ -34,15 +32,11 @@ export default function LockedInReferenda ({ address, refresh, setRefresh }: Pro const { api, decimal, token } = useInfo(address); const { price } = useTokenPrice(address); - const delegatedBalance = useHasDelegated(address, refresh); - const referendaLocks = useAccountLocks(address, 'referenda', 'convictionVoting', false, refresh); - const { lockedInRef, timeToUnlock, totalLocked, unlockableAmount } = useTimeToUnlock(address, referendaLocks, refresh); - const currentBlock = useCurrentBlockNumber(address); const [showReview, setShowReview] = useState(false); const [shake, setShake] = useState(); - const classToUnlock = currentBlock ? referendaLocks?.filter((ref) => ref.endBlock.ltn(currentBlock) && ref.classId.lt(BN_MAX_INTEGER)) : undefined; + const { classToUnlock, delegatedBalance, isDisable, lockedInRef, timeToUnlock, totalLocked, unlockableAmount } = useLockedInReferenda(address, refresh); useEffect(() => { if (unlockableAmount && !unlockableAmount.isZero()) { @@ -57,7 +51,7 @@ export default function LockedInReferenda ({ address, refresh, setRefresh }: Pro return ( <> - + {t('Locked in Referenda')} @@ -83,8 +77,8 @@ export default function LockedInReferenda ({ address, refresh, setRefresh }: Pro