Skip to content

Commit

Permalink
feat(wallet-dashboard): hook timelocked unstaking feature (#1788)
Browse files Browse the repository at this point in the history
* feat(wallet-dashboard): fetch supply increase timelocked portfolio

* feat(wallet-dashboard): add new route to vesting

* feat(wallet-dashboard): update request timelocked objets to get all

* feat(wallet-dashboard): improve the way to get timelock objects

* fix(wallet-dashboard): remove testing constants

* feat(wallet-dashboard): update filter in get timelocked objects

* fix(wallet-dashboard): remove TESTING_SUPPLY_INCREASE_VESTING_LABEL reference

* feat(wallet-dashboard): hook collect(unwrap) feature to dashboard WIP

* refactor: revert prettier changes coming from last merge

* fix(wallet-dashboard): remove debris

* fix(wallet-dashboard): add current epoch ms to getVestingOverview

* feat(wallet-dashboard): add stale time to get current epoch hook

* feat(wallet-dashboard): add timelock stakes to vesting portfolio logic

* feat(wallet-dashboard): add ptb to unlock all timelocked objects

* fix(wallet-dashboard): improvements

* fix(wallet-dashboard): improve filters to get timelocked objects

* feat(wallet-dashboard): map timelocked staked iota response

* fix(wallet-dashboard): update Date.now by current epoch

* fix(wallet-dashboard): improve transaction to unlock timelocked objects

* fix(wallet-dashboard): improve unlock timelocked objects transaction

* fix(wallet-dashboard): improvements and rename variables

* fix(wallet-dashboard): rename hook

* feat(wallet-dashboard): use new endpoint to get staked timelocked objects

* fix(wallet-dashboard): modify mapped data with the new response

* refactor: separate vesting from timelock

* fix(wallet-dashboard): create a util function to check if a timelocked object is unlocked

* fix(wallet-dashboard): remove redundant "all" in unlockedTimelockedObjects

* fix: mising files from refactor

* fix(wallet-dashboard): remove unnecessary conversion

* refactor: update iota-sdk name

* fix(wallet-dashboard): update logic with new DelegatedTimelockedStake interface

* fix(wallet-dashboard): update test logic with new DelegatedTimelockedStake interface

* feat(wallet-dashboard): add hook to get validators info

* feat(wallet-dashboard): group and display timelocked stakes

* feat(wallet-dashboard): hook timelocked unstaking feature

* fix(wallet-dashboard): invalidate query to fetch staked timelocked object after sign the tx

* fix(wallet-dashboard): update delegated staked timelocked interface

* fix(wallet-dashboard): invalidate queries after unstaking

* fix: handling of unstake transaction query invalidation

* fix(wallet-dashboard): rename timelocked staked object and modify the fall back validator name

* fix(wallet-dashboard): group timelocked staked objects

* fix(wallet-dashboard): add waitForTransactionBlock to collect function

* fix(wallet-dashboard): move groupTimelockedStakedObjects to timelock util

* fix(wallet-dashboard): rename useUnlockTimelockedObjects hook

* fix(wallet-dashboard): rename isTimelockedUnlockable function

* fix(wallet-dashboard): vesting tests

* fix(tooling): linter

* fix(wallet-dashboard): bad merge

* fix(wallet-dashboard): improvement startEpoch value

* fix(wallet-dashboard): rename startEpoch to stakeRequestEpoch in TimelockedStakedObjectsGrouped interface

* fix(wallet-dashboard): update merge conflicts

* fix(wallet-dashboard): improve styles

* fix(wallet-dashboard): rename key to invalidate query

---------

Co-authored-by: Begoña Álvarez de la Cruz <[email protected]>
Co-authored-by: Branko Bosnic <[email protected]>
Co-authored-by: Bran <[email protected]>
  • Loading branch information
4 people authored Aug 28, 2024
1 parent 88b8857 commit eba98c4
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 22 deletions.
25 changes: 25 additions & 0 deletions apps/core/src/utils/stake/createTimelockedUnstakeTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { TransactionBlock } from '@iota/iota-sdk/transactions';
import { IOTA_SYSTEM_STATE_OBJECT_ID } from '@iota/iota-sdk/utils';

export function createTimelockedUnstakeTransaction(timelockedStakedObjectIds: string[]) {
const tx = new TransactionBlock();
// TODO: check the max tx limit per ptb
for (const timelockedStakedObjectId of timelockedStakedObjectIds) {
tx.moveCall({
target: `0x3::timelocked_staking::request_withdraw_stake`,
arguments: [
tx.sharedObjectRef({
objectId: IOTA_SYSTEM_STATE_OBJECT_ID,
initialSharedVersion: 1,
mutable: true,
}),
tx.object(timelockedStakedObjectId),
],
});
}

return tx;
}
1 change: 1 addition & 0 deletions apps/core/src/utils/stake/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
export * from './createUnstakeTransaction';
export * from './formatDelegatedStake';
export * from './createStakeTransaction';
export * from './createTimelockedUnstakeTransaction';
57 changes: 35 additions & 22 deletions apps/wallet-dashboard/app/dashboard/vesting/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

'use client';

import { Button } from '@/components';
import { useGetCurrentEpochStartTimestamp, useNotifications } from '@/hooks';
import { Button, TimelockedUnstakePopup } from '@/components';
import { useGetCurrentEpochStartTimestamp, useNotifications, usePopups } from '@/hooks';
import {
formatDelegatedTimelockedStake,
getVestingOverview,
Expand All @@ -26,14 +26,15 @@ import {
useIotaClient,
useSignAndExecuteTransactionBlock,
} from '@iota/dapp-kit';
import { IotaValidatorSummary } from '@iota/iota-sdk/client';
import { useQueryClient } from '@tanstack/react-query';

function VestingDashboardPage(): JSX.Element {
const account = useCurrentAccount();
const queryClient = useQueryClient();
const iotaClient = useIotaClient();

const { addNotification } = useNotifications();
const { openPopup, closePopup } = usePopups();
const { data: currentEpochMs } = useGetCurrentEpochStartTimestamp();
const { data: activeValidators } = useGetActiveValidatorsInfo();
const { data: timelockedObjects } = useGetAllOwnedObjects(account?.address || '', {
Expand All @@ -53,11 +54,9 @@ function VestingDashboardPage(): JSX.Element {
Number(currentEpochMs),
);

function getValidatorName(validatorAddress: string): string {
return (
activeValidators?.find(
(activeValidator) => activeValidator.iotaAddress === validatorAddress,
)?.name || validatorAddress
function getValidatorByAddress(validatorAddress: string): IotaValidatorSummary | undefined {
return activeValidators?.find(
(activeValidator) => activeValidator.iotaAddress === validatorAddress,
);
}

Expand All @@ -78,7 +77,7 @@ function VestingDashboardPage(): JSX.Element {
})
.then(() => {
queryClient.invalidateQueries({
queryKey: ['get-staked-timelocked-objects', account?.address],
queryKey: ['get-timelocked-staked-objects', account?.address],
});
queryClient.invalidateQueries({
queryKey: [
Expand Down Expand Up @@ -119,8 +118,21 @@ function VestingDashboardPage(): JSX.Element {
};

function handleUnstake(delegatedTimelockedStake: TimelockedStakedObjectsGrouped): void {
// TODO: handle unstake logic
console.info('delegatedTimelockedStake', delegatedTimelockedStake);
const validatorInfo = getValidatorByAddress(delegatedTimelockedStake.validatorAddress);
if (!account || !validatorInfo) {
addNotification('Cannot create transaction', NotificationType.Error);
return;
}

openPopup(
<TimelockedUnstakePopup
accountAddress={account.address}
delegatedStake={delegatedTimelockedStake}
validatorInfo={validatorInfo}
closePopup={closePopup}
onSuccess={handleOnSuccess}
/>,
);
}

return (
Expand Down Expand Up @@ -174,7 +186,8 @@ function VestingDashboardPage(): JSX.Element {
>
<span>
Validator:{' '}
{getValidatorName(timelockedStakedObject.validatorAddress)}
{getValidatorByAddress(timelockedStakedObject.validatorAddress)
?.name || timelockedStakedObject.validatorAddress}
</span>
<span>
Stake Request Epoch: {timelockedStakedObject.stakeRequestEpoch}
Expand All @@ -188,17 +201,17 @@ function VestingDashboardPage(): JSX.Element {
);
})}
</div>
{account?.address && (
<div className="flex flex-row space-x-4">
{vestingSchedule.availableClaiming ? (
<Button onClick={handleCollect}>Collect</Button>
) : null}
{vestingSchedule.availableStaking ? (
<Button onClick={handleStake}>Stake</Button>
) : null}
</div>
)}
</div>
{account?.address && (
<div className="flex flex-row space-x-4">
{vestingSchedule.availableClaiming ? (
<Button onClick={handleCollect}>Collect</Button>
) : null}
{vestingSchedule.availableStaking ? (
<Button onClick={handleStake}>Stake</Button>
) : null}
</div>
)}
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import React from 'react';
import { Button } from '@/components';
import { useNotifications, useTimelockedUnstakeTransaction } from '@/hooks';
import { useSignAndExecuteTransactionBlock } from '@iota/dapp-kit';
import { IotaValidatorSummary } from '@iota/iota-sdk/client';
import { NotificationType } from '@/stores/notificationStore';
import { TimelockedStakedObjectsGrouped } from '@/lib/utils';

interface UnstakePopupProps {
accountAddress: string;
delegatedStake: TimelockedStakedObjectsGrouped;
validatorInfo: IotaValidatorSummary;
closePopup: () => void;
onSuccess?: (digest: string) => void;
}

function TimelockedUnstakePopup({
accountAddress,
delegatedStake,
validatorInfo,
closePopup,
onSuccess,
}: UnstakePopupProps): JSX.Element {
const objectIds = delegatedStake.stakes.map((stake) => stake.timelockedStakedIotaId);
const { data: timelockedUnstake } = useTimelockedUnstakeTransaction(objectIds, accountAddress);
const { mutateAsync: signAndExecuteTransactionBlock, isPending } =
useSignAndExecuteTransactionBlock();
const { addNotification } = useNotifications();

async function handleTimelockedUnstake(): Promise<void> {
if (!timelockedUnstake) return;
signAndExecuteTransactionBlock(
{
transactionBlock: timelockedUnstake.transaction,
},
{
onSuccess: (tx) => {
if (onSuccess) {
onSuccess(tx.digest);
}
},
},
)
.then(() => {
closePopup();
addNotification('Unstake transaction has been sent');
})
.catch(() => {
addNotification('Unstake transaction was not sent', NotificationType.Error);
});
}

return (
<div className="flex min-w-[300px] flex-col gap-2">
<p>Validator Name: {validatorInfo.name}</p>
<p>Validator Address: {delegatedStake.validatorAddress}</p>
<p>Stake Request Epoch: {delegatedStake.stakeRequestEpoch}</p>
<p>Rewards: {validatorInfo.rewardsPool}</p>
<p>Total Stakes: {delegatedStake.stakes.length}</p>
{delegatedStake.stakes.map((stake, index) => {
return (
<div key={stake.timelockedStakedIotaId} className="m-4 flex flex-col">
<span>
Stake {index + 1}: {stake.timelockedStakedIotaId}
</span>
<span>Expiration time: {stake.expirationTimestampMs}</span>
<span>Label: {stake.label}</span>
<span>Status: {stake.status}</span>
</div>
);
})}
<p>Gas Fees: {timelockedUnstake?.gasBudget?.toString() || '--'}</p>
{isPending ? (
<Button disabled>Loading...</Button>
) : (
<Button onClick={handleTimelockedUnstake}>Confirm Unstake</Button>
)}
</div>
);
}

export default TimelockedUnstakePopup;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

export { default as TimelockedUnstakePopup } from './TimelockedUnstakePopup';
1 change: 1 addition & 0 deletions apps/wallet-dashboard/components/Popup/Popups/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export { default as SendCoinPopup } from './SendCoinPopup/SendCoinPopup';
export { default as SendAssetPopup } from './SendAssetPopup';

export * from './SendCoinPopup';
export * from './VestingPopup';
export * from './NewStakePopup';
1 change: 1 addition & 0 deletions apps/wallet-dashboard/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './useNotifications';
export * from './useSendCoinTransaction';
export * from './useCreateSendAssetTransaction';
export * from './useGetCurrentEpochStartTimestamp';
export * from './useTimelockedUnstakeTransaction';
31 changes: 31 additions & 0 deletions apps/wallet-dashboard/hooks/useTimelockedUnstakeTransaction.ts
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 { createTimelockedUnstakeTransaction } from '@iota/core';
import { useIotaClient } from '@iota/dapp-kit';
import { useQuery } from '@tanstack/react-query';

export function useTimelockedUnstakeTransaction(
timelockedStakedObjectIds: string[],
senderAddress: string,
) {
const client = useIotaClient();
return useQuery({
// eslint-disable-next-line @tanstack/query/exhaustive-deps
queryKey: ['timelocked-unstake-transaction', timelockedStakedObjectIds, senderAddress],
queryFn: async () => {
const transaction = createTimelockedUnstakeTransaction(timelockedStakedObjectIds);
transaction.setSender(senderAddress);
await transaction.build({ client });
return transaction;
},
enabled: !!timelockedStakedObjectIds && !!senderAddress,
gcTime: 0,
select: (transaction) => {
return {
transaction,
gasBudget: transaction.blockData.gasConfig.budget,
};
},
});
}

0 comments on commit eba98c4

Please sign in to comment.