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": "按字母顺序",