fix: crypto amount calculation logic with max decimals (#2697)
What changed, and why it matters
This commit changes how Cake Wallet converts a fiat amount into a crypto amount when a user is sending funds. Previously, the app used a shared number formatter whose decimal precision was set per wallet type. Now it uses the currency's own configured decimal count. The change also adds helper methods for converting between whole-coin and smallest-unit amounts elsewhere in the code. The main risk is that a wrong number of decimal places could cause displayed or calculated amounts to be rounded, potentially leading users to send slightly more or less than they intended, or to misread the value.
Review the new decimal handling against each supported currency's actual chain precision, especially currencies whose decimals differ from the old switch values. Verify that toStringAsFixed does not introduce rounding that affects the subsequently parsed send amount. Test edge cases such as very small fiat amounts, low-priced tokens, and send-all flows. Consider adding unit tests for _updateCryptoAmount and the new CryptoCurrency amount helpers.
Security signals we found
Amount formatting/precision change in wallet send flow
Removal of per-wallet-type hard-coded maximum fraction digits
Use of currency-specific decimals for fiat-to-crypto conversion display
Addition of raw amount parsing/formatting helpers using fixed-point conversion
Evidence from the diff
The patch removes the OutputBase class’s manual NumberFormat-based crypto amount formatting and the per-wallet-type _setCryptoNumMaximumFractionDigits switch. Instead, _updateCryptoAmount now formats the computed crypto value with toStringAsFixed(cryptoCurrencyHandler().decimals). It also adds CryptoCurrency.formatAmount, parseAmount, and tryParseAmount helpers backed by formatFixed/parseFixed. The diff does not show these new helpers being called yet, so the immediate functional change is limited to the fiat-to-crypto display path. No explicit security bug is described by the vendor.
Changed components
lib/view_model/send/output.dartcw_core/lib/crypto_currency.dartInspect captured patch +21 / −48
diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart
index b35531b7..68066b12 100644
--- a/cw_core/lib/crypto_currency.dart
+++ b/cw_core/lib/crypto_currency.dart
@@ -1,6 +1,9 @@
import 'package:cw_core/currency.dart';
import 'package:cw_core/enumerable_item.dart';
import 'package:collection/collection.dart';
+import 'package:cw_core/parse_fixed.dart';
+
+import 'format_fixed.dart';
class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implements Currency {
const CryptoCurrency({
@@ -394,7 +397,15 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
@override
String toString() => title;
- bool titleAndTagEqual(CryptoCurrency other) {
- return title == other.title && tag == other.tag;
- }
+ bool titleAndTagEqual(CryptoCurrency other) => title == other.title && tag == other.tag;
+
+ /// Format the raw amount into its decimal representation eg. turn Sats into Bitcoin
+ String formatAmount(BigInt amount, {int? fractionalDigits, bool trimZeros = true}) =>
+ formatFixed(amount, decimals, fractionalDigits: fractionalDigits, trimZeros: trimZeros);
+
+ /// Parse the [value] and turn it into the smallest denomination eg. turn Bitcoin into Sats
+ BigInt parseAmount(String value) => parseFixed(value, decimals);
+
+ /// Try parsing the [value] and turn it into the smallest denomination eg. turn Bitcoin into Sats
+ BigInt? tryParseAmount(String value) => tryParseFixed(value, decimals);
}
diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart
index 0badd16d..c2e69dfc 100644
--- a/lib/view_model/send/output.dart
+++ b/lib/view_model/send/output.dart
@@ -19,7 +19,6 @@ import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/material.dart';
-import 'package:intl/intl.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cake_wallet/monero/monero.dart';
@@ -42,8 +41,7 @@ class Output = OutputBase with _$Output;
abstract class OutputBase with Store {
OutputBase(
this._wallet, this._settingsStore, this._fiatConversationStore, this.cryptoCurrencyHandler)
- : _cryptoNumberFormat = NumberFormat(cryptoNumberPattern),
- key = UniqueKey(),
+ : key = UniqueKey(),
sendAll = false,
cryptoAmount = '',
cryptoFullBalance = '',
@@ -53,7 +51,6 @@ abstract class OutputBase with Store {
extractedAddress = '',
estimatedFee = '0.0',
parsedAddress = ParsedAddress(addresses: []) {
- _setCryptoNumMaximumFractionDigits();
autorun((_) {
final status = _wallet.syncStatus;
printV("Sync status changed to $status. Recalculating fees");
@@ -289,7 +286,6 @@ abstract class OutputBase with Store {
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> _wallet;
final SettingsStore _settingsStore;
final FiatConversionStore _fiatConversationStore;
- final NumberFormat _cryptoNumberFormat;
@action
void setSendAll(String fullBalance) {
@@ -302,7 +298,6 @@ abstract class OutputBase with Store {
void updateWallet(
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> newWallet) {
_wallet = newWallet;
- _setCryptoNumMaximumFractionDigits();
}
@action
@@ -355,11 +350,12 @@ abstract class OutputBase with Store {
@action
void _updateCryptoAmount() {
try {
- final crypto = double.parse(fiatAmount.replaceAll(',', '.')) /
- _fiatConversationStore.prices[cryptoCurrencyHandler()]!;
- final cryptoAmountTmp = _cryptoNumberFormat.format(crypto);
- if (cryptoAmount != cryptoAmountTmp) {
- cryptoAmount = cryptoAmountTmp;
+ final crypto = (double.parse(fiatAmount.replaceAll(',', '.')) /
+ _fiatConversationStore.prices[cryptoCurrencyHandler()]!)
+ .toStringAsFixed(cryptoCurrencyHandler().decimals);
+
+ if (cryptoAmount != crypto) {
+ cryptoAmount = crypto;
}
} catch (e) {
cryptoAmount = '';
@@ -375,40 +371,6 @@ abstract class OutputBase with Store {
return fields;
}
- void _setCryptoNumMaximumFractionDigits() {
- var maximumFractionDigits = 0;
-
- switch (_wallet.type) {
- case WalletType.monero:
- case WalletType.ethereum:
- case WalletType.polygon:
- case WalletType.base:
- case WalletType.arbitrum:
- case WalletType.solana:
- case WalletType.tron:
- case WalletType.haven:
- case WalletType.zano:
- case WalletType.nano:
- case WalletType.decred:
- maximumFractionDigits = 12;
- break;
- case WalletType.bitcoin:
- case WalletType.litecoin:
- case WalletType.bitcoinCash:
- case WalletType.dogecoin:
- maximumFractionDigits = 8;
- break;
- case WalletType.wownero:
- maximumFractionDigits = 11;
- break;
- case WalletType.none:
- case WalletType.banano:
- break;
- }
-
- _cryptoNumberFormat.maximumFractionDigits = maximumFractionDigits;
- }
-
Future<void> fetchParsedAddress(BuildContext context) async {
final domain = address;
final currency = cryptoCurrencyHandler();
Why this scored 33/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.