From 64e731fb14ec88f78a5a2d3da6a7dd31f355c15b Mon Sep 17 00:00:00 2001
From: cpl121 <cpeon@boxfish.studio>
Date: Wed, 15 Jan 2025 13:35:26 +0100
Subject: [PATCH] feat(wallet-dashboard): improve tx history & details

---
 .../transaction/TransactionIcon.tsx           | 13 +++++++-
 apps/core/src/constants/timelock.constants.ts |  5 +--
 .../src/interfaces/transactions.interfaces.ts |  2 ++
 .../checkIfIsMigrationTransaction.ts          | 32 +++++++++++++++++++
 ...SupplyIncreaseVestingCollectTransaction.ts | 31 ++++++++++++++++++
 .../utils/transaction/getTransactionAction.ts | 23 ++++++++++---
 apps/core/src/utils/transaction/index.ts      |  2 ++
 apps/wallet-dashboard/lib/interfaces/index.ts |  1 -
 .../lib/interfaces/transactions.interfaces.ts | 29 -----------------
 9 files changed, 101 insertions(+), 37 deletions(-)
 create mode 100644 apps/core/src/utils/transaction/checkIfIsMigrationTransaction.ts
 create mode 100644 apps/core/src/utils/transaction/checkIfIsSupplyIncreaseVestingCollectTransaction.ts
 delete mode 100644 apps/wallet-dashboard/lib/interfaces/transactions.interfaces.ts

diff --git a/apps/core/src/components/transaction/TransactionIcon.tsx b/apps/core/src/components/transaction/TransactionIcon.tsx
index 55b54f0fc5b..02a547bf25f 100644
--- a/apps/core/src/components/transaction/TransactionIcon.tsx
+++ b/apps/core/src/components/transaction/TransactionIcon.tsx
@@ -2,7 +2,16 @@
 // SPDX-License-Identifier: Apache-2.0
 
 import { LoadingIndicator } from '@iota/apps-ui-kit';
-import { ArrowBottomLeft, ArrowTopRight, Info, IotaLogoMark, Person, Stake } from '@iota/ui-icons';
+import {
+    ArrowBottomLeft,
+    ArrowTopRight,
+    Info,
+    IotaLogoMark,
+    Migration,
+    Person,
+    Stake,
+    Vesting,
+} from '@iota/ui-icons';
 
 const ICON_COLORS = {
     primary: 'text-primary-30',
@@ -21,6 +30,8 @@ const icons = {
     PersonalMessage: <Person className={ICON_COLORS.primary} />,
     ['Timelocked Staked']: <Stake className={ICON_COLORS.primary} />,
     ['Timelocked Unstaked']: <Stake className={ICON_COLORS.primary} />,
+    Migration: <Migration className={ICON_COLORS.primary} />,
+    ['Timelocked Collect']: <Vesting className={ICON_COLORS.primary} />,
 };
 
 interface TransactionIconProps {
diff --git a/apps/core/src/constants/timelock.constants.ts b/apps/core/src/constants/timelock.constants.ts
index 6b9ab9c2627..21c7999aef0 100644
--- a/apps/core/src/constants/timelock.constants.ts
+++ b/apps/core/src/constants/timelock.constants.ts
@@ -1,5 +1,6 @@
 // Copyright (c) 2024 IOTA Stiftung
 // SPDX-License-Identifier: Apache-2.0
 
-export const TIMELOCK_IOTA_TYPE = '0x2::timelock::TimeLock<0x2::balance::Balance<0x2::iota::IOTA>>';
-export const TIMELOCK_STAKED_TYPE = '0x3::timelocked_staking::TimelockedStakedIota';
+export const TIMELOCK_MODULE = 'timelock';
+export const TIMELOCK_IOTA_TYPE = `0x2::${TIMELOCK_MODULE}::TimeLock<0x2::balance::Balance<0x2::iota::IOTA>>`;
+export const TIMELOCK_STAKED_TYPE = `0x3::timelocked_staking::TimelockedStakedIota`;
diff --git a/apps/core/src/interfaces/transactions.interfaces.ts b/apps/core/src/interfaces/transactions.interfaces.ts
index 4c4ea3c1962..9262652ee42 100644
--- a/apps/core/src/interfaces/transactions.interfaces.ts
+++ b/apps/core/src/interfaces/transactions.interfaces.ts
@@ -18,6 +18,8 @@ export enum TransactionAction {
     Unstaked = 'Unstaked',
     TimelockedStaked = 'Timelocked Staked',
     TimelockedUnstaked = 'Timelocked Unstaked',
+    TimelockedCollect = 'Timelocked Collect',
+    Migration = 'Migration',
     Rewards = 'Rewards',
     PersonalMessage = 'PersonalMessage',
 }
diff --git a/apps/core/src/utils/transaction/checkIfIsMigrationTransaction.ts b/apps/core/src/utils/transaction/checkIfIsMigrationTransaction.ts
new file mode 100644
index 00000000000..c3ddf762c85
--- /dev/null
+++ b/apps/core/src/utils/transaction/checkIfIsMigrationTransaction.ts
@@ -0,0 +1,32 @@
+// Copyright (c) 2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+import type {
+    IotaTransaction,
+    IotaTransactionBlockResponse,
+    MoveCallIotaTransaction,
+} from '@iota/iota-sdk/client';
+import { STARDUST_PACKAGE_ID } from '../../constants';
+
+export function checkIfIsMigrationTransaction(
+    transaction: IotaTransactionBlockResponse['transaction'],
+) {
+    if (!transaction || transaction.data.transaction.kind !== 'ProgrammableTransaction')
+        return { isMigration: false };
+    const moveCallTxs = transaction.data.transaction.transactions.filter(isMoveCall);
+    const isMigration = moveCallTxs.some(
+        (tx) =>
+            tx.MoveCall.package === STARDUST_PACKAGE_ID &&
+            tx.MoveCall.function === 'extract_assets',
+    );
+
+    return {
+        isMigration,
+    };
+}
+
+function isMoveCall(
+    transaction: IotaTransaction,
+): transaction is { MoveCall: MoveCallIotaTransaction } {
+    return 'MoveCall' in transaction;
+}
diff --git a/apps/core/src/utils/transaction/checkIfIsSupplyIncreaseVestingCollectTransaction.ts b/apps/core/src/utils/transaction/checkIfIsSupplyIncreaseVestingCollectTransaction.ts
new file mode 100644
index 00000000000..aa3f47a307c
--- /dev/null
+++ b/apps/core/src/utils/transaction/checkIfIsSupplyIncreaseVestingCollectTransaction.ts
@@ -0,0 +1,31 @@
+// Copyright (c) 2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+import type {
+    IotaTransaction,
+    IotaTransactionBlockResponse,
+    MoveCallIotaTransaction,
+} from '@iota/iota-sdk/client';
+import { TIMELOCK_MODULE } from '../../';
+
+export function checkIfIsSupplyIncreaseVestingCollectTransaction(
+    transaction: IotaTransactionBlockResponse['transaction'],
+) {
+    if (!transaction || transaction.data.transaction.kind !== 'ProgrammableTransaction')
+        return { isSupplyIncreaseVestingCollect: false };
+    const moveCallTxs = transaction.data.transaction.transactions
+        .filter(isMoveCall)
+        .filter((tx) => tx.MoveCall.module === TIMELOCK_MODULE);
+    const isSupplyIncreaseVestingCollect =
+        moveCallTxs.length > 0 && moveCallTxs.every((tx) => tx.MoveCall.function === 'unlock');
+
+    return {
+        isSupplyIncreaseVestingCollect,
+    };
+}
+
+function isMoveCall(
+    transaction: IotaTransaction,
+): transaction is { MoveCall: MoveCallIotaTransaction } {
+    return 'MoveCall' in transaction;
+}
diff --git a/apps/core/src/utils/transaction/getTransactionAction.ts b/apps/core/src/utils/transaction/getTransactionAction.ts
index 5eeb25a4946..c781a42bcce 100644
--- a/apps/core/src/utils/transaction/getTransactionAction.ts
+++ b/apps/core/src/utils/transaction/getTransactionAction.ts
@@ -4,12 +4,17 @@
 
 import { IotaTransactionBlockResponse } from '@iota/iota-sdk/client';
 import { TransactionAction } from '../../interfaces';
-import { checkIfIsTimelockedStaking } from '../stake/checkIfIsTimelockedStaking';
+import { checkIfIsTimelockedStaking } from '../stake';
+import {
+    checkIfIsMigrationTransaction,
+    checkIfIsSupplyIncreaseVestingCollectTransaction,
+} from '..';
 
 export const getTransactionAction = (
     transaction: IotaTransactionBlockResponse,
     currentAddress?: string,
 ) => {
+    const sender = transaction.transaction?.data.sender;
     const {
         isTimelockedStaking,
         isTimelockedUnstaking,
@@ -17,14 +22,24 @@ export const getTransactionAction = (
         unstakeTypeTransaction,
     } = checkIfIsTimelockedStaking(transaction?.events);
 
-    if (stakeTypeTransaction) {
+    const { isMigration } = checkIfIsMigrationTransaction(transaction.transaction);
+    const { isSupplyIncreaseVestingCollect } = checkIfIsSupplyIncreaseVestingCollectTransaction(
+        transaction.transaction,
+    );
+
+    if (isMigration) {
+        return TransactionAction.Migration;
+    } else if (isSupplyIncreaseVestingCollect) {
+        return TransactionAction.TimelockedCollect;
+    } else if (stakeTypeTransaction) {
         return isTimelockedStaking ? TransactionAction.TimelockedStaked : TransactionAction.Staked;
     } else if (unstakeTypeTransaction) {
         return isTimelockedUnstaking
             ? TransactionAction.TimelockedUnstaked
             : TransactionAction.Unstaked;
+    } else if (!!sender) {
+        return sender === currentAddress ? TransactionAction.Send : TransactionAction.Receive;
     } else {
-        const isSender = transaction.transaction?.data.sender === currentAddress;
-        return isSender ? TransactionAction.Transaction : TransactionAction.Receive;
+        return TransactionAction.Transaction;
     }
 };
diff --git a/apps/core/src/utils/transaction/index.ts b/apps/core/src/utils/transaction/index.ts
index 1951221f6e4..f0f235f6823 100644
--- a/apps/core/src/utils/transaction/index.ts
+++ b/apps/core/src/utils/transaction/index.ts
@@ -13,3 +13,5 @@ export * from './createTokenTransferTransaction';
 export * from './getObjectDisplayLookup';
 export * from './createNftSendValidationSchema';
 export * from './createUnlockTimelockedObjectsTransaction';
+export * from './checkIfIsMigrationTransaction';
+export * from './checkIfIsSupplyIncreaseVestingCollectTransaction';
diff --git a/apps/wallet-dashboard/lib/interfaces/index.ts b/apps/wallet-dashboard/lib/interfaces/index.ts
index 27ca0836eb4..8d652795abd 100644
--- a/apps/wallet-dashboard/lib/interfaces/index.ts
+++ b/apps/wallet-dashboard/lib/interfaces/index.ts
@@ -1,7 +1,6 @@
 // Copyright (c) 2024 IOTA Stiftung
 // SPDX-License-Identifier: Apache-2.0
 
-export * from './transactions.interfaces';
 export * from './timelock.interfaces';
 export * from './vesting.interfaces';
 export * from './appRoute.interfaces';
diff --git a/apps/wallet-dashboard/lib/interfaces/transactions.interfaces.ts b/apps/wallet-dashboard/lib/interfaces/transactions.interfaces.ts
deleted file mode 100644
index 4c4ea3c1962..00000000000
--- a/apps/wallet-dashboard/lib/interfaces/transactions.interfaces.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (c) 2024 IOTA Stiftung
-// SPDX-License-Identifier: Apache-2.0
-
-import { IotaTransactionBlockResponse } from '@iota/iota-sdk/client';
-
-export interface ExtendedTransaction {
-    action: TransactionAction;
-    timestamp?: number;
-    state: TransactionState;
-    raw: IotaTransactionBlockResponse;
-}
-
-export enum TransactionAction {
-    Send = 'Send',
-    Receive = 'Receive',
-    Transaction = 'Transaction',
-    Staked = 'Staked',
-    Unstaked = 'Unstaked',
-    TimelockedStaked = 'Timelocked Staked',
-    TimelockedUnstaked = 'Timelocked Unstaked',
-    Rewards = 'Rewards',
-    PersonalMessage = 'PersonalMessage',
-}
-
-export enum TransactionState {
-    Successful = 'successful',
-    Failed = 'failed',
-    Pending = 'pending',
-}