From 54aa5329ba3a9cecaf05f428a283b39202aca0d9 Mon Sep 17 00:00:00 2001 From: JCNoguera <88061365+VmMad@users.noreply.github.com> Date: Wed, 30 Oct 2024 12:23:24 +0100 Subject: [PATCH 01/87] feat(wallet-dashboard): add styles for Review & Send screen --- apps/core/package.json | 2 + .../src/components/coin/CoinIcon.tsx} | 43 ++++-- apps/core/src/components/coin/index.ts | 4 + .../src/components/icon/ImageIcon.tsx} | 14 +- apps/core/src/components/icon/index.ts | 4 + apps/core/src/components/index.ts | 3 + .../Dialogs/SendAndReviewDialog.tsx | 131 ++++++++++++++++++ .../components/Dialogs/index.ts | 4 + .../Popups/SendCoinPopup/SendCoinPopup.tsx | 2 + .../views/ReviewValuesFormView.tsx | 43 +++--- .../lib/constants/gas.constants.ts | 4 + apps/wallet-dashboard/lib/constants/index.ts | 1 + apps/wallet-dashboard/tailwind.config.ts | 1 + .../src/ui/app/components/DAppInfoCard.tsx | 2 +- .../components/active-coins-card/CoinItem.tsx | 4 +- apps/wallet/src/ui/app/components/index.ts | 1 - .../ui/app/components/iota-apps/IotaApp.tsx | 2 +- .../app/components/receipt-card/TxnAmount.tsx | 4 +- .../ui/app/pages/home/transfer-coin/index.tsx | 5 +- .../objectSummary/ObjectChangeDisplay.tsx | 3 +- .../src/ui/app/staking/home/StakedCard.tsx | 2 +- .../app/staking/validators/ValidatorLogo.tsx | 3 +- pnpm-lock.yaml | 42 +++--- 23 files changed, 255 insertions(+), 69 deletions(-) rename apps/{wallet/src/ui/app/components/coin-icon/index.tsx => core/src/components/coin/CoinIcon.tsx} (52%) create mode 100644 apps/core/src/components/coin/index.ts rename apps/{wallet/src/ui/app/shared/image-icon/index.tsx => core/src/components/icon/ImageIcon.tsx} (82%) create mode 100644 apps/core/src/components/icon/index.ts create mode 100644 apps/wallet-dashboard/components/Dialogs/SendAndReviewDialog.tsx create mode 100644 apps/wallet-dashboard/components/Dialogs/index.ts create mode 100644 apps/wallet-dashboard/lib/constants/gas.constants.ts diff --git a/apps/core/package.json b/apps/core/package.json index 31cb3e43e8f..ee3fcb5330f 100644 --- a/apps/core/package.json +++ b/apps/core/package.json @@ -29,9 +29,11 @@ "@iota/dapp-kit": "workspace:*", "@iota/iota-sdk": "workspace:*", "@iota/kiosk": "workspace:*", + "@iota/ui-icons": "workspace:*", "@sentry/react": "^7.59.2", "@tanstack/react-query": "^5.50.1", "bignumber.js": "^9.1.1", + "clsx": "^2.1.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.45.2", diff --git a/apps/wallet/src/ui/app/components/coin-icon/index.tsx b/apps/core/src/components/coin/CoinIcon.tsx similarity index 52% rename from apps/wallet/src/ui/app/components/coin-icon/index.tsx rename to apps/core/src/components/coin/CoinIcon.tsx index 5214a1e7420..c821ffb8168 100644 --- a/apps/wallet/src/ui/app/components/coin-icon/index.tsx +++ b/apps/core/src/components/coin/CoinIcon.tsx @@ -2,10 +2,11 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { ImageIcon, ImageIconSize } from '_app/shared/image-icon'; -import { useCoinMetadata } from '@iota/core'; -import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; +import React from 'react'; +import { useCoinMetadata } from '../../hooks'; import { IotaLogoMark } from '@iota/ui-icons'; +import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; +import { ImageIcon, ImageIconSize } from '../icon'; import cx from 'clsx'; interface NonIotaCoinProps { @@ -28,19 +29,45 @@ function NonIotaCoin({ coinType, size = ImageIconSize.Full, rounded }: NonIotaCo ); } - export interface CoinIconProps { coinType: string; size?: ImageIconSize; rounded?: boolean; + hasCoinWrapper?: boolean; } -export function CoinIcon({ coinType, size = ImageIconSize.Full, rounded }: CoinIconProps) { +export function CoinIcon({ + coinType, + size = ImageIconSize.Full, + rounded, + hasCoinWrapper, +}: CoinIconProps) { + const Component = hasCoinWrapper ? CoinIconWrapper : React.Fragment; return coinType === IOTA_TYPE_ARG ? ( -
- -
+ +
+ +
+
) : ( ); } + +type CoinIconWrapperProps = React.PropsWithChildren> & { + hasBorder?: boolean; +}; + +export function CoinIconWrapper({ children, size, hasBorder }: CoinIconWrapperProps) { + return ( +
+ {children} +
+ ); +} diff --git a/apps/core/src/components/coin/index.ts b/apps/core/src/components/coin/index.ts new file mode 100644 index 00000000000..f3b244cad16 --- /dev/null +++ b/apps/core/src/components/coin/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +export * from './CoinIcon'; diff --git a/apps/wallet/src/ui/app/shared/image-icon/index.tsx b/apps/core/src/components/icon/ImageIcon.tsx similarity index 82% rename from apps/wallet/src/ui/app/shared/image-icon/index.tsx rename to apps/core/src/components/icon/ImageIcon.tsx index 5ae46570b21..031fbbf13d6 100644 --- a/apps/wallet/src/ui/app/shared/image-icon/index.tsx +++ b/apps/core/src/components/icon/ImageIcon.tsx @@ -1,8 +1,7 @@ -// Copyright (c) Mysten Labs, Inc. -// Modifications Copyright (c) 2024 IOTA Stiftung +// Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { useState } from 'react'; +import React, { useState } from 'react'; import cn from 'clsx'; export enum ImageIconSize { @@ -37,16 +36,17 @@ function FallBackAvatar({ case ImageIconSize.Medium: return 'text-label-md'; case ImageIconSize.Large: - return 'text-title-md'; - case ImageIconSize.Full: return 'text-title-lg'; + case ImageIconSize.Full: + return 'text-display-lg'; } } return (
diff --git a/apps/core/src/components/icon/index.ts b/apps/core/src/components/icon/index.ts new file mode 100644 index 00000000000..3fda2c89887 --- /dev/null +++ b/apps/core/src/components/icon/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +export * from './ImageIcon'; diff --git a/apps/core/src/components/index.ts b/apps/core/src/components/index.ts index 67d6dc1fbc3..fe43fbae4ac 100644 --- a/apps/core/src/components/index.ts +++ b/apps/core/src/components/index.ts @@ -2,3 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 export * from './KioskClientProvider'; + +export * from './coin'; +export * from './icon'; diff --git a/apps/wallet-dashboard/components/Dialogs/SendAndReviewDialog.tsx b/apps/wallet-dashboard/components/Dialogs/SendAndReviewDialog.tsx new file mode 100644 index 00000000000..4156acae6d3 --- /dev/null +++ b/apps/wallet-dashboard/components/Dialogs/SendAndReviewDialog.tsx @@ -0,0 +1,131 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { GAS_SYMBOL } from '@/lib/constants'; +import { + Button, + Dialog, + DialogContent, + DialogBody, + Header, + Card, + CardType, + CardImage, + ImageType, + CardBody, + CardAction, + CardActionType, + KeyValueInfo, + Divider, + ButtonType, + DialogPosition, +} from '@iota/apps-ui-kit'; +import { CoinIcon, ImageIconSize, parseAmount, useCoinMetadata, useFormatCoin } from '@iota/core'; +import { formatAddress } from '@iota/iota-sdk/utils'; +import { Loader } from '@iota/ui-icons'; + +export type SendAndReviewDialogProps = { + coinType: string; + to: string; + amount: string; + approximation?: boolean; + gasBudget?: string; + open: boolean; + setOpen?: (open: boolean) => void; + onSend: () => void; + isPending?: boolean; + senderAddress: string; + onClose: () => void; + onBack: () => void; +}; + +export function SendAndReviewDialog({ + coinType, + senderAddress, + to, + amount, + approximation, + gasBudget, + open, + setOpen, + onSend, + isPending, + onClose, + onBack, +}: SendAndReviewDialogProps): React.JSX.Element { + const { data: metadata } = useCoinMetadata(coinType); + const amountWithoutDecimals = parseAmount(amount, metadata?.decimals ?? 0); + const [formatAmount, symbol] = useFormatCoin(Math.abs(Number(amountWithoutDecimals)), coinType); + + return ( + + +
+
+
+
+ +
+
+ {Number(amount) !== 0 ? ( + + + + + + + + ) : null} +
+ + + + + + + +
+
+
+
+ +
+
+
+ +
+ ); +} diff --git a/apps/wallet-dashboard/components/Dialogs/index.ts b/apps/wallet-dashboard/components/Dialogs/index.ts new file mode 100644 index 00000000000..8b7f60796b2 --- /dev/null +++ b/apps/wallet-dashboard/components/Dialogs/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +export * from './SendAndReviewDialog'; diff --git a/apps/wallet-dashboard/components/Popup/Popups/SendCoinPopup/SendCoinPopup.tsx b/apps/wallet-dashboard/components/Popup/Popups/SendCoinPopup/SendCoinPopup.tsx index c3f917df955..1e99fcd3ac5 100644 --- a/apps/wallet-dashboard/components/Popup/Popups/SendCoinPopup/SendCoinPopup.tsx +++ b/apps/wallet-dashboard/components/Popup/Popups/SendCoinPopup/SendCoinPopup.tsx @@ -120,6 +120,8 @@ function SendCoinPopup({ gasBudget={sendCoinData?.gasBudget?.toString() || '--'} error={error?.message} isPending={isPending} + coinType={selectedCoin.coinType} + onClose={onClose} /> )} diff --git a/apps/wallet-dashboard/components/Popup/Popups/SendCoinPopup/views/ReviewValuesFormView.tsx b/apps/wallet-dashboard/components/Popup/Popups/SendCoinPopup/views/ReviewValuesFormView.tsx index 0732b263cfc..cd59277f542 100644 --- a/apps/wallet-dashboard/components/Popup/Popups/SendCoinPopup/views/ReviewValuesFormView.tsx +++ b/apps/wallet-dashboard/components/Popup/Popups/SendCoinPopup/views/ReviewValuesFormView.tsx @@ -1,8 +1,9 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +import { SendAndReviewDialog } from '@/components/Dialogs'; import { FormDataValues } from '../SendCoinPopup'; -import { Button } from '@/components'; +import { useState } from 'react'; interface ReviewValuesFormProps { formData: FormDataValues; @@ -12,36 +13,38 @@ interface ReviewValuesFormProps { isPending: boolean; executeTransfer: () => void; onBack: () => void; + coinType: string; + onClose: () => void; } function ReviewValuesFormView({ formData: { amount, recipientAddress }, senderAddress, gasBudget, - error, isPending, executeTransfer, onBack, + coinType, + onClose, }: ReviewValuesFormProps): JSX.Element { + const [open, setOpen] = useState(true); return ( -
-

Review & Send

-
-

Sending: {amount}

-

From: {senderAddress}

-

To: {recipientAddress}

-

Gas fee: {gasBudget}

-
- {error ? {error} : null} -
- - {isPending ? ( - - ) : ( - - )} -
-
+ { + setOpen(false); + onClose(); + }} + onBack={onBack} + /> ); } export default ReviewValuesFormView; diff --git a/apps/wallet-dashboard/lib/constants/gas.constants.ts b/apps/wallet-dashboard/lib/constants/gas.constants.ts new file mode 100644 index 00000000000..5b45d7e3c8e --- /dev/null +++ b/apps/wallet-dashboard/lib/constants/gas.constants.ts @@ -0,0 +1,4 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +export const GAS_SYMBOL = 'IOTA'; diff --git a/apps/wallet-dashboard/lib/constants/index.ts b/apps/wallet-dashboard/lib/constants/index.ts index a7c8637513f..46a4dcdb069 100644 --- a/apps/wallet-dashboard/lib/constants/index.ts +++ b/apps/wallet-dashboard/lib/constants/index.ts @@ -3,3 +3,4 @@ export * from './time.constants'; export * from './vesting.constants'; +export * from './gas.constants'; diff --git a/apps/wallet-dashboard/tailwind.config.ts b/apps/wallet-dashboard/tailwind.config.ts index e8c9079ca3a..2ca1f1c0824 100644 --- a/apps/wallet-dashboard/tailwind.config.ts +++ b/apps/wallet-dashboard/tailwind.config.ts @@ -12,6 +12,7 @@ export default { './pages/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', './../ui-kit/src/lib/**/*.{js,jsx,ts,tsx}', + './../core/src/components/**/*.{js,jsx,ts,tsx}', ], darkMode: 'class', theme: { diff --git a/apps/wallet/src/ui/app/components/DAppInfoCard.tsx b/apps/wallet/src/ui/app/components/DAppInfoCard.tsx index 87cea050c73..6824f2978c5 100644 --- a/apps/wallet/src/ui/app/components/DAppInfoCard.tsx +++ b/apps/wallet/src/ui/app/components/DAppInfoCard.tsx @@ -12,7 +12,7 @@ import { useUnlockAccount } from './accounts/UnlockAccountContext'; import { DAppPermissionList } from './DAppPermissionList'; import { SummaryCard } from './SummaryCard'; import { Link } from 'react-router-dom'; -import { ImageIcon } from '../shared/image-icon'; +import { ImageIcon } from '@iota/core'; export interface DAppInfoCardProps { name: string; diff --git a/apps/wallet/src/ui/app/components/active-coins-card/CoinItem.tsx b/apps/wallet/src/ui/app/components/active-coins-card/CoinItem.tsx index 5e7dce765f5..324d553f997 100644 --- a/apps/wallet/src/ui/app/components/active-coins-card/CoinItem.tsx +++ b/apps/wallet/src/ui/app/components/active-coins-card/CoinItem.tsx @@ -2,8 +2,7 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { CoinIcon } from '_components'; -import { useFormatCoin } from '@iota/core'; +import { useFormatCoin, ImageIconSize, CoinIcon } from '@iota/core'; import { type ReactNode } from 'react'; import { Card, @@ -15,7 +14,6 @@ import { ImageType, } from '@iota/apps-ui-kit'; import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; -import { ImageIconSize } from '../../shared/image-icon'; interface CoinItemProps { coinType: string; diff --git a/apps/wallet/src/ui/app/components/index.ts b/apps/wallet/src/ui/app/components/index.ts index f162a483b93..dbab474d008 100644 --- a/apps/wallet/src/ui/app/components/index.ts +++ b/apps/wallet/src/ui/app/components/index.ts @@ -15,7 +15,6 @@ export * from './accounts'; export * from './active-coins-card'; export * from './active-coins-card/CoinItem'; export * from './address-input'; -export * from './coin-icon'; export * from './error-boundary'; export * from './explorer-link'; export * from './explorer-link/Explorer'; diff --git a/apps/wallet/src/ui/app/components/iota-apps/IotaApp.tsx b/apps/wallet/src/ui/app/components/iota-apps/IotaApp.tsx index ee69a96a670..aa4c2dc7e4b 100644 --- a/apps/wallet/src/ui/app/components/iota-apps/IotaApp.tsx +++ b/apps/wallet/src/ui/app/components/iota-apps/IotaApp.tsx @@ -2,7 +2,7 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { ImageIcon, ImageIconSize } from '_app/shared/image-icon'; +import { ImageIcon, ImageIconSize } from '@iota/core'; import { ExternalLink } from '_components'; import { ampli } from '_src/shared/analytics/ampli'; import { getDAppUrl } from '_src/shared/utils'; diff --git a/apps/wallet/src/ui/app/components/receipt-card/TxnAmount.tsx b/apps/wallet/src/ui/app/components/receipt-card/TxnAmount.tsx index 623ed671b9a..a993b4657ca 100644 --- a/apps/wallet/src/ui/app/components/receipt-card/TxnAmount.tsx +++ b/apps/wallet/src/ui/app/components/receipt-card/TxnAmount.tsx @@ -2,7 +2,7 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { useFormatCoin } from '@iota/core'; +import { useFormatCoin, ImageIconSize, CoinIcon } from '@iota/core'; import { Card, CardAction, @@ -12,8 +12,6 @@ import { CardType, ImageType, } from '@iota/apps-ui-kit'; -import { CoinIcon } from '../coin-icon'; -import { ImageIconSize } from '../../shared/image-icon'; interface TxnAmountProps { amount: string | number | bigint; diff --git a/apps/wallet/src/ui/app/pages/home/transfer-coin/index.tsx b/apps/wallet/src/ui/app/pages/home/transfer-coin/index.tsx index 111017d0bb0..84224ad3ae4 100644 --- a/apps/wallet/src/ui/app/pages/home/transfer-coin/index.tsx +++ b/apps/wallet/src/ui/app/pages/home/transfer-coin/index.tsx @@ -2,7 +2,7 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { CoinIcon, Loading, Overlay } from '_components'; +import { Loading, Overlay } from '_components'; import { ampli } from '_src/shared/analytics/ampli'; import { getSignerOperationErrorMessage } from '_src/ui/app/helpers/errorMessages'; import { useActiveAccount } from '_src/ui/app/hooks/useActiveAccount'; @@ -13,6 +13,8 @@ import { filterAndSortTokenBalances, useCoinMetadata, useFormatCoin, + ImageIconSize, + CoinIcon, } from '@iota/core'; // import * as Sentry from '@sentry/react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; @@ -26,7 +28,6 @@ import { Select, Button, type SelectOption, ButtonType } from '@iota/apps-ui-kit import { useActiveAddress, useCoinsReFetchingConfig } from '_src/ui/app/hooks'; import { useIotaClientQuery } from '@iota/dapp-kit'; import type { CoinBalance } from '@iota/iota-sdk/client'; -import { ImageIconSize } from '_src/ui/app/shared/image-icon'; import { Loader } from '@iota/ui-icons'; function TransferCoinPage() { diff --git a/apps/wallet/src/ui/app/shared/transaction-summary/cards/objectSummary/ObjectChangeDisplay.tsx b/apps/wallet/src/ui/app/shared/transaction-summary/cards/objectSummary/ObjectChangeDisplay.tsx index 5289019a18d..ce6bfd0acea 100644 --- a/apps/wallet/src/ui/app/shared/transaction-summary/cards/objectSummary/ObjectChangeDisplay.tsx +++ b/apps/wallet/src/ui/app/shared/transaction-summary/cards/objectSummary/ObjectChangeDisplay.tsx @@ -3,10 +3,9 @@ // SPDX-License-Identifier: Apache-2.0 import { ExplorerLink, ExplorerLinkType } from '_components'; -import { type IotaObjectChangeWithDisplay } from '@iota/core'; +import { type IotaObjectChangeWithDisplay, ImageIcon } from '@iota/core'; import { Card, CardAction, CardActionType, CardBody, CardImage, CardType } from '@iota/apps-ui-kit'; -import { ImageIcon } from '../../../image-icon'; import { ArrowTopRight } from '@iota/ui-icons'; export function ObjectChangeDisplay({ change }: { change: IotaObjectChangeWithDisplay }) { diff --git a/apps/wallet/src/ui/app/staking/home/StakedCard.tsx b/apps/wallet/src/ui/app/staking/home/StakedCard.tsx index 9ed1c130455..b945fbe025b 100644 --- a/apps/wallet/src/ui/app/staking/home/StakedCard.tsx +++ b/apps/wallet/src/ui/app/staking/home/StakedCard.tsx @@ -10,12 +10,12 @@ import { useFormatCoin, useGetTimeBeforeEpochNumber, useTimeAgo, + ImageIcon, } from '@iota/core'; import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; import { Card, CardImage, CardType, CardBody, CardAction, CardActionType } from '@iota/apps-ui-kit'; import { useMemo } from 'react'; import { Link } from 'react-router-dom'; -import { ImageIcon } from '../../shared/image-icon'; import { useIotaClientQuery } from '@iota/dapp-kit'; diff --git a/apps/wallet/src/ui/app/staking/validators/ValidatorLogo.tsx b/apps/wallet/src/ui/app/staking/validators/ValidatorLogo.tsx index 961cadc895b..dcb8954a274 100644 --- a/apps/wallet/src/ui/app/staking/validators/ValidatorLogo.tsx +++ b/apps/wallet/src/ui/app/staking/validators/ValidatorLogo.tsx @@ -1,7 +1,6 @@ // Copyright (c) Mysten Labs, Inc. // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { ImageIcon, ImageIconSize } from '_app/shared/image-icon'; import { Badge, BadgeType, @@ -15,7 +14,7 @@ import { import { useIotaClientQuery } from '@iota/dapp-kit'; import { formatAddress } from '@iota/iota-sdk/utils'; import { useMemo } from 'react'; -import { formatPercentageDisplay, useGetValidatorsApy } from '@iota/core'; +import { formatPercentageDisplay, useGetValidatorsApy, ImageIcon, ImageIconSize } from '@iota/core'; interface ValidatorLogoProps { validatorAddress: string; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92e2c8a9a2f..460c79809ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -179,7 +179,7 @@ importers: version: 5.2.1(@types/eslint@8.56.12)(eslint-config-prettier@9.1.0(eslint@8.57.1))(eslint@8.57.1)(prettier@3.3.3) jest: specifier: ^29.5.0 - version: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + version: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) prettier: specifier: ^3.3.1 version: 3.3.3 @@ -191,7 +191,7 @@ importers: version: 6.3.4 ts-jest: specifier: ^29.1.0 - version: 29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)))(typescript@5.6.2) + version: 29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)))(typescript@5.6.2) ts-loader: specifier: ^9.4.4 version: 9.5.1(typescript@5.6.2)(webpack@5.95.0(@swc/core@1.7.28)) @@ -231,6 +231,9 @@ importers: '@iota/kiosk': specifier: workspace:* version: link:../../sdk/kiosk + '@iota/ui-icons': + specifier: workspace:* + version: link:../ui-icons '@sentry/react': specifier: ^7.59.2 version: 7.119.0(react@18.3.1) @@ -240,6 +243,9 @@ importers: bignumber.js: specifier: ^9.1.1 version: 9.1.2 + clsx: + specifier: ^2.1.1 + version: 2.1.1 react: specifier: ^18.3.1 version: 18.3.1 @@ -1045,7 +1051,7 @@ importers: version: 14.2.3(eslint@8.57.1)(typescript@5.6.2) jest: specifier: ^29.5.0 - version: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + version: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) postcss: specifier: ^8.4.31 version: 8.4.47 @@ -1054,7 +1060,7 @@ importers: version: 3.4.13(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) ts-jest: specifier: ^29.1.0 - version: 29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)))(typescript@5.6.2) + version: 29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)))(typescript@5.6.2) typescript: specifier: ^5.5.3 version: 5.6.2 @@ -20540,7 +20546,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2))': + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0(node-notifier@10.0.0) @@ -20554,7 +20560,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + jest-config: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -26471,13 +26477,13 @@ snapshots: crc-32@1.2.2: {} - create-jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)): + create-jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + jest-config: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -29686,16 +29692,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)): + jest-cli@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + create-jest: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + jest-config: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -29707,7 +29713,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)): + jest-config@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)): dependencies: '@babel/core': 7.25.2 '@jest/test-sequencer': 29.7.0 @@ -29964,12 +29970,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)): + jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + jest-cli: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) optionalDependencies: node-notifier: 10.0.0 transitivePeerDependencies: @@ -34556,12 +34562,12 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)))(typescript@5.6.2): + ts-jest@29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)))(typescript@5.6.2): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + jest: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 From cc2a855362c75322e9f9f4dfef7ca2fd79e730b1 Mon Sep 17 00:00:00 2001 From: JCNoguera <88061365+VmMad@users.noreply.github.com> Date: Wed, 30 Oct 2024 17:23:58 +0100 Subject: [PATCH 02/87] fix: move CoinIcon to core --- .../{ => Send}/SendAndReviewDialog.tsx | 0 .../components/Dialogs/Send/index.ts | 4 ++ .../components/Dialogs/index.ts | 2 +- .../components/coins/CoinIcon.tsx | 45 ------------------- .../components/coins/CoinItem.tsx | 10 +++-- .../components/coins/index.ts | 1 - 6 files changed, 12 insertions(+), 50 deletions(-) rename apps/wallet-dashboard/components/Dialogs/{ => Send}/SendAndReviewDialog.tsx (100%) create mode 100644 apps/wallet-dashboard/components/Dialogs/Send/index.ts delete mode 100644 apps/wallet-dashboard/components/coins/CoinIcon.tsx diff --git a/apps/wallet-dashboard/components/Dialogs/SendAndReviewDialog.tsx b/apps/wallet-dashboard/components/Dialogs/Send/SendAndReviewDialog.tsx similarity index 100% rename from apps/wallet-dashboard/components/Dialogs/SendAndReviewDialog.tsx rename to apps/wallet-dashboard/components/Dialogs/Send/SendAndReviewDialog.tsx diff --git a/apps/wallet-dashboard/components/Dialogs/Send/index.ts b/apps/wallet-dashboard/components/Dialogs/Send/index.ts new file mode 100644 index 00000000000..8b7f60796b2 --- /dev/null +++ b/apps/wallet-dashboard/components/Dialogs/Send/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +export * from './SendAndReviewDialog'; diff --git a/apps/wallet-dashboard/components/Dialogs/index.ts b/apps/wallet-dashboard/components/Dialogs/index.ts index 74898078827..f691a041c8f 100644 --- a/apps/wallet-dashboard/components/Dialogs/index.ts +++ b/apps/wallet-dashboard/components/Dialogs/index.ts @@ -1,5 +1,5 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -export * from './SendAndReviewDialog'; +export * from './Send'; export * from './Staking'; diff --git a/apps/wallet-dashboard/components/coins/CoinIcon.tsx b/apps/wallet-dashboard/components/coins/CoinIcon.tsx deleted file mode 100644 index a5c24107baa..00000000000 --- a/apps/wallet-dashboard/components/coins/CoinIcon.tsx +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { useCoinMetadata } from '@iota/core'; -import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; -import { IotaLogoMark } from '@iota/ui-icons'; -import cx from 'clsx'; -import { ImageIcon, ImageIconSize } from '../ImageIcon'; - -interface NonIotaCoinProps { - coinType: string; - size?: ImageIconSize; - rounded?: boolean; -} - -function NonIotaCoin({ coinType, size = ImageIconSize.Full, rounded }: NonIotaCoinProps) { - const { data: coinMeta } = useCoinMetadata(coinType); - return ( -
- -
- ); -} - -export interface CoinIconProps { - coinType: string; - size?: ImageIconSize; - rounded?: boolean; -} - -export function CoinIcon({ coinType, size = ImageIconSize.Full, rounded }: CoinIconProps) { - return coinType === IOTA_TYPE_ARG ? ( -
- -
- ) : ( - - ); -} diff --git a/apps/wallet-dashboard/components/coins/CoinItem.tsx b/apps/wallet-dashboard/components/coins/CoinItem.tsx index 29e8be5aa35..26d6ba35f01 100644 --- a/apps/wallet-dashboard/components/coins/CoinItem.tsx +++ b/apps/wallet-dashboard/components/coins/CoinItem.tsx @@ -10,11 +10,10 @@ import { CardType, ImageType, } from '@iota/apps-ui-kit'; -import { useFormatCoin } from '@iota/core'; +import { useFormatCoin, CoinIcon } from '@iota/core'; import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; import { type ReactNode } from 'react'; import { ImageIconSize } from '../ImageIcon'; -import { CoinIcon } from './CoinIcon'; interface CoinItemProps { coinType: string; @@ -40,7 +39,12 @@ function CoinItem({
- +
Date: Wed, 30 Oct 2024 18:43:20 +0100 Subject: [PATCH 03/87] feat(wallet-dashboard): style send entry screen WIP --- .../components/Coins/MyCoins.tsx | 30 +-- .../components/Dialogs/SendTokenDialog.tsx | 216 ++++++++++++++++++ .../components/Dialogs/index.ts | 4 + apps/wallet-dashboard/components/index.ts | 1 + apps/wallet-dashboard/package.json | 1 + pnpm-lock.yaml | 39 ++-- 6 files changed, 258 insertions(+), 33 deletions(-) create mode 100644 apps/wallet-dashboard/components/Dialogs/SendTokenDialog.tsx create mode 100644 apps/wallet-dashboard/components/Dialogs/index.ts diff --git a/apps/wallet-dashboard/components/Coins/MyCoins.tsx b/apps/wallet-dashboard/components/Coins/MyCoins.tsx index 6fb6e8eed49..c89a2bc39c6 100644 --- a/apps/wallet-dashboard/components/Coins/MyCoins.tsx +++ b/apps/wallet-dashboard/components/Coins/MyCoins.tsx @@ -1,10 +1,9 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import React from 'react'; +import React, { useState } from 'react'; import { useCurrentAccount, useIotaClientQuery } from '@iota/dapp-kit'; -import { CoinItem, SendCoinPopup } from '@/components'; -import { usePopups } from '@/hooks'; +import { CoinItem, SendTokenDialog } from '@/components'; import { CoinBalance } from '@iota/iota-sdk/client'; import { COINS_QUERY_REFETCH_INTERVAL, @@ -14,9 +13,10 @@ import { } from '@iota/core'; function MyCoins(): React.JSX.Element { - const { openPopup, closePopup } = usePopups(); const account = useCurrentAccount(); const activeAccountAddress = account?.address; + const [isSendTokenDialogOpen, setIsSendTokenDialogOpen] = useState(false); + const [selectedCoinType, setSelectedCoinType] = useState(''); const { data: coinBalances } = useIotaClientQuery( 'getAllBalances', @@ -30,16 +30,10 @@ function MyCoins(): React.JSX.Element { ); const { recognized, unrecognized } = useSortedCoinsByCategories(coinBalances ?? []); - function openSendTokenPopup(coin: CoinBalance, address: string): void { + function openSendTokenPopup(coin: CoinBalance): void { if (coinBalances) { - openPopup( - , - ); + setIsSendTokenDialogOpen(true); + setSelectedCoinType(coin.coinType); } } @@ -52,7 +46,7 @@ function MyCoins(): React.JSX.Element { key={index} coinType={coin.coinType} balance={BigInt(coin.totalBalance)} - onClick={() => openSendTokenPopup(coin, account?.address ?? '')} + onClick={() => openSendTokenPopup(coin)} /> ); })} @@ -63,10 +57,16 @@ function MyCoins(): React.JSX.Element { key={index} coinType={coin.coinType} balance={BigInt(coin.totalBalance)} - onClick={() => openSendTokenPopup(coin, account?.address ?? '')} + onClick={() => openSendTokenPopup(coin)} /> ); })} +
); } diff --git a/apps/wallet-dashboard/components/Dialogs/SendTokenDialog.tsx b/apps/wallet-dashboard/components/Dialogs/SendTokenDialog.tsx new file mode 100644 index 00000000000..73773f480a3 --- /dev/null +++ b/apps/wallet-dashboard/components/Dialogs/SendTokenDialog.tsx @@ -0,0 +1,216 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { + InfoBox, + InfoBoxStyle, + InfoBoxType, + ButtonType, + ButtonHtmlType, + Button, + Dialog, + DialogContent, + DialogBody, + Header, + DialogPosition, +} from '@iota/apps-ui-kit'; +import { parseAmount, useCoinMetadata, useGetAllCoins, useIotaAddressValidation } from '@iota/core'; +import { CoinStruct } from '@iota/iota-sdk/client'; +import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; +import { Exclamation } from '@iota/ui-icons'; +import { Field, Form, Formik, useFormikContext } from 'formik'; +import Input from '../Input'; +import { ChangeEventHandler, useCallback } from 'react'; + +const INITIAL_VALUES = { + to: '', + amount: '', + isPayAllIota: false, + gasBudgetEst: '', +}; + +export type FormValues = typeof INITIAL_VALUES; + +export type SubmitProps = { + to: string; + amount: string; + isPayAllIota: boolean; + coinIds: string[]; + coins: CoinStruct[]; + gasBudgetEst: string; +}; + +export type SendTokenFormProps = { + coinType: string; + activeAddress: string; + setOpen: (bool: boolean) => void; + open: boolean; +}; + +function totalBalance(coins: CoinStruct[]): bigint { + return coins.reduce((partialSum, c) => partialSum + getBalanceFromCoinStruct(c), BigInt(0)); +} +function getBalanceFromCoinStruct(coin: CoinStruct): bigint { + return BigInt(coin.balance); +} + +export function SendTokenDialog({ + coinType, + activeAddress, + setOpen, + open, +}: SendTokenFormProps): React.JSX.Element { + const { data: coinsData } = useGetAllCoins(coinType, activeAddress!); + const { setFieldValue, validateField } = useFormikContext(); + const iotaAddressValidation = useIotaAddressValidation(); + + const { data: iotaCoinsData } = useGetAllCoins(IOTA_TYPE_ARG, activeAddress!); + + const iotaCoins = iotaCoinsData; + const coins = coinsData; + const coinBalance = totalBalance(coins || []); + const iotaBalance = totalBalance(iotaCoins || []); + + const coinMetadata = useCoinMetadata(coinType); + const coinDecimals = coinMetadata.data?.decimals ?? 0; + + // const validationSchemaStepOne = useMemo( + // () => createValidationSchemaStepOne(coinBalance, symbol, coinDecimals), + // [client, coinBalance, symbol, coinDecimals], + // ); + + // remove the comma from the token balance + const initAmountBig = parseAmount('0', coinDecimals); + // const initAmountBig = parseAmount(initialAmount, coinDecimals); + + const handleAddressChange = useCallback>( + (e) => { + const address = e.currentTarget.value; + setFieldValue(activeAddress, iotaAddressValidation.cast(address)).then(() => { + validateField(activeAddress); + }); + }, + [setFieldValue, activeAddress, iotaAddressValidation], + ); + + async function handleFormSubmit({ to, amount, isPayAllIota, gasBudgetEst }: FormValues) { + if (!coins || !iotaCoins) return; + const coinsIDs = [...coins] + .sort((a, b) => Number(b.balance) - Number(a.balance)) + .map(({ coinObjectId }) => coinObjectId); + + const data = { + to, + amount, + isPayAllIota, + coins, + coinIds: coinsIDs, + gasBudgetEst, + }; + console.log('data', data); + + // onSubmit(data); + } + + return ( + + +
setOpen(false)} /> + + + {({ isValid, isSubmitting, setFieldValue, values, submitForm }) => { + const newPayIotaAll = + parseAmount(values.amount, coinDecimals) === coinBalance && + coinType === IOTA_TYPE_ARG; + if (values.isPayAllIota !== newPayIotaAll) { + setFieldValue('isPayAllIota', newPayIotaAll); + } + + const hasEnoughBalance = + values.isPayAllIota || + iotaBalance > + parseAmount(values.gasBudgetEst, coinDecimals) + + parseAmount( + coinType === IOTA_TYPE_ARG ? values.amount : '0', + coinDecimals, + ); + + return ( +
+
+
+ {!hasEnoughBalance ? ( + } + /> + ) : null} + + {/* */} + handleAddressChange(e)} + label="Enter recipient address" + /> + } + allowNegative={false} + name="to" + placeholder="Enter Address" + /> +
+
+ +
+
+
+ ); + }} +
+
+ +
+ ); +} diff --git a/apps/wallet-dashboard/components/Dialogs/index.ts b/apps/wallet-dashboard/components/Dialogs/index.ts new file mode 100644 index 00000000000..ea5b76591ee --- /dev/null +++ b/apps/wallet-dashboard/components/Dialogs/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +export * from './SendTokenDialog'; diff --git a/apps/wallet-dashboard/components/index.ts b/apps/wallet-dashboard/components/index.ts index e7f010c273f..b1cd5fcb8f9 100644 --- a/apps/wallet-dashboard/components/index.ts +++ b/apps/wallet-dashboard/components/index.ts @@ -18,3 +18,4 @@ export * from './Popup'; export * from './AppList'; export * from './Cards'; export * from './Buttons'; +export * from './Dialogs'; diff --git a/apps/wallet-dashboard/package.json b/apps/wallet-dashboard/package.json index e8a4a107e2f..937086581d5 100644 --- a/apps/wallet-dashboard/package.json +++ b/apps/wallet-dashboard/package.json @@ -23,6 +23,7 @@ "@tanstack/react-query": "^5.50.1", "@tanstack/react-virtual": "^3.5.0", "clsx": "^2.1.1", + "formik": "^2.4.2", "next": "14.2.10", "react": "^18.3.1", "react-hot-toast": "^2.4.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92e2c8a9a2f..9bc6c22f23a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -179,7 +179,7 @@ importers: version: 5.2.1(@types/eslint@8.56.12)(eslint-config-prettier@9.1.0(eslint@8.57.1))(eslint@8.57.1)(prettier@3.3.3) jest: specifier: ^29.5.0 - version: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + version: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) prettier: specifier: ^3.3.1 version: 3.3.3 @@ -191,7 +191,7 @@ importers: version: 6.3.4 ts-jest: specifier: ^29.1.0 - version: 29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)))(typescript@5.6.2) + version: 29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)))(typescript@5.6.2) ts-loader: specifier: ^9.4.4 version: 9.5.1(typescript@5.6.2)(webpack@5.95.0(@swc/core@1.7.28)) @@ -1015,6 +1015,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + formik: + specifier: ^2.4.2 + version: 2.4.6(react@18.3.1) next: specifier: 14.2.10 version: 14.2.10(@babel/core@7.25.2)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.79.3) @@ -1045,7 +1048,7 @@ importers: version: 14.2.3(eslint@8.57.1)(typescript@5.6.2) jest: specifier: ^29.5.0 - version: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + version: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) postcss: specifier: ^8.4.31 version: 8.4.47 @@ -1054,7 +1057,7 @@ importers: version: 3.4.13(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) ts-jest: specifier: ^29.1.0 - version: 29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)))(typescript@5.6.2) + version: 29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)))(typescript@5.6.2) typescript: specifier: ^5.5.3 version: 5.6.2 @@ -20540,7 +20543,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2))': + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0(node-notifier@10.0.0) @@ -20554,7 +20557,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + jest-config: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -26471,13 +26474,13 @@ snapshots: crc-32@1.2.2: {} - create-jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)): + create-jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + jest-config: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -29686,16 +29689,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)): + jest-cli@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + create-jest: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + jest-config: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -29707,7 +29710,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)): + jest-config@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)): dependencies: '@babel/core': 7.25.2 '@jest/test-sequencer': 29.7.0 @@ -29964,12 +29967,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)): + jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + jest-cli: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) optionalDependencies: node-notifier: 10.0.0 transitivePeerDependencies: @@ -34556,12 +34559,12 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)))(typescript@5.6.2): + ts-jest@29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)))(typescript@5.6.2): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + jest: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 From 79c283b9d0f117922229024250e0833fcf90d0e3 Mon Sep 17 00:00:00 2001 From: cpl121 Date: Thu, 31 Oct 2024 18:31:50 +0100 Subject: [PATCH 04/87] feat(wallet-dashboard): style send entry screen WIP --- apps/core/package.json | 2 + apps/core/src/components/CoinSelector.tsx | 60 +++++ .../src/components/Inputs/AddressInput.tsx | 80 ++++++ apps/core/src/components/Inputs/FormInput.tsx | 73 +++++ .../components/Inputs/SendTokenFormInput.tsx | 63 +++++ apps/core/src/components/Inputs/index.ts | 6 + apps/core/src/components/index.ts | 3 + .../Dialogs/SendToken/SendCoinDialog.tsx | 128 +++++++++ .../SendToken}/index.ts | 4 +- .../SendToken/views/EnterValuesFormView.tsx | 254 ++++++++++++++++++ .../SendToken}/views/ReviewValuesFormView.tsx | 6 +- .../SendToken}/views/index.ts | 0 .../components/Dialogs/SendTokenDialog.tsx | 216 --------------- .../components/Dialogs/index.ts | 2 +- apps/wallet-dashboard/components/Dropdown.tsx | 53 ---- .../Popups/SendCoinPopup/SendCoinPopup.tsx | 129 --------- .../views/EnterValuesFormView.tsx | 74 ----- .../components/Popup/Popups/index.ts | 2 - .../account-balance/AccountBalance.tsx | 28 +- .../components/coins/MyCoins.tsx | 35 ++- apps/wallet-dashboard/components/index.ts | 2 +- apps/wallet-dashboard/tsconfig.json | 8 +- .../ui/app/components/address-input/index.tsx | 76 ------ .../src/ui/app/components/coin-icon/index.tsx | 16 -- .../home/nft-transfer/TransferNFTForm.tsx | 3 +- .../pages/home/transfer-coin/FormInput.tsx | 51 ---- .../home/transfer-coin/SendTokenForm.tsx | 84 +++--- .../ui/app/pages/home/transfer-coin/index.tsx | 111 +++----- pnpm-lock.yaml | 6 + 29 files changed, 784 insertions(+), 791 deletions(-) create mode 100644 apps/core/src/components/CoinSelector.tsx create mode 100644 apps/core/src/components/Inputs/AddressInput.tsx create mode 100644 apps/core/src/components/Inputs/FormInput.tsx create mode 100644 apps/core/src/components/Inputs/SendTokenFormInput.tsx create mode 100644 apps/core/src/components/Inputs/index.ts create mode 100644 apps/wallet-dashboard/components/Dialogs/SendToken/SendCoinDialog.tsx rename apps/wallet-dashboard/components/{Popup/Popups/SendCoinPopup => Dialogs/SendToken}/index.ts (55%) create mode 100644 apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx rename apps/wallet-dashboard/components/{Popup/Popups/SendCoinPopup => Dialogs/SendToken}/views/ReviewValuesFormView.tsx (90%) rename apps/wallet-dashboard/components/{Popup/Popups/SendCoinPopup => Dialogs/SendToken}/views/index.ts (100%) delete mode 100644 apps/wallet-dashboard/components/Dialogs/SendTokenDialog.tsx delete mode 100644 apps/wallet-dashboard/components/Dropdown.tsx delete mode 100644 apps/wallet-dashboard/components/Popup/Popups/SendCoinPopup/SendCoinPopup.tsx delete mode 100644 apps/wallet-dashboard/components/Popup/Popups/SendCoinPopup/views/EnterValuesFormView.tsx delete mode 100644 apps/wallet/src/ui/app/components/address-input/index.tsx delete mode 100644 apps/wallet/src/ui/app/pages/home/transfer-coin/FormInput.tsx diff --git a/apps/core/package.json b/apps/core/package.json index 31cb3e43e8f..faf538f5efd 100644 --- a/apps/core/package.json +++ b/apps/core/package.json @@ -27,6 +27,8 @@ "@growthbook/growthbook-react": "^1.0.0", "@hookform/resolvers": "^3.9.0", "@iota/dapp-kit": "workspace:*", + "@iota/apps-ui-kit": "workspace:*", + "@iota/ui-icons": "workspace:*", "@iota/iota-sdk": "workspace:*", "@iota/kiosk": "workspace:*", "@sentry/react": "^7.59.2", diff --git a/apps/core/src/components/CoinSelector.tsx b/apps/core/src/components/CoinSelector.tsx new file mode 100644 index 00000000000..9e011b1a924 --- /dev/null +++ b/apps/core/src/components/CoinSelector.tsx @@ -0,0 +1,60 @@ +import { useIotaClientQuery } from '@iota/dapp-kit'; +import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; +import { filterAndSortTokenBalances } from '../utils'; +import { COINS_QUERY_REFETCH_INTERVAL, COINS_QUERY_STALE_TIME } from '../constants'; +import { LoadingIndicator, Select, SelectOption } from '@iota/apps-ui-kit'; +import { CoinBalance } from '@iota/iota-sdk/client'; +import { useFormatCoin } from '../hooks'; +import { IotaLogoMark } from '@iota/ui-icons'; + +export function CoinSelector({ + activeCoinType = IOTA_TYPE_ARG, + coins = [], + onClick, +}: { + activeCoinType: string; + coins: CoinBalance[]; + onClick: (coinType: string) => void; +}) { + + const activeCoin = coins?.find(({ coinType }) => coinType === activeCoinType) ?? coins?.[0]; + const initialValue = activeCoin?.coinType; + const coinsOptions: SelectOption[] = + coins?.map((coin) => ({ + id: coin.coinType, + renderLabel: () => , + })) || []; + + return ( + + + + ) : undefined + } + /> + ); +} diff --git a/apps/core/src/components/Inputs/FormInput.tsx b/apps/core/src/components/Inputs/FormInput.tsx new file mode 100644 index 00000000000..b87d53648d2 --- /dev/null +++ b/apps/core/src/components/Inputs/FormInput.tsx @@ -0,0 +1,73 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { Input, InputType, type InputProps, type NumericFormatInputProps } from '@iota/apps-ui-kit'; +import React, { useMemo } from 'react'; + +interface FormInputProps extends Omit { + name: string; + value: string; + suffix: string; + allowNegative: boolean; + onChange: (value: string) => void; + onBlur?: React.FocusEventHandler; + errorMessage?: string; + renderAction?: (isDisabled?: boolean) => React.JSX.Element; + decimals?: boolean; + disabled?: boolean; + isSubmitting?: boolean; +} + +export default function FormInput({ + renderAction, + decimals, + value, + onChange, + onBlur, + errorMessage, + isSubmitting = false, + disabled, + name, + type, + placeholder, + amountCounter, + label, + suffix, + allowNegative, +}: FormInputProps) { + const isInputDisabled = isSubmitting || disabled; + const isNumericFormat = type === InputType.NumericFormat; + + const numericPropsOnly: Partial = useMemo( + () => ({ + decimalScale: decimals ? undefined : 0, + thousandSeparator: true, + onValueChange: (values) => { + onChange(values.value); + }, + }), + [decimals, onChange], + ); + + const isActionButtonDisabled = isInputDisabled || !value || !!errorMessage; + + return ( + onChange(e.currentTarget.value)} + amountCounter={!errorMessage ? amountCounter : undefined} + trailingElement={renderAction?.(isActionButtonDisabled)} + {...(isNumericFormat ? numericPropsOnly : {})} + /> + ); +} + diff --git a/apps/core/src/components/Inputs/SendTokenFormInput.tsx b/apps/core/src/components/Inputs/SendTokenFormInput.tsx new file mode 100644 index 00000000000..a39e661f7d4 --- /dev/null +++ b/apps/core/src/components/Inputs/SendTokenFormInput.tsx @@ -0,0 +1,63 @@ +import { ButtonPill, InputType } from '@iota/apps-ui-kit'; +import { CoinStruct } from '@iota/iota-sdk/client'; +import FormInput from './FormInput'; + +export interface SendTokenInputProps { + gasBudgetEstimation: string; + coins?: CoinStruct[]; + symbol: string; + values: { + amount: string; + isPayAllIota: boolean; + }; + onActionClick: () => Promise; + isActionButtonDisabled?: boolean | 'auto'; + value: string; + onChange: (value: string) => void; + onBlur?: React.FocusEventHandler; + errorMessage?: string; +} + +export function SendTokenFormInput({ + gasBudgetEstimation, + coins, + values, + symbol, + onActionClick, + isActionButtonDisabled, + value, + onChange, + onBlur, + errorMessage, +}: SendTokenInputProps) { + return ( + ( + + Max + + )} + /> + ); +} diff --git a/apps/core/src/components/Inputs/index.ts b/apps/core/src/components/Inputs/index.ts new file mode 100644 index 00000000000..da5352ee2a5 --- /dev/null +++ b/apps/core/src/components/Inputs/index.ts @@ -0,0 +1,6 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +export { default as AddressInput } from './AddressInput'; +export * from './FormInput'; +export * from './SendTokenFormInput'; diff --git a/apps/core/src/components/index.ts b/apps/core/src/components/index.ts index 67d6dc1fbc3..f4cf5900a8e 100644 --- a/apps/core/src/components/index.ts +++ b/apps/core/src/components/index.ts @@ -2,3 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 export * from './KioskClientProvider'; +export * from './CoinSelector'; + +export * from './Inputs'; diff --git a/apps/wallet-dashboard/components/Dialogs/SendToken/SendCoinDialog.tsx b/apps/wallet-dashboard/components/Dialogs/SendToken/SendCoinDialog.tsx new file mode 100644 index 00000000000..5a225f04a3c --- /dev/null +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/SendCoinDialog.tsx @@ -0,0 +1,128 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import React, { useState } from 'react'; +import { EnterValuesFormView, ReviewValuesFormView } from './views'; +import { CoinBalance } from '@iota/iota-sdk/client'; +import { useSendCoinTransaction, useNotifications } from '@/hooks'; +import { useSignAndExecuteTransaction } from '@iota/dapp-kit'; +import { NotificationType } from '@/stores/notificationStore'; +import { useGetAllCoins } from '@iota/core'; +import { Dialog, DialogBody, DialogContent, DialogPosition, Header } from '@iota/apps-ui-kit'; + +export interface FormDataValues { + amount: string; + to: string; + isPayAllIota: boolean; + gasBudgetEst: string; +} + +export const INITIAL_VALUES: FormDataValues = { + to: '', + amount: '', + isPayAllIota: false, + gasBudgetEst: '', +}; + +interface SendCoinPopupProps { + coin: CoinBalance; + activeAddress: string; + setOpen: (bool: boolean) => void; + open: boolean; +} + +enum FormStep { + EnterValues, + ReviewValues, +} + +function SendCoinDialog({ + coin, + activeAddress, + setOpen, + open, +}: SendCoinPopupProps): React.JSX.Element { + const [step, setStep] = useState(FormStep.EnterValues); + const [selectedCoin, setSelectedCoin] = useState(coin); + const [formData, setFormData] = useState(INITIAL_VALUES); + const { addNotification } = useNotifications(); + + const { data: coinsData } = useGetAllCoins(selectedCoin.coinType, activeAddress); + + const { + mutateAsync: signAndExecuteTransaction, + error, + isPending, + } = useSignAndExecuteTransaction(); + const { data: sendCoinData } = useSendCoinTransaction( + coinsData || [], + selectedCoin.coinType, + activeAddress, + formData.to, + formData.amount, + selectedCoin.totalBalance === formData.amount, + ); + + function handleTransfer() { + if (!sendCoinData?.transaction) { + addNotification('There was an error with the transaction', NotificationType.Error); + return; + } else { + signAndExecuteTransaction({ + transaction: sendCoinData.transaction, + }) + .then(() => { + setOpen(false); + addNotification('Transfer transaction has been sent'); + }) + .catch(() => { + addNotification('Transfer transaction was not sent', NotificationType.Error); + }); + } + } + + function onNext(): void { + setStep(FormStep.ReviewValues); + } + + function onBack(): void { + setStep(FormStep.EnterValues); + } + + return ( + + +
setOpen(false)} + /> +
+ + {step === FormStep.EnterValues && ( + + )} + {step === FormStep.ReviewValues && ( + + )} + +
+ +
+ ); +} + +export default SendCoinDialog; diff --git a/apps/wallet-dashboard/components/Popup/Popups/SendCoinPopup/index.ts b/apps/wallet-dashboard/components/Dialogs/SendToken/index.ts similarity index 55% rename from apps/wallet-dashboard/components/Popup/Popups/SendCoinPopup/index.ts rename to apps/wallet-dashboard/components/Dialogs/SendToken/index.ts index 50cde552014..4b62e506333 100644 --- a/apps/wallet-dashboard/components/Popup/Popups/SendCoinPopup/index.ts +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/index.ts @@ -1,4 +1,6 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -export { default as SendCoinPopup } from './SendCoinPopup'; +export * from './SendCoinDialog'; + +export * from './views'; diff --git a/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx b/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx new file mode 100644 index 00000000000..d74df95d1ac --- /dev/null +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx @@ -0,0 +1,254 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { CoinBalance, CoinStruct } from '@iota/iota-sdk/client'; +import { FormDataValues, INITIAL_VALUES } from '../SendCoinDialog'; +import { + AddressInput, + CoinFormat, + COINS_QUERY_REFETCH_INTERVAL, + COINS_QUERY_STALE_TIME, + CoinSelector, + filterAndSortTokenBalances, + parseAmount, + SendTokenFormInput, + useCoinMetadata, + useFormatCoin, + useGetAllCoins, +} from '@iota/core'; +import { + ButtonHtmlType, + ButtonType, + InfoBox, + InfoBoxType, + Button, + InfoBoxStyle, + LoadingIndicator, +} from '@iota/apps-ui-kit'; +import { useCurrentAccount, useIotaClientQuery } from '@iota/dapp-kit'; +import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; +import { Field, FieldInputProps, Form, Formik } from 'formik'; +import { Exclamation } from '@iota/ui-icons'; + +interface EnterValuesFormProps { + coin: CoinBalance; + gasBudget: string; + setFormData: React.Dispatch>; + setSelectedCoin: React.Dispatch>; + onNext: () => void; +} + +function totalBalance(coins: CoinStruct[]): bigint { + return coins.reduce((partialSum, c) => partialSum + getBalanceFromCoinStruct(c), BigInt(0)); +} +function getBalanceFromCoinStruct(coin: CoinStruct): bigint { + return BigInt(coin.balance); +} + +function EnterValuesFormView({ + coin, + gasBudget, + setFormData, + setSelectedCoin, + onNext, +}: EnterValuesFormProps): JSX.Element { + const account = useCurrentAccount(); + const activeAddress = account?.address; + + // Get all coins of the type + const { data: coinsData, isPending: coinsIsPending } = useGetAllCoins( + coin.coinType, + activeAddress!, + ); + const { data: iotaCoinsData, isPending: iotaCoinsIsPending } = useGetAllCoins( + IOTA_TYPE_ARG, + activeAddress!, + ); + + const { data: coinsBalance, isPending: coinsBalanceIsPending } = useIotaClientQuery( + 'getAllBalances', + { owner: activeAddress! }, + { + enabled: !!activeAddress, + refetchInterval: COINS_QUERY_REFETCH_INTERVAL, + staleTime: COINS_QUERY_STALE_TIME, + select: filterAndSortTokenBalances, + }, + ); + + const iotaCoins = iotaCoinsData; + const coins = coinsData; + const coinBalance = totalBalance(coins || []); + const iotaBalance = totalBalance(iotaCoins || []); + + const [tokenBalance, symbol, queryResult] = useFormatCoin( + coinBalance, + coin.coinType, + CoinFormat.FULL, + ); + + const coinMetadata = useCoinMetadata(coin.coinType); + const coinDecimals = coinMetadata.data?.decimals ?? 0; + + const formattedTokenBalance = tokenBalance.replace(/,/g, ''); + const initAmountBig = parseAmount('', coinDecimals); + + if (coinsBalanceIsPending || coinsIsPending || iotaCoinsIsPending) { + return ( +
+ +
+ ); + } + + async function handleFormSubmit({ to, amount, isPayAllIota, gasBudgetEst }: FormDataValues) { + if (!coins || !iotaCoins) return; + const coinsIDs = [...coins] + .sort((a, b) => Number(b.balance) - Number(a.balance)) + .map(({ coinObjectId }) => coinObjectId); + + const data = { + to, + amount, + isPayAllIota, + coins, + coinIds: coinsIDs, + gasBudgetEst, + }; + setFormData(data); + onNext(); + } + + return ( +
+ { + setFormData(INITIAL_VALUES); + const coin = coinsBalance?.find((coin) => coin.coinType === coinType); + setSelectedCoin(coin!); + }} + /> + + + {({ + isValid, + isSubmitting, + setFieldValue, + values, + submitForm, + touched, + errors, + handleBlur, + }) => { + const newPayIotaAll = + parseAmount(values.amount, coinDecimals) === coinBalance && + coin.coinType === IOTA_TYPE_ARG; + if (values.isPayAllIota !== newPayIotaAll) { + setFieldValue('isPayAllIota', newPayIotaAll); + } + + const hasEnoughBalance = + values.isPayAllIota || + iotaBalance > + parseAmount(values.gasBudgetEst, coinDecimals) + + parseAmount( + coin.coinType === IOTA_TYPE_ARG ? values.amount : '0', + coinDecimals, + ); + + async function onMaxTokenButtonClick() { + await setFieldValue('amount', formattedTokenBalance); + } + + const isMaxActionDisabled = + parseAmount(values?.amount, coinDecimals) === coinBalance || + queryResult.isPending || + !coinBalance; + + return ( +
+
+
+ {!hasEnoughBalance && ( + } + /> + )} + + }) => ( + setFieldValue('amount', value)} + onBlur={handleBlur} + errorMessage={ + touched.amount && errors.amount + ? errors.amount + : undefined + } + /> + )} + /> + + +
+
+ +
+
+
+ ); + }} +
+
+ ); +} + +export default EnterValuesFormView; diff --git a/apps/wallet-dashboard/components/Popup/Popups/SendCoinPopup/views/ReviewValuesFormView.tsx b/apps/wallet-dashboard/components/Dialogs/SendToken/views/ReviewValuesFormView.tsx similarity index 90% rename from apps/wallet-dashboard/components/Popup/Popups/SendCoinPopup/views/ReviewValuesFormView.tsx rename to apps/wallet-dashboard/components/Dialogs/SendToken/views/ReviewValuesFormView.tsx index 0732b263cfc..dc8b64b532c 100644 --- a/apps/wallet-dashboard/components/Popup/Popups/SendCoinPopup/views/ReviewValuesFormView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/views/ReviewValuesFormView.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { FormDataValues } from '../SendCoinPopup'; +import { FormDataValues } from '../SendCoinDialog'; import { Button } from '@/components'; interface ReviewValuesFormProps { @@ -15,7 +15,7 @@ interface ReviewValuesFormProps { } function ReviewValuesFormView({ - formData: { amount, recipientAddress }, + formData: { amount, to }, senderAddress, gasBudget, error, @@ -29,7 +29,7 @@ function ReviewValuesFormView({

Sending: {amount}

From: {senderAddress}

-

To: {recipientAddress}

+

To: {to}

Gas fee: {gasBudget}

{error ? {error} : null} diff --git a/apps/wallet-dashboard/components/Popup/Popups/SendCoinPopup/views/index.ts b/apps/wallet-dashboard/components/Dialogs/SendToken/views/index.ts similarity index 100% rename from apps/wallet-dashboard/components/Popup/Popups/SendCoinPopup/views/index.ts rename to apps/wallet-dashboard/components/Dialogs/SendToken/views/index.ts diff --git a/apps/wallet-dashboard/components/Dialogs/SendTokenDialog.tsx b/apps/wallet-dashboard/components/Dialogs/SendTokenDialog.tsx deleted file mode 100644 index 73773f480a3..00000000000 --- a/apps/wallet-dashboard/components/Dialogs/SendTokenDialog.tsx +++ /dev/null @@ -1,216 +0,0 @@ -// Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { - InfoBox, - InfoBoxStyle, - InfoBoxType, - ButtonType, - ButtonHtmlType, - Button, - Dialog, - DialogContent, - DialogBody, - Header, - DialogPosition, -} from '@iota/apps-ui-kit'; -import { parseAmount, useCoinMetadata, useGetAllCoins, useIotaAddressValidation } from '@iota/core'; -import { CoinStruct } from '@iota/iota-sdk/client'; -import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; -import { Exclamation } from '@iota/ui-icons'; -import { Field, Form, Formik, useFormikContext } from 'formik'; -import Input from '../Input'; -import { ChangeEventHandler, useCallback } from 'react'; - -const INITIAL_VALUES = { - to: '', - amount: '', - isPayAllIota: false, - gasBudgetEst: '', -}; - -export type FormValues = typeof INITIAL_VALUES; - -export type SubmitProps = { - to: string; - amount: string; - isPayAllIota: boolean; - coinIds: string[]; - coins: CoinStruct[]; - gasBudgetEst: string; -}; - -export type SendTokenFormProps = { - coinType: string; - activeAddress: string; - setOpen: (bool: boolean) => void; - open: boolean; -}; - -function totalBalance(coins: CoinStruct[]): bigint { - return coins.reduce((partialSum, c) => partialSum + getBalanceFromCoinStruct(c), BigInt(0)); -} -function getBalanceFromCoinStruct(coin: CoinStruct): bigint { - return BigInt(coin.balance); -} - -export function SendTokenDialog({ - coinType, - activeAddress, - setOpen, - open, -}: SendTokenFormProps): React.JSX.Element { - const { data: coinsData } = useGetAllCoins(coinType, activeAddress!); - const { setFieldValue, validateField } = useFormikContext(); - const iotaAddressValidation = useIotaAddressValidation(); - - const { data: iotaCoinsData } = useGetAllCoins(IOTA_TYPE_ARG, activeAddress!); - - const iotaCoins = iotaCoinsData; - const coins = coinsData; - const coinBalance = totalBalance(coins || []); - const iotaBalance = totalBalance(iotaCoins || []); - - const coinMetadata = useCoinMetadata(coinType); - const coinDecimals = coinMetadata.data?.decimals ?? 0; - - // const validationSchemaStepOne = useMemo( - // () => createValidationSchemaStepOne(coinBalance, symbol, coinDecimals), - // [client, coinBalance, symbol, coinDecimals], - // ); - - // remove the comma from the token balance - const initAmountBig = parseAmount('0', coinDecimals); - // const initAmountBig = parseAmount(initialAmount, coinDecimals); - - const handleAddressChange = useCallback>( - (e) => { - const address = e.currentTarget.value; - setFieldValue(activeAddress, iotaAddressValidation.cast(address)).then(() => { - validateField(activeAddress); - }); - }, - [setFieldValue, activeAddress, iotaAddressValidation], - ); - - async function handleFormSubmit({ to, amount, isPayAllIota, gasBudgetEst }: FormValues) { - if (!coins || !iotaCoins) return; - const coinsIDs = [...coins] - .sort((a, b) => Number(b.balance) - Number(a.balance)) - .map(({ coinObjectId }) => coinObjectId); - - const data = { - to, - amount, - isPayAllIota, - coins, - coinIds: coinsIDs, - gasBudgetEst, - }; - console.log('data', data); - - // onSubmit(data); - } - - return ( - - -
setOpen(false)} /> - - - {({ isValid, isSubmitting, setFieldValue, values, submitForm }) => { - const newPayIotaAll = - parseAmount(values.amount, coinDecimals) === coinBalance && - coinType === IOTA_TYPE_ARG; - if (values.isPayAllIota !== newPayIotaAll) { - setFieldValue('isPayAllIota', newPayIotaAll); - } - - const hasEnoughBalance = - values.isPayAllIota || - iotaBalance > - parseAmount(values.gasBudgetEst, coinDecimals) + - parseAmount( - coinType === IOTA_TYPE_ARG ? values.amount : '0', - coinDecimals, - ); - - return ( -
-
-
- {!hasEnoughBalance ? ( - } - /> - ) : null} - - {/* */} - handleAddressChange(e)} - label="Enter recipient address" - /> - } - allowNegative={false} - name="to" - placeholder="Enter Address" - /> -
-
- -
-
-
- ); - }} -
-
- -
- ); -} diff --git a/apps/wallet-dashboard/components/Dialogs/index.ts b/apps/wallet-dashboard/components/Dialogs/index.ts index 2020a47bf9e..4b335f387ac 100644 --- a/apps/wallet-dashboard/components/Dialogs/index.ts +++ b/apps/wallet-dashboard/components/Dialogs/index.ts @@ -1,5 +1,5 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -export * from './SendTokenDialog'; +export * from './SendToken'; export * from './Staking'; diff --git a/apps/wallet-dashboard/components/Dropdown.tsx b/apps/wallet-dashboard/components/Dropdown.tsx deleted file mode 100644 index 50ac7decf3f..00000000000 --- a/apps/wallet-dashboard/components/Dropdown.tsx +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import React from 'react'; - -interface DropdownProps { - options: T[]; - selectedOption: T | null | undefined; - onChange: (selectedOption: T) => void; - placeholder?: string; - disabled?: boolean; - getOptionId: (option: T) => string | number; -} - -function Dropdown({ - options, - selectedOption, - onChange, - placeholder, - disabled = false, - getOptionId, -}: DropdownProps): JSX.Element { - function handleSelectionChange(e: React.ChangeEvent): void { - const selectedKey = e.target.value; - const selectedOption = options.find((option) => getOptionId(option) === selectedKey); - if (selectedOption) { - onChange(selectedOption); - } - } - - return ( - - ); -} - -export default Dropdown; diff --git a/apps/wallet-dashboard/components/Popup/Popups/SendCoinPopup/SendCoinPopup.tsx b/apps/wallet-dashboard/components/Popup/Popups/SendCoinPopup/SendCoinPopup.tsx deleted file mode 100644 index c3f917df955..00000000000 --- a/apps/wallet-dashboard/components/Popup/Popups/SendCoinPopup/SendCoinPopup.tsx +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import React, { useState } from 'react'; -import { EnterValuesFormView, ReviewValuesFormView } from './views'; -import { CoinBalance } from '@iota/iota-sdk/client'; -import { useSendCoinTransaction, useNotifications } from '@/hooks'; -import { useSignAndExecuteTransaction } from '@iota/dapp-kit'; -import { NotificationType } from '@/stores/notificationStore'; -import { Dropdown } from '@/components'; -import { useGetAllCoins } from '@iota/core'; - -export interface FormDataValues { - amount: string; - recipientAddress: string; -} - -interface SendCoinPopupProps { - coin: CoinBalance; - senderAddress: string; - onClose: () => void; - coins: CoinBalance[]; -} - -enum FormStep { - EnterValues, - ReviewValues, -} - -function SendCoinPopup({ - coin, - senderAddress, - onClose, - coins, -}: SendCoinPopupProps): React.JSX.Element { - const [step, setStep] = useState(FormStep.EnterValues); - const [selectedCoin, setCoin] = useState(coin); - const [formData, setFormData] = useState({ - amount: '', - recipientAddress: '', - }); - const { addNotification } = useNotifications(); - - const { data: coinsData } = useGetAllCoins(selectedCoin.coinType, senderAddress); - - const { - mutateAsync: signAndExecuteTransaction, - error, - isPending, - } = useSignAndExecuteTransaction(); - const { data: sendCoinData } = useSendCoinTransaction( - coinsData || [], - selectedCoin.coinType, - senderAddress, - formData.recipientAddress, - formData.amount, - selectedCoin.totalBalance === formData.amount, - ); - - function handleTransfer() { - if (!sendCoinData?.transaction) { - addNotification('There was an error with the transaction', NotificationType.Error); - return; - } else { - signAndExecuteTransaction({ - transaction: sendCoinData.transaction, - }) - .then(() => { - onClose(); - addNotification('Transfer transaction has been sent'); - }) - .catch(() => { - addNotification('Transfer transaction was not sent', NotificationType.Error); - }); - } - } - - function onNext(): void { - setStep(FormStep.ReviewValues); - } - - function onBack(): void { - setStep(FormStep.EnterValues); - } - - function handleSelectedCoin(coin: CoinBalance): void { - setCoin(coin); - setFormData({ - amount: '', - recipientAddress: '', - }); - } - - return ( - <> - _selectedCoin.coinType} - /> - {step === FormStep.EnterValues && ( - - )} - {step === FormStep.ReviewValues && ( - - )} - - ); -} - -export default SendCoinPopup; diff --git a/apps/wallet-dashboard/components/Popup/Popups/SendCoinPopup/views/EnterValuesFormView.tsx b/apps/wallet-dashboard/components/Popup/Popups/SendCoinPopup/views/EnterValuesFormView.tsx deleted file mode 100644 index 8c094ac8a90..00000000000 --- a/apps/wallet-dashboard/components/Popup/Popups/SendCoinPopup/views/EnterValuesFormView.tsx +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { CoinBalance } from '@iota/iota-sdk/client'; -import { FormDataValues } from '../SendCoinPopup'; -import { Button } from '@/components'; -import { useFormatCoin } from '@iota/core'; - -interface EnterValuesFormProps { - coin: CoinBalance; - formData: FormDataValues; - gasBudget: string; - setFormData: React.Dispatch>; - onClose: () => void; - onNext: () => void; -} - -function EnterValuesFormView({ - coin: { totalBalance, coinType }, - formData: { amount, recipientAddress }, - gasBudget, - setFormData, - onClose, - onNext, -}: EnterValuesFormProps): JSX.Element { - const handleChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - setFormData((prevFormData) => ({ - ...prevFormData, - [name]: value, - })); - }; - const [formattedCoin, coinSymbol, { data: coinMeta }] = useFormatCoin(totalBalance, coinType); - - return ( -
-

Send

-
-

{coinMeta?.name.toUpperCase() ?? coinType}

-

- Balance: {formattedCoin} {coinSymbol} -

- - - - -

Gas fee: {gasBudget}

-
-
- - -
-
- ); -} - -export default EnterValuesFormView; diff --git a/apps/wallet-dashboard/components/Popup/Popups/index.ts b/apps/wallet-dashboard/components/Popup/Popups/index.ts index e91b80061b2..94a6a69a7cf 100644 --- a/apps/wallet-dashboard/components/Popup/Popups/index.ts +++ b/apps/wallet-dashboard/components/Popup/Popups/index.ts @@ -4,8 +4,6 @@ export { default as TransactionDetailsPopup } from './TransactionDetailsPopup'; export { default as StakeDetailsPopup } from './StakeDetailsPopup'; export { default as UnstakePopup } from './UnstakePopup'; -export { default as SendCoinPopup } from './SendCoinPopup/SendCoinPopup'; export { default as SendAssetPopup } from './SendAssetPopup'; -export * from './SendCoinPopup'; export * from './VestingPopup'; diff --git a/apps/wallet-dashboard/components/account-balance/AccountBalance.tsx b/apps/wallet-dashboard/components/account-balance/AccountBalance.tsx index 23707407541..e661ba5bbbf 100644 --- a/apps/wallet-dashboard/components/account-balance/AccountBalance.tsx +++ b/apps/wallet-dashboard/components/account-balance/AccountBalance.tsx @@ -12,14 +12,13 @@ import { } from '@iota/core'; import { Address, Button, ButtonSize, ButtonType, Panel } from '@iota/apps-ui-kit'; import { CoinBalance, getNetwork } from '@iota/iota-sdk/client'; -import { SendCoinPopup } from '../Popup'; -import { usePopups } from '@/hooks'; import toast from 'react-hot-toast'; +import SendCoinDialog from '../Dialogs/SendToken/SendCoinDialog'; +import { useState } from 'react'; export function AccountBalance() { const account = useCurrentAccount(); const address = account?.address; - const { openPopup, closePopup } = usePopups(); const { network } = useIotaClientContext(); const { explorer } = getNetwork(network); const { data: coinBalance, isPending } = useBalance(address!); @@ -35,18 +34,14 @@ export function AccountBalance() { select: filterAndSortTokenBalances, }, ); + const [isSendTokenDialogOpen, setIsSendTokenDialogOpen] = useState(false); + const [selectedCoin, setSelectedCoin] = useState(); const explorerLink = `${explorer}/address/${address}`; function openSendTokenPopup(coin: CoinBalance, address: string): void { if (coinBalances) { - openPopup( - , - ); + setIsSendTokenDialogOpen(true); + setSelectedCoin(coin); } } @@ -77,8 +72,7 @@ export function AccountBalance() {
)} + {selectedCoin && address && ( + + )} ); } diff --git a/apps/wallet-dashboard/components/coins/MyCoins.tsx b/apps/wallet-dashboard/components/coins/MyCoins.tsx index bf3a3d6a8f7..ac70e99b07c 100644 --- a/apps/wallet-dashboard/components/coins/MyCoins.tsx +++ b/apps/wallet-dashboard/components/coins/MyCoins.tsx @@ -3,8 +3,7 @@ import React, { useState } from 'react'; import { useCurrentAccount, useIotaClientQuery } from '@iota/dapp-kit'; -import { CoinItem, SendCoinPopup } from '@/components'; -import { usePopups } from '@/hooks'; +import { CoinItem } from '@/components'; import { CoinBalance } from '@iota/iota-sdk/client'; import { COINS_QUERY_REFETCH_INTERVAL, @@ -20,6 +19,7 @@ import { Title, } from '@iota/apps-ui-kit'; import { RecognizedBadge } from '@iota/ui-icons'; +import SendCoinDialog from '../Dialogs/SendToken/SendCoinDialog'; enum TokenCategory { All = 'All', @@ -44,8 +44,9 @@ const TOKEN_CATEGORIES = [ function MyCoins(): React.JSX.Element { const [selectedTokenCategory, setSelectedTokenCategory] = useState(TokenCategory.All); + const [isSendTokenDialogOpen, setIsSendTokenDialogOpen] = useState(false); + const [selectedCoin, setSelectedCoin] = useState(); - const { openPopup, closePopup } = usePopups(); const account = useCurrentAccount(); const activeAccountAddress = account?.address; @@ -61,16 +62,10 @@ function MyCoins(): React.JSX.Element { ); const { recognized, unrecognized } = useSortedCoinsByCategories(coinBalances ?? []); - function openSendTokenPopup(coin: CoinBalance, address: string): void { + function openSendTokenDialog(coin: CoinBalance): void { if (coinBalances) { - openPopup( - , - ); + setIsSendTokenDialogOpen(true); + setSelectedCoin(coin); } } @@ -111,9 +106,7 @@ function MyCoins(): React.JSX.Element { key={index} coinType={coin.coinType} balance={BigInt(coin.totalBalance)} - onClick={() => - openSendTokenPopup(coin, account?.address ?? '') - } + onClick={() => openSendTokenDialog(coin)} icon={ } @@ -129,15 +122,21 @@ function MyCoins(): React.JSX.Element { key={index} coinType={coin.coinType} balance={BigInt(coin.totalBalance)} - onClick={() => - openSendTokenPopup(coin, account?.address ?? '') - } + onClick={() => openSendTokenDialog(coin)} /> ); })} + {selectedCoin && activeAccountAddress && ( + + )} ); } diff --git a/apps/wallet-dashboard/components/index.ts b/apps/wallet-dashboard/components/index.ts index 3aad048346d..88d191e6750 100644 --- a/apps/wallet-dashboard/components/index.ts +++ b/apps/wallet-dashboard/components/index.ts @@ -10,7 +10,7 @@ export { default as Input } from './Input'; export { default as VirtualList } from './VirtualList'; export { default as ExternalImage } from './ExternalImage'; export { default as TransactionIcon } from './TransactionIcon'; -export { default as Dropdown } from './Dropdown'; +export { default as FormInput } from '@iota/core/src/components/Inputs/FormInput'; export * from './account-balance/AccountBalance'; export * from './coins'; diff --git a/apps/wallet-dashboard/tsconfig.json b/apps/wallet-dashboard/tsconfig.json index d452a8afa13..77538e96889 100644 --- a/apps/wallet-dashboard/tsconfig.json +++ b/apps/wallet-dashboard/tsconfig.json @@ -27,6 +27,12 @@ "@/stores/*": ["./stores/*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "../core/src/components/Inputs/FormInput.tsx" + ], "exclude": ["node_modules"] } diff --git a/apps/wallet/src/ui/app/components/address-input/index.tsx b/apps/wallet/src/ui/app/components/address-input/index.tsx deleted file mode 100644 index e5d61c02b22..00000000000 --- a/apps/wallet/src/ui/app/components/address-input/index.tsx +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// Modifications Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { useField, useFormikContext } from 'formik'; -import { useCallback, useMemo } from 'react'; -import type { ChangeEventHandler } from 'react'; -import { useIotaAddressValidation } from '@iota/core'; -import { Input, InputType } from '@iota/apps-ui-kit'; -import { Close } from '@iota/ui-icons'; - -export interface AddressInputProps { - disabled?: boolean; - placeholder?: string; - name: string; - label?: string; - shouldValidateManually?: boolean; -} - -export function AddressInput({ - disabled: forcedDisabled, - placeholder = '0x...', - name = 'to', - label = 'Enter Recipient Address', -}: AddressInputProps) { - const [field, meta] = useField(name); - - const { isSubmitting, setFieldValue, validateField } = useFormikContext(); - const iotaAddressValidation = useIotaAddressValidation(); - - const disabled = forcedDisabled !== undefined ? forcedDisabled : isSubmitting; - const handleOnChange = useCallback>( - (e) => { - const address = e.currentTarget.value; - setFieldValue(name, iotaAddressValidation.cast(address)).then(() => { - validateField(name); - }); - }, - [setFieldValue, name, iotaAddressValidation], - ); - const formattedValue = useMemo( - () => iotaAddressValidation.cast(field?.value), - [field?.value, iotaAddressValidation], - ); - - const clearAddress = useCallback(() => { - setFieldValue('to', ''); - }, [setFieldValue]); - - return ( - <> - - - - ) : undefined - } - /> - - ); -} diff --git a/apps/wallet/src/ui/app/components/coin-icon/index.tsx b/apps/wallet/src/ui/app/components/coin-icon/index.tsx index 5214a1e7420..1f3a66e7429 100644 --- a/apps/wallet/src/ui/app/components/coin-icon/index.tsx +++ b/apps/wallet/src/ui/app/components/coin-icon/index.tsx @@ -28,19 +28,3 @@ function NonIotaCoin({ coinType, size = ImageIconSize.Full, rounded }: NonIotaCo ); } - -export interface CoinIconProps { - coinType: string; - size?: ImageIconSize; - rounded?: boolean; -} - -export function CoinIcon({ coinType, size = ImageIconSize.Full, rounded }: CoinIconProps) { - return coinType === IOTA_TYPE_ARG ? ( -
- -
- ) : ( - - ); -} diff --git a/apps/wallet/src/ui/app/pages/home/nft-transfer/TransferNFTForm.tsx b/apps/wallet/src/ui/app/pages/home/nft-transfer/TransferNFTForm.tsx index 59d02cf434e..7806967edd0 100644 --- a/apps/wallet/src/ui/app/pages/home/nft-transfer/TransferNFTForm.tsx +++ b/apps/wallet/src/ui/app/pages/home/nft-transfer/TransferNFTForm.tsx @@ -2,13 +2,12 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { AddressInput } from '_components'; import { ampli } from '_src/shared/analytics/ampli'; import { getSignerOperationErrorMessage } from '_src/ui/app/helpers/errorMessages'; import { useActiveAddress } from '_src/ui/app/hooks'; import { useActiveAccount } from '_src/ui/app/hooks/useActiveAccount'; import { useSigner } from '_src/ui/app/hooks/useSigner'; -import { createNftSendValidationSchema, useGetKioskContents } from '@iota/core'; +import { createNftSendValidationSchema, useGetKioskContents, AddressInput } from '@iota/core'; import { Transaction } from '@iota/iota-sdk/transactions'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Field, Form, Formik } from 'formik'; diff --git a/apps/wallet/src/ui/app/pages/home/transfer-coin/FormInput.tsx b/apps/wallet/src/ui/app/pages/home/transfer-coin/FormInput.tsx deleted file mode 100644 index 88200721c5c..00000000000 --- a/apps/wallet/src/ui/app/pages/home/transfer-coin/FormInput.tsx +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { Input, InputType, type InputProps, type NumericFormatInputProps } from '@iota/apps-ui-kit'; -import { useField, useFormikContext } from 'formik'; - -interface FormInputWithFormixProps { - name: string; - renderAction?: (isDisabled?: boolean) => React.JSX.Element; - decimals?: boolean; -} - -export function FormInput({ - renderAction, - decimals, - ...props -}: InputProps & FormInputWithFormixProps) { - const [field, meta] = useField(props.name); - const form = useFormikContext(); - - const { isSubmitting } = form; - const isInputDisabled = isSubmitting || props.disabled; - - const isActionButtonDisabled = - isInputDisabled || meta?.initialValue === meta?.value || !!meta?.error; - const errorMessage = meta?.error ? meta.error : undefined; - - const isNumericFormat = props.type === InputType.NumericFormat; - const numericPropsOnly: Partial = { - decimalScale: decimals ? undefined : 0, - thousandSeparator: true, - onValueChange: (values) => { - form.setFieldValue(props.name, values.value).then(() => { - form.validateField(props.name); - }); - }, - }; - - return ( - - ); -} diff --git a/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx b/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx index 3ac0ad9e320..0426bbf577f 100644 --- a/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx +++ b/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 import { useActiveAddress } from '_app/hooks/useActiveAddress'; -import { AddressInput, Loading } from '_components'; +import { Loading } from '_components'; import { GAS_SYMBOL } from '_src/ui/app/redux/slices/iota-objects/Coin'; import { useGetAllCoins, @@ -12,12 +12,14 @@ import { useCoinMetadata, useFormatCoin, parseAmount, + AddressInput, + SendTokenFormInput, } from '@iota/core'; import { useIotaClient } from '@iota/dapp-kit'; import { type CoinStruct } from '@iota/iota-sdk/client'; import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; import { useQuery } from '@tanstack/react-query'; -import { Field, Form, Formik, useFormikContext } from 'formik'; +import { Field, FieldInputProps, Form, Formik, useFormikContext } from 'formik'; import { useEffect, useMemo } from 'react'; import { createValidationSchemaStepOne } from './validation'; @@ -25,13 +27,10 @@ import { InfoBox, InfoBoxStyle, InfoBoxType, - InputType, Button, ButtonType, ButtonHtmlType, - ButtonPill, } from '@iota/apps-ui-kit'; -import { FormInput } from './FormInput'; import { Exclamation } from '@iota/ui-icons'; const INITIAL_VALUES = { @@ -148,6 +147,11 @@ export function SendTokenForm({ const coinMetadata = useCoinMetadata(coinType); const coinDecimals = coinMetadata.data?.decimals ?? 0; + const gasBudgetEstimation = useGasBudgetEstimation({ + coinDecimals: coinDecimals, + coins: coins ?? [], + }); + const [tokenBalance, symbol, queryResult] = useFormatCoin( coinBalance, coinType, @@ -205,7 +209,7 @@ export function SendTokenForm({ validateOnBlur={false} onSubmit={handleFormSubmit} > - {({ isValid, isSubmitting, setFieldValue, values, submitForm }) => { + {({ isValid, isSubmitting, setFieldValue, values, submitForm, handleBlur, touched, errors }) => { const newPayIotaAll = parseAmount(values.amount, coinDecimals) === coinBalance && coinType === IOTA_TYPE_ARG; @@ -244,13 +248,26 @@ export function SendTokenForm({ /> ) : null} - }) => ( + setFieldValue('amount', value)} + onBlur={handleBlur} + errorMessage={ + touched.amount && errors.amount + ? errors.amount + : undefined + } + /> + )} /> Promise; isActionButtonDisabled?: boolean | 'auto'; } - -function SendTokenFormInput({ - coinDecimals, - coins, - values, - symbol, - onActionClick, - isActionButtonDisabled, -}: SendTokenInputProps) { - const gasBudgetEstimation = useGasBudgetEstimation({ - coinDecimals: coinDecimals, - coins: coins ?? [], - }); - - return ( - ( - - Max - - )} - /> - ); -} diff --git a/apps/wallet/src/ui/app/pages/home/transfer-coin/index.tsx b/apps/wallet/src/ui/app/pages/home/transfer-coin/index.tsx index 111017d0bb0..2c58581f114 100644 --- a/apps/wallet/src/ui/app/pages/home/transfer-coin/index.tsx +++ b/apps/wallet/src/ui/app/pages/home/transfer-coin/index.tsx @@ -2,17 +2,19 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { CoinIcon, Loading, Overlay } from '_components'; +import { Overlay } from '_components'; import { ampli } from '_src/shared/analytics/ampli'; import { getSignerOperationErrorMessage } from '_src/ui/app/helpers/errorMessages'; import { useActiveAccount } from '_src/ui/app/hooks/useActiveAccount'; import { useSigner } from '_src/ui/app/hooks/useSigner'; import { useUnlockedGuard } from '_src/ui/app/hooks/useUnlockedGuard'; import { + COINS_QUERY_REFETCH_INTERVAL, + COINS_QUERY_STALE_TIME, + CoinSelector, createTokenTransferTransaction, filterAndSortTokenBalances, useCoinMetadata, - useFormatCoin, } from '@iota/core'; // import * as Sentry from '@sentry/react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; @@ -21,13 +23,9 @@ import { toast } from 'react-hot-toast'; import { Navigate, useNavigate, useSearchParams } from 'react-router-dom'; import { PreviewTransfer } from './PreviewTransfer'; import { SendTokenForm, type SubmitProps } from './SendTokenForm'; -import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; -import { Select, Button, type SelectOption, ButtonType } from '@iota/apps-ui-kit'; -import { useActiveAddress, useCoinsReFetchingConfig } from '_src/ui/app/hooks'; -import { useIotaClientQuery } from '@iota/dapp-kit'; -import type { CoinBalance } from '@iota/iota-sdk/client'; -import { ImageIconSize } from '_src/ui/app/shared/image-icon'; +import { Button, ButtonType, LoadingIndicator } from '@iota/apps-ui-kit'; import { Loader } from '@iota/ui-icons'; +import { useIotaClientQuery } from 'node_modules/@iota/dapp-kit/src'; function TransferCoinPage() { const [searchParams] = useSearchParams(); @@ -41,6 +39,25 @@ function TransferCoinPage() { const address = activeAccount?.address; const queryClient = useQueryClient(); + const { data: coinsBalance, isPending: coinsBalanceIsPending } = useIotaClientQuery( + 'getAllBalances', + { owner: address! }, + { + enabled: !!address, + refetchInterval: COINS_QUERY_REFETCH_INTERVAL, + staleTime: COINS_QUERY_STALE_TIME, + select: filterAndSortTokenBalances, + }, + ); + + if (coinsBalanceIsPending) { + return ( +
+ +
+ ); + } + const transaction = useMemo(() => { if (!coinType || !signer || !formData || !address) return null; @@ -104,7 +121,7 @@ function TransferCoinPage() { return null; } - if (!coinType) { + if (!coinType || !coinsBalance) { return ; } @@ -147,8 +164,12 @@ function TransferCoinPage() { ) : ( <> setFormData(undefined)} activeCoinType={coinType} + coins={coinsBalance || []} + onClick={(coinType) => { + setFormData(undefined) + navigate(`/send?${new URLSearchParams({ type: coinType }).toString()}`); + }} /> void; -}) { - const selectedAddress = useActiveAddress(); - const navigate = useNavigate(); - - const { staleTime, refetchInterval } = useCoinsReFetchingConfig(); - const { data: coins, isPending } = useIotaClientQuery( - 'getAllBalances', - { owner: selectedAddress! }, - { - enabled: !!selectedAddress, - refetchInterval, - staleTime, - select: filterAndSortTokenBalances, - }, - ); - - if (!coins?.length) { - return ; - } - - const activeCoin = coins?.find(({ coinType }) => coinType === activeCoinType) ?? coins?.[0]; - const initialValue = activeCoin?.coinType; - const coinsOptions: SelectOption[] = - coins?.map((coin) => ({ - id: coin.coinType, - renderLabel: () => , - })) || []; - - return ( - - { + setFieldValue('gasBudgetEst', gasBudgetEstimation, false); + }, [gasBudgetEstimation, setFieldValue, values.amount]); + return ( void; } export function useGasBudgetEstimation({ @@ -27,8 +25,7 @@ export function useGasBudgetEstimation({ to, amount, isPayAllIota, - setFieldValue, -}: useGasBudgetEstimation) { +}: UseGasBudgetEstimationOptions) { const client = useIotaClient(); const { data: gasBudget } = useQuery({ // eslint-disable-next-line @tanstack/query/exhaustive-deps @@ -63,11 +60,6 @@ export function useGasBudgetEstimation({ }); const [formattedGas] = useFormatCoin(gasBudget, IOTA_TYPE_ARG); - // gasBudgetEstimation should change when the amount above changes - - useEffect(() => { - setFieldValue('gasBudgetEst', formattedGas, false); - }, [formattedGas, setFieldValue, amount]); return formattedGas ? formattedGas + ' ' + GAS_SYMBOL : '--'; } From b9a659e046d53a693dae07aa25c99a91d6186aad Mon Sep 17 00:00:00 2001 From: cpl121 Date: Tue, 12 Nov 2024 09:43:44 +0100 Subject: [PATCH 24/87] fix(wallet-dashboard): fixes --- .../src/components/Inputs/AddressInput.tsx | 4 +- .../Dialogs/SendToken/SendTokenDialog.tsx | 77 ++++++++++--------- 2 files changed, 42 insertions(+), 39 deletions(-) diff --git a/apps/core/src/components/Inputs/AddressInput.tsx b/apps/core/src/components/Inputs/AddressInput.tsx index e43c3001e82..5656f97cdfe 100644 --- a/apps/core/src/components/Inputs/AddressInput.tsx +++ b/apps/core/src/components/Inputs/AddressInput.tsx @@ -14,7 +14,7 @@ export interface AddressInputProps { onChange: (e: React.ChangeEvent) => void; onBlur: (e: React.FocusEvent) => void; }; - formContext: { + form: { setFieldValue: (field: string, value: string, shouldValidate?: boolean) => void; errors: Record; touched: Record; @@ -26,7 +26,7 @@ export interface AddressInputProps { export function AddressInput({ field, - formContext, + form: formContext, disabled, placeholder = '0x...', label = 'Enter Recipient Address', diff --git a/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx b/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx index 49fac690b55..ee325da2220 100644 --- a/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx @@ -36,7 +36,7 @@ enum FormStep { ReviewValues, } -function SendTokenDialog({ +function SendTokenDialogBody({ coin, activeAddress, setOpen, @@ -54,6 +54,7 @@ function SendTokenDialog({ error, isPending, } = useSignAndExecuteTransaction(); + const { data: sendCoinData } = useSendCoinTransaction( coinsData || [], selectedCoin?.coinType, @@ -63,12 +64,6 @@ function SendTokenDialog({ selectedCoin?.totalBalance === formData.amount, ); - useEffect(() => { - setSelectedCoin(coin); - setStep(FormStep.EnterValues); - setFormData(INITIAL_VALUES); - }, [open, setOpen, coin]); - function handleTransfer() { if (!sendCoinData?.transaction) { addNotification('There was an error with the transaction', NotificationType.Error); @@ -96,37 +91,45 @@ function SendTokenDialog({ } return ( - + <> +
setOpen(false)} + /> +
+ + {step === FormStep.EnterValues && ( + + )} + {step === FormStep.ReviewValues && ( + + )} + +
+ + ); +} + +function SendTokenDialog(props: SendCoinPopupProps): React.JSX.Element { + return ( + -
setOpen(false)} - /> -
- - {step === FormStep.EnterValues && ( - - )} - {step === FormStep.ReviewValues && ( - - )} - -
+
); From 58415c712916ac10a9695d1108e0edeacbd050b6 Mon Sep 17 00:00:00 2001 From: cpl121 Date: Tue, 12 Nov 2024 13:49:44 +0100 Subject: [PATCH 25/87] fix(wallet-dashboard): move FormInputs to a standalone component --- .../SendToken/views/EnterValuesFormView.tsx | 246 ++++++++++-------- 1 file changed, 135 insertions(+), 111 deletions(-) diff --git a/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx b/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx index 5d520e200ca..de6c598fd17 100644 --- a/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { CoinBalance, CoinStruct } from '@iota/iota-sdk/client'; +import { CoinBalance, CoinMetadata, CoinStruct } from '@iota/iota-sdk/client'; import { FormDataValues, INITIAL_VALUES } from '../SendTokenDialog'; import { AddressInput, @@ -28,8 +28,9 @@ import { } from '@iota/apps-ui-kit'; import { useIotaClientQuery } from '@iota/dapp-kit'; import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; -import { Field, FieldInputProps, Form, Formik } from 'formik'; +import { Field, FieldInputProps, Form, Formik, FormikProps } from 'formik'; import { Exclamation } from '@iota/ui-icons'; +import { UseQueryResult } from '@tanstack/react-query'; interface EnterValuesFormProps { coin: CoinBalance; @@ -40,6 +41,18 @@ interface EnterValuesFormProps { onNext: () => void; } +interface FormInputsProps extends FormikProps { + coinType: string; + coinDecimals: number; + coinBalance: bigint; + iotaBalance: bigint; + formattedTokenBalance: string; + symbol: string; + activeAddress: string; + coins: CoinStruct[]; + queryResult: UseQueryResult; +} + function totalBalance(coins: CoinStruct[]): bigint { return coins.reduce((partialSum, c) => partialSum + getBalanceFromCoinStruct(c), BigInt(0)); } @@ -47,6 +60,112 @@ function getBalanceFromCoinStruct(coin: CoinStruct): bigint { return BigInt(coin.balance); } +function FormInputs({ + isValid, + isSubmitting, + setFieldValue, + values, + submitForm, + touched, + errors, + handleBlur, + coinType, + coinDecimals, + coinBalance, + iotaBalance, + formattedTokenBalance, + symbol, + activeAddress, + coins, + queryResult, +}: FormInputsProps): React.JSX.Element { + const newPayIotaAll = + parseAmount(values.amount, coinDecimals) === coinBalance && coinType === IOTA_TYPE_ARG; + if (values.isPayAllIota !== newPayIotaAll) { + setFieldValue('isPayAllIota', newPayIotaAll); + } + + const hasEnoughBalance = + values.isPayAllIota || + iotaBalance > + parseAmount(values.gasBudgetEst, coinDecimals) + + parseAmount(coinType === IOTA_TYPE_ARG ? values.amount : '0', coinDecimals); + + async function onMaxTokenButtonClick() { + await setFieldValue('amount', formattedTokenBalance); + } + + function handleOnChangeAmountInput(value: string, symbol: string) { + const valueWithoutSuffix = value.replace(symbol, ''); + setFieldValue('amount', valueWithoutSuffix); + } + + const isMaxActionDisabled = + parseAmount(values?.amount, coinDecimals) === coinBalance || + queryResult.isPending || + !coinBalance; + + return ( +
+
+
+ {!hasEnoughBalance && ( + } + /> + )} + + + {({ field }: { field: FieldInputProps }) => { + return ( + handleOnChangeAmountInput(value, symbol)} + onBlur={handleBlur} + errorMessage={ + touched.amount && errors.amount ? errors.amount : undefined + } + /> + ); + }} + + + +
+
+ +
+
+
+ ); +} + function EnterValuesFormView({ coin, activeAddress, @@ -152,115 +271,20 @@ function EnterValuesFormView({ validateOnBlur={false} onSubmit={handleFormSubmit} > - {({ - isValid, - isSubmitting, - setFieldValue, - values, - submitForm, - touched, - errors, - handleBlur, - }) => { - const newPayIotaAll = - parseAmount(values.amount, coinDecimals) === coinBalance && - coin.coinType === IOTA_TYPE_ARG; - if (values.isPayAllIota !== newPayIotaAll) { - setFieldValue('isPayAllIota', newPayIotaAll); - } - - const hasEnoughBalance = - values.isPayAllIota || - iotaBalance > - parseAmount(values.gasBudgetEst, coinDecimals) + - parseAmount( - coin.coinType === IOTA_TYPE_ARG ? values.amount : '0', - coinDecimals, - ); - - async function onMaxTokenButtonClick() { - await setFieldValue('amount', formattedTokenBalance); - } - - function handleOnChangeAmountInput(value: string, symbol: string) { - const valueWithoutSuffix = value.replace(symbol, ''); - setFieldValue('amount', valueWithoutSuffix); - } - - const isMaxActionDisabled = - parseAmount(values?.amount, coinDecimals) === coinBalance || - queryResult.isPending || - !coinBalance; - - return ( -
-
-
- {!hasEnoughBalance && ( - } - /> - )} - - - {({ field }: { field: FieldInputProps }) => { - return ( - - handleOnChangeAmountInput(value, symbol) - } - onBlur={handleBlur} - errorMessage={ - touched.amount && errors.amount - ? errors.amount - : undefined - } - /> - ); - }} - - - -
-
- -
-
-
- ); - }} + {(props: FormikProps) => ( + + )} ); From 3b374253ffb5caaa127edf129299114045eb80a8 Mon Sep 17 00:00:00 2001 From: cpl121 Date: Tue, 12 Nov 2024 13:50:13 +0100 Subject: [PATCH 26/87] fix(wallet-dashboard): improve AddressInputs props --- apps/core/package.json | 1 + .../src/components/Inputs/AddressInput.tsx | 26 +++++++------------ pnpm-lock.yaml | 3 +++ 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/apps/core/package.json b/apps/core/package.json index d91abe24562..0a8cd7acfeb 100644 --- a/apps/core/package.json +++ b/apps/core/package.json @@ -35,6 +35,7 @@ "@tanstack/react-query": "^5.50.1", "bignumber.js": "^9.1.1", "clsx": "^2.1.1", + "formik": "^2.4.2", "qrcode.react": "^4.0.1", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/apps/core/src/components/Inputs/AddressInput.tsx b/apps/core/src/components/Inputs/AddressInput.tsx index 5656f97cdfe..f3835787832 100644 --- a/apps/core/src/components/Inputs/AddressInput.tsx +++ b/apps/core/src/components/Inputs/AddressInput.tsx @@ -5,20 +5,12 @@ import { Input, InputType } from '@iota/apps-ui-kit'; import { Close } from '@iota/ui-icons'; import { useIotaAddressValidation } from '../../hooks'; -import React, { useCallback } from 'react'; +import React, { ComponentProps, useCallback } from 'react'; +import type { Field, FieldInputProps } from 'formik'; export interface AddressInputProps { - field: { - name: string; - value: string; - onChange: (e: React.ChangeEvent) => void; - onBlur: (e: React.FocusEvent) => void; - }; - form: { - setFieldValue: (field: string, value: string, shouldValidate?: boolean) => void; - errors: Record; - touched: Record; - }; + field: FieldInputProps; + form: ComponentProps; disabled?: boolean; placeholder?: string; label?: string; @@ -26,7 +18,7 @@ export interface AddressInputProps { export function AddressInput({ field, - form: formContext, + form, disabled, placeholder = '0x...', label = 'Enter Recipient Address', @@ -39,16 +31,16 @@ export function AddressInput({ (e: React.ChangeEvent) => { const address = e.currentTarget.value; const validatedValue = iotaAddressValidation.cast(address); - formContext.setFieldValue(field.name, validatedValue, true); + form.setFieldValue(field.name, validatedValue, true); }, - [formContext, field.name, iotaAddressValidation], + [form, field.name, iotaAddressValidation], ); const clearAddress = () => { - formContext.setFieldValue(field.name, ''); + form.setFieldValue(field.name, ''); }; - const errorMessage = formContext.touched[field.name] && formContext.errors[field.name]; + const errorMessage = form.touched[field.name] && form.errors[field.name]; return ( Date: Tue, 12 Nov 2024 13:51:37 +0100 Subject: [PATCH 27/87] fix(wallet-dashboard): linter --- .../components/Dialogs/SendToken/SendTokenDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx b/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx index ee325da2220..139b9d0cbe1 100644 --- a/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { EnterValuesFormView, ReviewValuesFormView } from './views'; import { CoinBalance } from '@iota/iota-sdk/client'; import { useSendCoinTransaction, useNotifications } from '@/hooks'; From df00b07903d1903a2026fabe1aaaea82a13b1755 Mon Sep 17 00:00:00 2001 From: cpl121 Date: Tue, 12 Nov 2024 13:52:14 +0100 Subject: [PATCH 28/87] fix(wallet-dashboard): format core --- apps/core/src/components/icon/ImageIcon.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/core/src/components/icon/ImageIcon.tsx b/apps/core/src/components/icon/ImageIcon.tsx index d8555c9aa52..6b58dbf715a 100644 --- a/apps/core/src/components/icon/ImageIcon.tsx +++ b/apps/core/src/components/icon/ImageIcon.tsx @@ -26,11 +26,7 @@ interface FallBackAvatarProps { size?: ImageIconSize; } -function FallBackAvatar({ - str, - rounded, - size = ImageIconSize.Large, -}: FallBackAvatarProps) { +function FallBackAvatar({ str, rounded, size = ImageIconSize.Large }: FallBackAvatarProps) { function generateTextSize(size: ImageIconSize) { switch (size) { case ImageIconSize.Small: From ed3b9b5c8421c321cb584534143a5a1b554aa881 Mon Sep 17 00:00:00 2001 From: Branko Bosnic Date: Tue, 12 Nov 2024 14:55:57 +0100 Subject: [PATCH 29/87] feat: add staking overview --- apps/core/src/components/index.ts | 1 + .../src/components/staking/StakingCard.tsx} | 67 ++++----- .../src/components/staking/StakingStats.tsx | 26 ++++ apps/core/src/components/staking/index.ts | 5 + apps/core/src/constants/staking.constants.ts | 4 + .../src/utils/determineCountDownText.ts} | 0 apps/core/src/utils/index.ts | 1 + .../app/(protected)/staking/page.tsx | 131 ++++++++++++------ .../components/Cards/StakeCard.tsx | 27 ---- .../components/Cards/index.ts | 1 - apps/wallet/src/shared/constants.ts | 6 - .../components/receipt-card/StakeTxnInfo.tsx | 9 +- .../DelegationDetailCard.tsx | 2 +- .../src/ui/app/staking/stake/StakingCard.tsx | 2 +- .../app/staking/validators/ValidatorsCard.tsx | 28 +++- 15 files changed, 182 insertions(+), 128 deletions(-) rename apps/{wallet/src/ui/app/staking/home/StakedCard.tsx => core/src/components/staking/StakingCard.tsx} (70%) create mode 100644 apps/core/src/components/staking/StakingStats.tsx create mode 100644 apps/core/src/components/staking/index.ts rename apps/{wallet/src/ui/app/shared/countdown-timer/index.tsx => core/src/utils/determineCountDownText.ts} (100%) delete mode 100644 apps/wallet-dashboard/components/Cards/StakeCard.tsx diff --git a/apps/core/src/components/index.ts b/apps/core/src/components/index.ts index 0a8093eeadb..9b9bb2ef619 100644 --- a/apps/core/src/components/index.ts +++ b/apps/core/src/components/index.ts @@ -7,3 +7,4 @@ export * from './coin'; export * from './icon'; export * from './Inputs'; export * from './QR'; +export * from './staking'; diff --git a/apps/wallet/src/ui/app/staking/home/StakedCard.tsx b/apps/core/src/components/staking/StakingCard.tsx similarity index 70% rename from apps/wallet/src/ui/app/staking/home/StakedCard.tsx rename to apps/core/src/components/staking/StakingCard.tsx index b945fbe025b..bf3c4dc3beb 100644 --- a/apps/wallet/src/ui/app/staking/home/StakedCard.tsx +++ b/apps/core/src/components/staking/StakingCard.tsx @@ -2,22 +2,14 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE } from '_src/shared/constants'; -import { determineCountDownText } from '_src/ui/app/shared/countdown-timer'; -import { - type ExtendedDelegatedStake, - TimeUnit, - useFormatCoin, - useGetTimeBeforeEpochNumber, - useTimeAgo, - ImageIcon, -} from '@iota/core'; import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; import { Card, CardImage, CardType, CardBody, CardAction, CardActionType } from '@iota/apps-ui-kit'; import { useMemo } from 'react'; -import { Link } from 'react-router-dom'; - import { useIotaClientQuery } from '@iota/dapp-kit'; +import { ImageIcon } from '../icon'; +import { determineCountDownText, ExtendedDelegatedStake } from '../../utils'; +import { TimeUnit, useFormatCoin, useGetTimeBeforeEpochNumber, useTimeAgo } from '../../hooks'; +import { NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE } from '../../constants'; export enum StakeState { WarmUp = 'WARM_UP', @@ -35,21 +27,22 @@ const STATUS_COPY: { [key in StakeState]: string } = { [StakeState.InActive]: 'Inactive', }; -interface StakeCardProps { +interface StakingCardProps { extendedStake: ExtendedDelegatedStake; currentEpoch: number; inactiveValidator?: boolean; + onClick?: () => void; } // For delegationsRequestEpoch n through n + 2, show Start Earning // Show epoch number or date/time for n + 3 epochs -export function StakeCard({ +export function StakingCard({ extendedStake, currentEpoch, inactiveValidator = false, -}: StakeCardProps) { - const { stakedIotaId, principal, stakeRequestEpoch, estimatedReward, validatorAddress } = - extendedStake; + onClick, +}: StakingCardProps) { + const { principal, stakeRequestEpoch, estimatedReward, validatorAddress } = extendedStake; // TODO: Once two step withdraw is available, add cool down and withdraw now logic // For cool down epoch, show Available to withdraw add rewards to principal @@ -115,32 +108,20 @@ export function StakeCard({ }; return ( - - - - - - - + + - - + + + + ); } diff --git a/apps/core/src/components/staking/StakingStats.tsx b/apps/core/src/components/staking/StakingStats.tsx new file mode 100644 index 00000000000..49f41d4a6d8 --- /dev/null +++ b/apps/core/src/components/staking/StakingStats.tsx @@ -0,0 +1,26 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { useFormatCoin } from '../../hooks'; +import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; + +interface StakingStatsProps { + title: string; + balance: bigint | number | string; +} + +export function StakingStats({ balance, title }: StakingStatsProps) { + const [formatted, symbol] = useFormatCoin(balance, IOTA_TYPE_ARG); + + return ( +
+
{title}
+ +
+
{formatted}
+
{symbol}
+
+
+ ); +} diff --git a/apps/core/src/components/staking/index.ts b/apps/core/src/components/staking/index.ts new file mode 100644 index 00000000000..80c230a22a3 --- /dev/null +++ b/apps/core/src/components/staking/index.ts @@ -0,0 +1,5 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +export * from './StakingStats'; +export * from './StakingCard'; diff --git a/apps/core/src/constants/staking.constants.ts b/apps/core/src/constants/staking.constants.ts index e79939eee60..c93842c2408 100644 --- a/apps/core/src/constants/staking.constants.ts +++ b/apps/core/src/constants/staking.constants.ts @@ -6,3 +6,7 @@ export const UNSTAKING_REQUEST_EVENT = '0x3::validator::UnstakingRequestEvent'; export const DELEGATED_STAKES_QUERY_STALE_TIME = 10_000; export const DELEGATED_STAKES_QUERY_REFETCH_INTERVAL = 30_000; + +export const NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE = 2; +export const NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_STARTS = 1; +export const MIN_NUMBER_IOTA_TO_STAKE = 1; diff --git a/apps/wallet/src/ui/app/shared/countdown-timer/index.tsx b/apps/core/src/utils/determineCountDownText.ts similarity index 100% rename from apps/wallet/src/ui/app/shared/countdown-timer/index.tsx rename to apps/core/src/utils/determineCountDownText.ts diff --git a/apps/core/src/utils/index.ts b/apps/core/src/utils/index.ts index fb8bee8590c..59974630935 100644 --- a/apps/core/src/utils/index.ts +++ b/apps/core/src/utils/index.ts @@ -21,6 +21,7 @@ export * from './getDelegationDataByStakeId'; export * from './api-env'; export * from './getExplorerPaths'; export * from './getExplorerLink'; +export * from './determineCountDownText'; export * from './stake'; export * from './transaction'; diff --git a/apps/wallet-dashboard/app/(protected)/staking/page.tsx b/apps/wallet-dashboard/app/(protected)/staking/page.tsx index 771cc4f79a5..7b70842074c 100644 --- a/apps/wallet-dashboard/app/(protected)/staking/page.tsx +++ b/apps/wallet-dashboard/app/(protected)/staking/page.tsx @@ -3,26 +3,32 @@ 'use client'; -import { AmountBox, Box, StakeCard, StakeDialog, Button } from '@/components'; +import { StakeDialog } from '@/components'; import { StakeDetailsDialog } from '@/components/Dialogs'; +import { StartStaking } from '@/components/staking-overview/StartStaking'; +import { Button, ButtonSize, ButtonType, InfoBox, InfoBoxStyle, InfoBoxType, Panel, Title, TitleSize } from '@iota/apps-ui-kit'; import { ExtendedDelegatedStake, formatDelegatedStake, - useFormatCoin, useGetDelegatedStake, useTotalDelegatedRewards, useTotalDelegatedStake, DELEGATED_STAKES_QUERY_REFETCH_INTERVAL, DELEGATED_STAKES_QUERY_STALE_TIME, + StakingCard, + StakingStats, } from '@iota/core'; -import { useCurrentAccount } from '@iota/dapp-kit'; -import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; -import { useState } from 'react'; +import { useCurrentAccount, useIotaClientQuery } from '@iota/dapp-kit'; +import { IotaSystemStateSummary } from '@iota/iota-sdk/client'; +import { Info } from '@iota/ui-icons'; +import { useMemo, useState } from 'react'; function StakingDashboardPage(): JSX.Element { const account = useCurrentAccount(); const [isDialogStakeOpen, setIsDialogStakeOpen] = useState(false); const [selectedStake, setSelectedStake] = useState(null); + const { data: system } = useIotaClientQuery('getLatestIotaSystemState'); + const activeValidators = (system as IotaSystemStateSummary)?.activeValidators; const { data: delegatedStakeData } = useGetDelegatedStake({ address: account?.address || '', @@ -33,13 +39,24 @@ function StakingDashboardPage(): JSX.Element { const extendedStakes = delegatedStakeData ? formatDelegatedStake(delegatedStakeData) : []; const totalDelegatedStake = useTotalDelegatedStake(extendedStakes); const totalDelegatedRewards = useTotalDelegatedRewards(extendedStakes); - const [formattedDelegatedStake, stakeSymbol, stakeResult] = useFormatCoin( - totalDelegatedStake, - IOTA_TYPE_ARG, - ); - const [formattedDelegatedRewards, rewardsSymbol, rewardsResult] = useFormatCoin( - totalDelegatedRewards, - IOTA_TYPE_ARG, + + const delegations = useMemo(() => { + return delegatedStakeData?.flatMap((delegation) => { + return delegation.stakes.map((d) => ({ + ...d, + // flag any inactive validator for the stakeIota object + // if the stakingPoolId is not found in the activeValidators list flag as inactive + inactiveValidator: !activeValidators?.find( + ({ stakingPoolId }) => stakingPoolId === delegation.stakingPool, + ), + validatorAddress: delegation.validatorAddress, + })); + }); + }, [activeValidators, delegatedStakeData]); + + // Check if there are any inactive validators + const hasInactiveValidatorDelegation = delegations?.some( + ({ inactiveValidator }) => inactiveValidator, ); const viewStakeDetails = (extendedStake: ExtendedDelegatedStake) => { @@ -50,36 +67,68 @@ function StakingDashboardPage(): JSX.Element { setIsDialogStakeOpen(true); } - return ( - <> -
- - - -
-

List of stakes

- {extendedStakes?.map((extendedStake) => ( - 0 ? ( + + handleNewStake()} + size={ButtonSize.Small} + type={ButtonType.Primary} + text="Stake" + /> + } + /> + <div className="flex h-full w-full flex-col flex-nowrap gap-md p-md--rs"> + <div className="flex gap-xs"> + <StakingStats title="Your stake" balance={totalDelegatedStake} /> + <StakingStats title="Earned" balance={totalDelegatedRewards} /> + </div> + <Title title="In progress" size={TitleSize.Small} /> + <div className="flex max-h-[420px] w-full flex-1 flex-col items-start overflow-auto"> + {hasInactiveValidatorDelegation ? ( + <div className="mb-3"> + <InfoBox + type={InfoBoxType.Default} + title="Earn with active validators" + supportingText="Unstake IOTA from the inactive validators and stake on an active +validator to start earning rewards again." + icon={<Info />} + style={InfoBoxStyle.Elevated} /> - ))} + </div> + ) : null} + <div className="w-full gap-2"> + {system && + delegations + ?.filter(({ inactiveValidator }) => inactiveValidator) + .map((delegation) => ( + <StakingCard + extendedStake={delegation} + currentEpoch={Number(system.epoch)} + key={delegation.stakedIotaId} + inactiveValidator + onClick={() => viewStakeDetails(delegation)} + /> + ))} + </div> + <div className="w-full gap-2"> + {system && + delegations + ?.filter(({ inactiveValidator }) => !inactiveValidator) + .map((delegation) => ( + <StakingCard + extendedStake={delegation} + currentEpoch={Number(system.epoch)} + key={delegation.stakedIotaId} + onClick={() => viewStakeDetails(delegation)} + /> + ))} </div> - </Box> - <Button onClick={handleNewStake}>New Stake</Button> + </div> </div> - <StakeDialog isOpen={isDialogStakeOpen} setOpen={setIsDialogStakeOpen} />; + <StakeDialog isOpen={isDialogStakeOpen} setOpen={setIsDialogStakeOpen} /> {selectedStake && ( <StakeDetailsDialog extendedStake={selectedStake} @@ -87,7 +136,9 @@ function StakingDashboardPage(): JSX.Element { showActiveStatus /> )} - </> + </Panel> + ) : ( + <StartStaking /> ); } diff --git a/apps/wallet-dashboard/components/Cards/StakeCard.tsx b/apps/wallet-dashboard/components/Cards/StakeCard.tsx deleted file mode 100644 index 9e79abd31ad..00000000000 --- a/apps/wallet-dashboard/components/Cards/StakeCard.tsx +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import React from 'react'; -import { Box, Button } from '@/components/index'; -import { ExtendedDelegatedStake } from '@iota/core'; - -interface StakeCardProps { - extendedStake: ExtendedDelegatedStake; - onDetailsClick: (extendedStake: ExtendedDelegatedStake) => void; -} - -function StakeCard({ extendedStake, onDetailsClick }: StakeCardProps): JSX.Element { - return ( - <Box> - <div>Validator: {extendedStake.validatorAddress}</div> - <div>Stake: {extendedStake.principal}</div> - {extendedStake.status === 'Active' && ( - <p>Estimated reward: {extendedStake.estimatedReward}</p> - )} - <div>Status: {extendedStake.status}</div> - <Button onClick={() => onDetailsClick(extendedStake)}>Details</Button> - </Box> - ); -} - -export default StakeCard; diff --git a/apps/wallet-dashboard/components/Cards/index.ts b/apps/wallet-dashboard/components/Cards/index.ts index da687047e80..a6bf6b83bcd 100644 --- a/apps/wallet-dashboard/components/Cards/index.ts +++ b/apps/wallet-dashboard/components/Cards/index.ts @@ -1,5 +1,4 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -export { default as StakeCard } from './StakeCard'; export * from './VisualAssetDetailsCard'; diff --git a/apps/wallet/src/shared/constants.ts b/apps/wallet/src/shared/constants.ts index dd930448773..6f3b1d482ab 100644 --- a/apps/wallet/src/shared/constants.ts +++ b/apps/wallet/src/shared/constants.ts @@ -6,10 +6,4 @@ export const ToS_LINK = 'https://www.iota.org/terms-of-use'; export const PRIVACY_POLICY_LINK = 'https://www.iota.org/privacy-policy'; export const FAQ_LINK = 'https://wiki.iota.org/'; -// number of epochs before earning -// Staking Rewards Redeemable -export const NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE = 2; -export const NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_STARTS = 1; -export const MIN_NUMBER_IOTA_TO_STAKE = 1; - export const DEFAULT_APP_NAME = 'IOTA Wallet'; diff --git a/apps/wallet/src/ui/app/components/receipt-card/StakeTxnInfo.tsx b/apps/wallet/src/ui/app/components/receipt-card/StakeTxnInfo.tsx index 40064a9fcfb..41d9f994c6c 100644 --- a/apps/wallet/src/ui/app/components/receipt-card/StakeTxnInfo.tsx +++ b/apps/wallet/src/ui/app/components/receipt-card/StakeTxnInfo.tsx @@ -4,10 +4,13 @@ import { Divider, KeyValueInfo, Panel, TooltipPosition } from '@iota/apps-ui-kit'; import { - NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE, + useGetTimeBeforeEpochNumber, + useTimeAgo, + TimeUnit, + type GasSummaryType, NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_STARTS, -} from '_src/shared/constants'; -import { useGetTimeBeforeEpochNumber, useTimeAgo, TimeUnit, type GasSummaryType } from '@iota/core'; + NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE, +} from '@iota/core'; import { GasSummary } from '../../shared/transaction-summary/cards/GasSummary'; interface StakeTxnInfoProps { diff --git a/apps/wallet/src/ui/app/staking/delegation-detail/DelegationDetailCard.tsx b/apps/wallet/src/ui/app/staking/delegation-detail/DelegationDetailCard.tsx index 058c322baab..0ace5ac50de 100644 --- a/apps/wallet/src/ui/app/staking/delegation-detail/DelegationDetailCard.tsx +++ b/apps/wallet/src/ui/app/staking/delegation-detail/DelegationDetailCard.tsx @@ -4,7 +4,6 @@ import { useAppSelector } from '_hooks'; import { ampli } from '_src/shared/analytics/ampli'; -import { MIN_NUMBER_IOTA_TO_STAKE } from '_src/shared/constants'; import { useBalance, useCoinMetadata, @@ -14,6 +13,7 @@ import { DELEGATED_STAKES_QUERY_STALE_TIME, useFormatCoin, formatPercentageDisplay, + MIN_NUMBER_IOTA_TO_STAKE, } from '@iota/core'; import { useIotaClientQuery } from '@iota/dapp-kit'; import { Network, type StakeObject } from '@iota/iota-sdk/client'; diff --git a/apps/wallet/src/ui/app/staking/stake/StakingCard.tsx b/apps/wallet/src/ui/app/staking/stake/StakingCard.tsx index 1d045c11f13..ee8e7779a02 100644 --- a/apps/wallet/src/ui/app/staking/stake/StakingCard.tsx +++ b/apps/wallet/src/ui/app/staking/stake/StakingCard.tsx @@ -5,7 +5,6 @@ import { Loading } from '_components'; import { Coin } from '_redux/slices/iota-objects/Coin'; import { ampli } from '_src/shared/analytics/ampli'; -import { MIN_NUMBER_IOTA_TO_STAKE } from '_src/shared/constants'; import { createStakeTransaction, createUnstakeTransaction, @@ -16,6 +15,7 @@ import { DELEGATED_STAKES_QUERY_REFETCH_INTERVAL, DELEGATED_STAKES_QUERY_STALE_TIME, getStakeIotaByIotaId, + MIN_NUMBER_IOTA_TO_STAKE, } from '@iota/core'; import { useIotaClientQuery } from '@iota/dapp-kit'; import type { StakeObject } from '@iota/iota-sdk/client'; diff --git a/apps/wallet/src/ui/app/staking/validators/ValidatorsCard.tsx b/apps/wallet/src/ui/app/staking/validators/ValidatorsCard.tsx index d86c202cb97..da0b63a059e 100644 --- a/apps/wallet/src/ui/app/staking/validators/ValidatorsCard.tsx +++ b/apps/wallet/src/ui/app/staking/validators/ValidatorsCard.tsx @@ -10,12 +10,12 @@ import { useTotalDelegatedStake, DELEGATED_STAKES_QUERY_REFETCH_INTERVAL, DELEGATED_STAKES_QUERY_STALE_TIME, + StakingStats, + StakingCard, } from '@iota/core'; import { useIotaClientQuery } from '@iota/dapp-kit'; import { useMemo } from 'react'; import { useActiveAddress } from '../../hooks/useActiveAddress'; -import { StakeCard } from '../home/StakedCard'; -import { StatsDetail } from '_app/staking/validators/StatsDetail'; import { Title, TitleSize, @@ -106,8 +106,8 @@ export function ValidatorsCard() { return ( <div className="flex h-full w-full flex-col flex-nowrap"> <div className="flex gap-xs py-md"> - <StatsDetail title="Your stake" balance={totalDelegatedStake} /> - <StatsDetail title="Earned" balance={totalDelegatedRewards} /> + <StakingStats title="Your stake" balance={totalDelegatedStake} /> + <StakingStats title="Earned" balance={totalDelegatedRewards} /> </div> <Title title="In progress" size={TitleSize.Small} /> <div className="flex max-h-[420px] w-full flex-1 flex-col items-start overflow-auto"> @@ -128,11 +128,19 @@ validator to start earning rewards again." delegations ?.filter(({ inactiveValidator }) => inactiveValidator) .map((delegation) => ( - <StakeCard + <StakingCard extendedStake={delegation} currentEpoch={Number(system.epoch)} key={delegation.stakedIotaId} inactiveValidator + onClick={() => + navigate( + `/stake/delegation-detail?${new URLSearchParams({ + validator: delegation.validatorAddress, + staked: delegation.stakedIotaId, + }).toString()}`, + ) + } /> ))} </div> @@ -142,10 +150,18 @@ validator to start earning rewards again." delegations ?.filter(({ inactiveValidator }) => !inactiveValidator) .map((delegation) => ( - <StakeCard + <StakingCard extendedStake={delegation} currentEpoch={Number(system.epoch)} key={delegation.stakedIotaId} + onClick={() => + navigate( + `/stake/delegation-detail?${new URLSearchParams({ + validator: delegation.validatorAddress, + staked: delegation.stakedIotaId, + }).toString()}`, + ) + } /> ))} </div> From 290b7e9cbeeb191bbdb0b73b4d50cc338eeb36aa Mon Sep 17 00:00:00 2001 From: Branko Bosnic <brankobosnic1@gmail.com> Date: Tue, 12 Nov 2024 15:04:15 +0100 Subject: [PATCH 30/87] fix: prettier --- .../app/(protected)/staking/page.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/wallet-dashboard/app/(protected)/staking/page.tsx b/apps/wallet-dashboard/app/(protected)/staking/page.tsx index 7b70842074c..aa39aa82111 100644 --- a/apps/wallet-dashboard/app/(protected)/staking/page.tsx +++ b/apps/wallet-dashboard/app/(protected)/staking/page.tsx @@ -6,7 +6,17 @@ import { StakeDialog } from '@/components'; import { StakeDetailsDialog } from '@/components/Dialogs'; import { StartStaking } from '@/components/staking-overview/StartStaking'; -import { Button, ButtonSize, ButtonType, InfoBox, InfoBoxStyle, InfoBoxType, Panel, Title, TitleSize } from '@iota/apps-ui-kit'; +import { + Button, + ButtonSize, + ButtonType, + InfoBox, + InfoBoxStyle, + InfoBoxType, + Panel, + Title, + TitleSize, +} from '@iota/apps-ui-kit'; import { ExtendedDelegatedStake, formatDelegatedStake, From 13aaf0e9f278e6ed63f3f150b57db24145e249e4 Mon Sep 17 00:00:00 2001 From: cpl121 <cpeon@boxfish.studio> Date: Tue, 12 Nov 2024 16:58:24 +0100 Subject: [PATCH 31/87] fix(wallet-dashboard): clean debris --- apps/core/src/components/coin/CoinIcon.tsx | 35 +++------------------- 1 file changed, 4 insertions(+), 31 deletions(-) diff --git a/apps/core/src/components/coin/CoinIcon.tsx b/apps/core/src/components/coin/CoinIcon.tsx index 7d228bc89c5..77dd5e2a43c 100644 --- a/apps/core/src/components/coin/CoinIcon.tsx +++ b/apps/core/src/components/coin/CoinIcon.tsx @@ -33,41 +33,14 @@ export interface CoinIconProps { coinType: string; size?: ImageIconSize; rounded?: boolean; - hasCoinWrapper?: boolean; } -export function CoinIcon({ - coinType, - size = ImageIconSize.Full, - rounded, - hasCoinWrapper, -}: CoinIconProps) { - const Component = hasCoinWrapper ? CoinIconWrapper : React.Fragment; +export function CoinIcon({ coinType, size = ImageIconSize.Full, rounded }: CoinIconProps) { return coinType === IOTA_TYPE_ARG ? ( - <Component {...(hasCoinWrapper ? { hasBorder: true, size: ImageIconSize.Large } : {})}> - <div className={cx(size, 'text-neutral-10')}> - <IotaLogoMark className="h-full w-full" /> - </div> - </Component> + <div className={cx(size, 'text-neutral-10')}> + <IotaLogoMark className="h-full w-full" /> + </div> ) : ( <NonIotaCoin rounded={rounded} size={size} coinType={coinType} /> ); } - -type CoinIconWrapperProps = React.PropsWithChildren<Pick<CoinIconProps, 'size'>> & { - hasBorder?: boolean; -}; - -export function CoinIconWrapper({ children, size, hasBorder }: CoinIconWrapperProps) { - return ( - <div - className={cx( - size, - hasBorder && 'border border-shader-neutral-light-8', - 'flex items-center justify-center rounded-full bg-neutral-100', - )} - > - {children} - </div> - ); -} From 288483f30fe429415e3cad7b4050ff8dd6247d14 Mon Sep 17 00:00:00 2001 From: JCNoguera <88061365+VmMad@users.noreply.github.com> Date: Wed, 13 Nov 2024 10:16:36 +0100 Subject: [PATCH 32/87] fix: add ExplorerLink component and add missing dialog styles --- .../components/organisms/dialog/Dialog.tsx | 4 +-- apps/wallet-dashboard/app/globals.css | 8 ++++++ .../Dialogs/SendToken/SendTokenDialog.tsx | 2 +- .../SendToken/views/ReviewValuesFormView.tsx | 17 ++++++++++-- .../components/ExplorerLink.tsx | 27 +++++++++++++++++++ apps/wallet-dashboard/components/index.ts | 1 + 6 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 apps/wallet-dashboard/components/ExplorerLink.tsx diff --git a/apps/ui-kit/src/lib/components/organisms/dialog/Dialog.tsx b/apps/ui-kit/src/lib/components/organisms/dialog/Dialog.tsx index ddd77e87df7..53971681633 100644 --- a/apps/ui-kit/src/lib/components/organisms/dialog/Dialog.tsx +++ b/apps/ui-kit/src/lib/components/organisms/dialog/Dialog.tsx @@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef< >(({ showCloseIcon, ...props }, ref) => ( <RadixDialog.Overlay ref={ref} - className="absolute inset-0 z-[99998] bg-shader-neutral-light-48 backdrop-blur-md" + className="fixed inset-0 z-[99998] bg-shader-neutral-light-48 backdrop-blur-md" {...props} > <DialogClose className={cx('fixed right-3 top-3', { hidden: !showCloseIcon })}> @@ -70,7 +70,7 @@ const DialogContent = React.forwardRef< <RadixDialog.Content ref={ref} className={cx( - 'absolute z-[99999] flex flex-col justify-center overflow-hidden bg-primary-100 dark:bg-neutral-6 md:w-96', + 'fixed z-[99999] flex flex-col justify-center overflow-hidden bg-primary-100 dark:bg-neutral-6 md:w-96', positionClass, )} {...props} diff --git a/apps/wallet-dashboard/app/globals.css b/apps/wallet-dashboard/app/globals.css index 37de49ae479..f4151e588f5 100644 --- a/apps/wallet-dashboard/app/globals.css +++ b/apps/wallet-dashboard/app/globals.css @@ -85,4 +85,12 @@ body { .grid-template-visual-assets { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); } + + a { + @apply text-primary-30 dark:text-primary-80; + @apply transition-colors; + &:hover { + @apply text-opacity-80; + } + } } diff --git a/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx b/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx index 34ca90f7a9d..18ab3b857f3 100644 --- a/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx @@ -72,7 +72,7 @@ function SendTokenDialog({ } else { signAndExecuteTransaction( { - transaction: '', + transaction: sendCoinData.transaction, }, { onSuccess: () => { diff --git a/apps/wallet-dashboard/components/Dialogs/SendToken/views/ReviewValuesFormView.tsx b/apps/wallet-dashboard/components/Dialogs/SendToken/views/ReviewValuesFormView.tsx index e53170c8c08..fabf82dbc30 100644 --- a/apps/wallet-dashboard/components/Dialogs/SendToken/views/ReviewValuesFormView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/views/ReviewValuesFormView.tsx @@ -25,8 +25,10 @@ import { parseAmount, useCoinMetadata, useFormatCoin, + ExplorerLinkType, } from '@iota/core'; import { Loader } from '@iota/ui-icons'; +import { ExplorerLink } from '@/components'; interface ReviewValuesFormProps { formData: FormDataValues; @@ -73,14 +75,25 @@ export function ReviewValuesFormView({ <div className="flex flex-col gap-md--rs p-sm--rs"> <KeyValueInfo keyText={'From'} - value={formatAddress(senderAddress)} + value={ + <ExplorerLink + type={ExplorerLinkType.Address} + address={senderAddress} + > + {formatAddress(senderAddress)} + </ExplorerLink> + } fullwidth /> <Divider /> <KeyValueInfo keyText={'To'} - value={formatAddress(to || '')} + value={ + <ExplorerLink type={ExplorerLinkType.Address} address={to}> + {formatAddress(to || '')} + </ExplorerLink> + } fullwidth /> diff --git a/apps/wallet-dashboard/components/ExplorerLink.tsx b/apps/wallet-dashboard/components/ExplorerLink.tsx new file mode 100644 index 00000000000..2b671cf8cc5 --- /dev/null +++ b/apps/wallet-dashboard/components/ExplorerLink.tsx @@ -0,0 +1,27 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { useExplorerLinkGetter } from '@/hooks'; +import { getExplorerLink } from '@iota/core'; +import Link from 'next/link'; + +type GetExplorerLinkArgs = Parameters<typeof getExplorerLink>[0]; + +type ExplorerLinkProps = GetExplorerLinkArgs & { + isExternal?: boolean; +}; + +export function ExplorerLink({ + children, + isExternal, + ...getLinkProps +}: React.PropsWithChildren<ExplorerLinkProps>): React.JSX.Element { + const getExplorerLink = useExplorerLinkGetter(); + const href = getExplorerLink(getLinkProps) ?? '#'; + + return ( + <Link href={href} target="_blank" rel="noopener noreferrer"> + {children} + </Link> + ); +} diff --git a/apps/wallet-dashboard/components/index.ts b/apps/wallet-dashboard/components/index.ts index a6c298a4941..e545e4f9199 100644 --- a/apps/wallet-dashboard/components/index.ts +++ b/apps/wallet-dashboard/components/index.ts @@ -19,6 +19,7 @@ export * from './Cards'; export * from './Buttons'; export * from './transactions'; export * from './staking-overview'; +export * from './ExplorerLink'; export * from './Dialogs'; export * from './ImageIcon'; export * from './ValidatorStakingData'; From f1699c0047c8bfcaea21c1b8acd96923aa202847 Mon Sep 17 00:00:00 2001 From: JCNoguera <88061365+VmMad@users.noreply.github.com> Date: Wed, 13 Nov 2024 12:37:01 +0100 Subject: [PATCH 33/87] fix: use correct values for keyvalue --- apps/core/src/components/Inputs/SendTokenFormInput.tsx | 4 +++- apps/core/src/hooks/useGasBudgetEstimation.ts | 8 +++++++- .../components/Dialogs/SendToken/SendTokenDialog.tsx | 3 +-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/core/src/components/Inputs/SendTokenFormInput.tsx b/apps/core/src/components/Inputs/SendTokenFormInput.tsx index baee61e29a3..8f957245782 100644 --- a/apps/core/src/components/Inputs/SendTokenFormInput.tsx +++ b/apps/core/src/components/Inputs/SendTokenFormInput.tsx @@ -6,6 +6,7 @@ import { CoinStruct } from '@iota/iota-sdk/client'; import { useGasBudgetEstimation } from '../../hooks'; import { FormInput } from '..'; import React, { useEffect } from 'react'; +import { GAS_SYMBOL } from '../../constants'; export interface SendTokenInputProps { coins: CoinStruct[]; @@ -47,6 +48,7 @@ export function SendTokenFormInput({ to: values.to, amount: values.amount, isPayAllIota: values.isPayAllIota, + showGasSymbol: false, }); // gasBudgetEstimation should change when the amount above changes @@ -65,7 +67,7 @@ export function SendTokenFormInput({ decimals allowNegative={false} prefix={values.isPayAllIota ? '~ ' : undefined} - amountCounter={coins ? gasBudgetEstimation : '--'} + amountCounter={coins ? `${gasBudgetEstimation} ${GAS_SYMBOL}` : '--'} value={value} onChange={onChange} onBlur={onBlur} diff --git a/apps/core/src/hooks/useGasBudgetEstimation.ts b/apps/core/src/hooks/useGasBudgetEstimation.ts index 5dbe7d2ff1e..be6f6abba5f 100644 --- a/apps/core/src/hooks/useGasBudgetEstimation.ts +++ b/apps/core/src/hooks/useGasBudgetEstimation.ts @@ -16,8 +16,11 @@ interface UseGasBudgetEstimationOptions { to: string; amount: string; isPayAllIota: boolean; + showGasSymbol?: boolean; } +const FALLBACK_GAS_VALUE = '--'; + export function useGasBudgetEstimation({ coinDecimals, coins, @@ -25,6 +28,7 @@ export function useGasBudgetEstimation({ to, amount, isPayAllIota, + showGasSymbol = true, }: UseGasBudgetEstimationOptions) { const client = useIotaClient(); const { data: gasBudget } = useQuery({ @@ -61,5 +65,7 @@ export function useGasBudgetEstimation({ const [formattedGas] = useFormatCoin(gasBudget, IOTA_TYPE_ARG); - return formattedGas ? formattedGas + ' ' + GAS_SYMBOL : '--'; + return formattedGas + ? `${formattedGas}${showGasSymbol ? ` ${GAS_SYMBOL}` : ''}` + : FALLBACK_GAS_VALUE; } diff --git a/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx b/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx index 087a357fa3b..a39d2104a18 100644 --- a/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx @@ -114,11 +114,10 @@ function SendTokenDialogBody({ {step === FormStep.ReviewValues && ( <ReviewValuesFormView formData={formData} - onBack={onBack} executeTransfer={handleTransfer} senderAddress={activeAddress} - gasBudget={sendCoinData?.gasBudget?.toString() || '--'} isPending={isPending} + coinType={selectedCoin.coinType} /> )} </DialogBody> From 28a9353d27a87be4d246d7e90ed3b02d13ee060e Mon Sep 17 00:00:00 2001 From: cpl121 <cpeon@boxfish.studio> Date: Wed, 13 Nov 2024 14:50:18 +0100 Subject: [PATCH 34/87] fix(wallet-dashboard): bring back the validation field --- apps/core/src/components/Inputs/AddressInput.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/core/src/components/Inputs/AddressInput.tsx b/apps/core/src/components/Inputs/AddressInput.tsx index f3835787832..3b91350ebed 100644 --- a/apps/core/src/components/Inputs/AddressInput.tsx +++ b/apps/core/src/components/Inputs/AddressInput.tsx @@ -30,8 +30,10 @@ export function AddressInput({ const handleOnChange = useCallback( (e: React.ChangeEvent<HTMLInputElement>) => { const address = e.currentTarget.value; - const validatedValue = iotaAddressValidation.cast(address); - form.setFieldValue(field.name, validatedValue, true); + iotaAddressValidation.cast(address); + form.setFieldValue(field.name, iotaAddressValidation.cast(address)).then(() => { + form.validateField(field.name); + }); }, [form, field.name, iotaAddressValidation], ); From 72bfb38607cf6d7b7d7545a9a3005e5dc5899826 Mon Sep 17 00:00:00 2001 From: cpl121 <cpeon@boxfish.studio> Date: Wed, 13 Nov 2024 14:53:46 +0100 Subject: [PATCH 35/87] fix(wallet-dashboard): bad merge removing duplicated image components --- .../wallet-dashboard/components/ImageIcon.tsx | 80 ------------------- .../components/coins/CoinIcon.tsx | 45 ----------- .../components/coins/CoinItem.tsx | 4 +- .../components/coins/index.ts | 1 - apps/wallet-dashboard/components/index.ts | 1 - 5 files changed, 1 insertion(+), 130 deletions(-) delete mode 100644 apps/wallet-dashboard/components/ImageIcon.tsx delete mode 100644 apps/wallet-dashboard/components/coins/CoinIcon.tsx diff --git a/apps/wallet-dashboard/components/ImageIcon.tsx b/apps/wallet-dashboard/components/ImageIcon.tsx deleted file mode 100644 index 06939516971..00000000000 --- a/apps/wallet-dashboard/components/ImageIcon.tsx +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { useState } from 'react'; -import Image from 'next/image'; -import cn from 'clsx'; - -export enum ImageIconSize { - Small = 'w-5 h-5', - Medium = 'w-8 h-8', - Large = 'w-10 h-10', - Full = 'w-full h-full', -} - -interface FallBackAvatarProps { - text: string; - rounded?: boolean; - size?: ImageIconSize; -} -function FallBackAvatar({ text, rounded, size = ImageIconSize.Large }: FallBackAvatarProps) { - const textSize = (() => { - switch (size) { - case ImageIconSize.Small: - return 'text-label-sm'; - case ImageIconSize.Medium: - return 'text-label-md'; - case ImageIconSize.Large: - return 'text-title-md'; - case ImageIconSize.Full: - return 'text-title-lg'; - } - })(); - - return ( - <div - className={cn( - 'flex h-full w-full items-center justify-center bg-neutral-96 bg-gradient-to-r capitalize dark:bg-neutral-20', - { 'rounded-full': rounded }, - textSize, - )} - > - {text.slice(0, 2)} - </div> - ); -} -export interface ImageIconProps { - src: string | null | undefined; - label: string; - fallbackText: string; - alt?: string; - rounded?: boolean; - size?: ImageIconSize; -} - -export function ImageIcon({ - src, - label, - alt = label, - fallbackText, - rounded, - size, -}: ImageIconProps) { - const [error, setError] = useState(false); - return ( - <div role="img" aria-label={label} className={size}> - {error || !src ? ( - <FallBackAvatar rounded={rounded} text={fallbackText} size={size} /> - ) : ( - <Image - src={src} - alt={alt} - className="flex h-full w-full items-center justify-center rounded-full object-cover" - onError={() => setError(true)} - layout="fill" - objectFit="cover" - /> - )} - </div> - ); -} diff --git a/apps/wallet-dashboard/components/coins/CoinIcon.tsx b/apps/wallet-dashboard/components/coins/CoinIcon.tsx deleted file mode 100644 index a5c24107baa..00000000000 --- a/apps/wallet-dashboard/components/coins/CoinIcon.tsx +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { useCoinMetadata } from '@iota/core'; -import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; -import { IotaLogoMark } from '@iota/ui-icons'; -import cx from 'clsx'; -import { ImageIcon, ImageIconSize } from '../ImageIcon'; - -interface NonIotaCoinProps { - coinType: string; - size?: ImageIconSize; - rounded?: boolean; -} - -function NonIotaCoin({ coinType, size = ImageIconSize.Full, rounded }: NonIotaCoinProps) { - const { data: coinMeta } = useCoinMetadata(coinType); - return ( - <div className="flex h-full w-full items-center justify-center rounded-full bg-neutral-96 text-neutral-10 dark:bg-neutral-20 dark:text-neutral-100"> - <ImageIcon - src={coinMeta?.iconUrl} - label={coinMeta?.name || coinType} - fallbackText={coinMeta?.name || coinType} - size={size} - rounded={rounded} - /> - </div> - ); -} - -export interface CoinIconProps { - coinType: string; - size?: ImageIconSize; - rounded?: boolean; -} - -export function CoinIcon({ coinType, size = ImageIconSize.Full, rounded }: CoinIconProps) { - return coinType === IOTA_TYPE_ARG ? ( - <div className="flex h-full w-full items-center justify-center border border-shader-neutral-light-8 bg-neutral-100 text-neutral-10 dark:bg-neutral-0 dark:text-neutral-100"> - <IotaLogoMark className={cx(size)} /> - </div> - ) : ( - <NonIotaCoin rounded={rounded} size={size} coinType={coinType} /> - ); -} diff --git a/apps/wallet-dashboard/components/coins/CoinItem.tsx b/apps/wallet-dashboard/components/coins/CoinItem.tsx index 29e8be5aa35..01528cb5ac1 100644 --- a/apps/wallet-dashboard/components/coins/CoinItem.tsx +++ b/apps/wallet-dashboard/components/coins/CoinItem.tsx @@ -10,11 +10,9 @@ import { CardType, ImageType, } from '@iota/apps-ui-kit'; -import { useFormatCoin } from '@iota/core'; +import { CoinIcon, useFormatCoin, ImageIconSize } from '@iota/core'; import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; import { type ReactNode } from 'react'; -import { ImageIconSize } from '../ImageIcon'; -import { CoinIcon } from './CoinIcon'; interface CoinItemProps { coinType: string; diff --git a/apps/wallet-dashboard/components/coins/index.ts b/apps/wallet-dashboard/components/coins/index.ts index 51468194c31..d1519105eec 100644 --- a/apps/wallet-dashboard/components/coins/index.ts +++ b/apps/wallet-dashboard/components/coins/index.ts @@ -3,4 +3,3 @@ export { default as MyCoins } from './MyCoins'; export { default as CoinItem } from './CoinItem'; -export * from './CoinIcon'; diff --git a/apps/wallet-dashboard/components/index.ts b/apps/wallet-dashboard/components/index.ts index a6c298a4941..83697f63914 100644 --- a/apps/wallet-dashboard/components/index.ts +++ b/apps/wallet-dashboard/components/index.ts @@ -20,6 +20,5 @@ export * from './Buttons'; export * from './transactions'; export * from './staking-overview'; export * from './Dialogs'; -export * from './ImageIcon'; export * from './ValidatorStakingData'; export * from './tiles'; From 86c27ae148d69e742a225d5f315774cbd813f7a8 Mon Sep 17 00:00:00 2001 From: cpl121 <cpeon@boxfish.studio> Date: Wed, 13 Nov 2024 14:54:14 +0100 Subject: [PATCH 36/87] fix(wallet-dashboard): remove unnecesary InputForm component --- apps/core/src/components/Inputs/FormInput.tsx | 71 ------------------- .../components/Inputs/SendTokenFormInput.tsx | 64 ++++++++++------- apps/core/src/components/Inputs/index.ts | 1 - apps/core/src/components/index.ts | 1 + .../SendToken/views/EnterValuesFormView.tsx | 2 +- .../home/transfer-coin/SendTokenForm.tsx | 2 +- 6 files changed, 41 insertions(+), 100 deletions(-) delete mode 100644 apps/core/src/components/Inputs/FormInput.tsx diff --git a/apps/core/src/components/Inputs/FormInput.tsx b/apps/core/src/components/Inputs/FormInput.tsx deleted file mode 100644 index eeecd383a5c..00000000000 --- a/apps/core/src/components/Inputs/FormInput.tsx +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { Input, InputType, type InputProps, type NumericFormatInputProps } from '@iota/apps-ui-kit'; -import React from 'react'; - -interface FormInputProps extends Omit<InputProps, 'onChange'> { - name: string; - value: string; - suffix: string; - allowNegative: boolean; - onChange: (value: string) => void; - onBlur?: React.FocusEventHandler<HTMLInputElement>; - errorMessage?: string; - renderAction?: (isDisabled?: boolean) => React.JSX.Element; - decimals?: boolean; - disabled?: boolean; - isSubmitting?: boolean; -} - -export function FormInput({ - renderAction, - decimals, - value, - onChange, - onBlur, - errorMessage, - isSubmitting = false, - disabled, - name, - type, - placeholder, - caption, - amountCounter, - label, - suffix, - allowNegative, -}: FormInputProps) { - const isInputDisabled = isSubmitting || disabled; - const isNumericFormat = type === InputType.NumericFormat; - - const numericPropsOnly: Partial<NumericFormatInputProps> = { - decimalScale: decimals ? undefined : 0, - thousandSeparator: true, - onValueChange: (values) => { - onChange(values.value); - }, - }; - - const isActionButtonDisabled = isInputDisabled || !value || !!errorMessage; - - return ( - <Input - name={name} - value={value} - type={type} - caption={caption} - disabled={isInputDisabled} - placeholder={placeholder} - onBlur={onBlur} - label={label} - suffix={suffix} - allowNegative={allowNegative} - errorMessage={errorMessage} - onChange={(e) => onChange(e.currentTarget.value)} - amountCounter={!errorMessage ? amountCounter : undefined} - trailingElement={renderAction?.(isActionButtonDisabled)} - {...(isNumericFormat ? numericPropsOnly : {})} - /> - ); -} diff --git a/apps/core/src/components/Inputs/SendTokenFormInput.tsx b/apps/core/src/components/Inputs/SendTokenFormInput.tsx index baee61e29a3..c07d3edfb33 100644 --- a/apps/core/src/components/Inputs/SendTokenFormInput.tsx +++ b/apps/core/src/components/Inputs/SendTokenFormInput.tsx @@ -1,10 +1,9 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { ButtonPill, InputType } from '@iota/apps-ui-kit'; +import { ButtonPill, Input, InputType, NumericFormatInputProps } from '@iota/apps-ui-kit'; import { CoinStruct } from '@iota/iota-sdk/client'; import { useGasBudgetEstimation } from '../../hooks'; -import { FormInput } from '..'; import React, { useEffect } from 'react'; export interface SendTokenInputProps { @@ -19,7 +18,7 @@ export interface SendTokenInputProps { isPayAllIota: boolean; }; onActionClick: () => Promise<void>; - isActionButtonDisabled?: boolean | 'auto'; + isMaxActionDisabled?: boolean | 'auto'; value: string; onChange: (value: string) => void; onBlur?: React.FocusEventHandler<HTMLInputElement>; @@ -34,7 +33,7 @@ export function SendTokenFormInput({ activeAddress, setFieldValue, onActionClick, - isActionButtonDisabled, + isMaxActionDisabled, value, onChange, onBlur, @@ -48,6 +47,30 @@ export function SendTokenFormInput({ amount: values.amount, isPayAllIota: values.isPayAllIota, }); + + const numericPropsOnly: Partial<NumericFormatInputProps> = { + decimalScale: coinDecimals ? undefined : 0, + thousandSeparator: true, + onValueChange: (values) => { + onChange(values.value); + }, + }; + + + const isActionButtonDisabled = !value || !!errorMessage; + + const renderAction = (isButtonDisabled: boolean | undefined) => ( + <ButtonPill + disabled={ + isMaxActionDisabled === 'auto' + ? isButtonDisabled + : isActionButtonDisabled + } + onClick={onActionClick} + > + Max + </ButtonPill> + ) // gasBudgetEstimation should change when the amount above changes useEffect(() => { @@ -55,33 +78,22 @@ export function SendTokenFormInput({ }, [gasBudgetEstimation, setFieldValue, values.amount]); return ( - <FormInput + <Input type={InputType.NumericFormat} - name="amount" - label="Send Amount" - placeholder="0.00" + name={"amount"} + value={value} caption="Est. Gas Fees:" + placeholder="0.00" + onBlur={onBlur} + label="Send Amount" suffix={` ${symbol}`} - decimals - allowNegative={false} prefix={values.isPayAllIota ? '~ ' : undefined} - amountCounter={coins ? gasBudgetEstimation : '--'} - value={value} - onChange={onChange} - onBlur={onBlur} + allowNegative={false} errorMessage={errorMessage} - renderAction={(isButtonDisabled) => ( - <ButtonPill - disabled={ - isActionButtonDisabled === 'auto' - ? isButtonDisabled - : isActionButtonDisabled - } - onClick={onActionClick} - > - Max - </ButtonPill> - )} + onChange={(e) => onChange(e.currentTarget.value)} + amountCounter={!errorMessage ? (coins ? gasBudgetEstimation : '--') : undefined} + trailingElement={renderAction(isActionButtonDisabled)} + {...numericPropsOnly} /> ); } diff --git a/apps/core/src/components/Inputs/index.ts b/apps/core/src/components/Inputs/index.ts index 9f01e814469..5ab7c849a28 100644 --- a/apps/core/src/components/Inputs/index.ts +++ b/apps/core/src/components/Inputs/index.ts @@ -2,5 +2,4 @@ // SPDX-License-Identifier: Apache-2.0 export * from './AddressInput'; -export * from './FormInput'; export * from './SendTokenFormInput'; diff --git a/apps/core/src/components/index.ts b/apps/core/src/components/index.ts index 4dea9923bba..0a8093eeadb 100644 --- a/apps/core/src/components/index.ts +++ b/apps/core/src/components/index.ts @@ -4,5 +4,6 @@ export * from './KioskClientProvider'; export * from './coin'; +export * from './icon'; export * from './Inputs'; export * from './QR'; diff --git a/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx b/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx index de6c598fd17..b7c5e437f61 100644 --- a/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx @@ -129,7 +129,7 @@ function FormInputs({ setFieldValue={setFieldValue} values={values} onActionClick={onMaxTokenButtonClick} - isActionButtonDisabled={isMaxActionDisabled} + isMaxActionDisabled={isMaxActionDisabled} value={field.value} onChange={(value) => handleOnChangeAmountInput(value, symbol)} onBlur={handleBlur} diff --git a/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx b/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx index 796e62a998e..e60870cd219 100644 --- a/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx +++ b/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx @@ -208,7 +208,7 @@ export function SendTokenForm({ coins={coins ?? []} values={values} onActionClick={onMaxTokenButtonClick} - isActionButtonDisabled={isMaxActionDisabled} + isMaxActionDisabled={isMaxActionDisabled} value={field.value} onChange={(value) => handleOnChangeAmountInput(value, symbol) From 7e6c12d4c87471f6421213510a840f3a25c638a6 Mon Sep 17 00:00:00 2001 From: cpl121 <cpeon@boxfish.studio> Date: Wed, 13 Nov 2024 14:54:37 +0100 Subject: [PATCH 37/87] fix(wallet-dashboard): adjust to full height the dialog body --- .../components/Dialogs/SendToken/SendTokenDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx b/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx index 139b9d0cbe1..35118aa1228 100644 --- a/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx @@ -96,7 +96,7 @@ function SendTokenDialogBody({ title={step === FormStep.EnterValues ? 'Send' : 'Review & Send'} onClose={() => setOpen(false)} /> - <div className="h-full"> + <div className="h-full [&>div]:h-full"> <DialogBody> {step === FormStep.EnterValues && ( <EnterValuesFormView From af32d3a01b2ed4b1b9e6ea8d6ed5a4c0a768d59d Mon Sep 17 00:00:00 2001 From: cpl121 <cpeon@boxfish.studio> Date: Wed, 13 Nov 2024 15:32:25 +0100 Subject: [PATCH 38/87] fix(wallet-dashboard): prettier --- .../components/Inputs/SendTokenFormInput.tsx | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/apps/core/src/components/Inputs/SendTokenFormInput.tsx b/apps/core/src/components/Inputs/SendTokenFormInput.tsx index c07d3edfb33..2298d433260 100644 --- a/apps/core/src/components/Inputs/SendTokenFormInput.tsx +++ b/apps/core/src/components/Inputs/SendTokenFormInput.tsx @@ -47,7 +47,7 @@ export function SendTokenFormInput({ amount: values.amount, isPayAllIota: values.isPayAllIota, }); - + const numericPropsOnly: Partial<NumericFormatInputProps> = { decimalScale: coinDecimals ? undefined : 0, thousandSeparator: true, @@ -56,21 +56,16 @@ export function SendTokenFormInput({ }, }; - const isActionButtonDisabled = !value || !!errorMessage; - + const renderAction = (isButtonDisabled: boolean | undefined) => ( <ButtonPill - disabled={ - isMaxActionDisabled === 'auto' - ? isButtonDisabled - : isActionButtonDisabled - } + disabled={isMaxActionDisabled === 'auto' ? isButtonDisabled : isActionButtonDisabled} onClick={onActionClick} - > + > Max </ButtonPill> - ) + ); // gasBudgetEstimation should change when the amount above changes useEffect(() => { @@ -80,7 +75,7 @@ export function SendTokenFormInput({ return ( <Input type={InputType.NumericFormat} - name={"amount"} + name={'amount'} value={value} caption="Est. Gas Fees:" placeholder="0.00" From 754d2dbcb11f8208d9670d740d48815532918c5b Mon Sep 17 00:00:00 2001 From: JCNoguera <88061365+VmMad@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:43:20 +0100 Subject: [PATCH 39/87] fix: gas approximation --- apps/core/src/components/Inputs/SendTokenFormInput.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/core/src/components/Inputs/SendTokenFormInput.tsx b/apps/core/src/components/Inputs/SendTokenFormInput.tsx index 60266dd52b9..64dd3cae36c 100644 --- a/apps/core/src/components/Inputs/SendTokenFormInput.tsx +++ b/apps/core/src/components/Inputs/SendTokenFormInput.tsx @@ -89,7 +89,11 @@ export function SendTokenFormInput({ errorMessage={errorMessage} onChange={(e) => onChange(e.currentTarget.value)} amountCounter={ - !errorMessage ? (coins ? `${gasBudgetEstimation} ${GAS_SYMBOL}` : '--') : undefined + !errorMessage + ? coins && gasBudgetEstimation !== '--' + ? `${gasBudgetEstimation} ${GAS_SYMBOL}` + : '--' + : undefined } trailingElement={renderAction(isActionButtonDisabled)} {...numericPropsOnly} From 4d9339469a8b7533a64c1a195ced53d9dd3d2872 Mon Sep 17 00:00:00 2001 From: Eugene P <panteleymonchuk@gmail.com> Date: Wed, 13 Nov 2024 16:52:22 +0200 Subject: [PATCH 40/87] feat(wallet-dashboard): manage view for dialog outside. --- .../app/(protected)/staking/page.tsx | 19 ++++++------- .../Dialogs/Staking/StakeDialog.tsx | 27 +++++++++---------- .../Dialogs/Staking/views/DetailsView.tsx | 16 +++++------ .../Dialogs/Staking/views/Layout.tsx | 7 ++--- .../staking-overview/StartStaking.tsx | 20 +++++++++++--- 5 files changed, 50 insertions(+), 39 deletions(-) diff --git a/apps/wallet-dashboard/app/(protected)/staking/page.tsx b/apps/wallet-dashboard/app/(protected)/staking/page.tsx index 81cc0edc224..2a64ae657e4 100644 --- a/apps/wallet-dashboard/app/(protected)/staking/page.tsx +++ b/apps/wallet-dashboard/app/(protected)/staking/page.tsx @@ -4,7 +4,7 @@ 'use client'; import { AmountBox, Box, StakeCard, StakeDialog, Button } from '@/components'; -import { View } from '@/components/Dialogs/Staking/StakeDialog'; +import { StakeDialogView } from '@/components/Dialogs/Staking/StakeDialog'; import { ExtendedDelegatedStake, formatDelegatedStake, @@ -21,8 +21,7 @@ import { useState } from 'react'; function StakingDashboardPage(): JSX.Element { const account = useCurrentAccount(); - const [dialogStakeInitView, setDialogStakeInitView] = useState<View | undefined>(); - const [isDialogStakeOpen, setIsDialogStakeOpen] = useState(false); + const [dialogStakeView, setDialogStakeView] = useState<StakeDialogView | undefined>(); const [selectedStake, setSelectedStake] = useState<ExtendedDelegatedStake | null>(null); const { data: delegatedStakeData } = useGetDelegatedStake({ address: account?.address || '', @@ -43,21 +42,22 @@ function StakingDashboardPage(): JSX.Element { ); const viewStakeDetails = (extendedStake: ExtendedDelegatedStake) => { - setIsDialogStakeOpen(true); - setDialogStakeInitView(View.Details); + setDialogStakeView(StakeDialogView.Details); setSelectedStake(extendedStake); }; function handleCloseStakeDialog() { - setIsDialogStakeOpen(false); setSelectedStake(null); - setDialogStakeInitView(undefined); + setDialogStakeView(undefined); } function handleNewStake() { - setIsDialogStakeOpen(true); + setSelectedStake(null); + setDialogStakeView(undefined); } + const isDialogStakeOpen = dialogStakeView !== undefined; + return ( <> <div className="flex flex-col items-center justify-center gap-4 pt-12"> @@ -92,7 +92,8 @@ function StakingDashboardPage(): JSX.Element { stakedDetails={selectedStake} isOpen={isDialogStakeOpen} handleClose={handleCloseStakeDialog} - initView={dialogStakeInitView} + view={dialogStakeView} + setView={setDialogStakeView} /> )} </> diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx index df94fbe714a..31c4aa13a82 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx @@ -23,7 +23,7 @@ import { NotificationType } from '@/stores/notificationStore'; import { prepareObjectsForTimelockedStakingTransaction } from '@/lib/utils'; import { Dialog } from '@iota/apps-ui-kit'; -export enum View { +export enum StakeDialogView { Details, SelectValidator, EnterAmount, @@ -35,8 +35,9 @@ interface StakeDialogProps { onSuccess?: (digest: string) => void; isOpen: boolean; handleClose: () => void; - stakedDetails: ExtendedDelegatedStake | null; - initView?: View; + stakedDetails?: ExtendedDelegatedStake | null; + view: StakeDialogView; + setView: (nextView: StakeDialogView) => void; } function StakeDialog({ @@ -44,12 +45,10 @@ function StakeDialog({ isTimelockedStaking, isOpen, handleClose: handleClose, - initView, + view, + setView, stakedDetails, }: StakeDialogProps): JSX.Element { - const [view, setView] = useState<View>( - initView !== undefined ? initView : View.SelectValidator, - ); const [selectedValidator, setSelectedValidator] = useState<string>(''); const [amount, setAmount] = useState<string>(''); const account = useCurrentAccount(); @@ -87,11 +86,11 @@ function StakeDialog({ const validators = Object.keys(rollingAverageApys ?? {}) ?? []; function handleNext(): void { - setView(View.EnterAmount); + setView(StakeDialogView.EnterAmount); } function handleBack(): void { - setView(View.SelectValidator); + setView(StakeDialogView.SelectValidator); } function handleValidatorSelect(validator: string): void { @@ -129,16 +128,16 @@ function StakeDialog({ } function detailsHandleUnstake() { - setView(View.Unstake); + setView(StakeDialogView.Unstake); } function detailsHandleStake() { - setView(View.SelectValidator); + setView(StakeDialogView.SelectValidator); } return ( <Dialog open={isOpen} onOpenChange={() => handleClose()}> - {view === View.Details && stakedDetails && ( + {view === StakeDialogView.Details && stakedDetails && ( <DetailsView handleStake={detailsHandleStake} handleUnstake={detailsHandleUnstake} @@ -146,10 +145,10 @@ function StakeDialog({ handleClose={handleClose} /> )} - {view === View.SelectValidator && ( + {view === StakeDialogView.SelectValidator && ( <SelectValidatorView validators={validators} onSelect={handleValidatorSelect} /> )} - {view === View.EnterAmount && ( + {view === StakeDialogView.EnterAmount && ( <EnterAmountView selectedValidator={selectedValidator} amount={amount} diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/views/DetailsView.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/views/DetailsView.tsx index 7d0a285e7a6..c6bee99c1b6 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/views/DetailsView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/views/DetailsView.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import React, { useMemo } from 'react'; +import React from 'react'; import { useGetValidatorsApy, ExtendedDelegatedStake, @@ -32,6 +32,7 @@ import { Warning } from '@iota/ui-icons'; import { useIotaClientQuery } from '@iota/dapp-kit'; import { formatAddress, IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; import { Layout, LayoutFooter, LayoutBody } from './Layout'; + interface StakeDialogProps { stakedDetails: ExtendedDelegatedStake; showActiveStatus?: boolean; @@ -39,6 +40,7 @@ interface StakeDialogProps { handleUnstake: () => void; handleStake: () => void; } + export function DetailsView({ handleClose, handleUnstake, @@ -64,15 +66,9 @@ export function DetailsView({ // flag if the validator is at risk of being removed from the active set const isAtRisk = system?.atRiskValidators.some((item) => item[0] === validatorAddress); - const validatorSummary = useMemo(() => { - if (!system) return null; - - return ( - system.activeValidators.find( - (validator) => validator.iotaAddress === validatorAddress, - ) || null - ); - }, [validatorAddress, system]); + const validatorSummary = + system?.activeValidators.find((validator) => validator.iotaAddress === validatorAddress) || + null; const validatorName = validatorSummary?.name || ''; const stakingPoolActivationEpoch = Number(validatorSummary?.stakingPoolActivationEpoch || 0); diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/views/Layout.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/views/Layout.tsx index 638704fa2b6..7986ce592d0 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/views/Layout.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/views/Layout.tsx @@ -1,8 +1,9 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +import { PropsWithChildren } from 'react'; import { DialogBody, DialogContent, DialogPosition } from '@iota/apps-ui-kit'; -export function Layout({ children }: { children: React.ReactNode }) { +export function Layout({ children }: PropsWithChildren) { return ( <DialogContent containerId="overlay-portal-container" position={DialogPosition.Right}> <div className="flex h-full flex-col">{children}</div> @@ -10,7 +11,7 @@ export function Layout({ children }: { children: React.ReactNode }) { ); } -export function LayoutBody({ children }: { children: React.ReactNode }) { +export function LayoutBody({ children }: PropsWithChildren) { return ( <div className="flex w-full flex-1 overflow-y-hidden [&_>div]:w-full [&_>div]:overflow-y-auto"> <DialogBody>{children}</DialogBody> @@ -18,6 +19,6 @@ export function LayoutBody({ children }: { children: React.ReactNode }) { ); } -export function LayoutFooter({ children }: { children: React.ReactNode }) { +export function LayoutFooter({ children }: PropsWithChildren) { return <div className="p-md--rs">{children}</div>; } diff --git a/apps/wallet-dashboard/components/staking-overview/StartStaking.tsx b/apps/wallet-dashboard/components/staking-overview/StartStaking.tsx index 7317253a8d2..dd2e0ef9b81 100644 --- a/apps/wallet-dashboard/components/staking-overview/StartStaking.tsx +++ b/apps/wallet-dashboard/components/staking-overview/StartStaking.tsx @@ -5,15 +5,22 @@ import { Button, ButtonSize, ButtonType, Panel } from '@iota/apps-ui-kit'; import { Theme, useTheme } from '@/contexts'; import { useState } from 'react'; import { StakeDialog } from '../Dialogs'; +import { StakeDialogView } from '../Dialogs/Staking/StakeDialog'; export function StartStaking() { const { theme } = useTheme(); - const [isDialogStakeOpen, setIsDialogStakeOpen] = useState(false); + const [dialogStakeView, setDialogStakeView] = useState<StakeDialogView | undefined>(); function handleNewStake() { - setIsDialogStakeOpen(true); + setDialogStakeView(StakeDialogView.SelectValidator); } + function handleClose() { + setDialogStakeView(undefined); + } + + const isDialogStakeOpen = dialogStakeView !== undefined; + const videoSrc = theme === Theme.Dark ? 'https://files.iota.org/media/tooling/wallet-dashboard-staking-dark.mp4' @@ -50,7 +57,14 @@ export function StartStaking() { ></video> </div> </div> - <StakeDialog isOpen={isDialogStakeOpen} handleClose={setIsDialogStakeOpen} /> + {isDialogStakeOpen && ( + <StakeDialog + isOpen={isDialogStakeOpen} + handleClose={handleClose} + view={dialogStakeView} + setView={setDialogStakeView} + /> + )} </Panel> ); } From 8c17341c8ba3cc3e021b389781d93df8b4d14c02 Mon Sep 17 00:00:00 2001 From: Eugene P <panteleymonchuk@gmail.com> Date: Wed, 13 Nov 2024 18:48:42 +0200 Subject: [PATCH 41/87] feat(wallet-dashboard): join changes from PR 3854 --- .../Dialogs/Staking/StakeDialog.tsx | 17 ++- .../Staking/views/SelectValidatorView.tsx | 107 ++++++++++++++++-- apps/wallet-dashboard/hooks/index.ts | 1 + .../hooks/useValidatorInfo.tsx | 46 ++++++++ 4 files changed, 153 insertions(+), 18 deletions(-) create mode 100644 apps/wallet-dashboard/hooks/useValidatorInfo.tsx diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx index 31c4aa13a82..be8331b865f 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx @@ -85,17 +85,12 @@ function StakeDialog({ const validators = Object.keys(rollingAverageApys ?? {}) ?? []; - function handleNext(): void { - setView(StakeDialogView.EnterAmount); - } - function handleBack(): void { setView(StakeDialogView.SelectValidator); } function handleValidatorSelect(validator: string): void { setSelectedValidator(validator); - handleNext(); } function handleStake(): void { @@ -135,6 +130,10 @@ function StakeDialog({ setView(StakeDialogView.SelectValidator); } + function selectValidatorHandleNext() { + setView(StakeDialogView.EnterAmount); + } + return ( <Dialog open={isOpen} onOpenChange={() => handleClose()}> {view === StakeDialogView.Details && stakedDetails && ( @@ -146,7 +145,13 @@ function StakeDialog({ /> )} {view === StakeDialogView.SelectValidator && ( - <SelectValidatorView validators={validators} onSelect={handleValidatorSelect} /> + <SelectValidatorView + selectedValidator={selectedValidator} + handleClose={handleClose} + validators={validators} + onSelect={handleValidatorSelect} + onNext={selectValidatorHandleNext} + /> )} {view === StakeDialogView.EnterAmount && ( <EnterAmountView diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/views/SelectValidatorView.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/views/SelectValidatorView.tsx index 25dbeb276cf..73e25ef25d1 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/views/SelectValidatorView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/views/SelectValidatorView.tsx @@ -2,25 +2,108 @@ // SPDX-License-Identifier: Apache-2.0 import React from 'react'; -import { Button } from '@/components'; - +import { ImageIcon, ImageIconSize, formatPercentageDisplay } from '@iota/core'; +import { + Header, + Button, + Card, + CardBody, + CardImage, + CardAction, + CardActionType, + CardType, + Badge, + BadgeType, +} from '@iota/apps-ui-kit'; +import { formatAddress } from '@iota/iota-sdk/utils'; +import { useValidatorInfo } from '@/hooks'; +import { Layout, LayoutFooter, LayoutBody } from './Layout'; interface SelectValidatorViewProps { validators: string[]; onSelect: (validator: string) => void; + handleClose: () => void; + onNext: () => void; + selectedValidator: string; } -function SelectValidatorView({ validators, onSelect }: SelectValidatorViewProps): JSX.Element { +function SelectValidatorView({ + validators, + onSelect, + handleClose, + onNext, + selectedValidator, +}: SelectValidatorViewProps): JSX.Element { return ( - <div> - <h2>Select Validator</h2> - <div className="flex flex-col items-start gap-2"> - {validators.map((validator) => ( - <Button key={validator} onClick={() => onSelect(validator)}> - {validator} - </Button> - ))} - </div> + <Layout> + <Header + title="Select Validator" + onClose={handleClose} + onBack={handleClose} + titleCentered + /> + <LayoutBody> + <div className="flex w-full flex-col gap-md"> + {validators.map((validator) => ( + <Validator + key={validator} + address={validator} + onClick={onSelect} + isSelected={selectedValidator === validator} + /> + ))} + </div> + </LayoutBody> + <LayoutFooter> + {!!selectedValidator && ( + <Button + fullWidth + data-testid="select-validator-cta" + onClick={onNext} + text="Next" + /> + )} + </LayoutFooter> + </Layout> + ); +} + +function Validator({ + address, + showActiveStatus, + onClick, + isSelected, +}: { + isSelected: boolean; + address: string; + showActiveStatus?: boolean; + onClick: (address: string) => void; +}) { + const { name, newValidator, isAtRisk, apy, isApyApproxZero } = useValidatorInfo(address); + + const subtitle = showActiveStatus ? ( + <div className="flex items-center gap-1"> + {formatAddress(address)} + {newValidator && <Badge label="New" type={BadgeType.PrimarySoft} />} + {isAtRisk && <Badge label="At Risk" type={BadgeType.PrimarySolid} />} </div> + ) : ( + formatAddress(address) + ); + + const handleClick = onClick ? () => onClick(address) : undefined; + + return ( + <Card type={isSelected ? CardType.Filled : CardType.Default} onClick={handleClick}> + <CardImage> + <ImageIcon src={null} label={name} fallback={name} size={ImageIconSize.Large} /> + </CardImage> + <CardBody title={name} subtitle={subtitle} isTextTruncated /> + <CardAction + type={CardActionType.SupportingText} + title={formatPercentageDisplay(apy, '--', isApyApproxZero)} + iconAfterText + /> + </Card> ); } diff --git a/apps/wallet-dashboard/hooks/index.ts b/apps/wallet-dashboard/hooks/index.ts index 373c4e399dc..394f13ab30a 100644 --- a/apps/wallet-dashboard/hooks/index.ts +++ b/apps/wallet-dashboard/hooks/index.ts @@ -10,3 +10,4 @@ export * from './useCreateSendAssetTransaction'; export * from './useGetCurrentEpochStartTimestamp'; export * from './useTimelockedUnstakeTransaction'; export * from './useExplorerLinkGetter'; +export * from './useValidatorInfo'; diff --git a/apps/wallet-dashboard/hooks/useValidatorInfo.tsx b/apps/wallet-dashboard/hooks/useValidatorInfo.tsx new file mode 100644 index 00000000000..214dbe20e0a --- /dev/null +++ b/apps/wallet-dashboard/hooks/useValidatorInfo.tsx @@ -0,0 +1,46 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 +import { useMemo } from 'react'; +import { useIotaClientQuery } from '@iota/dapp-kit'; +import { useGetValidatorsApy } from '@iota/core'; + +export function useValidatorInfo(validatorAddress: string) { + const { data: system } = useIotaClientQuery('getLatestIotaSystemState'); + const { data: rollingAverageApys } = useGetValidatorsApy(); + + const currentEpoch = Number(system?.epoch || 0); + + const validatorSummary = useMemo(() => { + if (!system) return null; + + return ( + system.activeValidators.find( + (validator) => validator.iotaAddress === validatorAddress, + ) || null + ); + }, [validatorAddress, system]); + + const stakingPoolActivationEpoch = Number(validatorSummary?.stakingPoolActivationEpoch || 0); + + // flag as new validator if the validator was activated in the last epoch + // for genesis validators, this will be false + const newValidator = currentEpoch - stakingPoolActivationEpoch <= 1 && currentEpoch !== 0; + + // flag if the validator is at risk of being removed from the active set + const isAtRisk = system?.atRiskValidators.some((item) => item[0] === validatorAddress); + + const { apy, isApyApproxZero } = rollingAverageApys?.[validatorAddress] ?? { + apy: null, + }; + + return { + validatorSummary, + name: validatorSummary?.name || '', + stakingPoolActivationEpoch, + commission: validatorSummary ? Number(validatorSummary.commissionRate) / 100 : 0, + newValidator, + isAtRisk, + apy, + isApyApproxZero, + }; +} From 7f66cd086e4a25e76a0df6e31ef93cc6b0171246 Mon Sep 17 00:00:00 2001 From: Eugene P <panteleymonchuk@gmail.com> Date: Wed, 13 Nov 2024 20:18:12 +0200 Subject: [PATCH 42/87] feat(wallet-dashboard): join enter amount screen from PR 3874 --- apps/core/src/hooks/stake/index.ts | 1 + .../src/hooks/stake}/useValidatorInfo.tsx | 13 +- .../app/(protected)/staking/page.tsx | 14 +- .../StakeDetails/StakeDetailsDialog.tsx | 74 ------- .../components/Dialogs/StakeDetails/index.ts | 4 - .../StakeDetails/views/StakeDetailsView.tsx | 37 ---- .../Dialogs/StakeDetails/views/index.ts | 4 - .../Dialogs/Staking/StakeDialog.tsx | 46 ++-- .../Dialogs/Staking/StakedDetailsDialog.tsx | 197 ++++++++++++++++++ .../{Unstake => Staking/hooks}/index.ts | 2 +- .../Dialogs/Staking/hooks/useStakeTxsInfo.ts | 49 +++++ .../components/Dialogs/Staking/index.ts | 1 + .../Dialogs/Staking/views/EnterAmountView.tsx | 124 +++++++++-- .../Staking/views/SelectValidatorView.tsx | 98 ++------- .../Dialogs/Staking/views/StakedInfo.tsx | 100 +++++++++ .../views/UnstakeView.tsx} | 19 +- .../Dialogs/Staking/views/Validator.tsx | 61 ++++++ .../components/Dialogs/Staking/views/index.ts | 1 + .../components/Dialogs/Unstake/views/index.ts | 4 - .../components/Dialogs/index.ts | 1 - .../components/Stake/StakeTxnInfo.tsx | 82 ++++++++ .../staking-overview/StartStaking.tsx | 20 +- .../components/transactions/GasSummary.tsx | 4 +- apps/wallet-dashboard/hooks/index.ts | 2 +- 24 files changed, 681 insertions(+), 277 deletions(-) rename apps/{wallet-dashboard/hooks => core/src/hooks/stake}/useValidatorInfo.tsx (82%) delete mode 100644 apps/wallet-dashboard/components/Dialogs/StakeDetails/StakeDetailsDialog.tsx delete mode 100644 apps/wallet-dashboard/components/Dialogs/StakeDetails/index.ts delete mode 100644 apps/wallet-dashboard/components/Dialogs/StakeDetails/views/StakeDetailsView.tsx delete mode 100644 apps/wallet-dashboard/components/Dialogs/StakeDetails/views/index.ts create mode 100644 apps/wallet-dashboard/components/Dialogs/Staking/StakedDetailsDialog.tsx rename apps/wallet-dashboard/components/Dialogs/{Unstake => Staking/hooks}/index.ts (68%) create mode 100644 apps/wallet-dashboard/components/Dialogs/Staking/hooks/useStakeTxsInfo.ts create mode 100644 apps/wallet-dashboard/components/Dialogs/Staking/views/StakedInfo.tsx rename apps/wallet-dashboard/components/Dialogs/{Unstake/views/UnstakeDialogView.tsx => Staking/views/UnstakeView.tsx} (94%) create mode 100644 apps/wallet-dashboard/components/Dialogs/Staking/views/Validator.tsx delete mode 100644 apps/wallet-dashboard/components/Dialogs/Unstake/views/index.ts create mode 100644 apps/wallet-dashboard/components/Stake/StakeTxnInfo.tsx diff --git a/apps/core/src/hooks/stake/index.ts b/apps/core/src/hooks/stake/index.ts index 53e1fc44806..f422d9d1f61 100644 --- a/apps/core/src/hooks/stake/index.ts +++ b/apps/core/src/hooks/stake/index.ts @@ -4,3 +4,4 @@ export * from './useGetDelegatedStake'; export * from './useTotalDelegatedRewards'; export * from './useTotalDelegatedStake'; +export * from './useValidatorInfo'; diff --git a/apps/wallet-dashboard/hooks/useValidatorInfo.tsx b/apps/core/src/hooks/stake/useValidatorInfo.tsx similarity index 82% rename from apps/wallet-dashboard/hooks/useValidatorInfo.tsx rename to apps/core/src/hooks/stake/useValidatorInfo.tsx index 214dbe20e0a..3b8ec1bacf7 100644 --- a/apps/wallet-dashboard/hooks/useValidatorInfo.tsx +++ b/apps/core/src/hooks/stake/useValidatorInfo.tsx @@ -2,14 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 import { useMemo } from 'react'; import { useIotaClientQuery } from '@iota/dapp-kit'; -import { useGetValidatorsApy } from '@iota/core'; +import { useGetValidatorsApy } from '../'; -export function useValidatorInfo(validatorAddress: string) { +export function useValidatorInfo({ validatorAddress }: { validatorAddress: string }) { const { data: system } = useIotaClientQuery('getLatestIotaSystemState'); const { data: rollingAverageApys } = useGetValidatorsApy(); - const currentEpoch = Number(system?.epoch || 0); - const validatorSummary = useMemo(() => { if (!system) return null; @@ -20,6 +18,11 @@ export function useValidatorInfo(validatorAddress: string) { ); }, [validatorAddress, system]); + const currentEpoch = Number(system?.epoch || 0); + + //TODO: verify this is the correct validator stake balance + const totalValidatorStake = validatorSummary?.stakingPoolIotaBalance || 0; + const stakingPoolActivationEpoch = Number(validatorSummary?.stakingPoolActivationEpoch || 0); // flag as new validator if the validator was activated in the last epoch @@ -34,6 +37,7 @@ export function useValidatorInfo(validatorAddress: string) { }; return { + system, validatorSummary, name: validatorSummary?.name || '', stakingPoolActivationEpoch, @@ -42,5 +46,6 @@ export function useValidatorInfo(validatorAddress: string) { isAtRisk, apy, isApyApproxZero, + totalValidatorStake, }; } diff --git a/apps/wallet-dashboard/app/(protected)/staking/page.tsx b/apps/wallet-dashboard/app/(protected)/staking/page.tsx index 2a64ae657e4..3d5d03a5d15 100644 --- a/apps/wallet-dashboard/app/(protected)/staking/page.tsx +++ b/apps/wallet-dashboard/app/(protected)/staking/page.tsx @@ -21,7 +21,7 @@ import { useState } from 'react'; function StakingDashboardPage(): JSX.Element { const account = useCurrentAccount(); - const [dialogStakeView, setDialogStakeView] = useState<StakeDialogView | undefined>(); + const [stakeDialogView, setStakeDialogView] = useState<StakeDialogView | undefined>(); const [selectedStake, setSelectedStake] = useState<ExtendedDelegatedStake | null>(null); const { data: delegatedStakeData } = useGetDelegatedStake({ address: account?.address || '', @@ -42,21 +42,21 @@ function StakingDashboardPage(): JSX.Element { ); const viewStakeDetails = (extendedStake: ExtendedDelegatedStake) => { - setDialogStakeView(StakeDialogView.Details); + setStakeDialogView(StakeDialogView.Details); setSelectedStake(extendedStake); }; function handleCloseStakeDialog() { setSelectedStake(null); - setDialogStakeView(undefined); + setStakeDialogView(undefined); } function handleNewStake() { setSelectedStake(null); - setDialogStakeView(undefined); + setStakeDialogView(StakeDialogView.SelectValidator); } - const isDialogStakeOpen = dialogStakeView !== undefined; + const isDialogStakeOpen = stakeDialogView !== undefined; return ( <> @@ -92,8 +92,8 @@ function StakingDashboardPage(): JSX.Element { stakedDetails={selectedStake} isOpen={isDialogStakeOpen} handleClose={handleCloseStakeDialog} - view={dialogStakeView} - setView={setDialogStakeView} + view={stakeDialogView} + setView={setStakeDialogView} /> )} </> diff --git a/apps/wallet-dashboard/components/Dialogs/StakeDetails/StakeDetailsDialog.tsx b/apps/wallet-dashboard/components/Dialogs/StakeDetails/StakeDetailsDialog.tsx deleted file mode 100644 index 3d96393d4bc..00000000000 --- a/apps/wallet-dashboard/components/Dialogs/StakeDetails/StakeDetailsDialog.tsx +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { DialogView } from '@/lib/interfaces'; -import { StakeDialogView } from './views'; -import { useState } from 'react'; -import { ExtendedDelegatedStake } from '@iota/core'; -import { Dialog, DialogBody, DialogContent, DialogPosition, Header } from '@iota/apps-ui-kit'; -import { UnstakeDialogView } from '../Unstake'; - -enum DialogViewIdentifier { - StakeDetails = 'StakeDetails', - Unstake = 'Unstake', -} - -interface StakeDetailsProps { - extendedStake: ExtendedDelegatedStake; - showActiveStatus?: boolean; - handleClose: () => void; -} - -export function StakeDetailsDialog({ - extendedStake, - showActiveStatus, - handleClose, -}: StakeDetailsProps) { - const [open, setOpen] = useState(true); - const [currentViewId, setCurrentViewId] = useState<DialogViewIdentifier>( - DialogViewIdentifier.StakeDetails, - ); - - const VIEWS: Record<DialogViewIdentifier, DialogView> = { - [DialogViewIdentifier.StakeDetails]: { - header: <Header title="Stake Details" onClose={handleClose} />, - body: ( - <StakeDialogView - extendedStake={extendedStake} - onUnstake={() => setCurrentViewId(DialogViewIdentifier.Unstake)} - /> - ), - }, - [DialogViewIdentifier.Unstake]: { - header: <Header title="Unstake" onClose={handleClose} />, - body: ( - <UnstakeDialogView - extendedStake={extendedStake} - handleClose={handleClose} - showActiveStatus={showActiveStatus} - /> - ), - }, - }; - - const currentView = VIEWS[currentViewId]; - - return ( - <Dialog - open={open} - onOpenChange={(open) => { - if (!open) { - handleClose(); - } - setOpen(open); - }} - > - <DialogContent containerId="overlay-portal-container" position={DialogPosition.Right}> - {currentView.header} - <div className="flex h-full [&>div]:flex [&>div]:flex-1 [&>div]:flex-col"> - <DialogBody>{currentView.body}</DialogBody> - </div> - </DialogContent> - </Dialog> - ); -} diff --git a/apps/wallet-dashboard/components/Dialogs/StakeDetails/index.ts b/apps/wallet-dashboard/components/Dialogs/StakeDetails/index.ts deleted file mode 100644 index be500ac73c8..00000000000 --- a/apps/wallet-dashboard/components/Dialogs/StakeDetails/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -export * from './StakeDetailsDialog'; diff --git a/apps/wallet-dashboard/components/Dialogs/StakeDetails/views/StakeDetailsView.tsx b/apps/wallet-dashboard/components/Dialogs/StakeDetails/views/StakeDetailsView.tsx deleted file mode 100644 index 8ab7ef3ac75..00000000000 --- a/apps/wallet-dashboard/components/Dialogs/StakeDetails/views/StakeDetailsView.tsx +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import React from 'react'; -import { Button } from '@/components'; -import { ExtendedDelegatedStake } from '@iota/core'; - -interface StakeDialogProps { - extendedStake: ExtendedDelegatedStake; - onUnstake: () => void; -} - -export function StakeDialogView({ extendedStake, onUnstake }: StakeDialogProps): JSX.Element { - return ( - <> - <div className="flex w-full max-w-[336px] flex-1 flex-col"> - <div className="flex w-full max-w-full flex-1 flex-col gap-2 overflow-auto"> - <p>Stake ID: {extendedStake.stakedIotaId}</p> - <p>Validator: {extendedStake.validatorAddress}</p> - <p>Stake: {extendedStake.principal}</p> - <p>Stake Active Epoch: {extendedStake.stakeActiveEpoch}</p> - <p>Stake Request Epoch: {extendedStake.stakeRequestEpoch}</p> - {extendedStake.status === 'Active' && ( - <p>Estimated reward: {extendedStake.estimatedReward}</p> - )} - <p>Status: {extendedStake.status}</p> - </div> - </div> - <div className="flex justify-between gap-2"> - <Button onClick={onUnstake} disabled={extendedStake.status !== 'Active'}> - Unstake - </Button> - <Button onClick={() => console.log('Stake more')}>Stake more</Button> - </div> - </> - ); -} diff --git a/apps/wallet-dashboard/components/Dialogs/StakeDetails/views/index.ts b/apps/wallet-dashboard/components/Dialogs/StakeDetails/views/index.ts deleted file mode 100644 index 145902b67d8..00000000000 --- a/apps/wallet-dashboard/components/Dialogs/StakeDetails/views/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -export * from './StakeDetailsView'; diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx index be8331b865f..faf7487005d 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx @@ -2,26 +2,27 @@ // SPDX-License-Identifier: Apache-2.0 import React, { useState } from 'react'; -import { EnterAmountView, SelectValidatorView, DetailsView } from './views'; +import { EnterAmountView, SelectValidatorView } from './views'; import { useNotifications, useNewStakeTransaction, useGetCurrentEpochStartTimestamp, } from '@/hooks'; import { + ExtendedDelegatedStake, GroupedTimelockObject, parseAmount, TIMELOCK_IOTA_TYPE, useCoinMetadata, useGetAllOwnedObjects, useGetValidatorsApy, - ExtendedDelegatedStake, } from '@iota/core'; import { useCurrentAccount, useSignAndExecuteTransaction } from '@iota/dapp-kit'; import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; import { NotificationType } from '@/stores/notificationStore'; import { prepareObjectsForTimelockedStakingTransaction } from '@/lib/utils'; import { Dialog } from '@iota/apps-ui-kit'; +import { DetailsView, UnstakeView } from './views'; export enum StakeDialogView { Details, @@ -35,16 +36,16 @@ interface StakeDialogProps { onSuccess?: (digest: string) => void; isOpen: boolean; handleClose: () => void; - stakedDetails?: ExtendedDelegatedStake | null; view: StakeDialogView; - setView: (nextView: StakeDialogView) => void; + setView: (view: StakeDialogView) => void; + stakedDetails?: ExtendedDelegatedStake | null; } function StakeDialog({ onSuccess, isTimelockedStaking, isOpen, - handleClose: handleClose, + handleClose, view, setView, stakedDetails, @@ -93,6 +94,20 @@ function StakeDialog({ setSelectedValidator(validator); } + function selectValidatorHandleNext(): void { + if (selectedValidator) { + setView(StakeDialogView.EnterAmount); + } + } + + function detailsHandleUnstake() { + setView(StakeDialogView.Unstake); + } + + function detailsHandleStake() { + setView(StakeDialogView.SelectValidator); + } + function handleStake(): void { if (isTimelockedStaking && groupedTimelockObjects.length === 0) { addNotification('Invalid stake amount. Please try again.', NotificationType.Error); @@ -122,18 +137,6 @@ function StakeDialog({ }); } - function detailsHandleUnstake() { - setView(StakeDialogView.Unstake); - } - - function detailsHandleStake() { - setView(StakeDialogView.SelectValidator); - } - - function selectValidatorHandleNext() { - setView(StakeDialogView.EnterAmount); - } - return ( <Dialog open={isOpen} onOpenChange={() => handleClose()}> {view === StakeDialogView.Details && stakedDetails && ( @@ -157,10 +160,17 @@ function StakeDialog({ <EnterAmountView selectedValidator={selectedValidator} amount={amount} + handleClose={handleClose} onChange={(e) => setAmount(e.target.value)} onBack={handleBack} onStake={handleStake} - isStakeDisabled={!amount} + /> + )} + {view === StakeDialogView.Unstake && stakedDetails && ( + <UnstakeView + extendedStake={stakedDetails} + handleClose={handleClose} + showActiveStatus /> )} </Dialog> diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/StakedDetailsDialog.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/StakedDetailsDialog.tsx new file mode 100644 index 00000000000..4f32601a2dc --- /dev/null +++ b/apps/wallet-dashboard/components/Dialogs/Staking/StakedDetailsDialog.tsx @@ -0,0 +1,197 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; +import { + ExtendedDelegatedStake, + formatPercentageDisplay, + ImageIcon, + ImageIconSize, + useFormatCoin, + useValidatorInfo, +} from '@iota/core'; +import { + Badge, + BadgeType, + Button, + ButtonType, + Card, + CardBody, + CardImage, + CardType, + Dialog, + DialogBody, + DialogContent, + DialogPosition, + Divider, + Header, + InfoBox, + InfoBoxStyle, + InfoBoxType, + KeyValueInfo, + LoadingIndicator, + Panel, +} from '@iota/apps-ui-kit'; +import { Warning } from '@iota/ui-icons'; +import { useUnstakeTransaction } from '@/hooks'; +import { + useCurrentAccount, + useIotaClientQuery, + useSignAndExecuteTransaction, +} from '@iota/dapp-kit'; +import { formatAddress, IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; + +interface StakeDialogProps { + stakedDetails: ExtendedDelegatedStake; + showActiveStatus?: boolean; + handleClose: () => void; +} + +export function StakedDetailsDialog({ + handleClose, + stakedDetails, + showActiveStatus, +}: StakeDialogProps): JSX.Element { + const account = useCurrentAccount(); + const totalStake = BigInt(stakedDetails?.principal || 0n); + const validatorAddress = stakedDetails?.validatorAddress; + const { isPending: loadingValidators, isError: errorValidators } = useIotaClientQuery( + 'getLatestIotaSystemState', + ); + const iotaEarned = BigInt(stakedDetails?.estimatedReward || 0n); + const [iotaEarnedFormatted, iotaEarnedSymbol] = useFormatCoin(iotaEarned, IOTA_TYPE_ARG); + const [totalStakeFormatted, totalStakeSymbol] = useFormatCoin(totalStake, IOTA_TYPE_ARG); + + const { name, commission, newValidator, isAtRisk, apy, isApyApproxZero } = useValidatorInfo({ + validatorAddress: validatorAddress, + }); + + const { data: unstakeData } = useUnstakeTransaction( + stakedDetails.stakedIotaId, + account?.address || '', + ); + const { mutateAsync: signAndExecuteTransaction } = useSignAndExecuteTransaction(); + + const subtitle = showActiveStatus ? ( + <div className="flex items-center gap-1"> + {formatAddress(validatorAddress)} + {newValidator && <Badge label="New" type={BadgeType.PrimarySoft} />} + {isAtRisk && <Badge label="At Risk" type={BadgeType.PrimarySolid} />} + </div> + ) : ( + formatAddress(validatorAddress) + ); + + async function handleUnstake(): Promise<void> { + if (!unstakeData) return; + await signAndExecuteTransaction({ + transaction: unstakeData.transaction, + }); + } + + function handleAddNewStake() { + // pass + } + + if (loadingValidators) { + return ( + <div className="flex h-full w-full items-center justify-center p-2"> + <LoadingIndicator /> + </div> + ); + } + + if (errorValidators) { + return ( + <div className="mb-2 flex h-full w-full items-center justify-center p-2"> + <InfoBox + title="Something went wrong" + supportingText={'An error occurred'} + style={InfoBoxStyle.Default} + type={InfoBoxType.Error} + icon={<Warning />} + /> + </div> + ); + } + + return ( + <Dialog open onOpenChange={handleClose}> + <DialogContent containerId="overlay-portal-container" position={DialogPosition.Right}> + <div className="flex min-h-full flex-col"> + <Header + title="Validator" + onClose={handleClose} + onBack={handleClose} + titleCentered + /> + <div className="flex w-full flex-1 [&_>div]:flex [&_>div]:w-full [&_>div]:flex-col [&_>div]:justify-between"> + <DialogBody> + <div className="flex w-full flex-col gap-md"> + <Card type={CardType.Filled}> + <CardImage> + <ImageIcon + src={null} + label={name} + fallback={name} + size={ImageIconSize.Large} + /> + </CardImage> + <CardBody title={name} subtitle={subtitle} isTextTruncated /> + </Card> + <Panel hasBorder> + <div className="flex flex-col gap-y-sm p-md"> + <KeyValueInfo + keyText="Your Stake" + value={totalStakeFormatted} + supportingLabel={totalStakeSymbol} + fullwidth + /> + <KeyValueInfo + keyText="Earned" + value={iotaEarnedFormatted} + supportingLabel={iotaEarnedSymbol} + fullwidth + /> + <Divider /> + <KeyValueInfo + keyText="APY" + value={formatPercentageDisplay( + apy, + '--', + isApyApproxZero, + )} + fullwidth + /> + <KeyValueInfo + keyText="Commission" + value={`${commission.toString()}%`} + fullwidth + /> + </div> + </Panel> + </div> + <div> + <div className="my-3.75 flex w-full gap-2.5"> + <Button + type={ButtonType.Secondary} + onClick={handleUnstake} + text="Unstake" + fullWidth + /> + <Button + type={ButtonType.Primary} + text="Stake" + onClick={handleAddNewStake} + disabled + fullWidth + /> + </div> + </div> + </DialogBody> + </div> + </div> + </DialogContent> + </Dialog> + ); +} diff --git a/apps/wallet-dashboard/components/Dialogs/Unstake/index.ts b/apps/wallet-dashboard/components/Dialogs/Staking/hooks/index.ts similarity index 68% rename from apps/wallet-dashboard/components/Dialogs/Unstake/index.ts rename to apps/wallet-dashboard/components/Dialogs/Staking/hooks/index.ts index 4a9444c3b5b..9f0940ce152 100644 --- a/apps/wallet-dashboard/components/Dialogs/Unstake/index.ts +++ b/apps/wallet-dashboard/components/Dialogs/Staking/hooks/index.ts @@ -1,4 +1,4 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -export * from './views'; +export * from './useStakeTxsInfo'; diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/hooks/useStakeTxsInfo.ts b/apps/wallet-dashboard/components/Dialogs/Staking/hooks/useStakeTxsInfo.ts new file mode 100644 index 00000000000..86e3fe7cc8e --- /dev/null +++ b/apps/wallet-dashboard/components/Dialogs/Staking/hooks/useStakeTxsInfo.ts @@ -0,0 +1,49 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 +import { useGetTimeBeforeEpochNumber, useTimeAgo, TimeUnit } from '@iota/core'; + +export const NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE = 2; +export const NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_STARTS = 1; + +export function useStakeTxnInfo(startEpoch?: string | number) { + const startEarningRewardsEpoch = + Number(startEpoch || 0) + NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_STARTS; + + const redeemableRewardsEpoch = + Number(startEpoch || 0) + NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE; + + const { data: timeBeforeStakeRewardsStarts } = + useGetTimeBeforeEpochNumber(startEarningRewardsEpoch); + const timeBeforeStakeRewardsStartsAgo = useTimeAgo({ + timeFrom: timeBeforeStakeRewardsStarts, + shortedTimeLabel: false, + shouldEnd: true, + maxTimeUnit: TimeUnit.ONE_HOUR, + }); + const stakedRewardsStartEpoch = + timeBeforeStakeRewardsStarts > 0 + ? `${timeBeforeStakeRewardsStartsAgo === '--' ? '' : 'in'} ${timeBeforeStakeRewardsStartsAgo}` + : startEpoch + ? `Epoch #${Number(startEarningRewardsEpoch)}` + : '--'; + + const { data: timeBeforeStakeRewardsRedeemable } = + useGetTimeBeforeEpochNumber(redeemableRewardsEpoch); + const timeBeforeStakeRewardsRedeemableAgo = useTimeAgo({ + timeFrom: timeBeforeStakeRewardsRedeemable, + shortedTimeLabel: false, + shouldEnd: true, + maxTimeUnit: TimeUnit.ONE_HOUR, + }); + const timeBeforeStakeRewardsRedeemableAgoDisplay = + timeBeforeStakeRewardsRedeemable > 0 + ? `${timeBeforeStakeRewardsRedeemableAgo === '--' ? '' : 'in'} ${timeBeforeStakeRewardsRedeemableAgo}` + : startEpoch + ? `Epoch #${Number(redeemableRewardsEpoch)}` + : '--'; + + return { + stakedRewardsStartEpoch, + timeBeforeStakeRewardsRedeemableAgoDisplay, + }; +} diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/index.ts b/apps/wallet-dashboard/components/Dialogs/Staking/index.ts index 1e5ad764bbc..e415159b7c5 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/index.ts +++ b/apps/wallet-dashboard/components/Dialogs/Staking/index.ts @@ -2,3 +2,4 @@ // SPDX-License-Identifier: Apache-2.0 export { default as StakeDialog } from './StakeDialog'; +export * from './StakedDetailsDialog'; diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx index 98f61fe2cac..6451fa98644 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx @@ -2,41 +2,125 @@ // SPDX-License-Identifier: Apache-2.0 import React from 'react'; -import { Button, Input } from '@/components'; +import { useFormatCoin, useBalance, CoinFormat } from '@iota/core'; +import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; +import { + Button, + ButtonType, + KeyValueInfo, + Panel, + Divider, + Input, + InputType, + Header, +} from '@iota/apps-ui-kit'; +import { useStakeTxnInfo } from '../hooks'; +import { useCurrentAccount, useIotaClientQuery } from '@iota/dapp-kit'; +import { Validator } from './Validator'; +import { StakedInfo } from './StakedInfo'; +import { Layout, LayoutBody, LayoutFooter } from './Layout'; interface EnterAmountViewProps { - selectedValidator: string | null; + selectedValidator: string; amount: string; onChange: (e: React.ChangeEvent<HTMLInputElement>) => void; onBack: () => void; onStake: () => void; - isStakeDisabled: boolean; + showActiveStatus?: boolean; + gasBudget?: string | number | null; + handleClose: () => void; } function EnterAmountView({ - selectedValidator, + selectedValidator: selectedValidatorAddress, amount, onChange, onBack, onStake, - isStakeDisabled, + gasBudget = 0, + handleClose, }: EnterAmountViewProps): JSX.Element { + const account = useCurrentAccount(); + const accountAddress = account?.address; + + const { data: system } = useIotaClientQuery('getLatestIotaSystemState'); + const { data: iotaBalance } = useBalance(accountAddress!); + + const coinBalance = BigInt(iotaBalance?.totalBalance || 0); + const maxTokenBalance = coinBalance - BigInt(Number(gasBudget)); + const [maxTokenFormatted, maxTokenFormattedSymbol] = useFormatCoin( + maxTokenBalance, + IOTA_TYPE_ARG, + CoinFormat.FULL, + ); + const [gas, symbol] = useFormatCoin(gasBudget, IOTA_TYPE_ARG); + const { stakedRewardsStartEpoch, timeBeforeStakeRewardsRedeemableAgoDisplay } = useStakeTxnInfo( + system?.epoch, + ); + return ( - <div className="flex flex-col items-start gap-2"> - <p>Selected Validator: {selectedValidator}</p> - <Input - label="Amount" - value={amount} - onChange={onChange} - placeholder="Enter amount to stake" - /> - <div className="flex w-full justify-between gap-2"> - <Button onClick={onBack}>Back</Button> - <Button onClick={onStake} disabled={isStakeDisabled}> - Stake - </Button> - </div> - </div> + <Layout> + <Header title="Enter amount" onClose={handleClose} onBack={handleClose} titleCentered /> + <LayoutBody> + <div className="flex w-full flex-col justify-between"> + <div> + <Validator + address={selectedValidatorAddress} + isSelected + showAction={false} + /> + <StakedInfo + validatorAddress={selectedValidatorAddress} + accountAddress={accountAddress!} + /> + <div className="my-md w-full"> + <Input + type={InputType.NumericFormat} + label="Amount" + value={amount} + onChange={onChange} + placeholder="Enter amount to stake" + caption={`${maxTokenFormatted} ${maxTokenFormattedSymbol} Available`} + /> + </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} + onClick={onStake} + disabled={!amount} + text="Stake" + /> + </div> + </LayoutFooter> + </Layout> ); } diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/views/SelectValidatorView.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/views/SelectValidatorView.tsx index 73e25ef25d1..f04641ee1df 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/views/SelectValidatorView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/views/SelectValidatorView.tsx @@ -2,109 +2,55 @@ // SPDX-License-Identifier: Apache-2.0 import React from 'react'; -import { ImageIcon, ImageIconSize, formatPercentageDisplay } from '@iota/core'; -import { - Header, - Button, - Card, - CardBody, - CardImage, - CardAction, - CardActionType, - CardType, - Badge, - BadgeType, -} from '@iota/apps-ui-kit'; -import { formatAddress } from '@iota/iota-sdk/utils'; -import { useValidatorInfo } from '@/hooks'; -import { Layout, LayoutFooter, LayoutBody } from './Layout'; +import { Button, Header } from '@iota/apps-ui-kit'; + +import { Validator } from './Validator'; +import { Layout, LayoutBody, LayoutFooter } from './Layout'; + interface SelectValidatorViewProps { validators: string[]; onSelect: (validator: string) => void; - handleClose: () => void; onNext: () => void; selectedValidator: string; + handleClose: () => void; } function SelectValidatorView({ validators, onSelect, - handleClose, onNext, selectedValidator, + handleClose, }: SelectValidatorViewProps): JSX.Element { return ( <Layout> - <Header - title="Select Validator" - onClose={handleClose} - onBack={handleClose} - titleCentered - /> + <Header title="Validator" onClose={handleClose} onBack={handleClose} titleCentered /> <LayoutBody> <div className="flex w-full flex-col gap-md"> - {validators.map((validator) => ( - <Validator - key={validator} - address={validator} - onClick={onSelect} - isSelected={selectedValidator === validator} - /> - ))} + <div className="flex w-full flex-col"> + {validators.map((validator) => ( + <Validator + key={validator} + address={validator} + onClick={onSelect} + isSelected={selectedValidator === validator} + /> + ))} + </div> </div> </LayoutBody> - <LayoutFooter> - {!!selectedValidator && ( + {!!selectedValidator && ( + <LayoutFooter> <Button fullWidth data-testid="select-validator-cta" onClick={onNext} text="Next" /> - )} - </LayoutFooter> + </LayoutFooter> + )} </Layout> ); } -function Validator({ - address, - showActiveStatus, - onClick, - isSelected, -}: { - isSelected: boolean; - address: string; - showActiveStatus?: boolean; - onClick: (address: string) => void; -}) { - const { name, newValidator, isAtRisk, apy, isApyApproxZero } = useValidatorInfo(address); - - const subtitle = showActiveStatus ? ( - <div className="flex items-center gap-1"> - {formatAddress(address)} - {newValidator && <Badge label="New" type={BadgeType.PrimarySoft} />} - {isAtRisk && <Badge label="At Risk" type={BadgeType.PrimarySolid} />} - </div> - ) : ( - formatAddress(address) - ); - - const handleClick = onClick ? () => onClick(address) : undefined; - - return ( - <Card type={isSelected ? CardType.Filled : CardType.Default} onClick={handleClick}> - <CardImage> - <ImageIcon src={null} label={name} fallback={name} size={ImageIconSize.Large} /> - </CardImage> - <CardBody title={name} subtitle={subtitle} isTextTruncated /> - <CardAction - type={CardActionType.SupportingText} - title={formatPercentageDisplay(apy, '--', isApyApproxZero)} - iconAfterText - /> - </Card> - ); -} - export default SelectValidatorView; diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/views/StakedInfo.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/views/StakedInfo.tsx new file mode 100644 index 00000000000..024f9f9384a --- /dev/null +++ b/apps/wallet-dashboard/components/Dialogs/Staking/views/StakedInfo.tsx @@ -0,0 +1,100 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 +import { useMemo } from 'react'; +import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; +import { + formatPercentageDisplay, + calculateStakeShare, + useFormatCoin, + getTokenStakeIotaForValidator, + useGetDelegatedStake, + DELEGATED_STAKES_QUERY_REFETCH_INTERVAL, + DELEGATED_STAKES_QUERY_STALE_TIME, +} from '@iota/core'; +import { KeyValueInfo, Panel, TooltipPosition } from '@iota/apps-ui-kit'; +import { useValidatorInfo } from '@/hooks'; + +export function StakedInfo({ + validatorAddress, + accountAddress, +}: { + validatorAddress: string; + accountAddress: string; +}) { + const { data: delegatedStake } = useGetDelegatedStake({ + address: accountAddress || '', + staleTime: DELEGATED_STAKES_QUERY_STALE_TIME, + refetchInterval: DELEGATED_STAKES_QUERY_REFETCH_INTERVAL, + }); + const { apy, isApyApproxZero, validatorSummary, system } = useValidatorInfo({ + validatorAddress: validatorAddress, + }); + + const totalValidatorsStake = useMemo(() => { + if (!system) return 0; + return system.activeValidators.reduce( + (acc, curr) => (acc += BigInt(curr.stakingPoolIotaBalance)), + 0n, + ); + }, [system]); + + const totalStakePercentage = useMemo(() => { + if (!system || !validatorSummary) return null; + + return calculateStakeShare( + BigInt(validatorSummary.stakingPoolIotaBalance), + BigInt(totalValidatorsStake), + ); + }, [system, totalValidatorsStake, validatorSummary]); + + const totalStake = useMemo(() => { + if (!delegatedStake) return 0n; + return getTokenStakeIotaForValidator(delegatedStake, validatorAddress); + }, [delegatedStake, validatorAddress]); + + //TODO: verify this is the correct validator stake balance + const totalValidatorStake = validatorSummary?.stakingPoolIotaBalance || 0; + + const [totalValidatorStakeFormatted, totalValidatorStakeSymbol] = useFormatCoin( + totalValidatorStake, + IOTA_TYPE_ARG, + ); + const [totalStakeFormatted, totalStakeSymbol] = useFormatCoin(totalStake, IOTA_TYPE_ARG); + + return ( + <Panel hasBorder> + <div className="flex flex-col gap-y-sm p-md"> + <KeyValueInfo + keyText="Staking APY" + tooltipPosition={TooltipPosition.Right} + tooltipText="Annualized percentage yield based on past validator performance. Future APY may vary" + value={formatPercentageDisplay(apy, '--', isApyApproxZero)} + fullwidth + /> + <KeyValueInfo + keyText="Stake Share" + tooltipPosition={TooltipPosition.Right} + tooltipText="Stake percentage managed by this validator." + value={formatPercentageDisplay(totalStakePercentage)} + fullwidth + /> + <KeyValueInfo + keyText="Total Staked" + tooltipPosition={TooltipPosition.Right} + tooltipText="Stake percentage managed by this validator." + value={totalValidatorStakeFormatted} + supportingLabel={totalValidatorStakeSymbol} + fullwidth + /> + <KeyValueInfo + keyText="Your Staked IOTA" + tooltipPosition={TooltipPosition.Right} + tooltipText="Your current staked balance." + value={totalStakeFormatted} + supportingLabel={totalStakeSymbol} + fullwidth + /> + </div> + </Panel> + ); +} diff --git a/apps/wallet-dashboard/components/Dialogs/Unstake/views/UnstakeDialogView.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/views/UnstakeView.tsx similarity index 94% rename from apps/wallet-dashboard/components/Dialogs/Unstake/views/UnstakeDialogView.tsx rename to apps/wallet-dashboard/components/Dialogs/Staking/views/UnstakeView.tsx index 6863f730f30..bdc60e80d20 100644 --- a/apps/wallet-dashboard/components/Dialogs/Unstake/views/UnstakeDialogView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/views/UnstakeView.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { + Header, Button, KeyValueInfo, Divider, @@ -29,6 +30,7 @@ import { useCurrentAccount, useSignAndExecuteTransaction } from '@iota/dapp-kit' import { Loader, Warning } from '@iota/ui-icons'; import { useUnstakeTransaction } from '@/hooks'; import { ValidatorStakingData } from '@/components'; +import { Layout, LayoutFooter, LayoutBody } from './Layout'; interface UnstakeDialogProps { extendedStake: ExtendedDelegatedStake; @@ -36,10 +38,10 @@ interface UnstakeDialogProps { showActiveStatus?: boolean; } -export function UnstakeDialogView({ +export function UnstakeView({ extendedStake, handleClose, - showActiveStatus, + showActiveStatus = true, }: UnstakeDialogProps): JSX.Element { const stakingReward = BigInt(extendedStake.estimatedReward ?? '').toString(); const [rewards, rewardSymbol] = useFormatCoin(stakingReward, IOTA_TYPE_ARG); @@ -123,8 +125,9 @@ export function UnstakeDialogView({ } return ( - <> - <div className="flex h-full w-full flex-col justify-between"> + <Layout> + <Header title="Unstake" onClose={handleClose} onBack={handleClose} titleCentered /> + <LayoutBody> <div className="flex flex-col gap-y-md"> <ValidatorStakingData validatorAddress={extendedStake.validatorAddress} @@ -173,9 +176,9 @@ export function UnstakeDialogView({ </div> </Panel> </div> - </div> + </LayoutBody> - <div className="flex w-full gap-2.5"> + <LayoutFooter> <Button type={ButtonType.Secondary} fullWidth @@ -189,7 +192,7 @@ export function UnstakeDialogView({ } iconAfterText /> - </div> - </> + </LayoutFooter> + </Layout> ); } diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/views/Validator.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/views/Validator.tsx new file mode 100644 index 00000000000..1e281433a77 --- /dev/null +++ b/apps/wallet-dashboard/components/Dialogs/Staking/views/Validator.tsx @@ -0,0 +1,61 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 +import { ImageIcon, ImageIconSize, formatPercentageDisplay } from '@iota/core'; +import { + Card, + CardBody, + CardImage, + CardAction, + CardActionType, + CardType, + Badge, + BadgeType, +} from '@iota/apps-ui-kit'; +import { formatAddress } from '@iota/iota-sdk/utils'; +import { useValidatorInfo } from '@/hooks'; + +export function Validator({ + address, + showActiveStatus, + onClick, + isSelected, + showAction = true, +}: { + isSelected: boolean; + address: string; + showActiveStatus?: boolean; + onClick?: (address: string) => void; + showAction?: boolean; +}) { + const { name, newValidator, isAtRisk, apy, isApyApproxZero } = useValidatorInfo({ + validatorAddress: address, + }); + + const subtitle = showActiveStatus ? ( + <div className="flex items-center gap-1"> + {formatAddress(address)} + {newValidator && <Badge label="New" type={BadgeType.PrimarySoft} />} + {isAtRisk && <Badge label="At Risk" type={BadgeType.PrimarySolid} />} + </div> + ) : ( + formatAddress(address) + ); + + const handleClick = onClick ? () => onClick(address) : undefined; + + return ( + <Card type={isSelected ? CardType.Filled : CardType.Default} onClick={handleClick}> + <CardImage> + <ImageIcon src={null} label={name} fallback={name} size={ImageIconSize.Large} /> + </CardImage> + <CardBody title={name} subtitle={subtitle} isTextTruncated /> + {showAction && ( + <CardAction + type={CardActionType.SupportingText} + title={formatPercentageDisplay(apy, '--', isApyApproxZero)} + iconAfterText + /> + )} + </Card> + ); +} diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/views/index.ts b/apps/wallet-dashboard/components/Dialogs/Staking/views/index.ts index 99227eb60a7..69e70ed7315 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/views/index.ts +++ b/apps/wallet-dashboard/components/Dialogs/Staking/views/index.ts @@ -4,3 +4,4 @@ export { default as EnterAmountView } from './EnterAmountView'; export { default as SelectValidatorView } from './SelectValidatorView'; export * from './DetailsView'; +export * from './UnstakeView'; diff --git a/apps/wallet-dashboard/components/Dialogs/Unstake/views/index.ts b/apps/wallet-dashboard/components/Dialogs/Unstake/views/index.ts deleted file mode 100644 index 665d826ee12..00000000000 --- a/apps/wallet-dashboard/components/Dialogs/Unstake/views/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -export * from './UnstakeDialogView'; diff --git a/apps/wallet-dashboard/components/Dialogs/index.ts b/apps/wallet-dashboard/components/Dialogs/index.ts index e68e1329243..f775cc627b0 100644 --- a/apps/wallet-dashboard/components/Dialogs/index.ts +++ b/apps/wallet-dashboard/components/Dialogs/index.ts @@ -3,4 +3,3 @@ export * from './ReceiveFundsDialog'; export * from './Staking'; -export * from './StakeDetails'; diff --git a/apps/wallet-dashboard/components/Stake/StakeTxnInfo.tsx b/apps/wallet-dashboard/components/Stake/StakeTxnInfo.tsx new file mode 100644 index 00000000000..58daf586008 --- /dev/null +++ b/apps/wallet-dashboard/components/Stake/StakeTxnInfo.tsx @@ -0,0 +1,82 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { Divider, KeyValueInfo, Panel, TooltipPosition } from '@iota/apps-ui-kit'; +import { + NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE, + NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_STARTS, +} from '../../components/Dialogs/Staking/hooks/useStakeTxnInfo'; +import { useGetTimeBeforeEpochNumber, useTimeAgo, TimeUnit, type GasSummaryType } from '@iota/core'; +import { GasSummary } from '../../components/Transaction/GasSummary'; + +interface StakeTxnInfoProps { + apy?: string; + startEpoch?: string | number; + gasSummary?: GasSummaryType; +} + +export function StakeTxnInfo({ apy, startEpoch, gasSummary }: StakeTxnInfoProps) { + const startEarningRewardsEpoch = + Number(startEpoch || 0) + NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_STARTS; + + const redeemableRewardsEpoch = + Number(startEpoch || 0) + NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE; + + const { data: timeBeforeStakeRewardsStarts } = + useGetTimeBeforeEpochNumber(startEarningRewardsEpoch); + const timeBeforeStakeRewardsStartsAgo = useTimeAgo({ + timeFrom: timeBeforeStakeRewardsStarts, + shortedTimeLabel: false, + shouldEnd: true, + maxTimeUnit: TimeUnit.ONE_HOUR, + }); + const stakedRewardsStartEpoch = + timeBeforeStakeRewardsStarts > 0 + ? `${timeBeforeStakeRewardsStartsAgo === '--' ? '' : 'in'} ${timeBeforeStakeRewardsStartsAgo}` + : startEpoch + ? `Epoch #${Number(startEarningRewardsEpoch)}` + : '--'; + + const { data: timeBeforeStakeRewardsRedeemable } = + useGetTimeBeforeEpochNumber(redeemableRewardsEpoch); + const timeBeforeStakeRewardsRedeemableAgo = useTimeAgo({ + timeFrom: timeBeforeStakeRewardsRedeemable, + shortedTimeLabel: false, + shouldEnd: true, + maxTimeUnit: TimeUnit.ONE_HOUR, + }); + const timeBeforeStakeRewardsRedeemableAgoDisplay = + timeBeforeStakeRewardsRedeemable > 0 + ? `${timeBeforeStakeRewardsRedeemableAgo === '--' ? '' : 'in'} ${timeBeforeStakeRewardsRedeemableAgo}` + : startEpoch + ? `Epoch #${Number(redeemableRewardsEpoch)}` + : '--'; + return ( + <Panel hasBorder> + <div className="flex flex-col gap-y-sm p-md"> + {apy && ( + <KeyValueInfo + keyText="APY" + value={apy} + tooltipText="This is the Annualized Percentage Yield of the a specific validator’s past operations. Note there is no guarantee this APY will be true in the future." + tooltipPosition={TooltipPosition.Right} + fullwidth + /> + )} + <KeyValueInfo + keyText="Staking Rewards Start" + value={stakedRewardsStartEpoch} + fullwidth + /> + <KeyValueInfo + keyText="Redeem Rewards" + value={timeBeforeStakeRewardsRedeemableAgoDisplay} + fullwidth + /> + <Divider /> + {gasSummary && <GasSummary gasSummary={gasSummary} />} + </div> + </Panel> + ); +} diff --git a/apps/wallet-dashboard/components/staking-overview/StartStaking.tsx b/apps/wallet-dashboard/components/staking-overview/StartStaking.tsx index dd2e0ef9b81..dd28cca199c 100644 --- a/apps/wallet-dashboard/components/staking-overview/StartStaking.tsx +++ b/apps/wallet-dashboard/components/staking-overview/StartStaking.tsx @@ -5,22 +5,15 @@ import { Button, ButtonSize, ButtonType, Panel } from '@iota/apps-ui-kit'; import { Theme, useTheme } from '@/contexts'; import { useState } from 'react'; import { StakeDialog } from '../Dialogs'; -import { StakeDialogView } from '../Dialogs/Staking/StakeDialog'; export function StartStaking() { const { theme } = useTheme(); - const [dialogStakeView, setDialogStakeView] = useState<StakeDialogView | undefined>(); + const [isDialogStakeOpen, setIsDialogStakeOpen] = useState(false); function handleNewStake() { - setDialogStakeView(StakeDialogView.SelectValidator); + setIsDialogStakeOpen(true); } - function handleClose() { - setDialogStakeView(undefined); - } - - const isDialogStakeOpen = dialogStakeView !== undefined; - const videoSrc = theme === Theme.Dark ? 'https://files.iota.org/media/tooling/wallet-dashboard-staking-dark.mp4' @@ -57,14 +50,7 @@ export function StartStaking() { ></video> </div> </div> - {isDialogStakeOpen && ( - <StakeDialog - isOpen={isDialogStakeOpen} - handleClose={handleClose} - view={dialogStakeView} - setView={setDialogStakeView} - /> - )} + <StakeDialog isOpen={isDialogStakeOpen} setOpen={setIsDialogStakeOpen} /> </Panel> ); } diff --git a/apps/wallet-dashboard/components/transactions/GasSummary.tsx b/apps/wallet-dashboard/components/transactions/GasSummary.tsx index 3b522432a40..a0b45708b11 100644 --- a/apps/wallet-dashboard/components/transactions/GasSummary.tsx +++ b/apps/wallet-dashboard/components/transactions/GasSummary.tsx @@ -6,7 +6,7 @@ import { useFormatCoin, type GasSummaryType } from '@iota/core'; import { useCurrentAccount } from '@iota/dapp-kit'; import { formatAddress, IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; -export default function GasSummary({ gasSummary }: { gasSummary: GasSummaryType }) { +export function GasSummary({ gasSummary }: { gasSummary: GasSummaryType }) { const [gas, symbol] = useFormatCoin(gasSummary?.totalGas, IOTA_TYPE_ARG); const address = useCurrentAccount(); @@ -38,3 +38,5 @@ export default function GasSummary({ gasSummary }: { gasSummary: GasSummaryType </div> ); } + +export default GasSummary; diff --git a/apps/wallet-dashboard/hooks/index.ts b/apps/wallet-dashboard/hooks/index.ts index 394f13ab30a..f262ea2e616 100644 --- a/apps/wallet-dashboard/hooks/index.ts +++ b/apps/wallet-dashboard/hooks/index.ts @@ -10,4 +10,4 @@ export * from './useCreateSendAssetTransaction'; export * from './useGetCurrentEpochStartTimestamp'; export * from './useTimelockedUnstakeTransaction'; export * from './useExplorerLinkGetter'; -export * from './useValidatorInfo'; +export * from '@iota/core/src/hooks/stake/useValidatorInfo'; From 78e552795d9cfd63d895ad18a2544c5d142778aa Mon Sep 17 00:00:00 2001 From: cpl121 <cpeon@boxfish.studio> Date: Thu, 14 Nov 2024 12:05:33 +0100 Subject: [PATCH 43/87] fix(wallet-dashboard): max button disabled --- .../components/Inputs/SendTokenFormInput.tsx | 40 ++++++++----------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/apps/core/src/components/Inputs/SendTokenFormInput.tsx b/apps/core/src/components/Inputs/SendTokenFormInput.tsx index 2298d433260..43acef9d909 100644 --- a/apps/core/src/components/Inputs/SendTokenFormInput.tsx +++ b/apps/core/src/components/Inputs/SendTokenFormInput.tsx @@ -4,24 +4,23 @@ import { ButtonPill, Input, InputType, NumericFormatInputProps } from '@iota/apps-ui-kit'; import { CoinStruct } from '@iota/iota-sdk/client'; import { useGasBudgetEstimation } from '../../hooks'; -import React, { useEffect } from 'react'; +import React, { ComponentProps, useEffect } from 'react'; +import { Field, FieldInputProps } from 'formik'; export interface SendTokenInputProps { coins: CoinStruct[]; symbol: string; coinDecimals: number; activeAddress: string; - setFieldValue: (field: string, value: string, shouldValidate?: boolean) => void; values: { amount: string; to: string; isPayAllIota: boolean; }; onActionClick: () => Promise<void>; - isMaxActionDisabled?: boolean | 'auto'; - value: string; - onChange: (value: string) => void; - onBlur?: React.FocusEventHandler<HTMLInputElement>; + isMaxActionDisabled?: boolean; + field: FieldInputProps<string>; + form: ComponentProps<typeof Field>; errorMessage?: string; } @@ -31,12 +30,10 @@ export function SendTokenFormInput({ symbol, coinDecimals, activeAddress, - setFieldValue, onActionClick, isMaxActionDisabled, - value, - onChange, - onBlur, + field, + form, errorMessage, }: SendTokenInputProps) { const gasBudgetEstimation = useGasBudgetEstimation({ @@ -52,42 +49,39 @@ export function SendTokenFormInput({ decimalScale: coinDecimals ? undefined : 0, thousandSeparator: true, onValueChange: (values) => { - onChange(values.value); + form.setFieldValue(field.name, values.value).then(() => { + form.validateField(field.name); + }); }, }; - const isActionButtonDisabled = !value || !!errorMessage; + const isActionButtonDisabled = form.isSubmitting || !!errorMessage || isMaxActionDisabled; - const renderAction = (isButtonDisabled: boolean | undefined) => ( - <ButtonPill - disabled={isMaxActionDisabled === 'auto' ? isButtonDisabled : isActionButtonDisabled} - onClick={onActionClick} - > + const renderAction = () => ( + <ButtonPill disabled={isActionButtonDisabled} onClick={onActionClick}> Max </ButtonPill> ); // gasBudgetEstimation should change when the amount above changes useEffect(() => { - setFieldValue('gasBudgetEst', gasBudgetEstimation, false); - }, [gasBudgetEstimation, setFieldValue, values.amount]); + form.setFieldValue('gasBudgetEst', gasBudgetEstimation, false); + }, [gasBudgetEstimation, form.setFieldValue, values.amount]); return ( <Input type={InputType.NumericFormat} name={'amount'} - value={value} + value={field.value} caption="Est. Gas Fees:" placeholder="0.00" - onBlur={onBlur} label="Send Amount" suffix={` ${symbol}`} prefix={values.isPayAllIota ? '~ ' : undefined} allowNegative={false} errorMessage={errorMessage} - onChange={(e) => onChange(e.currentTarget.value)} amountCounter={!errorMessage ? (coins ? gasBudgetEstimation : '--') : undefined} - trailingElement={renderAction(isActionButtonDisabled)} + trailingElement={renderAction()} {...numericPropsOnly} /> ); From 1fe1d0580e8dbf0c3efc06453c01f1174ac9c330 Mon Sep 17 00:00:00 2001 From: cpl121 <cpeon@boxfish.studio> Date: Thu, 14 Nov 2024 12:05:45 +0100 Subject: [PATCH 44/87] feat(wallet-dashboard): improvements --- .../Dialogs/SendToken/SendTokenDialog.tsx | 11 +++----- .../SendToken/views/EnterValuesFormView.tsx | 28 +++++++++---------- .../SendToken/views/ReviewValuesFormView.tsx | 6 ++-- .../hooks/useSendCoinTransaction.ts | 6 ---- .../home/transfer-coin/SendTokenForm.tsx | 18 ++++++------ 5 files changed, 30 insertions(+), 39 deletions(-) diff --git a/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx b/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx index 35118aa1228..3c8f70b9e66 100644 --- a/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx @@ -40,14 +40,13 @@ function SendTokenDialogBody({ coin, activeAddress, setOpen, - open, }: SendCoinPopupProps): React.JSX.Element { const [step, setStep] = useState<FormStep>(FormStep.EnterValues); const [selectedCoin, setSelectedCoin] = useState<CoinBalance>(coin); const [formData, setFormData] = useState<FormDataValues>(INITIAL_VALUES); const { addNotification } = useNotifications(); - const { data: coinsData } = useGetAllCoins(selectedCoin?.coinType, activeAddress); + const { data: coinsData } = useGetAllCoins(selectedCoin.coinType, activeAddress); const { mutateAsync: signAndExecuteTransaction, @@ -55,7 +54,7 @@ function SendTokenDialogBody({ isPending, } = useSignAndExecuteTransaction(); - const { data: sendCoinData } = useSendCoinTransaction( + const { data: transaction } = useSendCoinTransaction( coinsData || [], selectedCoin?.coinType, activeAddress, @@ -65,12 +64,12 @@ function SendTokenDialogBody({ ); function handleTransfer() { - if (!sendCoinData?.transaction) { + if (!transaction) { addNotification('There was an error with the transaction', NotificationType.Error); return; } else { signAndExecuteTransaction({ - transaction: sendCoinData.transaction, + transaction, }) .then(() => { setOpen(false); @@ -102,7 +101,6 @@ function SendTokenDialogBody({ <EnterValuesFormView coin={selectedCoin} activeAddress={activeAddress} - gasBudget={sendCoinData?.gasBudget?.toString() || '--'} setSelectedCoin={setSelectedCoin} onNext={onNext} setFormData={setFormData} @@ -114,7 +112,6 @@ function SendTokenDialogBody({ onBack={onBack} executeTransfer={handleTransfer} senderAddress={activeAddress} - gasBudget={sendCoinData?.gasBudget?.toString() || '--'} error={error?.message} isPending={isPending} /> diff --git a/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx b/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx index b7c5e437f61..a77f81a36a9 100644 --- a/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx @@ -31,11 +31,11 @@ import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; import { Field, FieldInputProps, Form, Formik, FormikProps } from 'formik'; import { Exclamation } from '@iota/ui-icons'; import { UseQueryResult } from '@tanstack/react-query'; +import { ComponentProps } from 'react'; interface EnterValuesFormProps { coin: CoinBalance; activeAddress: string; - gasBudget: string; setFormData: React.Dispatch<React.SetStateAction<FormDataValues>>; setSelectedCoin: React.Dispatch<React.SetStateAction<CoinBalance>>; onNext: () => void; @@ -68,7 +68,6 @@ function FormInputs({ submitForm, touched, errors, - handleBlur, coinType, coinDecimals, coinBalance, @@ -95,13 +94,8 @@ function FormInputs({ await setFieldValue('amount', formattedTokenBalance); } - function handleOnChangeAmountInput(value: string, symbol: string) { - const valueWithoutSuffix = value.replace(symbol, ''); - setFieldValue('amount', valueWithoutSuffix); - } - const isMaxActionDisabled = - parseAmount(values?.amount, coinDecimals) === coinBalance || + parseAmount(values.amount, coinDecimals) === coinBalance || queryResult.isPending || !coinBalance; @@ -119,20 +113,24 @@ function FormInputs({ )} <Field name="amount"> - {({ field }: { field: FieldInputProps<string> }) => { + {({ + field, + form, + }: { + field: FieldInputProps<string>; + form: ComponentProps<typeof Field>; + }) => { return ( <SendTokenFormInput + form={form} + field={field} symbol={symbol} coins={coins} coinDecimals={coinDecimals} activeAddress={activeAddress} - setFieldValue={setFieldValue} values={values} onActionClick={onMaxTokenButtonClick} isMaxActionDisabled={isMaxActionDisabled} - value={field.value} - onChange={(value) => handleOnChangeAmountInput(value, symbol)} - onBlur={handleBlur} errorMessage={ touched.amount && errors.amount ? errors.amount : undefined } @@ -231,9 +229,11 @@ function EnterValuesFormView({ .sort((a, b) => Number(b.balance) - Number(a.balance)) .map(({ coinObjectId }) => coinObjectId); + const formattedAmount = parseAmount(amount, coinDecimals).toString(); + const data = { to, - amount, + amount: formattedAmount, isPayAllIota, coins, coinIds: coinsIDs, diff --git a/apps/wallet-dashboard/components/Dialogs/SendToken/views/ReviewValuesFormView.tsx b/apps/wallet-dashboard/components/Dialogs/SendToken/views/ReviewValuesFormView.tsx index 97151a1cf24..282f0a19be9 100644 --- a/apps/wallet-dashboard/components/Dialogs/SendToken/views/ReviewValuesFormView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/views/ReviewValuesFormView.tsx @@ -7,7 +7,6 @@ import { Button } from '@/components'; interface ReviewValuesFormProps { formData: FormDataValues; senderAddress: string; - gasBudget: string; error: string | undefined; isPending: boolean; executeTransfer: () => void; @@ -15,9 +14,8 @@ interface ReviewValuesFormProps { } function ReviewValuesFormView({ - formData: { amount, to }, + formData: { amount, to, gasBudgetEst }, senderAddress, - gasBudget, error, isPending, executeTransfer, @@ -30,7 +28,7 @@ function ReviewValuesFormView({ <p>Sending: {amount}</p> <p>From: {senderAddress}</p> <p>To: {to}</p> - <p>Gas fee: {gasBudget}</p> + <p>Gas fee: {gasBudgetEst}</p> </div> {error ? <span className="text-red-700">{error}</span> : null} <div className="mt-4 flex justify-around"> diff --git a/apps/wallet-dashboard/hooks/useSendCoinTransaction.ts b/apps/wallet-dashboard/hooks/useSendCoinTransaction.ts index 8b793e6405d..b439c27a2df 100644 --- a/apps/wallet-dashboard/hooks/useSendCoinTransaction.ts +++ b/apps/wallet-dashboard/hooks/useSendCoinTransaction.ts @@ -45,11 +45,5 @@ export function useSendCoinTransaction( }, enabled: !!recipientAddress && !!amount && !!coins && !!senderAddress && !!coinType, gcTime: 0, - select: (transaction) => { - return { - transaction, - gasBudget: transaction.getData().gasData.budget, - }; - }, }); } diff --git a/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx b/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx index e60870cd219..2c75f497387 100644 --- a/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx +++ b/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx @@ -17,7 +17,7 @@ import { import { type CoinStruct } from '@iota/iota-sdk/client'; import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; import { Field, type FieldInputProps, Form, Formik } from 'formik'; -import { useMemo } from 'react'; +import { ComponentProps, useMemo } from 'react'; import { InfoBox, @@ -198,22 +198,24 @@ export function SendTokenForm({ ) : null} <Field name="amount"> - {({ field }: { field: FieldInputProps<string> }) => { + {({ + field, + form, + }: { + field: FieldInputProps<string>; + form: ComponentProps<typeof Field>; + }) => { return ( <SendTokenFormInput + form={form} + field={field} symbol={symbol} coinDecimals={coinDecimals} activeAddress={activeAddress ?? ''} - setFieldValue={setFieldValue} coins={coins ?? []} values={values} onActionClick={onMaxTokenButtonClick} isMaxActionDisabled={isMaxActionDisabled} - value={field.value} - onChange={(value) => - handleOnChangeAmountInput(value, symbol) - } - onBlur={handleBlur} errorMessage={ touched.amount && errors.amount ? errors.amount From 94bcca1f99ad7cc2172a7935c7defa6fc5ad698a Mon Sep 17 00:00:00 2001 From: cpl121 <cpeon@boxfish.studio> Date: Thu, 14 Nov 2024 13:36:08 +0100 Subject: [PATCH 45/87] fix(wallet-dashboard): improve formik props --- .../src/components/Inputs/AddressInput.tsx | 23 ++++++++--------- .../components/Inputs/SendTokenFormInput.tsx | 21 ++++++++-------- .../SendToken/views/EnterValuesFormView.tsx | 19 ++++---------- .../home/transfer-coin/SendTokenForm.tsx | 25 ++++--------------- 4 files changed, 31 insertions(+), 57 deletions(-) diff --git a/apps/core/src/components/Inputs/AddressInput.tsx b/apps/core/src/components/Inputs/AddressInput.tsx index 3b91350ebed..bcfe125eaac 100644 --- a/apps/core/src/components/Inputs/AddressInput.tsx +++ b/apps/core/src/components/Inputs/AddressInput.tsx @@ -5,24 +5,23 @@ import { Input, InputType } from '@iota/apps-ui-kit'; import { Close } from '@iota/ui-icons'; import { useIotaAddressValidation } from '../../hooks'; -import React, { ComponentProps, useCallback } from 'react'; -import type { Field, FieldInputProps } from 'formik'; +import React, { useCallback } from 'react'; +import { useField } from 'formik'; export interface AddressInputProps { - field: FieldInputProps<string>; - form: ComponentProps<typeof Field>; + name: string, disabled?: boolean; placeholder?: string; label?: string; } export function AddressInput({ - field, - form, + name, disabled, placeholder = '0x...', label = 'Enter Recipient Address', }: AddressInputProps) { + const [field, meta, helpers] = useField<string>(name) const iotaAddressValidation = useIotaAddressValidation(); const formattedValue = iotaAddressValidation.cast(field.value); @@ -31,18 +30,16 @@ export function AddressInput({ (e: React.ChangeEvent<HTMLInputElement>) => { const address = e.currentTarget.value; iotaAddressValidation.cast(address); - form.setFieldValue(field.name, iotaAddressValidation.cast(address)).then(() => { - form.validateField(field.name); - }); + helpers.setValue(iotaAddressValidation.cast(address)); }, - [form, field.name, iotaAddressValidation], + [name, iotaAddressValidation], ); const clearAddress = () => { - form.setFieldValue(field.name, ''); + helpers.setValue(''); }; - const errorMessage = form.touched[field.name] && form.errors[field.name]; + const errorMessage = meta.touched && meta.error; return ( <Input @@ -68,4 +65,4 @@ export function AddressInput({ } /> ); -} +} \ No newline at end of file diff --git a/apps/core/src/components/Inputs/SendTokenFormInput.tsx b/apps/core/src/components/Inputs/SendTokenFormInput.tsx index 43acef9d909..3956c18f573 100644 --- a/apps/core/src/components/Inputs/SendTokenFormInput.tsx +++ b/apps/core/src/components/Inputs/SendTokenFormInput.tsx @@ -4,10 +4,10 @@ import { ButtonPill, Input, InputType, NumericFormatInputProps } from '@iota/apps-ui-kit'; import { CoinStruct } from '@iota/iota-sdk/client'; import { useGasBudgetEstimation } from '../../hooks'; -import React, { ComponentProps, useEffect } from 'react'; -import { Field, FieldInputProps } from 'formik'; +import { useEffect } from 'react'; +import { FormikProps, useField } from 'formik'; -export interface SendTokenInputProps { +export interface SendTokenInputProps<FormValues> { coins: CoinStruct[]; symbol: string; coinDecimals: number; @@ -19,12 +19,11 @@ export interface SendTokenInputProps { }; onActionClick: () => Promise<void>; isMaxActionDisabled?: boolean; - field: FieldInputProps<string>; - form: ComponentProps<typeof Field>; - errorMessage?: string; + name: string; + form: FormikProps<FormValues>; } -export function SendTokenFormInput({ +export function SendTokenFormInput<FormValues>({ coins, values, symbol, @@ -32,10 +31,9 @@ export function SendTokenFormInput({ activeAddress, onActionClick, isMaxActionDisabled, - field, + name, form, - errorMessage, -}: SendTokenInputProps) { +}: SendTokenInputProps<FormValues>) { const gasBudgetEstimation = useGasBudgetEstimation({ coinDecimals, coins: coins ?? [], @@ -45,6 +43,8 @@ export function SendTokenFormInput({ isPayAllIota: values.isPayAllIota, }); + const [field, meta] = useField<string>(name); + const numericPropsOnly: Partial<NumericFormatInputProps> = { decimalScale: coinDecimals ? undefined : 0, thousandSeparator: true, @@ -55,6 +55,7 @@ export function SendTokenFormInput({ }, }; + const errorMessage = meta?.error ? meta.error : undefined; const isActionButtonDisabled = form.isSubmitting || !!errorMessage || isMaxActionDisabled; const renderAction = () => ( diff --git a/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx b/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx index a77f81a36a9..91021dfd2e2 100644 --- a/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx @@ -31,7 +31,6 @@ import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; import { Field, FieldInputProps, Form, Formik, FormikProps } from 'formik'; import { Exclamation } from '@iota/ui-icons'; import { UseQueryResult } from '@tanstack/react-query'; -import { ComponentProps } from 'react'; interface EnterValuesFormProps { coin: CoinBalance; @@ -118,12 +117,12 @@ function FormInputs({ form, }: { field: FieldInputProps<string>; - form: ComponentProps<typeof Field>; + form: FormikProps<FormDataValues>; }) => { return ( <SendTokenFormInput form={form} - field={field} + name={field.name} symbol={symbol} coins={coins} coinDecimals={coinDecimals} @@ -131,20 +130,12 @@ function FormInputs({ values={values} onActionClick={onMaxTokenButtonClick} isMaxActionDisabled={isMaxActionDisabled} - errorMessage={ - touched.amount && errors.amount ? errors.amount : undefined - } /> ); }} </Field> - <Field - component={AddressInput} - name="to" - placeholder="Enter Address" - errorMessage={touched.to && errors.to ? errors.to : undefined} - /> + <AddressInput name="to" placeholder="Enter Address" /> </div> </Form> @@ -267,8 +258,8 @@ function EnterValuesFormView({ }} validationSchema={validationSchemaStepOne} enableReinitialize - validateOnChange={false} - validateOnBlur={false} + validateOnChange={true} + validateOnBlur={true} onSubmit={handleFormSubmit} > {(props: FormikProps<FormDataValues>) => ( diff --git a/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx b/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx index 2c75f497387..edc5fda3a04 100644 --- a/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx +++ b/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx @@ -16,8 +16,8 @@ import { } from '@iota/core'; import { type CoinStruct } from '@iota/iota-sdk/client'; import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; -import { Field, type FieldInputProps, Form, Formik } from 'formik'; -import { ComponentProps, useMemo } from 'react'; +import { Field, type FieldInputProps, Form, Formik, FormikProps } from 'formik'; +import { useMemo } from 'react'; import { InfoBox, @@ -174,11 +174,6 @@ export function SendTokenForm({ await setFieldValue('amount', formattedTokenBalance); } - function handleOnChangeAmountInput(value: string, symbol: string) { - const valueWithoutSuffix = value.replace(symbol, ''); - setFieldValue('amount', valueWithoutSuffix); - } - const isMaxActionDisabled = parseAmount(values?.amount, coinDecimals) === coinBalance || queryResult.isPending || @@ -203,12 +198,12 @@ export function SendTokenForm({ form, }: { field: FieldInputProps<string>; - form: ComponentProps<typeof Field>; + form: FormikProps<FormValues>; }) => { return ( <SendTokenFormInput form={form} - field={field} + name={field.name} symbol={symbol} coinDecimals={coinDecimals} activeAddress={activeAddress ?? ''} @@ -216,21 +211,11 @@ export function SendTokenForm({ values={values} onActionClick={onMaxTokenButtonClick} isMaxActionDisabled={isMaxActionDisabled} - errorMessage={ - touched.amount && errors.amount - ? errors.amount - : undefined - } /> ); }} </Field> - <Field - component={AddressInput} - allowNegative={false} - name="to" - placeholder="Enter Address" - /> + <AddressInput name="to" placeholder="Enter Address" /> </div> </Form> From 17058ad9be7bd965721c80515c172e537d86ae4c Mon Sep 17 00:00:00 2001 From: cpl121 <cpeon@boxfish.studio> Date: Thu, 14 Nov 2024 14:00:17 +0100 Subject: [PATCH 46/87] fix(wallet-dashboard): improvements --- .../src/components/Inputs/AddressInput.tsx | 1 - .../components/Inputs/SendTokenFormInput.tsx | 31 +++++++++---------- .../SendToken/views/EnterValuesFormView.tsx | 6 ++-- .../home/transfer-coin/SendTokenForm.tsx | 11 +++---- 4 files changed, 23 insertions(+), 26 deletions(-) diff --git a/apps/core/src/components/Inputs/AddressInput.tsx b/apps/core/src/components/Inputs/AddressInput.tsx index bcfe125eaac..187e773ad06 100644 --- a/apps/core/src/components/Inputs/AddressInput.tsx +++ b/apps/core/src/components/Inputs/AddressInput.tsx @@ -29,7 +29,6 @@ export function AddressInput({ const handleOnChange = useCallback( (e: React.ChangeEvent<HTMLInputElement>) => { const address = e.currentTarget.value; - iotaAddressValidation.cast(address); helpers.setValue(iotaAddressValidation.cast(address)); }, [name, iotaAddressValidation], diff --git a/apps/core/src/components/Inputs/SendTokenFormInput.tsx b/apps/core/src/components/Inputs/SendTokenFormInput.tsx index 3956c18f573..c3346cef1f9 100644 --- a/apps/core/src/components/Inputs/SendTokenFormInput.tsx +++ b/apps/core/src/components/Inputs/SendTokenFormInput.tsx @@ -6,17 +6,16 @@ import { CoinStruct } from '@iota/iota-sdk/client'; import { useGasBudgetEstimation } from '../../hooks'; import { useEffect } from 'react'; import { FormikProps, useField } from 'formik'; +import React from 'react'; export interface SendTokenInputProps<FormValues> { coins: CoinStruct[]; symbol: string; coinDecimals: number; activeAddress: string; - values: { - amount: string; - to: string; - isPayAllIota: boolean; - }; + amount: string; + to: string; + isPayAllIota: boolean; onActionClick: () => Promise<void>; isMaxActionDisabled?: boolean; name: string; @@ -25,7 +24,9 @@ export interface SendTokenInputProps<FormValues> { export function SendTokenFormInput<FormValues>({ coins, - values, + amount, + to, + isPayAllIota, symbol, coinDecimals, activeAddress, @@ -38,20 +39,18 @@ export function SendTokenFormInput<FormValues>({ coinDecimals, coins: coins ?? [], activeAddress, - to: values.to, - amount: values.amount, - isPayAllIota: values.isPayAllIota, + to: to, + amount: amount, + isPayAllIota: isPayAllIota, }); - const [field, meta] = useField<string>(name); + const [field, meta, helpers] = useField<string>(name); const numericPropsOnly: Partial<NumericFormatInputProps> = { decimalScale: coinDecimals ? undefined : 0, thousandSeparator: true, onValueChange: (values) => { - form.setFieldValue(field.name, values.value).then(() => { - form.validateField(field.name); - }); + helpers.setValue(values.value) }, }; @@ -67,18 +66,18 @@ export function SendTokenFormInput<FormValues>({ // gasBudgetEstimation should change when the amount above changes useEffect(() => { form.setFieldValue('gasBudgetEst', gasBudgetEstimation, false); - }, [gasBudgetEstimation, form.setFieldValue, values.amount]); + }, [gasBudgetEstimation, form.setFieldValue, amount]); return ( <Input type={InputType.NumericFormat} - name={'amount'} + name="amount" value={field.value} caption="Est. Gas Fees:" placeholder="0.00" label="Send Amount" suffix={` ${symbol}`} - prefix={values.isPayAllIota ? '~ ' : undefined} + prefix={isPayAllIota ? '~ ' : undefined} allowNegative={false} errorMessage={errorMessage} amountCounter={!errorMessage ? (coins ? gasBudgetEstimation : '--') : undefined} diff --git a/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx b/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx index 91021dfd2e2..d43b4ab5f21 100644 --- a/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx @@ -65,8 +65,6 @@ function FormInputs({ setFieldValue, values, submitForm, - touched, - errors, coinType, coinDecimals, coinBalance, @@ -122,12 +120,14 @@ function FormInputs({ return ( <SendTokenFormInput form={form} + amount={values.amount} + to={values.to} + isPayAllIota={values.isPayAllIota} name={field.name} symbol={symbol} coins={coins} coinDecimals={coinDecimals} activeAddress={activeAddress} - values={values} onActionClick={onMaxTokenButtonClick} isMaxActionDisabled={isMaxActionDisabled} /> diff --git a/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx b/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx index edc5fda3a04..b6b91783fd3 100644 --- a/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx +++ b/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx @@ -140,8 +140,8 @@ export function SendTokenForm({ }} validationSchema={validationSchemaStepOne} enableReinitialize - validateOnChange={false} - validateOnBlur={false} + validateOnChange={true} + validateOnBlur={true} onSubmit={handleFormSubmit} > {({ @@ -150,9 +150,6 @@ export function SendTokenForm({ setFieldValue, values, submitForm, - handleBlur, - touched, - errors, }) => { const newPayIotaAll = parseAmount(values.amount, coinDecimals) === coinBalance && @@ -204,11 +201,13 @@ export function SendTokenForm({ <SendTokenFormInput form={form} name={field.name} + amount={values.amount} + to={values.to} + isPayAllIota={values.isPayAllIota} symbol={symbol} coinDecimals={coinDecimals} activeAddress={activeAddress ?? ''} coins={coins ?? []} - values={values} onActionClick={onMaxTokenButtonClick} isMaxActionDisabled={isMaxActionDisabled} /> From 5932ed507588c294f19a829cf29f3329e13b841b Mon Sep 17 00:00:00 2001 From: Eugene P <panteleymonchuk@gmail.com> Date: Thu, 14 Nov 2024 15:40:10 +0200 Subject: [PATCH 47/87] feat(tooling-core): move validation schema --- apps/core/package.json | 4 ++-- .../src/utils/stake/createValidationSchema.ts} | 2 +- apps/core/src/utils/stake/index.ts | 1 + .../components/Dialogs/Staking/StakeDialog.tsx | 1 + .../Dialogs/Staking/views/EnterAmountView.tsx | 12 +++++++----- apps/wallet/src/ui/app/staking/stake/StakingCard.tsx | 2 +- apps/wallet/tsconfig.json | 2 +- pnpm-lock.yaml | 4 ++-- 8 files changed, 16 insertions(+), 12 deletions(-) rename apps/{wallet/src/ui/app/staking/stake/utils/validation.ts => core/src/utils/stake/createValidationSchema.ts} (97%) diff --git a/apps/core/package.json b/apps/core/package.json index f21c6ff85e5..cafc062f6aa 100644 --- a/apps/core/package.json +++ b/apps/core/package.json @@ -31,14 +31,14 @@ "@iota/kiosk": "workspace:*", "@sentry/react": "^7.59.2", "@tanstack/react-query": "^5.50.1", - "bignumber.js": "^9.1.1", + "bignumber.js": "^9.1.2", "clsx": "^2.1.1", "qrcode.react": "^4.0.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.45.2", "vanilla-cookieconsent": "^2.9.1", - "yup": "^1.1.1", + "yup": "^1.4.0", "zod": "^3.21.4" }, "devDependencies": { diff --git a/apps/wallet/src/ui/app/staking/stake/utils/validation.ts b/apps/core/src/utils/stake/createValidationSchema.ts similarity index 97% rename from apps/wallet/src/ui/app/staking/stake/utils/validation.ts rename to apps/core/src/utils/stake/createValidationSchema.ts index 198efb8a941..56e6ada0f1a 100644 --- a/apps/wallet/src/ui/app/staking/stake/utils/validation.ts +++ b/apps/core/src/utils/stake/createValidationSchema.ts @@ -2,7 +2,7 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { CoinFormat, formatBalance } from '@iota/core'; +import { CoinFormat, formatBalance } from '../../index'; import BigNumber from 'bignumber.js'; import { mixed, object } from 'yup'; diff --git a/apps/core/src/utils/stake/index.ts b/apps/core/src/utils/stake/index.ts index 5f7d694c839..6ecca7353f6 100644 --- a/apps/core/src/utils/stake/index.ts +++ b/apps/core/src/utils/stake/index.ts @@ -6,3 +6,4 @@ export * from './formatDelegatedStake'; export * from './createStakeTransaction'; export * from './createTimelockedUnstakeTransaction'; export * from './createTimelockedStakeTransaction'; +export * from './createValidationSchema'; diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx index faf7487005d..2507e411fd0 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx @@ -130,6 +130,7 @@ function StakeDialog({ }, ) .then(() => { + handleClose(); addNotification('Stake transaction has been sent'); }) .catch(() => { diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx index 6451fa98644..eac33a1437b 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx @@ -64,11 +64,13 @@ function EnterAmountView({ <LayoutBody> <div className="flex w-full flex-col justify-between"> <div> - <Validator - address={selectedValidatorAddress} - isSelected - showAction={false} - /> + <div className="mb-md"> + <Validator + address={selectedValidatorAddress} + isSelected + showAction={false} + /> + </div> <StakedInfo validatorAddress={selectedValidatorAddress} accountAddress={accountAddress!} diff --git a/apps/wallet/src/ui/app/staking/stake/StakingCard.tsx b/apps/wallet/src/ui/app/staking/stake/StakingCard.tsx index 1d045c11f13..110e014c68c 100644 --- a/apps/wallet/src/ui/app/staking/stake/StakingCard.tsx +++ b/apps/wallet/src/ui/app/staking/stake/StakingCard.tsx @@ -16,6 +16,7 @@ import { DELEGATED_STAKES_QUERY_REFETCH_INTERVAL, DELEGATED_STAKES_QUERY_STALE_TIME, getStakeIotaByIotaId, + createValidationSchema, } from '@iota/core'; import { useIotaClientQuery } from '@iota/dapp-kit'; import type { StakeObject } from '@iota/iota-sdk/client'; @@ -33,7 +34,6 @@ import { useSigner } from '../../hooks/useSigner'; import { getDelegationDataByStakeId } from '../getDelegationByStakeId'; import StakeForm from './StakeForm'; import { UnStakeForm } from './UnstakeForm'; -import { createValidationSchema } from './utils/validation'; import { ValidatorFormDetail } from './ValidatorFormDetail'; import { Button, ButtonType, CardType } from '@iota/apps-ui-kit'; import { ValidatorLogo } from '../validators/ValidatorLogo'; diff --git a/apps/wallet/tsconfig.json b/apps/wallet/tsconfig.json index 15c97c419b0..4da06a3efe1 100644 --- a/apps/wallet/tsconfig.json +++ b/apps/wallet/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "./configs/ts/tsconfig.dev", - "include": ["src", "configs", "tests"], + "include": ["src", "configs", "tests", "../core/src/utils/stake/createValidationSchema.ts"], "compilerOptions": { "noEmit": true } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ac50a94b23..3297cdf4ffa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -238,7 +238,7 @@ importers: specifier: ^5.50.1 version: 5.56.2(react@18.3.1) bignumber.js: - specifier: ^9.1.1 + specifier: ^9.1.2 version: 9.1.2 clsx: specifier: ^2.1.1 @@ -259,7 +259,7 @@ importers: specifier: ^2.9.1 version: 2.9.2 yup: - specifier: ^1.1.1 + specifier: ^1.4.0 version: 1.4.0 zod: specifier: ^3.21.4 From ddca44cc60bc16ea6e69f54ab65630678e070a62 Mon Sep 17 00:00:00 2001 From: marc2332 <mespinsanz@gmail.com> Date: Thu, 14 Nov 2024 15:46:12 +0100 Subject: [PATCH 48/87] refactor: Simplify SendTokenFormInput --- .../components/Inputs/SendTokenFormInput.tsx | 45 ++++++++----------- apps/core/src/forms/index.ts | 1 + apps/core/src/forms/token.ts | 6 +++ apps/core/src/index.ts | 1 + .../SendToken/views/EnterValuesFormView.tsx | 36 +++++---------- .../home/transfer-coin/SendTokenForm.tsx | 36 +++++---------- 6 files changed, 46 insertions(+), 79 deletions(-) create mode 100644 apps/core/src/forms/index.ts create mode 100644 apps/core/src/forms/token.ts diff --git a/apps/core/src/components/Inputs/SendTokenFormInput.tsx b/apps/core/src/components/Inputs/SendTokenFormInput.tsx index c3346cef1f9..932daeb3799 100644 --- a/apps/core/src/components/Inputs/SendTokenFormInput.tsx +++ b/apps/core/src/components/Inputs/SendTokenFormInput.tsx @@ -1,61 +1,48 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { ButtonPill, Input, InputType, NumericFormatInputProps } from '@iota/apps-ui-kit'; +import { ButtonPill, Input, InputType } from '@iota/apps-ui-kit'; import { CoinStruct } from '@iota/iota-sdk/client'; import { useGasBudgetEstimation } from '../../hooks'; import { useEffect } from 'react'; -import { FormikProps, useField } from 'formik'; -import React from 'react'; +import { useField, useFormikContext } from 'formik'; +import { TokenForm } from '../../forms'; -export interface SendTokenInputProps<FormValues> { +export interface SendTokenInputProps { coins: CoinStruct[]; symbol: string; coinDecimals: number; activeAddress: string; - amount: string; to: string; - isPayAllIota: boolean; onActionClick: () => Promise<void>; isMaxActionDisabled?: boolean; name: string; - form: FormikProps<FormValues>; } -export function SendTokenFormInput<FormValues>({ +export function SendTokenFormInput({ coins, - amount, to, - isPayAllIota, symbol, coinDecimals, activeAddress, onActionClick, isMaxActionDisabled, name, - form, -}: SendTokenInputProps<FormValues>) { +}: SendTokenInputProps) { + const { values, setFieldValue, isSubmitting } = useFormikContext<TokenForm>(); const gasBudgetEstimation = useGasBudgetEstimation({ coinDecimals, coins: coins ?? [], activeAddress, to: to, - amount: amount, - isPayAllIota: isPayAllIota, + amount: values.amount, + isPayAllIota: values.isPayAllIota, }); const [field, meta, helpers] = useField<string>(name); - const numericPropsOnly: Partial<NumericFormatInputProps> = { - decimalScale: coinDecimals ? undefined : 0, - thousandSeparator: true, - onValueChange: (values) => { - helpers.setValue(values.value) - }, - }; - const errorMessage = meta?.error ? meta.error : undefined; - const isActionButtonDisabled = form.isSubmitting || !!errorMessage || isMaxActionDisabled; + const isActionButtonDisabled = isSubmitting || !!errorMessage || isMaxActionDisabled; const renderAction = () => ( <ButtonPill disabled={isActionButtonDisabled} onClick={onActionClick}> @@ -65,8 +52,8 @@ export function SendTokenFormInput<FormValues>({ // gasBudgetEstimation should change when the amount above changes useEffect(() => { - form.setFieldValue('gasBudgetEst', gasBudgetEstimation, false); - }, [gasBudgetEstimation, form.setFieldValue, amount]); + setFieldValue('gasBudgetEst', gasBudgetEstimation, false); + }, [gasBudgetEstimation, setFieldValue, values.amount]); return ( <Input @@ -77,12 +64,16 @@ export function SendTokenFormInput<FormValues>({ placeholder="0.00" label="Send Amount" suffix={` ${symbol}`} - prefix={isPayAllIota ? '~ ' : undefined} + prefix={values.isPayAllIota ? '~ ' : undefined} allowNegative={false} errorMessage={errorMessage} amountCounter={!errorMessage ? (coins ? gasBudgetEstimation : '--') : undefined} trailingElement={renderAction()} - {...numericPropsOnly} + decimalScale={coinDecimals ? undefined : 0} + thousandSeparator + onValueChange={(values) => { + helpers.setValue(values.value) + }} /> ); } diff --git a/apps/core/src/forms/index.ts b/apps/core/src/forms/index.ts new file mode 100644 index 00000000000..b00ee65e230 --- /dev/null +++ b/apps/core/src/forms/index.ts @@ -0,0 +1 @@ +export * from './token' \ No newline at end of file diff --git a/apps/core/src/forms/token.ts b/apps/core/src/forms/token.ts new file mode 100644 index 00000000000..dde7d93379c --- /dev/null +++ b/apps/core/src/forms/token.ts @@ -0,0 +1,6 @@ +export type TokenForm = { + amount: string, + to: string, + isPayAllIota: boolean + gasBudgetEst: string, +} \ No newline at end of file diff --git a/apps/core/src/index.ts b/apps/core/src/index.ts index 5aed649aada..231b6bfd581 100644 --- a/apps/core/src/index.ts +++ b/apps/core/src/index.ts @@ -8,3 +8,4 @@ export * from './components'; export * from './utils'; export * from './hooks'; export * from './constants'; +export * from './forms' diff --git a/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx b/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx index d43b4ab5f21..0b5954630b5 100644 --- a/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx @@ -109,32 +109,16 @@ function FormInputs({ /> )} - <Field name="amount"> - {({ - field, - form, - }: { - field: FieldInputProps<string>; - form: FormikProps<FormDataValues>; - }) => { - return ( - <SendTokenFormInput - form={form} - amount={values.amount} - to={values.to} - isPayAllIota={values.isPayAllIota} - name={field.name} - symbol={symbol} - coins={coins} - coinDecimals={coinDecimals} - activeAddress={activeAddress} - onActionClick={onMaxTokenButtonClick} - isMaxActionDisabled={isMaxActionDisabled} - /> - ); - }} - </Field> - + <SendTokenFormInput + name="amount" + to={values.to} + symbol={symbol} + coins={coins} + coinDecimals={coinDecimals} + activeAddress={activeAddress} + onActionClick={onMaxTokenButtonClick} + isMaxActionDisabled={isMaxActionDisabled} + /> <AddressInput name="to" placeholder="Enter Address" /> </div> </Form> diff --git a/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx b/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx index b6b91783fd3..096cb3d0af0 100644 --- a/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx +++ b/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx @@ -188,32 +188,16 @@ export function SendTokenForm({ icon={<Exclamation />} /> ) : null} - - <Field name="amount"> - {({ - field, - form, - }: { - field: FieldInputProps<string>; - form: FormikProps<FormValues>; - }) => { - return ( - <SendTokenFormInput - form={form} - name={field.name} - amount={values.amount} - to={values.to} - isPayAllIota={values.isPayAllIota} - symbol={symbol} - coinDecimals={coinDecimals} - activeAddress={activeAddress ?? ''} - coins={coins ?? []} - onActionClick={onMaxTokenButtonClick} - isMaxActionDisabled={isMaxActionDisabled} - /> - ); - }} - </Field> + <SendTokenFormInput + name="amount" + to={values.to} + symbol={symbol} + coinDecimals={coinDecimals} + activeAddress={activeAddress ?? ''} + coins={coins ?? []} + onActionClick={onMaxTokenButtonClick} + isMaxActionDisabled={isMaxActionDisabled} + /> <AddressInput name="to" placeholder="Enter Address" /> </div> </Form> From 76d2e893413c2909a9e8f0835b8958e0fdaf1e4f Mon Sep 17 00:00:00 2001 From: marc2332 <mespinsanz@gmail.com> Date: Thu, 14 Nov 2024 15:50:20 +0100 Subject: [PATCH 49/87] refactor: prettier:fix --- .../src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx b/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx index 096cb3d0af0..887689a0f92 100644 --- a/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx +++ b/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx @@ -144,13 +144,7 @@ export function SendTokenForm({ validateOnBlur={true} onSubmit={handleFormSubmit} > - {({ - isValid, - isSubmitting, - setFieldValue, - values, - submitForm, - }) => { + {({ isValid, isSubmitting, setFieldValue, values, submitForm }) => { const newPayIotaAll = parseAmount(values.amount, coinDecimals) === coinBalance && coinType === IOTA_TYPE_ARG; From 6b388b6a93e3eea5636d3451ad584f5986415766 Mon Sep 17 00:00:00 2001 From: marc2332 <mespinsanz@gmail.com> Date: Thu, 14 Nov 2024 15:51:01 +0100 Subject: [PATCH 50/87] refactor: prettier:fix on apps/core --- apps/core/src/components/Inputs/AddressInput.tsx | 6 +++--- apps/core/src/components/Inputs/SendTokenFormInput.tsx | 2 +- apps/core/src/forms/index.ts | 2 +- apps/core/src/forms/token.ts | 10 +++++----- apps/core/src/index.ts | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/core/src/components/Inputs/AddressInput.tsx b/apps/core/src/components/Inputs/AddressInput.tsx index 187e773ad06..d0bf195ba0f 100644 --- a/apps/core/src/components/Inputs/AddressInput.tsx +++ b/apps/core/src/components/Inputs/AddressInput.tsx @@ -9,7 +9,7 @@ import React, { useCallback } from 'react'; import { useField } from 'formik'; export interface AddressInputProps { - name: string, + name: string; disabled?: boolean; placeholder?: string; label?: string; @@ -21,7 +21,7 @@ export function AddressInput({ placeholder = '0x...', label = 'Enter Recipient Address', }: AddressInputProps) { - const [field, meta, helpers] = useField<string>(name) + const [field, meta, helpers] = useField<string>(name); const iotaAddressValidation = useIotaAddressValidation(); const formattedValue = iotaAddressValidation.cast(field.value); @@ -64,4 +64,4 @@ export function AddressInput({ } /> ); -} \ No newline at end of file +} diff --git a/apps/core/src/components/Inputs/SendTokenFormInput.tsx b/apps/core/src/components/Inputs/SendTokenFormInput.tsx index 932daeb3799..1b7f0be19ca 100644 --- a/apps/core/src/components/Inputs/SendTokenFormInput.tsx +++ b/apps/core/src/components/Inputs/SendTokenFormInput.tsx @@ -72,7 +72,7 @@ export function SendTokenFormInput({ decimalScale={coinDecimals ? undefined : 0} thousandSeparator onValueChange={(values) => { - helpers.setValue(values.value) + helpers.setValue(values.value); }} /> ); diff --git a/apps/core/src/forms/index.ts b/apps/core/src/forms/index.ts index b00ee65e230..6b36029d17c 100644 --- a/apps/core/src/forms/index.ts +++ b/apps/core/src/forms/index.ts @@ -1 +1 @@ -export * from './token' \ No newline at end of file +export * from './token'; diff --git a/apps/core/src/forms/token.ts b/apps/core/src/forms/token.ts index dde7d93379c..e8732381984 100644 --- a/apps/core/src/forms/token.ts +++ b/apps/core/src/forms/token.ts @@ -1,6 +1,6 @@ export type TokenForm = { - amount: string, - to: string, - isPayAllIota: boolean - gasBudgetEst: string, -} \ No newline at end of file + amount: string; + to: string; + isPayAllIota: boolean; + gasBudgetEst: string; +}; diff --git a/apps/core/src/index.ts b/apps/core/src/index.ts index 231b6bfd581..6d113bf483b 100644 --- a/apps/core/src/index.ts +++ b/apps/core/src/index.ts @@ -8,4 +8,4 @@ export * from './components'; export * from './utils'; export * from './hooks'; export * from './constants'; -export * from './forms' +export * from './forms'; From 78af8b44e905318bdfcdd7877eb544769a57c6cb Mon Sep 17 00:00:00 2001 From: marc2332 <mespinsanz@gmail.com> Date: Thu, 14 Nov 2024 16:05:31 +0100 Subject: [PATCH 51/87] refactor: Add missing license header to token.ts --- apps/core/src/forms/token.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/core/src/forms/token.ts b/apps/core/src/forms/token.ts index e8732381984..abad79c1100 100644 --- a/apps/core/src/forms/token.ts +++ b/apps/core/src/forms/token.ts @@ -1,3 +1,6 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + export type TokenForm = { amount: string; to: string; From 3e9fa71e4f6fa7b7f198110159f73419d35d1ab6 Mon Sep 17 00:00:00 2001 From: cpl121 <cpeon@boxfish.studio> Date: Thu, 14 Nov 2024 16:13:00 +0100 Subject: [PATCH 52/87] fix: linter --- apps/core/src/forms/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/core/src/forms/index.ts b/apps/core/src/forms/index.ts index 6b36029d17c..220b7ec05d5 100644 --- a/apps/core/src/forms/index.ts +++ b/apps/core/src/forms/index.ts @@ -1 +1,4 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + export * from './token'; From 9bdb88706ce18b702c7099a71767b2b254417cda Mon Sep 17 00:00:00 2001 From: Eugene P <panteleymonchuk@gmail.com> Date: Thu, 14 Nov 2024 17:18:56 +0200 Subject: [PATCH 53/87] feat(wallet-dashboard): integrate Formik --- .../Dialogs/Staking/StakeDialog.tsx | 114 ++++++++++++------ .../Dialogs/Staking/views/EnterAmountView.tsx | 80 +++++++++--- apps/wallet-dashboard/package.json | 1 + pnpm-lock.yaml | 39 +++--- 4 files changed, 166 insertions(+), 68 deletions(-) diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx index 2507e411fd0..0eba55af41b 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { EnterAmountView, SelectValidatorView } from './views'; import { useNotifications, @@ -16,7 +16,11 @@ import { useCoinMetadata, useGetAllOwnedObjects, useGetValidatorsApy, + useBalance, + createValidationSchema, } from '@iota/core'; +import { Formik } from 'formik'; +import type { FormikHelpers } from 'formik'; import { useCurrentAccount, useSignAndExecuteTransaction } from '@iota/dapp-kit'; import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; import { NotificationType } from '@/stores/notificationStore'; @@ -24,6 +28,8 @@ import { prepareObjectsForTimelockedStakingTransaction } from '@/lib/utils'; import { Dialog } from '@iota/apps-ui-kit'; import { DetailsView, UnstakeView } from './views'; +export const MIN_NUMBER_IOTA_TO_STAKE = 1; + export enum StakeDialogView { Details, SelectValidator, @@ -31,6 +37,10 @@ export enum StakeDialogView { Unstake, } +const INITIAL_VALUES = { + amount: '', +}; + interface StakeDialogProps { isTimelockedStaking?: boolean; onSuccess?: (digest: string) => void; @@ -83,9 +93,26 @@ function StakeDialog({ const { mutateAsync: signAndExecuteTransaction } = useSignAndExecuteTransaction(); const { addNotification } = useNotifications(); const { data: rollingAverageApys } = useGetValidatorsApy(); + const { data: iotaBalance } = useBalance(senderAddress!); + const coinBalance = BigInt(iotaBalance?.totalBalance || 0); + const minimumStake = parseAmount(MIN_NUMBER_IOTA_TO_STAKE.toString(), coinDecimals); + const coinSymbol = metadata?.symbol ?? ''; + console.log('symbol', coinSymbol); const validators = Object.keys(rollingAverageApys ?? {}) ?? []; + const validationSchema = useMemo( + () => + createValidationSchema( + coinBalance, + coinSymbol, + coinDecimals, + view === StakeDialogView.Unstake, + minimumStake, + ), + [coinBalance, coinSymbol, coinDecimals, view, minimumStake], + ); + function handleBack(): void { setView(StakeDialogView.SelectValidator); } @@ -138,42 +165,59 @@ function StakeDialog({ }); } + function onSubmit( + values: Record<string, string>, + { resetForm }: FormikHelpers<Record<string, string>>, + ) { + setAmount(values.amount); + handleStake(); + resetForm(); + } + return ( <Dialog open={isOpen} onOpenChange={() => handleClose()}> - {view === StakeDialogView.Details && stakedDetails && ( - <DetailsView - handleStake={detailsHandleStake} - handleUnstake={detailsHandleUnstake} - stakedDetails={stakedDetails} - handleClose={handleClose} - /> - )} - {view === StakeDialogView.SelectValidator && ( - <SelectValidatorView - selectedValidator={selectedValidator} - handleClose={handleClose} - validators={validators} - onSelect={handleValidatorSelect} - onNext={selectValidatorHandleNext} - /> - )} - {view === StakeDialogView.EnterAmount && ( - <EnterAmountView - selectedValidator={selectedValidator} - amount={amount} - handleClose={handleClose} - onChange={(e) => setAmount(e.target.value)} - onBack={handleBack} - onStake={handleStake} - /> - )} - {view === StakeDialogView.Unstake && stakedDetails && ( - <UnstakeView - extendedStake={stakedDetails} - handleClose={handleClose} - showActiveStatus - /> - )} + <Formik + initialValues={INITIAL_VALUES} + validationSchema={validationSchema} + onSubmit={onSubmit} + validateOnMount + > + <> + {view === StakeDialogView.Details && stakedDetails && ( + <DetailsView + handleStake={detailsHandleStake} + handleUnstake={detailsHandleUnstake} + stakedDetails={stakedDetails} + handleClose={handleClose} + /> + )} + {view === StakeDialogView.SelectValidator && ( + <SelectValidatorView + selectedValidator={selectedValidator} + handleClose={handleClose} + validators={validators} + onSelect={handleValidatorSelect} + onNext={selectValidatorHandleNext} + /> + )} + {view === StakeDialogView.EnterAmount && ( + <EnterAmountView + selectedValidator={selectedValidator} + handleClose={handleClose} + onBack={handleBack} + onStake={handleStake} + gasBudget={newStakeData?.gasBudget} + /> + )} + {view === StakeDialogView.Unstake && stakedDetails && ( + <UnstakeView + extendedStake={stakedDetails} + handleClose={handleClose} + showActiveStatus + /> + )} + </> + </Formik> </Dialog> ); } diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx index eac33a1437b..d2accb21454 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import React from 'react'; -import { useFormatCoin, useBalance, CoinFormat } from '@iota/core'; +import { useFormatCoin, useBalance, CoinFormat, parseAmount, useCoinMetadata } from '@iota/core'; import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; import { Button, @@ -13,17 +13,25 @@ import { Input, InputType, Header, + InfoBoxType, + InfoBoxStyle, + InfoBox, } from '@iota/apps-ui-kit'; -import { useStakeTxnInfo } from '../hooks'; +import { Field, type FieldProps, useFormikContext } from 'formik'; +import { Exclamation } from '@iota/ui-icons'; import { useCurrentAccount, useIotaClientQuery } from '@iota/dapp-kit'; + +import { useStakeTxnInfo } from '../hooks'; import { Validator } from './Validator'; import { StakedInfo } from './StakedInfo'; import { Layout, LayoutBody, LayoutFooter } from './Layout'; +interface FormValues { + amount: string; +} + interface EnterAmountViewProps { selectedValidator: string; - amount: string; - onChange: (e: React.ChangeEvent<HTMLInputElement>) => void; onBack: () => void; onStake: () => void; showActiveStatus?: boolean; @@ -33,31 +41,48 @@ interface EnterAmountViewProps { function EnterAmountView({ selectedValidator: selectedValidatorAddress, - amount, - onChange, onBack, onStake, gasBudget = 0, handleClose, }: EnterAmountViewProps): JSX.Element { + const coinType = IOTA_TYPE_ARG; + const { data: metadata } = useCoinMetadata(coinType); + const decimals = metadata?.decimals ?? 0; + const account = useCurrentAccount(); const accountAddress = account?.address; + const { values } = useFormikContext<FormValues>(); + const amount = values.amount; + + console.log('amount', amount); + const { data: system } = useIotaClientQuery('getLatestIotaSystemState'); const { data: iotaBalance } = useBalance(accountAddress!); - const coinBalance = BigInt(iotaBalance?.totalBalance || 0); + const maxTokenBalance = coinBalance - BigInt(Number(gasBudget)); const [maxTokenFormatted, maxTokenFormattedSymbol] = useFormatCoin( maxTokenBalance, IOTA_TYPE_ARG, CoinFormat.FULL, ); + + const _gasBudget = BigInt(gasBudget ?? 0); const [gas, symbol] = useFormatCoin(gasBudget, IOTA_TYPE_ARG); + const { stakedRewardsStartEpoch, timeBeforeStakeRewardsRedeemableAgoDisplay } = useStakeTxnInfo( system?.epoch, ); + const hasEnoughRemaingBalance = + maxTokenBalance > parseAmount(values.amount, decimals) + BigInt(2) * _gasBudget; + const shouldShowInsufficientRemainingFundsWarning = + maxTokenFormatted >= values.amount && !hasEnoughRemaingBalance; + + console.log(gasBudget); + return ( <Layout> <Header title="Enter amount" onClose={handleClose} onBack={handleClose} titleCentered /> @@ -76,14 +101,39 @@ function EnterAmountView({ accountAddress={accountAddress!} /> <div className="my-md w-full"> - <Input - type={InputType.NumericFormat} - label="Amount" - value={amount} - onChange={onChange} - placeholder="Enter amount to stake" - caption={`${maxTokenFormatted} ${maxTokenFormattedSymbol} Available`} - /> + <Field name="amount"> + {({ + field: { onChange, ...field }, + form: { setFieldValue }, + meta, + }: FieldProps<FormValues>) => { + return ( + <Input + {...field} + onValueChange={(values) => + setFieldValue('amount', values.value, true) + } + type={InputType.NumericFormat} + label="Amount" + value={amount} + onChange={onChange} + placeholder="Enter amount to stake" + errorMessage={ + values.amount && meta.error ? meta.error : undefined + } + caption={`${maxTokenFormatted} ${maxTokenFormattedSymbol} Available`} + /> + ); + }} + </Field> + {shouldShowInsufficientRemainingFundsWarning ? ( + <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 />} + /> + ) : null} </div> <Panel hasBorder> diff --git a/apps/wallet-dashboard/package.json b/apps/wallet-dashboard/package.json index 9093f6da53e..44066a00ef1 100644 --- a/apps/wallet-dashboard/package.json +++ b/apps/wallet-dashboard/package.json @@ -25,6 +25,7 @@ "@tanstack/react-query": "^5.50.1", "@tanstack/react-virtual": "^3.5.0", "clsx": "^2.1.1", + "formik": "^2.4.2", "next": "14.2.10", "react": "^18.3.1", "react-hot-toast": "^2.4.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3297cdf4ffa..8f2f99ca591 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -179,7 +179,7 @@ importers: version: 5.2.1(@types/eslint@8.56.12)(eslint-config-prettier@9.1.0(eslint@8.57.1))(eslint@8.57.1)(prettier@3.3.3) jest: specifier: ^29.5.0 - version: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) + version: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) prettier: specifier: ^3.3.1 version: 3.3.3 @@ -191,7 +191,7 @@ importers: version: 6.3.4 ts-jest: specifier: ^29.1.0 - version: 29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)))(typescript@5.6.2) + version: 29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)))(typescript@5.6.2) ts-loader: specifier: ^9.4.4 version: 9.5.1(typescript@5.6.2)(webpack@5.95.0(@swc/core@1.7.28)) @@ -1024,6 +1024,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + formik: + specifier: ^2.4.2 + version: 2.4.6(react@18.3.1) next: specifier: 14.2.10 version: 14.2.10(@babel/core@7.25.2)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.79.3) @@ -1054,7 +1057,7 @@ importers: version: 14.2.3(eslint@8.57.1)(typescript@5.6.2) jest: specifier: ^29.5.0 - version: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) + version: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) postcss: specifier: ^8.4.31 version: 8.4.47 @@ -1063,7 +1066,7 @@ importers: version: 3.4.13(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) ts-jest: specifier: ^29.1.0 - version: 29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)))(typescript@5.6.2) + version: 29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)))(typescript@5.6.2) typescript: specifier: ^5.5.3 version: 5.6.2 @@ -20582,7 +20585,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2))': + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0(node-notifier@10.0.0) @@ -20596,7 +20599,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) + jest-config: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -26520,13 +26523,13 @@ snapshots: crc-32@1.2.2: {} - create-jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)): + create-jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) + jest-config: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -29753,16 +29756,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)): + jest-cli@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) + create-jest: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) + jest-config: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -29774,7 +29777,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)): + jest-config@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)): dependencies: '@babel/core': 7.25.2 '@jest/test-sequencer': 29.7.0 @@ -30031,12 +30034,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)): + jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) + jest-cli: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) optionalDependencies: node-notifier: 10.0.0 transitivePeerDependencies: @@ -34653,12 +34656,12 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)))(typescript@5.6.2): + ts-jest@29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)))(typescript@5.6.2): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) + jest: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 From 012b77304e86277c5ac9da4d35cb9eccd93e29f0 Mon Sep 17 00:00:00 2001 From: cpl121 <cpeon@boxfish.studio> Date: Thu, 14 Nov 2024 16:36:54 +0100 Subject: [PATCH 54/87] fix(wallet-dashboard): linter --- .../components/Dialogs/SendToken/views/EnterValuesFormView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx b/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx index 0b5954630b5..2ad0fdba7cc 100644 --- a/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx @@ -28,7 +28,7 @@ import { } from '@iota/apps-ui-kit'; import { useIotaClientQuery } from '@iota/dapp-kit'; import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; -import { Field, FieldInputProps, Form, Formik, FormikProps } from 'formik'; +import { Form, Formik, FormikProps } from 'formik'; import { Exclamation } from '@iota/ui-icons'; import { UseQueryResult } from '@tanstack/react-query'; From e97e8795c00928d62a415b8fa56d247e3849c991 Mon Sep 17 00:00:00 2001 From: cpl121 <cpeon@boxfish.studio> Date: Thu, 14 Nov 2024 16:42:37 +0100 Subject: [PATCH 55/87] fix(wallet-dashboard): linter --- .../src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx b/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx index 887689a0f92..57c8e28730d 100644 --- a/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx +++ b/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx @@ -16,7 +16,7 @@ import { } from '@iota/core'; import { type CoinStruct } from '@iota/iota-sdk/client'; import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; -import { Field, type FieldInputProps, Form, Formik, FormikProps } from 'formik'; +import { Form, Formik } from 'formik'; import { useMemo } from 'react'; import { From 14a2786b642c26dd9f72e18aba0279fe0dec4199 Mon Sep 17 00:00:00 2001 From: Eugene P <panteleymonchuk@gmail.com> Date: Thu, 14 Nov 2024 18:55:46 +0200 Subject: [PATCH 56/87] feat(wallet-dashboard): enhance StakeDialog and EnterAmountView with FormValues integration --- .../Dialogs/Staking/StakeDialog.tsx | 11 ++++---- .../Dialogs/Staking/views/EnterAmountView.tsx | 26 +++++++++++-------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx index 0eba55af41b..b59deef129b 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx @@ -27,6 +27,7 @@ import { NotificationType } from '@/stores/notificationStore'; import { prepareObjectsForTimelockedStakingTransaction } from '@/lib/utils'; import { Dialog } from '@iota/apps-ui-kit'; import { DetailsView, UnstakeView } from './views'; +import { FormValues } from './views/EnterAmountView'; export const MIN_NUMBER_IOTA_TO_STAKE = 1; @@ -62,6 +63,7 @@ function StakeDialog({ }: StakeDialogProps): JSX.Element { const [selectedValidator, setSelectedValidator] = useState<string>(''); const [amount, setAmount] = useState<string>(''); + const account = useCurrentAccount(); const senderAddress = account?.address ?? ''; @@ -165,15 +167,13 @@ function StakeDialog({ }); } - function onSubmit( - values: Record<string, string>, - { resetForm }: FormikHelpers<Record<string, string>>, - ) { - setAmount(values.amount); + function onSubmit(values: FormValues, { resetForm }: FormikHelpers<FormValues>) { handleStake(); resetForm(); } + console.log(newStakeData?.gasBudget); + return ( <Dialog open={isOpen} onOpenChange={() => handleClose()}> <Formik @@ -204,6 +204,7 @@ function StakeDialog({ <EnterAmountView selectedValidator={selectedValidator} handleClose={handleClose} + setAmount={setAmount} onBack={handleBack} onStake={handleStake} gasBudget={newStakeData?.gasBudget} diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx index d2accb21454..c0332d3efe7 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import React from 'react'; +import React, { useEffect } from 'react'; import { useFormatCoin, useBalance, CoinFormat, parseAmount, useCoinMetadata } from '@iota/core'; import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; import { @@ -26,12 +26,13 @@ import { Validator } from './Validator'; import { StakedInfo } from './StakedInfo'; import { Layout, LayoutBody, LayoutFooter } from './Layout'; -interface FormValues { +export interface FormValues { amount: string; } interface EnterAmountViewProps { selectedValidator: string; + setAmount: (amount: string) => void; onBack: () => void; onStake: () => void; showActiveStatus?: boolean; @@ -45,6 +46,7 @@ function EnterAmountView({ onStake, gasBudget = 0, handleClose, + setAmount, }: EnterAmountViewProps): JSX.Element { const coinType = IOTA_TYPE_ARG; const { data: metadata } = useCoinMetadata(coinType); @@ -56,8 +58,6 @@ function EnterAmountView({ const { values } = useFormikContext<FormValues>(); const amount = values.amount; - console.log('amount', amount); - const { data: system } = useIotaClientQuery('getLatestIotaSystemState'); const { data: iotaBalance } = useBalance(accountAddress!); const coinBalance = BigInt(iotaBalance?.totalBalance || 0); @@ -81,7 +81,9 @@ function EnterAmountView({ const shouldShowInsufficientRemainingFundsWarning = maxTokenFormatted >= values.amount && !hasEnoughRemaingBalance; - console.log(gasBudget); + useEffect(() => { + setAmount(amount); + }, [amount, setAmount]); return ( <Layout> @@ -127,12 +129,14 @@ function EnterAmountView({ }} </Field> {shouldShowInsufficientRemainingFundsWarning ? ( - <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 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> From c1341dc818d2296b8f7305d2fc6b9864fc57815c Mon Sep 17 00:00:00 2001 From: Eugene P <panteleymonchuk@gmail.com> Date: Thu, 14 Nov 2024 19:16:09 +0200 Subject: [PATCH 57/87] feat(wallet-dashboard): update StakeDialog to support selectedValidator and optional setView --- .../app/(protected)/staking/page.tsx | 4 + .../Dialogs/Staking/StakeDialog.tsx | 25 ++- .../Dialogs/Staking/StakedDetailsDialog.tsx | 197 ------------------ .../components/Dialogs/Staking/index.ts | 3 +- .../components/Stake/StakeTxnInfo.tsx | 82 -------- .../staking-overview/StartStaking.tsx | 7 +- 6 files changed, 23 insertions(+), 295 deletions(-) delete mode 100644 apps/wallet-dashboard/components/Dialogs/Staking/StakedDetailsDialog.tsx delete mode 100644 apps/wallet-dashboard/components/Stake/StakeTxnInfo.tsx diff --git a/apps/wallet-dashboard/app/(protected)/staking/page.tsx b/apps/wallet-dashboard/app/(protected)/staking/page.tsx index 3d5d03a5d15..c7387c327c8 100644 --- a/apps/wallet-dashboard/app/(protected)/staking/page.tsx +++ b/apps/wallet-dashboard/app/(protected)/staking/page.tsx @@ -23,6 +23,7 @@ function StakingDashboardPage(): JSX.Element { const account = useCurrentAccount(); const [stakeDialogView, setStakeDialogView] = useState<StakeDialogView | undefined>(); const [selectedStake, setSelectedStake] = useState<ExtendedDelegatedStake | null>(null); + const [selectedValidator, setSelectedValidator] = useState<string>(''); const { data: delegatedStakeData } = useGetDelegatedStake({ address: account?.address || '', staleTime: DELEGATED_STAKES_QUERY_STALE_TIME, @@ -47,6 +48,7 @@ function StakingDashboardPage(): JSX.Element { }; function handleCloseStakeDialog() { + setSelectedValidator(''); setSelectedStake(null); setStakeDialogView(undefined); } @@ -94,6 +96,8 @@ function StakingDashboardPage(): JSX.Element { handleClose={handleCloseStakeDialog} view={stakeDialogView} setView={setStakeDialogView} + selectedValidator={selectedValidator} + setSelectedValidator={setSelectedValidator} /> )} </> diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx index b59deef129b..c86dd3ae48c 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx @@ -48,11 +48,14 @@ interface StakeDialogProps { isOpen: boolean; handleClose: () => void; view: StakeDialogView; - setView: (view: StakeDialogView) => void; + setView?: (view: StakeDialogView) => void; stakedDetails?: ExtendedDelegatedStake | null; + + selectedValidator?: string; + setSelectedValidator?: (validator: string) => void; } -function StakeDialog({ +export function StakeDialog({ onSuccess, isTimelockedStaking, isOpen, @@ -60,8 +63,9 @@ function StakeDialog({ view, setView, stakedDetails, + selectedValidator = '', + setSelectedValidator, }: StakeDialogProps): JSX.Element { - const [selectedValidator, setSelectedValidator] = useState<string>(''); const [amount, setAmount] = useState<string>(''); const account = useCurrentAccount(); @@ -99,7 +103,6 @@ function StakeDialog({ const coinBalance = BigInt(iotaBalance?.totalBalance || 0); const minimumStake = parseAmount(MIN_NUMBER_IOTA_TO_STAKE.toString(), coinDecimals); const coinSymbol = metadata?.symbol ?? ''; - console.log('symbol', coinSymbol); const validators = Object.keys(rollingAverageApys ?? {}) ?? []; @@ -116,25 +119,25 @@ function StakeDialog({ ); function handleBack(): void { - setView(StakeDialogView.SelectValidator); + setView?.(StakeDialogView.SelectValidator); } function handleValidatorSelect(validator: string): void { - setSelectedValidator(validator); + setSelectedValidator?.(validator); } function selectValidatorHandleNext(): void { if (selectedValidator) { - setView(StakeDialogView.EnterAmount); + setView?.(StakeDialogView.EnterAmount); } } function detailsHandleUnstake() { - setView(StakeDialogView.Unstake); + setView?.(StakeDialogView.Unstake); } function detailsHandleStake() { - setView(StakeDialogView.SelectValidator); + setView?.(StakeDialogView.SelectValidator); } function handleStake(): void { @@ -172,8 +175,6 @@ function StakeDialog({ resetForm(); } - console.log(newStakeData?.gasBudget); - return ( <Dialog open={isOpen} onOpenChange={() => handleClose()}> <Formik @@ -222,5 +223,3 @@ function StakeDialog({ </Dialog> ); } - -export default StakeDialog; diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/StakedDetailsDialog.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/StakedDetailsDialog.tsx deleted file mode 100644 index 4f32601a2dc..00000000000 --- a/apps/wallet-dashboard/components/Dialogs/Staking/StakedDetailsDialog.tsx +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import React from 'react'; -import { - ExtendedDelegatedStake, - formatPercentageDisplay, - ImageIcon, - ImageIconSize, - useFormatCoin, - useValidatorInfo, -} from '@iota/core'; -import { - Badge, - BadgeType, - Button, - ButtonType, - Card, - CardBody, - CardImage, - CardType, - Dialog, - DialogBody, - DialogContent, - DialogPosition, - Divider, - Header, - InfoBox, - InfoBoxStyle, - InfoBoxType, - KeyValueInfo, - LoadingIndicator, - Panel, -} from '@iota/apps-ui-kit'; -import { Warning } from '@iota/ui-icons'; -import { useUnstakeTransaction } from '@/hooks'; -import { - useCurrentAccount, - useIotaClientQuery, - useSignAndExecuteTransaction, -} from '@iota/dapp-kit'; -import { formatAddress, IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; - -interface StakeDialogProps { - stakedDetails: ExtendedDelegatedStake; - showActiveStatus?: boolean; - handleClose: () => void; -} - -export function StakedDetailsDialog({ - handleClose, - stakedDetails, - showActiveStatus, -}: StakeDialogProps): JSX.Element { - const account = useCurrentAccount(); - const totalStake = BigInt(stakedDetails?.principal || 0n); - const validatorAddress = stakedDetails?.validatorAddress; - const { isPending: loadingValidators, isError: errorValidators } = useIotaClientQuery( - 'getLatestIotaSystemState', - ); - const iotaEarned = BigInt(stakedDetails?.estimatedReward || 0n); - const [iotaEarnedFormatted, iotaEarnedSymbol] = useFormatCoin(iotaEarned, IOTA_TYPE_ARG); - const [totalStakeFormatted, totalStakeSymbol] = useFormatCoin(totalStake, IOTA_TYPE_ARG); - - const { name, commission, newValidator, isAtRisk, apy, isApyApproxZero } = useValidatorInfo({ - validatorAddress: validatorAddress, - }); - - const { data: unstakeData } = useUnstakeTransaction( - stakedDetails.stakedIotaId, - account?.address || '', - ); - const { mutateAsync: signAndExecuteTransaction } = useSignAndExecuteTransaction(); - - const subtitle = showActiveStatus ? ( - <div className="flex items-center gap-1"> - {formatAddress(validatorAddress)} - {newValidator && <Badge label="New" type={BadgeType.PrimarySoft} />} - {isAtRisk && <Badge label="At Risk" type={BadgeType.PrimarySolid} />} - </div> - ) : ( - formatAddress(validatorAddress) - ); - - async function handleUnstake(): Promise<void> { - if (!unstakeData) return; - await signAndExecuteTransaction({ - transaction: unstakeData.transaction, - }); - } - - function handleAddNewStake() { - // pass - } - - if (loadingValidators) { - return ( - <div className="flex h-full w-full items-center justify-center p-2"> - <LoadingIndicator /> - </div> - ); - } - - if (errorValidators) { - return ( - <div className="mb-2 flex h-full w-full items-center justify-center p-2"> - <InfoBox - title="Something went wrong" - supportingText={'An error occurred'} - style={InfoBoxStyle.Default} - type={InfoBoxType.Error} - icon={<Warning />} - /> - </div> - ); - } - - return ( - <Dialog open onOpenChange={handleClose}> - <DialogContent containerId="overlay-portal-container" position={DialogPosition.Right}> - <div className="flex min-h-full flex-col"> - <Header - title="Validator" - onClose={handleClose} - onBack={handleClose} - titleCentered - /> - <div className="flex w-full flex-1 [&_>div]:flex [&_>div]:w-full [&_>div]:flex-col [&_>div]:justify-between"> - <DialogBody> - <div className="flex w-full flex-col gap-md"> - <Card type={CardType.Filled}> - <CardImage> - <ImageIcon - src={null} - label={name} - fallback={name} - size={ImageIconSize.Large} - /> - </CardImage> - <CardBody title={name} subtitle={subtitle} isTextTruncated /> - </Card> - <Panel hasBorder> - <div className="flex flex-col gap-y-sm p-md"> - <KeyValueInfo - keyText="Your Stake" - value={totalStakeFormatted} - supportingLabel={totalStakeSymbol} - fullwidth - /> - <KeyValueInfo - keyText="Earned" - value={iotaEarnedFormatted} - supportingLabel={iotaEarnedSymbol} - fullwidth - /> - <Divider /> - <KeyValueInfo - keyText="APY" - value={formatPercentageDisplay( - apy, - '--', - isApyApproxZero, - )} - fullwidth - /> - <KeyValueInfo - keyText="Commission" - value={`${commission.toString()}%`} - fullwidth - /> - </div> - </Panel> - </div> - <div> - <div className="my-3.75 flex w-full gap-2.5"> - <Button - type={ButtonType.Secondary} - onClick={handleUnstake} - text="Unstake" - fullWidth - /> - <Button - type={ButtonType.Primary} - text="Stake" - onClick={handleAddNewStake} - disabled - fullWidth - /> - </div> - </div> - </DialogBody> - </div> - </div> - </DialogContent> - </Dialog> - ); -} diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/index.ts b/apps/wallet-dashboard/components/Dialogs/Staking/index.ts index e415159b7c5..eb698da416e 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/index.ts +++ b/apps/wallet-dashboard/components/Dialogs/Staking/index.ts @@ -1,5 +1,4 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -export { default as StakeDialog } from './StakeDialog'; -export * from './StakedDetailsDialog'; +export * from './StakeDialog'; diff --git a/apps/wallet-dashboard/components/Stake/StakeTxnInfo.tsx b/apps/wallet-dashboard/components/Stake/StakeTxnInfo.tsx deleted file mode 100644 index 58daf586008..00000000000 --- a/apps/wallet-dashboard/components/Stake/StakeTxnInfo.tsx +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// Modifications Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { Divider, KeyValueInfo, Panel, TooltipPosition } from '@iota/apps-ui-kit'; -import { - NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE, - NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_STARTS, -} from '../../components/Dialogs/Staking/hooks/useStakeTxnInfo'; -import { useGetTimeBeforeEpochNumber, useTimeAgo, TimeUnit, type GasSummaryType } from '@iota/core'; -import { GasSummary } from '../../components/Transaction/GasSummary'; - -interface StakeTxnInfoProps { - apy?: string; - startEpoch?: string | number; - gasSummary?: GasSummaryType; -} - -export function StakeTxnInfo({ apy, startEpoch, gasSummary }: StakeTxnInfoProps) { - const startEarningRewardsEpoch = - Number(startEpoch || 0) + NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_STARTS; - - const redeemableRewardsEpoch = - Number(startEpoch || 0) + NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE; - - const { data: timeBeforeStakeRewardsStarts } = - useGetTimeBeforeEpochNumber(startEarningRewardsEpoch); - const timeBeforeStakeRewardsStartsAgo = useTimeAgo({ - timeFrom: timeBeforeStakeRewardsStarts, - shortedTimeLabel: false, - shouldEnd: true, - maxTimeUnit: TimeUnit.ONE_HOUR, - }); - const stakedRewardsStartEpoch = - timeBeforeStakeRewardsStarts > 0 - ? `${timeBeforeStakeRewardsStartsAgo === '--' ? '' : 'in'} ${timeBeforeStakeRewardsStartsAgo}` - : startEpoch - ? `Epoch #${Number(startEarningRewardsEpoch)}` - : '--'; - - const { data: timeBeforeStakeRewardsRedeemable } = - useGetTimeBeforeEpochNumber(redeemableRewardsEpoch); - const timeBeforeStakeRewardsRedeemableAgo = useTimeAgo({ - timeFrom: timeBeforeStakeRewardsRedeemable, - shortedTimeLabel: false, - shouldEnd: true, - maxTimeUnit: TimeUnit.ONE_HOUR, - }); - const timeBeforeStakeRewardsRedeemableAgoDisplay = - timeBeforeStakeRewardsRedeemable > 0 - ? `${timeBeforeStakeRewardsRedeemableAgo === '--' ? '' : 'in'} ${timeBeforeStakeRewardsRedeemableAgo}` - : startEpoch - ? `Epoch #${Number(redeemableRewardsEpoch)}` - : '--'; - return ( - <Panel hasBorder> - <div className="flex flex-col gap-y-sm p-md"> - {apy && ( - <KeyValueInfo - keyText="APY" - value={apy} - tooltipText="This is the Annualized Percentage Yield of the a specific validator’s past operations. Note there is no guarantee this APY will be true in the future." - tooltipPosition={TooltipPosition.Right} - fullwidth - /> - )} - <KeyValueInfo - keyText="Staking Rewards Start" - value={stakedRewardsStartEpoch} - fullwidth - /> - <KeyValueInfo - keyText="Redeem Rewards" - value={timeBeforeStakeRewardsRedeemableAgoDisplay} - fullwidth - /> - <Divider /> - {gasSummary && <GasSummary gasSummary={gasSummary} />} - </div> - </Panel> - ); -} diff --git a/apps/wallet-dashboard/components/staking-overview/StartStaking.tsx b/apps/wallet-dashboard/components/staking-overview/StartStaking.tsx index dd28cca199c..421df1beac6 100644 --- a/apps/wallet-dashboard/components/staking-overview/StartStaking.tsx +++ b/apps/wallet-dashboard/components/staking-overview/StartStaking.tsx @@ -5,6 +5,7 @@ import { Button, ButtonSize, ButtonType, Panel } from '@iota/apps-ui-kit'; import { Theme, useTheme } from '@/contexts'; import { useState } from 'react'; import { StakeDialog } from '../Dialogs'; +import { StakeDialogView } from '../Dialogs/Staking/StakeDialog'; export function StartStaking() { const { theme } = useTheme(); @@ -50,7 +51,11 @@ export function StartStaking() { ></video> </div> </div> - <StakeDialog isOpen={isDialogStakeOpen} setOpen={setIsDialogStakeOpen} /> + <StakeDialog + isOpen={isDialogStakeOpen} + handleClose={() => setIsDialogStakeOpen(false)} + view={StakeDialogView.SelectValidator} + /> </Panel> ); } From c4329f4d962abd696d41bda422409ca4cd78f759 Mon Sep 17 00:00:00 2001 From: Eugene P <panteleymonchuk@gmail.com> Date: Thu, 14 Nov 2024 19:21:58 +0200 Subject: [PATCH 58/87] feat(wallet-dashboard): refactor StakedInfo and Validator components to use core hooks and clean up imports --- .../components/Dialogs/Staking/views/StakedInfo.tsx | 2 +- .../components/Dialogs/Staking/views/UnstakeView.tsx | 2 +- .../components/Dialogs/Staking/views/Validator.tsx | 3 +-- apps/wallet-dashboard/hooks/index.ts | 1 - apps/wallet/tsconfig.json | 2 +- 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/views/StakedInfo.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/views/StakedInfo.tsx index 024f9f9384a..95890afd07f 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/views/StakedInfo.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/views/StakedInfo.tsx @@ -10,9 +10,9 @@ import { useGetDelegatedStake, DELEGATED_STAKES_QUERY_REFETCH_INTERVAL, DELEGATED_STAKES_QUERY_STALE_TIME, + useValidatorInfo, } from '@iota/core'; import { KeyValueInfo, Panel, TooltipPosition } from '@iota/apps-ui-kit'; -import { useValidatorInfo } from '@/hooks'; export function StakedInfo({ validatorAddress, diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/views/UnstakeView.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/views/UnstakeView.tsx index bdc60e80d20..bc2bbf01310 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/views/UnstakeView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/views/UnstakeView.tsx @@ -41,7 +41,7 @@ interface UnstakeDialogProps { export function UnstakeView({ extendedStake, handleClose, - showActiveStatus = true, + showActiveStatus, }: UnstakeDialogProps): JSX.Element { const stakingReward = BigInt(extendedStake.estimatedReward ?? '').toString(); const [rewards, rewardSymbol] = useFormatCoin(stakingReward, IOTA_TYPE_ARG); diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/views/Validator.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/views/Validator.tsx index 1e281433a77..1b35e06f91e 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/views/Validator.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/views/Validator.tsx @@ -1,6 +1,6 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { ImageIcon, ImageIconSize, formatPercentageDisplay } from '@iota/core'; +import { ImageIcon, ImageIconSize, formatPercentageDisplay, useValidatorInfo } from '@iota/core'; import { Card, CardBody, @@ -12,7 +12,6 @@ import { BadgeType, } from '@iota/apps-ui-kit'; import { formatAddress } from '@iota/iota-sdk/utils'; -import { useValidatorInfo } from '@/hooks'; export function Validator({ address, diff --git a/apps/wallet-dashboard/hooks/index.ts b/apps/wallet-dashboard/hooks/index.ts index f262ea2e616..373c4e399dc 100644 --- a/apps/wallet-dashboard/hooks/index.ts +++ b/apps/wallet-dashboard/hooks/index.ts @@ -10,4 +10,3 @@ export * from './useCreateSendAssetTransaction'; export * from './useGetCurrentEpochStartTimestamp'; export * from './useTimelockedUnstakeTransaction'; export * from './useExplorerLinkGetter'; -export * from '@iota/core/src/hooks/stake/useValidatorInfo'; diff --git a/apps/wallet/tsconfig.json b/apps/wallet/tsconfig.json index 4da06a3efe1..15c97c419b0 100644 --- a/apps/wallet/tsconfig.json +++ b/apps/wallet/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "./configs/ts/tsconfig.dev", - "include": ["src", "configs", "tests", "../core/src/utils/stake/createValidationSchema.ts"], + "include": ["src", "configs", "tests"], "compilerOptions": { "noEmit": true } From 1a2f009d22f85c2b65802f8490c9c61ff7f8f7ac Mon Sep 17 00:00:00 2001 From: JCNoguera <88061365+VmMad@users.noreply.github.com> Date: Fri, 15 Nov 2024 13:12:11 +0100 Subject: [PATCH 59/87] fix: amount format --- .../SendToken/views/ReviewValuesFormView.tsx | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/apps/wallet-dashboard/components/Dialogs/SendToken/views/ReviewValuesFormView.tsx b/apps/wallet-dashboard/components/Dialogs/SendToken/views/ReviewValuesFormView.tsx index fabf82dbc30..015d5087318 100644 --- a/apps/wallet-dashboard/components/Dialogs/SendToken/views/ReviewValuesFormView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/views/ReviewValuesFormView.tsx @@ -18,15 +18,7 @@ import { ButtonType, } from '@iota/apps-ui-kit'; import { formatAddress } from '@iota/iota-sdk/utils'; -import { - GAS_SYMBOL, - CoinIcon, - ImageIconSize, - parseAmount, - useCoinMetadata, - useFormatCoin, - ExplorerLinkType, -} from '@iota/core'; +import { GAS_SYMBOL, CoinIcon, ImageIconSize, useFormatCoin, ExplorerLinkType } from '@iota/core'; import { Loader } from '@iota/ui-icons'; import { ExplorerLink } from '@/components'; @@ -45,10 +37,7 @@ export function ReviewValuesFormView({ executeTransfer, coinType, }: ReviewValuesFormProps): JSX.Element { - const { data: metadata } = useCoinMetadata(coinType); - const amountWithoutDecimals = parseAmount(amount, metadata?.decimals ?? 0); - const [formatAmount, symbol] = useFormatCoin(amountWithoutDecimals, coinType); - + const [formatAmount, symbol] = useFormatCoin(amount, coinType); return ( <div className="flex h-full flex-col"> <div className="flex h-full w-full flex-col gap-md"> From 3a4ebb7f79b871f90f99665bc2db219c8cb71fe6 Mon Sep 17 00:00:00 2001 From: Eugene P <panteleymonchuk@gmail.com> Date: Fri, 15 Nov 2024 14:20:52 +0200 Subject: [PATCH 60/87] feat(wallet-dashboard): move useStakeTxnInfo hook to the core --- apps/core/src/hooks/stake/index.ts | 1 + .../src/hooks/stake/useStakeTxnInfo.ts} | 2 +- .../components/Dialogs/Staking/hooks/index.ts | 4 -- .../Dialogs/Staking/views/EnterAmountView.tsx | 10 ++++- .../components/receipt-card/StakeTxnInfo.tsx | 43 ++----------------- 5 files changed, 13 insertions(+), 47 deletions(-) rename apps/{wallet-dashboard/components/Dialogs/Staking/hooks/useStakeTxsInfo.ts => core/src/hooks/stake/useStakeTxnInfo.ts} (99%) delete mode 100644 apps/wallet-dashboard/components/Dialogs/Staking/hooks/index.ts diff --git a/apps/core/src/hooks/stake/index.ts b/apps/core/src/hooks/stake/index.ts index f422d9d1f61..2477e671351 100644 --- a/apps/core/src/hooks/stake/index.ts +++ b/apps/core/src/hooks/stake/index.ts @@ -5,3 +5,4 @@ export * from './useGetDelegatedStake'; export * from './useTotalDelegatedRewards'; export * from './useTotalDelegatedStake'; export * from './useValidatorInfo'; +export * from './useStakeTxnInfo'; diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/hooks/useStakeTxsInfo.ts b/apps/core/src/hooks/stake/useStakeTxnInfo.ts similarity index 99% rename from apps/wallet-dashboard/components/Dialogs/Staking/hooks/useStakeTxsInfo.ts rename to apps/core/src/hooks/stake/useStakeTxnInfo.ts index 86e3fe7cc8e..5d6af5b82e5 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/hooks/useStakeTxsInfo.ts +++ b/apps/core/src/hooks/stake/useStakeTxnInfo.ts @@ -1,6 +1,6 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { useGetTimeBeforeEpochNumber, useTimeAgo, TimeUnit } from '@iota/core'; +import { useGetTimeBeforeEpochNumber, useTimeAgo, TimeUnit } from '../../index'; export const NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE = 2; export const NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_STARTS = 1; diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/hooks/index.ts b/apps/wallet-dashboard/components/Dialogs/Staking/hooks/index.ts deleted file mode 100644 index 9f0940ce152..00000000000 --- a/apps/wallet-dashboard/components/Dialogs/Staking/hooks/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -export * from './useStakeTxsInfo'; diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx index c0332d3efe7..9fb52f10709 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx @@ -2,7 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 import React, { useEffect } from 'react'; -import { useFormatCoin, useBalance, CoinFormat, parseAmount, useCoinMetadata } from '@iota/core'; +import { + useFormatCoin, + useBalance, + CoinFormat, + parseAmount, + useCoinMetadata, + useStakeTxnInfo, +} from '@iota/core'; import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; import { Button, @@ -21,7 +28,6 @@ import { Field, type FieldProps, useFormikContext } from 'formik'; import { Exclamation } from '@iota/ui-icons'; import { useCurrentAccount, useIotaClientQuery } from '@iota/dapp-kit'; -import { useStakeTxnInfo } from '../hooks'; import { Validator } from './Validator'; import { StakedInfo } from './StakedInfo'; import { Layout, LayoutBody, LayoutFooter } from './Layout'; diff --git a/apps/wallet/src/ui/app/components/receipt-card/StakeTxnInfo.tsx b/apps/wallet/src/ui/app/components/receipt-card/StakeTxnInfo.tsx index 40064a9fcfb..c4b94e7eb6f 100644 --- a/apps/wallet/src/ui/app/components/receipt-card/StakeTxnInfo.tsx +++ b/apps/wallet/src/ui/app/components/receipt-card/StakeTxnInfo.tsx @@ -3,11 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Divider, KeyValueInfo, Panel, TooltipPosition } from '@iota/apps-ui-kit'; -import { - NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE, - NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_STARTS, -} from '_src/shared/constants'; -import { useGetTimeBeforeEpochNumber, useTimeAgo, TimeUnit, type GasSummaryType } from '@iota/core'; +import { type GasSummaryType, useStakeTxnInfo } from '@iota/core'; import { GasSummary } from '../../shared/transaction-summary/cards/GasSummary'; interface StakeTxnInfoProps { @@ -17,41 +13,8 @@ interface StakeTxnInfoProps { } export function StakeTxnInfo({ apy, startEpoch, gasSummary }: StakeTxnInfoProps) { - const startEarningRewardsEpoch = - Number(startEpoch || 0) + NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_STARTS; - - const redeemableRewardsEpoch = - Number(startEpoch || 0) + NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE; - - const { data: timeBeforeStakeRewardsStarts } = - useGetTimeBeforeEpochNumber(startEarningRewardsEpoch); - const timeBeforeStakeRewardsStartsAgo = useTimeAgo({ - timeFrom: timeBeforeStakeRewardsStarts, - shortedTimeLabel: false, - shouldEnd: true, - maxTimeUnit: TimeUnit.ONE_HOUR, - }); - const stakedRewardsStartEpoch = - timeBeforeStakeRewardsStarts > 0 - ? `${timeBeforeStakeRewardsStartsAgo === '--' ? '' : 'in'} ${timeBeforeStakeRewardsStartsAgo}` - : startEpoch - ? `Epoch #${Number(startEarningRewardsEpoch)}` - : '--'; - - const { data: timeBeforeStakeRewardsRedeemable } = - useGetTimeBeforeEpochNumber(redeemableRewardsEpoch); - const timeBeforeStakeRewardsRedeemableAgo = useTimeAgo({ - timeFrom: timeBeforeStakeRewardsRedeemable, - shortedTimeLabel: false, - shouldEnd: true, - maxTimeUnit: TimeUnit.ONE_HOUR, - }); - const timeBeforeStakeRewardsRedeemableAgoDisplay = - timeBeforeStakeRewardsRedeemable > 0 - ? `${timeBeforeStakeRewardsRedeemableAgo === '--' ? '' : 'in'} ${timeBeforeStakeRewardsRedeemableAgo}` - : startEpoch - ? `Epoch #${Number(redeemableRewardsEpoch)}` - : '--'; + const { stakedRewardsStartEpoch, timeBeforeStakeRewardsRedeemableAgoDisplay } = + useStakeTxnInfo(startEpoch); return ( <Panel hasBorder> <div className="flex flex-col gap-y-sm p-md"> From 54b1b1fd4c3301ad165b6dbad3fe0498bd3309e1 Mon Sep 17 00:00:00 2001 From: Eugene P <panteleymonchuk@gmail.com> Date: Fri, 15 Nov 2024 14:27:46 +0200 Subject: [PATCH 61/87] fix(tooling-core): downgrade bignumber.js to 9.1.1 and yup to 1.1.1 --- apps/core/package.json | 4 ++-- pnpm-lock.yaml | 40 ++++++++++++++++++++-------------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/apps/core/package.json b/apps/core/package.json index cafc062f6aa..f21c6ff85e5 100644 --- a/apps/core/package.json +++ b/apps/core/package.json @@ -31,14 +31,14 @@ "@iota/kiosk": "workspace:*", "@sentry/react": "^7.59.2", "@tanstack/react-query": "^5.50.1", - "bignumber.js": "^9.1.2", + "bignumber.js": "^9.1.1", "clsx": "^2.1.1", "qrcode.react": "^4.0.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.45.2", "vanilla-cookieconsent": "^2.9.1", - "yup": "^1.4.0", + "yup": "^1.1.1", "zod": "^3.21.4" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f2f99ca591..f41b49c893d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -179,7 +179,7 @@ importers: version: 5.2.1(@types/eslint@8.56.12)(eslint-config-prettier@9.1.0(eslint@8.57.1))(eslint@8.57.1)(prettier@3.3.3) jest: specifier: ^29.5.0 - version: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + version: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) prettier: specifier: ^3.3.1 version: 3.3.3 @@ -191,7 +191,7 @@ importers: version: 6.3.4 ts-jest: specifier: ^29.1.0 - version: 29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)))(typescript@5.6.2) + version: 29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)))(typescript@5.6.2) ts-loader: specifier: ^9.4.4 version: 9.5.1(typescript@5.6.2)(webpack@5.95.0(@swc/core@1.7.28)) @@ -238,7 +238,7 @@ importers: specifier: ^5.50.1 version: 5.56.2(react@18.3.1) bignumber.js: - specifier: ^9.1.2 + specifier: ^9.1.1 version: 9.1.2 clsx: specifier: ^2.1.1 @@ -259,7 +259,7 @@ importers: specifier: ^2.9.1 version: 2.9.2 yup: - specifier: ^1.4.0 + specifier: ^1.1.1 version: 1.4.0 zod: specifier: ^3.21.4 @@ -1057,7 +1057,7 @@ importers: version: 14.2.3(eslint@8.57.1)(typescript@5.6.2) jest: specifier: ^29.5.0 - version: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + version: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) postcss: specifier: ^8.4.31 version: 8.4.47 @@ -1066,7 +1066,7 @@ importers: version: 3.4.13(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) ts-jest: specifier: ^29.1.0 - version: 29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)))(typescript@5.6.2) + version: 29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)))(typescript@5.6.2) typescript: specifier: ^5.5.3 version: 5.6.2 @@ -20585,7 +20585,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2))': + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0(node-notifier@10.0.0) @@ -20599,7 +20599,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + jest-config: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -26523,13 +26523,13 @@ snapshots: crc-32@1.2.2: {} - create-jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)): + create-jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + jest-config: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -29756,16 +29756,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)): + jest-cli@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + create-jest: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + jest-config: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -29777,7 +29777,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)): + jest-config@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)): dependencies: '@babel/core': 7.25.2 '@jest/test-sequencer': 29.7.0 @@ -30034,12 +30034,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)): + jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + jest-cli: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) optionalDependencies: node-notifier: 10.0.0 transitivePeerDependencies: @@ -34656,12 +34656,12 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)))(typescript@5.6.2): + ts-jest@29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)))(typescript@5.6.2): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@swc/core@1.7.28)(@types/node@20.16.9)(typescript@5.6.2)) + jest: 29.7.0(@types/node@20.16.9)(babel-plugin-macros@3.1.0)(node-notifier@10.0.0)(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.6.2)) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 From ed42962839e56b6b4de1864afe26608868047f42 Mon Sep 17 00:00:00 2001 From: marc2332 <mespinsanz@gmail.com> Date: Fri, 15 Nov 2024 15:28:31 +0100 Subject: [PATCH 62/87] feat: Improve validation flow of sent screen --- apps/core/src/components/Inputs/AddressInput.tsx | 14 +++++++++++--- .../src/components/Inputs/SendTokenFormInput.tsx | 15 +++++++++++---- .../SendToken/views/EnterValuesFormView.tsx | 4 ++-- .../pages/home/transfer-coin/SendTokenForm.tsx | 4 ++-- 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/apps/core/src/components/Inputs/AddressInput.tsx b/apps/core/src/components/Inputs/AddressInput.tsx index d0bf195ba0f..777acc28cfc 100644 --- a/apps/core/src/components/Inputs/AddressInput.tsx +++ b/apps/core/src/components/Inputs/AddressInput.tsx @@ -5,8 +5,8 @@ import { Input, InputType } from '@iota/apps-ui-kit'; import { Close } from '@iota/ui-icons'; import { useIotaAddressValidation } from '../../hooks'; -import React, { useCallback } from 'react'; -import { useField } from 'formik'; +import React, { useCallback, useEffect } from 'react'; +import { useField, useFormikContext } from 'formik'; export interface AddressInputProps { name: string; @@ -21,6 +21,7 @@ export function AddressInput({ placeholder = '0x...', label = 'Enter Recipient Address', }: AddressInputProps) { + const { validateField } = useFormikContext(); const [field, meta, helpers] = useField<string>(name); const iotaAddressValidation = useIotaAddressValidation(); @@ -29,11 +30,18 @@ export function AddressInput({ const handleOnChange = useCallback( (e: React.ChangeEvent<HTMLInputElement>) => { const address = e.currentTarget.value; + helpers.setTouched(true) helpers.setValue(iotaAddressValidation.cast(address)); }, - [name, iotaAddressValidation], + [name, iotaAddressValidation, helpers.setTouched, helpers.setValue, validateField], ); + useEffect(() => { + if(meta.touched) { + validateField(name); + } + }, [field.value]) + const clearAddress = () => { helpers.setValue(''); }; diff --git a/apps/core/src/components/Inputs/SendTokenFormInput.tsx b/apps/core/src/components/Inputs/SendTokenFormInput.tsx index 1b7f0be19ca..5beddf04f11 100644 --- a/apps/core/src/components/Inputs/SendTokenFormInput.tsx +++ b/apps/core/src/components/Inputs/SendTokenFormInput.tsx @@ -29,7 +29,7 @@ export function SendTokenFormInput({ isMaxActionDisabled, name, }: SendTokenInputProps) { - const { values, setFieldValue, isSubmitting } = useFormikContext<TokenForm>(); + const { values, setFieldValue, isSubmitting, validateField } = useFormikContext<TokenForm>(); const gasBudgetEstimation = useGasBudgetEstimation({ coinDecimals, coins: coins ?? [], @@ -40,8 +40,7 @@ export function SendTokenFormInput({ }); const [field, meta, helpers] = useField<string>(name); - - const errorMessage = meta?.error ? meta.error : undefined; + const errorMessage = meta.error; const isActionButtonDisabled = isSubmitting || !!errorMessage || isMaxActionDisabled; const renderAction = () => ( @@ -50,6 +49,12 @@ export function SendTokenFormInput({ </ButtonPill> ); + useEffect(() => { + if(meta.touched) { + validateField(name); + } + }, [field.value, meta.touched]) + // gasBudgetEstimation should change when the amount above changes useEffect(() => { setFieldValue('gasBudgetEst', gasBudgetEstimation, false); @@ -58,7 +63,8 @@ export function SendTokenFormInput({ return ( <Input type={InputType.NumericFormat} - name="amount" + name={field.name} + onBlur={field.onBlur} value={field.value} caption="Est. Gas Fees:" placeholder="0.00" @@ -72,6 +78,7 @@ export function SendTokenFormInput({ decimalScale={coinDecimals ? undefined : 0} thousandSeparator onValueChange={(values) => { + helpers.setTouched(true); helpers.setValue(values.value); }} /> diff --git a/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx b/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx index 2ad0fdba7cc..9844cd098f1 100644 --- a/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx @@ -242,8 +242,8 @@ function EnterValuesFormView({ }} validationSchema={validationSchemaStepOne} enableReinitialize - validateOnChange={true} - validateOnBlur={true} + validateOnChange={false} + validateOnBlur={false} onSubmit={handleFormSubmit} > {(props: FormikProps<FormDataValues>) => ( diff --git a/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx b/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx index 57c8e28730d..4f26a6796fa 100644 --- a/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx +++ b/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx @@ -140,8 +140,8 @@ export function SendTokenForm({ }} validationSchema={validationSchemaStepOne} enableReinitialize - validateOnChange={true} - validateOnBlur={true} + validateOnChange={false} + validateOnBlur={false} onSubmit={handleFormSubmit} > {({ isValid, isSubmitting, setFieldValue, values, submitForm }) => { From 69dab33de68220256ac45d614c208654bb7bef07 Mon Sep 17 00:00:00 2001 From: marc2332 <mespinsanz@gmail.com> Date: Fri, 15 Nov 2024 15:30:02 +0100 Subject: [PATCH 63/87] fmt --- apps/core/src/components/Inputs/AddressInput.tsx | 6 +++--- apps/core/src/components/Inputs/SendTokenFormInput.tsx | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/core/src/components/Inputs/AddressInput.tsx b/apps/core/src/components/Inputs/AddressInput.tsx index 777acc28cfc..12d90d2a91c 100644 --- a/apps/core/src/components/Inputs/AddressInput.tsx +++ b/apps/core/src/components/Inputs/AddressInput.tsx @@ -30,17 +30,17 @@ export function AddressInput({ const handleOnChange = useCallback( (e: React.ChangeEvent<HTMLInputElement>) => { const address = e.currentTarget.value; - helpers.setTouched(true) + helpers.setTouched(true); helpers.setValue(iotaAddressValidation.cast(address)); }, [name, iotaAddressValidation, helpers.setTouched, helpers.setValue, validateField], ); useEffect(() => { - if(meta.touched) { + if (meta.touched) { validateField(name); } - }, [field.value]) + }, [field.value]); const clearAddress = () => { helpers.setValue(''); diff --git a/apps/core/src/components/Inputs/SendTokenFormInput.tsx b/apps/core/src/components/Inputs/SendTokenFormInput.tsx index 5beddf04f11..1454a2ca89b 100644 --- a/apps/core/src/components/Inputs/SendTokenFormInput.tsx +++ b/apps/core/src/components/Inputs/SendTokenFormInput.tsx @@ -50,10 +50,10 @@ export function SendTokenFormInput({ ); useEffect(() => { - if(meta.touched) { + if (meta.touched) { validateField(name); } - }, [field.value, meta.touched]) + }, [field.value, meta.touched]); // gasBudgetEstimation should change when the amount above changes useEffect(() => { From 824218512585313ef1351695feb7fbdaab4c9e60 Mon Sep 17 00:00:00 2001 From: Eugene P <panteleymonchuk@gmail.com> Date: Fri, 15 Nov 2024 16:47:06 +0200 Subject: [PATCH 64/87] refactor(tooling-dashboard): change export to default and clean up code structure --- apps/wallet-dashboard/components/transactions/GasSummary.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/wallet-dashboard/components/transactions/GasSummary.tsx b/apps/wallet-dashboard/components/transactions/GasSummary.tsx index a0b45708b11..3b522432a40 100644 --- a/apps/wallet-dashboard/components/transactions/GasSummary.tsx +++ b/apps/wallet-dashboard/components/transactions/GasSummary.tsx @@ -6,7 +6,7 @@ import { useFormatCoin, type GasSummaryType } from '@iota/core'; import { useCurrentAccount } from '@iota/dapp-kit'; import { formatAddress, IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; -export function GasSummary({ gasSummary }: { gasSummary: GasSummaryType }) { +export default function GasSummary({ gasSummary }: { gasSummary: GasSummaryType }) { const [gas, symbol] = useFormatCoin(gasSummary?.totalGas, IOTA_TYPE_ARG); const address = useCurrentAccount(); @@ -38,5 +38,3 @@ export function GasSummary({ gasSummary }: { gasSummary: GasSummaryType }) { </div> ); } - -export default GasSummary; From b394e26fbb61841039b2c4f8e2c062a488a5c3cf Mon Sep 17 00:00:00 2001 From: JCNoguera <88061365+VmMad@users.noreply.github.com> Date: Mon, 18 Nov 2024 10:48:33 +0100 Subject: [PATCH 65/87] fix: format gas outside of hook --- .../src/components/Inputs/SendTokenFormInput.tsx | 15 +++++++++------ apps/core/src/hooks/useGasBudgetEstimation.ts | 12 +----------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/apps/core/src/components/Inputs/SendTokenFormInput.tsx b/apps/core/src/components/Inputs/SendTokenFormInput.tsx index 6d0f436b3b2..51beab4ec9f 100644 --- a/apps/core/src/components/Inputs/SendTokenFormInput.tsx +++ b/apps/core/src/components/Inputs/SendTokenFormInput.tsx @@ -3,11 +3,12 @@ import { ButtonPill, Input, InputType } from '@iota/apps-ui-kit'; import { CoinStruct } from '@iota/iota-sdk/client'; -import { useGasBudgetEstimation } from '../../hooks'; +import { useFormatCoin, useGasBudgetEstimation } from '../../hooks'; import React, { useEffect } from 'react'; import { GAS_SYMBOL } from '../../constants'; import { useField, useFormikContext } from 'formik'; import { TokenForm } from '../../forms'; +import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; export interface SendTokenInputProps { coins: CoinStruct[]; @@ -31,7 +32,8 @@ export function SendTokenFormInput({ name, }: SendTokenInputProps) { const { values, setFieldValue, isSubmitting, validateField } = useFormikContext<TokenForm>(); - const gasBudgetEstimation = useGasBudgetEstimation({ + + const { data: gasBudgetEstimation } = useGasBudgetEstimation({ coinDecimals, coins: coins ?? [], activeAddress, @@ -40,6 +42,7 @@ export function SendTokenFormInput({ isPayAllIota: values.isPayAllIota, showGasSymbol: false, }); + const [formattedGasBudgetEstimation] = useFormatCoin(gasBudgetEstimation, IOTA_TYPE_ARG); const [field, meta, helpers] = useField<string>(name); const errorMessage = meta.error; @@ -59,8 +62,8 @@ export function SendTokenFormInput({ // gasBudgetEstimation should change when the amount above changes useEffect(() => { - setFieldValue('gasBudgetEst', gasBudgetEstimation, false); - }, [gasBudgetEstimation, setFieldValue, values.amount]); + setFieldValue('gasBudgetEst', formattedGasBudgetEstimation, false); + }, [formattedGasBudgetEstimation, setFieldValue, values.amount]); return ( <Input @@ -77,8 +80,8 @@ export function SendTokenFormInput({ errorMessage={errorMessage} amountCounter={ !errorMessage - ? coins && gasBudgetEstimation !== '--' - ? `${gasBudgetEstimation} ${GAS_SYMBOL}` + ? coins && formattedGasBudgetEstimation !== '--' + ? `${formattedGasBudgetEstimation} ${GAS_SYMBOL}` : '--' : undefined } diff --git a/apps/core/src/hooks/useGasBudgetEstimation.ts b/apps/core/src/hooks/useGasBudgetEstimation.ts index be6f6abba5f..0002eb6cff6 100644 --- a/apps/core/src/hooks/useGasBudgetEstimation.ts +++ b/apps/core/src/hooks/useGasBudgetEstimation.ts @@ -6,8 +6,6 @@ import { CoinStruct } from '@iota/iota-sdk/client'; import { useQuery } from '@tanstack/react-query'; import { createTokenTransferTransaction } from '../utils'; import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; -import { useFormatCoin } from './useFormatCoin'; -import { GAS_SYMBOL } from '../constants'; interface UseGasBudgetEstimationOptions { coinDecimals: number; @@ -19,8 +17,6 @@ interface UseGasBudgetEstimationOptions { showGasSymbol?: boolean; } -const FALLBACK_GAS_VALUE = '--'; - export function useGasBudgetEstimation({ coinDecimals, coins, @@ -31,7 +27,7 @@ export function useGasBudgetEstimation({ showGasSymbol = true, }: UseGasBudgetEstimationOptions) { const client = useIotaClient(); - const { data: gasBudget } = useQuery({ + return useQuery({ // eslint-disable-next-line @tanstack/query/exhaustive-deps queryKey: [ 'transaction-gas-budget-estimate', @@ -62,10 +58,4 @@ export function useGasBudgetEstimation({ return tx.getData().gasData.budget; }, }); - - const [formattedGas] = useFormatCoin(gasBudget, IOTA_TYPE_ARG); - - return formattedGas - ? `${formattedGas}${showGasSymbol ? ` ${GAS_SYMBOL}` : ''}` - : FALLBACK_GAS_VALUE; } From 7eb6be41c0c23588b4b891c7e66dcf593d939a27 Mon Sep 17 00:00:00 2001 From: cpl121 <cpeon@boxfish.studio> Date: Tue, 19 Nov 2024 09:38:42 +0100 Subject: [PATCH 66/87] fix(wallet-dashboard): fixes --- .../src/components/Inputs/AddressInput.tsx | 18 +++++------------- .../components/Inputs/SendTokenFormInput.tsx | 14 ++++---------- .../home/nft-transfer/TransferNFTForm.tsx | 9 ++------- 3 files changed, 11 insertions(+), 30 deletions(-) diff --git a/apps/core/src/components/Inputs/AddressInput.tsx b/apps/core/src/components/Inputs/AddressInput.tsx index 12d90d2a91c..04cfe38f669 100644 --- a/apps/core/src/components/Inputs/AddressInput.tsx +++ b/apps/core/src/components/Inputs/AddressInput.tsx @@ -28,26 +28,18 @@ export function AddressInput({ const formattedValue = iotaAddressValidation.cast(field.value); const handleOnChange = useCallback( - (e: React.ChangeEvent<HTMLInputElement>) => { + async (e: React.ChangeEvent<HTMLInputElement>) => { const address = e.currentTarget.value; - helpers.setTouched(true); - helpers.setValue(iotaAddressValidation.cast(address)); + await helpers.setValue(iotaAddressValidation.cast(address)); + validateField(name); }, - [name, iotaAddressValidation, helpers.setTouched, helpers.setValue, validateField], + [name, iotaAddressValidation], ); - useEffect(() => { - if (meta.touched) { - validateField(name); - } - }, [field.value]); - const clearAddress = () => { helpers.setValue(''); }; - const errorMessage = meta.touched && meta.error; - return ( <Input type={InputType.Text} @@ -58,7 +50,7 @@ export function AddressInput({ onBlur={field.onBlur} label={label} onChange={handleOnChange} - errorMessage={errorMessage as string} + errorMessage={meta.error} trailingElement={ formattedValue ? ( <button diff --git a/apps/core/src/components/Inputs/SendTokenFormInput.tsx b/apps/core/src/components/Inputs/SendTokenFormInput.tsx index 1454a2ca89b..b0fffbc2ea3 100644 --- a/apps/core/src/components/Inputs/SendTokenFormInput.tsx +++ b/apps/core/src/components/Inputs/SendTokenFormInput.tsx @@ -41,7 +41,7 @@ export function SendTokenFormInput({ const [field, meta, helpers] = useField<string>(name); const errorMessage = meta.error; - const isActionButtonDisabled = isSubmitting || !!errorMessage || isMaxActionDisabled; + const isActionButtonDisabled = isSubmitting || isMaxActionDisabled; const renderAction = () => ( <ButtonPill disabled={isActionButtonDisabled} onClick={onActionClick}> @@ -49,12 +49,6 @@ export function SendTokenFormInput({ </ButtonPill> ); - useEffect(() => { - if (meta.touched) { - validateField(name); - } - }, [field.value, meta.touched]); - // gasBudgetEstimation should change when the amount above changes useEffect(() => { setFieldValue('gasBudgetEst', gasBudgetEstimation, false); @@ -77,9 +71,9 @@ export function SendTokenFormInput({ trailingElement={renderAction()} decimalScale={coinDecimals ? undefined : 0} thousandSeparator - onValueChange={(values) => { - helpers.setTouched(true); - helpers.setValue(values.value); + onValueChange={async (values) => { + await helpers.setValue(values.value); + validateField(name); }} /> ); diff --git a/apps/wallet/src/ui/app/pages/home/nft-transfer/TransferNFTForm.tsx b/apps/wallet/src/ui/app/pages/home/nft-transfer/TransferNFTForm.tsx index 7806967edd0..9e4801aa3cb 100644 --- a/apps/wallet/src/ui/app/pages/home/nft-transfer/TransferNFTForm.tsx +++ b/apps/wallet/src/ui/app/pages/home/nft-transfer/TransferNFTForm.tsx @@ -10,7 +10,7 @@ import { useSigner } from '_src/ui/app/hooks/useSigner'; import { createNftSendValidationSchema, useGetKioskContents, AddressInput } from '@iota/core'; import { Transaction } from '@iota/iota-sdk/transactions'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { Field, Form, Formik } from 'formik'; +import { Form, Formik } from 'formik'; import { toast } from 'react-hot-toast'; import { useNavigate } from 'react-router-dom'; @@ -95,12 +95,7 @@ export function TransferNFTForm({ objectId, objectType }: TransferNFTFormProps) {({ isValid, dirty, isSubmitting }) => ( <Form autoComplete="off" className="h-full"> <div className="flex h-full flex-col justify-between"> - <Field - component={AddressInput} - allowNegative={false} - name="to" - placeholder="Enter Address" - /> + <AddressInput name="to" placeholder="Enter Address" /> <Button htmlType={ButtonHtmlType.Submit} From 4164afa7b8ec6f3d6d3209a173d7c49fc1a3cff2 Mon Sep 17 00:00:00 2001 From: cpl121 <cpeon@boxfish.studio> Date: Tue, 19 Nov 2024 09:55:48 +0100 Subject: [PATCH 67/87] fix(wallet-dashboard): linter --- apps/core/src/components/Inputs/AddressInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/core/src/components/Inputs/AddressInput.tsx b/apps/core/src/components/Inputs/AddressInput.tsx index 04cfe38f669..63920a9afe0 100644 --- a/apps/core/src/components/Inputs/AddressInput.tsx +++ b/apps/core/src/components/Inputs/AddressInput.tsx @@ -5,7 +5,7 @@ import { Input, InputType } from '@iota/apps-ui-kit'; import { Close } from '@iota/ui-icons'; import { useIotaAddressValidation } from '../../hooks'; -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback } from 'react'; import { useField, useFormikContext } from 'formik'; export interface AddressInputProps { From b8899042f79d11e915cb93b3facec81a9369f353 Mon Sep 17 00:00:00 2001 From: cpl121 <cpeon@boxfish.studio> Date: Tue, 19 Nov 2024 16:28:16 +0100 Subject: [PATCH 68/87] fix(wallet-dashboard): error to click max button --- .../Dialogs/SendToken/views/EnterValuesFormView.tsx | 10 +++++++--- .../ui/app/pages/home/transfer-coin/SendTokenForm.tsx | 11 +++++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx b/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx index 9844cd098f1..31650a6a965 100644 --- a/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx @@ -31,6 +31,7 @@ import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; import { Form, Formik, FormikProps } from 'formik'; import { Exclamation } from '@iota/ui-icons'; import { UseQueryResult } from '@tanstack/react-query'; +import { useEffect } from 'react'; interface EnterValuesFormProps { coin: CoinBalance; @@ -77,9 +78,6 @@ function FormInputs({ }: FormInputsProps): React.JSX.Element { const newPayIotaAll = parseAmount(values.amount, coinDecimals) === coinBalance && coinType === IOTA_TYPE_ARG; - if (values.isPayAllIota !== newPayIotaAll) { - setFieldValue('isPayAllIota', newPayIotaAll); - } const hasEnoughBalance = values.isPayAllIota || @@ -96,6 +94,12 @@ function FormInputs({ queryResult.isPending || !coinBalance; + useEffect(() => { + if (values.isPayAllIota !== newPayIotaAll) { + setFieldValue('isPayAllIota', newPayIotaAll); + } + }, [values.isPayAllIota, newPayIotaAll]); + return ( <div className="flex h-full w-full flex-col"> <Form autoComplete="off" noValidate className="flex-1"> diff --git a/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx b/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx index 4f26a6796fa..494d61ea877 100644 --- a/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx +++ b/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx @@ -17,7 +17,7 @@ import { import { type CoinStruct } from '@iota/iota-sdk/client'; import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; import { Form, Formik } from 'formik'; -import { useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { InfoBox, @@ -148,9 +148,6 @@ export function SendTokenForm({ const newPayIotaAll = parseAmount(values.amount, coinDecimals) === coinBalance && coinType === IOTA_TYPE_ARG; - if (values.isPayAllIota !== newPayIotaAll) { - setFieldValue('isPayAllIota', newPayIotaAll); - } const hasEnoughBalance = values.isPayAllIota || @@ -170,6 +167,12 @@ export function SendTokenForm({ queryResult.isPending || !coinBalance; + useEffect(() => { + if (values.isPayAllIota !== newPayIotaAll) { + setFieldValue('isPayAllIota', newPayIotaAll); + } + }, [values.isPayAllIota, newPayIotaAll]); + return ( <div className="flex h-full w-full flex-col"> <Form autoComplete="off" noValidate className="flex-1"> From be3b938bcb383651782b26ddd546ae9fba241942 Mon Sep 17 00:00:00 2001 From: Eugene P <panteleymonchuk@gmail.com> Date: Tue, 19 Nov 2024 18:04:54 +0200 Subject: [PATCH 69/87] refactor(wallet-dashboard): simplify validator info retrieval and add utility for total stake calculation --- .../core/src/hooks/stake/useValidatorInfo.tsx | 31 ++++++++------- .../src/utils/stake/getTotalValidatorStake.ts | 9 +++++ apps/core/src/utils/stake/index.ts | 1 + .../Dialogs/Staking/views/DetailsView.tsx | 38 +++++++------------ .../Dialogs/Staking/views/EnterAmountView.tsx | 4 +- apps/wallet/src/shared/constants.ts | 2 - .../src/ui/app/staking/home/StakedCard.tsx | 2 +- 7 files changed, 44 insertions(+), 43 deletions(-) create mode 100644 apps/core/src/utils/stake/getTotalValidatorStake.ts diff --git a/apps/core/src/hooks/stake/useValidatorInfo.tsx b/apps/core/src/hooks/stake/useValidatorInfo.tsx index 3b8ec1bacf7..064dd1d7d74 100644 --- a/apps/core/src/hooks/stake/useValidatorInfo.tsx +++ b/apps/core/src/hooks/stake/useValidatorInfo.tsx @@ -1,27 +1,24 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { useMemo } from 'react'; import { useIotaClientQuery } from '@iota/dapp-kit'; import { useGetValidatorsApy } from '../'; +import { getTotalValidatorStake } from '../../utils'; export function useValidatorInfo({ validatorAddress }: { validatorAddress: string }) { - const { data: system } = useIotaClientQuery('getLatestIotaSystemState'); + const { + data: system, + isPending: isPendingValidators, + isError: errorValidators, + } = useIotaClientQuery('getLatestIotaSystemState'); const { data: rollingAverageApys } = useGetValidatorsApy(); - const validatorSummary = useMemo(() => { - if (!system) return null; - - return ( - system.activeValidators.find( - (validator) => validator.iotaAddress === validatorAddress, - ) || null - ); - }, [validatorAddress, system]); + const validatorSummary = + system?.activeValidators.find((validator) => validator.iotaAddress === validatorAddress) || + null; const currentEpoch = Number(system?.epoch || 0); - //TODO: verify this is the correct validator stake balance - const totalValidatorStake = validatorSummary?.stakingPoolIotaBalance || 0; + const totalValidatorStake = getTotalValidatorStake(validatorSummary); const stakingPoolActivationEpoch = Number(validatorSummary?.stakingPoolActivationEpoch || 0); @@ -36,12 +33,18 @@ export function useValidatorInfo({ validatorAddress }: { validatorAddress: strin apy: null, }; + const commission = validatorSummary ? Number(validatorSummary.commissionRate) / 100 : 0; + return { system, + isPendingValidators, + errorValidators, + + currentEpoch, validatorSummary, name: validatorSummary?.name || '', stakingPoolActivationEpoch, - commission: validatorSummary ? Number(validatorSummary.commissionRate) / 100 : 0, + commission, newValidator, isAtRisk, apy, diff --git a/apps/core/src/utils/stake/getTotalValidatorStake.ts b/apps/core/src/utils/stake/getTotalValidatorStake.ts new file mode 100644 index 00000000000..2ecdaa8d887 --- /dev/null +++ b/apps/core/src/utils/stake/getTotalValidatorStake.ts @@ -0,0 +1,9 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { IotaValidatorSummary } from '@iota/iota-sdk/client'; + +//TODO: verify this is the correct validator stake balance +export function getTotalValidatorStake(validatorSummary: IotaValidatorSummary | null) { + return validatorSummary?.stakingPoolIotaBalance || 0; +} diff --git a/apps/core/src/utils/stake/index.ts b/apps/core/src/utils/stake/index.ts index 6ecca7353f6..6c4f408d42f 100644 --- a/apps/core/src/utils/stake/index.ts +++ b/apps/core/src/utils/stake/index.ts @@ -7,3 +7,4 @@ export * from './createStakeTransaction'; export * from './createTimelockedUnstakeTransaction'; export * from './createTimelockedStakeTransaction'; export * from './createValidationSchema'; +export * from './getTotalValidatorStake'; diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/views/DetailsView.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/views/DetailsView.tsx index c6bee99c1b6..79106443924 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/views/DetailsView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/views/DetailsView.tsx @@ -3,12 +3,12 @@ import React from 'react'; import { - useGetValidatorsApy, ExtendedDelegatedStake, ImageIcon, ImageIconSize, useFormatCoin, formatPercentageDisplay, + useValidatorInfo, } from '@iota/core'; import { Header, @@ -29,7 +29,6 @@ import { LoadingIndicator, } from '@iota/apps-ui-kit'; import { Warning } from '@iota/ui-icons'; -import { useIotaClientQuery } from '@iota/dapp-kit'; import { formatAddress, IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; import { Layout, LayoutFooter, LayoutBody } from './Layout'; @@ -51,33 +50,24 @@ export function DetailsView({ const totalStake = BigInt(stakedDetails?.principal || 0n); const validatorAddress = stakedDetails?.validatorAddress; const { - data: system, - isPending: loadingValidators, - isError: errorValidators, - } = useIotaClientQuery('getLatestIotaSystemState'); - const { data: rollingAverageApys } = useGetValidatorsApy(); - const { apy, isApyApproxZero } = rollingAverageApys?.[validatorAddress] ?? { - apy: null, - }; + isAtRisk, + isPendingValidators, + errorValidators, + validatorSummary, + apy, + isApyApproxZero, + newValidator, + commission, + } = useValidatorInfo({ + validatorAddress, + }); + const iotaEarned = BigInt(stakedDetails?.estimatedReward || 0n); const [iotaEarnedFormatted, iotaEarnedSymbol] = useFormatCoin(iotaEarned, IOTA_TYPE_ARG); const [totalStakeFormatted, totalStakeSymbol] = useFormatCoin(totalStake, IOTA_TYPE_ARG); - // flag if the validator is at risk of being removed from the active set - const isAtRisk = system?.atRiskValidators.some((item) => item[0] === validatorAddress); - - const validatorSummary = - system?.activeValidators.find((validator) => validator.iotaAddress === validatorAddress) || - null; - const validatorName = validatorSummary?.name || ''; - const stakingPoolActivationEpoch = Number(validatorSummary?.stakingPoolActivationEpoch || 0); - const currentEpoch = Number(system?.epoch || 0); - const commission = validatorSummary ? Number(validatorSummary.commissionRate) / 100 : 0; - // flag as new validator if the validator was activated in the last epoch - // for genesis validators, this will be false - const newValidator = currentEpoch - stakingPoolActivationEpoch <= 1 && currentEpoch !== 0; const subtitle = showActiveStatus ? ( <div className="flex items-center gap-1"> {formatAddress(validatorAddress)} @@ -88,7 +78,7 @@ export function DetailsView({ formatAddress(validatorAddress) ); - if (loadingValidators) { + if (isPendingValidators) { return ( <div className="flex h-full w-full items-center justify-center p-2"> <LoadingIndicator /> diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx index 9fb52f10709..991606f248d 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx @@ -75,7 +75,7 @@ function EnterAmountView({ CoinFormat.FULL, ); - const _gasBudget = BigInt(gasBudget ?? 0); + const gasBudgetBigInt = BigInt(gasBudget ?? 0); const [gas, symbol] = useFormatCoin(gasBudget, IOTA_TYPE_ARG); const { stakedRewardsStartEpoch, timeBeforeStakeRewardsRedeemableAgoDisplay } = useStakeTxnInfo( @@ -83,7 +83,7 @@ function EnterAmountView({ ); const hasEnoughRemaingBalance = - maxTokenBalance > parseAmount(values.amount, decimals) + BigInt(2) * _gasBudget; + maxTokenBalance > parseAmount(values.amount, decimals) + BigInt(2) * gasBudgetBigInt; const shouldShowInsufficientRemainingFundsWarning = maxTokenFormatted >= values.amount && !hasEnoughRemaingBalance; diff --git a/apps/wallet/src/shared/constants.ts b/apps/wallet/src/shared/constants.ts index 61f24fe3181..73f2f49935d 100644 --- a/apps/wallet/src/shared/constants.ts +++ b/apps/wallet/src/shared/constants.ts @@ -7,8 +7,6 @@ export const FAQ_LINK = 'https://docs.iota.org/about-iota/iota-wallet/FAQ '; // number of epochs before earning // Staking Rewards Redeemable -export const NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE = 2; -export const NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_STARTS = 1; export const MIN_NUMBER_IOTA_TO_STAKE = 1; export const DEFAULT_APP_NAME = 'IOTA Wallet'; diff --git a/apps/wallet/src/ui/app/staking/home/StakedCard.tsx b/apps/wallet/src/ui/app/staking/home/StakedCard.tsx index b945fbe025b..64ef1a09757 100644 --- a/apps/wallet/src/ui/app/staking/home/StakedCard.tsx +++ b/apps/wallet/src/ui/app/staking/home/StakedCard.tsx @@ -2,7 +2,7 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE } from '_src/shared/constants'; +import { NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE } from '@iota/core'; import { determineCountDownText } from '_src/ui/app/shared/countdown-timer'; import { type ExtendedDelegatedStake, From 80049ac1cceafc517b057e09a6c94dc8692a49a1 Mon Sep 17 00:00:00 2001 From: Eugene P <panteleymonchuk@gmail.com> Date: Tue, 19 Nov 2024 18:18:19 +0200 Subject: [PATCH 70/87] refactor(wallet-dashboard): streamline stake calculations and integrate new validator details utility --- .../Dialogs/Staking/views/StakedInfo.tsx | 54 ++++--------------- 1 file changed, 11 insertions(+), 43 deletions(-) diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/views/StakedInfo.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/views/StakedInfo.tsx index 95890afd07f..322d0c150e2 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/views/StakedInfo.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/views/StakedInfo.tsx @@ -1,16 +1,9 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { useMemo } from 'react'; -import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; import { formatPercentageDisplay, - calculateStakeShare, - useFormatCoin, - getTokenStakeIotaForValidator, - useGetDelegatedStake, - DELEGATED_STAKES_QUERY_REFETCH_INTERVAL, - DELEGATED_STAKES_QUERY_STALE_TIME, useValidatorInfo, + useGetStakingValidatorDetails, } from '@iota/core'; import { KeyValueInfo, Panel, TooltipPosition } from '@iota/apps-ui-kit'; @@ -21,45 +14,20 @@ export function StakedInfo({ validatorAddress: string; accountAddress: string; }) { - const { data: delegatedStake } = useGetDelegatedStake({ - address: accountAddress || '', - staleTime: DELEGATED_STAKES_QUERY_STALE_TIME, - refetchInterval: DELEGATED_STAKES_QUERY_REFETCH_INTERVAL, - }); - const { apy, isApyApproxZero, validatorSummary, system } = useValidatorInfo({ + const { apy, isApyApproxZero } = useValidatorInfo({ validatorAddress: validatorAddress, }); - const totalValidatorsStake = useMemo(() => { - if (!system) return 0; - return system.activeValidators.reduce( - (acc, curr) => (acc += BigInt(curr.stakingPoolIotaBalance)), - 0n, - ); - }, [system]); - - const totalStakePercentage = useMemo(() => { - if (!system || !validatorSummary) return null; - - return calculateStakeShare( - BigInt(validatorSummary.stakingPoolIotaBalance), - BigInt(totalValidatorsStake), - ); - }, [system, totalValidatorsStake, validatorSummary]); + const { totalValidatorsStake, totalStakePercentage, totalStake } = + useGetStakingValidatorDetails({ + accountAddress: accountAddress, + stakeId: null, + validatorAddress: validatorAddress, + unstake: false, + }); - const totalStake = useMemo(() => { - if (!delegatedStake) return 0n; - return getTokenStakeIotaForValidator(delegatedStake, validatorAddress); - }, [delegatedStake, validatorAddress]); - - //TODO: verify this is the correct validator stake balance - const totalValidatorStake = validatorSummary?.stakingPoolIotaBalance || 0; - - const [totalValidatorStakeFormatted, totalValidatorStakeSymbol] = useFormatCoin( - totalValidatorStake, - IOTA_TYPE_ARG, - ); - const [totalStakeFormatted, totalStakeSymbol] = useFormatCoin(totalStake, IOTA_TYPE_ARG); + const [totalValidatorStakeFormatted, totalValidatorStakeSymbol] = totalValidatorsStake; + const [totalStakeFormatted, totalStakeSymbol] = totalStake; return ( <Panel hasBorder> From 6eca20115a08aa23942d9c1d19e97371beda30af Mon Sep 17 00:00:00 2001 From: cpl121 <cpeon@boxfish.studio> Date: Wed, 20 Nov 2024 10:39:52 +0100 Subject: [PATCH 71/87] fix(wallet-dashboard): add setFieldValue in useEffect --- .../components/Dialogs/SendToken/views/EnterValuesFormView.tsx | 2 +- .../src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx b/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx index 31650a6a965..05d75a581af 100644 --- a/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx @@ -98,7 +98,7 @@ function FormInputs({ if (values.isPayAllIota !== newPayIotaAll) { setFieldValue('isPayAllIota', newPayIotaAll); } - }, [values.isPayAllIota, newPayIotaAll]); + }, [values.isPayAllIota, newPayIotaAll, setFieldValue]); return ( <div className="flex h-full w-full flex-col"> diff --git a/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx b/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx index 494d61ea877..a9b263403f3 100644 --- a/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx +++ b/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx @@ -171,7 +171,7 @@ export function SendTokenForm({ if (values.isPayAllIota !== newPayIotaAll) { setFieldValue('isPayAllIota', newPayIotaAll); } - }, [values.isPayAllIota, newPayIotaAll]); + }, [values.isPayAllIota, newPayIotaAll, setFieldValue]); return ( <div className="flex h-full w-full flex-col"> From 1292ff6367811819ba35c428f75eb47113e3ea12 Mon Sep 17 00:00:00 2001 From: JCNoguera <88061365+VmMad@users.noreply.github.com> Date: Wed, 20 Nov 2024 13:08:24 +0100 Subject: [PATCH 72/87] fix: gas ticker --- .../src/components/Inputs/SendTokenFormInput.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/core/src/components/Inputs/SendTokenFormInput.tsx b/apps/core/src/components/Inputs/SendTokenFormInput.tsx index 637087717e0..5c49038945b 100644 --- a/apps/core/src/components/Inputs/SendTokenFormInput.tsx +++ b/apps/core/src/components/Inputs/SendTokenFormInput.tsx @@ -39,9 +39,11 @@ export function SendTokenFormInput({ to: to, amount: values.amount, isPayAllIota: values.isPayAllIota, - showGasSymbol: false, }); - const [formattedGasBudgetEstimation] = useFormatCoin(gasBudgetEstimation, IOTA_TYPE_ARG); + const [formattedGasBudgetEstimation, gasToken] = useFormatCoin( + gasBudgetEstimation, + IOTA_TYPE_ARG, + ); const [field, meta, helpers] = useField<string>(name); const errorMessage = meta.error; @@ -53,6 +55,10 @@ export function SendTokenFormInput({ </ButtonPill> ); + const gasAmount = formattedGasBudgetEstimation + ? formattedGasBudgetEstimation + ' ' + gasToken + : undefined; + // gasBudgetEstimation should change when the amount above changes useEffect(() => { setFieldValue('gasBudgetEst', formattedGasBudgetEstimation, false); @@ -71,9 +77,7 @@ export function SendTokenFormInput({ prefix={values.isPayAllIota ? '~ ' : undefined} allowNegative={false} errorMessage={errorMessage} - amountCounter={ - !errorMessage ? (coins ? formattedGasBudgetEstimation : '--') : undefined - } + amountCounter={!errorMessage ? (coins ? gasAmount : '--') : undefined} trailingElement={renderAction()} decimalScale={coinDecimals ? undefined : 0} thousandSeparator From a5f5f583148828266dea45e2d735f83430160afc Mon Sep 17 00:00:00 2001 From: JCNoguera <88061365+VmMad@users.noreply.github.com> Date: Wed, 20 Nov 2024 13:52:30 +0100 Subject: [PATCH 73/87] fix: lint --- apps/wallet/src/ui/app/components/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/wallet/src/ui/app/components/index.ts b/apps/wallet/src/ui/app/components/index.ts index 418c14428a2..5dd1c520da6 100644 --- a/apps/wallet/src/ui/app/components/index.ts +++ b/apps/wallet/src/ui/app/components/index.ts @@ -13,7 +13,6 @@ export * from './SummaryCard'; export * from './WalletListSelect'; export * from './accounts'; export * from './active-coins-card'; -export * from './active-coins-card/CoinItem'; export * from './error-boundary'; export * from './explorer-link'; export * from './explorer-link/Explorer'; From 0cfd63733f840a61693beccd4364de9fe629d5ad Mon Sep 17 00:00:00 2001 From: Eugene P <panteleymonchuk@gmail.com> Date: Wed, 20 Nov 2024 15:37:57 +0200 Subject: [PATCH 74/87] refactor(wallet, core): update import paths for consistency and clarity --- apps/core/src/components/coin/CoinIcon.tsx | 3 +-- apps/core/src/components/index.ts | 1 - apps/wallet/src/ui/app/components/receipt-card/TxnAmount.tsx | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/core/src/components/coin/CoinIcon.tsx b/apps/core/src/components/coin/CoinIcon.tsx index d119ff5f44c..9f7e3b23ee7 100644 --- a/apps/core/src/components/coin/CoinIcon.tsx +++ b/apps/core/src/components/coin/CoinIcon.tsx @@ -3,10 +3,9 @@ // SPDX-License-Identifier: Apache-2.0 import React from 'react'; -import { useCoinMetadata, ImageIcon, ImageIconSize } from '@iota/core'; +import { useCoinMetadata, ImageIcon, ImageIconSize } from '../../index'; import { IotaLogoMark } from '@iota/ui-icons'; import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; -import { ImageIcon, ImageIconSize } from '../icon'; import cx from 'clsx'; interface NonIotaCoinProps { diff --git a/apps/core/src/components/index.ts b/apps/core/src/components/index.ts index 435a2aa63fd..0a8093eeadb 100644 --- a/apps/core/src/components/index.ts +++ b/apps/core/src/components/index.ts @@ -7,4 +7,3 @@ export * from './coin'; export * from './icon'; export * from './Inputs'; export * from './QR'; -export * from './image-icon'; diff --git a/apps/wallet/src/ui/app/components/receipt-card/TxnAmount.tsx b/apps/wallet/src/ui/app/components/receipt-card/TxnAmount.tsx index 120c6a0e0a8..411a42bb16c 100644 --- a/apps/wallet/src/ui/app/components/receipt-card/TxnAmount.tsx +++ b/apps/wallet/src/ui/app/components/receipt-card/TxnAmount.tsx @@ -12,7 +12,6 @@ import { CardType, ImageType, } from '@iota/apps-ui-kit'; -import { ImageIconSize } from '../../shared/image-icon'; interface TxnAmountProps { amount: string | number | bigint; From 3ce05be85a790298ab95850799ece410670a4a16 Mon Sep 17 00:00:00 2001 From: JCNoguera <88061365+VmMad@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:33:23 +0100 Subject: [PATCH 75/87] fix: improve codebase as reviewed --- .../core/src/components/coin/CoinSelector.tsx | 25 ++++++++++++++++--- .../Dialogs/SendToken/SendTokenDialog.tsx | 3 +++ .../Dialogs/SendToken/constants/index.ts | 1 + .../Dialogs/SendToken/interfaces/index.ts | 1 + .../SendToken/views/EnterValuesFormView.tsx | 12 ++++++--- .../SendToken/views/ReviewValuesFormView.tsx | 4 +-- .../components/coins/MyCoins.tsx | 2 +- apps/wallet-dashboard/next.config.mjs | 2 +- 8 files changed, 38 insertions(+), 12 deletions(-) diff --git a/apps/core/src/components/coin/CoinSelector.tsx b/apps/core/src/components/coin/CoinSelector.tsx index 254b0180982..3f2f568811c 100644 --- a/apps/core/src/components/coin/CoinSelector.tsx +++ b/apps/core/src/components/coin/CoinSelector.tsx @@ -8,7 +8,11 @@ import { useFormatCoin } from '../../hooks'; import { CoinIcon } from './CoinIcon'; import { ImageIconSize } from '../icon'; -interface CoinSelectorProps { +interface CoinSelectorBaseProps { + hasCoinWrapper?: boolean; +} + +interface CoinSelectorProps extends CoinSelectorBaseProps { activeCoinType: string; coins: CoinBalance[]; onClick: (coinType: string) => void; @@ -18,13 +22,14 @@ export function CoinSelector({ activeCoinType = IOTA_TYPE_ARG, coins, onClick, + hasCoinWrapper, }: CoinSelectorProps) { const activeCoin = coins?.find(({ coinType }) => coinType === activeCoinType) ?? coins?.[0]; const initialValue = activeCoin?.coinType; const coinsOptions: SelectOption[] = coins?.map((coin) => ({ id: coin.coinType, - renderLabel: () => <CoinSelectOption coin={coin} />, + renderLabel: () => <CoinSelectOption hasCoinWrapper={hasCoinWrapper} coin={coin} />, })) || []; return ( @@ -39,7 +44,14 @@ export function CoinSelector({ ); } -function CoinSelectOption({ coin: { coinType, totalBalance } }: { coin: CoinBalance }) { +interface CoinSelectOptionProps extends CoinSelectorBaseProps { + coin: CoinBalance; +} + +function CoinSelectOption({ + coin: { coinType, totalBalance }, + hasCoinWrapper, +}: CoinSelectOptionProps) { const [formatted, symbol, { data: coinMeta }] = useFormatCoin(totalBalance, coinType); const isIota = coinType === IOTA_TYPE_ARG; @@ -47,7 +59,12 @@ function CoinSelectOption({ coin: { coinType, totalBalance } }: { coin: CoinBala <div className="flex w-full flex-row items-center justify-between"> <div className="flex flex-row items-center gap-x-md"> <div className="flex h-6 w-6 items-center justify-center"> - <CoinIcon size={ImageIconSize.Small} coinType={coinType} rounded /> + <CoinIcon + size={ImageIconSize.Small} + coinType={coinType} + rounded + hasCoinWrapper={hasCoinWrapper} + /> </div> <span className="text-body-lg text-neutral-10"> {isIota ? (coinMeta?.name || '').toUpperCase() : coinMeta?.name || symbol} diff --git a/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx b/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx index 36a68d3bf6f..f6983a2428d 100644 --- a/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx @@ -76,6 +76,8 @@ function SendTokenDialogBody({ setStep(FormStep.EnterValues); } + console.log({ formData }); + return ( <> <Header @@ -92,6 +94,7 @@ function SendTokenDialogBody({ setSelectedCoin={setSelectedCoin} onNext={onNext} setFormData={setFormData} + initialFormValues={formData} /> )} {step === FormStep.ReviewValues && ( diff --git a/apps/wallet-dashboard/components/Dialogs/SendToken/constants/index.ts b/apps/wallet-dashboard/components/Dialogs/SendToken/constants/index.ts index 319d67de744..58f1e99acca 100644 --- a/apps/wallet-dashboard/components/Dialogs/SendToken/constants/index.ts +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/constants/index.ts @@ -6,6 +6,7 @@ import { FormDataValues } from '../interfaces'; export const INITIAL_VALUES: FormDataValues = { to: '', amount: '', + formattedAmount: '', isPayAllIota: false, gasBudgetEst: '', }; diff --git a/apps/wallet-dashboard/components/Dialogs/SendToken/interfaces/index.ts b/apps/wallet-dashboard/components/Dialogs/SendToken/interfaces/index.ts index 06ae882b260..983b77712a3 100644 --- a/apps/wallet-dashboard/components/Dialogs/SendToken/interfaces/index.ts +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/interfaces/index.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 export interface FormDataValues { amount: string; + formattedAmount: string; to: string; isPayAllIota: boolean; gasBudgetEst: string; diff --git a/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx b/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx index e7aa08c9825..ff5fb1a9c3e 100644 --- a/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx @@ -37,6 +37,7 @@ import { INITIAL_VALUES } from '../constants'; interface EnterValuesFormProps { coin: CoinBalance; activeAddress: string; + initialFormValues: FormDataValues; setFormData: React.Dispatch<React.SetStateAction<FormDataValues>>; setSelectedCoin: React.Dispatch<React.SetStateAction<CoinBalance>>; onNext: () => void; @@ -150,6 +151,7 @@ export function EnterValuesFormView({ setFormData, setSelectedCoin, onNext, + initialFormValues, }: EnterValuesFormProps): JSX.Element { // Get all coins of the type const { data: coinsData, isPending: coinsIsPending } = useGetAllCoins( @@ -213,7 +215,8 @@ export function EnterValuesFormView({ const data = { to, - amount: formattedAmount, + amount, + formattedAmount, isPayAllIota, coins, coinIds: coinsIDs, @@ -237,13 +240,14 @@ export function EnterValuesFormView({ <Formik initialValues={{ - amount: '', - to: '', + amount: initialFormValues.amount ?? '', + to: initialFormValues.to ?? '', + formattedAmount: initialFormValues.formattedAmount ?? '', isPayAllIota: !!initAmountBig && initAmountBig === coinBalance && coin.coinType === IOTA_TYPE_ARG, - gasBudgetEst: '', + gasBudgetEst: initialFormValues.gasBudgetEst ?? '', }} validationSchema={validationSchemaStepOne} enableReinitialize diff --git a/apps/wallet-dashboard/components/Dialogs/SendToken/views/ReviewValuesFormView.tsx b/apps/wallet-dashboard/components/Dialogs/SendToken/views/ReviewValuesFormView.tsx index 13aa977ed9e..9fe1c1a1e60 100644 --- a/apps/wallet-dashboard/components/Dialogs/SendToken/views/ReviewValuesFormView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/views/ReviewValuesFormView.tsx @@ -31,13 +31,13 @@ interface ReviewValuesFormProps { } export function ReviewValuesFormView({ - formData: { amount, to, isPayAllIota, gasBudgetEst }, + formData: { amount, to, formattedAmount, isPayAllIota, gasBudgetEst }, senderAddress, isPending, executeTransfer, coinType, }: ReviewValuesFormProps): JSX.Element { - const [formatAmount, symbol] = useFormatCoin(amount, coinType); + const [formatAmount, symbol] = useFormatCoin(formattedAmount, coinType); return ( <div className="flex h-full flex-col"> <div className="flex h-full w-full flex-col gap-md"> diff --git a/apps/wallet-dashboard/components/coins/MyCoins.tsx b/apps/wallet-dashboard/components/coins/MyCoins.tsx index ef3878b4e6f..fc441d76148 100644 --- a/apps/wallet-dashboard/components/coins/MyCoins.tsx +++ b/apps/wallet-dashboard/components/coins/MyCoins.tsx @@ -19,7 +19,7 @@ import { Title, } from '@iota/apps-ui-kit'; import { RecognizedBadge } from '@iota/ui-icons'; -import { SendTokenDialog } from '../Dialogs/SendToken/SendTokenDialog'; +import { SendTokenDialog } from '@/components'; enum TokenCategory { All = 'All', diff --git a/apps/wallet-dashboard/next.config.mjs b/apps/wallet-dashboard/next.config.mjs index 1c961b95df9..a7417e613fd 100644 --- a/apps/wallet-dashboard/next.config.mjs +++ b/apps/wallet-dashboard/next.config.mjs @@ -14,7 +14,7 @@ const nextConfig = { }, images: { // Remove this domain when fetching data - domains: ['d315pvdvxi2gex.cloudfront.net', 'd122fl2kiki5hg.cloudfront.net'], + domains: ['d315pvdvxi2gex.cloudfront.net'], }, }; From dc99beba2c1132df7d6e7a3b0051552fa3db3258 Mon Sep 17 00:00:00 2001 From: Eugene P <panteleymonchuk@gmail.com> Date: Wed, 20 Nov 2024 18:02:55 +0200 Subject: [PATCH 76/87] refactor(wallet-dashboard): integrate FormikProvider. Polish interfaces. --- .../Dialogs/Staking/StakeDialog.tsx | 57 ++++++++++--------- .../Dialogs/Staking/views/EnterAmountView.tsx | 8 +-- .../Dialogs/Staking/views/StakedInfo.tsx | 9 ++- .../Dialogs/Staking/views/Validator.tsx | 16 +++--- 4 files changed, 43 insertions(+), 47 deletions(-) diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx index c86dd3ae48c..912579c3ede 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import React, { useState, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { EnterAmountView, SelectValidatorView } from './views'; import { useNotifications, @@ -19,7 +19,7 @@ import { useBalance, createValidationSchema, } from '@iota/core'; -import { Formik } from 'formik'; +import { FormikProvider, useFormik } from 'formik'; import type { FormikHelpers } from 'formik'; import { useCurrentAccount, useSignAndExecuteTransaction } from '@iota/dapp-kit'; import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; @@ -66,13 +66,36 @@ export function StakeDialog({ selectedValidator = '', setSelectedValidator, }: StakeDialogProps): JSX.Element { - const [amount, setAmount] = useState<string>(''); - const account = useCurrentAccount(); const senderAddress = account?.address ?? ''; + const { data: iotaBalance } = useBalance(senderAddress!); + const coinBalance = BigInt(iotaBalance?.totalBalance || 0); const { data: metadata } = useCoinMetadata(IOTA_TYPE_ARG); const coinDecimals = metadata?.decimals ?? 0; + const coinSymbol = metadata?.symbol ?? ''; + const minimumStake = parseAmount(MIN_NUMBER_IOTA_TO_STAKE.toString(), coinDecimals); + + const validationSchema = useMemo( + () => + createValidationSchema( + coinBalance, + coinSymbol, + coinDecimals, + view === StakeDialogView.Unstake, + minimumStake, + ), + [coinBalance, coinSymbol, coinDecimals, view, minimumStake], + ); + + const formik = useFormik({ + initialValues: INITIAL_VALUES, + validationSchema: validationSchema, + onSubmit: onSubmit, + validateOnMount: true, + }); + + const amount = formik.values.amount; const amountWithoutDecimals = parseAmount(amount, coinDecimals); const { data: currentEpochMs } = useGetCurrentEpochStartTimestamp(); const { data: timelockedObjects } = useGetAllOwnedObjects(senderAddress, { @@ -99,25 +122,9 @@ export function StakeDialog({ const { mutateAsync: signAndExecuteTransaction } = useSignAndExecuteTransaction(); const { addNotification } = useNotifications(); const { data: rollingAverageApys } = useGetValidatorsApy(); - const { data: iotaBalance } = useBalance(senderAddress!); - const coinBalance = BigInt(iotaBalance?.totalBalance || 0); - const minimumStake = parseAmount(MIN_NUMBER_IOTA_TO_STAKE.toString(), coinDecimals); - const coinSymbol = metadata?.symbol ?? ''; const validators = Object.keys(rollingAverageApys ?? {}) ?? []; - const validationSchema = useMemo( - () => - createValidationSchema( - coinBalance, - coinSymbol, - coinDecimals, - view === StakeDialogView.Unstake, - minimumStake, - ), - [coinBalance, coinSymbol, coinDecimals, view, minimumStake], - ); - function handleBack(): void { setView?.(StakeDialogView.SelectValidator); } @@ -177,12 +184,7 @@ export function StakeDialog({ return ( <Dialog open={isOpen} onOpenChange={() => handleClose()}> - <Formik - initialValues={INITIAL_VALUES} - validationSchema={validationSchema} - onSubmit={onSubmit} - validateOnMount - > + <FormikProvider value={formik}> <> {view === StakeDialogView.Details && stakedDetails && ( <DetailsView @@ -205,7 +207,6 @@ export function StakeDialog({ <EnterAmountView selectedValidator={selectedValidator} handleClose={handleClose} - setAmount={setAmount} onBack={handleBack} onStake={handleStake} gasBudget={newStakeData?.gasBudget} @@ -219,7 +220,7 @@ export function StakeDialog({ /> )} </> - </Formik> + </FormikProvider> </Dialog> ); } diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx index 991606f248d..e30311cc826 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/views/EnterAmountView.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import React, { useEffect } from 'react'; +import React from 'react'; import { useFormatCoin, useBalance, @@ -38,7 +38,6 @@ export interface FormValues { interface EnterAmountViewProps { selectedValidator: string; - setAmount: (amount: string) => void; onBack: () => void; onStake: () => void; showActiveStatus?: boolean; @@ -52,7 +51,6 @@ function EnterAmountView({ onStake, gasBudget = 0, handleClose, - setAmount, }: EnterAmountViewProps): JSX.Element { const coinType = IOTA_TYPE_ARG; const { data: metadata } = useCoinMetadata(coinType); @@ -87,10 +85,6 @@ function EnterAmountView({ const shouldShowInsufficientRemainingFundsWarning = maxTokenFormatted >= values.amount && !hasEnoughRemaingBalance; - useEffect(() => { - setAmount(amount); - }, [amount, setAmount]); - return ( <Layout> <Header title="Enter amount" onClose={handleClose} onBack={handleClose} titleCentered /> diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/views/StakedInfo.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/views/StakedInfo.tsx index 322d0c150e2..0f5cfb87a78 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/views/StakedInfo.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/views/StakedInfo.tsx @@ -7,13 +7,12 @@ import { } from '@iota/core'; import { KeyValueInfo, Panel, TooltipPosition } from '@iota/apps-ui-kit'; -export function StakedInfo({ - validatorAddress, - accountAddress, -}: { +interface StakedInfoProps { validatorAddress: string; accountAddress: string; -}) { +} + +export function StakedInfo({ validatorAddress, accountAddress }: StakedInfoProps) { const { apy, isApyApproxZero } = useValidatorInfo({ validatorAddress: validatorAddress, }); diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/views/Validator.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/views/Validator.tsx index 1b35e06f91e..9154a6959da 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/views/Validator.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/views/Validator.tsx @@ -13,19 +13,21 @@ import { } from '@iota/apps-ui-kit'; import { formatAddress } from '@iota/iota-sdk/utils'; +interface ValidatorProps { + isSelected: boolean; + address: string; + showActiveStatus?: boolean; + onClick?: (address: string) => void; + showAction?: boolean; +} + export function Validator({ address, showActiveStatus, onClick, isSelected, showAction = true, -}: { - isSelected: boolean; - address: string; - showActiveStatus?: boolean; - onClick?: (address: string) => void; - showAction?: boolean; -}) { +}: ValidatorProps) { const { name, newValidator, isAtRisk, apy, isApyApproxZero } = useValidatorInfo({ validatorAddress: address, }); From 4737361a6324d385ae212649c367037571961a6a Mon Sep 17 00:00:00 2001 From: JCNoguera <88061365+VmMad@users.noreply.github.com> Date: Thu, 21 Nov 2024 10:02:34 +0100 Subject: [PATCH 77/87] fix: remove log and use isPayAllIota from form values --- .../components/Dialogs/SendToken/SendTokenDialog.tsx | 2 -- .../Dialogs/SendToken/views/EnterValuesFormView.tsx | 7 ++++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx b/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx index f6983a2428d..9ec1e45bf0b 100644 --- a/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/SendTokenDialog.tsx @@ -76,8 +76,6 @@ function SendTokenDialogBody({ setStep(FormStep.EnterValues); } - console.log({ formData }); - return ( <> <Header diff --git a/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx b/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx index ff5fb1a9c3e..2f9598c29ef 100644 --- a/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx +++ b/apps/wallet-dashboard/components/Dialogs/SendToken/views/EnterValuesFormView.tsx @@ -244,9 +244,10 @@ export function EnterValuesFormView({ to: initialFormValues.to ?? '', formattedAmount: initialFormValues.formattedAmount ?? '', isPayAllIota: - !!initAmountBig && - initAmountBig === coinBalance && - coin.coinType === IOTA_TYPE_ARG, + initialFormValues.isPayAllIota ?? + (!!initAmountBig && + initAmountBig === coinBalance && + coin.coinType === IOTA_TYPE_ARG), gasBudgetEst: initialFormValues.gasBudgetEst ?? '', }} validationSchema={validationSchemaStepOne} From 111e1ad3e2bfcc5f2580073e98da58a319be9425 Mon Sep 17 00:00:00 2001 From: Branko Bosnic <brankobosnic1@gmail.com> Date: Fri, 22 Nov 2024 10:37:33 +0100 Subject: [PATCH 78/87] fix: lint --- apps/core/src/hooks/stake/useStakeTxnInfo.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/core/src/hooks/stake/useStakeTxnInfo.ts b/apps/core/src/hooks/stake/useStakeTxnInfo.ts index 5d6af5b82e5..212af05d0c7 100644 --- a/apps/core/src/hooks/stake/useStakeTxnInfo.ts +++ b/apps/core/src/hooks/stake/useStakeTxnInfo.ts @@ -1,9 +1,12 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { useGetTimeBeforeEpochNumber, useTimeAgo, TimeUnit } from '../../index'; - -export const NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE = 2; -export const NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_STARTS = 1; +import { + useGetTimeBeforeEpochNumber, + useTimeAgo, + TimeUnit, + NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_STARTS, + NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE, +} from '../../index'; export function useStakeTxnInfo(startEpoch?: string | number) { const startEarningRewardsEpoch = From bcbdebd4dc2a09e7fca5626288ff617b437a4b07 Mon Sep 17 00:00:00 2001 From: Panteleymonchuk <panteleymonchuk@gmail.com> Date: Fri, 22 Nov 2024 14:21:14 +0200 Subject: [PATCH 79/87] feat(wallet-dashboard): refactor staking dialog management for home --- apps/core/src/hooks/stake/useStakeTxnInfo.ts | 4 +- .../app/(protected)/staking/page.tsx | 36 +++++++++--------- .../Dialogs/Staking/hooks/useStakeDialog.ts | 38 +++++++++++++++++++ .../staking-overview/StartStaking.tsx | 34 +++++++++++------ 4 files changed, 79 insertions(+), 33 deletions(-) create mode 100644 apps/wallet-dashboard/components/Dialogs/Staking/hooks/useStakeDialog.ts diff --git a/apps/core/src/hooks/stake/useStakeTxnInfo.ts b/apps/core/src/hooks/stake/useStakeTxnInfo.ts index 5d6af5b82e5..75d25047020 100644 --- a/apps/core/src/hooks/stake/useStakeTxnInfo.ts +++ b/apps/core/src/hooks/stake/useStakeTxnInfo.ts @@ -22,7 +22,7 @@ export function useStakeTxnInfo(startEpoch?: string | number) { }); const stakedRewardsStartEpoch = timeBeforeStakeRewardsStarts > 0 - ? `${timeBeforeStakeRewardsStartsAgo === '--' ? '' : 'in'} ${timeBeforeStakeRewardsStartsAgo}` + ? `in ${timeBeforeStakeRewardsStartsAgo}` : startEpoch ? `Epoch #${Number(startEarningRewardsEpoch)}` : '--'; @@ -37,7 +37,7 @@ export function useStakeTxnInfo(startEpoch?: string | number) { }); const timeBeforeStakeRewardsRedeemableAgoDisplay = timeBeforeStakeRewardsRedeemable > 0 - ? `${timeBeforeStakeRewardsRedeemableAgo === '--' ? '' : 'in'} ${timeBeforeStakeRewardsRedeemableAgo}` + ? `in ${timeBeforeStakeRewardsRedeemableAgo}` : startEpoch ? `Epoch #${Number(redeemableRewardsEpoch)}` : '--'; diff --git a/apps/wallet-dashboard/app/(protected)/staking/page.tsx b/apps/wallet-dashboard/app/(protected)/staking/page.tsx index c7387c327c8..67978b8451f 100644 --- a/apps/wallet-dashboard/app/(protected)/staking/page.tsx +++ b/apps/wallet-dashboard/app/(protected)/staking/page.tsx @@ -4,7 +4,8 @@ 'use client'; import { AmountBox, Box, StakeCard, StakeDialog, Button } from '@/components'; -import { StakeDialogView } from '@/components/Dialogs/Staking/StakeDialog'; +import { useStakeDialog } from '@/components/Dialogs/Staking/hooks/useStakeDialog'; + import { ExtendedDelegatedStake, formatDelegatedStake, @@ -17,13 +18,23 @@ import { } from '@iota/core'; import { useCurrentAccount } from '@iota/dapp-kit'; import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; -import { useState } from 'react'; +import { StakeDialogView } from '@/components/Dialogs/Staking/StakeDialog'; function StakingDashboardPage(): JSX.Element { const account = useCurrentAccount(); - const [stakeDialogView, setStakeDialogView] = useState<StakeDialogView | undefined>(); - const [selectedStake, setSelectedStake] = useState<ExtendedDelegatedStake | null>(null); - const [selectedValidator, setSelectedValidator] = useState<string>(''); + + const { + isDialogStakeOpen, + stakeDialogView, + setStakeDialogView, + selectedStake, + setSelectedStake, + selectedValidator, + setSelectedValidator, + handleCloseStakeDialog, + handleNewStake, + } = useStakeDialog(); + const { data: delegatedStakeData } = useGetDelegatedStake({ address: account?.address || '', staleTime: DELEGATED_STAKES_QUERY_STALE_TIME, @@ -47,19 +58,6 @@ function StakingDashboardPage(): JSX.Element { setSelectedStake(extendedStake); }; - function handleCloseStakeDialog() { - setSelectedValidator(''); - setSelectedStake(null); - setStakeDialogView(undefined); - } - - function handleNewStake() { - setSelectedStake(null); - setStakeDialogView(StakeDialogView.SelectValidator); - } - - const isDialogStakeOpen = stakeDialogView !== undefined; - return ( <> <div className="flex flex-col items-center justify-center gap-4 pt-12"> @@ -89,7 +87,7 @@ function StakingDashboardPage(): JSX.Element { </Box> <Button onClick={handleNewStake}>New Stake</Button> </div> - {isDialogStakeOpen && ( + {isDialogStakeOpen && stakeDialogView && ( <StakeDialog stakedDetails={selectedStake} isOpen={isDialogStakeOpen} diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/hooks/useStakeDialog.ts b/apps/wallet-dashboard/components/Dialogs/Staking/hooks/useStakeDialog.ts new file mode 100644 index 00000000000..f14b97db803 --- /dev/null +++ b/apps/wallet-dashboard/components/Dialogs/Staking/hooks/useStakeDialog.ts @@ -0,0 +1,38 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { useState } from 'react'; +import { ExtendedDelegatedStake } from '@iota/core'; + +import { StakeDialogView } from '@/components/Dialogs/Staking/StakeDialog'; + +export function useStakeDialog() { + const [stakeDialogView, setStakeDialogView] = useState<StakeDialogView | undefined>(); + const [selectedStake, setSelectedStake] = useState<ExtendedDelegatedStake | null>(null); + const [selectedValidator, setSelectedValidator] = useState<string>(''); + + const isDialogStakeOpen = stakeDialogView !== undefined; + + function handleCloseStakeDialog() { + setSelectedValidator(''); + setSelectedStake(null); + setStakeDialogView(undefined); + } + + function handleNewStake() { + setSelectedStake(null); + setStakeDialogView(StakeDialogView.SelectValidator); + } + + return { + isDialogStakeOpen, + stakeDialogView, + setStakeDialogView, + selectedStake, + setSelectedStake, + selectedValidator, + setSelectedValidator, + handleCloseStakeDialog, + handleNewStake, + }; +} diff --git a/apps/wallet-dashboard/components/staking-overview/StartStaking.tsx b/apps/wallet-dashboard/components/staking-overview/StartStaking.tsx index 421df1beac6..77a99f1b9ad 100644 --- a/apps/wallet-dashboard/components/staking-overview/StartStaking.tsx +++ b/apps/wallet-dashboard/components/staking-overview/StartStaking.tsx @@ -3,17 +3,21 @@ import { Button, ButtonSize, ButtonType, Panel } from '@iota/apps-ui-kit'; import { Theme, useTheme } from '@/contexts'; -import { useState } from 'react'; import { StakeDialog } from '../Dialogs'; -import { StakeDialogView } from '../Dialogs/Staking/StakeDialog'; +import { useStakeDialog } from '../Dialogs/Staking/hooks/useStakeDialog'; export function StartStaking() { const { theme } = useTheme(); - const [isDialogStakeOpen, setIsDialogStakeOpen] = useState(false); - - function handleNewStake() { - setIsDialogStakeOpen(true); - } + const { + isDialogStakeOpen, + stakeDialogView, + setStakeDialogView, + selectedStake, + selectedValidator, + setSelectedValidator, + handleCloseStakeDialog, + handleNewStake, + } = useStakeDialog(); const videoSrc = theme === Theme.Dark @@ -51,11 +55,17 @@ export function StartStaking() { ></video> </div> </div> - <StakeDialog - isOpen={isDialogStakeOpen} - handleClose={() => setIsDialogStakeOpen(false)} - view={StakeDialogView.SelectValidator} - /> + {isDialogStakeOpen && stakeDialogView && ( + <StakeDialog + stakedDetails={selectedStake} + isOpen={isDialogStakeOpen} + handleClose={handleCloseStakeDialog} + view={stakeDialogView} + setView={setStakeDialogView} + selectedValidator={selectedValidator} + setSelectedValidator={setSelectedValidator} + /> + )} </Panel> ); } From b964e75688061440b42329eb4445c0e23ec824c9 Mon Sep 17 00:00:00 2001 From: Panteleymonchuk <panteleymonchuk@gmail.com> Date: Fri, 22 Nov 2024 14:28:38 +0200 Subject: [PATCH 80/87] feat(wallet-dashboard): move constants to another folder. --- apps/core/src/constants/staking.constants.ts | 3 +++ apps/core/src/hooks/stake/useStakeTxnInfo.ts | 11 +++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/core/src/constants/staking.constants.ts b/apps/core/src/constants/staking.constants.ts index e79939eee60..5c1a2ea2b7e 100644 --- a/apps/core/src/constants/staking.constants.ts +++ b/apps/core/src/constants/staking.constants.ts @@ -6,3 +6,6 @@ export const UNSTAKING_REQUEST_EVENT = '0x3::validator::UnstakingRequestEvent'; export const DELEGATED_STAKES_QUERY_STALE_TIME = 10_000; export const DELEGATED_STAKES_QUERY_REFETCH_INTERVAL = 30_000; + +export const NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE = 2; +export const NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_STARTS = 1; diff --git a/apps/core/src/hooks/stake/useStakeTxnInfo.ts b/apps/core/src/hooks/stake/useStakeTxnInfo.ts index 75d25047020..75fb1336c4c 100644 --- a/apps/core/src/hooks/stake/useStakeTxnInfo.ts +++ b/apps/core/src/hooks/stake/useStakeTxnInfo.ts @@ -1,9 +1,12 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { useGetTimeBeforeEpochNumber, useTimeAgo, TimeUnit } from '../../index'; - -export const NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE = 2; -export const NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_STARTS = 1; +import { + useGetTimeBeforeEpochNumber, + useTimeAgo, + TimeUnit, + NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE, + NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_STARTS, +} from '../../index'; export function useStakeTxnInfo(startEpoch?: string | number) { const startEarningRewardsEpoch = From 21f13fbed4ef6374194a26003271c7e2386d5ae2 Mon Sep 17 00:00:00 2001 From: Branko Bosnic <brankobosnic1@gmail.com> Date: Fri, 22 Nov 2024 13:45:07 +0100 Subject: [PATCH 81/87] fix: remove leftover comments --- apps/wallet-dashboard/app/(protected)/staking/page.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/wallet-dashboard/app/(protected)/staking/page.tsx b/apps/wallet-dashboard/app/(protected)/staking/page.tsx index 888ed15b8dd..2b0e26775a2 100644 --- a/apps/wallet-dashboard/app/(protected)/staking/page.tsx +++ b/apps/wallet-dashboard/app/(protected)/staking/page.tsx @@ -3,7 +3,6 @@ 'use client'; -// import { StakeDetailsDialog } from '@/components/Dialogs'; import { StartStaking } from '@/components/staking-overview/StartStaking'; import { Button, @@ -36,7 +35,6 @@ import { useMemo, useState } from 'react'; function StakingDashboardPage(): JSX.Element { const account = useCurrentAccount(); - // const [isDialogStakeOpen, setIsDialogStakeOpen] = useState(false); const [stakeDialogView, setStakeDialogView] = useState<StakeDialogView | undefined>(); const [selectedStake, setSelectedStake] = useState<ExtendedDelegatedStake | null>(null); const { data: system } = useIotaClientQuery('getLatestIotaSystemState'); From 17be2c3aeb903b678e82720b87e87bc0c1040fa1 Mon Sep 17 00:00:00 2001 From: Branko <brankobosnic1@gmail.com> Date: Tue, 26 Nov 2024 14:32:55 +0100 Subject: [PATCH 82/87] fix: comments and remove stakingStats component for DisplatStats from ui-kit --- .../src/components/staking/StakingStats.tsx | 26 ------------------- apps/core/src/components/staking/index.ts | 5 ---- apps/core/src/contexts/index.ts | 5 ---- .../app/(protected)/staking/page.tsx | 25 +++++++++++++++--- .../app/staking/validators/ValidatorsCard.tsx | 22 +++++++++++++--- 5 files changed, 39 insertions(+), 44 deletions(-) delete mode 100644 apps/core/src/components/staking/StakingStats.tsx diff --git a/apps/core/src/components/staking/StakingStats.tsx b/apps/core/src/components/staking/StakingStats.tsx deleted file mode 100644 index 49f41d4a6d8..00000000000 --- a/apps/core/src/components/staking/StakingStats.tsx +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// Modifications Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { useFormatCoin } from '../../hooks'; -import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; - -interface StakingStatsProps { - title: string; - balance: bigint | number | string; -} - -export function StakingStats({ balance, title }: StakingStatsProps) { - const [formatted, symbol] = useFormatCoin(balance, IOTA_TYPE_ARG); - - return ( - <div className="flex h-[96px] flex-1 flex-col justify-between rounded-xl bg-neutral-96 p-md"> - <div className="text-label-sm text-neutral-10">{title}</div> - - <div className="flex items-baseline gap-xxs"> - <div className="text-title-md text-neutral-10">{formatted}</div> - <div className="text-label-md text-neutral-10 opacity-40">{symbol}</div> - </div> - </div> - ); -} diff --git a/apps/core/src/components/staking/index.ts b/apps/core/src/components/staking/index.ts index 3ac4c79790e..0778b62f834 100644 --- a/apps/core/src/components/staking/index.ts +++ b/apps/core/src/components/staking/index.ts @@ -1,9 +1,4 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -<<<<<<<< HEAD:apps/core/src/components/staking/index.ts -export * from './StakingStats'; export * from './StakingCard'; -======== -export * from './ThemeContext'; ->>>>>>>> develop:apps/core/src/contexts/index.ts diff --git a/apps/core/src/contexts/index.ts b/apps/core/src/contexts/index.ts index 3ac4c79790e..c592171afa8 100644 --- a/apps/core/src/contexts/index.ts +++ b/apps/core/src/contexts/index.ts @@ -1,9 +1,4 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -<<<<<<<< HEAD:apps/core/src/components/staking/index.ts -export * from './StakingStats'; -export * from './StakingCard'; -======== export * from './ThemeContext'; ->>>>>>>> develop:apps/core/src/contexts/index.ts diff --git a/apps/wallet-dashboard/app/(protected)/staking/page.tsx b/apps/wallet-dashboard/app/(protected)/staking/page.tsx index 2d695913212..0512193b4bb 100644 --- a/apps/wallet-dashboard/app/(protected)/staking/page.tsx +++ b/apps/wallet-dashboard/app/(protected)/staking/page.tsx @@ -8,6 +8,7 @@ import { Button, ButtonSize, ButtonType, + DisplayStats, InfoBox, InfoBoxStyle, InfoBoxType, @@ -26,13 +27,14 @@ import { DELEGATED_STAKES_QUERY_REFETCH_INTERVAL, DELEGATED_STAKES_QUERY_STALE_TIME, StakingCard, - StakingStats, + useFormatCoin, } from '@iota/core'; import { useCurrentAccount, useIotaClientQuery } from '@iota/dapp-kit'; import { IotaSystemStateSummary } from '@iota/iota-sdk/client'; import { Info } from '@iota/ui-icons'; import { useMemo } from 'react'; import { useStakeDialog } from '@/components/Dialogs/Staking/hooks/useStakeDialog'; +import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; function StakingDashboardPage(): JSX.Element { const account = useCurrentAccount(); @@ -60,6 +62,11 @@ function StakingDashboardPage(): JSX.Element { const extendedStakes = delegatedStakeData ? formatDelegatedStake(delegatedStakeData) : []; const totalDelegatedStake = useTotalDelegatedStake(extendedStakes); const totalDelegatedRewards = useTotalDelegatedRewards(extendedStakes); + const [totalDelegatedStakeFormatted, symbol] = useFormatCoin( + totalDelegatedStake, + IOTA_TYPE_ARG, + ); + const [totalDelegatedRewardsFormatted] = useFormatCoin(totalDelegatedRewards, IOTA_TYPE_ARG); const delegations = useMemo(() => { return delegatedStakeData?.flatMap((delegation) => { @@ -100,8 +107,16 @@ function StakingDashboardPage(): JSX.Element { /> <div className="flex h-full w-full flex-col flex-nowrap gap-md p-md--rs"> <div className="flex gap-xs"> - <StakingStats title="Your stake" balance={totalDelegatedStake} /> - <StakingStats title="Earned" balance={totalDelegatedRewards} /> + <DisplayStats + label="Your stake" + value={totalDelegatedStakeFormatted} + supportingLabel={symbol} + /> + <DisplayStats + label="Earned" + value={totalDelegatedRewardsFormatted} + supportingLabel={symbol} + /> </div> <Title title="In progress" size={TitleSize.Small} /> <div className="flex max-h-[420px] w-full flex-1 flex-col items-start overflow-auto"> @@ -158,7 +173,9 @@ function StakingDashboardPage(): JSX.Element { )} </Panel> ) : ( - <StartStaking /> + <div className="flex h-[270px] p-lg"> + <StartStaking /> + </div> ); } diff --git a/apps/wallet/src/ui/app/staking/validators/ValidatorsCard.tsx b/apps/wallet/src/ui/app/staking/validators/ValidatorsCard.tsx index da0b63a059e..4d657179ff6 100644 --- a/apps/wallet/src/ui/app/staking/validators/ValidatorsCard.tsx +++ b/apps/wallet/src/ui/app/staking/validators/ValidatorsCard.tsx @@ -10,8 +10,8 @@ import { useTotalDelegatedStake, DELEGATED_STAKES_QUERY_REFETCH_INTERVAL, DELEGATED_STAKES_QUERY_STALE_TIME, - StakingStats, StakingCard, + useFormatCoin, } from '@iota/core'; import { useIotaClientQuery } from '@iota/dapp-kit'; import { useMemo } from 'react'; @@ -25,9 +25,11 @@ import { InfoBoxStyle, InfoBoxType, LoadingIndicator, + DisplayStats, } from '@iota/apps-ui-kit'; import { useNavigate } from 'react-router-dom'; import { Info, Warning } from '@iota/ui-icons'; +import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; export function ValidatorsCard() { const accountAddress = useActiveAddress(); @@ -49,7 +51,10 @@ export function ValidatorsCard() { // Total active stake for all Staked validators const totalDelegatedStake = useTotalDelegatedStake(delegatedStake); - + const [totalDelegatedStakeFormatted, symbol] = useFormatCoin( + totalDelegatedStake, + IOTA_TYPE_ARG, + ); const delegations = useMemo(() => { return delegatedStakeData?.flatMap((delegation) => { return delegation.stakes.map((d) => ({ @@ -72,6 +77,7 @@ export function ValidatorsCard() { // Get total rewards for all delegations const delegatedStakes = delegatedStakeData ? formatDelegatedStake(delegatedStakeData) : []; const totalDelegatedRewards = useTotalDelegatedRewards(delegatedStakes); + const [totalDelegatedRewardsFormatted] = useFormatCoin(totalDelegatedRewards, IOTA_TYPE_ARG); const handleNewStake = () => { ampli.clickedStakeIota({ @@ -106,8 +112,16 @@ export function ValidatorsCard() { return ( <div className="flex h-full w-full flex-col flex-nowrap"> <div className="flex gap-xs py-md"> - <StakingStats title="Your stake" balance={totalDelegatedStake} /> - <StakingStats title="Earned" balance={totalDelegatedRewards} /> + <DisplayStats + label="Your stake" + value={totalDelegatedStakeFormatted} + supportingLabel={symbol} + /> + <DisplayStats + label="Earned" + value={totalDelegatedRewardsFormatted} + supportingLabel={symbol} + /> </div> <Title title="In progress" size={TitleSize.Small} /> <div className="flex max-h-[420px] w-full flex-1 flex-col items-start overflow-auto"> From 072f01c5fd44d00d6c9b400747b1602a0900952a Mon Sep 17 00:00:00 2001 From: Branko <brankobosnic1@gmail.com> Date: Tue, 26 Nov 2024 16:29:10 +0100 Subject: [PATCH 83/87] fix: remove unused files --- .../src/components/staking/StakingCard.tsx | 2 +- .../core/src/hooks/stake/useValidatorInfo.tsx | 4 - apps/core/src/hooks/useNftDetails.ts | 85 +++++++++++++++++++ .../src/utils/stake/getTotalValidatorStake.ts | 9 -- apps/core/src/utils/stake/index.ts | 1 - 5 files changed, 86 insertions(+), 15 deletions(-) create mode 100644 apps/core/src/hooks/useNftDetails.ts delete mode 100644 apps/core/src/utils/stake/getTotalValidatorStake.ts diff --git a/apps/core/src/components/staking/StakingCard.tsx b/apps/core/src/components/staking/StakingCard.tsx index bf3c4dc3beb..f78396944d7 100644 --- a/apps/core/src/components/staking/StakingCard.tsx +++ b/apps/core/src/components/staking/StakingCard.tsx @@ -31,7 +31,7 @@ interface StakingCardProps { extendedStake: ExtendedDelegatedStake; currentEpoch: number; inactiveValidator?: boolean; - onClick?: () => void; + onClick: () => void; } // For delegationsRequestEpoch n through n + 2, show Start Earning diff --git a/apps/core/src/hooks/stake/useValidatorInfo.tsx b/apps/core/src/hooks/stake/useValidatorInfo.tsx index 064dd1d7d74..2ae4d23c933 100644 --- a/apps/core/src/hooks/stake/useValidatorInfo.tsx +++ b/apps/core/src/hooks/stake/useValidatorInfo.tsx @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 import { useIotaClientQuery } from '@iota/dapp-kit'; import { useGetValidatorsApy } from '../'; -import { getTotalValidatorStake } from '../../utils'; export function useValidatorInfo({ validatorAddress }: { validatorAddress: string }) { const { @@ -18,8 +17,6 @@ export function useValidatorInfo({ validatorAddress }: { validatorAddress: strin const currentEpoch = Number(system?.epoch || 0); - const totalValidatorStake = getTotalValidatorStake(validatorSummary); - const stakingPoolActivationEpoch = Number(validatorSummary?.stakingPoolActivationEpoch || 0); // flag as new validator if the validator was activated in the last epoch @@ -49,6 +46,5 @@ export function useValidatorInfo({ validatorAddress }: { validatorAddress: strin isAtRisk, apy, isApyApproxZero, - totalValidatorStake, }; } diff --git a/apps/core/src/hooks/useNftDetails.ts b/apps/core/src/hooks/useNftDetails.ts new file mode 100644 index 00000000000..f28d27192a9 --- /dev/null +++ b/apps/core/src/hooks/useNftDetails.ts @@ -0,0 +1,85 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 +import { useGetNFTMeta, useOwnedNFT, useNFTBasicData, useGetKioskContents } from './'; +import { formatAddress } from '@iota/iota-sdk/utils'; +import { isAssetTransferable, truncateString } from '../utils'; + +type NftFields = { + metadata?: { fields?: { attributes?: { fields?: { keys: string[]; values: string[] } } } }; +}; + +export function useNftDetails(nftId: string, accountAddress: string | null) { + const { data: objectData, isPending: isNftLoading } = useOwnedNFT(nftId || '', accountAddress); + const { data } = useGetKioskContents(accountAddress); + + const isContainedInKiosk = data?.lookup.get(nftId!); + const kioskItem = data?.list.find((k) => k.data?.objectId === nftId); + + const isTransferable = isAssetTransferable(objectData); + + const { nftFields } = useNFTBasicData(objectData); + + const { data: nftMeta, isPending: isPendingMeta } = useGetNFTMeta(nftId); + + const nftName = nftMeta?.name || formatAddress(nftId); + const nftImageUrl = nftMeta?.imageUrl || ''; + + // Extract either the attributes, or use the top-level NFT fields: + const { keys: metaKeys, values: metaValues } = + (nftFields as NftFields)?.metadata?.fields?.attributes?.fields || + Object.entries(nftFields ?? {}) + .filter(([key]) => key !== 'id') + .reduce( + (acc, [key, value]) => { + acc.keys.push(key); + acc.values.push(value as string); + return acc; + }, + { keys: [] as string[], values: [] as string[] }, + ); + + const ownerAddress = + (objectData?.owner && + typeof objectData?.owner === 'object' && + 'AddressOwner' in objectData.owner && + objectData.owner.AddressOwner) || + ''; + + function formatMetaValue(value: string | object) { + if (typeof value === 'object') { + return { + value: JSON.stringify(value), + valueLink: undefined, + }; + } else { + if (value.includes('http')) { + return { + value: value.startsWith('http') + ? truncateString(value, 20, 8) + : formatAddress(value), + valueLink: value, + }; + } + return { + value: value, + valueLink: undefined, + }; + } + } + + return { + objectData, + isNftLoading, + nftName, + nftImageUrl, + ownerAddress, + isTransferable, + metaKeys, + metaValues, + formatMetaValue, + isContainedInKiosk, + kioskItem, + nftMeta, + isPendingMeta, + }; +} diff --git a/apps/core/src/utils/stake/getTotalValidatorStake.ts b/apps/core/src/utils/stake/getTotalValidatorStake.ts deleted file mode 100644 index 2ecdaa8d887..00000000000 --- a/apps/core/src/utils/stake/getTotalValidatorStake.ts +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { IotaValidatorSummary } from '@iota/iota-sdk/client'; - -//TODO: verify this is the correct validator stake balance -export function getTotalValidatorStake(validatorSummary: IotaValidatorSummary | null) { - return validatorSummary?.stakingPoolIotaBalance || 0; -} diff --git a/apps/core/src/utils/stake/index.ts b/apps/core/src/utils/stake/index.ts index 6c4f408d42f..6ecca7353f6 100644 --- a/apps/core/src/utils/stake/index.ts +++ b/apps/core/src/utils/stake/index.ts @@ -7,4 +7,3 @@ export * from './createStakeTransaction'; export * from './createTimelockedUnstakeTransaction'; export * from './createTimelockedStakeTransaction'; export * from './createValidationSchema'; -export * from './getTotalValidatorStake'; From cc7f9655133e276d5f2ab9d73a2f2f1bece30986 Mon Sep 17 00:00:00 2001 From: Branko <brankobosnic1@gmail.com> Date: Wed, 27 Nov 2024 16:08:55 +0100 Subject: [PATCH 84/87] fix: remove the StatsDetail component --- .../ui/app/staking/validators/StatsDetail.tsx | 30 ------------------- 1 file changed, 30 deletions(-) delete mode 100644 apps/wallet/src/ui/app/staking/validators/StatsDetail.tsx diff --git a/apps/wallet/src/ui/app/staking/validators/StatsDetail.tsx b/apps/wallet/src/ui/app/staking/validators/StatsDetail.tsx deleted file mode 100644 index ba8a478c00e..00000000000 --- a/apps/wallet/src/ui/app/staking/validators/StatsDetail.tsx +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// Modifications Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { useFormatCoin } from '@iota/core'; -import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; - -interface DisplayStatsProps { - title: string; - balance: bigint | number | string; -} - -export function StatsDetail({ balance, title }: DisplayStatsProps) { - const [formatted, symbol] = useFormatCoin(balance, IOTA_TYPE_ARG); - - return ( - <div className="flex h-[96px] flex-1 flex-col justify-between rounded-xl bg-neutral-96 p-md dark:bg-neutral-12"> - <div className="text-label-sm text-neutral-10 dark:text-neutral-92">{title}</div> - - <div className="flex items-baseline gap-xxs"> - <div className="text-title-md text-neutral-10 dark:text-neutral-92"> - {formatted} - </div> - <div className="text-label-md text-neutral-10 opacity-40 dark:text-neutral-92"> - {symbol} - </div> - </div> - </div> - ); -} From c38a8b30d023fedcba0d7223ce3d65827b21227a Mon Sep 17 00:00:00 2001 From: Branko <brankobosnic1@gmail.com> Date: Wed, 27 Nov 2024 16:51:54 +0100 Subject: [PATCH 85/87] fix: conditions to display stake dialog --- .../app/(protected)/staking/page.tsx | 20 +++++++++---------- .../Dialogs/Staking/StakeDialog.tsx | 4 ++-- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/apps/wallet-dashboard/app/(protected)/staking/page.tsx b/apps/wallet-dashboard/app/(protected)/staking/page.tsx index 0512193b4bb..06ece822cf5 100644 --- a/apps/wallet-dashboard/app/(protected)/staking/page.tsx +++ b/apps/wallet-dashboard/app/(protected)/staking/page.tsx @@ -160,17 +160,15 @@ function StakingDashboardPage(): JSX.Element { </div> </div> </div> - {isDialogStakeOpen && stakeDialogView && ( - <StakeDialog - stakedDetails={selectedStake} - isOpen={isDialogStakeOpen} - handleClose={handleCloseStakeDialog} - view={stakeDialogView} - setView={setStakeDialogView} - selectedValidator={selectedValidator} - setSelectedValidator={setSelectedValidator} - /> - )} + <StakeDialog + stakedDetails={selectedStake} + isOpen={isDialogStakeOpen} + handleClose={handleCloseStakeDialog} + view={stakeDialogView} + setView={setStakeDialogView} + selectedValidator={selectedValidator} + setSelectedValidator={setSelectedValidator} + /> </Panel> ) : ( <div className="flex h-[270px] p-lg"> diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx index 6eab280f6e9..298ebb7e80d 100644 --- a/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx +++ b/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx @@ -43,10 +43,10 @@ const INITIAL_VALUES = { interface StakeDialogProps { isTimelockedStaking?: boolean; - onSuccess?: (digest: string) => void; isOpen: boolean; handleClose: () => void; - view: StakeDialogView; + onSuccess?: (digest: string) => void; + view?: StakeDialogView; setView?: (view: StakeDialogView) => void; stakedDetails?: ExtendedDelegatedStake | null; From 54d8dc8e1229f4e68d348b0855cf8ca371f95fd6 Mon Sep 17 00:00:00 2001 From: Branko <brankobosnic1@gmail.com> Date: Fri, 29 Nov 2024 12:18:16 +0100 Subject: [PATCH 86/87] fix: add useIsAsetTransferable hook --- apps/apps-backend/src/features/features.controller.ts | 4 ++-- apps/core/src/hooks/useNftDetails.ts | 5 +++-- apps/wallet-dashboard/lib/constants/vesting.constants.ts | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/apps-backend/src/features/features.controller.ts b/apps/apps-backend/src/features/features.controller.ts index 82bbafbe419..e915b437c07 100644 --- a/apps/apps-backend/src/features/features.controller.ts +++ b/apps/apps-backend/src/features/features.controller.ts @@ -65,10 +65,10 @@ export class FeaturesController { defaultValue: false, }, [Feature.StardustMigration]: { - defaultValue: false, + defaultValue: true, }, [Feature.SupplyIncreaseVesting]: { - defaultValue: false, + defaultValue: true, }, }, dateUpdated: new Date().toISOString(), diff --git a/apps/core/src/hooks/useNftDetails.ts b/apps/core/src/hooks/useNftDetails.ts index f28d27192a9..e951637930b 100644 --- a/apps/core/src/hooks/useNftDetails.ts +++ b/apps/core/src/hooks/useNftDetails.ts @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 import { useGetNFTMeta, useOwnedNFT, useNFTBasicData, useGetKioskContents } from './'; import { formatAddress } from '@iota/iota-sdk/utils'; -import { isAssetTransferable, truncateString } from '../utils'; +import { truncateString } from '../utils'; +import { useIsAssetTransferable } from './useIsAssetTransferable'; type NftFields = { metadata?: { fields?: { attributes?: { fields?: { keys: string[]; values: string[] } } } }; @@ -15,7 +16,7 @@ export function useNftDetails(nftId: string, accountAddress: string | null) { const isContainedInKiosk = data?.lookup.get(nftId!); const kioskItem = data?.list.find((k) => k.data?.objectId === nftId); - const isTransferable = isAssetTransferable(objectData); + const isTransferable = useIsAssetTransferable(objectData); const { nftFields } = useNFTBasicData(objectData); diff --git a/apps/wallet-dashboard/lib/constants/vesting.constants.ts b/apps/wallet-dashboard/lib/constants/vesting.constants.ts index 28bf1a4e026..0e4690bdfd2 100644 --- a/apps/wallet-dashboard/lib/constants/vesting.constants.ts +++ b/apps/wallet-dashboard/lib/constants/vesting.constants.ts @@ -20,7 +20,7 @@ export const MIN_STAKING_THRESHOLD = 1_000_000_000; // https://github.com/iotaledger/iota/blob/b0db487868fd5d61241a43eb8bc9886d7c1be1c9/crates/iota-types/src/timelock/stardust_upgrade_label.rs#L12 export const SUPPLY_INCREASE_VESTING_LABEL = - '000000000000000000000000000000000000000000000000000000000000107a::stardust_upgrade_label::STARDUST_UPGRADE_LABEL'; + '79215a9583c840892aee230dbe293aac54fce3f4873f3c9b358e662027f3efd7::stardust_upgrade_label::STARDUST_UPGRADE_LABEL'; export const MOCKED_SUPPLY_INCREASE_VESTING_TIMELOCKED_OBJECTS: TimelockedObject[] = [ { From c3b02437db209eac2706bc05e84d754372bc752b Mon Sep 17 00:00:00 2001 From: Branko <brankobosnic1@gmail.com> Date: Fri, 29 Nov 2024 12:24:40 +0100 Subject: [PATCH 87/87] fix: revert constant changes --- apps/apps-backend/src/features/features.controller.ts | 4 ++-- apps/wallet-dashboard/lib/constants/vesting.constants.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/apps-backend/src/features/features.controller.ts b/apps/apps-backend/src/features/features.controller.ts index e915b437c07..82bbafbe419 100644 --- a/apps/apps-backend/src/features/features.controller.ts +++ b/apps/apps-backend/src/features/features.controller.ts @@ -65,10 +65,10 @@ export class FeaturesController { defaultValue: false, }, [Feature.StardustMigration]: { - defaultValue: true, + defaultValue: false, }, [Feature.SupplyIncreaseVesting]: { - defaultValue: true, + defaultValue: false, }, }, dateUpdated: new Date().toISOString(), diff --git a/apps/wallet-dashboard/lib/constants/vesting.constants.ts b/apps/wallet-dashboard/lib/constants/vesting.constants.ts index 0e4690bdfd2..28bf1a4e026 100644 --- a/apps/wallet-dashboard/lib/constants/vesting.constants.ts +++ b/apps/wallet-dashboard/lib/constants/vesting.constants.ts @@ -20,7 +20,7 @@ export const MIN_STAKING_THRESHOLD = 1_000_000_000; // https://github.com/iotaledger/iota/blob/b0db487868fd5d61241a43eb8bc9886d7c1be1c9/crates/iota-types/src/timelock/stardust_upgrade_label.rs#L12 export const SUPPLY_INCREASE_VESTING_LABEL = - '79215a9583c840892aee230dbe293aac54fce3f4873f3c9b358e662027f3efd7::stardust_upgrade_label::STARDUST_UPGRADE_LABEL'; + '000000000000000000000000000000000000000000000000000000000000107a::stardust_upgrade_label::STARDUST_UPGRADE_LABEL'; export const MOCKED_SUPPLY_INCREASE_VESTING_TIMELOCKED_OBJECTS: TimelockedObject[] = [ {