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/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/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, - ) => ( - - ), - )} - -
-
-
+
- - setOpen(false)} - onSendComplete={() => setOpen(false)} - resource_type="Confidential" - resource_address={XTR} - /> - - ); -} - -export interface SendMoneyDialogProps { - open: boolean; - resource_address?: ResourceAddress; - resource_type: ResourceType; - onSendComplete?: () => void; - handleClose: () => void; -} - -export function SendMoneyDialog(props: SendMoneyDialogProps) { - const INITIAL_VALUES = { - publicKey: "", - outputToConfidential: false, - inputSelection: "PreferRevealed", - amount: "", - fee: "", - badge: null, - }; - const isConfidential = props.resource_type === "Confidential"; - const isStealth = props.resource_type === "Stealth"; - const [useBadge, setUseBadge] = useState(false); - const [disabled, setDisabled] = useState(false); - const [transferFormState, setTransferFormState] = useState(INITIAL_VALUES); - const [validity, setValidity] = useState({ - publicKey: false, - amount: false, - }); - const [allValid, setAllValid] = useState(false); - 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[]; - - 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 - 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, - }; - - function setFormValue(e: React.ChangeEvent) { - setTransferFormState({ - ...transferFormState, - [e.target.name]: e.target.value, - }); - if (validity[e.target.name as keyof object] !== undefined) { - setValidity({ - ...validity, - [e.target.name]: e.target.validity.valid, - }); - } - } - - function setSelectFormValue(e: SelectChangeEvent) { - setTransferFormState({ - ...transferFormState, - [e.target.name]: e.target.value, - }); - } - - function setCheckboxFormValue(e: React.ChangeEvent) { - setTransferFormState({ - ...transferFormState, - [e.target.name]: e.target.checked, - }); - } - - const onTransfer = async (e: FormEvent) => { - e.preventDefault(); - if (!account) { - return; - } - - setDisabled(true); - if (!isNaN(parseInt(transferFormState.fee))) { - 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); - }); - } - }; - - const handleClose = () => { - props.handleClose?.(); - }; - - const handleUseBadgeChange = (e: React.ChangeEvent) => { - setUseBadge(e.target.checked); - if (!e.target.checked) { - setTransferFormState({ - ...transferFormState, - badge: null, - }); - } - }; - - useEffect(() => { - setAllValid(Object.values(validity).every((v) => v)); - }, [validity]); - - return ( - - Send {props.resource_address} - -
- {badges && ( - <> - } - label="Use Badge" - /> - Badge - - - )} - - {(isConfidential || isStealth) && ( - <> - - } - label="Send Confidential Outputs" - /> - Input Selection - - - )} - - - - - - - -
-
- ); -} - -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/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/Tokens.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/Tokens.tsx new file mode 100644 index 0000000000..d628fb2a94 --- /dev/null +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/Tokens.tsx @@ -0,0 +1,196 @@ +// 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 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 { useState } from "react"; +import FetchStatusCheck from "@components/FetchStatusCheck"; +import { DataTableCell } from "@components/StyledComponents"; +import { useAccountsGetBalances } from "@api/hooks/useAccounts"; +import useAccountStore from "@store/accountStore"; +import { bigintToDecimalString, shortenSubstateId, substateIdToString } from "@utils/helpers"; +import { Button } from "@mui/material"; +import { SendMoneyDialog } from "./components/SendMoney"; +import { + ResourceAddress, + ResourceType, + VaultId, + BalanceEntry, + Account, + Amount, +} from "@tari-project/typescript-bindings"; +import CopyAddress from "@components/CopyAddress"; + +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; +} + +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 <>{show ? bigintToDecimalString(balance, divisibility) + " " + token_symbol : "**************"}; + default: + return <>--; + } +} + +function BalanceRow({ + token_symbol, + resource_address, + resource_type, + balance, + confidential_balance, + vault_address, + divisibility, + onSendClicked, +}: BalanceRowProps) { + const showBalance = useAccountStore((state) => state.showBalance); + return ( + + {vault_address ? : "--"} + + + + + {showBalance ? bigintToDecimalString(balance, divisibility) + " " + token_symbol : "*************"} + + + + + + + + + ); +} + +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!} + token_symbol={ + balancesData?.balances.find((b) => b.resource_address === resourceToSend?.address)?.token_symbol || "" + } + /> + + + + + + 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/Tokens/components/SendMoney.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx new file mode 100644 index 0000000000..ef83c6fef2 --- /dev/null +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx @@ -0,0 +1,389 @@ +// Copyright 2022. 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, useState, useEffect } from "react"; +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 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"; +import { + BalanceEntry, + ConfidentialTransferInputSelection, + ResourceAddress, + ResourceType, + substateIdToString, + XTR, +} from "@tari-project/typescript-bindings"; +import { transactionsWaitResult } from "@utils/json_rpc"; +import FormStep, { SendMoneyFormState } from "../steps/FormStep"; +import ConfirmationStep from "../steps/ConfirmationStep"; +import ResultStep, { TransferResult } from "../steps/ResultStep"; +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 ( + <> + + setOpen(false)} + onSendComplete={() => setOpen(false)} + resource_type="Confidential" + resource_address={XTR} + token_symbol={xtrBalanceEntry?.token_symbol || ""} + /> + + ); +} + +export interface SendMoneyDialogProps { + open: boolean; + resource_address?: ResourceAddress; + resource_type: ResourceType; + onSendComplete?: () => void; + handleClose: () => void; + token_symbol: string; +} + +export function SendMoneyDialog(props: SendMoneyDialogProps) { + const INITIAL_VALUES: SendMoneyFormState = { + publicKey: "", + outputToConfidential: false, + inputSelection: "PreferRevealed", + amount: "", + fee: "", + badge: null, + }; + + 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 [transferResult, setTransferResult] = useState(); + const { mutateAsync: sendIt } = useAccountsTransfer(); + + const { account, setPopup } = useAccountStore(); + + if (!account) { + return null; + } + + 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), + ); + + // 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) * Math.pow(10, balanceEntry?.divisibility || 6)), + 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, + }; + + 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, + [name]: processedValue, + ...(shouldClearFee ? { fee: "" } : {}), + }); + } + + function setSelectFormValue(e: SelectChangeEvent) { + setTransferFormState({ + ...transferFormState, + [e.target.name]: e.target.value, + }); + } + + function setCheckboxFormValue(e: React.ChangeEvent) { + setTransferFormState({ + ...transferFormState, + [e.target.name]: e.target.checked, + }); + } + + 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) * Math.pow(10, balanceEntry?.divisibility || 6)), + 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); + setActiveStep(2); + + try { + await sendIt?.({ + ...transfer, + dry_run: false, + max_fee: parseInt(transferFormState.fee), + }); + + setTransferResult({ + success: true, + message: "Transfer completed successfully", + }); + // Auto-close after 10 seconds - don't call onSendComplete immediately + } catch (error) { + setTransferResult({ + success: false, + message: error instanceof Error ? error.message : "Transfer failed", + }); + } finally { + setDisabled(false); + } + }; + + 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 = () => { + setActiveStep(activeStep - 1); + }; + + // Auto-estimate fee when user enters valid public key and amount + useEffect(() => { + 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 () => clearTimeout(timeoutId); + } + }, [transferFormState.publicKey, transferFormState.amount]); + + const renderStepContent = () => { + switch (activeStep) { + case 0: + return ( + + ); + case 1: + return ( + + ); + case 2: + return ; + default: + return null; + } + }; + + return ( + + + + Send Tari + + + + + + + + {renderStepContent()} + + + ); +} 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..25c00fe2f0 --- /dev/null +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ConfirmationStep.tsx @@ -0,0 +1,121 @@ +// 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, Divider } from "@mui/material"; +import type { ResourceAddress, ResourceType } from "@tari-project/typescript-bindings"; +import CopyAddress from "@components/CopyAddress"; +import { formatCurrency, bigintToDecimalString } from "@utils/helpers"; +import { SendMoneyFormState } from "./FormStep"; + +interface ConfirmationStepProps { + resource_address?: ResourceAddress; + resource_type: ResourceType; + transferFormState: SendMoneyFormState; + disabled: boolean; + onBack: () => void; + onConfirm: () => void; + token_symbol: string; + divisibility: number; +} + +export default function ConfirmationStep({ + resource_address, + resource_type, + transferFormState, + disabled, + onBack, + onConfirm, + token_symbol, +}: ConfirmationStepProps) { + return ( + + + + + You are about to send: + + + + + + Amount: + + + {transferFormState.amount} + {token_symbol ? ` ${token_symbol}` : ""} + + + + + + Transaction Fee: + + {formatCurrency(parseInt(transferFormState.fee) || 0)} + + + + + To Public Key: + + + + + + + + + From: + + {resource_address} + + + {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/FormStep.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx new file mode 100644 index 0000000000..4bcc8b887b --- /dev/null +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx @@ -0,0 +1,250 @@ +// 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, SUCH DAMAGE. + +import { FormEvent, useState } 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 FormControlLabel from "@mui/material/FormControlLabel"; +import { Divider, InputLabel, Stack, InputAdornment, Typography, Box } from "@mui/material"; +import { SelectChangeEvent } from "@mui/material/Select/Select"; +import { ResourceType, ResourceAddress } from "@tari-project/typescript-bindings"; +import { validateAddress, formatDisplayCurrency } from "@utils/helpers"; +import { CURRENCY } from "@utils/constants"; + +export interface SendMoneyFormState { + publicKey: string; + outputToConfidential: boolean; + inputSelection: string; + amount: string; + fee: string; + badge: string | null; +} + +interface FormStepProps { + resource_address?: ResourceAddress; + resource_type: ResourceType; + badges?: string[]; + transferFormState: SendMoneyFormState; + disabled: boolean; + useBadge: boolean; + isEstimatingFee: boolean; + availableBalance?: number; + token_symbol: string; + divisibility: number; + onSubmit: (e: FormEvent) => void; + onCancel: () => void; + onFormValueChange: (e: React.ChangeEvent) => void; + onSelectFormValueChange: (e: SelectChangeEvent) => void; + onCheckboxFormValueChange: (e: React.ChangeEvent) => void; + onUseBadgeChange: (e: React.ChangeEvent) => void; +} + +export default function FormStep({ + resource_address, + resource_type, + badges, + transferFormState, + disabled, + useBadge, + isEstimatingFee, + availableBalance, + token_symbol, + divisibility, + onSubmit, + onCancel, + onFormValueChange, + onSelectFormValueChange, + onCheckboxFormValueChange, + onUseBadgeChange, +}: 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); + + const enteredAmount = parseFloat(transferFormState.amount) || 0; + 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: divisibility, + }); + }; + + return ( +
+ + {resource_address && ( + + + Resource Address: + + {resource_address} + + )} + + {badges && ( + <> + } + label="Use Badge" + /> + {useBadge && ( + <> + Badge + + + )} + + )} + + + + + {(isConfidential || isStealth) && ( + <> + + } + label="Send Confidential Outputs" + /> + Input Selection + + + )} + + setIsFocusedAmount(true)} + onBlur={() => setIsFocusedAmount(false)} + style={{ flexGrow: 1 }} + disabled={disabled} + error={hasInsufficientFunds} + helperText={ + hasInsufficientFunds + ? `Insufficient funds. Available balance: ${formatDisplayCurrency(availableBalance || 0)}` + : availableBalance !== undefined + ? `Available balance: ${formatDisplayCurrency(availableBalance)}` + : undefined + } + InputProps={{ + placeholder: "0.0", + endAdornment: token_symbol ? {token_symbol} : undefined, + }} + /> + + {token_symbol} : null, + }} + /> + + + + + + + +
+ ); +} 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..2504ebeb4c --- /dev/null +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/ResultStep.tsx @@ -0,0 +1,81 @@ +// 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 { useEffect } from "react"; +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({ disabled, transferResult, onClose }: ResultStepProps) { + useEffect(() => { + if (!disabled && transferResult) { + const timer = setTimeout(() => { + onClose(); + }, 10000); + + return () => clearTimeout(timer); + } + }, [disabled, transferResult, onClose]); + + return ( + + {disabled ? ( + <> + + Sending Money... + 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/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/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, { 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/*"], 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"),