From 5eefd6a31bbc0dd4f037fe6c0d52f23cf02921ba Mon Sep 17 00:00:00 2001 From: M Date: Tue, 25 Aug 2020 19:32:40 +0300 Subject: [PATCH 1/9] TMP --- .gitignore | 4 +- ios/Runner.xcodeproj/project.pbxproj | 12 +- ios/Runner/Info.plist | 2 +- lib/bitcoin/bitcoin_address_record.dart | 8 +- .../bitcoin_transaction_credentials.dart | 5 +- lib/bitcoin/bitcoin_transaction_history.dart | 184 ++++-- lib/bitcoin/bitcoin_transaction_info.dart | 102 +++- ...tcoin_transaction_no_inputs_exception.dart | 4 + ...n_transaction_wrong_balance_exception.dart | 4 + lib/bitcoin/bitcoin_unspent.dart | 17 + lib/bitcoin/bitcoin_wallet.dart | 235 ++++++-- lib/bitcoin/electrum.dart | 115 +++- lib/bitcoin/pending_bitcoin_transaction.dart | 47 ++ lib/bitcoin/script_hash.dart | 18 + lib/bitcoin/utils.dart | 26 + lib/core/amount_validator.dart | 4 +- lib/core/pending_transaction.dart | 6 + lib/core/transaction_history.dart | 17 +- lib/core/wallet_base.dart | 6 +- lib/di.dart | 47 +- lib/monero/monero_transaction_history.dart | 23 +- lib/monero/monero_wallet.dart | 32 +- lib/src/domain/common/transaction_info.dart | 1 + lib/src/screens/send/send_page.dart | 530 +++++++++--------- .../screens/send/widgets/sending_alert.dart | 2 +- lib/src/screens/settings/settings.dart | 8 +- .../widgets/settings_picker_cell.dart | 35 +- lib/store/settings_store.dart | 8 +- .../dashboard/balance_view_model.dart | 6 +- .../dashboard/dashboard_view_model.dart | 49 +- lib/view_model/send/send_view_model.dart | 171 ++++++ .../send/send_view_model_state.dart | 18 + lib/view_model/send_view_model.dart | 94 ---- lib/view_model/settings/picker_list_item.dart | 15 +- .../settings/settings_view_model.dart | 36 +- 35 files changed, 1273 insertions(+), 618 deletions(-) create mode 100644 lib/bitcoin/bitcoin_transaction_no_inputs_exception.dart create mode 100644 lib/bitcoin/bitcoin_transaction_wrong_balance_exception.dart create mode 100644 lib/bitcoin/bitcoin_unspent.dart create mode 100644 lib/bitcoin/pending_bitcoin_transaction.dart create mode 100644 lib/bitcoin/script_hash.dart create mode 100644 lib/bitcoin/utils.dart create mode 100644 lib/core/pending_transaction.dart create mode 100644 lib/view_model/send/send_view_model.dart create mode 100644 lib/view_model/send/send_view_model_state.dart delete mode 100644 lib/view_model/send_view_model.dart diff --git a/.gitignore b/.gitignore index 4102cec3e..0e6907b4f 100644 --- a/.gitignore +++ b/.gitignore @@ -89,4 +89,6 @@ android/key.properties **/tool/.secrets-prod.json **/lib/.secrets.g.dart -vendor/ \ No newline at end of file +vendor/ + +android/app/.cxx/** diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 54901a5a7..34c4e48a4 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -373,7 +373,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -387,7 +387,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 3.1.28; + MARKETING_VERSION = 3.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.cakewallet.cakewallet; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -509,7 +509,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -523,7 +523,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 3.1.28; + MARKETING_VERSION = 3.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.cakewallet.cakewallet; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -540,7 +540,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -554,7 +554,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 3.1.28; + MARKETING_VERSION = 3.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.cakewallet.cakewallet; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 40f292e53..0b40211a4 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -19,7 +19,7 @@ CFBundleSignature ???? CFBundleVersion - $(FLUTTER_BUILD_NUMBER) + $(CURRENT_PROJECT_VERSION) LSRequiresIPhoneOS UILaunchStoryboardName diff --git a/lib/bitcoin/bitcoin_address_record.dart b/lib/bitcoin/bitcoin_address_record.dart index cd2e21495..98cd1c9da 100644 --- a/lib/bitcoin/bitcoin_address_record.dart +++ b/lib/bitcoin/bitcoin_address_record.dart @@ -1,17 +1,19 @@ import 'dart:convert'; class BitcoinAddressRecord { - BitcoinAddressRecord(this.address, {this.label}); + BitcoinAddressRecord(this.address, {this.label, this.index}); factory BitcoinAddressRecord.fromJSON(String jsonSource) { final decoded = json.decode(jsonSource) as Map; return BitcoinAddressRecord(decoded['address'] as String, - label: decoded['label'] as String); + label: decoded['label'] as String, index: decoded['index'] as int); } final String address; + int index; String label; - String toJSON() => json.encode({'label': label, 'address': address}); + String toJSON() => + json.encode({'label': label, 'address': address, 'index': index}); } diff --git a/lib/bitcoin/bitcoin_transaction_credentials.dart b/lib/bitcoin/bitcoin_transaction_credentials.dart index 52f2d5ec6..874da1263 100644 --- a/lib/bitcoin/bitcoin_transaction_credentials.dart +++ b/lib/bitcoin/bitcoin_transaction_credentials.dart @@ -1,6 +1,9 @@ +import 'package:cake_wallet/src/domain/common/transaction_priority.dart'; + class BitcoinTransactionCredentials { - const BitcoinTransactionCredentials(this.address, this.amount); + BitcoinTransactionCredentials(this.address, this.amount, this.priority); final String address; final double amount; + TransactionPriority priority; } diff --git a/lib/bitcoin/bitcoin_transaction_history.dart b/lib/bitcoin/bitcoin_transaction_history.dart index 1a3990bf7..4a18d18c1 100644 --- a/lib/bitcoin/bitcoin_transaction_history.dart +++ b/lib/bitcoin/bitcoin_transaction_history.dart @@ -6,8 +6,6 @@ import 'package:cake_wallet/bitcoin/file.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart'; import 'package:cake_wallet/bitcoin/electrum.dart'; -import 'package:cake_wallet/src/domain/common/transaction_info.dart'; -import 'package:cake_wallet/src/domain/common/transaction_direction.dart'; part 'bitcoin_transaction_history.g.dart'; @@ -24,100 +22,176 @@ abstract class BitcoinTransactionHistoryBase {this.eclient, String dirPath, @required String password}) : path = '$dirPath/$_transactionsHistoryFileName', _password = password, - _height = 0; + _height = 0, + _isUpdating = false { + transactions = ObservableMap(); + } BitcoinWalletBase wallet; final ElectrumClient eclient; final String path; final String _password; int _height; + bool _isUpdating; Future init() async { - final info = await _read(); - _height = info['height'] as int ?? _height; - transactions = ObservableList.of( - info['transactions'] as List ?? - []); + await _load(); } @override Future update() async { - await super.update(); - _updateHeight(); + if (_isUpdating) { + return; + } + + try { + _isUpdating = true; + final txs = await fetchTransactions(); + await add(txs); + _isUpdating = false; + } catch (_) { + _isUpdating = false; + rethrow; + } } @override - Future> fetchTransactions() async { - final addresses = wallet.addresses; + Future> fetchTransactions() async { final histories = - addresses.map((record) => eclient.getHistory(address: record.address)); + wallet.scriptHashes.map((scriptHash) => eclient.getHistory(scriptHash)); final _historiesWithDetails = await Future.wait(histories) .then((histories) => histories - .map((h) => h.where((tx) => (tx['height'] as int) > _height)) +// .map((h) => h.where((tx) { +// final height = tx['height'] as int ?? 0; +// // FIXME: Filter only needed transactions +// final _tx = get(tx['tx_hash'] as String); +// +// return height == 0 || height > _height; +// })) .expand((i) => i) .toList()) .then((histories) => histories.map((tx) => fetchTransactionInfo( hash: tx['tx_hash'] as String, height: tx['height'] as int))); final historiesWithDetails = await Future.wait(_historiesWithDetails); - return historiesWithDetails - .map((info) => BitcoinTransactionInfo.fromHexAndHeader( - info['raw'] as String, info['header'] as Map, - addresses: addresses.map((record) => record.address).toList())) - .toList(); + return historiesWithDetails.fold>( + {}, (acc, tx) { + acc[tx.id] = tx; + return acc; + }); } - Future> fetchTransactionInfo( + Future fetchTransactionInfo( {@required String hash, @required int height}) async { - final rawFetching = eclient.getTransactionRaw(hash: hash); - final headerFetching = eclient.getHeader(height: height); - final result = await Future.wait([rawFetching, headerFetching]); - final raw = result.first as String; - final header = result[1] as Map; - - return {'raw': raw, 'header': header}; + final tx = await eclient.getTransactionExpanded(hash: hash); + return BitcoinTransactionInfo.fromElectrumVerbose(tx, + height: height, addresses: wallet.addresses); } - Future add(List transactions) async { - this.transactions.addAll(transactions); + Future add(Map transactionsList) async { + transactionsList.entries.forEach((entry) { + _updateOrInsert(entry.value); + + if (entry.value.height > _height) { + _height = entry.value.height; + } + }); + await save(); } Future addOne(BitcoinTransactionInfo tx) async { - transactions.add(tx); + _updateOrInsert(tx); + + if (tx.height > _height) { + _height = tx.height; + } + await save(); } - Future save() async => writeData( - path: path, - password: _password, - data: json.encode({'height': _height, 'transactions': transactions})); + BitcoinTransactionInfo get(String id) => transactions[id]; + + Future save() async { + final data = json.encode({'height': _height, 'transactions': transactions}); + + print('data'); + print(data); + + await writeData(path: path, password: _password, data: data); + } + + @override + void updateAsync({void Function() onFinished}) { + fetchTransactionsAsync((transaction) => _updateOrInsert(transaction), + onFinished: onFinished); + } + + @override + void fetchTransactionsAsync( + void Function(BitcoinTransactionInfo transaction) onTransactionLoaded, + {void Function() onFinished}) async { + final histories = await Future.wait(wallet.scriptHashes + .map((scriptHash) async => await eclient.getHistory(scriptHash))); + final transactionsCount = + histories.fold(0, (acc, m) => acc + m.length); + var counter = 0; + + final batches = histories.map((metaList) => + _fetchBatchOfTransactions(metaList, onTransactionLoaded: (transaction) { + onTransactionLoaded(transaction); + counter += 1; + + if (counter == transactionsCount) { + onFinished?.call(); + } + })); + + await Future.wait(batches); + } + + Future _fetchBatchOfTransactions( + Iterable> metaList, + {void Function(BitcoinTransactionInfo tranasaction) + onTransactionLoaded}) async => + metaList.forEach((txMeta) => fetchTransactionInfo( + hash: txMeta['tx_hash'] as String, + height: txMeta['height'] as int) + .then((transaction) => onTransactionLoaded(transaction))); Future> _read() async { + final content = await read(path: path, password: _password); + return json.decode(content) as Map; + } + + Future _load() async { try { - final content = await read(path: path, password: _password); - final jsoned = json.decode(content) as Map; - final height = jsoned['height'] as int; - final transactions = (jsoned['transactions'] as List) - .map((dynamic row) { - if (row is Map) { - return BitcoinTransactionInfo.fromJson(row); - } + final content = await _read(); + final txs = content['transactions'] as Map ?? {}; - return null; - }) - .where((el) => el != null) - .toList(); + txs.entries.forEach((entry) { + final val = entry.value; - return {'transactions': transactions, 'height': height}; - } catch (_) { - return {'transactions': [], 'height': 0}; + if (val is Map) { + final tx = BitcoinTransactionInfo.fromJson(val); + _updateOrInsert(tx); + } + }); + + _height = content['height'] as int; + } catch (_) {} + } + + void _updateOrInsert(BitcoinTransactionInfo transaction) { + if (transactions[transaction.id] == null) { + transactions[transaction.id] = transaction; + } else { + final originalTx = transactions[transaction.id]; + originalTx.confirmations = transaction.confirmations; + originalTx.amount = transaction.amount; + originalTx.height = transaction.height; + originalTx.date ??= transaction.date; + originalTx.isPending = transaction.isPending; } } - - void _updateHeight() { - final newHeight = transactions.fold( - 0, (int acc, val) => val.height > acc ? val.height : acc); - _height = newHeight > _height ? newHeight : _height; - } } diff --git a/lib/bitcoin/bitcoin_transaction_info.dart b/lib/bitcoin/bitcoin_transaction_info.dart index 583d1615c..d3c56b530 100644 --- a/lib/bitcoin/bitcoin_transaction_info.dart +++ b/lib/bitcoin/bitcoin_transaction_info.dart @@ -1,7 +1,9 @@ -import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; import 'package:flutter/foundation.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart'; import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; +import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; +import 'package:cake_wallet/src/domain/bitcoin/bitcoin_amount_format.dart'; import 'package:cake_wallet/src/domain/common/transaction_direction.dart'; import 'package:cake_wallet/src/domain/common/transaction_info.dart'; import 'package:cake_wallet/src/domain/common/format_amount.dart'; @@ -13,7 +15,8 @@ class BitcoinTransactionInfo extends TransactionInfo { @required int amount, @required TransactionDirection direction, @required bool isPending, - @required DateTime date}) { + @required DateTime date, + @required this.confirmations}) { this.height = height; this.amount = amount; this.direction = direction; @@ -21,34 +24,87 @@ class BitcoinTransactionInfo extends TransactionInfo { this.isPending = isPending; } - factory BitcoinTransactionInfo.fromHexAndHeader( - String hex, Map header, - {List addresses}) { + factory BitcoinTransactionInfo.fromElectrumVerbose(Map obj, + {@required List addresses, @required int height}) { + final addressesSet = addresses.map((addr) => addr.address).toSet(); + final id = obj['txid'] as String; + final vins = obj['vin'] as List ?? []; + final vout = (obj['vout'] as List ?? []); + final date = obj['time'] is int + ? DateTime.fromMillisecondsSinceEpoch((obj['time'] as int) * 1000) + : DateTime.now(); + final confirmations = obj['confirmations'] as int ?? 0; + var direction = TransactionDirection.incoming; + + for (dynamic vin in vins) { + final vout = vin['vout'] as int; + final out = vin['tx']['vout'][vout] as Map; + final outAddresses = + (out['scriptPubKey']['addresses'] as List)?.toSet(); + + if (outAddresses?.intersection(addressesSet)?.isNotEmpty ?? false) { + direction = TransactionDirection.outgoing; + break; + } + } + + final amount = vout.fold(0, (int acc, dynamic out) { + final outAddresses = + out['scriptPubKey']['addresses'] as List ?? []; + final ntrs = outAddresses.toSet().intersection(addressesSet); + var amount = acc; + + if ((direction == TransactionDirection.incoming && ntrs.isNotEmpty) || + (direction == TransactionDirection.outgoing && ntrs.isEmpty)) { + amount += doubleToBitcoinAmount(out['value'] as double ?? 0.0); + } + + return amount; + }); + + return BitcoinTransactionInfo( + id: id, + height: height, + isPending: false, + direction: direction, + amount: amount, + date: date, + confirmations: confirmations); + } + + factory BitcoinTransactionInfo.fromHexAndHeader(String hex, + {List addresses, int height, int timestamp, int confirmations}) { final tx = bitcoin.Transaction.fromHex(hex); var exist = false; var amount = 0; - tx.outs.forEach((out) { - try { - final p2pkh = bitcoin.P2PKH( - data: PaymentData(output: out.script), network: bitcoin.bitcoin); - exist = addresses.contains(p2pkh.data.address); + if (addresses != null) { + tx.outs.forEach((out) { + try { + final p2pkh = bitcoin.P2PKH( + data: PaymentData(output: out.script), network: bitcoin.bitcoin); + exist = addresses.contains(p2pkh.data.address); - if (exist) { - amount += out.value; - } - } catch (_) {} - }); + if (exist) { + amount += out.value; + } + } catch (_) {} + }); + } + + final date = timestamp != null + ? DateTime.fromMillisecondsSinceEpoch(timestamp * 1000) + : DateTime.now(); // FIXME: Get transaction is pending return BitcoinTransactionInfo( id: tx.getId(), - height: header['block_height'] as int, + height: height, isPending: false, direction: TransactionDirection.incoming, amount: amount, - date: DateTime.fromMillisecondsSinceEpoch( - (header['timestamp'] as int) * 1000)); + date: date, + confirmations: confirmations); } factory BitcoinTransactionInfo.fromJson(Map data) { @@ -58,15 +114,18 @@ class BitcoinTransactionInfo extends TransactionInfo { amount: data['amount'] as int, direction: parseTransactionDirectionFromInt(data['direction'] as int), date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), - isPending: data['isPending'] as bool); + isPending: data['isPending'] as bool, + confirmations: data['confirmations'] as int); } final String id; + int confirmations; String _fiatAmount; @override - String amountFormatted() => '${formatAmount(bitcoinAmountToString(amount: amount))} BTC'; + String amountFormatted() => + '${formatAmount(bitcoinAmountToString(amount: amount))} BTC'; @override String fiatAmount() => _fiatAmount ?? ''; @@ -75,13 +134,14 @@ class BitcoinTransactionInfo extends TransactionInfo { void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); Map toJson() { - final m = Map(); + final m = {}; m['id'] = id; m['height'] = height; m['amount'] = amount; m['direction'] = direction.index; m['date'] = date.millisecondsSinceEpoch; m['isPending'] = isPending; + m['confirmations'] = confirmations; return m; } } diff --git a/lib/bitcoin/bitcoin_transaction_no_inputs_exception.dart b/lib/bitcoin/bitcoin_transaction_no_inputs_exception.dart new file mode 100644 index 000000000..2e925fdb3 --- /dev/null +++ b/lib/bitcoin/bitcoin_transaction_no_inputs_exception.dart @@ -0,0 +1,4 @@ +class BitcoinTransactionNoInputsException implements Exception { + @override + String toString() => 'No inputs for the transaction.'; +} \ No newline at end of file diff --git a/lib/bitcoin/bitcoin_transaction_wrong_balance_exception.dart b/lib/bitcoin/bitcoin_transaction_wrong_balance_exception.dart new file mode 100644 index 000000000..9d1401818 --- /dev/null +++ b/lib/bitcoin/bitcoin_transaction_wrong_balance_exception.dart @@ -0,0 +1,4 @@ +class BitcoinTransactionWrongBalanceException implements Exception { + @override + String toString() => 'Wrong balance. Not enough BTC on your balance.'; +} \ No newline at end of file diff --git a/lib/bitcoin/bitcoin_unspent.dart b/lib/bitcoin/bitcoin_unspent.dart new file mode 100644 index 000000000..bacc03dd4 --- /dev/null +++ b/lib/bitcoin/bitcoin_unspent.dart @@ -0,0 +1,17 @@ +import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart'; + +class BitcoinUnspent { + BitcoinUnspent(this.address, this.hash, this.value, this.vout); + + factory BitcoinUnspent.fromJSON( + BitcoinAddressRecord address, Map json) => + BitcoinUnspent(address, json['tx_hash'] as String, json['value'] as int, + json['tx_pos'] as int); + + final BitcoinAddressRecord address; + final String hash; + final int value; + final int vout; + + bool get isP2wpkh => address.address.startsWith('bc1'); +} diff --git a/lib/bitcoin/bitcoin_wallet.dart b/lib/bitcoin/bitcoin_wallet.dart index fa2ce0f0a..b83e95ef1 100644 --- a/lib/bitcoin/bitcoin_wallet.dart +++ b/lib/bitcoin/bitcoin_wallet.dart @@ -1,10 +1,20 @@ import 'dart:typed_data'; import 'dart:convert'; import 'package:cake_wallet/bitcoin/bitcoin_transaction_credentials.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_transaction_no_inputs_exception.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_transaction_wrong_balance_exception.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_unspent.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet_keys.dart'; +import 'package:cake_wallet/bitcoin/pending_bitcoin_transaction.dart'; +import 'package:cake_wallet/bitcoin/script_hash.dart'; +import 'package:cake_wallet/bitcoin/utils.dart'; import 'package:cake_wallet/src/domain/bitcoin/bitcoin_amount_format.dart'; import 'package:cake_wallet/src/domain/common/crypto_currency.dart'; import 'package:cake_wallet/src/domain/common/sync_status.dart'; +import 'package:cake_wallet/src/domain/common/transaction_direction.dart'; +import 'package:cake_wallet/src/domain/common/transaction_priority.dart'; +import 'package:cw_monero/transaction_history.dart'; import 'package:flutter/cupertino.dart'; import 'package:mobx/mobx.dart'; import 'package:bip39/bip39.dart' as bip39; @@ -20,12 +30,39 @@ import 'package:cake_wallet/bitcoin/bitcoin_balance.dart'; import 'package:cake_wallet/src/domain/common/node.dart'; import 'package:cake_wallet/core/wallet_base.dart'; import 'package:rxdart/rxdart.dart'; +import 'package:hex/hex.dart'; part 'bitcoin_wallet.g.dart'; class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet; abstract class BitcoinWalletBase extends WalletBase with Store { + BitcoinWalletBase._internal( + {@required this.eclient, + @required this.path, + @required String password, + @required this.name, + List initialAddresses, + int accountIndex = 0, + this.transactionHistory, + this.mnemonic, + BitcoinBalance initialBalance}) + : balance = + initialBalance ?? BitcoinBalance(confirmed: 0, unconfirmed: 0), + hd = bitcoin.HDWallet.fromSeed(bip39.mnemonicToSeed(mnemonic), + network: bitcoin.bitcoin), + addresses = initialAddresses != null + ? ObservableList.of(initialAddresses) + : ObservableList(), + syncStatus = NotConnectedSyncStatus(), + _password = password, + _accountIndex = accountIndex, + _addressesKeys = {} { + type = WalletType.bitcoin; + currency = CryptoCurrency.btc; + _scripthashesUpdateSubject = {}; + } + static BitcoinWallet fromJSON( {@required String password, @required String name, @@ -37,12 +74,12 @@ abstract class BitcoinWalletBase extends WalletBase with Store { (data['account_index'] == 'null' || data['account_index'] == null) ? 0 : int.parse(data['account_index'] as String); - final _addresses = data['addresses'] as List; + final _addresses = data['addresses'] as List ?? []; final addresses = []; final balance = BitcoinBalance.fromJSON(data['balance'] as String) ?? BitcoinBalance(confirmed: 0, unconfirmed: 0); - _addresses?.forEach((Object el) { + _addresses.forEach((Object el) { if (el is String) { addresses.add(BitcoinAddressRecord.fromJSON(el)); } @@ -83,34 +120,10 @@ abstract class BitcoinWalletBase extends WalletBase with Store { transactionHistory: history); } - BitcoinWalletBase._internal( - {@required this.eclient, - @required this.path, - @required String password, - @required this.name, - List initialAddresses, - int accountIndex = 0, - this.transactionHistory, - this.mnemonic, - BitcoinBalance initialBalance}) { - type = WalletType.bitcoin; - currency = CryptoCurrency.btc; - balance = initialBalance ?? BitcoinBalance(confirmed: 0, unconfirmed: 0); - hd = bitcoin.HDWallet.fromSeed(bip39.mnemonicToSeed(mnemonic), - network: bitcoin.bitcoin); - addresses = initialAddresses != null - ? ObservableList.of(initialAddresses) - : ObservableList(); - syncStatus = NotConnectedSyncStatus(); - - _password = password; - _accountIndex = accountIndex; - } - @override final BitcoinTransactionHistory transactionHistory; final String path; - bitcoin.HDWallet hd; + final bitcoin.HDWallet hd; final ElectrumClient eclient; final String mnemonic; @@ -131,6 +144,11 @@ abstract class BitcoinWalletBase extends WalletBase with Store { ObservableList addresses; + Map _addressesKeys; + + List get scriptHashes => + addresses.map((addr) => scriptHash(addr.address)).toList(); + String get xpub => hd.base58; @override @@ -142,11 +160,13 @@ abstract class BitcoinWalletBase extends WalletBase with Store { int _accountIndex; String _password; - BehaviorSubject _addressUpdateSubject; + Map> _scripthashesUpdateSubject; Future init() async { if (addresses.isEmpty) { - addresses.add(BitcoinAddressRecord(_getAddress(hd: hd, index: 0))); + final index = 0; + addresses + .add(BitcoinAddressRecord(_getAddress(index: index), index: index)); } address = addresses.first.address; @@ -156,9 +176,8 @@ abstract class BitcoinWalletBase extends WalletBase with Store { Future generateNewAddress({String label}) async { _accountIndex += 1; - final address = BitcoinAddressRecord( - _getAddress(hd: hd, index: _accountIndex), - label: label); + final address = BitcoinAddressRecord(_getAddress(index: _accountIndex), + index: _accountIndex, label: label); addresses.add(address); await save(); @@ -181,9 +200,8 @@ abstract class BitcoinWalletBase extends WalletBase with Store { Future startSync() async { try { syncStatus = StartingSyncStatus(); - await _addressUpdateSubject?.close(); - _addressUpdateSubject = eclient.addressUpdate(address: address); - await transactionHistory.update(); + transactionHistory.updateAsync(onFinished: () => print('finished!')); + _subscribeForUpdates(); await _updateBalance(); syncStatus = SyncedSyncStatus(); } catch (e) { @@ -197,39 +215,102 @@ abstract class BitcoinWalletBase extends WalletBase with Store { Future connectToNode({@required Node node}) async { try { syncStatus = ConnectingSyncStatus(); - await eclient.connect(host: 'electrum2.hodlister.co', port: 50002); + // electrum2.hodlister.co + // bitcoin.electrumx.multicoin.co:50002 + // electrum2.taborsky.cz:5002 + await eclient.connect( + host: 'bitcoin.electrumx.multicoin.co', port: 50002); syncStatus = ConnectedSyncStatus(); } catch (e) { - print(e.toString); + print(e.toString()); syncStatus = FailedSyncStatus(); } } @override - Future createTransaction(Object credentials) async { + Future createTransaction( + Object credentials) async { final transactionCredentials = credentials as BitcoinTransactionCredentials; - + final inputs = []; + final fee = _feeMultiplier(transactionCredentials.priority); + final amount = transactionCredentials.amount != null + ? doubleToBitcoinAmount(transactionCredentials.amount) + : balance.total - fee; + final totalAmount = amount + fee; final txb = bitcoin.TransactionBuilder(network: bitcoin.bitcoin); - final keyPair = bitcoin.ECPair.fromWIF(hd.wif); - final transactions = transactionHistory.transactions; - transactions.sort((q, w) => q.height.compareTo(w.height)); - final prevTx = transactions.first; + var leftAmount = totalAmount; + final changeAddress = address; + var totalInputAmount = 0; + + final unspent = addresses.map((address) => eclient + .getListUnspentWithAddress(address.address) + .then((unspent) => unspent + .map((unspent) => BitcoinUnspent.fromJSON(address, unspent)))); + + for (final unptsFutures in unspent) { + final utxs = await unptsFutures; + + for (final utx in utxs) { + final inAmount = utx.value > totalAmount ? totalAmount : utx.value; + leftAmount = leftAmount - inAmount; + totalInputAmount += inAmount; + inputs.add(utx); + + if (leftAmount <= 0) { + break; + } + } + + if (leftAmount <= 0) { + break; + } + } + + if (inputs.isEmpty) { + throw BitcoinTransactionNoInputsException(); + } + + if (amount <= 0 || totalInputAmount < amount) { + throw BitcoinTransactionWrongBalanceException(); + } + + final changeValue = totalInputAmount - amount - fee; txb.setVersion(1); - txb.addInput(prevTx, 0); - txb.addOutput(transactionCredentials.address, - doubleToBitcoinAmount(transactionCredentials.amount)); - txb.sign(vin: 0, keyPair: keyPair); - final encoded = txb.build().toHex(); - print('Enoded transaction $encoded'); - await eclient.broadcastTransaction(transactionRaw: encoded); + inputs.forEach((input) { + if (input.isP2wpkh) { + final p2wpkh = bitcoin + .P2WPKH( + data: generatePaymentData(hd: hd, index: input.address.index), + network: bitcoin.bitcoin) + .data; + + txb.addInput(input.hash, input.vout, null, p2wpkh.output); + } else { + txb.addInput(input.hash, input.vout); + } + }); + + txb.addOutput(transactionCredentials.address, amount); + + if (changeValue > 0) { + txb.addOutput(changeAddress, changeValue); + } + + for (var i = 0; i < inputs.length; i++) { + final input = inputs[i]; + final keyPair = generateKeyPair(hd: hd, index: input.address.index); + final witnessValue = input.isP2wpkh ? input.value : null; + + txb.sign(vin: i, keyPair: keyPair, witnessValue: witnessValue); + } + + return PendingBitcoinTransaction(txb.build(), + eclient: eclient, amount: amount, fee: fee) + ..addListener((transaction) => transactionHistory.addOne(transaction)); } - @override - Future save() async => - await write(path: path, password: _password, data: toJSON()); - String toJSON() => json.encode({ 'mnemonic': mnemonic, 'account_index': _accountIndex.toString(), @@ -237,16 +318,32 @@ abstract class BitcoinWalletBase extends WalletBase with Store { 'balance': balance?.toJSON() }); - String _getAddress({bitcoin.HDWallet hd, int index}) => bitcoin - .P2WPKH( - data: PaymentData( - pubkey: Uint8List.fromList(hd.derive(index).pubKey.codeUnits))) - .data - .address; + @override + double calculateEstimatedFee(TransactionPriority priority) => + bitcoinAmountToDouble(amount: _feeMultiplier(priority)); + + @override + Future save() async => + await write(path: path, password: _password, data: toJSON()); + + bitcoin.ECPair keyPairFor({@required int index}) => + generateKeyPair(hd: hd, index: index); + + void _subscribeForUpdates() { + scriptHashes.forEach((sh) async { + await _scripthashesUpdateSubject[sh]?.close(); + _scripthashesUpdateSubject[sh] = eclient.scripthashUpdate(sh); + _scripthashesUpdateSubject[sh].listen((event) async { + print('event $event'); + transactionHistory.updateAsync(); + await _updateBalance(); + }); + }); + } Future _fetchBalances() async { final balances = await Future.wait( - addresses.map((record) => eclient.getBalance(address: record.address))); + scriptHashes.map((sHash) => eclient.getBalance(sHash))); final balance = balances.fold( BitcoinBalance(confirmed: 0, unconfirmed: 0), (BitcoinBalance acc, val) => BitcoinBalance( @@ -261,4 +358,20 @@ abstract class BitcoinWalletBase extends WalletBase with Store { balance = await _fetchBalances(); await save(); } + + String _getAddress({@required int index}) => + generateAddress(hd: hd, index: index); + + int _feeMultiplier(TransactionPriority priority) { + switch (priority) { + case TransactionPriority.slow: + return 6000; + case TransactionPriority.regular: + return 9000; + case TransactionPriority.fast: + return 15000; + default: + return 0; + } + } } diff --git a/lib/bitcoin/electrum.dart b/lib/bitcoin/electrum.dart index 70a01a293..f24f6611f 100644 --- a/lib/bitcoin/electrum.dart +++ b/lib/bitcoin/electrum.dart @@ -1,17 +1,18 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:cake_wallet/bitcoin/script_hash.dart'; import 'package:flutter/foundation.dart'; import 'package:rxdart/rxdart.dart'; String jsonrpcparams(List params) { final _params = params?.map((val) => '"${val.toString()}"')?.join(','); - return "[$_params]"; + return '[$_params]'; } String jsonrpc( {String method, List params, int id, double version = 2.0}) => - '{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${jsonrpcparams(params)}}\n'; + '{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${json.encode(params)}}\n'; class SocketTask { SocketTask({this.completer, this.isSubscription, this.subject}); @@ -50,6 +51,7 @@ class ElectrumClient { socket.listen((List event) { try { final jsoned = json.decode(utf8.decode(event)) as Map; +// print(jsoned); final method = jsoned['method']; if (method is String) { @@ -93,18 +95,18 @@ class ElectrumClient { return []; }); - Future> getBalance({String address}) => - call(method: 'blockchain.address.get_balance', params: [address]) + Future> getBalance(String scriptHash) => + call(method: 'blockchain.scripthash.get_balance', params: [scriptHash]) .then((dynamic result) { if (result is Map) { return result; } - return Map(); + return {}; }); - Future>> getHistory({String address}) => - call(method: 'blockchain.address.get_history', params: [address]) + Future>> getHistory(String scriptHash) => + call(method: 'blockchain.scripthash.get_history', params: [scriptHash]) .then((dynamic result) { if (result is List) { return result.map((dynamic val) { @@ -112,26 +114,94 @@ class ElectrumClient { return val; } - return Map(); + return {}; }).toList(); } return []; }); - Future getTransactionRaw({@required String hash}) async => - call(method: 'blockchain.transaction.get', params: [hash]) + Future>> getListUnspentWithAddress( + String address) => + call( + method: 'blockchain.scripthash.listunspent', + params: [scriptHash(address)]).then((dynamic result) { + if (result is List) { + return result.map((dynamic val) { + if (val is Map) { + val['address'] = address; + return val; + } + + return {}; + }).toList(); + } + + return []; + }); + + Future>> getListUnspent(String scriptHash) => + call(method: 'blockchain.scripthash.listunspent', params: [scriptHash]) .then((dynamic result) { - if (result is String) { + if (result is List) { + return result.map((dynamic val) { + if (val is Map) { + return val; + } + + return {}; + }).toList(); + } + + return []; + }); + + Future>> getMempool(String scriptHash) => + call(method: 'blockchain.scripthash.get_mempool', params: [scriptHash]) + .then((dynamic result) { + if (result is List) { + return result.map((dynamic val) { + if (val is Map) { + return val; + } + + return {}; + }).toList(); + } + + return []; + }); + + Future> getTransactionRaw( + {@required String hash}) async => + call(method: 'blockchain.transaction.get', params: [hash, true]) + .then((dynamic result) { + if (result is Map) { return result; } - return ''; + return {}; }); - Future broadcastTransaction({@required String transactionRaw}) async => + Future> getTransactionExpanded( + {@required String hash}) async { + final originalTx = await getTransactionRaw(hash: hash); + final vins = originalTx['vin'] as List; + + for (dynamic vin in vins) { + if (vin is Map) { + vin['tx'] = await getTransactionRaw(hash: vin['txid'] as String); + } + } + + return originalTx; + } + + Future broadcastTransaction( + {@required String transactionRaw}) async => call(method: 'blockchain.transaction.broadcast', params: [transactionRaw]) .then((dynamic result) { + print('result $result'); if (result is String) { return result; } @@ -163,11 +233,11 @@ class ElectrumClient { return 0; }); - BehaviorSubject addressUpdate({@required String address}) => + BehaviorSubject scripthashUpdate(String scripthash) => subscribe( - id: 'blockchain.address.subscribe:$address', - method: 'blockchain.address.subscribe', - params: [address]); + id: 'blockchain.scripthash.subscribe:$scripthash', + method: 'blockchain.scripthash.subscribe', + params: [scripthash]); BehaviorSubject subscribe( {@required String id, @@ -218,15 +288,12 @@ class ElectrumClient { void _methodHandler( {@required String method, @required Map request}) { switch (method) { - case 'blockchain.address.subscribe': + case 'blockchain.scripthash.subscribe': final params = request['params'] as List; - final address = params.first as String; - final id = 'blockchain.address.subscribe:$address'; - - if (_tasks[id] != null) { - _tasks[id].subject.add(params.last); - } + final scripthash = params.first as String; + final id = 'blockchain.scripthash.subscribe:$scripthash'; + _tasks[id]?.subject?.add(params.last); break; default: break; diff --git a/lib/bitcoin/pending_bitcoin_transaction.dart b/lib/bitcoin/pending_bitcoin_transaction.dart new file mode 100644 index 000000000..64b317169 --- /dev/null +++ b/lib/bitcoin/pending_bitcoin_transaction.dart @@ -0,0 +1,47 @@ +import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart'; +import 'package:cake_wallet/src/domain/common/transaction_direction.dart'; +import 'package:flutter/foundation.dart'; +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:cake_wallet/core/pending_transaction.dart'; +import 'package:cake_wallet/bitcoin/electrum.dart'; + +class PendingBitcoinTransaction with PendingTransaction { + PendingBitcoinTransaction(this._tx, + {@required this.eclient, @required this.amount, @required this.fee}) + : _listeners = []; + + final bitcoin.Transaction _tx; + final ElectrumClient eclient; + final int amount; + final int fee; + + String get id => _tx.getId(); + + @override + String get amountFormatted => bitcoinAmountToString(amount: amount); + + @override + String get feeFormatted => bitcoinAmountToString(amount: fee); + + final List _listeners; + + @override + Future commit() async { + await eclient.broadcastTransaction(transactionRaw: _tx.toHex()); + _listeners?.forEach((listener) => listener(transactionInfo())); + } + + void addListener( + void Function(BitcoinTransactionInfo transaction) listener) => + _listeners.add(listener); + + BitcoinTransactionInfo transactionInfo() => BitcoinTransactionInfo( + id: id, + height: 0, + amount: amount, + direction: TransactionDirection.outgoing, + date: DateTime.now(), + isPending: true, + confirmations: 0); +} diff --git a/lib/bitcoin/script_hash.dart b/lib/bitcoin/script_hash.dart new file mode 100644 index 000000000..b252a0700 --- /dev/null +++ b/lib/bitcoin/script_hash.dart @@ -0,0 +1,18 @@ +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:crypto/crypto.dart'; + +String scriptHash(String address) { + final outputScript = bitcoin.Address.addressToOutputScript(address); + final splitted = sha256.convert(outputScript).toString().split(''); + var res = ''; + + for (var i = splitted.length - 1; i >= 0; i--) { + final char = splitted[i]; + i--; + final nextChar = splitted[i]; + res += nextChar; + res += char; + } + + return res; +} \ No newline at end of file diff --git a/lib/bitcoin/utils.dart b/lib/bitcoin/utils.dart new file mode 100644 index 000000000..257c8b176 --- /dev/null +++ b/lib/bitcoin/utils.dart @@ -0,0 +1,26 @@ +import 'dart:typed_data'; +import 'package:flutter/foundation.dart'; +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; +import 'package:hex/hex.dart'; + +bitcoin.PaymentData generatePaymentData( + {@required bitcoin.HDWallet hd, @required int index}) => + PaymentData( + pubkey: Uint8List.fromList(HEX.decode(hd.derive(index).pubKey))); + +bitcoin.ECPair generateKeyPair( + {@required bitcoin.HDWallet hd, + @required int index, + bitcoin.NetworkType network}) => + bitcoin.ECPair.fromWIF(hd.derive(index).wif, + network: network ?? bitcoin.bitcoin); + +String generateAddress({@required bitcoin.HDWallet hd, @required int index}) => + bitcoin + .P2WPKH( + data: PaymentData( + pubkey: + Uint8List.fromList(HEX.decode(hd.derive(index).pubKey)))) + .data + .address; diff --git a/lib/core/amount_validator.dart b/lib/core/amount_validator.dart index 0394e9244..7df223080 100644 --- a/lib/core/amount_validator.dart +++ b/lib/core/amount_validator.dart @@ -13,10 +13,10 @@ class AmountValidator extends TextValidator { static String _pattern(WalletType type) { switch (type) { case WalletType.monero: - return '^([0-9]+([.][0-9]{0,12})?|[.][0-9]{1,12})\$'; + return '^([0-9]+([.\,][0-9]{0,12})?|[.\,][0-9]{1,12})\$'; case WalletType.bitcoin: // FIXME: Incorrect pattern for bitcoin - return '^([0-9]+([.][0-9]{0,12})?|[.][0-9]{1,12})\$'; + return '^([0-9]+([.\,][0-9]{0,12})?|[.\,][0-9]{1,12})\$'; default: return ''; } diff --git a/lib/core/pending_transaction.dart b/lib/core/pending_transaction.dart new file mode 100644 index 000000000..c7adb4478 --- /dev/null +++ b/lib/core/pending_transaction.dart @@ -0,0 +1,6 @@ +mixin PendingTransaction { + String get amountFormatted; + String get feeFormatted; + + Future commit(); +} \ No newline at end of file diff --git a/lib/core/transaction_history.dart b/lib/core/transaction_history.dart index 78711905e..dd2c5a0f2 100644 --- a/lib/core/transaction_history.dart +++ b/lib/core/transaction_history.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/src/domain/common/transaction_info.dart'; @@ -5,7 +6,7 @@ abstract class TransactionHistoryBase { TransactionHistoryBase() : _isUpdating = false; @observable - ObservableList transactions; + ObservableMap transactions; bool _isUpdating; @@ -24,5 +25,15 @@ abstract class TransactionHistoryBase { } } - Future> fetchTransactions(); -} \ No newline at end of file + void updateAsync({void Function() onFinished}) { + fetchTransactionsAsync( + (transaction) => transactions[transaction.id] = transaction, + onFinished: onFinished); + } + + void fetchTransactionsAsync( + void Function(TransactionType transaction) onTransactionLoaded, + {void Function() onFinished}); + + Future> fetchTransactions(); +} diff --git a/lib/core/wallet_base.dart b/lib/core/wallet_base.dart index dfbf31aef..3d0246251 100644 --- a/lib/core/wallet_base.dart +++ b/lib/core/wallet_base.dart @@ -1,5 +1,7 @@ import 'package:flutter/foundation.dart'; +import 'package:cake_wallet/core/pending_transaction.dart'; import 'package:cake_wallet/core/transaction_history.dart'; +import 'package:cake_wallet/src/domain/common/transaction_priority.dart'; import 'package:cake_wallet/src/domain/common/crypto_currency.dart'; import 'package:cake_wallet/src/domain/common/sync_status.dart'; import 'package:cake_wallet/src/domain/common/node.dart'; @@ -30,7 +32,9 @@ abstract class WalletBase { Future startSync(); - Future createTransaction(Object credentials); + Future createTransaction(Object credentials); + + double calculateEstimatedFee(TransactionPriority priority); Future save(); } diff --git a/lib/di.dart b/lib/di.dart index 1229fbed1..4c4c038a7 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -36,7 +36,7 @@ import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart'; -import 'package:cake_wallet/view_model/send_view_model.dart'; +import 'package:cake_wallet/view_model/send/send_view_model.dart'; import 'package:cake_wallet/view_model/settings/settings_view_model.dart'; import 'package:cake_wallet/view_model/wallet_keys_view_model.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; @@ -105,8 +105,7 @@ Future setup( getIt.registerSingleton( ContactService(contactSource, getIt.get().contactListStore)); getIt.registerSingleton(TradesStore( - tradesSource: tradesSource, - settingsStore: getIt.get())); + tradesSource: tradesSource, settingsStore: getIt.get())); getIt.registerSingleton( TradeFilterStore(wallet: getIt.get().wallet)); getIt.registerSingleton(TransactionFilterStore()); @@ -143,21 +142,18 @@ Future setup( getIt.registerFactory( () => WalletAddressListViewModel(wallet: getIt.get().wallet)); - getIt.registerFactory( - () => BalanceViewModel( - wallet: getIt.get().wallet, - settingsStore: getIt.get(), - fiatConvertationStore: getIt.get())); + getIt.registerFactory(() => BalanceViewModel( + wallet: getIt.get().wallet, + settingsStore: getIt.get(), + fiatConvertationStore: getIt.get())); - getIt.registerFactory( - () => DashboardViewModel( - balanceViewModel: getIt.get(), - appStore: getIt.get(), - tradesStore: getIt.get(), - tradeFilterStore: getIt.get(), - transactionFilterStore: getIt.get(), - pageViewStore: getIt.get() - )); + getIt.registerFactory(() => DashboardViewModel( + balanceViewModel: getIt.get(), + appStore: getIt.get(), + tradesStore: getIt.get(), + tradeFilterStore: getIt.get(), + transactionFilterStore: getIt.get(), + pageViewStore: getIt.get())); getIt.registerFactory(() => AuthService( secureStorage: getIt.get(), @@ -185,10 +181,9 @@ Future setup( onAuthenticationFinished: onAuthFinished, closable: false)); - getIt.registerFactory( - () => DashboardPage( - walletViewModel: getIt.get(), - addressListViewModel: getIt.get())); + getIt.registerFactory(() => DashboardPage( + walletViewModel: getIt.get(), + addressListViewModel: getIt.get())); getIt.registerFactory(() => ReceivePage( addressListViewModel: getIt.get())); @@ -203,7 +198,9 @@ Future setup( getIt.get(param1: item))); getIt.registerFactory(() => SendViewModel( - getIt.get().wallet, getIt.get().settingsStore)); + getIt.get().wallet, + getIt.get().settingsStore, + getIt.get())); getIt.registerFactory( () => SendPage(sendViewModel: getIt.get())); @@ -243,8 +240,10 @@ Future setup( moneroAccountCreationViewModel: getIt.get())); - getIt.registerFactory( - () => SettingsViewModel(getIt.get().settingsStore)); + getIt.registerFactory(() { + final appStore = getIt.get(); + return SettingsViewModel(appStore.settingsStore, appStore.wallet); + }); getIt.registerFactory(() => SettingsPage(getIt.get())); diff --git a/lib/monero/monero_transaction_history.dart b/lib/monero/monero_transaction_history.dart index 7eeeef423..cd3484389 100644 --- a/lib/monero/monero_transaction_history.dart +++ b/lib/monero/monero_transaction_history.dart @@ -20,12 +20,29 @@ class MoneroTransactionHistory = MoneroTransactionHistoryBase abstract class MoneroTransactionHistoryBase extends TransactionHistoryBase with Store { MoneroTransactionHistoryBase() { - transactions = ObservableList(); + transactions = ObservableMap(); } @override - Future> fetchTransactions() async { + Future> fetchTransactions() async { monero_transaction_history.refreshTransactions(); - return _getAllTransactions(null); + return _getAllTransactions(null).fold>( + {}, + (Map acc, MoneroTransactionInfo tx) { + acc[tx.id] = tx; + return acc; + }); } + + @override + void updateAsync({void Function() onFinished}) { + fetchTransactionsAsync( + (transaction) => transactions[transaction.id] = transaction, + onFinished: onFinished); + } + + @override + void fetchTransactionsAsync( + void Function(MoneroTransactionInfo transaction) onTransactionLoaded, + {void Function() onFinished}) {} } diff --git a/lib/monero/monero_wallet.dart b/lib/monero/monero_wallet.dart index e36e6cb29..a98e76230 100644 --- a/lib/monero/monero_wallet.dart +++ b/lib/monero/monero_wallet.dart @@ -16,6 +16,9 @@ import 'package:cake_wallet/src/domain/monero/account.dart'; import 'package:cake_wallet/src/domain/monero/account_list.dart'; import 'package:cake_wallet/src/domain/monero/subaddress.dart'; import 'package:cake_wallet/src/domain/common/node.dart'; +import 'package:cake_wallet/core/pending_transaction.dart'; +import 'package:cake_wallet/src/domain/common/transaction_priority.dart'; +import 'package:cake_wallet/src/domain/common/calculate_fiat_amount.dart' as cfa; part 'monero_wallet.g.dart'; @@ -133,7 +136,7 @@ abstract class MoneroWalletBase extends WalletBase with Store { } @override - Future createTransaction(Object credentials) async { + Future createTransaction(Object credentials) async { // final _credentials = credentials as MoneroTransactionCreationCredentials; // final transactionDescription = await transaction_history.createTransaction( // address: _credentials.address, @@ -146,6 +149,33 @@ abstract class MoneroWalletBase extends WalletBase with Store { // transactionDescription); } + @override + double calculateEstimatedFee(TransactionPriority priority) { + // FIXME: hardcoded value; + + if (priority == TransactionPriority.slow) { + return 0.00002459; + } + + if (priority == TransactionPriority.regular) { + return 0.00012305; + } + + if (priority == TransactionPriority.medium) { + return 0.00024503; + } + + if (priority == TransactionPriority.fast) { + return 0.00061453; + } + + if (priority == TransactionPriority.fastest) { + return 0.0260216; + } + + return 0; + } + @override Future save() async { // if (_isSaving) { diff --git a/lib/src/domain/common/transaction_info.dart b/lib/src/domain/common/transaction_info.dart index d954fb72f..06a84316d 100644 --- a/lib/src/domain/common/transaction_info.dart +++ b/lib/src/domain/common/transaction_info.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/src/domain/common/transaction_direction.dart'; abstract class TransactionInfo extends Object { + String id; int amount; TransactionDirection direction; bool isPending; diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 20c44ba5d..8bb4cd2b9 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -1,40 +1,27 @@ -import 'package:cake_wallet/core/address_validator.dart'; -import 'package:cake_wallet/core/amount_validator.dart'; -import 'package:cake_wallet/src/screens/auth/auth_page.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; -import 'package:cake_wallet/view_model/send_view_model.dart'; +import 'dart:ui'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx/mobx.dart'; -import 'package:provider/provider.dart'; import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/src/widgets/address_text_field.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/stores/settings/settings_store.dart'; -import 'package:cake_wallet/src/stores/balance/balance_store.dart'; -import 'package:cake_wallet/src/stores/wallet/wallet_store.dart'; -import 'package:cake_wallet/src/stores/send/send_store.dart'; - -//import 'package:cake_wallet/src/stores/send/sending_state.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/view_model/send/send_view_model.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/domain/common/crypto_currency.dart'; -import 'package:cake_wallet/src/domain/common/calculate_estimated_fee.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/domain/common/sync_status.dart'; -import 'package:cake_wallet/src/stores/sync/sync_store.dart'; import 'package:cake_wallet/src/widgets/top_panel.dart'; -import 'package:dotted_border/dotted_border.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart'; -import 'package:cake_wallet/src/screens/send/widgets/sending_alert.dart'; -import 'package:cake_wallet/src/widgets/template_tile.dart'; -import 'package:cake_wallet/src/stores/send_template/send_template_store.dart'; +import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; import 'package:cake_wallet/src/widgets/trail_button.dart'; +// FIXME: Refactor this screen. + class SendPage extends BasePage { SendPage({@required this.sendViewModel}); @@ -53,11 +40,8 @@ class SendPage extends BasePage { bool get resizeToAvoidBottomPadding => false; @override - Widget trailing(context) { -// final sendStore = Provider.of(context); - - return TrailButton(caption: S.of(context).clear, onPressed: () => null); - } + Widget trailing(context) => TrailButton( + caption: S.of(context).clear, onPressed: () => sendViewModel.reset()); @override Widget body(BuildContext context) => SendForm(sendViewModel: sendViewModel); @@ -95,36 +79,28 @@ class SendFormState extends State { } Future getOpenaliasRecord(BuildContext context) async { - final sendStore = Provider.of(context); - final isOpenalias = - await sendStore.isOpenaliasRecord(_addressController.text); - - if (isOpenalias) { - _addressController.text = sendStore.recordAddress; - - await showDialog( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.of(context).openalias_alert_title, - alertContent: - S.of(context).openalias_alert_content(sendStore.recordName), - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); - } +// final sendStore = Provider.of(context); +// final isOpenalias = +// await sendStore.isOpenaliasRecord(_addressController.text); +// +// if (isOpenalias) { +// _addressController.text = sendStore.recordAddress; +// +// await showDialog( +// context: context, +// builder: (BuildContext context) { +// return AlertWithOneAction( +// alertTitle: S.of(context).openalias_alert_title, +// alertContent: +// S.of(context).openalias_alert_content(sendStore.recordName), +// buttonText: S.of(context).ok, +// buttonAction: () => Navigator.of(context).pop()); +// }); +// } } @override Widget build(BuildContext context) { -// final settingsStore = Provider.of(context); -// final sendStore = Provider.of(context); -// sendStore.settingsStore = settingsStore; -// final balanceStore = Provider.of(context); -// final walletStore = Provider.of(context); -// final syncStore = Provider.of(context); -// final sendTemplateStore = Provider.of(context); - _setEffects(context); return Container( @@ -140,7 +116,8 @@ class SendFormState extends State { child: Column(children: [ AddressTextField( controller: _addressController, - placeholder: S.of(context).send_monero_address, + placeholder: 'Address', + //S.of(context).send_monero_address, FIXME: placeholder for btc and xmr address text field. focusNode: _focusNode, onURIScanned: (uri) { var address = ''; @@ -163,110 +140,86 @@ class SendFormState extends State { buttonColor: Theme.of(context).accentTextTheme.title.color, validator: widget.sendViewModel.addressValidator, ), - Observer(builder: (_) { - return Padding( - padding: const EdgeInsets.only(top: 20), - child: TextFormField( - style: TextStyle( - fontSize: 16.0, - color: Theme.of(context) - .primaryTextTheme - .title - .color), - controller: _cryptoAmountController, - keyboardType: TextInputType.numberWithOptions( - signed: false, decimal: true), - inputFormatters: [ - BlacklistingTextInputFormatter( - RegExp('[\\-|\\ |\\,]')) - ], - decoration: InputDecoration( - prefixIcon: Padding( - padding: EdgeInsets.only(top: 12), - child: Text('XMR:', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, + Padding( + padding: const EdgeInsets.only(top: 20), + child: TextFormField( + onChanged: (value) => + widget.sendViewModel.setCryptoAmount(value), + style: TextStyle( + fontSize: 16.0, + color: + Theme.of(context).primaryTextTheme.title.color), + controller: _cryptoAmountController, + keyboardType: TextInputType.numberWithOptions( + signed: false, decimal: true), +// inputFormatters: [ +// BlacklistingTextInputFormatter( +// RegExp('[\\-|\\ |\\,]')) +// ], + decoration: InputDecoration( + prefixIcon: Padding( + padding: EdgeInsets.only(top: 12), + child: Text('${widget.sendViewModel.currency.toString()}:', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context) + .primaryTextTheme + .title + .color, + )), + ), + suffixIcon: Padding( + padding: EdgeInsets.only(bottom: 5), + child: Container( + height: 32, + width: 32, + margin: EdgeInsets.only( + left: 12, bottom: 7, top: 4), + decoration: BoxDecoration( color: Theme.of(context) - .primaryTextTheme + .accentTextTheme .title .color, - )), - ), - suffixIcon: Padding( - padding: EdgeInsets.only(bottom: 5), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Container( - width: - MediaQuery.of(context).size.width / 2, - alignment: Alignment.centerLeft, - child: Text( - ' / ' + widget.sendViewModel.balance, - maxLines: 1, - overflow: TextOverflow.ellipsis, + borderRadius: + BorderRadius.all(Radius.circular(6))), + child: InkWell( + onTap: () => widget.sendViewModel.setAll(), + child: Center( + child: Text(S.of(context).all, + textAlign: TextAlign.center, style: TextStyle( - fontSize: 16, + fontSize: 9, + fontWeight: FontWeight.bold, color: Theme.of(context) .primaryTextTheme .caption .color)), ), - Container( - height: 32, - width: 32, - margin: EdgeInsets.only( - left: 12, bottom: 7, top: 4), - decoration: BoxDecoration( - color: Theme.of(context) - .accentTextTheme - .title - .color, - borderRadius: BorderRadius.all( - Radius.circular(6))), - child: InkWell( - onTap: () => null, - // widget.sendViewModel, - child: Center( - child: Text(S.of(context).all, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 9, - fontWeight: FontWeight.bold, - color: Theme.of(context) - .primaryTextTheme - .caption - .color)), - ), - ), - ) - ], - ), - ), - hintStyle: TextStyle( - fontSize: 16.0, - color: Theme.of(context) - .primaryTextTheme - .title - .color), - hintText: '0.0000', - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).dividerColor, - width: 1.0)), - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).dividerColor, - width: 1.0))), - validator: widget.sendViewModel.amountValidator), - ); - }), + ), + )), + hintStyle: TextStyle( + fontSize: 16.0, + color: Theme.of(context) + .primaryTextTheme + .title + .color), + hintText: '0.0000', + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).dividerColor, + width: 1.0)), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).dividerColor, + width: 1.0))), + validator: widget.sendViewModel.amountValidator), + ), Padding( padding: const EdgeInsets.only(top: 20), child: TextFormField( + onChanged: (value) => + widget.sendViewModel.setFiatAmount(value), style: TextStyle( fontSize: 16.0, color: @@ -274,10 +227,10 @@ class SendFormState extends State { controller: _fiatAmountController, keyboardType: TextInputType.numberWithOptions( signed: false, decimal: true), - inputFormatters: [ - BlacklistingTextInputFormatter( - RegExp('[\\-|\\ |\\,]')) - ], +// inputFormatters: [ +// BlacklistingTextInputFormatter( +// RegExp('[\\-|\\ |\\,]')) +// ], decoration: InputDecoration( prefixIcon: Padding( padding: EdgeInsets.only(top: 12), @@ -426,52 +379,43 @@ class SendFormState extends State { bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), bottomSection: Observer(builder: (_) { return LoadingPrimaryButton( - onPressed: () => null, -// syncStore.status is SyncedSyncStatus -// ? () async { -// // Hack. Don't ask me. -// FocusScope.of(context).requestFocus(FocusNode()); -// -// if (_formKey.currentState.validate()) { -// await showDialog( -// context: context, -// builder: (dialogContext) { -// return AlertWithTwoActions( -// alertTitle: -// S.of(context).send_creating_transaction, -// alertContent: S.of(context).confirm_sending, -// leftButtonText: S.of(context).send, -// rightButtonText: S.of(context).cancel, -// actionLeftButton: () async { -// await Navigator.of(dialogContext) -// .popAndPushNamed(Routes.auth, arguments: -// (bool isAuthenticatedSuccessfully, -// AuthPageState auth) { -// if (!isAuthenticatedSuccessfully) { -// return; -// } -// -// Navigator.of(auth.context).pop(); -// -// sendStore.createTransaction( -// address: _addressController.text, -// paymentId: ''); -// }); -// }, -// actionRightButton: () => -// Navigator.of(context).pop()); -// }); -// } -// } -// : null, + onPressed: () async { + FocusScope.of(context).requestFocus(FocusNode()); + + if (!_formKey.currentState.validate()) { + return; + } + + await showDialog( + context: context, + builder: (dialogContext) { + return AlertWithTwoActions( + alertTitle: S.of(context).send_creating_transaction, + alertContent: S.of(context).confirm_sending, + leftButtonText: S.of(context).send, + rightButtonText: S.of(context).cancel, + actionLeftButton: () async { + await Navigator.of(dialogContext) + .popAndPushNamed(Routes.auth, arguments: + (bool isAuthenticatedSuccessfully, + AuthPageState auth) { + if (!isAuthenticatedSuccessfully) { + return; + } + + Navigator.of(auth.context).pop(); + widget.sendViewModel.createTransaction(); + }); + }, + actionRightButton: () => Navigator.of(context).pop()); + }); + }, text: S.of(context).send, color: Colors.blue, textColor: Colors.white, isLoading: widget.sendViewModel.state is TransactionIsCreating || widget.sendViewModel.state is TransactionCommitting, - isDisabled: - false // FIXME !(syncStore.status is SyncedSyncStatus), - ); + isDisabled: !widget.sendViewModel.isReadyForSend); }), ), ); @@ -482,47 +426,42 @@ class SendFormState extends State { return; } -// reaction((_) => widget.sendViewModel.fiatAmount, (String amount) { -// if (amount != _fiatAmountController.text) { -// _fiatAmountController.text = amount; -// } -// }); -// -// reaction((_) => widget.sendViewModel.cryptoAmount, (String amount) { -// if (amount != _cryptoAmountController.text) { -// _cryptoAmountController.text = amount; -// } -// }); -// -// reaction((_) => widget.sendViewModel.address, (String address) { -// if (address != _addressController.text) { -// _addressController.text = address; -// } -// }); -// -// _addressController.addListener(() { -// final address = _addressController.text; -// -// if (widget.sendViewModel.address != address) { -// widget.sendViewModel.changeAddress(address); -// } -// }); + reaction((_) => widget.sendViewModel.all, (bool all) { + if (all) { + _cryptoAmountController.text = S.current.all; + _fiatAmountController.text = null; + } + }); -// _fiatAmountController.addListener(() { -// final fiatAmount = _fiatAmountController.text; -// -// if (sendStore.fiatAmount != fiatAmount) { -// sendStore.changeFiatAmount(fiatAmount); -// } -// }); + reaction((_) => widget.sendViewModel.fiatAmount, (String amount) { + if (amount != _fiatAmountController.text) { + _fiatAmountController.text = amount; + } + }); -// _cryptoAmountController.addListener(() { -// final cryptoAmount = _cryptoAmountController.text; -// -// if (sendStore.cryptoAmount != cryptoAmount) { -// sendStore.changeCryptoAmount(cryptoAmount); -// } -// }); + reaction((_) => widget.sendViewModel.cryptoAmount, (String amount) { + if (widget.sendViewModel.all && amount != S.current.all) { + widget.sendViewModel.all = false; + } + + if (amount != _cryptoAmountController.text) { + _cryptoAmountController.text = amount; + } + }); + + reaction((_) => widget.sendViewModel.address, (String address) { + if (address != _addressController.text) { + _addressController.text = address; + } + }); + + _addressController.addListener(() { + final address = _addressController.text; + + if (widget.sendViewModel.address != address) { + widget.sendViewModel.address = address; + } + }); reaction((_) => widget.sendViewModel.state, (SendViewModelState state) { if (state is SendingFailed) { @@ -540,30 +479,117 @@ class SendFormState extends State { } if (state is TransactionCreatedSuccessfully) { -// WidgetsBinding.instance.addPostFrameCallback((_) { -// showDialog( -// context: context, -// builder: (BuildContext context) { -// return ConfirmSendingAlert( -// alertTitle: S.of(context).confirm_sending, -// amount: S.of(context).send_amount, -// amountValue: sendStore.pendingTransaction.amount, -// fee: S.of(context).send_fee, -// feeValue: sendStore.pendingTransaction.fee, -// leftButtonText: S.of(context).ok, -// rightButtonText: S.of(context).cancel, -// actionLeftButton: () { -// Navigator.of(context).pop(); -// sendStore.commitTransaction(); -// showDialog( -// context: context, -// builder: (BuildContext context) { -// return SendingAlert(sendStore: sendStore); -// }); -// }, -// actionRightButton: () => Navigator.of(context).pop()); -// }); -// }); + WidgetsBinding.instance.addPostFrameCallback((_) { + showDialog( + context: context, + builder: (BuildContext context) { + return ConfirmSendingAlert( + alertTitle: S.of(context).confirm_sending, + amount: S.of(context).send_amount, + amountValue: + widget.sendViewModel.pendingTransaction.amountFormatted, + fee: S.of(context).send_fee, + feeValue: + widget.sendViewModel.pendingTransaction.feeFormatted, + leftButtonText: S.of(context).ok, + rightButtonText: S.of(context).cancel, + actionLeftButton: () { + Navigator.of(context).pop(); + widget.sendViewModel.commitTransaction(); + showDialog( + context: context, + builder: (BuildContext context) { + return Observer(builder: (_) { + final state = widget.sendViewModel.state; + + if (state is TransactionCommitted) { + return Stack( + children: [ + Container( + color: Theme.of(context).backgroundColor, + child: Center( + child: Image.asset( + 'assets/images/birthday_cake.png'), + ), + ), + Center( + child: Padding( + padding: EdgeInsets.only( + top: 220, left: 24, right: 24), + child: Text( + S.of(context).send_success, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Theme.of(context) + .primaryTextTheme + .title + .color, + decoration: TextDecoration.none, + ), + ), + ), + ), + Positioned( + left: 24, + right: 24, + bottom: 24, + child: PrimaryButton( + onPressed: () => + Navigator.of(context).pop(), + text: S.of(context).send_got_it, + color: Colors.blue, + textColor: Colors.white)) + ], + ); + } + + return Stack( + children: [ + Container( + color: Theme.of(context).backgroundColor, + child: Center( + child: Image.asset( + 'assets/images/birthday_cake.png'), + ), + ), + BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 3.0, sigmaY: 3.0), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context) + .backgroundColor + .withOpacity(0.25)), + child: Center( + child: Padding( + padding: EdgeInsets.only(top: 220), + child: Text( + S.of(context).send_sending, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Theme.of(context) + .primaryTextTheme + .title + .color, + decoration: TextDecoration.none, + ), + ), + ), + ), + ), + ) + ], + ); + }); + }); + }, + actionRightButton: () => Navigator.of(context).pop()); + }); + }); } if (state is TransactionCommitted) { diff --git a/lib/src/screens/send/widgets/sending_alert.dart b/lib/src/screens/send/widgets/sending_alert.dart index db6803ead..08848fb6f 100644 --- a/lib/src/screens/send/widgets/sending_alert.dart +++ b/lib/src/screens/send/widgets/sending_alert.dart @@ -1,6 +1,6 @@ import 'dart:ui'; -import 'package:cake_wallet/src/stores/send/sending_state.dart'; import 'package:flutter/material.dart'; +import 'package:cake_wallet/src/stores/send/sending_state.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/stores/send/send_store.dart'; import 'package:cake_wallet/generated/i18n.dart'; diff --git a/lib/src/screens/settings/settings.dart b/lib/src/screens/settings/settings.dart index c1f4cfabe..e35508d81 100644 --- a/lib/src/screens/settings/settings.dart +++ b/lib/src/screens/settings/settings.dart @@ -39,9 +39,11 @@ class SettingsPage extends BasePage { if (item is PickerListItem) { return Observer(builder: (_) { return SettingsPickerCell( - title: item.title, - selectedItem: item.selectedItem(), - items: item.items); + title: item.title, + selectedItem: item.selectedItem(), + items: item.items, + onItemSelected: (dynamic value) => item.onItemSelected(value), + ); }); } diff --git a/lib/src/screens/settings/widgets/settings_picker_cell.dart b/lib/src/screens/settings/widgets/settings_picker_cell.dart index a94bdfe0d..1fc574b7a 100644 --- a/lib/src/screens/settings/widgets/settings_picker_cell.dart +++ b/lib/src/screens/settings/widgets/settings_picker_cell.dart @@ -4,25 +4,30 @@ import 'package:cake_wallet/src/widgets/standard_list.dart'; import 'package:cake_wallet/generated/i18n.dart'; class SettingsPickerCell extends StandardListRow { - SettingsPickerCell({@required String title, this.selectedItem, this.items}) + SettingsPickerCell( + {@required String title, + this.selectedItem, + this.items, + this.onItemSelected}) : super( - title: title, - isSelected: false, - onTap: (BuildContext context) async { - final selectedAtIndex = items.indexOf(selectedItem); + title: title, + isSelected: false, + onTap: (BuildContext context) async { + final selectedAtIndex = items.indexOf(selectedItem); - await showDialog( - context: context, - builder: (_) => Picker( - items: items, - selectedAtIndex: selectedAtIndex, - title: S.current.please_select, - mainAxisAlignment: MainAxisAlignment.center, - onItemSelected: (Object _) {})); - }); + await showDialog( + context: context, + builder: (_) => Picker( + items: items, + selectedAtIndex: selectedAtIndex, + title: S.current.please_select, + mainAxisAlignment: MainAxisAlignment.center, + onItemSelected: (ItemType item) => onItemSelected?.call(item))); + }); final ItemType selectedItem; final List items; + final void Function(ItemType item) onItemSelected; @override Widget buildTrailing(BuildContext context) { @@ -35,4 +40,4 @@ class SettingsPickerCell extends StandardListRow { color: Theme.of(context).primaryTextTheme.caption.color), ); } -} \ No newline at end of file +} diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 91e658fdd..7db75d309 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -42,12 +42,6 @@ abstract class SettingsStoreBase with Store { languageCode = initialLanguageCode; currentLocale = initialCurrentLocale; itemHeaders = {}; - -// actionlistDisplayMode.observe( -// (dynamic _) => _sharedPreferences.setInt(displayActionListModeKey, -// serializeActionlistDisplayModes(actionlistDisplayMode)), -// fireImmediately: false); - _sharedPreferences = sharedPreferences; _nodeSource = nodeSource; } @@ -120,7 +114,7 @@ abstract class SettingsStoreBase with Store { sharedPreferences.getBool(shouldSaveRecipientAddressKey); final allowBiometricalAuthentication = sharedPreferences.getBool(allowBiometricalAuthenticationKey) ?? false; - final savedDarkTheme = sharedPreferences.getBool(currentDarkTheme) ?? false; + final savedDarkTheme = sharedPreferences.getBool(currentDarkTheme) ?? true; final actionListDisplayMode = ObservableList(); actionListDisplayMode.addAll(deserializeActionlistDisplayModes( sharedPreferences.getInt(displayActionListModeKey) ?? diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 62f7a44ef..83a5a7275 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -35,9 +35,11 @@ abstract class BalanceViewModelBase with Store { if (_wallet is BitcoinWallet) { return WalletBalance( - unlockedBalance: _wallet.balance.confirmedFormatted, - totalBalance: _wallet.balance.unconfirmedFormatted); + unlockedBalance: _wallet.balance.totalFormatted, + totalBalance: _wallet.balance.totalFormatted); } + + return null; } String _getFiatBalance({double price, String cryptoAmount}) { diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 64530b655..7b79a772c 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -29,24 +29,24 @@ part 'dashboard_view_model.g.dart'; class DashboardViewModel = DashboardViewModelBase with _$DashboardViewModel; abstract class DashboardViewModelBase with Store { - DashboardViewModelBase({ - this.balanceViewModel, - this.appStore, - this.tradesStore, - this.tradeFilterStore, - this.transactionFilterStore, - this.pageViewStore}) { - + DashboardViewModelBase( + {this.balanceViewModel, + this.appStore, + this.tradesStore, + this.tradeFilterStore, + this.transactionFilterStore, + this.pageViewStore}) { name = appStore.wallet?.name; wallet ??= appStore.wallet; type = wallet.type; - transactions = ObservableList.of(wallet.transactionHistory.transactions + transactions = ObservableList.of(wallet + .transactionHistory.transactions.values .map((transaction) => TransactionListItem( - transaction: transaction, - price: price, - fiatCurrency: appStore.settingsStore.fiatCurrency, - displayMode: balanceDisplayMode))); + transaction: transaction, + price: price, + fiatCurrency: appStore.settingsStore.fiatCurrency, + displayMode: balanceDisplayMode))); _reaction = reaction((_) => appStore.wallet, _onWalletChange); @@ -83,15 +83,11 @@ abstract class DashboardViewModelBase with Store { var statusText = ''; if (status is SyncingSyncStatus) { - statusText = S.current - .Blocks_remaining( - status.toString()); + statusText = S.current.Blocks_remaining(status.toString()); } if (status is FailedSyncStatus) { - statusText = S - .current - .please_try_to_connect_to_another_node; + statusText = S.current.please_try_to_connect_to_another_node; } return statusText; @@ -111,8 +107,7 @@ abstract class DashboardViewModelBase with Store { List get items { final _items = []; - _items - .addAll(transactionFilterStore.filtered(transactions: transactions)); + _items.addAll(transactionFilterStore.filtered(transactions: transactions)); _items.addAll(tradeFilterStore.filtered(trades: trades)); return formattedItemsList(_items); @@ -137,11 +132,11 @@ abstract class DashboardViewModelBase with Store { void _onWalletChange(WalletBase wallet) { name = wallet.name; transactions.clear(); - transactions.addAll(wallet.transactionHistory.transactions - .map((transaction) => TransactionListItem( - transaction: transaction, - price: price, - fiatCurrency: appStore.settingsStore.fiatCurrency, - displayMode: balanceDisplayMode))); + transactions.addAll(wallet.transactionHistory.transactions.values.map( + (transaction) => TransactionListItem( + transaction: transaction, + price: price, + fiatCurrency: appStore.settingsStore.fiatCurrency, + displayMode: balanceDisplayMode))); } } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart new file mode 100644 index 000000000..0a128dc0a --- /dev/null +++ b/lib/view_model/send/send_view_model.dart @@ -0,0 +1,171 @@ +import 'package:cake_wallet/src/domain/common/calculate_fiat_amount.dart'; +import 'package:cake_wallet/store/dashboard/fiat_convertation_store.dart'; +import 'package:intl/intl.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/core/address_validator.dart'; +import 'package:cake_wallet/core/amount_validator.dart'; +import 'package:cake_wallet/core/pending_transaction.dart'; +import 'package:cake_wallet/core/validator.dart'; +import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; +import 'package:cake_wallet/monero/monero_wallet.dart'; +import 'package:cake_wallet/src/domain/common/sync_status.dart'; +import 'package:cake_wallet/src/domain/common/crypto_currency.dart'; +import 'package:cake_wallet/src/domain/common/fiat_currency.dart'; +import 'package:cake_wallet/src/domain/common/transaction_priority.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_transaction_credentials.dart'; + +part 'send_view_model.g.dart'; + +class SendViewModel = SendViewModelBase with _$SendViewModel; + +abstract class SendViewModelBase with Store { + SendViewModelBase( + this._wallet, this._settingsStore, this._fiatConversationStore) + : state = InitialSendViewModelState(), + _cryptoNumberFormat = NumberFormat()..maximumFractionDigits = 12, + all = false; + + @observable + SendViewModelState state; + + @observable + String fiatAmount; + + @observable + String cryptoAmount; + + @observable + String address; + + @observable + bool all; + + FiatCurrency get fiat => _settingsStore.fiatCurrency; + + TransactionPriority get transactionPriority => + _settingsStore.transactionPriority; + + double get estimatedFee => + _wallet.calculateEstimatedFee(_settingsStore.transactionPriority); + + CryptoCurrency get currency => _wallet.currency; + + Validator get amountValidator => AmountValidator(type: _wallet.type); + + Validator get addressValidator => AddressValidator(type: _wallet.currency); + + PendingTransaction pendingTransaction; + + @computed + String get balance { + if (_wallet is MoneroWallet) { + _wallet.balance.formattedUnlockedBalance; + } + + if (_wallet is BitcoinWallet) { + _wallet.balance.confirmedFormatted; + } + + return '0.0'; + } + + @computed + bool get isReadyForSend => _wallet.syncStatus is SyncedSyncStatus; + + final WalletBase _wallet; + final SettingsStore _settingsStore; + final FiatConvertationStore _fiatConversationStore; + NumberFormat _cryptoNumberFormat; + + @action + void setAll() => all = true; + + @action + void reset() { + cryptoAmount = ''; + fiatAmount = ''; + address = ''; + } + + @action + Future createTransaction() async { + try { + state = TransactionIsCreating(); + pendingTransaction = await _wallet.createTransaction(_credentials()); + state = TransactionCreatedSuccessfully(); + } catch (e) { + state = SendingFailed(error: e.toString()); + } + } + + @action + Future commitTransaction() async { + try { + state = TransactionCommitting(); + await pendingTransaction.commit(); + state = TransactionCommitted(); + } catch (e) { + state = SendingFailed(error: e.toString()); + } + } + + @action + void setCryptoAmount(String amount) { + cryptoAmount = amount; + _updateFiatAmount(); + } + + @action + void setFiatAmount(String amount) { + fiatAmount = amount; + _updateCryptoAmount(); + } + + @action + void _updateFiatAmount() { + try { + final fiat = calculateFiatAmount( + price: _fiatConversationStore.price, cryptoAmount: cryptoAmount); + if (fiatAmount != fiat) { + fiatAmount = fiat; + } + } catch (_) { + fiatAmount = ''; + } + } + + @action + void _updateCryptoAmount() { + try { + final crypto = double.parse(fiatAmount) / _fiatConversationStore.price; + final cryptoAmountTmp = _cryptoNumberFormat.format(crypto); + + if (cryptoAmount != cryptoAmountTmp) { + cryptoAmount = cryptoAmountTmp; + } + } catch (e) { + cryptoAmount = ''; + } + } + + Object _credentials() { + final amount = + !all ? double.parse(cryptoAmount.replaceAll(',', '.')) : null; + + switch (_wallet.type) { + case WalletType.bitcoin: + return BitcoinTransactionCredentials( + address, amount, _settingsStore.transactionPriority); + case WalletType.monero: + // FIXME: Wrong credentials + return BitcoinTransactionCredentials( + address, amount, _settingsStore.transactionPriority); + default: + return null; + } + } +} diff --git a/lib/view_model/send/send_view_model_state.dart b/lib/view_model/send/send_view_model_state.dart new file mode 100644 index 000000000..b43e95378 --- /dev/null +++ b/lib/view_model/send/send_view_model_state.dart @@ -0,0 +1,18 @@ +import 'package:flutter/foundation.dart'; + +abstract class SendViewModelState {} + +class InitialSendViewModelState extends SendViewModelState {} + +class TransactionIsCreating extends SendViewModelState {} +class TransactionCreatedSuccessfully extends SendViewModelState {} + +class TransactionCommitting extends SendViewModelState {} + +class TransactionCommitted extends SendViewModelState {} + +class SendingFailed extends SendViewModelState { + SendingFailed({@required this.error}); + + String error; +} \ No newline at end of file diff --git a/lib/view_model/send_view_model.dart b/lib/view_model/send_view_model.dart deleted file mode 100644 index eada27683..000000000 --- a/lib/view_model/send_view_model.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'package:cake_wallet/core/address_validator.dart'; -import 'package:cake_wallet/core/amount_validator.dart'; -import 'package:cake_wallet/core/validator.dart'; -import 'package:cake_wallet/core/wallet_base.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; -import 'package:cake_wallet/monero/monero_wallet.dart'; -import 'package:cake_wallet/src/domain/common/balance.dart'; -import 'package:cake_wallet/src/domain/common/calculate_estimated_fee.dart'; -import 'package:cake_wallet/src/domain/common/crypto_currency.dart'; -import 'package:cake_wallet/src/domain/common/fiat_currency.dart'; -import 'package:cake_wallet/src/domain/common/transaction_priority.dart'; -import 'package:cake_wallet/store/settings_store.dart'; -import 'package:flutter/foundation.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/monero/monero_wallet_service.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_wallet_creation_credentials.dart'; -import 'package:cake_wallet/core/wallet_creation_service.dart'; -import 'package:cake_wallet/core/wallet_credentials.dart'; -import 'package:cake_wallet/src/domain/common/wallet_type.dart'; -import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; - -part 'send_view_model.g.dart'; - -abstract class SendViewModelState {} - -class InitialSendViewModelState extends SendViewModelState {} - -class TransactionIsCreating extends SendViewModelState {} - -class TransactionCreatedSuccessfully extends SendViewModelState {} - -class TransactionCommitting extends SendViewModelState {} - -class TransactionCommitted extends SendViewModelState {} - -class SendingFailed extends SendViewModelState { - SendingFailed({@required this.error}); - - String error; -} - -class SendViewModel = SendViewModelBase with _$SendViewModel; - -abstract class SendViewModelBase with Store { - SendViewModelBase(this._wallet, this._settingsStore) - : state = InitialSendViewModelState(); - - @observable - SendViewModelState state; - - @observable - String fiatAmount; - - @observable - String cryptoAmount; - - @observable - String address; - - FiatCurrency get fiat => _settingsStore.fiatCurrency; - - TransactionPriority get transactionPriority => - _settingsStore.transactionPriority; - - double get estimatedFee => - calculateEstimatedFee(priority: transactionPriority); - - CryptoCurrency get currency => _wallet.currency; - - Validator get amountValidator => AmountValidator(type: _wallet.type); - - Validator get addressValidator => AddressValidator(type: _wallet.currency); - - @computed - String get balance { - if (_wallet is MoneroWallet) { - _wallet.balance.formattedUnlockedBalance; - } - - if (_wallet is BitcoinWallet) { - _wallet.balance.confirmedFormatted; - } - - return '0.0'; - } - - WalletBase _wallet; - - SettingsStore _settingsStore; - - Future createTransaction() async {} - - Future commitTransaction() async {} -} diff --git a/lib/view_model/settings/picker_list_item.dart b/lib/view_model/settings/picker_list_item.dart index 532f90a0d..1dfc9c06d 100644 --- a/lib/view_model/settings/picker_list_item.dart +++ b/lib/view_model/settings/picker_list_item.dart @@ -4,10 +4,19 @@ import 'package:cake_wallet/view_model/settings/settings_list_item.dart'; class PickerListItem extends SettingsListItem { PickerListItem( {@required String title, - @required this.selectedItem, - @required this.items}) - : super(title); + @required this.selectedItem, + @required this.items, + void Function(ItemType item) onItemSelected}) + : _onItemSelected = onItemSelected, + super(title); final ItemType Function() selectedItem; final List items; + final void Function(ItemType item) _onItemSelected; + + void onItemSelected(dynamic item) { + if (item is ItemType) { + _onItemSelected?.call(item); + } + } } diff --git a/lib/view_model/settings/settings_view_model.dart b/lib/view_model/settings/settings_view_model.dart index 892b8493e..dbca8ef66 100644 --- a/lib/view_model/settings/settings_view_model.dart +++ b/lib/view_model/settings/settings_view_model.dart @@ -1,3 +1,5 @@ +import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; import 'package:flutter/cupertino.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/routes.dart'; @@ -20,7 +22,8 @@ part 'settings_view_model.g.dart'; class SettingsViewModel = SettingsViewModelBase with _$SettingsViewModel; abstract class SettingsViewModelBase with Store { - SettingsViewModelBase(this._settingsStore) : itemHeaders = {} { + SettingsViewModelBase(this._settingsStore, WalletBase wallet) + : itemHeaders = {} { sections = [ [ PickerListItem( @@ -33,8 +36,10 @@ abstract class SettingsViewModelBase with Store { selectedItem: () => fiatCurrency), PickerListItem( title: S.current.settings_fee_priority, - items: TransactionPriority.all, - selectedItem: () => transactionPriority), + items: _transactionPriorities(wallet.type), + selectedItem: () => transactionPriority, + onItemSelected: (TransactionPriority priority) => + _settingsStore.transactionPriority = priority), SwitcherListItem( title: S.current.settings_save_recipient_address, value: () => shouldSaveRecipientAddress, @@ -146,12 +151,9 @@ abstract class SettingsViewModelBase with Store { _settingsStore.allowBiometricalAuthentication = value; // @observable -// bool isDarkTheme; -// -// @observable -// int defaultPinLength; // @observable + final Map itemHeaders; List> sections; final SettingsStore _settingsStore; @@ -182,4 +184,24 @@ abstract class SettingsViewModelBase with Store { @action void _showTrades() => actionlistDisplayMode.add(ActionListDisplayMode.trades); + +// +// @observable +// int defaultPinLength; +// bool isDarkTheme; + + static List _transactionPriorities(WalletType type) { + switch (type) { + case WalletType.monero: + return TransactionPriority.all; + case WalletType.bitcoin: + return [ + TransactionPriority.slow, + TransactionPriority.regular, + TransactionPriority.fast + ]; + default: + return []; + } + } } From 6aaac93fa8501af3faff14450374bb3a4cc99930 Mon Sep 17 00:00:00 2001 From: M Date: Thu, 27 Aug 2020 19:54:34 +0300 Subject: [PATCH 2/9] TMP 0 --- assets/electrum_server_list.yml | 4 +- ios/Podfile.lock | 4 +- ios/Runner.xcodeproj/project.pbxproj | 6 +- ios/Runner/Info.plist | 2 + lib/bitcoin/bitcoin_wallet.dart | 9 +-- lib/bitcoin/bitcoin_wallet_service.dart | 7 +- lib/bitcoin/electrum.dart | 12 ++- lib/di.dart | 26 ++++-- lib/main.dart | 4 +- lib/reactions/bootstrap.dart | 29 +++---- .../common/default_settings_migration.dart | 59 +++++++++++--- lib/src/domain/common/node.dart | 8 +- lib/src/domain/common/node_list.dart | 13 ++- lib/src/screens/dashboard/dashboard_page.dart | 5 +- lib/src/screens/dashboard/wallet_menu.dart | 14 ++-- .../dashboard/widgets/menu_widget.dart | 30 +++---- lib/src/screens/nodes/nodes_list_page.dart | 42 ++++++++-- .../screens/wallet_list/wallet_list_page.dart | 8 -- lib/src/screens/wallet_list/wallet_menu.dart | 2 +- lib/src/stores/settings/settings_store.dart | 6 +- lib/store/settings_store.dart | 40 ++++++++-- lib/utils/mobx.dart | 45 +++++++++++ .../dashboard/dashboard_view_model.dart | 5 ++ .../node_list/node_list_view_model.dart | 80 +++++++++++++++++-- lib/view_model/send/send_view_model.dart | 9 ++- .../settings/settings_view_model.dart | 12 ++- .../wallet_list/wallet_list_item.dart | 3 +- .../wallet_list/wallet_list_view_model.dart | 23 ++++-- 28 files changed, 364 insertions(+), 143 deletions(-) create mode 100644 lib/utils/mobx.dart diff --git a/assets/electrum_server_list.yml b/assets/electrum_server_list.yml index 4baf56aec..0331f42ea 100644 --- a/assets/electrum_server_list.yml +++ b/assets/electrum_server_list.yml @@ -1,2 +1,4 @@ - - uri: electrum2.hodlister.co:50002 \ No newline at end of file + uri: electrum2.hodlister.co:50002 +- + uri: bitcoin.electrumx.multicoin.co:50002 \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 959c597e1..641750308 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -61,7 +61,7 @@ DEPENDENCIES: - cw_monero (from `.symlinks/plugins/cw_monero/ios`) - devicelocale (from `.symlinks/plugins/devicelocale/ios`) - esys_flutter_share (from `.symlinks/plugins/esys_flutter_share/ios`) - - Flutter (from `.symlinks/flutter/ios`) + - Flutter (from `.symlinks/flutter/ios-release`) - flutter_plugin_android_lifecycle (from `.symlinks/plugins/flutter_plugin_android_lifecycle/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - local_auth (from `.symlinks/plugins/local_auth/ios`) @@ -92,7 +92,7 @@ EXTERNAL SOURCES: esys_flutter_share: :path: ".symlinks/plugins/esys_flutter_share/ios" Flutter: - :path: ".symlinks/flutter/ios" + :path: ".symlinks/flutter/ios-release" flutter_plugin_android_lifecycle: :path: ".symlinks/plugins/flutter_plugin_android_lifecycle/ios" flutter_secure_storage: diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 34c4e48a4..987e086fc 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -373,7 +373,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -509,7 +509,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -540,7 +540,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 0b40211a4..65000c894 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -22,6 +22,8 @@ $(CURRENT_PROJECT_VERSION) LSRequiresIPhoneOS + NSCameraUsageDescription + Cake Wallet requires access to your phone’s camera. UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/lib/bitcoin/bitcoin_wallet.dart b/lib/bitcoin/bitcoin_wallet.dart index b83e95ef1..4f67fa59f 100644 --- a/lib/bitcoin/bitcoin_wallet.dart +++ b/lib/bitcoin/bitcoin_wallet.dart @@ -200,7 +200,8 @@ abstract class BitcoinWalletBase extends WalletBase with Store { Future startSync() async { try { syncStatus = StartingSyncStatus(); - transactionHistory.updateAsync(onFinished: () => print('finished!')); + transactionHistory.updateAsync( + onFinished: () => print('transactionHistory update finished!')); _subscribeForUpdates(); await _updateBalance(); syncStatus = SyncedSyncStatus(); @@ -215,11 +216,7 @@ abstract class BitcoinWalletBase extends WalletBase with Store { Future connectToNode({@required Node node}) async { try { syncStatus = ConnectingSyncStatus(); - // electrum2.hodlister.co - // bitcoin.electrumx.multicoin.co:50002 - // electrum2.taborsky.cz:5002 - await eclient.connect( - host: 'bitcoin.electrumx.multicoin.co', port: 50002); + await eclient.connectToUri(node.uri); syncStatus = ConnectedSyncStatus(); } catch (e) { print(e.toString()); diff --git a/lib/bitcoin/bitcoin_wallet_service.dart b/lib/bitcoin/bitcoin_wallet_service.dart index d5827df18..e18692968 100644 --- a/lib/bitcoin/bitcoin_wallet_service.dart +++ b/lib/bitcoin/bitcoin_wallet_service.dart @@ -49,10 +49,9 @@ class BitcoinWalletService extends WalletService< } @override - Future remove(String wallet) { - // TODO: implement remove - throw UnimplementedError(); - } + Future remove(String wallet) async => + File(await pathForWalletDir(name: wallet, type: WalletType.bitcoin)) + .delete(recursive: true); @override Future restoreFromKeys( diff --git a/lib/bitcoin/electrum.dart b/lib/bitcoin/electrum.dart index f24f6611f..d92c69405 100644 --- a/lib/bitcoin/electrum.dart +++ b/lib/bitcoin/electrum.dart @@ -37,11 +37,15 @@ class ElectrumClient { bool _isConnected; Timer _aliveTimer; - Future connect({@required String host, @required int port}) async { - if (socket != null) { - await socket.close(); - } + Future connectToUri(String uri) async { + final _uri = Uri.parse(uri); + final host = _uri.scheme; + final port = int.parse(_uri.path); + await connect(host: host, port: port); + } + Future connect({@required String host, @required int port}) async { + await socket?.close(); final start = DateTime.now(); socket = await SecureSocket.connect(host, port, timeout: connectionTimeout); diff --git a/lib/di.dart b/lib/di.dart index 4c4c038a7..d7f425edf 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -25,6 +25,7 @@ import 'package:cake_wallet/src/screens/send/send_page.dart'; import 'package:cake_wallet/src/screens/subaddress/address_edit_or_create_page.dart'; import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart'; import 'package:cake_wallet/store/wallet_list_store.dart'; +import 'package:cake_wallet/utils/mobx.dart'; import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart'; import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart'; @@ -74,8 +75,20 @@ NodeListStore setupNodeListStore(Box nodeSource) { _nodeListStore = NodeListStore(); _nodeListStore.replaceValues(nodeSource.values); _onNodesSourceChange = nodeSource.watch(); - _onNodesSourceChange - .listen((_) => _nodeListStore.replaceValues(nodeSource.values)); + _onNodesSourceChange.listen((event) { +// print(event); + + if (event.deleted) { + _nodeListStore.nodes.removeWhere((n) { + return n.key != null ? n.key == event.key : true; + }); + } + + if (event.value is Node) { + final val = event.value as Node; + _nodeListStore.nodes.add(val); + } + }); return _nodeListStore; } @@ -274,10 +287,11 @@ Future setup( getIt.registerFactoryParam((Contact contact, _) => ContactPage(getIt.get(param1: contact))); - getIt.registerFactory(() => NodeListViewModel( - getIt.get().nodeListStore, - nodeSource, - getIt.get().wallet)); + getIt.registerFactory(() { + final appStore = getIt.get(); + return NodeListViewModel(appStore.nodeListStore, nodeSource, + appStore.wallet, appStore.settingsStore); + }); getIt.registerFactory(() => NodeListPage(getIt.get())); diff --git a/lib/main.dart b/lib/main.dart index d1be6866d..6e69f4410 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -127,7 +127,7 @@ void main() async { contactSource: contacts, tradesSource: trades, fiatConvertationService: fiatConvertationService, - initialMigrationVersion: 3); + initialMigrationVersion: 4); setReactions( settingsStore: settingsStore, @@ -169,7 +169,7 @@ Future initialSetup( @required Box contactSource, @required Box tradesSource, @required FiatConvertationService fiatConvertationService, - int initialMigrationVersion = 3}) async { + int initialMigrationVersion = 4}) async { await defaultSettingsMigration( version: initialMigrationVersion, sharedPreferences: sharedPreferences, diff --git a/lib/reactions/bootstrap.dart b/lib/reactions/bootstrap.dart index b190697c5..76990eb55 100644 --- a/lib/reactions/bootstrap.dart +++ b/lib/reactions/bootstrap.dart @@ -50,7 +50,8 @@ ReactionDisposer _onCurrentWalletChangeReaction; ReactionDisposer _onWalletSyncStatusChangeReaction; ReactionDisposer _onCurrentFiatCurrencyChangeDisposer; -Future bootstrap({FiatConvertationService fiatConvertationService}) async { +Future bootstrap( + {FiatConvertationService fiatConvertationService}) async { final authenticationStore = getIt.get(); final settingsStore = getIt.get(); final fiatConvertationStore = getIt.get(); @@ -72,12 +73,10 @@ Future bootstrap({FiatConvertationService fiatConvertationService}) async _onCurrentWalletChangeReaction ??= reaction((_) => getIt.get().wallet, (WalletBase wallet) async { - print('Wallet name ${wallet.name}'); - _onWalletSyncStatusChangeReaction?.reaction?.dispose(); - _onWalletSyncStatusChangeReaction = when( + _onWalletSyncStatusChangeReaction = reaction( (_) => wallet.syncStatus is ConnectedSyncStatus, - () async => await wallet.startSync()); + (_) async => await wallet.startSync()); await getIt .get() @@ -87,30 +86,24 @@ Future bootstrap({FiatConvertationService fiatConvertationService}) async .get() .setInt('current_wallet_type', serializeToInt(wallet.type)); - await wallet.connectToNode(node: null); - + final node = settingsStore.getCurrentNode(wallet.type); final cryptoCurrency = wallet.currency; final fiatCurrency = settingsStore.fiatCurrency; + await wallet.connectToNode(node: node); + final price = await fiatConvertationService.getPrice( - crypto: cryptoCurrency, - fiat: fiatCurrency - ); + crypto: cryptoCurrency, fiat: fiatCurrency); fiatConvertationStore.setPrice(price); }); - // - - _onCurrentFiatCurrencyChangeDisposer ??= - reaction((_) => settingsStore.fiatCurrency, - (FiatCurrency fiatCurrency) async { + _onCurrentFiatCurrencyChangeDisposer ??= reaction( + (_) => settingsStore.fiatCurrency, (FiatCurrency fiatCurrency) async { final cryptoCurrency = getIt.get().wallet.currency; final price = await fiatConvertationService.getPrice( - crypto: cryptoCurrency, - fiat: fiatCurrency - ); + crypto: cryptoCurrency, fiat: fiatCurrency); fiatConvertationStore.setPrice(price); }); diff --git a/lib/src/domain/common/default_settings_migration.dart b/lib/src/domain/common/default_settings_migration.dart index d89223a4d..4ad41dff2 100644 --- a/lib/src/domain/common/default_settings_migration.dart +++ b/lib/src/domain/common/default_settings_migration.dart @@ -29,15 +29,19 @@ Future defaultSettingsMigration( switch (version) { case 1: await sharedPreferences.setString( - SettingsStoreBase.currentFiatCurrencyKey, FiatCurrency.usd.toString()); + SettingsStoreBase.currentFiatCurrencyKey, + FiatCurrency.usd.toString()); await sharedPreferences.setInt( - SettingsStoreBase.currentTransactionPriorityKey, TransactionPriority.standart.raw); + SettingsStoreBase.currentTransactionPriorityKey, + TransactionPriority.standart.raw); await sharedPreferences.setInt( SettingsStoreBase.currentBalanceDisplayModeKey, BalanceDisplayMode.availableBalance.raw); await sharedPreferences.setBool('save_recipient_address', true); await resetToDefault(nodes); - await changeCurrentNodeToDefault( + await changeMoneroCurrentNodeToDefault( + sharedPreferences: sharedPreferences, nodes: nodes); + await changeBitcoinCurrentElectrumServerToDefault( sharedPreferences: sharedPreferences, nodes: nodes); break; @@ -50,6 +54,11 @@ Future defaultSettingsMigration( case 3: await updateNodeTypes(nodes: nodes); await addBitcoinElectrumServerList(nodes: nodes); + + break; + case 4: + await changeBitcoinCurrentElectrumServerToDefault( + sharedPreferences: sharedPreferences, nodes: nodes); break; default: break; @@ -69,10 +78,11 @@ Future defaultSettingsMigration( Future replaceNodesMigration({@required Box nodes}) async { final replaceNodes = { 'eu-node.cakewallet.io:18081': - Node(uri: 'xmr-node-eu.cakewallet.com:18081'), - 'node.cakewallet.io:18081': - Node(uri: 'xmr-node-usa-east.cakewallet.com:18081'), - 'node.xmr.ru:13666': Node(uri: 'node.monero.net:18081') + Node(uri: 'xmr-node-eu.cakewallet.com:18081', type: WalletType.monero), + 'node.cakewallet.io:18081': Node( + uri: 'xmr-node-usa-east.cakewallet.com:18081', type: WalletType.monero), + 'node.xmr.ru:13666': + Node(uri: 'node.monero.net:18081', type: WalletType.monero) }; nodes.values.forEach((Node node) async { @@ -87,11 +97,27 @@ Future replaceNodesMigration({@required Box nodes}) async { }); } -Future changeCurrentNodeToDefault( +Future changeMoneroCurrentNodeToDefault( {@required SharedPreferences sharedPreferences, @required Box nodes}) async { + final node = getMoneroDefaultNode(nodes: nodes); + final nodeId = node?.key as int ?? 0; // 0 - England + + await sharedPreferences.setInt('current_node_id', nodeId); +} + +Node getBitcoinDefaultElectrumServer({@required Box nodes}) { + final uri = 'bitcoin.electrumx.multicoin.co:50002'; + + return nodes.values + .firstWhere((Node node) => node.uri == uri, orElse: () => null) ?? + nodes.values.firstWhere((node) => node.type == WalletType.bitcoin, + orElse: () => null); +} + +Node getMoneroDefaultNode({@required Box nodes}) { final timeZone = DateTime.now().timeZoneOffset.inHours; - String nodeUri = ''; + var nodeUri = ''; if (timeZone >= 1) { // Eurasia @@ -101,11 +127,18 @@ Future changeCurrentNodeToDefault( nodeUri = 'xmr-node-usa-east.cakewallet.com:18081'; } - final node = nodes.values.firstWhere((Node node) => node.uri == nodeUri) ?? + return nodes.values + .firstWhere((Node node) => node.uri == nodeUri, orElse: () => null) ?? nodes.values.first; - final nodeId = node != null ? node.key as int : 0; // 0 - England +} - await sharedPreferences.setInt('current_node_id', nodeId); +Future changeBitcoinCurrentElectrumServerToDefault( + {@required SharedPreferences sharedPreferences, + @required Box nodes}) async { + final server = getBitcoinDefaultElectrumServer(nodes: nodes); + final serverId = server?.key as int ?? 0; + + await sharedPreferences.setInt('current_node_id_btc', serverId); } Future replaceDefaultNode( @@ -126,7 +159,7 @@ Future replaceDefaultNode( return; } - await changeCurrentNodeToDefault( + await changeMoneroCurrentNodeToDefault( sharedPreferences: sharedPreferences, nodes: nodes); } diff --git a/lib/src/domain/common/node.dart b/lib/src/domain/common/node.dart index f5b91547d..b47f002e9 100644 --- a/lib/src/domain/common/node.dart +++ b/lib/src/domain/common/node.dart @@ -9,12 +9,16 @@ part 'node.g.dart'; @HiveType(typeId: 1) class Node extends HiveObject { - Node({@required this.uri, @required WalletType type, this.login, this.password}) { + Node( + {@required this.uri, + @required WalletType type, + this.login, + this.password}) { this.type = type; } Node.fromMap(Map map) - : uri = (map['uri'] ?? '') as String, + : uri = map['uri'] as String ?? '', login = map['login'] as String, password = map['password'] as String, typeRaw = map['typeRaw'] as int; diff --git a/lib/src/domain/common/node_list.dart b/lib/src/domain/common/node_list.dart index ad5db8a5d..67bfe6eac 100644 --- a/lib/src/domain/common/node_list.dart +++ b/lib/src/domain/common/node_list.dart @@ -10,7 +10,10 @@ Future> loadDefaultNodes() async { return nodes.map((dynamic raw) { if (raw is Map) { - return Node.fromMap(raw); + final node = Node.fromMap(raw); + node?.type = WalletType.monero; + + return node; } return null; @@ -38,13 +41,7 @@ Future resetToDefault(Box nodeSource) async { final moneroNodes = await loadDefaultNodes(); final bitcoinElectrumServerList = await loadElectrumServerList(); final nodes = moneroNodes + bitcoinElectrumServerList; - final entities = {}; await nodeSource.clear(); - - for (var i = 0; i < nodes.length; i++) { - entities[i] = nodes[i]; - } - - await nodeSource.putAll(entities); + await nodeSource.addAll(nodes); } diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index 1d5eab711..7a81d66cc 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -50,10 +50,7 @@ class DashboardPage extends BasePage { padding: EdgeInsets.all(0), onPressed: () async { await showDialog( - builder: (_) => MenuWidget( - name: walletViewModel.name, - subname: walletViewModel.subname, - type: walletViewModel.type), + builder: (_) => MenuWidget(walletViewModel), context: context); }, child: menuButton diff --git a/lib/src/screens/dashboard/wallet_menu.dart b/lib/src/screens/dashboard/wallet_menu.dart index 2190a8d34..2de702bb9 100644 --- a/lib/src/screens/dashboard/wallet_menu.dart +++ b/lib/src/screens/dashboard/wallet_menu.dart @@ -6,8 +6,10 @@ import 'package:cake_wallet/src/stores/wallet/wallet_store.dart'; import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +// FIXME: terrible design + class WalletMenu { - WalletMenu(this.context); + WalletMenu(this.context, this.reconnect); final List items = [ S.current.reconnect, @@ -30,6 +32,7 @@ class WalletMenu { ]; final BuildContext context; + final Future Function() reconnect; void action(int index) { switch (index) { @@ -70,8 +73,6 @@ class WalletMenu { } Future _presentReconnectAlert(BuildContext context) async { - final walletStore = Provider.of(context); - await showDialog( context: context, builder: (BuildContext context) { @@ -80,12 +81,11 @@ class WalletMenu { alertContent: S.of(context).reconnect_alert_text, leftButtonText: S.of(context).ok, rightButtonText: S.of(context).cancel, - actionLeftButton: () { - walletStore.reconnect(); + actionLeftButton: () async { + await reconnect?.call(); Navigator.of(context).pop(); }, - actionRightButton: () => Navigator.of(context).pop() - ); + actionRightButton: () => Navigator.of(context).pop()); }); } } diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index fdd9a704c..fab768773 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -1,14 +1,15 @@ import 'dart:ui'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/src/domain/common/wallet_type.dart'; import 'package:cake_wallet/src/screens/dashboard/wallet_menu.dart'; -class MenuWidget extends StatefulWidget { - MenuWidget({this.type, this.name, this.subname}); +// FIXME: terrible design. - final WalletType type; - final String name; - final String subname; +class MenuWidget extends StatefulWidget { + MenuWidget(this.dashboardViewModel); + + final DashboardViewModel dashboardViewModel; @override MenuWidgetState createState() => MenuWidgetState(); @@ -65,8 +66,8 @@ class MenuWidgetState extends State { @override Widget build(BuildContext context) { - final walletMenu = WalletMenu(context); -// final walletStore = Provider.of(context); + final walletMenu = + WalletMenu(context, () async => widget.dashboardViewModel.reconnect()); final itemCount = walletMenu.items.length; return Row( @@ -118,19 +119,20 @@ class MenuWidgetState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ - _iconFor(type: widget.type), + _iconFor(type: widget.dashboardViewModel.type), SizedBox(width: 16), Expanded( child: Container( height: 40, child: Column( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: widget.subname != null - ? MainAxisAlignment.spaceBetween - : MainAxisAlignment.center, + mainAxisAlignment: + widget.dashboardViewModel.subname != null + ? MainAxisAlignment.spaceBetween + : MainAxisAlignment.center, children: [ Text( - widget.name, + widget.dashboardViewModel.name, style: TextStyle( color: Theme.of(context) .primaryTextTheme @@ -141,9 +143,9 @@ class MenuWidgetState extends State { fontSize: 20, fontWeight: FontWeight.bold), ), - if (widget.subname != null) + if (widget.dashboardViewModel.subname != null) Text( - widget.subname, + widget.dashboardViewModel.subname, style: TextStyle( color: Theme.of(context) .primaryTextTheme diff --git a/lib/src/screens/nodes/nodes_list_page.dart b/lib/src/screens/nodes/nodes_list_page.dart index 2777e25b7..2182c38e7 100644 --- a/lib/src/screens/nodes/nodes_list_page.dart +++ b/lib/src/screens/nodes/nodes_list_page.dart @@ -74,15 +74,41 @@ class NodeListPage extends BasePage { } final node = nodeListViewModel.nodes[index]; - final isSelected = index == 1; // FIXME: hardcoded value. final nodeListRow = NodeListRow( - title: node.uri, - isSelected: isSelected, - isAlive: node.requestNode(), - onTap: (_) {}); + title: node.value.uri, + isSelected: node.isSelected, + isAlive: node.value.requestNode(), + onTap: (_) async { + if (node.isSelected) { + return; + } + + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + content: Text( + S.of(context).change_current_node(node.value.uri), + textAlign: TextAlign.center, + ), + actions: [ + FlatButton( + onPressed: () => Navigator.pop(context), + child: Text(S.of(context).cancel)), + FlatButton( + onPressed: () async { + Navigator.of(context).pop(); + await nodeListViewModel + .setAsCurrent(node.value); + }, + child: Text(S.of(context).change)), + ], + ); + }); + }); final dismissibleRow = Dismissible( - key: Key('${node.key}'), + key: Key('${node.value.key}'), confirmDismiss: (direction) async { return await showDialog( context: context, @@ -99,7 +125,7 @@ class NodeListPage extends BasePage { }); }, onDismissed: (direction) async => - nodeListViewModel.delete(node), + nodeListViewModel.delete(node.value), direction: DismissDirection.endToStart, background: Container( padding: EdgeInsets.only(right: 10.0), @@ -120,7 +146,7 @@ class NodeListPage extends BasePage { )), child: nodeListRow); - return isSelected ? nodeListRow : dismissibleRow; + return node.isSelected ? nodeListRow : dismissibleRow; }, itemCounter: (int sectionIndex) { if (sectionIndex == 0) { diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index 6b8e047cf..a500b2d20 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -63,14 +63,6 @@ class WalletListBodyState extends State { itemBuilder: (__, index) { final wallet = widget.walletListViewModel.wallets[index]; final screenWidth = MediaQuery.of(context).size.width; -// String shortAddress = ''; - -// if (wallet.isCurrent) { -// shortAddress = wallet.address; -// shortAddress = shortAddress.replaceRange( -// 4, shortAddress.length - 4, '...'); -// } - final walletMenu = WalletMenu(context, widget.walletListViewModel); final items = walletMenu.generateItemsForWalletMenu(wallet.isCurrent); diff --git a/lib/src/screens/wallet_list/wallet_menu.dart b/lib/src/screens/wallet_list/wallet_menu.dart index b6654d0e8..a52eb8f8e 100644 --- a/lib/src/screens/wallet_list/wallet_menu.dart +++ b/lib/src/screens/wallet_list/wallet_menu.dart @@ -109,7 +109,7 @@ class WalletMenu { try { auth.changeProcessText( S.of(context).wallet_list_removing_wallet(wallet.name)); -// await _walletListStore.remove(wallet); + await walletListViewModel.remove(wallet); auth.close(); } catch (e) { auth.changeProcessText(S diff --git a/lib/src/stores/settings/settings_store.dart b/lib/src/stores/settings/settings_store.dart index b3fe22e5a..21b56a967 100644 --- a/lib/src/stores/settings/settings_store.dart +++ b/lib/src/stores/settings/settings_store.dart @@ -42,7 +42,7 @@ abstract class SettingsStoreBase with Store { _sharedPreferences = sharedPreferences; _nodes = nodes; allowBiometricalAuthentication = initialAllowBiometricalAuthentication; - isDarkTheme = initialDarkTheme; + isDarkTheme = true; defaultPinLength = initialPinLength; languageCode = initialLanguageCode; currentLocale = initialCurrentLocale; @@ -143,7 +143,7 @@ abstract class SettingsStoreBase with Store { bool allowBiometricalAuthentication; @observable - bool isDarkTheme; + bool isDarkTheme = true; @observable int defaultPinLength; @@ -285,7 +285,7 @@ abstract class SettingsStoreBase with Store { } Future setCurrentNodeToDefault() async { - await changeCurrentNodeToDefault(sharedPreferences: _sharedPreferences, nodes: _nodes); +// await changeCurrentNodeToDefault(sharedPreferences: _sharedPreferences, nodes: _nodes); await loadSettings(); } diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 7db75d309..0251fbce8 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; @@ -29,8 +30,9 @@ abstract class SettingsStoreBase with Store { @required int initialPinLength, @required String initialLanguageCode, @required String initialCurrentLocale, - @required this.node, +// @required this.node, @required this.appVersion, + @required Map nodes, this.actionlistDisplayMode}) { fiatCurrency = initialFiatCurrency; transactionPriority = initialTransactionPriority; @@ -44,9 +46,11 @@ abstract class SettingsStoreBase with Store { itemHeaders = {}; _sharedPreferences = sharedPreferences; _nodeSource = nodeSource; + _nodes = nodes; } static const currentNodeIdKey = 'current_node_id'; + static const currentBitcoinElectrumSererIdKey = 'current_node_id_btc'; static const currentFiatCurrencyKey = 'current_fiat_currency'; static const currentTransactionPriorityKey = 'current_fee_priority'; static const currentBalanceDisplayModeKey = 'current_balance_display_mode'; @@ -58,8 +62,8 @@ abstract class SettingsStoreBase with Store { static const currentPinLength = 'current_pin_length'; static const currentLanguageCode = 'language_code'; - @observable - Node node; +// @observable +// Node node; @observable FiatCurrency fiatCurrency; @@ -97,6 +101,26 @@ abstract class SettingsStoreBase with Store { SharedPreferences _sharedPreferences; Box _nodeSource; + Map _nodes; + + Node getCurrentNode(WalletType walletType) => _nodes[walletType]; + + Future setCurrentNode(Node node, WalletType walletType) async { + switch (walletType) { + case WalletType.bitcoin: + await _sharedPreferences.setInt( + currentBitcoinElectrumSererIdKey, node.key as int); + break; + case WalletType.monero: + await _sharedPreferences.setInt(currentNodeIdKey, node.key as int); + break; + default: + break; + } + + _nodes[walletType] = node; + } + static Future load( {@required Box nodeSource, FiatCurrency initialFiatCurrency = FiatCurrency.usd, @@ -126,12 +150,18 @@ abstract class SettingsStoreBase with Store { await Language.localeDetection(); final initialCurrentLocale = await Devicelocale.currentLocale; final nodeId = sharedPreferences.getInt(currentNodeIdKey); - final node = nodeSource.get(nodeId); + final bitcoinElectrumServerId = + sharedPreferences.getInt(currentBitcoinElectrumSererIdKey); + final moneroNode = nodeSource.get(nodeId); + final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final packageInfo = await PackageInfo.fromPlatform(); return SettingsStore( sharedPreferences: sharedPreferences, - node: node, + nodes: { + WalletType.monero: moneroNode, + WalletType.bitcoin: bitcoinElectrumServer + }, nodeSource: nodeSource, appVersion: packageInfo.version, initialFiatCurrency: currentFiatCurrency, diff --git a/lib/utils/mobx.dart b/lib/utils/mobx.dart new file mode 100644 index 000000000..4dd9b4100 --- /dev/null +++ b/lib/utils/mobx.dart @@ -0,0 +1,45 @@ +import 'package:mobx/mobx.dart'; + +Dispose connectDifferent(ObservableList source, ObservableList dest, + Y Function(T) transform, {bool Function(T) filter}) { + return source.observe((change) { + switch (change.type) { + case OperationType.add: + final _values = change.added; + Iterable values; + + if (filter != null) { + values = _values.where(filter); + } + + dest.addAll(values.map((e) => transform(e))); + break; + case OperationType.remove: + print(change.index); + print(change.removed); + change.removed.forEach((element) { dest.remove(element); }); + +// dest.removeAt(change.index); + break; + case OperationType.update: +// change.index + break; + } + }); +} + +Dispose connect(ObservableList source, ObservableList dest) { + return source.observe((change) { + switch (change.type) { + case OperationType.add: + dest.addAll(change.added); + break; + case OperationType.remove: + dest.removeAt(change.index); + break; + case OperationType.update: +// change.index + break; + } + }); +} diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 7b79a772c..acb0d3d0a 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -129,6 +129,11 @@ abstract class DashboardViewModelBase with Store { ReactionDisposer _reaction; + Future reconnect() async { + final node = appStore.settingsStore.getCurrentNode(wallet.type); + await wallet.connectToNode(node: node); + } + void _onWalletChange(WalletBase wallet) { name = wallet.name; transactions.clear(); diff --git a/lib/view_model/node_list/node_list_view_model.dart b/lib/view_model/node_list/node_list_view_model.dart index 6821ae574..73f2b934e 100644 --- a/lib/view_model/node_list/node_list_view_model.dart +++ b/lib/view_model/node_list/node_list_view_model.dart @@ -1,26 +1,92 @@ +import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/core/wallet_base.dart'; import 'package:cake_wallet/src/domain/common/node.dart'; import 'package:cake_wallet/src/domain/common/node_list.dart'; import 'package:cake_wallet/store/node_list_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/src/domain/common/default_settings_migration.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; +import 'package:cake_wallet/utils/mobx.dart'; part 'node_list_view_model.g.dart'; class NodeListViewModel = NodeListViewModelBase with _$NodeListViewModel; -abstract class NodeListViewModelBase with Store { - NodeListViewModelBase(this._nodeListStore, this._nodeSource, this._wallet); +class ItemCell { + ItemCell(this.value, {@required this.isSelected}); - @computed - ObservableList get nodes => ObservableList.of( - _nodeListStore.nodes.where((node) => node.type == _wallet.type)); + final Item value; + final bool isSelected; +} + +abstract class NodeListViewModelBase with Store { + NodeListViewModelBase( + this._nodeListStore, this._nodeSource, this._wallet, this._settingsStore) + : nodes = ObservableList>() { + final currentNode = _settingsStore.getCurrentNode(_wallet.type); + final values = _nodeListStore.nodes; + nodes.clear(); + nodes.addAll(values.where((Node node) => node.type == _wallet.type).map( + (Node val) => + ItemCell(val, isSelected: val.key == currentNode.key))); + connectDifferent( + _nodeListStore.nodes, + nodes, + (Node val) => + ItemCell(val, isSelected: val.key == currentNode.key), + filter: (Node val) { + return val.type == _wallet.type; + }); + } + + ObservableList> nodes; final WalletBase _wallet; final Box _nodeSource; final NodeListStore _nodeListStore; + final SettingsStore _settingsStore; - Future reset() async => await resetToDefault(_nodeSource); + Future reset() async { + await resetToDefault(_nodeSource); - Future delete(Node node) async => node.delete(); + Node node; + + switch (_wallet.type) { + case WalletType.bitcoin: + node = getBitcoinDefaultElectrumServer(nodes: _nodeSource); + break; + case WalletType.monero: + node = getMoneroDefaultNode( + nodes: _nodeSource, + ); + break; + default: + break; + } + + await _wallet.connectToNode(node: node); + } + + Future delete(Node node) async => _nodeSource.delete(node.key); + + Future setAsCurrent(Node node) async { + await _wallet.connectToNode(node: node); + await _settingsStore.setCurrentNode(node, _wallet.type); + _updateCurrentNode(); + } + + void _updateCurrentNode() { + final currentNode = _settingsStore.getCurrentNode(_wallet.type); + + for (var i = 0; i < nodes.length; i++) { + final item = nodes[i]; + final isSelected = item.value.key == currentNode.key; + + if (item.isSelected != isSelected) { + nodes[i] = ItemCell(item.value, isSelected: isSelected); + } + } + } } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 0a128dc0a..75926a0b2 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -27,6 +27,7 @@ abstract class SendViewModelBase with Store { this._wallet, this._settingsStore, this._fiatConversationStore) : state = InitialSendViewModelState(), _cryptoNumberFormat = NumberFormat()..maximumFractionDigits = 12, + // FIXME: need to be based on wallet type. all = false; @observable @@ -79,7 +80,7 @@ abstract class SendViewModelBase with Store { final WalletBase _wallet; final SettingsStore _settingsStore; final FiatConvertationStore _fiatConversationStore; - NumberFormat _cryptoNumberFormat; + final NumberFormat _cryptoNumberFormat; @action void setAll() => all = true; @@ -129,7 +130,8 @@ abstract class SendViewModelBase with Store { void _updateFiatAmount() { try { final fiat = calculateFiatAmount( - price: _fiatConversationStore.price, cryptoAmount: cryptoAmount); + price: _fiatConversationStore.price, + cryptoAmount: cryptoAmount.replaceAll(',', '.')); if (fiatAmount != fiat) { fiatAmount = fiat; } @@ -141,7 +143,8 @@ abstract class SendViewModelBase with Store { @action void _updateCryptoAmount() { try { - final crypto = double.parse(fiatAmount) / _fiatConversationStore.price; + final crypto = double.parse(fiatAmount.replaceAll(',', '.')) / + _fiatConversationStore.price; final cryptoAmountTmp = _cryptoNumberFormat.format(crypto); if (cryptoAmount != cryptoAmountTmp) { diff --git a/lib/view_model/settings/settings_view_model.dart b/lib/view_model/settings/settings_view_model.dart index dbca8ef66..4eb68286f 100644 --- a/lib/view_model/settings/settings_view_model.dart +++ b/lib/view_model/settings/settings_view_model.dart @@ -23,7 +23,8 @@ class SettingsViewModel = SettingsViewModelBase with _$SettingsViewModel; abstract class SettingsViewModelBase with Store { SettingsViewModelBase(this._settingsStore, WalletBase wallet) - : itemHeaders = {} { + : itemHeaders = {}, + _walletType = wallet.type { sections = [ [ PickerListItem( @@ -117,7 +118,7 @@ abstract class SettingsViewModelBase with Store { } @computed - Node get node => _settingsStore.node; + Node get node => _settingsStore.getCurrentNode(_walletType); @computed FiatCurrency get fiatCurrency => _settingsStore.fiatCurrency; @@ -150,13 +151,10 @@ abstract class SettingsViewModelBase with Store { set allowBiometricalAuthentication(bool value) => _settingsStore.allowBiometricalAuthentication = value; -// @observable - -// @observable - final Map itemHeaders; - List> sections; final SettingsStore _settingsStore; + final WalletType _walletType; + List> sections; @action void toggleTransactionsDisplay() => diff --git a/lib/view_model/wallet_list/wallet_list_item.dart b/lib/view_model/wallet_list/wallet_list_item.dart index 669a2be79..5e2210c7a 100644 --- a/lib/view_model/wallet_list/wallet_list_item.dart +++ b/lib/view_model/wallet_list/wallet_list_item.dart @@ -3,9 +3,10 @@ import 'package:cake_wallet/src/domain/common/wallet_type.dart'; class WalletListItem { const WalletListItem( - {@required this.name, @required this.type, this.isCurrent = false}); + {@required this.name, @required this.type, @required this.key, this.isCurrent = false}); final String name; final WalletType type; final bool isCurrent; + final dynamic key; } diff --git a/lib/view_model/wallet_list/wallet_list_view_model.dart b/lib/view_model/wallet_list/wallet_list_view_model.dart index e84959e91..15683d1f1 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -17,11 +17,7 @@ abstract class WalletListViewModelBase with Store { WalletListViewModelBase( this._walletInfoSource, this._appStore, this._keyService) { wallets = ObservableList(); - wallets.addAll(_walletInfoSource.values.map((info) => WalletListItem( - name: info.name, - type: info.type, - isCurrent: info.name == _appStore.wallet.name && - info.type == _appStore.wallet.type))); + _updateList(); } @observable @@ -40,7 +36,12 @@ abstract class WalletListViewModelBase with Store { } @action - Future remove(WalletListItem wallet) async {} + Future remove(WalletListItem wallet) async { + final walletService = _getWalletService(wallet.type); + await walletService.remove(wallet.name); + await _walletInfoSource.delete(wallet.key); + _updateList(); + } WalletService _getWalletService(WalletType type) { switch (type) { @@ -52,4 +53,14 @@ abstract class WalletListViewModelBase with Store { return null; } } + + void _updateList() { + wallets.clear(); + wallets.addAll(_walletInfoSource.values.map((info) => WalletListItem( + name: info.name, + type: info.type, + key: info.key, + isCurrent: info.name == _appStore.wallet.name && + info.type == _appStore.wallet.type))); + } } From 24d139e540096339844de029528dd443e4f99d1e Mon Sep 17 00:00:00 2001 From: M Date: Sat, 29 Aug 2020 13:19:27 +0300 Subject: [PATCH 3/9] TMP 2 --- ios/Runner.xcodeproj/project.pbxproj | 6 +- lib/bitcoin/bitcoin_transaction_info.dart | 4 +- lib/bitcoin/bitcoin_wallet.dart | 10 ++- lib/bitcoin/electrum.dart | 79 +++++++++++++------ lib/reactions/bootstrap.dart | 14 ++++ lib/src/domain/common/sync_status.dart | 8 ++ lib/src/domain/common/transaction_info.dart | 1 + lib/src/screens/send/send_page.dart | 12 ++- .../transaction_details_page.dart | 4 + .../dashboard/dashboard_view_model.dart | 2 +- lib/view_model/send/send_view_model.dart | 5 ++ 11 files changed, 114 insertions(+), 31 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 987e086fc..539756848 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -373,7 +373,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 12; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -509,7 +509,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 12; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -540,7 +540,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 12; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/lib/bitcoin/bitcoin_transaction_info.dart b/lib/bitcoin/bitcoin_transaction_info.dart index d3c56b530..4198ca9e2 100644 --- a/lib/bitcoin/bitcoin_transaction_info.dart +++ b/lib/bitcoin/bitcoin_transaction_info.dart @@ -16,12 +16,13 @@ class BitcoinTransactionInfo extends TransactionInfo { @required TransactionDirection direction, @required bool isPending, @required DateTime date, - @required this.confirmations}) { + @required int confirmations}) { this.height = height; this.amount = amount; this.direction = direction; this.date = date; this.isPending = isPending; + this.confirmations = confirmations; } factory BitcoinTransactionInfo.fromElectrumVerbose(Map obj, @@ -119,7 +120,6 @@ class BitcoinTransactionInfo extends TransactionInfo { } final String id; - int confirmations; String _fiatAmount; diff --git a/lib/bitcoin/bitcoin_wallet.dart b/lib/bitcoin/bitcoin_wallet.dart index 4f67fa59f..b69bccb9b 100644 --- a/lib/bitcoin/bitcoin_wallet.dart +++ b/lib/bitcoin/bitcoin_wallet.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:typed_data'; import 'dart:convert'; import 'package:cake_wallet/bitcoin/bitcoin_transaction_credentials.dart'; @@ -31,9 +32,12 @@ import 'package:cake_wallet/src/domain/common/node.dart'; import 'package:cake_wallet/core/wallet_base.dart'; import 'package:rxdart/rxdart.dart'; import 'package:hex/hex.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:shared_preferences/shared_preferences.dart'; part 'bitcoin_wallet.g.dart'; + class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet; abstract class BitcoinWalletBase extends WalletBase with Store { @@ -217,6 +221,11 @@ abstract class BitcoinWalletBase extends WalletBase with Store { try { syncStatus = ConnectingSyncStatus(); await eclient.connectToUri(node.uri); + eclient.onConnectionStatusChange = (bool isConnected) { + if (!isConnected) { + syncStatus = LostConnectionSyncStatus(); + } + }; syncStatus = ConnectedSyncStatus(); } catch (e) { print(e.toString()); @@ -331,7 +340,6 @@ abstract class BitcoinWalletBase extends WalletBase with Store { await _scripthashesUpdateSubject[sh]?.close(); _scripthashesUpdateSubject[sh] = eclient.scripthashUpdate(sh); _scripthashesUpdateSubject[sh].listen((event) async { - print('event $event'); transactionHistory.updateAsync(); await _updateBalance(); }); diff --git a/lib/bitcoin/electrum.dart b/lib/bitcoin/electrum.dart index d92c69405..5476bb459 100644 --- a/lib/bitcoin/electrum.dart +++ b/lib/bitcoin/electrum.dart @@ -32,6 +32,7 @@ class ElectrumClient { bool get isConnected => _isConnected; Socket socket; + void Function(bool) onConnectionStatusChange; int _id; final Map _tasks; bool _isConnected; @@ -45,50 +46,49 @@ class ElectrumClient { } Future connect({@required String host, @required int port}) async { - await socket?.close(); - final start = DateTime.now(); + try { + await socket?.close(); + } catch (_) {} socket = await SecureSocket.connect(host, port, timeout: connectionTimeout); - - _isConnected = true; - + _setIsConnected(true); socket.listen((List event) { try { final jsoned = json.decode(utf8.decode(event)) as Map; -// print(jsoned); + print(jsoned); final method = jsoned['method']; + final id = jsoned['id'] as String; + final params = jsoned['result']; if (method is String) { _methodHandler(method: method, request: jsoned); return; } - final id = jsoned['id'] as String; - final params = jsoned['result']; - _finish(id, params); } catch (e) { print(e); } - }, onError: (Object error) { - print('ElectrumClient error: ${error.toString()}'); - }, onDone: () { - final end = DateTime.now(); - final diff = end.millisecondsSinceEpoch - start.millisecondsSinceEpoch; - print('On done: $diff'); - }); - - print('Connected to ${socket.remoteAddress}'); + }, + onError: (_) => _setIsConnected(false), + onDone: () => _setIsConnected(false)); keepAlive(); } void keepAlive() { _aliveTimer?.cancel(); // FIXME: Unnamed constant. - _aliveTimer = Timer.periodic(Duration(seconds: 30), (_) async => ping()); + _aliveTimer = Timer.periodic(Duration(seconds: 2), (_) async => ping()); } - Future ping() => call(method: 'server.ping'); + Future ping() async { + try { + await callWithTimeout(method: 'server.ping'); + _setIsConnected(true); + } on RequestFailedTimeoutException catch (_) { + _setIsConnected(false); + } + } Future> version() => call(method: 'server.version').then((dynamic result) { @@ -205,7 +205,6 @@ class ElectrumClient { {@required String transactionRaw}) async => call(method: 'blockchain.transaction.broadcast', params: [transactionRaw]) .then((dynamic result) { - print('result $result'); if (result is String) { return result; } @@ -264,6 +263,25 @@ class ElectrumClient { return completer.future; } + Future callWithTimeout( + {String method, + List params = const [], + int timeout = 2000}) async { + final completer = Completer(); + _id += 1; + final id = _id; + _regisryTask(id, completer); + socket.write(jsonrpc(method: method, id: _id, params: params)); + + Timer(Duration(milliseconds: timeout), () { + if (!completer.isCompleted) { + completer.completeError(RequestFailedTimeoutException(method, _id)); + } + }); + + return completer.future; + } + void request({String method, List params = const []}) { _id += 1; socket.write(jsonrpc(method: method, id: _id, params: params)); @@ -280,7 +298,9 @@ class ElectrumClient { return; } - _tasks[id]?.completer?.complete(data); + if (!(_tasks[id]?.completer?.isCompleted ?? false)) { + _tasks[id]?.completer?.complete(data); + } if (!(_tasks[id]?.isSubscription ?? false)) { _tasks[id] = null; @@ -303,4 +323,19 @@ class ElectrumClient { break; } } + + void _setIsConnected(bool isConnected) { + if (_isConnected != isConnected) { + onConnectionStatusChange?.call(isConnected); + } + + _isConnected = isConnected; + } +} + +class RequestFailedTimeoutException implements Exception { + RequestFailedTimeoutException(this.method, this.id); + + final String method; + final int id; } diff --git a/lib/reactions/bootstrap.dart b/lib/reactions/bootstrap.dart index 76990eb55..6e91c3c50 100644 --- a/lib/reactions/bootstrap.dart +++ b/lib/reactions/bootstrap.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/src/domain/common/sync_status.dart'; import 'package:mobx/mobx.dart'; @@ -49,6 +51,7 @@ ReactionDisposer _initialAuthReaction; ReactionDisposer _onCurrentWalletChangeReaction; ReactionDisposer _onWalletSyncStatusChangeReaction; ReactionDisposer _onCurrentFiatCurrencyChangeDisposer; +Timer _reconnectionTimer; Future bootstrap( {FiatConvertationService fiatConvertationService}) async { @@ -74,10 +77,21 @@ Future bootstrap( _onCurrentWalletChangeReaction ??= reaction((_) => getIt.get().wallet, (WalletBase wallet) async { _onWalletSyncStatusChangeReaction?.reaction?.dispose(); + _reconnectionTimer?.cancel(); _onWalletSyncStatusChangeReaction = reaction( (_) => wallet.syncStatus is ConnectedSyncStatus, (_) async => await wallet.startSync()); + _reconnectionTimer = Timer.periodic(Duration(seconds: 5), (_) async { + if (wallet.syncStatus is LostConnectionSyncStatus || + wallet.syncStatus is FailedSyncStatus) { + try { + await wallet.connectToNode( + node: settingsStore.getCurrentNode(wallet.type)); + } catch (_) {} + } + }); + await getIt .get() .setString('current_wallet_name', wallet.name); diff --git a/lib/src/domain/common/sync_status.dart b/lib/src/domain/common/sync_status.dart index 3e2eae4a2..3ee2c6b83 100644 --- a/lib/src/domain/common/sync_status.dart +++ b/lib/src/domain/common/sync_status.dart @@ -73,3 +73,11 @@ class ConnectedSyncStatus extends SyncStatus { @override String title() => S.current.sync_status_connected; } + +class LostConnectionSyncStatus extends SyncStatus { + @override + double progress() => 1.0; + + @override + String title() => S.current.sync_status_failed_connect; +} \ No newline at end of file diff --git a/lib/src/domain/common/transaction_info.dart b/lib/src/domain/common/transaction_info.dart index 06a84316d..e5da3bed6 100644 --- a/lib/src/domain/common/transaction_info.dart +++ b/lib/src/domain/common/transaction_info.dart @@ -7,6 +7,7 @@ abstract class TransactionInfo extends Object { bool isPending; DateTime date; int height; + int confirmations; String amountFormatted(); String fiatAmount(); void changeFiatAmount(String amount); diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 8bb4cd2b9..dad4952c2 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -159,7 +159,8 @@ class SendFormState extends State { decoration: InputDecoration( prefixIcon: Padding( padding: EdgeInsets.only(top: 12), - child: Text('${widget.sendViewModel.currency.toString()}:', + child: Text( + '${widget.sendViewModel.currency.toString()}:', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, @@ -213,7 +214,14 @@ class SendFormState extends State { borderSide: BorderSide( color: Theme.of(context).dividerColor, width: 1.0))), - validator: widget.sendViewModel.amountValidator), + validator: (String value) { + if (widget.sendViewModel.all) { + return null; + } + + return widget.sendViewModel.amountValidator + .call(value); + }), ), Padding( padding: const EdgeInsets.only(top: 20), diff --git a/lib/src/screens/transaction_details/transaction_details_page.dart b/lib/src/screens/transaction_details/transaction_details_page.dart index 4351089ed..7b3573e03 100644 --- a/lib/src/screens/transaction_details/transaction_details_page.dart +++ b/lib/src/screens/transaction_details/transaction_details_page.dart @@ -1,3 +1,4 @@ + import 'package:intl/intl.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -48,6 +49,9 @@ class TransactionDetailsPage extends BasePage { StandartListItem( title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), + StandartListItem( + title: 'Confirmations', + value: tx.confirmations?.toString()), StandartListItem( title: S.current.transaction_details_height, value: '${tx.height}'), StandartListItem( diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index acb0d3d0a..d59792a8d 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -86,7 +86,7 @@ abstract class DashboardViewModelBase with Store { statusText = S.current.Blocks_remaining(status.toString()); } - if (status is FailedSyncStatus) { + if (status is FailedSyncStatus || status is LostConnectionSyncStatus) { statusText = S.current.please_try_to_connect_to_another_node; } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 75926a0b2..81be508e6 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -116,6 +116,11 @@ abstract class SendViewModelBase with Store { @action void setCryptoAmount(String amount) { + // FIXME: hardcoded value. + if (amount.toUpperCase() != 'ALL') { + all = false; + } + cryptoAmount = amount; _updateFiatAmount(); } From fec2ec28450cb59287ff9338a74f7b3f8ba922ca Mon Sep 17 00:00:00 2001 From: M Date: Mon, 31 Aug 2020 11:44:58 +0300 Subject: [PATCH 4/9] Updated for flutter 1.20.x --- ios/Flutter/.last_build_id | 1 + ios/Podfile.lock | 16 +- ios/Runner.xcodeproj/project.pbxproj | 3 - lib/bitcoin/electrum.dart | 2 +- lib/reactions/bootstrap.dart | 2 +- lib/utils/mobx.dart | 71 ++++---- .../settings/settings_view_model.dart | 20 +-- pubspec.lock | 152 ++++++++++-------- pubspec.yaml | 16 +- 9 files changed, 155 insertions(+), 128 deletions(-) create mode 100644 ios/Flutter/.last_build_id diff --git a/ios/Flutter/.last_build_id b/ios/Flutter/.last_build_id new file mode 100644 index 000000000..8e3e3da7e --- /dev/null +++ b/ios/Flutter/.last_build_id @@ -0,0 +1 @@ +a2dce69f54a78f5b00e19850e4b2d402 \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 641750308..64e81a4e8 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -44,6 +44,8 @@ PODS: - Flutter - shared_preferences (0.0.1): - Flutter + - shared_preferences_linux (0.0.1): + - Flutter - shared_preferences_macos (0.0.1): - Flutter - shared_preferences_web (0.0.1): @@ -51,6 +53,8 @@ PODS: - SwiftProtobuf (1.8.0) - url_launcher (0.0.1): - Flutter + - url_launcher_linux (0.0.1): + - Flutter - url_launcher_macos (0.0.1): - Flutter - url_launcher_web (0.0.1): @@ -61,7 +65,7 @@ DEPENDENCIES: - cw_monero (from `.symlinks/plugins/cw_monero/ios`) - devicelocale (from `.symlinks/plugins/devicelocale/ios`) - esys_flutter_share (from `.symlinks/plugins/esys_flutter_share/ios`) - - Flutter (from `.symlinks/flutter/ios-release`) + - Flutter (from `.symlinks/flutter/ios`) - flutter_plugin_android_lifecycle (from `.symlinks/plugins/flutter_plugin_android_lifecycle/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - local_auth (from `.symlinks/plugins/local_auth/ios`) @@ -71,9 +75,11 @@ DEPENDENCIES: - path_provider_macos (from `.symlinks/plugins/path_provider_macos/ios`) - share (from `.symlinks/plugins/share/ios`) - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) + - shared_preferences_linux (from `.symlinks/plugins/shared_preferences_linux/ios`) - shared_preferences_macos (from `.symlinks/plugins/shared_preferences_macos/ios`) - shared_preferences_web (from `.symlinks/plugins/shared_preferences_web/ios`) - url_launcher (from `.symlinks/plugins/url_launcher/ios`) + - url_launcher_linux (from `.symlinks/plugins/url_launcher_linux/ios`) - url_launcher_macos (from `.symlinks/plugins/url_launcher_macos/ios`) - url_launcher_web (from `.symlinks/plugins/url_launcher_web/ios`) @@ -92,7 +98,7 @@ EXTERNAL SOURCES: esys_flutter_share: :path: ".symlinks/plugins/esys_flutter_share/ios" Flutter: - :path: ".symlinks/flutter/ios-release" + :path: ".symlinks/flutter/ios" flutter_plugin_android_lifecycle: :path: ".symlinks/plugins/flutter_plugin_android_lifecycle/ios" flutter_secure_storage: @@ -111,12 +117,16 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/share/ios" shared_preferences: :path: ".symlinks/plugins/shared_preferences/ios" + shared_preferences_linux: + :path: ".symlinks/plugins/shared_preferences_linux/ios" shared_preferences_macos: :path: ".symlinks/plugins/shared_preferences_macos/ios" shared_preferences_web: :path: ".symlinks/plugins/shared_preferences_web/ios" url_launcher: :path: ".symlinks/plugins/url_launcher/ios" + url_launcher_linux: + :path: ".symlinks/plugins/url_launcher_linux/ios" url_launcher_macos: :path: ".symlinks/plugins/url_launcher_macos/ios" url_launcher_web: @@ -138,10 +148,12 @@ SPEC CHECKSUMS: path_provider_macos: f760a3c5b04357c380e2fddb6f9db6f3015897e0 share: 0b2c3e82132f5888bccca3351c504d0003b3b410 shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d + shared_preferences_linux: afefbfe8d921e207f01ede8b60373d9e3b566b78 shared_preferences_macos: f3f29b71ccbb56bf40c9dd6396c9acf15e214087 shared_preferences_web: 141cce0c3ed1a1c5bf2a0e44f52d31eeb66e5ea9 SwiftProtobuf: 2cbd9409689b7df170d82a92a33443c8e3e14a70 url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef + url_launcher_linux: ac237cb7a8058736e4aae38bdbcc748a4b394cc0 url_launcher_macos: fd7894421cd39320dce5f292fc99ea9270b2a313 url_launcher_web: e5527357f037c87560776e36436bf2b0288b965c diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 539756848..cf8308171 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -319,7 +319,6 @@ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -399,7 +398,6 @@ }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -455,7 +453,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; diff --git a/lib/bitcoin/electrum.dart b/lib/bitcoin/electrum.dart index 5476bb459..fbd219ddb 100644 --- a/lib/bitcoin/electrum.dart +++ b/lib/bitcoin/electrum.dart @@ -70,7 +70,7 @@ class ElectrumClient { print(e); } }, - onError: (_) => _setIsConnected(false), + onError: (Error _) => _setIsConnected(false), onDone: () => _setIsConnected(false)); keepAlive(); } diff --git a/lib/reactions/bootstrap.dart b/lib/reactions/bootstrap.dart index 6e91c3c50..797f2cfb6 100644 --- a/lib/reactions/bootstrap.dart +++ b/lib/reactions/bootstrap.dart @@ -80,7 +80,7 @@ Future bootstrap( _reconnectionTimer?.cancel(); _onWalletSyncStatusChangeReaction = reaction( (_) => wallet.syncStatus is ConnectedSyncStatus, - (_) async => await wallet.startSync()); + (Object _) async => await wallet.startSync()); _reconnectionTimer = Timer.periodic(Duration(seconds: 5), (_) async { if (wallet.syncStatus is LostConnectionSyncStatus || diff --git a/lib/utils/mobx.dart b/lib/utils/mobx.dart index 4dd9b4100..0065a939d 100644 --- a/lib/utils/mobx.dart +++ b/lib/utils/mobx.dart @@ -1,45 +1,46 @@ import 'package:mobx/mobx.dart'; -Dispose connectDifferent(ObservableList source, ObservableList dest, - Y Function(T) transform, {bool Function(T) filter}) { - return source.observe((change) { - switch (change.type) { - case OperationType.add: - final _values = change.added; - Iterable values; +void connectDifferent( + ObservableList source, ObservableList dest, Y Function(T) transform, + {bool Function(T) filter}) { + source.observe((ListChange change) { +// switch (change.type) { +// case OperationType.add: +// final _values = change.added; +// Iterable values; - if (filter != null) { - values = _values.where(filter); - } +// if (filter != null) { +// values = _values.where(filter); +// } - dest.addAll(values.map((e) => transform(e))); - break; - case OperationType.remove: - print(change.index); - print(change.removed); - change.removed.forEach((element) { dest.remove(element); }); +// dest.addAll(values.map((e) => transform(e))); +// break; +// case OperationType.remove: +// change.removed.forEach((element) { +// dest.remove(element); +// }); -// dest.removeAt(change.index); - break; - case OperationType.update: -// change.index - break; - } +// // dest.removeAt(change.index); +// break; +// case OperationType.update: +// // change.index +// break; +// } }); } -Dispose connect(ObservableList source, ObservableList dest) { - return source.observe((change) { - switch (change.type) { - case OperationType.add: - dest.addAll(change.added); - break; - case OperationType.remove: - dest.removeAt(change.index); - break; - case OperationType.update: -// change.index - break; - } +void connect(ObservableList source, ObservableList dest) { + source.observe((ListChange change) { +// switch (change.type) { +// case OperationType.add: +// dest.addAll(change.added); +// break; +// case OperationType.remove: +// dest.removeAt(change.index); +// break; +// case OperationType.update: +// // change.index +// break; +// } }); } diff --git a/lib/view_model/settings/settings_view_model.dart b/lib/view_model/settings/settings_view_model.dart index 4eb68286f..c2dd12a93 100644 --- a/lib/view_model/settings/settings_view_model.dart +++ b/lib/view_model/settings/settings_view_model.dart @@ -44,7 +44,7 @@ abstract class SettingsViewModelBase with Store { SwitcherListItem( title: S.current.settings_save_recipient_address, value: () => shouldSaveRecipientAddress, - onValueChange: (bool value) => shouldSaveRecipientAddress = value) + onValueChange: (bool value) => setShouldSaveRecipientAddress(value)) ], [ RegularListItem( @@ -70,7 +70,7 @@ abstract class SettingsViewModelBase with Store { title: S.current.settings_allow_biometrical_authentication, value: () => allowBiometricalAuthentication, onValueChange: (bool value) => - allowBiometricalAuthentication = value), + setAllowBiometricalAuthentication(value)), SwitcherListItem( title: S.current.settings_dark_mode, value: () => _settingsStore.isDarkTheme, @@ -139,23 +139,23 @@ abstract class SettingsViewModelBase with Store { bool get shouldSaveRecipientAddress => _settingsStore.shouldSaveRecipientAddress; - @action - set shouldSaveRecipientAddress(bool value) => - _settingsStore.shouldSaveRecipientAddress = value; - @computed bool get allowBiometricalAuthentication => _settingsStore.allowBiometricalAuthentication; - @action - set allowBiometricalAuthentication(bool value) => - _settingsStore.allowBiometricalAuthentication = value; - final Map itemHeaders; final SettingsStore _settingsStore; final WalletType _walletType; List> sections; + @action + void setShouldSaveRecipientAddress(bool value) => + _settingsStore.shouldSaveRecipientAddress = value; + + @action + void setAllowBiometricalAuthentication(bool value) => + _settingsStore.allowBiometricalAuthentication = value; + @action void toggleTransactionsDisplay() => actionlistDisplayMode.contains(ActionListDisplayMode.transactions) diff --git a/pubspec.lock b/pubspec.lock index f8e7304da..be43947b0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,13 +1,20 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: - analyzer: + _fe_analyzer_shared: dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.0" + analyzer: + dependency: "direct overridden" description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "0.36.4" + version: "0.39.14" archive: dependency: transitive description: @@ -28,14 +35,14 @@ packages: name: asn1lib url: "https://pub.dartlang.org" source: hosted - version: "0.6.4" + version: "0.6.5" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.4.1" + version: "2.4.2" auto_size_text: dependency: "direct main" description: @@ -105,7 +112,7 @@ packages: name: build url: "https://pub.dartlang.org" source: hosted - version: "1.2.2" + version: "1.3.0" build_config: dependency: transitive description: @@ -121,26 +128,26 @@ packages: source: hosted version: "2.1.4" build_resolvers: - dependency: transitive + dependency: "direct dev" description: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.3.11" build_runner: dependency: "direct dev" description: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "1.9.0" + version: "1.10.1" build_runner_core: dependency: transitive description: name: build_runner_core url: "https://pub.dartlang.org" source: hosted - version: "5.1.0" + version: "6.0.1" built_collection: dependency: transitive description: @@ -161,7 +168,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "0.3.1" + version: "1.0.0" charcode: dependency: transitive description: @@ -176,6 +183,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" clock: dependency: transitive description: @@ -189,14 +203,14 @@ packages: name: code_builder url: "https://pub.dartlang.org" source: hosted - version: "3.3.0" + version: "3.4.1" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.14.12" + version: "1.14.13" convert: dependency: transitive description: @@ -210,14 +224,14 @@ packages: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "2.1.4" + version: "2.1.5" csslib: dependency: transitive description: name: csslib url: "https://pub.dartlang.org" source: hosted - version: "0.16.1" + version: "0.16.2" cupertino_icons: dependency: "direct main" description: @@ -245,7 +259,7 @@ packages: name: dartx url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.5.0" date_range_picker: dependency: "direct main" description: @@ -280,7 +294,7 @@ packages: name: dotted_border url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "1.0.6" encrypt: dependency: "direct main" description: @@ -295,6 +309,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" ffi: dependency: transitive description: @@ -308,7 +329,7 @@ packages: name: file url: "https://pub.dartlang.org" source: hosted - version: "5.2.0" + version: "5.2.1" fixnum: dependency: transitive description: @@ -346,7 +367,7 @@ packages: name: flutter_mobx url: "https://pub.dartlang.org" source: hosted - version: "0.3.0+1" + version: "1.1.0+2" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -367,7 +388,7 @@ packages: name: flutter_slidable url: "https://pub.dartlang.org" source: hosted - version: "0.5.4" + version: "0.5.7" flutter_test: dependency: "direct dev" description: flutter @@ -378,20 +399,13 @@ packages: description: flutter source: sdk version: "0.0.0" - front_end: - dependency: transitive - description: - name: front_end - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.19" get_it: dependency: "direct main" description: name: get_it url: "https://pub.dartlang.org" source: hosted - version: "4.0.2" + version: "4.0.4" glob: dependency: transitive description: @@ -419,21 +433,21 @@ packages: name: hive url: "https://pub.dartlang.org" source: hosted - version: "1.4.1+1" + version: "1.4.4" hive_flutter: dependency: "direct main" description: name: hive_flutter url: "https://pub.dartlang.org" source: hosted - version: "0.3.0+2" + version: "0.3.1" hive_generator: dependency: "direct dev" description: name: hive_generator url: "https://pub.dartlang.org" source: hosted - version: "0.7.0+2" + version: "0.7.1" html: dependency: transitive description: @@ -447,7 +461,7 @@ packages: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.12.1" + version: "0.12.2" http_multi_server: dependency: transitive description: @@ -468,7 +482,7 @@ packages: name: image url: "https://pub.dartlang.org" source: hosted - version: "2.1.12" + version: "2.1.14" intl: dependency: "direct main" description: @@ -497,20 +511,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.1" - kernel: - dependency: transitive - description: - name: kernel - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.19" local_auth: dependency: "direct main" description: name: local_auth url: "https://pub.dartlang.org" source: hosted - version: "0.6.2+3" + version: "0.6.3+1" logging: dependency: transitive description: @@ -524,7 +531,7 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.6" + version: "0.12.8" meta: dependency: transitive description: @@ -538,21 +545,21 @@ packages: name: mime url: "https://pub.dartlang.org" source: hosted - version: "0.9.6+3" + version: "0.9.7" mobx: dependency: "direct main" description: name: mobx url: "https://pub.dartlang.org" source: hosted - version: "0.3.10" + version: "1.2.1+2" mobx_codegen: dependency: "direct dev" description: name: mobx_codegen url: "https://pub.dartlang.org" source: hosted - version: "0.3.3+1" + version: "1.1.0+1" node_interop: dependency: transitive description: @@ -580,14 +587,7 @@ packages: name: package_info url: "https://pub.dartlang.org" source: hosted - version: "0.4.1" - package_resolver: - dependency: transitive - description: - name: package_resolver - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.10" + version: "0.4.3" password: dependency: "direct main" description: @@ -601,14 +601,14 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.6.4" + version: "1.7.0" path_drawing: dependency: transitive description: name: path_drawing url: "https://pub.dartlang.org" source: hosted - version: "0.4.1" + version: "0.4.1+1" path_parsing: dependency: transitive description: @@ -622,14 +622,14 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "1.6.11" + version: "1.6.14" path_provider_linux: dependency: transitive description: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "0.0.1+1" + version: "0.0.1+2" path_provider_macos: dependency: transitive description: @@ -643,7 +643,7 @@ packages: name: path_provider_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.3" pedantic: dependency: "direct dev" description: @@ -657,7 +657,7 @@ packages: name: petitparser url: "https://pub.dartlang.org" source: hosted - version: "2.4.0" + version: "3.0.4" platform: dependency: transitive description: @@ -755,14 +755,21 @@ packages: name: share url: "https://pub.dartlang.org" source: hosted - version: "0.6.4+3" + version: "0.6.5" shared_preferences: dependency: "direct main" description: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "0.5.7+3" + version: "0.5.10" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.2+2" shared_preferences_macos: dependency: transitive description: @@ -790,7 +797,7 @@ packages: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "0.7.7" + version: "0.7.9" shelf_web_socket: dependency: transitive description: @@ -823,7 +830,7 @@ packages: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.9.3" + version: "1.9.5" stream_channel: dependency: transitive description: @@ -858,7 +865,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.15" + version: "0.2.17" time: dependency: transitive description: @@ -879,14 +886,21 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.1.6" + version: "1.2.0" url_launcher: dependency: "direct main" description: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "5.4.11" + version: "5.5.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1+1" url_launcher_macos: dependency: transitive description: @@ -900,14 +914,14 @@ packages: name: url_launcher_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.7" + version: "1.0.8" url_launcher_web: dependency: transitive description: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "0.1.1+6" + version: "0.1.3" uuid: dependency: "direct main" description: @@ -949,7 +963,7 @@ packages: name: xml url: "https://pub.dartlang.org" source: hosted - version: "3.6.1" + version: "4.2.0" yaml: dependency: "direct main" description: @@ -958,5 +972,5 @@ packages: source: hosted version: "2.2.1" sdks: - dart: ">=2.7.0 <3.0.0" + dart: ">=2.9.0-14.0.dev <3.0.0" flutter: ">=1.12.13+hotfix.5 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index cbc8b5c67..dc13637f1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,8 +34,8 @@ dependencies: barcode_scan: any http: ^0.12.0+2 path_provider: ^1.3.0 - mobx: ^0.3.7 - flutter_mobx: 0.3.0+1 + mobx: ^1.2.1+2 + flutter_mobx: ^1.1.0+2 flutter_slidable: ^0.5.3 share: ^0.6.2+1 esys_flutter_share: ^1.0.2 @@ -43,8 +43,8 @@ dependencies: dio: 3.0.7 cw_monero: path: ./cw_monero - hive: ^1.4.1+1 - hive_flutter: ^0.3.0+2 + hive: ^1.4.2 + hive_flutter: ^0.3.1 local_auth: ^0.6.1 package_info: ^0.4.0+13 devicelocale: ^0.2.1 @@ -64,15 +64,17 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - build_runner: ^1.3.1 - mobx_codegen: 0.3.3+1 + build_runner: 1.10.1 + build_resolvers: ^1.3.10 + mobx_codegen: ^1.1.0+1 hive_generator: ^0.7.0+2 flutter_launcher_icons: ^0.7.4 pedantic: ^1.8.0 # Fix for hive https://github.com/hivedb/hive/issues/247#issuecomment-606838497 dependency_overrides: - dartx: ^0.3.0 + dartx: ^0.5.0 + analyzer: 0.39.14 flutter_icons: image_path: "assets/images/app_logo.png" From eaa5d48bb6847103645a48f1241e0e34325d6725 Mon Sep 17 00:00:00 2001 From: M Date: Mon, 31 Aug 2020 17:39:12 +0300 Subject: [PATCH 5/9] Fixes --- lib/bitcoin/bitcoin_wallet.dart | 1 - lib/bitcoin/bitcoin_wallet_service.dart | 1 - lib/bitcoin/electrum.dart | 14 +++++++++----- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/bitcoin/bitcoin_wallet.dart b/lib/bitcoin/bitcoin_wallet.dart index b69bccb9b..4fc46f8fa 100644 --- a/lib/bitcoin/bitcoin_wallet.dart +++ b/lib/bitcoin/bitcoin_wallet.dart @@ -37,7 +37,6 @@ import 'package:shared_preferences/shared_preferences.dart'; part 'bitcoin_wallet.g.dart'; - class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet; abstract class BitcoinWalletBase extends WalletBase with Store { diff --git a/lib/bitcoin/bitcoin_wallet_service.dart b/lib/bitcoin/bitcoin_wallet_service.dart index e18692968..cb2d241fb 100644 --- a/lib/bitcoin/bitcoin_wallet_service.dart +++ b/lib/bitcoin/bitcoin_wallet_service.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:convert'; import 'package:bip39/bip39.dart' as bip39; import 'package:cake_wallet/bitcoin/file.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet_creation_credentials.dart'; diff --git a/lib/bitcoin/electrum.dart b/lib/bitcoin/electrum.dart index fbd219ddb..bd1ac09ac 100644 --- a/lib/bitcoin/electrum.dart +++ b/lib/bitcoin/electrum.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; import 'package:cake_wallet/bitcoin/script_hash.dart'; import 'package:flutter/foundation.dart'; import 'package:rxdart/rxdart.dart'; @@ -52,9 +53,11 @@ class ElectrumClient { socket = await SecureSocket.connect(host, port, timeout: connectionTimeout); _setIsConnected(true); - socket.listen((List event) { + + socket.listen((Uint8List event) { try { - final jsoned = json.decode(utf8.decode(event)) as Map; + final jsoned = + json.decode(utf8.decode(event.toList())) as Map; print(jsoned); final method = jsoned['method']; final id = jsoned['id'] as String; @@ -69,9 +72,10 @@ class ElectrumClient { } catch (e) { print(e); } - }, - onError: (Error _) => _setIsConnected(false), - onDone: () => _setIsConnected(false)); + }, onError: (Object error) { + print(error.toString()); + _setIsConnected(false); + }, onDone: () => _setIsConnected(false)); keepAlive(); } From d6a7f1fb616d2837189f7a9c8467e0600e2a3a79 Mon Sep 17 00:00:00 2001 From: Oleksandr Sobol Date: Mon, 31 Aug 2020 20:53:53 +0300 Subject: [PATCH 6/9] CAKE-28 | added available balance row and applied transactions priority picker to base send widget --- .../send/widgets/base_send_widget.dart | 190 ++++++++++-------- lib/view_model/send_view_model.dart | 5 + 2 files changed, 115 insertions(+), 80 deletions(-) diff --git a/lib/src/screens/send/widgets/base_send_widget.dart b/lib/src/screens/send/widgets/base_send_widget.dart index c8d03f389..94f970b95 100644 --- a/lib/src/screens/send/widgets/base_send_widget.dart +++ b/lib/src/screens/send/widgets/base_send_widget.dart @@ -1,3 +1,5 @@ +import 'package:cake_wallet/src/domain/common/transaction_priority.dart'; +import 'package:cake_wallet/src/widgets/picker.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; import 'package:flutter/cupertino.dart'; @@ -152,48 +154,26 @@ class BaseSendWidget extends StatelessWidget { ), suffixIcon: isTemplate ? Offstage() - : Padding( - padding: EdgeInsets.only(bottom: 2), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - width: MediaQuery.of(context).size.width/2, - alignment: Alignment.centerLeft, - child: Text( - ' / ' + sendViewModel.balance, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).primaryTextTheme.headline.decorationColor - ) - ), + : Container( + height: 32, + width: 32, + margin: EdgeInsets.only(left: 14, top: 4, bottom: 10), + decoration: BoxDecoration( + color: Theme.of(context).primaryTextTheme.display1.color, + borderRadius: BorderRadius.all(Radius.circular(6)) + ), + child: InkWell( + onTap: () => sendViewModel.setSendAll(), + child: Center( + child: Text(S.of(context).all, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Theme.of(context).primaryTextTheme.display1.decorationColor + ) ), - Container( - height: 34, - width: 34, - margin: EdgeInsets.only(left: 12, bottom: 8), - decoration: BoxDecoration( - color: Theme.of(context).primaryTextTheme.display1.color, - borderRadius: BorderRadius.all(Radius.circular(6)) - ), - child: InkWell( - onTap: () => sendViewModel.setSendAll(), - child: Center( - child: Text(S.of(context).all, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Theme.of(context).primaryTextTheme.display1.decorationColor - ) - ), - ), - ), - ) - ], + ), ), ), hintText: '0.0000', @@ -212,6 +192,37 @@ class BaseSendWidget extends StatelessWidget { ); } ), + isTemplate + ? Offstage() + : Observer( + builder: (_) => Padding( + padding: EdgeInsets.only(top: 10), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + S.of(context).available_balance + ':', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Theme.of(context).primaryTextTheme.headline.decorationColor + ), + ) + ), + Text( + sendViewModel.balance, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Theme.of(context).primaryTextTheme.headline.decorationColor + ), + ) + ], + ), + ) + ), Padding( padding: const EdgeInsets.only(top: 20), child: BaseTextFormField( @@ -247,45 +258,47 @@ class BaseSendWidget extends StatelessWidget { ), isTemplate ? Offstage() - : GestureDetector( - onTap: () {}, - child: Container( - padding: EdgeInsets.only(top: 24), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(S.of(context).send_estimated_fee, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - //color: Theme.of(context).primaryTextTheme.display2.color, - color: Colors.white - )), - Container( - child: Row( - children: [ - Text( - sendViewModel.estimatedFee.toString() + ' ' - + sendViewModel.currency.title, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - //color: Theme.of(context).primaryTextTheme.display2.color, - color: Colors.white - )), - Padding( - padding: EdgeInsets.only(left: 5), - child: Icon( - Icons.arrow_forward_ios, - size: 12, - color: Colors.white,), - ) - ], - ), - ) - ], - ), - ), + : Observer( + builder: (_) => GestureDetector( + onTap: () => _setTransactionPriority(context), + child: Container( + padding: EdgeInsets.only(top: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(S.of(context).send_estimated_fee, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + //color: Theme.of(context).primaryTextTheme.display2.color, + color: Colors.white + )), + Container( + child: Row( + children: [ + Text( + sendViewModel.estimatedFee.toString() + ' ' + + sendViewModel.currency.title, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + //color: Theme.of(context).primaryTextTheme.display2.color, + color: Colors.white + )), + Padding( + padding: EdgeInsets.only(left: 5), + child: Icon( + Icons.arrow_forward_ios, + size: 12, + color: Colors.white,), + ) + ], + ), + ) + ], + ), + ), + ) ) ], ), @@ -575,4 +588,21 @@ class BaseSendWidget extends StatelessWidget { }); } } + + Future _setTransactionPriority(BuildContext context) async { + final items = TransactionPriority.all; + final selectedItem = items.indexOf(sendViewModel.transactionPriority); + + await showDialog( + builder: (_) => Picker( + items: items, + selectedAtIndex: selectedItem, + title: S.of(context).please_select, + mainAxisAlignment: MainAxisAlignment.center, + onItemSelected: (TransactionPriority priority) => + sendViewModel.setTransactionPriority(priority), + isAlwaysShowScrollThumb: true, + ), + context: context); + } } \ No newline at end of file diff --git a/lib/view_model/send_view_model.dart b/lib/view_model/send_view_model.dart index f5018a2b0..0942c7841 100644 --- a/lib/view_model/send_view_model.dart +++ b/lib/view_model/send_view_model.dart @@ -193,6 +193,11 @@ abstract class SendViewModelBase with Store { fiatAmount = ''; } + @action + void setTransactionPriority(TransactionPriority transactionPriority) { + _settingsStore.transactionPriority = transactionPriority; + } + final WalletBase _wallet; final SettingsStore _settingsStore; From 9074bee6db574c12b241617fe95c928f88c8b7f9 Mon Sep 17 00:00:00 2001 From: M Date: Wed, 2 Sep 2020 11:47:41 +0300 Subject: [PATCH 7/9] Merge redesign part 2. --- lib/src/screens/send/send_page.dart | 24 +- lib/src/screens/send/send_template_page.dart | 25 +- .../send/widgets/base_send_widget.dart | 869 ++++++++++-------- lib/store/settings_store.dart | 2 +- .../exchange/exchange_view_model.dart | 73 +- lib/view_model/send/send_view_model.dart | 17 +- 6 files changed, 570 insertions(+), 440 deletions(-) diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 53ee495e6..187523de2 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -95,16 +95,26 @@ class SendPage extends BasePage { // } } - @override - Widget body(BuildContext context) { - return super.build(context); - } - // @override - // Widget build(BuildContext context) { - // return BaseSendWidget(sendViewModel: sendViewModel); + // Widget body(BuildContext context) { + // return super.build(context); // } + @override + Widget body(BuildContext context) => BaseSendWidget( + sendViewModel: sendViewModel, + leading: leading(context), + middle: middle(context), + ); + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomPadding: resizeToAvoidBottomPadding, + body: Container( + color: Theme.of(context).backgroundColor, child: body(context))); + } + // @override // Widget build(BuildContext context) { // _setEffects(context); diff --git a/lib/src/screens/send/send_template_page.dart b/lib/src/screens/send/send_template_page.dart index 1f49a3f9a..5487dc71e 100644 --- a/lib/src/screens/send/send_template_page.dart +++ b/lib/src/screens/send/send_template_page.dart @@ -2,7 +2,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/view_model/send_view_model.dart'; +import 'package:cake_wallet/view_model/send/send_view_model.dart'; import 'package:cake_wallet/src/screens/send/widgets/base_send_widget.dart'; class SendTemplatePage extends BasePage { @@ -26,22 +26,17 @@ class SendTemplatePage extends BasePage { bool get resizeToAvoidBottomPadding => false; @override - Widget body(BuildContext context) => - BaseSendWidget( - sendViewModel: sendViewModel, - leading: leading(context), - middle: middle(context), - isTemplate: true - ); + Widget body(BuildContext context) => BaseSendWidget( + sendViewModel: sendViewModel, + leading: leading(context), + middle: middle(context), + isTemplate: true); @override Widget build(BuildContext context) { return Scaffold( - resizeToAvoidBottomPadding: resizeToAvoidBottomPadding, - body: Container( - color: Theme.of(context).backgroundColor, - child: body(context) - ) - ); + resizeToAvoidBottomPadding: resizeToAvoidBottomPadding, + body: Container( + color: Theme.of(context).backgroundColor, child: body(context))); } -} \ No newline at end of file +} diff --git a/lib/src/screens/send/widgets/base_send_widget.dart b/lib/src/screens/send/widgets/base_send_widget.dart index 94f970b95..bfbf3ae9a 100644 --- a/lib/src/screens/send/widgets/base_send_widget.dart +++ b/lib/src/screens/send/widgets/base_send_widget.dart @@ -1,10 +1,12 @@ +import 'dart:ui'; import 'package:cake_wallet/src/domain/common/transaction_priority.dart'; import 'package:cake_wallet/src/widgets/picker.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:cake_wallet/view_model/send_view_model.dart'; +import 'package:cake_wallet/view_model/send/send_view_model.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx/mobx.dart'; @@ -22,12 +24,11 @@ import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/routes.dart'; class BaseSendWidget extends StatelessWidget { - BaseSendWidget({ - @required this.sendViewModel, - @required this.leading, - @required this.middle, - this.isTemplate = false - }); + BaseSendWidget( + {@required this.sendViewModel, + @required this.leading, + @required this.middle, + this.isTemplate = false}); final SendViewModel sendViewModel; final bool isTemplate; @@ -45,7 +46,6 @@ class BaseSendWidget extends StatelessWidget { @override Widget build(BuildContext context) { - _setEffects(context); return ScrollableWithBottomSection( @@ -57,9 +57,7 @@ class BaseSendWidget extends StatelessWidget { gradient: LinearGradient(colors: [ Theme.of(context).primaryTextTheme.subhead.color, Theme.of(context).primaryTextTheme.subhead.decorationColor, - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight), + ], begin: Alignment.topLeft, end: Alignment.bottomRight), widget: Form( key: _formKey, child: Column(children: [ @@ -74,28 +72,34 @@ class BaseSendWidget extends StatelessWidget { child: Column( children: [ isTemplate - ? BaseTextFormField( - controller: _nameController, - hintText: S.of(context).send_name, - borderColor: Theme.of(context).primaryTextTheme.headline.color, - textStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white - ), - placeholderTextStyle: TextStyle( - color: Theme.of(context).primaryTextTheme.headline.decorationColor, - fontWeight: FontWeight.w500, - fontSize: 14), - validator: sendViewModel.templateValidator, - ) - : Offstage(), + ? BaseTextFormField( + controller: _nameController, + hintText: S.of(context).send_name, + borderColor: Theme.of(context) + .primaryTextTheme + .headline + .color, + textStyle: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.white), + placeholderTextStyle: TextStyle( + color: Theme.of(context) + .primaryTextTheme + .headline + .decorationColor, + fontWeight: FontWeight.w500, + fontSize: 14), + validator: sendViewModel.templateValidator, + ) + : Offstage(), Padding( padding: EdgeInsets.only(top: isTemplate ? 20 : 0), child: AddressTextField( controller: _addressController, - placeholder: S.of(context).send_address( - sendViewModel.cryptoCurrencyTitle), + // placeholder: S + // .of(context) + // .send_address(sendViewModel.cryptoCurrencyTitle), focusNode: _focusNode, onURIScanned: (uri) { var address = ''; @@ -116,113 +120,127 @@ class BaseSendWidget extends StatelessWidget { AddressTextFieldOption.qrCode, AddressTextFieldOption.addressBook ], - buttonColor: Theme.of(context).primaryTextTheme.display1.color, - borderColor: Theme.of(context).primaryTextTheme.headline.color, + buttonColor: + Theme.of(context).primaryTextTheme.display1.color, + borderColor: + Theme.of(context).primaryTextTheme.headline.color, textStyle: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: Colors.white - ), + color: Colors.white), hintStyle: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: Theme.of(context).primaryTextTheme.headline.decorationColor - ), + color: Theme.of(context) + .primaryTextTheme + .headline + .decorationColor), validator: sendViewModel.addressValidator, ), ), - Observer( - builder: (_) { - return Padding( - padding: const EdgeInsets.only(top: 20), - child: BaseTextFormField( - controller: _cryptoAmountController, - keyboardType: TextInputType.numberWithOptions( - signed: false, decimal: true), - inputFormatters: [ - BlacklistingTextInputFormatter( - RegExp('[\\-|\\ |\\,]')) - ], - prefixIcon: Padding( - padding: EdgeInsets.only(top: 9), - child: Text(sendViewModel.currency.title + ':', + Observer(builder: (_) { + return Padding( + padding: const EdgeInsets.only(top: 20), + child: BaseTextFormField( + controller: _cryptoAmountController, + keyboardType: TextInputType.numberWithOptions( + signed: false, decimal: true), + inputFormatters: [ + BlacklistingTextInputFormatter( + RegExp('[\\-|\\ |\\,]')) + ], + prefixIcon: Padding( + padding: EdgeInsets.only(top: 9), + child: + Text(sendViewModel.currency.title + ':', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white, )), - ), - suffixIcon: isTemplate + ), + suffixIcon: isTemplate ? Offstage() : Container( - height: 32, - width: 32, - margin: EdgeInsets.only(left: 14, top: 4, bottom: 10), - decoration: BoxDecoration( - color: Theme.of(context).primaryTextTheme.display1.color, - borderRadius: BorderRadius.all(Radius.circular(6)) - ), - child: InkWell( - onTap: () => sendViewModel.setSendAll(), - child: Center( - child: Text(S.of(context).all, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Theme.of(context).primaryTextTheme.display1.decorationColor - ) + height: 32, + width: 32, + margin: EdgeInsets.only( + left: 14, top: 4, bottom: 10), + decoration: BoxDecoration( + color: Theme.of(context) + .primaryTextTheme + .display1 + .color, + borderRadius: BorderRadius.all( + Radius.circular(6))), + child: InkWell( + onTap: () => + sendViewModel.setSendAll(), + child: Center( + child: Text(S.of(context).all, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Theme.of(context) + .primaryTextTheme + .display1 + .decorationColor)), ), ), ), - ), - hintText: '0.0000', - borderColor: Theme.of(context).primaryTextTheme.headline.color, - textStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white - ), - placeholderTextStyle: TextStyle( - color: Theme.of(context).primaryTextTheme.headline.decorationColor, - fontWeight: FontWeight.w500, - fontSize: 14), - validator: sendViewModel.amountValidator - ) - ); - } - ), + hintText: '0.0000', + borderColor: Theme.of(context) + .primaryTextTheme + .headline + .color, + textStyle: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.white), + placeholderTextStyle: TextStyle( + color: Theme.of(context) + .primaryTextTheme + .headline + .decorationColor, + fontWeight: FontWeight.w500, + fontSize: 14), + validator: sendViewModel.amountValidator)); + }), isTemplate - ? Offstage() - : Observer( - builder: (_) => Padding( - padding: EdgeInsets.only(top: 10), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - S.of(context).available_balance + ':', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Theme.of(context).primaryTextTheme.headline.decorationColor - ), - ) - ), - Text( - sendViewModel.balance, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Theme.of(context).primaryTextTheme.headline.decorationColor - ), - ) - ], - ), - ) - ), + ? Offstage() + : Observer( + builder: (_) => Padding( + padding: EdgeInsets.only(top: 10), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + S.of(context).available_balance + ':', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .primaryTextTheme + .headline + .decorationColor), + )), + Text( + sendViewModel.balance, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .primaryTextTheme + .headline + .decorationColor), + ) + ], + ), + )), Padding( padding: const EdgeInsets.only(top: 20), child: BaseTextFormField( @@ -235,8 +253,7 @@ class BaseSendWidget extends StatelessWidget { ], prefixIcon: Padding( padding: EdgeInsets.only(top: 9), - child: Text( - sendViewModel.fiat.title + ':', + child: Text(sendViewModel.fiat.title + ':', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, @@ -244,62 +261,71 @@ class BaseSendWidget extends StatelessWidget { )), ), hintText: '0.00', - borderColor: Theme.of(context).primaryTextTheme.headline.color, + borderColor: Theme.of(context) + .primaryTextTheme + .headline + .color, textStyle: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: Colors.white - ), + color: Colors.white), placeholderTextStyle: TextStyle( - color: Theme.of(context).primaryTextTheme.headline.decorationColor, + color: Theme.of(context) + .primaryTextTheme + .headline + .decorationColor, fontWeight: FontWeight.w500, fontSize: 14), - ) - ), + )), isTemplate - ? Offstage() - : Observer( - builder: (_) => GestureDetector( - onTap: () => _setTransactionPriority(context), - child: Container( - padding: EdgeInsets.only(top: 24), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(S.of(context).send_estimated_fee, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - //color: Theme.of(context).primaryTextTheme.display2.color, - color: Colors.white - )), - Container( - child: Row( - children: [ - Text( - sendViewModel.estimatedFee.toString() + ' ' - + sendViewModel.currency.title, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - //color: Theme.of(context).primaryTextTheme.display2.color, - color: Colors.white - )), - Padding( - padding: EdgeInsets.only(left: 5), - child: Icon( - Icons.arrow_forward_ios, - size: 12, - color: Colors.white,), - ) - ], + ? Offstage() + : Observer( + builder: (_) => GestureDetector( + onTap: () => + _setTransactionPriority(context), + child: Container( + padding: EdgeInsets.only(top: 24), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text(S.of(context).send_estimated_fee, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + //color: Theme.of(context).primaryTextTheme.display2.color, + color: Colors.white)), + Container( + child: Row( + children: [ + Text( + sendViewModel.estimatedFee + .toString() + + ' ' + + sendViewModel + .currency.title, + style: TextStyle( + fontSize: 12, + fontWeight: + FontWeight.w600, + //color: Theme.of(context).primaryTextTheme.display2.color, + color: Colors.white)), + Padding( + padding: + EdgeInsets.only(left: 5), + child: Icon( + Icons.arrow_forward_ios, + size: 12, + color: Colors.white, + ), + ) + ], + ), + ) + ], + ), ), - ) - ], - ), - ), - ) - ) + )) ], ), ) @@ -307,156 +333,158 @@ class BaseSendWidget extends StatelessWidget { ), ), isTemplate - ? Offstage() - : Padding( - padding: EdgeInsets.only( - top: 30, - left: 24, - bottom: 24 - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - S.of(context).send_templates, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Theme.of(context).primaryTextTheme.display4.color + ? Offstage() + : Padding( + padding: EdgeInsets.only(top: 30, left: 24, bottom: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + S.of(context).send_templates, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .primaryTextTheme + .display4 + .color), + ) + ], ), - ) - ], - ), - ), + ), isTemplate - ? Offstage() - : Container( - height: 40, - width: double.infinity, - padding: EdgeInsets.only(left: 24), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - GestureDetector( - onTap: () => Navigator.of(context) - .pushNamed(Routes.sendTemplate), - child: Container( - padding: EdgeInsets.only(left: 1, right: 10), - child: DottedBorder( - borderType: BorderType.RRect, - dashPattern: [6, 4], - color: Theme.of(context).primaryTextTheme.display2.decorationColor, - strokeWidth: 2, - radius: Radius.circular(20), + ? Offstage() + : Container( + height: 40, + width: double.infinity, + padding: EdgeInsets.only(left: 24), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + GestureDetector( + onTap: () => Navigator.of(context) + .pushNamed(Routes.sendTemplate), child: Container( - height: 34, - width: 75, - padding: EdgeInsets.only(left: 10, right: 10), - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(20)), - color: Colors.transparent, - ), - child: Text( - S.of(context).send_new, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: Theme.of(context).primaryTextTheme.display3.color - ), - ), - ) - ), + padding: EdgeInsets.only(left: 1, right: 10), + child: DottedBorder( + borderType: BorderType.RRect, + dashPattern: [6, 4], + color: Theme.of(context) + .primaryTextTheme + .display2 + .decorationColor, + strokeWidth: 2, + radius: Radius.circular(20), + child: Container( + height: 34, + width: 75, + padding: EdgeInsets.only(left: 10, right: 10), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: + BorderRadius.all(Radius.circular(20)), + color: Colors.transparent, + ), + child: Text( + S.of(context).send_new, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .primaryTextTheme + .display3 + .color), + ), + )), + ), + ), + // Observer( + // builder: (_) { + // final templates = sendViewModel.templates; + // final itemCount = templates.length; + + // return ListView.builder( + // scrollDirection: Axis.horizontal, + // shrinkWrap: true, + // physics: NeverScrollableScrollPhysics(), + // itemCount: itemCount, + // itemBuilder: (context, index) { + // final template = templates[index]; + + // return TemplateTile( + // key: UniqueKey(), + // to: template.name, + // amount: template.amount, + // from: template.cryptoCurrency, + // onTap: () { + // _addressController.text = template.address; + // _cryptoAmountController.text = template.amount; + // getOpenaliasRecord(context); + // }, + // onRemove: () { + // showDialog( + // context: context, + // builder: (dialogContext) { + // return AlertWithTwoActions( + // alertTitle: S.of(context).template, + // alertContent: S.of(context).confirm_delete_template, + // leftButtonText: S.of(context).delete, + // rightButtonText: S.of(context).cancel, + // actionLeftButton: () { + // Navigator.of(dialogContext).pop(); + // sendViewModel.sendTemplateStore.remove(template: template); + // sendViewModel.sendTemplateStore.update(); + // }, + // actionRightButton: () => Navigator.of(dialogContext).pop() + // ); + // } + // ); + // }, + // ); + // } + // ); + // } + // ) + ], ), ), - Observer( - builder: (_) { - final templates = sendViewModel.templates; - final itemCount = templates.length; - - return ListView.builder( - scrollDirection: Axis.horizontal, - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: itemCount, - itemBuilder: (context, index) { - final template = templates[index]; - - return TemplateTile( - key: UniqueKey(), - to: template.name, - amount: template.amount, - from: template.cryptoCurrency, - onTap: () { - _addressController.text = template.address; - _cryptoAmountController.text = template.amount; - getOpenaliasRecord(context); - }, - onRemove: () { - showDialog( - context: context, - builder: (dialogContext) { - return AlertWithTwoActions( - alertTitle: S.of(context).template, - alertContent: S.of(context).confirm_delete_template, - leftButtonText: S.of(context).delete, - rightButtonText: S.of(context).cancel, - actionLeftButton: () { - Navigator.of(dialogContext).pop(); - sendViewModel.sendTemplateStore.remove(template: template); - sendViewModel.sendTemplateStore.update(); - }, - actionRightButton: () => Navigator.of(dialogContext).pop() - ); - } - ); - }, - ); - } - ); - } - ) - ], - ), - ), - ) + ) ], ), bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), bottomSection: isTemplate ? PrimaryButton( - onPressed: () { - if (_formKey.currentState.validate()) { - sendViewModel.sendTemplateStore.addTemplate( - name: _nameController.text, - address: _addressController.text, - cryptoCurrency: sendViewModel.currency.title, - amount: _cryptoAmountController.text - ); - sendViewModel.sendTemplateStore.update(); - Navigator.of(context).pop(); - } - }, - text: S.of(context).save, - color: Colors.green, - textColor: Colors.white) + onPressed: () { + if (_formKey.currentState.validate()) { + // sendViewModel.sendTemplateStore.addTemplate( + // name: _nameController.text, + // address: _addressController.text, + // cryptoCurrency: sendViewModel.currency.title, + // amount: _cryptoAmountController.text); + // sendViewModel.sendTemplateStore.update(); + Navigator.of(context).pop(); + } + }, + text: S.of(context).save, + color: Colors.green, + textColor: Colors.white) : Observer(builder: (_) { - return LoadingPrimaryButton( - onPressed: () { - if (_formKey.currentState.validate()) { - print('SENT!!!'); - } - }, - text: S.of(context).send, - color: Palette.blueCraiola, - textColor: Colors.white, - isLoading: sendViewModel.state is TransactionIsCreating || - sendViewModel.state is TransactionCommitting, - isDisabled: - false // FIXME !(syncStore.status is SyncedSyncStatus), - ); - }), + return LoadingPrimaryButton( + onPressed: () { + if (_formKey.currentState.validate()) { + print('SENT!!!'); + } + }, + text: S.of(context).send, + color: Palette.blueCraiola, + textColor: Colors.white, + isLoading: sendViewModel.state is TransactionIsCreating || + sendViewModel.state is TransactionCommitting, + isDisabled: + false // FIXME !(syncStore.status is SyncedSyncStatus), + ); + }), ); } @@ -465,6 +493,13 @@ class BaseSendWidget extends StatelessWidget { return; } + reaction((_) => sendViewModel.sendAll, (bool all) { + if (all) { + _cryptoAmountController.text = S.current.all; + _fiatAmountController.text = null; + } + }); + reaction((_) => sendViewModel.fiatAmount, (String amount) { if (amount != _fiatAmountController.text) { _fiatAmountController.text = amount; @@ -472,6 +507,10 @@ class BaseSendWidget extends StatelessWidget { }); reaction((_) => sendViewModel.cryptoAmount, (String amount) { + if (sendViewModel.sendAll && amount != S.current.all) { + sendViewModel.sendAll = false; + } + if (amount != _cryptoAmountController.text) { _cryptoAmountController.text = amount; } @@ -487,29 +526,7 @@ class BaseSendWidget extends StatelessWidget { final address = _addressController.text; if (sendViewModel.address != address) { - sendViewModel.changeAddress(address); - } - }); - - _fiatAmountController.addListener(() { - final fiatAmount = _fiatAmountController.text; - - if (sendViewModel.fiatAmount != fiatAmount) { - sendViewModel.changeFiatAmount(fiatAmount); - } - }); - - _cryptoAmountController.addListener(() { - final cryptoAmount = _cryptoAmountController.text; - - if (sendViewModel.cryptoAmount != cryptoAmount) { - sendViewModel.changeCryptoAmount(cryptoAmount); - } - }); - - _focusNode.addListener(() { - if (!_focusNode.hasFocus && _addressController.text.isNotEmpty) { - getOpenaliasRecord(context); + sendViewModel.address = address; } }); @@ -519,44 +536,126 @@ class BaseSendWidget extends StatelessWidget { showDialog( context: context, builder: (BuildContext context) { - return AlertDialog( - title: Text(S.of(context).error), - content: Text(state.error), - actions: [ - FlatButton( - child: Text(S.of(context).ok), - onPressed: () => Navigator.of(context).pop()) - ], - ); + return AlertWithOneAction( + alertTitle: S.of(context).error, + alertContent: state.error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); }); }); } if (state is TransactionCreatedSuccessfully) { -// WidgetsBinding.instance.addPostFrameCallback((_) { -// showDialog( -// context: context, -// builder: (BuildContext context) { -// return ConfirmSendingAlert( -// alertTitle: S.of(context).confirm_sending, -// amount: S.of(context).send_amount, -// amountValue: sendStore.pendingTransaction.amount, -// fee: S.of(context).send_fee, -// feeValue: sendStore.pendingTransaction.fee, -// leftButtonText: S.of(context).ok, -// rightButtonText: S.of(context).cancel, -// actionLeftButton: () { -// Navigator.of(context).pop(); -// sendStore.commitTransaction(); -// showDialog( -// context: context, -// builder: (BuildContext context) { -// return SendingAlert(sendStore: sendStore); -// }); -// }, -// actionRightButton: () => Navigator.of(context).pop()); -// }); -// }); + WidgetsBinding.instance.addPostFrameCallback((_) { + showDialog( + context: context, + builder: (BuildContext context) { + return ConfirmSendingAlert( + alertTitle: S.of(context).confirm_sending, + amount: S.of(context).send_amount, + amountValue: + sendViewModel.pendingTransaction.amountFormatted, + fee: S.of(context).send_fee, + feeValue: sendViewModel.pendingTransaction.feeFormatted, + leftButtonText: S.of(context).ok, + rightButtonText: S.of(context).cancel, + actionLeftButton: () { + Navigator.of(context).pop(); + sendViewModel.commitTransaction(); + showDialog( + context: context, + builder: (BuildContext context) { + return Observer(builder: (_) { + final state = sendViewModel.state; + + if (state is TransactionCommitted) { + return Stack( + children: [ + Container( + color: Theme.of(context).backgroundColor, + child: Center( + child: Image.asset( + 'assets/images/birthday_cake.png'), + ), + ), + Center( + child: Padding( + padding: EdgeInsets.only( + top: 220, left: 24, right: 24), + child: Text( + S.of(context).send_success, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Theme.of(context) + .primaryTextTheme + .title + .color, + decoration: TextDecoration.none, + ), + ), + ), + ), + Positioned( + left: 24, + right: 24, + bottom: 24, + child: PrimaryButton( + onPressed: () => + Navigator.of(context).pop(), + text: S.of(context).send_got_it, + color: Colors.blue, + textColor: Colors.white)) + ], + ); + } + + return Stack( + children: [ + Container( + color: Theme.of(context).backgroundColor, + child: Center( + child: Image.asset( + 'assets/images/birthday_cake.png'), + ), + ), + BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 3.0, sigmaY: 3.0), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context) + .backgroundColor + .withOpacity(0.25)), + child: Center( + child: Padding( + padding: EdgeInsets.only(top: 220), + child: Text( + S.of(context).send_sending, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Theme.of(context) + .primaryTextTheme + .title + .color, + decoration: TextDecoration.none, + ), + ), + ), + ), + ), + ) + ], + ); + }); + }); + }, + actionRightButton: () => Navigator.of(context).pop()); + }); + }); } if (state is TransactionCommitted) { @@ -571,22 +670,24 @@ class BaseSendWidget extends StatelessWidget { } Future getOpenaliasRecord(BuildContext context) async { - final isOpenalias = await sendViewModel.isOpenaliasRecord(_addressController.text); + // final isOpenalias = + // await sendViewModel.isOpenaliasRecord(_addressController.text); - if (isOpenalias) { - _addressController.text = sendViewModel.recordAddress; + // if (isOpenalias) { + // _addressController.text = sendViewModel.recordAddress; - await showDialog( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.of(context).openalias_alert_title, - alertContent: S.of(context).openalias_alert_content(sendViewModel.recordName), - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop() - ); - }); - } + // await showDialog( + // context: context, + // builder: (BuildContext context) { + // return AlertWithOneAction( + // alertTitle: S.of(context).openalias_alert_title, + // alertContent: S + // .of(context) + // .openalias_alert_content(sendViewModel.recordName), + // buttonText: S.of(context).ok, + // buttonAction: () => Navigator.of(context).pop()); + // }); + // } } Future _setTransactionPriority(BuildContext context) async { @@ -595,14 +696,14 @@ class BaseSendWidget extends StatelessWidget { await showDialog( builder: (_) => Picker( - items: items, - selectedAtIndex: selectedItem, - title: S.of(context).please_select, - mainAxisAlignment: MainAxisAlignment.center, - onItemSelected: (TransactionPriority priority) => - sendViewModel.setTransactionPriority(priority), - isAlwaysShowScrollThumb: true, - ), + items: items, + selectedAtIndex: selectedItem, + title: S.of(context).please_select, + mainAxisAlignment: MainAxisAlignment.center, + onItemSelected: (TransactionPriority priority) => null, + // sendViewModel.setTransactionPriority(priority), + isAlwaysShowScrollThumb: true, + ), context: context); } -} \ No newline at end of file +} diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 0251fbce8..f27943be4 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -138,7 +138,7 @@ abstract class SettingsStoreBase with Store { sharedPreferences.getBool(shouldSaveRecipientAddressKey); final allowBiometricalAuthentication = sharedPreferences.getBool(allowBiometricalAuthenticationKey) ?? false; - final savedDarkTheme = sharedPreferences.getBool(currentDarkTheme) ?? true; + final savedDarkTheme = sharedPreferences.getBool(currentDarkTheme) ?? false; final actionListDisplayMode = ObservableList(); actionListDisplayMode.addAll(deserializeActionlistDisplayModes( sharedPreferences.getInt(displayActionListModeKey) ?? diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 44430a073..7ab3a1044 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/core/wallet_base.dart'; import 'package:cake_wallet/src/domain/common/crypto_currency.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; import 'package:cake_wallet/src/domain/exchange/exchange_provider.dart'; import 'package:cake_wallet/src/domain/exchange/limits.dart'; import 'package:cake_wallet/src/domain/exchange/trade.dart'; @@ -25,22 +26,18 @@ part 'exchange_view_model.g.dart'; class ExchangeViewModel = ExchangeViewModelBase with _$ExchangeViewModel; abstract class ExchangeViewModelBase with Store { - ExchangeViewModelBase({ - this.wallet, - this.trades, - this.exchangeTemplateStore, - this.tradesStore}) { - + ExchangeViewModelBase( + {this.wallet, + this.trades, + this.exchangeTemplateStore, + this.tradesStore}) { providerList = [ XMRTOExchangeProvider(), ChangeNowExchangeProvider(), MorphTokenExchangeProvider(trades: trades) ]; - provider = providerList[ 0 ]; - - depositCurrency = CryptoCurrency.xmr; - receiveCurrency = CryptoCurrency.btc; + _initialPairBasedOnWallet(); isDepositAddressEnabled = !(depositCurrency == wallet.currency); isReceiveAddressEnabled = !(receiveCurrency == wallet.currency); depositAmount = ''; @@ -50,6 +47,7 @@ abstract class ExchangeViewModelBase with Store { limitsState = LimitsInitialState(); tradeState = ExchangeTradeStateInitial(); _cryptoNumberFormat = NumberFormat()..maximumFractionDigits = 12; + provider = providersForCurrentPair().first; loadLimits(); } @@ -140,8 +138,11 @@ abstract class ExchangeViewModelBase with Store { provider .calculateAmount( - from: depositCurrency, to: receiveCurrency, amount: _amount) - .then((amount) => _cryptoNumberFormat.format(amount).toString().replaceAll(RegExp("\\,"), "")) + from: depositCurrency, to: receiveCurrency, amount: _amount) + .then((amount) => _cryptoNumberFormat + .format(amount) + .toString() + .replaceAll(RegExp('\\,'), '')) .then((amount) => depositAmount = amount); } @@ -158,8 +159,11 @@ abstract class ExchangeViewModelBase with Store { final _amount = double.parse(amount); provider .calculateAmount( - from: depositCurrency, to: receiveCurrency, amount: _amount) - .then((amount) => _cryptoNumberFormat.format(amount).toString().replaceAll(RegExp("\\,"), "")) + from: depositCurrency, to: receiveCurrency, amount: _amount) + .then((amount) => _cryptoNumberFormat + .format(amount) + .toString() + .replaceAll(RegExp('\\,'), '')) .then((amount) => receiveAmount = amount); } @@ -217,11 +221,13 @@ abstract class ExchangeViewModelBase with Store { if (limitsState is LimitsLoadedSuccessfully && amount != null) { if (double.parse(amount) < limits.min) { - tradeState = TradeIsCreatedFailure(error: S.current.error_text_minimal_limit('${provider.description}', - '${limits.min}', currency.toString())); + tradeState = TradeIsCreatedFailure( + error: S.current.error_text_minimal_limit('${provider.description}', + '${limits.min}', currency.toString())); } else if (limits.max != null && double.parse(amount) > limits.max) { - tradeState = TradeIsCreatedFailure(error: S.current.error_text_maximum_limit('${provider.description}', - '${limits.max}', currency.toString())); + tradeState = TradeIsCreatedFailure( + error: S.current.error_text_maximum_limit('${provider.description}', + '${limits.max}', currency.toString())); } else { try { tradeState = TradeIsCreating(); @@ -235,9 +241,10 @@ abstract class ExchangeViewModelBase with Store { } } } else { - tradeState = TradeIsCreatedFailure(error: S.current.error_text_limits_loading_failed('${provider.description}')); + tradeState = TradeIsCreatedFailure( + error: S.current + .error_text_limits_loading_failed('${provider.description}')); } - } @action @@ -261,9 +268,9 @@ abstract class ExchangeViewModelBase with Store { {CryptoCurrency from, CryptoCurrency to}) { final providers = providerList .where((provider) => provider.pairList - .where((pair) => - pair.from == depositCurrency && pair.to == receiveCurrency) - .isNotEmpty) + .where((pair) => + pair.from == depositCurrency && pair.to == receiveCurrency) + .isNotEmpty) .toList(); return providers; @@ -272,12 +279,12 @@ abstract class ExchangeViewModelBase with Store { void _onPairChange() { final isPairExist = provider.pairList .where((pair) => - pair.from == depositCurrency && pair.to == receiveCurrency) + pair.from == depositCurrency && pair.to == receiveCurrency) .isNotEmpty; if (!isPairExist) { final provider = - _providerForPair(from: depositCurrency, to: receiveCurrency); + _providerForPair(from: depositCurrency, to: receiveCurrency); if (provider != null) { changeProvider(provider: provider); @@ -295,4 +302,18 @@ abstract class ExchangeViewModelBase with Store { return providers.isNotEmpty ? providers[0] : null; } -} \ No newline at end of file + void _initialPairBasedOnWallet() { + switch (wallet.type) { + case WalletType.monero: + depositCurrency = CryptoCurrency.xmr; + receiveCurrency = CryptoCurrency.btc; + break; + case WalletType.bitcoin: + depositCurrency = CryptoCurrency.btc; + receiveCurrency = CryptoCurrency.xmr; + break; + default: + break; + } + } +} diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 81be508e6..b03ca02d1 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -1,7 +1,8 @@ -import 'package:cake_wallet/src/domain/common/calculate_fiat_amount.dart'; -import 'package:cake_wallet/store/dashboard/fiat_convertation_store.dart'; import 'package:intl/intl.dart'; import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/core/template_validator.dart'; +import 'package:cake_wallet/src/domain/common/calculate_fiat_amount.dart'; +import 'package:cake_wallet/store/dashboard/fiat_convertation_store.dart'; import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/core/amount_validator.dart'; import 'package:cake_wallet/core/pending_transaction.dart'; @@ -28,7 +29,7 @@ abstract class SendViewModelBase with Store { : state = InitialSendViewModelState(), _cryptoNumberFormat = NumberFormat()..maximumFractionDigits = 12, // FIXME: need to be based on wallet type. - all = false; + sendAll = false; @observable SendViewModelState state; @@ -43,7 +44,7 @@ abstract class SendViewModelBase with Store { String address; @observable - bool all; + bool sendAll; FiatCurrency get fiat => _settingsStore.fiatCurrency; @@ -59,6 +60,8 @@ abstract class SendViewModelBase with Store { Validator get addressValidator => AddressValidator(type: _wallet.currency); + Validator get templateValidator => TemplateValidator(); + PendingTransaction pendingTransaction; @computed @@ -83,7 +86,7 @@ abstract class SendViewModelBase with Store { final NumberFormat _cryptoNumberFormat; @action - void setAll() => all = true; + void setSendAll() => sendAll = true; @action void reset() { @@ -118,7 +121,7 @@ abstract class SendViewModelBase with Store { void setCryptoAmount(String amount) { // FIXME: hardcoded value. if (amount.toUpperCase() != 'ALL') { - all = false; + sendAll = false; } cryptoAmount = amount; @@ -162,7 +165,7 @@ abstract class SendViewModelBase with Store { Object _credentials() { final amount = - !all ? double.parse(cryptoAmount.replaceAll(',', '.')) : null; + !sendAll ? double.parse(cryptoAmount.replaceAll(',', '.')) : null; switch (_wallet.type) { case WalletType.bitcoin: From 70ff011fc6943693dc699b8309578488eb0d57cf Mon Sep 17 00:00:00 2001 From: M Date: Mon, 7 Sep 2020 18:13:39 +0300 Subject: [PATCH 8/9] Merge redesign part 3. --- lib/bitcoin/electrum.dart | 2 +- lib/core/contact_service.dart | 2 +- lib/di.dart | 6 +- lib/src/domain/common/contact.dart | 6 +- lib/src/domain/common/node.dart | 3 +- lib/src/screens/nodes/nodes_list_page.dart | 2 +- lib/store/settings_store.dart | 8 +- lib/utils/item_cell.dart | 11 ++ lib/utils/mobx.dart | 112 ++++++++++++------ .../contact_list/contact_list_view_model.dart | 22 +++- .../contact_list/contact_view_model.dart | 18 ++- .../node_list/node_list_view_model.dart | 30 ++--- .../wallet_address_list_view_model.dart | 10 +- 13 files changed, 156 insertions(+), 76 deletions(-) create mode 100644 lib/utils/item_cell.dart diff --git a/lib/bitcoin/electrum.dart b/lib/bitcoin/electrum.dart index bd1ac09ac..5f034bc6a 100644 --- a/lib/bitcoin/electrum.dart +++ b/lib/bitcoin/electrum.dart @@ -87,7 +87,7 @@ class ElectrumClient { Future ping() async { try { - await callWithTimeout(method: 'server.ping'); + // await callWithTimeout(method: 'server.ping'); _setIsConnected(true); } on RequestFailedTimeoutException catch (_) { _setIsConnected(false); diff --git a/lib/core/contact_service.dart b/lib/core/contact_service.dart index 73a5bed27..1141a1d06 100644 --- a/lib/core/contact_service.dart +++ b/lib/core/contact_service.dart @@ -12,7 +12,7 @@ class ContactService { Future add(Contact contact) async { await contactSource.add(contact); - contactListStore.contacts.add(contact); + // contactListStore.contacts.add(contact); } Future update(Contact contact) async { diff --git a/lib/di.dart b/lib/di.dart index 7c08d9e49..1b533d69c 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -294,11 +294,13 @@ Future setup( getIt.registerFactoryParam( (Contact contact, _) => ContactViewModel( - getIt.get(), getIt.get().wallet, + contactSource, getIt.get().wallet, contact: contact)); getIt.registerFactory(() => ContactListViewModel( - getIt.get().contactListStore, getIt.get())); + getIt.get().contactListStore, + getIt.get(), + contactSource)); getIt.registerFactory( () => ContactListPage(getIt.get())); diff --git a/lib/src/domain/common/contact.dart b/lib/src/domain/common/contact.dart index faa4f8ecf..4aca925eb 100644 --- a/lib/src/domain/common/contact.dart +++ b/lib/src/domain/common/contact.dart @@ -1,11 +1,12 @@ import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:cake_wallet/src/domain/common/crypto_currency.dart'; +import 'package:cake_wallet/utils/mobx.dart'; part 'contact.g.dart'; @HiveType(typeId: 0) -class Contact extends HiveObject { +class Contact extends HiveObject with Keyable { Contact({@required this.name, @required this.address, CryptoCurrency type}) : raw = type?.raw; @@ -22,6 +23,9 @@ class Contact extends HiveObject { CryptoCurrency get type => CryptoCurrency.deserialize(raw: raw); + @override + dynamic get keyIndex => key; + @override bool operator ==(Object o) => o is Contact && o.key == key; diff --git a/lib/src/domain/common/node.dart b/lib/src/domain/common/node.dart index b47f002e9..931eabf6c 100644 --- a/lib/src/domain/common/node.dart +++ b/lib/src/domain/common/node.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/utils/mobx.dart'; import 'package:flutter/foundation.dart'; import 'dart:convert'; import 'package:http/http.dart' as http; @@ -8,7 +9,7 @@ import 'package:cake_wallet/src/domain/common/digest_request.dart'; part 'node.g.dart'; @HiveType(typeId: 1) -class Node extends HiveObject { +class Node extends HiveObject with Keyable { Node( {@required this.uri, @required WalletType type, diff --git a/lib/src/screens/nodes/nodes_list_page.dart b/lib/src/screens/nodes/nodes_list_page.dart index ab715058c..b5a6dfba1 100644 --- a/lib/src/screens/nodes/nodes_list_page.dart +++ b/lib/src/screens/nodes/nodes_list_page.dart @@ -108,7 +108,7 @@ class NodeListPage extends BasePage { }); final dismissibleRow = Dismissible( - key: Key('${node.value.key}'), + key: Key('${node.keyIndex}'), confirmDismiss: (direction) async { return await showDialog( context: context, diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index f27943be4..87eb8f4d9 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -44,9 +44,9 @@ abstract class SettingsStoreBase with Store { languageCode = initialLanguageCode; currentLocale = initialCurrentLocale; itemHeaders = {}; + this.nodes = ObservableMap.of(nodes); _sharedPreferences = sharedPreferences; _nodeSource = nodeSource; - _nodes = nodes; } static const currentNodeIdKey = 'current_node_id'; @@ -101,9 +101,9 @@ abstract class SettingsStoreBase with Store { SharedPreferences _sharedPreferences; Box _nodeSource; - Map _nodes; + ObservableMap nodes; - Node getCurrentNode(WalletType walletType) => _nodes[walletType]; + Node getCurrentNode(WalletType walletType) => nodes[walletType]; Future setCurrentNode(Node node, WalletType walletType) async { switch (walletType) { @@ -118,7 +118,7 @@ abstract class SettingsStoreBase with Store { break; } - _nodes[walletType] = node; + nodes[walletType] = node; } static Future load( diff --git a/lib/utils/item_cell.dart b/lib/utils/item_cell.dart new file mode 100644 index 000000000..4aca57b46 --- /dev/null +++ b/lib/utils/item_cell.dart @@ -0,0 +1,11 @@ +import 'package:flutter/foundation.dart'; +import 'package:cake_wallet/utils/mobx.dart'; + +class ItemCell with Keyable { + ItemCell(this.value, {@required this.isSelected, @required dynamic key}) { + keyIndex = key; + } + + final Item value; + final bool isSelected; +} diff --git a/lib/utils/mobx.dart b/lib/utils/mobx.dart index 0065a939d..6a8413aff 100644 --- a/lib/utils/mobx.dart +++ b/lib/utils/mobx.dart @@ -1,46 +1,90 @@ +import 'dart:async'; + +import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; -void connectDifferent( +mixin Keyable { + dynamic keyIndex; +} + +void connectDifferent( ObservableList source, ObservableList dest, Y Function(T) transform, {bool Function(T) filter}) { source.observe((ListChange change) { -// switch (change.type) { -// case OperationType.add: -// final _values = change.added; -// Iterable values; + change.elementChanges.forEach((change) { + switch (change.type) { + case OperationType.add: + if (filter?.call(change.newValue as T) ?? true) { + dest.add(transform(change.newValue as T)); + } + break; + case OperationType.remove: + // Hive could has equal index and key + dest.removeWhere( + (elem) => elem.keyIndex == (change.oldValue.key ?? change.index)); + break; + case OperationType.update: + for (var i = 0; i < dest.length; i++) { + final item = dest[i]; -// if (filter != null) { -// values = _values.where(filter); -// } - -// dest.addAll(values.map((e) => transform(e))); -// break; -// case OperationType.remove: -// change.removed.forEach((element) { -// dest.remove(element); -// }); - -// // dest.removeAt(change.index); -// break; -// case OperationType.update: -// // change.index -// break; -// } + if (item.keyIndex == change.newValue.key) { + dest[i] = transform(change.newValue as T); + } + } + break; + } + }); }); } -void connect(ObservableList source, ObservableList dest) { +void connect( + ObservableList source, ObservableList dest) { source.observe((ListChange change) { -// switch (change.type) { -// case OperationType.add: -// dest.addAll(change.added); -// break; -// case OperationType.remove: -// dest.removeAt(change.index); -// break; -// case OperationType.update: -// // change.index -// break; -// } + source.observe((ListChange change) { + change.elementChanges.forEach((change) { + switch (change.type) { + case OperationType.add: + // if (filter?.call(change.newValue as T) ?? true) { + dest.add(change.newValue as T); + // } + break; + case OperationType.remove: + // Hive could has equal index and key + dest.removeWhere((elem) => + elem.keyIndex == (change.oldValue.key ?? change.index)); + break; + case OperationType.update: + for (var i = 0; i < dest.length; i++) { + final item = dest[i]; + + if (item.keyIndex == change.newValue.key) { + dest[i] = change.newValue as T; + } + } + break; + } + }); + }); + }); +} + +StreamSubscription bindBox( + Box source, ObservableList dest) { + return source.watch().listen((event) { + if (event.deleted) { + dest.removeWhere((el) => el.keyIndex == event.key); + } + + final dynamic value = event.value; + + if (value is T) { + final elIndex = dest.indexWhere((el) => el.keyIndex == value.keyIndex); + + if (elIndex > -1) { + dest[elIndex] = value; + } else { + dest.add(value); + } + } }); } diff --git a/lib/view_model/contact_list/contact_list_view_model.dart b/lib/view_model/contact_list/contact_list_view_model.dart index a3f526a86..87917b25a 100644 --- a/lib/view_model/contact_list/contact_list_view_model.dart +++ b/lib/view_model/contact_list/contact_list_view_model.dart @@ -1,20 +1,34 @@ +import 'dart:async'; + +import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/core/contact_service.dart'; import 'package:cake_wallet/store/contact_list_store.dart'; import 'package:cake_wallet/src/domain/common/contact.dart'; +import 'package:cake_wallet/utils/mobx.dart'; part 'contact_list_view_model.g.dart'; -class ContactListViewModel = ContactListViewModelBase with _$ContactListViewModel; +class ContactListViewModel = ContactListViewModelBase + with _$ContactListViewModel; abstract class ContactListViewModelBase with Store { - ContactListViewModelBase(this.addressBookStore, this.contactService); + ContactListViewModelBase( + this.addressBookStore, this.contactService, this.contactSource) { + _subscription = bindBox(contactSource, addressBookStore.contacts); + } final ContactListStore addressBookStore; final ContactService contactService; + final Box contactSource; - @computed ObservableList get contacts => addressBookStore.contacts; + StreamSubscription _subscription; + + void dispose() { + _subscription.cancel(); + } + Future delete(Contact contact) async => contactService.delete(contact); -} \ No newline at end of file +} diff --git a/lib/view_model/contact_list/contact_view_model.dart b/lib/view_model/contact_list/contact_view_model.dart index addb56606..780f5ca66 100644 --- a/lib/view_model/contact_list/contact_view_model.dart +++ b/lib/view_model/contact_list/contact_view_model.dart @@ -1,3 +1,4 @@ +import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/core/wallet_base.dart'; import 'package:cake_wallet/core/contact_service.dart'; @@ -10,7 +11,7 @@ part 'contact_view_model.g.dart'; class ContactViewModel = ContactViewModelBase with _$ContactViewModel; abstract class ContactViewModelBase with Store { - ContactViewModelBase(this._contactService, this._wallet, {Contact contact}) + ContactViewModelBase(this._contacts, this._wallet, {Contact contact}) : state = InitialContactViewModelState(), currencies = CryptoCurrency.all, _contact = contact { @@ -33,12 +34,14 @@ abstract class ContactViewModelBase with Store { @computed bool get isReady => - (name?.isNotEmpty ?? false) && (currency?.toString()?.isNotEmpty ?? false) - && (address?.isNotEmpty ?? false); + (name?.isNotEmpty ?? false) && + (currency?.toString()?.isNotEmpty ?? false) && + (address?.isNotEmpty ?? false); final List currencies; - final ContactService _contactService; + // final ContactService _contactService; final WalletBase _wallet; + final Box _contacts; final Contact _contact; @action @@ -57,10 +60,13 @@ abstract class ContactViewModelBase with Store { _contact.name = name; _contact.address = address; _contact.updateCryptoCurrency(currency: currency); - await _contactService.update(_contact); + await _contacts.put(_contact.key, _contact); + // await _contactService.update(_contact); } else { - await _contactService + await _contacts .add(Contact(name: name, address: address, type: currency)); + // await _contactService + // .add(Contact(name: name, address: address, type: currency)); } state = ContactSavingSuccessfully(); diff --git a/lib/view_model/node_list/node_list_view_model.dart b/lib/view_model/node_list/node_list_view_model.dart index 73f2b934e..4a68d23aa 100644 --- a/lib/view_model/node_list/node_list_view_model.dart +++ b/lib/view_model/node_list/node_list_view_model.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/utils/item_cell.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; @@ -14,13 +15,6 @@ part 'node_list_view_model.g.dart'; class NodeListViewModel = NodeListViewModelBase with _$NodeListViewModel; -class ItemCell { - ItemCell(this.value, {@required this.isSelected}); - - final Item value; - final bool isSelected; -} - abstract class NodeListViewModelBase with Store { NodeListViewModelBase( this._nodeListStore, this._nodeSource, this._wallet, this._settingsStore) @@ -29,16 +23,16 @@ abstract class NodeListViewModelBase with Store { final values = _nodeListStore.nodes; nodes.clear(); nodes.addAll(values.where((Node node) => node.type == _wallet.type).map( - (Node val) => - ItemCell(val, isSelected: val.key == currentNode.key))); + (Node val) => ItemCell(val, + isSelected: val.key == currentNode.key, key: val.key))); connectDifferent( _nodeListStore.nodes, nodes, - (Node val) => - ItemCell(val, isSelected: val.key == currentNode.key), - filter: (Node val) { - return val.type == _wallet.type; - }); + (Node val) => ItemCell(val, + isSelected: val.key == currentNode.key, key: val.key), + filter: (Node val) => val.type == _wallet.type); + reaction((_) => _settingsStore.nodes[_wallet.type], + (Node _) => _updateCurrentNode()); } ObservableList> nodes; @@ -66,17 +60,18 @@ abstract class NodeListViewModelBase with Store { break; } - await _wallet.connectToNode(node: node); + await setAsCurrent(node); } Future delete(Node node) async => _nodeSource.delete(node.key); Future setAsCurrent(Node node) async { - await _wallet.connectToNode(node: node); await _settingsStore.setCurrentNode(node, _wallet.type); _updateCurrentNode(); + await _wallet.connectToNode(node: node); } + @action void _updateCurrentNode() { final currentNode = _settingsStore.getCurrentNode(_wallet.type); @@ -85,7 +80,8 @@ abstract class NodeListViewModelBase with Store { final isSelected = item.value.key == currentNode.key; if (item.isSelected != isSelected) { - nodes[i] = ItemCell(item.value, isSelected: isSelected); + nodes[i] = ItemCell(item.value, + isSelected: isSelected, key: item.keyIndex); } } } diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index 4f2e3662c..0b02bc08a 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -67,7 +67,8 @@ abstract class WalletAddressListViewModelBase with Store { WalletType get type => _wallet.type; @computed - WalletAddressListItem get address => WalletAddressListItem(address: _wallet.address); + WalletAddressListItem get address => + WalletAddressListItem(address: _wallet.address); @computed PaymentURI get uri { @@ -100,15 +101,16 @@ abstract class WalletAddressListViewModelBase with Store { } if (wallet is BitcoinWallet) { - final bitcoinAddresses = wallet.addresses.map( - (addr) => WalletAddressListItem(name: addr.label, address: addr.address)); + final bitcoinAddresses = wallet.addresses.map((addr) => + WalletAddressListItem(name: addr.label, address: addr.address)); addressList.addAll(bitcoinAddresses); } return addressList; } - set address(WalletAddressListItem address) => _wallet.address = address.address; + set address(WalletAddressListItem address) => + _wallet.address = address.address; bool hasAccounts; From 16fbd9250dc55724efaab678530a1ea5d108e88f Mon Sep 17 00:00:00 2001 From: M Date: Wed, 9 Sep 2020 17:13:44 +0300 Subject: [PATCH 9/9] Merge redesign part 4. --- lib/bitcoin/electrum.dart | 24 +++++++++++++++---- lib/src/screens/dashboard/dashboard_page.dart | 15 +++--------- .../dashboard/widgets/action_button.dart | 23 +++++++----------- 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/lib/bitcoin/electrum.dart b/lib/bitcoin/electrum.dart index 5f034bc6a..69d1c332f 100644 --- a/lib/bitcoin/electrum.dart +++ b/lib/bitcoin/electrum.dart @@ -6,6 +6,16 @@ import 'package:cake_wallet/bitcoin/script_hash.dart'; import 'package:flutter/foundation.dart'; import 'package:rxdart/rxdart.dart'; +class UriParseException implements Exception { + UriParseException(this.uri); + + final String uri; + + @override + String toString() => + 'Cannot parse host and port from uri. Invalid uri format. Uri: $uri'; +} + String jsonrpcparams(List params) { final _params = params?.map((val) => '"${val.toString()}"')?.join(','); return '[$_params]'; @@ -40,9 +50,14 @@ class ElectrumClient { Timer _aliveTimer; Future connectToUri(String uri) async { - final _uri = Uri.parse(uri); - final host = _uri.scheme; - final port = int.parse(_uri.path); + final splittedUri = uri.split(':'); + + if (splittedUri.length != 2) { + throw UriParseException(uri); + } + + final host = splittedUri.first; + final port = int.parse(splittedUri.last); await connect(host: host, port: port); } @@ -51,7 +66,8 @@ class ElectrumClient { await socket?.close(); } catch (_) {} - socket = await SecureSocket.connect(host, port, timeout: connectionTimeout); + socket = await SecureSocket.connect(host, port, + timeout: connectionTimeout, onBadCertificate: (_) => true); _setIsConnected(true); socket.listen((Uint8List event) { diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index df93362b4..d3ae08cbe 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -88,9 +88,7 @@ class DashboardPage extends BasePage { child: PageView.builder( controller: controller, itemCount: pages.length, - itemBuilder: (context, index) { - return pages[index]; - })), + itemBuilder: (context, index) => pages[index])), Padding( padding: EdgeInsets.only(bottom: 24), child: SmoothPageIndicator( @@ -106,8 +104,9 @@ class DashboardPage extends BasePage { )), Container( width: double.infinity, - padding: EdgeInsets.only(left: 45, right: 45, bottom: 24), + padding: EdgeInsets.only(left: 44, right: 0, bottom: 24), child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( child: ActionButton( @@ -122,14 +121,6 @@ class DashboardPage extends BasePage { image: exchangeImage, title: S.of(context).exchange, route: Routes.exchange), - ), - Flexible( - child: ActionButton( - image: receiveImage, - title: S.of(context).receive, - route: Routes.receive, - alignment: Alignment.centerRight, - ), ) ], ), diff --git a/lib/src/screens/dashboard/widgets/action_button.dart b/lib/src/screens/dashboard/widgets/action_button.dart index 4ab08d97e..3cf1d6ded 100644 --- a/lib/src/screens/dashboard/widgets/action_button.dart +++ b/lib/src/screens/dashboard/widgets/action_button.dart @@ -1,12 +1,11 @@ import 'package:flutter/material.dart'; -class ActionButton extends StatelessWidget{ - ActionButton({ - @required this.image, - @required this.title, - @required this.route, - this.alignment = Alignment.center - }); +class ActionButton extends StatelessWidget { + ActionButton( + {@required this.image, + @required this.title, + @required this.route, + this.alignment = Alignment.center}); final Image image; final String title; @@ -35,20 +34,16 @@ class ActionButton extends StatelessWidget{ width: 60, alignment: Alignment.center, decoration: BoxDecoration( - color: Theme.of(context).buttonColor, - shape: BoxShape.circle), + color: Theme.of(context).buttonColor, shape: BoxShape.circle), child: image, ), ), Text( title, - style: TextStyle( - fontSize: 14, - color: Colors.white - ), + style: TextStyle(fontSize: 14, color: Colors.white), ) ], ), ); } -} \ No newline at end of file +}