mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2024-12-21 19:19:54 +00:00
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:
parent
4ca50b5e63
commit
9cd69c4ba3
40 changed files with 402 additions and 125 deletions
|
@ -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
|
||||
|
|
|
@ -304,6 +304,7 @@ abstract class ElectrumWalletBase
|
|||
Future<void> 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<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 {
|
||||
|
@ -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();
|
||||
|
||||
Future<String?> canReplaceByFee(ElectrumTransactionInfo tx) async {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
27
cw_core/lib/unspent_comparable_mixin.dart
Normal file
27
cw_core/lib/unspent_comparable_mixin.dart
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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<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
|
||||
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<void> Function(BuildContext context) handleOnPopInvoked;
|
||||
|
||||
@override
|
||||
UnspentCoinsListFormState createState() => UnspentCoinsListFormState(unspentCoinsListViewModel);
|
||||
|
@ -35,36 +73,126 @@ class UnspentCoinsListFormState extends State<UnspentCoinsListForm> {
|
|||
|
||||
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
|
||||
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<void>(
|
||||
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<CakeTextTheme>()!.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);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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: <Widget>[
|
||||
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),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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> unspentCoinsInfo,
|
||||
this.coinTypeToSpendFrom = UnspentCoinType.any,
|
||||
}) : _unspentCoinsInfo = unspentCoinsInfo,
|
||||
_items = ObservableList<UnspentCoinsItem>() {
|
||||
_updateUnspentCoinsInfo();
|
||||
_updateUnspents();
|
||||
}
|
||||
items = ObservableList<UnspentCoinsItem>(),
|
||||
_originalState = {};
|
||||
|
||||
WalletBase wallet;
|
||||
final WalletBase wallet;
|
||||
final Box<UnspentCoinsInfo> _unspentCoinsInfo;
|
||||
final UnspentCoinType coinTypeToSpendFrom;
|
||||
|
||||
@observable
|
||||
ObservableList<UnspentCoinsItem> _items;
|
||||
ObservableList<UnspentCoinsItem> items;
|
||||
|
||||
final Map<String, Map<String, dynamic>> _originalState;
|
||||
|
||||
@observable
|
||||
bool isDisposing = false;
|
||||
|
||||
@computed
|
||||
ObservableList<UnspentCoinsItem> get items => _items;
|
||||
bool get isAllSelected => items.every((element) => element.isFrozen || element.isSending);
|
||||
|
||||
Future<void> saveUnspentCoinInfo(UnspentCoinsItem item) async {
|
||||
try {
|
||||
final info =
|
||||
getUnspentCoinInfo(item.hash, item.address, item.amountRaw, item.vout, item.keyImage);
|
||||
Future<void> 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<void> 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<UnspentCoinsItem> 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<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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
"agree_to": "من خلال إنشاء حساب فإنك توافق على",
|
||||
"alert_notice": "يلاحظ",
|
||||
"all": "الكل",
|
||||
"all_coins": "كل العملات المعدنية",
|
||||
"all_trades": "جميع عمليات التداول",
|
||||
"all_transactions": "كل التحركات المالية",
|
||||
"alphabetical": "مرتب حسب الحروف الأبجدية",
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
"agree_to": "Чрез създаването на акаунт вие се съгласявате с ",
|
||||
"alert_notice": "Забележете",
|
||||
"all": "ALL",
|
||||
"all_coins": "Всички монети",
|
||||
"all_trades": "Всички сделкки",
|
||||
"all_transactions": "Всички транзакции",
|
||||
"alphabetical": "Азбучен ред",
|
||||
|
|
|
@ -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í",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
"agree_to": "खाता बनाकर आप इससे सहमत होते हैं ",
|
||||
"alert_notice": "सूचना",
|
||||
"all": "सब",
|
||||
"all_coins": "सभी सिक्के",
|
||||
"all_trades": "सभी व्यापार",
|
||||
"all_transactions": "सभी लेन - देन",
|
||||
"alphabetical": "वर्णमाला",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
"agree_to": "Ստեղծելով հաշիվ դուք համաձայնում եք ",
|
||||
"alert_notice": "Ծանուցում",
|
||||
"all": "Բոլորը",
|
||||
"all_coins": "Բոլոր մետաղադրամները",
|
||||
"all_trades": "Բոլոր գործարքները",
|
||||
"all_transactions": "Բոլոր գործառնությունները",
|
||||
"alphabetical": "Այբբենական",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
"agree_to": "アカウントを作成することにより、",
|
||||
"alert_notice": "知らせ",
|
||||
"all": "すべて",
|
||||
"all_coins": "すべてのコイン",
|
||||
"all_trades": "すべての取引",
|
||||
"all_transactions": "全取引",
|
||||
"alphabetical": "アルファベット順",
|
||||
|
|
|
@ -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": "다른 노드에 연결을 시도하십시오",
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
"agree_to": "အကောင့်ဖန်တီးခြင်းဖြင့် သင်သည် ဤအရာကို သဘောတူပါသည်။",
|
||||
"alert_notice": "မှတ်သား",
|
||||
"all": "အားလုံး",
|
||||
"all_coins": "အားလုံးဒင်္ဂါးများ",
|
||||
"all_trades": "ကုန်သွယ်မှုအားလုံး",
|
||||
"all_transactions": "အရောင်းအဝယ်အားလုံး",
|
||||
"alphabetical": "အက္ခရာစဉ်",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
"agree_to": "Создавая аккаунт, вы соглашаетесь с ",
|
||||
"alert_notice": "Уведомление",
|
||||
"all": "ВСЕ",
|
||||
"all_coins": "Все монеты",
|
||||
"all_trades": "Все сделки",
|
||||
"all_transactions": "Все транзакции",
|
||||
"alphabetical": "Алфавитный",
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
"agree_to": "การสร้างบัญชีของคุณยอมรับเงื่อนไขของ",
|
||||
"alert_notice": "สังเกต",
|
||||
"all": "ทั้งหมด",
|
||||
"all_coins": "เหรียญทั้งหมด",
|
||||
"all_trades": "การซื้อขายทั้งหมด",
|
||||
"all_transactions": "การทำธุรกรรมทั้งหมด",
|
||||
"alphabetical": "ตามตัวอักษร",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
"agree_to": "Створюючи обліковий запис, ви погоджуєтеся з ",
|
||||
"alert_notice": "Ув'язнення",
|
||||
"all": "ВСЕ",
|
||||
"all_coins": "Всі монети",
|
||||
"all_trades": "Всі операції",
|
||||
"all_transactions": "Всі транзакції",
|
||||
"alphabetical": "Алфавітний",
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
"agree_to": "اکاؤنٹ بنا کر آپ اس سے اتفاق کرتے ہیں۔",
|
||||
"alert_notice": "نوٹس",
|
||||
"all": "تمام",
|
||||
"all_coins": "تمام سکے",
|
||||
"all_trades": "تمام تجارت",
|
||||
"all_transactions": "تمام لین دین",
|
||||
"alphabetical": "حروف تہجی کے مطابق",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
"agree_to": "创建账户即表示您同意 ",
|
||||
"alert_notice": "注意",
|
||||
"all": "全部",
|
||||
"all_coins": "所有硬币",
|
||||
"all_trades": "所有的变化",
|
||||
"all_transactions": "所有交易",
|
||||
"alphabetical": "按字母顺序",
|
||||
|
|
Loading…
Reference in a new issue