Skip to content

Commit

Permalink
refactor(wallet-dashboard): improve staking dialog (#4476)
Browse files Browse the repository at this point in the history
* feat(dashboard): add unstake timelocked objects view

* refactor: improve function

* feat: add more unstaking information and rename constant

* refactor: imports

* fix: imports

* fix: remove duplication collapsible

* refactor: include both single stake and grouped in a unstake hook

* refactor: unify usntake panel

* fix: go back correctly to previous screen in staking

* refactor: cleanup

* refactor: remove popups

* refactor: remove popups

* revert "refactor: remove popups"

* refactor: remove only unnecessary popups

* refactor: divide hooks and move deeper inside components

* fix: resolve re rendering issue

* feat: remove redundant code

* fix: add enter amount dialog component

* fix: update conditionally showing the stake dialog

* fix(tooling-dashboard): improve stake wizard (#4454)

* fix: improve stake wizzard

* fix: add set view helper function

* fix: add check for stake details

---------

Co-authored-by: evavirseda <[email protected]>

* refactor: imports / exports

* feat: add wait for transaction and refresh

* feat: improve dialogs

* feat(dashboard): minor fixes

* fix: query key for timelocked staking

* fix: revert constants changes

* fix: disable stake button if no available amount for staking

* fix: add infoMessage var

* fix: revert pnpm lock changes

---------

Co-authored-by: JCNoguera <[email protected]>
Co-authored-by: evavirseda <[email protected]>
Co-authored-by: cpl121 <[email protected]>
Co-authored-by: cpl121 <[email protected]>
  • Loading branch information
5 people authored Dec 18, 2024
1 parent 33d49eb commit 699790c
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 271 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import React from 'react';
import { useFormatCoin, 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 { useIotaClientQuery } from '@iota/dapp-kit';

import { Validator } from './Validator';
import { StakedInfo } from './StakedInfo';
import { DialogLayout, DialogLayoutBody, DialogLayoutFooter } from '../../layout';

export interface FormValues {
amount: string;
}

interface EnterAmountDialogLayoutProps {
selectedValidator: string;
senderAddress: string;
caption: string;
showInfo: boolean;
infoMessage: string;
isLoading: boolean;
onBack: () => void;
handleClose: () => void;
handleStake: () => void;
isStakeDisabled?: boolean;
gasBudget?: string | number | null;
}

function EnterAmountDialogLayout({
selectedValidator,
gasBudget,
senderAddress,
caption,
showInfo,
infoMessage,
isLoading,
isStakeDisabled,
onBack,
handleClose,
handleStake,
}: EnterAmountDialogLayoutProps): JSX.Element {
const { data: system } = useIotaClientQuery('getLatestIotaSystemState');
const { values, errors } = useFormikContext<FormValues>();
const amount = values.amount;

const [gas, symbol] = useFormatCoin(gasBudget ?? 0, IOTA_TYPE_ARG);

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

return (
<DialogLayout>
<Header title="Enter amount" onClose={handleClose} onBack={onBack} titleCentered />
<DialogLayoutBody>
<div className="flex w-full flex-col justify-between">
<div>
<div className="mb-md">
<Validator address={selectedValidator} isSelected showAction={false} />
</div>
<StakedInfo
validatorAddress={selectedValidator}
accountAddress={senderAddress}
/>
<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>
{showInfo ? (
<div className="mt-md">
<InfoBox
type={InfoBoxType.Error}
supportingText={infoMessage}
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>
</DialogLayoutBody>
<DialogLayoutFooter>
<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 || isLoading || isStakeDisabled}
onClick={handleStake}
text="Stake"
icon={
isLoading ? (
<Loader className="animate-spin" data-testid="loading-indicator" />
) : null
}
iconAfterText
/>
</div>
</DialogLayoutFooter>
</DialogLayout>
);
}

export default EnterAmountDialogLayout;
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,13 @@
// SPDX-License-Identifier: Apache-2.0

import React from 'react';
import {
useFormatCoin,
useBalance,
CoinFormat,
parseAmount,
useCoinMetadata,
useStakeTxnInfo,
} from '@iota/core';
import { useFormatCoin, useBalance, CoinFormat, parseAmount, useCoinMetadata } 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 } from '@iota/ui-icons';
import { useIotaClientQuery, useSignAndExecuteTransaction } from '@iota/dapp-kit';

import { Validator } from './Validator';
import { StakedInfo } from './StakedInfo';
import { DialogLayout, DialogLayoutBody, DialogLayoutFooter } from '../../layout';
import { useFormikContext } from 'formik';
import { useSignAndExecuteTransaction } from '@iota/dapp-kit';
import { useNewStakeTransaction, useNotifications } from '@/hooks';
import { NotificationType } from '@/stores/notificationStore';
import EnterAmountDialogLayout from './EnterAmountDialogLayout';

export interface FormValues {
amount: string;
Expand All @@ -49,51 +25,41 @@ interface EnterAmountViewProps {
}

function EnterAmountView({
selectedValidator: selectedValidatorAddress,
selectedValidator,
onBack,
handleClose,
amountWithoutDecimals,
senderAddress,
onSuccess,
}: EnterAmountViewProps): JSX.Element {
const { addNotification } = useNotifications();
const { mutateAsync: signAndExecuteTransaction } = useSignAndExecuteTransaction();
const { values, resetForm } = useFormikContext<FormValues>();

const coinType = IOTA_TYPE_ARG;
const { data: metadata } = useCoinMetadata(coinType);
const decimals = metadata?.decimals ?? 0;

const { addNotification } = useNotifications();

const { values, errors, resetForm } = useFormikContext<FormValues>();
const amount = values.amount;
const { data: iotaBalance } = useBalance(senderAddress);
const coinBalance = BigInt(iotaBalance?.totalBalance || 0);

const { data: newStakeData, isLoading: isTransactionLoading } = useNewStakeTransaction(
selectedValidatorAddress,
selectedValidator,
amountWithoutDecimals,
senderAddress,
);

const { data: system } = useIotaClientQuery('getLatestIotaSystemState');
const { data: iotaBalance } = useBalance(senderAddress!);
const coinBalance = BigInt(iotaBalance?.totalBalance || 0);
const { mutateAsync: signAndExecuteTransaction } = useSignAndExecuteTransaction();

const gasBudgetBigInt = BigInt(newStakeData?.gasBudget ?? 0);
const [gas, symbol] = useFormatCoin(newStakeData?.gasBudget, IOTA_TYPE_ARG);

const maxTokenBalance = coinBalance - gasBudgetBigInt;
const [maxTokenFormatted, maxTokenFormattedSymbol] = useFormatCoin(
maxTokenBalance,
IOTA_TYPE_ARG,
CoinFormat.FULL,
);

const caption = isTransactionLoading
? '--'
: `${maxTokenFormatted} ${maxTokenFormattedSymbol} Available`;

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

const caption = `${maxTokenFormatted} ${maxTokenFormattedSymbol} Available`;
const infoMessage =
'You have selected an amount that will leave you with insufficient funds to pay for gas fees for unstaking or any other transactions.';
const hasEnoughRemaingBalance =
maxTokenBalance > parseAmount(values.amount, decimals) + BigInt(2) * gasBudgetBigInt;

Expand All @@ -120,97 +86,18 @@ function EnterAmountView({
}

return (
<DialogLayout>
<Header title="Enter amount" onClose={handleClose} onBack={onBack} titleCentered />
<DialogLayoutBody>
<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={senderAddress!}
/>
<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={coinBalance ? caption : ''}
/>
);
}}
</Field>
{!hasEnoughRemaingBalance ? (
<div className="mt-md">
<InfoBox
type={InfoBoxType.Error}
supportingText="You have selected an amount that will leave you with insufficient funds to pay for gas fees for unstaking or any other transactions."
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>
</DialogLayoutBody>
<DialogLayoutFooter>
<div className="flex w-full justify-between gap-sm">
<Button fullWidth type={ButtonType.Secondary} onClick={onBack} text="Back" />
<Button
fullWidth
type={ButtonType.Primary}
onClick={handleStake}
disabled={!amount || !!errors?.amount}
text="Stake"
/>
</div>
</DialogLayoutFooter>
</DialogLayout>
<EnterAmountDialogLayout
selectedValidator={selectedValidator}
gasBudget={newStakeData?.gasBudget}
senderAddress={senderAddress}
caption={caption}
showInfo={!hasEnoughRemaingBalance}
infoMessage={infoMessage}
isLoading={isTransactionLoading}
onBack={onBack}
handleClose={handleClose}
handleStake={handleStake}
/>
);
}

Expand Down
Loading

0 comments on commit 699790c

Please sign in to comment.