Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(wallet-dashboard): style staked vesting overview #4303

Merged
merged 20 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4f0d2b8
feat(dashboard): style vesting
panteleymonchuk Nov 28, 2024
6e28750
feat(dashboard): show properly data
panteleymonchuk Nov 29, 2024
0ec2ca6
fix(wallet-dashboard): wrong buttonType for dist
panteleymonchuk Dec 2, 2024
436344f
Merge remote-tracking branch 'origin/develop' into tooling-dashboard/…
panteleymonchuk Dec 2, 2024
3727278
Merge branch 'develop' into tooling-dashboard/staked-vesting-overview
panteleymonchuk Dec 4, 2024
305f352
feat(dashboard): update UI after resolve conflicts.
panteleymonchuk Dec 4, 2024
7618e28
feat(dashboard): add earned amount
panteleymonchuk Dec 4, 2024
9c40729
Merge remote-tracking branch 'origin/develop' into tooling-dashboard/…
panteleymonchuk Dec 4, 2024
46d52c3
Merge remote-tracking branch 'origin/develop' into tooling-dashboard/…
panteleymonchuk Dec 5, 2024
6b71103
feat(wallet-dashboard): implement StakedTimelockObject component and …
panteleymonchuk Dec 5, 2024
0b0b34d
feat(dashboard): add Stake button to Staked Vesting title and simplif…
panteleymonchuk Dec 5, 2024
47d1ae1
feat(dashboard): add useStakeRewardStatus hook and integrate into Sta…
panteleymonchuk Dec 5, 2024
4b45942
feat(vesting): update vesting interface and calculations to use bigin…
panteleymonchuk Dec 6, 2024
91cbf88
feat(dashboard): add loader, change style, fix BigInt round issue.
panteleymonchuk Dec 6, 2024
c6e6e80
Merge branch 'develop' into tooling-dashboard/staked-vesting-overview
panteleymonchuk Dec 6, 2024
14ef0ec
refactor(dashboard): move StakeState and STATUS_COPY to useStakeRewar…
panteleymonchuk Dec 6, 2024
2f04d53
feat(dashboard): implement bigInt to tests.
panteleymonchuk Dec 6, 2024
f22e643
fix(dashboard): adjust padding for sidebar overlap on small screens a…
panteleymonchuk Dec 6, 2024
2ca2b7b
Merge remote-tracking branch 'origin/develop' into tooling-dashboard/…
panteleymonchuk Dec 9, 2024
43c320f
feat(dashboard): update estimatedReward and principal to use BigInt f…
panteleymonchuk Dec 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions apps/ui-kit/src/lib/components/molecules/card/CardAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
// SPDX-License-Identifier: Apache-2.0

import { ArrowRight } from '@iota/ui-icons';
import { Button, ButtonSize, ButtonType } from '@/components/atoms/button';
import { Button, ButtonSize } from '@/components/atoms/button';
import { CardActionType } from './card.enums';
import { ButtonType } from '../../atoms/button';

export type CardActionProps = {
title?: string;
Expand All @@ -12,6 +13,7 @@ export type CardActionProps = {
onClick?: () => void;
icon?: React.ReactNode;
iconAfterText?: boolean;
buttonType?: ButtonType;
};

export function CardAction({
Expand All @@ -21,6 +23,7 @@ export function CardAction({
title,
icon,
iconAfterText,
buttonType,
}: CardActionProps) {
function handleActionClick(event: React.MouseEvent) {
event?.stopPropagation();
Expand Down Expand Up @@ -58,7 +61,7 @@ export function CardAction({
return (
<div className="shrink-0">
<Button
type={ButtonType.Outlined}
type={buttonType || ButtonType.Outlined}
size={ButtonSize.Small}
text={title}
onClick={handleActionClick}
Expand Down
254 changes: 187 additions & 67 deletions apps/wallet-dashboard/app/(protected)/vesting/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

'use client';

import { Button, TimelockedUnstakePopup } from '@/components';
import { TimelockedUnstakePopup } from '@/components';
import { useGetCurrentEpochStartTimestamp, useNotifications, usePopups } from '@/hooks';
import {
formatDelegatedTimelockedStake,
Expand All @@ -17,17 +17,35 @@ import { NotificationType } from '@/stores/notificationStore';
import { useFeature } from '@growthbook/growthbook-react';
import {
Feature,
ImageIcon,
ImageIconSize,
TIMELOCK_IOTA_TYPE,
useFormatCoin,
useGetActiveValidatorsInfo,
useGetAllOwnedObjects,
useGetTimelockedStakedObjects,
useUnlockTimelockedObjectsTransaction,
} from '@iota/core';
import { useCurrentAccount, useIotaClient, useSignAndExecuteTransaction } from '@iota/dapp-kit';
import { Stake } from '@iota/ui-icons';
import {
Panel,
Title,
DisplayStats,
Card,
CardImage,
CardBody,
CardAction,
CardActionType,
ImageShape,
CardType,
ButtonType,
} from '@iota/apps-ui-kit';
import { IotaValidatorSummary } from '@iota/iota-sdk/client';
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils';

function VestingDashboardPage(): JSX.Element {
const account = useCurrentAccount();
Expand All @@ -44,6 +62,7 @@ function VestingDashboardPage(): JSX.Element {
const { data: timelockedStakedObjects } = useGetTimelockedStakedObjects(account?.address || '');
const { mutateAsync: signAndExecuteTransaction } = useSignAndExecuteTransaction();

console.log('dd', timelockedStakedObjects);
panteleymonchuk marked this conversation as resolved.
Show resolved Hide resolved
const supplyIncreaseVestingEnabled = useFeature<boolean>(Feature.SupplyIncreaseVesting).value;

const timelockedMapped = mapTimelockObjects(timelockedObjects || []);
Expand All @@ -62,6 +81,33 @@ function VestingDashboardPage(): JSX.Element {
(activeValidator) => activeValidator.iotaAddress === validatorAddress,
);
}
const [totalVestedFormatted, totalVestedSymbol] = useFormatCoin(
vestingSchedule.totalVested,
IOTA_TYPE_ARG,
);
const [totalLockedFormatted, totalLockedSymbol] = useFormatCoin(
vestingSchedule.totalLocked,
IOTA_TYPE_ARG,
);
const [availableClaimingFormatted, availableClaimingSymbol] = useFormatCoin(
vestingSchedule.availableClaiming,
IOTA_TYPE_ARG,
);
panteleymonchuk marked this conversation as resolved.
Show resolved Hide resolved

const [availableStakingFormatted, availableStakingSymbol] = useFormatCoin(
vestingSchedule.availableStaking,
IOTA_TYPE_ARG,
);

const [totalStakedFormatted, totalStakedSymbol] = useFormatCoin(
vestingSchedule.totalStaked,
IOTA_TYPE_ARG,
);

const [totalUnlockedFormatted, totalUnlockedSymbol] = useFormatCoin(
vestingSchedule.totalUnlocked,
IOTA_TYPE_ARG,
);

const unlockedTimelockedObjects = timelockedMapped?.filter((timelockedObject) =>
isTimelockedUnlockable(timelockedObject, Number(currentEpochMs)),
Expand Down Expand Up @@ -142,81 +188,155 @@ function VestingDashboardPage(): JSX.Element {
}, [router, supplyIncreaseVestingEnabled]);

return (
<div className="flex flex-row">
<div className="flex w-1/2 flex-col items-center justify-center space-y-4 pt-12">
<h1>VESTING</h1>
<div className="flex flex-row space-x-4">
<div className="flex flex-col items-center rounded-lg border p-4">
<span>Total Vested</span>
<span>{vestingSchedule.totalVested}</span>
<div className="flex flex-row gap-lg">
<Panel>
<Title title="Vesting" />
<div className="flex flex-col gap-md px-lg py-sm">
<div className="flex flex-row gap-md">
<DisplayStats
label="Total Vested"
value={`${totalVestedFormatted} ${totalVestedSymbol}`}
/>
<DisplayStats
label="Total Locked"
value={`${totalLockedFormatted} ${totalLockedSymbol}`}
/>
</div>
<div className="flex flex-col items-center rounded-lg border p-4">
<span>Total Locked</span>
<span>{vestingSchedule.totalLocked}</span>
</div>
</div>
<hr />
<div className="flex flex-row space-x-4">
<div className="flex flex-col items-center rounded-lg border p-4">
<span>Available Claiming</span>
<span>{vestingSchedule.availableClaiming}</span>
<div className="mt-md flex flex-row gap-md">
<DisplayStats
label="Available Claiming"
value={`${availableClaimingFormatted} ${availableClaimingSymbol}`}
/>
<DisplayStats
label="Available Staking"
value={`${availableStakingFormatted} ${availableStakingSymbol}`}
/>
</div>
<div className="flex flex-col items-center rounded-lg border p-4">
<span>Available Staking</span>
<span>{vestingSchedule.availableStaking}</span>

<div className="">
{account?.address && vestingSchedule.availableClaiming ? (
<Card type={CardType.Outlined}>
<CardImage shape={ImageShape.SquareRounded}>
<Stake />
</CardImage>
<CardBody
title={`${availableClaimingFormatted} ${availableClaimingSymbol}`}
subtitle={`Available Rewards`}
isTextTruncated
/>
<CardAction
buttonType={ButtonType.Primary}
type={CardActionType.Button}
title="Collect"
onClick={handleCollect}
/>
</Card>
) : null}
panteleymonchuk marked this conversation as resolved.
Show resolved Hide resolved
</div>
</div>
</div>
<div className="flex w-1/2 flex-col items-center justify-center space-y-4 pt-12">
<h1>Staked Vesting</h1>
<div className="flex flex-row space-x-4">
<div className="flex flex-col items-center rounded-lg border p-4">
<span>Your stake</span>
<span>{vestingSchedule.totalStaked}</span>
</Panel>
<Panel>
<Title title="Staked Vesting" />
panteleymonchuk marked this conversation as resolved.
Show resolved Hide resolved
<div className="flex flex-col px-lg py-sm">
<div className="flex flex-row gap-md">
<DisplayStats
label="Your stake"
value={`${totalStakedFormatted} ${totalStakedSymbol}`}
/>
<DisplayStats
label="Total Unlocked"
panteleymonchuk marked this conversation as resolved.
Show resolved Hide resolved
value={`${totalUnlockedFormatted} ${totalUnlockedSymbol}`}
/>
</div>
<div className="flex flex-col items-center rounded-lg border p-4">
<span>Total Unlocked</span>
<span>{vestingSchedule.totalUnlocked}</span>
</div>
</div>
<div className="flex w-full flex-col items-center justify-center space-y-4 pt-4">
{timelockedStakedObjectsGrouped?.map((timelockedStakedObject) => {
return (
<div
key={
timelockedStakedObject.validatorAddress +
timelockedStakedObject.stakeRequestEpoch +
timelockedStakedObject.label
}
className="flex w-full flex-row items-center justify-center space-x-4"
>
<span>
Validator:{' '}
{getValidatorByAddress(timelockedStakedObject.validatorAddress)
?.name || timelockedStakedObject.validatorAddress}
</span>
<span>
Stake Request Epoch: {timelockedStakedObject.stakeRequestEpoch}
</span>
<span>Stakes: {timelockedStakedObject.stakes.length}</span>

<Button onClick={() => handleUnstake(timelockedStakedObject)}>
Unstake
</Button>
</div>
);
})}
</div>
{account?.address && (
<div className="flex flex-row space-x-4">
{vestingSchedule.availableClaiming ? (
<Button onClick={handleCollect}>Collect</Button>
) : null}
<div className="flex flex-col px-lg py-sm">
<div className="flex w-full flex-col items-center justify-center space-y-4 pt-4">
{timelockedStakedObjectsGrouped?.map((timelockedStakedObject) => {
return (
<TimelockedStakedObject
key={
timelockedStakedObject.validatorAddress +
timelockedStakedObject.stakeRequestEpoch +
timelockedStakedObject.label
}
getValidatorByAddress={getValidatorByAddress}
timelockedStakedObject={timelockedStakedObject}
handleUnstake={handleUnstake}
/>
);
})}
</div>
)}
</div>
</div>
</Panel>
</div>
);
}

interface TimelockedStakedObjectProps {
panteleymonchuk marked this conversation as resolved.
Show resolved Hide resolved
timelockedStakedObject: TimelockedStakedObjectsGrouped;
handleUnstake: (timelockedStakedObject: TimelockedStakedObjectsGrouped) => void;
getValidatorByAddress: (validatorAddress: string) => IotaValidatorSummary | undefined;
}
function TimelockedStakedObject({
getValidatorByAddress,
timelockedStakedObject,
handleUnstake,
}: TimelockedStakedObjectProps) {
const name =
getValidatorByAddress(timelockedStakedObject.validatorAddress)?.name ||
timelockedStakedObject.validatorAddress;
const sum = timelockedStakedObject.stakes.reduce(
(acc, stake) => {
const estimatedReward = stake.status === 'Active' ? stake.estimatedReward : 0;

return {
principal: Number(stake.principal) + acc.principal,
estimatedReward: Number(estimatedReward) + acc.estimatedReward,
};
},
{
principal: 0,
estimatedReward: 0,
},
);

const [sumPrincipalFormatted, sumPrincipalSymbol] = useFormatCoin(sum.principal, IOTA_TYPE_ARG);
const [estimatedRewardFormatted, estimatedRewardSymbol] = useFormatCoin(
sum.estimatedReward,
IOTA_TYPE_ARG,
);

const supportingText = (() => {
if (timelockedStakedObject.stakes.every((s) => s.status === 'Active')) {
return {
title: 'Estimated Reward',
subtitle: `${estimatedRewardFormatted} ${estimatedRewardSymbol}`,
};
}

return {
title: 'Stake Request Epoch',
panteleymonchuk marked this conversation as resolved.
Show resolved Hide resolved
subtitle: timelockedStakedObject.stakeRequestEpoch,
};
})();

return (
<Card onClick={() => handleUnstake(timelockedStakedObject)}>
<CardImage>
<ImageIcon src={null} label={name} fallback={name} size={ImageIconSize.Large} />
</CardImage>
<CardBody
title={name}
subtitle={`${sumPrincipalFormatted} ${sumPrincipalSymbol}`}
isTextTruncated
/>
<CardAction
type={CardActionType.SupportingText}
title={supportingText.title}
subtitle={supportingText.subtitle}
/>
</Card>
);
}

export default VestingDashboardPage;
Loading