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();