Skip to content

Commit

Permalink
feat(wallet-dashboard): add supply increase vesting overview to homep…
Browse files Browse the repository at this point in the history
…age (#4420)

* feat: add supply increase vesting overview to homepage

* fix: linter

* fix(dashboard): remove isHoverable prop

* fix(dashboard): use showDays in formatCountdown

* fix(dashboard): rename vesting by supplyIncreaseVesting

* fix: format

* fix(dashboard): build

* fix(dashboard): format

* fix(dashboard): styles

* fix(dashboard): isTimelockedStakedObjectsLoading name and join 2 invalidateQueries in 1

* fix(dashboard): format

* fix(dasboard): undo the split of the query invalidation

* fix(dashboard): rename unlockAllsupplyIncreaseVesting and disable new stake button

* feat(dashboard): add object for the options in useCountdownByTimestamp

* fix: linter

* fix: linter

* fix(core): improve naming

* fix(core): linter

---------

Co-authored-by: evavirseda <[email protected]>
Co-authored-by: Bran <[email protected]>
  • Loading branch information
3 people authored Dec 18, 2024
1 parent 6b52248 commit b207e70
Show file tree
Hide file tree
Showing 8 changed files with 383 additions and 94 deletions.
34 changes: 26 additions & 8 deletions apps/core/src/hooks/useCountdownByTimestamp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,17 @@ import {
MILLISECONDS_PER_SECOND,
} from '../constants';

export function useCountdownByTimestamp(initialTimestamp: number | null): string {
interface FormatCountdownOptions {
showSeconds?: boolean;
showMinutes?: boolean;
showHours?: boolean;
showDays?: boolean;
}

export function useCountdownByTimestamp(
initialTimestamp: number | null,
options?: FormatCountdownOptions,
): string {
const [timeRemainingMs, setTimeRemainingMs] = useState<number>(0);

useEffect(() => {
Expand All @@ -22,11 +32,19 @@ export function useCountdownByTimestamp(initialTimestamp: number | null): string

return () => clearInterval(interval);
}, [initialTimestamp]);
const formattedCountdown = formatCountdown(timeRemainingMs);
const formattedCountdown = formatCountdown(timeRemainingMs, options);
return formattedCountdown;
}

function formatCountdown(totalMilliseconds: number) {
function formatCountdown(
totalMilliseconds: number,
{
showSeconds = true,
showMinutes = true,
showHours = true,
showDays = true,
}: FormatCountdownOptions = {},
) {
const days = Math.floor(totalMilliseconds / MILLISECONDS_PER_DAY);
const hours = Math.floor((totalMilliseconds % MILLISECONDS_PER_DAY) / MILLISECONDS_PER_HOUR);
const minutes = Math.floor(
Expand All @@ -36,11 +54,11 @@ function formatCountdown(totalMilliseconds: number) {
(totalMilliseconds % MILLISECONDS_PER_MINUTE) / MILLISECONDS_PER_SECOND,
);

const timeUnits = [];
if (days > 0) timeUnits.push(`${days}d`);
if (hours > 0) timeUnits.push(`${hours}h`);
if (minutes > 0) timeUnits.push(`${minutes}m`);
if (seconds > 0 || timeUnits.length === 0) timeUnits.push(`${seconds}s`);
const timeUnits: string[] = [];
if (showDays && days > 0) timeUnits.push(`${days}d`);
if (showHours && hours > 0) timeUnits.push(`${hours}h`);
if (showMinutes && minutes > 0) timeUnits.push(`${minutes}m`);
if (showSeconds && (seconds > 0 || timeUnits.length === 0)) timeUnits.push(`${seconds}s`);

return timeUnits.join(' ');
}
6 changes: 3 additions & 3 deletions apps/wallet-dashboard/app/(protected)/home/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
TransactionsOverview,
StakingOverview,
MigrationOverview,
SupplyIncreaseVestingOverview,
} from '@/components';
import { useFeature } from '@growthbook/growthbook-react';
import { Feature } from '@iota/core';
Expand All @@ -18,6 +19,7 @@ function HomeDashboardPage(): JSX.Element {
const account = useCurrentAccount();

const stardustMigrationEnabled = useFeature<boolean>(Feature.StardustMigration).value;
const supplyIncreaseVestingEnabled = useFeature<boolean>(Feature.SupplyIncreaseVesting).value;

return (
<main className="flex flex-1 flex-col items-center space-y-8 py-md">
Expand All @@ -34,9 +36,7 @@ function HomeDashboardPage(): JSX.Element {
<div style={{ gridArea: 'coins' }}>
<MyCoins />
</div>
<div style={{ gridArea: 'vesting' }} className="flex grow overflow-hidden">
Vesting
</div>
{supplyIncreaseVestingEnabled && <SupplyIncreaseVestingOverview />}
<div style={{ gridArea: 'activity' }} className="flex grow overflow-hidden">
<TransactionsOverview />
</div>
Expand Down
118 changes: 35 additions & 83 deletions apps/wallet-dashboard/app/(protected)/vesting/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,8 @@ import {
} from '@/components';
import { UnstakeDialogView } from '@/components/Dialogs/unstake/enums';
import { useUnstakeDialog } from '@/components/Dialogs/unstake/hooks';
import { useGetCurrentEpochStartTimestamp, useNotifications } from '@/hooks';
import {
buildSupplyIncreaseVestingSchedule,
formatDelegatedTimelockedStake,
getLatestOrEarliestSupplyIncreaseVestingPayout,
getVestingOverview,
groupTimelockedStakedObjects,
isTimelockedUnlockable,
mapTimelockObjects,
TimelockedStakedObjectsGrouped,
} from '@/lib/utils';
import { useGetSupplyIncreaseVestingObjects, useNotifications } from '@/hooks';
import { groupTimelockedStakedObjects, TimelockedStakedObjectsGrouped } from '@/lib/utils';
import { NotificationType } from '@/stores/notificationStore';
import { useFeature } from '@growthbook/growthbook-react';
import {
Expand All @@ -46,13 +37,9 @@ import {
} from '@iota/apps-ui-kit';
import {
Theme,
TIMELOCK_IOTA_TYPE,
useFormatCoin,
useGetActiveValidatorsInfo,
useGetAllOwnedObjects,
useGetTimelockedStakedObjects,
useTheme,
useUnlockTimelockedObjectsTransaction,
useCountdownByTimestamp,
Feature,
} from '@iota/core';
Expand All @@ -74,24 +61,13 @@ export default function VestingDashboardPage(): JSX.Element {
const [timelockedObjectsToUnstake, setTimelockedObjectsToUnstake] =
useState<TimelockedStakedObjectsGrouped | null>(null);
const account = useCurrentAccount();
const address = account?.address || '';
const iotaClient = useIotaClient();
const router = useRouter();
const { data: system } = useIotaClientQuery('getLatestIotaSystemState');
const [isVestingScheduleDialogOpen, setIsVestingScheduleDialogOpen] = useState(false);
const { addNotification } = useNotifications();
const { data: currentEpochMs } = useGetCurrentEpochStartTimestamp();
const { data: activeValidators } = useGetActiveValidatorsInfo();
const { data: timelockedObjects, refetch: refetchGetAllOwnedObjects } = useGetAllOwnedObjects(
account?.address || '',
{
StructType: TIMELOCK_IOTA_TYPE,
},
);
const {
data: timelockedStakedObjects,
isLoading: istimelockedStakedObjectsLoading,
refetch: refetchTimelockedStakedObjects,
} = useGetTimelockedStakedObjects(account?.address || '');
const { mutateAsync: signAndExecuteTransaction } = useSignAndExecuteTransaction();
const { theme } = useTheme();

Expand All @@ -102,16 +78,19 @@ export default function VestingDashboardPage(): JSX.Element {

const supplyIncreaseVestingEnabled = useFeature<boolean>(Feature.SupplyIncreaseVesting).value;

const timelockedMapped = mapTimelockObjects(timelockedObjects || []);
const timelockedstakedMapped = formatDelegatedTimelockedStake(timelockedStakedObjects || []);
const {
nextPayout,
supplyIncreaseVestingPortfolio,
supplyIncreaseVestingSchedule,
supplyIncreaseVestingMapped,
supplyIncreaseVestingStakedMapped,
isTimelockedStakedObjectsLoading,
unlockAllSupplyIncreaseVesting,
refreshStakeList,
} = useGetSupplyIncreaseVestingObjects(address);

const timelockedStakedObjectsGrouped: TimelockedStakedObjectsGrouped[] =
groupTimelockedStakedObjects(timelockedstakedMapped || []);

const vestingSchedule = getVestingOverview(
[...timelockedMapped, ...timelockedstakedMapped],
Number(currentEpochMs),
);
groupTimelockedStakedObjects(supplyIncreaseVestingStakedMapped || []);

const {
isDialogStakeOpen,
Expand All @@ -132,37 +111,22 @@ export default function VestingDashboardPage(): JSX.Element {
setView: setUnstakeDialogView,
} = useUnstakeDialog();

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),
);

const [formattedTotalVested, vestedSymbol] = useFormatCoin(
vestingSchedule.totalVested,
supplyIncreaseVestingSchedule.totalVested,
IOTA_TYPE_ARG,
);

const [formattedTotalLocked, lockedSymbol] = useFormatCoin(
vestingSchedule.totalLocked,
supplyIncreaseVestingSchedule.totalLocked,
IOTA_TYPE_ARG,
);

const [formattedAvailableClaiming, availableClaimingSymbol] = useFormatCoin(
vestingSchedule.availableClaiming,
supplyIncreaseVestingSchedule.availableClaiming,
IOTA_TYPE_ARG,
);

Expand All @@ -178,30 +142,15 @@ export default function VestingDashboardPage(): JSX.Element {
}

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

const [totalEarnedFormatted, totalEarnedSymbol] = useFormatCoin(
vestingSchedule.totalEarned,
supplyIncreaseVestingSchedule.totalEarned,
IOTA_TYPE_ARG,
);

const unlockedTimelockedObjects = timelockedMapped?.filter((timelockedObject) =>
isTimelockedUnlockable(timelockedObject, Number(currentEpochMs)),
);
const unlockedTimelockedObjectIds: string[] =
unlockedTimelockedObjects.map((timelocked) => timelocked.id.id) || [];
const { data: unlockAllTimelockedObjects } = useUnlockTimelockedObjectsTransaction(
account?.address || '',
unlockedTimelockedObjectIds,
);

function refreshStakeList() {
refetchTimelockedStakedObjects();
refetchGetAllOwnedObjects();
}

function handleOnSuccess(digest: string): void {
setTimelockedObjectsToUnstake(null);

Expand All @@ -213,13 +162,13 @@ export default function VestingDashboardPage(): JSX.Element {
}

const handleCollect = () => {
if (!unlockAllTimelockedObjects?.transactionBlock) {
if (!unlockAllSupplyIncreaseVesting?.transactionBlock) {
addNotification('Failed to create a Transaction', NotificationType.Error);
return;
}
signAndExecuteTransaction(
{
transaction: unlockAllTimelockedObjects.transactionBlock,
transaction: unlockAllSupplyIncreaseVesting.transactionBlock,
},
{
onSuccess: (tx) => {
Expand Down Expand Up @@ -258,7 +207,7 @@ export default function VestingDashboardPage(): JSX.Element {
}
}, [router, supplyIncreaseVestingEnabled]);

if (istimelockedStakedObjectsLoading) {
if (isTimelockedStakedObjectsLoading) {
return (
<div className="flex w-full max-w-4xl items-start justify-center justify-self-center">
<LoadingIndicator />
Expand Down Expand Up @@ -304,8 +253,8 @@ export default function VestingDashboardPage(): JSX.Element {
title="Collect"
buttonType={ButtonType.Primary}
buttonDisabled={
!vestingSchedule.availableClaiming ||
vestingSchedule.availableClaiming === 0n
!supplyIncreaseVestingSchedule.availableClaiming ||
supplyIncreaseVestingSchedule.availableClaiming === 0n
}
/>
</Card>
Expand All @@ -329,20 +278,20 @@ export default function VestingDashboardPage(): JSX.Element {
onClick={openReceiveTokenDialog}
title="See All"
buttonType={ButtonType.Secondary}
buttonDisabled={!vestingPortfolio}
buttonDisabled={!supplyIncreaseVestingPortfolio}
/>
</Card>
{vestingPortfolio && (
{supplyIncreaseVestingPortfolio && (
<VestingScheduleDialog
open={isVestingScheduleDialogOpen}
setOpen={setIsVestingScheduleDialogOpen}
vestingPortfolio={vestingPortfolio}
vestingPortfolio={supplyIncreaseVestingPortfolio}
/>
)}
</div>
</Panel>

{timelockedstakedMapped.length === 0 ? (
{supplyIncreaseVestingMapped.length === 0 ? (
<Banner
videoSrc={videoSrc}
title="Stake Vested Tokens"
Expand All @@ -353,7 +302,7 @@ export default function VestingDashboardPage(): JSX.Element {
) : null}
</div>

{timelockedstakedMapped.length !== 0 ? (
{supplyIncreaseVestingMapped.length !== 0 ? (
<div className="flex w-full md:w-1/2">
<Panel>
<Title
Expand All @@ -362,7 +311,9 @@ export default function VestingDashboardPage(): JSX.Element {
<Button
type={ButtonType.Primary}
text="Stake"
disabled={vestingSchedule.availableStaking === 0n}
disabled={
supplyIncreaseVestingSchedule.availableStaking === 0n
}
onClick={() => {
setStakeDialogView(StakeDialogView.SelectValidator);
}}
Expand Down Expand Up @@ -422,8 +373,9 @@ export default function VestingDashboardPage(): JSX.Element {
setView={setStakeDialogView}
selectedValidator={selectedValidator}
setSelectedValidator={setSelectedValidator}
maxStakableTimelockedAmount={BigInt(vestingSchedule.availableStaking)}
onUnstakeClick={openUnstakeDialog}
maxStakableTimelockedAmount={BigInt(
supplyIncreaseVestingSchedule.availableStaking,
)}
/>
)}

Expand Down
Loading

0 comments on commit b207e70

Please sign in to comment.