Skip to content

Commit

Permalink
feat(wallet-dashboard): style staked vesting overview (#4303)
Browse files Browse the repository at this point in the history
* feat(dashboard): style vesting

* feat(dashboard): show properly data

* fix(wallet-dashboard): wrong buttonType for dist

* feat(dashboard): update UI after resolve conflicts.

* feat(dashboard): add earned amount

* feat(wallet-dashboard): implement StakedTimelockObject component and refactor vesting page

* feat(dashboard): add Stake button to Staked Vesting title and simplify StakeDialog prop

* feat(dashboard): add useStakeRewardStatus hook and integrate into StakedTimelockObject component

* feat(vesting): update vesting interface and calculations to use bigint for improved precision

* feat(dashboard): add loader, change style, fix BigInt round issue.

* refactor(dashboard): move StakeState and STATUS_COPY to useStakeRewardStatus hook

* feat(dashboard): implement bigInt to tests.

* fix(dashboard): adjust padding for sidebar overlap on small screens and improve flex layout in vesting page

* feat(dashboard): update estimatedReward and principal to use BigInt for improved precision
  • Loading branch information
panteleymonchuk authored Dec 11, 2024
1 parent 55c14e8 commit d2c04ac
Show file tree
Hide file tree
Showing 11 changed files with 426 additions and 238 deletions.
78 changes: 9 additions & 69 deletions apps/core/src/components/stake/StakedCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,10 @@ import { Card, CardImage, CardType, CardBody, CardAction, CardActionType } from
import { useMemo } from 'react';
import { useIotaClientQuery } from '@iota/dapp-kit';
import { ImageIcon } from '../icon';
import { determineCountDownText, ExtendedDelegatedStake } from '../../utils';
import { TimeUnit, useFormatCoin, useGetTimeBeforeEpochNumber, useTimeAgo } from '../../hooks';
import { NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE } from '../../constants';
import { ExtendedDelegatedStake } from '../../utils';
import { useFormatCoin, useStakeRewardStatus } from '../../hooks';
import React from 'react';

export enum StakeState {
WarmUp = 'WARM_UP',
Earning = 'EARNING',
CoolDown = 'COOL_DOWN',
Withdraw = 'WITHDRAW',
InActive = 'IN_ACTIVE',
}

const STATUS_COPY: { [key in StakeState]: string } = {
[StakeState.WarmUp]: 'Starts Earning',
[StakeState.Earning]: 'Staking Rewards',
[StakeState.CoolDown]: 'Available to withdraw',
[StakeState.Withdraw]: 'Withdraw',
[StakeState.InActive]: 'Inactive',
};

interface StakedCardProps {
extendedStake: ExtendedDelegatedStake;
currentEpoch: number;
Expand All @@ -45,48 +28,20 @@ export function StakedCard({
}: StakedCardProps) {
const { principal, stakeRequestEpoch, estimatedReward, validatorAddress } = extendedStake;

// TODO: Once two step withdraw is available, add cool down and withdraw now logic
// For cool down epoch, show Available to withdraw add rewards to principal
// Reward earning epoch is 2 epochs after stake request epoch
const earningRewardsEpoch =
Number(stakeRequestEpoch) + NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE;
const isEarnedRewards = currentEpoch >= Number(earningRewardsEpoch);
const delegationState = inactiveValidator
? StakeState.InActive
: isEarnedRewards
? StakeState.Earning
: StakeState.WarmUp;

const rewards = isEarnedRewards && estimatedReward ? BigInt(estimatedReward) : 0n;
const { rewards, title, subtitle } = useStakeRewardStatus({
stakeRequestEpoch,
currentEpoch,
estimatedReward,
inactiveValidator,
});

// For inactive validator, show principal + rewards
const [principalStaked, symbol] = useFormatCoin(
inactiveValidator ? principal + rewards : principal,
IOTA_TYPE_ARG,
);
const [rewardsStaked] = useFormatCoin(rewards, IOTA_TYPE_ARG);

// Applicable only for warm up
const epochBeforeRewards = delegationState === StakeState.WarmUp ? earningRewardsEpoch : null;

const statusText = {
// Epoch time before earning
[StakeState.WarmUp]: `Epoch #${earningRewardsEpoch}`,
[StakeState.Earning]: `${rewardsStaked} ${symbol}`,
// Epoch time before redrawing
[StakeState.CoolDown]: `Epoch #`,
[StakeState.Withdraw]: 'Now',
[StakeState.InActive]: 'Not earning rewards',
};

const { data } = useIotaClientQuery('getLatestIotaSystemState');
const { data: rewardEpochTime } = useGetTimeBeforeEpochNumber(Number(epochBeforeRewards) || 0);
const timeAgo = useTimeAgo({
timeFrom: rewardEpochTime || null,
shortedTimeLabel: false,
shouldEnd: true,
maxTimeUnit: TimeUnit.ONE_HOUR,
});

const validatorMeta = useMemo(() => {
if (!data) return null;
Expand All @@ -97,17 +52,6 @@ export function StakedCard({
);
}, [validatorAddress, data]);

const rewardTime = () => {
if (Number(epochBeforeRewards) && rewardEpochTime > 0) {
return determineCountDownText({
timeAgo,
label: 'in',
});
}

return statusText[delegationState];
};

return (
<Card testId="staked-card" type={CardType.Default} isHoverable onClick={onClick}>
<CardImage>
Expand All @@ -118,11 +62,7 @@ export function StakedCard({
/>
</CardImage>
<CardBody title={validatorMeta?.name || ''} subtitle={`${principalStaked} ${symbol}`} />
<CardAction
title={rewardTime()}
subtitle={STATUS_COPY[delegationState]}
type={CardActionType.SupportingText}
/>
<CardAction title={title} subtitle={subtitle} type={CardActionType.SupportingText} />
</Card>
);
}
1 change: 1 addition & 0 deletions apps/core/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,6 @@ export * from './useNFTBasicData';
export * from './useOwnedNFT';
export * from './useNftDetails';
export * from './useCountdownByTimestamp';
export * from './useStakeRewardStatus';

export * from './stake';
92 changes: 92 additions & 0 deletions apps/core/src/hooks/useStakeRewardStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils';
import { NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE } from '../constants';
import { useFormatCoin, useGetTimeBeforeEpochNumber, useTimeAgo, TimeUnit } from '.';
import { determineCountDownText } from '../utils';

export function useStakeRewardStatus({
stakeRequestEpoch,
currentEpoch,
inactiveValidator,
estimatedReward,
}: {
stakeRequestEpoch: string;
currentEpoch: number;
inactiveValidator: boolean;
estimatedReward?: string | number | bigint;
}) {
// TODO: Once two step withdraw is available, add cool down and withdraw now logic
// For cool down epoch, show Available to withdraw add rewards to principal
// Reward earning epoch is 2 epochs after stake request epoch
const earningRewardsEpoch =
Number(stakeRequestEpoch) + NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE;

const isEarnedRewards = currentEpoch >= Number(earningRewardsEpoch);

const delegationState = inactiveValidator
? StakeState.InActive
: isEarnedRewards
? StakeState.Earning
: StakeState.WarmUp;

const rewards = isEarnedRewards && estimatedReward ? BigInt(estimatedReward) : 0n;

const [rewardsStaked, symbol] = useFormatCoin(rewards, IOTA_TYPE_ARG);

// Applicable only for warm up
const epochBeforeRewards = delegationState === StakeState.WarmUp ? earningRewardsEpoch : null;

const statusText = {
// Epoch time before earning
[StakeState.WarmUp]: `Epoch #${earningRewardsEpoch}`,
[StakeState.Earning]: `${rewardsStaked} ${symbol}`,
// Epoch time before redrawing
[StakeState.CoolDown]: `Epoch #`,
[StakeState.Withdraw]: 'Now',
[StakeState.InActive]: 'Not earning rewards',
};

const { data: rewardEpochTime } = useGetTimeBeforeEpochNumber(Number(epochBeforeRewards) || 0);

const timeAgo = useTimeAgo({
timeFrom: rewardEpochTime || null,
shortedTimeLabel: false,
shouldEnd: true,
maxTimeUnit: TimeUnit.ONE_HOUR,
});

const rewardTime = () => {
if (Number(epochBeforeRewards) && rewardEpochTime > 0) {
return determineCountDownText({
timeAgo,
label: 'in',
});
}

return statusText[delegationState];
};

return {
rewards,
title: rewardTime(),
subtitle: STATUS_COPY[delegationState],
};
}
export enum StakeState {
WarmUp = 'WARM_UP',
Earning = 'EARNING',
CoolDown = 'COOL_DOWN',
Withdraw = 'WITHDRAW',
InActive = 'IN_ACTIVE',
}
export const STATUS_COPY: {
[key in StakeState]: string;
} = {
[StakeState.WarmUp]: 'Starts Earning',
[StakeState.Earning]: 'Staking Rewards',
[StakeState.CoolDown]: 'Available to withdraw',
[StakeState.Withdraw]: 'Withdraw',
[StakeState.InActive]: 'Inactive',
};
11 changes: 7 additions & 4 deletions apps/wallet-dashboard/app/(protected)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@ function DashboardLayout({ children }: PropsWithChildren): JSX.Element {
<Sidebar />
</div>

<div className="container relative flex min-h-screen flex-col">
<div className="sticky top-0 z-10 backdrop-blur-lg">
<TopNav />
{/* This padding need to have aligned left/right content's position, because of sidebar overlap on the small screens */}
<div className="pl-[72px]">
<div className="container relative flex min-h-screen flex-col">
<div className="sticky top-0 z-10 backdrop-blur-lg">
<TopNav />
</div>
<div className="flex-1 py-md--rs">{children}</div>
</div>
<div className="flex-1 py-md--rs">{children}</div>
</div>

<div className="fixed bottom-5 right-5">
Expand Down
Loading

0 comments on commit d2c04ac

Please sign in to comment.