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
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 (
+
+ );
+}
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 (
+
+ );
+}
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"),