diff --git a/apps/wallet-dashboard/components/Dialogs/StakedDetails/StakedDetailsDialog.tsx b/apps/wallet-dashboard/components/Dialogs/StakedDetails/StakedDetailsDialog.tsx index 5ae0b5eda36..15e557bea61 100644 --- a/apps/wallet-dashboard/components/Dialogs/StakedDetails/StakedDetailsDialog.tsx +++ b/apps/wallet-dashboard/components/Dialogs/StakedDetails/StakedDetailsDialog.tsx @@ -1,36 +1,35 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import React, { useMemo } from 'react'; +import React from 'react'; import { - useGetValidatorsApy, ExtendedDelegatedStake, + formatPercentageDisplay, ImageIcon, ImageIconSize, useFormatCoin, - formatPercentageDisplay, } from '@iota/core'; import { - Dialog, - DialogBody, - DialogContent, - DialogPosition, - Header, + Badge, + BadgeType, Button, ButtonType, Card, CardBody, CardImage, CardType, - Panel, - KeyValueInfo, - Badge, - BadgeType, + Dialog, + DialogBody, + DialogContent, + DialogPosition, Divider, + Header, InfoBox, InfoBoxStyle, InfoBoxType, + KeyValueInfo, LoadingIndicator, + Panel, } from '@iota/apps-ui-kit'; import { Warning } from '@iota/ui-icons'; import { useUnstakeTransaction } from '@/hooks'; @@ -40,11 +39,14 @@ import { useSignAndExecuteTransaction, } from '@iota/dapp-kit'; import { formatAddress, IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; +import { useValidatorInfo } from '@/hooks'; + interface StakeDialogProps { stakedDetails: ExtendedDelegatedStake; showActiveStatus?: boolean; handleClose: () => void; } + export function StakedDetailsDialog({ handleClose, stakedDetails, @@ -53,46 +55,22 @@ export function StakedDetailsDialog({ const account = useCurrentAccount(); const totalStake = BigInt(stakedDetails?.principal || 0n); const validatorAddress = stakedDetails?.validatorAddress; - const { - data: system, - isPending: loadingValidators, - isError: errorValidators, - } = useIotaClientQuery('getLatestIotaSystemState'); - const { data: rollingAverageApys } = useGetValidatorsApy(); - const { apy, isApyApproxZero } = rollingAverageApys?.[validatorAddress] ?? { - apy: null, - }; + const { isPending: loadingValidators, isError: errorValidators } = useIotaClientQuery( + 'getLatestIotaSystemState', + ); const iotaEarned = BigInt(stakedDetails?.estimatedReward || 0n); const [iotaEarnedFormatted, iotaEarnedSymbol] = useFormatCoin(iotaEarned, IOTA_TYPE_ARG); const [totalStakeFormatted, totalStakeSymbol] = useFormatCoin(totalStake, IOTA_TYPE_ARG); + const { name, commission, newValidator, isAtRisk, apy, isApyApproxZero } = + useValidatorInfo(validatorAddress); + const { data: unstakeData } = useUnstakeTransaction( stakedDetails.stakedIotaId, account?.address || '', ); const { mutateAsync: signAndExecuteTransaction } = useSignAndExecuteTransaction(); - // flag if the validator is at risk of being removed from the active set - const isAtRisk = system?.atRiskValidators.some((item) => item[0] === validatorAddress); - - const validatorSummary = useMemo(() => { - if (!system) return null; - - return ( - system.activeValidators.find( - (validator) => validator.iotaAddress === validatorAddress, - ) || null - ); - }, [validatorAddress, system]); - - const validatorName = validatorSummary?.name || ''; - const stakingPoolActivationEpoch = Number(validatorSummary?.stakingPoolActivationEpoch || 0); - const currentEpoch = Number(system?.epoch || 0); - const commission = validatorSummary ? Number(validatorSummary.commissionRate) / 100 : 0; - - // flag as new validator if the validator was activated in the last epoch - // for genesis validators, this will be false - const newValidator = currentEpoch - stakingPoolActivationEpoch <= 1 && currentEpoch !== 0; const subtitle = showActiveStatus ? (
{formatAddress(validatorAddress)} @@ -153,16 +131,12 @@ export function StakedDetailsDialog({ - +
diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx index d7e9d1cbc75..afa705ff4a0 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx @@ -119,28 +119,37 @@ function StakeDialog({ }); } + const title = { + [Step.SelectValidator]: 'Select Validator', + [Step.EnterAmount]: 'Enter Amount', + }; + return ( -
setOpen(false)} /> - - {step === Step.SelectValidator && ( - - )} - {step === Step.EnterAmount && ( - setAmount(e.target.value)} - onBack={handleBack} - onStake={handleStake} - isStakeDisabled={!amount} - /> - )} - +
+
setOpen(false)} /> +
+ + {step === Step.SelectValidator && ( + + )} + {step === Step.EnterAmount && ( + setAmount(e.target.value)} + onBack={handleBack} + onStake={handleStake} + isStakeDisabled={!amount} + /> + )} + +
+
); diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/views/SelectValidatorView.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/views/SelectValidatorView.tsx index 25dbeb276cf..ac3bdeb1a5f 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/views/SelectValidatorView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/views/SelectValidatorView.tsx @@ -2,7 +2,19 @@ // SPDX-License-Identifier: Apache-2.0 import React from 'react'; -import { Button } from '@/components'; +import { ImageIcon, ImageIconSize, formatPercentageDisplay } from '@iota/core'; +import { + Card, + CardBody, + CardImage, + CardAction, + CardActionType, + CardType, + Badge, + BadgeType, +} from '@iota/apps-ui-kit'; +import { formatAddress } from '@iota/iota-sdk/utils'; +import { useValidatorInfo } from '@/hooks'; interface SelectValidatorViewProps { validators: string[]; @@ -11,17 +23,47 @@ interface SelectValidatorViewProps { function SelectValidatorView({ validators, onSelect }: SelectValidatorViewProps): JSX.Element { return ( -
-

Select Validator

-
- {validators.map((validator) => ( - - ))} -
+
+ {validators.map((validator) => ( + + ))}
); } +function Validator({ + address, + showActiveStatus, + onClick, +}: { + address: string; + showActiveStatus?: boolean; + onClick: (address: string) => void; +}) { + const { name, newValidator, isAtRisk, apy, isApyApproxZero } = useValidatorInfo(address); + + const subtitle = showActiveStatus ? ( +
+ {formatAddress(address)} + {newValidator && } + {isAtRisk && } +
+ ) : ( + formatAddress(address) + ); + return ( + onClick(address)}> + + + + + + + ); +} + export default SelectValidatorView; diff --git a/apps/wallet-dashboard/hooks/index.ts b/apps/wallet-dashboard/hooks/index.ts index 07ccfe796f8..96072fcc720 100644 --- a/apps/wallet-dashboard/hooks/index.ts +++ b/apps/wallet-dashboard/hooks/index.ts @@ -9,3 +9,4 @@ export * from './useSendCoinTransaction'; export * from './useCreateSendAssetTransaction'; export * from './useGetCurrentEpochStartTimestamp'; export * from './useTimelockedUnstakeTransaction'; +export * from './useValidatorInfo'; diff --git a/apps/wallet-dashboard/hooks/useValidatorInfo.tsx b/apps/wallet-dashboard/hooks/useValidatorInfo.tsx new file mode 100644 index 00000000000..214dbe20e0a --- /dev/null +++ b/apps/wallet-dashboard/hooks/useValidatorInfo.tsx @@ -0,0 +1,46 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 +import { useMemo } from 'react'; +import { useIotaClientQuery } from '@iota/dapp-kit'; +import { useGetValidatorsApy } from '@iota/core'; + +export function useValidatorInfo(validatorAddress: string) { + const { data: system } = useIotaClientQuery('getLatestIotaSystemState'); + const { data: rollingAverageApys } = useGetValidatorsApy(); + + const currentEpoch = Number(system?.epoch || 0); + + const validatorSummary = useMemo(() => { + if (!system) return null; + + return ( + system.activeValidators.find( + (validator) => validator.iotaAddress === validatorAddress, + ) || null + ); + }, [validatorAddress, system]); + + const stakingPoolActivationEpoch = Number(validatorSummary?.stakingPoolActivationEpoch || 0); + + // flag as new validator if the validator was activated in the last epoch + // for genesis validators, this will be false + const newValidator = currentEpoch - stakingPoolActivationEpoch <= 1 && currentEpoch !== 0; + + // flag if the validator is at risk of being removed from the active set + const isAtRisk = system?.atRiskValidators.some((item) => item[0] === validatorAddress); + + const { apy, isApyApproxZero } = rollingAverageApys?.[validatorAddress] ?? { + apy: null, + }; + + return { + validatorSummary, + name: validatorSummary?.name || '', + stakingPoolActivationEpoch, + commission: validatorSummary ? Number(validatorSummary.commissionRate) / 100 : 0, + newValidator, + isAtRisk, + apy, + isApyApproxZero, + }; +}