From 9cd69c4ba3649a1a928de806d925f540b07b5ad6 Mon Sep 17 00:00:00 2001 From: Serhii Date: Thu, 28 Nov 2024 17:53:03 +0200 Subject: [PATCH] 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 --- cw_bitcoin/lib/bitcoin_wallet_service.dart | 9 + cw_bitcoin/lib/electrum_wallet.dart | 57 +++-- cw_bitcoin/lib/litecoin_wallet_service.dart | 9 + .../lib/src/bitcoin_cash_wallet_service.dart | 9 + cw_core/lib/unspent_coins_info.dart | 3 +- cw_core/lib/unspent_comparable_mixin.dart | 27 +++ cw_core/lib/unspent_transaction_output.dart | 4 +- .../unspent_coins_list_page.dart | 194 +++++++++++++++--- .../widgets/alert_with_no_action.dart.dart | 8 +- lib/src/widgets/base_alert_dialog.dart | 7 +- .../unspent_coins/unspent_coins_item.dart | 7 +- .../unspent_coins_list_view_model.dart | 163 +++++++++------ res/values/strings_ar.arb | 1 + res/values/strings_bg.arb | 1 + res/values/strings_cs.arb | 1 + res/values/strings_de.arb | 1 + res/values/strings_en.arb | 1 + res/values/strings_es.arb | 1 + res/values/strings_fr.arb | 1 + res/values/strings_ha.arb | 1 + res/values/strings_hi.arb | 1 + res/values/strings_hr.arb | 1 + res/values/strings_hy.arb | 1 + res/values/strings_id.arb | 1 + res/values/strings_it.arb | 1 + res/values/strings_ja.arb | 1 + res/values/strings_ko.arb | 3 +- res/values/strings_my.arb | 1 + res/values/strings_nl.arb | 1 + res/values/strings_pl.arb | 1 + res/values/strings_pt.arb | 1 + res/values/strings_ru.arb | 1 + res/values/strings_th.arb | 1 + res/values/strings_tl.arb | 1 + res/values/strings_tr.arb | 1 + res/values/strings_uk.arb | 1 + res/values/strings_ur.arb | 1 + res/values/strings_vi.arb | 1 + res/values/strings_yo.arb | 1 + res/values/strings_zh.arb | 1 + 40 files changed, 402 insertions(+), 125 deletions(-) create mode 100644 cw_core/lib/unspent_comparable_mixin.dart diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index 06f2082e4..7ee1534bf 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -106,6 +106,15 @@ class BitcoinWalletService extends WalletService< final walletInfo = walletInfoSource.values .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!; await walletInfoSource.delete(walletInfo.key); + + final unspentCoinsToDelete = unspentCoinsInfoSource.values.where( + (unspentCoin) => unspentCoin.walletId == walletInfo.id).toList(); + + final keysToDelete = unspentCoinsToDelete.map((unspentCoin) => unspentCoin.key).toList(); + + if (keysToDelete.isNotEmpty) { + await unspentCoinsInfoSource.deleteAll(keysToDelete); + } } @override diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 17ced5adf..771d135a0 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -304,6 +304,7 @@ abstract class ElectrumWalletBase Future init() async { await walletAddresses.init(); await transactionHistory.init(); + await cleanUpDuplicateUnspentCoins(); await save(); _autoSaveTimer = @@ -1379,10 +1380,11 @@ abstract class ElectrumWalletBase })); unspentCoins = updatedUnspentCoins; + + final currentWalletUnspentCoins = unspentCoinsInfo.values.where((element) => element.walletId == id); - if (unspentCoinsInfo.length != updatedUnspentCoins.length) { + if (currentWalletUnspentCoins.length != updatedUnspentCoins.length) { unspentCoins.forEach((coin) => addCoinInfo(coin)); - return; } await updateCoins(unspentCoins); @@ -1408,6 +1410,7 @@ abstract class ElectrumWalletBase coin.isFrozen = coinInfo.isFrozen; coin.isSending = coinInfo.isSending; coin.note = coinInfo.note; + if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) coin.bitcoinAddressRecord.balance += coinInfo.value; } else { @@ -1445,20 +1448,27 @@ abstract class ElectrumWalletBase @action Future addCoinInfo(BitcoinUnspent coin) async { - final newInfo = UnspentCoinsInfo( - walletId: id, - hash: coin.hash, - isFrozen: coin.isFrozen, - isSending: coin.isSending, - noteRaw: coin.note, - address: coin.bitcoinAddressRecord.address, - value: coin.value, - vout: coin.vout, - isChange: coin.isChange, - isSilentPayment: coin is BitcoinSilentPaymentsUnspent, - ); - await unspentCoinsInfo.add(newInfo); + // Check if the coin is already in the unspentCoinsInfo for the wallet + final existingCoinInfo = unspentCoinsInfo.values.firstWhereOrNull( + (element) => element.walletId == walletInfo.id && element == coin); + + if (existingCoinInfo == null) { + final newInfo = UnspentCoinsInfo( + walletId: id, + hash: coin.hash, + isFrozen: coin.isFrozen, + isSending: coin.isSending, + noteRaw: coin.note, + address: coin.bitcoinAddressRecord.address, + value: coin.value, + vout: coin.vout, + isChange: coin.isChange, + isSilentPayment: coin is BitcoinSilentPaymentsUnspent, + ); + + await unspentCoinsInfo.add(newInfo); + } } Future _refreshUnspentCoinsInfo() async { @@ -1486,6 +1496,23 @@ abstract class ElectrumWalletBase } } + Future cleanUpDuplicateUnspentCoins() async { + final currentWalletUnspentCoins = unspentCoinsInfo.values.where((element) => element.walletId == id); + final Map uniqueUnspentCoins = {}; + final List duplicateKeys = []; + + for (final unspentCoin in currentWalletUnspentCoins) { + final key = '${unspentCoin.hash}:${unspentCoin.vout}'; + if (!uniqueUnspentCoins.containsKey(key)) { + uniqueUnspentCoins[key] = unspentCoin; + } else { + duplicateKeys.add(unspentCoin.key); + } + } + + if (duplicateKeys.isNotEmpty) await unspentCoinsInfo.deleteAll(duplicateKeys); + } + int transactionVSize(String transactionHex) => BtcTransaction.fromRaw(transactionHex).getVSize(); Future canReplaceByFee(ElectrumTransactionInfo tx) async { diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index d519f4d0a..89ae384d4 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -126,6 +126,15 @@ class LitecoinWalletService extends WalletService< mwebdLogs.deleteSync(); } } + + final unspentCoinsToDelete = unspentCoinsInfoSource.values.where( + (unspentCoin) => unspentCoin.walletId == walletInfo.id).toList(); + + final keysToDelete = unspentCoinsToDelete.map((unspentCoin) => unspentCoin.key).toList(); + + if (keysToDelete.isNotEmpty) { + await unspentCoinsInfoSource.deleteAll(keysToDelete); + } } @override diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart index d14dc582d..931893ef8 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart @@ -85,6 +85,15 @@ class BitcoinCashWalletService extends WalletService< final walletInfo = walletInfoSource.values .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!; await walletInfoSource.delete(walletInfo.key); + + final unspentCoinsToDelete = unspentCoinsInfoSource.values.where( + (unspentCoin) => unspentCoin.walletId == walletInfo.id).toList(); + + final keysToDelete = unspentCoinsToDelete.map((unspentCoin) => unspentCoin.key).toList(); + + if (keysToDelete.isNotEmpty) { + await unspentCoinsInfoSource.deleteAll(keysToDelete); + } } @override diff --git a/cw_core/lib/unspent_coins_info.dart b/cw_core/lib/unspent_coins_info.dart index ed09e17e0..a60feb634 100644 --- a/cw_core/lib/unspent_coins_info.dart +++ b/cw_core/lib/unspent_coins_info.dart @@ -1,10 +1,11 @@ import 'package:cw_core/hive_type_ids.dart'; +import 'package:cw_core/unspent_comparable_mixin.dart'; import 'package:hive/hive.dart'; part 'unspent_coins_info.g.dart'; @HiveType(typeId: UnspentCoinsInfo.typeId) -class UnspentCoinsInfo extends HiveObject { +class UnspentCoinsInfo extends HiveObject with UnspentComparable { UnspentCoinsInfo({ required this.walletId, required this.hash, diff --git a/cw_core/lib/unspent_comparable_mixin.dart b/cw_core/lib/unspent_comparable_mixin.dart new file mode 100644 index 000000000..ee0c05496 --- /dev/null +++ b/cw_core/lib/unspent_comparable_mixin.dart @@ -0,0 +1,27 @@ +mixin UnspentComparable { + String get address; + + String get hash; + + int get value; + + int get vout; + + String? get keyImage; + + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is UnspentComparable && + other.hash == hash && + other.address == address && + other.value == value && + other.vout == vout && + other.keyImage == keyImage; + } + + @override + int get hashCode { + return Object.hash(address, hash, value, vout, keyImage); + } +} diff --git a/cw_core/lib/unspent_transaction_output.dart b/cw_core/lib/unspent_transaction_output.dart index d225493e9..da71f6983 100644 --- a/cw_core/lib/unspent_transaction_output.dart +++ b/cw_core/lib/unspent_transaction_output.dart @@ -1,4 +1,6 @@ -class Unspent { +import 'package:cw_core/unspent_comparable_mixin.dart'; + +class Unspent with UnspentComparable { Unspent(this.address, this.hash, this.value, this.vout, this.keyImage) : isSending = true, isFrozen = false, diff --git a/lib/src/screens/unspent_coins/unspent_coins_list_page.dart b/lib/src/screens/unspent_coins/unspent_coins_list_page.dart index ee6d6dc73..f26a2a17f 100644 --- a/lib/src/screens/unspent_coins/unspent_coins_list_page.dart +++ b/lib/src/screens/unspent_coins/unspent_coins_list_page.dart @@ -1,13 +1,14 @@ -import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart'; +import 'package:cake_wallet/src/widgets/alert_with_no_action.dart.dart'; +import 'package:cake_wallet/src/widgets/standard_checkbox.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; class UnspentCoinsListPage extends BasePage { UnspentCoinsListPage({required this.unspentCoinsListViewModel}); @@ -15,16 +16,53 @@ class UnspentCoinsListPage extends BasePage { @override String get title => S.current.unspent_coins_title; + @override + Widget leading(BuildContext context) { + return MergeSemantics( + child: SizedBox( + height: 37, + width: 37, + child: ButtonTheme( + minWidth: double.minPositive, + child: Semantics( + label: S.of(context).seed_alert_back, + child: TextButton( + style: ButtonStyle( + overlayColor: WidgetStateColor.resolveWith((states) => Colors.transparent), + ), + onPressed: () async => await handleOnPopInvoked(context), + child: backButton(context), + ), + ), + ), + ), + ); + } + final UnspentCoinsListViewModel unspentCoinsListViewModel; + Future handleOnPopInvoked(BuildContext context) async { + final hasChanged = unspentCoinsListViewModel.hasAdjustableFieldChanged; + if (unspentCoinsListViewModel.items.isEmpty || !hasChanged) { + Navigator.of(context).pop(); + } else { + unspentCoinsListViewModel.setIsDisposing(true); + await unspentCoinsListViewModel.dispose(); + Navigator.of(context).pop(); + Navigator.of(context).pop(); + } + } + @override - Widget body(BuildContext context) => UnspentCoinsListForm(unspentCoinsListViewModel); + Widget body(BuildContext context) => + UnspentCoinsListForm(unspentCoinsListViewModel, handleOnPopInvoked); } class UnspentCoinsListForm extends StatefulWidget { - UnspentCoinsListForm(this.unspentCoinsListViewModel); + UnspentCoinsListForm(this.unspentCoinsListViewModel, this.handleOnPopInvoked); final UnspentCoinsListViewModel unspentCoinsListViewModel; + final Future Function(BuildContext context) handleOnPopInvoked; @override UnspentCoinsListFormState createState() => UnspentCoinsListFormState(unspentCoinsListViewModel); @@ -35,36 +73,126 @@ class UnspentCoinsListFormState extends State { final UnspentCoinsListViewModel unspentCoinsListViewModel; + late Future _initialization; + ReactionDisposer? _disposer; + + @override + void initState() { + super.initState(); + _initialization = unspentCoinsListViewModel.initialSetup(); + _setupReactions(); + } + + void _setupReactions() { + _disposer = reaction( + (_) => unspentCoinsListViewModel.isDisposing, + (isDisposing) { + if (isDisposing) { + _showSavingDataAlert(); + } + }, + ); + } + + void _showSavingDataAlert() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertWithNoAction( + alertContent: 'Updating, please wait…', + alertBarrierDismissible: false, + ); + }, + ); + } + + @override + void dispose() { + _disposer?.call(); + super.dispose(); + } + @override Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.fromLTRB(24, 12, 24, 24), - child: Observer( - builder: (_) => ListView.separated( - itemCount: unspentCoinsListViewModel.items.length, - separatorBuilder: (_, __) => SizedBox(height: 15), - itemBuilder: (_, int index) { - return Observer(builder: (_) { - final item = unspentCoinsListViewModel.items[index]; + return PopScope( + canPop: false, + onPopInvokedWithResult: (bool didPop, Object? result) async { + if (didPop) return; + if(mounted) + await widget.handleOnPopInvoked(context); + }, + child: FutureBuilder( + future: _initialization, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center(child: CircularProgressIndicator()); + } - return GestureDetector( - onTap: () => Navigator.of(context).pushNamed(Routes.unspentCoinsDetails, - arguments: [item, unspentCoinsListViewModel]), - child: UnspentCoinsListItem( - note: item.note, - amount: item.amount, - address: item.address, - isSending: item.isSending, - isFrozen: item.isFrozen, - isChange: item.isChange, - isSilentPayment: item.isSilentPayment, - onCheckBoxTap: item.isFrozen - ? null - : () async { - item.isSending = !item.isSending; - await unspentCoinsListViewModel.saveUnspentCoinInfo(item); - })); - }); - }))); + if (snapshot.hasError) return Center(child: Text('Failed to load unspent coins')); + + return Container( + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12), + child: Observer( + builder: (_) => Column( + children: [ + if (unspentCoinsListViewModel.items.isNotEmpty) + Row( + children: [ + SizedBox(width: 12), + StandardCheckbox( + iconColor: Theme.of(context).extension()!.buttonTextColor, + value: unspentCoinsListViewModel.isAllSelected, + onChanged: (value) => unspentCoinsListViewModel.toggleSelectAll(value), + ), + SizedBox(width: 12), + Text( + S.current.all_coins, + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ], + ), + SizedBox(height: 15), + Expanded( + child: unspentCoinsListViewModel.items.isEmpty + ? Center(child: Text('No unspent coins available\ntry to reconnect',textAlign: TextAlign.center)) + : ListView.separated( + itemCount: unspentCoinsListViewModel.items.length, + separatorBuilder: (_, __) => SizedBox(height: 15), + itemBuilder: (_, int index) { + final item = unspentCoinsListViewModel.items[index]; + return Observer( + builder: (_) => GestureDetector( + onTap: () => Navigator.of(context).pushNamed( + Routes.unspentCoinsDetails, + arguments: [item, unspentCoinsListViewModel], + ), + child: UnspentCoinsListItem( + note: item.note, + amount: item.amount, + address: item.address, + isSending: item.isSending, + isFrozen: item.isFrozen, + isChange: item.isChange, + isSilentPayment: item.isSilentPayment, + onCheckBoxTap: item.isFrozen + ? null + : () async { + item.isSending = !item.isSending; + await unspentCoinsListViewModel + .saveUnspentCoinInfo(item); + }, + ), + ), + ); + }, + ), + ), + ], + ), + ), + ); + }, + ), + ); } } diff --git a/lib/src/widgets/alert_with_no_action.dart.dart b/lib/src/widgets/alert_with_no_action.dart.dart index 623656397..75c1785cd 100644 --- a/lib/src/widgets/alert_with_no_action.dart.dart +++ b/lib/src/widgets/alert_with_no_action.dart.dart @@ -3,18 +3,18 @@ import 'package:cake_wallet/src/widgets/base_alert_dialog.dart'; class AlertWithNoAction extends BaseAlertDialog { AlertWithNoAction({ - required this.alertTitle, + this.alertTitle, required this.alertContent, this.alertBarrierDismissible = true, Key? key, }); - final String alertTitle; + final String? alertTitle; final String alertContent; final bool alertBarrierDismissible; @override - String get titleText => alertTitle; + String? get titleText => alertTitle; @override String get contentText => alertContent; @@ -26,5 +26,5 @@ class AlertWithNoAction extends BaseAlertDialog { bool get isBottomDividerExists => false; @override - Widget actionButtons(BuildContext context) => Container(height: 60); + Widget actionButtons(BuildContext context) => Container(); } diff --git a/lib/src/widgets/base_alert_dialog.dart b/lib/src/widgets/base_alert_dialog.dart index 1b521a427..53b7a9cbf 100644 --- a/lib/src/widgets/base_alert_dialog.dart +++ b/lib/src/widgets/base_alert_dialog.dart @@ -7,7 +7,7 @@ import 'package:flutter/material.dart'; class BaseAlertDialog extends StatelessWidget { String? get headerText => ''; - String get titleText => ''; + String? get titleText => ''; String get contentText => ''; @@ -43,7 +43,7 @@ class BaseAlertDialog extends StatelessWidget { Widget title(BuildContext context) { return Text( - titleText, + titleText!, textAlign: TextAlign.center, style: TextStyle( fontSize: 20, @@ -191,10 +191,11 @@ class BaseAlertDialog extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ if (headerText?.isNotEmpty ?? false) headerTitle(context), + titleText != null ? Padding( padding: EdgeInsets.fromLTRB(24, 20, 24, 0), child: title(context), - ), + ) : SizedBox(height: 16), isDividerExists ? Padding( padding: EdgeInsets.only(top: 16, bottom: 8), diff --git a/lib/view_model/unspent_coins/unspent_coins_item.dart b/lib/view_model/unspent_coins/unspent_coins_item.dart index 4ca5a10a2..70488f8ee 100644 --- a/lib/view_model/unspent_coins/unspent_coins_item.dart +++ b/lib/view_model/unspent_coins/unspent_coins_item.dart @@ -1,10 +1,11 @@ +import 'package:cw_core/unspent_comparable_mixin.dart'; import 'package:mobx/mobx.dart'; part 'unspent_coins_item.g.dart'; class UnspentCoinsItem = UnspentCoinsItemBase with _$UnspentCoinsItem; -abstract class UnspentCoinsItemBase with Store { +abstract class UnspentCoinsItemBase with Store, UnspentComparable { UnspentCoinsItemBase({ required this.address, required this.amount, @@ -13,7 +14,7 @@ abstract class UnspentCoinsItemBase with Store { required this.note, required this.isSending, required this.isChange, - required this.amountRaw, + required this.value, required this.vout, required this.keyImage, required this.isSilentPayment, @@ -41,7 +42,7 @@ abstract class UnspentCoinsItemBase with Store { bool isChange; @observable - int amountRaw; + int value; @observable int vout; 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 f16b8390f..6c15511b5 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 @@ -10,6 +10,7 @@ import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/cupertino.dart'; import 'package:hive/hive.dart'; +import 'package:collection/collection.dart'; import 'package:mobx/mobx.dart'; part 'unspent_coins_list_view_model.g.dart'; @@ -22,55 +23,66 @@ abstract class UnspentCoinsListViewModelBase with Store { required Box unspentCoinsInfo, this.coinTypeToSpendFrom = UnspentCoinType.any, }) : _unspentCoinsInfo = unspentCoinsInfo, - _items = ObservableList() { - _updateUnspentCoinsInfo(); - _updateUnspents(); - } + items = ObservableList(), + _originalState = {}; - WalletBase wallet; + final WalletBase wallet; final Box _unspentCoinsInfo; final UnspentCoinType coinTypeToSpendFrom; @observable - ObservableList _items; + ObservableList items; + + final Map> _originalState; + + @observable + bool isDisposing = false; @computed - ObservableList get items => _items; + bool get isAllSelected => items.every((element) => element.isFrozen || element.isSending); - Future saveUnspentCoinInfo(UnspentCoinsItem item) async { - try { - final info = - getUnspentCoinInfo(item.hash, item.address, item.amountRaw, item.vout, item.keyImage); + Future initialSetup() async { + await _updateUnspents(); + _storeOriginalState(); + } - if (info == null) { - return; - } - - info.isFrozen = item.isFrozen; - info.isSending = item.isSending; - info.note = item.note; - - await info.save(); - await _updateUnspents(); - await wallet.updateBalance(); - } catch (e) { - print(e.toString()); + void _storeOriginalState() { + _originalState.clear(); + for (final item in items) { + _originalState[item.hash] = { + 'isFrozen': item.isFrozen, + 'note': item.note, + 'isSending': item.isSending, + }; } } - UnspentCoinsInfo? getUnspentCoinInfo( - String hash, String address, int value, int vout, String? keyImage) { + bool _hasAdjustableFieldChanged(UnspentCoinsItem item) { + final original = _originalState[item.hash]; + if (original == null) return false; + return original['isFrozen'] != item.isFrozen || + original['note'] != item.note || + original['isSending'] != item.isSending; + } + + bool get hasAdjustableFieldChanged => items.any(_hasAdjustableFieldChanged); + + + Future saveUnspentCoinInfo(UnspentCoinsItem item) async { try { - return _unspentCoinsInfo.values.firstWhere((element) => - element.walletId == wallet.id && - element.hash == hash && - element.address == address && - element.value == value && - element.vout == vout && - element.keyImage == keyImage); + final existingInfo = _unspentCoinsInfo.values + .firstWhereOrNull((element) => element.walletId == wallet.id && element == item); + if (existingInfo == null) return; + + existingInfo.isFrozen = item.isFrozen; + existingInfo.isSending = item.isSending; + existingInfo.note = item.note; + + + await existingInfo.save(); + _updateUnspentCoinsInfo(); } catch (e) { - print("UnspentCoinsInfo not found for coin: $e"); - return null; + print('Error saving coin info: $e'); } } @@ -115,37 +127,60 @@ abstract class UnspentCoinsListViewModelBase with Store { @action void _updateUnspentCoinsInfo() { - _items.clear(); + items.clear(); - List unspents = []; - _getUnspents().forEach((Unspent elem) { - try { - final info = - getUnspentCoinInfo(elem.hash, elem.address, elem.value, elem.vout, elem.keyImage); - if (info == null) { - return; - } + final unspents = _getUnspents() + .map((elem) { + try { + final existingItem = _unspentCoinsInfo.values + .firstWhereOrNull((item) => item.walletId == wallet.id && item == elem); - unspents.add(UnspentCoinsItem( - address: elem.address, - amount: '${formatAmountToString(elem.value)} ${wallet.currency.title}', - hash: elem.hash, - isFrozen: info.isFrozen, - note: info.note, - isSending: info.isSending, - amountRaw: elem.value, - vout: elem.vout, - keyImage: elem.keyImage, - isChange: elem.isChange, - isSilentPayment: info.isSilentPayment ?? false, - )); - } catch (e, s) { - print(s); - print(e.toString()); - ExceptionHandler.onError(FlutterErrorDetails(exception: e, stack: s)); - } - }); + if (existingItem == null) return null; - _items.addAll(unspents); + return UnspentCoinsItem( + address: elem.address, + amount: '${formatAmountToString(elem.value)} ${wallet.currency.title}', + hash: elem.hash, + isFrozen: existingItem.isFrozen, + note: existingItem.note, + isSending: existingItem.isSending, + value: elem.value, + vout: elem.vout, + keyImage: elem.keyImage, + isChange: elem.isChange, + isSilentPayment: existingItem.isSilentPayment ?? false, + ); + } catch (e, s) { + print('Error: $e\nStack: $s'); + ExceptionHandler.onError( + FlutterErrorDetails(exception: e, stack: s), + ); + return null; + } + }) + .whereType() + .toList(); + + unspents.sort((a, b) => b.value.compareTo(a.value)); + + items.addAll(unspents); + } + + @action + void toggleSelectAll(bool value) { + for (final item in items) { + if (item.isFrozen || item.isSending == value) continue; + item.isSending = value; + saveUnspentCoinInfo(item); + } + } + + @action + void setIsDisposing(bool value) => isDisposing = value; + + @action + Future dispose() async { + await _updateUnspents(); + await wallet.updateBalance(); } } diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 25a3d5fa5..968f627ca 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -38,6 +38,7 @@ "agree_to": "من خلال إنشاء حساب فإنك توافق على", "alert_notice": "يلاحظ", "all": "الكل", + "all_coins": "كل العملات المعدنية", "all_trades": "جميع عمليات التداول", "all_transactions": "كل التحركات المالية", "alphabetical": "مرتب حسب الحروف الأبجدية", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index ef3dc48df..24a7cf803 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -38,6 +38,7 @@ "agree_to": "Чрез създаването на акаунт вие се съгласявате с ", "alert_notice": "Забележете", "all": "ALL", + "all_coins": "Всички монети", "all_trades": "Всички сделкки", "all_transactions": "Всички транзакции", "alphabetical": "Азбучен ред", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 1e8e9eff6..6626a3119 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -38,6 +38,7 @@ "agree_to": "Vytvořením účtu souhlasíte s ", "alert_notice": "Oznámení", "all": "VŠE", + "all_coins": "Všechny mince", "all_trades": "Všechny obchody", "all_transactions": "Všechny transakce", "alphabetical": "Abecední", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 871301833..78431ff5f 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -38,6 +38,7 @@ "agree_to": "Indem Sie ein Konto erstellen, stimmen Sie den ", "alert_notice": "Beachten", "all": "ALLES", + "all_coins": "Alle Münzen", "all_trades": "Alle Trades", "all_transactions": "Alle Transaktionen", "alphabetical": "Alphabetisch", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 057bc6d6b..8acf49d76 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -38,6 +38,7 @@ "agree_to": "By creating account you agree to the ", "alert_notice": "Notice", "all": "ALL", + "all_coins": "All Coins", "all_trades": "All trades", "all_transactions": "All transactions", "alphabetical": "Alphabetical", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index c9135f054..3c009c5ea 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -38,6 +38,7 @@ "agree_to": "Al crear una cuenta, aceptas ", "alert_notice": "Aviso", "all": "Todos", + "all_coins": "Todas las monedas", "all_trades": "Todos los oficios", "all_transactions": "Todas las transacciones", "alphabetical": "Alfabético", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index eaeac58a9..17408cd44 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -38,6 +38,7 @@ "agree_to": "En créant un compte, vous acceptez les ", "alert_notice": "Avis", "all": "TOUT", + "all_coins": "Toutes les pièces", "all_trades": "Tous échanges", "all_transactions": "Toutes transactions", "alphabetical": "Alphabétique", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 6a0713d6d..2fa206298 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -38,6 +38,7 @@ "agree_to": "Ta hanyar ƙirƙirar asusu kun yarda da", "alert_notice": "Sanarwa", "all": "DUK", + "all_coins": "Duk tsabar kudi", "all_trades": "Duk ciniki", "all_transactions": "Dukan Ma'amaloli", "alphabetical": "Harafi", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index f1e294d22..f31635c75 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -38,6 +38,7 @@ "agree_to": "खाता बनाकर आप इससे सहमत होते हैं ", "alert_notice": "सूचना", "all": "सब", + "all_coins": "सभी सिक्के", "all_trades": "सभी व्यापार", "all_transactions": "सभी लेन - देन", "alphabetical": "वर्णमाला", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 6b9f20259..17f161ce3 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -38,6 +38,7 @@ "agree_to": "Stvaranjem računa pristajete na ", "alert_notice": "Obavijest", "all": "SVE", + "all_coins": "Sve kovanice", "all_trades": "Svi obrti", "all_transactions": "Sve transakcije", "alphabetical": "Abecedno", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index d3d7987d2..8736f1fc2 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -38,6 +38,7 @@ "agree_to": "Ստեղծելով հաշիվ դուք համաձայնում եք ", "alert_notice": "Ծանուցում", "all": "Բոլորը", + "all_coins": "Բոլոր մետաղադրամները", "all_trades": "Բոլոր գործարքները", "all_transactions": "Բոլոր գործառնությունները", "alphabetical": "Այբբենական", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index fae208549..44a04ae9b 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -38,6 +38,7 @@ "agree_to": "Dengan membuat akun Anda setuju dengan ", "alert_notice": "Melihat", "all": "SEMUA", + "all_coins": "Semua koin", "all_trades": "Semua perdagangan", "all_transactions": "Semua transaksi", "alphabetical": "Alfabetis", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index e6e4928c5..44358c450 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -38,6 +38,7 @@ "agree_to": "Creando un account accetti il ​​", "alert_notice": "Avviso", "all": "TUTTO", + "all_coins": "Tutte le monete", "all_trades": "Svi obrti", "all_transactions": "Sve transakcije", "alphabetical": "Alfabetico", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index fd4d83cc8..6bcde5a09 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -38,6 +38,7 @@ "agree_to": "アカウントを作成することにより、", "alert_notice": "知らせ", "all": "すべて", + "all_coins": "すべてのコイン", "all_trades": "すべての取引", "all_transactions": "全取引", "alphabetical": "アルファベット順", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 6fce665ec..b18657bc9 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -38,6 +38,7 @@ "agree_to": "계정을 생성하면 ", "alert_notice": "알아채다", "all": "모든", + "all_coins": "모든 동전", "all_trades": "A모든 거래", "all_transactions": "모든 거래 창구", "alphabetical": "알파벳순", @@ -495,8 +496,8 @@ "placeholder_transactions": "거래가 여기에 표시됩니다", "please_fill_totp": "다른 기기에 있는 8자리 코드를 입력하세요.", "please_make_selection": "아래에서 선택하십시오 지갑 만들기 또는 복구.", - "Please_reference_document": "자세한 내용은 아래 문서를 참조하십시오.", "please_reference_document": "자세한 내용은 아래 문서를 참조하십시오.", + "Please_reference_document": "자세한 내용은 아래 문서를 참조하십시오.", "please_select": "선택 해주세요:", "please_select_backup_file": "백업 파일을 선택하고 백업 암호를 입력하십시오.", "please_try_to_connect_to_another_node": "다른 노드에 연결을 시도하십시오", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index e933010fd..45bad4d13 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -38,6 +38,7 @@ "agree_to": "အကောင့်ဖန်တီးခြင်းဖြင့် သင်သည် ဤအရာကို သဘောတူပါသည်။", "alert_notice": "မှတ်သား", "all": "အားလုံး", + "all_coins": "အားလုံးဒင်္ဂါးများ", "all_trades": "ကုန်သွယ်မှုအားလုံး", "all_transactions": "အရောင်းအဝယ်အားလုံး", "alphabetical": "အက္ခရာစဉ်", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index fc6496c3f..c332956c6 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -38,6 +38,7 @@ "agree_to": "Door een account aan te maken gaat u akkoord met de ", "alert_notice": "Kennisgeving", "all": "ALLE", + "all_coins": "Alle munten", "all_trades": "Alle transacties", "all_transactions": "Alle transacties", "alphabetical": "Alfabetisch", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index c7cf09cd8..26cbf7ac7 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -38,6 +38,7 @@ "agree_to": "Tworząc konto wyrażasz zgodę na ", "alert_notice": "Ogłoszenie", "all": "WSZYSTKO", + "all_coins": "Wszystkie monety", "all_trades": "Wszystkie operacje", "all_transactions": "Wszystkie transakcje", "alphabetical": "Alfabetyczny", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 50c7f4d6c..74d0d3cf5 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -38,6 +38,7 @@ "agree_to": "Ao criar conta você concorda com ", "alert_notice": "Perceber", "all": "TUDO", + "all_coins": "Todas as moedas", "all_trades": "Todas as negociações", "all_transactions": "Todas as transacções", "alphabetical": "alfabética", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 639ddfe63..d454bddbe 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -38,6 +38,7 @@ "agree_to": "Создавая аккаунт, вы соглашаетесь с ", "alert_notice": "Уведомление", "all": "ВСЕ", + "all_coins": "Все монеты", "all_trades": "Все сделки", "all_transactions": "Все транзакции", "alphabetical": "Алфавитный", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index e0b352fea..b9c51258e 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -38,6 +38,7 @@ "agree_to": "การสร้างบัญชีของคุณยอมรับเงื่อนไขของ", "alert_notice": "สังเกต", "all": "ทั้งหมด", + "all_coins": "เหรียญทั้งหมด", "all_trades": "การซื้อขายทั้งหมด", "all_transactions": "การทำธุรกรรมทั้งหมด", "alphabetical": "ตามตัวอักษร", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 5f05e3d51..45080b80d 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -38,6 +38,7 @@ "agree_to": "Sa pamamagitan ng paggawa ng account sumasang-ayon ka sa ", "alert_notice": "PAUNAWA", "all": "LAHAT", + "all_coins": "Lahat ng mga barya", "all_trades": "Lahat ng mga trade", "all_transactions": "Lahat ng mga transaksyon", "alphabetical": "Alpabeto", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 80990ae2d..6e990ab09 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -38,6 +38,7 @@ "agree_to": "Hesap oluşturarak bunları kabul etmiş olursunuz ", "alert_notice": "Fark etme", "all": "HEPSİ", + "all_coins": "Tüm Paralar", "all_trades": "Tüm takaslar", "all_transactions": "Tüm transferler", "alphabetical": "Alfabetik", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index b2d7a22e5..c66b7e4d7 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -38,6 +38,7 @@ "agree_to": "Створюючи обліковий запис, ви погоджуєтеся з ", "alert_notice": "Ув'язнення", "all": "ВСЕ", + "all_coins": "Всі монети", "all_trades": "Всі операції", "all_transactions": "Всі транзакції", "alphabetical": "Алфавітний", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 289bfb958..3da895eae 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -38,6 +38,7 @@ "agree_to": "اکاؤنٹ بنا کر آپ اس سے اتفاق کرتے ہیں۔", "alert_notice": "نوٹس", "all": "تمام", + "all_coins": "تمام سکے", "all_trades": "تمام تجارت", "all_transactions": "تمام لین دین", "alphabetical": "حروف تہجی کے مطابق", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index e7c95b8ec..9003be56c 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -38,6 +38,7 @@ "agree_to": "Bằng cách tạo tài khoản, bạn đồng ý với ", "alert_notice": "Để ý", "all": "TẤT CẢ", + "all_coins": "Tất cả các đồng tiền", "all_trades": "Tất cả giao dịch", "all_transactions": "Tất cả giao dịch", "alphabetical": "Theo thứ tự chữ cái", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index c9b8c4cbd..fdb574432 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -38,6 +38,7 @@ "agree_to": "Tẹ́ ẹ bá dá àkáǹtì ẹ jọ rò ", "alert_notice": "Akiyesi", "all": "Gbogbo", + "all_coins": "Gbogbo awọn owó", "all_trades": "Gbogbo àwọn pàṣípààrọ̀", "all_transactions": "Gbogbo àwọn àránṣẹ́", "alphabetical": "Labidibi", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 37b35ea6d..b75b4be68 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -38,6 +38,7 @@ "agree_to": "创建账户即表示您同意 ", "alert_notice": "注意", "all": "全部", + "all_coins": "所有硬币", "all_trades": "所有的变化", "all_transactions": "所有交易", "alphabetical": "按字母顺序",