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>
This commit is contained in:
Serhii 2024-11-28 17:53:03 +02:00 committed by GitHub
parent 4ca50b5e63
commit 9cd69c4ba3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 402 additions and 125 deletions

View file

@ -106,6 +106,15 @@ class BitcoinWalletService extends WalletService<
final walletInfo = walletInfoSource.values final walletInfo = walletInfoSource.values
.firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!; .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!;
await walletInfoSource.delete(walletInfo.key); 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 @override

View file

@ -304,6 +304,7 @@ abstract class ElectrumWalletBase
Future<void> init() async { Future<void> init() async {
await walletAddresses.init(); await walletAddresses.init();
await transactionHistory.init(); await transactionHistory.init();
await cleanUpDuplicateUnspentCoins();
await save(); await save();
_autoSaveTimer = _autoSaveTimer =
@ -1380,9 +1381,10 @@ abstract class ElectrumWalletBase
unspentCoins = updatedUnspentCoins; unspentCoins = updatedUnspentCoins;
if (unspentCoinsInfo.length != updatedUnspentCoins.length) { final currentWalletUnspentCoins = unspentCoinsInfo.values.where((element) => element.walletId == id);
if (currentWalletUnspentCoins.length != updatedUnspentCoins.length) {
unspentCoins.forEach((coin) => addCoinInfo(coin)); unspentCoins.forEach((coin) => addCoinInfo(coin));
return;
} }
await updateCoins(unspentCoins); await updateCoins(unspentCoins);
@ -1408,6 +1410,7 @@ abstract class ElectrumWalletBase
coin.isFrozen = coinInfo.isFrozen; coin.isFrozen = coinInfo.isFrozen;
coin.isSending = coinInfo.isSending; coin.isSending = coinInfo.isSending;
coin.note = coinInfo.note; coin.note = coinInfo.note;
if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
coin.bitcoinAddressRecord.balance += coinInfo.value; coin.bitcoinAddressRecord.balance += coinInfo.value;
} else { } else {
@ -1445,20 +1448,27 @@ abstract class ElectrumWalletBase
@action @action
Future<void> addCoinInfo(BitcoinUnspent coin) async { Future<void> 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<void> _refreshUnspentCoinsInfo() async { Future<void> _refreshUnspentCoinsInfo() async {
@ -1486,6 +1496,23 @@ abstract class ElectrumWalletBase
} }
} }
Future<void> cleanUpDuplicateUnspentCoins() async {
final currentWalletUnspentCoins = unspentCoinsInfo.values.where((element) => element.walletId == id);
final Map<String, UnspentCoinsInfo> uniqueUnspentCoins = {};
final List<dynamic> 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(); int transactionVSize(String transactionHex) => BtcTransaction.fromRaw(transactionHex).getVSize();
Future<String?> canReplaceByFee(ElectrumTransactionInfo tx) async { Future<String?> canReplaceByFee(ElectrumTransactionInfo tx) async {

View file

@ -126,6 +126,15 @@ class LitecoinWalletService extends WalletService<
mwebdLogs.deleteSync(); 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 @override

View file

@ -85,6 +85,15 @@ class BitcoinCashWalletService extends WalletService<
final walletInfo = walletInfoSource.values final walletInfo = walletInfoSource.values
.firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!; .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!;
await walletInfoSource.delete(walletInfo.key); 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 @override

View file

@ -1,10 +1,11 @@
import 'package:cw_core/hive_type_ids.dart'; import 'package:cw_core/hive_type_ids.dart';
import 'package:cw_core/unspent_comparable_mixin.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
part 'unspent_coins_info.g.dart'; part 'unspent_coins_info.g.dart';
@HiveType(typeId: UnspentCoinsInfo.typeId) @HiveType(typeId: UnspentCoinsInfo.typeId)
class UnspentCoinsInfo extends HiveObject { class UnspentCoinsInfo extends HiveObject with UnspentComparable {
UnspentCoinsInfo({ UnspentCoinsInfo({
required this.walletId, required this.walletId,
required this.hash, required this.hash,

View file

@ -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);
}
}

View file

@ -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) Unspent(this.address, this.hash, this.value, this.vout, this.keyImage)
: isSending = true, : isSending = true,
isFrozen = false, isFrozen = false,

View file

@ -1,13 +1,14 @@
import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.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/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: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/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
class UnspentCoinsListPage extends BasePage { class UnspentCoinsListPage extends BasePage {
UnspentCoinsListPage({required this.unspentCoinsListViewModel}); UnspentCoinsListPage({required this.unspentCoinsListViewModel});
@ -15,16 +16,53 @@ class UnspentCoinsListPage extends BasePage {
@override @override
String get title => S.current.unspent_coins_title; 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; final UnspentCoinsListViewModel unspentCoinsListViewModel;
Future<void> 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 @override
Widget body(BuildContext context) => UnspentCoinsListForm(unspentCoinsListViewModel); Widget body(BuildContext context) =>
UnspentCoinsListForm(unspentCoinsListViewModel, handleOnPopInvoked);
} }
class UnspentCoinsListForm extends StatefulWidget { class UnspentCoinsListForm extends StatefulWidget {
UnspentCoinsListForm(this.unspentCoinsListViewModel); UnspentCoinsListForm(this.unspentCoinsListViewModel, this.handleOnPopInvoked);
final UnspentCoinsListViewModel unspentCoinsListViewModel; final UnspentCoinsListViewModel unspentCoinsListViewModel;
final Future<void> Function(BuildContext context) handleOnPopInvoked;
@override @override
UnspentCoinsListFormState createState() => UnspentCoinsListFormState(unspentCoinsListViewModel); UnspentCoinsListFormState createState() => UnspentCoinsListFormState(unspentCoinsListViewModel);
@ -35,36 +73,126 @@ class UnspentCoinsListFormState extends State<UnspentCoinsListForm> {
final UnspentCoinsListViewModel unspentCoinsListViewModel; final UnspentCoinsListViewModel unspentCoinsListViewModel;
late Future<void> _initialization;
ReactionDisposer? _disposer;
@override
void initState() {
super.initState();
_initialization = unspentCoinsListViewModel.initialSetup();
_setupReactions();
}
void _setupReactions() {
_disposer = reaction<bool>(
(_) => unspentCoinsListViewModel.isDisposing,
(isDisposing) {
if (isDisposing) {
_showSavingDataAlert();
}
},
);
}
void _showSavingDataAlert() {
showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertWithNoAction(
alertContent: 'Updating, please wait…',
alertBarrierDismissible: false,
);
},
);
}
@override
void dispose() {
_disposer?.call();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return PopScope(
padding: EdgeInsets.fromLTRB(24, 12, 24, 24), canPop: false,
child: Observer( onPopInvokedWithResult: (bool didPop, Object? result) async {
builder: (_) => ListView.separated( if (didPop) return;
itemCount: unspentCoinsListViewModel.items.length, if(mounted)
separatorBuilder: (_, __) => SizedBox(height: 15), await widget.handleOnPopInvoked(context);
itemBuilder: (_, int index) { },
return Observer(builder: (_) { child: FutureBuilder<void>(
final item = unspentCoinsListViewModel.items[index]; future: _initialization,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
return GestureDetector( if (snapshot.hasError) return Center(child: Text('Failed to load unspent coins'));
onTap: () => Navigator.of(context).pushNamed(Routes.unspentCoinsDetails,
arguments: [item, unspentCoinsListViewModel]), return Container(
child: UnspentCoinsListItem( padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
note: item.note, child: Observer(
amount: item.amount, builder: (_) => Column(
address: item.address, children: [
isSending: item.isSending, if (unspentCoinsListViewModel.items.isNotEmpty)
isFrozen: item.isFrozen, Row(
isChange: item.isChange, children: [
isSilentPayment: item.isSilentPayment, SizedBox(width: 12),
onCheckBoxTap: item.isFrozen StandardCheckbox(
? null iconColor: Theme.of(context).extension<CakeTextTheme>()!.buttonTextColor,
: () async { value: unspentCoinsListViewModel.isAllSelected,
item.isSending = !item.isSending; onChanged: (value) => unspentCoinsListViewModel.toggleSelectAll(value),
await unspentCoinsListViewModel.saveUnspentCoinInfo(item); ),
})); 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);
},
),
),
);
},
),
),
],
),
),
);
},
),
);
} }
} }

View file

@ -3,18 +3,18 @@ import 'package:cake_wallet/src/widgets/base_alert_dialog.dart';
class AlertWithNoAction extends BaseAlertDialog { class AlertWithNoAction extends BaseAlertDialog {
AlertWithNoAction({ AlertWithNoAction({
required this.alertTitle, this.alertTitle,
required this.alertContent, required this.alertContent,
this.alertBarrierDismissible = true, this.alertBarrierDismissible = true,
Key? key, Key? key,
}); });
final String alertTitle; final String? alertTitle;
final String alertContent; final String alertContent;
final bool alertBarrierDismissible; final bool alertBarrierDismissible;
@override @override
String get titleText => alertTitle; String? get titleText => alertTitle;
@override @override
String get contentText => alertContent; String get contentText => alertContent;
@ -26,5 +26,5 @@ class AlertWithNoAction extends BaseAlertDialog {
bool get isBottomDividerExists => false; bool get isBottomDividerExists => false;
@override @override
Widget actionButtons(BuildContext context) => Container(height: 60); Widget actionButtons(BuildContext context) => Container();
} }

View file

@ -7,7 +7,7 @@ import 'package:flutter/material.dart';
class BaseAlertDialog extends StatelessWidget { class BaseAlertDialog extends StatelessWidget {
String? get headerText => ''; String? get headerText => '';
String get titleText => ''; String? get titleText => '';
String get contentText => ''; String get contentText => '';
@ -43,7 +43,7 @@ class BaseAlertDialog extends StatelessWidget {
Widget title(BuildContext context) { Widget title(BuildContext context) {
return Text( return Text(
titleText, titleText!,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 20, fontSize: 20,
@ -191,10 +191,11 @@ class BaseAlertDialog extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[ children: <Widget>[
if (headerText?.isNotEmpty ?? false) headerTitle(context), if (headerText?.isNotEmpty ?? false) headerTitle(context),
titleText != null ?
Padding( Padding(
padding: EdgeInsets.fromLTRB(24, 20, 24, 0), padding: EdgeInsets.fromLTRB(24, 20, 24, 0),
child: title(context), child: title(context),
), ) : SizedBox(height: 16),
isDividerExists isDividerExists
? Padding( ? Padding(
padding: EdgeInsets.only(top: 16, bottom: 8), padding: EdgeInsets.only(top: 16, bottom: 8),

View file

@ -1,10 +1,11 @@
import 'package:cw_core/unspent_comparable_mixin.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
part 'unspent_coins_item.g.dart'; part 'unspent_coins_item.g.dart';
class UnspentCoinsItem = UnspentCoinsItemBase with _$UnspentCoinsItem; class UnspentCoinsItem = UnspentCoinsItemBase with _$UnspentCoinsItem;
abstract class UnspentCoinsItemBase with Store { abstract class UnspentCoinsItemBase with Store, UnspentComparable {
UnspentCoinsItemBase({ UnspentCoinsItemBase({
required this.address, required this.address,
required this.amount, required this.amount,
@ -13,7 +14,7 @@ abstract class UnspentCoinsItemBase with Store {
required this.note, required this.note,
required this.isSending, required this.isSending,
required this.isChange, required this.isChange,
required this.amountRaw, required this.value,
required this.vout, required this.vout,
required this.keyImage, required this.keyImage,
required this.isSilentPayment, required this.isSilentPayment,
@ -41,7 +42,7 @@ abstract class UnspentCoinsItemBase with Store {
bool isChange; bool isChange;
@observable @observable
int amountRaw; int value;
@observable @observable
int vout; int vout;

View file

@ -10,6 +10,7 @@ import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:collection/collection.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
part 'unspent_coins_list_view_model.g.dart'; part 'unspent_coins_list_view_model.g.dart';
@ -22,55 +23,66 @@ abstract class UnspentCoinsListViewModelBase with Store {
required Box<UnspentCoinsInfo> unspentCoinsInfo, required Box<UnspentCoinsInfo> unspentCoinsInfo,
this.coinTypeToSpendFrom = UnspentCoinType.any, this.coinTypeToSpendFrom = UnspentCoinType.any,
}) : _unspentCoinsInfo = unspentCoinsInfo, }) : _unspentCoinsInfo = unspentCoinsInfo,
_items = ObservableList<UnspentCoinsItem>() { items = ObservableList<UnspentCoinsItem>(),
_updateUnspentCoinsInfo(); _originalState = {};
_updateUnspents();
}
WalletBase wallet; final WalletBase wallet;
final Box<UnspentCoinsInfo> _unspentCoinsInfo; final Box<UnspentCoinsInfo> _unspentCoinsInfo;
final UnspentCoinType coinTypeToSpendFrom; final UnspentCoinType coinTypeToSpendFrom;
@observable @observable
ObservableList<UnspentCoinsItem> _items; ObservableList<UnspentCoinsItem> items;
final Map<String, Map<String, dynamic>> _originalState;
@observable
bool isDisposing = false;
@computed @computed
ObservableList<UnspentCoinsItem> get items => _items; bool get isAllSelected => items.every((element) => element.isFrozen || element.isSending);
Future<void> saveUnspentCoinInfo(UnspentCoinsItem item) async { Future<void> initialSetup() async {
try { await _updateUnspents();
final info = _storeOriginalState();
getUnspentCoinInfo(item.hash, item.address, item.amountRaw, item.vout, item.keyImage); }
if (info == null) { void _storeOriginalState() {
return; _originalState.clear();
} for (final item in items) {
_originalState[item.hash] = {
info.isFrozen = item.isFrozen; 'isFrozen': item.isFrozen,
info.isSending = item.isSending; 'note': item.note,
info.note = item.note; 'isSending': item.isSending,
};
await info.save();
await _updateUnspents();
await wallet.updateBalance();
} catch (e) {
print(e.toString());
} }
} }
UnspentCoinsInfo? getUnspentCoinInfo( bool _hasAdjustableFieldChanged(UnspentCoinsItem item) {
String hash, String address, int value, int vout, String? keyImage) { 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<void> saveUnspentCoinInfo(UnspentCoinsItem item) async {
try { try {
return _unspentCoinsInfo.values.firstWhere((element) => final existingInfo = _unspentCoinsInfo.values
element.walletId == wallet.id && .firstWhereOrNull((element) => element.walletId == wallet.id && element == item);
element.hash == hash && if (existingInfo == null) return;
element.address == address &&
element.value == value && existingInfo.isFrozen = item.isFrozen;
element.vout == vout && existingInfo.isSending = item.isSending;
element.keyImage == keyImage); existingInfo.note = item.note;
await existingInfo.save();
_updateUnspentCoinsInfo();
} catch (e) { } catch (e) {
print("UnspentCoinsInfo not found for coin: $e"); print('Error saving coin info: $e');
return null;
} }
} }
@ -115,37 +127,60 @@ abstract class UnspentCoinsListViewModelBase with Store {
@action @action
void _updateUnspentCoinsInfo() { void _updateUnspentCoinsInfo() {
_items.clear(); items.clear();
List<UnspentCoinsItem> unspents = []; final unspents = _getUnspents()
_getUnspents().forEach((Unspent elem) { .map((elem) {
try { try {
final info = final existingItem = _unspentCoinsInfo.values
getUnspentCoinInfo(elem.hash, elem.address, elem.value, elem.vout, elem.keyImage); .firstWhereOrNull((item) => item.walletId == wallet.id && item == elem);
if (info == null) {
return;
}
unspents.add(UnspentCoinsItem( if (existingItem == null) return null;
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));
}
});
_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<UnspentCoinsItem>()
.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<void> dispose() async {
await _updateUnspents();
await wallet.updateBalance();
} }
} }

View file

@ -38,6 +38,7 @@
"agree_to": "من خلال إنشاء حساب فإنك توافق على", "agree_to": "من خلال إنشاء حساب فإنك توافق على",
"alert_notice": "يلاحظ", "alert_notice": "يلاحظ",
"all": "الكل", "all": "الكل",
"all_coins": "كل العملات المعدنية",
"all_trades": "جميع عمليات التداول", "all_trades": "جميع عمليات التداول",
"all_transactions": "كل التحركات المالية", "all_transactions": "كل التحركات المالية",
"alphabetical": "مرتب حسب الحروف الأبجدية", "alphabetical": "مرتب حسب الحروف الأبجدية",

View file

@ -38,6 +38,7 @@
"agree_to": "Чрез създаването на акаунт вие се съгласявате с ", "agree_to": "Чрез създаването на акаунт вие се съгласявате с ",
"alert_notice": "Забележете", "alert_notice": "Забележете",
"all": "ALL", "all": "ALL",
"all_coins": "Всички монети",
"all_trades": "Всички сделкки", "all_trades": "Всички сделкки",
"all_transactions": "Всички транзакции", "all_transactions": "Всички транзакции",
"alphabetical": "Азбучен ред", "alphabetical": "Азбучен ред",

View file

@ -38,6 +38,7 @@
"agree_to": "Vytvořením účtu souhlasíte s ", "agree_to": "Vytvořením účtu souhlasíte s ",
"alert_notice": "Oznámení", "alert_notice": "Oznámení",
"all": "VŠE", "all": "VŠE",
"all_coins": "Všechny mince",
"all_trades": "Všechny obchody", "all_trades": "Všechny obchody",
"all_transactions": "Všechny transakce", "all_transactions": "Všechny transakce",
"alphabetical": "Abecední", "alphabetical": "Abecední",

View file

@ -38,6 +38,7 @@
"agree_to": "Indem Sie ein Konto erstellen, stimmen Sie den ", "agree_to": "Indem Sie ein Konto erstellen, stimmen Sie den ",
"alert_notice": "Beachten", "alert_notice": "Beachten",
"all": "ALLES", "all": "ALLES",
"all_coins": "Alle Münzen",
"all_trades": "Alle Trades", "all_trades": "Alle Trades",
"all_transactions": "Alle Transaktionen", "all_transactions": "Alle Transaktionen",
"alphabetical": "Alphabetisch", "alphabetical": "Alphabetisch",

View file

@ -38,6 +38,7 @@
"agree_to": "By creating account you agree to the ", "agree_to": "By creating account you agree to the ",
"alert_notice": "Notice", "alert_notice": "Notice",
"all": "ALL", "all": "ALL",
"all_coins": "All Coins",
"all_trades": "All trades", "all_trades": "All trades",
"all_transactions": "All transactions", "all_transactions": "All transactions",
"alphabetical": "Alphabetical", "alphabetical": "Alphabetical",

View file

@ -38,6 +38,7 @@
"agree_to": "Al crear una cuenta, aceptas ", "agree_to": "Al crear una cuenta, aceptas ",
"alert_notice": "Aviso", "alert_notice": "Aviso",
"all": "Todos", "all": "Todos",
"all_coins": "Todas las monedas",
"all_trades": "Todos los oficios", "all_trades": "Todos los oficios",
"all_transactions": "Todas las transacciones", "all_transactions": "Todas las transacciones",
"alphabetical": "Alfabético", "alphabetical": "Alfabético",

View file

@ -38,6 +38,7 @@
"agree_to": "En créant un compte, vous acceptez les ", "agree_to": "En créant un compte, vous acceptez les ",
"alert_notice": "Avis", "alert_notice": "Avis",
"all": "TOUT", "all": "TOUT",
"all_coins": "Toutes les pièces",
"all_trades": "Tous échanges", "all_trades": "Tous échanges",
"all_transactions": "Toutes transactions", "all_transactions": "Toutes transactions",
"alphabetical": "Alphabétique", "alphabetical": "Alphabétique",

View file

@ -38,6 +38,7 @@
"agree_to": "Ta hanyar ƙirƙirar asusu kun yarda da", "agree_to": "Ta hanyar ƙirƙirar asusu kun yarda da",
"alert_notice": "Sanarwa", "alert_notice": "Sanarwa",
"all": "DUK", "all": "DUK",
"all_coins": "Duk tsabar kudi",
"all_trades": "Duk ciniki", "all_trades": "Duk ciniki",
"all_transactions": "Dukan Ma'amaloli", "all_transactions": "Dukan Ma'amaloli",
"alphabetical": "Harafi", "alphabetical": "Harafi",

View file

@ -38,6 +38,7 @@
"agree_to": "खाता बनाकर आप इससे सहमत होते हैं ", "agree_to": "खाता बनाकर आप इससे सहमत होते हैं ",
"alert_notice": "सूचना", "alert_notice": "सूचना",
"all": "सब", "all": "सब",
"all_coins": "सभी सिक्के",
"all_trades": "सभी व्यापार", "all_trades": "सभी व्यापार",
"all_transactions": "सभी लेन - देन", "all_transactions": "सभी लेन - देन",
"alphabetical": "वर्णमाला", "alphabetical": "वर्णमाला",

View file

@ -38,6 +38,7 @@
"agree_to": "Stvaranjem računa pristajete na ", "agree_to": "Stvaranjem računa pristajete na ",
"alert_notice": "Obavijest", "alert_notice": "Obavijest",
"all": "SVE", "all": "SVE",
"all_coins": "Sve kovanice",
"all_trades": "Svi obrti", "all_trades": "Svi obrti",
"all_transactions": "Sve transakcije", "all_transactions": "Sve transakcije",
"alphabetical": "Abecedno", "alphabetical": "Abecedno",

View file

@ -38,6 +38,7 @@
"agree_to": "Ստեղծելով հաշիվ դուք համաձայնում եք ", "agree_to": "Ստեղծելով հաշիվ դուք համաձայնում եք ",
"alert_notice": "Ծանուցում", "alert_notice": "Ծանուցում",
"all": "Բոլորը", "all": "Բոլորը",
"all_coins": "Բոլոր մետաղադրամները",
"all_trades": "Բոլոր գործարքները", "all_trades": "Բոլոր գործարքները",
"all_transactions": "Բոլոր գործառնությունները", "all_transactions": "Բոլոր գործառնությունները",
"alphabetical": "Այբբենական", "alphabetical": "Այբբենական",

View file

@ -38,6 +38,7 @@
"agree_to": "Dengan membuat akun Anda setuju dengan ", "agree_to": "Dengan membuat akun Anda setuju dengan ",
"alert_notice": "Melihat", "alert_notice": "Melihat",
"all": "SEMUA", "all": "SEMUA",
"all_coins": "Semua koin",
"all_trades": "Semua perdagangan", "all_trades": "Semua perdagangan",
"all_transactions": "Semua transaksi", "all_transactions": "Semua transaksi",
"alphabetical": "Alfabetis", "alphabetical": "Alfabetis",

View file

@ -38,6 +38,7 @@
"agree_to": "Creando un account accetti il ", "agree_to": "Creando un account accetti il ",
"alert_notice": "Avviso", "alert_notice": "Avviso",
"all": "TUTTO", "all": "TUTTO",
"all_coins": "Tutte le monete",
"all_trades": "Svi obrti", "all_trades": "Svi obrti",
"all_transactions": "Sve transakcije", "all_transactions": "Sve transakcije",
"alphabetical": "Alfabetico", "alphabetical": "Alfabetico",

View file

@ -38,6 +38,7 @@
"agree_to": "アカウントを作成することにより、", "agree_to": "アカウントを作成することにより、",
"alert_notice": "知らせ", "alert_notice": "知らせ",
"all": "すべて", "all": "すべて",
"all_coins": "すべてのコイン",
"all_trades": "すべての取引", "all_trades": "すべての取引",
"all_transactions": "全取引", "all_transactions": "全取引",
"alphabetical": "アルファベット順", "alphabetical": "アルファベット順",

View file

@ -38,6 +38,7 @@
"agree_to": "계정을 생성하면 ", "agree_to": "계정을 생성하면 ",
"alert_notice": "알아채다", "alert_notice": "알아채다",
"all": "모든", "all": "모든",
"all_coins": "모든 동전",
"all_trades": "A모든 거래", "all_trades": "A모든 거래",
"all_transactions": "모든 거래 창구", "all_transactions": "모든 거래 창구",
"alphabetical": "알파벳순", "alphabetical": "알파벳순",
@ -495,8 +496,8 @@
"placeholder_transactions": "거래가 여기에 표시됩니다", "placeholder_transactions": "거래가 여기에 표시됩니다",
"please_fill_totp": "다른 기기에 있는 8자리 코드를 입력하세요.", "please_fill_totp": "다른 기기에 있는 8자리 코드를 입력하세요.",
"please_make_selection": "아래에서 선택하십시오 지갑 만들기 또는 복구.", "please_make_selection": "아래에서 선택하십시오 지갑 만들기 또는 복구.",
"Please_reference_document": "자세한 내용은 아래 문서를 참조하십시오.",
"please_reference_document": "자세한 내용은 아래 문서를 참조하십시오.", "please_reference_document": "자세한 내용은 아래 문서를 참조하십시오.",
"Please_reference_document": "자세한 내용은 아래 문서를 참조하십시오.",
"please_select": "선택 해주세요:", "please_select": "선택 해주세요:",
"please_select_backup_file": "백업 파일을 선택하고 백업 암호를 입력하십시오.", "please_select_backup_file": "백업 파일을 선택하고 백업 암호를 입력하십시오.",
"please_try_to_connect_to_another_node": "다른 노드에 연결을 시도하십시오", "please_try_to_connect_to_another_node": "다른 노드에 연결을 시도하십시오",

View file

@ -38,6 +38,7 @@
"agree_to": "အကောင့်ဖန်တီးခြင်းဖြင့် သင်သည် ဤအရာကို သဘောတူပါသည်။", "agree_to": "အကောင့်ဖန်တီးခြင်းဖြင့် သင်သည် ဤအရာကို သဘောတူပါသည်။",
"alert_notice": "မှတ်သား", "alert_notice": "မှတ်သား",
"all": "အားလုံး", "all": "အားလုံး",
"all_coins": "အားလုံးဒင်္ဂါးများ",
"all_trades": "ကုန်သွယ်မှုအားလုံး", "all_trades": "ကုန်သွယ်မှုအားလုံး",
"all_transactions": "အရောင်းအဝယ်အားလုံး", "all_transactions": "အရောင်းအဝယ်အားလုံး",
"alphabetical": "အက္ခရာစဉ်", "alphabetical": "အက္ခရာစဉ်",

View file

@ -38,6 +38,7 @@
"agree_to": "Door een account aan te maken gaat u akkoord met de ", "agree_to": "Door een account aan te maken gaat u akkoord met de ",
"alert_notice": "Kennisgeving", "alert_notice": "Kennisgeving",
"all": "ALLE", "all": "ALLE",
"all_coins": "Alle munten",
"all_trades": "Alle transacties", "all_trades": "Alle transacties",
"all_transactions": "Alle transacties", "all_transactions": "Alle transacties",
"alphabetical": "Alfabetisch", "alphabetical": "Alfabetisch",

View file

@ -38,6 +38,7 @@
"agree_to": "Tworząc konto wyrażasz zgodę na ", "agree_to": "Tworząc konto wyrażasz zgodę na ",
"alert_notice": "Ogłoszenie", "alert_notice": "Ogłoszenie",
"all": "WSZYSTKO", "all": "WSZYSTKO",
"all_coins": "Wszystkie monety",
"all_trades": "Wszystkie operacje", "all_trades": "Wszystkie operacje",
"all_transactions": "Wszystkie transakcje", "all_transactions": "Wszystkie transakcje",
"alphabetical": "Alfabetyczny", "alphabetical": "Alfabetyczny",

View file

@ -38,6 +38,7 @@
"agree_to": "Ao criar conta você concorda com ", "agree_to": "Ao criar conta você concorda com ",
"alert_notice": "Perceber", "alert_notice": "Perceber",
"all": "TUDO", "all": "TUDO",
"all_coins": "Todas as moedas",
"all_trades": "Todas as negociações", "all_trades": "Todas as negociações",
"all_transactions": "Todas as transacções", "all_transactions": "Todas as transacções",
"alphabetical": "alfabética", "alphabetical": "alfabética",

View file

@ -38,6 +38,7 @@
"agree_to": "Создавая аккаунт, вы соглашаетесь с ", "agree_to": "Создавая аккаунт, вы соглашаетесь с ",
"alert_notice": "Уведомление", "alert_notice": "Уведомление",
"all": "ВСЕ", "all": "ВСЕ",
"all_coins": "Все монеты",
"all_trades": "Все сделки", "all_trades": "Все сделки",
"all_transactions": "Все транзакции", "all_transactions": "Все транзакции",
"alphabetical": "Алфавитный", "alphabetical": "Алфавитный",

View file

@ -38,6 +38,7 @@
"agree_to": "การสร้างบัญชีของคุณยอมรับเงื่อนไขของ", "agree_to": "การสร้างบัญชีของคุณยอมรับเงื่อนไขของ",
"alert_notice": "สังเกต", "alert_notice": "สังเกต",
"all": "ทั้งหมด", "all": "ทั้งหมด",
"all_coins": "เหรียญทั้งหมด",
"all_trades": "การซื้อขายทั้งหมด", "all_trades": "การซื้อขายทั้งหมด",
"all_transactions": "การทำธุรกรรมทั้งหมด", "all_transactions": "การทำธุรกรรมทั้งหมด",
"alphabetical": "ตามตัวอักษร", "alphabetical": "ตามตัวอักษร",

View file

@ -38,6 +38,7 @@
"agree_to": "Sa pamamagitan ng paggawa ng account sumasang-ayon ka sa ", "agree_to": "Sa pamamagitan ng paggawa ng account sumasang-ayon ka sa ",
"alert_notice": "PAUNAWA", "alert_notice": "PAUNAWA",
"all": "LAHAT", "all": "LAHAT",
"all_coins": "Lahat ng mga barya",
"all_trades": "Lahat ng mga trade", "all_trades": "Lahat ng mga trade",
"all_transactions": "Lahat ng mga transaksyon", "all_transactions": "Lahat ng mga transaksyon",
"alphabetical": "Alpabeto", "alphabetical": "Alpabeto",

View file

@ -38,6 +38,7 @@
"agree_to": "Hesap oluşturarak bunları kabul etmiş olursunuz ", "agree_to": "Hesap oluşturarak bunları kabul etmiş olursunuz ",
"alert_notice": "Fark etme", "alert_notice": "Fark etme",
"all": "HEPSİ", "all": "HEPSİ",
"all_coins": "Tüm Paralar",
"all_trades": "Tüm takaslar", "all_trades": "Tüm takaslar",
"all_transactions": "Tüm transferler", "all_transactions": "Tüm transferler",
"alphabetical": "Alfabetik", "alphabetical": "Alfabetik",

View file

@ -38,6 +38,7 @@
"agree_to": "Створюючи обліковий запис, ви погоджуєтеся з ", "agree_to": "Створюючи обліковий запис, ви погоджуєтеся з ",
"alert_notice": "Ув'язнення", "alert_notice": "Ув'язнення",
"all": "ВСЕ", "all": "ВСЕ",
"all_coins": "Всі монети",
"all_trades": "Всі операції", "all_trades": "Всі операції",
"all_transactions": "Всі транзакції", "all_transactions": "Всі транзакції",
"alphabetical": "Алфавітний", "alphabetical": "Алфавітний",

View file

@ -38,6 +38,7 @@
"agree_to": "اکاؤنٹ بنا کر آپ اس سے اتفاق کرتے ہیں۔", "agree_to": "اکاؤنٹ بنا کر آپ اس سے اتفاق کرتے ہیں۔",
"alert_notice": "نوٹس", "alert_notice": "نوٹس",
"all": "تمام", "all": "تمام",
"all_coins": "تمام سکے",
"all_trades": "تمام تجارت", "all_trades": "تمام تجارت",
"all_transactions": "تمام لین دین", "all_transactions": "تمام لین دین",
"alphabetical": "حروف تہجی کے مطابق", "alphabetical": "حروف تہجی کے مطابق",

View file

@ -38,6 +38,7 @@
"agree_to": "Bằng cách tạo tài khoản, bạn đồng ý với ", "agree_to": "Bằng cách tạo tài khoản, bạn đồng ý với ",
"alert_notice": "Để ý", "alert_notice": "Để ý",
"all": "TẤT CẢ", "all": "TẤT CẢ",
"all_coins": "Tất cả các đồng tiền",
"all_trades": "Tất cả giao dịch", "all_trades": "Tất cả giao dịch",
"all_transactions": "Tất cả giao dịch", "all_transactions": "Tất cả giao dịch",
"alphabetical": "Theo thứ tự chữ cái", "alphabetical": "Theo thứ tự chữ cái",

View file

@ -38,6 +38,7 @@
"agree_to": "Tẹ́ ẹ bá dá àkáǹtì ẹ jọ rò ", "agree_to": "Tẹ́ ẹ bá dá àkáǹtì ẹ jọ rò ",
"alert_notice": "Akiyesi", "alert_notice": "Akiyesi",
"all": "Gbogbo", "all": "Gbogbo",
"all_coins": "Gbogbo awọn owó",
"all_trades": "Gbogbo àwọn pàṣípààrọ̀", "all_trades": "Gbogbo àwọn pàṣípààrọ̀",
"all_transactions": "Gbogbo àwọn àránṣẹ́", "all_transactions": "Gbogbo àwọn àránṣẹ́",
"alphabetical": "Labidibi", "alphabetical": "Labidibi",

View file

@ -38,6 +38,7 @@
"agree_to": "创建账户即表示您同意 ", "agree_to": "创建账户即表示您同意 ",
"alert_notice": "注意", "alert_notice": "注意",
"all": "全部", "all": "全部",
"all_coins": "所有硬币",
"all_trades": "所有的变化", "all_trades": "所有的变化",
"all_transactions": "所有交易", "all_transactions": "所有交易",
"alphabetical": "按字母顺序", "alphabetical": "按字母顺序",