Skip to content

Commit

Permalink
feat(wallet-dashboard): select validator.
Browse files Browse the repository at this point in the history
  • Loading branch information
panteleymonchuk committed Nov 1, 2024
1 parent a852fab commit 7d16bd2
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 78 deletions.
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
Expand All @@ -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 ? (
<div className="flex items-center gap-1">
{formatAddress(validatorAddress)}
Expand Down Expand Up @@ -153,16 +131,12 @@ export function StakedDetailsDialog({
<CardImage>
<ImageIcon
src={null}
label={validatorName}
fallback={validatorName}
label={name}
fallback={name}
size={ImageIconSize.Large}
/>
</CardImage>
<CardBody
title={validatorName}
subtitle={subtitle}
isTextTruncated
/>
<CardBody title={name} subtitle={subtitle} isTextTruncated />
</Card>
<Panel hasBorder>
<div className="flex flex-col gap-y-sm p-md">
Expand Down
47 changes: 28 additions & 19 deletions apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,28 +119,37 @@ function StakeDialog({
});
}

const title = {
[Step.SelectValidator]: 'Select Validator',
[Step.EnterAmount]: 'Enter Amount',
};

return (
<Dialog open={isOpen} onOpenChange={setOpen}>
<DialogContent containerId="overlay-portal-container" position={DialogPosition.Right}>
<Header title="Receive" onClose={() => setOpen(false)} />
<DialogBody>
{step === Step.SelectValidator && (
<SelectValidatorView
validators={validators}
onSelect={handleValidatorSelect}
/>
)}
{step === Step.EnterAmount && (
<EnterAmountView
selectedValidator={selectedValidator}
amount={amount}
onChange={(e) => setAmount(e.target.value)}
onBack={handleBack}
onStake={handleStake}
isStakeDisabled={!amount}
/>
)}
</DialogBody>
<div className="flex min-h-full flex-col">
<Header title={title[step]} onClose={() => setOpen(false)} />
<div className="flex w-full flex-1 [&_>div]:flex [&_>div]:w-full [&_>div]:flex-1">
<DialogBody>
{step === Step.SelectValidator && (
<SelectValidatorView
validators={validators}
onSelect={handleValidatorSelect}
/>
)}
{step === Step.EnterAmount && (
<EnterAmountView
selectedValidator={selectedValidator}
amount={amount}
onChange={(e) => setAmount(e.target.value)}
onBack={handleBack}
onStake={handleStake}
isStakeDisabled={!amount}
/>
)}
</DialogBody>
</div>
</div>
</DialogContent>
</Dialog>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand All @@ -11,17 +23,47 @@ interface SelectValidatorViewProps {

function SelectValidatorView({ validators, onSelect }: SelectValidatorViewProps): JSX.Element {
return (
<div>
<h2>Select Validator</h2>
<div className="flex flex-col items-start gap-2">
{validators.map((validator) => (
<Button key={validator} onClick={() => onSelect(validator)}>
{validator}
</Button>
))}
</div>
<div className="flex w-full flex-col items-start gap-2">
{validators.map((validator) => (
<Validator key={validator} address={validator} onClick={onSelect} />
))}
</div>
);
}

function Validator({
address,
showActiveStatus,
onClick,
}: {
address: string;
showActiveStatus?: boolean;
onClick: (address: string) => void;
}) {
const { name, newValidator, isAtRisk, apy, isApyApproxZero } = useValidatorInfo(address);

const subtitle = showActiveStatus ? (
<div className="flex items-center gap-1">
{formatAddress(address)}
{newValidator && <Badge label="New" type={BadgeType.PrimarySoft} />}
{isAtRisk && <Badge label="At Risk" type={BadgeType.PrimarySolid} />}
</div>
) : (
formatAddress(address)
);
return (
<Card type={CardType.Default} onClick={() => onClick(address)}>
<CardImage>
<ImageIcon src={null} label={name} fallback={name} size={ImageIconSize.Large} />
</CardImage>
<CardBody title={name} subtitle={subtitle} isTextTruncated />
<CardAction
type={CardActionType.SupportingText}
title={formatPercentageDisplay(apy, '--', isApyApproxZero)}
iconAfterText
/>
</Card>
);
}

export default SelectValidatorView;
1 change: 1 addition & 0 deletions apps/wallet-dashboard/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './useSendCoinTransaction';
export * from './useCreateSendAssetTransaction';
export * from './useGetCurrentEpochStartTimestamp';
export * from './useTimelockedUnstakeTransaction';
export * from './useValidatorInfo';
46 changes: 46 additions & 0 deletions apps/wallet-dashboard/hooks/useValidatorInfo.tsx
Original file line number Diff line number Diff line change
@@ -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,
};
}

0 comments on commit 7d16bd2

Please sign in to comment.