From 68e7009ee41d178eed7b03674daf431aa6ea0017 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Mon, 24 Nov 2025 15:45:48 +0100 Subject: [PATCH 01/19] feat: introduce `formatFixed` integration and sats display preference --- cw_bitcoin/lib/electrum_balance.dart | 38 +++++++++++------ cw_bitcoin/lib/electrum_transaction_info.dart | 5 ++- cw_bitcoin/lib/electrum_wallet.dart | 41 ++++++++++--------- cw_core/lib/balance.dart | 1 - cw_evm/lib/evm_chain_transaction_info.dart | 15 ++----- cw_monero/lib/monero_transaction_info.dart | 19 ++++----- cw_monero/lib/monero_wallet.dart | 1 + lib/entities/preferences_key.dart | 1 + .../settings/display_settings_page.dart | 5 +++ lib/store/settings_store.dart | 10 +++++ lib/view_model/send/output.dart | 3 +- lib/view_model/send/send_view_model.dart | 4 +- .../settings/display_settings_view_model.dart | 6 +++ 13 files changed, 90 insertions(+), 59 deletions(-) diff --git a/cw_bitcoin/lib/electrum_balance.dart b/cw_bitcoin/lib/electrum_balance.dart index aeb06f1f07..a0beb2afb5 100644 --- a/cw_bitcoin/lib/electrum_balance.dart +++ b/cw_bitcoin/lib/electrum_balance.dart @@ -1,6 +1,8 @@ import 'dart:convert'; -import 'package:cw_bitcoin/bitcoin_amount_format.dart'; + import 'package:cw_core/balance.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/format_fixed.dart'; class ElectrumBalance extends Balance { ElectrumBalance({ @@ -9,6 +11,7 @@ class ElectrumBalance extends Balance { required this.frozen, this.secondConfirmed = 0, this.secondUnconfirmed = 0, + this.showInSats = false, }) : super( confirmed, unconfirmed, @@ -17,9 +20,7 @@ class ElectrumBalance extends Balance { ); static ElectrumBalance? fromJSON(String? jsonSource) { - if (jsonSource == null) { - return null; - } + if (jsonSource == null) return null; final decoded = json.decode(jsonSource) as Map; @@ -37,28 +38,41 @@ class ElectrumBalance extends Balance { final int frozen; int secondConfirmed = 0; int secondUnconfirmed = 0; + bool showInSats; @override - String get formattedAvailableBalance => bitcoinAmountToString(amount: ((confirmed + unconfirmed) - frozen) ); + String get formattedAvailableBalance => showInSats + ? ((confirmed + unconfirmed) - frozen).toString() + : formatFixed(BigInt.from((confirmed + unconfirmed) - frozen), CryptoCurrency.btc.decimals); @override - String get formattedAdditionalBalance => bitcoinAmountToString(amount: unconfirmed); + String get formattedAdditionalBalance => showInSats + ? unconfirmed.toString() + : formatFixed(BigInt.from(unconfirmed), CryptoCurrency.btc.decimals); @override String get formattedUnAvailableBalance { - final frozenFormatted = bitcoinAmountToString(amount: frozen); - return frozenFormatted == '0.0' ? '' : frozenFormatted; + if (frozen == 0) return ''; + return showInSats + ? frozen.toString() + : formatFixed(BigInt.from(frozen), CryptoCurrency.btc.decimals); } @override - String get formattedSecondAvailableBalance => bitcoinAmountToString(amount: secondConfirmed); + String get formattedSecondAvailableBalance => showInSats + ? secondConfirmed.toString() + : formatFixed(BigInt.from(secondConfirmed), CryptoCurrency.btc.decimals); @override - String get formattedSecondAdditionalBalance => bitcoinAmountToString(amount: secondUnconfirmed); + String get formattedSecondAdditionalBalance => showInSats + ? secondUnconfirmed.toString() + : formatFixed(BigInt.from(secondUnconfirmed), CryptoCurrency.btc.decimals); @override - String get formattedFullAvailableBalance => - bitcoinAmountToString(amount: (confirmed + unconfirmed) + secondConfirmed - frozen); + String get formattedFullAvailableBalance => showInSats + ? ((confirmed + unconfirmed) + secondConfirmed - frozen).toString() + : formatFixed(BigInt.from((confirmed + unconfirmed) + secondConfirmed - frozen), + CryptoCurrency.btc.decimals); String toJSON() => json.encode({ 'confirmed': confirmed, diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index 407e405e10..eae58fb585 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -6,6 +6,7 @@ import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; +import 'package:cw_core/format_fixed.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/format_amount.dart'; @@ -225,11 +226,11 @@ class ElectrumTransactionInfo extends TransactionInfo { @override String amountFormatted() => - '${formatAmount(bitcoinAmountToString(amount: amount))} ${walletTypeToCryptoCurrency(type).title}'; + '${formatFixed(BigInt.from(amount), walletTypeToCryptoCurrency(type).decimals)} ${walletTypeToCryptoCurrency(type).title}'; @override String? feeFormatted() => fee != null - ? '${formatAmount(bitcoinAmountToString(amount: fee!))} ${walletTypeToCryptoCurrency(type).title}' + ? '${formatFixed(BigInt.from(fee!), walletTypeToCryptoCurrency(type).decimals)} ${walletTypeToCryptoCurrency(type).title}' : ''; @override diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index dc611d5007..610ffb90e5 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -4,22 +4,15 @@ import 'dart:io'; import 'dart:isolate'; import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:cw_core/hardware/hardware_wallet_service.dart'; -import 'package:cw_core/root_dir.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; -import 'package:cw_bitcoin/bitcoin_amount_format.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_bitcoin/bitcoin_wallet.dart'; -import 'package:cw_bitcoin/litecoin_wallet.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:collection/collection.dart'; import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; +import 'package:cw_bitcoin/bitcoin_wallet.dart'; import 'package:cw_bitcoin/bitcoin_wallet_keys.dart'; import 'package:cw_bitcoin/electrum.dart' as electrum; import 'package:cw_bitcoin/electrum_balance.dart'; @@ -28,31 +21,37 @@ import 'package:cw_bitcoin/electrum_transaction_history.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/exceptions.dart'; +import 'package:cw_bitcoin/litecoin_wallet.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/get_height_by_date.dart'; +import 'package:cw_core/hardware/hardware_wallet_service.dart'; import 'package:cw_core/node.dart'; +import 'package:cw_core/output_info.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/root_dir.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; +import 'package:cw_core/utils/socket_health_logger.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cw_core/unspent_coin_type.dart'; -import 'package:cw_core/output_info.dart'; import 'package:flutter/foundation.dart'; +import 'package:hex/hex.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:rxdart/subjects.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:sp_scanner/sp_scanner.dart'; -import 'package:hex/hex.dart'; -import 'package:cw_core/utils/socket_health_logger.dart'; part 'electrum_wallet.g.dart'; @@ -77,8 +76,8 @@ abstract class ElectrumWalletBase ElectrumBalance? initialBalance, CryptoCurrency? currency, bool? alwaysScan, - }) : accountHD = - getAccountHDWallet(currency, network, seedBytes, xpub, derivationInfo, walletInfo.hardwareWalletType), + }) : accountHD = getAccountHDWallet( + currency, network, seedBytes, xpub, derivationInfo, walletInfo.hardwareWalletType), syncStatus = NotConnectedSyncStatus(), _password = password, _feeRates = [], @@ -254,6 +253,9 @@ abstract class ElectrumWalletBase return isMempoolAPIEnabled; } + Future checkIfShouldShowInSats() async => + (await sharedPrefs.future).getBool("prefer_sats") ?? false; + @action Future setSilentPaymentsScanning(bool active) async { silentPaymentsScanningActive = active; @@ -773,7 +775,6 @@ abstract class ElectrumWalletBase pubKeyHex = hd.childKey(Bip32KeyIndex(utx.bitcoinAddressRecord.index)).publicKey.toHex(); } - final derivationPath = "${_hardenedDerivationPath(derivationInfo.derivationPath ?? electrum_path)}" "/${utx.bitcoinAddressRecord.isHidden ? "1" : "0"}" @@ -899,7 +900,6 @@ abstract class ElectrumWalletBase throw BitcoinTransactionNoDustException(); } - // If there is only one output, and the amount to send is more than the max spendable amount // then it is actually a send all transaction @@ -1135,7 +1135,6 @@ abstract class ElectrumWalletBase bool hasSilentPayment = false, UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) async { - final utxoDetailsAll = _createUTXOS( sendAll: true, paysToSilentPayment: hasSilentPayment, @@ -2441,6 +2440,8 @@ abstract class ElectrumWalletBase } Future fetchBalances() async { + final showInSats = await checkIfShouldShowInSats(); + final addresses = walletAddresses.allAddresses .where((address) => RegexUtils.addressTypeFromStr(address.address, network) is! MwebAddress) .toList(); @@ -2492,7 +2493,8 @@ abstract class ElectrumWalletBase // if we got null balance responses from the server, set our connection status to lost and return our last known balance: printV("got null balance responses from the server, setting connection status to lost"); syncStatus = LostConnectionSyncStatus(); - return balance[currency] ?? ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0); + return balance[currency] ?? + ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0, showInSats: showInSats); } for (var i = 0; i < balances.length; i++) { @@ -2514,6 +2516,7 @@ abstract class ElectrumWalletBase confirmed: totalConfirmed, unconfirmed: totalUnconfirmed, frozen: totalFrozen, + showInSats: showInSats, ); } diff --git a/cw_core/lib/balance.dart b/cw_core/lib/balance.dart index 7350c80f18..12c5c48203 100644 --- a/cw_core/lib/balance.dart +++ b/cw_core/lib/balance.dart @@ -2,7 +2,6 @@ abstract class Balance { const Balance(this.available, this.additional, {this.secondAvailable, this.secondAdditional}); final int available; - final int additional; final int? secondAvailable; diff --git a/cw_evm/lib/evm_chain_transaction_info.dart b/cw_evm/lib/evm_chain_transaction_info.dart index 781dd57388..ac9227f17a 100644 --- a/cw_evm/lib/evm_chain_transaction_info.dart +++ b/cw_evm/lib/evm_chain_transaction_info.dart @@ -1,8 +1,6 @@ // ignore_for_file: overridden_fields, annotate_overrides - -import 'dart:math'; - import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/format_fixed.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; @@ -47,10 +45,8 @@ abstract class EVMChainTransactionInfo extends TransactionInfo { String get feeCurrency; @override - String amountFormatted() { - final amount = formatAmount((ethAmount / BigInt.from(10).pow(exponent)).toString()); - return '${amount.substring(0, min(10, amount.length))} $tokenSymbol'; - } + String amountFormatted() => + '${formatFixed(ethAmount, exponent, fractionalDigits: 10)} $tokenSymbol'; @override String fiatAmount() => _fiatAmount ?? ''; @@ -59,10 +55,7 @@ abstract class EVMChainTransactionInfo extends TransactionInfo { void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); @override - String feeFormatted() { - final amount = (ethFee / BigInt.from(10).pow(18)).toString(); - return '${amount.substring(0, min(18, amount.length))} $feeCurrency'; - } + String feeFormatted() => '${formatFixed(ethFee, 18)} $feeCurrency'; Map toJson() => { 'id': id, diff --git a/cw_monero/lib/monero_transaction_info.dart b/cw_monero/lib/monero_transaction_info.dart index 0ac48dcba9..d0b3080087 100644 --- a/cw_monero/lib/monero_transaction_info.dart +++ b/cw_monero/lib/monero_transaction_info.dart @@ -1,13 +1,13 @@ -import 'package:cw_core/transaction_info.dart'; -import 'package:cw_core/monero_amount_format.dart'; -import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/format_fixed.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/transaction_info.dart'; class MoneroTransactionInfo extends TransactionInfo { - MoneroTransactionInfo(this.txHash, this.height, this.direction, this.date, - this.isPending, this.amount, this.accountIndex, this.addressIndex, this.fee, - this.confirmations) : - id = "${txHash}_${amount}_${accountIndex}_${addressIndex}"; + MoneroTransactionInfo(this.txHash, this.height, this.direction, this.date, this.isPending, + this.amount, this.accountIndex, this.addressIndex, this.fee, this.confirmations) + : id = "${txHash}_${amount}_${accountIndex}_${addressIndex}"; final String id; final String txHash; @@ -26,7 +26,7 @@ class MoneroTransactionInfo extends TransactionInfo { @override String amountFormatted() => - '${formatAmount(moneroAmountToString(amount: amount))} XMR'; + '${formatFixed(BigInt.from(amount), CryptoCurrency.xmr.decimals)} XMR'; @override String fiatAmount() => _fiatAmount ?? ''; @@ -35,6 +35,5 @@ class MoneroTransactionInfo extends TransactionInfo { void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); @override - String feeFormatted() => - '${formatAmount(moneroAmountToString(amount: fee))} XMR'; + String feeFormatted() => '${formatFixed(BigInt.from(fee), CryptoCurrency.xmr.decimals)} XMR'; } diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 9f5986e267..81771193b1 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -3,6 +3,7 @@ import 'dart:ffi'; import 'dart:io'; import 'dart:isolate'; +import 'package:cw_core/format_fixed.dart'; import 'package:cw_core/monero_amount_format.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/transaction_priority.dart'; diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index a9acc2a6bf..6fc6c6df52 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -24,6 +24,7 @@ class PreferencesKey { static const currentWowneroNodeIdKey = 'current_node_id_wow'; static const currentTransactionPriorityKeyLegacy = 'current_fee_priority'; static const currentBalanceDisplayModeKey = 'current_balance_display_mode'; + static const preferBalanceInSats = 'prefer_sats'; static const shouldSaveRecipientAddressKey = 'save_recipient_address'; static const isAppSecureKey = 'is_app_secure'; static const disableTradeOption = 'disable_buy'; diff --git a/lib/src/screens/settings/display_settings_page.dart b/lib/src/screens/settings/display_settings_page.dart index c28db5855b..8a06c5210f 100644 --- a/lib/src/screens/settings/display_settings_page.dart +++ b/lib/src/screens/settings/display_settings_page.dart @@ -59,6 +59,11 @@ class DisplaySettingsPage extends BasePage { displayItem: (SyncStatusDisplayMode mode) => mode.title, isGridView: false, ), + SettingsSwitcherCell( + title: "Prefer sats", // ToDo: + value: _displaySettingsViewModel.preferBalanceInSats, + onValueChange: (_, bool value) => _displaySettingsViewModel.setPreferBalanceInSats(value), + ), //if (!isHaven) it does not work correctly if (!_displaySettingsViewModel.disabledFiatApiMode) SettingsPickerCell( diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index a825ee2439..68a8b6c39f 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -89,6 +89,7 @@ abstract class SettingsStoreBase with Store { required this.deviceName, required Map nodes, required Map powNodes, + required this.preferBalanceInSats, required this.shouldShowYatPopup, required this.shouldShowDEuroDisclaimer, required this.shouldShowRepWarning, @@ -425,6 +426,10 @@ abstract class SettingsStoreBase with Store { (BalanceDisplayMode mode) => sharedPreferences.setInt( PreferencesKey.currentBalanceDisplayModeKey, mode.serialize())); + reaction( + (_) => preferBalanceInSats, + (bool _preferBalanceInSats) => sharedPreferences.setBool(PreferencesKey.preferBalanceInSats, _preferBalanceInSats)); + reaction((_) => currentSyncMode, (SyncMode syncMode) { sharedPreferences.setInt(PreferencesKey.syncModeKey, syncMode.type.index); FlutterDaemon().startBackgroundSync(syncMode.frequency.inMinutes); @@ -712,6 +717,9 @@ abstract class SettingsStoreBase with Store { @observable BalanceDisplayMode balanceDisplayMode; + @observable + bool preferBalanceInSats; + @observable FiatApiMode fiatApiMode; @@ -1033,6 +1041,7 @@ abstract class SettingsStoreBase with Store { final currentBalanceDisplayMode = BalanceDisplayMode.deserialize( raw: sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey)!); + final preferBalanceInSats = sharedPreferences.getBool(PreferencesKey.preferBalanceInSats) ?? false; // FIX-ME: Check for which default value we should have here final shouldSaveRecipientAddress = sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ?? false; @@ -1374,6 +1383,7 @@ abstract class SettingsStoreBase with Store { powNodes: powNodes, appVersion: packageInfo.version, deviceName: deviceName, + preferBalanceInSats: preferBalanceInSats, isBitcoinBuyEnabled: isBitcoinBuyEnabled, initialFiatCurrency: currentFiatCurrency, initialCakePayCountry: currentCakePayCountry, diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index 0badd16df9..c27926df74 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -270,8 +270,7 @@ abstract class OutputBase with Store { try { final currency = (isEVMCompatibleChain(_wallet.type) || - _wallet.type == WalletType.solana || - _wallet.type == WalletType.tron) + [WalletType.solana, WalletType.tron].contains(_wallet.type)) ? _wallet.currency : cryptoCurrencyHandler(); final fiat = calculateFiatAmountRaw( diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 1b446e0203..868cc2dc47 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -310,11 +310,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor @computed String get pendingTransactionFiatAmountFormatted => - isFiatDisabled ? '' : pendingTransactionFiatAmount + ' ' + fiat.title; + isFiatDisabled ? '' : '$pendingTransactionFiatAmount ${fiat.title}'; @computed String get pendingTransactionFeeFiatAmountFormatted => - isFiatDisabled ? '' : pendingTransactionFeeFiatAmount + ' ' + fiat.title; + isFiatDisabled ? '' : '$pendingTransactionFeeFiatAmount ${fiat.title}'; @computed bool get isReadyForSend => diff --git a/lib/view_model/settings/display_settings_view_model.dart b/lib/view_model/settings/display_settings_view_model.dart index 02500083ab..9bcc1b3da0 100644 --- a/lib/view_model/settings/display_settings_view_model.dart +++ b/lib/view_model/settings/display_settings_view_model.dart @@ -35,6 +35,9 @@ abstract class DisplaySettingsViewModelBase with Store { @computed bool get shouldShowMarketPlaceInDashboard => _settingsStore.shouldShowMarketPlaceInDashboard; + @computed + bool get preferBalanceInSats => _settingsStore.preferBalanceInSats; + @computed MaterialThemeBase get currentTheme => _themeStore.currentTheme; @@ -103,6 +106,9 @@ abstract class DisplaySettingsViewModelBase with Store { @action void setBalanceDisplayMode(BalanceDisplayMode value) => _settingsStore.balanceDisplayMode = value; + @action + void setPreferBalanceInSats(bool value) => _settingsStore.preferBalanceInSats = value; + @action void setShouldDisplayBalance(bool value) { if (value) { From eccd4819c879e65fdae647ab290acf8d02cda548 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 25 Nov 2025 15:52:45 +0100 Subject: [PATCH 02/19] feat: enable display of Bitcoin amounts in satoshis and refactor amount formatting logic --- cw_bitcoin/lib/electrum_balance.dart | 47 ++---------- cw_bitcoin/lib/electrum_transaction_info.dart | 5 +- cw_bitcoin/lib/electrum_wallet.dart | 9 +-- cw_core/lib/balance.dart | 13 ++-- cw_core/lib/crypto_currency.dart | 34 +++++---- cw_core/lib/monero_balance.dart | 14 +--- cw_core/lib/wownero_balance.dart | 17 +---- cw_evm/lib/evm_erc20_balance.dart | 7 -- cw_monero/lib/monero_transaction_info.dart | 6 +- cw_tron/lib/tron_balance.dart | 11 +-- .../receive/widgets/currency_input_field.dart | 9 ++- .../screens/receive/widgets/qr_widget.dart | 3 +- .../dashboard/balance_view_model.dart | 76 +++++++++---------- .../dashboard/transaction_list_item.dart | 5 +- lib/view_model/send/send_view_model.dart | 14 +++- .../transaction_details_view_model.dart | 13 +++- .../wallet_address_list_view_model.dart | 24 ++++-- 17 files changed, 131 insertions(+), 176 deletions(-) diff --git a/cw_bitcoin/lib/electrum_balance.dart b/cw_bitcoin/lib/electrum_balance.dart index a0beb2afb5..c129f245bd 100644 --- a/cw_bitcoin/lib/electrum_balance.dart +++ b/cw_bitcoin/lib/electrum_balance.dart @@ -1,8 +1,6 @@ import 'dart:convert'; import 'package:cw_core/balance.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/format_fixed.dart'; class ElectrumBalance extends Balance { ElectrumBalance({ @@ -11,13 +9,8 @@ class ElectrumBalance extends Balance { required this.frozen, this.secondConfirmed = 0, this.secondUnconfirmed = 0, - this.showInSats = false, - }) : super( - confirmed, - unconfirmed, - secondAvailable: secondConfirmed, - secondAdditional: secondUnconfirmed, - ); + }) : super(confirmed, unconfirmed, + secondAvailable: secondConfirmed, secondAdditional: secondUnconfirmed, frozen: frozen); static ElectrumBalance? fromJSON(String? jsonSource) { if (jsonSource == null) return null; @@ -35,44 +28,14 @@ class ElectrumBalance extends Balance { int confirmed; int unconfirmed; + @override final int frozen; + int secondConfirmed = 0; int secondUnconfirmed = 0; - bool showInSats; - - @override - String get formattedAvailableBalance => showInSats - ? ((confirmed + unconfirmed) - frozen).toString() - : formatFixed(BigInt.from((confirmed + unconfirmed) - frozen), CryptoCurrency.btc.decimals); - - @override - String get formattedAdditionalBalance => showInSats - ? unconfirmed.toString() - : formatFixed(BigInt.from(unconfirmed), CryptoCurrency.btc.decimals); - - @override - String get formattedUnAvailableBalance { - if (frozen == 0) return ''; - return showInSats - ? frozen.toString() - : formatFixed(BigInt.from(frozen), CryptoCurrency.btc.decimals); - } - - @override - String get formattedSecondAvailableBalance => showInSats - ? secondConfirmed.toString() - : formatFixed(BigInt.from(secondConfirmed), CryptoCurrency.btc.decimals); - - @override - String get formattedSecondAdditionalBalance => showInSats - ? secondUnconfirmed.toString() - : formatFixed(BigInt.from(secondUnconfirmed), CryptoCurrency.btc.decimals); @override - String get formattedFullAvailableBalance => showInSats - ? ((confirmed + unconfirmed) + secondConfirmed - frozen).toString() - : formatFixed(BigInt.from((confirmed + unconfirmed) + secondConfirmed - frozen), - CryptoCurrency.btc.decimals); + int get fullAvailableBalance => (confirmed + unconfirmed) + secondConfirmed - frozen; String toJSON() => json.encode({ 'confirmed': confirmed, diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index eae58fb585..12bfa8a6b3 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -6,7 +6,6 @@ import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; -import 'package:cw_core/format_fixed.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/format_amount.dart'; @@ -226,11 +225,11 @@ class ElectrumTransactionInfo extends TransactionInfo { @override String amountFormatted() => - '${formatFixed(BigInt.from(amount), walletTypeToCryptoCurrency(type).decimals)} ${walletTypeToCryptoCurrency(type).title}'; + '${walletTypeToCryptoCurrency(type).formatAmount(BigInt.from(amount))} ${walletTypeToCryptoCurrency(type).title}'; @override String? feeFormatted() => fee != null - ? '${formatFixed(BigInt.from(fee!), walletTypeToCryptoCurrency(type).decimals)} ${walletTypeToCryptoCurrency(type).title}' + ? '${walletTypeToCryptoCurrency(type).formatAmount(BigInt.from(fee!))} ${walletTypeToCryptoCurrency(type).title}' : ''; @override diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 610ffb90e5..5221b48c40 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -253,9 +253,6 @@ abstract class ElectrumWalletBase return isMempoolAPIEnabled; } - Future checkIfShouldShowInSats() async => - (await sharedPrefs.future).getBool("prefer_sats") ?? false; - @action Future setSilentPaymentsScanning(bool active) async { silentPaymentsScanningActive = active; @@ -2440,8 +2437,6 @@ abstract class ElectrumWalletBase } Future fetchBalances() async { - final showInSats = await checkIfShouldShowInSats(); - final addresses = walletAddresses.allAddresses .where((address) => RegexUtils.addressTypeFromStr(address.address, network) is! MwebAddress) .toList(); @@ -2493,8 +2488,7 @@ abstract class ElectrumWalletBase // if we got null balance responses from the server, set our connection status to lost and return our last known balance: printV("got null balance responses from the server, setting connection status to lost"); syncStatus = LostConnectionSyncStatus(); - return balance[currency] ?? - ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0, showInSats: showInSats); + return balance[currency] ?? ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0); } for (var i = 0; i < balances.length; i++) { @@ -2516,7 +2510,6 @@ abstract class ElectrumWalletBase confirmed: totalConfirmed, unconfirmed: totalUnconfirmed, frozen: totalFrozen, - showInSats: showInSats, ); } diff --git a/cw_core/lib/balance.dart b/cw_core/lib/balance.dart index 12c5c48203..6394855370 100644 --- a/cw_core/lib/balance.dart +++ b/cw_core/lib/balance.dart @@ -1,5 +1,6 @@ +/// Balance Data class with all amounts in the lowest possible currency (e.g. satoshis or wei) abstract class Balance { - const Balance(this.available, this.additional, {this.secondAvailable, this.secondAdditional}); + const Balance(this.available, this.additional, {this.secondAvailable, this.secondAdditional, this.frozen}); final int available; final int additional; @@ -7,10 +8,10 @@ abstract class Balance { final int? secondAvailable; final int? secondAdditional; - String get formattedAvailableBalance; - String get formattedAdditionalBalance; + final int? frozen; + + int get fullAvailableBalance => available; + + @deprecated String get formattedUnAvailableBalance => ''; - String get formattedSecondAvailableBalance => ''; - String get formattedSecondAdditionalBalance => ''; - String get formattedFullAvailableBalance => formattedAvailableBalance; } diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index b35531b724..5f96aa081f 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -1,6 +1,7 @@ import 'package:cw_core/currency.dart'; import 'package:cw_core/enumerable_item.dart'; import 'package:collection/collection.dart'; +import 'package:cw_core/format_fixed.dart'; class CryptoCurrency extends EnumerableItem with Serializable implements Currency { const CryptoCurrency({ @@ -377,24 +378,25 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen int? decimals, bool? enabled, bool? isPotentialScam, - }) { - return CryptoCurrency( - title: title ?? this.title, - raw: raw ?? this.raw, - name: name ?? this.name, - fullName: fullName ?? this.fullName, - iconPath: iconPath ?? this.iconPath, - tag: tag ?? this.tag, - decimals: decimals ?? this.decimals, - enabled: enabled ?? this.enabled, - isPotentialScam: isPotentialScam ?? this.isPotentialScam, - ); - } + }) => + CryptoCurrency( + title: title ?? this.title, + raw: raw ?? this.raw, + name: name ?? this.name, + fullName: fullName ?? this.fullName, + iconPath: iconPath ?? this.iconPath, + tag: tag ?? this.tag, + decimals: decimals ?? this.decimals, + enabled: enabled ?? this.enabled, + isPotentialScam: isPotentialScam ?? this.isPotentialScam, + ); @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 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); } diff --git a/cw_core/lib/monero_balance.dart b/cw_core/lib/monero_balance.dart index 34f51faf97..9c14a7d4f6 100644 --- a/cw_core/lib/monero_balance.dart +++ b/cw_core/lib/monero_balance.dart @@ -3,25 +3,15 @@ import 'package:cw_core/monero_amount_format.dart'; class MoneroBalance extends Balance { MoneroBalance({required this.fullBalance, required this.unlockedBalance, this.frozenBalance = 0}) - : formattedUnconfirmedBalance = moneroAmountToString(amount: fullBalance - unlockedBalance), - formattedUnlockedBalance = moneroAmountToString(amount: unlockedBalance), - formattedFrozenBalance = moneroAmountToString(amount: frozenBalance), - super(unlockedBalance, fullBalance); + : formattedFrozenBalance = moneroAmountToString(amount: frozenBalance), + super(unlockedBalance, fullBalance, frozen: frozenBalance); final int fullBalance; final int unlockedBalance; final int frozenBalance; - final String formattedUnconfirmedBalance; - final String formattedUnlockedBalance; final String formattedFrozenBalance; @override String get formattedUnAvailableBalance => formattedFrozenBalance == '0.0' ? '' : formattedFrozenBalance; - - @override - String get formattedAvailableBalance => formattedUnlockedBalance; - - @override - String get formattedAdditionalBalance => formattedUnconfirmedBalance; } diff --git a/cw_core/lib/wownero_balance.dart b/cw_core/lib/wownero_balance.dart index 916fa90bc6..c309226b9c 100644 --- a/cw_core/lib/wownero_balance.dart +++ b/cw_core/lib/wownero_balance.dart @@ -3,26 +3,15 @@ import 'package:cw_core/wownero_amount_format.dart'; class WowneroBalance extends Balance { WowneroBalance({required this.fullBalance, required this.unlockedBalance, this.frozenBalance = 0}) - : formattedUnconfirmedBalance = wowneroAmountToString(amount: fullBalance - unlockedBalance), - formattedUnlockedBalance = wowneroAmountToString(amount: unlockedBalance), - formattedFrozenBalance = - wowneroAmountToString(amount: frozenBalance), - super(unlockedBalance, fullBalance); + : formattedFrozenBalance = wowneroAmountToString(amount: frozenBalance), + super(unlockedBalance, fullBalance, frozen: frozenBalance); final int fullBalance; final int unlockedBalance; final int frozenBalance; - final String formattedUnconfirmedBalance; - final String formattedUnlockedBalance; final String formattedFrozenBalance; @override String get formattedUnAvailableBalance => formattedFrozenBalance == '0.0' ? '' : formattedFrozenBalance; - - @override - String get formattedAvailableBalance => formattedUnlockedBalance; - - @override - String get formattedAdditionalBalance => formattedUnconfirmedBalance; -} \ No newline at end of file +} diff --git a/cw_evm/lib/evm_erc20_balance.dart b/cw_evm/lib/evm_erc20_balance.dart index a29b741885..369f950d9a 100644 --- a/cw_evm/lib/evm_erc20_balance.dart +++ b/cw_evm/lib/evm_erc20_balance.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'package:cw_core/format_fixed.dart'; import 'package:cw_core/balance.dart'; @@ -10,12 +9,6 @@ class EVMChainERC20Balance extends Balance { final BigInt balance; final int exponent; - @override - String get formattedAdditionalBalance => formatFixed(balance, exponent, fractionalDigits: 12); - - @override - String get formattedAvailableBalance => formatFixed(balance, exponent, fractionalDigits: 12); - String toJSON() => json.encode({ 'balanceInWei': balance.toString(), 'exponent': exponent, diff --git a/cw_monero/lib/monero_transaction_info.dart b/cw_monero/lib/monero_transaction_info.dart index d0b3080087..b7ddd0ca69 100644 --- a/cw_monero/lib/monero_transaction_info.dart +++ b/cw_monero/lib/monero_transaction_info.dart @@ -1,6 +1,5 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/format_amount.dart'; -import 'package:cw_core/format_fixed.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; @@ -25,8 +24,7 @@ class MoneroTransactionInfo extends TransactionInfo { String? _fiatAmount; @override - String amountFormatted() => - '${formatFixed(BigInt.from(amount), CryptoCurrency.xmr.decimals)} XMR'; + String amountFormatted() => '${CryptoCurrency.xmr.formatAmount(BigInt.from(amount))} XMR'; @override String fiatAmount() => _fiatAmount ?? ''; @@ -35,5 +33,5 @@ class MoneroTransactionInfo extends TransactionInfo { void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); @override - String feeFormatted() => '${formatFixed(BigInt.from(fee), CryptoCurrency.xmr.decimals)} XMR'; + String feeFormatted() => '${CryptoCurrency.xmr.formatAmount(BigInt.from(fee))} XMR'; } diff --git a/cw_tron/lib/tron_balance.dart b/cw_tron/lib/tron_balance.dart index 5b2ba3fa7c..001478bfc2 100644 --- a/cw_tron/lib/tron_balance.dart +++ b/cw_tron/lib/tron_balance.dart @@ -1,22 +1,13 @@ import 'dart:convert'; import 'package:cw_core/balance.dart'; -import 'package:on_chain/on_chain.dart'; class TronBalance extends Balance { TronBalance(this.balance) : super(balance.toInt(), balance.toInt()); final BigInt balance; - @override - String get formattedAdditionalBalance => TronHelper.fromSun(balance); - - @override - String get formattedAvailableBalance => TronHelper.fromSun(balance); - - String toJSON() => json.encode({ - 'balance': balance.toString(), - }); + String toJSON() => json.encode({ 'balance': balance.toString() }); static TronBalance? fromJSON(String? jsonSource) { if (jsonSource == null) { diff --git a/lib/src/screens/receive/widgets/currency_input_field.dart b/lib/src/screens/receive/widgets/currency_input_field.dart index 2a061c4d02..a5b0800e28 100644 --- a/lib/src/screens/receive/widgets/currency_input_field.dart +++ b/lib/src/screens/receive/widgets/currency_input_field.dart @@ -184,7 +184,7 @@ class CurrencyAmountTextField extends StatelessWidget { inputFormatters: [ FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]')), ], - hintText: hintText ?? '0.0000', + hintText: hintText ?? (selectedCurrencyDecimals == 0 ? '0' : '0.0000'), fillColor: fillColor, textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( fontSize: 16, @@ -198,8 +198,13 @@ class CurrencyAmountTextField extends StatelessWidget { ), validator: isAmountEditable ? currencyValueValidator : null, onChanged: (value) { - final sanitized = + var sanitized = value.replaceAll(',', '.').withMaxDecimals(selectedCurrencyDecimals); + + if (selectedCurrencyDecimals == 0) { + sanitized = sanitized.replaceAll('.', ''); + } + if (sanitized != amountController.text) { // Update text while preserving a sane cursor position to avoid auto-selection amountController.value = amountController.value.copyWith( diff --git a/lib/src/screens/receive/widgets/qr_widget.dart b/lib/src/screens/receive/widgets/qr_widget.dart index 6a40850b64..a7687c898d 100644 --- a/lib/src/screens/receive/widgets/qr_widget.dart +++ b/lib/src/screens/receive/widgets/qr_widget.dart @@ -204,7 +204,7 @@ class QRWidget extends StatelessWidget { borderWidth: 0.0, selectedCurrency: _currencyName, selectedCurrencyDecimals: - addressListViewModel.selectedCurrency.decimals, + addressListViewModel.selectedCurrencyDecimals, amountFocusNode: amountTextFieldFocusNode, amountController: amountController, padding: EdgeInsets.only(top: 20, left: _width / 4), @@ -283,6 +283,7 @@ class QRWidget extends StatelessWidget { String get _currencyName { if (addressListViewModel.selectedCurrency is CryptoCurrency) { + if (addressListViewModel.useSatoshis) return "SATS"; return (addressListViewModel.selectedCurrency as CryptoCurrency).title.toUpperCase(); } return addressListViewModel.selectedCurrency.name.toUpperCase(); diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 552d318e4a..86773c9527 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -202,11 +202,11 @@ abstract class BalanceViewModelBase with Store { String additionalBalance(CryptoCurrency cryptoCurrency) { final balance = _currencyBalance(cryptoCurrency); - if (displayMode == BalanceDisplayMode.hiddenBalance) { + if (displayMode == BalanceDisplayMode.hiddenBalance || balance.additional == 0) { return '0.0'; } - return balance.formattedAdditionalBalance; + return cryptoCurrency.formatAmount(BigInt.from(balance.additional)); } @computed @@ -237,51 +237,44 @@ abstract class BalanceViewModelBase with Store { // throw Exception('Price is null for: $key'); // } - final additionalFiatBalance = isFiatDisabled + final availableFiatBalance = isFiatDisabled ? '' - : (fiatCurrency.toString() + - ' ' + - _getFiatBalance(price: price, cryptoAmount: value.formattedAdditionalBalance)); + : '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: key.formatAmount(BigInt.from(value.available)))}'; - final availableFiatBalance = isFiatDisabled + final additionalFiatBalance = isFiatDisabled ? '' - : (fiatCurrency.toString() + - ' ' + - _getFiatBalance(price: price, cryptoAmount: value.formattedAvailableBalance)); + : '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: key.formatAmount(BigInt.from(value.additional)))}'; final frozenFiatBalance = isFiatDisabled ? '' - : (fiatCurrency.toString() + - ' ' + - _getFiatBalance(price: price, cryptoAmount: getFormattedFrozenBalance(value))); + : '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: value.frozen != null ? key.formatAmount(BigInt.from(value.frozen!)) : null)}'; - final secondAdditionalFiatBalance = isFiatDisabled + final secondAvailableFiatBalance = isFiatDisabled ? '' - : (fiatCurrency.toString() + - ' ' + - _getFiatBalance(price: price, cryptoAmount: value.formattedSecondAdditionalBalance)); + : '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: value.secondAvailable != null ? key.formatAmount(BigInt.from(value.secondAvailable!)) : null)}'; - final secondAvailableFiatBalance = isFiatDisabled + final secondAdditionalFiatBalance = isFiatDisabled ? '' - : (fiatCurrency.toString() + - ' ' + - _getFiatBalance(price: price, cryptoAmount: value.formattedSecondAvailableBalance)); + : '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: value.secondAdditional != null ? key.formatAmount(BigInt.from(value.secondAdditional!)) : null)}'; return MapEntry( - key, - BalanceRecord( - availableBalance: value.formattedAvailableBalance, - additionalBalance: value.formattedAdditionalBalance, - frozenBalance: getFormattedFrozenBalance(value), - secondAvailableBalance: value.formattedSecondAvailableBalance, - secondAdditionalBalance: value.formattedSecondAdditionalBalance, - fiatAdditionalBalance: additionalFiatBalance, - fiatAvailableBalance: availableFiatBalance, - fiatFrozenBalance: frozenFiatBalance, - fiatSecondAvailableBalance: secondAvailableFiatBalance, - fiatSecondAdditionalBalance: secondAdditionalFiatBalance, - asset: key, - formattedAssetTitle: _formatterAsset(key))); + key, + BalanceRecord( + availableBalance: _getFormattedCryptoAmount(key, value.available), + fiatAvailableBalance: availableFiatBalance, + additionalBalance: _getFormattedCryptoAmount(key, value.additional), + fiatAdditionalBalance: additionalFiatBalance, + frozenBalance: + (value.frozen ?? 0) > 0 ? _getFormattedCryptoAmount(key, value.frozen) : '', + fiatFrozenBalance: frozenFiatBalance, + secondAvailableBalance: _getFormattedCryptoAmount(key, value.secondAvailable), + fiatSecondAvailableBalance: secondAvailableFiatBalance, + secondAdditionalBalance: _getFormattedCryptoAmount(key, value.secondAdditional), + fiatSecondAdditionalBalance: secondAdditionalFiatBalance, + asset: key, + formattedAssetTitle: _formatterAsset(key), + ), + ); }); } @@ -331,6 +324,16 @@ abstract class BalanceViewModelBase with Store { return false; } + String _getFormattedCryptoAmount(CryptoCurrency cryptoCurrency, int? amount) { + if (amount == null) return ""; + + if (settingsStore.preferBalanceInSats && cryptoCurrency == CryptoCurrency.btc) { + return "$amount"; + } + + return cryptoCurrency.formatAmount(BigInt.from(amount)); + } + @computed List get formattedBalances { final balance = balances.values.toList(); @@ -452,7 +455,4 @@ abstract class BalanceViewModelBase with Store { return asset.toString(); } } - - String getFormattedFrozenBalance(Balance walletBalance) => - walletBalance.formattedUnAvailableBalance; } diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 33df3ea241..b8f1cd9090 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -51,7 +51,10 @@ class TransactionListItem extends ActionListItem with Keyable { balanceViewModel.wallet.type == WalletType.tron; String get formattedCryptoAmount { - return displayMode == BalanceDisplayMode.hiddenBalance ? '---' : transaction.amountFormatted(); + if (displayMode == BalanceDisplayMode.hiddenBalance) return '---'; + if (balanceViewModel.wallet.type == WalletType.bitcoin && settingsStore.preferBalanceInSats) + return "${transaction.amount}"; + return transaction.amountFormatted(); } String get formattedTitle { diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 3df30d3773..3d5f870ae0 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -266,7 +266,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor coinTypeToSpendFrom == UnspentCoinType.nonMweb) { return balanceViewModel.balances.values.first.availableBalance; } - return wallet.balance[selectedCryptoCurrency]!.formattedFullAvailableBalance; + if (walletType == WalletType.bitcoin && _settingsStore.preferBalanceInSats) { + return "${wallet.balance[selectedCryptoCurrency]!.fullAvailableBalance}"; + } + + return selectedCryptoCurrency.formatAmount(BigInt.from(wallet.balance[selectedCryptoCurrency]!.fullAvailableBalance)); } @action @@ -293,14 +297,18 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor // only for electrum, monero, wownero, decred wallets atm: switch (wallet.type) { case WalletType.bitcoin: + final sendingBalance = await unspentCoinsListViewModel.getSendingBalance(coinTypeToSpendFrom); + if (_settingsStore.preferBalanceInSats) + return sendingBalance.toString(); + return walletTypeToCryptoCurrency(walletType).formatAmount(BigInt.from(sendingBalance)); case WalletType.litecoin: case WalletType.bitcoinCash: case WalletType.dogecoin: case WalletType.monero: case WalletType.wownero: case WalletType.decred: - return wallet.formatCryptoAmount( - (await unspentCoinsListViewModel.getSendingBalance(coinTypeToSpendFrom)).toString()); + final sendingBalance = await unspentCoinsListViewModel.getSendingBalance(coinTypeToSpendFrom); + return walletTypeToCryptoCurrency(walletType).formatAmount(BigInt.from(sendingBalance)); default: return balance; } diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index ffe22facf0..13abee1e89 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -330,6 +330,15 @@ abstract class TransactionDetailsViewModelBase with Store { } void _addElectrumListItems(TransactionInfo tx, DateFormat dateFormat) { + + String amountFormatted = tx.amountFormatted(); + String? feeFormatted = tx.feeFormatted(); + + if (wallet.type == WalletType.bitcoin && settingsStore.preferBalanceInSats){ + amountFormatted = "${tx.amount}"; + if (tx.fee != null) feeFormatted = "${tx.fee}"; + } + final _items = [ StandartListItem( title: S.current.transaction_details_transaction_id, @@ -353,13 +362,13 @@ abstract class TransactionDetailsViewModelBase with Store { ), StandartListItem( title: S.current.transaction_details_amount, - value: tx.amountFormatted(), + value: amountFormatted, key: ValueKey('standard_list_item_transaction_details_amount_key'), ), if (tx.feeFormatted()?.isNotEmpty ?? false) StandartListItem( title: S.current.transaction_details_fee, - value: tx.feeFormatted()!, + value: feeFormatted!, key: ValueKey('standard_list_item_transaction_details_fee_key'), ), ]; diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index fd6b29bdd5..7b0b52464c 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -30,7 +30,7 @@ import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_hidden import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; import 'package:cake_wallet/wownero/wownero.dart'; -import 'package:cw_core/amount_converter.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/currency.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; import 'package:cw_core/wallet_type.dart'; @@ -88,6 +88,12 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo @observable Currency selectedCurrency; + @computed + int get selectedCurrencyDecimals => useSatoshis ? 0 : selectedCurrency.decimals; + + @computed + bool get useSatoshis => selectedCurrency == CryptoCurrency.btc && _settingsStore.preferBalanceInSats; + @observable String searchText = ''; @@ -194,6 +200,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo } if (isElectrumWallet) { + final useSatoshi = wallet.type == WalletType.bitcoin && _settingsStore.preferBalanceInSats; if (bitcoin!.hasSelectedSilentPayments(wallet)) { final addressItems = bitcoin!.getSilentPaymentAddresses(wallet).map((address) { final isPrimary = address.id == 0; @@ -204,8 +211,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo name: address.name, address: address.address, txCount: address.txCount, - balance: AmountConverter.amountIntToString( - walletTypeToCryptoCurrency(type), address.balance), + balance: useSatoshi + ? "${address.balance}" + : walletTypeToCryptoCurrency(type).formatAmount(BigInt.from(address.balance)), isChange: address.isChange, ); }); @@ -220,8 +228,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo name: address.name, address: address.address, txCount: address.txCount, - balance: AmountConverter.amountIntToString( - walletTypeToCryptoCurrency(type), address.balance), + balance: useSatoshi + ? "${address.balance}" + : walletTypeToCryptoCurrency(type).formatAmount(BigInt.from(address.balance)), isChange: address.isChange, isOneTimeReceiveAddress: true, ); @@ -237,8 +246,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo name: subaddress.name, address: subaddress.address, txCount: subaddress.txCount, - balance: AmountConverter.amountIntToString( - walletTypeToCryptoCurrency(type), subaddress.balance), + balance: useSatoshi + ? "${subaddress.balance}" + : walletTypeToCryptoCurrency(type).formatAmount(BigInt.from(subaddress.balance)), isChange: subaddress.isChange); }); From 71c74e56e9ba3446a5e76f02885610cef24d1ea8 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 25 Nov 2025 16:45:07 +0100 Subject: [PATCH 03/19] refactor: consolidate fee formatting logic and add support for MWEB availability check --- lib/src/screens/send/widgets/send_card.dart | 10 ++-- lib/view_model/send/output.dart | 58 +++++++++++---------- lib/view_model/send/send_view_model.dart | 7 +++ 3 files changed, 40 insertions(+), 35 deletions(-) diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index df3af561ba..612ff97fe7 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -648,9 +648,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin Padding( padding: EdgeInsets.only(top: 14), diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index c27926df74..d0f7a4fed5 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -1,37 +1,37 @@ +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; import 'package:cake_wallet/entities/calculate_fiat_amount_raw.dart'; +import 'package:cake_wallet/entities/contact_base.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart'; +import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/currency_for_wallet_type.dart'; import 'package:cw_core/format_fixed.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_type.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'; -import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; -import 'package:cake_wallet/store/settings_store.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/arbitrum/arbitrum.dart'; - -import 'package:cake_wallet/entities/contact_base.dart'; part 'output.g.dart'; @@ -177,7 +177,12 @@ abstract class OutputBase with Store { switch (_wallet.type) { case WalletType.monero: - estimatedFee = monero!.formatterMoneroAmountToDouble(amount: fee).toString(); + case WalletType.wownero: + case WalletType.litecoin: + case WalletType.bitcoinCash: + case WalletType.dogecoin: + case WalletType.decred: + estimatedFee = walletTypeToCryptoCurrency(_wallet.type).formatAmount(BigInt.from(fee)); break; case WalletType.bitcoin: if (_settingsStore.priority[_wallet.type] == @@ -186,28 +191,21 @@ abstract class OutputBase with Store { _wallet, _settingsStore.customBitcoinFeeRate, formattedCryptoAmount); } - estimatedFee = bitcoin!.formatterBitcoinAmountToDouble(amount: fee).toString(); - break; - case WalletType.litecoin: - case WalletType.bitcoinCash: - case WalletType.dogecoin: - estimatedFee = bitcoin!.formatterBitcoinAmountToDouble(amount: fee).toString(); + if (_settingsStore.preferBalanceInSats) { + estimatedFee = "$fee"; + } else { + estimatedFee = walletTypeToCryptoCurrency(_wallet.type).formatAmount(BigInt.from(fee)); + } break; case WalletType.solana: estimatedFee = solana!.getEstimateFees(_wallet).toString(); break; - case WalletType.wownero: - estimatedFee = wownero!.formatterWowneroAmountToDouble(amount: fee).toString(); - break; case WalletType.zano: estimatedFee = zano! .formatterIntAmountToDouble( amount: fee, currency: cryptoCurrencyHandler(), forFee: true) .toString(); break; - case WalletType.decred: - estimatedFee = decred!.formatterDecredAmountToDouble(amount: fee).toString(); - break; case WalletType.tron: if (cryptoCurrencyHandler() == CryptoCurrency.trx) { estimatedFee = tron!.getTronNativeEstimatedFee(_wallet).toString(); @@ -273,10 +271,14 @@ abstract class OutputBase with Store { [WalletType.solana, WalletType.tron].contains(_wallet.type)) ? _wallet.currency : cryptoCurrencyHandler(); - final fiat = calculateFiatAmountRaw( - price: _fiatConversationStore.prices[currency]!, - cryptoAmount: double.parse(estimatedFee)); - return fiat; + + var cryptoAmount = double.parse(estimatedFee); + if (_settingsStore.preferBalanceInSats) { + cryptoAmount = double.parse(currency.formatAmount(BigInt.parse(estimatedFee))); + } + + return calculateFiatAmountRaw( + price: _fiatConversationStore.prices[currency]!, cryptoAmount: cryptoAmount); } catch (_) { return '0.00'; } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 3d5f870ae0..eba66a16b9 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -136,6 +136,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor bool get isMwebEnabled => balanceViewModel.mwebEnabled; + bool get isMwebAvailable => wallet.currency == CryptoCurrency.ltc && balanceViewModel.mwebEnabled; + bool get isEVMWallet => isEVMCompatibleChain(walletType); @action void setShowAddressBookPopup(bool value) { @@ -236,6 +238,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor CryptoCurrency get currency => wallet.currency; + String get feeCurrencySymbol => + wallet.currency == CryptoCurrency.btc && _settingsStore.preferBalanceInSats + ? "SATS" + : currency.toString(); + Validator amountValidator(Output output) => AmountValidator( currency: walletTypeToCryptoCurrency(wallet.type), minValue: isSendToSilentPayments(output) From fab275e324443fc4c55f642c5bc9c25860bc69cc Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 25 Nov 2025 17:33:35 +0100 Subject: [PATCH 04/19] feat: add support for satoshi-based amount display and enhance fiat conversion logic --- lib/src/screens/send/widgets/send_card.dart | 4 +-- lib/view_model/send/output.dart | 36 +++++++++++++++------ 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index 612ff97fe7..150bd820e2 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -506,8 +506,8 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin parsedAddress.parseFrom != ParseFrom.notParsed && parsedAddress.name.isNotEmpty; + @computed + bool get useSatoshis => walletType == WalletType.bitcoin && _settingsStore.preferBalanceInSats; + @observable String? stealthAddress; @@ -113,7 +117,11 @@ abstract class OutputBase with Store { case WalletType.litecoin: case WalletType.bitcoinCash: case WalletType.dogecoin: - _amount = bitcoin!.formatterStringDoubleToBitcoinAmount(_cryptoAmount); + if (useSatoshis) { + _amount = int.parse(_cryptoAmount); + } else { + _amount = bitcoin!.formatterStringDoubleToBitcoinAmount(_cryptoAmount); + } break; case WalletType.decred: _amount = decred!.formatterStringDoubleToDecredAmount(_cryptoAmount); @@ -191,11 +199,9 @@ abstract class OutputBase with Store { _wallet, _settingsStore.customBitcoinFeeRate, formattedCryptoAmount); } - if (_settingsStore.preferBalanceInSats) { - estimatedFee = "$fee"; - } else { - estimatedFee = walletTypeToCryptoCurrency(_wallet.type).formatAmount(BigInt.from(fee)); - } + estimatedFee = useSatoshis + ? "$fee" + : walletTypeToCryptoCurrency(_wallet.type).formatAmount(BigInt.from(fee)); break; case WalletType.solana: estimatedFee = solana!.getEstimateFees(_wallet).toString(); @@ -273,7 +279,7 @@ abstract class OutputBase with Store { : cryptoCurrencyHandler(); var cryptoAmount = double.parse(estimatedFee); - if (_settingsStore.preferBalanceInSats) { + if (useSatoshis) { cryptoAmount = double.parse(currency.formatAmount(BigInt.parse(estimatedFee))); } @@ -341,10 +347,16 @@ abstract class OutputBase with Store { @action void _updateFiatAmount() { try { + var cryptoAmount_ = sendAll ? cryptoFullBalance.replaceAll(",", ".") : cryptoAmount.replaceAll(',', '.'); + + if (useSatoshis) { + cryptoAmount_ = + walletTypeToCryptoCurrency(walletType).formatAmount(BigInt.parse(cryptoAmount_)); + } + final fiat = calculateFiatAmount( price: _fiatConversationStore.prices[cryptoCurrencyHandler()]!, - cryptoAmount: - sendAll ? cryptoFullBalance.replaceAll(",", ".") : cryptoAmount.replaceAll(',', '.')); + cryptoAmount: cryptoAmount_); if (fiatAmount != fiat) { fiatAmount = fiat; } @@ -360,7 +372,11 @@ abstract class OutputBase with Store { _fiatConversationStore.prices[cryptoCurrencyHandler()]!; final cryptoAmountTmp = _cryptoNumberFormat.format(crypto); if (cryptoAmount != cryptoAmountTmp) { - cryptoAmount = cryptoAmountTmp; + if (useSatoshis) { + cryptoAmount = parseFixed(cryptoAmountTmp, 8).toString(); + } else { + cryptoAmount = cryptoAmountTmp; + } } } catch (e) { cryptoAmount = ''; From ea4cfa0462aecad24d7285123794122ac05154b3 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Wed, 26 Nov 2025 15:59:57 +0100 Subject: [PATCH 05/19] feat: add support for satoshi-based amount parsing, formatting, and display preferences across buy/sell flow and UI adjustments --- cw_core/lib/crypto_currency.dart | 9 ++- lib/src/screens/buy/buy_sell_page.dart | 1 + .../exchange/widgets/exchange_card.dart | 7 +- lib/view_model/buy/buy_sell_view_model.dart | 69 ++++++++++++------- 4 files changed, 60 insertions(+), 26 deletions(-) diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index 5f96aa081f..e2a42169a0 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -2,6 +2,7 @@ import 'package:cw_core/currency.dart'; import 'package:cw_core/enumerable_item.dart'; import 'package:collection/collection.dart'; import 'package:cw_core/format_fixed.dart'; +import 'package:cw_core/parse_fixed.dart'; class CryptoCurrency extends EnumerableItem with Serializable implements Currency { const CryptoCurrency({ @@ -396,7 +397,13 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen bool titleAndTagEqual(CryptoCurrency other) => title == other.title && tag == other.tag; - /// Format the raw raw amount into its decimal representation eg. turn Sats into Bitcoin + /// 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/src/screens/buy/buy_sell_page.dart b/lib/src/screens/buy/buy_sell_page.dart index 53c8cdec95..7517cafabd 100644 --- a/lib/src/screens/buy/buy_sell_page.dart +++ b/lib/src/screens/buy/buy_sell_page.dart @@ -453,6 +453,7 @@ class BuySellPage extends BasePage { fillColor: buySellViewModel.isBuyAction ? Theme.of(context).colorScheme.surfaceContainerLow : Theme.of(context).colorScheme.surfaceContainer, + useSatoshis: buySellViewModel.useSatoshis, ), ); diff --git a/lib/src/screens/exchange/widgets/exchange_card.dart b/lib/src/screens/exchange/widgets/exchange_card.dart index 15426b668b..86f98c7dac 100644 --- a/lib/src/screens/exchange/widgets/exchange_card.dart +++ b/lib/src/screens/exchange/widgets/exchange_card.dart @@ -47,6 +47,7 @@ class ExchangeCard extends StatefulWidget { this.onPushPasteButton, this.onPushAddressBookButton, this.onDispose, + this.useSatoshis = false, required this.cardInstanceName, }) : super(key: key); @@ -82,6 +83,7 @@ class ExchangeCard extends StatefulWidget { final Function()? onDispose; final String cardInstanceName; final Color fillColor; + final bool useSatoshis; @override ExchangeCardState createState() => ExchangeCardState(); @@ -136,6 +138,7 @@ class ExchangeCardState extends State> { } void changeLimits({String? min, String? max}) { + setState(() { _min = min; _max = max; @@ -224,8 +227,8 @@ class ExchangeCardState extends State> { currencyAmountTextFieldWidgetKey: ValueKey('${_cardInstanceName}_currency_amount_textfield_widget_key'), imageArrow: widget.imageArrow, - selectedCurrency: _selectedCurrency.toString(), - selectedCurrencyDecimals: _selectedCurrency.decimals, + selectedCurrency: widget.useSatoshis ? "SATS" : "$_selectedCurrency", + selectedCurrencyDecimals: widget.useSatoshis ? 0 : _selectedCurrency.decimals, amountFocusNode: widget.amountFocusNode, amountController: amountController, onTapPicker: () => _presentPicker(context), diff --git a/lib/view_model/buy/buy_sell_view_model.dart b/lib/view_model/buy/buy_sell_view_model.dart index 76e6e9edf2..0ddb294442 100644 --- a/lib/view_model/buy/buy_sell_view_model.dart +++ b/lib/view_model/buy/buy_sell_view_model.dart @@ -12,6 +12,7 @@ import 'package:cake_wallet/entities/provider_types.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/store/app_store.dart'; +import 'package:cw_core/crypto_amount_format.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:flutter/cupertino.dart'; import 'package:intl/intl.dart'; @@ -80,10 +81,16 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S double get amount { final formattedFiatAmount = double.tryParse(fiatAmount) ?? 200.0; - final formattedCryptoAmount = - double.tryParse(cryptoAmount) ?? (cryptoCurrency == CryptoCurrency.btc ? 0.001 : 1); + var formattedCryptoAmount = double.tryParse(cryptoAmount); - return isBuyAction ? formattedFiatAmount : formattedCryptoAmount; + if (useSatoshis && cryptoAmount.isNotEmpty) { + formattedCryptoAmount = + double.tryParse(cryptoCurrency.formatAmount(BigInt.parse(cryptoAmount))); + } + + return isBuyAction + ? formattedFiatAmount + : formattedCryptoAmount ?? (cryptoCurrency == CryptoCurrency.btc ? 0.001 : 1); } final AppStore _appStore; @@ -143,6 +150,10 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S @observable bool skipIsReadyToTradeReaction = false; + @computed + bool get useSatoshis => + _appStore.settingsStore.preferBalanceInSats && cryptoCurrency == CryptoCurrency.btc; + @computed bool get isReadyToTrade { final hasSelectedQuote = selectedQuote != null; @@ -215,11 +226,18 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S } if (bestRateQuote != null) { - _cryptoNumberFormat.maximumFractionDigits = cryptoCurrency.decimals; - cryptoAmount = _cryptoNumberFormat - .format(enteredAmount / bestRateQuote!.rate) - .toString() - .replaceAll(RegExp('\\,'), ''); + final amount = enteredAmount / bestRateQuote!.rate; + if (useSatoshis) { + cryptoAmount = cryptoCurrency + .parseAmount(amount.toString().withMaxDecimals(cryptoCurrency.decimals)) + .toString(); + } else { + _cryptoNumberFormat.maximumFractionDigits = cryptoCurrency.decimals; + cryptoAmount = _cryptoNumberFormat + .format(enteredAmount / bestRateQuote!.rate) + .toString() + .replaceAll(RegExp('\\,'), ''); + } } else { await calculateBestRate(); } @@ -235,6 +253,10 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S return; } + if (useSatoshis) { + amount = cryptoCurrency.formatAmount(BigInt.parse(amount)); + } + final enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0; if (!isReadyToTrade && !isBuySellQuoteFailed) { @@ -355,12 +377,11 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S Future _getAvailablePaymentTypes() async { paymentMethodState = PaymentMethodLoading(); selectedPaymentMethod = null; - final result = await Future.wait(providerList.map((element) => element - .getAvailablePaymentTypes(fiatCurrency.title, cryptoCurrency, isBuyAction) - .timeout( - Duration(seconds: 10), - onTimeout: () => [], - ))); + final result = await Future.wait(providerList.map((element) => + element.getAvailablePaymentTypes(fiatCurrency.title, cryptoCurrency, isBuyAction).timeout( + Duration(seconds: 10), + onTimeout: () => [], + ))); final List tempPaymentMethods = []; @@ -395,11 +416,11 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S final List validProviders = providerList.where((provider) { if (isBuyAction) { - return provider.supportedCryptoList.any((pair) => - pair.from == cryptoCurrency && pair.to == fiatCurrency); + return provider.supportedCryptoList + .any((pair) => pair.from == cryptoCurrency && pair.to == fiatCurrency); } else { - return provider.supportedFiatList.any((pair) => - pair.from == fiatCurrency && pair.to == cryptoCurrency); + return provider.supportedFiatList + .any((pair) => pair.from == fiatCurrency && pair.to == cryptoCurrency); } }).toList(); @@ -445,10 +466,11 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S return true; }).toList(); - final List successRateQuotes = validQuotes.where((element) => - element.provider is OnRamperBuyProvider && - element.recommendations.contains(ProviderRecommendation.successRate) - ).toList(); + final List successRateQuotes = validQuotes + .where((element) => + element.provider is OnRamperBuyProvider && + element.recommendations.contains(ProviderRecommendation.successRate)) + .toList(); for (final quote in successRateQuotes) { if (!uniqueProviderQuotes.contains(quote)) { @@ -495,7 +517,8 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S if (e.toString().contains("403")) { buySellQuotState = BuySellQuotFailed(errorMessage: "Using Tor is not supported"); } else { - buySellQuotState = BuySellQuotFailed(errorMessage: "Something went wrong please try again later"); + buySellQuotState = + BuySellQuotFailed(errorMessage: "Something went wrong please try again later"); } } } From 10203dc0a499d55d211b2d305cb9cb3b4131e1cf Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 2 Dec 2025 12:07:46 +0100 Subject: [PATCH 06/19] feat: implement `AmountParsingProxy` with unit tests for parsing and formatting crypto amounts in various display modes --- lib/core/amount.dart | 37 ----- lib/core/amount_parsing_proxy.dart | 32 +++++ lib/entities/bitcoin_amount_display_mode.dart | 32 +++++ lib/entities/preferences_key.dart | 1 + test/core/amount_parsing_proxy_test.dart | 132 ++++++++++++++++++ 5 files changed, 197 insertions(+), 37 deletions(-) delete mode 100644 lib/core/amount.dart create mode 100644 lib/core/amount_parsing_proxy.dart create mode 100644 lib/entities/bitcoin_amount_display_mode.dart create mode 100644 test/core/amount_parsing_proxy_test.dart diff --git a/lib/core/amount.dart b/lib/core/amount.dart deleted file mode 100644 index 3d64f1d669..0000000000 --- a/lib/core/amount.dart +++ /dev/null @@ -1,37 +0,0 @@ -// abstract class Amount { -// Amount(this.value); - -// int value; - -// int minorDigits; - -// String code; - -// String formatted(); -// } - -// class MoneroAmount extends Amount { -// MoneroAmount(int value) : super(value) { -// minorDigits = 12; -// code = 'XMR'; -// } - -// // const moneroAmountLength = 12; -// // const moneroAmountDivider = 1000000000000; -// // final moneroAmountFormat = NumberFormat() -// // ..maximumFractionDigits = moneroAmountLength -// // ..minimumFractionDigits = 1; - -// // String moneroAmountToString({int amount}) => -// // moneroAmountFormat.format(cryptoAmountToDouble(amount: amount, divider: moneroAmountDivider)); - -// // double moneroAmountToDouble({int amount}) => cryptoAmountToDouble(amount: amount, divider: moneroAmountDivider); - -// // int moneroParseAmount({String amount}) => moneroAmountFormat.parse(amount).toInt(); - -// @override -// String formatted() { -// // TODO: implement formatted -// throw UnimplementedError(); -// } -// } diff --git a/lib/core/amount_parsing_proxy.dart b/lib/core/amount_parsing_proxy.dart new file mode 100644 index 0000000000..5eb88a3f31 --- /dev/null +++ b/lib/core/amount_parsing_proxy.dart @@ -0,0 +1,32 @@ +import 'package:cake_wallet/entities/bitcoin_amount_display_mode.dart'; +import 'package:cw_core/crypto_currency.dart'; + +class AmountParsingProxy { + final BitcoinAmountDisplayMode displayMode; + + const AmountParsingProxy(this.displayMode); + + /// [getCryptoInputAmount] turns the input [amount] into the canonical representation of [cryptoCurrency] + String getCryptoInputAmount(String amount, CryptoCurrency cryptoCurrency) { + if (_requiresConversion(cryptoCurrency)) { + return cryptoCurrency.formatAmount(BigInt.parse(amount)); + } + + return amount; + } + + /// [getCryptoOutputAmount] turns the input [amount] into the preferred representation of [cryptoCurrency] + String getCryptoOutputAmount(String amount, CryptoCurrency cryptoCurrency) { + if (_requiresConversion(cryptoCurrency)) { + return cryptoCurrency.parseAmount(amount).toString(); + } + + return amount; + } + + bool _requiresConversion(CryptoCurrency cryptoCurrency) => + ([CryptoCurrency.btc, CryptoCurrency.btcln].contains(cryptoCurrency) && + displayMode == BitcoinAmountDisplayMode.satoshi) || + (CryptoCurrency.btcln == cryptoCurrency && + displayMode == BitcoinAmountDisplayMode.satoshiForLightning); +} diff --git a/lib/entities/bitcoin_amount_display_mode.dart b/lib/entities/bitcoin_amount_display_mode.dart new file mode 100644 index 0000000000..ea15dcfd82 --- /dev/null +++ b/lib/entities/bitcoin_amount_display_mode.dart @@ -0,0 +1,32 @@ +import 'package:cw_core/enumerable_item.dart'; + +class BitcoinAmountDisplayMode extends EnumerableItem with Serializable { + const BitcoinAmountDisplayMode({required String title, required int raw}) + : super(title: title, raw: raw); + + static const all = [ + BitcoinAmountDisplayMode.satoshiForLightning, + BitcoinAmountDisplayMode.bitcoin, + BitcoinAmountDisplayMode.satoshi, + ]; + static const satoshiForLightning = + BitcoinAmountDisplayMode(raw: 0, title: 'Lightning in Satoshi'); + static const bitcoin = BitcoinAmountDisplayMode(raw: 1, title: 'Bitcoin'); + static const satoshi = BitcoinAmountDisplayMode(raw: 2, title: 'Satoshi'); + + static BitcoinAmountDisplayMode deserialize({required int raw}) { + switch (raw) { + case 0: + return satoshiForLightning; + case 1: + return bitcoin; + case 2: + return satoshi; + default: + throw Exception('Unexpected token: $raw for BalanceDisplayMode deserialize'); + } + } + + @override + String toString() => title; +} diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 6fc6c6df52..31b44beced 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -24,6 +24,7 @@ class PreferencesKey { static const currentWowneroNodeIdKey = 'current_node_id_wow'; static const currentTransactionPriorityKeyLegacy = 'current_fee_priority'; static const currentBalanceDisplayModeKey = 'current_balance_display_mode'; + static const displayAmountsInSatoshi = 'display_amounts_in_satoshi'; static const preferBalanceInSats = 'prefer_sats'; static const shouldSaveRecipientAddressKey = 'save_recipient_address'; static const isAppSecureKey = 'is_app_secure'; diff --git a/test/core/amount_parsing_proxy_test.dart b/test/core/amount_parsing_proxy_test.dart new file mode 100644 index 0000000000..69a17a0816 --- /dev/null +++ b/test/core/amount_parsing_proxy_test.dart @@ -0,0 +1,132 @@ +import "package:cake_wallet/core/amount_parsing_proxy.dart"; +import "package:cake_wallet/entities/bitcoin_amount_display_mode.dart"; +import "package:cw_core/crypto_currency.dart"; +import "package:flutter_test/flutter_test.dart"; + +void main() { + group("AmountParsingProxy", () { + group("BitcoinAmountDisplayMode.satoshi", () { + final amountParsingProxy = AmountParsingProxy(BitcoinAmountDisplayMode.satoshi); + + group("getCryptoInputAmount", () { + test("Amount should be parsed from Satoshi for Bitcoin", () { + final amount = amountParsingProxy.getCryptoInputAmount("100", CryptoCurrency.btc); + expect(amount, "0.000001"); + }); + + test("Amount should be parsed from Satoshi for Bitcoin Lightning", () { + final amount = amountParsingProxy.getCryptoInputAmount("100", CryptoCurrency.btcln); + expect(amount, "0.000001"); + }); + + test("Amount should not be parsed from Satoshi for Ethereum", () { + final amount = amountParsingProxy.getCryptoInputAmount("100", CryptoCurrency.eth); + expect(amount, "100"); + }); + }); + + group("getCryptoOutputAmount", () { + test("Amount should be formated to Satoshi for Bitcoin", () { + final amount = amountParsingProxy.getCryptoOutputAmount("0.000001", CryptoCurrency.btc); + expect(amount, "100"); + }); + + test("Amount should be formated to Satoshi for Bitcoin Lightning", () { + final amount = amountParsingProxy.getCryptoOutputAmount("100", CryptoCurrency.btcln); + expect(amount, "10000000000"); + }); + + test("Amount should not be formated to Satoshi for Ethereum", () { + final amount = amountParsingProxy.getCryptoOutputAmount("100", CryptoCurrency.eth); + expect(amount, "100"); + }); + }); + }); + + group("BitcoinAmountDisplayMode.bitcoin", () { + final amountParsingProxy = AmountParsingProxy(BitcoinAmountDisplayMode.bitcoin); + + group("getCryptoInputAmount", () { + test("Amount should not change for Bitcoin", () { + final amount = amountParsingProxy.getCryptoInputAmount("0.000001", CryptoCurrency.btc); + expect(amount, "0.000001"); + }); + + test("Amount should not change for Bitcoin: potentially wrong input Satoshi", () { + final amount = amountParsingProxy.getCryptoInputAmount("100", CryptoCurrency.btc); + expect(amount, "100"); + }); + + test("Amount should not change for Bitcoin", () { + final amount = amountParsingProxy.getCryptoInputAmount("0.000001", CryptoCurrency.btcln); + expect(amount, "0.000001"); + }); + + test("Amount should not change for Bitcoin Lightning: potentially wrong input Satoshi", () { + final amount = amountParsingProxy.getCryptoInputAmount("100", CryptoCurrency.btcln); + expect(amount, "100"); + }); + + test("Amount should not change on ETH", () { + final amount = amountParsingProxy.getCryptoInputAmount("100", CryptoCurrency.eth); + expect(amount, "100"); + }); + }); + + group("getCryptoOutputAmount", () { + test("Amount should not be formated to Satoshi for Bitcoin", () { + final amount = amountParsingProxy.getCryptoOutputAmount("0.000001", CryptoCurrency.btc); + expect(amount, "0.000001"); + }); + + test("Amount should not be formated to Satoshi for Bitcoin Lightning", () { + final amount = amountParsingProxy.getCryptoOutputAmount("100", CryptoCurrency.btcln); + expect(amount, "100"); + }); + + test("Amount should not be formated to Satoshi for Ethereum", () { + final amount = amountParsingProxy.getCryptoOutputAmount("100", CryptoCurrency.eth); + expect(amount, "100"); + }); + }); + }); + + group("BitcoinAmountDisplayMode.satoshiForLightning", () { + final amountParsingProxy = AmountParsingProxy(BitcoinAmountDisplayMode.satoshiForLightning); + + group("getCryptoInputAmount", () { + test("Amount should not change for Bitcoin", () { + final amount = amountParsingProxy.getCryptoInputAmount("100", CryptoCurrency.btc); + expect(amount, "100"); + }); + + test("Amount should get formated from Satoshi for Bitcoin Lightning", () { + final amount = amountParsingProxy.getCryptoInputAmount("100", CryptoCurrency.btcln); + expect(amount, "0.000001"); + }); + + test("Amount should not change for Ethereum", () { + final amount = amountParsingProxy.getCryptoInputAmount("100", CryptoCurrency.eth); + expect(amount, "100"); + }); + }); + + group("getCryptoOutputAmount", () { + test("Amount should not be formated to Satoshi for Bitcoin", () { + final amount = amountParsingProxy.getCryptoOutputAmount("0.000001", CryptoCurrency.btc); + expect(amount, "0.000001"); + }); + + test("Amount should be formated to Satoshi for Bitcoin Lightning", () { + final amount = amountParsingProxy.getCryptoOutputAmount("0.000001", CryptoCurrency.btcln); + expect(amount, "100"); + }); + + test("Amount should not be formated to Satoshi for Ethereum", () { + final amount = amountParsingProxy.getCryptoOutputAmount("100", CryptoCurrency.eth); + expect(amount, "100"); + }); + }); + }); + }); +} From 7b3317aa03de7aebc670fd16938fdea305a5cf4e Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 2 Dec 2025 13:59:20 +0100 Subject: [PATCH 07/19] feat: replace `preferBalanceInSats` with `displayAmountsInSatoshi` and introduce `AmountParsingProxy` for unified crypto amount handling across the app --- lib/core/amount_parsing_proxy.dart | 24 +++++++-- lib/di.dart | 3 ++ lib/entities/preferences_key.dart | 1 - lib/src/screens/buy/buy_sell_page.dart | 2 +- lib/src/screens/exchange/exchange_page.dart | 2 + .../screens/receive/widgets/qr_widget.dart | 2 +- lib/src/screens/send/widgets/send_card.dart | 4 +- .../settings/display_settings_page.dart | 12 +++-- lib/store/settings_store.dart | 15 +++--- lib/view_model/buy/buy_sell_view_model.dart | 45 ++++++----------- .../dashboard/balance_view_model.dart | 8 ++- .../dashboard/transaction_list_item.dart | 9 +++- .../exchange/exchange_view_model.dart | 50 ++++++++++++------- lib/view_model/send/output.dart | 37 ++++++-------- lib/view_model/send/send_view_model.dart | 20 ++++---- .../settings/display_settings_view_model.dart | 5 +- .../transaction_details_view_model.dart | 20 ++++---- .../wallet_address_list_view_model.dart | 24 ++++----- 18 files changed, 156 insertions(+), 127 deletions(-) diff --git a/lib/core/amount_parsing_proxy.dart b/lib/core/amount_parsing_proxy.dart index 5eb88a3f31..26d4cc04e8 100644 --- a/lib/core/amount_parsing_proxy.dart +++ b/lib/core/amount_parsing_proxy.dart @@ -8,7 +8,7 @@ class AmountParsingProxy { /// [getCryptoInputAmount] turns the input [amount] into the canonical representation of [cryptoCurrency] String getCryptoInputAmount(String amount, CryptoCurrency cryptoCurrency) { - if (_requiresConversion(cryptoCurrency)) { + if (useSatoshi(cryptoCurrency) && amount.isNotEmpty) { return cryptoCurrency.formatAmount(BigInt.parse(amount)); } @@ -17,14 +17,32 @@ class AmountParsingProxy { /// [getCryptoOutputAmount] turns the input [amount] into the preferred representation of [cryptoCurrency] String getCryptoOutputAmount(String amount, CryptoCurrency cryptoCurrency) { - if (_requiresConversion(cryptoCurrency)) { + if (useSatoshi(cryptoCurrency) && amount.isNotEmpty) { return cryptoCurrency.parseAmount(amount).toString(); } return amount; } - bool _requiresConversion(CryptoCurrency cryptoCurrency) => + /// [getCryptoStringRepresentation] turns the input [amount] into the preferred representation of [cryptoCurrency] + String getCryptoString(int amount, CryptoCurrency cryptoCurrency) { + if (useSatoshi(cryptoCurrency)) { + return "$amount"; + } + + return cryptoCurrency.formatAmount(BigInt.from(amount)); + } + + /// [parseCryptoString] turns the input [string] into a `BigInt presentation of the [cryptoCurrency] + BigInt parseCryptoString(String amount, CryptoCurrency cryptoCurrency) { + if (useSatoshi(cryptoCurrency)) { + return BigInt.parse(amount); + } + + return cryptoCurrency.parseAmount(amount); + } + + bool useSatoshi(CryptoCurrency cryptoCurrency) => ([CryptoCurrency.btc, CryptoCurrency.btcln].contains(cryptoCurrency) && displayMode == BitcoinAmountDisplayMode.satoshi) || (CryptoCurrency.btcln == cryptoCurrency && diff --git a/lib/di.dart b/lib/di.dart index 2d793144e8..0f9af9b4d1 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -11,6 +11,7 @@ import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart'; import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; +import 'package:cake_wallet/core/amount_parsing_proxy.dart'; import 'package:cake_wallet/order/order.dart'; import 'package:cake_wallet/core/backup_service_v3.dart'; import 'package:cake_wallet/core/new_wallet_arguments.dart'; @@ -396,6 +397,8 @@ Future setup({ AnonpayTransactionsStore(anonpayInvoiceInfoSource: _anonpayInvoiceInfoSource)); getIt.registerSingleton(SeedSettingsStore()); + getIt.registerFactory(() => AmountParsingProxy(getIt().displayAmountsInSatoshi)); + getIt.registerFactoryParam((type, _) { switch(type) { case HardwareWalletType.bitbox: return getIt(); diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 31b44beced..382615328e 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -25,7 +25,6 @@ class PreferencesKey { static const currentTransactionPriorityKeyLegacy = 'current_fee_priority'; static const currentBalanceDisplayModeKey = 'current_balance_display_mode'; static const displayAmountsInSatoshi = 'display_amounts_in_satoshi'; - static const preferBalanceInSats = 'prefer_sats'; static const shouldSaveRecipientAddressKey = 'save_recipient_address'; static const isAppSecureKey = 'is_app_secure'; static const disableTradeOption = 'disable_buy'; diff --git a/lib/src/screens/buy/buy_sell_page.dart b/lib/src/screens/buy/buy_sell_page.dart index 7517cafabd..b74192795f 100644 --- a/lib/src/screens/buy/buy_sell_page.dart +++ b/lib/src/screens/buy/buy_sell_page.dart @@ -453,7 +453,7 @@ class BuySellPage extends BasePage { fillColor: buySellViewModel.isBuyAction ? Theme.of(context).colorScheme.surfaceContainerLow : Theme.of(context).colorScheme.surfaceContainer, - useSatoshis: buySellViewModel.useSatoshis, + useSatoshis: buySellViewModel.useSatoshi, ), ); diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index 32a1b4eeb8..4e9403bd35 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -734,6 +734,7 @@ class ExchangePage extends BasePage { exchangeViewModel.depositAddress = await fetchParsedAddress(context, domain, exchangeViewModel.depositCurrency); }, + useSatoshis: exchangeViewModel.useSatoshiDeposit, ), ); @@ -781,6 +782,7 @@ class ExchangePage extends BasePage { exchangeViewModel.receiveAddress = await fetchParsedAddress(context, domain, exchangeViewModel.receiveCurrency); }, + useSatoshis: exchangeViewModel.useSatoshisReceive, ), ); diff --git a/lib/src/screens/receive/widgets/qr_widget.dart b/lib/src/screens/receive/widgets/qr_widget.dart index a7687c898d..7d38c973a6 100644 --- a/lib/src/screens/receive/widgets/qr_widget.dart +++ b/lib/src/screens/receive/widgets/qr_widget.dart @@ -283,7 +283,7 @@ class QRWidget extends StatelessWidget { String get _currencyName { if (addressListViewModel.selectedCurrency is CryptoCurrency) { - if (addressListViewModel.useSatoshis) return "SATS"; + if (addressListViewModel.useSatoshi) return "SATS"; return (addressListViewModel.selectedCurrency as CryptoCurrency).title.toUpperCase(); } return addressListViewModel.selectedCurrency.name.toUpperCase(); diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index 150bd820e2..3a284fb6af 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -506,8 +506,8 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin mode.title, isGridView: false, ), - SettingsSwitcherCell( - title: "Prefer sats", // ToDo: - value: _displaySettingsViewModel.preferBalanceInSats, - onValueChange: (_, bool value) => _displaySettingsViewModel.setPreferBalanceInSats(value), + SettingsPickerCell( + title: "Bitcoin Amount Display", // ToDo (Konsti) + items: BitcoinAmountDisplayMode.all, + selectedItem: _displaySettingsViewModel.displayAmountsInSatoshi, + onItemSelected: _displaySettingsViewModel.setDisplayAmountsInSatoshi, + displayItem: (mode) => mode.title, + isGridView: false, ), //if (!isHaven) it does not work correctly if (!_displaySettingsViewModel.disabledFiatApiMode) diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 026412406d..85876e5f60 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -12,6 +12,7 @@ import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/action_list_display_mode.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/balance_display_mode.dart'; +import 'package:cake_wallet/entities/bitcoin_amount_display_mode.dart'; import 'package:cake_wallet/entities/cake_2fa_preset_options.dart'; import 'package:cake_wallet/entities/country.dart'; import 'package:cake_wallet/entities/default_settings_migration.dart'; @@ -90,7 +91,7 @@ abstract class SettingsStoreBase with Store { required this.deviceName, required Map nodes, required Map powNodes, - required this.preferBalanceInSats, + required this.displayAmountsInSatoshi, required this.shouldShowYatPopup, required this.shouldShowDEuroDisclaimer, required this.shouldShowRepWarning, @@ -429,8 +430,9 @@ abstract class SettingsStoreBase with Store { PreferencesKey.currentBalanceDisplayModeKey, mode.serialize())); reaction( - (_) => preferBalanceInSats, - (bool _preferBalanceInSats) => sharedPreferences.setBool(PreferencesKey.preferBalanceInSats, _preferBalanceInSats)); + (_) => displayAmountsInSatoshi, + (BitcoinAmountDisplayMode displayAmountsInSatoshi) => sharedPreferences.setInt( + PreferencesKey.displayAmountsInSatoshi, displayAmountsInSatoshi.raw)); reaction((_) => currentSyncMode, (SyncMode syncMode) { sharedPreferences.setInt(PreferencesKey.syncModeKey, syncMode.type.index); @@ -726,7 +728,7 @@ abstract class SettingsStoreBase with Store { BalanceDisplayMode balanceDisplayMode; @observable - bool preferBalanceInSats; + BitcoinAmountDisplayMode displayAmountsInSatoshi; @observable FiatApiMode fiatApiMode; @@ -1052,7 +1054,8 @@ abstract class SettingsStoreBase with Store { final currentBalanceDisplayMode = BalanceDisplayMode.deserialize( raw: sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey)!); - final preferBalanceInSats = sharedPreferences.getBool(PreferencesKey.preferBalanceInSats) ?? false; + final displayAmountsInSatoshi = BitcoinAmountDisplayMode.deserialize( + raw: sharedPreferences.getInt(PreferencesKey.displayAmountsInSatoshi) ?? 0); // FIX-ME: Check for which default value we should have here final shouldSaveRecipientAddress = sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ?? false; @@ -1401,7 +1404,7 @@ abstract class SettingsStoreBase with Store { powNodes: powNodes, appVersion: packageInfo.version, deviceName: deviceName, - preferBalanceInSats: preferBalanceInSats, + displayAmountsInSatoshi: displayAmountsInSatoshi, isBitcoinBuyEnabled: isBitcoinBuyEnabled, initialFiatCurrency: currentFiatCurrency, initialCakePayCountry: currentCakePayCountry, diff --git a/lib/view_model/buy/buy_sell_view_model.dart b/lib/view_model/buy/buy_sell_view_model.dart index 0ddb294442..db56463221 100644 --- a/lib/view_model/buy/buy_sell_view_model.dart +++ b/lib/view_model/buy/buy_sell_view_model.dart @@ -5,14 +5,15 @@ import 'package:cake_wallet/buy/buy_quote.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; import 'package:cake_wallet/buy/payment_method.dart'; import 'package:cake_wallet/buy/sell_buy_states.dart'; +import 'package:cake_wallet/core/amount_parsing_proxy.dart'; import 'package:cake_wallet/core/selectable_option.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; +import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/entities/provider_types.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/store/app_store.dart'; -import 'package:cw_core/crypto_amount_format.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:flutter/cupertino.dart'; import 'package:intl/intl.dart'; @@ -80,16 +81,12 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S } double get amount { - final formattedFiatAmount = double.tryParse(fiatAmount) ?? 200.0; - var formattedCryptoAmount = double.tryParse(cryptoAmount); - - if (useSatoshis && cryptoAmount.isNotEmpty) { - formattedCryptoAmount = - double.tryParse(cryptoCurrency.formatAmount(BigInt.parse(cryptoAmount))); - } + final formattedFiatAmount = double.tryParse(fiatAmount); + var formattedCryptoAmount = double.tryParse( + getIt().getCryptoInputAmount(cryptoAmount, cryptoCurrency)); return isBuyAction - ? formattedFiatAmount + ? formattedFiatAmount ?? 200.0 : formattedCryptoAmount ?? (cryptoCurrency == CryptoCurrency.btc ? 0.001 : 1); } @@ -150,10 +147,6 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S @observable bool skipIsReadyToTradeReaction = false; - @computed - bool get useSatoshis => - _appStore.settingsStore.preferBalanceInSats && cryptoCurrency == CryptoCurrency.btc; - @computed bool get isReadyToTrade { final hasSelectedQuote = selectedQuote != null; @@ -175,6 +168,9 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S ? (buySellQuotState as BuySellQuotFailed).errorMessage : null; + @computed + bool get useSatoshi => getIt().useSatoshi(cryptoCurrency); + @action void reset() { cryptoCurrency = wallet.currency; @@ -227,17 +223,12 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S if (bestRateQuote != null) { final amount = enteredAmount / bestRateQuote!.rate; - if (useSatoshis) { - cryptoAmount = cryptoCurrency - .parseAmount(amount.toString().withMaxDecimals(cryptoCurrency.decimals)) - .toString(); - } else { - _cryptoNumberFormat.maximumFractionDigits = cryptoCurrency.decimals; - cryptoAmount = _cryptoNumberFormat - .format(enteredAmount / bestRateQuote!.rate) - .toString() - .replaceAll(RegExp('\\,'), ''); - } + + _cryptoNumberFormat.maximumFractionDigits = cryptoCurrency.decimals; + cryptoAmount = getIt().getCryptoOutputAmount( + _cryptoNumberFormat.format(amount).replaceAll(RegExp('\\,'), ''), + cryptoCurrency, + ); } else { await calculateBestRate(); } @@ -245,7 +236,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S @action Future changeCryptoAmount({required String amount}) async { - cryptoAmount = amount; + cryptoAmount = getIt().getCryptoInputAmount(amount, cryptoCurrency); if (amount.isEmpty) { fiatAmount = ''; @@ -253,10 +244,6 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S return; } - if (useSatoshis) { - amount = cryptoCurrency.formatAmount(BigInt.parse(amount)); - } - final enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0; if (!isReadyToTrade && !isBuySellQuoteFailed) { diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 86773c9527..d2abdb3faf 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -1,4 +1,6 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/core/amount_parsing_proxy.dart'; +import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/sort_balance_types.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; @@ -327,11 +329,7 @@ abstract class BalanceViewModelBase with Store { String _getFormattedCryptoAmount(CryptoCurrency cryptoCurrency, int? amount) { if (amount == null) return ""; - if (settingsStore.preferBalanceInSats && cryptoCurrency == CryptoCurrency.btc) { - return "$amount"; - } - - return cryptoCurrency.formatAmount(BigInt.from(amount)); + return getIt().getCryptoString(amount, cryptoCurrency); } @computed diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index b8f1cd9090..a6f5fe3b2b 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -1,6 +1,8 @@ import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; +import 'package:cake_wallet/core/amount_parsing_proxy.dart'; import 'package:cake_wallet/decred/decred.dart'; +import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/balance_display_mode.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; @@ -52,8 +54,11 @@ class TransactionListItem extends ActionListItem with Keyable { String get formattedCryptoAmount { if (displayMode == BalanceDisplayMode.hiddenBalance) return '---'; - if (balanceViewModel.wallet.type == WalletType.bitcoin && settingsStore.preferBalanceInSats) - return "${transaction.amount}"; + if (balanceViewModel.wallet.type == WalletType.bitcoin) { + final isLightning = (transaction.additionalInfo["isLightning"] as bool?) ?? false; + return getIt().getCryptoString( + transaction.amount, isLightning ? CryptoCurrency.btcln : CryptoCurrency.btc); + } return transaction.amountFormatted(); } diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 8e471804b6..b77249d3b6 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -6,9 +6,11 @@ import 'dart:developer'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/core/amount_parsing_proxy.dart'; import 'package:cake_wallet/core/create_trade_result.dart'; import 'package:cake_wallet/core/fiat_conversion_service.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; +import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; @@ -370,6 +372,12 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with @observable double bestRate = 0.0; + @computed + bool get useSatoshiDeposit => getIt().useSatoshi(depositCurrency); + + @computed + bool get useSatoshisReceive => getIt().useSatoshi(receiveCurrency); + late Timer bestRateSync; final FiatConversionStore fiatConversionStore; @@ -436,7 +444,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with return; } - final _enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0; + final _enteredAmount = double.tryParse(getIt() + .getCryptoInputAmount(amount.replaceAll(',', '.'), receiveCurrency)) ?? + 0; if (bestRate == 0) { depositAmount = S.current.fetching; @@ -444,12 +454,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with await calculateBestRate(); } - _cryptoNumberFormat.maximumFractionDigits = depositMaxDigits; - - depositAmount = _cryptoNumberFormat - .format(_enteredAmount / bestRate) - .toString() - .replaceAll(RegExp('\\,'), ''); + final amount_ = _enteredAmount / bestRate; + _cryptoNumberFormat.maximumFractionDigits = receiveMaxDigits; + receiveAmount = getIt().getCryptoOutputAmount( + _cryptoNumberFormat.format(amount_).replaceAll(RegExp('\\,'), ''), depositCurrency); } @action @@ -479,7 +487,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with /// as it should remain exactly what the user set if (isFixedRateMode) return; - final _enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0; + final _enteredAmount = double.tryParse(getIt() + .getCryptoInputAmount(amount.replaceAll(',', '.'), depositCurrency)) ?? + 0; /// in case the best rate was not calculated yet if (bestRate == 0) { @@ -488,12 +498,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with await calculateBestRate(); } + final amount_ = _enteredAmount * bestRate; _cryptoNumberFormat.maximumFractionDigits = receiveMaxDigits; - - receiveAmount = _cryptoNumberFormat - .format(bestRate * _enteredAmount) - .toString() - .replaceAll(RegExp('\\,'), ''); + receiveAmount = getIt().getCryptoOutputAmount( + _cryptoNumberFormat.format(amount_).replaceAll(RegExp('\\,'), ''), receiveCurrency); } bool checkIfInputMeetsMinOrMaxCondition(String input) { @@ -513,9 +521,14 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with bestRate = 0.0; return; } - final amount = double.tryParse(isFixedRateMode ? receiveAmount : depositAmount) ?? + final amount = double.tryParse( + getIt().getCryptoInputAmount( + isFixedRateMode ? receiveAmount : depositAmount, + isFixedRateMode ? receiveCurrency : depositCurrency), + ) ?? initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency); + print(amount); final validProvidersForAmount = _tradeAvailableProviders.where((provider) { final limits = _providerLimits[provider]; @@ -639,7 +652,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with Future createTrade() async { if (isSendAllEnabled) { await calculateDepositAllAmount(); - final amount = double.tryParse(depositAmount); + final amount = double.tryParse(getIt() + .getCryptoInputAmount(depositAmount.replaceAll(',', '.'), depositCurrency)); if (limits.min != null && amount != null && amount < limits.min!) { tradeState = TradeIsCreatedFailure( @@ -678,8 +692,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with final request = TradeRequest( fromCurrency: depositCurrency, toCurrency: receiveCurrency, - fromAmount: depositAmount.replaceAll(',', '.'), - toAmount: receiveAmount.replaceAll(',', '.'), + fromAmount: getIt() + .getCryptoInputAmount(depositAmount.replaceAll(',', '.'), depositCurrency), + toAmount: getIt() + .getCryptoInputAmount(receiveAmount.replaceAll(',', '.'), receiveCurrency), refundAddress: depositAddress, toAddress: receiveAddress, isFixedRate: isFixedRateMode, diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index 05f81696a3..65c6a0a52f 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/core/amount_parsing_proxy.dart'; import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; @@ -24,7 +25,6 @@ import 'package:cw_core/balance.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; import 'package:cw_core/format_fixed.dart'; -import 'package:cw_core/parse_fixed.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/utils/print_verbose.dart'; @@ -61,6 +61,8 @@ abstract class OutputBase with Store { } Key key; + + bool get useSatoshi => getIt().useSatoshi(cryptoCurrencyHandler()); @observable String fiatAmount; @@ -92,9 +94,6 @@ abstract class OutputBase with Store { bool get isParsedAddress => parsedAddress.parseFrom != ParseFrom.notParsed && parsedAddress.name.isNotEmpty; - @computed - bool get useSatoshis => walletType == WalletType.bitcoin && _settingsStore.preferBalanceInSats; - @observable String? stealthAddress; @@ -114,11 +113,9 @@ abstract class OutputBase with Store { case WalletType.litecoin: case WalletType.bitcoinCash: case WalletType.dogecoin: - if (useSatoshis) { - _amount = int.parse(_cryptoAmount); - } else { - _amount = bitcoin!.formatterStringDoubleToBitcoinAmount(_cryptoAmount); - } + _amount = getIt() + .parseCryptoString(_cryptoAmount, cryptoCurrencyHandler()) + .toInt(); break; case WalletType.decred: _amount = decred!.formatterStringDoubleToDecredAmount(_cryptoAmount); @@ -196,9 +193,7 @@ abstract class OutputBase with Store { _wallet, _settingsStore.customBitcoinFeeRate, formattedCryptoAmount); } - estimatedFee = useSatoshis - ? "$fee" - : walletTypeToCryptoCurrency(_wallet.type).formatAmount(BigInt.from(fee)); + estimatedFee = getIt().getCryptoString(fee, cryptoCurrencyHandler()); break; case WalletType.solana: estimatedFee = solana!.getEstimateFees(_wallet).toString(); @@ -275,10 +270,8 @@ abstract class OutputBase with Store { ? _wallet.currency : cryptoCurrencyHandler(); - var cryptoAmount = double.parse(estimatedFee); - if (useSatoshis) { - cryptoAmount = double.parse(currency.formatAmount(BigInt.parse(estimatedFee))); - } + final cryptoAmount = + double.parse(getIt().getCryptoInputAmount(estimatedFee, currency)); return calculateFiatAmountRaw( price: _fiatConversationStore.prices[currency]!, cryptoAmount: cryptoAmount); @@ -342,12 +335,11 @@ abstract class OutputBase with Store { @action void _updateFiatAmount() { try { - var cryptoAmount_ = sendAll ? cryptoFullBalance.replaceAll(",", ".") : cryptoAmount.replaceAll(',', '.'); + var cryptoAmount_ = + sendAll ? cryptoFullBalance.replaceAll(",", ".") : cryptoAmount.replaceAll(',', '.'); - if (useSatoshis) { - cryptoAmount_ = - walletTypeToCryptoCurrency(walletType).formatAmount(BigInt.parse(cryptoAmount_)); - } + cryptoAmount_ = + getIt().getCryptoInputAmount(cryptoAmount_, cryptoCurrencyHandler()); final fiat = calculateFiatAmount( price: _fiatConversationStore.prices[cryptoCurrencyHandler()]!, @@ -368,7 +360,8 @@ abstract class OutputBase with Store { .toStringAsFixed(cryptoCurrencyHandler().decimals); if (cryptoAmount != crypto) { - cryptoAmount = getIt.p. crypto; + cryptoAmount = getIt() + .getCryptoOutputAmount(crypto, cryptoCurrencyHandler()); } } catch (e) { cryptoAmount = ''; diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index eba66a16b9..bea1c83f03 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/address_validator.dart'; +import 'package:cake_wallet/core/amount_parsing_proxy.dart'; import 'package:cake_wallet/core/amount_validator.dart'; import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/core/open_crypto_pay/models.dart'; @@ -10,6 +11,7 @@ import 'package:cake_wallet/core/open_crypto_pay/open_cryptopay_service.dart'; import 'package:cake_wallet/core/validator.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; import 'package:cake_wallet/decred/decred.dart'; +import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/contact_record.dart'; @@ -239,9 +241,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor CryptoCurrency get currency => wallet.currency; String get feeCurrencySymbol => - wallet.currency == CryptoCurrency.btc && _settingsStore.preferBalanceInSats - ? "SATS" - : currency.toString(); + getIt().useSatoshi(currency) ? "SATS" : currency.toString(); Validator amountValidator(Output output) => AmountValidator( currency: walletTypeToCryptoCurrency(wallet.type), @@ -273,11 +273,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor coinTypeToSpendFrom == UnspentCoinType.nonMweb) { return balanceViewModel.balances.values.first.availableBalance; } - if (walletType == WalletType.bitcoin && _settingsStore.preferBalanceInSats) { - return "${wallet.balance[selectedCryptoCurrency]!.fullAvailableBalance}"; - } - return selectedCryptoCurrency.formatAmount(BigInt.from(wallet.balance[selectedCryptoCurrency]!.fullAvailableBalance)); + return getIt().getCryptoString( + wallet.balance[selectedCryptoCurrency]!.fullAvailableBalance, selectedCryptoCurrency); } @action @@ -304,10 +302,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor // only for electrum, monero, wownero, decred wallets atm: switch (wallet.type) { case WalletType.bitcoin: - final sendingBalance = await unspentCoinsListViewModel.getSendingBalance(coinTypeToSpendFrom); - if (_settingsStore.preferBalanceInSats) - return sendingBalance.toString(); - return walletTypeToCryptoCurrency(walletType).formatAmount(BigInt.from(sendingBalance)); + final sendingBalance = + await unspentCoinsListViewModel.getSendingBalance(coinTypeToSpendFrom); + return getIt() + .getCryptoString(sendingBalance, walletTypeToCryptoCurrency(walletType)); case WalletType.litecoin: case WalletType.bitcoinCash: case WalletType.dogecoin: diff --git a/lib/view_model/settings/display_settings_view_model.dart b/lib/view_model/settings/display_settings_view_model.dart index 9bcc1b3da0..6acd252e3f 100644 --- a/lib/view_model/settings/display_settings_view_model.dart +++ b/lib/view_model/settings/display_settings_view_model.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/entities/balance_display_mode.dart'; +import 'package:cake_wallet/entities/bitcoin_amount_display_mode.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/entities/sync_status_display_mode.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -36,7 +37,7 @@ abstract class DisplaySettingsViewModelBase with Store { bool get shouldShowMarketPlaceInDashboard => _settingsStore.shouldShowMarketPlaceInDashboard; @computed - bool get preferBalanceInSats => _settingsStore.preferBalanceInSats; + BitcoinAmountDisplayMode get displayAmountsInSatoshi => _settingsStore.displayAmountsInSatoshi; @computed MaterialThemeBase get currentTheme => _themeStore.currentTheme; @@ -107,7 +108,7 @@ abstract class DisplaySettingsViewModelBase with Store { void setBalanceDisplayMode(BalanceDisplayMode value) => _settingsStore.balanceDisplayMode = value; @action - void setPreferBalanceInSats(bool value) => _settingsStore.preferBalanceInSats = value; + void setDisplayAmountsInSatoshi(BitcoinAmountDisplayMode value) => _settingsStore.displayAmountsInSatoshi = value; @action void setShouldDisplayBalance(bool value) { diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index 8032e130d6..4b95a35437 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -1,5 +1,8 @@ +import 'package:cake_wallet/core/amount_parsing_proxy.dart'; +import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_base.dart'; @@ -331,14 +334,13 @@ abstract class TransactionDetailsViewModelBase with Store { } void _addElectrumListItems(TransactionInfo tx, DateFormat dateFormat) { - - String amountFormatted = tx.amountFormatted(); - String? feeFormatted = tx.feeFormatted(); - - if (wallet.type == WalletType.bitcoin && settingsStore.preferBalanceInSats){ - amountFormatted = "${tx.amount}"; - if (tx.fee != null) feeFormatted = "${tx.fee}"; - } + final isLightning = (tx.additionalInfo["isLightning"] as bool?) ?? false; + final amountFormatted = getIt() + .getCryptoString(tx.amount, isLightning ? CryptoCurrency.btcln : CryptoCurrency.btc); + final feeFormatted = (tx.fee != null) + ? getIt() + .getCryptoString(tx.fee!, isLightning ? CryptoCurrency.btcln : CryptoCurrency.btc) + : ""; final _items = [ StandartListItem( @@ -369,7 +371,7 @@ abstract class TransactionDetailsViewModelBase with Store { if (tx.feeFormatted()?.isNotEmpty ?? false) StandartListItem( title: S.current.transaction_details_fee, - value: feeFormatted!, + value: feeFormatted, key: ValueKey('standard_list_item_transaction_details_fee_key'), ), ]; diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index 7b0b52464c..63c12d3df1 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -3,9 +3,11 @@ import 'dart:core'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/core/amount_parsing_proxy.dart'; import 'package:cake_wallet/core/fiat_conversion_service.dart'; import 'package:cake_wallet/core/payment_uris.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; +import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; @@ -89,10 +91,12 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo Currency selectedCurrency; @computed - int get selectedCurrencyDecimals => useSatoshis ? 0 : selectedCurrency.decimals; + int get selectedCurrencyDecimals => useSatoshi ? 0 : selectedCurrency.decimals; @computed - bool get useSatoshis => selectedCurrency == CryptoCurrency.btc && _settingsStore.preferBalanceInSats; + bool get useSatoshi => + selectedCurrency is CryptoCurrency && + getIt().useSatoshi(selectedCurrency as CryptoCurrency); @observable String searchText = ''; @@ -200,7 +204,6 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo } if (isElectrumWallet) { - final useSatoshi = wallet.type == WalletType.bitcoin && _settingsStore.preferBalanceInSats; if (bitcoin!.hasSelectedSilentPayments(wallet)) { final addressItems = bitcoin!.getSilentPaymentAddresses(wallet).map((address) { final isPrimary = address.id == 0; @@ -211,9 +214,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo name: address.name, address: address.address, txCount: address.txCount, - balance: useSatoshi - ? "${address.balance}" - : walletTypeToCryptoCurrency(type).formatAmount(BigInt.from(address.balance)), + balance: getIt() + .getCryptoString(address.balance, walletTypeToCryptoCurrency(type)), isChange: address.isChange, ); }); @@ -228,9 +230,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo name: address.name, address: address.address, txCount: address.txCount, - balance: useSatoshi - ? "${address.balance}" - : walletTypeToCryptoCurrency(type).formatAmount(BigInt.from(address.balance)), + balance: getIt() + .getCryptoString(address.balance, walletTypeToCryptoCurrency(type)), isChange: address.isChange, isOneTimeReceiveAddress: true, ); @@ -246,9 +247,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo name: subaddress.name, address: subaddress.address, txCount: subaddress.txCount, - balance: useSatoshi - ? "${subaddress.balance}" - : walletTypeToCryptoCurrency(type).formatAmount(BigInt.from(subaddress.balance)), + balance: getIt() + .getCryptoString(subaddress.balance, walletTypeToCryptoCurrency(type)), isChange: subaddress.isChange); }); From ef28f937fad7116f1644654fdcb2be071c711253 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 2 Dec 2025 14:16:49 +0100 Subject: [PATCH 08/19] refactor: streamline amount formatting logic and enhance unspent coins handling with `AmountParsingProxy` integration for unified crypto processing --- .../unspent_coins_details_view_model.dart | 13 +++----- .../unspent_coins_list_view_model.dart | 31 ++++++++----------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/lib/view_model/unspent_coins/unspent_coins_details_view_model.dart b/lib/view_model/unspent_coins/unspent_coins_details_view_model.dart index 9cf6ed4bf3..ae464bb3b5 100644 --- a/lib/view_model/unspent_coins/unspent_coins_details_view_model.dart +++ b/lib/view_model/unspent_coins/unspent_coins_details_view_model.dart @@ -1,5 +1,4 @@ import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/src/screens/transaction_details/blockexplorer_list_item.dart'; import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; import 'package:cake_wallet/src/screens/transaction_details/textfield_list_item.dart'; @@ -42,9 +41,7 @@ abstract class UnspentCoinsDetailsViewModelBase with Store { onSwitchValueChange: (value) async { isFrozen = value; unspentCoinsItem.isFrozen = value; - if (value) { - unspentCoinsItem.isSending = !value; - } + if (value) unspentCoinsItem.isSending = !value; await unspentCoinsListViewModel.saveUnspentCoinInfo(unspentCoinsItem); }) ]; @@ -81,13 +78,13 @@ abstract class UnspentCoinsDetailsViewModelBase with Store { String _explorerDescription(WalletType type) { switch (type) { case WalletType.bitcoin: - return S.current.view_transaction_on + 'Ordinals.com'; + return '${S.current.view_transaction_on}Ordinals.com'; case WalletType.litecoin: - return S.current.view_transaction_on + 'Earlyordies.com'; + return '${S.current.view_transaction_on}Earlyordies.com'; case WalletType.bitcoinCash: - return S.current.view_transaction_on + 'Blockchair.com'; + return '${S.current.view_transaction_on}Blockchair.com'; case WalletType.dogecoin: - return S.current.view_transaction_on + 'Dogechain.info'; + return '${S.current.view_transaction_on}Dogechain.info'; default: return ''; } diff --git a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart index deca2bc33e..04878bf3df 100644 --- a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart +++ b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/entities/calculate_fiat_amount_raw.dart'; +import 'package:cake_wallet/core/amount_parsing_proxy.dart'; +import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/monero/monero.dart'; @@ -76,7 +77,7 @@ abstract class UnspentCoinsListViewModelBase with Store { final formatted = formatAmountToString(item.value); final cryptoAmount = double.tryParse(formatted.replaceAll(',', '')) ?? 0.0; final fiatValue = price * cryptoAmount; - result[item.hash] = fiatCurrency.title + ' ' + fiatValue.toStringAsFixed(2); + result[item.hash] = '${fiatCurrency.title} ${fiatValue.toStringAsFixed(2)}'; } return result; @@ -125,17 +126,8 @@ abstract class UnspentCoinsListViewModelBase with Store { } } - String formatAmountToString(int fullBalance) { - if (wallet.type == WalletType.monero) - return monero!.formatterMoneroAmountToString(amount: fullBalance); - if (wallet.type == WalletType.wownero) - return wownero!.formatterWowneroAmountToString(amount: fullBalance); - if ([WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash, WalletType.dogecoin].contains(wallet.type)) - return bitcoin!.formatterBitcoinAmountToString(amount: fullBalance); - if (wallet.type == WalletType.decred) - return decred!.formatterDecredAmountToString(amount: fullBalance); - return ''; - } + String formatAmountToString(int fullBalance) => + wallet.currency.formatAmount(BigInt.from(fullBalance)); Future _updateUnspents() async { if (wallet.type == WalletType.monero) { @@ -211,13 +203,18 @@ abstract class UnspentCoinsListViewModelBase with Store { .map((elem) { try { final existingItem = _unspentCoinsInfo.values - .firstWhereOrNull((item) => item.walletId == wallet.id && item == elem);; + .firstWhereOrNull((item) => item.walletId == wallet.id && item == elem); if (existingItem == null) return null; + final symbol = getIt().useSatoshi(wallet.currency) + ? "SATS" + : wallet.currency.title; + return UnspentCoinsItem( address: elem.address, - amount: '${formatAmountToString(elem.value)} ${wallet.currency.title}', + amount: + '${getIt().getCryptoString(elem.value, wallet.currency)} $symbol', hash: elem.hash, isFrozen: existingItem.isFrozen, note: existingItem.note, @@ -268,9 +265,7 @@ abstract class UnspentCoinsListViewModelBase with Store { void setIsDisposing(bool value) => isDisposing = value; @action - void updateWallet(WalletBase newWallet) { - wallet = newWallet; - } + void updateWallet(WalletBase newWallet) => wallet = newWallet; @action Future dispose() async { From 19107e887c87b4d52933aea533d1e902b1e4fc4d Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 2 Dec 2025 15:09:08 +0100 Subject: [PATCH 09/19] refactor: replace direct bitcoin amount formatting with `AmountParsingProxy` and remove debug print in `exchange_view_model` --- lib/src/screens/dashboard/pages/transactions_page.dart | 6 ++++-- lib/view_model/exchange/exchange_view_model.dart | 1 - 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/src/screens/dashboard/pages/transactions_page.dart b/lib/src/screens/dashboard/pages/transactions_page.dart index 85ffbcbe47..e97e25ea61 100644 --- a/lib/src/screens/dashboard/pages/transactions_page.dart +++ b/lib/src/screens/dashboard/pages/transactions_page.dart @@ -1,4 +1,6 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/core/amount_parsing_proxy.dart'; +import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/balance_display_mode.dart'; import 'package:cake_wallet/order/order_source_description.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/anonpay_transaction_row.dart'; @@ -155,8 +157,8 @@ class TransactionsPage extends StatelessWidget { ), currency: "BTC", state: item.status, - amount: bitcoin! - .formatterBitcoinAmountToString(amount: session.amount.toInt()), + amount: getIt() + .getCryptoString(session.amount.toInt(), CryptoCurrency.btc), createdAt: DateFormat('HH:mm').format(session.inProgressSince!), isSending: session.isSenderSession, ); diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index b77249d3b6..c9f1c94dfe 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -528,7 +528,6 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with ) ?? initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency); - print(amount); final validProvidersForAmount = _tradeAvailableProviders.where((provider) { final limits = _providerLimits[provider]; From 3f43e7ed2fec2278004be6798f0cfc43a878c7f1 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 2 Dec 2025 15:43:38 +0100 Subject: [PATCH 10/19] refactor: replace `getIt` with `_appStore.amountParsingProxy`, remove redundant dependencies across view models and adjust related imports --- lib/di.dart | 7 ++- .../dashboard/pages/transactions_page.dart | 4 +- lib/store/app_store.dart | 4 ++ lib/view_model/buy/buy_sell_view_model.dart | 10 ++-- .../dashboard/balance_view_model.dart | 4 +- .../dashboard/dashboard_view_model.dart | 15 +++--- .../dashboard/transaction_list_item.dart | 14 +++--- .../exchange/exchange_view_model.dart | 34 ++++++------- lib/view_model/send/output.dart | 33 ++++++------ .../send/send_template_view_model.dart | 12 ++--- lib/view_model/send/send_view_model.dart | 50 ++++++++----------- lib/view_model/send/template_view_model.dart | 12 ++--- .../transaction_details_view_model.dart | 31 ++++++------ .../unspent_coins_list_view_model.dart | 18 +++---- .../wallet_address_list_view_model.dart | 26 +++++----- 15 files changed, 129 insertions(+), 145 deletions(-) diff --git a/lib/di.dart b/lib/di.dart index 0f9af9b4d1..ee88ddbb7b 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -554,7 +554,6 @@ Future setup({ _tradesSource, getIt.get(), getIt.get(), - getIt.get().settingsStore, getIt.get(), getIt.get(), getIt.get(), @@ -830,7 +829,7 @@ Future setup({ getIt.registerFactory(() => SendTemplateViewModel( getIt.get().wallet!, - getIt.get().settingsStore, + getIt.get(), getIt.get(), getIt.get())); @@ -1307,7 +1306,7 @@ Future setup({ transactionInfo: transactionInfo, transactionDescriptionBox: _transactionDescriptionBox, wallet: wallet, - settingsStore: getIt.get(), + appStore: getIt.get(), sendViewModel: getIt.get(), canReplaceByFee: canReplaceByFee, ); @@ -1447,7 +1446,7 @@ Future setup({ wallet: wallet!, unspentCoinsInfo: _unspentCoinsInfoSource, fiatConversationStore: getIt.get(), - settingsStore: getIt.get(), + appStore: getIt.get(), coinTypeToSpendFrom: coinTypeToSpendFrom ?? UnspentCoinType.any, ); }); diff --git a/lib/src/screens/dashboard/pages/transactions_page.dart b/lib/src/screens/dashboard/pages/transactions_page.dart index e97e25ea61..7bc7c10602 100644 --- a/lib/src/screens/dashboard/pages/transactions_page.dart +++ b/lib/src/screens/dashboard/pages/transactions_page.dart @@ -1,6 +1,4 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/core/amount_parsing_proxy.dart'; -import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/balance_display_mode.dart'; import 'package:cake_wallet/order/order_source_description.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/anonpay_transaction_row.dart'; @@ -157,7 +155,7 @@ class TransactionsPage extends StatelessWidget { ), currency: "BTC", state: item.status, - amount: getIt() + amount: dashboardViewModel.appStore.amountParsingProxy .getCryptoString(session.amount.toInt(), CryptoCurrency.btc), createdAt: DateFormat('HH:mm').format(session.inProgressSince!), isSending: session.isSenderSession, diff --git a/lib/store/app_store.dart b/lib/store/app_store.dart index c3e7cc4562..f80f599876 100644 --- a/lib/store/app_store.dart +++ b/lib/store/app_store.dart @@ -1,4 +1,5 @@ +import 'package:cake_wallet/core/amount_parsing_proxy.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; @@ -42,6 +43,9 @@ abstract class AppStoreBase with Store { ThemeStore themeStore; + AmountParsingProxy get amountParsingProxy => + AmountParsingProxy(settingsStore.displayAmountsInSatoshi); + @action Future changeCurrentWallet( WalletBase, TransactionInfo> wallet) async { diff --git a/lib/view_model/buy/buy_sell_view_model.dart b/lib/view_model/buy/buy_sell_view_model.dart index db56463221..0f2554ebc9 100644 --- a/lib/view_model/buy/buy_sell_view_model.dart +++ b/lib/view_model/buy/buy_sell_view_model.dart @@ -5,10 +5,8 @@ import 'package:cake_wallet/buy/buy_quote.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; import 'package:cake_wallet/buy/payment_method.dart'; import 'package:cake_wallet/buy/sell_buy_states.dart'; -import 'package:cake_wallet/core/amount_parsing_proxy.dart'; import 'package:cake_wallet/core/selectable_option.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; -import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/entities/provider_types.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -83,7 +81,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S double get amount { final formattedFiatAmount = double.tryParse(fiatAmount); var formattedCryptoAmount = double.tryParse( - getIt().getCryptoInputAmount(cryptoAmount, cryptoCurrency)); + _appStore.amountParsingProxy.getCryptoInputAmount(cryptoAmount, cryptoCurrency)); return isBuyAction ? formattedFiatAmount ?? 200.0 @@ -169,7 +167,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S : null; @computed - bool get useSatoshi => getIt().useSatoshi(cryptoCurrency); + bool get useSatoshi => _appStore.amountParsingProxy.useSatoshi(cryptoCurrency); @action void reset() { @@ -225,7 +223,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S final amount = enteredAmount / bestRateQuote!.rate; _cryptoNumberFormat.maximumFractionDigits = cryptoCurrency.decimals; - cryptoAmount = getIt().getCryptoOutputAmount( + cryptoAmount = _appStore.amountParsingProxy.getCryptoOutputAmount( _cryptoNumberFormat.format(amount).replaceAll(RegExp('\\,'), ''), cryptoCurrency, ); @@ -236,7 +234,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S @action Future changeCryptoAmount({required String amount}) async { - cryptoAmount = getIt().getCryptoInputAmount(amount, cryptoCurrency); + cryptoAmount = _appStore.amountParsingProxy.getCryptoInputAmount(amount, cryptoCurrency); if (amount.isEmpty) { fiatAmount = ''; diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index d2abdb3faf..91a7655471 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -1,6 +1,4 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/core/amount_parsing_proxy.dart'; -import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/sort_balance_types.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; @@ -329,7 +327,7 @@ abstract class BalanceViewModelBase with Store { String _getFormattedCryptoAmount(CryptoCurrency cryptoCurrency, int? amount) { if (amount == null) return ""; - return getIt().getCryptoString(amount, cryptoCurrency); + return appStore.amountParsingProxy.getCryptoString(amount, cryptoCurrency); } @computed diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 50e8d62b7d..f3d6bab8da 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -43,7 +43,6 @@ import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart'; import 'package:cake_wallet/view_model/settings/sync_mode.dart'; import 'package:cryptography/cryptography.dart'; import 'package:cw_core/balance.dart'; -import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_history.dart'; @@ -230,7 +229,7 @@ abstract class DashboardViewModelBase with Store { (transaction) => TransactionListItem( transaction: transaction, balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore, + appStore: appStore, key: ValueKey('monero_transaction_history_item_${transaction.id}_key'), ), ), @@ -260,7 +259,7 @@ abstract class DashboardViewModelBase with Store { (transaction) => TransactionListItem( transaction: transaction, balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore, + appStore: appStore, key: ValueKey('wownero_transaction_history_item_${transaction.id}_key'), ), ), @@ -274,7 +273,7 @@ abstract class DashboardViewModelBase with Store { (transaction) => TransactionListItem( transaction: transaction, balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore, + appStore: appStore, key: ValueKey('${_wallet.type.name}_transaction_history_item_${transaction.id}_key'), ), ), @@ -362,7 +361,7 @@ abstract class DashboardViewModelBase with Store { transactions.addAll(relevantTxs.map((tx) => TransactionListItem( transaction: tx, balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore, + appStore: appStore, key: ValueKey('${wallet.type.name}_transaction_history_item_${tx.id}_key'), ))); } finally { @@ -973,7 +972,7 @@ abstract class DashboardViewModelBase with Store { (transaction) => TransactionListItem( transaction: transaction, balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore, + appStore: appStore, key: ValueKey('${wallet.type.name}_transaction_history_item_${transaction.id}_key'), ), ), @@ -1025,7 +1024,7 @@ abstract class DashboardViewModelBase with Store { (transaction) => TransactionListItem( transaction: transaction, balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore, + appStore: appStore, key: ValueKey('monero_transaction_history_item_${transaction.id}_key'), ), ), @@ -1045,7 +1044,7 @@ abstract class DashboardViewModelBase with Store { (transaction) => TransactionListItem( transaction: transaction, balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore, + appStore: appStore, key: ValueKey('wownero_transaction_history_item_${transaction.id}_key'), ), ), diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index a6f5fe3b2b..12a2256313 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -1,8 +1,6 @@ import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; -import 'package:cake_wallet/core/amount_parsing_proxy.dart'; import 'package:cake_wallet/decred/decred.dart'; -import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/balance_display_mode.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; @@ -11,13 +9,13 @@ import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/solana/solana.dart'; +import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; -import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; @@ -30,17 +28,17 @@ class TransactionListItem extends ActionListItem with Keyable { TransactionListItem({ required this.transaction, required this.balanceViewModel, - required this.settingsStore, + required AppStore appStore, required super.key, - }); + }) : _appStore = appStore; final TransactionInfo transaction; final BalanceViewModel balanceViewModel; - final SettingsStore settingsStore; + final AppStore _appStore; double get price => balanceViewModel.price; - FiatCurrency get fiatCurrency => settingsStore.fiatCurrency; + FiatCurrency get fiatCurrency => _appStore.settingsStore.fiatCurrency; BalanceDisplayMode get displayMode => balanceViewModel.displayMode; @@ -56,7 +54,7 @@ class TransactionListItem extends ActionListItem with Keyable { if (displayMode == BalanceDisplayMode.hiddenBalance) return '---'; if (balanceViewModel.wallet.type == WalletType.bitcoin) { final isLightning = (transaction.additionalInfo["isLightning"] as bool?) ?? false; - return getIt().getCryptoString( + return _appStore.amountParsingProxy.getCryptoString( transaction.amount, isLightning ? CryptoCurrency.btcln : CryptoCurrency.btc); } return transaction.amountFormatted(); diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index c9f1c94dfe..139f0f5307 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -6,11 +6,9 @@ import 'dart:developer'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/core/amount_parsing_proxy.dart'; import 'package:cake_wallet/core/create_trade_result.dart'; import 'package:cake_wallet/core/fiat_conversion_service.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; -import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; @@ -73,11 +71,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with } ExchangeViewModelBase( - AppStore appStore, + this._appStore, this.trades, this._exchangeTemplateStore, this.tradesStore, - this._settingsStore, this.sharedPreferences, this.contactListViewModel, this.unspentCoinsListViewModel, @@ -99,11 +96,11 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with limits = Limits(min: 0, max: 0), tradeState = ExchangeTradeStateInitial(), limitsState = LimitsInitialState(), - receiveCurrency = appStore.wallet!.currency, - depositCurrency = appStore.wallet!.currency, + receiveCurrency = _appStore.wallet!.currency, + depositCurrency = _appStore.wallet!.currency, providerList = [], selectedProviders = ObservableList(), - super(appStore: appStore) { + super(appStore: _appStore) { _useTorOnly = _settingsStore.exchangeStatus == ExchangeApiMode.torOnly; _setProviders(); const excludeDepositCurrencies = [CryptoCurrency.btt]; @@ -361,7 +358,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with final NumberFormat _cryptoNumberFormat; - final SettingsStore _settingsStore; + final AppStore _appStore; + SettingsStore get _settingsStore => _appStore.settingsStore; final ContactListViewModel contactListViewModel; @@ -373,10 +371,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with double bestRate = 0.0; @computed - bool get useSatoshiDeposit => getIt().useSatoshi(depositCurrency); + bool get useSatoshiDeposit => _appStore.amountParsingProxy.useSatoshi(depositCurrency); @computed - bool get useSatoshisReceive => getIt().useSatoshi(receiveCurrency); + bool get useSatoshisReceive => _appStore.amountParsingProxy.useSatoshi(receiveCurrency); late Timer bestRateSync; @@ -444,7 +442,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with return; } - final _enteredAmount = double.tryParse(getIt() + final _enteredAmount = double.tryParse(_appStore.amountParsingProxy .getCryptoInputAmount(amount.replaceAll(',', '.'), receiveCurrency)) ?? 0; @@ -456,7 +454,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with final amount_ = _enteredAmount / bestRate; _cryptoNumberFormat.maximumFractionDigits = receiveMaxDigits; - receiveAmount = getIt().getCryptoOutputAmount( + receiveAmount = _appStore.amountParsingProxy.getCryptoOutputAmount( _cryptoNumberFormat.format(amount_).replaceAll(RegExp('\\,'), ''), depositCurrency); } @@ -487,7 +485,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with /// as it should remain exactly what the user set if (isFixedRateMode) return; - final _enteredAmount = double.tryParse(getIt() + final _enteredAmount = double.tryParse(_appStore.amountParsingProxy .getCryptoInputAmount(amount.replaceAll(',', '.'), depositCurrency)) ?? 0; @@ -500,7 +498,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with final amount_ = _enteredAmount * bestRate; _cryptoNumberFormat.maximumFractionDigits = receiveMaxDigits; - receiveAmount = getIt().getCryptoOutputAmount( + receiveAmount = _appStore.amountParsingProxy.getCryptoOutputAmount( _cryptoNumberFormat.format(amount_).replaceAll(RegExp('\\,'), ''), receiveCurrency); } @@ -522,7 +520,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with return; } final amount = double.tryParse( - getIt().getCryptoInputAmount( + _appStore.amountParsingProxy.getCryptoInputAmount( isFixedRateMode ? receiveAmount : depositAmount, isFixedRateMode ? receiveCurrency : depositCurrency), ) ?? @@ -651,7 +649,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with Future createTrade() async { if (isSendAllEnabled) { await calculateDepositAllAmount(); - final amount = double.tryParse(getIt() + final amount = double.tryParse(_appStore.amountParsingProxy .getCryptoInputAmount(depositAmount.replaceAll(',', '.'), depositCurrency)); if (limits.min != null && amount != null && amount < limits.min!) { @@ -691,9 +689,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with final request = TradeRequest( fromCurrency: depositCurrency, toCurrency: receiveCurrency, - fromAmount: getIt() + fromAmount: _appStore.amountParsingProxy .getCryptoInputAmount(depositAmount.replaceAll(',', '.'), depositCurrency), - toAmount: getIt() + toAmount: _appStore.amountParsingProxy .getCryptoInputAmount(receiveAmount.replaceAll(',', '.'), receiveCurrency), refundAddress: depositAddress, toAddress: receiveAddress, diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index 65c6a0a52f..0b4f57b556 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -1,7 +1,6 @@ import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/core/amount_parsing_proxy.dart'; import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; @@ -16,6 +15,7 @@ import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart'; +import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/tron/tron.dart'; @@ -40,8 +40,7 @@ const String cryptoNumberPattern = '0.0'; class Output = OutputBase with _$Output; abstract class OutputBase with Store { - OutputBase( - this._wallet, this._settingsStore, this._fiatConversationStore, this.cryptoCurrencyHandler) + OutputBase(this._wallet, this._appStore, this._fiatConversationStore, this.cryptoCurrencyHandler) : key = UniqueKey(), sendAll = false, cryptoAmount = '', @@ -61,8 +60,8 @@ abstract class OutputBase with Store { } Key key; - - bool get useSatoshi => getIt().useSatoshi(cryptoCurrencyHandler()); + + bool get useSatoshi => _appStore.amountParsingProxy.useSatoshi(cryptoCurrencyHandler()); @observable String fiatAmount; @@ -113,7 +112,7 @@ abstract class OutputBase with Store { case WalletType.litecoin: case WalletType.bitcoinCash: case WalletType.dogecoin: - _amount = getIt() + _amount = _appStore.amountParsingProxy .parseCryptoString(_cryptoAmount, cryptoCurrencyHandler()) .toInt(); break; @@ -193,7 +192,7 @@ abstract class OutputBase with Store { _wallet, _settingsStore.customBitcoinFeeRate, formattedCryptoAmount); } - estimatedFee = getIt().getCryptoString(fee, cryptoCurrencyHandler()); + estimatedFee = _appStore.amountParsingProxy.getCryptoString(fee, cryptoCurrencyHandler()); break; case WalletType.solana: estimatedFee = solana!.getEstimateFees(_wallet).toString(); @@ -271,7 +270,7 @@ abstract class OutputBase with Store { : cryptoCurrencyHandler(); final cryptoAmount = - double.parse(getIt().getCryptoInputAmount(estimatedFee, currency)); + double.parse(_appStore.amountParsingProxy.getCryptoInputAmount(estimatedFee, currency)); return calculateFiatAmountRaw( price: _fiatConversationStore.prices[currency]!, cryptoAmount: cryptoAmount); @@ -280,12 +279,16 @@ abstract class OutputBase with Store { } } - WalletType get walletType => _wallet.type; - final CryptoCurrency Function() cryptoCurrencyHandler; @observable WalletBase, TransactionInfo> _wallet; - final SettingsStore _settingsStore; + + WalletType get walletType => _wallet.type; + + final CryptoCurrency Function() cryptoCurrencyHandler; final FiatConversionStore _fiatConversationStore; + final AppStore _appStore; + + SettingsStore get _settingsStore => _appStore.settingsStore; @action void setSendAll(String fullBalance) { @@ -339,7 +342,7 @@ abstract class OutputBase with Store { sendAll ? cryptoFullBalance.replaceAll(",", ".") : cryptoAmount.replaceAll(',', '.'); cryptoAmount_ = - getIt().getCryptoInputAmount(cryptoAmount_, cryptoCurrencyHandler()); + _appStore.amountParsingProxy.getCryptoInputAmount(cryptoAmount_, cryptoCurrencyHandler()); final fiat = calculateFiatAmount( price: _fiatConversationStore.prices[cryptoCurrencyHandler()]!, @@ -360,8 +363,8 @@ abstract class OutputBase with Store { .toStringAsFixed(cryptoCurrencyHandler().decimals); if (cryptoAmount != crypto) { - cryptoAmount = getIt() - .getCryptoOutputAmount(crypto, cryptoCurrencyHandler()); + cryptoAmount = + _appStore.amountParsingProxy.getCryptoOutputAmount(crypto, cryptoCurrencyHandler()); } } catch (e) { cryptoAmount = ''; @@ -400,7 +403,7 @@ extension OutputCopyWith on Output { }) { final clone = Output( _wallet, - _settingsStore, + _appStore, _fiatConversationStore, cryptoCurrencyHandler, ); diff --git a/lib/view_model/send/send_template_view_model.dart b/lib/view_model/send/send_template_view_model.dart index 29f576d666..db093485f8 100644 --- a/lib/view_model/send/send_template_view_model.dart +++ b/lib/view_model/send/send_template_view_model.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/reactions/wallet_connect.dart'; +import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/view_model/send/template_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; @@ -11,7 +12,6 @@ import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/core/amount_validator.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; -import 'package:cake_wallet/store/settings_store.dart'; part 'send_template_view_model.g.dart'; @@ -19,12 +19,12 @@ class SendTemplateViewModel = SendTemplateViewModelBase with _$SendTemplateViewM abstract class SendTemplateViewModelBase with Store { final WalletBase _wallet; - final SettingsStore _settingsStore; + final AppStore _appStore; final SendTemplateStore _sendTemplateStore; final FiatConversionStore _fiatConversationStore; SendTemplateViewModelBase( - this._wallet, this._settingsStore, this._sendTemplateStore, this._fiatConversationStore) + this._wallet, this._appStore, this._sendTemplateStore, this._fiatConversationStore) : recipients = ObservableList() { addRecipient(); } @@ -35,7 +35,7 @@ abstract class SendTemplateViewModelBase with Store { void addRecipient() { recipients.add(TemplateViewModel( wallet: _wallet, - settingsStore: _settingsStore, + appStore: _appStore, fiatConversationStore: _fiatConversationStore)); } @@ -65,10 +65,10 @@ abstract class SendTemplateViewModelBase with Store { CryptoCurrency get cryptoCurrency => _wallet.currency; @computed - String get fiatCurrency => _settingsStore.fiatCurrency.title; + String get fiatCurrency => _appStore.settingsStore.fiatCurrency.title; @computed - int get fiatCurrencyDecimals => _settingsStore.fiatCurrency.decimals; + int get fiatCurrencyDecimals => _appStore.settingsStore.fiatCurrency.decimals; @computed ObservableList