CW-997 coin control enhancements / send ALL fixes (#2114)

* computed sending balance value * files that didnt get added before * monero + move unspent calc to view model * working * remove old code * Update lib/view_model/send/send_view_model.dart --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

Matthew Fosse committed Mar 30, 2025 at 07:52 UTC 40a0989956fc87e20257594dd1cfd6d69764952c
8 files changed +106 -12
cw_bitcoin/lib/electrum_balance.dart
+1 -1
@@ -65,6 +65,6 @@ class ElectrumBalance extends Balance {
65 'unconfirmed': unconfirmed,
66 'frozen': frozen,
67 'secondConfirmed': secondConfirmed,
68 - 'secondUnconfirmed': secondUnconfirmed
68 + 'secondUnconfirmed': secondUnconfirmed,
69 });
70 }
cw_bitcoin/lib/electrum_wallet.dart
+11 -2
@@ -4,6 +4,8 @@ import 'dart:io';
4 import 'dart:isolate';
5
6 import 'package:bitcoin_base/bitcoin_base.dart';
7 +import 'package:cw_bitcoin/bitcoin_amount_format.dart';
8 +import 'package:cw_core/format_amount.dart';
9 import 'package:cw_core/utils/print_verbose.dart';
10 import 'package:cw_bitcoin/bitcoin_wallet.dart';
11 import 'package:cw_bitcoin/litecoin_wallet.dart';
@@ -2240,10 +2242,11 @@ abstract class ElectrumWalletBase
2242
2243 if (element.hash == info.hash &&
2244 element.vout == info.vout &&
2243 - info.isFrozen &&
2245 element.bitcoinAddressRecord.address == info.address &&
2246 element.value == info.value) {
2246 - totalFrozen += element.value;
2247 + if (info.isFrozen) {
2248 + totalFrozen += element.value;
2249 + }
2250 }
2251 });
2252 });
@@ -2499,6 +2502,12 @@ abstract class ElectrumWalletBase
2502 transactionHistory.addOne(tx);
2503 }
2504 }
2505 +
2506 + @override
2507 + String formatCryptoAmount(String amount) {
2508 + final amountInt = int.parse(amount);
2509 + return bitcoinAmountToString(amount: amountInt);
2510 + }
2511 }
2512
2513 class ScanNode {
cw_core/lib/wallet_base.dart
+2
@@ -36,6 +36,8 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
36
37 ObservableMap<CryptoCurrency, BalanceType> get balance;
38
39 + String formatCryptoAmount(String amount) => amount;
40 +
41 SyncStatus get syncStatus;
42
43 set syncStatus(SyncStatus status);
cw_monero/lib/monero_wallet.dart
+6
@@ -3,6 +3,7 @@ import 'dart:ffi';
3 import 'dart:io';
4 import 'dart:isolate';
5
6 +import 'package:cw_core/monero_amount_format.dart';
7 import 'package:cw_core/pathForWallet.dart';
8 import 'package:cw_core/transaction_priority.dart';
9 import 'package:cw_core/account.dart';
@@ -905,4 +906,9 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
906 monero.WalletManager_openWallet(wmPtr, path: '', password: '');
907 enableLedgerExchange(dummyWPtr, connection);
908 }
909 +
910 + @override
911 + String formatCryptoAmount(String amount) {
912 + return moneroAmountToString(amount: int.parse(amount));
913 + }
914 }
cw_wownero/lib/wownero_wallet.dart
+5
@@ -770,4 +770,9 @@ abstract class WowneroWalletBase
770
771 return wownero_wallet.verifyMessage(message, address, signature);
772 }
773 +
774 + @override
775 + String formatCryptoAmount(String amount) {
776 + return wowneroAmountToString(amount: int.parse(amount));
777 + }
778 }
lib/src/screens/send/widgets/send_card.dart
+13 -7
@@ -244,7 +244,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
244 currencyValueValidator: output.sendAll
245 ? sendViewModel.allAmountValidator
246 : sendViewModel.amountValidator,
247 - allAmountCallback: () async => output.setSendAll(sendViewModel.balance)),
247 + allAmountCallback: () async => output.setSendAll(sendViewModel.sendingBalance)),
248 Divider(
249 height: 1,
250 color: Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor),
@@ -266,7 +266,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
266 ),
267 ),
268 Text(
269 - sendViewModel.balance,
269 + sendViewModel.sendingBalance,
270 style: TextStyle(
271 fontSize: 12,
272 fontWeight: FontWeight.w600,
@@ -384,10 +384,16 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
384 padding: EdgeInsets.only(top: 6),
385 child: GestureDetector(
386 key: ValueKey('send_page_unspent_coin_button_key'),
387 - onTap: () => Navigator.of(context).pushNamed(
388 - Routes.unspentCoinsList,
389 - arguments: widget.sendViewModel.coinTypeToSpendFrom,
390 - ),
387 + onTap: () async {
388 + await Navigator.of(context).pushNamed(
389 + Routes.unspentCoinsList,
390 + arguments: widget.sendViewModel.coinTypeToSpendFrom,
391 + );
392 + if (mounted) {
393 + // we just got back from the unspent coins list screen, so we need to recompute the sending balance:
394 + sendViewModel.updateSendingBalance();
395 + }
396 + },
397 child: Container(
398 color: Colors.transparent,
399 child: Row(
@@ -505,7 +511,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
511
512 reaction((_) => sendViewModel.selectedCryptoCurrency, (Currency currency) {
513 if (output.sendAll) {
508 - output.setSendAll(sendViewModel.balance);
514 + output.setSendAll(sendViewModel.sendingBalance);
515 }
516
517 output.setCryptoAmount(cryptoAmountController.text);
lib/view_model/send/send_view_model.dart
+38 -2
@@ -225,6 +225,42 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
225 return wallet.balance[selectedCryptoCurrency]!.formattedFullAvailableBalance;
226 }
227
228 + @action
229 + Future<void> updateSendingBalance() async {
230 + // force the sendingBalance to recompute since unspent coins aren't observable
231 + // or at least mobx can't detect the changes
232 +
233 + final currentType = coinTypeToSpendFrom;
234 +
235 + if (currentType == UnspentCoinType.any) {
236 + coinTypeToSpendFrom = UnspentCoinType.nonMweb;
237 + } else if (currentType == UnspentCoinType.nonMweb) {
238 + coinTypeToSpendFrom = UnspentCoinType.any;
239 + } else if (currentType == UnspentCoinType.mweb) {
240 + coinTypeToSpendFrom = UnspentCoinType.nonMweb;
241 + }
242 +
243 + // set it back to the original value:
244 + coinTypeToSpendFrom = currentType;
245 + }
246 +
247 + @computed
248 + String get sendingBalance {
249 + // only for electrum, monero, wownero, decred wallets atm:
250 + switch (wallet.type) {
251 + case WalletType.bitcoin:
252 + case WalletType.litecoin:
253 + case WalletType.bitcoinCash:
254 + case WalletType.monero:
255 + case WalletType.wownero:
256 + case WalletType.decred:
257 + return wallet.formatCryptoAmount(
258 + unspentCoinsListViewModel.getSendingBalance(coinTypeToSpendFrom).toString());
259 + default:
260 + return balance;
261 + }
262 + }
263 +
264 @computed
265 bool get isFiatDisabled => balanceViewModel.isFiatDisabled;
266
@@ -502,14 +538,14 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
538 return bitcoin!.createBitcoinTransactionCredentials(
539 outputs,
540 priority: priority!,
505 - feeRate:feesViewModel. customBitcoinFeeRate,
541 + feeRate: feesViewModel.customBitcoinFeeRate,
542 coinTypeToSpendFrom: coinTypeToSpendFrom,
543 );
544 case WalletType.litecoin:
545 return bitcoin!.createBitcoinTransactionCredentials(
546 outputs,
547 priority: priority!,
512 - feeRate:feesViewModel. customBitcoinFeeRate,
548 + feeRate: feesViewModel.customBitcoinFeeRate,
549 // if it's an exchange flow then disable sending from mweb coins
550 coinTypeToSpendFrom: provider != null ? UnspentCoinType.nonMweb : coinTypeToSpendFrom,
551 );
lib/view_model/unspent_coins/unspent_coins_list_view_model.dart
+30
@@ -131,6 +131,36 @@ abstract class UnspentCoinsListViewModelBase with Store {
131 }
132 }
133
134 + List<Unspent> _getSpecificUnspents(UnspentCoinType overrideCoinTypeToSpendFrom) {
135 + switch (wallet.type) {
136 + case WalletType.monero:
137 + return monero!.getUnspents(wallet);
138 + case WalletType.wownero:
139 + return wownero!.getUnspents(wallet);
140 + case WalletType.bitcoin:
141 + case WalletType.litecoin:
142 + case WalletType.bitcoinCash:
143 + return bitcoin!.getUnspents(wallet, coinTypeToSpendFrom: overrideCoinTypeToSpendFrom);
144 + case WalletType.decred:
145 + return decred!.getUnspents(wallet);
146 + default:
147 + return List.empty();
148 + }
149 + }
150 +
151 + @action
152 + int getSendingBalance(UnspentCoinType overrideCoinTypeToSpendFrom) {
153 + // return items.where((element) => element.isSending).fold(0, (previousValue, element) => previousValue + element.value);
154 + // go through all unspent coins and add up the value minus frozen and non sending:
155 + int total = 0;
156 +
157 + for (final item in _getSpecificUnspents(overrideCoinTypeToSpendFrom)) {
158 + if (item.isFrozen || !item.isSending) continue;
159 + total += item.value;
160 + }
161 + return total;
162 + }
163 +
164 @action
165 void _updateUnspentCoinsInfo() {
166 items.clear();