Skip to content

Commit

Permalink
feat(wallet-dashboard): add staking dialog to vesting page (#4297)
Browse files Browse the repository at this point in the history
* fix: add staking dialog to vesting page

* fix: add enter timlocked amount step in staking dialog

* fix: remove comment

* fix: add stake new button

* fix: add hook new stake to existing banner

---------

Co-authored-by: Begoña Álvarez de la Cruz <[email protected]>
  • Loading branch information
brancoder and begonaalvarezd authored Dec 4, 2024
1 parent fdffc67 commit b4b2dc5
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 15 deletions.
31 changes: 27 additions & 4 deletions apps/wallet-dashboard/app/(protected)/vesting/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

'use client';

import { Banner, TimelockedUnstakePopup } from '@/components';
import { Banner, StakeDialog, TimelockedUnstakePopup } from '@/components';
import { useStakeDialog } from '@/components/Dialogs/Staking/hooks/useStakeDialog';
import { useGetCurrentEpochStartTimestamp, useNotifications, usePopups } from '@/hooks';
import {
formatDelegatedTimelockedStake,
Expand Down Expand Up @@ -87,6 +88,17 @@ function VestingDashboardPage(): JSX.Element {
Number(currentEpochMs),
);

const {
isDialogStakeOpen,
stakeDialogView,
setStakeDialogView,
selectedStake,
selectedValidator,
setSelectedValidator,
handleCloseStakeDialog,
handleNewStake,
} = useStakeDialog();

const nextPayout = getLatestOrEarliestSupplyIncreaseVestingPayout(
[...timelockedMapped, ...timelockedstakedMapped],
false,
Expand Down Expand Up @@ -271,9 +283,7 @@ function VestingDashboardPage(): JSX.Element {
videoSrc={videoSrc}
title="Stake Vested Tokens"
subtitle="Earn Rewards"
onButtonClick={() => {
/*Add stake vested tokens dialog flow*/
}}
onButtonClick={() => handleNewStake()}
buttonText="Stake"
/>
</>
Expand Down Expand Up @@ -321,8 +331,21 @@ function VestingDashboardPage(): JSX.Element {
);
})}
</div>
<Button onClick={() => handleNewStake()} text="Stake" />
</div>
)}
<StakeDialog
isTimelockedStaking={true}
stakedDetails={selectedStake}
onSuccess={handleOnSuccess}
isOpen={isDialogStakeOpen}
handleClose={handleCloseStakeDialog}
view={stakeDialogView}
setView={setStakeDialogView}
selectedValidator={selectedValidator}
setSelectedValidator={setSelectedValidator}
maxStakableTimelockedAmount={BigInt(vestingSchedule.availableStaking)}
/>
</div>
);
}
Expand Down
32 changes: 25 additions & 7 deletions apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import React, { useMemo } from 'react';
import { EnterAmountView, SelectValidatorView } from './views';
import { EnterAmountView, EnterTimelockedAmountView, SelectValidatorView } from './views';
import {
useNotifications,
useNewStakeTransaction,
Expand Down Expand Up @@ -34,6 +34,7 @@ export enum StakeDialogView {
Details = 'Details',
SelectValidator = 'SelectValidator',
EnterAmount = 'EnterAmount',
EnterTimelockedAmount = 'EnterTimelockedAmount',
Unstake = 'Unstake',
}

Expand All @@ -44,11 +45,12 @@ const INITIAL_VALUES = {
interface StakeDialogProps {
isOpen: boolean;
handleClose: () => void;
isTimelockedStaking?: boolean;
onSuccess?: (digest: string) => void;
view?: StakeDialogView;
setView?: (view: StakeDialogView) => void;
stakedDetails?: ExtendedDelegatedStake | null;
maxStakableTimelockedAmount?: bigint;
isTimelockedStaking?: boolean;
onSuccess?: (digest: string) => void;
selectedValidator?: string;
setSelectedValidator?: (validator: string) => void;
}
Expand All @@ -61,6 +63,7 @@ export function StakeDialog({
view,
setView,
stakedDetails,
maxStakableTimelockedAmount,
selectedValidator = '',
setSelectedValidator,
}: StakeDialogProps): JSX.Element {
Expand All @@ -77,13 +80,13 @@ export function StakeDialog({
const validationSchema = useMemo(
() =>
createValidationSchema(
coinBalance,
maxStakableTimelockedAmount ?? coinBalance,
coinSymbol,
coinDecimals,
view === StakeDialogView.Unstake,
minimumStake,
),
[coinBalance, coinSymbol, coinDecimals, view, minimumStake],
[maxStakableTimelockedAmount, coinBalance, coinSymbol, coinDecimals, view, minimumStake],
);

const formik = useFormik({
Expand All @@ -99,7 +102,6 @@ export function StakeDialog({
const { data: timelockedObjects } = useGetAllOwnedObjects(senderAddress, {
StructType: TIMELOCK_IOTA_TYPE,
});

let groupedTimelockObjects: GroupedTimelockObject[] = [];
if (isTimelockedStaking && timelockedObjects && currentEpochMs) {
groupedTimelockObjects = prepareObjectsForTimelockedStakingTransaction(
Expand Down Expand Up @@ -133,7 +135,11 @@ export function StakeDialog({

function selectValidatorHandleNext(): void {
if (selectedValidator) {
setView?.(StakeDialogView.EnterAmount);
setView?.(
isTimelockedStaking
? StakeDialogView.EnterTimelockedAmount
: StakeDialogView.EnterAmount,
);
}
}

Expand Down Expand Up @@ -211,6 +217,18 @@ export function StakeDialog({
isTransactionLoading={isTransactionLoading}
/>
)}
{view === StakeDialogView.EnterTimelockedAmount && (
<EnterTimelockedAmountView
selectedValidator={selectedValidator}
maxStakableTimelockedAmount={maxStakableTimelockedAmount ?? BigInt(0)}
hasGroupedTimelockObjects={groupedTimelockObjects.length > 0}
handleClose={handleClose}
onBack={handleBack}
onStake={handleStake}
gasBudget={newStakeData?.gasBudget}
isTransactionLoading={isTransactionLoading}
/>
)}
{view === StakeDialogView.Unstake && stakedDetails && (
<UnstakeView
extendedStake={stakedDetails}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,6 @@ function EnterAmountView({

const hasEnoughRemaingBalance =
maxTokenBalance > parseAmount(values.amount, decimals) + BigInt(2) * gasBudgetBigInt;
const shouldShowInsufficientRemainingFundsWarning =
maxTokenFormatted >= values.amount && !hasEnoughRemaingBalance;

return (
<Layout>
Expand Down Expand Up @@ -134,7 +132,7 @@ function EnterAmountView({
);
}}
</Field>
{shouldShowInsufficientRemainingFundsWarning ? (
{!hasEnoughRemaingBalance ? (
<div className="mt-md">
<InfoBox
type={InfoBoxType.Error}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import React from 'react';
import { useFormatCoin, CoinFormat, useStakeTxnInfo } from '@iota/core';
import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils';
import {
Button,
ButtonType,
KeyValueInfo,
Panel,
Divider,
Input,
InputType,
Header,
InfoBoxType,
InfoBoxStyle,
InfoBox,
} from '@iota/apps-ui-kit';
import { Field, type FieldProps, useFormikContext } from 'formik';
import { Exclamation, Loader } from '@iota/ui-icons';
import { useCurrentAccount, useIotaClientQuery } from '@iota/dapp-kit';

import { Validator } from './Validator';
import { StakedInfo } from './StakedInfo';
import { Layout, LayoutBody, LayoutFooter } from './Layout';

export interface FormValues {
amount: string;
}

interface EnterTimelockedAmountViewProps {
selectedValidator: string;
maxStakableTimelockedAmount: bigint;
onBack: () => void;
onStake: () => void;
gasBudget?: string | number | null;
handleClose: () => void;
hasGroupedTimelockObjects?: boolean;
isTransactionLoading?: boolean;
}

function EnterTimelockedAmountView({
selectedValidator: selectedValidatorAddress,
maxStakableTimelockedAmount,
hasGroupedTimelockObjects,
onBack,
onStake,
gasBudget,
handleClose,
isTransactionLoading,
}: EnterTimelockedAmountViewProps): JSX.Element {
const account = useCurrentAccount();
const accountAddress = account?.address;

const { values, errors } = useFormikContext<FormValues>();
const amount = values.amount;

const { data: system } = useIotaClientQuery('getLatestIotaSystemState');
const [gas, symbol] = useFormatCoin(gasBudget ?? 0, IOTA_TYPE_ARG);

const [maxTokenFormatted, maxTokenFormattedSymbol] = useFormatCoin(
maxStakableTimelockedAmount,
IOTA_TYPE_ARG,
CoinFormat.FULL,
);

const caption = `${maxTokenFormatted} ${maxTokenFormattedSymbol} Available`;

const { stakedRewardsStartEpoch, timeBeforeStakeRewardsRedeemableAgoDisplay } = useStakeTxnInfo(
system?.epoch,
);

return (
<Layout>
<Header title="Enter amount" onClose={handleClose} onBack={onBack} titleCentered />
<LayoutBody>
<div className="flex w-full flex-col justify-between">
<div>
<div className="mb-md">
<Validator
address={selectedValidatorAddress}
isSelected
showAction={false}
/>
</div>
<StakedInfo
validatorAddress={selectedValidatorAddress}
accountAddress={accountAddress!}
/>
<div className="my-md w-full">
<Field name="amount">
{({
field: { onChange, ...field },
form: { setFieldValue },
meta,
}: FieldProps<FormValues>) => {
return (
<Input
{...field}
onValueChange={({ value }) => {
setFieldValue('amount', value, true);
}}
type={InputType.NumericFormat}
label="Amount"
value={amount}
suffix={` ${symbol}`}
placeholder="Enter amount to stake"
errorMessage={
values.amount && meta.error ? meta.error : undefined
}
caption={caption}
/>
);
}}
</Field>
{!hasGroupedTimelockObjects && !isTransactionLoading ? (
<div className="mt-md">
<InfoBox
type={InfoBoxType.Error}
supportingText="It is not possible to combine timelocked objects to stake the entered amount. Please try a different amount."
style={InfoBoxStyle.Elevated}
icon={<Exclamation />}
/>
</div>
) : null}
</div>

<Panel hasBorder>
<div className="flex flex-col gap-y-sm p-md">
<KeyValueInfo
keyText="Staking Rewards Start"
value={stakedRewardsStartEpoch}
fullwidth
/>
<KeyValueInfo
keyText="Redeem Rewards"
value={timeBeforeStakeRewardsRedeemableAgoDisplay}
fullwidth
/>
<Divider />
<KeyValueInfo
keyText="Gas fee"
value={gas || '--'}
supportingLabel={symbol}
fullwidth
/>
</div>
</Panel>
</div>
</div>
</LayoutBody>
<LayoutFooter>
<div className="flex w-full justify-between gap-sm">
<Button fullWidth type={ButtonType.Secondary} onClick={onBack} text="Back" />
<Button
fullWidth
type={ButtonType.Primary}
disabled={
!amount ||
!!errors?.amount ||
isTransactionLoading ||
!hasGroupedTimelockObjects
}
onClick={onStake}
text="Stake"
icon={
isTransactionLoading ? (
<Loader className="animate-spin" data-testid="loading-indicator" />
) : null
}
iconAfterText
/>
</div>
</LayoutFooter>
</Layout>
);
}

export default EnterTimelockedAmountView;
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

export { default as EnterAmountView } from './EnterAmountView';
export { default as EnterTimelockedAmountView } from './EnterTimelockedAmountView';
export { default as SelectValidatorView } from './SelectValidatorView';
export * from './DetailsView';
export * from './UnstakeView';
2 changes: 1 addition & 1 deletion apps/wallet-dashboard/lib/utils/vesting/vesting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ export function getVestingOverview(
const totalAvailableStakingAmount = timelockedObjects.reduce(
(acc, current) =>
current.expirationTimestampMs > currentEpochTimestamp &&
current.locked.value > MIN_STAKING_THRESHOLD
current.locked.value >= MIN_STAKING_THRESHOLD
? acc + current.locked.value
: acc,
0,
Expand Down

0 comments on commit b4b2dc5

Please sign in to comment.