Cw 830 coin control getting cleared (#1825)

* init commit * add select all button * localisation all coins * fix isSending and isFrozen state updates * fix: clean up electrum UTXOs * ui fixes * address the review comments[skip ci] * remove onPopInvoked[skip ci] --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

Serhii committed Nov 28, 2024 at 17:53 UTC 9cd69c4ba3649a1a928de806d925f540b07b5ad6
40 files changed +405 -128
cw_bitcoin/lib/bitcoin_wallet_service.dart
+9
@@ -106,6 +106,15 @@ class BitcoinWalletService extends WalletService<
106 final walletInfo = walletInfoSource.values
107 .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!;
108 await walletInfoSource.delete(walletInfo.key);
109 +
110 + final unspentCoinsToDelete = unspentCoinsInfoSource.values.where(
111 + (unspentCoin) => unspentCoin.walletId == walletInfo.id).toList();
112 +
113 + final keysToDelete = unspentCoinsToDelete.map((unspentCoin) => unspentCoin.key).toList();
114 +
115 + if (keysToDelete.isNotEmpty) {
116 + await unspentCoinsInfoSource.deleteAll(keysToDelete);
117 + }
118 }
119
120 @override
cw_bitcoin/lib/electrum_wallet.dart
+42 -15
@@ -304,6 +304,7 @@ abstract class ElectrumWalletBase
304 Future<void> init() async {
305 await walletAddresses.init();
306 await transactionHistory.init();
307 + await cleanUpDuplicateUnspentCoins();
308 await save();
309
310 _autoSaveTimer =
@@ -1379,10 +1380,11 @@ abstract class ElectrumWalletBase
1380 }));
1381
1382 unspentCoins = updatedUnspentCoins;
1383 +
1384 + final currentWalletUnspentCoins = unspentCoinsInfo.values.where((element) => element.walletId == id);
1385
1383 - if (unspentCoinsInfo.length != updatedUnspentCoins.length) {
1386 + if (currentWalletUnspentCoins.length != updatedUnspentCoins.length) {
1387 unspentCoins.forEach((coin) => addCoinInfo(coin));
1385 - return;
1388 }
1389
1390 await updateCoins(unspentCoins);
@@ -1408,6 +1410,7 @@ abstract class ElectrumWalletBase
1410 coin.isFrozen = coinInfo.isFrozen;
1411 coin.isSending = coinInfo.isSending;
1412 coin.note = coinInfo.note;
1413 +
1414 if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
1415 coin.bitcoinAddressRecord.balance += coinInfo.value;
1416 } else {
@@ -1445,20 +1448,27 @@ abstract class ElectrumWalletBase
1448
1449 @action
1450 Future<void> addCoinInfo(BitcoinUnspent coin) async {
1448 - final newInfo = UnspentCoinsInfo(
1449 - walletId: id,
1450 - hash: coin.hash,
1451 - isFrozen: coin.isFrozen,
1452 - isSending: coin.isSending,
1453 - noteRaw: coin.note,
1454 - address: coin.bitcoinAddressRecord.address,
1455 - value: coin.value,
1456 - vout: coin.vout,
1457 - isChange: coin.isChange,
1458 - isSilentPayment: coin is BitcoinSilentPaymentsUnspent,
1459 - );
1451
1461 - await unspentCoinsInfo.add(newInfo);
1452 + // Check if the coin is already in the unspentCoinsInfo for the wallet
1453 + final existingCoinInfo = unspentCoinsInfo.values.firstWhereOrNull(
1454 + (element) => element.walletId == walletInfo.id && element == coin);
1455 +
1456 + if (existingCoinInfo == null) {
1457 + final newInfo = UnspentCoinsInfo(
1458 + walletId: id,
1459 + hash: coin.hash,
1460 + isFrozen: coin.isFrozen,
1461 + isSending: coin.isSending,
1462 + noteRaw: coin.note,
1463 + address: coin.bitcoinAddressRecord.address,
1464 + value: coin.value,
1465 + vout: coin.vout,
1466 + isChange: coin.isChange,
1467 + isSilentPayment: coin is BitcoinSilentPaymentsUnspent,
1468 + );
1469 +
1470 + await unspentCoinsInfo.add(newInfo);
1471 + }
1472 }
1473
1474 Future<void> _refreshUnspentCoinsInfo() async {
@@ -1486,6 +1496,23 @@ abstract class ElectrumWalletBase
1496 }
1497 }
1498
1499 + Future<void> cleanUpDuplicateUnspentCoins() async {
1500 + final currentWalletUnspentCoins = unspentCoinsInfo.values.where((element) => element.walletId == id);
1501 + final Map<String, UnspentCoinsInfo> uniqueUnspentCoins = {};
1502 + final List<dynamic> duplicateKeys = [];
1503 +
1504 + for (final unspentCoin in currentWalletUnspentCoins) {
1505 + final key = '${unspentCoin.hash}:${unspentCoin.vout}';
1506 + if (!uniqueUnspentCoins.containsKey(key)) {
1507 + uniqueUnspentCoins[key] = unspentCoin;
1508 + } else {
1509 + duplicateKeys.add(unspentCoin.key);
1510 + }
1511 + }
1512 +
1513 + if (duplicateKeys.isNotEmpty) await unspentCoinsInfo.deleteAll(duplicateKeys);
1514 + }
1515 +
1516 int transactionVSize(String transactionHex) => BtcTransaction.fromRaw(transactionHex).getVSize();
1517
1518 Future<String?> canReplaceByFee(ElectrumTransactionInfo tx) async {
cw_bitcoin/lib/litecoin_wallet_service.dart
+9
@@ -126,6 +126,15 @@ class LitecoinWalletService extends WalletService<
126 mwebdLogs.deleteSync();
127 }
128 }
129 +
130 + final unspentCoinsToDelete = unspentCoinsInfoSource.values.where(
131 + (unspentCoin) => unspentCoin.walletId == walletInfo.id).toList();
132 +
133 + final keysToDelete = unspentCoinsToDelete.map((unspentCoin) => unspentCoin.key).toList();
134 +
135 + if (keysToDelete.isNotEmpty) {
136 + await unspentCoinsInfoSource.deleteAll(keysToDelete);
137 + }
138 }
139
140 @override
cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart
+9
@@ -85,6 +85,15 @@ class BitcoinCashWalletService extends WalletService<
85 final walletInfo = walletInfoSource.values
86 .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!;
87 await walletInfoSource.delete(walletInfo.key);
88 +
89 + final unspentCoinsToDelete = unspentCoinsInfoSource.values.where(
90 + (unspentCoin) => unspentCoin.walletId == walletInfo.id).toList();
91 +
92 + final keysToDelete = unspentCoinsToDelete.map((unspentCoin) => unspentCoin.key).toList();
93 +
94 + if (keysToDelete.isNotEmpty) {
95 + await unspentCoinsInfoSource.deleteAll(keysToDelete);
96 + }
97 }
98
99 @override
cw_core/lib/unspent_coins_info.dart
+2 -1
@@ -1,10 +1,11 @@
1 import 'package:cw_core/hive_type_ids.dart';
2 +import 'package:cw_core/unspent_comparable_mixin.dart';
3 import 'package:hive/hive.dart';
4
5 part 'unspent_coins_info.g.dart';
6
7 @HiveType(typeId: UnspentCoinsInfo.typeId)
7 -class UnspentCoinsInfo extends HiveObject {
8 +class UnspentCoinsInfo extends HiveObject with UnspentComparable {
9 UnspentCoinsInfo({
10 required this.walletId,
11 required this.hash,
cw_core/lib/unspent_comparable_mixin.dart new
+27
@@ -0,0 +1,27 @@
1 +mixin UnspentComparable {
2 + String get address;
3 +
4 + String get hash;
5 +
6 + int get value;
7 +
8 + int get vout;
9 +
10 + String? get keyImage;
11 +
12 + bool operator ==(Object other) {
13 + if (identical(this, other)) return true;
14 +
15 + return other is UnspentComparable &&
16 + other.hash == hash &&
17 + other.address == address &&
18 + other.value == value &&
19 + other.vout == vout &&
20 + other.keyImage == keyImage;
21 + }
22 +
23 + @override
24 + int get hashCode {
25 + return Object.hash(address, hash, value, vout, keyImage);
26 + }
27 +}
cw_core/lib/unspent_transaction_output.dart
+3 -1
@@ -1,4 +1,6 @@
1 -class Unspent {
1 +import 'package:cw_core/unspent_comparable_mixin.dart';
2 +
3 +class Unspent with UnspentComparable {
4 Unspent(this.address, this.hash, this.value, this.vout, this.keyImage)
5 : isSending = true,
6 isFrozen = false,
lib/src/screens/unspent_coins/unspent_coins_list_page.dart
+162 -34
@@ -1,13 +1,14 @@
1 -import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
1 import 'package:cake_wallet/generated/i18n.dart';
2 import 'package:cake_wallet/routes.dart';
3 import 'package:cake_wallet/src/screens/base_page.dart';
4 import 'package:cake_wallet/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart';
5 +import 'package:cake_wallet/src/widgets/alert_with_no_action.dart.dart';
6 +import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
7 +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
8 import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart';
7 -import 'package:cw_core/wallet_type.dart';
8 -import 'package:flutter/cupertino.dart';
9 import 'package:flutter/material.dart';
10 import 'package:flutter_mobx/flutter_mobx.dart';
11 +import 'package:mobx/mobx.dart';
12
13 class UnspentCoinsListPage extends BasePage {
14 UnspentCoinsListPage({required this.unspentCoinsListViewModel});
@@ -15,16 +16,53 @@ class UnspentCoinsListPage extends BasePage {
16 @override
17 String get title => S.current.unspent_coins_title;
18
19 + @override
20 + Widget leading(BuildContext context) {
21 + return MergeSemantics(
22 + child: SizedBox(
23 + height: 37,
24 + width: 37,
25 + child: ButtonTheme(
26 + minWidth: double.minPositive,
27 + child: Semantics(
28 + label: S.of(context).seed_alert_back,
29 + child: TextButton(
30 + style: ButtonStyle(
31 + overlayColor: WidgetStateColor.resolveWith((states) => Colors.transparent),
32 + ),
33 + onPressed: () async => await handleOnPopInvoked(context),
34 + child: backButton(context),
35 + ),
36 + ),
37 + ),
38 + ),
39 + );
40 + }
41 +
42 final UnspentCoinsListViewModel unspentCoinsListViewModel;
43
44 + Future<void> handleOnPopInvoked(BuildContext context) async {
45 + final hasChanged = unspentCoinsListViewModel.hasAdjustableFieldChanged;
46 + if (unspentCoinsListViewModel.items.isEmpty || !hasChanged) {
47 + Navigator.of(context).pop();
48 + } else {
49 + unspentCoinsListViewModel.setIsDisposing(true);
50 + await unspentCoinsListViewModel.dispose();
51 + Navigator.of(context).pop();
52 + Navigator.of(context).pop();
53 + }
54 + }
55 +
56 @override
21 - Widget body(BuildContext context) => UnspentCoinsListForm(unspentCoinsListViewModel);
57 + Widget body(BuildContext context) =>
58 + UnspentCoinsListForm(unspentCoinsListViewModel, handleOnPopInvoked);
59 }
60
61 class UnspentCoinsListForm extends StatefulWidget {
25 - UnspentCoinsListForm(this.unspentCoinsListViewModel);
62 + UnspentCoinsListForm(this.unspentCoinsListViewModel, this.handleOnPopInvoked);
63
64 final UnspentCoinsListViewModel unspentCoinsListViewModel;
65 + final Future<void> Function(BuildContext context) handleOnPopInvoked;
66
67 @override
68 UnspentCoinsListFormState createState() => UnspentCoinsListFormState(unspentCoinsListViewModel);
@@ -35,36 +73,126 @@ class UnspentCoinsListFormState extends State<UnspentCoinsListForm> {
73
74 final UnspentCoinsListViewModel unspentCoinsListViewModel;
75
76 + late Future<void> _initialization;
77 + ReactionDisposer? _disposer;
78 +
79 + @override
80 + void initState() {
81 + super.initState();
82 + _initialization = unspentCoinsListViewModel.initialSetup();
83 + _setupReactions();
84 + }
85 +
86 + void _setupReactions() {
87 + _disposer = reaction<bool>(
88 + (_) => unspentCoinsListViewModel.isDisposing,
89 + (isDisposing) {
90 + if (isDisposing) {
91 + _showSavingDataAlert();
92 + }
93 + },
94 + );
95 + }
96 +
97 + void _showSavingDataAlert() {
98 + showDialog<void>(
99 + context: context,
100 + builder: (BuildContext context) {
101 + return AlertWithNoAction(
102 + alertContent: 'Updating, please wait…',
103 + alertBarrierDismissible: false,
104 + );
105 + },
106 + );
107 + }
108 +
109 + @override
110 + void dispose() {
111 + _disposer?.call();
112 + super.dispose();
113 + }
114 +
115 @override
116 Widget build(BuildContext context) {
40 - return Container(
41 - padding: EdgeInsets.fromLTRB(24, 12, 24, 24),
42 - child: Observer(
43 - builder: (_) => ListView.separated(
44 - itemCount: unspentCoinsListViewModel.items.length,
45 - separatorBuilder: (_, __) => SizedBox(height: 15),
46 - itemBuilder: (_, int index) {
47 - return Observer(builder: (_) {
48 - final item = unspentCoinsListViewModel.items[index];
49 -
50 - return GestureDetector(
51 - onTap: () => Navigator.of(context).pushNamed(Routes.unspentCoinsDetails,
52 - arguments: [item, unspentCoinsListViewModel]),
53 - child: UnspentCoinsListItem(
54 - note: item.note,
55 - amount: item.amount,
56 - address: item.address,
57 - isSending: item.isSending,
58 - isFrozen: item.isFrozen,
59 - isChange: item.isChange,
60 - isSilentPayment: item.isSilentPayment,
61 - onCheckBoxTap: item.isFrozen
62 - ? null
63 - : () async {
64 - item.isSending = !item.isSending;
65 - await unspentCoinsListViewModel.saveUnspentCoinInfo(item);
66 - }));
67 - });
68 - })));
117 + return PopScope(
118 + canPop: false,
119 + onPopInvokedWithResult: (bool didPop, Object? result) async {
120 + if (didPop) return;
121 + if(mounted)
122 + await widget.handleOnPopInvoked(context);
123 + },
124 + child: FutureBuilder<void>(
125 + future: _initialization,
126 + builder: (context, snapshot) {
127 + if (snapshot.connectionState == ConnectionState.waiting) {
128 + return Center(child: CircularProgressIndicator());
129 + }
130 +
131 + if (snapshot.hasError) return Center(child: Text('Failed to load unspent coins'));
132 +
133 + return Container(
134 + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
135 + child: Observer(
136 + builder: (_) => Column(
137 + children: [
138 + if (unspentCoinsListViewModel.items.isNotEmpty)
139 + Row(
140 + children: [
141 + SizedBox(width: 12),
142 + StandardCheckbox(
143 + iconColor: Theme.of(context).extension<CakeTextTheme>()!.buttonTextColor,
144 + value: unspentCoinsListViewModel.isAllSelected,
145 + onChanged: (value) => unspentCoinsListViewModel.toggleSelectAll(value),
146 + ),
147 + SizedBox(width: 12),
148 + Text(
149 + S.current.all_coins,
150 + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
151 + ),
152 + ],
153 + ),
154 + SizedBox(height: 15),
155 + Expanded(
156 + child: unspentCoinsListViewModel.items.isEmpty
157 + ? Center(child: Text('No unspent coins available\ntry to reconnect',textAlign: TextAlign.center))
158 + : ListView.separated(
159 + itemCount: unspentCoinsListViewModel.items.length,
160 + separatorBuilder: (_, __) => SizedBox(height: 15),
161 + itemBuilder: (_, int index) {
162 + final item = unspentCoinsListViewModel.items[index];
163 + return Observer(
164 + builder: (_) => GestureDetector(
165 + onTap: () => Navigator.of(context).pushNamed(
166 + Routes.unspentCoinsDetails,
167 + arguments: [item, unspentCoinsListViewModel],
168 + ),
169 + child: UnspentCoinsListItem(
170 + note: item.note,
171 + amount: item.amount,
172 + address: item.address,
173 + isSending: item.isSending,
174 + isFrozen: item.isFrozen,
175 + isChange: item.isChange,
176 + isSilentPayment: item.isSilentPayment,
177 + onCheckBoxTap: item.isFrozen
178 + ? null
179 + : () async {
180 + item.isSending = !item.isSending;
181 + await unspentCoinsListViewModel
182 + .saveUnspentCoinInfo(item);
183 + },
184 + ),
185 + ),
186 + );
187 + },
188 + ),
189 + ),
190 + ],
191 + ),
192 + ),
193 + );
194 + },
195 + ),
196 + );
197 }
198 }
lib/src/widgets/alert_with_no_action.dart.dart
+4 -4
@@ -3,18 +3,18 @@ import 'package:cake_wallet/src/widgets/base_alert_dialog.dart';
3
4 class AlertWithNoAction extends BaseAlertDialog {
5 AlertWithNoAction({
6 - required this.alertTitle,
6 + this.alertTitle,
7 required this.alertContent,
8 this.alertBarrierDismissible = true,
9 Key? key,
10 });
11
12 - final String alertTitle;
12 + final String? alertTitle;
13 final String alertContent;
14 final bool alertBarrierDismissible;
15
16 @override
17 - String get titleText => alertTitle;
17 + String? get titleText => alertTitle;
18
19 @override
20 String get contentText => alertContent;
@@ -26,5 +26,5 @@ class AlertWithNoAction extends BaseAlertDialog {
26 bool get isBottomDividerExists => false;
27
28 @override
29 - Widget actionButtons(BuildContext context) => Container(height: 60);
29 + Widget actionButtons(BuildContext context) => Container();
30 }
lib/src/widgets/base_alert_dialog.dart
+4 -3
@@ -7,7 +7,7 @@ import 'package:flutter/material.dart';
7 class BaseAlertDialog extends StatelessWidget {
8 String? get headerText => '';
9
10 - String get titleText => '';
10 + String? get titleText => '';
11
12 String get contentText => '';
13
@@ -43,7 +43,7 @@ class BaseAlertDialog extends StatelessWidget {
43
44 Widget title(BuildContext context) {
45 return Text(
46 - titleText,
46 + titleText!,
47 textAlign: TextAlign.center,
48 style: TextStyle(
49 fontSize: 20,
@@ -191,10 +191,11 @@ class BaseAlertDialog extends StatelessWidget {
191 crossAxisAlignment: CrossAxisAlignment.center,
192 children: <Widget>[
193 if (headerText?.isNotEmpty ?? false) headerTitle(context),
194 + titleText != null ?
195 Padding(
196 padding: EdgeInsets.fromLTRB(24, 20, 24, 0),
197 child: title(context),
197 - ),
198 + ) : SizedBox(height: 16),
199 isDividerExists
200 ? Padding(
201 padding: EdgeInsets.only(top: 16, bottom: 8),
lib/view_model/unspent_coins/unspent_coins_item.dart
+4 -3
@@ -1,10 +1,11 @@
1 +import 'package:cw_core/unspent_comparable_mixin.dart';
2 import 'package:mobx/mobx.dart';
3
4 part 'unspent_coins_item.g.dart';
5
6 class UnspentCoinsItem = UnspentCoinsItemBase with _$UnspentCoinsItem;
7
7 -abstract class UnspentCoinsItemBase with Store {
8 +abstract class UnspentCoinsItemBase with Store, UnspentComparable {
9 UnspentCoinsItemBase({
10 required this.address,
11 required this.amount,
@@ -13,7 +14,7 @@ abstract class UnspentCoinsItemBase with Store {
14 required this.note,
15 required this.isSending,
16 required this.isChange,
16 - required this.amountRaw,
17 + required this.value,
18 required this.vout,
19 required this.keyImage,
20 required this.isSilentPayment,
@@ -41,7 +42,7 @@ abstract class UnspentCoinsItemBase with Store {
42 bool isChange;
43
44 @observable
44 - int amountRaw;
45 + int value;
46
47 @observable
48 int vout;
lib/view_model/unspent_coins/unspent_coins_list_view_model.dart
+101 -66
@@ -10,6 +10,7 @@ import 'package:cw_core/wallet_base.dart';
10 import 'package:cw_core/wallet_type.dart';
11 import 'package:flutter/cupertino.dart';
12 import 'package:hive/hive.dart';
13 +import 'package:collection/collection.dart';
14 import 'package:mobx/mobx.dart';
15
16 part 'unspent_coins_list_view_model.g.dart';
@@ -22,55 +23,66 @@ abstract class UnspentCoinsListViewModelBase with Store {
23 required Box<UnspentCoinsInfo> unspentCoinsInfo,
24 this.coinTypeToSpendFrom = UnspentCoinType.any,
25 }) : _unspentCoinsInfo = unspentCoinsInfo,
25 - _items = ObservableList<UnspentCoinsItem>() {
26 - _updateUnspentCoinsInfo();
27 - _updateUnspents();
28 - }
26 + items = ObservableList<UnspentCoinsItem>(),
27 + _originalState = {};
28
30 - WalletBase wallet;
29 + final WalletBase wallet;
30 final Box<UnspentCoinsInfo> _unspentCoinsInfo;
31 final UnspentCoinType coinTypeToSpendFrom;
32
33 @observable
35 - ObservableList<UnspentCoinsItem> _items;
34 + ObservableList<UnspentCoinsItem> items;
35
37 - @computed
38 - ObservableList<UnspentCoinsItem> get items => _items;
36 + final Map<String, Map<String, dynamic>> _originalState;
37
40 - Future<void> saveUnspentCoinInfo(UnspentCoinsItem item) async {
41 - try {
42 - final info =
43 - getUnspentCoinInfo(item.hash, item.address, item.amountRaw, item.vout, item.keyImage);
38 + @observable
39 + bool isDisposing = false;
40
45 - if (info == null) {
46 - return;
47 - }
41 + @computed
42 + bool get isAllSelected => items.every((element) => element.isFrozen || element.isSending);
43
49 - info.isFrozen = item.isFrozen;
50 - info.isSending = item.isSending;
51 - info.note = item.note;
44 + Future<void> initialSetup() async {
45 + await _updateUnspents();
46 + _storeOriginalState();
47 + }
48
53 - await info.save();
54 - await _updateUnspents();
55 - await wallet.updateBalance();
56 - } catch (e) {
57 - print(e.toString());
49 + void _storeOriginalState() {
50 + _originalState.clear();
51 + for (final item in items) {
52 + _originalState[item.hash] = {
53 + 'isFrozen': item.isFrozen,
54 + 'note': item.note,
55 + 'isSending': item.isSending,
56 + };
57 }
58 }
59
61 - UnspentCoinsInfo? getUnspentCoinInfo(
62 - String hash, String address, int value, int vout, String? keyImage) {
60 + bool _hasAdjustableFieldChanged(UnspentCoinsItem item) {
61 + final original = _originalState[item.hash];
62 + if (original == null) return false;
63 + return original['isFrozen'] != item.isFrozen ||
64 + original['note'] != item.note ||
65 + original['isSending'] != item.isSending;
66 + }
67 +
68 + bool get hasAdjustableFieldChanged => items.any(_hasAdjustableFieldChanged);
69 +
70 +
71 + Future<void> saveUnspentCoinInfo(UnspentCoinsItem item) async {
72 try {
64 - return _unspentCoinsInfo.values.firstWhere((element) =>
65 - element.walletId == wallet.id &&
66 - element.hash == hash &&
67 - element.address == address &&
68 - element.value == value &&
69 - element.vout == vout &&
70 - element.keyImage == keyImage);
73 + final existingInfo = _unspentCoinsInfo.values
74 + .firstWhereOrNull((element) => element.walletId == wallet.id && element == item);
75 + if (existingInfo == null) return;
76 +
77 + existingInfo.isFrozen = item.isFrozen;
78 + existingInfo.isSending = item.isSending;
79 + existingInfo.note = item.note;
80 +
81 +
82 + await existingInfo.save();
83 + _updateUnspentCoinsInfo();
84 } catch (e) {
72 - print("UnspentCoinsInfo not found for coin: $e");
73 - return null;
85 + print('Error saving coin info: $e');
86 }
87 }
88
@@ -115,37 +127,60 @@ abstract class UnspentCoinsListViewModelBase with Store {
127
128 @action
129 void _updateUnspentCoinsInfo() {
118 - _items.clear();
119 -
120 - List<UnspentCoinsItem> unspents = [];
121 - _getUnspents().forEach((Unspent elem) {
122 - try {
123 - final info =
124 - getUnspentCoinInfo(elem.hash, elem.address, elem.value, elem.vout, elem.keyImage);
125 - if (info == null) {
126 - return;
127 - }
128 -
129 - unspents.add(UnspentCoinsItem(
130 - address: elem.address,
131 - amount: '${formatAmountToString(elem.value)} ${wallet.currency.title}',
132 - hash: elem.hash,
133 - isFrozen: info.isFrozen,
134 - note: info.note,
135 - isSending: info.isSending,
136 - amountRaw: elem.value,
137 - vout: elem.vout,
138 - keyImage: elem.keyImage,
139 - isChange: elem.isChange,
140 - isSilentPayment: info.isSilentPayment ?? false,
141 - ));
142 - } catch (e, s) {
143 - print(s);
144 - print(e.toString());
145 - ExceptionHandler.onError(FlutterErrorDetails(exception: e, stack: s));
146 - }
147 - });
148 -
149 - _items.addAll(unspents);
130 + items.clear();
131 +
132 + final unspents = _getUnspents()
133 + .map((elem) {
134 + try {
135 + final existingItem = _unspentCoinsInfo.values
136 + .firstWhereOrNull((item) => item.walletId == wallet.id && item == elem);
137 +
138 + if (existingItem == null) return null;
139 +
140 + return UnspentCoinsItem(
141 + address: elem.address,
142 + amount: '${formatAmountToString(elem.value)} ${wallet.currency.title}',
143 + hash: elem.hash,
144 + isFrozen: existingItem.isFrozen,
145 + note: existingItem.note,
146 + isSending: existingItem.isSending,
147 + value: elem.value,
148 + vout: elem.vout,
149 + keyImage: elem.keyImage,
150 + isChange: elem.isChange,
151 + isSilentPayment: existingItem.isSilentPayment ?? false,
152 + );
153 + } catch (e, s) {
154 + print('Error: $e\nStack: $s');
155 + ExceptionHandler.onError(
156 + FlutterErrorDetails(exception: e, stack: s),
157 + );
158 + return null;
159 + }
160 + })
161 + .whereType<UnspentCoinsItem>()
162 + .toList();
163 +
164 + unspents.sort((a, b) => b.value.compareTo(a.value));
165 +
166 + items.addAll(unspents);
167 + }
168 +
169 + @action
170 + void toggleSelectAll(bool value) {
171 + for (final item in items) {
172 + if (item.isFrozen || item.isSending == value) continue;
173 + item.isSending = value;
174 + saveUnspentCoinInfo(item);
175 + }
176 + }
177 +
178 + @action
179 + void setIsDisposing(bool value) => isDisposing = value;
180 +
181 + @action
182 + Future<void> dispose() async {
183 + await _updateUnspents();
184 + await wallet.updateBalance();
185 }
186 }
res/values/strings_ar.arb
+1
@@ -38,6 +38,7 @@
38 "agree_to": "من خلال إنشاء حساب فإنك توافق على",
39 "alert_notice": "يلاحظ",
40 "all": "الكل",
41 + "all_coins": "كل العملات المعدنية",
42 "all_trades": "جميع عمليات التداول",
43 "all_transactions": "كل التحركات المالية",
44 "alphabetical": "مرتب حسب الحروف الأبجدية",
res/values/strings_bg.arb
+1
@@ -38,6 +38,7 @@
38 "agree_to": "Чрез създаването на акаунт вие се съгласявате с ",
39 "alert_notice": "Забележете",
40 "all": "ALL",
41 + "all_coins": "Всички монети",
42 "all_trades": "Всички сделкки",
43 "all_transactions": "Всички транзакции",
44 "alphabetical": "Азбучен ред",
res/values/strings_cs.arb
+1
@@ -38,6 +38,7 @@
38 "agree_to": "Vytvořením účtu souhlasíte s ",
39 "alert_notice": "Oznámení",
40 "all": "VŠE",
41 + "all_coins": "Všechny mince",
42 "all_trades": "Všechny obchody",
43 "all_transactions": "Všechny transakce",
44 "alphabetical": "Abecední",
res/values/strings_de.arb
+1
@@ -38,6 +38,7 @@
38 "agree_to": "Indem Sie ein Konto erstellen, stimmen Sie den ",
39 "alert_notice": "Beachten",
40 "all": "ALLES",
41 + "all_coins": "Alle Münzen",
42 "all_trades": "Alle Trades",
43 "all_transactions": "Alle Transaktionen",
44 "alphabetical": "Alphabetisch",
res/values/strings_en.arb
+1
@@ -38,6 +38,7 @@
38 "agree_to": "By creating account you agree to the ",
39 "alert_notice": "Notice",
40 "all": "ALL",
41 + "all_coins": "All Coins",
42 "all_trades": "All trades",
43 "all_transactions": "All transactions",
44 "alphabetical": "Alphabetical",
res/values/strings_es.arb
+1
@@ -38,6 +38,7 @@
38 "agree_to": "Al crear una cuenta, aceptas ",
39 "alert_notice": "Aviso",
40 "all": "Todos",
41 + "all_coins": "Todas las monedas",
42 "all_trades": "Todos los oficios",
43 "all_transactions": "Todas las transacciones",
44 "alphabetical": "Alfabético",
res/values/strings_fr.arb
+1
@@ -38,6 +38,7 @@
38 "agree_to": "En créant un compte, vous acceptez les ",
39 "alert_notice": "Avis",
40 "all": "TOUT",
41 + "all_coins": "Toutes les pièces",
42 "all_trades": "Tous échanges",
43 "all_transactions": "Toutes transactions",
44 "alphabetical": "Alphabétique",
res/values/strings_ha.arb
+1
@@ -38,6 +38,7 @@
38 "agree_to": "Ta hanyar ƙirƙirar asusu kun yarda da",
39 "alert_notice": "Sanarwa",
40 "all": "DUK",
41 + "all_coins": "Duk tsabar kudi",
42 "all_trades": "Duk ciniki",
43 "all_transactions": "Dukan Ma'amaloli",
44 "alphabetical": "Harafi",
res/values/strings_hi.arb
+1
@@ -38,6 +38,7 @@
38 "agree_to": "खाता बनाकर आप इससे सहमत होते हैं ",
39 "alert_notice": "सूचना",
40 "all": "सब",
41 + "all_coins": "सभी सिक्के",
42 "all_trades": "सभी व्यापार",
43 "all_transactions": "सभी लेन - देन",
44 "alphabetical": "वर्णमाला",
res/values/strings_hr.arb
+1
@@ -38,6 +38,7 @@
38 "agree_to": "Stvaranjem računa pristajete na ",
39 "alert_notice": "Obavijest",
40 "all": "SVE",
41 + "all_coins": "Sve kovanice",
42 "all_trades": "Svi obrti",
43 "all_transactions": "Sve transakcije",
44 "alphabetical": "Abecedno",
res/values/strings_hy.arb
+1
@@ -38,6 +38,7 @@
38 "agree_to": "Ստեղծելով հաշիվ դուք համաձայնում եք ",
39 "alert_notice": "Ծանուցում",
40 "all": "Բոլորը",
41 + "all_coins": "Բոլոր մետաղադրամները",
42 "all_trades": "Բոլոր գործարքները",
43 "all_transactions": "Բոլոր գործառնությունները",
44 "alphabetical": "Այբբենական",
res/values/strings_id.arb
+1
@@ -38,6 +38,7 @@
38 "agree_to": "Dengan membuat akun Anda setuju dengan ",
39 "alert_notice": "Melihat",
40 "all": "SEMUA",
41 + "all_coins": "Semua koin",
42 "all_trades": "Semua perdagangan",
43 "all_transactions": "Semua transaksi",
44 "alphabetical": "Alfabetis",
res/values/strings_it.arb
+1
@@ -38,6 +38,7 @@
38 "agree_to": "Creando un account accetti il ​​",
39 "alert_notice": "Avviso",
40 "all": "TUTTO",
41 + "all_coins": "Tutte le monete",
42 "all_trades": "Svi obrti",
43 "all_transactions": "Sve transakcije",
44 "alphabetical": "Alfabetico",
res/values/strings_ja.arb
+1
@@ -38,6 +38,7 @@
38 "agree_to": "アカウントを作成することにより、",
39 "alert_notice": "知らせ",
40 "all": "すべて",
41 + "all_coins": "すべてのコイン",
42 "all_trades": "すべての取引",
43 "all_transactions": "全取引",
44 "alphabetical": "アルファベット順",
res/values/strings_ko.arb
+2 -1
@@ -38,6 +38,7 @@
38 "agree_to": "계정을 생성하면 ",
39 "alert_notice": "알아채다",
40 "all": "모든",
41 + "all_coins": "모든 동전",
42 "all_trades": "A모든 거래",
43 "all_transactions": "모든 거래 창구",
44 "alphabetical": "알파벳순",
@@ -495,8 +496,8 @@
496 "placeholder_transactions": "거래가 여기에 표시됩니다",
497 "please_fill_totp": "다른 기기에 있는 8자리 코드를 입력하세요.",
498 "please_make_selection": "아래에서 선택하십시오 지갑 만들기 또는 복구.",
498 - "Please_reference_document": "자세한 내용은 아래 문서를 참조하십시오.",
499 "please_reference_document": "자세한 내용은 아래 문서를 참조하십시오.",
500 + "Please_reference_document": "자세한 내용은 아래 문서를 참조하십시오.",
501 "please_select": "선택 해주세요:",
502 "please_select_backup_file": "백업 파일을 선택하고 백업 암호를 입력하십시오.",
503 "please_try_to_connect_to_another_node": "다른 노드에 연결을 시도하십시오",
res/values/strings_my.arb
+1
@@ -38,6 +38,7 @@
38 "agree_to": "အကောင့်ဖန်တီးခြင်းဖြင့် သင်သည် ဤအရာကို သဘောတူပါသည်။",
39 "alert_notice": "မှတ်သား",
40 "all": "အားလုံး",
41 + "all_coins": "အားလုံးဒင်္ဂါးများ",
42 "all_trades": "ကုန်သွယ်မှုအားလုံး",
43 "all_transactions": "အရောင်းအဝယ်အားလုံး",
44 "alphabetical": "အက္ခရာစဉ်",
res/values/strings_nl.arb
+1
@@ -38,6 +38,7 @@
38 "agree_to": "Door een account aan te maken gaat u akkoord met de ",
39 "alert_notice": "Kennisgeving",
40 "all": "ALLE",
41 + "all_coins": "Alle munten",
42 "all_trades": "Alle transacties",
43 "all_transactions": "Alle transacties",
44 "alphabetical": "Alfabetisch",
res/values/strings_pl.arb
+1
@@ -38,6 +38,7 @@
38 "agree_to": "Tworząc konto wyrażasz zgodę na ",
39 "alert_notice": "Ogłoszenie",
40 "all": "WSZYSTKO",
41 + "all_coins": "Wszystkie monety",
42 "all_trades": "Wszystkie operacje",
43 "all_transactions": "Wszystkie transakcje",
44 "alphabetical": "Alfabetyczny",
res/values/strings_pt.arb
+1
@@ -38,6 +38,7 @@
38 "agree_to": "Ao criar conta você concorda com ",
39 "alert_notice": "Perceber",
40 "all": "TUDO",
41 + "all_coins": "Todas as moedas",
42 "all_trades": "Todas as negociações",
43 "all_transactions": "Todas as transacções",
44 "alphabetical": "alfabética",
res/values/strings_ru.arb
+1
@@ -38,6 +38,7 @@
38 "agree_to": "Создавая аккаунт, вы соглашаетесь с ",
39 "alert_notice": "Уведомление",
40 "all": "ВСЕ",
41 + "all_coins": "Все монеты",
42 "all_trades": "Все сделки",
43 "all_transactions": "Все транзакции",
44 "alphabetical": "Алфавитный",
res/values/strings_th.arb
+1
@@ -38,6 +38,7 @@
38 "agree_to": "การสร้างบัญชีของคุณยอมรับเงื่อนไขของ",
39 "alert_notice": "สังเกต",
40 "all": "ทั้งหมด",
41 + "all_coins": "เหรียญทั้งหมด",
42 "all_trades": "การซื้อขายทั้งหมด",
43 "all_transactions": "การทำธุรกรรมทั้งหมด",
44 "alphabetical": "ตามตัวอักษร",
res/values/strings_tl.arb
+1
@@ -38,6 +38,7 @@
38 "agree_to": "Sa pamamagitan ng paggawa ng account sumasang-ayon ka sa ",
39 "alert_notice": "PAUNAWA",
40 "all": "LAHAT",
41 + "all_coins": "Lahat ng mga barya",
42 "all_trades": "Lahat ng mga trade",
43 "all_transactions": "Lahat ng mga transaksyon",
44 "alphabetical": "Alpabeto",
res/values/strings_tr.arb
+1
@@ -38,6 +38,7 @@
38 "agree_to": "Hesap oluşturarak bunları kabul etmiş olursunuz ",
39 "alert_notice": "Fark etme",
40 "all": "HEPSİ",
41 + "all_coins": "Tüm Paralar",
42 "all_trades": "Tüm takaslar",
43 "all_transactions": "Tüm transferler",
44 "alphabetical": "Alfabetik",
res/values/strings_uk.arb
+1
@@ -38,6 +38,7 @@
38 "agree_to": "Створюючи обліковий запис, ви погоджуєтеся з ",
39 "alert_notice": "Ув'язнення",
40 "all": "ВСЕ",
41 + "all_coins": "Всі монети",
42 "all_trades": "Всі операції",
43 "all_transactions": "Всі транзакції",
44 "alphabetical": "Алфавітний",
res/values/strings_ur.arb
+1
@@ -38,6 +38,7 @@
38 "agree_to": "اکاؤنٹ بنا کر آپ اس سے اتفاق کرتے ہیں۔",
39 "alert_notice": "نوٹس",
40 "all": "تمام",
41 + "all_coins": "تمام سکے",
42 "all_trades": "تمام تجارت",
43 "all_transactions": "تمام لین دین",
44 "alphabetical": "حروف تہجی کے مطابق",
res/values/strings_vi.arb
+1
@@ -38,6 +38,7 @@
38 "agree_to": "Bằng cách tạo tài khoản, bạn đồng ý với ",
39 "alert_notice": "Để ý",
40 "all": "TẤT CẢ",
41 + "all_coins": "Tất cả các đồng tiền",
42 "all_trades": "Tất cả giao dịch",
43 "all_transactions": "Tất cả giao dịch",
44 "alphabetical": "Theo thứ tự chữ cái",
res/values/strings_yo.arb
+1
@@ -38,6 +38,7 @@
38 "agree_to": "Tẹ́ ẹ bá dá àkáǹtì ẹ jọ rò ",
39 "alert_notice": "Akiyesi",
40 "all": "Gbogbo",
41 + "all_coins": "Gbogbo awọn owó",
42 "all_trades": "Gbogbo àwọn pàṣípààrọ̀",
43 "all_transactions": "Gbogbo àwọn àránṣẹ́",
44 "alphabetical": "Labidibi",
res/values/strings_zh.arb
+1
@@ -38,6 +38,7 @@
38 "agree_to": "创建账户即表示您同意 ",
39 "alert_notice": "注意",
40 "all": "全部",
41 + "all_coins": "所有硬币",
42 "all_trades": "所有的变化",
43 "all_transactions": "所有交易",
44 "alphabetical": "按字母顺序",