diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountDialogLayout.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountDialogLayout.tsx new file mode 100644 index 00000000000..9ec233b1838 --- /dev/null +++ b/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountDialogLayout.tsx @@ -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(); + const amount = values.amount; + + const [gas, symbol] = useFormatCoin(gasBudget ?? 0, IOTA_TYPE_ARG); + + const { stakedRewardsStartEpoch, timeBeforeStakeRewardsRedeemableAgoDisplay } = useStakeTxnInfo( + system?.epoch, + ); + + return ( + +
+ +
+
+
+ +
+ +
+ + {({ + field: { onChange, ...field }, + form: { setFieldValue }, + meta, + }: FieldProps) => { + return ( + { + 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} + /> + ); + }} + + {showInfo ? ( +
+ } + /> +
+ ) : null} +
+ + +
+ + + + +
+
+
+
+
+ +
+
+
+ + ); +} + +export default EnterAmountDialogLayout; diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx index a2c7f61d0db..b90de664794 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx @@ -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; @@ -49,36 +25,31 @@ 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(); + const coinType = IOTA_TYPE_ARG; const { data: metadata } = useCoinMetadata(coinType); const decimals = metadata?.decimals ?? 0; - const { addNotification } = useNotifications(); - - const { values, errors, resetForm } = useFormikContext(); - 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, @@ -86,14 +57,9 @@ function EnterAmountView({ 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; @@ -120,97 +86,18 @@ function EnterAmountView({ } return ( - -
- -
-
-
- -
- -
- - {({ - field: { onChange, ...field }, - form: { setFieldValue }, - meta, - }: FieldProps) => { - return ( - { - 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 : ''} - /> - ); - }} - - {!hasEnoughRemaingBalance ? ( -
- } - /> -
- ) : null} -
- - -
- - - - -
-
-
-
-
- -
-
-
- + ); } diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterTimelockedAmountView.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterTimelockedAmountView.tsx index af365e15f30..b21a6ad3bd1 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterTimelockedAmountView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterTimelockedAmountView.tsx @@ -5,31 +5,13 @@ import React, { useEffect, useState } from 'react'; import { useFormatCoin, CoinFormat, - useStakeTxnInfo, GroupedTimelockObject, useGetAllOwnedObjects, TIMELOCK_IOTA_TYPE, } 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, 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 { useGetCurrentEpochStartTimestamp, useNewStakeTimelockedTransaction, @@ -37,6 +19,7 @@ import { } from '@/hooks'; import { NotificationType } from '@/stores/notificationStore'; import { prepareObjectsForTimelockedStakingTransaction } from '@/lib/utils'; +import EnterAmountDialogLayout from './EnterAmountDialogLayout'; export interface FormValues { amount: string; @@ -63,15 +46,18 @@ function EnterTimelockedAmountView({ }: EnterTimelockedAmountViewProps): JSX.Element { const { addNotification } = useNotifications(); const { mutateAsync: signAndExecuteTransaction } = useSignAndExecuteTransaction(); + const { resetForm } = useFormikContext(); + + const { data: currentEpochMs } = useGetCurrentEpochStartTimestamp(); + const { data: timelockedObjects } = useGetAllOwnedObjects(senderAddress, { + StructType: TIMELOCK_IOTA_TYPE, + }); const [groupedTimelockObjects, setGroupedTimelockObjects] = useState( [], ); + const { data: newStakeData, isLoading: isTransactionLoading } = useNewStakeTimelockedTransaction(selectedValidator, senderAddress, groupedTimelockObjects); - const { data: currentEpochMs } = useGetCurrentEpochStartTimestamp(); - const { data: timelockedObjects } = useGetAllOwnedObjects(senderAddress, { - StructType: TIMELOCK_IOTA_TYPE, - }); useEffect(() => { if (timelockedObjects && currentEpochMs) { @@ -84,13 +70,8 @@ function EnterTimelockedAmountView({ } }, [timelockedObjects, currentEpochMs, amountWithoutDecimals]); - const { values, errors, resetForm } = useFormikContext(); - const amount = values.amount; const hasGroupedTimelockObjects = groupedTimelockObjects.length > 0; - const { data: system } = useIotaClientQuery('getLatestIotaSystemState'); - const [gas, symbol] = useFormatCoin(newStakeData?.gasBudget ?? 0, IOTA_TYPE_ARG); - const [maxTokenFormatted, maxTokenFormattedSymbol] = useFormatCoin( maxStakableTimelockedAmount, IOTA_TYPE_ARG, @@ -98,10 +79,8 @@ function EnterTimelockedAmountView({ ); const caption = `${maxTokenFormatted} ${maxTokenFormattedSymbol} Available`; - - const { stakedRewardsStartEpoch, timeBeforeStakeRewardsRedeemableAgoDisplay } = useStakeTxnInfo( - system?.epoch, - ); + const infoMessage = + 'It is not possible to combine timelocked objects to stake the entered amount. Please try a different amount.'; function handleStake(): void { if (groupedTimelockObjects.length === 0) { @@ -130,104 +109,19 @@ function EnterTimelockedAmountView({ } return ( - -
- -
-
-
- -
- -
- - {({ - field: { onChange, ...field }, - form: { setFieldValue }, - meta, - }: FieldProps) => { - return ( - { - 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} - /> - ); - }} - - {!hasGroupedTimelockObjects && !isTransactionLoading ? ( -
- } - /> -
- ) : null} -
- - -
- - - - -
-
-
-
-
- -
-
-
- + ); } diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/views/index.ts b/apps/wallet-dashboard/components/Dialogs/Staking/views/index.ts index 685926f46f6..5a0ffed2be6 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/views/index.ts +++ b/apps/wallet-dashboard/components/Dialogs/Staking/views/index.ts @@ -3,5 +3,6 @@ export { default as EnterAmountView } from './EnterAmountView'; export { default as EnterTimelockedAmountView } from './EnterTimelockedAmountView'; +export { default as EnterAmountDialogLayout } from './EnterAmountDialogLayout'; export { default as SelectValidatorView } from './SelectValidatorView'; export * from './DetailsView';