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/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/assets/images/2.0x/back_vector.png b/assets/images/2.0x/back_vector.png new file mode 100644 index 000000000..8d9239aa9 Binary files /dev/null and b/assets/images/2.0x/back_vector.png differ diff --git a/assets/images/2.0x/question_mark.png b/assets/images/2.0x/question_mark.png new file mode 100644 index 000000000..37f5edfcc Binary files /dev/null and b/assets/images/2.0x/question_mark.png differ diff --git a/assets/images/3.0x/back_vector.png b/assets/images/3.0x/back_vector.png new file mode 100644 index 000000000..d01030e5d Binary files /dev/null and b/assets/images/3.0x/back_vector.png differ diff --git a/assets/images/3.0x/question_mark.png b/assets/images/3.0x/question_mark.png new file mode 100644 index 000000000..7b08436f2 Binary files /dev/null and b/assets/images/3.0x/question_mark.png differ diff --git a/assets/images/back_vector.png b/assets/images/back_vector.png new file mode 100644 index 000000000..8717a3463 Binary files /dev/null and b/assets/images/back_vector.png differ diff --git a/assets/images/question_mark.png b/assets/images/question_mark.png new file mode 100644 index 000000000..8a4b4fa70 Binary files /dev/null and b/assets/images/question_mark.png differ 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 959c597e1..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): @@ -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`) @@ -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 54901a5a7..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; @@ -373,7 +372,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CURRENT_PROJECT_VERSION = 12; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -387,7 +386,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"; @@ -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; @@ -509,7 +506,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CURRENT_PROJECT_VERSION = 12; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -523,7 +520,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 +537,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CURRENT_PROJECT_VERSION = 12; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -554,7 +551,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..65000c894 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -19,9 +19,11 @@ CFBundleSignature ???? CFBundleVersion - $(FLUTTER_BUILD_NUMBER) + $(CURRENT_PROJECT_VERSION) LSRequiresIPhoneOS + NSCameraUsageDescription + Cake Wallet requires access to your phone’s camera. UILaunchStoryboardName LaunchScreen UIMainStoryboardFile 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..4198ca9e2 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,42 +15,97 @@ class BitcoinTransactionInfo extends TransactionInfo { @required int amount, @required TransactionDirection direction, @required bool isPending, - @required DateTime date}) { + @required DateTime date, + @required int confirmations}) { this.height = height; this.amount = amount; this.direction = direction; this.date = date; this.isPending = isPending; + this.confirmations = confirmations; } - 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,7 +115,8 @@ 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; @@ -66,7 +124,8 @@ class BitcoinTransactionInfo extends TransactionInfo { 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..4fc46f8fa 100644 --- a/lib/bitcoin/bitcoin_wallet.dart +++ b/lib/bitcoin/bitcoin_wallet.dart @@ -1,10 +1,21 @@ +import 'dart:async'; 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 +31,41 @@ 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'; +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 { + 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 +77,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 +123,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 +147,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 +163,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 +179,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 +203,9 @@ 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('transactionHistory update finished!')); + _subscribeForUpdates(); await _updateBalance(); syncStatus = SyncedSyncStatus(); } catch (e) { @@ -197,39 +219,103 @@ 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); + await eclient.connectToUri(node.uri); + eclient.onConnectionStatusChange = (bool isConnected) { + if (!isConnected) { + syncStatus = LostConnectionSyncStatus(); + } + }; 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 +323,31 @@ 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 { + 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 +362,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/bitcoin_wallet_service.dart b/lib/bitcoin/bitcoin_wallet_service.dart index d5827df18..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'; @@ -49,10 +48,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 70a01a293..bd1ac09ac 100644 --- a/lib/bitcoin/electrum.dart +++ b/lib/bitcoin/electrum.dart @@ -1,17 +1,19 @@ 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'; 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}); @@ -31,58 +33,66 @@ class ElectrumClient { bool get isConnected => _isConnected; Socket socket; + void Function(bool) onConnectionStatusChange; int _id; final Map _tasks; 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); + } - final start = DateTime.now(); + Future connect({@required String host, @required int port}) async { + try { + await socket?.close(); + } catch (_) {} socket = await SecureSocket.connect(host, port, timeout: connectionTimeout); + _setIsConnected(true); - _isConnected = 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; + 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}'); + print(error.toString()); + _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) { @@ -93,18 +103,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,24 +122,91 @@ 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) { if (result is String) { @@ -163,11 +240,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, @@ -190,6 +267,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)); @@ -206,7 +302,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; @@ -218,18 +316,30 @@ 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; } } + + 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/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 8ed81e0a6..cee4fe8e1 100644 --- a/lib/core/amount_validator.dart +++ b/lib/core/amount_validator.dart @@ -14,10 +14,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}|ALL)\$'; + 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}|ALL)\$'; + 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 dabb7c4e4..7c08d9e49 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -4,6 +4,8 @@ import 'package:cake_wallet/src/domain/common/node.dart'; import 'package:cake_wallet/src/domain/exchange/trade.dart'; import 'package:cake_wallet/src/screens/contact/contact_list_page.dart'; import 'package:cake_wallet/src/screens/contact/contact_page.dart'; +import 'package:cake_wallet/src/screens/exchange_trade/exchange_confirm_page.dart'; +import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_page.dart'; import 'package:cake_wallet/src/screens/nodes/node_create_or_edit_page.dart'; import 'package:cake_wallet/src/screens/nodes/nodes_list_page.dart'; import 'package:cake_wallet/src/screens/seed/wallet_seed_page.dart'; @@ -27,9 +29,13 @@ import 'package:cake_wallet/src/screens/receive/receive_page.dart'; 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/theme_changer_store.dart'; import 'package:cake_wallet/store/wallet_list_store.dart'; +import 'package:cake_wallet/utils/mobx.dart'; +import 'package:cake_wallet/theme_changer.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/exchange/exchange_trade_view_model.dart'; import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart'; import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart'; @@ -39,7 +45,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'; @@ -81,8 +87,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; } @@ -114,10 +132,8 @@ Future setup( getIt.registerSingleton( ContactService(contactSource, getIt.get().contactListStore)); getIt.registerSingleton(TradesStore( - tradesSource: tradesSource, - settingsStore: getIt.get())); - getIt.registerSingleton( - TradeFilterStore(wallet: getIt.get().wallet)); + tradesSource: tradesSource, settingsStore: getIt.get())); + getIt.registerSingleton(TradeFilterStore()); getIt.registerSingleton(TransactionFilterStore()); getIt.registerSingleton(FiatConvertationStore()); getIt.registerSingleton( @@ -155,20 +171,17 @@ 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() - )); + getIt.registerFactory(() => DashboardViewModel( + balanceViewModel: getIt.get(), + appStore: getIt.get(), + tradesStore: getIt.get(), + tradeFilterStore: getIt.get(), + transactionFilterStore: getIt.get())); getIt.registerFactory(() => AuthService( secureStorage: getIt.get(), @@ -196,10 +209,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())); @@ -213,17 +225,17 @@ Future setup( addressEditOrCreateViewModel: getIt.get(param1: item))); + // getIt.get() getIt.registerFactory(() => SendViewModel( getIt.get().wallet, getIt.get().settingsStore, - getIt.get(), - getIt.get())); + getIt.get())); getIt.registerFactory( () => SendPage(sendViewModel: getIt.get())); - getIt.registerFactory( - () => SendTemplatePage(sendViewModel: getIt.get())); + // getIt.registerFactory( + // () => SendTemplatePage(sendViewModel: getIt.get())); getIt.registerFactory(() => WalletListViewModel( walletInfoSource, getIt.get(), getIt.get())); @@ -260,8 +272,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())); @@ -292,10 +306,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())); @@ -305,16 +320,30 @@ Future setup( getIt.registerFactory( () => NodeCreateOrEditPage(getIt.get())); - getIt.registerFactory(() => - ExchangeViewModel( - wallet: getIt.get().wallet, - exchangeTemplateStore: getIt.get(), - trades: tradesSource - )); + getIt.registerFactory(() => ExchangeViewModel( + wallet: getIt.get().wallet, + exchangeTemplateStore: getIt.get(), + trades: tradesSource, + tradesStore: getIt.get())); - getIt.registerFactory(() => - ExchangePage(getIt.get())); + getIt.registerFactory(() => ExchangeTradeViewModel( + wallet: getIt.get().wallet, + trades: tradesSource, + tradesStore: getIt.get())); - getIt.registerFactory(() => - ExchangeTemplatePage(getIt.get())); + getIt.registerFactory(() => ExchangePage(getIt.get())); + + getIt.registerFactory( + () => ExchangeConfirmPage(tradesStore: getIt.get())); + + getIt.registerFactory(() => ExchangeTradePage( + exchangeTradeViewModel: getIt.get())); + + getIt.registerFactory( + () => ExchangeTemplatePage(getIt.get())); +} + +void setupThemeChangerStore(ThemeChanger themeChanger) { + getIt.registerSingleton( + ThemeChangerStore(themeChanger: themeChanger)); } diff --git a/lib/generated/i18n.dart b/lib/generated/i18n.dart index 4c9a59474..5408734ae 100644 --- a/lib/generated/i18n.dart +++ b/lib/generated/i18n.dart @@ -96,7 +96,7 @@ class S implements WidgetsLocalizations { String get expired => "Expired"; String get faq => "FAQ"; String get fetching => "Fetching"; - String get filters => "Filters"; + String get filters => "Filter"; String get first_wallet_text => "Awesome wallet for Monero"; String get full_balance => "Full Balance"; String get hidden_balance => "Hidden Balance"; @@ -312,8 +312,8 @@ class S implements WidgetsLocalizations { String error_text_limits_loading_failed(String provider) => "Trade for ${provider} is not created. Limits loading failed"; String error_text_maximum_limit(String provider, String max, String currency) => "Trade for ${provider} is not created. Amount is more then maximum: ${max} ${currency}"; String error_text_minimal_limit(String provider, String min, String currency) => "Trade for ${provider} is not created. Amount is less then minimal: ${min} ${currency}"; - String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "By pressing confirm, you will be sending ${fetchingLabel} ${from} from your wallet called ${walletName} to the address shown above. Or you can send from your external wallet to the above address/QR code.\n\nPlease press confirm to continue or go back to change the amounts.\n\n"; - String exchange_result_description(String fetchingLabel, String from) => "Please send ${fetchingLabel} ${from} to the address shown above.\n\n"; + String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "By pressing confirm, you will be sending ${fetchingLabel} ${from} from your wallet called ${walletName} to the address shown above. Or you can send from your external wallet to the above address/QR code.\n\nPlease press confirm to continue or go back to change the amounts."; + String exchange_result_description(String fetchingLabel, String from) => "Please send ${fetchingLabel} ${from} to the address shown above."; String failed_authentication(String state_error) => "Failed authentication. ${state_error}"; String max_value(String value, String currency) => "Max: ${value} ${currency}"; String min_value(String value, String currency) => "Min: ${value} ${currency}"; @@ -948,11 +948,11 @@ class $de extends S { @override String Blocks_remaining(String status) => "${status} Verbleibende Blöcke"; @override - String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "Durch Drücken von Bestätigen wird gesendet ${fetchingLabel} ${from} von Ihrer Brieftasche aus angerufen ${walletName} an die oben angegebene Adresse. Oder Sie können von Ihrem externen Portemonnaie an die oben angegebene Adresse / QR-Code senden.\n\nBitte bestätigen Sie, um fortzufahren, oder gehen Sie zurück, um die Beträge zu änderns.\n\n"; + String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "Durch Drücken von Bestätigen wird gesendet ${fetchingLabel} ${from} von Ihrer Brieftasche aus angerufen ${walletName} an die oben angegebene Adresse. Oder Sie können von Ihrem externen Portemonnaie an die oben angegebene Adresse / QR-Code senden.\n\nBitte bestätigen Sie, um fortzufahren, oder gehen Sie zurück, um die Beträge zu änderns."; @override String error_text_limits_loading_failed(String provider) => "Handel für ${provider} wird nicht erstellt. Das Laden der Limits ist fehlgeschlagen"; @override - String exchange_result_description(String fetchingLabel, String from) => "Bitte senden ${fetchingLabel} ${from} an die oben angegebene Adresse.\n\n'"; + String exchange_result_description(String fetchingLabel, String from) => "Bitte senden ${fetchingLabel} ${from} an die oben angegebene Adresse.'"; @override String commit_transaction_amount_fee(String amount, String fee) => "Transaktion festschreiben\nMenge: ${amount}\nGebühr: ${fee}"; @override @@ -1216,7 +1216,7 @@ class $hi extends S { @override String get estimated => "अनुमानित"; @override - String get filters => "फिल्टर"; + String get filters => "फ़िल्टर"; @override String get settings_current_node => "वर्तमान नोड"; @override @@ -1580,11 +1580,11 @@ class $hi extends S { @override String Blocks_remaining(String status) => "${status} शेष रहते हैं"; @override - String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "पुष्टि दबाकर, आप भेज रहे होंगे ${fetchingLabel} ${from} अपने बटुए से ${walletName} ऊपर दिखाए गए पते पर। या आप अपने बाहरी वॉलेट से उपरोक्त पते / क्यूआर कोड पर भेज सकते हैं।\n\nकृपया जारी रखने या राशि बदलने के लिए वापस जाने के लिए पुष्टि करें दबाएं.\n\n"; + String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "पुष्टि दबाकर, आप भेज रहे होंगे ${fetchingLabel} ${from} अपने बटुए से ${walletName} ऊपर दिखाए गए पते पर। या आप अपने बाहरी वॉलेट से उपरोक्त पते / क्यूआर कोड पर भेज सकते हैं।\n\nकृपया जारी रखने या राशि बदलने के लिए वापस जाने के लिए पुष्टि करें दबाएं."; @override String error_text_limits_loading_failed(String provider) => "व्यापार ${provider} के लिए नहीं बनाया गया है। लोडिंग की सीमाएं विफल रहीं"; @override - String exchange_result_description(String fetchingLabel, String from) => "कृपया भेजें ${fetchingLabel} ${from} ऊपर दिखाए गए पते पर\n\n'"; + String exchange_result_description(String fetchingLabel, String from) => "कृपया भेजें ${fetchingLabel} ${from} ऊपर दिखाए गए पते पर"; @override String commit_transaction_amount_fee(String amount, String fee) => "लेन-देन करें\nरकम: ${amount}\nशुल्क: ${fee}"; @override @@ -1848,7 +1848,7 @@ class $ru extends S { @override String get estimated => "Примерно"; @override - String get filters => "Фильтры"; + String get filters => "Фильтр"; @override String get settings_current_node => "Текущая нода"; @override @@ -2212,11 +2212,11 @@ class $ru extends S { @override String Blocks_remaining(String status) => "${status} Осталось блоков"; @override - String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "Нажимая подтвердить, вы отправите ${fetchingLabel} ${from} с вашего кошелька ${walletName} на адрес указанный выше. Или вы можете отправить со своего внешнего кошелька на вышеуказанный адрес/QR-код.\n\nПожалуйста, нажмите подтвердить для продолжения, или вернитесь назад для изменения суммы.\n\n"; + String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "Нажимая подтвердить, вы отправите ${fetchingLabel} ${from} с вашего кошелька ${walletName} на адрес указанный выше. Или вы можете отправить со своего внешнего кошелька на вышеуказанный адрес/QR-код.\n\nПожалуйста, нажмите подтвердить для продолжения, или вернитесь назад для изменения суммы."; @override String error_text_limits_loading_failed(String provider) => "Сделка для ${provider} не создана. Ошибка загрузки лимитов"; @override - String exchange_result_description(String fetchingLabel, String from) => "Пожалуйста отправьте ${fetchingLabel} ${from} на адрес, указанный выше.\n\n'"; + String exchange_result_description(String fetchingLabel, String from) => "Пожалуйста отправьте ${fetchingLabel} ${from} на адрес, указанный выше."; @override String commit_transaction_amount_fee(String amount, String fee) => "Подтвердить транзакцию \nСумма: ${amount}\nКомиссия: ${fee}"; @override @@ -2844,11 +2844,11 @@ class $ko extends S { @override String Blocks_remaining(String status) => "${status} 남은 블록"; @override - String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "확인을 누르면 전송됩니다 ${fetchingLabel} ${from} 지갑에서 ${walletName} 위에 표시된 주소로. 또는 외부 지갑에서 위의 주소 / QR 코드로 보낼 수 있습니다.\n\n확인을 눌러 계속하거나 금액을 변경하려면 돌아가십시오.\n\n"; + String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "확인을 누르면 전송됩니다 ${fetchingLabel} ${from} 지갑에서 ${walletName} 위에 표시된 주소로. 또는 외부 지갑에서 위의 주소 / QR 코드로 보낼 수 있습니다.\n\n확인을 눌러 계속하거나 금액을 변경하려면 돌아가십시오."; @override String error_text_limits_loading_failed(String provider) => "거래 ${provider} 가 생성되지 않습니다. 로딩 실패"; @override - String exchange_result_description(String fetchingLabel, String from) => "보내주세요 ${fetchingLabel} ${from} 위에 표시된 주소로.\n\n'"; + String exchange_result_description(String fetchingLabel, String from) => "보내주세요 ${fetchingLabel} ${from} 위에 표시된 주소로."; @override String commit_transaction_amount_fee(String amount, String fee) => "커밋 거래\n양: ${amount}\n보수: ${fee}"; @override @@ -3112,7 +3112,7 @@ class $pt extends S { @override String get estimated => "Estimado"; @override - String get filters => "Filtros"; + String get filters => "Filtro"; @override String get settings_current_node => "Nó atual"; @override @@ -3476,11 +3476,11 @@ class $pt extends S { @override String Blocks_remaining(String status) => "${status} blocos restantes"; @override - String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "Ao confirmar, você enviará ${fetchingLabel} ${from} da sua carteira ${walletName} para o endereço exibido acima. Você também pode enviar com uma carteira externa para o endereço/código QR acima.\n\nPressione Confirmar para continuar ou volte para alterar os valores.\n\n"; + String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "Ao confirmar, você enviará ${fetchingLabel} ${from} da sua carteira ${walletName} para o endereço exibido acima. Você também pode enviar com uma carteira externa para o endereço/código QR acima.\n\nPressione Confirmar para continuar ou volte para alterar os valores."; @override String error_text_limits_loading_failed(String provider) => "A troca por ${provider} não é criada. Falha no carregamento dos limites"; @override - String exchange_result_description(String fetchingLabel, String from) => "Por favor, envie ${fetchingLabel} ${from} para o endereço mostrado acima.\n\n'"; + String exchange_result_description(String fetchingLabel, String from) => "Por favor, envie ${fetchingLabel} ${from} para o endereço mostrado acima."; @override String commit_transaction_amount_fee(String amount, String fee) => "Confirmar transação\nQuantia: ${amount}\nTaxa: ${fee}"; @override @@ -3744,7 +3744,7 @@ class $uk extends S { @override String get estimated => "Приблизно "; @override - String get filters => "Фільтри"; + String get filters => "Фільтр"; @override String get settings_current_node => "Поточний вузол"; @override @@ -4108,11 +4108,11 @@ class $uk extends S { @override String Blocks_remaining(String status) => "${status} Залишилось блоків"; @override - String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "Натиснувши підтвердити, ви відправите ${fetchingLabel} ${from} з вашого гаманця ${walletName} на адресу вказану вище. Або ви можете відправити зі свого зовнішнього гаманця на вищевказану адресу/QR-код.\n\nБудь ласка, натисніть підтвердити для продовження або поверніться назад щоб змінити суму.\n\n"; + String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "Натиснувши підтвердити, ви відправите ${fetchingLabel} ${from} з вашого гаманця ${walletName} на адресу вказану вище. Або ви можете відправити зі свого зовнішнього гаманця на вищевказану адресу/QR-код.\n\nБудь ласка, натисніть підтвердити для продовження або поверніться назад щоб змінити суму."; @override String error_text_limits_loading_failed(String provider) => "Операція для ${provider} не створена. Помилка завантаження лімітів"; @override - String exchange_result_description(String fetchingLabel, String from) => "Будь ласка, відправте ${fetchingLabel} ${from} на адресу, вказану вище.\n\n'"; + String exchange_result_description(String fetchingLabel, String from) => "Будь ласка, відправте ${fetchingLabel} ${from} на адресу, вказану вище."; @override String commit_transaction_amount_fee(String amount, String fee) => "Підтвердити транзакцію \nСума: ${amount}\nКомісія: ${fee}"; @override @@ -4376,7 +4376,7 @@ class $ja extends S { @override String get estimated => "推定"; @override - String get filters => "フィルター"; + String get filters => "フィルタ"; @override String get settings_current_node => "現在のノード"; @override @@ -4740,11 +4740,11 @@ class $ja extends S { @override String Blocks_remaining(String status) => "${status} 残りのブロック"; @override - String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "確認を押すと、送信されます ${fetchingLabel} ${from} と呼ばれるあなたの財布から ${walletName} 上記のアドレスへ. または、外部ウォレットから上記のアドレス/ QRコードに送信できます.\n\n確認を押して続行するか、戻って金額を変更してください.\n\n"; + String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "確認を押すと、送信されます ${fetchingLabel} ${from} と呼ばれるあなたの財布から ${walletName} 上記のアドレスへ. または、外部ウォレットから上記のアドレス/ QRコードに送信できます.\n\n確認を押して続行するか、戻って金額を変更してください."; @override String error_text_limits_loading_failed(String provider) => "${provider} の取引は作成されません。 制限の読み込みに失敗しました"; @override - String exchange_result_description(String fetchingLabel, String from) => "送信してください ${fetchingLabel} ${from} 上記のアドレスへ.\n\n'"; + String exchange_result_description(String fetchingLabel, String from) => "送信してください ${fetchingLabel} ${from} 上記のアドレスへ."; @override String commit_transaction_amount_fee(String amount, String fee) => "トランザクションをコミット\n量: ${amount}\n費用: ${fee}"; @override @@ -5012,7 +5012,7 @@ class $pl extends S { @override String get estimated => "Oszacowano"; @override - String get filters => "Filtry"; + String get filters => "Filtr"; @override String get settings_current_node => "Bieżący węzeł"; @override @@ -5376,11 +5376,11 @@ class $pl extends S { @override String Blocks_remaining(String status) => "${status} Bloki pozostałe"; @override - String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "Naciskając Potwierdź, wyślesz ${fetchingLabel} ${from} z twojego portfela ${walletName} z twojego portfela. Lub możesz wysłać z zewnętrznego portfela na powyższy adres / kod QR.\n\nNaciśnij Potwierdź, aby kontynuować lub wróć, aby zmienić kwoty.\n\n"; + String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "Naciskając Potwierdź, wyślesz ${fetchingLabel} ${from} z twojego portfela ${walletName} z twojego portfela. Lub możesz wysłać z zewnętrznego portfela na powyższy adres / kod QR.\n\nNaciśnij Potwierdź, aby kontynuować lub wróć, aby zmienić kwoty."; @override String error_text_limits_loading_failed(String provider) => "Wymiana dla ${provider} nie została utworzona. Ładowanie limitów nie powiodło się"; @override - String exchange_result_description(String fetchingLabel, String from) => "Proszę wyślij ${fetchingLabel} ${from} na adres podany powyżej.\n\n'"; + String exchange_result_description(String fetchingLabel, String from) => "Proszę wyślij ${fetchingLabel} ${from} na adres podany powyżej."; @override String commit_transaction_amount_fee(String amount, String fee) => "Zatwierdź transakcję\nIlość: ${amount}\nOpłata: ${fee}"; @override @@ -5644,7 +5644,7 @@ class $es extends S { @override String get estimated => "Estimado"; @override - String get filters => "Filtros"; + String get filters => "Filtrar"; @override String get settings_current_node => "Nodo actual"; @override @@ -6008,11 +6008,11 @@ class $es extends S { @override String Blocks_remaining(String status) => "${status} Bloques restantes"; @override - String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "Al presionar confirmar, enviará ${fetchingLabel} ${from} desde su billetera llamada ${walletName} a la dirección que se muestra arriba. O puede enviar desde su billetera externa a la dirección / código QR anterior.\n\nPresione confirmar para continuar o regrese para cambiar los montos.\n\n"; + String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "Al presionar confirmar, enviará ${fetchingLabel} ${from} desde su billetera llamada ${walletName} a la dirección que se muestra arriba. O puede enviar desde su billetera externa a la dirección / código QR anterior.\n\nPresione confirmar para continuar o regrese para cambiar los montos."; @override String error_text_limits_loading_failed(String provider) => "El comercio por ${provider} no se crea. Límites de carga fallidos"; @override - String exchange_result_description(String fetchingLabel, String from) => "Envíe ${fetchingLabel} ${from} a la dirección que se muestra arriba.\n\n'"; + String exchange_result_description(String fetchingLabel, String from) => "Envíe ${fetchingLabel} ${from} a la dirección que se muestra arriba."; @override String commit_transaction_amount_fee(String amount, String fee) => "Confirmar transacción\nCantidad: ${amount}\nCuota: ${fee}"; @override @@ -6276,7 +6276,7 @@ class $nl extends S { @override String get estimated => "Geschatte"; @override - String get filters => "Filters"; + String get filters => "Filter"; @override String get settings_current_node => "Huidige knooppunt"; @override @@ -6640,11 +6640,11 @@ class $nl extends S { @override String Blocks_remaining(String status) => "${status} Resterende blokken"; @override - String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "Door op bevestigen te drukken, wordt u verzonden ${fetchingLabel} ${from} uit je portemonnee genoemd ${walletName} naar bovenstaand adres. Of u kunt uw externe portemonnee naar bovenstaand adres / QR-code sturen.\n\nDruk op bevestigen om door te gaan of terug te gaan om de bedragen te wijzigen.\n\n"; + String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "Door op bevestigen te drukken, wordt u verzonden ${fetchingLabel} ${from} uit je portemonnee genoemd ${walletName} naar bovenstaand adres. Of u kunt uw externe portemonnee naar bovenstaand adres / QR-code sturen.\n\nDruk op bevestigen om door te gaan of terug te gaan om de bedragen te wijzigen."; @override String error_text_limits_loading_failed(String provider) => "Ruil voor ${provider} is niet gemaakt. Beperkingen laden mislukt"; @override - String exchange_result_description(String fetchingLabel, String from) => "Zend alstublieft ${fetchingLabel} ${from} naar bovenstaand adres.\n\n'"; + String exchange_result_description(String fetchingLabel, String from) => "Zend alstublieft ${fetchingLabel} ${from} naar bovenstaand adres."; @override String commit_transaction_amount_fee(String amount, String fee) => "Verricht transactie\nBedrag: ${amount}\nhonorarium: ${fee}"; @override @@ -6908,7 +6908,7 @@ class $zh extends S { @override String get estimated => "估计的"; @override - String get filters => "筛选器"; + String get filters => "過濾"; @override String get settings_current_node => "当前节点"; @override @@ -7272,11 +7272,11 @@ class $zh extends S { @override String Blocks_remaining(String status) => "${status} 剩余的块"; @override - String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "点击确认 您将发送 ${fetchingLabel} ${from} 从你的钱包里 ${walletName} 到上面显示的地址. 或者,您也可以从外部钱包发送上述地址/ QR码。\n\n请按确认继续或返回以更改金额\n\n"; + String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "点击确认 您将发送 ${fetchingLabel} ${from} 从你的钱包里 ${walletName} 到上面显示的地址. 或者,您也可以从外部钱包发送上述地址/ QR码。\n\n请按确认继续或返回以更改金额"; @override String error_text_limits_loading_failed(String provider) => "未創建 ${provider} 交易。 限制加載失敗"; @override - String exchange_result_description(String fetchingLabel, String from) => "请发送 ${fetchingLabel} ${from} 到上面显示的地址.\n\n'"; + String exchange_result_description(String fetchingLabel, String from) => "请发送 ${fetchingLabel} ${from} 到上面显示的地址."; @override String commit_transaction_amount_fee(String amount, String fee) => "提交交易\n量: ${amount}\nFee: ${fee}"; @override diff --git a/lib/main.dart b/lib/main.dart index 912099978..700ec9c2b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/reactions/bootstrap.dart'; +import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/authentication_store.dart'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet_service.dart'; @@ -52,6 +53,8 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/domain/common/language.dart'; import 'package:cake_wallet/src/stores/seed_language/seed_language_store.dart'; +bool isThemeChangerRegistered = false; + void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -129,7 +132,7 @@ void main() async { fiatConvertationService: fiatConvertationService, templates: templates, exchangeTemplates: exchangeTemplates, - initialMigrationVersion: 3); + initialMigrationVersion: 4); setReactions( settingsStore: settingsStore, @@ -169,11 +172,11 @@ Future initialSetup( @required Box nodes, @required Box walletInfoSource, @required Box contactSource, - @required Box tradesSource, + @required Box tradesSource, @required FiatConvertationService fiatConvertationService, @required Box