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