diff --git a/applications/tari_walletd/web_ui/src/App.tsx b/applications/tari_walletd/web_ui/src/App.tsx index 002a54d16e..0c2384ff5e 100644 --- a/applications/tari_walletd/web_ui/src/App.tsx +++ b/applications/tari_walletd/web_ui/src/App.tsx @@ -34,7 +34,7 @@ import AssetVault from "@routes/AssetVault/AssetVault"; import SettingsPage from "@routes/Settings/Settings"; import Auth, { AUTH_TOKEN_FOR_NONE_AUTH } from "@routes/Auth/Auth"; import Webauthn from "@routes/WebauthnRegistration/Webauthn"; -import useAuthStore from "@store/authStore"; +import useAuthStore from "./services/store/authStore"; import { useEffect } from "react"; import { useAuthMethod } from "@api/hooks/useAuth"; import AccessToken from "@routes/AccessToken/AccessToken"; @@ -42,6 +42,8 @@ import { jwtDecode } from "jwt-decode"; import Templates from "@routes/Templates/Templates"; import Manifest from "@routes/Manifest/Manifest"; import FlowEditor from "@routes/FlowEditor/FlowEditor"; +import { useCurrencySync } from "@store/hooks/useCurrencySync"; +import { ErrorNotificationProvider } from "./contexts/ErrorNotificationContext"; export const breadcrumbRoutes = [ { @@ -160,6 +162,8 @@ function App() { const { authToken } = authStore; let isAuthenticated = !!authToken; + useCurrencySync(); + useEffect(() => { if (isTokenExpired(authToken) && authToken !== AUTH_TOKEN_FOR_NONE_AUTH) { authStore.clearToken(); @@ -193,10 +197,11 @@ function App() { }, [authMethod, authMethodsIsError]); return ( -
- - }> - } /> + +
+ + }> + } /> } /> } />
+
); } diff --git a/applications/tari_walletd/web_ui/src/Components/PopupTitle.tsx b/applications/tari_walletd/web_ui/src/Components/PopupTitle.tsx new file mode 100644 index 0000000000..46b0561106 --- /dev/null +++ b/applications/tari_walletd/web_ui/src/Components/PopupTitle.tsx @@ -0,0 +1,57 @@ +// 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 { DialogTitle, IconButton, Stack, Typography, Divider } from "@mui/material"; +import CloseIcon from "@mui/icons-material/Close"; + +interface PopupTitleProps { + title: string; + onClose?: () => void; +} + +function PopupTitle({ title, onClose }: PopupTitleProps) { + return ( + + + + + {title} + + + {onClose && ( + + + + )} + + + + ); +} + +export default PopupTitle; diff --git a/applications/tari_walletd/web_ui/src/contexts/ErrorNotificationContext.tsx b/applications/tari_walletd/web_ui/src/contexts/ErrorNotificationContext.tsx new file mode 100644 index 0000000000..e7e8a699b5 --- /dev/null +++ b/applications/tari_walletd/web_ui/src/contexts/ErrorNotificationContext.tsx @@ -0,0 +1,110 @@ +// 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 React, { createContext, useContext, useState, useRef } from "react"; +import { Snackbar, Alert, AlertColor } from "@mui/material"; + +interface ErrorNotification { + message: string; + severity?: AlertColor; + duration?: number; +} + +interface ErrorNotificationContextType { + showError: (message: string, severity?: AlertColor, duration?: number) => void; + showSuccess: (message: string, duration?: number) => void; + showWarning: (message: string, duration?: number) => void; + showInfo: (message: string, duration?: number) => void; + clearNotification: () => void; +} + +const ErrorNotificationContext = createContext(undefined); + +export const ErrorNotificationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [notification, setNotification] = useState(null); + const timeoutRef = useRef(null); + + const showNotification = (message: string, severity: AlertColor = "error", duration: number = 8000) => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + setNotification({ message, severity, duration }); + + timeoutRef.current = setTimeout(() => { + setNotification(null); + }, duration); + }; + + const clearNotification = () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + setNotification(null); + }; + + const contextValue: ErrorNotificationContextType = { + showError: (message, severity = "error", duration = 8000) => showNotification(message, severity, duration), + showSuccess: (message, duration = 4000) => showNotification(message, "success", duration), + showWarning: (message, duration = 6000) => showNotification(message, "warning", duration), + showInfo: (message, duration = 4000) => showNotification(message, "info", duration), + clearNotification, + }; + + return ( + + {children} + {notification && ( + { + if (reason === "clickaway") { + return; + } + clearNotification(); + }} + anchorOrigin={{ vertical: "bottom", horizontal: "center" }} + > + + {notification.message} + + + )} + + ); +}; + +export const useErrorNotification = () => { + const context = useContext(ErrorNotificationContext); + if (context === undefined) { + throw new Error("useErrorNotification must be used within an ErrorNotificationProvider"); + } + return context; +}; diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/AssetVault.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/AssetVault.tsx index 960224f2d6..49ae6624c6 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/AssetVault.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/AssetVault.tsx @@ -27,6 +27,7 @@ import MyAssets from "./Components/MyAssets"; import { useEffect } from "react"; import FetchStatusCheck from "@components/FetchStatusCheck"; import useAuthStore from "@store/authStore"; +import { useWalletInfo } from "@api/hooks/useWalletInfo"; function AssetVault() { const account = useAccountStore((state) => state.account); @@ -34,6 +35,7 @@ function AssetVault() { const setPublicKey = useAccountStore((state) => state.setPublicKey); const { data: defaultAccount, isLoading, isError, error } = useAccountsGetDefault(); const authStore = useAuthStore(); + const { data: walletInfo } = useWalletInfo(); useEffect(() => { if (!isError && defaultAccount) { @@ -48,6 +50,8 @@ function AssetVault() { } }, [defaultAccount, isError]); + console.log("walletInfo", walletInfo); + return ( {account ? : } 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 3c503b5f85..f420c1738c 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 @@ -20,59 +20,24 @@ // 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 from "@mui/material/Box"; -import Button from "@mui/material/Button"; -import { useTheme } from "@mui/material/styles"; -import { useAccountsCreateFreeTestCoins } from "@api/hooks/useAccounts"; +import Stack from "@mui/material/Stack"; import ClaimBurn from "./ClaimBurn"; import useAccountStore from "@store/accountStore"; -import SendMoney from "../Tokens/components/SendMoney"; import ClaimFees from "./ClaimFees"; import PublishTemplate from "./PublishTemplate"; -import { substateIdToString } from "@tari-project/typescript-bindings"; function ActionMenu() { - const { mutate: claimTestnetFaucetFunds } = useAccountsCreateFreeTestCoins(); const account = useAccountStore((state) => state.account); - const setAccount = useAccountStore((state) => state.setAccount); - const setPublicKey = useAccountStore((state) => state.setPublicKey); - const theme = useTheme(); if (!account) { return null; } - const onClaimFreeCoins = () => { - claimTestnetFaucetFunds( - { - account: { ComponentAddress: substateIdToString(account.address) }, - amount: 1_000_000_000, - fee: 1000, - }, - { - onSuccess: (resp) => { - setAccount(resp.account); - setPublicKey(resp.public_key); - }, - }, - ); - }; - return ( - - + - - + ); } diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/AddAccount.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/AddAccount.tsx index 8b8183fa96..348b9cde08 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/AddAccount.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/AddAccount.tsx @@ -26,41 +26,44 @@ import Button from "@mui/material/Button"; import TextField from "@mui/material/TextField"; import Dialog from "@mui/material/Dialog"; import DialogContent from "@mui/material/DialogContent"; -import DialogTitle from "@mui/material/DialogTitle"; -import Box from "@mui/material/Box"; -import Snackbar from "@mui/material/Snackbar"; +import { CircularProgress, Alert } from "@mui/material"; import { useAccountsCreate } from "@api/hooks/useAccounts"; +import useAccountStore from "@store/accountStore"; +import type { AccountsCreateResponse } from "@tari-project/typescript-bindings"; import { useTheme } from "@mui/material/styles"; import queryClient from "@api/queryClient"; +import { Stack, Fade, Typography } from "@mui/material"; +import CheckCircleRoundedIcon from "@mui/icons-material/CheckCircleRounded"; +import PopupTitle from "@/components/PopupTitle"; function AddAccount({ open, setOpen }: { open: boolean; setOpen: React.Dispatch> }) { const [accountFormState, setAccountFormState] = useState({ accountName: "", }); - const { mutateAsync: mutateAddAccount } = useAccountsCreate(); + const { mutateAsync: mutateAddAccount, isPending, error, isSuccess, data, reset } = useAccountsCreate(); const theme = useTheme(); - const [isBusy, setIsBusy] = useState(false); + const setAccount = useAccountStore((state) => state.setAccount); + const setPublicKey = useAccountStore((state) => state.setPublicKey); const handleClose = () => { + setAccountFormState({ accountName: "" }); + reset(); setOpen(false); }; const onSubmitAddAccount = async (e: FormEvent) => { e.preventDefault(); - setIsBusy(true); - await mutateAddAccount( - { accountName: accountFormState.accountName }, - { - onSettled: () => { - setAccountFormState({ - accountName: "", - }); - setOpen(false); - queryClient.invalidateQueries({ queryKey: ["accounts"] }); - }, - }, - ); - setIsBusy(false); + try { + const newAccount: AccountsCreateResponse = await mutateAddAccount({ accountName: accountFormState.accountName }); + setAccount(newAccount.account); + setPublicKey(newAccount.public_key); + queryClient.invalidateQueries({ queryKey: ["accounts"] }); + setTimeout(() => { + handleClose(); + }, 3000); + } catch (error) { + console.error("Failed to create account:", error); + } }; const onAccountChange = (e: React.ChangeEvent) => { @@ -70,37 +73,77 @@ function AddAccount({ open, setOpen }: { open: boolean; setOpen: React.Dispatch< }); }; + const getErrorMessage = (error: any): string => { + if (!error) return ""; + const message = error.message || ""; + const invalidRequestMatch = message.match(/Invalid request:\s*(.+)/); + if (invalidRequestMatch) { + return invalidRequestMatch[1]; + } + return message || "Failed to create account. Please try again."; + }; + return ( - Add Account - -
- - + {!isSuccess ? ( + + + + + + + + + + + ) : ( + + - - - - -
+ + + + Account created successfully! + + + )}
); } diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/ClaimBurn.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/ClaimBurn.tsx index 3d21527744..ffccf4130b 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/ClaimBurn.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/ClaimBurn.tsx @@ -26,7 +26,6 @@ import Button from "@mui/material/Button"; import TextField from "@mui/material/TextField"; import Dialog from "@mui/material/Dialog"; import DialogContent from "@mui/material/DialogContent"; -import DialogTitle from "@mui/material/DialogTitle"; import FormControl from "@mui/material/FormControl"; import InputLabel from "@mui/material/InputLabel"; import Select, { SelectChangeEvent } from "@mui/material/Select/Select"; @@ -36,8 +35,8 @@ import { useAccountsList } from "@api/hooks/useAccounts"; import { useTheme } from "@mui/material/styles"; import { accountsClaimBurn, transactionsWaitResult } from "@utils/json_rpc"; import useAccountStore from "@store/accountStore"; -import { useKeysList } from "@api/hooks/useKeys"; -import type { AccountInfo, ComponentAddress } from "@tari-project/typescript-bindings"; +import type { ComponentAddress, AccountInfo } from "@tari-project/typescript-bindings"; +import PopupTitle from "@/components/PopupTitle"; type FormState = { account: ComponentAddress; @@ -139,7 +138,7 @@ export default function ClaimBurn() { Claim Burn - Claim Burn +
@@ -153,7 +152,7 @@ export default function ClaimBurn() { style={{ flexGrow: 1, minWidth: "200px" }} disabled={claimBurnFormState.disabled} > - {accountsList?.accounts?.map((account, i) => ( + {accountsList?.accounts?.map((account: AccountInfo, i: number) => (
diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/ClaimFees.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/ClaimFees.tsx index 27a6200ea6..b7e23cdadf 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/ClaimFees.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/ClaimFees.tsx @@ -26,7 +26,6 @@ import Button from "@mui/material/Button"; import TextField from "@mui/material/TextField"; import Dialog from "@mui/material/Dialog"; import DialogContent from "@mui/material/DialogContent"; -import DialogTitle from "@mui/material/DialogTitle"; import Box from "@mui/material/Box"; import { useAccountsList } from "@api/hooks/useAccounts"; import { useTheme } from "@mui/material/styles"; @@ -42,8 +41,7 @@ import { substateIdToString, TransactionResult, } from "@tari-project/typescript-bindings"; -import { FileContent } from "use-file-picker/types"; -import { toHexString } from "@utils/helpers"; +import PopupTitle from "@/components/PopupTitle"; interface FormState { account: string | null; @@ -214,7 +212,7 @@ export default function ClaimFees() { Claim Fees - Claim Fees + diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/MyAssets.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/MyAssets.tsx index d0d1bf92ed..909922d5f6 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/MyAssets.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/MyAssets.tsx @@ -24,6 +24,7 @@ import Grid from "@mui/material/Grid"; import Box from "@mui/material/Box"; import Divider from "@mui/material/Divider"; import Typography from "@mui/material/Typography"; +import IconButton from "@mui/material/IconButton"; import { useTheme } from "@mui/material/styles"; import { InnerHeading, StyledPaper } from "@components/StyledComponents"; import { refreshAccountsBalances } from "@api/hooks/useAccounts"; @@ -35,8 +36,8 @@ import ActionMenu from "./ActionMenu"; import Assets from "./Assets"; import SelectAccount from "./SelectAccount"; import { substateIdToString } from "@tari-project/typescript-bindings"; -import { Button } from "@mui/material"; import { Refresh } from "@mui/icons-material"; +import queryClient from "@api/queryClient"; function MyAssets() { const theme = useTheme(); @@ -49,6 +50,12 @@ function MyAssets() { const refreshBalances = refreshAccountsBalances(substateIdToString(account.address)); const handleRefreshClicked = () => { refreshBalances.mutate(); + queryClient.invalidateQueries({ + predicate: (query) => { + const key = query.queryKey[0]; + return typeof key === "string" && (key === "nfts" || key === "list_nfts" || key === "nfts_list"); + }, + }); }; return ( @@ -112,14 +119,18 @@ function MyAssets() { Assets - + diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/PublishTemplate.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/PublishTemplate.tsx index 73d9b2a5e4..228d2fba29 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/PublishTemplate.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/PublishTemplate.tsx @@ -26,7 +26,6 @@ import Button from "@mui/material/Button"; import TextField from "@mui/material/TextField"; import Dialog from "@mui/material/Dialog"; import DialogContent from "@mui/material/DialogContent"; -import DialogTitle from "@mui/material/DialogTitle"; import Box from "@mui/material/Box"; import { useAccountsList } from "@api/hooks/useAccounts"; import { useTheme } from "@mui/material/styles"; @@ -35,12 +34,19 @@ import Select from "@mui/material/Select"; import { SelectChangeEvent } from "@mui/material/Select/Select"; import MenuItem from "@mui/material/MenuItem"; import { useFilePicker } from "use-file-picker"; -import { ResourceAddress, ResourceType, substateIdToString } from "@tari-project/typescript-bindings"; +import { + ResourceAddress, + ResourceType, + substateIdToString, + PublishTemplateResponse, + AccountInfo, +} from "@tari-project/typescript-bindings"; import InputLabel from "@mui/material/InputLabel"; import { usePublishTemplate } from "@api/hooks/useTransactions"; import { FileAmountLimitValidator, FileSizeValidator, FileTypeValidator } from "use-file-picker/validators"; import { FileContent } from "use-file-picker/types"; import { base64FromArrayBuffer } from "@utils/helpers"; +import PopupTitle from "@/components/PopupTitle"; export default function PublishTemplate() { const [open, setOpen] = useState(false); @@ -135,7 +141,7 @@ function PublishTemplateDialog(props: DialogProps) { detect_inputs: true, dry_run: isDryRun, }) - .then((resp) => { + .then((resp: PublishTemplateResponse) => { if (isDryRun) { setFormState({ ...formState, maxFee: resp.dry_run_fee! }); } else { @@ -144,7 +150,7 @@ function PublishTemplateDialog(props: DialogProps) { setPopup({ title: "Publish template transaction submitted", error: false }); } }) - .catch((e) => { + .catch((e: Error) => { setPopup({ title: "Publish failed", error: true, message: e.message }); }) .finally(() => { @@ -198,7 +204,7 @@ function PublishTemplateDialog(props: DialogProps) { }); useEffect(() => { - let account = accounts?.find((a) => a.account.is_default)?.account.name || null; + let account = accounts?.find((a: AccountInfo) => a.account.is_default)?.account.name || null; if (account) { setFormState({ ...INITIAL_VALUES, account }); setValidity({ ...validity, account: true }); @@ -207,7 +213,7 @@ function PublishTemplateDialog(props: DialogProps) { return ( - Publish Template + {accounts && ( @@ -218,11 +224,11 @@ function PublishTemplateDialog(props: DialogProps) { name="account" disabled={disabled} displayEmpty - value={formState.account || accounts.find((a) => a.account.is_default) || ""} + value={formState.account || accounts.find((a: AccountInfo) => a.account.is_default) || ""} onChange={setSelectFormValue} variant="outlined" > - {accounts.map((account, i) => ( + {accounts.map((account: AccountInfo, i: number) => ( {account.account.name} {account.account.is_default ? "(default)" : ""} diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/SelectAccount.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/SelectAccount.tsx index 4c2782625b..8c160211db 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/SelectAccount.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/SelectAccount.tsx @@ -68,8 +68,10 @@ function SelectAccount() { labelId="account-select-label" id="account-select" value={ - dataAccountsList?.accounts.some((info: AccountInfo) => info.account.address === account?.address) - ? substateIdToString(account!.address) + account && dataAccountsList?.accounts.some((info: AccountInfo) => info.account.address === account?.address) + ? substateIdToString(account.address) + : dataAccountsList?.accounts.length + ? substateIdToString(dataAccountsList.accounts[0].account.address) : "addAccount" } label="Account" diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/NFTList.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/NFTList.tsx index 4286bf1738..f3ca8fc926 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/NFTList.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/NFTList.tsx @@ -31,6 +31,8 @@ import { TableHead, TablePagination, TableRow, + Typography, + Box, } from "@mui/material"; import type { ListNftsResponse, NonFungibleToken } from "@tari-project/typescript-bindings"; import React, { useState } from "react"; @@ -105,7 +107,25 @@ export default function NFTList(props: NftListProps) { - {viewMode === "grid" ? ( + {displayedNfts.length === 0 && !nftsListIsFetching ? ( + + + No NFTs found + + + You don't have any NFTs in this account yet. Try claiming some testnet NFTs to get started. + + + ) : viewMode === "grid" ? ( {displayedNfts.map((nft: NonFungibleToken, index: number) => ( diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/components/ClaimNftsButton.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/components/ClaimNftsButton.tsx index 600203b4a4..e28226119b 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/components/ClaimNftsButton.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/components/ClaimNftsButton.tsx @@ -25,10 +25,12 @@ import { useMintTestnetFaucetNfts } from "@api/hooks/useAccounts"; import useAccountStore from "@store/accountStore"; import { substateIdToString } from "@tari-project/typescript-bindings"; import queryClient from "@api/queryClient"; +import { useErrorNotification } from "../../../../contexts/ErrorNotificationContext"; function ClaimNftsButton() { - const { mutate: claimTestnetFaucetNfts } = useMintTestnetFaucetNfts(); + const { mutate: claimTestnetFaucetNfts, isPending } = useMintTestnetFaucetNfts(); const account = useAccountStore((state) => state.account); + const { showError, showSuccess } = useErrorNotification(); if (!account) { return <>; @@ -45,27 +47,34 @@ function ClaimNftsButton() { maxFee: 2000, }, { - onSuccess: (resp) => { + onSuccess: (resp: any) => { console.log(resp); + showSuccess("Successfully claimed NFTs!"); // Invalidate NFT queries to refresh the list - queryClient.invalidateQueries({ + queryClient.invalidateQueries({ predicate: (query) => { const key = query.queryKey[0]; - return typeof key === "string" && ( - key === "nfts" || - key === "list_nfts" || - key.startsWith("nfts_list_") - ); - } + return typeof key === "string" && (key === "nfts" || key === "list_nfts" || key === "nfts_list"); + }, }); }, + onError: (error: any) => { + console.error("Error claiming NFTs:", error); + // Show user-friendly error message + const errorMessage = error?.message || "Failed to claim NFTs. Please ensure you have sufficient funds to pay for transaction fees."; + showError(errorMessage); + }, }, ); }; return ( - ); } diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/components/SendNft.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/components/SendNft.tsx index ee78c06599..1c9ede534f 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/components/SendNft.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/components/SendNft.tsx @@ -24,8 +24,6 @@ import { FormEvent, useEffect, useState, useMemo } 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 { Stepper, Step, StepLabel } from "@mui/material"; import { SelectChangeEvent } from "@mui/material/Select/Select"; import useAccountStore from "@store/accountStore"; import type { @@ -42,6 +40,7 @@ import { useNftTransferStore } from "@store/nftTransferStore"; import FormStep from "../steps/FormStep"; import ConfirmationStep from "../steps/ConfirmationStep"; import ResultStep from "../steps/ResultStep"; +import PopupTitle from "@/components/PopupTitle"; interface TransferNftProps { account?: Account; @@ -132,7 +131,7 @@ export function TransferNftDialog(props: TransferNftDialogProps) { const [isEstimatingFee, setLocalIsEstimatingFee] = useState(false); // Memoize account selectors to prevent infinite re-renders - now nullable - const sourceAccount = useMemo(() => account ? getAccountSelector(account) : null, [account]); + const sourceAccount = useMemo(() => (account ? getAccountSelector(account) : null), [account]); const feePayerAccount = useMemo(() => { if (!sourceAccount) return null; return transferFormState.payerAccount ? { ComponentAddress: transferFormState.payerAccount } : sourceAccount; @@ -392,19 +391,10 @@ export function TransferNftDialog(props: TransferNftDialogProps) { return ( - - Transfer NFT - - {steps.map((label) => ( - - {label} - - ))} - - + {!account ? ( -
+

Please select an account first to transfer NFTs.

) : ( diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/Tokens.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/Tokens.tsx index d628fb2a94..470cfbc050 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/Tokens.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/Tokens.tsx @@ -26,6 +26,7 @@ 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 { Typography, Box, Stack } from "@mui/material"; import { useState } from "react"; import FetchStatusCheck from "@components/FetchStatusCheck"; import { DataTableCell } from "@components/StyledComponents"; @@ -34,6 +35,7 @@ import useAccountStore from "@store/accountStore"; import { bigintToDecimalString, shortenSubstateId, substateIdToString } from "@utils/helpers"; import { Button } from "@mui/material"; import { SendMoneyDialog } from "./components/SendMoney"; +import ClaimCoinsButton from "./components/ClaimCoinsButton"; import { ResourceAddress, ResourceType, @@ -130,6 +132,9 @@ function Tokens({ account }: { account: Account }) { const handleSendResourceClicked = (address: ResourceAddress, resource_type: ResourceType) => { setResourceToSend({ address, resource_type }); }; + + const hasBalances = balancesData?.balances && balancesData.balances.length > 0; + return ( <> b.resource_address === resourceToSend?.address)?.token_symbol || "" + balancesData?.balances.find((b: BalanceEntry) => 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, - ) => ( - - ), - )} - -
-
+ + + + + + {balancesData && !hasBalances ? ( + + + No tokens found + + + This account doesn't have any tokens yet. Try claiming some testnet coins to get started. + + + ) : ( + + + + + Vault + Resource + Revealed Balance + Confidential Balance + + + + + {balancesData?.balances.map( + ( + { + resource_address, + balance, + resource_type, + confidential_balance, + token_symbol, + vault_address, + divisibility, + }: BalanceEntry, + i: number, + ) => ( + void + } + /> + ), + )} + +
+
+ )} +
); diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/ClaimCoinsButton.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/ClaimCoinsButton.tsx new file mode 100644 index 0000000000..fa3f5b3b1c --- /dev/null +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/ClaimCoinsButton.tsx @@ -0,0 +1,79 @@ +// 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 Button from "@mui/material/Button"; +import { useAccountsCreateFreeTestCoins } from "@api/hooks/useAccounts"; +import useAccountStore from "@store/accountStore"; +import { substateIdToString, AccountsCreateFreeTestCoinsResponse } from "@tari-project/typescript-bindings"; +import queryClient from "@api/queryClient"; +import { useErrorNotification } from "@/contexts/ErrorNotificationContext"; + +function ClaimCoinsButton() { + const { mutate: claimTestnetFaucetFunds, isPending } = useAccountsCreateFreeTestCoins(); + const account = useAccountStore((state) => state.account); + const setAccount = useAccountStore((state) => state.setAccount); + const setPublicKey = useAccountStore((state) => state.setPublicKey); + const { showError, showSuccess } = useErrorNotification(); + + if (!account) { + return <>; + } + + const onClaimFreeCoins = () => { + claimTestnetFaucetFunds( + { + account: { ComponentAddress: substateIdToString(account.address) }, + amount: 1_000_000_000, + fee: 1000, + }, + { + onSuccess: (resp: AccountsCreateFreeTestCoinsResponse) => { + setAccount(resp.account); + setPublicKey(resp.public_key); + showSuccess("Successfully claimed testnet coins!"); + queryClient.invalidateQueries({ + predicate: (query) => { + const key = query.queryKey[0]; + return ( + typeof key === "string" && + (key === "balances" || key === "accounts_balances" || key.startsWith("accounts_get_balances")) + ); + }, + }); + }, + onError: (error: any) => { + console.error("Error claiming coins:", error); + const errorMessage = error?.message || "Failed to claim testnet coins. Please try again."; + showError(errorMessage); + }, + }, + ); + }; + + return ( + + ); +} + +export default ClaimCoinsButton; diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx index ef83c6fef2..c888d573af 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx @@ -24,9 +24,6 @@ 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"; @@ -42,14 +39,14 @@ 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"; +import PopupTitle from "@/components/PopupTitle"; 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); + const xtrBalanceEntry = data?.balances?.find((b: BalanceEntry) => b.resource_address === XTR); return ( <> @@ -372,18 +369,8 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) { return ( - - - Send Tari - - - - - - - - {renderStepContent()} - + + {renderStepContent()} ); } 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 1f16c3f763..28fd75a58f 100644 --- a/applications/tari_walletd/web_ui/src/routes/FlowEditor/FlowEditor.tsx +++ b/applications/tari_walletd/web_ui/src/routes/FlowEditor/FlowEditor.tsx @@ -25,7 +25,7 @@ import PageHeading from "@components/PageHeading"; import Grid from "@mui/material/Grid"; import { StyledPaper } from "@components/StyledComponents"; import { QueryBuilder, TemplateReader, useStore } from "@tari-project/tari-extension-query-builder"; -import useThemeStore from "../../store/themeStore"; +import useThemeStore from "@store/themeStore"; import { useCallback, useEffect, useRef } from "react"; import { Button, @@ -52,15 +52,15 @@ 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 { useTemplateGet } from "../../api/hooks/useTemplate"; +import { useTemplateGet } from "@api/hooks/useTemplate"; import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline"; import FunctionsIcon from "@mui/icons-material/Functions"; import SettingsEthernetIcon from "@mui/icons-material/SettingsEthernet"; import { GeneratedCodeType, TariNetwork, TransactionProps } from "@tari-project/tari-extension-common"; -import { substateIdToString } from "../../utils/helpers"; +import { substateIdToString } from "@utils/helpers"; import CloseIcon from "@mui/icons-material/Close"; import { Highlight } from "prism-react-renderer"; -import useFlowEditorStore, { INITIAL_FLOW_STATE } from "../../store/flowEditorStore"; +import useFlowEditorStore, { INITIAL_FLOW_STATE } from "@store/flowEditorStore"; import { shortenString, UnsignedTransactionV1, @@ -70,7 +70,7 @@ import { TESTNET_XTR_FAUCET_ADDRESS, } from "@tari-project/typescript-bindings"; import { settingsGet, submitTransactionDryRun, transactionsSubmit, transactionsWaitResult } from "../../utils/json_rpc"; -import { useAccountsList } from "../../api/hooks/useAccounts"; +import { useAccountsList } from "@api/hooks/useAccounts"; import CopyAddress from "@components/CopyAddress"; const KNOWN_TEMPLATES = [ @@ -161,7 +161,7 @@ function FlowEditor() { useEffect(() => { if (dataAccountsList?.accounts && dataAccountsList.accounts.length > 0) { - const defaultAcc = dataAccountsList.accounts.find((acc) => acc.account.is_default); + const defaultAcc = dataAccountsList.accounts.find((acc: any) => acc.account.is_default); setAccount(defaultAcc || dataAccountsList.accounts[0]); } }, [dataAccountsList]); @@ -178,7 +178,7 @@ function FlowEditor() { const onAccountChange = (e: SelectChangeEvent) => { const selected = dataAccountsList?.accounts.find( - (acc) => substateIdToString(acc.account.address) === e.target.value, + (acc: any) => substateIdToString(acc.account.address) === e.target.value, ); setAccount(selected); }; @@ -322,7 +322,7 @@ function FlowEditor() { value={account ? substateIdToString(account.account.address) : ""} onChange={onAccountChange} > - {dataAccountsList?.accounts?.map((acc) => ( + {dataAccountsList?.accounts?.map((acc: any) => ( {acc.account.name || substateIdToString(acc.account.address)} @@ -415,7 +415,7 @@ function FlowEditor() { )} - {methods.map((m, i) => ( + {methods.map((m: any, i: number) => ( ([]); @@ -150,15 +162,14 @@ export default function TransactionDetails() { Total Fees - {feeReceipt?.total_fees_paid.toString() || 0} + {feeReceipt ? formatCurrency(feeReceipt.total_fees_paid) : "0"} {feeReceipt?.total_fee_overcharge ? ( <> {" "} - ({feeReceipt.total_fee_overcharge} overcharge{" "} - + ({formatCurrency(feeReceipt.total_fee_overcharge)} overcharge{" "} + + + ) ) : ( diff --git a/applications/tari_walletd/web_ui/src/routes/Transactions/Transactions.tsx b/applications/tari_walletd/web_ui/src/routes/Transactions/Transactions.tsx index 58393a874a..bee340de55 100644 --- a/applications/tari_walletd/web_ui/src/routes/Transactions/Transactions.tsx +++ b/applications/tari_walletd/web_ui/src/routes/Transactions/Transactions.tsx @@ -40,7 +40,7 @@ import FetchStatusCheck from "@components/FetchStatusCheck"; import StatusChip from "@components/StatusChip"; import { DataTableCell } from "@components/StyledComponents"; import { useGetAllTransactions } from "@api/hooks/useTransactions"; -import { emptyRows, handleChangePage, handleChangeRowsPerPage } from "@utils/helpers"; +import { emptyRows, handleChangePage, handleChangeRowsPerPage, formatCurrency } from "@utils/helpers"; import { Account, WalletTransaction } from "@tari-project/typescript-bindings"; import TimeChip from "./TimeChip"; @@ -101,7 +101,9 @@ export default function Transactions({ account }: { account: Account; ownerPubli - {fee_receipt?.total_fees_paid.toString() || "--"} + + {fee_receipt?.total_fees_paid ? formatCurrency(fee_receipt.total_fees_paid) : "--"} + { const publicKeyCredentialRequestOptions: PublicKeyCredentialRequestOptions = { 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 91e0c68161..3c053ac91e 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 @@ -11,7 +11,7 @@ import { FormEvent, useState } from "react"; import { webauthnFinishRegistration, webauthnStartRegistration } from "../../../utils/json_rpc"; import { Buffer } from "buffer"; import Loading from "@components/Loading"; -import useAuthStore from "../../../store/authStore"; +import useAuthStore from "@store/authStore"; const WEBAUTHN_RP_ID = import.meta.env.VITE_DAEMON_WEBAUTHN_RP_ID || window.location.hostname; 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 8baec93fb3..6d7c543217 100644 --- a/applications/tari_walletd/web_ui/src/routes/WebauthnRegistration/Webauthn.tsx +++ b/applications/tari_walletd/web_ui/src/routes/WebauthnRegistration/Webauthn.tsx @@ -3,10 +3,10 @@ import { useEffect, useState } from "react"; import WebauthnLogin from "./Components/Login"; -import { useWebauthnAlreadyRegistered } from "../../api/hooks/useWebauthn"; +import { useWebauthnAlreadyRegistered } from "@api/hooks/useWebauthn"; import Loading from "@components/Loading"; import WebauthnRegistration from "./Components/Registration"; -import useAuthStore from "../../store/authStore"; +import useAuthStore from "@store/authStore"; import { useNavigate, useSearchParams } from "react-router-dom"; function Webauthn() { diff --git a/applications/tari_walletd/web_ui/src/api/helpers/types.ts b/applications/tari_walletd/web_ui/src/services/api/helpers/types.ts similarity index 100% rename from applications/tari_walletd/web_ui/src/api/helpers/types.ts rename to applications/tari_walletd/web_ui/src/services/api/helpers/types.ts diff --git a/applications/tari_walletd/web_ui/src/api/hooks/useAccounts.ts b/applications/tari_walletd/web_ui/src/services/api/hooks/useAccounts.ts similarity index 100% rename from applications/tari_walletd/web_ui/src/api/hooks/useAccounts.ts rename to applications/tari_walletd/web_ui/src/services/api/hooks/useAccounts.ts diff --git a/applications/tari_walletd/web_ui/src/api/hooks/useAuth.tsx b/applications/tari_walletd/web_ui/src/services/api/hooks/useAuth.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/api/hooks/useAuth.tsx rename to applications/tari_walletd/web_ui/src/services/api/hooks/useAuth.tsx diff --git a/applications/tari_walletd/web_ui/src/api/hooks/useKeys.tsx b/applications/tari_walletd/web_ui/src/services/api/hooks/useKeys.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/api/hooks/useKeys.tsx rename to applications/tari_walletd/web_ui/src/services/api/hooks/useKeys.tsx diff --git a/applications/tari_walletd/web_ui/src/api/hooks/useNfts.tsx b/applications/tari_walletd/web_ui/src/services/api/hooks/useNfts.tsx similarity index 97% rename from applications/tari_walletd/web_ui/src/api/hooks/useNfts.tsx rename to applications/tari_walletd/web_ui/src/services/api/hooks/useNfts.tsx index 7e7d9f68a5..aee22c17fd 100644 --- a/applications/tari_walletd/web_ui/src/api/hooks/useNfts.tsx +++ b/applications/tari_walletd/web_ui/src/services/api/hooks/useNfts.tsx @@ -60,7 +60,7 @@ export const useNftsTransfer = (request: TransferNftRequest) => { return typeof key === "string" && ( key === "nfts" || key === "list_nfts" || - key.startsWith("nfts_list_") + key === "nfts_list" ); } }); diff --git a/applications/tari_walletd/web_ui/src/api/hooks/useTemplate.tsx b/applications/tari_walletd/web_ui/src/services/api/hooks/useTemplate.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/api/hooks/useTemplate.tsx rename to applications/tari_walletd/web_ui/src/services/api/hooks/useTemplate.tsx diff --git a/applications/tari_walletd/web_ui/src/api/hooks/useTemplatesAuthored.tsx b/applications/tari_walletd/web_ui/src/services/api/hooks/useTemplatesAuthored.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/api/hooks/useTemplatesAuthored.tsx rename to applications/tari_walletd/web_ui/src/services/api/hooks/useTemplatesAuthored.tsx diff --git a/applications/tari_walletd/web_ui/src/api/hooks/useTokens.tsx b/applications/tari_walletd/web_ui/src/services/api/hooks/useTokens.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/api/hooks/useTokens.tsx rename to applications/tari_walletd/web_ui/src/services/api/hooks/useTokens.tsx diff --git a/applications/tari_walletd/web_ui/src/api/hooks/useTransactions.tsx b/applications/tari_walletd/web_ui/src/services/api/hooks/useTransactions.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/api/hooks/useTransactions.tsx rename to applications/tari_walletd/web_ui/src/services/api/hooks/useTransactions.tsx diff --git a/applications/tari_walletd/web_ui/src/services/api/hooks/useWalletInfo.ts b/applications/tari_walletd/web_ui/src/services/api/hooks/useWalletInfo.ts new file mode 100644 index 0000000000..27a9befc3c --- /dev/null +++ b/applications/tari_walletd/web_ui/src/services/api/hooks/useWalletInfo.ts @@ -0,0 +1,31 @@ +// 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 { useQuery } from "@tanstack/react-query"; +import { walletGetInfo } from "@utils/json_rpc"; + +export const useWalletInfo = () => { + return useQuery({ + queryKey: ["wallet_info"], + queryFn: () => walletGetInfo(), + }); +}; diff --git a/applications/tari_walletd/web_ui/src/api/hooks/useWebauthn.tsx b/applications/tari_walletd/web_ui/src/services/api/hooks/useWebauthn.tsx similarity index 100% rename from applications/tari_walletd/web_ui/src/api/hooks/useWebauthn.tsx rename to applications/tari_walletd/web_ui/src/services/api/hooks/useWebauthn.tsx diff --git a/applications/tari_walletd/web_ui/src/api/queryClient.ts b/applications/tari_walletd/web_ui/src/services/api/queryClient.ts similarity index 100% rename from applications/tari_walletd/web_ui/src/api/queryClient.ts rename to applications/tari_walletd/web_ui/src/services/api/queryClient.ts diff --git a/applications/tari_walletd/web_ui/src/store/accountStore.ts b/applications/tari_walletd/web_ui/src/services/store/accountStore.ts similarity index 100% rename from applications/tari_walletd/web_ui/src/store/accountStore.ts rename to applications/tari_walletd/web_ui/src/services/store/accountStore.ts diff --git a/applications/tari_walletd/web_ui/src/store/authStore.ts b/applications/tari_walletd/web_ui/src/services/store/authStore.ts similarity index 100% rename from applications/tari_walletd/web_ui/src/store/authStore.ts rename to applications/tari_walletd/web_ui/src/services/store/authStore.ts diff --git a/applications/tari_walletd/web_ui/src/services/store/currencyStore.ts b/applications/tari_walletd/web_ui/src/services/store/currencyStore.ts new file mode 100644 index 0000000000..5678891daa --- /dev/null +++ b/applications/tari_walletd/web_ui/src/services/store/currencyStore.ts @@ -0,0 +1,35 @@ +// 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 { create } from "zustand"; + +interface CurrencyStore { + currencySymbol: string; + setCurrencySymbol: (symbol: string) => void; +} + +const useCurrencyStore = create((set) => ({ + currencySymbol: "XTR", + setCurrencySymbol: (symbol) => set({ currencySymbol: symbol }), +})); + +export default useCurrencyStore; diff --git a/applications/tari_walletd/web_ui/src/store/flowEditorStore.ts b/applications/tari_walletd/web_ui/src/services/store/flowEditorStore.ts similarity index 100% rename from applications/tari_walletd/web_ui/src/store/flowEditorStore.ts rename to applications/tari_walletd/web_ui/src/services/store/flowEditorStore.ts diff --git a/applications/tari_walletd/web_ui/src/services/store/hooks/useCurrencySync.ts b/applications/tari_walletd/web_ui/src/services/store/hooks/useCurrencySync.ts new file mode 100644 index 0000000000..bdbbe5fb75 --- /dev/null +++ b/applications/tari_walletd/web_ui/src/services/store/hooks/useCurrencySync.ts @@ -0,0 +1,43 @@ +// 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 { useEffect } from "react"; +import { useAccountsGetBalances, useAccountsGetDefault } from "@api/hooks/useAccounts"; +import { substateIdToString } from "@utils/helpers"; +import useCurrencyStore from "@store/currencyStore"; + +export const useCurrencySync = () => { + const { data: defaultAccount } = useAccountsGetDefault(); + const { data: balancesData, isLoading } = useAccountsGetBalances( + defaultAccount ? substateIdToString(defaultAccount.account.address) : "" + ); + const { setCurrencySymbol } = useCurrencyStore(); + + useEffect(() => { + if (defaultAccount && balancesData?.balances && balancesData.balances.length > 0) { + const mainBalance = balancesData.balances[0]; + if (mainBalance.token_symbol) { + setCurrencySymbol(mainBalance.token_symbol); + } + } + }, [defaultAccount, balancesData?.balances, isLoading, setCurrencySymbol]); +}; diff --git a/applications/tari_walletd/web_ui/src/store/manifestStore.ts b/applications/tari_walletd/web_ui/src/services/store/manifestStore.ts similarity index 100% rename from applications/tari_walletd/web_ui/src/store/manifestStore.ts rename to applications/tari_walletd/web_ui/src/services/store/manifestStore.ts diff --git a/applications/tari_walletd/web_ui/src/store/nftTransferStore.ts b/applications/tari_walletd/web_ui/src/services/store/nftTransferStore.ts similarity index 100% rename from applications/tari_walletd/web_ui/src/store/nftTransferStore.ts rename to applications/tari_walletd/web_ui/src/services/store/nftTransferStore.ts diff --git a/applications/tari_walletd/web_ui/src/store/themeStore.ts b/applications/tari_walletd/web_ui/src/services/store/themeStore.ts similarity index 100% rename from applications/tari_walletd/web_ui/src/store/themeStore.ts rename to applications/tari_walletd/web_ui/src/services/store/themeStore.ts diff --git a/applications/tari_walletd/web_ui/src/utils/helpers.tsx b/applications/tari_walletd/web_ui/src/utils/helpers.tsx index cef04c8b30..fab6305ba0 100644 --- a/applications/tari_walletd/web_ui/src/utils/helpers.tsx +++ b/applications/tari_walletd/web_ui/src/utils/helpers.tsx @@ -23,6 +23,7 @@ import { ChangeEvent } from "react"; import type { Amount, SubstateId, NonFungibleId } from "@tari-project/typescript-bindings"; import { CURRENCY } from "@utils/constants"; +import useCurrencyStore from "@store/currencyStore"; export const renderJson = (json: any) => { if (Array.isArray(json)) { @@ -251,35 +252,35 @@ export function bigintToDecimalString(int: bigint | Amount, decimalPlaces: numbe } export const formatCurrency = (amount: number | bigint): string => { + const currencySymbol = useCurrencyStore.getState().currencySymbol; + if (typeof amount === "bigint") { - // Handle bigint: divide by divisor to get integer and remainder for fractional part const divisor = BigInt(CURRENCY.DIVISOR); const integerPart = amount / divisor; const remainder = amount % divisor; - // Convert remainder to fractional string padded to CURRENCY.DECIMALS const fractionalPart = remainder.toString().padStart(CURRENCY.DECIMALS, "0"); - return `${Number(integerPart).toLocaleString("en-US")}.${fractionalPart} ${CURRENCY.SYMBOL}`; + return `${Number(integerPart).toLocaleString("en-US")}.${fractionalPart} ${currencySymbol}`; } else if (typeof amount === "number") { - // Handle number: guard against NaN and use existing toFixed logic if (isNaN(amount)) { - return `0 ${CURRENCY.SYMBOL}`; + return `0 ${currencySymbol}`; } const convertedAmount = amount / CURRENCY.DIVISOR; - return `${convertedAmount.toLocaleString("en-US", { minimumFractionDigits: 0, maximumFractionDigits: CURRENCY.DECIMALS })} ${CURRENCY.SYMBOL}`; + return `${convertedAmount.toLocaleString("en-US", { minimumFractionDigits: 0, maximumFractionDigits: CURRENCY.DECIMALS })} ${currencySymbol}`; } else { - // Handle invalid types - return `0 ${CURRENCY.SYMBOL}`; + return `0 ${currencySymbol}`; } }; // Helper function for formatting amounts that are already in display units (XTR) export const formatDisplayCurrency = (amount: number): string => { + const currencySymbol = useCurrencyStore.getState().currencySymbol; + if (isNaN(amount)) { - return `0 ${CURRENCY.SYMBOL}`; + return `0 ${currencySymbol}`; } - return `${amount.toLocaleString("en-US", { minimumFractionDigits: 0, maximumFractionDigits: CURRENCY.DECIMALS })} ${CURRENCY.SYMBOL}`; + return `${amount.toLocaleString("en-US", { minimumFractionDigits: 0, maximumFractionDigits: CURRENCY.DECIMALS })} ${currencySymbol}`; }; export function validateHash(hash: string): boolean { diff --git a/applications/tari_walletd/web_ui/tsconfig.json b/applications/tari_walletd/web_ui/tsconfig.json index 710c4257bf..196e012ae4 100644 --- a/applications/tari_walletd/web_ui/tsconfig.json +++ b/applications/tari_walletd/web_ui/tsconfig.json @@ -30,8 +30,8 @@ "@utils/*": ["./src/utils/*"], "@assets/*": ["./src/assets/*"], "@hooks/*": ["./src/hooks/*"], - "@api/*": ["./src/api/*"], - "@store/*": ["./src/store/*"], + "@api/*": ["./src/services/api/*"], + "@store/*": ["./src/services/store/*"], "@theme/*": ["./src/theme/*"] } }, diff --git a/applications/tari_walletd/web_ui/vite.config.ts b/applications/tari_walletd/web_ui/vite.config.ts index 0947ee6a5d..8307d88729 100644 --- a/applications/tari_walletd/web_ui/vite.config.ts +++ b/applications/tari_walletd/web_ui/vite.config.ts @@ -35,8 +35,8 @@ export default defineConfig({ "@utils": path.resolve(__dirname, "./src/utils"), "@assets": path.resolve(__dirname, "./src/assets"), "@hooks": path.resolve(__dirname, "./src/hooks"), - "@api": path.resolve(__dirname, "./src/api"), - "@store": path.resolve(__dirname, "./src/store"), + "@api": path.resolve(__dirname, "./src/services/api"), + "@store": path.resolve(__dirname, "./src/services/store"), "@theme": path.resolve(__dirname, "./src/theme"), }, },