Skip to content

fix(wallet/webui): honour resource divisibility in UI - #1582

Merged
sdbondi merged 2 commits into
tari-project:developmentfrom
sdbondi:wallet-minor-ui-fixes
Sep 24, 2025
Merged

fix(wallet/webui): honour resource divisibility in UI#1582
sdbondi merged 2 commits into
tari-project:developmentfrom
sdbondi:wallet-minor-ui-fixes

Conversation

@sdbondi

@sdbondi sdbondi commented Sep 24, 2025

Copy link
Copy Markdown
Member

Description

fix(wallet/webui): honour resource divisibility in UI
Display NFT metadata (originalOwner is only metadata that was added for the testnet faucet NFT)
Remove "Active" chip from NFTs (could be confusing)

Motivation and Context

Use correct divisibility and token symbol when formatting currency

How Has This Been Tested?

What process can a PR reviewer use to test or verify this change?

Breaking Changes

  • None
  • Requires data directory to be deleted
  • Other - Please specify

Summary by CodeRabbit

  • New Features

    • Display NFT data as key/value details within NFT cards and rows.
  • Improvements

    • Token list hides assets with zero balance.
    • Amount formatting and placeholders now respect each token’s divisibility.
    • Balance and “Insufficient funds” messages include token symbol and precise formatting.
    • Send dialog only appears when initiating a send.
  • Bug Fixes

    • Burn status is shown only for burnt NFTs.
    • Prevents send/fee actions when no balance is available for the selected token, avoiding inconsistent transfers.

@coderabbitai

coderabbitai Bot commented Sep 24, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Render NFT metadata as generic key/value pairs and simplify burn status rendering; tighten token send flow with conditional dialog mounting, exact balance lookups, unified divisibility-based amount handling and formatting; change currency formatter signature; add ordering to stealth outputs query and a minor formatting edit in the Stealth UTXO list.

Changes

Cohort / File(s) Summary of changes
NFT UI refactor
applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/components/NftParts.tsx
Added internal NftData to render nft.data as key/value pairs; removed inline original_owner extraction/display; show Burnt chip only when nft.is_burnt is true; added Fragment import.
Tokens page behavior
applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/Tokens.tsx
Render SendMoneyDialog only when resourceToSend exists (otherwise null); filter balances to entries with balance > 0 or confidential_balance > 0; pass handleSendResourceClicked directly.
Send flow logic
applications/.../Tokens/components/SendMoney.tsx
Require exact balanceEntry match for resource_address; add early guards with console warnings and return null when missing; defer/standardize amount calculation using balanceEntry.divisibility; construct transfer payload only after validation; remove optional divisibility fallbacks.
Form step formatting
applications/.../Tokens/steps/FormStep.tsx
Always use minimumFractionDigits = 0; maximumFractionDigits set to divisibility; placeholder reflects divisibility (e.g., 0. + zeros); helper texts use formatDisplayCurrency(divisibility, token_symbol).
Currency formatter API
applications/tari_walletd/web_ui/src/utils/helpers.tsx
formatDisplayCurrency signature changed to (amount, divisibility, currencySymbol); formatting uses provided divisibility and currencySymbol; preserved NaN handling.
Stealth UTXO list (minor)
applications/tari_walletd/web_ui/src/routes/StealthUtxoList/StealthUtxoList.tsx
Minor JSX formatting change for the Value cell; no behavioral change.
SQLite stealth outputs ordering
crates/wallet/storage_sqlite/src/reader.rs
stealth_outputs_get_many now orders query results by stealth_outputs::id.desc() before fetching, changing returned iteration order.

Sequence Diagram(s)

sequenceDiagram
  participant U as User
  participant N as NFT UI
  participant D as NftData component
  U->>N: View NFT card/row
  alt nft.data exists
    N->>D: Render key/value pairs from nft.data
    D-->>N: Display keys with CopyAddress formatting
  else no data
    N-->>N: Skip NftData block
  end
  N-->>U: Show chip only if nft.is_burnt == true
  note right of N: Original owner inline display removed
Loading
sequenceDiagram
  participant U as User
  participant T as Tokens Page
  participant D as SendMoneyDialog
  participant H as Helpers (formatDisplayCurrency)
  participant S as Submit Transfer

  U->>T: Click "Send" on token
  alt resourceToSend exists
    T->>D: Mount & open dialog
    D->>D: Lookup exact balanceEntry by resource_address
    alt balanceEntry missing
      D-->>T: Log warning, return/null (no dialog flow)
    else balanceEntry present
      U->>D: Enter amount + recipient
      D->>H: Scale/format amount using divisibility
      D->>D: estimateFee using scaled amount
      U->>D: Confirm
      D->>S: Build transfer payload using validated balanceEntry & scaled amount
      S-->>D: Success / Error
      D-->>T: Close dialog on completion
    end
  else
    T-->>U: No dialog mounted
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

P-acks_required, P-reviews_required

Poem

A rabbit taps the keyboard bright,
I stitch the tokens, scale by light.
NFTs now show their secret keys,
Burnt flags bloom when owners seize.
Hop, copy, send — I thump with glee! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "fix(wallet/webui): honour resource divisibility in UI" succinctly captures the primary intent and matches the changeset (notably using divisibility and token symbol when formatting currency across helpers, FormStep, SendMoney, and Tokens). It is concise, specific, and follows a conventional commit-style prefix indicating a bugfix scope. The title provides enough context for a reviewer to understand the main change without enumerating files.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sdbondi
sdbondi force-pushed the wallet-minor-ui-fixes branch from 83e314c to 72db080 Compare September 24, 2025 05:32
@sdbondi
sdbondi merged commit e7f46a2 into tari-project:development Sep 24, 2025
11 of 13 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (12)
applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/components/NftParts.tsx (5)

88-105: Make NftData robust: stable keys and type-aware value rendering.

  • Avoid index keys; use the actual key.
  • Handle non-string values (numbers, booleans, objects) without forcing [object Object].
  • Align prop type with the guard above.

Apply this diff:

-function NftData({ data }: { data: Record<string, any> }) {
-  return (
-    <>
-      {Object.keys(data).map((key, i) => {
-        const value = data[key];
-        return (
-          <Fragment key={i}>
-            <Typography variant="subtitle2">{key}</Typography>
-            <Typography variant="body2" color="text.secondary" gutterBottom>
-              <CopyAddress address={String(value)} />
-            </Typography>
-          </Fragment>
-        );
-      })}
-    </>
-  );
-}
+function NftData({ data }: { data: Record<string, unknown> }) {
+  return (
+    <>
+      {Object.entries(data).map(([key, value]) => (
+        <Fragment key={key}>
+          <Typography variant="subtitle2">{key}</Typography>
+          <Typography variant="body2" color="text.secondary" gutterBottom>
+            {typeof value === "string" ? (
+              <CopyAddress address={value} />
+            ) : (
+              String(
+                typeof value === "object" && value !== null
+                  ? JSON.stringify(value)
+                  : value
+              )
+            )}
+          </Typography>
+        </Fragment>
+      ))}
+    </>
+  );
+}

32-32: Import Fragment from 'react' (or keep <> if you don't need keyed fragments).

Importing from "react/jsx-runtime" is atypical; prefer from "react" for keyed fragments.

-import { Fragment } from "react/jsx-runtime";
+import { Fragment } from "react";

51-55: Type the onError handler to avoid any and use currentTarget.

Small TS hygiene and correctness improvement.

-          onError={(e: any) => {
-            e.target.src =
+          onError={(e: React.SyntheticEvent<HTMLImageElement>) => {
+            e.currentTarget.src =
               "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDMwMCAyMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIzMDAiIGhlaWdodD0iMjAwIiBmaWxsPSIjRjVGNUY1Ii8+CjxwYXRoIGQ9Ik0xMjUgNzVIMTc1VjEyNUgxMjVWNzVaIiBmaWxsPSIjRERERUREIi8+CjxwYXRoIGQ9Ik0xNDAgOTBIMTYwVjExMEgxNDBWOTBaIiBmaWxsPSIjQkJCQkJCIi8+Cjx0ZXh0IHg9IjE1MCIgeT0iMTQwIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTIiIGZpbGw9IiM5OTk5OTkiIHRleHQtYW5jaG9yPSJtaWRkbGUiPk5GVDwvdGV4dD4KPC9zdmc+";
           }}

139-142: Avoid rendering a “blank address” when original owner is missing.

Rendering CopyAddress with "" produces an empty/odd UI. Guard this cell.

-        <Typography variant="body2">
-          <CopyAddress address={originalOwner || ""} />
-        </Typography>
+        {typeof originalOwner === "string" && originalOwner.length > 0 ? (
+          <Typography variant="body2">
+            <CopyAddress address={originalOwner} />
+          </Typography>
+        ) : null}

144-156: Align status chip behavior with card (show only when burnt).

Unifying reduces visual noise and keeps status semantics consistent.

-        <Chip
-          icon={
-            nft.is_burnt ? (
-              <CancelRoundedIcon style={{ height: 16, width: 16 }} />
-            ) : (
-              <CheckCircleRoundedIcon style={{ height: 16, width: 16 }} />
-            )
-          }
-          label={nft.is_burnt ? "Burnt" : "Active"}
-          color={nft.is_burnt ? "error" : "success"}
-          size="small"
-          variant="outlined"
-        />
+        {nft.is_burnt && (
+          <Chip
+            icon={<CancelRoundedIcon style={{ height: 16, width: 16 }} />}
+            label="Burnt"
+            color="error"
+            size="small"
+            variant="outlined"
+          />
+        )}
crates/wallet/storage_sqlite/src/reader.rs (2)

837-848: Apply the same explicit ordering to keep list views consistent.

This path returns unspent stealth outputs without an explicit order. Align it with stealth_outputs_get_many to avoid UI jitter and ensure deterministic results.

         let rows = stealth_outputs::table
             .filter(
                 stealth_outputs::owner_account_id.eq(accounts::table
                     .select(accounts::id)
                     .filter(accounts::address.eq(account_addr.to_string()))
                     .limit(1)
                     .single_value()
                     .assume_not_null()),
             )
             .filter(stealth_outputs::status.eq(OutputStatus::Unspent.as_key_str()))
+            .order_by(stealth_outputs::id.desc())
             .get_results::<models::StealthOutput>(self.connection())
             .map_err(|e| WalletStorageError::general(OPERATION, e))?;

860-864: Order locked stealth outputs for deterministic display.

Mirror the “newest first” behavior here as well.

         let rows = stealth_outputs::table
             .filter(stealth_outputs::lock_id.eq(lock_id))
+            .order_by(stealth_outputs::id.desc())
             .get_results::<models::StealthOutput>(self.connection())
             .map_err(|e| WalletStorageError::general(OPERATION, e))?;
applications/tari_walletd/web_ui/src/utils/helpers.tsx (1)

280-293: Avoid "undefined" symbol and clamp fraction digits

Guard divisibility, and don’t append a trailing space or "undefined" when no symbol is provided.

 export const formatDisplayCurrency = (
   amount: number,
   divisibility: number,
   currencySymbol: string | undefined,
 ): string => {
-  if (isNaN(amount)) {
-    return `0 ${currencySymbol}`;
-  }
-  return `${amount.toLocaleString("en-US", {
-    minimumFractionDigits: 0,
-    maximumFractionDigits: divisibility,
-  })} ${currencySymbol}`;
+  const maxFrac = Math.max(0, Math.min(20, Number.isFinite(divisibility) ? divisibility : 0));
+  const suffix = currencySymbol ? ` ${currencySymbol}` : "";
+  if (isNaN(amount)) {
+    return `0${suffix}`;
+  }
+  return `${amount.toLocaleString("en-US", {
+    minimumFractionDigits: 0,
+    maximumFractionDigits: maxFrac,
+  })}${suffix}`;
 };
applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/Tokens.tsx (1)

207-234: Use a stable key for rows

Avoid index-based keys to prevent row mis-association on list changes.

-                        <BalanceRow
-                          key={i}
+                        <BalanceRow
+                          key={resource_address}
                           token_symbol={token_symbol || ""}
                           resource_address={resource_address}
applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx (1)

213-215: Placeholder length nit

For very large divisibility, the placeholder can become long. Consider capping the visible zeros (e.g., show up to 8) while still allowing full precision input.

Example:

- placeholder: "0" + (divisibility > 0 ? "." + "0".repeat(divisibility) : ""),
+ placeholder: "0" + (divisibility > 0 ? "." + "0".repeat(Math.min(8, divisibility)) : ""),
applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx (2)

83-88: Good guard; consider graceful UX instead of null

Returning null avoids crashes, but consider auto-closing with a user-visible error to prevent a silent no-op.

   const balanceEntry = data?.balances?.find((b: BalanceEntry) => b.resource_address === props.resource_address);

   if (!balanceEntry) {
     console.warn("No balance entry found for resource", props.resource_address);
-    return null;
+    try { props.handleClose?.(); } catch {}
+    return null;
   }

330-331: Unify divisibility source; remove fallback

Given the non-null balanceEntry guard, pass the exact divisibility here too.

-            divisibility={balanceEntry?.divisibility || 6}
+            divisibility={balanceEntry.divisibility}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8af88cf and 72db080.

📒 Files selected for processing (7)
  • applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/components/NftParts.tsx (4 hunks)
  • applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/Tokens.tsx (2 hunks)
  • applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx (4 hunks)
  • applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx (2 hunks)
  • applications/tari_walletd/web_ui/src/routes/StealthUtxoList/StealthUtxoList.tsx (1 hunks)
  • applications/tari_walletd/web_ui/src/utils/helpers.tsx (1 hunks)
  • crates/wallet/storage_sqlite/src/reader.rs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
applications/tari_walletd/web_ui/src/routes/StealthUtxoList/StealthUtxoList.tsx (1)
applications/tari_walletd/web_ui/src/utils/helpers.tsx (1)
  • bigintToDecimalString (233-253)
crates/wallet/storage_sqlite/src/reader.rs (1)
crates/wallet/storage_sqlite/src/writer.rs (9)
  • stealth_outputs (1010-1024)
  • stealth_outputs (1092-1092)
  • stealth_outputs (1093-1093)
  • stealth_outputs (1122-1122)
  • stealth_outputs (1123-1123)
  • stealth_outputs (1135-1135)
  • stealth_outputs (1136-1136)
  • stealth_outputs (1155-1155)
  • stealth_outputs (1156-1156)
applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx (3)
bindings/src/types/wallet-daemon-client/BalanceEntry.ts (1)
  • BalanceEntry (7-15)
applications/tari_walletd/web_ui/src/utils/helpers.tsx (1)
  • substateIdToString (100-109)
bindings/src/types/ConfidentialTransferInputSelection.ts (1)
  • ConfidentialTransferInputSelection (3-7)
applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/Tokens.tsx (2)
applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx (1)
  • SendMoneyDialog (52-346)
bindings/src/types/wallet-daemon-client/BalanceEntry.ts (1)
  • BalanceEntry (7-15)
applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/components/NftParts.tsx (2)
bindings/src/types/NonFungibleToken.ts (1)
  • NonFungibleToken (6-13)
applications/tari_walletd/web_ui/src/components/CopyAddress.tsx (1)
  • CopyAddress (32-43)
applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx (1)
applications/tari_walletd/web_ui/src/utils/helpers.tsx (1)
  • formatDisplayCurrency (281-293)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: test
  • GitHub Check: check stable
  • GitHub Check: check nightly
  • GitHub Check: clippy
🔇 Additional comments (9)
applications/tari_walletd/web_ui/src/routes/StealthUtxoList/StealthUtxoList.tsx (1)

112-114: Use resource divisibility and symbol; avoid hard-coded 6 and global currency symbol

This still formats with 6 decimals and a global symbol, which contradicts the PR objective to honor per‑resource divisibility/symbol. Derive both from the selected stealth resource metadata.

Apply this diff to the value cell:

-                      <DataTableCell>
-                        {bigintToDecimalString(utxo.value, 6)} {currencySymbol}
-                      </DataTableCell>
+                      <DataTableCell>
+                        {bigintToDecimalString(utxo.value, divisibility)} {symbol}
+                      </DataTableCell>

Add these definitions near where resourceToUse is computed (e.g., after Line 43):

// Derive divisibility/symbol from the selected stealth resource; fall back sensibly
const stealthMeta = stealthResources.find((b) => b.resource_address === resourceToUse);
const divisibility =
  (stealthMeta as any)?.divisibility ??
  (stealthMeta as any)?.decimals ??
  6; // fallback if metadata missing
const symbol = (stealthMeta as any)?.symbol ?? currencySymbol;

Additionally, to safely support higher divisibility (e.g., 18), fix bigint exponentiation to avoid Number precision in bigintToDecimalString:

applications/tari_walletd/web_ui/src/utils/helpers.tsx:

-  const wholeValues = (number / BigInt(10 ** decimalPlaces)).toLocaleString(locale, {
+  const wholeValues = (number / (BigInt(10) ** BigInt(decimalPlaces))).toLocaleString(locale, {

Please verify the actual metadata field names (divisibility/decimals/symbol) on balances and adjust the casts accordingly.

applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/components/NftParts.tsx (2)

61-69: LGTM: Burn status chip only when burnt.

This avoids showing an “Active” chip and matches a less noisy UI.


79-79: LGTM: Conditional NftData render.

With the object-guard above, this will avoid rendering junk rows for primitive data.

crates/wallet/storage_sqlite/src/reader.rs (1)

954-957: Deterministic ordering added (newest-first) — LGTM. Confirmed callers: crates/wallet/sdk/src/apis/stealth_outputs.rs::utxos_get_many (used by applications/tari_walletd/src/handlers/stealth_utxos.rs). diesel schema (crates/wallet/storage_sqlite/src/schema.rs) shows stealth_outputs (id) is the PK and the migration creates the table/indexes — ordering by stealth_outputs::id.desc() corresponds to newest-first and should leverage the PK index; avoid returning very large/unpaged result sets.

applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/Tokens.tsx (1)

153-165: Conditional mount of Send dialog is good

Mounting only when a resource is selected avoids unnecessary renders and state churn. Looks good.

applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/steps/FormStep.tsx (2)

108-110: Formatting change is reasonable

Using minimumFractionDigits: 0 with divisibility as the cap aligns with divisibility-based display.


207-211: Correct: pass divisibility and symbol to formatter

This aligns helper usage with resource-specific divisibility/symbol.

applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx (2)

174-177: Nice defensive check in estimateFee

Early-exit if the balance entry is missing prevents bad requests.


311-311: LGTM: pass exact divisibility

Using the exact resource divisibility improves formatting/validation downstream.

function NftCard({ nft }: { nft: NonFungibleToken }) {
const mutableData = convertCborValue(nft.mutable_data);
const data = convertCborValue(nft.data);
const data = convertCborValue(nft.data) as Record<string, any> | undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Guard against non-object NFT data before rendering.

convertCborValue(nft.data) can return primitives/arrays; passing truthy non-objects into NftData will render character-by-character rows. Narrow to plain objects first.

Apply this diff:

-  const data = convertCborValue(nft.data) as Record<string, any> | undefined;
+  const rawData = convertCborValue(nft.data);
+  const data =
+    rawData && typeof rawData === "object" && !Array.isArray(rawData)
+      ? (rawData as Record<string, unknown>)
+      : undefined;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const data = convertCborValue(nft.data) as Record<string, any> | undefined;
const rawData = convertCborValue(nft.data);
const data =
rawData && typeof rawData === "object" && !Array.isArray(rawData)
? (rawData as Record<string, unknown>)
: undefined;
🤖 Prompt for AI Agents
In
applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/components/NftParts.tsx
around line 36, convertCborValue(nft.data) can return primitives or arrays which
then render incorrectly; instead of blindly casting, first check the result is a
plain object (data !== null && typeof data === 'object' && !Array.isArray(data))
and only then assign/cast it to Record<string, any>; otherwise set data to
undefined (or skip rendering NftData) so NftData receives only plain objects.

Comment on lines +182 to 187
let amount = Math.floor((parseFloat(transferFormState.amount) || 0) * Math.pow(10, balanceEntry.divisibility));
// Create transfer object with current form state
const currentTransfer = {
account: substateIdToString(account.component_address),
amount: Math.floor((parseFloat(transferFormState.amount) || 0) * Math.pow(10, balanceEntry?.divisibility || 6)),
amount,
resource_address: props.resource_address || XTR,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

🧩 Analysis chain

Avoid precision loss; compute atomic amount via BigInt; remove XTR fallback

Using Number with Math.pow can lose precision for high divisibility tokens. Convert the decimal string to atomic units using BigInt and pass as a string (if API supports it). Also, the XTR fallback is unnecessary/inconsistent given the earlier guard.

-      let amount = Math.floor((parseFloat(transferFormState.amount) || 0) * Math.pow(10, balanceEntry.divisibility));
+      const amount = decimalToAtomic(transferFormState.amount, balanceEntry.divisibility);
       // Create transfer object with current form state
       const currentTransfer = {
         account: substateIdToString(account.component_address),
-        amount,
-        resource_address: props.resource_address || XTR,
+        amount, // string to avoid precision loss
+        resource_address: props.resource_address!,

Helper to add (outside this hunk), e.g. near the top of the file:

function decimalToAtomic(amountStr: string, divisibility: number): string {
  const sanitized = (amountStr || "").trim();
  if (!sanitized) return "0";
  const [wholeRaw, fracRaw = ""] = sanitized.split(".");
  const whole = wholeRaw.replace(/\D/g, "") || "0";
  const fracClean = fracRaw.replace(/\D/g, "");
  const frac = fracClean.slice(0, Math.max(0, divisibility)).padEnd(Math.max(0, divisibility), "0");
  const base = BigInt(10) ** BigInt(Math.max(0, divisibility));
  const atomic = BigInt(whole || "0") * base + BigInt(frac || "0");
  return atomic.toString();
}

To verify the request type accepts a string amount, run:


🏁 Script executed:

#!/bin/bash
# Locate the accounts transfer hook/type and its request shape
rg -nC3 'useAccountsTransfer\(|accountsTransfer\(|Transfer' --type=ts --type=tsx
rg -nC3 '\bamount\b' --type=ts --type=tsx

Length of output: 55


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching for SendMoney.tsx..."
files=$(find . -type f -name 'SendMoney.tsx' 2>/dev/null || true)
if [ -z "$files" ]; then
  echo "SendMoney.tsx not found via find"
else
  for f in $files; do
    echo "== $f =="
    sed -n '1,360p' "$f"
  done
fi

echo
echo "--- Searching for transfer-related symbols and amount/resource_address usages ---"
rg -n --hidden -S 'useAccountsTransfer' || true
rg -n --hidden -S 'accountsTransfer' || true
rg -n --hidden -S '\btransfer\b' || true
rg -n --hidden -S 'resource_address' || true
rg -n --hidden -S '\bamount\b' || true
rg -n --hidden -S '\bXTR\b' || true

Length of output: 270100


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== bindings/src/types/wallet-daemon-client/AccountsTransferRequest.ts =="
sed -n '1,240p' bindings/src/types/wallet-daemon-client/AccountsTransferRequest.ts || true
echo
echo "== applications/tari_walletd/web_ui/src/services/api/hooks/useAccounts.ts =="
sed -n '1,420p' applications/tari_walletd/web_ui/src/services/api/hooks/useAccounts.ts || true
echo
echo "== clients/javascript/wallet_daemon_client/src/index.ts (accountsTransfer) =="
sed -n '1,420p' clients/javascript/wallet_daemon_client/src/index.ts || true
echo
echo "== bindings/src/helpers/consts.ts =="
sed -n '1,240p' bindings/src/helpers/consts.ts || true

Length of output: 23867


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for Amount type in bindings =="
rg -n --hidden -S '(^|\s)(export )?(type|interface) Amount\b' bindings || true

echo
echo "== bindings/src/types/Amount.ts =="
sed -n '1,240p' bindings/src/types/Amount.ts || true

echo
echo "== show generated bindings index to confirm path imports =="
sed -n '1,240p' bindings/src/index.ts || true

Length of output: 7836


Convert decimal input to atomic units with BigInt, send as string, and remove XTR fallback (update hook types)

  • Rationale: Number + Math.pow loses precision for high-divisibility tokens; convert user decimal string -> atomic units with BigInt (decimalToAtomic) and send that atomic value as a string (bindings allow string amounts: bindings/src/types/Amount.ts -> export type Amount = string | number).
  • Change SendMoney.tsx: replace Math.floor((parseFloat(... ) || 0) * Math.pow(10, balanceEntry.divisibility)) in both estimateFee and handleConfirm with decimalToAtomic(transferFormState.amount, balanceEntry.divisibility). Add the decimalToAtomic helper (returns string) and pass amount as that string. Remove the resource_address fallback (use props.resource_address! instead of props.resource_address || XTR). Location: applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx (estimateFee + handleConfirm).
  • Change hook typing: update TransferParams.amount in applications/tari_walletd/web_ui/src/services/api/hooks/useAccounts.ts from number -> Amount (imported from @tari-project/typescript-bindings) or at least string | number so passing a string does not break TypeScript; confirm and update any other callers that rely on TransferParams.
🤖 Prompt for AI Agents
In
applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx
around lines 182-187 (and the matching spots in estimateFee and handleConfirm),
replace the Math.floor(parseFloat(...) * Math.pow(10, ...)) logic with a
decimalToAtomic helper that converts the decimal string and divisibility into
atomic units as a string, pass that string as the transfer amount, and remove
the XTR fallback by using props.resource_address! instead of
props.resource_address || XTR; additionally add the decimalToAtomic helper
(returns string) to this file or a shared util and ensure both estimateFee and
handleConfirm use it. Update the TransferParams.amount type in
applications/tari_walletd/web_ui/src/services/api/hooks/useAccounts.ts to use
Amount (imported from @tari-project/typescript-bindings) or at minimum string |
number so the string atomic amount is accepted, and update any callers of
TransferParams to comply with the new type.

Comment on lines +246 to +257
let amount = Math.floor((parseFloat(transferFormState.amount) || 0) * Math.pow(10, balanceEntry.divisibility));
const transfer = {
account: substateIdToString(account.component_address),
amount,
resource_address: props.resource_address!,
destination_address: transferFormState.address,
resourceType: props.resource_type,
output_to_revealed: !transferFormState.outputToConfidential,
input_selection: transferFormState.inputSelection as ConfidentialTransferInputSelection,
badge: transferFormState.badge,
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Apply the same BigInt amount conversion in confirm path

Mirror the safe atomic amount conversion in the submit path for consistency.

-      let amount = Math.floor((parseFloat(transferFormState.amount) || 0) * Math.pow(10, balanceEntry.divisibility));
-      const transfer = {
-        account: substateIdToString(account.component_address),
-        amount,
+      const amount = decimalToAtomic(transferFormState.amount, balanceEntry.divisibility);
+      const transfer = {
+        account: substateIdToString(account.component_address),
+        amount, // string
         resource_address: props.resource_address!,
         destination_address: transferFormState.address,
         resourceType: props.resource_type,
         output_to_revealed: !transferFormState.outputToConfidential,
         input_selection: transferFormState.inputSelection as ConfidentialTransferInputSelection,
         badge: transferFormState.badge,
       };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let amount = Math.floor((parseFloat(transferFormState.amount) || 0) * Math.pow(10, balanceEntry.divisibility));
const transfer = {
account: substateIdToString(account.component_address),
amount,
resource_address: props.resource_address!,
destination_address: transferFormState.address,
resourceType: props.resource_type,
output_to_revealed: !transferFormState.outputToConfidential,
input_selection: transferFormState.inputSelection as ConfidentialTransferInputSelection,
badge: transferFormState.badge,
};
const amount = decimalToAtomic(transferFormState.amount, balanceEntry.divisibility);
const transfer = {
account: substateIdToString(account.component_address),
amount, // string
resource_address: props.resource_address!,
destination_address: transferFormState.address,
resourceType: props.resource_type,
output_to_revealed: !transferFormState.outputToConfidential,
input_selection: transferFormState.inputSelection as ConfidentialTransferInputSelection,
badge: transferFormState.badge,
};
🤖 Prompt for AI Agents
In
applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx
around lines 246 to 257, the confirm path constructs transfer.amount as a Number
using Math.floor which is inconsistent with the submit path; change the atomic
amount computation to produce a BigInt the same way the submit path does by
parsing transferFormState.amount, multiplying by 10**balanceEntry.divisibility,
flooring, and wrapping the result with BigInt (e.g. const amount =
BigInt(Math.floor((parseFloat(...) || 0) * Math.pow(10, divisibility)))); then
assign that BigInt to transfer.amount and ensure types align with the transfer
object.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants