From 20e0c830cf6d6703fc00d2faeec23f4404a04321 Mon Sep 17 00:00:00 2001 From: OleksandrSobol Date: Mon, 5 Jul 2021 16:52:24 +0300 Subject: [PATCH] CAKE-334 | applied unspent coins control to the app; added unspent_coins_info.dart; reworked createTransaction(), calculateEstimatedFee() and updateUnspent() methods in the electrum_wallet.dart; fixed unspent_coins_list_view_model.dart, unspent_coins_details_view_model.dart, unspent_coins_list_item.dart, unspent_coins_list_page.dart and unspent_coins_details_page.dart; fixed bitcoin_transaction_wrong_balance_exception.dart; added properties to bitcoin_unspent.dart; applied localization to unspent coins pages --- ...n_transaction_wrong_balance_exception.dart | 8 +- lib/bitcoin/bitcoin_unspent.dart | 8 +- lib/bitcoin/bitcoin_wallet.dart | 6 + lib/bitcoin/bitcoin_wallet_service.dart | 13 +- lib/bitcoin/electrum_wallet.dart | 171 ++++++++++++++---- lib/bitcoin/litecoin_wallet.dart | 6 + lib/bitcoin/litecoin_wallet_service.dart | 13 +- lib/bitcoin/unspent_coins_info.dart | 32 ++++ lib/di.dart | 43 +++-- lib/main.dart | 12 +- lib/router.dart | 4 +- lib/src/screens/send/send_page.dart | 4 +- .../unspent_coins_details_page.dart | 3 +- .../unspent_coins_list_page.dart | 33 ++-- .../widgets/unspent_coins_list_item.dart | 138 +++++++------- lib/src/widgets/standart_switch.dart | 1 - lib/view_model/send/send_view_model.dart | 6 +- .../unspent_coins_details_view_model.dart | 22 ++- .../unspent_coins/unspent_coins_item.dart | 35 +++- .../unspent_coins_list_view_model.dart | 97 +++++----- res/values/strings_de.arb | 7 +- res/values/strings_en.arb | 7 +- res/values/strings_es.arb | 7 +- res/values/strings_hi.arb | 7 +- res/values/strings_hr.arb | 7 +- res/values/strings_it.arb | 7 +- res/values/strings_ja.arb | 7 +- res/values/strings_ko.arb | 7 +- res/values/strings_nl.arb | 7 +- res/values/strings_pl.arb | 7 +- res/values/strings_pt.arb | 7 +- res/values/strings_ru.arb | 7 +- res/values/strings_uk.arb | 7 +- res/values/strings_zh.arb | 7 +- 34 files changed, 520 insertions(+), 233 deletions(-) create mode 100644 lib/bitcoin/unspent_coins_info.dart diff --git a/lib/bitcoin/bitcoin_transaction_wrong_balance_exception.dart b/lib/bitcoin/bitcoin_transaction_wrong_balance_exception.dart index 9d140181..b699ade2 100644 --- a/lib/bitcoin/bitcoin_transaction_wrong_balance_exception.dart +++ b/lib/bitcoin/bitcoin_transaction_wrong_balance_exception.dart @@ -1,4 +1,10 @@ +import 'package:cake_wallet/entities/crypto_currency.dart'; + class BitcoinTransactionWrongBalanceException implements Exception { + BitcoinTransactionWrongBalanceException(this.currency); + + final CryptoCurrency currency; + @override - String toString() => 'Wrong balance. Not enough BTC on your balance.'; + String toString() => 'Wrong balance. Not enough ${currency.title} on your balance.'; } \ No newline at end of file diff --git a/lib/bitcoin/bitcoin_unspent.dart b/lib/bitcoin/bitcoin_unspent.dart index 846eb8c7..b95ed9bc 100644 --- a/lib/bitcoin/bitcoin_unspent.dart +++ b/lib/bitcoin/bitcoin_unspent.dart @@ -1,7 +1,10 @@ import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart'; class BitcoinUnspent { - BitcoinUnspent(this.address, this.hash, this.value, this.vout); + BitcoinUnspent(this.address, this.hash, this.value, this.vout) + : isSending = true, + isFrozen = false, + note = ''; factory BitcoinUnspent.fromJSON( BitcoinAddressRecord address, Map json) => @@ -15,4 +18,7 @@ class BitcoinUnspent { bool get isP2wpkh => address.address.startsWith('bc') || address.address.startsWith('ltc'); + bool isSending; + bool isFrozen; + String note; } diff --git a/lib/bitcoin/bitcoin_wallet.dart b/lib/bitcoin/bitcoin_wallet.dart index fd840288..022ed478 100644 --- a/lib/bitcoin/bitcoin_wallet.dart +++ b/lib/bitcoin/bitcoin_wallet.dart @@ -1,3 +1,5 @@ +import 'package:cake_wallet/bitcoin/unspent_coins_info.dart'; +import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:flutter/foundation.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; @@ -17,6 +19,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { {@required String mnemonic, @required String password, @required WalletInfo walletInfo, + @required Box unspentCoinsInfo, List initialAddresses, ElectrumBalance initialBalance, int accountIndex = 0}) @@ -24,6 +27,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { mnemonic: mnemonic, password: password, walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, networkType: bitcoin.bitcoin, initialAddresses: initialAddresses, initialBalance: initialBalance, @@ -32,6 +36,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { static Future open({ @required String name, @required WalletInfo walletInfo, + @required Box unspentCoinsInfo, @required String password, }) async { final snp = ElectrumWallletSnapshot(name, walletInfo.type, password); @@ -40,6 +45,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { mnemonic: snp.mnemonic, password: password, walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, initialAddresses: snp.addresses, initialBalance: snp.balance, accountIndex: snp.accountIndex); diff --git a/lib/bitcoin/bitcoin_wallet_service.dart b/lib/bitcoin/bitcoin_wallet_service.dart index aefe0fad..8e5b3193 100644 --- a/lib/bitcoin/bitcoin_wallet_service.dart +++ b/lib/bitcoin/bitcoin_wallet_service.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:cake_wallet/bitcoin/bitcoin_mnemonic.dart'; import 'package:cake_wallet/bitcoin/bitcoin_mnemonic_is_incorrect_exception.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet_creation_credentials.dart'; +import 'package:cake_wallet/bitcoin/unspent_coins_info.dart'; import 'package:cake_wallet/core/wallet_base.dart'; import 'package:cake_wallet/core/wallet_service.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; @@ -14,9 +15,10 @@ class BitcoinWalletService extends WalletService< BitcoinNewWalletCredentials, BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromWIFCredentials> { - BitcoinWalletService(this.walletInfoSource); + BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); final Box walletInfoSource; + final Box unspentCoinsInfoSource; @override WalletType getType() => WalletType.bitcoin; @@ -26,7 +28,8 @@ class BitcoinWalletService extends WalletService< final wallet = BitcoinWallet( mnemonic: await generateMnemonic(), password: credentials.password, - walletInfo: credentials.walletInfo); + walletInfo: credentials.walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); await wallet.save(); await wallet.init(); return wallet; @@ -42,7 +45,8 @@ class BitcoinWalletService extends WalletService< (info) => info.id == WalletBase.idFor(name, getType()), orElse: () => null); final wallet = await BitcoinWalletBase.open( - password: password, name: name, walletInfo: walletInfo); + password: password, name: name, walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); await wallet.init(); return wallet; } @@ -67,7 +71,8 @@ class BitcoinWalletService extends WalletService< final wallet = BitcoinWallet( password: credentials.password, mnemonic: credentials.mnemonic, - walletInfo: credentials.walletInfo); + walletInfo: credentials.walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); await wallet.save(); await wallet.init(); return wallet; diff --git a/lib/bitcoin/electrum_wallet.dart b/lib/bitcoin/electrum_wallet.dart index bf3dfd54..43c5cd17 100644 --- a/lib/bitcoin/electrum_wallet.dart +++ b/lib/bitcoin/electrum_wallet.dart @@ -1,5 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:cake_wallet/bitcoin/unspent_coins_info.dart'; +import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:rxdart/subjects.dart'; import 'package:flutter/foundation.dart'; @@ -38,6 +40,7 @@ abstract class ElectrumWalletBase extends WalletBase unspentCoinsInfo, @required List initialAddresses, @required this.networkType, @required this.mnemonic, @@ -59,9 +62,10 @@ abstract class ElectrumWalletBase extends WalletBase unspentCoinsInfo; @override @observable @@ -103,7 +108,7 @@ abstract class ElectrumWalletBase extends WalletBase _unspent; + List unspentCoins; List _feeRates; int _accountIndex; Map> _scripthashesUpdateSubject; @@ -178,10 +183,10 @@ abstract class ElectrumWalletBase extends WalletBase startSync() async { try { syncStatus = StartingSyncStatus(); - updateTransactions(); + await updateTransactions(); _subscribeForUpdates(); await _updateBalance(); - await _updateUnspent(); + await updateUnspent(); _feeRates = await electrumClient.feeRates(); Timer.periodic(const Duration(minutes: 1), @@ -218,33 +223,65 @@ abstract class ElectrumWalletBase extends WalletBase[]; + var allInputsAmount = 0; + + if (unspentCoins.isEmpty) { + await updateUnspent(); + } + + for (final utx in unspentCoins) { + if (utx.isSending) { + allInputsAmount += utx.value; + inputs.add(utx); + } + } + + if (inputs.isEmpty) { + throw BitcoinTransactionNoInputsException(); + } + final allAmountFee = - calculateEstimatedFee(transactionCredentials.priority, null); - final allAmount = balance.confirmed - allAmountFee; - var fee = 0; + feeAmountForPriority(transactionCredentials.priority, inputs.length, 1); + final allAmount = allInputsAmount - allAmountFee; + final credentialsAmount = transactionCredentials.amount != null ? stringDoubleToBitcoinAmount(transactionCredentials.amount) : 0; final amount = transactionCredentials.amount == null || - allAmount - credentialsAmount < minAmount + allAmount - credentialsAmount < minAmount ? allAmount : credentialsAmount; + final fee = transactionCredentials.amount == null || amount == allAmount + ? allAmountFee + : calculateEstimatedFee(transactionCredentials.priority, amount); + + if (fee == 0) { + throw BitcoinTransactionWrongBalanceException(currency); + } + + final totalAmount = amount + fee; + + if (totalAmount > balance.confirmed || totalAmount > allInputsAmount) { + throw BitcoinTransactionWrongBalanceException(currency); + } + final txb = bitcoin.TransactionBuilder(network: networkType); final changeAddress = address; - var leftAmount = amount; + + var leftAmount = totalAmount; var totalInputAmount = 0; - if (_unspent.isEmpty) { - await _updateUnspent(); - } + inputs.clear(); - for (final utx in _unspent) { - leftAmount = leftAmount - utx.value; - totalInputAmount += utx.value; - inputs.add(utx); + for (final utx in unspentCoins) { + if (utx.isSending) { + leftAmount = leftAmount - utx.value; + totalInputAmount += utx.value; + inputs.add(utx); - if (leftAmount <= 0) { - break; + if (leftAmount <= 0) { + break; + } } } @@ -252,18 +289,8 @@ abstract class ElectrumWalletBase extends WalletBase balance.confirmed) { - throw BitcoinTransactionWrongBalanceException(); - } - - if (amount <= 0 || totalInputAmount < amount) { - throw BitcoinTransactionWrongBalanceException(); + if (amount <= 0 || totalInputAmount < totalAmount) { + throw BitcoinTransactionWrongBalanceException(currency); } txb.setVersion(1); @@ -338,17 +365,26 @@ abstract class ElectrumWalletBase extends WalletBase= amount) { break; } - totalValue += input.value; - inputsCount += 1; + if (input.isSending) { + totalValue += input.value; + inputsCount += 1; + } } + + if (totalValue < amount) return 0; } else { - inputsCount = _unspent.length; + for (final input in unspentCoins) { + if (input.isSending) { + inputsCount += 1; + } + } } + // If send all, then we have no change value return feeAmountForPriority( priority, inputsCount, amount != null ? 2 : 1); @@ -382,12 +418,73 @@ abstract class ElectrumWalletBase extends WalletBase makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); - Future _updateUnspent() async { + Future updateUnspent() async { final unspent = await Future.wait(addresses.map((address) => electrumClient .getListUnspentWithAddress(address.address, networkType) .then((unspent) => unspent .map((unspent) => BitcoinUnspent.fromJSON(address, unspent))))); - _unspent = unspent.expand((e) => e).toList(); + unspentCoins = unspent.expand((e) => e).toList(); + + if (unspentCoinsInfo.isEmpty) { + unspentCoins.forEach((coin) => _addCoinInfo(coin)); + return; + } + + if (unspentCoins.isNotEmpty) { + unspentCoins.forEach((coin) { + final coinInfoList = unspentCoinsInfo.values.where((element) => + element.walletId.contains(id) && element.hash.contains(coin.hash)); + + if (coinInfoList.isNotEmpty) { + final coinInfo = coinInfoList.first; + + coin.isFrozen = coinInfo.isFrozen; + coin.isSending = coinInfo.isSending; + coin.note = coinInfo.note; + } else { + _addCoinInfo(coin); + } + }); + } + + await _refreshUnspentCoinsInfo(); + } + + Future _addCoinInfo(BitcoinUnspent coin) async { + final newInfo = UnspentCoinsInfo( + walletId: id, + hash: coin.hash, + isFrozen: coin.isFrozen, + isSending: coin.isSending, + note: coin.note + ); + + await unspentCoinsInfo.add(newInfo); + } + + Future _refreshUnspentCoinsInfo() async { + try { + final List keys = []; + final currentWalletUnspentCoins = unspentCoinsInfo.values + .where((element) => element.walletId.contains(id)); + + if (currentWalletUnspentCoins.isNotEmpty) { + currentWalletUnspentCoins.forEach((element) { + final existUnspentCoins = unspentCoins + ?.where((coin) => element.hash.contains(coin?.hash)); + + if (existUnspentCoins?.isEmpty ?? true) { + keys.add(element.key); + } + }); + } + + if (keys.isNotEmpty) { + await unspentCoinsInfo.deleteAll(keys); + } + } catch (e) { + print(e.toString()); + } } Future fetchTransactionInfo( @@ -438,7 +535,7 @@ abstract class ElectrumWalletBase extends WalletBase unspentCoinsInfo, List initialAddresses, ElectrumBalance initialBalance, int accountIndex = 0}) @@ -28,6 +31,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { mnemonic: mnemonic, password: password, walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, networkType: litecoinNetwork, initialAddresses: initialAddresses, initialBalance: initialBalance, @@ -36,6 +40,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { static Future open({ @required String name, @required WalletInfo walletInfo, + @required Box unspentCoinsInfo, @required String password, }) async { final snp = ElectrumWallletSnapshot(name, walletInfo.type, password); @@ -44,6 +49,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { mnemonic: snp.mnemonic, password: password, walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, initialAddresses: snp.addresses, initialBalance: snp.balance, accountIndex: snp.accountIndex); diff --git a/lib/bitcoin/litecoin_wallet_service.dart b/lib/bitcoin/litecoin_wallet_service.dart index 053fd785..dc5081e1 100644 --- a/lib/bitcoin/litecoin_wallet_service.dart +++ b/lib/bitcoin/litecoin_wallet_service.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:cake_wallet/bitcoin/unspent_coins_info.dart'; import 'package:hive/hive.dart'; import 'package:cake_wallet/bitcoin/bitcoin_mnemonic.dart'; import 'package:cake_wallet/bitcoin/bitcoin_mnemonic_is_incorrect_exception.dart'; @@ -14,9 +15,10 @@ class LitecoinWalletService extends WalletService< BitcoinNewWalletCredentials, BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromWIFCredentials> { - LitecoinWalletService(this.walletInfoSource); + LitecoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); final Box walletInfoSource; + final Box unspentCoinsInfoSource; @override WalletType getType() => WalletType.litecoin; @@ -26,7 +28,8 @@ class LitecoinWalletService extends WalletService< final wallet = LitecoinWallet( mnemonic: await generateMnemonic(), password: credentials.password, - walletInfo: credentials.walletInfo); + walletInfo: credentials.walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); await wallet.save(); await wallet.init(); @@ -43,7 +46,8 @@ class LitecoinWalletService extends WalletService< (info) => info.id == WalletBase.idFor(name, getType()), orElse: () => null); final wallet = await LitecoinWalletBase.open( - password: password, name: name, walletInfo: walletInfo); + password: password, name: name, walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); await wallet.init(); return wallet; } @@ -68,7 +72,8 @@ class LitecoinWalletService extends WalletService< final wallet = LitecoinWallet( password: credentials.password, mnemonic: credentials.mnemonic, - walletInfo: credentials.walletInfo); + walletInfo: credentials.walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); await wallet.save(); await wallet.init(); return wallet; diff --git a/lib/bitcoin/unspent_coins_info.dart b/lib/bitcoin/unspent_coins_info.dart new file mode 100644 index 00000000..6ce647dc --- /dev/null +++ b/lib/bitcoin/unspent_coins_info.dart @@ -0,0 +1,32 @@ +import 'package:hive/hive.dart'; + +part 'unspent_coins_info.g.dart'; + +@HiveType(typeId: UnspentCoinsInfo.typeId) +class UnspentCoinsInfo extends HiveObject { + UnspentCoinsInfo({ + this.walletId, + this.hash, + this.isFrozen, + this.isSending, + this.note}); + + static const typeId = 9; + static const boxName = 'Unspent'; + static const boxKey = 'unspentBoxKey'; + + @HiveField(0) + String walletId; + + @HiveField(1) + String hash; + + @HiveField(2) + bool isFrozen; + + @HiveField(3) + bool isSending; + + @HiveField(4) + String note; +} \ No newline at end of file diff --git a/lib/di.dart b/lib/di.dart index d887d883..82ac29b6 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/bitcoin/bitcoin_wallet_service.dart'; import 'package:cake_wallet/bitcoin/litecoin_wallet_service.dart'; +import 'package:cake_wallet/bitcoin/unspent_coins_info.dart'; import 'package:cake_wallet/core/backup_service.dart'; import 'package:cake_wallet/core/wallet_service.dart'; import 'package:cake_wallet/entities/biometric_auth.dart'; @@ -130,6 +131,7 @@ Box