Skip to content

Commit

Permalink
feat(dashboard): add migration overview (#4329)
Browse files Browse the repository at this point in the history
* feat(dashboard): add migration overview

* feat: refine values

* fix: update summarizeMigratableObjectValues function
  • Loading branch information
VmMad authored Dec 12, 2024
1 parent f11d8cc commit c477d40
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 51 deletions.
154 changes: 108 additions & 46 deletions apps/wallet-dashboard/app/(protected)/migrations/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,39 @@
// SPDX-License-Identifier: Apache-2.0
'use client';

import { VirtualList } from '@/components';
import MigratePopup from '@/components/Popup/Popups/MigratePopup';
import { useGetStardustMigratableObjects, usePopups } from '@/hooks';
import { Button } from '@iota/apps-ui-kit';
import { useCurrentAccount, useIotaClient, useIotaClientContext } from '@iota/dapp-kit';
import { STARDUST_BASIC_OUTPUT_TYPE, STARDUST_NFT_OUTPUT_TYPE } from '@iota/core';
import { getNetwork, IotaObjectData } from '@iota/iota-sdk/client';
import { usePopups } from '@/hooks';
import { summarizeMigratableObjectValues } from '@/lib/utils';
import {
Button,
ButtonSize,
ButtonType,
Card,
CardBody,
CardImage,
ImageShape,
Panel,
Title,
} from '@iota/apps-ui-kit';
import { useCurrentAccount, useIotaClient } from '@iota/dapp-kit';
import { STARDUST_BASIC_OUTPUT_TYPE, STARDUST_NFT_OUTPUT_TYPE, useFormatCoin } from '@iota/core';
import { useGetStardustMigratableObjects } from '@/hooks';
import { useQueryClient } from '@tanstack/react-query';
import { Assets, Clock, IotaLogoMark, Tokens } from '@iota/ui-icons';
import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils';

interface MigrationDisplayCard {
title: string;
subtitle: string;
icon: React.FC;
}

function MigrationDashboardPage(): JSX.Element {
const account = useCurrentAccount();
const address = account?.address || '';
const { openPopup, closePopup } = usePopups();
const queryClient = useQueryClient();
const iotaClient = useIotaClient();
const { network } = useIotaClientContext();
const { explorer } = getNetwork(network);

const {
migratableBasicOutputs,
Expand All @@ -30,12 +46,6 @@ function MigrationDashboardPage(): JSX.Element {
const hasMigratableObjects =
migratableBasicOutputs.length > 0 || migratableNftOutputs.length > 0;

const virtualItem = (asset: IotaObjectData): JSX.Element => (
<a href={`${explorer}/object/${asset.objectId}`} target="_blank" rel="noreferrer">
{asset.objectId}
</a>
);

function handleOnSuccess(digest: string): void {
iotaClient
.waitForTransaction({
Expand Down Expand Up @@ -73,41 +83,93 @@ function MigrationDashboardPage(): JSX.Element {
);
}

const {
accumulatedIotaAmount: accumulatedTimelockedIotaAmount,
totalNativeTokens,
totalVisualAssets,
} = summarizeMigratableObjectValues({
migratableBasicOutputs,
migratableNftOutputs,
address,
});

const [timelockedIotaTokens, symbol] = useFormatCoin(
accumulatedTimelockedIotaAmount,
IOTA_TYPE_ARG,
);

const MIGRATION_CARDS: MigrationDisplayCard[] = [
{
title: `${timelockedIotaTokens} ${symbol}`,
subtitle: 'IOTA Tokens',
icon: IotaLogoMark,
},
{
title: `${totalNativeTokens}`,
subtitle: 'Native Tokens',
icon: Tokens,
},
{
title: `${totalVisualAssets}`,
subtitle: 'Visual Assets',
icon: Assets,
},
];

const timelockedAssetsAmount = unmigratableBasicOutputs.length + unmigratableNftOutputs.length;
const TIMELOCKED_ASSETS_CARDS: MigrationDisplayCard[] = [
{
title: `${timelockedAssetsAmount}`,
subtitle: 'Time-locked',
icon: Clock,
},
];

return (
<div className="flex h-full w-full flex-wrap items-center justify-center space-y-4">
<div className="flex w-1/2 flex-col">
<h1>Migratable Basic Outputs: {migratableBasicOutputs.length}</h1>
<VirtualList
items={migratableBasicOutputs}
estimateSize={() => 30}
render={virtualItem}
/>
</div>
<div className="flex w-1/2 flex-col">
<h1>Unmigratable Basic Outputs: {unmigratableBasicOutputs.length}</h1>
<VirtualList
items={unmigratableBasicOutputs}
estimateSize={() => 30}
render={virtualItem}
/>
</div>
<div className="flex w-1/2 flex-col">
<h1>Migratable NFT Outputs: {migratableNftOutputs.length}</h1>
<VirtualList
items={migratableNftOutputs}
estimateSize={() => 30}
render={virtualItem}
/>
</div>
<div className="flex w-1/2 flex-col">
<h1>Unmigratable NFT Outputs: {unmigratableNftOutputs.length}</h1>
<VirtualList
items={unmigratableNftOutputs}
estimateSize={() => 30}
render={virtualItem}
/>
<div className="flex w-full flex-row justify-center">
<div className="flex w-1/3 flex-col gap-md--rs">
<Panel>
<Title
title="Migration"
trailingElement={
<Button
text="Migrate All"
disabled={!hasMigratableObjects}
onClick={openMigratePopup}
size={ButtonSize.Small}
/>
}
/>
<div className="flex flex-col gap-xs p-md--rs">
{MIGRATION_CARDS.map((card) => (
<Card key={card.subtitle}>
<CardImage shape={ImageShape.SquareRounded}>
<card.icon />
</CardImage>
<CardBody title={card.title} subtitle={card.subtitle} />
</Card>
))}
<Button text="See All" type={ButtonType.Ghost} fullWidth />
</div>
</Panel>

<Panel>
<Title title="Time-locked Assets" />
<div className="flex flex-col gap-xs p-md--rs">
{TIMELOCKED_ASSETS_CARDS.map((card) => (
<Card key={card.subtitle}>
<CardImage shape={ImageShape.SquareRounded}>
<card.icon />
</CardImage>
<CardBody title={card.title} subtitle={card.subtitle} />
</Card>
))}
<Button text="See All" type={ButtonType.Ghost} fullWidth />
</div>
</Panel>
</div>
</div>
<Button text="Migrate" disabled={!hasMigratableObjects} onClick={openMigratePopup} />
</div>
);
}
Expand Down
60 changes: 55 additions & 5 deletions apps/wallet-dashboard/lib/utils/migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,14 @@ export function groupStardustObjectsByMigrationStatus(
const epochUnix = epochTimestamp / 1000;

for (const outputObject of stardustOutputObjects) {
const outputObjectFields = (
outputObject.content as unknown as {
fields: CommonOutputObjectWithUc;
}
).fields;
const outputObjectFields = extractOutputFields(outputObject);

if (outputObjectFields.expiration_uc) {
const unlockableAddress =
outputObjectFields.expiration_uc.fields.unix_time <= epochUnix
? outputObjectFields.expiration_uc.fields.return_address
: outputObjectFields.expiration_uc.fields.owner;

if (unlockableAddress !== address) {
unmigratable.push(outputObject);
continue;
Expand All @@ -49,3 +46,56 @@ export function groupStardustObjectsByMigrationStatus(

return { migratable, unmigratable };
}

interface MigratableObjectsData {
totalNativeTokens: number;
totalVisualAssets: number;
accumulatedIotaAmount: number;
}

export function summarizeMigratableObjectValues({
migratableBasicOutputs,
migratableNftOutputs,
address,
}: {
migratableBasicOutputs: IotaObjectData[];
migratableNftOutputs: IotaObjectData[];
address: string;
}): MigratableObjectsData {
let totalNativeTokens = 0;
let totalIotaAmount = 0;

const totalVisualAssets = migratableNftOutputs.length;
const outputObjects = [...migratableBasicOutputs, ...migratableNftOutputs];

for (const output of outputObjects) {
const outputObjectFields = extractOutputFields(output);

totalIotaAmount += parseInt(outputObjectFields.balance);
totalNativeTokens += parseInt(outputObjectFields.native_tokens.fields.size);
totalIotaAmount += extractStorageDepositReturnAmount(outputObjectFields, address) || 0;
}

return { totalNativeTokens, totalVisualAssets, accumulatedIotaAmount: totalIotaAmount };
}

function extractStorageDepositReturnAmount(
{ storage_deposit_return_uc }: CommonOutputObjectWithUc,
address: string,
): number | null {
if (
storage_deposit_return_uc?.fields &&
storage_deposit_return_uc?.fields.return_address === address
) {
return parseInt(storage_deposit_return_uc?.fields.return_amount);
}
return null;
}

function extractOutputFields(outputObject: IotaObjectData): CommonOutputObjectWithUc {
return (
outputObject.content as unknown as {
fields: CommonOutputObjectWithUc;
}
).fields;
}

0 comments on commit c477d40

Please sign in to comment.