-
Notifications
You must be signed in to change notification settings - Fork 56
fix(wallet/webui): honour resource divisibility in UI #1582
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -80,9 +80,12 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) { | |||||||||||||||||||||||||||||||||||||||||||||
| .map((b: BalanceEntry) => b.resource_address) as string[]; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Find the available balance for the resource we're trying to send | ||||||||||||||||||||||||||||||||||||||||||||||
| const balanceEntry = data?.balances?.find( | ||||||||||||||||||||||||||||||||||||||||||||||
| (b: BalanceEntry) => b.resource_address === (props.resource_address || XTR), | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
| 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; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Function to calculate available balance based on input selection | ||||||||||||||||||||||||||||||||||||||||||||||
| const calculateAvailableBalance = () => { | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -115,17 +118,6 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) { | |||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const availableBalance = calculateAvailableBalance(); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const transfer = { | ||||||||||||||||||||||||||||||||||||||||||||||
| account: substateIdToString(account.component_address), | ||||||||||||||||||||||||||||||||||||||||||||||
| amount: Math.floor((parseFloat(transferFormState.amount) || 0) * Math.pow(10, balanceEntry?.divisibility || 6)), | ||||||||||||||||||||||||||||||||||||||||||||||
| 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, | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| function setFormValue(e: React.ChangeEvent<HTMLInputElement>) { | ||||||||||||||||||||||||||||||||||||||||||||||
| const { name, value } = e.target; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -179,14 +171,19 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) { | |||||||||||||||||||||||||||||||||||||||||||||
| if (!account || isEstimatingFee || !transferFormState.address.trim() || !transferFormState.amount) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| if (!balanceEntry) { | ||||||||||||||||||||||||||||||||||||||||||||||
| console.warn("No balance entry found for resource", props.resource_address); | ||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| setIsEstimatingFee(true); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||
| 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, | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+182
to
187
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 🧩 Analysis chainAvoid 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=tsxLength 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' || trueLength 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 || trueLength 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 || trueLength of output: 7836 Convert decimal input to atomic units with BigInt, send as string, and remove XTR fallback (update hook types)
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
| destination_address: transferFormState.address, | ||||||||||||||||||||||||||||||||||||||||||||||
| resourceType: props.resource_type, | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -246,6 +243,18 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) { | |||||||||||||||||||||||||||||||||||||||||||||
| setActiveStep(2); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||
| 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, | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+246
to
+257
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
| await sendIt?.({ | ||||||||||||||||||||||||||||||||||||||||||||||
| ...transfer, | ||||||||||||||||||||||||||||||||||||||||||||||
| dry_run: false, | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -299,7 +308,7 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) { | |||||||||||||||||||||||||||||||||||||||||||||
| isEstimatingFee={isEstimatingFee} | ||||||||||||||||||||||||||||||||||||||||||||||
| availableBalance={availableBalance} | ||||||||||||||||||||||||||||||||||||||||||||||
| token_symbol={props.token_symbol} | ||||||||||||||||||||||||||||||||||||||||||||||
| divisibility={balanceEntry?.divisibility || 6} | ||||||||||||||||||||||||||||||||||||||||||||||
| divisibility={balanceEntry.divisibility} | ||||||||||||||||||||||||||||||||||||||||||||||
| onSubmit={handleFormSubmit} | ||||||||||||||||||||||||||||||||||||||||||||||
| onCancel={handleClose} | ||||||||||||||||||||||||||||||||||||||||||||||
| onFormValueChange={setFormValue} | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard against non-object NFT data before rendering.
convertCborValue(nft.data)can return primitives/arrays; passing truthy non-objects intoNftDatawill render character-by-character rows. Narrow to plain objects first.Apply this diff:
📝 Committable suggestion
🤖 Prompt for AI Agents