Skip to content

Commit

Permalink
feat(wallet-dashboard): refactor staking dialogs and introduce Valida…
Browse files Browse the repository at this point in the history
…tor component
  • Loading branch information
panteleymonchuk committed Nov 7, 2024
1 parent f413060 commit 33e3089
Show file tree
Hide file tree
Showing 10 changed files with 199 additions and 197 deletions.
14 changes: 2 additions & 12 deletions apps/core/src/hooks/stake/useValidatorInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,11 @@
// SPDX-License-Identifier: Apache-2.0
import { useMemo } from 'react';
import { useIotaClientQuery } from '@iota/dapp-kit';
import { useGetValidatorsApy, useGetDelegatedStake } from '../';
import {
DELEGATED_STAKES_QUERY_REFETCH_INTERVAL,
DELEGATED_STAKES_QUERY_STALE_TIME,
} from '../../constants';
import { useGetValidatorsApy } from '../';

export function useValidatorInfo(validatorAddress: string) {
export function useValidatorInfo({ validatorAddress }: { validatorAddress: string }) {
const { data: system } = useIotaClientQuery('getLatestIotaSystemState');
const { data: rollingAverageApys } = useGetValidatorsApy();
const { data: delegatedStake } = useGetDelegatedStake({
address: validatorAddress || '',
staleTime: DELEGATED_STAKES_QUERY_STALE_TIME,
refetchInterval: DELEGATED_STAKES_QUERY_REFETCH_INTERVAL,
});

const validatorSummary = useMemo(() => {
if (!system) return null;
Expand Down Expand Up @@ -47,7 +38,6 @@ export function useValidatorInfo(validatorAddress: string) {

return {
system,
delegatedStake,
validatorSummary,
name: validatorSummary?.name || '',
stakingPoolActivationEpoch,
Expand Down
1 change: 0 additions & 1 deletion apps/wallet-dashboard/app/(protected)/staking/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ function StakingDashboardPage(): JSX.Element {
function handleNewStake() {
setIsDialogStakeOpen(true);
}
console.log(stakedDetails);

return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ function StakeDialog({
validators={validators}
onSelect={handleValidatorSelect}
onNext={handleSelectValidatorNext}
isValidatorSelected={!!selectedValidator}
selectedValidator={selectedValidator}
/>
)}
{step === Step.EnterAmount && (
Expand All @@ -153,7 +153,6 @@ function StakeDialog({
onChange={(e) => setAmount(e.target.value)}
onBack={handleBack}
onStake={handleStake}
isStakeDisabled={!amount}
/>
)}
</DialogBody>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ interface StakeDialogProps {
handleClose: () => void;
}

export function DialogStakedDetails({
export function StakedDetailsDialog({
handleClose,
stakedDetails,
showActiveStatus,
Expand All @@ -62,8 +62,9 @@ export function DialogStakedDetails({
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 { name, commission, newValidator, isAtRisk, apy, isApyApproxZero } = useValidatorInfo({
validatorAddress: validatorAddress,
});

const { data: unstakeData } = useUnstakeTransaction(
stakedDetails.stakedIotaId,
Expand All @@ -89,7 +90,7 @@ export function DialogStakedDetails({
}

function handleAddNewStake() {
console.log('Stake');
// pass
}

if (loadingValidators) {
Expand Down
1 change: 1 addition & 0 deletions apps/wallet-dashboard/components/Dialogs/Staking/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
// SPDX-License-Identifier: Apache-2.0

export { default as StakeDialog } from './StakeDialog';
export * from './StakedDetailsDialog';
Original file line number Diff line number Diff line change
@@ -1,175 +1,68 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import React, { useMemo } from 'react';
// import { Button } from '@/components';
import {
ImageIcon,
ImageIconSize,
formatPercentageDisplay,
calculateStakeShare,
useFormatCoin,
getTokenStakeIotaForValidator,
useBalance,
CoinFormat,
} from '@iota/core';
import { IOTA_TYPE_ARG, formatAddress } from '@iota/iota-sdk/utils';
import { useValidatorInfo } from '@/hooks';
import React from 'react';
import { useFormatCoin, useBalance, CoinFormat } from '@iota/core';
import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils';
import {
Button,
ButtonType,
Card,
CardBody,
CardImage,
CardType,
Badge,
BadgeType,
KeyValueInfo,
Panel,
TooltipPosition,
Divider,
Input,
InputType,
} from '@iota/apps-ui-kit';
import { useStakeTxnInfo } from '../hooks';
import { useCurrentAccount } from '@iota/dapp-kit';
import { useCurrentAccount, useIotaClientQuery } from '@iota/dapp-kit';
import { Validator } from './Validator';
import { StakedInfo } from './StakedInfo';

interface EnterAmountViewProps {
selectedValidator: string;
amount: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onBack: () => void;
onStake: () => void;
isStakeDisabled: boolean;
showActiveStatus?: boolean;
gasBudget?: string | number | null;
}

function EnterAmountView({
selectedValidator: validatorAddress,
selectedValidator: selectedValidatorAddress,
amount,
showActiveStatus,
onChange,
onBack,
onStake,
isStakeDisabled,
gasBudget = 0,
}: EnterAmountViewProps): JSX.Element {
const account = useCurrentAccount();
const accountAddress = account?.address;

const { data: system } = useIotaClientQuery('getLatestIotaSystemState');
const { data: iotaBalance } = useBalance(accountAddress!);

const coinBalance = BigInt(iotaBalance?.totalBalance || 0);
const maxTokenBalance = coinBalance - BigInt(Number(gasBudget));
const [maxTokenFormatted, maxTokenFormattedSymbol] = useFormatCoin(
maxTokenBalance,
IOTA_TYPE_ARG,
CoinFormat.FULL,
);
const {
name,
newValidator,
isAtRisk,
apy,
isApyApproxZero,
validatorSummary,
delegatedStake: stakeData,
system,
} = useValidatorInfo(validatorAddress);
const [gas, symbol] = useFormatCoin(gasBudget, IOTA_TYPE_ARG);
const { stakedRewardsStartEpoch, timeBeforeStakeRewardsRedeemableAgoDisplay } = useStakeTxnInfo(
system?.epoch,
);

const totalStake = useMemo(() => {
if (!stakeData) return 0n;
return getTokenStakeIotaForValidator(stakeData, validatorAddress);
}, [stakeData, validatorAddress]);

const totalValidatorsStake = useMemo(() => {
if (!system) return 0;
return system.activeValidators.reduce(
(acc, curr) => (acc += BigInt(curr.stakingPoolIotaBalance)),
0n,
);
}, [system]);

const totalStakePercentage = useMemo(() => {
if (!system || !validatorSummary) return null;

return calculateStakeShare(
BigInt(validatorSummary.stakingPoolIotaBalance),
BigInt(totalValidatorsStake),
);
}, [system, totalValidatorsStake, validatorSummary]);

//TODO: verify this is the correct validator stake balance
const totalValidatorStake = validatorSummary?.stakingPoolIotaBalance || 0;

const [totalValidatorStakeFormatted, totalValidatorStakeSymbol] = useFormatCoin(
totalValidatorStake,
IOTA_TYPE_ARG,
);
const [totalStakeFormatted, totalStakeSymbol] = useFormatCoin(totalStake, IOTA_TYPE_ARG);

const subtitle = showActiveStatus ? (
<div className="flex items-center gap-1">
{formatAddress(validatorAddress)}
{newValidator && <Badge label="New" type={BadgeType.PrimarySoft} />}
{isAtRisk && <Badge label="At Risk" type={BadgeType.PrimarySolid} />}
</div>
) : (
formatAddress(validatorAddress)
);
return (
<div className="flex w-full flex-col justify-between">
<div>
<Card type={CardType.Default}>
<CardImage>
<ImageIcon
src={null}
label={name}
fallback={name}
size={ImageIconSize.Large}
/>
</CardImage>
<CardBody title={name} subtitle={subtitle} isTextTruncated />
</Card>

<Panel hasBorder>
<div className="flex flex-col gap-y-sm p-md">
<KeyValueInfo
keyText="Staking APY"
tooltipPosition={TooltipPosition.Right}
tooltipText="Annualized percentage yield based on past validator performance. Future APY may vary"
value={formatPercentageDisplay(apy, '--', isApyApproxZero)}
fullwidth
/>
<KeyValueInfo
keyText="Stake Share"
tooltipPosition={TooltipPosition.Right}
tooltipText="Stake percentage managed by this validator."
value={formatPercentageDisplay(totalStakePercentage)}
fullwidth
/>
<KeyValueInfo
keyText="Total Staked"
tooltipPosition={TooltipPosition.Right}
tooltipText="Stake percentage managed by this validator."
value={totalValidatorStakeFormatted}
supportingLabel={totalValidatorStakeSymbol}
fullwidth
/>
<KeyValueInfo
keyText="Your Staked IOTA"
tooltipPosition={TooltipPosition.Right}
tooltipText="Your current staked balance."
value={totalStakeFormatted}
supportingLabel={totalStakeSymbol}
fullwidth
/>
</div>
</Panel>

<Validator address={selectedValidatorAddress} isSelected showAction={false} />
<StakedInfo
validatorAddress={selectedValidatorAddress}
accountAddress={accountAddress!}
/>
<div className="my-md w-full">
<Input
type={InputType.NumericFormat}
Expand Down Expand Up @@ -210,7 +103,7 @@ function EnterAmountView({
fullWidth
type={ButtonType.Primary}
onClick={onStake}
disabled={isStakeDisabled}
disabled={!amount}
text="Stake"
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,81 +2,39 @@
// SPDX-License-Identifier: Apache-2.0

import React from 'react';
import { ImageIcon, ImageIconSize, formatPercentageDisplay } from '@iota/core';
import {
Button,
Card,
CardBody,
CardImage,
CardAction,
CardActionType,
CardType,
Badge,
BadgeType,
} from '@iota/apps-ui-kit';
import { formatAddress } from '@iota/iota-sdk/utils';
import { useValidatorInfo } from '@/hooks';
import { Button } from '@iota/apps-ui-kit';
import { Validator } from './Validator';

interface SelectValidatorViewProps {
validators: string[];
onSelect: (validator: string) => void;
onNext: () => void;
isValidatorSelected: boolean;
selectedValidator: string;
}

function SelectValidatorView({
validators,
onSelect,
onNext,
isValidatorSelected,
selectedValidator,
}: SelectValidatorViewProps): JSX.Element {
return (
<div className="flex w-full flex-1 flex-col justify-between">
<div className="flex w-full flex-col items-start gap-md">
<div className="flex w-full flex-col">
{validators.map((validator) => (
<Validator key={validator} address={validator} onClick={onSelect} />
<Validator
key={validator}
address={validator}
onClick={onSelect}
isSelected={selectedValidator === validator}
/>
))}
</div>
{isValidatorSelected && (
{!!selectedValidator && (
<Button fullWidth data-testid="select-validator-cta" onClick={onNext} text="Next" />
)}
</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;
Loading

0 comments on commit 33e3089

Please sign in to comment.