From faca7128137d233d50f1d37c4a43238661fe4d20 Mon Sep 17 00:00:00 2001 From: Erika Date: Fri, 5 Sep 2025 09:48:34 +0200 Subject: [PATCH 1/7] chore: restructure tokens tab --- .../AssetVault/Components/ActionMenu.tsx | 2 +- .../routes/AssetVault/Components/Assets.tsx | 162 +-------------- .../src/routes/AssetVault/Tokens/Tokens.tsx | 183 +++++++++++++++++ .../components}/SendMoney.tsx | 0 .../Tokens/steps/ConfirmationStep.tsx | 155 ++++++++++++++ .../AssetVault/Tokens/steps/FormStep.tsx | 193 ++++++++++++++++++ .../AssetVault/Tokens/steps/ResultStep.tsx | 66 ++++++ .../tari_walletd/web_ui/vite.config.ts | 2 +- 8 files changed, 604 insertions(+), 159 deletions(-) create mode 100644 applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/Tokens.tsx rename applications/tari_walletd/web_ui/src/routes/AssetVault/{Components => Tokens/components}/SendMoney.tsx (100%) create mode 100644 applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ConfirmationStep.tsx create mode 100644 applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx create mode 100644 applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ResultStep.tsx diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/ActionMenu.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/ActionMenu.tsx index 32954afb27..3c503b5f85 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/ActionMenu.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/ActionMenu.tsx @@ -26,7 +26,7 @@ import { useTheme } from "@mui/material/styles"; import { useAccountsCreateFreeTestCoins } from "@api/hooks/useAccounts"; import ClaimBurn from "./ClaimBurn"; import useAccountStore from "@store/accountStore"; -import SendMoney from "./SendMoney"; +import SendMoney from "../Tokens/components/SendMoney"; import ClaimFees from "./ClaimFees"; import PublishTemplate from "./PublishTemplate"; import { substateIdToString } from "@tari-project/typescript-bindings"; diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/Assets.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/Assets.tsx index d5829ea065..471a4ef8fb 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/Assets.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/Assets.tsx @@ -22,40 +22,16 @@ import Box from "@mui/material/Box"; import Tab from "@mui/material/Tab"; -import Table from "@mui/material/Table"; -import TableBody from "@mui/material/TableBody"; -import TableCell from "@mui/material/TableCell"; -import TableContainer from "@mui/material/TableContainer"; -import TableHead from "@mui/material/TableHead"; -import TableRow from "@mui/material/TableRow"; import Tabs from "@mui/material/Tabs"; import Typography from "@mui/material/Typography"; import React, { useState } from "react"; -import FetchStatusCheck from "@components/FetchStatusCheck"; -import { DataTableCell } from "@components/StyledComponents"; -import { useAccountNFTsList, useAccountsGetBalances } from "@api/hooks/useAccounts"; +import { useAccountNFTsList } from "@api/hooks/useAccounts"; import { ApiError } from "@api/helpers/types"; import { useListNfts } from "@api/hooks/useNfts"; -import useAccountStore from "@store/accountStore"; -import { - bigintToDecimalString, - shortenSubstateId, - substateIdToString, - handleChangePage, - handleChangeRowsPerPage, -} from "@utils/helpers"; +import { substateIdToString, handleChangePage, handleChangeRowsPerPage } from "@utils/helpers"; import NFTList from "@routes/AssetVault/NFTs/NFTList"; -import { Button } from "@mui/material"; -import { SendMoneyDialog } from "./SendMoney"; -import { - ResourceAddress, - ResourceType, - VaultId, - BalanceEntry, - Account, - Amount, -} from "@tari-project/typescript-bindings"; -import CopyAddress from "@components/CopyAddress"; +import { Account } from "@tari-project/typescript-bindings"; +import Tokens from "@routes/AssetVault/Tokens/Tokens"; interface TabPanelProps { children?: React.ReactNode; @@ -63,66 +39,6 @@ interface TabPanelProps { value: number; } -interface BalanceRowProps { - token_symbol: string; - resource_address: ResourceAddress; - resource_type: ResourceType; - vault_address?: VaultId; - balance: Amount; - confidential_balance: Amount; - divisibility: number; - onSendClicked?: (resource_address: ResourceAddress, resource_type: ResourceType) => void; -} - -function BalanceRow(props: BalanceRowProps) { - const { - token_symbol, - resource_address, - resource_type, - balance, - confidential_balance, - vault_address, - divisibility, - onSendClicked, - } = props; - const showBalance = useAccountStore((state) => state.showBalance); - return ( - - {vault_address ? : "--"} - - - - {showBalance ? bigintToDecimalString(balance, divisibility) : "*************"} - - - - - - - - ); -} - -function ConfidentialBalance(props: { show: boolean; balance: Amount; resourceType: string; divisibility: number }) { - switch (props.resourceType) { - case "Confidential": - case "Stealth": - return <>{props.show ? bigintToDecimalString(props.balance, props.divisibility) : "**************"}; - default: - return <>--; - } -} - function TabPanel(props: TabPanelProps) { const { children, value, index, ...other } = props; @@ -152,10 +68,6 @@ function tabProps(index: number) { function Assets({ account }: { account: Account }) { const [assetTab, setAssetTab] = useState(0); - const [resourceToSend, setResourceToSend] = useState<{ - address: ResourceAddress; - resource_type: ResourceType; - } | null>(null); const [nftPage, setNftPage] = useState(0); const [nftRowsPerPage, setNftRowsPerPage] = useState(12); @@ -165,13 +77,6 @@ function Assets({ account }: { account: Account }) { setAssetTab(0); }, [account]); - const { - data: balancesData, - isError: balancesIsError, - error: balancesError, - isFetching: balancesIsFetching, - } = useAccountsGetBalances(substateIdToString(account.address)); - const { data: nftsListData, isError: nftsListIsError, @@ -195,19 +100,8 @@ function Assets({ account }: { account: Account }) { setAssetTab(newValue); }; - const handleSendResourceClicked = (address: ResourceAddress, resource_type: ResourceType) => { - setResourceToSend({ address, resource_type }); - }; - return ( - setResourceToSend(null)} - onSendComplete={() => setResourceToSend(null)} - resource_address={resourceToSend?.address} - resource_type={resourceToSend?.resource_type!} - /> @@ -215,53 +109,7 @@ function Assets({ account }: { account: Account }) { - - - - - - Vault - Resource - Revealed Balance - Confidential Balance - - - - - {balancesData?.balances.map( - ( - { - resource_address, - balance, - resource_type, - confidential_balance, - token_symbol, - vault_address, - divisibility, - }: BalanceEntry, - i: number, - ) => ( - - ), - )} - -
-
-
+
void; +} + +function ConfidentialBalance(props: { show: boolean; balance: Amount; resourceType: string; divisibility: number }) { + switch (props.resourceType) { + case "Confidential": + case "Stealth": + return <>{props.show ? bigintToDecimalString(props.balance, props.divisibility) : "**************"}; + default: + return <>--; + } +} + +function BalanceRow(props: BalanceRowProps) { + const { + token_symbol, + resource_address, + resource_type, + balance, + confidential_balance, + vault_address, + divisibility, + onSendClicked, + } = props; + const showBalance = useAccountStore((state) => state.showBalance); + return ( + + {vault_address ? : "--"} + + + + {showBalance ? bigintToDecimalString(balance, divisibility) : "*************"} + + + + + + + + ); +} + +function Tokens({ account }: { account: Account }) { + const [resourceToSend, setResourceToSend] = useState<{ + address: ResourceAddress; + resource_type: ResourceType; + } | null>(null); + + const { + data: balancesData, + isError: balancesIsError, + error: balancesError, + isFetching: balancesIsFetching, + } = useAccountsGetBalances(substateIdToString(account.address)); + + const handleSendResourceClicked = (address: ResourceAddress, resource_type: ResourceType) => { + setResourceToSend({ address, resource_type }); + }; + return ( + <> + setResourceToSend(null)} + onSendComplete={() => setResourceToSend(null)} + resource_address={resourceToSend?.address} + resource_type={resourceToSend?.resource_type!} + /> + + + + + + Vault + Resource + Revealed Balance + Confidential Balance + + + + + {balancesData?.balances.map( + ( + { + resource_address, + balance, + resource_type, + confidential_balance, + token_symbol, + vault_address, + divisibility, + }: BalanceEntry, + i: number, + ) => ( + + ), + )} + +
+
+
+ + ); +} + +export default Tokens; diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/SendMoney.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/routes/AssetVault/Components/SendMoney.tsx rename to applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ConfirmationStep.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ConfirmationStep.tsx new file mode 100644 index 0000000000..8ca220ba81 --- /dev/null +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ConfirmationStep.tsx @@ -0,0 +1,155 @@ +// Copyright 2025. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { Box, Button, Stack, Typography, Avatar, Divider } from "@mui/material"; +import type { Account, NonFungibleId, NonFungibleToken } from "@tari-project/typescript-bindings"; +import CopyAddress from "@components/CopyAddress"; +import { useNftTransferStore } from "@store/nftTransferStore"; +import { formatXTM, substateIdToString, displayNftId } from "@utils/helpers"; +import { convertCborValue } from "@utils/cbor"; + +interface ConfirmationStepProps { + accounts: Array<{ account: Account }> | undefined; + preSelectedNftId?: NonFungibleId; + availableNfts?: NonFungibleToken[]; + onBack: () => void; + onConfirm: () => void; +} + +function nftIdToString(nftId: NonFungibleId): string { + const key = Object.keys(nftId)[0]; + // @ts-ignore + const id = nftId[key].toString(); + const typeName = getNftIdTypeAsName(nftId); + return typeName + "_" + id; +} + +function getNftIdTypeAsName(nftId: NonFungibleId): string { + const key = Object.keys(nftId)[0]; + switch (key) { + case "U256": + return "uuid"; + case "String": + return "str"; + case "Uint32": + return "u32"; + case "Uint64": + return "u64"; + default: + return ""; + } +} + +export default function ConfirmationStep({ + accounts, + preSelectedNftId, + availableNfts, + onBack, + onConfirm, +}: ConfirmationStepProps) { + const { transferFormState, disabled } = useNftTransferStore(); + + // Find the NFT being transferred to show its image + const selectedNft = availableNfts?.find( + (nft) => preSelectedNftId && nftIdToString(nft.nft_id) === nftIdToString(preSelectedNftId), + ); + + const nftMutableData = selectedNft ? convertCborValue(selectedNft.mutable_data) : null; + const nftImageUrl = nftMutableData?.image_url; + + return ( + + + + + {preSelectedNftId && selectedNft ? ( + + { + e.target.src = + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODAiIGhlaWdodD0iODAiIHZpZXdCb3g9IjAgMCA4MCA4MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjgwIiBoZWlnaHQ9IjgwIiBmaWxsPSIjRjVGNUY1Ii8+CjxwYXRoIGQ9Ik0zMCAyNUg1MFY1NUgzMFYyNVoiIGZpbGw9IiNERERERUREIi8+CjxwYXRoIGQ9Ik0zNiAzMUg0NFY0M0gzNlYzMVoiIGZpbGw9IiNCQkJCQkIiLz4KPHR1eHQgeD0iNDAiIHk9IjUyIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iOCIgZmlsbD0iIzk5OTk5OSIgdGV4dC1hbmNob3I9Im1pZGRsZSI+TkZUPC90ZXh0Pgo8L3N2Zz4K"; + }} + > + NFT + + + ) : ( + {preSelectedNftId ? displayNftId(preSelectedNftId) : "Multiple NFTs"} + )} + + + {preSelectedNftId && ( + + + You are about to send: + + {displayNftId(preSelectedNftId)} + + )} + + + To Account: + + + + + + + + + Transaction Fee: + + {formatXTM(parseInt(transferFormState.maxFee))} + + + + + Fee paid by: + + + {accounts?.find((acc) => substateIdToString(acc.account.address) === transferFormState.payerAccount) + ?.account.name || transferFormState.payerAccount} + + + + + + + + + + + + ); +} diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx new file mode 100644 index 0000000000..69802b2798 --- /dev/null +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx @@ -0,0 +1,193 @@ +// Copyright 2025. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { FormEvent } from "react"; +import { Form } from "react-router-dom"; +import Button from "@mui/material/Button"; +import TextField from "@mui/material/TextField"; +import Select from "@mui/material/Select"; +import MenuItem from "@mui/material/MenuItem"; +import Checkbox from "@mui/material/Checkbox"; +import ListItemText from "@mui/material/ListItemText"; +import { Divider, InputLabel, Stack } from "@mui/material"; +import { SelectChangeEvent } from "@mui/material/Select/Select"; +import type { NonFungibleId, NonFungibleToken, Account } from "@tari-project/typescript-bindings"; +import { substateIdToString, formatXTM, validateAddress, displayNftId } from "@utils/helpers"; +import { useNftTransferStore } from "@store/nftTransferStore"; + +interface FormStepProps { + account: Account; + accounts: Array<{ account: Account }> | undefined; + availableNfts: NonFungibleToken[]; + preSelectedNftId?: NonFungibleId; + isEstimatingFee: boolean; + onSubmit: (e: FormEvent) => void; + onCancel: () => void; + onNftsChange: (event: SelectChangeEvent) => void; + onPayerAccountChange: (event: SelectChangeEvent) => void; +} + +function nftIdToString(nftId: NonFungibleId): string { + const key = Object.keys(nftId)[0]; + // @ts-ignore + const id = nftId[key].toString(); + const typeName = getNftIdTypeAsName(nftId); + return typeName + "_" + id; +} + +function getNftIdTypeAsName(nftId: NonFungibleId): string { + const key = Object.keys(nftId)[0]; + switch (key) { + case "U256": + return "uuid"; + case "String": + return "str"; + case "Uint32": + return "u32"; + case "Uint64": + return "u64"; + default: + return ""; + } +} + +export default function FormStep({ + accounts, + availableNfts, + preSelectedNftId, + isEstimatingFee, + onSubmit, + onCancel, + onNftsChange, + onPayerAccountChange, +}: FormStepProps) { + const { transferFormState, disabled, updateFormValue } = useNftTransferStore(); + + const setFormValue = (e: React.ChangeEvent) => { + const { name, value } = e.target; + updateFormValue(name, value, e.target.validity.valid); + }; + + return ( +
+ + {accounts && ( + <> + Account (to pay fees) + + + )} + + + + + + {!preSelectedNftId ? ( + <> + Select NFT(s) + + + ) : ( + + )} + + + + + + +
+ ); +} diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ResultStep.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ResultStep.tsx new file mode 100644 index 0000000000..263df80d85 --- /dev/null +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ResultStep.tsx @@ -0,0 +1,66 @@ +// Copyright 2025. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { Typography, Stack, Button, CircularProgress, Fade, Divider } from "@mui/material"; +import { useNftTransferStore } from "@store/nftTransferStore"; +import CancelRoundedIcon from "@mui/icons-material/CancelRounded"; +import CheckCircleRoundedIcon from "@mui/icons-material/CheckCircleRounded"; + +interface ResultStepProps { + onClose: () => void; +} + +export default function ResultStep({ onClose }: ResultStepProps) { + const { disabled, transferResult } = useNftTransferStore(); + + return ( + + {disabled ? ( + <> + + Sending NFT... + Please wait while your transaction is processed. + + ) : transferResult ? ( + <> + + {transferResult.success ? ( + + + + ) : ( + + + + )} + {transferResult.success ? "Transfer Successful!" : "Transfer Failed"} + + {transferResult.message} + + + + ) : null} + + ); +} diff --git a/applications/tari_walletd/web_ui/vite.config.ts b/applications/tari_walletd/web_ui/vite.config.ts index efbb27c2c5..0947ee6a5d 100644 --- a/applications/tari_walletd/web_ui/vite.config.ts +++ b/applications/tari_walletd/web_ui/vite.config.ts @@ -30,7 +30,7 @@ export default defineConfig({ resolve: { alias: { "@": path.resolve(__dirname, "./src"), - "@components": path.resolve(__dirname, "./src/Components"), + "@components": path.resolve(__dirname, "./src/components"), "@routes": path.resolve(__dirname, "./src/routes"), "@utils": path.resolve(__dirname, "./src/utils"), "@assets": path.resolve(__dirname, "./src/assets"), From d44eb73df1ae6b8c106e793da04441f9c5496452 Mon Sep 17 00:00:00 2001 From: Erika Date: Fri, 5 Sep 2025 14:01:13 +0200 Subject: [PATCH 2/7] feat: add stepped send flow --- .../NFTs/steps/ConfirmationStep.tsx | 4 +- .../routes/AssetVault/NFTs/steps/FormStep.tsx | 4 +- .../Tokens/components/SendMoney.tsx | 384 +++++++++--------- .../Tokens/steps/ConfirmationStep.tsx | 178 ++++---- .../AssetVault/Tokens/steps/FormStep.tsx | 271 ++++++------ .../AssetVault/Tokens/steps/ResultStep.tsx | 14 +- .../web_ui/src/utils/constants.ts | 2 +- .../tari_walletd/web_ui/src/utils/helpers.tsx | 17 +- 8 files changed, 454 insertions(+), 420 deletions(-) diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/steps/ConfirmationStep.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/steps/ConfirmationStep.tsx index 8ca220ba81..110a547436 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/steps/ConfirmationStep.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/steps/ConfirmationStep.tsx @@ -24,7 +24,7 @@ import { Box, Button, Stack, Typography, Avatar, Divider } from "@mui/material"; import type { Account, NonFungibleId, NonFungibleToken } from "@tari-project/typescript-bindings"; import CopyAddress from "@components/CopyAddress"; import { useNftTransferStore } from "@store/nftTransferStore"; -import { formatXTM, substateIdToString, displayNftId } from "@utils/helpers"; +import { formatCurrency, substateIdToString, displayNftId } from "@utils/helpers"; import { convertCborValue } from "@utils/cbor"; interface ConfirmationStepProps { @@ -126,7 +126,7 @@ export default function ConfirmationStep({ Transaction Fee: - {formatXTM(parseInt(transferFormState.maxFee))} + {formatCurrency(parseInt(transferFormState.maxFee))}
diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/steps/FormStep.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/steps/FormStep.tsx index 69802b2798..a58b7f8093 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/steps/FormStep.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/steps/FormStep.tsx @@ -31,7 +31,7 @@ import ListItemText from "@mui/material/ListItemText"; import { Divider, InputLabel, Stack } from "@mui/material"; import { SelectChangeEvent } from "@mui/material/Select/Select"; import type { NonFungibleId, NonFungibleToken, Account } from "@tari-project/typescript-bindings"; -import { substateIdToString, formatXTM, validateAddress, displayNftId } from "@utils/helpers"; +import { substateIdToString, formatCurrency, validateAddress, displayNftId } from "@utils/helpers"; import { useNftTransferStore } from "@store/nftTransferStore"; interface FormStepProps { @@ -133,7 +133,7 @@ export default function FormStep({ isEstimatingFee ? "Estimating..." : transferFormState.maxFee - ? formatXTM(parseInt(transferFormState.maxFee)) + ? formatCurrency(parseInt(transferFormState.maxFee)) : "Will be calculated automatically" } placeholder="Fee will be estimated automatically" diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx index 3164e52822..91e7bcec6d 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx @@ -20,33 +20,30 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import { FormEvent, useEffect, useState } from "react"; -import { Form } from "react-router-dom"; +import { FormEvent, useState, useEffect } from "react"; import Button from "@mui/material/Button"; -import CheckBox from "@mui/material/Checkbox"; -import TextField from "@mui/material/TextField"; import Dialog from "@mui/material/Dialog"; import DialogContent from "@mui/material/DialogContent"; import DialogTitle from "@mui/material/DialogTitle"; -import FormControlLabel from "@mui/material/FormControlLabel"; -import Box from "@mui/material/Box"; +import { Stepper, Step, StepLabel } from "@mui/material"; import { useAccountsGetBalances, useAccountsTransfer } from "@api/hooks/useAccounts"; -import { useTheme } from "@mui/material/styles"; import useAccountStore from "@store/accountStore"; -import Select from "@mui/material/Select"; import { SelectChangeEvent } from "@mui/material/Select/Select"; -import MenuItem from "@mui/material/MenuItem"; import { BalanceEntry, ConfidentialTransferInputSelection, ResourceAddress, ResourceType, substateIdToString, - TransactionResult, XTR, } from "@tari-project/typescript-bindings"; -import InputLabel from "@mui/material/InputLabel"; import { transactionsWaitResult } from "@utils/json_rpc"; +import { CURRENCY } from "@utils/constants"; +import FormStep, { SendMoneyFormState } from "../steps/FormStep"; +import ConfirmationStep from "../steps/ConfirmationStep"; +import ResultStep, { TransferResult } from "../steps/ResultStep"; + +const steps = ["Enter Details", "Confirm Transfer", "Result"]; export default function SendMoney() { const [open, setOpen] = useState(false); @@ -76,7 +73,7 @@ export interface SendMoneyDialogProps { } export function SendMoneyDialog(props: SendMoneyDialogProps) { - const INITIAL_VALUES = { + const INITIAL_VALUES: SendMoneyFormState = { publicKey: "", outputToConfidential: false, inputSelection: "PreferRevealed", @@ -84,34 +81,36 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) { fee: "", badge: null, }; - const isConfidential = props.resource_type === "Confidential"; - const isStealth = props.resource_type === "Stealth"; + + const [activeStep, setActiveStep] = useState(0); const [useBadge, setUseBadge] = useState(false); const [disabled, setDisabled] = useState(false); + const [isEstimatingFee, setIsEstimatingFee] = useState(false); const [transferFormState, setTransferFormState] = useState(INITIAL_VALUES); - const [validity, setValidity] = useState({ - publicKey: false, - amount: false, - }); - const [allValid, setAllValid] = useState(false); + const [transferResult, setTransferResult] = useState(); const { mutateAsync: sendIt } = useAccountsTransfer(); const { account, setPopup } = useAccountStore(); + if (!account) { return null; } - const theme = useTheme(); - const { data } = useAccountsGetBalances(substateIdToString(account.address)); const badges = data?.balances ?.filter((b: BalanceEntry) => b.resource_type === "NonFungible" && BigInt(b.balance) > 0n) .map((b: BalanceEntry) => b.resource_address) as string[]; + // Find the available balance for the resource we're trying to send + const balanceEntry = data?.balances?.find( + (b: BalanceEntry) => b.resource_address === (props.resource_address || XTR), + ); + // Balance is in micro XTR units, convert to XTR for display + const availableBalance = balanceEntry?.balance ? Number(balanceEntry.balance) / CURRENCY.DIVISOR : undefined; + const transfer = { account: substateIdToString(account.address), - amount: parseInt(transferFormState.amount), - // HACK: default to XTR2 because the resource is only set when open==true, and we cannot conditionally call hooks i.e. when props.resource_address is set + amount: Math.floor((parseFloat(transferFormState.amount) || 0) * CURRENCY.DIVISOR), resource_address: props.resource_address || XTR, destination_public_key: transferFormState.publicKey, resourceType: props.resource_type, @@ -121,16 +120,28 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) { }; function setFormValue(e: React.ChangeEvent) { + const { name, value } = e.target; + + // For amount field, parse the input to allow decimal values + let processedValue = value; + if (name === "amount" && value) { + // Remove currency symbol and extra spaces, but keep numbers and decimal point + processedValue = value.replace(/[^\d.]/g, ""); + // Ensure only one decimal point + const parts = processedValue.split("."); + if (parts.length > 2) { + processedValue = parts[0] + "." + parts.slice(1).join(""); + } + } + + // Clear fee when amount or publicKey changes to trigger re-estimation + const shouldClearFee = (name === "amount" || name === "publicKey") && transferFormState.fee; + setTransferFormState({ ...transferFormState, - [e.target.name]: e.target.value, + [name]: processedValue, + ...(shouldClearFee ? { fee: "" } : {}), }); - if (validity[e.target.name as keyof object] !== undefined) { - setValidity({ - ...validity, - [e.target.name]: e.target.validity.valid, - }); - } } function setSelectFormValue(e: SelectChangeEvent) { @@ -147,188 +158,187 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) { }); } - const onTransfer = async (e: FormEvent) => { + const handleUseBadgeChange = (e: React.ChangeEvent) => { + setUseBadge(e.target.checked); + if (!e.target.checked) { + setTransferFormState({ + ...transferFormState, + badge: null, + }); + } + }; + + const estimateFee = async () => { + if (!account || isEstimatingFee || !transferFormState.publicKey.trim() || !transferFormState.amount) { + return; + } + + setIsEstimatingFee(true); + + try { + // Create transfer object with current form state + const currentTransfer = { + account: substateIdToString(account.address), + amount: Math.floor((parseFloat(transferFormState.amount) || 0) * CURRENCY.DIVISOR), + resource_address: props.resource_address || XTR, + destination_public_key: transferFormState.publicKey, + resourceType: props.resource_type, + output_to_revealed: !transferFormState.outputToConfidential, + input_selection: transferFormState.inputSelection as ConfidentialTransferInputSelection, + badge: transferFormState.badge, + }; + + const result = await sendIt?.({ ...currentTransfer, dry_run: true, max_fee: 3000 }); + const resp = await transactionsWaitResult({ transaction_id: result.transaction_id, timeout_secs: null }); + const transactionResult = resp.result?.result; + + if (!transactionResult || !("Accept" in transactionResult)) { + throw new Error("Fee estimation failed"); + } + + const fee = resp.final_fee + 100; + setTransferFormState((prevState) => ({ ...prevState, fee: fee.toString() })); + } catch (error) { + console.error("Fee estimation error:", error); + // Don't block the user if fee estimation fails + } finally { + setIsEstimatingFee(false); + } + }; + + const handleFormSubmit = async (e: FormEvent) => { e.preventDefault(); if (!account) { return; } + // Check if required fields are filled + if (!transferFormState.publicKey.trim() || !transferFormState.amount) { + return; + } + + // If no fee is calculated yet, estimate it before proceeding + if (!transferFormState.fee) { + try { + await estimateFee(); + } catch (error) { + console.error("Fee estimation failed:", error); + return; + } + } + + setActiveStep(1); + }; + + const handleConfirm = async () => { + if (!account) { + return; + } + setDisabled(true); - if (!isNaN(parseInt(transferFormState.fee))) { - sendIt?.({ + setActiveStep(2); + + try { + await sendIt?.({ ...transfer, dry_run: false, max_fee: parseInt(transferFormState.fee), - }) - .then(() => { - setTransferFormState(INITIAL_VALUES); - props.onSendComplete?.(); - setPopup({ title: "Send successful", error: false }); - }) - .catch((e) => { - setPopup({ title: "Send failed", error: true, message: e.message }); - }) - .finally(() => { - setDisabled(false); - }); - } else { - sendIt?.({ ...transfer, dry_run: true, max_fee: 3000 }) - .then((result) => transactionsWaitResult({ transaction_id: result.transaction_id, timeout_secs: null })) - .then((resp) => { - const result = resp.result?.result; - if (!result) { - throw new Error("No result in response: " + JSON.stringify(resp)); - } - if (!("Accept" in result)) { - setPopup({ - title: "Fee estimate failed", - error: true, - // TODO: fix this - message: JSON.stringify( - unionGet(result, "Reject" as keyof TransactionResult) || - unionGet(result, "AcceptFeeRejectRest" as keyof TransactionResult)?.[1], - ), - }); - return; - } - // Simple fix for the estimated fee differing between the dry-run and non-dry-run transactions. - // Since fees are charged for the transaction byte size and for confidential transfers, the rangeproof - // may differ in length and, therefore in fees. The fees may differ typically by 2/3, this more than - // accounts for that. See https://github.com/tari-project/tari-ootle/issues/1312 - // TODO: remove once this is no longer an issue - const fee = resp.final_fee + 100; - setTransferFormState({ ...transferFormState, fee: fee.toString() }); - }) - .catch((e) => { - setPopup({ title: "Fee estimate failed", error: true, message: e.message }); - }) - .finally(() => { - setDisabled(false); - }); + }); + + setTransferResult({ + success: true, + message: "Transfer completed successfully", + }); + props.onSendComplete?.(); + } catch (error) { + setTransferResult({ + success: false, + message: error instanceof Error ? error.message : "Transfer failed", + }); + } finally { + setDisabled(false); } }; const handleClose = () => { + setActiveStep(0); + setTransferFormState(INITIAL_VALUES); + setTransferResult(undefined); + setUseBadge(false); + setDisabled(false); props.handleClose?.(); }; - const handleUseBadgeChange = (e: React.ChangeEvent) => { - setUseBadge(e.target.checked); - if (!e.target.checked) { - setTransferFormState({ - ...transferFormState, - badge: null, - }); - } + const handleBack = () => { + setActiveStep(activeStep - 1); }; + // Auto-estimate fee when user enters valid public key and amount useEffect(() => { - setAllValid(Object.values(validity).every((v) => v)); - }, [validity]); + const { publicKey, amount } = transferFormState; + if (publicKey.trim() && publicKey.match(/^[0-9a-fA-F]+$/) && amount.trim()) { + // Small delay to let state update, then estimate fee + const timeoutId = setTimeout(() => { + estimateFee().catch(() => { + // Fee estimation failed, but don't block the user + }); + }, 500); - return ( - - Send {props.resource_address} - -
- {badges && ( - <> - } - label="Use Badge" - /> - Badge - - - )} - - {(isConfidential || isStealth) && ( - <> - - } - label="Send Confidential Outputs" - /> - Input Selection - - - )} - clearTimeout(timeoutId); + } + }, [transferFormState.publicKey, transferFormState.amount]); + + const renderStepContent = () => { + switch (activeStep) { + case 0: + return ( + - - - - - - + ); + case 2: + return ; + default: + return null; + } + }; + + return ( + + Send {props.resource_address} + + + {steps.map((label) => ( + + {label} + + ))} + + {renderStepContent()} ); } - -function unionGet(object: T, key: keyof T): T[keyof T] | null { - return key in object ? object[key] : null; -} diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ConfirmationStep.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ConfirmationStep.tsx index 8ca220ba81..00ed842bcc 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ConfirmationStep.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ConfirmationStep.tsx @@ -20,127 +20,97 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import { Box, Button, Stack, Typography, Avatar, Divider } from "@mui/material"; -import type { Account, NonFungibleId, NonFungibleToken } from "@tari-project/typescript-bindings"; +import { Box, Button, Stack, Typography, Divider } from "@mui/material"; +import type { ResourceAddress, ResourceType } from "@tari-project/typescript-bindings"; import CopyAddress from "@components/CopyAddress"; -import { useNftTransferStore } from "@store/nftTransferStore"; -import { formatXTM, substateIdToString, displayNftId } from "@utils/helpers"; -import { convertCborValue } from "@utils/cbor"; +import { formatDisplayCurrency, formatCurrency } from "@utils/helpers"; +import { CURRENCY } from "@utils/constants"; +import { SendMoneyFormState } from "./FormStep"; interface ConfirmationStepProps { - accounts: Array<{ account: Account }> | undefined; - preSelectedNftId?: NonFungibleId; - availableNfts?: NonFungibleToken[]; + resource_address?: ResourceAddress; + resource_type: ResourceType; + transferFormState: SendMoneyFormState; + disabled: boolean; onBack: () => void; onConfirm: () => void; } -function nftIdToString(nftId: NonFungibleId): string { - const key = Object.keys(nftId)[0]; - // @ts-ignore - const id = nftId[key].toString(); - const typeName = getNftIdTypeAsName(nftId); - return typeName + "_" + id; -} - -function getNftIdTypeAsName(nftId: NonFungibleId): string { - const key = Object.keys(nftId)[0]; - switch (key) { - case "U256": - return "uuid"; - case "String": - return "str"; - case "Uint32": - return "u32"; - case "Uint64": - return "u64"; - default: - return ""; - } -} - export default function ConfirmationStep({ - accounts, - preSelectedNftId, - availableNfts, + resource_address, + resource_type, + transferFormState, + disabled, onBack, onConfirm, }: ConfirmationStepProps) { - const { transferFormState, disabled } = useNftTransferStore(); + return ( + + + + + Confirm Transfer + + - // Find the NFT being transferred to show its image - const selectedNft = availableNfts?.find( - (nft) => preSelectedNftId && nftIdToString(nft.nft_id) === nftIdToString(preSelectedNftId), - ); + + + Resource: + + {resource_address} + - const nftMutableData = selectedNft ? convertCborValue(selectedNft.mutable_data) : null; - const nftImageUrl = nftMutableData?.image_url; + + + To Public Key: + + + + + - return ( - - - - - {preSelectedNftId && selectedNft ? ( - - { - e.target.src = - "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODAiIGhlaWdodD0iODAiIHZpZXdCb3g9IjAgMCA4MCA4MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjgwIiBoZWlnaHQ9IjgwIiBmaWxsPSIjRjVGNUY1Ii8+CjxwYXRoIGQ9Ik0zMCAyNUg1MFY1NUgzMFYyNVoiIGZpbGw9IiNERERERUREIi8+CjxwYXRoIGQ9Ik0zNiAzMUg0NFY0M0gzNlYzMVoiIGZpbGw9IiNCQkJCQkIiLz4KPHR1eHQgeD0iNDAiIHk9IjUyIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iOCIgZmlsbD0iIzk5OTk5OSIgdGV4dC1hbmNob3I9Im1pZGRsZSI+TkZUPC90ZXh0Pgo8L3N2Zz4K"; - }} - > - NFT - - - ) : ( - {preSelectedNftId ? displayNftId(preSelectedNftId) : "Multiple NFTs"} - )} - - - {preSelectedNftId && ( - - - You are about to send: - - {displayNftId(preSelectedNftId)} - - )} - - - To Account: - - - - - + + + Amount: + + + {(() => { + const amount = parseFloat(transferFormState.amount) || 0; + const hasDecimals = transferFormState.amount.includes('.') && transferFormState.amount.split('.')[1].length > 0; + return `${amount.toLocaleString('en-US', { + minimumFractionDigits: hasDecimals ? 0 : 2, + maximumFractionDigits: CURRENCY.DECIMALS + })} ${CURRENCY.SYMBOL}`; + })()} + + - - - Transaction Fee: - - {formatXTM(parseInt(transferFormState.maxFee))} - + + + Transaction Fee: + + {formatCurrency(parseInt(transferFormState.fee) || 0)} + - - - Fee paid by: - - - {accounts?.find((acc) => substateIdToString(acc.account.address) === transferFormState.payerAccount) - ?.account.name || transferFormState.payerAccount} - - - - + {resource_type === "Confidential" && ( + + + Send Confidential Outputs: + + {transferFormState.outputToConfidential ? "Yes" : "No"} + + )} + + {transferFormState.badge && ( + + + Using Badge: + + {transferFormState.badge} + + )} + - diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ResultStep.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ResultStep.tsx index 263df80d85..58bc88561c 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ResultStep.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ResultStep.tsx @@ -21,23 +21,27 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import { Typography, Stack, Button, CircularProgress, Fade, Divider } from "@mui/material"; -import { useNftTransferStore } from "@store/nftTransferStore"; import CancelRoundedIcon from "@mui/icons-material/CancelRounded"; import CheckCircleRoundedIcon from "@mui/icons-material/CheckCircleRounded"; +export interface TransferResult { + success: boolean; + message: string; +} + interface ResultStepProps { + disabled: boolean; + transferResult?: TransferResult; onClose: () => void; } -export default function ResultStep({ onClose }: ResultStepProps) { - const { disabled, transferResult } = useNftTransferStore(); - +export default function ResultStep({ disabled, transferResult, onClose }: ResultStepProps) { return ( {disabled ? ( <> - Sending NFT... + Sending Money... Please wait while your transaction is processed. ) : transferResult ? ( diff --git a/applications/tari_walletd/web_ui/src/utils/constants.ts b/applications/tari_walletd/web_ui/src/utils/constants.ts index c821edcf72..61e425f8a2 100644 --- a/applications/tari_walletd/web_ui/src/utils/constants.ts +++ b/applications/tari_walletd/web_ui/src/utils/constants.ts @@ -21,7 +21,7 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. export const CURRENCY = { - SYMBOL: "tXTM", + SYMBOL: "tXTR", DECIMALS: 6, DIVISOR: 1_000_000, } as const; diff --git a/applications/tari_walletd/web_ui/src/utils/helpers.tsx b/applications/tari_walletd/web_ui/src/utils/helpers.tsx index 232832b5a9..cef04c8b30 100644 --- a/applications/tari_walletd/web_ui/src/utils/helpers.tsx +++ b/applications/tari_walletd/web_ui/src/utils/helpers.tsx @@ -250,7 +250,7 @@ export function bigintToDecimalString(int: bigint | Amount, decimalPlaces: numbe return `${wholeValues}.${padding}${fractionalValues}`; } -export const formatXTM = (amount: number | bigint): string => { +export const formatCurrency = (amount: number | bigint): string => { if (typeof amount === "bigint") { // Handle bigint: divide by divisor to get integer and remainder for fractional part const divisor = BigInt(CURRENCY.DIVISOR); @@ -260,19 +260,28 @@ export const formatXTM = (amount: number | bigint): string => { // Convert remainder to fractional string padded to CURRENCY.DECIMALS const fractionalPart = remainder.toString().padStart(CURRENCY.DECIMALS, "0"); - return `${integerPart.toString()}.${fractionalPart} ${CURRENCY.SYMBOL}`; + return `${Number(integerPart).toLocaleString("en-US")}.${fractionalPart} ${CURRENCY.SYMBOL}`; } else if (typeof amount === "number") { // Handle number: guard against NaN and use existing toFixed logic if (isNaN(amount)) { return `0 ${CURRENCY.SYMBOL}`; } - return `${(amount / CURRENCY.DIVISOR).toFixed(CURRENCY.DECIMALS)} ${CURRENCY.SYMBOL}`; + const convertedAmount = amount / CURRENCY.DIVISOR; + return `${convertedAmount.toLocaleString("en-US", { minimumFractionDigits: 0, maximumFractionDigits: CURRENCY.DECIMALS })} ${CURRENCY.SYMBOL}`; } else { // Handle invalid types return `0 ${CURRENCY.SYMBOL}`; } }; +// Helper function for formatting amounts that are already in display units (XTR) +export const formatDisplayCurrency = (amount: number): string => { + if (isNaN(amount)) { + return `0 ${CURRENCY.SYMBOL}`; + } + return `${amount.toLocaleString("en-US", { minimumFractionDigits: 0, maximumFractionDigits: CURRENCY.DECIMALS })} ${CURRENCY.SYMBOL}`; +}; + export function validateHash(hash: string): boolean { const regex = /^[a-fA-F0-9]{64}$/; return regex.test(hash); @@ -314,7 +323,7 @@ const normalizeTimestamp = (rawTimestamp: string | null | undefined): Date | nul export const formatTimestamp = (rawTimestamp: string | null | undefined): string => { const date = normalizeTimestamp(rawTimestamp); - + if (!date) return ""; return date.toLocaleString(undefined, { From d94d5e95c5a3d6914af7ab3daae86c70dce9bed0 Mon Sep 17 00:00:00 2001 From: Erika Date: Fri, 5 Sep 2025 14:14:36 +0200 Subject: [PATCH 3/7] fix: build errors --- .../tari_walletd/web_ui/src/routes/Accounts/Accounts.tsx | 4 ++-- .../routes/AssetVault/Tokens/steps/ConfirmationStep.tsx | 2 +- .../web_ui/src/routes/FlowEditor/FlowEditor.tsx | 8 ++++---- applications/tari_walletd/web_ui/src/routes/Keys/Keys.tsx | 4 ++-- .../web_ui/src/routes/Templates/Templates.tsx | 4 ++-- .../web_ui/src/routes/Wallet/Components/AccessTokens.tsx | 6 +++--- .../src/routes/WebauthnRegistration/Components/Login.tsx | 2 +- .../WebauthnRegistration/Components/Registration.tsx | 8 ++++++-- .../web_ui/src/routes/WebauthnRegistration/Webauthn.tsx | 2 +- applications/tari_walletd/web_ui/tsconfig.json | 2 +- 10 files changed, 23 insertions(+), 19 deletions(-) diff --git a/applications/tari_walletd/web_ui/src/routes/Accounts/Accounts.tsx b/applications/tari_walletd/web_ui/src/routes/Accounts/Accounts.tsx index 7b842ac992..582942ba7b 100644 --- a/applications/tari_walletd/web_ui/src/routes/Accounts/Accounts.tsx +++ b/applications/tari_walletd/web_ui/src/routes/Accounts/Accounts.tsx @@ -20,9 +20,9 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import PageHeading from "../../Components/PageHeading"; +import PageHeading from "@components/PageHeading"; import Grid from "@mui/material/Grid"; -import { StyledPaper } from "../../Components/StyledComponents"; +import { StyledPaper } from "@components/StyledComponents"; import Accounts from "../Wallet/Components/Accounts"; function AccountsLayout() { diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ConfirmationStep.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ConfirmationStep.tsx index 00ed842bcc..8e8bee4c0e 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ConfirmationStep.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ConfirmationStep.tsx @@ -23,7 +23,7 @@ import { Box, Button, Stack, Typography, Divider } from "@mui/material"; import type { ResourceAddress, ResourceType } from "@tari-project/typescript-bindings"; import CopyAddress from "@components/CopyAddress"; -import { formatDisplayCurrency, formatCurrency } from "@utils/helpers"; +import { formatCurrency } from "@utils/helpers"; import { CURRENCY } from "@utils/constants"; import { SendMoneyFormState } from "./FormStep"; diff --git a/applications/tari_walletd/web_ui/src/routes/FlowEditor/FlowEditor.tsx b/applications/tari_walletd/web_ui/src/routes/FlowEditor/FlowEditor.tsx index f3a7930a26..1f16c3f763 100644 --- a/applications/tari_walletd/web_ui/src/routes/FlowEditor/FlowEditor.tsx +++ b/applications/tari_walletd/web_ui/src/routes/FlowEditor/FlowEditor.tsx @@ -21,9 +21,9 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import "@tari-project/tari-extension-query-builder/dist/tari-extension-query-builder.css"; -import PageHeading from "../../Components/PageHeading"; +import PageHeading from "@components/PageHeading"; import Grid from "@mui/material/Grid"; -import { StyledPaper } from "../../Components/StyledComponents"; +import { StyledPaper } from "@components/StyledComponents"; import { QueryBuilder, TemplateReader, useStore } from "@tari-project/tari-extension-query-builder"; import useThemeStore from "../../store/themeStore"; import { useCallback, useEffect, useRef } from "react"; @@ -51,7 +51,7 @@ import { import ChevronRightIcon from "@mui/icons-material/ChevronRight"; import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; import { useTheme } from "@mui/material/styles"; -import Loading from "../../Components/Loading"; +import Loading from "@components/Loading"; import { useTemplateGet } from "../../api/hooks/useTemplate"; import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline"; import FunctionsIcon from "@mui/icons-material/Functions"; @@ -71,7 +71,7 @@ import { } from "@tari-project/typescript-bindings"; import { settingsGet, submitTransactionDryRun, transactionsSubmit, transactionsWaitResult } from "../../utils/json_rpc"; import { useAccountsList } from "../../api/hooks/useAccounts"; -import CopyAddress from "../../Components/CopyAddress"; +import CopyAddress from "@components/CopyAddress"; const KNOWN_TEMPLATES = [ { diff --git a/applications/tari_walletd/web_ui/src/routes/Keys/Keys.tsx b/applications/tari_walletd/web_ui/src/routes/Keys/Keys.tsx index 85aafa0046..be7c03ea51 100644 --- a/applications/tari_walletd/web_ui/src/routes/Keys/Keys.tsx +++ b/applications/tari_walletd/web_ui/src/routes/Keys/Keys.tsx @@ -20,9 +20,9 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import PageHeading from "../../Components/PageHeading"; +import PageHeading from "@components/PageHeading"; import Grid from "@mui/material/Grid"; -import { StyledPaper } from "../../Components/StyledComponents"; +import { StyledPaper } from "@components/StyledComponents"; import Keys from "../Wallet/Components/Keys"; function KeysLayout() { diff --git a/applications/tari_walletd/web_ui/src/routes/Templates/Templates.tsx b/applications/tari_walletd/web_ui/src/routes/Templates/Templates.tsx index 03e8199cf2..6061e9d622 100644 --- a/applications/tari_walletd/web_ui/src/routes/Templates/Templates.tsx +++ b/applications/tari_walletd/web_ui/src/routes/Templates/Templates.tsx @@ -23,8 +23,8 @@ import TableRow from "@mui/material/TableRow"; import TableCell from "@mui/material/TableCell"; import TableBody from "@mui/material/TableBody"; import TableContainer from "@mui/material/TableContainer"; -import CopyAddress from "../../Components/CopyAddress"; -import { AccordionIconButton, DataTableCell } from "../../Components/StyledComponents"; +import CopyAddress from "@components/CopyAddress"; +import { AccordionIconButton, DataTableCell } from "@components/StyledComponents"; import Grid from "@mui/material/Grid"; import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; diff --git a/applications/tari_walletd/web_ui/src/routes/Wallet/Components/AccessTokens.tsx b/applications/tari_walletd/web_ui/src/routes/Wallet/Components/AccessTokens.tsx index 2d6a91fe88..7784e933aa 100644 --- a/applications/tari_walletd/web_ui/src/routes/Wallet/Components/AccessTokens.tsx +++ b/applications/tari_walletd/web_ui/src/routes/Wallet/Components/AccessTokens.tsx @@ -44,12 +44,12 @@ import DialogTitle from "@mui/material/DialogTitle"; import IconButton from "@mui/material/IconButton"; import { useState } from "react"; import { IoCloseCircleOutline } from "react-icons/io5"; -import FetchStatusCheck from "../../../Components/FetchStatusCheck"; -import { AccordionIconButton, CodeBlock, DataTableCell } from "../../../Components/StyledComponents"; +import FetchStatusCheck from "@components/FetchStatusCheck"; +import { AccordionIconButton, CodeBlock, DataTableCell } from "@components/StyledComponents"; import { useAuthRevokeToken, useGetAllTokens } from "../../../api/hooks/useTokens"; import type { Claims, JrpcPermission, JrpcPermissions } from "@tari-project/typescript-bindings"; import { jrpcPermissionToString } from "@tari-project/typescript-bindings"; -import CopyAddress from "../../../Components/CopyAddress"; +import CopyAddress from "@components/CopyAddress"; function AlertDialog({ fn, row }: any) { const [open, setOpen] = useState(false); diff --git a/applications/tari_walletd/web_ui/src/routes/WebauthnRegistration/Components/Login.tsx b/applications/tari_walletd/web_ui/src/routes/WebauthnRegistration/Components/Login.tsx index 6b7c962505..650318ee58 100644 --- a/applications/tari_walletd/web_ui/src/routes/WebauthnRegistration/Components/Login.tsx +++ b/applications/tari_walletd/web_ui/src/routes/WebauthnRegistration/Components/Login.tsx @@ -4,7 +4,7 @@ import { useTheme } from "@mui/material/styles"; import { FormEvent, useState } from "react"; import { Form, useNavigate, useSearchParams } from "react-router-dom"; -import Loading from "../../../Components/Loading"; +import Loading from "@components/Loading"; import Typography from "@mui/material/Typography"; import Grid from "@mui/material/Grid"; import Box from "@mui/material/Box"; diff --git a/applications/tari_walletd/web_ui/src/routes/WebauthnRegistration/Components/Registration.tsx b/applications/tari_walletd/web_ui/src/routes/WebauthnRegistration/Components/Registration.tsx index 3795ea73c3..91e0c68161 100644 --- a/applications/tari_walletd/web_ui/src/routes/WebauthnRegistration/Components/Registration.tsx +++ b/applications/tari_walletd/web_ui/src/routes/WebauthnRegistration/Components/Registration.tsx @@ -10,12 +10,16 @@ import { useTheme } from "@mui/material/styles"; import { FormEvent, useState } from "react"; import { webauthnFinishRegistration, webauthnStartRegistration } from "../../../utils/json_rpc"; import { Buffer } from "buffer"; -import Loading from "../../../Components/Loading"; +import Loading from "@components/Loading"; import useAuthStore from "../../../store/authStore"; const WEBAUTHN_RP_ID = import.meta.env.VITE_DAEMON_WEBAUTHN_RP_ID || window.location.hostname; -const createCredential = async (rpOptions: { rpId: string; rpName: string }, username: string, challenge: BufferSource) => { +const createCredential = async ( + rpOptions: { rpId: string; rpName: string }, + username: string, + challenge: BufferSource, +) => { const publicKeyCredentialCreationOptions: PublicKeyCredentialCreationOptions = { rp: { name: rpOptions.rpName, diff --git a/applications/tari_walletd/web_ui/src/routes/WebauthnRegistration/Webauthn.tsx b/applications/tari_walletd/web_ui/src/routes/WebauthnRegistration/Webauthn.tsx index a20dd93428..8baec93fb3 100644 --- a/applications/tari_walletd/web_ui/src/routes/WebauthnRegistration/Webauthn.tsx +++ b/applications/tari_walletd/web_ui/src/routes/WebauthnRegistration/Webauthn.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from "react"; import WebauthnLogin from "./Components/Login"; import { useWebauthnAlreadyRegistered } from "../../api/hooks/useWebauthn"; -import Loading from "../../Components/Loading"; +import Loading from "@components/Loading"; import WebauthnRegistration from "./Components/Registration"; import useAuthStore from "../../store/authStore"; import { useNavigate, useSearchParams } from "react-router-dom"; diff --git a/applications/tari_walletd/web_ui/tsconfig.json b/applications/tari_walletd/web_ui/tsconfig.json index c7c14859de..710c4257bf 100644 --- a/applications/tari_walletd/web_ui/tsconfig.json +++ b/applications/tari_walletd/web_ui/tsconfig.json @@ -25,7 +25,7 @@ "baseUrl": ".", "paths": { "@/*": ["./src/*"], - "@components/*": ["./src/Components/*"], + "@components/*": ["./src/components/*"], "@routes/*": ["./src/routes/*"], "@utils/*": ["./src/utils/*"], "@assets/*": ["./src/assets/*"], From 8710721889b7249a41efa96b4bd774495df14b43 Mon Sep 17 00:00:00 2001 From: Erika Date: Fri, 5 Sep 2025 17:06:34 +0200 Subject: [PATCH 4/7] feat: improve send flow --- .../Tokens/components/SendMoney.tsx | 24 ++++--- .../AssetVault/Tokens/steps/FormStep.tsx | 67 ++++++++++++------- 2 files changed, 54 insertions(+), 37 deletions(-) diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx index 91e7bcec6d..dc66e6a695 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx @@ -25,7 +25,8 @@ import Button from "@mui/material/Button"; import Dialog from "@mui/material/Dialog"; import DialogContent from "@mui/material/DialogContent"; import DialogTitle from "@mui/material/DialogTitle"; -import { Stepper, Step, StepLabel } from "@mui/material"; +import IconButton from "@mui/material/IconButton"; +import CloseIcon from "@mui/icons-material/Close"; import { useAccountsGetBalances, useAccountsTransfer } from "@api/hooks/useAccounts"; import useAccountStore from "@store/accountStore"; import { SelectChangeEvent } from "@mui/material/Select/Select"; @@ -42,8 +43,7 @@ import { CURRENCY } from "@utils/constants"; import FormStep, { SendMoneyFormState } from "../steps/FormStep"; import ConfirmationStep from "../steps/ConfirmationStep"; import ResultStep, { TransferResult } from "../steps/ResultStep"; - -const steps = ["Enter Details", "Confirm Transfer", "Result"]; +import { Divider, Stack, Typography } from "@mui/material"; export default function SendMoney() { const [open, setOpen] = useState(false); @@ -293,6 +293,7 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) { case 0: return ( - Send {props.resource_address} + + + Send Tari + + + + + - - {steps.map((label) => ( - - {label} - - ))} - + {renderStepContent()}
diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx index b2653a1167..8ac8d50723 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx @@ -28,9 +28,9 @@ import Select from "@mui/material/Select"; import MenuItem from "@mui/material/MenuItem"; import CheckBox from "@mui/material/Checkbox"; import FormControlLabel from "@mui/material/FormControlLabel"; -import { Divider, InputLabel, Stack, InputAdornment } from "@mui/material"; +import { Divider, InputLabel, Stack, InputAdornment, Typography, Box } from "@mui/material"; import { SelectChangeEvent } from "@mui/material/Select/Select"; -import { ResourceType } from "@tari-project/typescript-bindings"; +import { ResourceType, ResourceAddress } from "@tari-project/typescript-bindings"; import { validateAddress, formatDisplayCurrency } from "@utils/helpers"; import { CURRENCY } from "@utils/constants"; @@ -44,6 +44,7 @@ export interface SendMoneyFormState { } interface FormStepProps { + resource_address?: ResourceAddress; resource_type: ResourceType; badges?: string[]; transferFormState: SendMoneyFormState; @@ -60,6 +61,7 @@ interface FormStepProps { } export default function FormStep({ + resource_address, resource_type, badges, transferFormState, @@ -76,7 +78,7 @@ export default function FormStep({ }: FormStepProps) { const isConfidential = resource_type === "Confidential"; const isStealth = resource_type === "Stealth"; - + // Track if the user is currently typing in the amount field const [isFocusedAmount, setIsFocusedAmount] = useState(false); @@ -84,29 +86,38 @@ export default function FormStep({ const hasInsufficientFunds = availableBalance !== undefined && enteredAmount > availableBalance; const isFormValid = validateAddress(transferFormState.publicKey) && transferFormState.amount && !hasInsufficientFunds; - + // Format amount for display const formatAmountValue = (amount: string) => { if (!amount) return ""; const num = parseFloat(amount); if (isNaN(num)) return amount; - + // If user is currently typing, show raw value to avoid cursor jumping if (isFocusedAmount) { return amount; } - + // Otherwise, show formatted value - const hasDecimals = amount.includes('.') && amount.split('.')[1].length > 0; - return num.toLocaleString('en-US', { - minimumFractionDigits: hasDecimals ? 0 : 2, - maximumFractionDigits: CURRENCY.DECIMALS + const hasDecimals = amount.includes(".") && amount.split(".")[1].length > 0; + return num.toLocaleString("en-US", { + minimumFractionDigits: hasDecimals ? 0 : 2, + maximumFractionDigits: CURRENCY.DECIMALS, }); }; return ( + {resource_address && ( + + + Resource Address: + + {resource_address} + + )} + {badges && ( <> )} - - + + + To Public Key: + + + {(isConfidential || isStealth) && ( <> @@ -196,7 +211,7 @@ export default function FormStep({ } InputProps={{ placeholder: "0.0", - endAdornment: {CURRENCY.SYMBOL} + endAdornment: {CURRENCY.SYMBOL}, }} /> @@ -204,9 +219,9 @@ export default function FormStep({ name="fee" label="Fee" value={ - isEstimatingFee - ? "Estimating..." - : transferFormState.fee + isEstimatingFee + ? "Estimating..." + : transferFormState.fee ? (parseInt(transferFormState.fee) / CURRENCY.DIVISOR).toString() : "" } @@ -215,7 +230,7 @@ export default function FormStep({ disabled={true} style={{ flexGrow: 1 }} InputProps={{ - endAdornment: !isEstimatingFee ? {CURRENCY.SYMBOL} : null + endAdornment: !isEstimatingFee ? {CURRENCY.SYMBOL} : null, }} /> From 5e218b0b82f7d30ec6383a476b24c1dd2243fd73 Mon Sep 17 00:00:00 2001 From: Erika Date: Mon, 8 Sep 2025 15:12:04 +0200 Subject: [PATCH 5/7] feat: improve send flow --- .../Tokens/components/SendMoney.tsx | 7 +++- .../Tokens/steps/ConfirmationStep.tsx | 37 ++++++++++--------- .../AssetVault/Tokens/steps/ResultStep.tsx | 13 ++++++- 3 files changed, 37 insertions(+), 20 deletions(-) diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx index dc66e6a695..20921eb469 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx @@ -249,7 +249,7 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) { success: true, message: "Transfer completed successfully", }); - props.onSendComplete?.(); + // Auto-close after 10 seconds - don't call onSendComplete immediately } catch (error) { setTransferResult({ success: false, @@ -261,12 +261,17 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) { }; const handleClose = () => { + const wasSuccessful = transferResult?.success; setActiveStep(0); setTransferFormState(INITIAL_VALUES); setTransferResult(undefined); setUseBadge(false); setDisabled(false); props.handleClose?.(); + // Call onSendComplete only after successful transfer when dialog closes + if (wasSuccessful) { + props.onSendComplete?.(); + } }; const handleBack = () => { diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ConfirmationStep.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ConfirmationStep.tsx index 8e8bee4c0e..d25d3eea96 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ConfirmationStep.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ConfirmationStep.tsx @@ -49,47 +49,48 @@ export default function ConfirmationStep({ - Confirm Transfer + You are about to send: - Resource: + Amount: + + + {(() => { + const amount = parseFloat(transferFormState.amount) || 0; + const hasDecimals = + transferFormState.amount.includes(".") && transferFormState.amount.split(".")[1].length > 0; + return `${amount.toLocaleString("en-US", { + minimumFractionDigits: hasDecimals ? 0 : 2, + maximumFractionDigits: CURRENCY.DECIMALS, + })} ${CURRENCY.SYMBOL}`; + })()} - {resource_address} - To Public Key: - - - + Transaction Fee: + {formatCurrency(parseInt(transferFormState.fee) || 0)} - Amount: + To Public Key: - {(() => { - const amount = parseFloat(transferFormState.amount) || 0; - const hasDecimals = transferFormState.amount.includes('.') && transferFormState.amount.split('.')[1].length > 0; - return `${amount.toLocaleString('en-US', { - minimumFractionDigits: hasDecimals ? 0 : 2, - maximumFractionDigits: CURRENCY.DECIMALS - })} ${CURRENCY.SYMBOL}`; - })()} + - Transaction Fee: + From: - {formatCurrency(parseInt(transferFormState.fee) || 0)} + {resource_address} {resource_type === "Confidential" && ( diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ResultStep.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ResultStep.tsx index 58bc88561c..2504ebeb4c 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ResultStep.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ResultStep.tsx @@ -21,6 +21,7 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import { Typography, Stack, Button, CircularProgress, Fade, Divider } from "@mui/material"; +import { useEffect } from "react"; import CancelRoundedIcon from "@mui/icons-material/CancelRounded"; import CheckCircleRoundedIcon from "@mui/icons-material/CheckCircleRounded"; @@ -36,8 +37,18 @@ interface ResultStepProps { } export default function ResultStep({ disabled, transferResult, onClose }: ResultStepProps) { + useEffect(() => { + if (!disabled && transferResult) { + const timer = setTimeout(() => { + onClose(); + }, 10000); + + return () => clearTimeout(timer); + } + }, [disabled, transferResult, onClose]); + return ( - + {disabled ? ( <> From 77c0cb7a827b4502f232737a6437944c31a54971 Mon Sep 17 00:00:00 2001 From: Erika Date: Tue, 9 Sep 2025 14:35:27 +0200 Subject: [PATCH 6/7] feat: improve send flow --- .../src/routes/AssetVault/Tokens/Tokens.tsx | 43 +++++++++++------ .../Tokens/components/SendMoney.tsx | 47 +++++++++++++++++-- .../Tokens/steps/ConfirmationStep.tsx | 17 +++---- .../AssetVault/Tokens/steps/FormStep.tsx | 13 ++--- 4 files changed, 83 insertions(+), 37 deletions(-) diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/Tokens.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/Tokens.tsx index c359d40c55..d628fb2a94 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/Tokens.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/Tokens.tsx @@ -55,27 +55,34 @@ interface BalanceRowProps { onSendClicked?: (resource_address: ResourceAddress, resource_type: ResourceType) => void; } -function ConfidentialBalance(props: { show: boolean; balance: Amount; resourceType: string; divisibility: number }) { - switch (props.resourceType) { +interface ConfidentialBalanceProps { + show: boolean; + balance: Amount; + resourceType: string; + divisibility: number; + token_symbol?: string; +} + +function ConfidentialBalance({ show, resourceType, balance, divisibility, token_symbol }: ConfidentialBalanceProps) { + switch (resourceType) { case "Confidential": case "Stealth": - return <>{props.show ? bigintToDecimalString(props.balance, props.divisibility) : "**************"}; + return <>{show ? bigintToDecimalString(balance, divisibility) + " " + token_symbol : "**************"}; default: return <>--; } } -function BalanceRow(props: BalanceRowProps) { - const { - token_symbol, - resource_address, - resource_type, - balance, - confidential_balance, - vault_address, - divisibility, - onSendClicked, - } = props; +function BalanceRow({ + token_symbol, + resource_address, + resource_type, + balance, + confidential_balance, + vault_address, + divisibility, + onSendClicked, +}: BalanceRowProps) { const showBalance = useAccountStore((state) => state.showBalance); return ( @@ -86,13 +93,16 @@ function BalanceRow(props: BalanceRowProps) { display={`${token_symbol || shortenSubstateId(resource_address)} ${resource_type}`} /> - {showBalance ? bigintToDecimalString(balance, divisibility) : "*************"} + + {showBalance ? bigintToDecimalString(balance, divisibility) + " " + token_symbol : "*************"} + @@ -128,6 +138,9 @@ function Tokens({ account }: { account: Account }) { onSendComplete={() => setResourceToSend(null)} resource_address={resourceToSend?.address} resource_type={resourceToSend?.resource_type!} + token_symbol={ + balancesData?.balances.find((b) => b.resource_address === resourceToSend?.address)?.token_symbol || "" + } /> setOpen(false)} resource_type="Confidential" resource_address={XTR} + token_symbol="tXTR" /> ); @@ -70,6 +70,7 @@ export interface SendMoneyDialogProps { resource_type: ResourceType; onSendComplete?: () => void; handleClose: () => void; + token_symbol: string; } export function SendMoneyDialog(props: SendMoneyDialogProps) { @@ -101,16 +102,48 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) { ?.filter((b: BalanceEntry) => b.resource_type === "NonFungible" && BigInt(b.balance) > 0n) .map((b: BalanceEntry) => b.resource_address) as string[]; + console.log("data", data); + console.log("badges", badges); + // Find the available balance for the resource we're trying to send const balanceEntry = data?.balances?.find( (b: BalanceEntry) => b.resource_address === (props.resource_address || XTR), ); - // Balance is in micro XTR units, convert to XTR for display - const availableBalance = balanceEntry?.balance ? Number(balanceEntry.balance) / CURRENCY.DIVISOR : undefined; + + // Function to calculate available balance based on input selection + const calculateAvailableBalance = () => { + if (!balanceEntry) return undefined; + + const revealedBalance = BigInt(balanceEntry.balance); + const confidentialBalance = BigInt(balanceEntry.confidential_balance); + const divisor = Math.pow(10, balanceEntry.divisibility); + + let result; + switch (transferFormState.inputSelection) { + case "RevealedOnly": + result = Number(revealedBalance) / divisor; + break; + case "ConfidentialOnly": + result = Number(confidentialBalance) / divisor; + break; + case "PreferRevealed": + case "PreferConfidential": + // For prefer options, show total available (revealed + confidential) + result = Number(revealedBalance + confidentialBalance) / divisor; + break; + default: + result = Number(revealedBalance + confidentialBalance) / divisor; + break; + } + + return result; + }; + + const availableBalance = calculateAvailableBalance(); const transfer = { account: substateIdToString(account.address), - amount: Math.floor((parseFloat(transferFormState.amount) || 0) * CURRENCY.DIVISOR), + amount: Math.floor((parseFloat(transferFormState.amount) || 0) * Math.pow(10, balanceEntry?.divisibility || 6)), resource_address: props.resource_address || XTR, destination_public_key: transferFormState.publicKey, resourceType: props.resource_type, @@ -179,7 +212,7 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) { // Create transfer object with current form state const currentTransfer = { account: substateIdToString(account.address), - amount: Math.floor((parseFloat(transferFormState.amount) || 0) * CURRENCY.DIVISOR), + amount: Math.floor((parseFloat(transferFormState.amount) || 0) * Math.pow(10, balanceEntry?.divisibility || 6)), resource_address: props.resource_address || XTR, destination_public_key: transferFormState.publicKey, resourceType: props.resource_type, @@ -306,6 +339,8 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) { useBadge={useBadge} isEstimatingFee={isEstimatingFee} availableBalance={availableBalance} + token_symbol={props.token_symbol} + divisibility={balanceEntry?.divisibility || 6} onSubmit={handleFormSubmit} onCancel={handleClose} onFormValueChange={setFormValue} @@ -323,6 +358,8 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) { disabled={disabled} onBack={handleBack} onConfirm={handleConfirm} + token_symbol={props.token_symbol} + divisibility={balanceEntry?.divisibility || 6} /> ); case 2: diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ConfirmationStep.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ConfirmationStep.tsx index d25d3eea96..3814484124 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ConfirmationStep.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ConfirmationStep.tsx @@ -23,8 +23,7 @@ import { Box, Button, Stack, Typography, Divider } from "@mui/material"; import type { ResourceAddress, ResourceType } from "@tari-project/typescript-bindings"; import CopyAddress from "@components/CopyAddress"; -import { formatCurrency } from "@utils/helpers"; -import { CURRENCY } from "@utils/constants"; +import { formatCurrency, bigintToDecimalString } from "@utils/helpers"; import { SendMoneyFormState } from "./FormStep"; interface ConfirmationStepProps { @@ -34,6 +33,8 @@ interface ConfirmationStepProps { disabled: boolean; onBack: () => void; onConfirm: () => void; + token_symbol: string; + divisibility: number; } export default function ConfirmationStep({ @@ -43,6 +44,8 @@ export default function ConfirmationStep({ disabled, onBack, onConfirm, + token_symbol, + divisibility, }: ConfirmationStepProps) { return ( @@ -58,15 +61,7 @@ export default function ConfirmationStep({ Amount: - {(() => { - const amount = parseFloat(transferFormState.amount) || 0; - const hasDecimals = - transferFormState.amount.includes(".") && transferFormState.amount.split(".")[1].length > 0; - return `${amount.toLocaleString("en-US", { - minimumFractionDigits: hasDecimals ? 0 : 2, - maximumFractionDigits: CURRENCY.DECIMALS, - })} ${CURRENCY.SYMBOL}`; - })()} + {transferFormState.amount} {token_symbol} diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx index 8ac8d50723..58fc8a5d0e 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx @@ -52,6 +52,8 @@ interface FormStepProps { useBadge: boolean; isEstimatingFee: boolean; availableBalance?: number; + token_symbol: string; + divisibility: number; onSubmit: (e: FormEvent) => void; onCancel: () => void; onFormValueChange: (e: React.ChangeEvent) => void; @@ -69,6 +71,8 @@ export default function FormStep({ useBadge, isEstimatingFee, availableBalance, + token_symbol, + divisibility, onSubmit, onCancel, onFormValueChange, @@ -102,7 +106,7 @@ export default function FormStep({ const hasDecimals = amount.includes(".") && amount.split(".")[1].length > 0; return num.toLocaleString("en-US", { minimumFractionDigits: hasDecimals ? 0 : 2, - maximumFractionDigits: CURRENCY.DECIMALS, + maximumFractionDigits: divisibility, }); }; @@ -146,9 +150,6 @@ export default function FormStep({ )} - - To Public Key: - {CURRENCY.SYMBOL}, + endAdornment: {token_symbol}, }} /> @@ -230,7 +231,7 @@ export default function FormStep({ disabled={true} style={{ flexGrow: 1 }} InputProps={{ - endAdornment: !isEstimatingFee ? {CURRENCY.SYMBOL} : null, + endAdornment: !isEstimatingFee ? {token_symbol} : null, }} /> From a650b5da66885ab0c8fdc10a9f7b516b5a30ae9c Mon Sep 17 00:00:00 2001 From: Erika Date: Wed, 10 Sep 2025 21:47:42 +0200 Subject: [PATCH 7/7] feat: send flow improvements --- .../AssetVault/Components/AccountBalance.tsx | 30 ++++++++----------- .../Tokens/components/SendMoney.tsx | 19 ++++++------ .../Tokens/steps/ConfirmationStep.tsx | 4 +-- .../AssetVault/Tokens/steps/FormStep.tsx | 4 +-- 4 files changed, 27 insertions(+), 30 deletions(-) diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/AccountBalance.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/AccountBalance.tsx index 6a379c36a6..5d5727ae51 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/AccountBalance.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/AccountBalance.tsx @@ -31,7 +31,7 @@ import { useAccountsGetBalances } from "@api/hooks/useAccounts"; import useAccountStore from "@store/accountStore"; import { useEffect } from "react"; import { substateIdToString, bigintToDecimalString } from "@utils/helpers"; -import { CURRENCY } from "@utils/constants"; +// import { CURRENCY } from "@utils/constants"; import { Account } from "@tari-project/typescript-bindings"; const XTR_RESOURCE = "resource_0101010101010101010101010101010101010101010101010101010101010101"; @@ -42,26 +42,20 @@ export default function AccountBalance() { if (!account) return <>; - return ( - - ); + return ; } -function AccountBalanceInner({ - account, - showBalance, - setShowBalance -}: { - account: Account; - showBalance: boolean; +function AccountBalanceInner({ + account, + showBalance, + setShowBalance, +}: { + account: Account; + showBalance: boolean; setShowBalance: (show: boolean) => void; }) { const theme = useTheme(); - + const { data: balancesData, isError: balancesIsError, @@ -89,6 +83,8 @@ function AccountBalanceInner({ } } + const symbol = balancesData?.balances.find((b) => b.resource_address === XTR_RESOURCE)?.token_symbol || ""; + return ( - {formattedBalance} {CURRENCY.SYMBOL} + {formattedBalance} {symbol} setShowBalance(!showBalance)}> {showBalance ? ( diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx index d714114838..ef83c6fef2 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx @@ -46,6 +46,10 @@ import { Divider, Stack, Typography } from "@mui/material"; export default function SendMoney() { const [open, setOpen] = useState(false); + const { account } = useAccountStore(); + + const { data } = useAccountsGetBalances(account ? substateIdToString(account.address) : ""); + const xtrBalanceEntry = data?.balances?.find((b) => b.resource_address === XTR); return ( <> @@ -58,7 +62,7 @@ export default function SendMoney() { onSendComplete={() => setOpen(false)} resource_type="Confidential" resource_address={XTR} - token_symbol="tXTR" + token_symbol={xtrBalanceEntry?.token_symbol || ""} /> ); @@ -102,22 +106,19 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) { ?.filter((b: BalanceEntry) => b.resource_type === "NonFungible" && BigInt(b.balance) > 0n) .map((b: BalanceEntry) => b.resource_address) as string[]; - console.log("data", data); - console.log("badges", badges); - // Find the available balance for the resource we're trying to send const balanceEntry = data?.balances?.find( (b: BalanceEntry) => b.resource_address === (props.resource_address || XTR), ); - + // Function to calculate available balance based on input selection const calculateAvailableBalance = () => { if (!balanceEntry) return undefined; - + const revealedBalance = BigInt(balanceEntry.balance); const confidentialBalance = BigInt(balanceEntry.confidential_balance); const divisor = Math.pow(10, balanceEntry.divisibility); - + let result; switch (transferFormState.inputSelection) { case "RevealedOnly": @@ -135,10 +136,10 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) { result = Number(revealedBalance + confidentialBalance) / divisor; break; } - + return result; }; - + const availableBalance = calculateAvailableBalance(); const transfer = { diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ConfirmationStep.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ConfirmationStep.tsx index 3814484124..25c00fe2f0 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ConfirmationStep.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ConfirmationStep.tsx @@ -45,7 +45,6 @@ export default function ConfirmationStep({ onBack, onConfirm, token_symbol, - divisibility, }: ConfirmationStepProps) { return ( @@ -61,7 +60,8 @@ export default function ConfirmationStep({ Amount: - {transferFormState.amount} {token_symbol} + {transferFormState.amount} + {token_symbol ? ` ${token_symbol}` : ""} diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx index 58fc8a5d0e..4bcc8b887b 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx @@ -212,7 +212,7 @@ export default function FormStep({ } InputProps={{ placeholder: "0.0", - endAdornment: {token_symbol}, + endAdornment: token_symbol ? {token_symbol} : undefined, }} /> @@ -231,7 +231,7 @@ export default function FormStep({ disabled={true} style={{ flexGrow: 1 }} InputProps={{ - endAdornment: !isEstimatingFee ? {token_symbol} : null, + endAdornment: !isEstimatingFee && token_symbol ? {token_symbol} : null, }} />