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..5003adc1c 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/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/lib/bitcoin/address_to_output_script.dart b/lib/bitcoin/address_to_output_script.dart index 2f6698ff0..01c7b67a5 100644 --- a/lib/bitcoin/address_to_output_script.dart +++ b/lib/bitcoin/address_to_output_script.dart @@ -1,25 +1,29 @@ import 'dart:typed_data'; +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bs58check/bs58check.dart' as bs58check; import 'package:bitcoin_flutter/src/utils/constants/op.dart'; import 'package:bitcoin_flutter/src/utils/script.dart' as bscript; import 'package:bitcoin_flutter/src/address.dart'; - Uint8List p2shAddressToOutputScript(String address) { final decodeBase58 = bs58check.decode(address); final hash = decodeBase58.sublist(1); return bscript.compile([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_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 8e3035a2c..8b09accd3 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: 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 29e85afa5..ec099037d 100644 --- a/lib/bitcoin/electrum.dart +++ b/lib/bitcoin/electrum.dart @@ -2,6 +2,7 @@ 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'; @@ -173,10 +174,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 +261,7 @@ class ElectrumClient { if (result is String) { return result; } - print(result); + return ''; }); @@ -303,14 +305,25 @@ class ElectrumClient { }); Future> feeRates() async { - final topDoubleString = await estimatefee(p: 1); - final middleDoubleString = await estimatefee(p: 20); - final bottomDoubleString = await estimatefee(p: 150); - 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: 150); + 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]; + final res = [bottom, middle, top]; + return res; + } 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 84% rename from lib/bitcoin/bitcoin_transaction_info.dart rename to lib/bitcoin/electrum_transaction_info.dart index fb1400c5e..7aeebad1b 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,6 +132,8 @@ class BitcoinTransactionInfo extends TransactionInfo { confirmations: data['confirmations'] as int); } + final WalletType type; + String _fiatAmount; @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..89902ad27 --- /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 BitcoinTransactionPriority) { + switch (priority) { + case BitcoinTransactionPriority.slow: + return 1; + case BitcoinTransactionPriority.medium: + return 2; + case BitcoinTransactionPriority.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..04533640e --- /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: 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/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..1885a54b8 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; } 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..18e8a362a 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: @@ -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.contains(cakeWalletMoneroNodeUriPattern); nodeSource.values.forEach((node) async { - if (node.type == WalletType.monero && node.uri.contains(cakeWalletMoneroNodeUriPattern)) { + if (node.type == WalletType.monero && + node.uri.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.contains('electrumx.cakewallet.com'), + orElse: () => null); + var cakeWalletNode = nodeSource.values.firstWhere( + (node) => node.uri == 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_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/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 ac7f0cd94..cca24a979 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(); @@ -111,7 +113,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); @@ -238,6 +240,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(); } @@ -264,6 +273,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); @@ -315,7 +358,7 @@ abstract class MoneroWalletBase extends WalletBase with Store { } Future _askForUpdateTransactionHistory() async => - await transactionHistory.update(); + await updateTransactions(); int _getFullBalance() => monero_wallet.getFullBalance(accountIndex: account.id); @@ -389,11 +432,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 d964f0232..669c83593 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/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/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/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 06e0de1b2..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..7a8a55f00 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,6 @@ abstract class DashboardViewModelBase with Store { name = appStore.wallet?.name; wallet ??= appStore.wallet; type = wallet.type; - - _reaction = reaction((_) => appStore.wallet, _onWalletChange); - final _wallet = wallet; if (_wallet is MoneroWallet) { @@ -133,6 +114,8 @@ abstract class DashboardViewModelBase with Store { settingsStore: appStore.settingsStore))); } + reaction((_) => appStore.wallet, _onWalletChange); + connectMapToListWithTransform( appStore.wallet.transactionHistory.transactions, transactions, @@ -215,7 +198,8 @@ abstract class DashboardViewModelBase with Store { } @observable - WalletBase wallet; + WalletBase, TransactionInfo> + wallet; bool get hasRescan => wallet.type == WalletType.monero; @@ -241,8 +225,6 @@ abstract class DashboardViewModelBase with Store { bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled; - ReactionDisposer _reaction; - ReactionDisposer _onMoneroAccountChangeReaction; ReactionDisposer _onMoneroBalanceChangeReaction; @@ -253,7 +235,10 @@ abstract class DashboardViewModelBase with Store { } @action - void _onWalletChange(WalletBase wallet) { + void _onWalletChange( + WalletBase, + TransactionInfo> + wallet) { this.wallet = wallet; type = wallet.type; name = wallet.name; @@ -286,17 +271,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 +304,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 fc860e011..026bec348 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -33,9 +33,7 @@ class ExchangeViewModel = ExchangeViewModelBase with _$ExchangeViewModel; abstract class ExchangeViewModelBase with Store { ExchangeViewModelBase(this.wallet, this.trades, this._exchangeTemplateStore, this.tradesStore, this._settingsStore) { - providerList = [ - ChangeNowExchangeProvider() - ]; + providerList = [ChangeNowExchangeProvider()]; _initialPairBasedOnWallet(); isDepositAddressEnabled = !(depositCurrency == wallet.currency); @@ -57,9 +55,11 @@ abstract class ExchangeViewModelBase with Store { _onPairChange(); } }); - receiveCurrencies = CryptoCurrency.all.where((cryptoCurrency) => - (cryptoCurrency != CryptoCurrency.xlm)&& - (cryptoCurrency != CryptoCurrency.xrp)).toList(); + receiveCurrencies = CryptoCurrency.all + .where((cryptoCurrency) => + (cryptoCurrency != CryptoCurrency.xlm) && + (cryptoCurrency != CryptoCurrency.xrp)) + .toList(); _defineIsReceiveAmountEditable(); isFixedRateMode = false; isReceiveAmountEntered = false; @@ -218,8 +218,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()); @@ -283,8 +285,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); @@ -320,7 +322,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) { @@ -402,6 +405,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..3cf940257 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,7 +370,7 @@ 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)'; } diff --git a/lib/view_model/settings/settings_view_model.dart b/lib/view_model/settings/settings_view_model.dart index 71088d339..eb73b2dd6 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 BitcoinTransactionPriority.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,7 +85,7 @@ 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)'; } 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..74f448185 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,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:cake_wallet/store/app_store.dart'; import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; @@ -56,12 +58,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 +122,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 +148,8 @@ abstract class WalletAddressListViewModelBase with Store { bool get hasAddressList => _wallet.type == WalletType.monero; @observable - WalletBase _wallet; + WalletBase, TransactionInfo> + _wallet; List _baseItems; @@ -155,7 +157,6 @@ abstract class WalletAddressListViewModelBase with Store { ReactionDisposer _onWalletChangeReaction; - @action void setAddress(WalletAddressListItem address) => _wallet.address = address.address; 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/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 e93b34fa6..abb78776a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,7 @@ description: Cake Wallet. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html version: 4.1.4+43 +publish_to: none environment: sdk: ">=2.7.0 <3.0.0" @@ -65,7 +66,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 +109,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/