Skip to content

Commit

Permalink
feat(wallet-dasboard): style vesting schedule (#4340)
Browse files Browse the repository at this point in the history
* feat: add new icon

* feat: update carAction component

* feat: update feature flags

* feat: update icon

* polish

* minor fix

* feat: countdown

* feat: improve import and naming

* feat: add hook

* feat: polishes

* fix imports

* minor fixes

* fix build

* fix build

* feat: rename function, variables and fix improts

* feat: add vesting schedule

* cleanup

* feat: rename countdown and fix it

* feat: improvements

* cleanup

* feat: remove undefined from button type

* fix lint

* feat: remove debris

* fix build

* feat: improve naming

* minor fix

* fix useffect and filter payouts

* feat: use last paoyut and fix condition

* feat:use current epoch

* feat: update to toLocaleDateString

* feat: add current epoch

* fix build

* fix tests

* fix: use dynamic constant

---------

Co-authored-by: Marc Espin <[email protected]>
Co-authored-by: Bran <[email protected]>
  • Loading branch information
3 people authored Dec 5, 2024
1 parent 21f8072 commit 2fd6d93
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 28 deletions.
11 changes: 8 additions & 3 deletions apps/ui-kit/src/lib/components/organisms/dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const DialogContent = React.forwardRef<
containerId?: string;
showCloseOnOverlay?: boolean;
position?: DialogPosition;
customWidth?: string;
}
>(
(
Expand All @@ -50,6 +51,7 @@ const DialogContent = React.forwardRef<
showCloseOnOverlay,
children,
position = DialogPosition.Center,
customWidth = 'w-80 max-w-[85vw] md:w-96',
...props
},
ref,
Expand All @@ -66,16 +68,19 @@ const DialogContent = React.forwardRef<
}, [containerId]);
const positionClass =
position === DialogPosition.Right
? 'right-0 h-screen top-0 w-full max-w-[500px]'
: 'max-h-[60vh] left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-xl w-80 max-w-[85vw]';
? 'right-0 h-screen top-0 w-full'
: 'max-h-[60vh] left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-xl';
const widthClass =
position === DialogPosition.Right ? 'md:w-96 max-w-[500px]' : customWidth;
return (
<RadixDialog.Portal container={containerElement}>
<DialogOverlay showCloseIcon={showCloseOnOverlay} position={position} />
<RadixDialog.Content
ref={ref}
className={cx(
'fixed z-[99998] flex flex-col justify-center overflow-hidden bg-primary-100 dark:bg-neutral-6 md:w-96',
'fixed z-[99999] flex flex-col justify-center overflow-hidden bg-primary-100 dark:bg-neutral-6',
positionClass,
widthClass,
)}
{...props}
>
Expand Down
44 changes: 33 additions & 11 deletions apps/wallet-dashboard/app/(protected)/vesting/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@

'use client';

import { Banner, StakeDialog, TimelockedUnstakePopup } from '@/components';
import { useStakeDialog } from '@/components/Dialogs/Staking/hooks/useStakeDialog';
import {
Banner,
StakeDialog,
TimelockedUnstakePopup,
useStakeDialog,
VestingScheduleDialog,
} from '@/components';
import { useGetCurrentEpochStartTimestamp, useNotifications, usePopups } from '@/hooks';
import {
buildSupplyIncreaseVestingSchedule,
formatDelegatedTimelockedStake,
getLatestOrEarliestSupplyIncreaseVestingPayout,
getVestingOverview,
Expand Down Expand Up @@ -52,13 +58,14 @@ import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils';
import { Calendar, StarHex } from '@iota/ui-icons';
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';

function VestingDashboardPage(): JSX.Element {
const account = useCurrentAccount();
const queryClient = useQueryClient();
const iotaClient = useIotaClient();
const router = useRouter();
const [isVestingScheduleDialogOpen, setIsVestingScheduleDialogOpen] = useState(false);
const { addNotification } = useNotifications();
const { openPopup, closePopup } = usePopups();
const { data: currentEpochMs } = useGetCurrentEpochStartTimestamp();
Expand Down Expand Up @@ -101,9 +108,19 @@ function VestingDashboardPage(): JSX.Element {

const nextPayout = getLatestOrEarliestSupplyIncreaseVestingPayout(
[...timelockedMapped, ...timelockedstakedMapped],
Number(currentEpochMs),
false,
);

const lastPayout = getLatestOrEarliestSupplyIncreaseVestingPayout(
[...timelockedMapped, ...timelockedstakedMapped],
Number(currentEpochMs),
true,
);

const vestingPortfolio =
lastPayout && buildSupplyIncreaseVestingSchedule(lastPayout, Number(currentEpochMs));

const formattedLastPayoutExpirationTime = useCountdownByTimestamp(
Number(nextPayout?.expirationTimestampMs),
);
Expand Down Expand Up @@ -206,12 +223,15 @@ function VestingDashboardPage(): JSX.Element {
);
}

function openReceiveTokenPopup(): void {
setIsVestingScheduleDialogOpen(true);
}

useEffect(() => {
if (!supplyIncreaseVestingEnabled) {
router.push('/');
}
}, [router, supplyIncreaseVestingEnabled]);

return (
<div className="flex w-full max-w-xl flex-col gap-lg justify-self-center">
<Panel>
Expand Down Expand Up @@ -264,17 +284,19 @@ function VestingDashboardPage(): JSX.Element {
/>
<CardAction
type={CardActionType.Button}
onClick={() => {
/*Open schedule dialog*/
}}
onClick={openReceiveTokenPopup}
title="See All"
buttonType={ButtonType.Secondary}
buttonDisabled={
!vestingSchedule.availableStaking ||
vestingSchedule.availableStaking === 0
}
buttonDisabled={!vestingPortfolio}
/>
</Card>
{vestingPortfolio && (
<VestingScheduleDialog
open={isVestingScheduleDialogOpen}
setOpen={setIsVestingScheduleDialogOpen}
vestingPortfolio={vestingPortfolio}
/>
)}
</div>
</Panel>
{timelockedstakedMapped.length === 0 ? (
Expand Down
1 change: 1 addition & 0 deletions apps/wallet-dashboard/components/Dialogs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
export * from './SendToken';
export * from './ReceiveFundsDialog';
export * from './Staking';
export * from './vesting';
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { useGetCurrentEpochStartTimestamp } from '@/hooks';
import { DisplayStats, DisplayStatsType } from '@iota/apps-ui-kit';
import { useFormatCoin } from '@iota/core';
import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils';
import { LockLocked } from '@iota/ui-icons';

interface VestingScheduleBoxProps {
amount: number;
expirationTimestampMs: number;
}

export function VestingScheduleBox({
amount,
expirationTimestampMs,
}: VestingScheduleBoxProps): React.JSX.Element {
const [formattedAmountVested, amountVestedSymbol] = useFormatCoin(amount, IOTA_TYPE_ARG);
const { data: currentEpochMs } = useGetCurrentEpochStartTimestamp();

const isLocked = expirationTimestampMs > Number(currentEpochMs);
return (
<DisplayStats
label={new Date(expirationTimestampMs).toLocaleDateString()}
value={`${formattedAmountVested} ${amountVestedSymbol}`}
type={isLocked ? DisplayStatsType.Default : DisplayStatsType.Secondary}
icon={isLocked && <LockLocked className="h-4 w-4" />}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { SupplyIncreaseVestingPortfolio } from '@/lib/interfaces';
import { Dialog, DialogContent, DialogBody, Header } from '@iota/apps-ui-kit';
import { VestingScheduleBox } from './VestingScheduleBox';

interface VestingScheduleDialogProps {
setOpen: (bool: boolean) => void;
open: boolean;
vestingPortfolio: SupplyIncreaseVestingPortfolio;
}

export function VestingScheduleDialog({
open,
setOpen,
vestingPortfolio,
}: VestingScheduleDialogProps): React.JSX.Element {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent
containerId="overlay-portal-container"
customWidth="max-w-md sm:max-w-xl md:max-w-5xl w-full"
>
<Header title="Rewards Schedule" onClose={() => setOpen(false)} titleCentered />
<DialogBody>
<div className="h-[440px] overflow-y-auto">
<div className="grid grid-cols-1 gap-sm sm:grid-cols-2 md:grid-cols-4">
{vestingPortfolio?.map((vestingObject, index) => (
<VestingScheduleBox
key={index}
amount={vestingObject.amount}
expirationTimestampMs={vestingObject.expirationTimestampMs}
/>
))}
</div>
</div>
</DialogBody>
</DialogContent>
</Dialog>
);
}
5 changes: 5 additions & 0 deletions apps/wallet-dashboard/components/Dialogs/vesting/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

export * from './VestingScheduleDialog';
export * from './VestingScheduleBox';
26 changes: 20 additions & 6 deletions apps/wallet-dashboard/lib/utils/vesting/vesting.spec.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { MILLISECONDS_PER_HOUR } from '@iota/core/constants/time.constants';
import {
mockedTimelockedStackedObjectsWithDynamicDate,
MOCKED_SUPPLY_INCREASE_VESTING_TIMELOCKED_OBJECTS,
MOCKED_VESTING_TIMELOCKED_STAKED_OBJECTS,
SUPPLY_INCREASE_STAKER_VESTING_DURATION,
SUPPLY_INCREASE_VESTING_PAYOUTS_IN_1_YEAR,
} from '../../constants';

import { SupplyIncreaseUserType, SupplyIncreaseVestingPayout } from '../../interfaces';
import { formatDelegatedTimelockedStake, isTimelockedObject } from '../timelock';

import {
getVestingOverview,
buildSupplyIncreaseVestingSchedule as buildVestingPortfolio,
Expand All @@ -20,6 +19,8 @@ import {
getSupplyIncreaseVestingUserType,
} from './vesting';

const MOCKED_CURRENT_EPOCH_TIMESTAMP = Date.now() + MILLISECONDS_PER_HOUR * 6; // 6 hours later

describe('get last supply increase vesting payout', () => {
it('should get the object with highest expirationTimestampMs', () => {
const timelockedObjects = MOCKED_SUPPLY_INCREASE_VESTING_TIMELOCKED_OBJECTS;
Expand All @@ -30,7 +31,10 @@ describe('get last supply increase vesting payout', () => {
MOCKED_SUPPLY_INCREASE_VESTING_TIMELOCKED_OBJECTS.length - 1
];

const lastPayout = getLatestOrEarliestSupplyIncreaseVestingPayout(timelockedObjects);
const lastPayout = getLatestOrEarliestSupplyIncreaseVestingPayout(
timelockedObjects,
MOCKED_CURRENT_EPOCH_TIMESTAMP,
);

expect(lastPayout?.expirationTimestampMs).toEqual(expectedObject.expirationTimestampMs);
expect(lastPayout?.amount).toEqual(expectedObject.locked.value);
Expand Down Expand Up @@ -61,7 +65,10 @@ describe('build supply increase staker vesting portfolio', () => {
it('should build with mocked timelocked objects', () => {
const timelockedObjects = MOCKED_SUPPLY_INCREASE_VESTING_TIMELOCKED_OBJECTS;

const lastPayout = getLatestOrEarliestSupplyIncreaseVestingPayout(timelockedObjects);
const lastPayout = getLatestOrEarliestSupplyIncreaseVestingPayout(
timelockedObjects,
MOCKED_CURRENT_EPOCH_TIMESTAMP,
);

expect(lastPayout).toBeDefined();

Expand All @@ -78,6 +85,7 @@ describe('build supply increase staker vesting portfolio', () => {
formatDelegatedTimelockedStake(timelockedStakedObjects);
const lastPayout = getLatestOrEarliestSupplyIncreaseVestingPayout(
extendedTimelockedStakedObjects,
MOCKED_CURRENT_EPOCH_TIMESTAMP,
);

expect(lastPayout).toBeDefined();
Expand All @@ -96,7 +104,10 @@ describe('build supply increase staker vesting portfolio', () => {
formatDelegatedTimelockedStake(timelockedStakedObjects);
const mixedObjects = [...timelockedObjects, ...extendedTimelockedStakedObjects];

const lastPayout = getLatestOrEarliestSupplyIncreaseVestingPayout(mixedObjects);
const lastPayout = getLatestOrEarliestSupplyIncreaseVestingPayout(
mixedObjects,
MOCKED_CURRENT_EPOCH_TIMESTAMP,
);
expect(lastPayout).toBeDefined();

const vestingPortfolio = buildVestingPortfolio(lastPayout!, Date.now());
Expand Down Expand Up @@ -209,7 +220,10 @@ describe('vesting overview', () => {
formatDelegatedTimelockedStake(timelockedStakedObjects);
const mixedObjects = [...timelockedObjects, ...extendedTimelockedStakedObjects];

const lastPayout = getLatestOrEarliestSupplyIncreaseVestingPayout(mixedObjects)!;
const lastPayout = getLatestOrEarliestSupplyIncreaseVestingPayout(
mixedObjects,
MOCKED_CURRENT_EPOCH_TIMESTAMP,
)!;
const totalAmount =
(SUPPLY_INCREASE_STAKER_VESTING_DURATION *
SUPPLY_INCREASE_VESTING_PAYOUTS_IN_1_YEAR *
Expand Down
22 changes: 14 additions & 8 deletions apps/wallet-dashboard/lib/utils/vesting/vesting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { IotaObjectData } from '@iota/iota-sdk/client';

export function getLatestOrEarliestSupplyIncreaseVestingPayout(
objects: (TimelockedObject | ExtendedDelegatedTimelockedStake)[],
currentEpochTimestamp: number,
useLastPayout: boolean = true,
): SupplyIncreaseVestingPayout | undefined {
const vestingObjects = objects.filter(isSupplyIncreaseVestingObject);
Expand All @@ -41,7 +42,7 @@ export function getLatestOrEarliestSupplyIncreaseVestingPayout(
let payouts: SupplyIncreaseVestingPayout[] = Array.from(vestingPayoutMap.values());

if (!useLastPayout) {
payouts = payouts.filter((payout) => payout.expirationTimestampMs >= Date.now());
payouts = payouts.filter((payout) => payout.expirationTimestampMs >= currentEpochTimestamp);
}

return payouts.sort((a, b) =>
Expand Down Expand Up @@ -126,20 +127,25 @@ export function buildSupplyIncreaseVestingSchedule(

const payoutsCount = getSupplyIncreaseVestingPayoutsCount(userType);

return Array.from({ length: payoutsCount }).map((_, i) => ({
amount: referencePayout.amount,
expirationTimestampMs:
referencePayout.expirationTimestampMs -
SUPPLY_INCREASE_VESTING_PAYOUT_SCHEDULE_MILLISECONDS * i,
}));
return Array.from({ length: payoutsCount })
.map((_, i) => ({
amount: referencePayout.amount,
expirationTimestampMs:
referencePayout.expirationTimestampMs -
SUPPLY_INCREASE_VESTING_PAYOUT_SCHEDULE_MILLISECONDS * i,
}))
.sort((a, b) => a.expirationTimestampMs - b.expirationTimestampMs);
}

export function getVestingOverview(
objects: (TimelockedObject | ExtendedDelegatedTimelockedStake)[],
currentEpochTimestamp: number,
): VestingOverview {
const vestingObjects = objects.filter(isSupplyIncreaseVestingObject);
const latestPayout = getLatestOrEarliestSupplyIncreaseVestingPayout(vestingObjects);
const latestPayout = getLatestOrEarliestSupplyIncreaseVestingPayout(
vestingObjects,
currentEpochTimestamp,
);

if (vestingObjects.length === 0 || !latestPayout) {
return {
Expand Down

0 comments on commit 2fd6d93

Please sign in to comment.