diff --git a/.gitignore b/.gitignore index db43e964a..0580d006c 100644 --- a/.gitignore +++ b/.gitignore @@ -96,3 +96,5 @@ vendor/ android/app/.cxx/** ios/Flutter/.last_build_id /lib/generated/** +#**# +/**/#**# \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml index 52f1e3224..d74761d26 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -25,7 +25,6 @@ linter: - empty_constructor_bodies - empty_statements - hash_and_equals - - implementation_imports - invariant_booleans - iterable_contains_unrelated_type - library_names diff --git a/assets/electrum_server_list.yml b/assets/bitcoin_electrum_server_list.yml similarity index 100% rename from assets/electrum_server_list.yml rename to assets/bitcoin_electrum_server_list.yml diff --git a/assets/images/litecoin_icon.png b/assets/images/litecoin_icon.png new file mode 100644 index 000000000..9cc47b6fb Binary files /dev/null and b/assets/images/litecoin_icon.png differ diff --git a/assets/images/litecoin_menu.png b/assets/images/litecoin_menu.png new file mode 100644 index 000000000..d39aff717 Binary files /dev/null and b/assets/images/litecoin_menu.png differ diff --git a/assets/images/monero_menu.png b/assets/images/monero_menu.png index 2bb420a80..51b1e2240 100644 Binary files a/assets/images/monero_menu.png and b/assets/images/monero_menu.png differ diff --git a/assets/litecoin_electrum_server_list.yml b/assets/litecoin_electrum_server_list.yml new file mode 100644 index 000000000..4922dfba2 --- /dev/null +++ b/assets/litecoin_electrum_server_list.yml @@ -0,0 +1,2 @@ +- + uri: 128.199.34.116:50002 \ No newline at end of file diff --git a/cw_monero/ios/Classes/monero_api.cpp b/cw_monero/ios/Classes/monero_api.cpp index d0313a194..dd1ab3cbb 100644 --- a/cw_monero/ios/Classes/monero_api.cpp +++ b/cw_monero/ios/Classes/monero_api.cpp @@ -179,8 +179,6 @@ extern "C" Monero::SubaddressAccount *m_account; uint64_t m_last_known_wallet_height; uint64_t m_cached_syncing_blockchain_height = 0; - std::mutex store_mutex; - void change_current_wallet(Monero::Wallet *wallet) { @@ -451,9 +449,7 @@ extern "C" void store(char *path) { - store_mutex.lock(); get_current_wallet()->store(std::string(path)); - store_mutex.unlock(); } bool transaction_create(char *address, char *payment_id, char *amount, diff --git a/cw_monero/ios/cw_monero.podspec b/cw_monero/ios/cw_monero.podspec index 78416a9e7..ad8a94d04 100644 --- a/cw_monero/ios/cw_monero.podspec +++ b/cw_monero/ios/cw_monero.podspec @@ -5,20 +5,18 @@ Pod::Spec.new do |s| s.name = 'cw_monero' s.version = '0.0.2' - s.summary = 'A new flutter plugin project.' - s.description = <<-DESC -A new flutter plugin project. - DESC - s.homepage = 'http://example.com' + s.summary = 'CW Monero' + s.description = 'Cake Wallet wrapper over Monero project.' + s.homepage = 'http://cakewallet.com' s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } + s.author = { 'CakeWallet' => 'support@cakewallet.com' } s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h, Classes/*.h, External/ios/libs/monero/include/src/**/*.h, External/ios/libs/monero/include/contrib/**/*.h, External/ios/libs/monero/include/External/ios/**/*.h' s.dependency 'Flutter' s.platform = :ios, '9.0' s.swift_version = '4.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS' => 'arm64' } + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS' => 'arm64', 'ENABLE_BITCODE' => 'NO' } s.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/Classes/*.h" } s.subspec 'OpenSSL' do |openssl| @@ -53,4 +51,4 @@ A new flutter plugin project. lmdb.vendored_libraries = 'External/ios/libs/lmdb/liblmdb.a' lmdb.libraries = 'lmdb' end -end \ No newline at end of file +end diff --git a/cw_monero/lib/wallet_manager.dart b/cw_monero/lib/wallet_manager.dart index e48055cf9..2400c5f3f 100644 --- a/cw_monero/lib/wallet_manager.dart +++ b/cw_monero/lib/wallet_manager.dart @@ -1,12 +1,12 @@ import 'dart:ffi'; -import 'package:cw_monero/exceptions/wallet_opening_exception.dart'; -import 'package:cw_monero/wallet.dart'; import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart'; import 'package:cw_monero/convert_utf8_to_string.dart'; import 'package:cw_monero/signatures.dart'; import 'package:cw_monero/types.dart'; import 'package:cw_monero/monero_api.dart'; +import 'package:cw_monero/wallet.dart'; +import 'package:cw_monero/exceptions/wallet_opening_exception.dart'; import 'package:cw_monero/exceptions/wallet_creation_exception.dart'; import 'package:cw_monero/exceptions/wallet_restore_from_keys_exception.dart'; import 'package:cw_monero/exceptions/wallet_restore_from_seed_exception.dart'; diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 648dce288..d44aacc6b 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -7,49 +7,49 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.4.2" + version: "2.5.0" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0" characters: dependency: transitive description: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.1.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.1.3" + version: "1.2.0" clock: dependency: transitive description: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.1.0" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.14.13" + version: "1.15.0" fake_async: dependency: transitive description: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" ffi: dependency: "direct main" description: @@ -73,21 +73,21 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.8" + version: "0.12.10" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.1.8" + version: "1.3.0" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" path_provider: dependency: "direct main" description: @@ -113,56 +113,56 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" stack_trace: dependency: transitive description: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.9.5" + version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "1.1.0" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.17" + version: "0.2.19" typed_data: dependency: transitive description: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.1.0" sdks: - dart: ">=2.9.0-14.0.dev <3.0.0" - flutter: ">=0.1.4 <2.0.0" + dart: ">=2.12.0-0.0 <3.0.0" + flutter: ">=0.1.4" diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 03302eab3..879041cac 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -157,7 +157,7 @@ SPEC CHECKSUMS: barcode_scan: a5c27959edfafaa0c771905bad0b29d6d39e4479 connectivity: c4130b2985d4ef6fd26f9702e886bd5260681467 CryptoSwift: 093499be1a94b0cae36e6c26b70870668cb56060 - cw_monero: 2e1f79929880cc2293b5bc1b25e28152e4d84649 + cw_monero: 78f369253cc913efc23db9cf6be81a11eaf40fe1 devicelocale: b22617f40038496deffba44747101255cee005b0 DKImagePickerController: b5eb7f7a388e4643264105d648d01f727110fc3d DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 7b8e304db..f51e0e3fe 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -362,7 +362,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 39; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -379,7 +379,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 4.1.7; + MARKETING_VERSION = 4.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -505,7 +505,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 39; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -522,7 +522,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 4.1.7; + MARKETING_VERSION = 4.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -540,7 +540,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 39; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -557,7 +557,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 4.1.7; + MARKETING_VERSION = 4.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140cfd..fb2dffc49 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -27,8 +27,6 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> - - - - + + - - [OPS['OP_HASH160'], hash, OPS['OP_EQUAL']]); } -Uint8List addressToOutputScript(String address) { +Uint8List addressToOutputScript( + String address, bitcoin.NetworkType networkType) { try { // FIXME: improve validation for p2sh addresses - if (address.startsWith('3')) { + // 3 for bitcoin + // m for litecoin + if (address.startsWith('3') || address.toLowerCase().startsWith('m')) { return p2shAddressToOutputScript(address); } - return Address.addressToOutputScript(address); - } catch (_) { + return Address.addressToOutputScript(address, networkType); + } catch (err) { + print(err); return Uint8List(0); } -} \ No newline at end of file +} diff --git a/lib/bitcoin/bitcoin_address_record.dart b/lib/bitcoin/bitcoin_address_record.dart index af492de2d..5e3967308 100644 --- a/lib/bitcoin/bitcoin_address_record.dart +++ b/lib/bitcoin/bitcoin_address_record.dart @@ -1,14 +1,14 @@ import 'dart:convert'; -import 'package:quiver/core.dart'; class BitcoinAddressRecord { - BitcoinAddressRecord(this.address, {this.index}); + BitcoinAddressRecord(this.address, {this.index, bool isHidden}) + : _isHidden = isHidden; factory BitcoinAddressRecord.fromJSON(String jsonSource) { final decoded = json.decode(jsonSource) as Map; return BitcoinAddressRecord(decoded['address'] as String, - index: decoded['index'] as int); + index: decoded['index'] as int, isHidden: decoded['isHidden'] as bool); } @override @@ -16,10 +16,13 @@ class BitcoinAddressRecord { o is BitcoinAddressRecord && address == o.address; final String address; + bool get isHidden => _isHidden ?? false; int index; + final bool _isHidden; @override int get hashCode => address.hashCode; - String toJSON() => json.encode({'address': address, 'index': index}); + String toJSON() => + json.encode({'address': address, 'index': index, 'isHidden': isHidden}); } diff --git a/lib/bitcoin/bitcoin_transaction_history.dart b/lib/bitcoin/bitcoin_transaction_history.dart deleted file mode 100644 index 16171267c..000000000 --- a/lib/bitcoin/bitcoin_transaction_history.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'dart:convert'; -import 'package:flutter/foundation.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/core/transaction_history.dart'; -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'; - -part 'bitcoin_transaction_history.g.dart'; - -const _transactionsHistoryFileName = 'transactions.json'; - -class BitcoinTransactionHistory = BitcoinTransactionHistoryBase - with _$BitcoinTransactionHistory; - -abstract class BitcoinTransactionHistoryBase - extends TransactionHistoryBase with Store { - BitcoinTransactionHistoryBase( - {this.eclient, String dirPath, @required String password}) - : path = '$dirPath/$_transactionsHistoryFileName', - _password = password, - _height = 0, - _isUpdating = false { - transactions = ObservableMap(); - } - - BitcoinWalletBase wallet; - final ElectrumClient eclient; - final String path; - final String _password; - int _height; - bool _isUpdating; - - Future init() async { - await _load(); - } - - @override - Future update() async { - if (_isUpdating) { - return; - } - - try { - _isUpdating = true; - final txs = await fetchTransactions(); - await add(txs); - _isUpdating = false; - } catch (_) { - _isUpdating = false; - rethrow; - } - } - - @override - Future> fetchTransactions() async { - final histories = - wallet.scriptHashes.map((scriptHash) => eclient.getHistory(scriptHash)); - final _historiesWithDetails = await Future.wait(histories) - .then((histories) => histories.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.fold>( - {}, (acc, tx) { - acc[tx.id] = acc[tx.id]?.updated(tx) ?? tx; - return acc; - }); - } - - Future fetchTransactionInfo( - {@required String hash, @required int height}) async { - final tx = await eclient.getTransactionExpanded(hash: hash); - return BitcoinTransactionInfo.fromElectrumVerbose(tx, - height: height, addresses: wallet.addresses); - } - - 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 { - _updateOrInsert(tx); - - if (tx.height > _height) { - _height = tx.height; - } - - await save(); - } - - BitcoinTransactionInfo get(String id) => transactions[id]; - - Future save() async { - try { - final data = json.encode({'height': _height, 'transactions': transactions}); - await writeData(path: path, password: _password, data: data); - } catch(e) { - print('Error while save bitcoin transaction history: ${e.toString()}'); - } - } - - @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(); - final txs = content['transactions'] as Map ?? {}; - - txs.entries.forEach((entry) { - final val = entry.value; - - if (val is Map) { - final tx = BitcoinTransactionInfo.fromJson(val); - _updateOrInsert(tx); - } - }); - - _height = content['height'] as int; - } catch (e) { - print(e); - } - } - - void _updateOrInsert(BitcoinTransactionInfo transaction) { - if (transaction.id == null) { - return; - } - - 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; - } - } -} diff --git a/lib/bitcoin/bitcoin_transaction_priority.dart b/lib/bitcoin/bitcoin_transaction_priority.dart index 9e2fbc24b..455c4a5ad 100644 --- a/lib/bitcoin/bitcoin_transaction_priority.dart +++ b/lib/bitcoin/bitcoin_transaction_priority.dart @@ -26,6 +26,8 @@ class BitcoinTransactionPriority extends TransactionPriority { } } + String get units => 'sat'; + @override String toString() { var label = ''; @@ -46,4 +48,56 @@ class BitcoinTransactionPriority extends TransactionPriority { return label; } + + String labelWithRate(int rate) => '${toString()} ($rate ${units}/byte)'; +} + +class LitecoinTransactionPriority extends BitcoinTransactionPriority { + const LitecoinTransactionPriority({String title, int raw}) + : super(title: title, raw: raw); + + static const List all = [fast, medium, slow]; + static const LitecoinTransactionPriority slow = + LitecoinTransactionPriority(title: 'Slow', raw: 0); + static const LitecoinTransactionPriority medium = + LitecoinTransactionPriority(title: 'Medium', raw: 1); + static const LitecoinTransactionPriority fast = + LitecoinTransactionPriority(title: 'Fast', raw: 2); + + static LitecoinTransactionPriority deserialize({int raw}) { + switch (raw) { + case 0: + return slow; + case 1: + return medium; + case 2: + return fast; + default: + return null; + } + } + + @override + String get units => 'Latoshi'; + + @override + String toString() { + var label = ''; + + switch (this) { + case LitecoinTransactionPriority.slow: + label = S.current.transaction_priority_slow; + break; + case LitecoinTransactionPriority.medium: + label = S.current.transaction_priority_medium; + break; + case LitecoinTransactionPriority.fast: + label = S.current.transaction_priority_fast; + break; + default: + break; + } + + return label; + } } diff --git a/lib/bitcoin/bitcoin_unspent.dart b/lib/bitcoin/bitcoin_unspent.dart index bacc03dd4..846eb8c7d 100644 --- a/lib/bitcoin/bitcoin_unspent.dart +++ b/lib/bitcoin/bitcoin_unspent.dart @@ -13,5 +13,6 @@ class BitcoinUnspent { final int value; final int vout; - bool get isP2wpkh => address.address.startsWith('bc1'); + bool get isP2wpkh => + address.address.startsWith('bc') || address.address.startsWith('ltc'); } diff --git a/lib/bitcoin/bitcoin_wallet.dart b/lib/bitcoin/bitcoin_wallet.dart index 6cd113904..fd8402887 100644 --- a/lib/bitcoin/bitcoin_wallet.dart +++ b/lib/bitcoin/bitcoin_wallet.dart @@ -1,472 +1,51 @@ -import 'dart:async'; -import 'dart:convert'; -import 'package:cake_wallet/bitcoin/address_to_output_script.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_mnemonic.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart'; -import 'package:cake_wallet/entities/transaction_priority.dart'; import 'package:mobx/mobx.dart'; import 'package:flutter/foundation.dart'; -import 'package:rxdart/rxdart.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; -import 'package:cake_wallet/bitcoin/bitcoin_transaction_credentials.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/electrum.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/bitcoin/bitcoin_amount_format.dart'; -import 'package:cake_wallet/entities/sync_status.dart'; +import 'package:cake_wallet/bitcoin/electrum_wallet_snapshot.dart'; +import 'package:cake_wallet/bitcoin/electrum_wallet.dart'; import 'package:cake_wallet/entities/wallet_info.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_transaction_history.dart'; import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart'; -import 'package:cake_wallet/bitcoin/file.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_balance.dart'; -import 'package:cake_wallet/entities/node.dart'; -import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/bitcoin/electrum_balance.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 WalletInfo walletInfo, - @required List initialAddresses, - int accountIndex = 0, - this.transactionHistory, - this.mnemonic, - BitcoinBalance initialBalance}) - : balance = - initialBalance ?? BitcoinBalance(confirmed: 0, unconfirmed: 0), - hd = bitcoin.HDWallet.fromSeed(mnemonicToSeedBytes(mnemonic), - network: bitcoin.bitcoin) - .derivePath("m/0'/0"), - addresses = initialAddresses != null - ? ObservableList.of(initialAddresses.toSet()) - : ObservableList(), - syncStatus = NotConnectedSyncStatus(), - _password = password, - _accountIndex = accountIndex, - _feeRates = [], - super(walletInfo) { - _unspent = []; - _scripthashesUpdateSubject = {}; - } - - static BitcoinWallet fromJSON( - {@required String password, - @required String name, - @required String dirPath, - @required WalletInfo walletInfo, - String jsonSource}) { - final data = json.decode(jsonSource) as Map; - final mnemonic = data['mnemonic'] as String; - final accountIndex = - (data['account_index'] == 'null' || data['account_index'] == null) - ? 0 - : int.parse(data['account_index'] as String); - 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) { - if (el is String) { - addresses.add(BitcoinAddressRecord.fromJSON(el)); - } - }); - - return BitcoinWalletBase.build( - dirPath: dirPath, - mnemonic: mnemonic, - password: password, - name: name, - accountIndex: accountIndex, - initialAddresses: addresses, - initialBalance: balance, - walletInfo: walletInfo); - } - - static BitcoinWallet build( +abstract class BitcoinWalletBase extends ElectrumWallet with Store { + BitcoinWalletBase( {@required String mnemonic, @required String password, - @required String name, - @required String dirPath, @required WalletInfo walletInfo, List initialAddresses, - BitcoinBalance initialBalance, - int accountIndex = 0}) { - final walletPath = '$dirPath/$name'; - final eclient = ElectrumClient(); - final history = BitcoinTransactionHistory( - eclient: eclient, dirPath: dirPath, password: password); + ElectrumBalance initialBalance, + int accountIndex = 0}) + : super( + mnemonic: mnemonic, + password: password, + walletInfo: walletInfo, + networkType: bitcoin.bitcoin, + initialAddresses: initialAddresses, + initialBalance: initialBalance, + accountIndex: accountIndex); - return BitcoinWallet._internal( - eclient: eclient, - path: walletPath, - mnemonic: mnemonic, + static Future open({ + @required String name, + @required WalletInfo walletInfo, + @required String password, + }) async { + final snp = ElectrumWallletSnapshot(name, walletInfo.type, password); + await snp.load(); + return BitcoinWallet( + mnemonic: snp.mnemonic, password: password, - accountIndex: accountIndex, - initialAddresses: initialAddresses, - initialBalance: initialBalance, - transactionHistory: history, - walletInfo: walletInfo); - } - - static int estimatedTransactionSize(int inputsCount, int outputsCounts) => - inputsCount * 146 + outputsCounts * 33 + 8; - - @override - final BitcoinTransactionHistory transactionHistory; - final String path; - final bitcoin.HDWallet hd; - final ElectrumClient eclient; - final String mnemonic; - - List _unspent; - - @override - @observable - String address; - - @override - @observable - BitcoinBalance balance; - - @override - @observable - SyncStatus syncStatus; - - ObservableList addresses; - - List get scriptHashes => - addresses.map((addr) => scriptHash(addr.address)).toList(); - - String get xpub => hd.base58; - - @override - String get seed => mnemonic; - - @override - BitcoinWalletKeys get keys => BitcoinWalletKeys( - wif: hd.wif, privateKey: hd.privKey, publicKey: hd.pubKey); - - final String _password; - List _feeRates; - int _accountIndex; - Map> _scripthashesUpdateSubject; - - Future init() async { - if (addresses.isEmpty || addresses.length < 33) { - final addressesCount = 33 - addresses.length; - await generateNewAddresses(addressesCount, startIndex: addresses.length); - } - - address = addresses[_accountIndex].address; - transactionHistory.wallet = this; - await transactionHistory.init(); - } - - @action - Future nextAddress() async { - _accountIndex += 1; - - if (_accountIndex >= addresses.length) { - _accountIndex = 0; - } - - address = addresses[_accountIndex].address; - - await save(); - } - - Future generateNewAddress() async { - _accountIndex += 1; - final address = BitcoinAddressRecord(_getAddress(index: _accountIndex), - index: _accountIndex); - addresses.add(address); - - await save(); - - return address; - } - - Future> generateNewAddresses(int count, - {int startIndex = 0}) async { - final list = []; - - for (var i = startIndex; i < count + startIndex; i++) { - final address = BitcoinAddressRecord(_getAddress(index: i), index: i); - list.add(address); - } - - addresses.addAll(list); - await save(); - - return list; - } - - Future updateAddress(String address) async { - for (final addr in addresses) { - if (addr.address == address) { - await save(); - break; - } - } - } - - @action - @override - Future startSync() async { - try { - syncStatus = StartingSyncStatus(); - transactionHistory.updateAsync(onFinished: () { - print('transactionHistory update finished!'); - transactionHistory.save(); - }); - _subscribeForUpdates(); - await _updateBalance(); - await _updateUnspent(); - _feeRates = await eclient.feeRates(); - - Timer.periodic(const Duration(minutes: 1), - (timer) async => _feeRates = await eclient.feeRates()); - - syncStatus = SyncedSyncStatus(); - } catch (e) { - print(e.toString()); - syncStatus = FailedSyncStatus(); - } - } - - @action - @override - Future connectToNode({@required Node node}) async { - try { - syncStatus = ConnectingSyncStatus(); - await eclient.connectToUri(node.uri); - eclient.onConnectionStatusChange = (bool isConnected) { - if (!isConnected) { - syncStatus = LostConnectionSyncStatus(); - } - }; - syncStatus = ConnectedSyncStatus(); - } catch (e) { - print(e.toString()); - syncStatus = FailedSyncStatus(); - } + walletInfo: walletInfo, + initialAddresses: snp.addresses, + initialBalance: snp.balance, + accountIndex: snp.accountIndex); } @override - Future createTransaction( - Object credentials) async { - const minAmount = 546; - final transactionCredentials = credentials as BitcoinTransactionCredentials; - final inputs = []; - final allAmountFee = - calculateEstimatedFee(transactionCredentials.priority, null); - final allAmount = balance.confirmed - allAmountFee; - var fee = 0; - final credentialsAmount = transactionCredentials.amount != null - ? stringDoubleToBitcoinAmount(transactionCredentials.amount) - : 0; - final amount = transactionCredentials.amount == null || - allAmount - credentialsAmount < minAmount - ? allAmount - : credentialsAmount; - final txb = bitcoin.TransactionBuilder(network: bitcoin.bitcoin); - final changeAddress = address; - var leftAmount = amount; - var totalInputAmount = 0; - - if (_unspent.isEmpty) { - await _updateUnspent(); - } - - for (final utx in _unspent) { - leftAmount = leftAmount - utx.value; - totalInputAmount += utx.value; - inputs.add(utx); - - if (leftAmount <= 0) { - break; - } - } - - if (inputs.isEmpty) { - throw BitcoinTransactionNoInputsException(); - } - - final totalAmount = amount + fee; - fee = transactionCredentials.amount != null - ? feeAmountForPriority(transactionCredentials.priority, inputs.length, - amount == allAmount ? 1 : 2) - : allAmountFee; - - if (totalAmount > balance.confirmed) { - throw BitcoinTransactionWrongBalanceException(); - } - - if (amount <= 0 || totalInputAmount < amount) { - throw BitcoinTransactionWrongBalanceException(); - } - - txb.setVersion(1); - - 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( - addressToOutputScript(transactionCredentials.address), amount); - - final estimatedSize = estimatedTransactionSize(inputs.length, 2); - final feeAmount = feeRate(transactionCredentials.priority) * estimatedSize; - final changeValue = totalInputAmount - amount - feeAmount; - - if (changeValue > minAmount) { - 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) async { - transactionHistory.addOne(transaction); - await _updateBalance(); - }); - } - - String toJSON() => json.encode({ - 'mnemonic': mnemonic, - 'account_index': _accountIndex.toString(), - 'addresses': addresses.map((addr) => addr.toJSON()).toList(), - 'balance': balance?.toJSON() - }); - - int feeRate(TransactionPriority priority) { - if (priority is BitcoinTransactionPriority) { - return _feeRates[priority.raw]; - } - - return 0; - } - - int feeAmountForPriority(BitcoinTransactionPriority priority, int inputsCount, - int outputsCount) => - feeRate(priority) * estimatedTransactionSize(inputsCount, outputsCount); - - @override - int calculateEstimatedFee(TransactionPriority priority, int amount) { - if (priority is BitcoinTransactionPriority) { - int inputsCount = 0; - - if (amount != null) { - int totalValue = 0; - - for (final input in _unspent) { - if (totalValue >= amount) { - break; - } - - totalValue += input.value; - inputsCount += 1; - } - } else { - inputsCount = _unspent.length; - } - // If send all, then we have no change value - return feeAmountForPriority( - priority, inputsCount, amount != null ? 2 : 1); - } - - return 0; - } - - @override - Future save() async { - await write(path: path, password: _password, data: toJSON()); - await transactionHistory.save(); - } - - bitcoin.ECPair keyPairFor({@required int index}) => - generateKeyPair(hd: hd, index: index); - - @override - Future rescan({int height}) async { - // FIXME: Unimplemented - } - - @override - void close() async { - await eclient.close(); - } - - Future _updateUnspent() async { - final unspent = await Future.wait(addresses.map((address) => eclient - .getListUnspentWithAddress(address.address) - .then((unspent) => unspent - .map((unspent) => BitcoinUnspent.fromJSON(address, unspent))))); - _unspent = unspent.expand((e) => e).toList(); - } - - void _subscribeForUpdates() { - scriptHashes.forEach((sh) async { - await _scripthashesUpdateSubject[sh]?.close(); - _scripthashesUpdateSubject[sh] = eclient.scripthashUpdate(sh); - _scripthashesUpdateSubject[sh].listen((event) async { - try { - await _updateBalance(); - await _updateUnspent(); - transactionHistory.updateAsync(); - } catch (e) { - print(e.toString()); - } - }); - }); - } - - Future _fetchBalances() async { - final balances = await Future.wait( - scriptHashes.map((sHash) => eclient.getBalance(sHash))); - final balance = balances.fold( - BitcoinBalance(confirmed: 0, unconfirmed: 0), - (BitcoinBalance acc, val) => BitcoinBalance( - confirmed: (val['confirmed'] as int ?? 0) + (acc.confirmed ?? 0), - unconfirmed: - (val['unconfirmed'] as int ?? 0) + (acc.unconfirmed ?? 0))); - - return balance; - } - - Future _updateBalance() async { - balance = await _fetchBalances(); - await save(); - } - - String _getAddress({@required int index}) => - generateAddress(hd: hd, index: index); + String getAddress({@required int index, @required bitcoin.HDWallet hd}) => + generateP2WPKHAddress(hd: hd, index: index, networkType: networkType); } diff --git a/lib/bitcoin/bitcoin_wallet_service.dart b/lib/bitcoin/bitcoin_wallet_service.dart index e7f7862a0..aefe0fadf 100644 --- a/lib/bitcoin/bitcoin_wallet_service.dart +++ b/lib/bitcoin/bitcoin_wallet_service.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:cake_wallet/bitcoin/bitcoin_mnemonic.dart'; import 'package:cake_wallet/bitcoin/bitcoin_mnemonic_is_incorrect_exception.dart'; -import 'package:cake_wallet/bitcoin/file.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet_creation_credentials.dart'; import 'package:cake_wallet/core/wallet_base.dart'; import 'package:cake_wallet/core/wallet_service.dart'; @@ -19,44 +18,32 @@ class BitcoinWalletService extends WalletService< final Box walletInfoSource; + @override + WalletType getType() => WalletType.bitcoin; + @override Future create(BitcoinNewWalletCredentials credentials) async { - final dirPath = await pathForWalletDir( - type: WalletType.bitcoin, name: credentials.name); - final wallet = BitcoinWalletBase.build( - dirPath: dirPath, + final wallet = BitcoinWallet( mnemonic: await generateMnemonic(), password: credentials.password, - name: credentials.name, walletInfo: credentials.walletInfo); await wallet.save(); await wallet.init(); - return wallet; } @override Future isWalletExit(String name) async => - File(await pathForWallet(name: name, type: WalletType.bitcoin)) - .existsSync(); + File(await pathForWallet(name: name, type: getType())).existsSync(); @override Future openWallet(String name, String password) async { - final walletDirPath = - await pathForWalletDir(name: name, type: WalletType.bitcoin); - final walletPath = '$walletDirPath/$name'; - final walletJSONRaw = await read(path: walletPath, password: password); final walletInfo = walletInfoSource.values.firstWhere( - (info) => info.id == WalletBase.idFor(name, WalletType.bitcoin), + (info) => info.id == WalletBase.idFor(name, getType()), orElse: () => null); - final wallet = BitcoinWalletBase.fromJSON( - password: password, - name: name, - dirPath: walletDirPath, - jsonSource: walletJSONRaw, - walletInfo: walletInfo); + final wallet = await BitcoinWalletBase.open( + password: password, name: name, walletInfo: walletInfo); await wallet.init(); - return wallet; } @@ -67,10 +54,8 @@ class BitcoinWalletService extends WalletService< @override Future restoreFromKeys( - BitcoinRestoreWalletFromWIFCredentials credentials) async { - // TODO: implement restoreFromKeys - throw UnimplementedError(); - } + BitcoinRestoreWalletFromWIFCredentials credentials) async => + throw UnimplementedError(); @override Future restoreFromSeed( @@ -79,17 +64,12 @@ class BitcoinWalletService extends WalletService< throw BitcoinMnemonicIsIncorrectException(); } - final dirPath = await pathForWalletDir( - type: WalletType.bitcoin, name: credentials.name); - final wallet = BitcoinWalletBase.build( - dirPath: dirPath, - name: credentials.name, + final wallet = BitcoinWallet( password: credentials.password, mnemonic: credentials.mnemonic, walletInfo: credentials.walletInfo); await wallet.save(); await wallet.init(); - return wallet; } } diff --git a/lib/bitcoin/electrum.dart b/lib/bitcoin/electrum.dart index 2592d2c22..71813ac3f 100644 --- a/lib/bitcoin/electrum.dart +++ b/lib/bitcoin/electrum.dart @@ -2,21 +2,12 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; +import 'package:bitcoin_flutter/bitcoin_flutter.dart'; import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; 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]'; @@ -53,17 +44,8 @@ class ElectrumClient { Timer _aliveTimer; String unterminatedString; - Future connectToUri(String uri) async { - 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); - } + Future connectToUri(Uri uri) async => + await connect(host: uri.host, port: uri.port); Future connect({@required String host, @required int port}) async { try { @@ -173,10 +155,11 @@ class ElectrumClient { }); Future>> getListUnspentWithAddress( - String address) => + String address, NetworkType networkType) => call( - method: 'blockchain.scripthash.listunspent', - params: [scriptHash(address)]).then((dynamic result) { + method: 'blockchain.scripthash.listunspent', + params: [scriptHash(address, networkType: networkType)]) + .then((dynamic result) { if (result is List) { return result.map((dynamic val) { if (val is Map) { @@ -259,7 +242,7 @@ class ElectrumClient { if (result is String) { return result; } - print(result); + return ''; }); @@ -303,19 +286,24 @@ class ElectrumClient { }); Future> feeRates() async { - final topDoubleString = await estimatefee(p: 1); - final middleDoubleString = await estimatefee(p: 20); - final bottomDoubleString = await estimatefee(p: 100); - final top = (stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000) - .round(); - final middle = - (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000) - .round(); - final bottom = - (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000) - .round(); + try { + final topDoubleString = await estimatefee(p: 1); + final middleDoubleString = await estimatefee(p: 20); + final bottomDoubleString = await estimatefee(p: 100); + final top = + (stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000) + .round(); + final middle = + (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000) + .round(); + final bottom = + (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000) + .round(); - return [bottom, middle, top]; + return [bottom, middle, top]; + } catch (_) { + return []; + } } BehaviorSubject scripthashUpdate(String scripthash) { diff --git a/lib/bitcoin/bitcoin_balance.dart b/lib/bitcoin/electrum_balance.dart similarity index 70% rename from lib/bitcoin/bitcoin_balance.dart rename to lib/bitcoin/electrum_balance.dart index 7d8441250..66e53f921 100644 --- a/lib/bitcoin/bitcoin_balance.dart +++ b/lib/bitcoin/electrum_balance.dart @@ -1,21 +1,20 @@ import 'dart:convert'; - import 'package:flutter/foundation.dart'; import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; import 'package:cake_wallet/entities/balance.dart'; -class BitcoinBalance extends Balance { - const BitcoinBalance({@required this.confirmed, @required this.unconfirmed}) +class ElectrumBalance extends Balance { + const ElectrumBalance({@required this.confirmed, @required this.unconfirmed}) : super(confirmed, unconfirmed); - factory BitcoinBalance.fromJSON(String jsonSource) { + factory ElectrumBalance.fromJSON(String jsonSource) { if (jsonSource == null) { return null; } final decoded = json.decode(jsonSource) as Map; - return BitcoinBalance( + return ElectrumBalance( confirmed: decoded['confirmed'] as int ?? 0, unconfirmed: decoded['unconfirmed'] as int ?? 0); } @@ -24,7 +23,8 @@ class BitcoinBalance extends Balance { final int unconfirmed; @override - String get formattedAvailableBalance => bitcoinAmountToString(amount: confirmed); + String get formattedAvailableBalance => + bitcoinAmountToString(amount: confirmed); @override String get formattedAdditionalBalance => diff --git a/lib/bitcoin/electrum_transaction_history.dart b/lib/bitcoin/electrum_transaction_history.dart new file mode 100644 index 000000000..1d5e894c0 --- /dev/null +++ b/lib/bitcoin/electrum_transaction_history.dart @@ -0,0 +1,98 @@ +import 'dart:convert'; +import 'package:cake_wallet/entities/pathForWallet.dart'; +import 'package:cake_wallet/entities/wallet_info.dart'; +import 'package:flutter/foundation.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/core/transaction_history.dart'; +import 'package:cake_wallet/bitcoin/file.dart'; +import 'package:cake_wallet/bitcoin/electrum_transaction_info.dart'; + +part 'electrum_transaction_history.g.dart'; + +const _transactionsHistoryFileName = 'transactions.json'; + +class ElectrumTransactionHistory = ElectrumTransactionHistoryBase + with _$ElectrumTransactionHistory; + +abstract class ElectrumTransactionHistoryBase + extends TransactionHistoryBase with Store { + ElectrumTransactionHistoryBase( + {@required this.walletInfo, @required String password}) + : _password = password, + _height = 0 { + transactions = ObservableMap(); + } + + final WalletInfo walletInfo; + final String _password; + int _height; + + Future init() async => await _load(); + + @override + void addOne(ElectrumTransactionInfo transaction) => + transactions[transaction.id] = transaction; + + @override + void addMany(Map transactions) => + this.transactions.addAll(transactions); + + @override + Future save() async { + try { + final dirPath = + await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + final path = '$dirPath/$_transactionsHistoryFileName'; + final data = + json.encode({'height': _height, 'transactions': transactions}); + await writeData(path: path, password: _password, data: data); + } catch (e) { + print('Error while save bitcoin transaction history: ${e.toString()}'); + } + } + + Future> _read() async { + final dirPath = + await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + final path = '$dirPath/$_transactionsHistoryFileName'; + final content = await read(path: path, password: _password); + return json.decode(content) as Map; + } + + Future _load() async { + try { + final content = await _read(); + final txs = content['transactions'] as Map ?? {}; + + txs.entries.forEach((entry) { + final val = entry.value; + + if (val is Map) { + final tx = ElectrumTransactionInfo.fromJson(val, walletInfo.type); + _updateOrInsert(tx); + } + }); + + _height = content['height'] as int; + } catch (e) { + print(e); + } + } + + void _updateOrInsert(ElectrumTransactionInfo transaction) { + if (transaction.id == null) { + return; + } + + 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; + } + } +} diff --git a/lib/bitcoin/bitcoin_transaction_info.dart b/lib/bitcoin/electrum_transaction_info.dart similarity index 81% rename from lib/bitcoin/bitcoin_transaction_info.dart rename to lib/bitcoin/electrum_transaction_info.dart index fb1400c5e..47005edc7 100644 --- a/lib/bitcoin/bitcoin_transaction_info.dart +++ b/lib/bitcoin/electrum_transaction_info.dart @@ -6,9 +6,10 @@ import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; import 'package:cake_wallet/entities/transaction_direction.dart'; import 'package:cake_wallet/entities/transaction_info.dart'; import 'package:cake_wallet/entities/format_amount.dart'; +import 'package:cake_wallet/entities/wallet_type.dart'; -class BitcoinTransactionInfo extends TransactionInfo { - BitcoinTransactionInfo( +class ElectrumTransactionInfo extends TransactionInfo { + ElectrumTransactionInfo(this.type, {@required String id, @required int height, @required int amount, @@ -27,7 +28,8 @@ class BitcoinTransactionInfo extends TransactionInfo { this.confirmations = confirmations; } - factory BitcoinTransactionInfo.fromElectrumVerbose(Map obj, + factory ElectrumTransactionInfo.fromElectrumVerbose( + Map obj, WalletType type, {@required List addresses, @required int height}) { final addressesSet = addresses.map((addr) => addr.address).toSet(); final id = obj['txid'] as String; @@ -47,7 +49,8 @@ class BitcoinTransactionInfo extends TransactionInfo { final out = vin['tx']['vout'][vout] as Map; final outAddresses = (out['scriptPubKey']['addresses'] as List)?.toSet(); - inputsAmount += stringDoubleToBitcoinAmount((out['value'] as double ?? 0).toString()); + inputsAmount += + stringDoubleToBitcoinAmount((out['value'] as double ?? 0).toString()); if (outAddresses?.intersection(addressesSet)?.isNotEmpty ?? false) { direction = TransactionDirection.outgoing; @@ -58,7 +61,8 @@ class BitcoinTransactionInfo extends TransactionInfo { final outAddresses = out['scriptPubKey']['addresses'] as List ?? []; final ntrs = outAddresses.toSet().intersection(addressesSet); - final value = stringDoubleToBitcoinAmount((out['value'] as double ?? 0.0).toString()); + final value = stringDoubleToBitcoinAmount( + (out['value'] as double ?? 0.0).toString()); totalOutAmount += value; if ((direction == TransactionDirection.incoming && ntrs.isNotEmpty) || @@ -69,7 +73,7 @@ class BitcoinTransactionInfo extends TransactionInfo { final fee = inputsAmount - totalOutAmount; - return BitcoinTransactionInfo( + return ElectrumTransactionInfo(type, id: id, height: height, isPending: false, @@ -80,7 +84,7 @@ class BitcoinTransactionInfo extends TransactionInfo { confirmations: confirmations); } - factory BitcoinTransactionInfo.fromHexAndHeader(String hex, + factory ElectrumTransactionInfo.fromHexAndHeader(WalletType type, String hex, {List addresses, int height, int timestamp, int confirmations}) { final tx = bitcoin.Transaction.fromHex(hex); var exist = false; @@ -104,7 +108,7 @@ class BitcoinTransactionInfo extends TransactionInfo { ? DateTime.fromMillisecondsSinceEpoch(timestamp * 1000) : DateTime.now(); - return BitcoinTransactionInfo( + return ElectrumTransactionInfo(type, id: tx.getId(), height: height, isPending: false, @@ -115,8 +119,9 @@ class BitcoinTransactionInfo extends TransactionInfo { confirmations: confirmations); } - factory BitcoinTransactionInfo.fromJson(Map data) { - return BitcoinTransactionInfo( + factory ElectrumTransactionInfo.fromJson( + Map data, WalletType type) { + return ElectrumTransactionInfo(type, id: data['id'] as String, height: data['height'] as int, amount: data['amount'] as int, @@ -127,15 +132,17 @@ class BitcoinTransactionInfo extends TransactionInfo { confirmations: data['confirmations'] as int); } + final WalletType type; + String _fiatAmount; @override String amountFormatted() => - '${formatAmount(bitcoinAmountToString(amount: amount))} BTC'; + '${formatAmount(bitcoinAmountToString(amount: amount))} ${walletTypeToCryptoCurrency(type).title}'; @override String feeFormatted() => fee != null - ? '${formatAmount(bitcoinAmountToString(amount: fee))} BTC' + ? '${formatAmount(bitcoinAmountToString(amount: fee))} ${walletTypeToCryptoCurrency(type).title}' : ''; @override @@ -144,8 +151,8 @@ class BitcoinTransactionInfo extends TransactionInfo { @override void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); - BitcoinTransactionInfo updated(BitcoinTransactionInfo info) { - return BitcoinTransactionInfo( + ElectrumTransactionInfo updated(ElectrumTransactionInfo info) { + return ElectrumTransactionInfo(info.type, id: id, height: info.height, amount: info.amount, diff --git a/lib/bitcoin/electrum_wallet.dart b/lib/bitcoin/electrum_wallet.dart new file mode 100644 index 000000000..bf3dfd542 --- /dev/null +++ b/lib/bitcoin/electrum_wallet.dart @@ -0,0 +1,467 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:mobx/mobx.dart'; +import 'package:rxdart/subjects.dart'; +import 'package:flutter/foundation.dart'; +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:cake_wallet/bitcoin/electrum_transaction_info.dart'; +import 'package:cake_wallet/entities/pathForWallet.dart'; +import 'package:cake_wallet/bitcoin/address_to_output_script.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; +import 'package:cake_wallet/bitcoin/electrum_balance.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_mnemonic.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_transaction_credentials.dart'; +import 'package:cake_wallet/bitcoin/electrum_transaction_history.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_transaction_no_inputs_exception.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.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/file.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/core/wallet_base.dart'; +import 'package:cake_wallet/entities/node.dart'; +import 'package:cake_wallet/entities/sync_status.dart'; +import 'package:cake_wallet/entities/transaction_priority.dart'; +import 'package:cake_wallet/entities/wallet_info.dart'; +import 'package:cake_wallet/bitcoin/electrum.dart'; + +part 'electrum_wallet.g.dart'; + +class ElectrumWallet = ElectrumWalletBase with _$ElectrumWallet; + +abstract class ElectrumWalletBase extends WalletBase with Store { + ElectrumWalletBase( + {@required String password, + @required WalletInfo walletInfo, + @required List initialAddresses, + @required this.networkType, + @required this.mnemonic, + ElectrumClient electrumClient, + int accountIndex = 0, + ElectrumBalance initialBalance}) + : balance = initialBalance ?? + const ElectrumBalance(confirmed: 0, unconfirmed: 0), + hd = bitcoin.HDWallet.fromSeed(mnemonicToSeedBytes(mnemonic), + network: networkType) + .derivePath("m/0'/0"), + addresses = ObservableList.of( + (initialAddresses ?? []).toSet()), + syncStatus = NotConnectedSyncStatus(), + _password = password, + _accountIndex = accountIndex, + _feeRates = [], + _isTransactionUpdating = false, + super(walletInfo) { + this.electrumClient = electrumClient ?? ElectrumClient(); + this.walletInfo = walletInfo; + transactionHistory = + ElectrumTransactionHistory(walletInfo: walletInfo, password: password); + _unspent = []; + _scripthashesUpdateSubject = {}; + } + + static int estimatedTransactionSize(int inputsCount, int outputsCounts) => + inputsCount * 146 + outputsCounts * 33 + 8; + + final bitcoin.HDWallet hd; + final String mnemonic; + + ElectrumClient electrumClient; + + @override + @observable + String address; + + @override + @observable + ElectrumBalance balance; + + @override + @observable + SyncStatus syncStatus; + + ObservableList addresses; + + List get scriptHashes => addresses + .map((addr) => scriptHash(addr.address, networkType: networkType)) + .toList(); + + String get xpub => hd.base58; + + @override + String get seed => mnemonic; + + bitcoin.NetworkType networkType; + + @override + BitcoinWalletKeys get keys => BitcoinWalletKeys( + wif: hd.wif, privateKey: hd.privKey, publicKey: hd.pubKey); + + final String _password; + List _unspent; + List _feeRates; + int _accountIndex; + Map> _scripthashesUpdateSubject; + bool _isTransactionUpdating; + + Future init() async { + await generateAddresses(); + address = addresses[_accountIndex].address; + await transactionHistory.init(); + } + + @action + Future nextAddress() async { + _accountIndex += 1; + + if (_accountIndex >= addresses.length) { + _accountIndex = 0; + } + + address = addresses[_accountIndex].address; + + await save(); + } + + Future generateAddresses() async { + if (addresses.length < 33) { + final addressesCount = 33 - addresses.length; + await generateNewAddresses(addressesCount, + startIndex: addresses.length, hd: hd); + } + } + + Future generateNewAddress( + {bool isHidden = false, bitcoin.HDWallet hd}) async { + _accountIndex += 1; + final _hd = hd ?? this.hd; + final address = BitcoinAddressRecord( + getAddress(index: _accountIndex, hd: _hd), + index: _accountIndex, + isHidden: isHidden); + addresses.add(address); + await save(); + return address; + } + + Future> generateNewAddresses(int count, + {int startIndex = 0, bitcoin.HDWallet hd, bool isHidden = false}) async { + final list = []; + + for (var i = startIndex; i < count + startIndex; i++) { + final address = BitcoinAddressRecord(getAddress(index: i, hd: hd), + index: i, isHidden: isHidden); + list.add(address); + } + + addresses.addAll(list); + await save(); + return list; + } + + Future updateAddress(String address) async { + for (final addr in addresses) { + if (addr.address == address) { + await save(); + break; + } + } + } + + @action + @override + Future startSync() async { + try { + syncStatus = StartingSyncStatus(); + updateTransactions(); + _subscribeForUpdates(); + await _updateBalance(); + await _updateUnspent(); + _feeRates = await electrumClient.feeRates(); + + Timer.periodic(const Duration(minutes: 1), + (timer) async => _feeRates = await electrumClient.feeRates()); + + syncStatus = SyncedSyncStatus(); + } catch (e) { + print(e.toString()); + syncStatus = FailedSyncStatus(); + } + } + + @action + @override + Future connectToNode({@required Node node}) async { + try { + syncStatus = ConnectingSyncStatus(); + await electrumClient.connectToUri(node.uri); + electrumClient.onConnectionStatusChange = (bool isConnected) { + if (!isConnected) { + syncStatus = LostConnectionSyncStatus(); + } + }; + syncStatus = ConnectedSyncStatus(); + } catch (e) { + print(e.toString()); + syncStatus = FailedSyncStatus(); + } + } + + @override + Future createTransaction( + Object credentials) async { + const minAmount = 546; + final transactionCredentials = credentials as BitcoinTransactionCredentials; + final inputs = []; + final allAmountFee = + calculateEstimatedFee(transactionCredentials.priority, null); + final allAmount = balance.confirmed - allAmountFee; + var fee = 0; + final credentialsAmount = transactionCredentials.amount != null + ? stringDoubleToBitcoinAmount(transactionCredentials.amount) + : 0; + final amount = transactionCredentials.amount == null || + allAmount - credentialsAmount < minAmount + ? allAmount + : credentialsAmount; + final txb = bitcoin.TransactionBuilder(network: networkType); + final changeAddress = address; + var leftAmount = amount; + var totalInputAmount = 0; + + if (_unspent.isEmpty) { + await _updateUnspent(); + } + + for (final utx in _unspent) { + leftAmount = leftAmount - utx.value; + totalInputAmount += utx.value; + inputs.add(utx); + + if (leftAmount <= 0) { + break; + } + } + + if (inputs.isEmpty) { + throw BitcoinTransactionNoInputsException(); + } + + final totalAmount = amount + fee; + fee = transactionCredentials.amount != null + ? feeAmountForPriority(transactionCredentials.priority, inputs.length, + amount == allAmount ? 1 : 2) + : allAmountFee; + + if (totalAmount > balance.confirmed) { + throw BitcoinTransactionWrongBalanceException(); + } + + if (amount <= 0 || totalInputAmount < amount) { + throw BitcoinTransactionWrongBalanceException(); + } + + txb.setVersion(1); + + inputs.forEach((input) { + if (input.isP2wpkh) { + final p2wpkh = bitcoin + .P2WPKH( + data: generatePaymentData(hd: hd, index: input.address.index), + network: networkType) + .data; + + txb.addInput(input.hash, input.vout, null, p2wpkh.output); + } else { + txb.addInput(input.hash, input.vout); + } + }); + + txb.addOutput( + addressToOutputScript(transactionCredentials.address, networkType), + amount); + + final estimatedSize = estimatedTransactionSize(inputs.length, 2); + final feeAmount = feeRate(transactionCredentials.priority) * estimatedSize; + final changeValue = totalInputAmount - amount - feeAmount; + + if (changeValue > minAmount) { + 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, network: networkType); + final witnessValue = input.isP2wpkh ? input.value : null; + + txb.sign(vin: i, keyPair: keyPair, witnessValue: witnessValue); + } + + return PendingBitcoinTransaction(txb.build(), type, + electrumClient: electrumClient, amount: amount, fee: fee) + ..addListener((transaction) async { + transactionHistory.addOne(transaction); + await _updateBalance(); + }); + } + + String toJSON() => json.encode({ + 'mnemonic': mnemonic, + 'account_index': _accountIndex.toString(), + 'addresses': addresses.map((addr) => addr.toJSON()).toList(), + 'balance': balance?.toJSON() + }); + + int feeRate(TransactionPriority priority) { + if (priority is BitcoinTransactionPriority) { + return _feeRates[priority.raw]; + } + + return 0; + } + + int feeAmountForPriority(BitcoinTransactionPriority priority, int inputsCount, + int outputsCount) => + feeRate(priority) * estimatedTransactionSize(inputsCount, outputsCount); + + @override + int calculateEstimatedFee(TransactionPriority priority, int amount) { + if (priority is BitcoinTransactionPriority) { + int inputsCount = 0; + + if (amount != null) { + int totalValue = 0; + + for (final input in _unspent) { + if (totalValue >= amount) { + break; + } + + totalValue += input.value; + inputsCount += 1; + } + } else { + inputsCount = _unspent.length; + } + // If send all, then we have no change value + return feeAmountForPriority( + priority, inputsCount, amount != null ? 2 : 1); + } + + return 0; + } + + @override + Future save() async { + final path = await makePath(); + await write(path: path, password: _password, data: toJSON()); + await transactionHistory.save(); + } + + bitcoin.ECPair keyPairFor({@required int index}) => + generateKeyPair(hd: hd, index: index, network: networkType); + + @override + Future rescan({int height}) async => throw UnimplementedError(); + + @override + Future close() async { + try { + await electrumClient?.close(); + } catch (_) {} + } + + String getAddress({@required int index, @required bitcoin.HDWallet hd}) => ''; + + Future makePath() async => + pathForWallet(name: walletInfo.name, type: walletInfo.type); + + Future _updateUnspent() async { + final unspent = await Future.wait(addresses.map((address) => electrumClient + .getListUnspentWithAddress(address.address, networkType) + .then((unspent) => unspent + .map((unspent) => BitcoinUnspent.fromJSON(address, unspent))))); + _unspent = unspent.expand((e) => e).toList(); + } + + Future fetchTransactionInfo( + {@required String hash, @required int height}) async { + final tx = await electrumClient.getTransactionExpanded(hash: hash); + return ElectrumTransactionInfo.fromElectrumVerbose(tx, walletInfo.type, + height: height, addresses: addresses); + } + + @override + Future> fetchTransactions() async { + final histories = + scriptHashes.map((scriptHash) => electrumClient.getHistory(scriptHash)); + final _historiesWithDetails = await Future.wait(histories) + .then((histories) => histories.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.fold>( + {}, (acc, tx) { + acc[tx.id] = acc[tx.id]?.updated(tx) ?? tx; + return acc; + }); + } + + Future updateTransactions() async { + try { + if (_isTransactionUpdating) { + return; + } + + _isTransactionUpdating = true; + final transactions = await fetchTransactions(); + transactionHistory.addMany(transactions); + await transactionHistory.save(); + _isTransactionUpdating = false; + } catch (e) { + print(e); + _isTransactionUpdating = false; + } + } + + void _subscribeForUpdates() { + scriptHashes.forEach((sh) async { + await _scripthashesUpdateSubject[sh]?.close(); + _scripthashesUpdateSubject[sh] = electrumClient.scripthashUpdate(sh); + _scripthashesUpdateSubject[sh].listen((event) async { + try { + await _updateBalance(); + await _updateUnspent(); + await updateTransactions(); + } catch (e) { + print(e.toString()); + } + }); + }); + } + + Future _fetchBalances() async { + final balances = await Future.wait( + scriptHashes.map((sh) => electrumClient.getBalance(sh))); + final balance = balances.fold( + ElectrumBalance(confirmed: 0, unconfirmed: 0), + (ElectrumBalance acc, val) => ElectrumBalance( + confirmed: (val['confirmed'] as int ?? 0) + (acc.confirmed ?? 0), + unconfirmed: + (val['unconfirmed'] as int ?? 0) + (acc.unconfirmed ?? 0))); + + return balance; + } + + Future _updateBalance() async { + balance = await _fetchBalances(); + await save(); + } +} diff --git a/lib/bitcoin/electrum_wallet_snapshot.dart b/lib/bitcoin/electrum_wallet_snapshot.dart new file mode 100644 index 000000000..9347157ad --- /dev/null +++ b/lib/bitcoin/electrum_wallet_snapshot.dart @@ -0,0 +1,42 @@ +import 'dart:convert'; +import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart'; +import 'package:cake_wallet/bitcoin/electrum_balance.dart'; +import 'package:cake_wallet/bitcoin/file.dart'; +import 'package:cake_wallet/entities/pathForWallet.dart'; +import 'package:cake_wallet/entities/wallet_type.dart'; + +class ElectrumWallletSnapshot { + ElectrumWallletSnapshot(this.name, this.type, this.password); + + final String name; + final String password; + final WalletType type; + + String mnemonic; + List addresses; + ElectrumBalance balance; + int accountIndex; + + Future load() async { + try { + final path = await pathForWallet(name: name, type: type); + final jsonSource = await read(path: path, password: password); + final data = json.decode(jsonSource) as Map; + final addressesTmp = data['addresses'] as List ?? []; + mnemonic = data['mnemonic'] as String; + addresses = addressesTmp + .whereType() + .map((addr) => BitcoinAddressRecord.fromJSON(addr)) + .toList(); + balance = ElectrumBalance.fromJSON(data['balance'] as String) ?? + ElectrumBalance(confirmed: 0, unconfirmed: 0); + accountIndex = 0; + + try { + accountIndex = int.parse(data['account_index'] as String); + } catch (_) {} + } catch (e) { + print(e); + } + } +} diff --git a/lib/bitcoin/litecoin_network.dart b/lib/bitcoin/litecoin_network.dart new file mode 100644 index 000000000..d7ad2f837 --- /dev/null +++ b/lib/bitcoin/litecoin_network.dart @@ -0,0 +1,9 @@ +import 'package:bitcoin_flutter/bitcoin_flutter.dart'; + +final litecoinNetwork = NetworkType( + messagePrefix: '\x19Litecoin Signed Message:\n', + bech32: 'ltc', + bip32: Bip32Type(public: 0x0488b21e, private: 0x0488ade4), + pubKeyHash: 0x30, + scriptHash: 0x32, + wif: 0xb0); diff --git a/lib/bitcoin/litecoin_wallet.dart b/lib/bitcoin/litecoin_wallet.dart new file mode 100644 index 000000000..209d1904e --- /dev/null +++ b/lib/bitcoin/litecoin_wallet.dart @@ -0,0 +1,88 @@ +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:cake_wallet/bitcoin/bitcoin_mnemonic.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart'; +import 'package:cake_wallet/entities/transaction_priority.dart'; +import 'package:flutter/foundation.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/entities/wallet_info.dart'; +import 'package:cake_wallet/bitcoin/electrum_wallet_snapshot.dart'; +import 'package:cake_wallet/bitcoin/electrum_wallet.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart'; +import 'package:cake_wallet/bitcoin/electrum_balance.dart'; +import 'package:cake_wallet/bitcoin/litecoin_network.dart'; +import 'package:cake_wallet/bitcoin/utils.dart'; + +part 'litecoin_wallet.g.dart'; + +class LitecoinWallet = LitecoinWalletBase with _$LitecoinWallet; + +abstract class LitecoinWalletBase extends ElectrumWallet with Store { + LitecoinWalletBase( + {@required String mnemonic, + @required String password, + @required WalletInfo walletInfo, + List initialAddresses, + ElectrumBalance initialBalance, + int accountIndex = 0}) + : super( + mnemonic: mnemonic, + password: password, + walletInfo: walletInfo, + networkType: litecoinNetwork, + initialAddresses: initialAddresses, + initialBalance: initialBalance, + accountIndex: accountIndex); + + static Future open({ + @required String name, + @required WalletInfo walletInfo, + @required String password, + }) async { + final snp = ElectrumWallletSnapshot(name, walletInfo.type, password); + await snp.load(); + return LitecoinWallet( + mnemonic: snp.mnemonic, + password: password, + walletInfo: walletInfo, + initialAddresses: snp.addresses, + initialBalance: snp.balance, + accountIndex: snp.accountIndex); + } + + @override + String getAddress({@required int index, @required bitcoin.HDWallet hd}) => + generateP2WPKHAddress(hd: hd, index: index, networkType: networkType); + + @override + Future generateAddresses() async { + if (addresses.length < 33) { + final addressesCount = 22 - addresses.length; + await generateNewAddresses(addressesCount, + hd: hd, startIndex: addresses.length); + + final changeRoot = bitcoin.HDWallet.fromSeed( + mnemonicToSeedBytes(mnemonic), + network: networkType) + .derivePath("m/0'/1"); + + await generateNewAddresses(11, + startIndex: 0, hd: changeRoot, isHidden: true); + } + } + + @override + int feeRate(TransactionPriority priority) { + if (priority is LitecoinTransactionPriority) { + switch (priority) { + case LitecoinTransactionPriority.slow: + return 1; + case LitecoinTransactionPriority.medium: + return 2; + case LitecoinTransactionPriority.fast: + return 3; + } + } + + return 0; + } +} diff --git a/lib/bitcoin/litecoin_wallet_service.dart b/lib/bitcoin/litecoin_wallet_service.dart new file mode 100644 index 000000000..053fd785f --- /dev/null +++ b/lib/bitcoin/litecoin_wallet_service.dart @@ -0,0 +1,76 @@ +import 'dart:io'; +import 'package:hive/hive.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_mnemonic.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_mnemonic_is_incorrect_exception.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_wallet_creation_credentials.dart'; +import 'package:cake_wallet/bitcoin/litecoin_wallet.dart'; +import 'package:cake_wallet/core/wallet_service.dart'; +import 'package:cake_wallet/entities/pathForWallet.dart'; +import 'package:cake_wallet/entities/wallet_type.dart'; +import 'package:cake_wallet/entities/wallet_info.dart'; +import 'package:cake_wallet/core/wallet_base.dart'; + +class LitecoinWalletService extends WalletService< + BitcoinNewWalletCredentials, + BitcoinRestoreWalletFromSeedCredentials, + BitcoinRestoreWalletFromWIFCredentials> { + LitecoinWalletService(this.walletInfoSource); + + final Box walletInfoSource; + + @override + WalletType getType() => WalletType.litecoin; + + @override + Future create(BitcoinNewWalletCredentials credentials) async { + final wallet = LitecoinWallet( + mnemonic: await generateMnemonic(), + password: credentials.password, + walletInfo: credentials.walletInfo); + await wallet.save(); + await wallet.init(); + + return wallet; + } + + @override + Future isWalletExit(String name) async => + File(await pathForWallet(name: name, type: getType())).existsSync(); + + @override + Future openWallet(String name, String password) async { + final walletInfo = walletInfoSource.values.firstWhere( + (info) => info.id == WalletBase.idFor(name, getType()), + orElse: () => null); + final wallet = await LitecoinWalletBase.open( + password: password, name: name, walletInfo: walletInfo); + await wallet.init(); + return wallet; + } + + @override + Future remove(String wallet) async => + File(await pathForWalletDir(name: wallet, type: getType())) + .delete(recursive: true); + + @override + Future restoreFromKeys( + BitcoinRestoreWalletFromWIFCredentials credentials) async => + throw UnimplementedError(); + + @override + Future restoreFromSeed( + BitcoinRestoreWalletFromSeedCredentials credentials) async { + if (!validateMnemonic(credentials.mnemonic)) { + throw BitcoinMnemonicIsIncorrectException(); + } + + final wallet = LitecoinWallet( + password: credentials.password, + mnemonic: credentials.mnemonic, + walletInfo: credentials.walletInfo); + await wallet.save(); + await wallet.init(); + return wallet; + } +} diff --git a/lib/bitcoin/pending_bitcoin_transaction.dart b/lib/bitcoin/pending_bitcoin_transaction.dart index edd5a0450..ec3e3a985 100644 --- a/lib/bitcoin/pending_bitcoin_transaction.dart +++ b/lib/bitcoin/pending_bitcoin_transaction.dart @@ -1,18 +1,22 @@ -import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart'; -import 'package:cake_wallet/entities/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'; +import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; +import 'package:cake_wallet/bitcoin/electrum_transaction_info.dart'; +import 'package:cake_wallet/entities/transaction_direction.dart'; +import 'package:cake_wallet/entities/wallet_type.dart'; class PendingBitcoinTransaction with PendingTransaction { - PendingBitcoinTransaction(this._tx, - {@required this.eclient, @required this.amount, @required this.fee}) - : _listeners = []; + PendingBitcoinTransaction(this._tx, this.type, + {@required this.electrumClient, + @required this.amount, + @required this.fee}) + : _listeners = []; + final WalletType type; final bitcoin.Transaction _tx; - final ElectrumClient eclient; + final ElectrumClient electrumClient; final int amount; final int fee; @@ -25,24 +29,25 @@ class PendingBitcoinTransaction with PendingTransaction { @override String get feeFormatted => bitcoinAmountToString(amount: fee); - final List _listeners; + final List _listeners; @override Future commit() async { - await eclient.broadcastTransaction(transactionRaw: _tx.toHex()); + await electrumClient.broadcastTransaction(transactionRaw: _tx.toHex()); _listeners?.forEach((listener) => listener(transactionInfo())); } void addListener( - void Function(BitcoinTransactionInfo transaction) listener) => + void Function(ElectrumTransactionInfo transaction) listener) => _listeners.add(listener); - BitcoinTransactionInfo transactionInfo() => BitcoinTransactionInfo( + ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type, id: id, height: 0, amount: amount, direction: TransactionDirection.outgoing, date: DateTime.now(), isPending: true, - confirmations: 0); + confirmations: 0, + fee: fee); } diff --git a/lib/bitcoin/script_hash.dart b/lib/bitcoin/script_hash.dart index b252a0700..b1025f66b 100644 --- a/lib/bitcoin/script_hash.dart +++ b/lib/bitcoin/script_hash.dart @@ -1,18 +1,20 @@ +import 'package:flutter/foundation.dart'; 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(''); +String scriptHash(String address, {@required bitcoin.NetworkType networkType}) { + final outputScript = + bitcoin.Address.addressToOutputScript(address, networkType); + final parts = sha256.convert(outputScript).toString().split(''); var res = ''; - for (var i = splitted.length - 1; i >= 0; i--) { - final char = splitted[i]; + for (var i = parts.length - 1; i >= 0; i--) { + final char = parts[i]; i--; - final nextChar = splitted[i]; + final nextChar = parts[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 index 257c8b176..3a638555a 100644 --- a/lib/bitcoin/utils.dart +++ b/lib/bitcoin/utils.dart @@ -13,14 +13,43 @@ bitcoin.ECPair generateKeyPair( {@required bitcoin.HDWallet hd, @required int index, bitcoin.NetworkType network}) => - bitcoin.ECPair.fromWIF(hd.derive(index).wif, - network: network ?? bitcoin.bitcoin); + bitcoin.ECPair.fromWIF(hd.derive(index).wif, network: network); -String generateAddress({@required bitcoin.HDWallet hd, @required int index}) => +String generateP2WPKHAddress( + {@required bitcoin.HDWallet hd, + @required int index, + bitcoin.NetworkType networkType}) => bitcoin .P2WPKH( data: PaymentData( pubkey: - Uint8List.fromList(HEX.decode(hd.derive(index).pubKey)))) + Uint8List.fromList(HEX.decode(hd.derive(index).pubKey))), + network: networkType) + .data + .address; + +String generateP2WPKHAddressByPath( + {@required bitcoin.HDWallet hd, + @required String path, + bitcoin.NetworkType networkType}) => + bitcoin + .P2WPKH( + data: PaymentData( + pubkey: + Uint8List.fromList(HEX.decode(hd.derivePath(path).pubKey))), + network: networkType) + .data + .address; + +String generateP2PKHAddress( + {@required bitcoin.HDWallet hd, + @required int index, + bitcoin.NetworkType networkType}) => + bitcoin + .P2PKH( + data: PaymentData( + pubkey: + Uint8List.fromList(HEX.decode(hd.derive(index).pubKey))), + network: networkType) .data .address; diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 273a6ca14..f481d0b1d 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -72,7 +72,7 @@ class AddressValidator extends TextValidator { case CryptoCurrency.eth: return [42]; case CryptoCurrency.ltc: - return [34]; + return [34, 43]; case CryptoCurrency.nano: return [64, 65]; case CryptoCurrency.trx: diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index fa984f3dd..09edc6c1f 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -8,7 +8,6 @@ import 'package:path_provider/path_provider.dart'; import 'package:cryptography/cryptography.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:archive/archive_io.dart'; -import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/entities/encrypt.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; diff --git a/lib/core/fiat_conversion_service.dart b/lib/core/fiat_conversion_service.dart index 1744f3df8..9bda4e7ad 100644 --- a/lib/core/fiat_conversion_service.dart +++ b/lib/core/fiat_conversion_service.dart @@ -3,7 +3,6 @@ import 'package:cake_wallet/entities/fiat_currency.dart'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart'; -import 'package:cake_wallet/entities/currency_formatter.dart'; const fiatApiAuthority = 'fiat-api.cakewallet.com'; const fiatApiPath = '/v1/rates'; @@ -15,9 +14,8 @@ Future _fetchPrice(Map args) async { try { final fiatStringified = fiat.toString(); - final uri = - Uri.https(fiatApiAuthority, fiatApiPath, - {'convert': fiatStringified}); + final uri = Uri.https(fiatApiAuthority, fiatApiPath, + {'convert': fiatStringified}); final response = await get(uri.toString()); if (response.statusCode != 200) { @@ -28,7 +26,7 @@ Future _fetchPrice(Map args) async { final data = responseJSON['data'] as List; for (final item in data) { - if (item['symbol'] == cryptoToString(crypto)) { + if (item['symbol'] == crypto.title) { price = item['quote'][fiatStringified]['price'] as double; break; } @@ -45,6 +43,7 @@ Future _fetchPriceAsync( compute(_fetchPrice, {'fiat': fiat, 'crypto': crypto}); class FiatConversionService { - static Future fetchPrice(CryptoCurrency crypto, FiatCurrency fiat) async => + static Future fetchPrice( + CryptoCurrency crypto, FiatCurrency fiat) async => await _fetchPriceAsync(crypto, fiat); } diff --git a/lib/core/generate_wallet_password.dart b/lib/core/generate_wallet_password.dart index abf2e6dac..9f126d8c2 100644 --- a/lib/core/generate_wallet_password.dart +++ b/lib/core/generate_wallet_password.dart @@ -4,9 +4,9 @@ import 'package:cake_wallet/entities/wallet_type.dart'; String generateWalletPassword(WalletType type) { switch (type) { - case WalletType.bitcoin: - return generateKey(); - default: + case WalletType.monero: return Uuid().v4(); + default: + return generateKey(); } } diff --git a/lib/core/node_port_validator.dart b/lib/core/node_port_validator.dart index 16be00dde..89d4ec1da 100644 --- a/lib/core/node_port_validator.dart +++ b/lib/core/node_port_validator.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/core/validator.dart'; diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart index 94ada7481..fd2e4851b 100644 --- a/lib/core/seed_validator.dart +++ b/lib/core/seed_validator.dart @@ -24,6 +24,8 @@ class SeedValidator extends Validator { switch (type) { case WalletType.bitcoin: return getBitcoinWordList(language); + case WalletType.litecoin: + return getBitcoinWordList(language); case WalletType.monero: return getMoneroWordList(language); default: diff --git a/lib/core/transaction_history.dart b/lib/core/transaction_history.dart index dd91fb203..ee386e392 100644 --- a/lib/core/transaction_history.dart +++ b/lib/core/transaction_history.dart @@ -3,43 +3,50 @@ import 'package:mobx/mobx.dart'; import 'package:cake_wallet/entities/transaction_info.dart'; abstract class TransactionHistoryBase { - TransactionHistoryBase() : _isUpdating = false; + TransactionHistoryBase(); + // : _isUpdating = false; @observable ObservableMap transactions; - bool _isUpdating; + Future save(); - @action - Future update() async { - if (_isUpdating) { - return; - } + void addOne(TransactionType transaction); - try { - _isUpdating = true; - final _transactions = await fetchTransactions(); - transactions.keys - .toSet() - .difference(_transactions.keys.toSet()) - .forEach((k) => transactions.remove(k)); - _transactions.forEach((key, value) => transactions[key] = value); - _isUpdating = false; - } catch (e) { - _isUpdating = false; - rethrow; - } - } + void addMany(Map transactions); - void updateAsync({void Function() onFinished}) { - fetchTransactionsAsync( - (transaction) => transactions[transaction.id] = transaction, - onFinished: onFinished); - } + // bool _isUpdating; - void fetchTransactionsAsync( - void Function(TransactionType transaction) onTransactionLoaded, - {void Function() onFinished}); + // @action + // Future update() async { + // if (_isUpdating) { + // return; + // } - Future> fetchTransactions(); + // try { + // _isUpdating = true; + // final _transactions = await fetchTransactions(); + // transactions.keys + // .toSet() + // .difference(_transactions.keys.toSet()) + // .forEach((k) => transactions.remove(k)); + // _transactions.forEach((key, value) => transactions[key] = value); + // _isUpdating = false; + // } catch (e) { + // _isUpdating = false; + // rethrow; + // } + // } + + // 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 8369ed113..ced918342 100644 --- a/lib/core/wallet_base.dart +++ b/lib/core/wallet_base.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/entities/balance.dart'; +import 'package:cake_wallet/entities/transaction_info.dart'; import 'package:cake_wallet/entities/transaction_priority.dart'; import 'package:flutter/foundation.dart'; import 'package:cake_wallet/entities/wallet_info.dart'; @@ -11,7 +12,10 @@ import 'package:cake_wallet/entities/sync_status.dart'; import 'package:cake_wallet/entities/node.dart'; import 'package:cake_wallet/entities/wallet_type.dart'; -abstract class WalletBase { +abstract class WalletBase< + BalanceType extends Balance, + HistoryType extends TransactionHistoryBase, + TransactionType extends TransactionInfo> { WalletBase(this.walletInfo); static String idFor(String name, WalletType type) => @@ -41,7 +45,7 @@ abstract class WalletBase { Object get keys; - TransactionHistoryBase transactionHistory; + HistoryType transactionHistory; Future connectToNode({@required Node node}); @@ -51,6 +55,12 @@ abstract class WalletBase { int calculateEstimatedFee(TransactionPriority priority, int amount); + // void fetchTransactionsAsync( + // void Function(TransactionType transaction) onTransactionLoaded, + // {void Function() onFinished}); + + Future> fetchTransactions(); + Future save(); Future rescan({int height}); diff --git a/lib/core/wallet_service.dart b/lib/core/wallet_service.dart index f26fc10c2..2d207c31e 100644 --- a/lib/core/wallet_service.dart +++ b/lib/core/wallet_service.dart @@ -1,8 +1,11 @@ import 'package:cake_wallet/core/wallet_base.dart'; import 'package:cake_wallet/core/wallet_credentials.dart'; +import 'package:cake_wallet/entities/wallet_type.dart'; abstract class WalletService { + WalletType getType(); + Future create(N credentials); Future restoreFromSeed(RFS credentials); diff --git a/lib/di.dart b/lib/di.dart index 66ac22231..cae3a068f 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/bitcoin/bitcoin_wallet_service.dart'; +import 'package:cake_wallet/bitcoin/litecoin_wallet_service.dart'; import 'package:cake_wallet/core/backup_service.dart'; import 'package:cake_wallet/core/wallet_service.dart'; import 'package:cake_wallet/entities/biometric_auth.dart'; @@ -443,6 +444,8 @@ Future setup( getIt.registerFactory(() => BitcoinWalletService(_walletInfoSource)); + getIt.registerFactory(() => LitecoinWalletService(_walletInfoSource)); + getIt.registerFactoryParam( (WalletType param1, __) { switch (param1) { @@ -450,6 +453,8 @@ Future setup( return getIt.get(); case WalletType.bitcoin: return getIt.get(); + case WalletType.litecoin: + return getIt.get(); default: return null; } @@ -533,8 +538,7 @@ Future setup( TradeDetailsPage(getIt.get(param1: trade))); getIt.registerFactory(() { - final wallet = getIt.get().wallet; - return WyreService(wallet: wallet); + return WyreService(appStore: getIt.get()); }); getIt.registerFactory(() { @@ -546,10 +550,9 @@ Future setup( WyrePage(getIt.get(), ordersStore: getIt.get(), url: url)); - getIt.registerFactoryParam( - (order, _) => OrderDetailsViewModel( - wyreViewModel: getIt.get(), - orderForDetails: order)); + getIt.registerFactoryParam((order, _) => + OrderDetailsViewModel( + wyreViewModel: getIt.get(), orderForDetails: order)); getIt.registerFactoryParam((Order order, _) => OrderDetailsPage(getIt.get(param1: order))); diff --git a/lib/entities/calculate_fiat_amount.dart b/lib/entities/calculate_fiat_amount.dart index 6302c23a0..407030d7f 100644 --- a/lib/entities/calculate_fiat_amount.dart +++ b/lib/entities/calculate_fiat_amount.dart @@ -4,11 +4,12 @@ String calculateFiatAmount({double price, String cryptoAmount}) { } final _amount = double.parse(cryptoAmount); - final result = price * _amount; + final _result = price * _amount; + final result = _result < 0 ? _result * -1 : _result; if (result == 0.0) { return '0.00'; } return result > 0.01 ? result.toStringAsFixed(2) : '< 0.01'; -} \ No newline at end of file +} diff --git a/lib/entities/currency_for_wallet_type.dart b/lib/entities/currency_for_wallet_type.dart index 840e75dd1..7f13d12b0 100644 --- a/lib/entities/currency_for_wallet_type.dart +++ b/lib/entities/currency_for_wallet_type.dart @@ -7,7 +7,9 @@ CryptoCurrency currencyForWalletType(WalletType type) { return CryptoCurrency.btc; case WalletType.monero: return CryptoCurrency.xmr; + case WalletType.litecoin: + return CryptoCurrency.ltc; default: return null; } -} \ No newline at end of file +} diff --git a/lib/entities/currency_formatter.dart b/lib/entities/currency_formatter.dart deleted file mode 100644 index 40326a7e6..000000000 --- a/lib/entities/currency_formatter.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:cake_wallet/entities/crypto_currency.dart'; - -String cryptoToString(CryptoCurrency crypto) { - switch (crypto) { - case CryptoCurrency.xmr: - return 'XMR'; - case CryptoCurrency.btc: - return 'BTC'; - default: - return ''; - } -} \ No newline at end of file diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 1d8dd37f9..e8edd28aa 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -1,11 +1,7 @@ import 'dart:io' show File, Platform; import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart'; -import 'package:cake_wallet/core/generate_wallet_password.dart'; -import 'package:cake_wallet/core/key_service.dart'; -import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/pathForWallet.dart'; import 'package:cake_wallet/entities/secret_store_key.dart'; -import 'package:cake_wallet/monero/monero_wallet_service.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:hive/hive.dart'; @@ -24,7 +20,8 @@ import 'package:cake_wallet/exchange/trade.dart'; import 'package:encrypt/encrypt.dart' as encrypt; const newCakeWalletMoneroUri = 'xmr-node.cakewallet.com:18081'; -const cakeWalletElectrumUri = 'electrum.cakewallet.com:50002'; +const cakeWalletBitcoinElectrumUri = 'electrum.cakewallet.com:50002'; +const cakeWalletLitecoinElectrumUri = '128.199.34.116:50002'; Future defaultSettingsMigration( {@required int version, @@ -68,6 +65,8 @@ Future defaultSettingsMigration( sharedPreferences: sharedPreferences, nodes: nodes); await changeBitcoinCurrentElectrumServerToDefault( sharedPreferences: sharedPreferences, nodes: nodes); + await changeLitecoinCurrentElectrumServerToDefault( + sharedPreferences: sharedPreferences, nodes: nodes); break; case 2: @@ -97,6 +96,7 @@ Future defaultSettingsMigration( case 9: await generateBackupPassword(secureStorage); break; + case 10: await changeTransactionPriorityAndFeeRateKeys(sharedPreferences); break; @@ -110,7 +110,14 @@ Future defaultSettingsMigration( break; case 13: - await resetElectrumServer(nodes, sharedPreferences); + await resetBitcoinElectrumServer(nodes, sharedPreferences); + break; + + case 15: + await addLitecoinElectrumServerList(nodes: nodes); + await changeLitecoinCurrentElectrumServerToDefault( + sharedPreferences: sharedPreferences, nodes: nodes); + await checkCurrentNodes(nodes, sharedPreferences); break; default: @@ -142,7 +149,7 @@ Future replaceNodesMigration({@required Box nodes}) async { final nodeToReplace = replaceNodes[node.uri]; if (nodeToReplace != null) { - node.uri = nodeToReplace.uri; + node.uriRaw = nodeToReplace.uriRaw; node.login = nodeToReplace.login; node.password = nodeToReplace.password; await node.save(); @@ -160,12 +167,21 @@ Future changeMoneroCurrentNodeToDefault( } Node getBitcoinDefaultElectrumServer({@required Box nodes}) { - return nodes.values - .firstWhere((Node node) => node.uri == cakeWalletElectrumUri, orElse: () => null) ?? + return nodes.values.firstWhere( + (Node node) => node.uri == cakeWalletBitcoinElectrumUri, + orElse: () => null) ?? nodes.values.firstWhere((node) => node.type == WalletType.bitcoin, orElse: () => null); } +Node getLitecoinDefaultElectrumServer({@required Box nodes}) { + return nodes.values.firstWhere( + (Node node) => node.uri == cakeWalletLitecoinElectrumUri, + orElse: () => null) ?? + nodes.values.firstWhere((node) => node.type == WalletType.litecoin, + orElse: () => null); +} + Node getMoneroDefaultNode({@required Box nodes}) { final timeZone = DateTime.now().timeZoneOffset.inHours; var nodeUri = ''; @@ -192,6 +208,15 @@ Future changeBitcoinCurrentElectrumServerToDefault( await sharedPreferences.setInt('current_node_id_btc', serverId); } +Future changeLitecoinCurrentElectrumServerToDefault( + {@required SharedPreferences sharedPreferences, + @required Box nodes}) async { + final server = getLitecoinDefaultElectrumServer(nodes: nodes); + final serverId = server?.key as int ?? 0; + + await sharedPreferences.setInt('current_node_id_ltc', serverId); +} + Future replaceDefaultNode( {@required SharedPreferences sharedPreferences, @required Box nodes}) async { @@ -224,7 +249,12 @@ Future updateNodeTypes({@required Box nodes}) async { } Future addBitcoinElectrumServerList({@required Box nodes}) async { - final serverList = await loadElectrumServerList(); + final serverList = await loadBitcoinElectrumServerList(); + await nodes.addAll(serverList); +} + +Future addLitecoinElectrumServerList({@required Box nodes}) async { + final serverList = await loadLitecoinElectrumServerList(); await nodes.addAll(serverList); } @@ -284,57 +314,97 @@ Future changeTransactionPriorityAndFeeRateKeys( Future changeDefaultMoneroNode( Box nodeSource, SharedPreferences sharedPreferences) async { const cakeWalletMoneroNodeUriPattern = '.cakewallet.com'; - final currentMoneroNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); - final currentMoneroNode = nodeSource.values.firstWhere((node) => node.key == currentMoneroNodeId); - final needToReplaceCurrentMoneroNode = currentMoneroNode.uri.contains(cakeWalletMoneroNodeUriPattern); + final currentMoneroNodeId = + sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); + final currentMoneroNode = + nodeSource.values.firstWhere((node) => node.key == currentMoneroNodeId); + final needToReplaceCurrentMoneroNode = + currentMoneroNode.uri.toString().contains(cakeWalletMoneroNodeUriPattern); nodeSource.values.forEach((node) async { - if (node.type == WalletType.monero && node.uri.contains(cakeWalletMoneroNodeUriPattern)) { + if (node.type == WalletType.monero && + node.uri.toString().contains(cakeWalletMoneroNodeUriPattern)) { await node.delete(); } }); - final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero); + final newCakeWalletNode = + Node(uri: newCakeWalletMoneroUri, type: WalletType.monero); await nodeSource.add(newCakeWalletNode); if (needToReplaceCurrentMoneroNode) { - await sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, newCakeWalletNode.key as int); + await sharedPreferences.setInt( + PreferencesKey.currentNodeIdKey, newCakeWalletNode.key as int); } } -Future checkCurrentNodes(Box nodeSource, SharedPreferences sharedPreferences) async { - final currentMoneroNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); - final currentElectrumSeverId = await sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); - final currentMoneroNode = nodeSource.values.firstWhere((node) => node.key == currentMoneroNodeId, orElse: () => null); - final currentElectrumServer = nodeSource.values.firstWhere((node) => node.key == currentElectrumSeverId, orElse: () => null); +Future checkCurrentNodes( + Box nodeSource, SharedPreferences sharedPreferences) async { + final currentMoneroNodeId = + sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); + final currentBitcoinElectrumSeverId = + sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); + final currentLitecoinElectrumSeverId = sharedPreferences + .getInt(PreferencesKey.currentLitecoinElectrumSererIdKey); + final currentMoneroNode = nodeSource.values.firstWhere( + (node) => node.key == currentMoneroNodeId, + orElse: () => null); + final currentBitcoinElectrumServer = nodeSource.values.firstWhere( + (node) => node.key == currentBitcoinElectrumSeverId, + orElse: () => null); + final currentLitecoinElectrumServer = nodeSource.values.firstWhere( + (node) => node.key == currentLitecoinElectrumSeverId, + orElse: () => null); if (currentMoneroNode == null) { - final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero); + final newCakeWalletNode = + Node(uri: newCakeWalletMoneroUri, type: WalletType.monero); await nodeSource.add(newCakeWalletNode); - await sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, newCakeWalletNode.key as int); + await sharedPreferences.setInt( + PreferencesKey.currentNodeIdKey, newCakeWalletNode.key as int); } - if (currentElectrumServer == null) { - final cakeWalletElectrum = Node(uri: cakeWalletElectrumUri, type: WalletType.bitcoin); + if (currentBitcoinElectrumServer == null) { + final cakeWalletElectrum = + Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin); await nodeSource.add(cakeWalletElectrum); - await sharedPreferences.setInt(PreferencesKey.currentBitcoinElectrumSererIdKey, cakeWalletElectrum.key as int); + await sharedPreferences.setInt( + PreferencesKey.currentBitcoinElectrumSererIdKey, + cakeWalletElectrum.key as int); + } + + if (currentLitecoinElectrumServer == null) { + final cakeWalletElectrum = + Node(uri: cakeWalletLitecoinElectrumUri, type: WalletType.litecoin); + await nodeSource.add(cakeWalletElectrum); + await sharedPreferences.setInt( + PreferencesKey.currentLitecoinElectrumSererIdKey, + cakeWalletElectrum.key as int); } } - -Future resetElectrumServer(Box nodeSource, SharedPreferences sharedPreferences) async { - final currentElectrumSeverId = sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); - final oldElectrumServer = nodeSource.values.firstWhere((node) => node.uri.contains('electrumx.cakewallet.com'), orElse: () => null); - var cakeWalletNode = nodeSource.values.firstWhere((node) => node.uri == cakeWalletElectrumUri, orElse: () => null); +Future resetBitcoinElectrumServer( + Box nodeSource, SharedPreferences sharedPreferences) async { + final currentElectrumSeverId = + sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); + final oldElectrumServer = nodeSource.values.firstWhere( + (node) => node.uri.toString().contains('electrumx.cakewallet.com'), + orElse: () => null); + var cakeWalletNode = nodeSource.values.firstWhere( + (node) => node.uri.toString() == cakeWalletBitcoinElectrumUri, + orElse: () => null); if (cakeWalletNode == null) { - cakeWalletNode = Node(uri: cakeWalletElectrumUri, type: WalletType.bitcoin); + cakeWalletNode = + Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin); await nodeSource.add(cakeWalletNode); } if (currentElectrumSeverId == oldElectrumServer?.key) { - await sharedPreferences.setInt(PreferencesKey.currentBitcoinElectrumSererIdKey, cakeWalletNode.key as int); + await sharedPreferences.setInt( + PreferencesKey.currentBitcoinElectrumSererIdKey, + cakeWalletNode.key as int); } await oldElectrumServer?.delete(); diff --git a/lib/entities/fs_migration.dart b/lib/entities/fs_migration.dart index 3e7513117..efaefdd64 100644 --- a/lib/entities/fs_migration.dart +++ b/lib/entities/fs_migration.dart @@ -1,7 +1,10 @@ import 'dart:io'; import 'dart:convert'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:hive/hive.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:cake_wallet/core/key_service.dart'; -import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/crypto_currency.dart'; import 'package:cake_wallet/entities/encrypt.dart'; @@ -14,11 +17,6 @@ import 'package:cake_wallet/entities/wallet_info.dart'; import 'package:cake_wallet/entities/wallet_type.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/exchange/trade.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:flutter/foundation.dart'; -import 'package:hive/hive.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; const reservedNames = ["flutter_assets", "wallets", "db"]; @@ -407,7 +405,7 @@ Future ios_migrate_address_book(Box contactSource) async { } final List addresses = - json.decode(addressBookJSON.readAsStringSync()) as List; + json.decode(addressBookJSON.readAsStringSync()) as List; final contacts = addresses.map((dynamic item) { final _item = item as Map; final type = _item["type"] as String; @@ -420,7 +418,7 @@ Future ios_migrate_address_book(Box contactSource) async { await contactSource.addAll(contacts); await prefs.setBool('ios_migration_address_book_completed', true); - } catch(e) { + } catch (e) { print(e.toString()); } } diff --git a/lib/entities/node.dart b/lib/entities/node.dart index 2636f9601..fe2823352 100644 --- a/lib/entities/node.dart +++ b/lib/entities/node.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:cake_wallet/utils/mobx.dart'; import 'package:flutter/foundation.dart'; import 'dart:convert'; @@ -8,19 +10,23 @@ import 'package:cake_wallet/entities/digest_request.dart'; part 'node.g.dart'; +Uri createUriFromElectrumAddress(String address) => + Uri.tryParse('tcp://$address'); + @HiveType(typeId: Node.typeId) class Node extends HiveObject with Keyable { Node( - {@required this.uri, + {@required String uri, @required WalletType type, this.login, this.password, this.useSSL}) { + uriRaw = uri; this.type = type; } Node.fromMap(Map map) - : uri = map['uri'] as String ?? '', + : uriRaw = map['uri'] as String ?? '', login = map['login'] as String, password = map['password'] as String, typeRaw = map['typeRaw'] as int, @@ -30,7 +36,7 @@ class Node extends HiveObject with Keyable { static const boxName = 'Nodes'; @HiveField(0) - String uri; + String uriRaw; @HiveField(1) String login; @@ -46,6 +52,19 @@ class Node extends HiveObject with Keyable { bool get isSSL => useSSL ?? false; + Uri get uri { + switch (type) { + case WalletType.monero: + return Uri.http(uriRaw, ''); + case WalletType.bitcoin: + return createUriFromElectrumAddress(uriRaw); + case WalletType.litecoin: + return createUriFromElectrumAddress(uriRaw); + default: + return null; + } + } + @override dynamic get keyIndex { _keyIndex ??= key; @@ -64,7 +83,9 @@ class Node extends HiveObject with Keyable { case WalletType.monero: return requestMoneroNode(); case WalletType.bitcoin: - return requestBitcoinElectrumServer(); + return requestElectrumServer(); + case WalletType.litecoin: + return requestElectrumServer(); default: return false; } @@ -80,15 +101,15 @@ class Node extends HiveObject with Keyable { if (login != null && password != null) { final digestRequest = DigestRequest(); final response = await digestRequest.request( - uri: uri, login: login, password: password); + uri: uri.toString(), login: login, password: password); resBody = response.data as Map; } else { - final url = Uri.http(uri, '/json_rpc'); + final rpcUri = Uri.http(uri.toString(), '/json_rpc'); final headers = {'Content-type': 'application/json'}; final body = json.encode({'jsonrpc': '2.0', 'id': '0', 'method': 'get_info'}); final response = - await http.post(url.toString(), headers: headers, body: body); + await http.post(rpcUri.toString(), headers: headers, body: body); resBody = json.decode(response.body) as Map; } @@ -98,8 +119,13 @@ class Node extends HiveObject with Keyable { } } - Future requestBitcoinElectrumServer() async { - // FIXME: IMPLEMENT ME - return true; + Future requestElectrumServer() async { + try { + await SecureSocket.connect(uri.host, uri.port, + timeout: Duration(seconds: 5), onBadCertificate: (_) => true); + return true; + } catch (_) { + return false; + } } } diff --git a/lib/entities/node_list.dart b/lib/entities/node_list.dart index 118394e6f..a35ed47d4 100644 --- a/lib/entities/node_list.dart +++ b/lib/entities/node_list.dart @@ -20,9 +20,9 @@ Future> loadDefaultNodes() async { }).toList(); } -Future> loadElectrumServerList() async { +Future> loadBitcoinElectrumServerList() async { final serverListRaw = - await rootBundle.loadString('assets/electrum_server_list.yml'); + await rootBundle.loadString('assets/bitcoin_electrum_server_list.yml'); final serverList = loadYaml(serverListRaw) as YamlList; return serverList.map((dynamic raw) { @@ -37,10 +37,29 @@ Future> loadElectrumServerList() async { }).toList(); } +Future> loadLitecoinElectrumServerList() async { + final serverListRaw = + await rootBundle.loadString('assets/litecoin_electrum_server_list.yml'); + final serverList = loadYaml(serverListRaw) as YamlList; + + return serverList.map((dynamic raw) { + if (raw is Map) { + final node = Node.fromMap(raw); + node?.type = WalletType.litecoin; + + return node; + } + + return null; + }).toList(); +} + Future resetToDefault(Box nodeSource) async { final moneroNodes = await loadDefaultNodes(); - final bitcoinElectrumServerList = await loadElectrumServerList(); - final nodes = moneroNodes + bitcoinElectrumServerList; + final bitcoinElectrumServerList = await loadBitcoinElectrumServerList(); + final litecoinElectrumServerList = await loadLitecoinElectrumServerList(); + final nodes = + moneroNodes + bitcoinElectrumServerList + litecoinElectrumServerList; await nodeSource.clear(); await nodeSource.addAll(nodes); diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 15dbc2fb8..6d55748cc 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -1,8 +1,9 @@ class PreferencesKey { - static const currentWalletType ='current_wallet_type'; - static const currentWalletName ='current_wallet_name'; + static const currentWalletType = 'current_wallet_type'; + static const currentWalletName = 'current_wallet_name'; static const currentNodeIdKey = 'current_node_id'; static const currentBitcoinElectrumSererIdKey = 'current_node_id_btc'; + static const currentLitecoinElectrumSererIdKey = 'current_node_id_ltc'; static const currentFiatCurrencyKey = 'current_fiat_currency'; static const currentTransactionPriorityKeyLegacy = 'current_fee_priority'; static const currentBalanceDisplayModeKey = 'current_balance_display_mode'; @@ -14,7 +15,8 @@ class PreferencesKey { static const displayActionListModeKey = 'display_list_mode'; static const currentPinLength = 'current_pin_length'; static const currentLanguageCode = 'language_code'; - static const currentDefaultSettingsMigrationVersion = 'current_default_settings_migration_version'; + static const currentDefaultSettingsMigrationVersion = + 'current_default_settings_migration_version'; static const moneroTransactionPriority = 'current_fee_priority_monero'; static const bitcoinTransactionPriority = 'current_fee_priority_bitcoin'; -} \ No newline at end of file +} diff --git a/lib/entities/wallet_type.dart b/lib/entities/wallet_type.dart index f4375e43e..d59f336c0 100644 --- a/lib/entities/wallet_type.dart +++ b/lib/entities/wallet_type.dart @@ -3,7 +3,11 @@ import 'package:hive/hive.dart'; part 'wallet_type.g.dart'; -const walletTypes = [WalletType.monero, WalletType.bitcoin]; +const walletTypes = [ + WalletType.monero, + WalletType.bitcoin, + WalletType.litecoin +]; const walletTypeTypeId = 5; @HiveType(typeId: walletTypeTypeId) @@ -15,7 +19,10 @@ enum WalletType { none, @HiveField(2) - bitcoin + bitcoin, + + @HiveField(3) + litecoin } int serializeToInt(WalletType type) { @@ -24,6 +31,8 @@ int serializeToInt(WalletType type) { return 0; case WalletType.bitcoin: return 1; + case WalletType.litecoin: + return 2; default: return -1; } @@ -35,6 +44,8 @@ WalletType deserializeFromInt(int raw) { return WalletType.monero; case 1: return WalletType.bitcoin; + case 2: + return WalletType.litecoin; default: return null; } @@ -46,6 +57,8 @@ String walletTypeToString(WalletType type) { return 'Monero'; case WalletType.bitcoin: return 'Bitcoin'; + case WalletType.litecoin: + return 'Litecoin'; default: return ''; } @@ -57,6 +70,8 @@ String walletTypeToDisplayName(WalletType type) { return 'Monero'; case WalletType.bitcoin: return 'Bitcoin (Electrum)'; + case WalletType.litecoin: + return 'Litecoin'; default: return ''; } @@ -68,6 +83,8 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type) { return CryptoCurrency.xmr; case WalletType.bitcoin: return CryptoCurrency.btc; + case WalletType.litecoin: + return CryptoCurrency.ltc; default: return null; } diff --git a/lib/entities/wyre_service.dart b/lib/entities/wyre_service.dart index b83bfce68..63346f0e4 100644 --- a/lib/entities/wyre_service.dart +++ b/lib/entities/wyre_service.dart @@ -1,7 +1,7 @@ import 'dart:convert'; -import 'package:cake_wallet/core/wallet_base.dart'; import 'package:cake_wallet/entities/wyre_exception.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; +import 'package:cake_wallet/store/app_store.dart'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; @@ -9,15 +9,9 @@ import 'package:cake_wallet/entities/order.dart'; import 'package:cake_wallet/entities/wallet_type.dart'; class WyreService { - WyreService({ - @required this.wallet, - this.isTestEnvironment = false}) { - baseApiUrl = isTestEnvironment - ? _baseTestApiUrl - : _baseProductApiUrl; - trackUrl = isTestEnvironment - ? _trackTestUrl - : _trackProductUrl; + WyreService({@required this.appStore, this.isTestEnvironment = false}) { + baseApiUrl = isTestEnvironment ? _baseTestApiUrl : _baseProductApiUrl; + trackUrl = isTestEnvironment ? _trackTestUrl : _trackProductUrl; } static const _baseTestApiUrl = 'https://api.testwyre.com'; @@ -31,24 +25,28 @@ class WyreService { static const _trackSuffix = '/track'; final bool isTestEnvironment; - final WalletBase wallet; + final AppStore appStore; - WalletType get walletType => wallet.type; - String get walletAddress => wallet.address; - String get walletId => wallet.id; + WalletType get walletType => appStore.wallet.type; + String get walletAddress => appStore.wallet.address; + String get walletId => appStore.wallet.id; String baseApiUrl; String trackUrl; Future getWyreUrl() async { final timestamp = DateTime.now().millisecondsSinceEpoch.toString(); - final url = baseApiUrl + _ordersSuffix + _reserveSuffix + - _timeStampSuffix + timestamp; + final url = baseApiUrl + + _ordersSuffix + + _reserveSuffix + + _timeStampSuffix + + timestamp; final secretKey = secrets.wyreSecretKey; final accountId = secrets.wyreAccountId; final body = { 'destCurrency': walletTypeToCryptoCurrency(walletType).title, - 'dest': walletTypeToString(walletType).toLowerCase() + ':' + walletAddress, + 'dest': + walletTypeToString(walletType).toLowerCase() + ':' + walletAddress, 'referrerAccountId': accountId, 'lockFields': ['destCurrency', 'dest'] }; @@ -79,7 +77,7 @@ class WyreService { } final orderResponseJSON = - json.decode(orderResponse.body) as Map; + json.decode(orderResponse.body) as Map; final transferId = orderResponseJSON['transferId'] as String; final from = orderResponseJSON['sourceCurrency'] as String; final to = orderResponseJSON['destCurrency'] as String; @@ -87,7 +85,7 @@ class WyreService { final state = TradeState.deserialize(raw: status.toLowerCase()); final createdAtRaw = orderResponseJSON['createdAt'] as int; final createdAt = - DateTime.fromMillisecondsSinceEpoch(createdAtRaw).toLocal(); + DateTime.fromMillisecondsSinceEpoch(createdAtRaw).toLocal(); final transferUrl = baseApiUrl + _transferSuffix + transferId + _trackSuffix; @@ -98,7 +96,7 @@ class WyreService { } final transferResponseJSON = - json.decode(transferResponse.body) as Map; + json.decode(transferResponse.body) as Map; final amount = transferResponseJSON['destAmount'] as double; return Order( @@ -110,7 +108,6 @@ class WyreService { createdAt: createdAt, amount: amount.toString(), receiveAddress: walletAddress, - walletId: walletId - ); + walletId: walletId); } -} \ No newline at end of file +} diff --git a/lib/main.dart b/lib/main.dart index 48b54c416..1a1724e3e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -107,7 +107,7 @@ Future main() async { exchangeTemplates: exchangeTemplates, transactionDescriptions: transactionDescriptions, secureStorage: secureStorage, - initialMigrationVersion: 13); + initialMigrationVersion: 15); runApp(App()); } catch (e) { runApp(MaterialApp( @@ -135,7 +135,7 @@ Future initialSetup( @required Box exchangeTemplates, @required Box transactionDescriptions, FlutterSecureStorage secureStorage, - int initialMigrationVersion = 13}) async { + int initialMigrationVersion = 15}) async { LanguageService.loadLocaleList(); await defaultSettingsMigration( secureStorage: secureStorage, diff --git a/lib/monero/monero_account_list.dart b/lib/monero/monero_account_list.dart index 9792d3c1d..9e3b14931 100644 --- a/lib/monero/monero_account_list.dart +++ b/lib/monero/monero_account_list.dart @@ -49,13 +49,13 @@ abstract class MoneroAccountListBase with Store { Future addAccount({String label}) async { await account_list.addAccount(label: label); - await update(); + update(); } Future setLabelAccount({int accountIndex, String label}) async { await account_list.setLabelForAccount( accountIndex: accountIndex, label: label); - await update(); + update(); } void refresh() { diff --git a/lib/monero/monero_transaction_history.dart b/lib/monero/monero_transaction_history.dart index 93c00f376..11eaff4b6 100644 --- a/lib/monero/monero_transaction_history.dart +++ b/lib/monero/monero_transaction_history.dart @@ -1,18 +1,10 @@ import 'dart:core'; import 'package:mobx/mobx.dart'; -import 'package:cw_monero/transaction_history.dart' - as monero_transaction_history; import 'package:cake_wallet/core/transaction_history.dart'; import 'package:cake_wallet/monero/monero_transaction_info.dart'; part 'monero_transaction_history.g.dart'; -List _getAllTransactions(dynamic _) => - monero_transaction_history - .getAllTransations() - .map((row) => MoneroTransactionInfo.fromRow(row)) - .toList(); - class MoneroTransactionHistory = MoneroTransactionHistoryBase with _$MoneroTransactionHistory; @@ -23,30 +15,13 @@ abstract class MoneroTransactionHistoryBase } @override - Future> fetchTransactions() async { - monero_transaction_history.refreshTransactions(); - return _getAllTransactions(null).fold>( - {}, - (Map acc, MoneroTransactionInfo tx) { - acc[tx.id] = tx; - return acc; - }); - } + Future save() async {} @override - @action - void updateAsync({void Function() onFinished}) { - fetchTransactionsAsync( - (transaction) => transactions[transaction.id] = transaction, - onFinished: onFinished); - } + void addOne(MoneroTransactionInfo transaction) => + transactions[transaction.id] = transaction; @override - void fetchTransactionsAsync( - void Function(MoneroTransactionInfo transaction) onTransactionLoaded, - {void Function() onFinished}) async { - final transactions = await fetchTransactions(); - transactions.values.forEach((tx) => onTransactionLoaded(tx)); - onFinished?.call(); - } + void addMany(Map transactions) => + this.transactions.addAll(transactions); } diff --git a/lib/monero/monero_transaction_info.dart b/lib/monero/monero_transaction_info.dart index 71bc5957a..6f099fbfa 100644 --- a/lib/monero/monero_transaction_info.dart +++ b/lib/monero/monero_transaction_info.dart @@ -59,6 +59,7 @@ class MoneroTransactionInfo extends TransactionInfo { @override void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); + @override String feeFormatted() => '${formatAmount(moneroAmountToString(amount: fee))} XMR'; } diff --git a/lib/monero/monero_wallet.dart b/lib/monero/monero_wallet.dart index 0891100c8..92742273c 100644 --- a/lib/monero/monero_wallet.dart +++ b/lib/monero/monero_wallet.dart @@ -1,10 +1,12 @@ import 'dart:async'; - import 'package:cake_wallet/entities/transaction_priority.dart'; import 'package:cake_wallet/monero/monero_amount_format.dart'; import 'package:cake_wallet/monero/monero_transaction_creation_exception.dart'; +import 'package:cake_wallet/monero/monero_transaction_info.dart'; import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; +import 'package:cw_monero/transaction_history.dart' + as monero_transaction_history; import 'package:cw_monero/wallet.dart'; import 'package:cw_monero/wallet.dart' as monero_wallet; import 'package:cw_monero/transaction_history.dart' as transaction_history; @@ -30,19 +32,21 @@ const moneroBlockSize = 1000; class MoneroWallet = MoneroWalletBase with _$MoneroWallet; -abstract class MoneroWalletBase extends WalletBase with Store { - MoneroWalletBase({String filename, WalletInfo walletInfo}) - : transactionHistory = MoneroTransactionHistory(), - accountList = MoneroAccountList(), +abstract class MoneroWalletBase extends WalletBase with Store { + MoneroWalletBase({WalletInfo walletInfo}) + : accountList = MoneroAccountList(), subaddressList = MoneroSubaddressList(), super(walletInfo) { - _filename = filename; + transactionHistory = MoneroTransactionHistory(); balance = MoneroBalance( fullBalance: monero_wallet.getFullBalance(accountIndex: 0), unlockedBalance: monero_wallet.getFullBalance(accountIndex: 0)); _lastAutosaveTimestamp = 0; + _lastSaveTimestamp = 0; _isSavingAfterSync = false; _isSavingAfterNewTransaction = false; + _isTransactionUpdating = false; _onAccountChangeReaction = reaction((_) => account, (Account account) { balance = MoneroBalance( fullBalance: monero_wallet.getFullBalance(accountIndex: account.id), @@ -56,9 +60,6 @@ abstract class MoneroWalletBase extends WalletBase with Store { static const int _autoAfterSyncSaveInterval = 60000; - @override - final MoneroTransactionHistory transactionHistory; - @observable Account account; @@ -91,12 +92,13 @@ abstract class MoneroWalletBase extends WalletBase with Store { final MoneroAccountList accountList; - String _filename; SyncListener _listener; ReactionDisposer _onAccountChangeReaction; int _lastAutosaveTimestamp; bool _isSavingAfterSync; bool _isSavingAfterNewTransaction; + bool _isTransactionUpdating; + int _lastSaveTimestamp; Future init() async { accountList.update(); @@ -109,7 +111,7 @@ abstract class MoneroWalletBase extends WalletBase with Store { monero_wallet.getUnlockedBalance(accountIndex: account.id)); address = subaddress.address; _setListeners(); - await transactionHistory.update(); + await updateTransactions(); if (walletInfo.isRecovery) { monero_wallet.setRecoveringFromSeed(isRecovery: walletInfo.isRecovery); @@ -150,7 +152,7 @@ abstract class MoneroWalletBase extends WalletBase with Store { try { syncStatus = ConnectingSyncStatus(); await monero_wallet.setupNode( - address: node.uri, + address: node.uri.toString(), login: node.login, password: node.password, useSSL: node.isSSL, @@ -236,6 +238,13 @@ abstract class MoneroWalletBase extends WalletBase with Store { @override Future save() async { + final now = DateTime.now().millisecondsSinceEpoch; + + if (now - _lastSaveTimestamp < Duration(seconds: 10).inMilliseconds) { + return; + } + + _lastSaveTimestamp = now; await monero_wallet.store(); } @@ -262,6 +271,40 @@ abstract class MoneroWalletBase extends WalletBase with Store { await walletInfo.save(); } + @override + Future> fetchTransactions() async { + monero_transaction_history.refreshTransactions(); + return _getAllTransactions(null).fold>( + {}, + (Map acc, MoneroTransactionInfo tx) { + acc[tx.id] = tx; + return acc; + }); + } + + Future updateTransactions() async { + try { + if (_isTransactionUpdating) { + return; + } + + _isTransactionUpdating = true; + final transactions = await fetchTransactions(); + transactionHistory.addMany(transactions); + await transactionHistory.save(); + _isTransactionUpdating = false; + } catch (e) { + print(e); + _isTransactionUpdating = false; + } + } + + List _getAllTransactions(dynamic _) => + monero_transaction_history + .getAllTransations() + .map((row) => MoneroTransactionInfo.fromRow(row)) + .toList(); + void _setListeners() { _listener?.stop(); _listener = monero_wallet.setListeners(_onNewBlock, _onNewTransaction); @@ -313,7 +356,7 @@ abstract class MoneroWalletBase extends WalletBase with Store { } Future _askForUpdateTransactionHistory() async => - await transactionHistory.update(); + await updateTransactions(); int _getFullBalance() => monero_wallet.getFullBalance(accountIndex: account.id); @@ -387,11 +430,12 @@ abstract class MoneroWalletBase extends WalletBase with Store { } } - void _onNewTransaction() { + void _onNewTransaction() async { try { - _askForUpdateTransactionHistory(); + await _askForUpdateTransactionHistory(); _askForUpdateBalance(); - Timer(Duration(seconds: 1), () => _afterNewTransactionSave()); + await Future.delayed(Duration(seconds: 1)); + await _afterNewTransactionSave(); } catch (e) { print(e.toString()); } diff --git a/lib/monero/monero_wallet_service.dart b/lib/monero/monero_wallet_service.dart index 79882c0d3..7795b8700 100644 --- a/lib/monero/monero_wallet_service.dart +++ b/lib/monero/monero_wallet_service.dart @@ -68,18 +68,18 @@ class MoneroWalletService extends WalletService< static bool walletFilesExist(String path) => !File(path).existsSync() && !File('$path.keys').existsSync(); + @override + WalletType getType() => WalletType.monero; + @override Future create(MoneroNewWalletCredentials credentials) async { try { - final path = - await pathForWallet(name: credentials.name, type: WalletType.monero); + final path = await pathForWallet(name: credentials.name, type: getType()); await monero_wallet_manager.createWallet( path: path, password: credentials.password, language: credentials.language); - final wallet = MoneroWallet( - filename: monero_wallet.getFilename(), - walletInfo: credentials.walletInfo); + final wallet = MoneroWallet(walletInfo: credentials.walletInfo); await wallet.init(); return wallet; @@ -93,7 +93,7 @@ class MoneroWalletService extends WalletService< @override Future isWalletExit(String name) async { try { - final path = await pathForWallet(name: name, type: WalletType.monero); + final path = await pathForWallet(name: name, type: getType()); return monero_wallet_manager.isWalletExist(path: path); } catch (e) { // TODO: Implement Exception for wallet list service. @@ -105,7 +105,7 @@ class MoneroWalletService extends WalletService< @override Future openWallet(String name, String password) async { try { - final path = await pathForWallet(name: name, type: WalletType.monero); + final path = await pathForWallet(name: name, type: getType()); if (walletFilesExist(path)) { await repairOldAndroidWallet(name); @@ -114,10 +114,9 @@ class MoneroWalletService extends WalletService< await monero_wallet_manager .openWalletAsync({'path': path, 'password': password}); final walletInfo = walletInfoSource.values.firstWhere( - (info) => info.id == WalletBase.idFor(name, WalletType.monero), + (info) => info.id == WalletBase.idFor(name, getType()), orElse: () => null); - final wallet = MoneroWallet( - filename: monero_wallet.getFilename(), walletInfo: walletInfo); + final wallet = MoneroWallet(walletInfo: walletInfo); final isValid = wallet.validate(); if (!isValid) { @@ -146,7 +145,7 @@ class MoneroWalletService extends WalletService< @override Future remove(String wallet) async { - final path = await pathForWalletDir(name: wallet, type: WalletType.monero); + final path = await pathForWalletDir(name: wallet, type: getType()); final file = Directory(path); final isExist = file.existsSync(); @@ -159,8 +158,7 @@ class MoneroWalletService extends WalletService< Future restoreFromKeys( MoneroRestoreWalletFromKeysCredentials credentials) async { try { - final path = - await pathForWallet(name: credentials.name, type: WalletType.monero); + final path = await pathForWallet(name: credentials.name, type: getType()); await monero_wallet_manager.restoreFromKeys( path: path, password: credentials.password, @@ -169,9 +167,7 @@ class MoneroWalletService extends WalletService< address: credentials.address, viewKey: credentials.viewKey, spendKey: credentials.spendKey); - final wallet = MoneroWallet( - filename: monero_wallet.getFilename(), - walletInfo: credentials.walletInfo); + final wallet = MoneroWallet(walletInfo: credentials.walletInfo); await wallet.init(); return wallet; @@ -186,16 +182,13 @@ class MoneroWalletService extends WalletService< Future restoreFromSeed( MoneroRestoreWalletFromSeedCredentials credentials) async { try { - final path = - await pathForWallet(name: credentials.name, type: WalletType.monero); + final path = await pathForWallet(name: credentials.name, type: getType()); await monero_wallet_manager.restoreFromSeed( path: path, password: credentials.password, seed: credentials.mnemonic, restoreHeight: credentials.height); - final wallet = MoneroWallet( - filename: monero_wallet.getFilename(), - walletInfo: credentials.walletInfo); + final wallet = MoneroWallet(walletInfo: credentials.walletInfo); await wallet.init(); return wallet; @@ -221,7 +214,7 @@ class MoneroWalletService extends WalletService< } final newWalletDirPath = - await pathForWalletDir(name: name, type: WalletType.monero); + await pathForWalletDir(name: name, type: getType()); dir.listSync().forEach((f) { final file = File(f.path); diff --git a/lib/reactions/check_connection.dart b/lib/reactions/check_connection.dart index f89943856..b5550fcff 100644 --- a/lib/reactions/check_connection.dart +++ b/lib/reactions/check_connection.dart @@ -7,19 +7,22 @@ import 'package:connectivity/connectivity.dart'; Timer _checkConnectionTimer; -void startCheckConnectionReaction(WalletBase wallet, SettingsStore settingsStore, {int timeInterval = 5}) { +void startCheckConnectionReaction( + WalletBase wallet, SettingsStore settingsStore, + {int timeInterval = 5}) { _checkConnectionTimer?.cancel(); - _checkConnectionTimer = Timer.periodic(Duration(seconds: timeInterval), (_) async { - final connectivityResult = await (Connectivity().checkConnectivity()); + _checkConnectionTimer = + Timer.periodic(Duration(seconds: timeInterval), (_) async { + try { + final connectivityResult = await (Connectivity().checkConnectivity()); - if (connectivityResult == ConnectivityResult.none) { - wallet.syncStatus = FailedSyncStatus(); - return; - } + if (connectivityResult == ConnectivityResult.none) { + wallet.syncStatus = FailedSyncStatus(); + return; + } - if (wallet.syncStatus is LostConnectionSyncStatus || - wallet.syncStatus is FailedSyncStatus) { - try { + if (wallet.syncStatus is LostConnectionSyncStatus || + wallet.syncStatus is FailedSyncStatus) { final alive = await settingsStore.getCurrentNode(wallet.type).requestNode(); @@ -27,9 +30,9 @@ void startCheckConnectionReaction(WalletBase wallet, SettingsStore settingsStore await wallet.connectToNode( node: settingsStore.getCurrentNode(wallet.type)); } - } catch (_) { - // FIXME: empty catch clojure } + } catch (e) { + print(e.toString()); } }); } diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index 17e2ca4f9..428f2703e 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -1,4 +1,6 @@ +import 'package:cake_wallet/core/transaction_history.dart'; import 'package:cake_wallet/entities/balance.dart'; +import 'package:cake_wallet/entities/transaction_info.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cake_wallet/di.dart'; @@ -18,9 +20,11 @@ ReactionDisposer _onCurrentWalletChangeFiatRateUpdateReaction; void startCurrentWalletChangeReaction(AppStore appStore, SettingsStore settingsStore, FiatConversionStore fiatConversionStore) { _onCurrentWalletChangeReaction?.reaction?.dispose(); + _onCurrentWalletChangeFiatRateUpdateReaction?.reaction?.dispose(); - _onCurrentWalletChangeReaction = - reaction((_) => appStore.wallet, (WalletBase wallet) async { + _onCurrentWalletChangeReaction = reaction((_) => appStore.wallet, (WalletBase< + Balance, TransactionHistoryBase, TransactionInfo> + wallet) async { try { final node = settingsStore.getCurrentNode(wallet.type); startWalletSyncStatusChangeReaction(wallet); @@ -45,7 +49,9 @@ void startCurrentWalletChangeReaction(AppStore appStore, }); _onCurrentWalletChangeFiatRateUpdateReaction = - reaction((_) => appStore.wallet, (WalletBase wallet) async { + reaction((_) => appStore.wallet, (WalletBase, TransactionInfo> + wallet) async { try { fiatConversionStore.prices[wallet.currency] = 0; fiatConversionStore.prices[wallet.currency] = diff --git a/lib/reactions/on_wallet_sync_status_change.dart b/lib/reactions/on_wallet_sync_status_change.dart index 8ad1c2e1b..f02dd0441 100644 --- a/lib/reactions/on_wallet_sync_status_change.dart +++ b/lib/reactions/on_wallet_sync_status_change.dart @@ -1,16 +1,21 @@ -import 'package:cake_wallet/entities/balance.dart'; import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/core/transaction_history.dart'; import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/entities/balance.dart'; +import 'package:cake_wallet/entities/transaction_info.dart'; import 'package:cake_wallet/entities/sync_status.dart'; ReactionDisposer _onWalletSyncStatusChangeReaction; -void startWalletSyncStatusChangeReaction(WalletBase wallet) { +void startWalletSyncStatusChangeReaction( + WalletBase, + TransactionInfo> + wallet) { _onWalletSyncStatusChangeReaction?.reaction?.dispose(); _onWalletSyncStatusChangeReaction = reaction((_) => wallet.syncStatus, (SyncStatus status) async { - if (status is ConnectedSyncStatus) { - await wallet.startSync(); - } - }); -} \ No newline at end of file + if (status is ConnectedSyncStatus) { + await wallet.startSync(); + } + }); +} diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index 400b34060..46a4185ce 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -16,6 +16,7 @@ import 'package:cake_wallet/src/screens/dashboard/widgets/transactions_page.dart import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; import 'package:smooth_page_indicator/smooth_page_indicator.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; @@ -26,8 +27,8 @@ class DashboardPage extends BasePage { }); @override - Color get backgroundLightColor => currentTheme.type == ThemeType.bright - ? Colors.transparent : Colors.white; + Color get backgroundLightColor => + currentTheme.type == ThemeType.bright ? Colors.transparent : Colors.white; @override Color get backgroundDarkColor => Colors.transparent; @@ -56,9 +57,8 @@ class DashboardPage extends BasePage { @override Widget trailing(BuildContext context) { - final menuButton = - Image.asset('assets/images/menu.png', - color: Theme.of(context).accentTextTheme.display3.backgroundColor); + final menuButton = Image.asset('assets/images/menu.png', + color: Theme.of(context).accentTextTheme.display3.backgroundColor); return Container( alignment: Alignment.centerRight, @@ -81,15 +81,18 @@ class DashboardPage extends BasePage { @override Widget body(BuildContext context) { final sendImage = Image.asset('assets/images/upload.png', - height: 22.24, width: 24, + height: 22.24, + width: 24, color: Theme.of(context).accentTextTheme.display3.backgroundColor); final exchangeImage = Image.asset('assets/images/transfer.png', - height: 24.27, width: 22.25, + height: 24.27, + width: 22.25, color: Theme.of(context).accentTextTheme.display3.backgroundColor); final buyImage = Image.asset('assets/images/coins.png', - height: 22.24, width: 24, + height: 22.24, + width: 24, color: Theme.of(context).accentTextTheme.display3.backgroundColor); - _setEffects(); + _setEffects(context); return SafeArea( child: Column( @@ -111,7 +114,9 @@ class DashboardPage extends BasePage { dotWidth: 6.0, dotHeight: 6.0, dotColor: Theme.of(context).indicatorColor, - activeDotColor: Theme.of(context).accentTextTheme.display1 + activeDotColor: Theme.of(context) + .accentTextTheme + .display1 .backgroundColor), )), Container( @@ -129,25 +134,27 @@ class DashboardPage extends BasePage { route: Routes.exchange), Observer( builder: (_) => Stack( - clipBehavior: Clip.none, - alignment: Alignment.topCenter, - children: [ - if (walletViewModel.isRunningWebView) Positioned( - top: -5, - child: SpinKitRing( - color: Theme.of(context).buttonColor, - lineWidth: 3, - size: 70.0, - ), - ), - ActionButton( - image: buyImage, - title: S.of(context).buy, - onClick: walletViewModel.isRunningWebView - ? null - : () async => await _onClickBuyButton(context)) - ], - )), + clipBehavior: Clip.none, + alignment: Alignment.topCenter, + children: [ + if (walletViewModel.isRunningWebView) + Positioned( + top: -5, + child: SpinKitRing( + color: Theme.of(context).buttonColor, + lineWidth: 3, + size: 70.0, + ), + ), + ActionButton( + image: buyImage, + title: S.of(context).buy, + onClick: walletViewModel.isRunningWebView + ? null + : () async => + await _onClickBuyButton(context)) + ], + )), ], ), ) @@ -155,7 +162,7 @@ class DashboardPage extends BasePage { )); } - void _setEffects() { + void _setEffects(BuildContext context) { if (_isEffectsInstalled) { return; } @@ -164,14 +171,42 @@ class DashboardPage extends BasePage { pages.add(BalancePage(dashboardViewModel: walletViewModel)); pages.add(TransactionsPage(dashboardViewModel: walletViewModel)); + autorun((_) async { + if (!walletViewModel.isOutdatedElectrumWallet) { + return; + } + + await Future.delayed(Duration(seconds: 1)); + await showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.of(context).pre_seed_title, + alertContent: + S.of(context).outdated_electrum_wallet_desceription, + buttonText: S.of(context).understand, + buttonAction: () => Navigator.of(context).pop()); + }); + }); + _isEffectsInstalled = true; } - Future _onClickBuyButton(BuildContext context) async { + Future _onClickBuyButton(BuildContext context) async { final walletType = walletViewModel.type; switch (walletType) { - case WalletType.monero: + case WalletType.bitcoin: + try { + walletViewModel.isRunningWebView = true; + final url = await walletViewModel.wyreViewModel.wyreUrl; + await Navigator.of(context).pushNamed(Routes.wyre, arguments: url); + walletViewModel.isRunningWebView = false; + } catch (_) { + walletViewModel.isRunningWebView = false; + } + break; + default: await showPopUp( context: context, builder: (BuildContext context) { @@ -182,16 +217,6 @@ class DashboardPage extends BasePage { buttonAction: () => Navigator.of(context).pop()); }); break; - default: - try { - walletViewModel.isRunningWebView = true; - final url = await walletViewModel.wyreViewModel.wyreUrl; - await Navigator.of(context).pushNamed(Routes.wyre, arguments: url); - walletViewModel.isRunningWebView = false; - } catch(_) { - walletViewModel.isRunningWebView = false; - } - break; } } } diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index 5e45152ce..d9e34280a 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -21,6 +21,7 @@ class MenuWidget extends StatefulWidget { class MenuWidgetState extends State { Image moneroIcon; Image bitcoinIcon; + Image litecoinIcon; final largeScreen = 731; double menuWidth; @@ -76,6 +77,7 @@ class MenuWidgetState extends State { color: Theme.of(context).accentTextTheme.overline.decorationColor); bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png', color: Theme.of(context).accentTextTheme.overline.decorationColor); + litecoinIcon = Image.asset('assets/images/litecoin_menu.png'); return Row( mainAxisSize: MainAxisSize.max, @@ -238,6 +240,8 @@ class MenuWidgetState extends State { return moneroIcon; case WalletType.bitcoin: return bitcoinIcon; + case WalletType.litecoin: + return litecoinIcon; default: return null; } diff --git a/lib/src/screens/new_wallet/new_wallet_type_page.dart b/lib/src/screens/new_wallet/new_wallet_type_page.dart index 3e74c3855..f808a237c 100644 --- a/lib/src/screens/new_wallet/new_wallet_type_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_type_page.dart @@ -59,6 +59,8 @@ class WalletTypeFormState extends State { Image.asset('assets/images/monero_logo.png', height: 24, width: 24); final bitcoinIcon = Image.asset('assets/images/bitcoin.png', height: 24, width: 24); + final litecoinIcon = + Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24); final walletTypeImage = Image.asset('assets/images/wallet_type.png'); final walletTypeLightImage = Image.asset('assets/images/wallet_type_light.png'); @@ -69,7 +71,7 @@ class WalletTypeFormState extends State { @override void initState() { - types = [WalletType.bitcoin, WalletType.monero]; + types = [WalletType.bitcoin, WalletType.monero, WalletType.litecoin]; super.initState(); } @@ -84,8 +86,7 @@ class WalletTypeFormState extends State { padding: EdgeInsets.only(left: 12, right: 12), child: AspectRatio( aspectRatio: aspectRatioImage, - child: - FittedBox(child: widget.walletImage, fit: BoxFit.fill)), + child: FittedBox(child: widget.walletImage, fit: BoxFit.fill)), ), Padding( padding: EdgeInsets.only(top: 48), @@ -99,13 +100,13 @@ class WalletTypeFormState extends State { ), ), ...types.map((type) => Padding( - padding: EdgeInsets.only(top: 24), - child: SelectButton( - image: _iconFor(type), - text: walletTypeToDisplayName(type), - isSelected: selected == type, - onTap: () => setState(() => selected = type)), - )) + padding: EdgeInsets.only(top: 24), + child: SelectButton( + image: _iconFor(type), + text: walletTypeToDisplayName(type), + isSelected: selected == type, + onTap: () => setState(() => selected = type)), + )) ], ), bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), @@ -125,6 +126,8 @@ class WalletTypeFormState extends State { return moneroIcon; case WalletType.bitcoin: return bitcoinIcon; + case WalletType.litecoin: + return litecoinIcon; default: return null; } diff --git a/lib/src/screens/nodes/nodes_list_page.dart b/lib/src/screens/nodes/nodes_list_page.dart index 87753e1ce..fa4762068 100644 --- a/lib/src/screens/nodes/nodes_list_page.dart +++ b/lib/src/screens/nodes/nodes_list_page.dart @@ -87,7 +87,7 @@ class NodeListPage extends BasePage { final isSelected = node.keyIndex == nodeListViewModel.currentNode?.keyIndex; final nodeListRow = NodeListRow( - title: node.uri, + title: node.uriRaw, isSelected: isSelected, isAlive: node.requestNode(), onTap: (_) async { @@ -101,8 +101,9 @@ class NodeListPage extends BasePage { return AlertWithTwoActions( alertTitle: S.of(context).change_current_node_title, - alertContent: - S.of(context).change_current_node(node.uri), + alertContent: S + .of(context) + .change_current_node(node.uriRaw), leftButtonText: S.of(context).cancel, rightButtonText: S.of(context).change, actionLeftButton: () => diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index 58ef36797..e7856d685 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -40,6 +40,8 @@ class WalletListBodyState extends State { Image.asset('assets/images/monero_logo.png', height: 24, width: 24); final bitcoinIcon = Image.asset('assets/images/bitcoin.png', height: 24, width: 24); + final litecoinIcon = + Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24); final scrollController = ScrollController(); final double tileHeight = 60; Flushbar _progressBar; @@ -193,6 +195,8 @@ class WalletListBodyState extends State { return bitcoinIcon; case WalletType.monero: return moneroIcon; + case WalletType.litecoin: + return litecoinIcon; default: return null; } diff --git a/lib/store/app_store.dart b/lib/store/app_store.dart index e90b76375..aee7610cd 100644 --- a/lib/store/app_store.dart +++ b/lib/store/app_store.dart @@ -1,6 +1,8 @@ -import 'package:cake_wallet/entities/balance.dart'; +import 'package:cake_wallet/entities/transaction_info.dart'; import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/entities/balance.dart'; import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/core/transaction_history.dart'; import 'package:cake_wallet/store/wallet_list_store.dart'; import 'package:cake_wallet/store/authentication_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -20,7 +22,8 @@ abstract class AppStoreBase with Store { AuthenticationStore authenticationStore; @observable - WalletBase wallet; + WalletBase, TransactionInfo> + wallet; WalletListStore walletList; @@ -29,7 +32,10 @@ abstract class AppStoreBase with Store { NodeListStore nodeListStore; @action - void changeCurrentWallet(WalletBase wallet) { + void changeCurrentWallet( + WalletBase, + TransactionInfo> + wallet) { this.wallet?.close(); this.wallet = wallet; } diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 6e16e2324..da2410378 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -149,7 +149,7 @@ abstract class SettingsStoreBase with Store { static Future load( {@required Box nodeSource, - @required bool isBitcoinBuyEnabled, + @required bool isBitcoinBuyEnabled, FiatCurrency initialFiatCurrency = FiatCurrency.usd, MoneroTransactionPriority initialMoneroTransactionPriority = MoneroTransactionPriority.slow, @@ -205,15 +205,19 @@ abstract class SettingsStoreBase with Store { final nodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); final bitcoinElectrumServerId = sharedPreferences .getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); + final litecoinElectrumServerId = sharedPreferences + .getInt(PreferencesKey.currentLitecoinElectrumSererIdKey); final moneroNode = nodeSource.get(nodeId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); + final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); final packageInfo = await PackageInfo.fromPlatform(); return SettingsStore( sharedPreferences: sharedPreferences, nodes: { WalletType.monero: moneroNode, - WalletType.bitcoin: bitcoinElectrumServer + WalletType.bitcoin: bitcoinElectrumServer, + WalletType.litecoin: litecoinElectrumServer }, appVersion: packageInfo.version, isBitcoinBuyEnabled: isBitcoinBuyEnabled, @@ -263,6 +267,10 @@ abstract class SettingsStoreBase with Store { await _sharedPreferences.setInt( PreferencesKey.currentBitcoinElectrumSererIdKey, node.key as int); break; + case WalletType.litecoin: + await _sharedPreferences.setInt( + PreferencesKey.currentLitecoinElectrumSererIdKey, node.key as int); + break; case WalletType.monero: await _sharedPreferences.setInt( PreferencesKey.currentNodeIdKey, node.key as int); diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index fb9b07a27..a0935a936 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -1,7 +1,9 @@ import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; +import 'package:cake_wallet/core/transaction_history.dart'; import 'package:cake_wallet/core/wallet_base.dart'; import 'package:cake_wallet/entities/balance.dart'; import 'package:cake_wallet/entities/crypto_currency.dart'; +import 'package:cake_wallet/entities/transaction_info.dart'; import 'package:cake_wallet/entities/wallet_type.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/monero/monero_wallet.dart'; @@ -23,11 +25,7 @@ abstract class BalanceViewModelBase with Store { @required this.settingsStore, @required this.fiatConvertationStore}) { isReversing = false; - wallet ??= appStore.wallet; - - _reaction = reaction((_) => appStore.wallet, _onWalletChange); - final _wallet = wallet; if (_wallet is MoneroWallet) { @@ -38,6 +36,8 @@ abstract class BalanceViewModelBase with Store { balance = _wallet.balance; } + reaction((_) => appStore.wallet, _onWalletChange); + _onCurrentWalletChangeReaction = reaction((_) => wallet.balance, (dynamic balance) { if (balance is Balance) { @@ -59,7 +59,8 @@ abstract class BalanceViewModelBase with Store { Balance balance; @observable - WalletBase wallet; + WalletBase, TransactionInfo> + wallet; @computed double get price => fiatConvertationStore.prices[appStore.wallet.currency]; @@ -70,8 +71,8 @@ abstract class BalanceViewModelBase with Store { @computed BalanceDisplayMode get displayMode => isReversing ? savedDisplayMode == BalanceDisplayMode.hiddenBalance - ? BalanceDisplayMode.displayableBalance - : savedDisplayMode + ? BalanceDisplayMode.displayableBalance + : savedDisplayMode : savedDisplayMode; @computed @@ -153,14 +154,14 @@ abstract class BalanceViewModelBase with Store { CryptoCurrency get currency => appStore.wallet.currency; ReactionDisposer _onCurrentWalletChangeReaction; - ReactionDisposer _reaction; @action - void _onWalletChange(WalletBase wallet) { + void _onWalletChange( + WalletBase, + TransactionInfo> + wallet) { this.wallet = wallet; - balance = wallet.balance; - _onCurrentWalletChangeReaction?.reaction?.dispose(); _onCurrentWalletChangeReaction = reaction( (_) => wallet.balance, (Balance balance) => this.balance = balance); diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 66fc53269..97ed234e6 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -1,23 +1,13 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; +import 'package:cake_wallet/core/transaction_history.dart'; import 'package:cake_wallet/entities/balance.dart'; import 'package:cake_wallet/entities/order.dart'; -import 'package:cake_wallet/entities/transaction_history.dart'; -import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/monero/account.dart'; import 'package:cake_wallet/monero/monero_balance.dart'; -import 'package:cake_wallet/monero/monero_transaction_history.dart'; import 'package:cake_wallet/monero/monero_transaction_info.dart'; import 'package:cake_wallet/monero/monero_wallet.dart'; import 'package:cake_wallet/entities/balance_display_mode.dart'; -import 'package:cake_wallet/entities/crypto_currency.dart'; -import 'package:cake_wallet/entities/transaction_direction.dart'; import 'package:cake_wallet/entities/transaction_info.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; -import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/dashboard/orders_store.dart'; import 'package:cake_wallet/utils/mobx.dart'; @@ -27,12 +17,8 @@ import 'package:cake_wallet/view_model/dashboard/order_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; -import 'package:cake_wallet/view_model/dashboard/action_list_display_mode.dart'; import 'package:cake_wallet/view_model/wyre_view_model.dart'; -import 'package:crypto/crypto.dart'; -import 'package:flutter/services.dart'; import 'package:hive/hive.dart'; -import 'package:http/http.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/core/wallet_base.dart'; import 'package:cake_wallet/entities/sync_status.dart'; @@ -43,8 +29,6 @@ import 'package:cake_wallet/store/dashboard/trades_store.dart'; import 'package:cake_wallet/store/dashboard/trade_filter_store.dart'; import 'package:cake_wallet/store/dashboard/transaction_filter_store.dart'; import 'package:cake_wallet/view_model/dashboard/formatted_item_list.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:convert/convert.dart'; part 'dashboard_view_model.g.dart'; @@ -100,9 +84,8 @@ abstract class DashboardViewModelBase with Store { name = appStore.wallet?.name; wallet ??= appStore.wallet; type = wallet.type; - - _reaction = reaction((_) => appStore.wallet, _onWalletChange); - + isOutdatedElectrumWallet = + wallet.type == WalletType.bitcoin && wallet.seed.split(' ').length < 24; final _wallet = wallet; if (_wallet is MoneroWallet) { @@ -133,6 +116,8 @@ abstract class DashboardViewModelBase with Store { settingsStore: appStore.settingsStore))); } + reaction((_) => appStore.wallet, _onWalletChange); + connectMapToListWithTransform( appStore.wallet.transactionHistory.transactions, transactions, @@ -215,7 +200,8 @@ abstract class DashboardViewModelBase with Store { } @observable - WalletBase wallet; + WalletBase, TransactionInfo> + wallet; bool get hasRescan => wallet.type == WalletType.monero; @@ -241,8 +227,6 @@ abstract class DashboardViewModelBase with Store { bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled; - ReactionDisposer _reaction; - ReactionDisposer _onMoneroAccountChangeReaction; ReactionDisposer _onMoneroBalanceChangeReaction; @@ -252,11 +236,19 @@ abstract class DashboardViewModelBase with Store { await wallet.connectToNode(node: node); } + @observable + bool isOutdatedElectrumWallet; + @action - void _onWalletChange(WalletBase wallet) { + void _onWalletChange( + WalletBase, + TransactionInfo> + wallet) { this.wallet = wallet; type = wallet.type; name = wallet.name; + isOutdatedElectrumWallet = + wallet.type == WalletType.bitcoin && wallet.seed.split(' ').length < 24; if (wallet is MoneroWallet) { subname = wallet.account?.label; @@ -286,17 +278,17 @@ abstract class DashboardViewModelBase with Store { connectMapToListWithTransform( appStore.wallet.transactionHistory.transactions, transactions, - (TransactionInfo val) => TransactionListItem( + (TransactionInfo val) => TransactionListItem( transaction: val, balanceViewModel: balanceViewModel, settingsStore: appStore.settingsStore), filter: (TransactionInfo tx) { - if (tx is MoneroTransactionInfo && wallet is MoneroWallet) { - return tx.accountIndex == wallet.account.id; - } + if (tx is MoneroTransactionInfo && wallet is MoneroWallet) { + return tx.accountIndex == wallet.account.id; + } - return true; - }); + return true; + }); } @action @@ -319,6 +311,4 @@ abstract class DashboardViewModelBase with Store { balanceViewModel: balanceViewModel, settingsStore: appStore.settingsStore))); } - - } diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 83477544e..9974e3b11 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -4,7 +4,7 @@ import 'package:cake_wallet/entities/transaction_info.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/mobx.dart'; import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart'; +import 'package:cake_wallet/bitcoin/electrum_transaction_info.dart'; import 'package:cake_wallet/monero/monero_transaction_info.dart'; import 'package:cake_wallet/monero/monero_amount_format.dart'; import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; @@ -42,7 +42,7 @@ class TransactionListItem extends ActionListItem with Keyable { transaction.changeFiatAmount(amount); } - if (transaction is BitcoinTransactionInfo) { + if (transaction is ElectrumTransactionInfo) { final amount = calculateFiatAmountRaw( cryptoAmount: bitcoinAmountToDouble(amount: transaction.amount), price: price); @@ -56,4 +56,4 @@ class TransactionListItem extends ActionListItem with Keyable { @override DateTime get date => transaction.date; -} \ No newline at end of file +} diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index d93213c45..f3f00cd77 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -33,10 +33,8 @@ class ExchangeViewModel = ExchangeViewModelBase with _$ExchangeViewModel; abstract class ExchangeViewModelBase with Store { ExchangeViewModelBase(this.wallet, this.trades, this._exchangeTemplateStore, this.tradesStore, this._settingsStore) { - providerList = [ - ChangeNowExchangeProvider() - ]; - + const excludeCurrencies = [CryptoCurrency.xlm, CryptoCurrency.xrp, CryptoCurrency.bnb]; + providerList = [ChangeNowExchangeProvider()]; _initialPairBasedOnWallet(); isDepositAddressEnabled = !(depositCurrency == wallet.currency); isReceiveAddressEnabled = !(receiveCurrency == wallet.currency); @@ -57,10 +55,9 @@ abstract class ExchangeViewModelBase with Store { _onPairChange(); } }); - receiveCurrencies = CryptoCurrency.all.where((cryptoCurrency) => - (cryptoCurrency != CryptoCurrency.xlm)&& - (cryptoCurrency != CryptoCurrency.xrp)&& - (cryptoCurrency != CryptoCurrency.bnb)).toList(); + receiveCurrencies = CryptoCurrency.all + .where((cryptoCurrency) => !excludeCurrencies.contains(cryptoCurrency)) + .toList(); _defineIsReceiveAmountEditable(); isFixedRateMode = false; isReceiveAmountEntered = false; @@ -219,8 +216,10 @@ abstract class ExchangeViewModelBase with Store { limitsState = LimitsIsLoading(); try { - limits = await provider.fetchLimits(from: depositCurrency, - to: receiveCurrency, isFixedRateMode: isFixedRateMode); + limits = await provider.fetchLimits( + from: depositCurrency, + to: receiveCurrency, + isFixedRateMode: isFixedRateMode); limitsState = LimitsLoadedSuccessfully(limits: limits); } catch (e) { limitsState = LimitsLoadedFailure(error: e.toString()); @@ -284,8 +283,8 @@ abstract class ExchangeViewModelBase with Store { } else { try { tradeState = TradeIsCreating(); - final trade = await provider.createTrade(request: request, - isFixedRateMode: isFixedRateMode); + final trade = await provider.createTrade( + request: request, isFixedRateMode: isFixedRateMode); trade.walletId = wallet.id; tradesStore.setTrade(trade); await trades.add(trade); @@ -321,7 +320,8 @@ abstract class ExchangeViewModelBase with Store { void calculateDepositAllAmount() { if (wallet is BitcoinWallet) { final availableBalance = wallet.balance.available; - final priority = _settingsStore.priority[wallet.type] as BitcoinTransactionPriority; + final priority = + _settingsStore.priority[wallet.type] as BitcoinTransactionPriority; final fee = wallet.calculateEstimatedFee(priority, null); if (availableBalance < fee || availableBalance == 0) { @@ -403,6 +403,10 @@ abstract class ExchangeViewModelBase with Store { depositCurrency = CryptoCurrency.btc; receiveCurrency = CryptoCurrency.xmr; break; + case WalletType.litecoin: + depositCurrency = CryptoCurrency.ltc; + receiveCurrency = CryptoCurrency.xmr; + break; default: break; } 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 9cf800705..c680cc859 100644 --- a/lib/view_model/node_list/node_list_view_model.dart +++ b/lib/view_model/node_list/node_list_view_model.dart @@ -39,6 +39,9 @@ abstract class NodeListViewModelBase with Store { case WalletType.monero: node = getMoneroDefaultNode(nodes: _nodeSource); break; + case WalletType.litecoin: + node = getLitecoinDefaultElectrumServer(nodes: _nodeSource); + break; default: break; } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index b92672ff8..689032223 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart'; +import 'package:cake_wallet/bitcoin/electrum_wallet.dart'; import 'package:cake_wallet/entities/balance_display_mode.dart'; import 'package:cake_wallet/entities/calculate_fiat_amount_raw.dart'; import 'package:cake_wallet/entities/transaction_description.dart'; @@ -90,6 +91,9 @@ abstract class SendViewModelBase with Store { case WalletType.bitcoin: _amount = stringDoubleToBitcoinAmount(_cryptoAmount); break; + case WalletType.litecoin: + _amount = stringDoubleToBitcoinAmount(_cryptoAmount); + break; default: break; } @@ -102,7 +106,7 @@ abstract class SendViewModelBase with Store { final fee = _wallet.calculateEstimatedFee( _settingsStore.priority[_wallet.type], amount); - if (_wallet is BitcoinWallet) { + if (_wallet is ElectrumWallet) { return bitcoinAmountToDouble(amount: fee); } @@ -304,6 +308,12 @@ abstract class SendViewModelBase with Store { final amount = !sendAll ? _amount : null; final priority = _settingsStore.priority[_wallet.type]; + return BitcoinTransactionCredentials( + address, amount, priority as BitcoinTransactionPriority); + case WalletType.litecoin: + final amount = !sendAll ? _amount : null; + final priority = _settingsStore.priority[_wallet.type]; + return BitcoinTransactionCredentials( address, amount, priority as BitcoinTransactionPriority); case WalletType.monero: @@ -330,6 +340,9 @@ abstract class SendViewModelBase with Store { case WalletType.bitcoin: maximumFractionDigits = 8; break; + case WalletType.litecoin: + maximumFractionDigits = 8; + break; default: break; } @@ -357,9 +370,9 @@ abstract class SendViewModelBase with Store { final _priority = priority as TransactionPriority; final wallet = _wallet; - if (wallet is BitcoinWallet) { + if (wallet is ElectrumWallet) { final rate = wallet.feeRate(_priority); - return '${priority.toString()} ($rate sat/byte)'; + return '${priority.labelWithRate(rate)}'; } return priority.toString(); diff --git a/lib/view_model/settings/settings_view_model.dart b/lib/view_model/settings/settings_view_model.dart index 71088d339..57de6d7d9 100644 --- a/lib/view_model/settings/settings_view_model.dart +++ b/lib/view_model/settings/settings_view_model.dart @@ -1,10 +1,3 @@ -import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; -import 'package:cake_wallet/entities/balance.dart'; -import 'package:cake_wallet/entities/transaction_priority.dart'; -import 'package:cake_wallet/themes/theme_base.dart'; -import 'package:cake_wallet/themes/theme_list.dart'; -import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; import 'package:flutter/cupertino.dart'; import 'package:mobx/mobx.dart'; import 'package:package_info/package_info.dart'; @@ -25,6 +18,15 @@ import 'package:cake_wallet/view_model/settings/regular_list_item.dart'; import 'package:cake_wallet/view_model/settings/settings_list_item.dart'; import 'package:cake_wallet/view_model/settings/switcher_list_item.dart'; import 'package:cake_wallet/src/screens/auth/auth_page.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart'; +import 'package:cake_wallet/bitcoin/electrum_wallet.dart'; +import 'package:cake_wallet/core/transaction_history.dart'; +import 'package:cake_wallet/entities/balance.dart'; +import 'package:cake_wallet/entities/transaction_info.dart'; +import 'package:cake_wallet/entities/transaction_priority.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/themes/theme_list.dart'; +import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; part 'settings_view_model.g.dart'; @@ -36,13 +38,19 @@ List priorityForWalletType(WalletType type) { return MoneroTransactionPriority.all; case WalletType.bitcoin: return BitcoinTransactionPriority.all; + case WalletType.litecoin: + return LitecoinTransactionPriority.all; default: return []; } } abstract class SettingsViewModelBase with Store { - SettingsViewModelBase(this._settingsStore, WalletBase wallet) + SettingsViewModelBase( + this._settingsStore, + WalletBase, + TransactionInfo> + wallet) : itemHeaders = {}, _walletType = wallet.type, _biometricAuth = BiometricAuth() { @@ -77,9 +85,9 @@ abstract class SettingsViewModelBase with Store { displayItem: (dynamic priority) { final _priority = priority as TransactionPriority; - if (wallet is BitcoinWallet) { + if (wallet is ElectrumWallet) { final rate = wallet.feeRate(_priority); - return '${priority.toString()} ($rate sat/byte)'; + return '${priority.labelWithRate(rate)}'; } return priority.toString(); diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index 67822e207..47b0957d7 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -1,5 +1,6 @@ -import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart'; +import 'package:cake_wallet/bitcoin/electrum_transaction_info.dart'; import 'package:cake_wallet/entities/transaction_info.dart'; +import 'package:cake_wallet/entities/wallet_type.dart'; import 'package:cake_wallet/monero/monero_transaction_info.dart'; import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; import 'package:cake_wallet/src/screens/transaction_details/textfield_list_item.dart'; @@ -43,12 +44,6 @@ abstract class TransactionDetailsViewModelBase with Store { value: tx.amountFormatted()), StandartListItem( title: S.current.transaction_details_fee, value: tx.feeFormatted()), - BlockExplorerListItem( - title: "View in Block Explorer", - value: "View Transaction on XMRChain.net", - onTap: () { - launch("https://xmrchain.net/search?value=${tx.id}"); - }) ]; if (tx.key?.isNotEmpty ?? null) { @@ -59,7 +54,7 @@ abstract class TransactionDetailsViewModelBase with Store { items.addAll(_items); } - if (tx is BitcoinTransactionInfo) { + if (tx is ElectrumTransactionInfo) { final _items = [ StandartListItem( title: S.current.transaction_details_transaction_id, value: tx.id), @@ -78,12 +73,6 @@ abstract class TransactionDetailsViewModelBase with Store { StandartListItem( title: S.current.transaction_details_fee, value: tx.feeFormatted()), - BlockExplorerListItem( - title: "View in Block Explorer", - value: "View Transaction on Blockchain.com", - onTap: () { - launch("https://www.blockchain.com/btc/tx/${tx.id}"); - }) ]; items.addAll(_items); @@ -101,6 +90,19 @@ abstract class TransactionDetailsViewModelBase with Store { } } + WalletType type; + + if (tx is MoneroTransactionInfo) { + type = WalletType.monero; + } else if (tx is ElectrumTransactionInfo) { + type = tx.type; + } + + items.add(BlockExplorerListItem( + title: "View in Block Explorer", + value: _explorerDescription(type), + onTap: () => launch(_explorerUrl(type, tx.id)))); + final description = transactionDescriptionBox.values.firstWhere( (val) => val.id == transactionInfo.id, orElse: () => TransactionDescription(id: transactionInfo.id)); @@ -125,4 +127,30 @@ abstract class TransactionDetailsViewModelBase with Store { final List items; bool showRecipientAddress; + + String _explorerUrl(WalletType type, String txId) { + switch (type) { + case WalletType.monero: + return 'https://xmrchain.net/search?value=${txId}'; + case WalletType.bitcoin: + return 'https://www.blockchain.com/btc/tx/${txId}'; + case WalletType.litecoin: + return 'https://blockchair.com/litecoin/transaction/${txId}'; + default: + return ''; + } + } + + String _explorerDescription(WalletType type) { + switch (type) { + case WalletType.monero: + return 'View Transaction on XMRChain.net'; + case WalletType.bitcoin: + return 'View Transaction on Blockchain.com'; + case WalletType.litecoin: + return 'View Transaction on Blockchair.com'; + default: + return ''; + } + } } diff --git a/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart index b06a00e99..b018c542d 100644 --- a/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart @@ -1,6 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/bitcoin/electrum_wallet.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; import 'package:cake_wallet/monero/monero_wallet.dart'; @@ -62,7 +63,7 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { Future _createNew() async { final wallet = _wallet; - if (wallet is BitcoinWallet) { + if (wallet is ElectrumWallet) { await wallet.generateNewAddress(); } 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 9f9531864..b15443222 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 @@ -1,5 +1,3 @@ -import 'package:cake_wallet/entities/balance.dart'; -import 'package:cake_wallet/store/app_store.dart'; import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; @@ -10,6 +8,11 @@ import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_h import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; import 'package:cake_wallet/entities/wallet_type.dart'; +import 'package:cake_wallet/bitcoin/electrum_wallet.dart'; +import 'package:cake_wallet/core/transaction_history.dart'; +import 'package:cake_wallet/entities/balance.dart'; +import 'package:cake_wallet/entities/transaction_info.dart'; +import 'package:cake_wallet/store/app_store.dart'; part 'wallet_address_list_view_model.g.dart'; @@ -56,12 +59,13 @@ class BitcoinURI extends PaymentURI { } abstract class WalletAddressListViewModelBase with Store { - WalletAddressListViewModelBase( - {@required AppStore appStore}) { + WalletAddressListViewModelBase({@required AppStore appStore}) { _appStore = appStore; _wallet = _appStore.wallet; hasAccounts = _wallet?.type == WalletType.monero; - _onWalletChangeReaction = reaction((_) => _appStore.wallet, (WalletBase wallet) { + _onWalletChangeReaction = reaction((_) => _appStore.wallet, (WalletBase< + Balance, TransactionHistoryBase, TransactionInfo> + wallet) { _wallet = wallet; hasAccounts = _wallet.type == WalletType.monero; }); @@ -119,9 +123,7 @@ abstract class WalletAddressListViewModelBase with Store { final isPrimary = addr == primaryAddress; return WalletAddressListItem( - isPrimary: isPrimary, - name: null, - address: addr.address); + isPrimary: isPrimary, name: null, address: addr.address); }); addressList.addAll(bitcoinAddresses); } @@ -147,7 +149,8 @@ abstract class WalletAddressListViewModelBase with Store { bool get hasAddressList => _wallet.type == WalletType.monero; @observable - WalletBase _wallet; + WalletBase, TransactionInfo> + _wallet; List _baseItems; @@ -155,7 +158,6 @@ abstract class WalletAddressListViewModelBase with Store { ReactionDisposer _onWalletChangeReaction; - @action void setAddress(WalletAddressListItem address) => _wallet.address = address.address; @@ -174,7 +176,7 @@ abstract class WalletAddressListViewModelBase with Store { void nextAddress() { final wallet = _wallet; - if (wallet is BitcoinWallet) { + if (wallet is ElectrumWallet) { wallet.nextAddress(); } } diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index 4a5315c9a..b56425ee9 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -2,7 +2,7 @@ import 'package:mobx/mobx.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/core/wallet_base.dart'; import 'package:cake_wallet/monero/monero_wallet.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; +import 'package:cake_wallet/bitcoin/electrum_wallet.dart'; import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; part 'wallet_keys_view_model.g.dart'; @@ -24,12 +24,11 @@ abstract class WalletKeysViewModelBase with Store { title: S.current.view_key_public, value: keys.publicViewKey), StandartListItem( title: S.current.view_key_private, value: keys.privateViewKey), - StandartListItem( - title: S.current.wallet_seed, value: wallet.seed), + StandartListItem(title: S.current.wallet_seed, value: wallet.seed), ]); } - if (wallet is BitcoinWallet) { + if (wallet is ElectrumWallet) { final keys = wallet.keys; items.addAll([ diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index c21f1b8c1..0611786a8 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -37,6 +37,8 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { name: name, language: options as String); case WalletType.bitcoin: return BitcoinNewWalletCredentials(name: name); + case WalletType.litecoin: + return BitcoinNewWalletCredentials(name: name); default: return null; } diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index b015535ae..4fe81c7d0 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -62,6 +62,9 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { case WalletType.bitcoin: return BitcoinRestoreWalletFromSeedCredentials( name: name, mnemonic: seed, password: password); + case WalletType.litecoin: + return BitcoinRestoreWalletFromSeedCredentials( + name: name, mnemonic: seed, password: password); default: break; } diff --git a/pubspec.lock b/pubspec.lock index 75b67e638..3ff596074 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -67,10 +67,12 @@ packages: bech32: dependency: transitive description: - name: bech32 - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.2" + path: "." + ref: cake + resolved-ref: "02fef082f20af13de00b4e64efb93a2c1e5e1cf2" + url: "git@github.com:cake-tech/bech32.git" + source: git + version: "0.2.0" bip32: dependency: transitive description: @@ -88,9 +90,11 @@ packages: bitcoin_flutter: dependency: "direct main" description: - name: bitcoin_flutter - url: "https://pub.dartlang.org" - source: hosted + path: "." + ref: cake + resolved-ref: b3ab2926c665f0e68b74a4a5f31059f7fcd817b7 + url: "git@github.com:cake-tech/bitcoin_flutter.git" + source: git version: "2.0.2" boolean_selector: dependency: transitive diff --git a/pubspec.yaml b/pubspec.yaml index 194a1915e..27fb93b0d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Cake Wallet. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 4.1.7+46 +version: 4.2.0+49 environment: sdk: ">=2.7.0 <3.0.0" @@ -65,7 +65,10 @@ dependencies: crypto: ^2.1.5 password: ^1.0.0 basic_utils: ^2.0.3 - bitcoin_flutter: ^2.0.0 + bitcoin_flutter: + git: + url: git@github.com:cake-tech/bitcoin_flutter.git + ref: cake get_it: ^6.0.0 connectivity: ^3.0.3 keyboard_actions: ^3.3.0 @@ -105,7 +108,8 @@ flutter: assets: - assets/images/ - assets/node_list.yml - - assets/electrum_server_list.yml + - assets/bitcoin_electrum_server_list.yml + - assets/litecoin_electrum_server_list.yml - assets/text/ - assets/faq/ diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index c9836f13b..6b9822ba7 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -471,5 +471,8 @@ "submit_request" : "Einen Antrag stellen", - "buy_alert_content" : "Derzeit unterstützen wir nur den Kauf von Bitcoin. Um Bitcoin zu kaufen, erstellen Sie bitte Ihre Bitcoin-Brieftasche oder wechseln Sie zu dieser" + "buy_alert_content" : "Derzeit unterstützen wir nur den Kauf von Bitcoin. Um Bitcoin zu kaufen, erstellen Sie bitte Ihre Bitcoin-Brieftasche oder wechseln Sie zu dieser", + + "outdated_electrum_wallet_desceription" : "New Bitcoin wallets created in Cake now have the 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-seed wallet and stop using wallets with the 12-word seed. Please do this immediately to secure your funds.", + "understand" : "I undersand" } \ No newline at end of file diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 2364bca10..38e769d0a 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -471,5 +471,8 @@ "submit_request" : "submit a request", - "buy_alert_content" : "Currently we only support the purchase of Bitcoin. To buy Bitcoin, please create or switch to your Bitcoin wallet" + "buy_alert_content" : "Currently we only support the purchase of Bitcoin. To buy Bitcoin, please create or switch to your Bitcoin wallet", + + "outdated_electrum_wallet_desceription" : "New Bitcoin wallets created in Cake now have the 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-seed wallet and stop using wallets with the 12-word seed. Please do this immediately to secure your funds.", + "understand" : "I undersand" } \ No newline at end of file diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index a57886fe8..dd4707ddf 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -471,5 +471,8 @@ "submit_request" : "presentar una solicitud", - "buy_alert_content" : "Actualmente solo apoyamos la compra de Bitcoin. Para comprar Bitcoin, cree o cambie a su billetera Bitcoin" + "buy_alert_content" : "Actualmente solo apoyamos la compra de Bitcoin. Para comprar Bitcoin, cree o cambie a su billetera Bitcoin", + + "outdated_electrum_wallet_desceription" : "New Bitcoin wallets created in Cake now have the 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-seed wallet and stop using wallets with the 12-word seed. Please do this immediately to secure your funds.", + "understand" : "I undersand" } \ No newline at end of file diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 852e249c9..4bcb4e9fa 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -471,5 +471,8 @@ "submit_request" : "एक अनुरोध सबमिट करें", - "buy_alert_content" : "वर्तमान में हम केवल बिटकॉइन की खरीद का समर्थन करते हैं। बिटकॉइन खरीदने के लिए, कृपया अपना बिटकॉइन वॉलेट बनाएं या स्विच करें" + "buy_alert_content" : "वर्तमान में हम केवल बिटकॉइन की खरीद का समर्थन करते हैं। बिटकॉइन खरीदने के लिए, कृपया अपना बिटकॉइन वॉलेट बनाएं या स्विच करें", + + "outdated_electrum_wallet_desceription" : "New Bitcoin wallets created in Cake now have the 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-seed wallet and stop using wallets with the 12-word seed. Please do this immediately to secure your funds.", + "understand" : "I undersand" } \ No newline at end of file diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index a27384dde..ae1b4d8ec 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -469,5 +469,10 @@ "unconfirmed" : "Nepotvrđeno", "displayable" : "Dostupno za prikaz", - "submit_request" : "podnesi zahtjev" + "submit_request" : "podnesi zahtjev", + + "buy_alert_content" : "Currently we only support the purchase of Bitcoin. To buy Bitcoin, please create or switch to your Bitcoin wallet", + + "outdated_electrum_wallet_desceription" : "New Bitcoin wallets created in Cake now have the 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-seed wallet and stop using wallets with the 12-word seed. Please do this immediately to secure your funds.", + "understand" : "I undersand" } \ No newline at end of file diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 21f61ec6f..32e59cd3b 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -469,5 +469,10 @@ "unconfirmed" : "Non confermato", "displayable" : "Visualizzabile", - "submit_request" : "invia una richiesta" + "submit_request" : "invia una richiesta", + + "buy_alert_content" : "Currently we only support the purchase of Bitcoin. To buy Bitcoin, please create or switch to your Bitcoin wallet", + + "outdated_electrum_wallet_desceription" : "New Bitcoin wallets created in Cake now have the 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-seed wallet and stop using wallets with the 12-word seed. Please do this immediately to secure your funds.", + "understand" : "I undersand" } \ No newline at end of file diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 0df4e9dda..af8b3e405 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -471,5 +471,8 @@ "submit_request" : "リクエストを送信する", - "buy_alert_content" : "現在、ビットコインの購入のみをサポートしています。 ビットコインを購入するには、ビットコインウォレットを作成するか切り替えてください" + "buy_alert_content" : "現在、ビットコインの購入のみをサポートしています。 ビットコインを購入するには、ビットコインウォレットを作成するか切り替えてください", + + "outdated_electrum_wallet_desceription" : "New Bitcoin wallets created in Cake now have the 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-seed wallet and stop using wallets with the 12-word seed. Please do this immediately to secure your funds.", + "understand" : "I undersand" } \ No newline at end of file diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index ade3cc847..b5347e1b9 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -471,5 +471,8 @@ "submit_request" : "요청을 제출", - "buy_alert_content" : "현재 우리는 비트 코인 구매 만 지원합니다. 비트 코인을 구매하려면 비트 코인 지갑을 생성하거나 전환하십시오" + "buy_alert_content" : "현재 우리는 비트 코인 구매 만 지원합니다. 비트 코인을 구매하려면 비트 코인 지갑을 생성하거나 전환하십시오", + + "outdated_electrum_wallet_desceription" : "New Bitcoin wallets created in Cake now have the 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-seed wallet and stop using wallets with the 12-word seed. Please do this immediately to secure your funds.", + "understand" : "I undersand" } \ No newline at end of file diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 8f50f7102..d9530aaa5 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -471,5 +471,8 @@ "submit_request" : "een verzoek indienen", - "buy_alert_content" : "Momenteel ondersteunen we alleen de aankoop van Bitcoin. Om Bitcoin te kopen, moet u uw Bitcoin-portemonnee aanmaken of naar uw Bitcoin-portemonnee overschakelen" + "buy_alert_content" : "Momenteel ondersteunen we alleen de aankoop van Bitcoin. Om Bitcoin te kopen, moet u uw Bitcoin-portemonnee aanmaken of naar uw Bitcoin-portemonnee overschakelen", + + "outdated_electrum_wallet_desceription" : "New Bitcoin wallets created in Cake now have the 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-seed wallet and stop using wallets with the 12-word seed. Please do this immediately to secure your funds.", + "understand" : "I undersand" } \ No newline at end of file diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 84a8ba338..53787f1d6 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -471,5 +471,8 @@ "submit_request" : "złożyć wniosek", - "buy_alert_content" : "Obecnie obsługujemy tylko zakup Bitcoinów. Aby kupić Bitcoin, utwórz lub przełącz się na swój portfel Bitcoin" + "buy_alert_content" : "Obecnie obsługujemy tylko zakup Bitcoinów. Aby kupić Bitcoin, utwórz lub przełącz się na swój portfel Bitcoin", + + "outdated_electrum_wallet_desceription" : "New Bitcoin wallets created in Cake now have the 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-seed wallet and stop using wallets with the 12-word seed. Please do this immediately to secure your funds.", + "understand" : "I undersand" } \ No newline at end of file diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 18a5ad9b5..ec7e72f06 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -471,5 +471,8 @@ "submit_request" : "enviar um pedido", - "buy_alert_content" : "Atualmente, apoiamos apenas a compra de Bitcoin. Para comprar Bitcoin, crie ou mude para sua carteira Bitcoin" + "buy_alert_content" : "Atualmente, apoiamos apenas a compra de Bitcoin. Para comprar Bitcoin, crie ou mude para sua carteira Bitcoin", + + "outdated_electrum_wallet_desceription" : "New Bitcoin wallets created in Cake now have the 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-seed wallet and stop using wallets with the 12-word seed. Please do this immediately to secure your funds.", + "understand" : "I undersand" } \ No newline at end of file diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 711d4fa07..b1b085a35 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -471,5 +471,8 @@ "submit_request" : "отправить запрос", - "buy_alert_content" : "В настоящее время мы поддерживаем только покупку Bitcoin. Чтобы купить Bitcoin, создайте или переключитесь на ваш Bitcoin кошелек" + "buy_alert_content" : "В настоящее время мы поддерживаем только покупку Bitcoin. Чтобы купить Bitcoin, создайте или переключитесь на ваш Bitcoin кошелек", + + "outdated_electrum_wallet_desceription" : "New Bitcoin wallets created in Cake now have the 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-seed wallet and stop using wallets with the 12-word seed. Please do this immediately to secure your funds.", + "understand" : "I undersand" } \ No newline at end of file diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 46c82da58..92ccf7f05 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -471,5 +471,8 @@ "submit_request" : "надіслати запит", - "buy_alert_content" : "На даний час ми підтримуємо тільки покупку Bitcoin. Щоб купити Bitcoin, будь ласка, створіть або переключіться на ваш Bitcoin гаманець" + "buy_alert_content" : "На даний час ми підтримуємо тільки покупку Bitcoin. Щоб купити Bitcoin, будь ласка, створіть або переключіться на ваш Bitcoin гаманець", + + "outdated_electrum_wallet_desceription" : "New Bitcoin wallets created in Cake now have the 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-seed wallet and stop using wallets with the 12-word seed. Please do this immediately to secure your funds.", + "understand" : "I undersand" } \ No newline at end of file diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index cff7833b1..1ee4e3c26 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -468,5 +468,8 @@ "unconfirmed" : "未经证实", "displayable" : "可显示", "submit_request" : "提交请求", - "buy_alert_content" : "目前,我們僅支持購買比特幣。 要購買比特幣,請創建或切換到您的比特幣錢包" + "buy_alert_content" : "目前,我們僅支持購買比特幣。 要購買比特幣,請創建或切換到您的比特幣錢包", + + "outdated_electrum_wallet_desceription" : "New Bitcoin wallets created in Cake now have the 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-seed wallet and stop using wallets with the 12-word seed. Please do this immediately to secure your funds.", + "understand" : "I undersand" } \ No newline at end of file