From 1d793ab284a6a5cbae241b4b90236fed4595fdf5 Mon Sep 17 00:00:00 2001 From: M Date: Tue, 12 May 2020 15:04:54 +0300 Subject: [PATCH] BTC --- android/.project | 17 ++ .../org.eclipse.buildship.core.prefs | 2 + android/app/.classpath | 6 + android/app/.project | 23 ++ .../org.eclipse.buildship.core.prefs | 2 + cw_monero/android/.classpath | 6 + cw_monero/android/.project | 23 ++ .../org.eclipse.buildship.core.prefs | 13 + cw_monero/ios/cw_monero.podspec | 5 + ios/Podfile.lock | 29 +- .../xcshareddata/WorkspaceSettings.xcsettings | 8 + lib/bitcoin/api.dart | 21 ++ lib/bitcoin/bitcoin_amount_format.dart | 13 + lib/bitcoin/bitcoin_balance.dart | 14 + lib/bitcoin/bitcoin_transaction_history.dart | 145 +++++++++ lib/bitcoin/bitcoin_transaction_info.dart | 81 ++++++ lib/bitcoin/bitcoin_wallet.dart | 274 ++++++++++++++++++ lib/bitcoin/bitcoin_wallet.manager.dart | 59 ++++ lib/bitcoin/electrum.dart | 218 ++++++++++++++ lib/bitcoin/file.dart | 43 +++ lib/bitcoin/key.dart | 34 +++ lib/main.dart | 44 ++- lib/router.dart | 40 ++- lib/src/domain/common/pathForWallet.dart | 21 ++ .../domain/common/transaction_history.dart | 2 - lib/src/domain/common/transaction_info.dart | 56 +--- lib/src/domain/common/wallet_type.dart | 11 +- .../monero/monero_transaction_history.dart | 21 +- .../monero/monero_transaction_info.dart | 50 ++++ .../domain/monero/monero_wallets_manager.dart | 13 +- .../domain/services/wallet_list_service.dart | 31 +- lib/src/domain/services/wallet_service.dart | 2 +- lib/src/screens/receive/receive_page.dart | 169 ++++++----- .../screens/restore/restore_options_page.dart | 4 + .../restore/restore_wallet_options_page.dart | 8 +- lib/src/screens/show_keys/show_keys_page.dart | 12 +- .../transaction_details_page.dart | 47 +-- lib/src/screens/welcome/welcome_page.dart | 32 +- .../stores/action_list/action_list_store.dart | 37 ++- lib/src/stores/balance/balance_store.dart | 26 +- lib/src/stores/wallet/wallet_keys_store.dart | 16 +- lib/src/stores/wallet/wallet_store.dart | 7 + pubspec.lock | 42 +++ pubspec.yaml | 1 + 44 files changed, 1503 insertions(+), 225 deletions(-) create mode 100644 android/.project create mode 100644 android/.settings/org.eclipse.buildship.core.prefs create mode 100644 android/app/.classpath create mode 100644 android/app/.project create mode 100644 android/app/.settings/org.eclipse.buildship.core.prefs create mode 100644 cw_monero/android/.classpath create mode 100644 cw_monero/android/.project create mode 100644 cw_monero/android/.settings/org.eclipse.buildship.core.prefs create mode 100644 ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 lib/bitcoin/api.dart create mode 100644 lib/bitcoin/bitcoin_amount_format.dart create mode 100644 lib/bitcoin/bitcoin_balance.dart create mode 100644 lib/bitcoin/bitcoin_transaction_history.dart create mode 100644 lib/bitcoin/bitcoin_transaction_info.dart create mode 100644 lib/bitcoin/bitcoin_wallet.dart create mode 100644 lib/bitcoin/bitcoin_wallet.manager.dart create mode 100644 lib/bitcoin/electrum.dart create mode 100644 lib/bitcoin/file.dart create mode 100644 lib/bitcoin/key.dart create mode 100644 lib/src/domain/common/pathForWallet.dart create mode 100644 lib/src/domain/monero/monero_transaction_info.dart diff --git a/android/.project b/android/.project new file mode 100644 index 000000000..17c95d4b1 --- /dev/null +++ b/android/.project @@ -0,0 +1,17 @@ + + + android + Project android_ created by Buildship. + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.buildship.core.gradleprojectnature + + diff --git a/android/.settings/org.eclipse.buildship.core.prefs b/android/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 000000000..e8895216f --- /dev/null +++ b/android/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,2 @@ +connection.project.dir= +eclipse.preferences.version=1 diff --git a/android/app/.classpath b/android/app/.classpath new file mode 100644 index 000000000..4a04201ca --- /dev/null +++ b/android/app/.classpath @@ -0,0 +1,6 @@ + + + + + + diff --git a/android/app/.project b/android/app/.project new file mode 100644 index 000000000..ac485d7c3 --- /dev/null +++ b/android/app/.project @@ -0,0 +1,23 @@ + + + app + Project app created by Buildship. + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.buildship.core.gradleprojectnature + + diff --git a/android/app/.settings/org.eclipse.buildship.core.prefs b/android/app/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 000000000..b1886adb4 --- /dev/null +++ b/android/app/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,2 @@ +connection.project.dir=.. +eclipse.preferences.version=1 diff --git a/cw_monero/android/.classpath b/cw_monero/android/.classpath new file mode 100644 index 000000000..4a04201ca --- /dev/null +++ b/cw_monero/android/.classpath @@ -0,0 +1,6 @@ + + + + + + diff --git a/cw_monero/android/.project b/cw_monero/android/.project new file mode 100644 index 000000000..e0799208f --- /dev/null +++ b/cw_monero/android/.project @@ -0,0 +1,23 @@ + + + cw_monero + Project android created by Buildship. + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.buildship.core.gradleprojectnature + + diff --git a/cw_monero/android/.settings/org.eclipse.buildship.core.prefs b/cw_monero/android/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 000000000..a88c4d484 --- /dev/null +++ b/cw_monero/android/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,13 @@ +arguments= +auto.sync=false +build.scans.enabled=false +connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(6.0-20191016123526+0000)) +connection.project.dir=../../android +eclipse.preferences.version=1 +gradle.user.home= +java.home= +jvm.arguments= +offline.mode=false +override.workspace.settings=true +show.console.view=true +show.executions.view=true diff --git a/cw_monero/ios/cw_monero.podspec b/cw_monero/ios/cw_monero.podspec index 1fd4a89c2..78416a9e7 100644 --- a/cw_monero/ios/cw_monero.podspec +++ b/cw_monero/ios/cw_monero.podspec @@ -48,4 +48,9 @@ A new flutter plugin project. sodium.libraries = 'sodium' sodium.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/ios/libs/sodium/include/**" } end + + s.subspec 'lmdb' do |lmdb| + lmdb.vendored_libraries = 'External/ios/libs/lmdb/liblmdb.a' + lmdb.libraries = 'lmdb' + end end \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index e7f54fa8f..32b99bcb9 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -4,24 +4,33 @@ PODS: - MTBBarcodeScanner - cw_monero (0.0.2): - cw_monero/Boost (= 0.0.2) + - cw_monero/lmdb (= 0.0.2) - cw_monero/Monero (= 0.0.2) - cw_monero/OpenSSL (= 0.0.2) - cw_monero/Sodium (= 0.0.2) - Flutter - cw_monero/Boost (0.0.2): - Flutter + - cw_monero/lmdb (0.0.2): + - Flutter - cw_monero/Monero (0.0.2): - Flutter - cw_monero/OpenSSL (0.0.2): - Flutter - cw_monero/Sodium (0.0.2): - Flutter + - devicelocale (0.0.1): + - Flutter - esys_flutter_share (0.0.1): - Flutter - Flutter (1.0.0) - flutter_secure_storage (3.3.1): - Flutter + - local_auth (0.0.1): + - Flutter - MTBBarcodeScanner (5.0.11) + - package_info (0.0.1): + - Flutter - path_provider (0.0.1): - Flutter - share (0.5.2): @@ -36,9 +45,12 @@ PODS: DEPENDENCIES: - barcode_scan (from `.symlinks/plugins/barcode_scan/ios`) - cw_monero (from `.symlinks/plugins/cw_monero/ios`) + - devicelocale (from `.symlinks/plugins/devicelocale/ios`) - esys_flutter_share (from `.symlinks/plugins/esys_flutter_share/ios`) - - Flutter (from `.symlinks/flutter/ios-release`) + - Flutter (from `.symlinks/flutter/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) + - local_auth (from `.symlinks/plugins/local_auth/ios`) + - package_info (from `.symlinks/plugins/package_info/ios`) - path_provider (from `.symlinks/plugins/path_provider/ios`) - share (from `.symlinks/plugins/share/ios`) - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) @@ -54,12 +66,18 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/barcode_scan/ios" cw_monero: :path: ".symlinks/plugins/cw_monero/ios" + devicelocale: + :path: ".symlinks/plugins/devicelocale/ios" esys_flutter_share: :path: ".symlinks/plugins/esys_flutter_share/ios" Flutter: - :path: ".symlinks/flutter/ios-release" + :path: ".symlinks/flutter/ios" flutter_secure_storage: :path: ".symlinks/plugins/flutter_secure_storage/ios" + local_auth: + :path: ".symlinks/plugins/local_auth/ios" + package_info: + :path: ".symlinks/plugins/package_info/ios" path_provider: :path: ".symlinks/plugins/path_provider/ios" share: @@ -73,11 +91,14 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: barcode_scan: 33f586d02270046fc6559135038b34b5754eaa4f - cw_monero: ca40a57b99f7753ed93d3b50af671a637277eb89 + cw_monero: 2e1f79929880cc2293b5bc1b25e28152e4d84649 + devicelocale: feebbe5e7a30adb8c4f83185de1b50ff19b44f00 esys_flutter_share: 403498dab005b36ce1f8d7aff377e81f0621b0b4 Flutter: 0e3d915762c693b495b44d77113d4970485de6ec flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec + local_auth: 2571c49920ae469f46d5557435fad8fa473a5e88 MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb + package_info: 48b108e75b8802c2d5e126f208ef540561c98aef path_provider: fb74bd0465e96b594bb3b5088ee4a4e7bb1f2a9d share: bae0a282aab4483288913fc4dc0b935d4b491f2e shared_preferences: 430726339841afefe5142b9c1f50cb6bd7793e01 @@ -86,4 +107,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: f1916a43bb28badbd408be80e8e4b8652a74e93e -COCOAPODS: 1.8.4 +COCOAPODS: 1.9.1 diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..f9b0d7c5e --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/lib/bitcoin/api.dart b/lib/bitcoin/api.dart new file mode 100644 index 000000000..829637f0a --- /dev/null +++ b/lib/bitcoin/api.dart @@ -0,0 +1,21 @@ +import 'dart:convert'; +import 'package:http/http.dart'; + +const blockchainInfoBaseURI = 'https://blockchain.info'; +const multiAddressURI = '$blockchainInfoBaseURI/multiaddr'; + +Future> fetchAllAddresses({String xpub}) async { + final uri = '$multiAddressURI?active=$xpub'; + final response = await get(uri); + final responseJSON = json.decode(response.body) as Map; + + print(responseJSON); + + return (responseJSON['addresses'] as List).map((dynamic row) { + if (row is Map) { + return row['address'] as String; + } + + return ''; + }).toList(); +} diff --git a/lib/bitcoin/bitcoin_amount_format.dart b/lib/bitcoin/bitcoin_amount_format.dart new file mode 100644 index 000000000..a1ff8cecf --- /dev/null +++ b/lib/bitcoin/bitcoin_amount_format.dart @@ -0,0 +1,13 @@ +import 'package:intl/intl.dart'; +import 'package:cake_wallet/src/domain/common/crypto_amount_format.dart'; + +const bitcoinAmountLength = 8; +const bitcoinAmountDivider = 100000000; +final bitcoinAmountFormat = NumberFormat() + ..maximumFractionDigits = bitcoinAmountLength + ..minimumFractionDigits = 1; + +String bitcoinAmountToString({int amount}) => + bitcoinAmountFormat.format(cryptoAmountToDouble(amount: amount, divider: bitcoinAmountDivider)); + +double bitcoinAmountToDouble({int amount}) => cryptoAmountToDouble(amount: amount, divider: bitcoinAmountDivider); diff --git a/lib/bitcoin/bitcoin_balance.dart b/lib/bitcoin/bitcoin_balance.dart new file mode 100644 index 000000000..8f9d06fa0 --- /dev/null +++ b/lib/bitcoin/bitcoin_balance.dart @@ -0,0 +1,14 @@ +import 'package:flutter/foundation.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; +import 'package:cake_wallet/src/domain/common/balance.dart'; + +class BitcoinBalance extends Balance { + BitcoinBalance({@required this.confirmed, @required this.unconfirmed}); + + final int confirmed; + final int unconfirmed; + int get total => confirmed + unconfirmed; + String get confirmedFormatted => bitcoinAmountToString(amount: confirmed); + String get unconfirmedFormatted => bitcoinAmountToString(amount: unconfirmed); + String get totalFormatted => bitcoinAmountToString(amount: total); +} diff --git a/lib/bitcoin/bitcoin_transaction_history.dart b/lib/bitcoin/bitcoin_transaction_history.dart new file mode 100644 index 000000000..cbea45ef6 --- /dev/null +++ b/lib/bitcoin/bitcoin_transaction_history.dart @@ -0,0 +1,145 @@ +import 'dart:convert'; +import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; +import 'package:flutter/foundation.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart'; +import 'package:cake_wallet/bitcoin/electrum.dart'; +import 'package:cake_wallet/src/domain/common/transaction_history.dart'; +import 'package:cake_wallet/src/domain/common/transaction_info.dart'; +import 'package:cake_wallet/bitcoin/file.dart'; + +class BitcoinTransactionHistory extends TransactionHistory { + BitcoinTransactionHistory( + {@required this.eclient, + @required this.path, + @required String password, + @required this.wallet}) + : _transactions = BehaviorSubject>.seeded([]), + _password = password, + _height = 0; + + final BitcoinWallet wallet; + final ElectrumClient eclient; + final String path; + final String _password; + int _height; + + @override + Observable> get transactions => _transactions.stream; + List get transactionsAll => _transactions.value; + final BehaviorSubject> _transactions; + bool _isUpdating = false; + + Future init() async { + final info = await _read(); + _height = (info['height'] as int) ?? _height; + _transactions.value = info['transactions'] as List; + } + + @override + Future> getAll() async => _transactions.value; + + @override + Future update() async { + if (_isUpdating) { + return; + } + + try { + _isUpdating = true; + final newTransasctions = await fetchTransactions(); + _transactions.value = _transactions.value + newTransasctions; + _updateHeight(); + await save(); + _isUpdating = false; + } catch (e) { + _isUpdating = false; + rethrow; + } + } + + Future> fetchTransactionInfo( + {@required String hash, @required int height}) async { + final rawFetching = eclient.getTransactionRaw(hash: hash); + final headerFetching = eclient.getHeader(height: height); + final result = await Future.wait([rawFetching, headerFetching]); + final raw = result.first as String; + final header = result[1] as Map; + + return {'raw': raw, 'header': header}; + } + + Future> fetchTransactions() async { + final addresses = wallet.getAddresses(); + final histories = + addresses.map((address) => eclient.getHistory(address: address)); + final _historiesWithDetails = await Future.wait(histories) + .then((histories) => histories + .map((h) => h.where((tx) => (tx['height'] as int) > _height)) + .expand((i) => i) + .toList()) + .then((histories) => histories.map((tx) => fetchTransactionInfo( + hash: tx['tx_hash'] as String, height: tx['height'] as int))); + final historiesWithDetails = await Future.wait(_historiesWithDetails); + + return historiesWithDetails + .map((info) => BitcoinTransactionInfo.fromHexAndHeader( + info['raw'] as String, info['header'] as Map, + addresses: addresses)) + .toList(); + } + + Future add(List transactions) async { + final txs = await getAll() + ..addAll(transactions); + await writeData( + path: path, + password: _password, + data: json + .encode(txs.map((tx) => (tx as BitcoinTransactionInfo).toJson()))); + } + + Future addOne(BitcoinTransactionInfo tx) async { + final txs = await getAll() + ..add(tx); + await writeData( + path: path, + password: _password, + data: json + .encode(txs.map((tx) => (tx as BitcoinTransactionInfo).toJson()))); + } + + Future save() async => writeData( + path: path, + password: _password, + data: json + .encode({'height': _height, 'transactions': _transactions.value})); + + Future> _read() async { + try { + final content = await read(path: path, password: _password); + final jsoned = json.decode(content) as Map; + final height = jsoned['height'] as int; + final transactions = (jsoned['transactions'] as List) + .map((dynamic row) { + if (row is Map) { + return BitcoinTransactionInfo.fromJson(row); + } + + return null; + }) + .where((el) => el != null) + .toList(); + + return {'transactions': transactions, 'height': height}; + } catch (_) { + return {'transactions': List(), 'height': 0}; + } + } + + void _updateHeight() { + final int newHeight = _transactions.value + .fold(0, (acc, val) => val.height > acc ? val.height : acc); + _height = newHeight > _height ? newHeight : _height; + } +} diff --git a/lib/bitcoin/bitcoin_transaction_info.dart b/lib/bitcoin/bitcoin_transaction_info.dart new file mode 100644 index 000000000..92e0276d8 --- /dev/null +++ b/lib/bitcoin/bitcoin_transaction_info.dart @@ -0,0 +1,81 @@ +import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; +import 'package:flutter/foundation.dart'; +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; +import 'package:cake_wallet/src/domain/common/transaction_direction.dart'; +import 'package:cake_wallet/src/domain/common/transaction_info.dart'; + +class BitcoinTransactionInfo extends TransactionInfo { + BitcoinTransactionInfo( + {@required this.id, + @required int height, + @required int amount, + @required TransactionDirection direction, + @required bool isPending, + @required DateTime date}) { + this.height = height; + this.amount = amount; + this.direction = direction; + this.date = date; + this.isPending = isPending; + } + + factory BitcoinTransactionInfo.fromHexAndHeader( + String hex, Map header, + {List addresses}) { + final tx = bitcoin.Transaction.fromHex(hex); + var exist = false; + var amount = 0; + + tx.outs.forEach((out) { + try { + final p2pkh = bitcoin.P2PKH( + data: PaymentData(output: out.script), network: bitcoin.bitcoin); + exist = addresses.contains(p2pkh.data.address); + + if (exist) { + amount += out.value; + } + } catch (_) {} + }); + + // FIXME: Get transaction is pending + return BitcoinTransactionInfo( + id: tx.getId(), + height: header['block_height'] as int, + isPending: false, + direction: TransactionDirection.incoming, + amount: amount, + date: DateTime.fromMillisecondsSinceEpoch( + (header['timestamp'] as int) * 1000)); + } + + factory BitcoinTransactionInfo.fromJson(Map data) { + return BitcoinTransactionInfo( + id: data['id'] as String, + height: data['height'] as int, + amount: data['amount'] as int, + direction: parseTransactionDirectionFromInt(data['direction'] as int), + date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), + isPending: data['isPending'] as bool); + } + + final String id; + + @override + String amountFormatted() => bitcoinAmountToString(amount: amount); + + @override + String fiatAmount() => ''; + + Map toJson() { + final m = Map(); + m['id'] = id; + m['height'] = height; + m['amount'] = amount; + m['direction'] = direction.index; + m['date'] = date.millisecondsSinceEpoch; + m['isPending'] = isPending; + return m; + } +} diff --git a/lib/bitcoin/bitcoin_wallet.dart b/lib/bitcoin/bitcoin_wallet.dart new file mode 100644 index 000000000..6034ab701 --- /dev/null +++ b/lib/bitcoin/bitcoin_wallet.dart @@ -0,0 +1,274 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_balance.dart'; +import 'package:cake_wallet/src/domain/common/sync_status.dart'; +import 'package:flutter/foundation.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:bip39/bip39.dart' as bip39; +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; +import 'package:cake_wallet/bitcoin/file.dart'; +import 'package:cake_wallet/bitcoin/electrum.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_transaction_history.dart'; +import 'package:cake_wallet/src/domain/common/pathForWallet.dart'; +import 'package:cake_wallet/src/domain/common/node.dart'; +import 'package:cake_wallet/src/domain/common/pending_transaction.dart'; +import 'package:cake_wallet/src/domain/common/transaction_creation_credentials.dart'; +import 'package:cake_wallet/src/domain/common/transaction_history.dart'; +import 'package:cake_wallet/src/domain/common/wallet.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; + +class BitcoinWallet extends Wallet { + BitcoinWallet( + {@required this.hdwallet, + @required this.eclient, + @required this.path, + @required String password, + int accountIndex = 0, + this.mnemonic}) + : _accountIndex = accountIndex, + _password = password, + _syncStatus = BehaviorSubject(), + _onBalanceChange = BehaviorSubject(), + _onAddressChange = BehaviorSubject(), + _onNameChange = BehaviorSubject(); + + @override + Observable get onBalanceChange => _onBalanceChange.stream; + + @override + Observable get syncStatus => _syncStatus.stream; + + @override + String get name => path.split('/').last ?? ''; + @override + String get address => hdwallet.address; + String get xpub => hdwallet.base58; + + final String path; + final bitcoin.HDWallet hdwallet; + final ElectrumClient eclient; + final String mnemonic; + BitcoinTransactionHistory history; + + final BehaviorSubject _syncStatus; + final BehaviorSubject _onBalanceChange; + final BehaviorSubject _onAddressChange; + final BehaviorSubject _onNameChange; + BehaviorSubject _addressUpdatesSubject; + StreamSubscription _addressUpdatesSubscription; + final String _password; + int _accountIndex; + + static Future load( + {@required String name, @required 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 jsoned = json.decode(walletJSONRaw) as Map; + final mnemonic = jsoned['mnemonic'] as String; + final accountIndex = + (jsoned['account_index'] == "null" || jsoned['account_index'] == null) + ? 0 + : int.parse(jsoned['account_index'] as String); + + return await build( + mnemonic: mnemonic, + password: password, + name: name, + accountIndex: accountIndex); + } + + static Future build( + {@required String mnemonic, + @required String password, + @required String name, + int accountIndex = 0}) async { + final hd = bitcoin.HDWallet.fromSeed(bip39.mnemonicToSeed(mnemonic), + network: bitcoin.bitcoin); + final walletDirPath = + await pathForWalletDir(name: name, type: WalletType.bitcoin); + final walletPath = '$walletDirPath/$name'; + final historyPath = '$walletDirPath/transactions.json'; + final eclient = ElectrumClient(); + final wallet = BitcoinWallet( + hdwallet: hd, + eclient: eclient, + path: walletPath, + mnemonic: mnemonic, + password: password, + accountIndex: accountIndex); + final history = BitcoinTransactionHistory( + eclient: eclient, + path: historyPath, + password: password, + wallet: wallet); + wallet.history = history; + await history.init(); + + // await wallet.connectToNode( + // node: Node(uri: 'https://electrum2.hodlister.co:50002')); + + // final transactions = await history.fetchTransactions(); + + // final balance = await wallet.fetchBalance(); + + // print('balance\n$balance'); + + // transactions.forEach((tx) => print(tx.id)); + + await wallet.updateInfo(); + + return wallet; + } + + List getAddresses() => _accountIndex == 0 + ? [address] + : List.generate( + _accountIndex, (i) => _getAddress(hd: hdwallet, index: i)); + + Future newAddress() async { + _accountIndex += 1; + final address = _getAddress(hd: hdwallet, index: _accountIndex); + await save(); + + return address; + } + + @override + Future close() async { + await _addressUpdatesSubscription?.cancel(); + } + + @override + Future connectToNode( + {Node node, bool useSSL = false, bool isLightWallet = false}) async { + try { + // FIXME: Hardcoded server address + // final uri = Uri.parse(node.uri); + // https://electrum2.hodlister.co:50002 + await eclient.connect(host: 'electrum2.hodlister.co', port: 50002); + _syncStatus.value = ConnectedSyncStatus(); + } catch (e) { + print(e.toString()); + _syncStatus.value = FailedSyncStatus(); + } + } + + @override + Future createTransaction( + TransactionCreationCredentials credentials) { + final txb = TransactionBuilder(network: bitcoin.bitcoin); + + // TODO: implement createTransaction + return null; + } + + @override + Future getAddress() async => address; + + @override + Future getCurrentHeight() async => 0; + + @override + Future getFilename() async => path.split('/').last ?? ''; + + @override + Future getFullBalance() async => + bitcoinAmountToString(amount: _onBalanceChange.value.total); + + @override + TransactionHistory getHistory() => history; + + @override + Future> getKeys() async => + {'publicKey': hdwallet.pubKey, 'privateKey': hdwallet.privKey}; + + @override + Future getName() async => path.split('/').last ?? ''; + + @override + Future getNodeHeight() async => 0; + + @override + Future getSeed() async => mnemonic; + + @override + WalletType getType() => WalletType.bitcoin; + + @override + Future getUnlockedBalance() async => + bitcoinAmountToString(amount: _onBalanceChange.value.total); + + @override + Future isConnected() async => eclient.isConnected; + + @override + Observable get onAddressChange => _onAddressChange.stream; + + @override + Observable get onNameChange => _onNameChange.stream; + + @override + Future rescan({int restoreHeight = 0}) { + // TODO: implement rescan + return null; + } + + @override + Future startSync() async { + _addressUpdatesSubject = eclient.addressUpdate(address: address); + _addressUpdatesSubscription = + _addressUpdatesSubject.listen((obj) => print('new obj: $obj')); + _onBalanceChange.value = await fetchBalance(); + getHistory().update(); + } + + @override + Future updateInfo() async { + _onNameChange.value = await getName(); + // _addressUpdatesSubject = eclient.addressUpdate(address: address); + // _addressUpdatesSubscription = + // _addressUpdatesSubject.listen((obj) => print('new obj: $obj')); + _onBalanceChange.value = BitcoinBalance(confirmed: 0, unconfirmed: 0); + print(await getKeys()); + } + + Future fetchBalance() async { + final balance = await _fetchBalances(); + + return BitcoinBalance( + confirmed: balance['confirmed'], unconfirmed: balance['unconfirmed']); + } + + Future save() async => await write( + path: path, + password: _password, + obj: {'mnemonic': mnemonic, 'account_index': _accountIndex.toString()}); + + String _getAddress({bitcoin.HDWallet hd, int index}) => bitcoin + .P2PKH( + data: PaymentData( + pubkey: Uint8List.fromList(hd.derive(index).pubKey.codeUnits))) + .data + .address; + + Future> _fetchBalances() async { + final balances = await Future.wait( + getAddresses().map((address) => eclient.getBalance(address: address))); + final balance = + balances.fold(Map(), (Map acc, val) { + acc['confirmed'] = + (val['confirmed'] as int ?? 0) + (acc['confirmed'] ?? 0); + acc['unconfirmed'] = + (val['unconfirmed'] as int ?? 0) + (acc['unconfirmed'] ?? 0); + + return acc; + }); + + return balance; + } +} diff --git a/lib/bitcoin/bitcoin_wallet.manager.dart b/lib/bitcoin/bitcoin_wallet.manager.dart new file mode 100644 index 000000000..5f94b610d --- /dev/null +++ b/lib/bitcoin/bitcoin_wallet.manager.dart @@ -0,0 +1,59 @@ +import 'dart:io'; +import 'package:bip39/bip39.dart' as bip39; +import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; +import 'package:cake_wallet/src/domain/common/pathForWallet.dart'; +import 'package:cake_wallet/src/domain/common/wallet.dart'; +import 'package:cake_wallet/src/domain/common/wallet_description.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; +import 'package:cake_wallet/src/domain/common/wallets_manager.dart'; + +class BitcoinWalletManager extends WalletsManager { + @override + Future create(String name, String password, String language) async { + final wallet = await BitcoinWallet.build( + mnemonic: bip39.generateMnemonic(), password: password, name: name); + await wallet.save(); + + return wallet; + } + + @override + Future isWalletExit(String name) async => + File(await pathForWallet(name: name, type: WalletType.bitcoin)) + .existsSync(); + + @override + Future openWallet(String name, String password) async { + return BitcoinWallet.load( + name: name, password: password); + } + + @override + Future remove(WalletDescription wallet) async { + final path = await pathForWalletDir(name: wallet.name, type: wallet.type); + final f = File(path); + + if (!f.existsSync()) { + return; + } + + f.deleteSync(); + } + + @override + Future restoreFromKeys(String name, String password, String language, + int restoreHeight, String address, String viewKey, String spendKey) { + // TODO: implement restoreFromKeys + return null; + } + + @override + Future restoreFromSeed( + String name, String password, String seed, int restoreHeight) async { + final wallet = await BitcoinWallet.build( + name: name, password: password, mnemonic: seed); + await wallet.save(); + + return wallet; + } +} diff --git a/lib/bitcoin/electrum.dart b/lib/bitcoin/electrum.dart new file mode 100644 index 000000000..79462c658 --- /dev/null +++ b/lib/bitcoin/electrum.dart @@ -0,0 +1,218 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:rxdart/rxdart.dart'; + +String jsonrpcparams(List params) { + final _params = params?.map((val) => '"${val.toString()}"')?.join(','); + return "[$_params]"; +} + +String jsonrpc( + {String method, List params, int id, double version = 2.0}) => + '{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${jsonrpcparams(params)}}\n'; + +class SocketTask { + SocketTask({this.completer, this.isSubscription, this.subject}); + + final Completer completer; + final BehaviorSubject subject; + final bool isSubscription; +} + +class ElectrumClient { + ElectrumClient() + : _id = 0, + _isConnected = false, + _tasks = {}; + + static const connectionTimeout = Duration(seconds: 5); + + bool get isConnected => _isConnected; + Socket socket; + int _id; + final Map _tasks; + bool _isConnected; + + Future connect({@required String host, @required int port}) async { + if (socket != null) { + await socket.close(); + } + + final start = DateTime.now(); + + socket = await SecureSocket.connect(host, port, timeout: connectionTimeout); + + _isConnected = true; + + socket.listen((List event) { + try { + final Map jsoned = + json.decode(utf8.decode(event)) as Map; + final method = jsoned['method']; + + if (method is String) { + _methodHandler(method: method, request: jsoned); + return; + } + + final id = jsoned['id'] as String; + final params = jsoned['result']; + + _finish(id, params); + } catch (e) { + print(e); + } + }, onError: (Object error) { + print('ElectrumClient error: ${error.toString()}'); + }, onDone: () { + final end = DateTime.now(); + final diff = end.millisecondsSinceEpoch - start.millisecondsSinceEpoch; + print('On done: $diff'); + }); + + print('Connected to ${socket.remoteAddress}'); + } + + Future ping() => call(method: 'server.ping'); + + Future> version() => + call(method: 'server.version').then((dynamic result) { + if (result is List) { + return result.map((dynamic val) => val.toString()).toList(); + } + + return []; + }); + + Future> getBalance({String address}) => + call(method: 'blockchain.address.get_balance', params: [address]) + .then((dynamic result) { + if (result is Map) { + return result; + } + + return Map(); + }); + + Future>> getHistory({String address}) => + call(method: 'blockchain.address.get_history', params: [address]) + .then((dynamic result) { + if (result is List) { + return result.map((dynamic val) { + if (val is Map) { + return val; + } + + return Map(); + }).toList(); + } + + return []; + }); + + Future getTransactionRaw({@required String hash}) async => + call(method: 'blockchain.transaction.get', params: [hash]) + .then((dynamic result) { + if (result is String) { + return result; + } + + return ''; + }); + + Future> getMerkle( + {@required String hash, @required int height}) async => + await call( + method: 'blockchain.transaction.get_merkle', + params: [hash, height]) as Map; + + Future> getHeader({@required int height}) async => + await call(method: 'blockchain.block.get_header', params: [height]) + as Map; + + Future estimatefee({@required int p}) => + call(method: 'blockchain.estimatefee', params: [p]) + .then((dynamic result) { + if (result is double) { + return result; + } + + if (result is String) { + return double.parse(result); + } + + return 0; + }); + + BehaviorSubject addressUpdate({@required String address}) => + subscribe( + id: 'blockchain.address.subscribe:$address', + method: 'blockchain.address.subscribe', + params: [address]); + + BehaviorSubject subscribe( + {@required String id, + @required String method, + List params = const []}) { + final subscription = BehaviorSubject(); + _regisrySubscription(id, subscription); + socket.write(jsonrpc(method: method, id: _id, params: params)); + + return subscription; + } + + Future call({String method, List params = const []}) { + final completer = Completer(); + _id += 1; + final id = _id; + _regisryTask(id, completer); + socket.write(jsonrpc(method: method, id: _id, params: params)); + + return completer.future; + } + + void request({String method, List params = const []}) { + _id += 1; + socket.write(jsonrpc(method: method, id: _id, params: params)); + } + + void _regisryTask(int id, Completer completer) => _tasks[id.toString()] = + SocketTask(completer: completer, isSubscription: false); + + void _regisrySubscription(String id, BehaviorSubject subject) => + _tasks[id] = SocketTask(subject: subject, isSubscription: true); + + void _finish(String id, Object data) { + if (_tasks[id] == null) { + return; + } + + _tasks[id]?.completer?.complete(data); + + if (!(_tasks[id]?.isSubscription ?? false)) { + _tasks[id] = null; + } else { + _tasks[id].subject.add(data); + } + } + + void _methodHandler( + {@required String method, @required Map request}) { + switch (method) { + case 'blockchain.address.subscribe': + final params = request['params'] as List; + final address = params.first as String; + final id = 'blockchain.address.subscribe:$address'; + + if (_tasks[id] != null) { + _tasks[id].subject.add(params.last); + } + + break; + default: + break; + } + } +} diff --git a/lib/bitcoin/file.dart b/lib/bitcoin/file.dart new file mode 100644 index 000000000..054eb38b4 --- /dev/null +++ b/lib/bitcoin/file.dart @@ -0,0 +1,43 @@ +import 'dart:io'; +import 'dart:convert'; +import 'package:cake_wallet/bitcoin/key.dart'; +import 'package:encrypt/encrypt.dart' as encrypt; +import 'package:flutter/foundation.dart'; + +Future write( + {@required String path, + @required String password, + @required Map obj}) async { + final jsoned = json.encode(obj); + final keys = extractKeys(password); + final key = encrypt.Key.fromBase64(keys.first); + final iv = encrypt.IV.fromBase64(keys.last); + final encrypted = await encode(key: key, iv: iv, data: jsoned); + final f = File(path); + f.writeAsStringSync(encrypted); +} + +Future writeData( + {@required String path, + @required String password, + @required String data}) async { + final keys = extractKeys(password); + final key = encrypt.Key.fromBase64(keys.first); + final iv = encrypt.IV.fromBase64(keys.last); + final encrypted = await encode(key: key, iv: iv, data: data); + final f = File(path); + f.writeAsStringSync(encrypted); +} + +Future read( + {@required String path, @required String password}) async { + final file = File(path); + + if (!file.existsSync()) { + file.createSync(); + } + + final encrypted = file.readAsStringSync(); + + return decode(password: password, data: encrypted); +} diff --git a/lib/bitcoin/key.dart b/lib/bitcoin/key.dart new file mode 100644 index 000000000..bdabe7852 --- /dev/null +++ b/lib/bitcoin/key.dart @@ -0,0 +1,34 @@ +import 'package:encrypt/encrypt.dart' as encrypt; + +const ivEncodedStringLength = 12; + +String generateKey() { + final key = encrypt.Key.fromSecureRandom(512); + final iv = encrypt.IV.fromSecureRandom(8); + + return key.base64 + iv.base64; +} + +List extractKeys(String key) { + final _key = key.substring(0, key.length - ivEncodedStringLength); + final iv = key.substring(key.length - ivEncodedStringLength); + + return [_key, iv]; +} + +Future encode({encrypt.Key key, encrypt.IV iv, String data}) async { + final encrypter = encrypt.Encrypter(encrypt.Salsa20(key)); + final encrypted = encrypter.encrypt(data, iv: iv); + + return encrypted.base64; +} + +Future decode({String password, String data}) async { + final keys = extractKeys(password); + final key = encrypt.Key.fromBase64(keys.first); + final iv = encrypt.IV.fromBase64(keys.last); + final encrypter = encrypt.Encrypter(encrypt.Salsa20(key)); + final encrypted = encrypter.decrypt64(data, iv: iv); + + return encrypted; +} diff --git a/lib/main.dart b/lib/main.dart index 9b1dec6c3..2562905c7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,7 @@ +import 'package:cake_wallet/bitcoin/api.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_wallet.manager.dart'; +import 'package:cake_wallet/bitcoin/key.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -36,6 +40,8 @@ import 'package:cake_wallet/src/domain/services/wallet_service.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/domain/common/language.dart'; import 'package:cake_wallet/src/stores/seed_language/seed_language_store.dart'; +import 'package:cake_wallet/bitcoin/electrum.dart'; + void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -111,6 +117,42 @@ void main() async { authenticationStore: authenticationStore, loginStore: loginStore); + // final addresses = await fetchAllAddresses(xpub: '6CUGRUonZSQ4TWtTMmzXdrXDtypWKiKrhko4egpiMZbpiaQL2jkwSB1icqYh2cfDfVxdx4df189oLKnC5fSwqPfgyP3hooxujYzAu3fDVmz'); + // print(addresses); + + final eclient = ElectrumClient(); + await eclient.connect(host: "electrum2.hodlister.co", port: 50002); + await eclient.getMerkle(hash: '3780302c523831311afaccd883f04c814bc13c3ad7c2c13810bb5bfe2c3fa621', height: 629341); + print(await eclient.getHeader(height: 629341)); + // final version = await eclient.version(); + // print('version $version'); + + // eclient.banner(); + // eclient.headersSubscribe(); + + // final history = await eclient.getHistory(address: '1QEWnc4mSxUoP1fWPs32eZNRtc9zX55TwE'); + // print('history $history'); + + // final balance = await eclient.getBalance(address: '1QEWnc4mSxUoP1fWPs32eZNRtc9zX55TwE'); + // print('balance $balance'); + + // final estimateFee = await eclient.estimatefee(p: 6); + // print('estimateFee $estimateFee'); + + // final walletManager = BitcoinWalletManager(); + // final key = "zLEjJ96r4WPzlc8rWbuaP2HgxoNfec6sbWjKAcNwqTjzBfiE62A8/0Wp5P3a8Ryo3GUIs/GDG7KfwkoI1FpyuhzWZNU1P8sMN/fp88sB9ktffU5V4B9GZJU5ufSblQOKsvZxqxLJWA8nhL7iaUGcifr9TkwbpqHxBTZDlQxlZXAf/DlRUFEF2LLwo8EJ0HcCn+iPVsnqeGtgtjmOG6l7puP31AErKzaLX4yEgXaKxrdqo0ljS4g7fn4UUXpipv7ry83ZId9ZhpkcdqMRnzu84Msyg/UGGg3BX7VTtbO/ko7ojIBoyzEaF355Tg+sgbfwYAY0CNvOJqPpIhDwu+sq4mhb5H592JP426rDTcy9KV1JbZWbnbbWcqcb04vE2zvXN0x37bd4WfO77qkdoGN5m1XZB2+F2wzNUxvf25WPp5L/nvPZFk/rJGGFoy6X8mASnmIXcq5bRzwC+F2zkZSbXoRFx3yXxlaRnzltVDjWlLUrh8S01TV2llUJEFQhefzR3Xz7mgmHRXANIqRztb1AmjD7eVZid84OfedhD2Lfg9rzFcXeMTcBlaKR36ChIY5zw+ljpnqAm86pSwcJXOAJVKcQ0fJLT6dYbHYkOQqdiSs4cJQMdr/xshrkFd1raVDyL8CTNznfxSvWqSrCUqxbuvylGfrWgHzJfK5CB0oLZnA=WBZ/TzHfP4A="; + // await walletManager.openWallet('name', password) + // print(await walletManager.isWalletExit('qwerty')); + // final wallet = await walletManager.openWallet('green', key); + // final keys = await wallet.getKeys(); + // final seed = await wallet.getSeed(); + // final address = await wallet.getAddress(); + + // print('key $key'); + // print('keys $keys'); + // print('seed $seed'); + // print('address $address'); + runApp(MultiProvider(providers: [ Provider(create: (_) => sharedPreferences), Provider(create: (_) => walletService), @@ -136,7 +178,7 @@ Future initialSetup( Box nodes, AuthenticationStore authStore, int initialMigrationVersion = 1, - WalletType initialWalletType = WalletType.monero}) async { + WalletType initialWalletType = WalletType.bitcoin}) async { await walletListService.changeWalletManger(walletType: initialWalletType); await defaultSettingsMigration( version: initialMigrationVersion, diff --git a/lib/router.dart b/lib/router.dart index d3d6c74ca..909a81b13 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -109,6 +110,9 @@ class Router { return MaterialPageRoute(builder: (_) => createWelcomePage()); case Routes.newWalletFromWelcome: + final type = settings.arguments as WalletType; + walletListService.changeWalletManger(walletType: type); + return CupertinoPageRoute( builder: (_) => Provider( create: (_) => UserStore( @@ -116,10 +120,14 @@ class Router { secureStorage: FlutterSecureStorage(), sharedPreferences: sharedPreferences)), child: SetupPinCodePage( - onPinCodeSetup: (context, _) => - Navigator.pushNamed(context, Routes.newWallet)))); + onPinCodeSetup: (context, _) => Navigator.pushNamed( + context, Routes.newWallet, + arguments: type)))); case Routes.newWallet: + final type = settings.arguments as WalletType; + walletListService.changeWalletManger(walletType: type); + return CupertinoPageRoute( builder: (_) => @@ -152,11 +160,18 @@ class Router { fullscreenDialog: true); case Routes.restoreOptions: - return CupertinoPageRoute(builder: (_) => RestoreOptionsPage()); + final type = settings.arguments as WalletType; + walletListService.changeWalletManger(walletType: type); + + return CupertinoPageRoute( + builder: (_) => RestoreOptionsPage(type: type)); case Routes.restoreWalletOptions: + final type = settings.arguments as WalletType; + walletListService.changeWalletManger(walletType: type); + return CupertinoPageRoute( - builder: (_) => RestoreWalletOptionsPage()); + builder: (_) => RestoreWalletOptionsPage(type: type)); case Routes.restoreWalletOptionsFromWelcome: return CupertinoPageRoute( @@ -177,6 +192,9 @@ class Router { callback: settings.arguments as void Function())); case Routes.restoreWalletFromSeed: + final type = settings.arguments as WalletType; + walletListService.changeWalletManger(walletType: type); + return CupertinoPageRoute( builder: (_) => ProxyProvider( @@ -235,11 +253,15 @@ class Router { case Routes.receive: return CupertinoPageRoute( fullscreenDialog: true, - builder: (_) => MultiProvider(providers: [ - Provider( - create: (_) => - SubaddressListStore(walletService: walletService)) - ], child: ReceivePage())); + builder: (_) => MultiProvider( + providers: walletService.getType() == WalletType.monero + ? [ + Provider( + create: (_) => SubaddressListStore( + walletService: walletService)) + ] + : [], + child: ReceivePage())); case Routes.transactionDetails: return CupertinoPageRoute( diff --git a/lib/src/domain/common/pathForWallet.dart b/lib/src/domain/common/pathForWallet.dart new file mode 100644 index 000000000..d7caf8c1d --- /dev/null +++ b/lib/src/domain/common/pathForWallet.dart @@ -0,0 +1,21 @@ +import 'dart:io'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; +import 'package:flutter/foundation.dart'; +import 'package:path_provider/path_provider.dart'; + +Future pathForWalletDir({@required String name, @required WalletType type}) async { + final root = await getApplicationDocumentsDirectory(); + final prefix = walletTypeToString(type).toLowerCase(); + final walletsDir = Directory('${root.path}/wallets'); + final walletDire = Directory('${walletsDir.path}/$prefix/$name'); + + if (!walletDire.existsSync()) { + walletDire.createSync(recursive: true); + } + + return walletDire.path; +} + +Future pathForWallet({@required String name, @required WalletType type}) async => + await pathForWalletDir(name: name, type: type) + .then((path) => path + '/$name'); diff --git a/lib/src/domain/common/transaction_history.dart b/lib/src/domain/common/transaction_history.dart index ed449f555..57e508b59 100644 --- a/lib/src/domain/common/transaction_history.dart +++ b/lib/src/domain/common/transaction_history.dart @@ -4,7 +4,5 @@ import 'package:cake_wallet/src/domain/common/transaction_info.dart'; abstract class TransactionHistory { Observable> transactions; Future> getAll(); - Future count(); - Future refresh(); Future update(); } diff --git a/lib/src/domain/common/transaction_info.dart b/lib/src/domain/common/transaction_info.dart index e2daaec7a..ebbb33777 100644 --- a/lib/src/domain/common/transaction_info.dart +++ b/lib/src/domain/common/transaction_info.dart @@ -1,49 +1,11 @@ -import 'package:cake_wallet/src/domain/monero/monero_amount_format.dart'; -import 'package:cw_monero/structs/transaction_info_row.dart'; -import 'package:cake_wallet/src/domain/common/parseBoolFromString.dart'; import 'package:cake_wallet/src/domain/common/transaction_direction.dart'; -import 'package:cake_wallet/src/domain/common/format_amount.dart'; -class TransactionInfo { - TransactionInfo(this.id, this.height, this.direction, this.date, - this.isPending, this.amount, this.accountIndex); - - TransactionInfo.fromMap(Map map) - : id = (map['hash'] ?? '') as String, - height = (map['height'] ?? 0) as int, - direction = - parseTransactionDirectionFromNumber(map['direction'] as String) ?? - TransactionDirection.incoming, - date = DateTime.fromMillisecondsSinceEpoch( - (int.parse(map['timestamp'] as String) ?? 0) * 1000), - isPending = parseBoolFromString(map['isPending'] as String), - amount = map['amount'] as int, - accountIndex = int.parse(map['accountIndex'] as String); - - TransactionInfo.fromRow(TransactionInfoRow row) - : id = row.getHash(), - height = row.blockHeight, - direction = parseTransactionDirectionFromInt(row.direction) ?? - TransactionDirection.incoming, - date = DateTime.fromMillisecondsSinceEpoch(row.getDatetime() * 1000), - isPending = row.isPending != 0, - amount = row.getAmount(), - accountIndex = row.subaddrAccount; - - final String id; - final int height; - final TransactionDirection direction; - final DateTime date; - final int accountIndex; - final bool isPending; - final int amount; - String recipientAddress; - - String _fiatAmount; - - String amountFormatted() => '${formatAmount(moneroAmountToString(amount: amount))} XMR'; - - String fiatAmount() => _fiatAmount ?? ''; - - void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); -} +abstract class TransactionInfo extends Object { + int amount; + TransactionDirection direction; + bool isPending; + DateTime date; + int height; + String amountFormatted(); + String fiatAmount(); +} \ No newline at end of file diff --git a/lib/src/domain/common/wallet_type.dart b/lib/src/domain/common/wallet_type.dart index e66799413..76a4d8ad5 100644 --- a/lib/src/domain/common/wallet_type.dart +++ b/lib/src/domain/common/wallet_type.dart @@ -2,13 +2,18 @@ import 'package:hive/hive.dart'; part 'wallet_type.g.dart'; +const walletTypes = [WalletType.monero, WalletType.bitcoin]; + @HiveType() enum WalletType { @HiveField(0) monero, @HiveField(1) - none + none, + + @HiveField(2) + bitcoin } int serializeToInt(WalletType type) { @@ -33,7 +38,9 @@ String walletTypeToString(WalletType type) { switch (type) { case WalletType.monero: return 'Monero'; + case WalletType.bitcoin: + return 'Bitcoin'; default: return ''; } -} \ No newline at end of file +} diff --git a/lib/src/domain/monero/monero_transaction_history.dart b/lib/src/domain/monero/monero_transaction_history.dart index fb65e631f..c6f4fe0de 100644 --- a/lib/src/domain/monero/monero_transaction_history.dart +++ b/lib/src/domain/monero/monero_transaction_history.dart @@ -1,4 +1,5 @@ import 'dart:core'; +import 'package:cake_wallet/src/domain/monero/monero_transaction_info.dart'; import 'package:flutter/services.dart'; import 'package:rxdart/rxdart.dart'; import 'package:cw_monero/transaction_history.dart' @@ -6,10 +7,11 @@ import 'package:cw_monero/transaction_history.dart' import 'package:cake_wallet/src/domain/common/transaction_history.dart'; import 'package:cake_wallet/src/domain/common/transaction_info.dart'; -List _getAllTransactions(dynamic _) => monero_transaction_history - .getAllTransations() - .map((row) => TransactionInfo.fromRow(row)) - .toList(); +List _getAllTransactions(dynamic _) => + monero_transaction_history + .getAllTransations() + .map((row) => MoneroTransactionInfo.fromRow(row)) + .toList(); class MoneroTransactionHistory extends TransactionHistory { MoneroTransactionHistory() @@ -31,7 +33,6 @@ class MoneroTransactionHistory extends TransactionHistory { try { _isUpdating = true; - await refresh(); _transactions.value = await getAll(force: true); _isUpdating = false; @@ -46,13 +47,11 @@ class MoneroTransactionHistory extends TransactionHistory { } @override - Future> getAll({bool force = false}) async => - _getAllTransactions(null); + Future> getAll({bool force = false}) async { + await refresh(); + return _getAllTransactions(null); + } - @override - Future count() async => monero_transaction_history.countOfTransactions(); - - @override Future refresh() async { if (_isRefreshing) { return; diff --git a/lib/src/domain/monero/monero_transaction_info.dart b/lib/src/domain/monero/monero_transaction_info.dart new file mode 100644 index 000000000..682727406 --- /dev/null +++ b/lib/src/domain/monero/monero_transaction_info.dart @@ -0,0 +1,50 @@ +import 'package:cake_wallet/src/domain/common/transaction_info.dart'; +import 'package:cake_wallet/src/domain/monero/monero_amount_format.dart'; +import 'package:cw_monero/structs/transaction_info_row.dart'; +import 'package:cake_wallet/src/domain/common/parseBoolFromString.dart'; +import 'package:cake_wallet/src/domain/common/transaction_direction.dart'; +import 'package:cake_wallet/src/domain/common/format_amount.dart'; + +class MoneroTransactionInfo extends TransactionInfo { + MoneroTransactionInfo(this.id, this.height, this.direction, this.date, + this.isPending, this.amount, this.accountIndex); + + MoneroTransactionInfo.fromMap(Map map) + : id = (map['hash'] ?? '') as String, + height = (map['height'] ?? 0) as int, + direction = + parseTransactionDirectionFromNumber(map['direction'] as String) ?? + TransactionDirection.incoming, + date = DateTime.fromMillisecondsSinceEpoch( + (int.parse(map['timestamp'] as String) ?? 0) * 1000), + isPending = parseBoolFromString(map['isPending'] as String), + amount = map['amount'] as int, + accountIndex = int.parse(map['accountIndex'] as String); + + MoneroTransactionInfo.fromRow(TransactionInfoRow row) + : id = row.getHash(), + height = row.blockHeight, + direction = parseTransactionDirectionFromInt(row.direction) ?? + TransactionDirection.incoming, + date = DateTime.fromMillisecondsSinceEpoch(row.getDatetime() * 1000), + isPending = row.isPending != 0, + amount = row.getAmount(), + accountIndex = row.subaddrAccount; + + final String id; + final int height; + final TransactionDirection direction; + final DateTime date; + final int accountIndex; + final bool isPending; + final int amount; + String recipientAddress; + + String _fiatAmount; + + String amountFormatted() => '${formatAmount(moneroAmountToString(amount: amount))} XMR'; + + String fiatAmount() => _fiatAmount ?? ''; + + void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); +} diff --git a/lib/src/domain/monero/monero_wallets_manager.dart b/lib/src/domain/monero/monero_wallets_manager.dart index 38abcf5d7..df540c832 100644 --- a/lib/src/domain/monero/monero_wallets_manager.dart +++ b/lib/src/domain/monero/monero_wallets_manager.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'package:cake_wallet/src/domain/common/pathForWallet.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:path_provider/path_provider.dart'; @@ -11,17 +12,7 @@ import 'package:cake_wallet/src/domain/common/wallet.dart'; import 'package:cake_wallet/src/domain/monero/monero_wallet.dart'; import 'package:cake_wallet/src/domain/common/wallet_description.dart'; -Future pathForWallet({String name}) async { - final directory = await getApplicationDocumentsDirectory(); - final pathDir = directory.path + '/$name'; - final dir = Directory(pathDir); - if (!await dir.exists()) { - await dir.create(); - } - - return pathDir + '/$name'; -} class MoneroWalletsManager extends WalletsManager { MoneroWalletsManager({@required this.walletInfoSource}); @@ -34,7 +25,7 @@ class MoneroWalletsManager extends WalletsManager { Future create(String name, String password, String language) async { try { const isRecovery = false; - final path = await pathForWallet(name: name); + final path = await pathForWallet(name: name, type: WalletType.monero); await monero_wallet_manager.createWallet(path: path, password: password, language: language); diff --git a/lib/src/domain/services/wallet_list_service.dart b/lib/src/domain/services/wallet_list_service.dart index c979c7518..6d05c9328 100644 --- a/lib/src/domain/services/wallet_list_service.dart +++ b/lib/src/domain/services/wallet_list_service.dart @@ -1,4 +1,6 @@ import 'dart:async'; +import 'package:cake_wallet/bitcoin/bitcoin_wallet.manager.dart'; +import 'package:cake_wallet/bitcoin/key.dart'; import 'package:cake_wallet/src/domain/common/wallet_info.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; @@ -29,13 +31,15 @@ class WalletListService { this.walletInfoSource, this.walletsManager, @required this.walletService, - @required this.sharedPreferences}); + @required this.sharedPreferences}) + : _type = WalletType.monero; final FlutterSecureStorage secureStorage; final WalletService walletService; final Box walletInfoSource; final SharedPreferences sharedPreferences; WalletsManager walletsManager; + WalletType _type; Future> getAll() async => walletInfoSource.values .map((info) => WalletDescription(name: info.name, type: info.type)) @@ -50,7 +54,7 @@ class WalletListService { await walletService.close(); } - final password = Uuid().v4(); + final password = _generatePassword(); await saveWalletPassword(password: password, walletName: name); final wallet = await walletsManager.create(name, password, language); @@ -67,7 +71,7 @@ class WalletListService { await walletService.close(); } - final password = Uuid().v4(); + final password = _generatePassword(); await saveWalletPassword(password: password, walletName: name); final wallet = await walletsManager.restoreFromSeed( @@ -76,8 +80,8 @@ class WalletListService { await onWalletChange(wallet); } - Future restoreFromKeys(String name, String language, int restoreHeight, String address, - String viewKey, String spendKey) async { + Future restoreFromKeys(String name, String language, int restoreHeight, + String address, String viewKey, String spendKey) async { if (await walletsManager.isWalletExit(name)) { throw WalletIsExistException(name); } @@ -86,7 +90,7 @@ class WalletListService { await walletService.close(); } - final password = Uuid().v4(); + final password = _generatePassword(); await saveWalletPassword(password: password, walletName: name); final wallet = await walletsManager.restoreFromKeys( @@ -107,11 +111,16 @@ class WalletListService { } Future changeWalletManger({WalletType walletType}) async { + _type = walletType; + switch (walletType) { case WalletType.monero: walletsManager = MoneroWalletsManager(walletInfoSource: walletInfoSource); break; + case WalletType.bitcoin: + walletsManager = BitcoinWalletManager(); + break; case WalletType.none: walletsManager = null; break; @@ -121,6 +130,7 @@ class WalletListService { Future onWalletChange(Wallet wallet) async { walletService.currentWallet = wallet; final walletName = await wallet.getName(); + print('walletName $walletName '); await sharedPreferences.setString('current_wallet_name', walletName); } @@ -142,4 +152,13 @@ class WalletListService { await secureStorage.write(key: key, value: encodedPassword); } + + String _generatePassword() { + switch (_type) { + case WalletType.bitcoin: + return generateKey(); + default: + return Uuid().v4(); + } + } } diff --git a/lib/src/domain/services/wallet_service.dart b/lib/src/domain/services/wallet_service.dart index c72807c8e..b5aef353a 100644 --- a/lib/src/domain/services/wallet_service.dart +++ b/lib/src/domain/services/wallet_service.dart @@ -70,7 +70,7 @@ class WalletService extends Wallet { WalletDescription description; @override - WalletType getType() => WalletType.monero; + WalletType getType() => _currentWallet.getType(); @override Future getFilename() => _currentWallet.getFilename(); diff --git a/lib/src/screens/receive/receive_page.dart b/lib/src/screens/receive/receive_page.dart index 718f2bb1c..2b6357501 100644 --- a/lib/src/screens/receive/receive_page.dart +++ b/lib/src/screens/receive/receive_page.dart @@ -33,7 +33,7 @@ class ReceivePage extends BasePage { splashColor: Colors.transparent, padding: EdgeInsets.all(0), onPressed: () => Share.text( - 'Share address', walletStore.subaddress.address, 'text/plain'), + 'Share address', walletStore.getAddress, 'text/plain'), child: Icon( Icons.share, size: 30.0, @@ -65,7 +65,11 @@ class ReceiveBodyState extends State { @override Widget build(BuildContext context) { final walletStore = Provider.of(context); - final subaddressListStore = Provider.of(context); + SubaddressListStore subaddressListStore; + + try { + subaddressListStore = Provider.of(context); + } catch (_) {} final currentColor = Theme.of(context).selectedRowColor; final notCurrentColor = Theme.of(context).scaffoldBackgroundColor; @@ -101,7 +105,7 @@ class ReceiveBodyState extends State { padding: EdgeInsets.all(5), color: Colors.white, child: QrImage( - data: walletStore.subaddress.address + + data: walletStore.getAddress + walletStore.amountValue, backgroundColor: Colors.transparent, ), @@ -122,8 +126,8 @@ class ReceiveBodyState extends State { child: Center( child: GestureDetector( onTap: () { - Clipboard.setData(ClipboardData( - text: walletStore.subaddress.address)); + Clipboard.setData( + ClipboardData(text: walletStore.getAddress)); Scaffold.of(context).showSnackBar(SnackBar( content: Text( S.of(context).copied_to_clipboard, @@ -133,7 +137,7 @@ class ReceiveBodyState extends State { )); }, child: Text( - walletStore.subaddress.address, + walletStore.getAddress, textAlign: TextAlign.center, style: TextStyle( fontSize: 14.0, @@ -187,78 +191,17 @@ class ReceiveBodyState extends State { ], ), ), - Row( - children: [ - Expanded( - child: Container( - color: Theme.of(context).accentTextTheme.headline.color, - child: Column( + subaddressListStore != null + ? Row( children: [ - ListTile( - title: Text( - S.of(context).subaddresses, - style: TextStyle( - fontSize: 16.0, - color: Theme.of(context) - .primaryTextTheme - .headline - .color), - ), - trailing: Container( - width: 28.0, - height: 28.0, - decoration: BoxDecoration( - color: Theme.of(context).selectedRowColor, - shape: BoxShape.circle), - child: InkWell( - onTap: () => Navigator.of(context) - .pushNamed(Routes.newSubaddress), - borderRadius: BorderRadius.all(Radius.circular(14.0)), - child: Icon( - Icons.add, - color: Palette.violet, - size: 22.0, - ), - ), - ), - ), - Divider( - color: Theme.of(context).dividerTheme.color, - height: 1.0, - ) - ], - ), - )) - ], - ), - Observer(builder: (_) { - return ListView.separated( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: subaddressListStore.subaddresses.length, - separatorBuilder: (context, i) { - return Divider( - color: Theme.of(context).dividerTheme.color, - height: 1.0, - ); - }, - itemBuilder: (context, i) { - return Observer(builder: (_) { - final subaddress = subaddressListStore.subaddresses[i]; - final isCurrent = - walletStore.subaddress.address == subaddress.address; - final label = subaddress.label.isNotEmpty - ? subaddress.label - : subaddress.address; - - return InkWell( - onTap: () => walletStore.setSubaddress(subaddress), - child: Container( - color: isCurrent ? currentColor : notCurrentColor, - child: Column(children: [ + Expanded( + child: Container( + color: Theme.of(context).accentTextTheme.headline.color, + child: Column( + children: [ ListTile( title: Text( - label, + S.of(context).subaddresses, style: TextStyle( fontSize: 16.0, color: Theme.of(context) @@ -266,13 +209,79 @@ class ReceiveBodyState extends State { .headline .color), ), + trailing: Container( + width: 28.0, + height: 28.0, + decoration: BoxDecoration( + color: Theme.of(context).selectedRowColor, + shape: BoxShape.circle), + child: InkWell( + onTap: () => Navigator.of(context) + .pushNamed(Routes.newSubaddress), + borderRadius: + BorderRadius.all(Radius.circular(14.0)), + child: Icon( + Icons.add, + color: Palette.violet, + size: 22.0, + ), + ), + ), + ), + Divider( + color: Theme.of(context).dividerTheme.color, + height: 1.0, ) - ]), + ], ), - ); - }); - }); - }) + )) + ], + ) + : Container(), + subaddressListStore != null + ? Observer(builder: (_) { + return ListView.separated( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: subaddressListStore.subaddresses.length, + separatorBuilder: (context, i) { + return Divider( + color: Theme.of(context).dividerTheme.color, + height: 1.0, + ); + }, + itemBuilder: (context, i) { + return Observer(builder: (_) { + final subaddress = subaddressListStore.subaddresses[i]; + final isCurrent = + walletStore.getAddress == subaddress.address; + final label = subaddress.label.isNotEmpty + ? subaddress.label + : subaddress.address; + + return InkWell( + onTap: () => walletStore.setSubaddress(subaddress), + child: Container( + color: isCurrent ? currentColor : notCurrentColor, + child: Column(children: [ + ListTile( + title: Text( + label, + style: TextStyle( + fontSize: 16.0, + color: Theme.of(context) + .primaryTextTheme + .headline + .color), + ), + ) + ]), + ), + ); + }); + }); + }) + : Container() ], ))); } diff --git a/lib/src/screens/restore/restore_options_page.dart b/lib/src/screens/restore/restore_options_page.dart index 6a0dec278..e47356ad7 100644 --- a/lib/src/screens/restore/restore_options_page.dart +++ b/lib/src/screens/restore/restore_options_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/routes.dart'; @@ -9,7 +10,10 @@ import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/generated/i18n.dart'; class RestoreOptionsPage extends BasePage { + RestoreOptionsPage({@required this.type}); + static const _aspectRatioImage = 2.086; + final WalletType type; @override String get title => S.current.restore_restore_wallet; diff --git a/lib/src/screens/restore/restore_wallet_options_page.dart b/lib/src/screens/restore/restore_wallet_options_page.dart index 39ac97a4d..ff9b9ff8c 100644 --- a/lib/src/screens/restore/restore_wallet_options_page.dart +++ b/lib/src/screens/restore/restore_wallet_options_page.dart @@ -6,12 +6,16 @@ import 'package:cake_wallet/src/screens/restore/widgets/restore_button.dart'; import 'package:cake_wallet/src/screens/restore/widgets/image_widget.dart'; import 'package:cake_wallet/src/screens/restore/widgets/base_restore_widget.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/stores/seed_language/seed_language_store.dart'; import 'package:provider/provider.dart'; class RestoreWalletOptionsPage extends BasePage { + RestoreWalletOptionsPage({@required this.type}); + static const _aspectRatioImage = 2.086; + final WalletType type; @override String get title => S.current.restore_seed_keys_restore; @@ -31,7 +35,7 @@ class RestoreWalletOptionsPage extends BasePage { firstRestoreButton: RestoreButton( onPressed: () { seedLanguageStore.setCurrentRoute(Routes.restoreWalletFromSeed); - Navigator.pushNamed(context, Routes.seedLanguage); + Navigator.pushNamed(context, Routes.seedLanguage, arguments: type); }, imageWidget: ImageWidget( image: _imageSeed, @@ -46,7 +50,7 @@ class RestoreWalletOptionsPage extends BasePage { secondRestoreButton: RestoreButton( onPressed: () { seedLanguageStore.setCurrentRoute(Routes.restoreWalletFromKeys); - Navigator.pushNamed(context, Routes.seedLanguage); + Navigator.pushNamed(context, Routes.seedLanguage, arguments: type); }, imageWidget: ImageWidget( image: _imageKeys, diff --git a/lib/src/screens/show_keys/show_keys_page.dart b/lib/src/screens/show_keys/show_keys_page.dart index 445873e49..2ee21f372 100644 --- a/lib/src/screens/show_keys/show_keys_page.dart +++ b/lib/src/screens/show_keys/show_keys_page.dart @@ -25,11 +25,19 @@ class ShowKeysPage extends BasePage { builder: (_) { final keysMap = { S.of(context).view_key_public: walletKeysStore.publicViewKey, - S.of(context).view_key_private: walletKeysStore.privateViewKey, - S.of(context).spend_key_public: walletKeysStore.publicSpendKey, S.of(context).spend_key_private: walletKeysStore.privateSpendKey }; + if (walletKeysStore.privateViewKey.isNotEmpty) { + keysMap[S.of(context).view_key_private] = + walletKeysStore.privateViewKey; + } + + if (walletKeysStore.publicSpendKey.isNotEmpty) { + keysMap[S.of(context).spend_key_public] = + walletKeysStore.publicSpendKey; + } + return ListView.separated( separatorBuilder: (_, __) => Container( padding: EdgeInsets.only(left: 30.0, right: 20.0), diff --git a/lib/src/screens/transaction_details/transaction_details_page.dart b/lib/src/screens/transaction_details/transaction_details_page.dart index c25dbaa1e..a26999705 100644 --- a/lib/src/screens/transaction_details/transaction_details_page.dart +++ b/lib/src/screens/transaction_details/transaction_details_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/src/domain/monero/monero_transaction_info.dart'; import 'package:provider/provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; @@ -46,31 +47,33 @@ class TransactionDetailsFormState extends State { @override void initState() { final _dateFormat = widget.settingsStore.getCurrentDateFormat( - formatUSA: "yyyy.MM.dd, HH:mm", - formatDefault: "dd.MM.yyyy, HH:mm"); - final items = [ - StandartListItem( - title: S.current.transaction_details_transaction_id, - value: widget.transactionInfo.id), - StandartListItem( - title: S.current.transaction_details_date, - value: _dateFormat.format(widget.transactionInfo.date)), - StandartListItem( - title: S.current.transaction_details_height, - value: '${widget.transactionInfo.height}'), - StandartListItem( - title: S.current.transaction_details_amount, - value: widget.transactionInfo.amountFormatted()) - ]; + formatUSA: "yyyy.MM.dd, HH:mm", formatDefault: "dd.MM.yyyy, HH:mm"); + final tx = widget.transactionInfo; - if (widget.settingsStore.shouldSaveRecipientAddress && - widget.transactionInfo.recipientAddress != null) { - items.add(StandartListItem( - title: S.current.transaction_details_recipient_address, - value: widget.transactionInfo.recipientAddress)); + if (tx is MoneroTransactionInfo) { + final items = [ + StandartListItem( + title: S.current.transaction_details_transaction_id, value: tx.id), + StandartListItem( + title: S.current.transaction_details_date, + value: _dateFormat.format(tx.date)), + StandartListItem( + title: S.current.transaction_details_height, value: '${tx.height}'), + StandartListItem( + title: S.current.transaction_details_amount, + value: tx.amountFormatted()) + ]; + + if (widget.settingsStore.shouldSaveRecipientAddress && + tx.recipientAddress != null) { + items.add(StandartListItem( + title: S.current.transaction_details_recipient_address, + value: tx.recipientAddress)); + } + + _items.addAll(items); } - _items.addAll(items); super.initState(); } diff --git a/lib/src/screens/welcome/welcome_page.dart b/lib/src/screens/welcome/welcome_page.dart index 1f559458c..b59f3a2f3 100644 --- a/lib/src/screens/welcome/welcome_page.dart +++ b/lib/src/screens/welcome/welcome_page.dart @@ -1,3 +1,5 @@ +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; +import 'package:cake_wallet/src/widgets/picker.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/routes.dart'; @@ -5,14 +7,38 @@ import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/generated/i18n.dart'; +WalletType _selectedType; + class WelcomePage extends BasePage { static const _aspectRatioImage = 1.26; static const _baseWidth = 411.43; final _image = Image.asset('assets/images/welcomeImg.png'); final _cakeLogo = Image.asset('assets/images/cake_logo.png'); + final Map _picker = + walletTypes.fold(Map(), (acc, item) { + acc[walletTypeToString(item)] = item; + return acc; + }); @override Widget build(BuildContext context) { + WidgetsBinding.instance.addPostFrameCallback((_) => _selectedType == null + ? showDialog( + builder: (_) => Picker( + items: walletTypes + .map((item) => walletTypeToString(item)) + .toList(), + selectedAtIndex: -1, + title: 'Select wallet type', + pickerHeight: 510, + onItemSelected: (String item) { + print('before $_selectedType'); + _selectedType = _picker[item]; + print('after $_selectedType'); + }), + context: context) + : null); + return Scaffold( backgroundColor: Theme.of(context).backgroundColor, resizeToAvoidBottomPadding: false, @@ -72,7 +98,8 @@ class WelcomePage extends BasePage { child: Column(children: [ PrimaryButton( onPressed: () { - Navigator.pushNamed(context, Routes.newWalletFromWelcome); + Navigator.pushNamed(context, Routes.newWalletFromWelcome, + arguments: _selectedType); }, text: S.of(context).create_new, color: @@ -82,7 +109,8 @@ class WelcomePage extends BasePage { SizedBox(height: 10), PrimaryButton( onPressed: () { - Navigator.pushNamed(context, Routes.restoreOptions); + Navigator.pushNamed(context, Routes.restoreOptions, + arguments: _selectedType); }, color: Theme.of(context).accentTextTheme.caption.backgroundColor, borderColor: diff --git a/lib/src/stores/action_list/action_list_store.dart b/lib/src/stores/action_list/action_list_store.dart index 2affe8672..4f79d9c2b 100644 --- a/lib/src/stores/action_list/action_list_store.dart +++ b/lib/src/stores/action_list/action_list_store.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:cake_wallet/src/domain/exchange/trade.dart'; +import 'package:cake_wallet/src/domain/monero/monero_transaction_info.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:flutter/foundation.dart'; @@ -98,10 +99,14 @@ abstract class ActionListBase with Store { final price = _priceStore.prices[symbol]; _transactions.forEach((item) { - final amount = calculateFiatAmountRaw( - cryptoAmount: moneroAmountToDouble(amount: item.transaction.amount), - price: price); - item.transaction.changeFiatAmount(amount); + final tx = item.transaction; + + if (tx is MoneroTransactionInfo) { + final amount = calculateFiatAmountRaw( + cryptoAmount: moneroAmountToDouble(amount: tx.amount), + price: price); + tx.changeFiatAmount(amount); + } }); return _transactions; @@ -171,7 +176,6 @@ abstract class ActionListBase with Store { tradesSource.values.map((trade) => TradeListItem(trade: trade)).toList(); Future _updateTransactionsList() async { - await _history.refresh(); final _transactions = await _history.getAll(); await _setTransactions(_transactions); } @@ -203,22 +207,29 @@ abstract class ActionListBase with Store { Future _setTransactions(List transactions) async { final wallet = _walletService.currentWallet; List sortedTransactions = transactions.map((transaction) { - if (transactionDescriptions.values.isNotEmpty) { - final description = transactionDescriptions.values.firstWhere( - (desc) => desc.id == transaction.id, - orElse: () => null); + if (transaction is MoneroTransactionInfo) { + if (transactionDescriptions.values.isNotEmpty) { + final description = transactionDescriptions.values.firstWhere( + (desc) => desc.id == transaction.id, + orElse: () => null); - if (description != null && description.recipientAddress != null) { - transaction.recipientAddress = description.recipientAddress; + if (description != null && description.recipientAddress != null) { + transaction.recipientAddress = description.recipientAddress; + } } + + return transaction; } return transaction; }).toList(); if (wallet is MoneroWallet) { - sortedTransactions = - transactions.where((tx) => tx.accountIndex == _account.id).toList(); + sortedTransactions = transactions + .where((tx) => tx is MoneroTransactionInfo + ? tx.accountIndex == _account.id + : false) + .toList(); } this._transactions = sortedTransactions diff --git a/lib/src/stores/balance/balance_store.dart b/lib/src/stores/balance/balance_store.dart index e279caf97..11f552558 100644 --- a/lib/src/stores/balance/balance_store.dart +++ b/lib/src/stores/balance/balance_store.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:cake_wallet/bitcoin/bitcoin_balance.dart'; import 'package:mobx/mobx.dart'; import 'package:flutter/foundation.dart'; import 'package:cake_wallet/src/domain/common/wallet.dart'; @@ -87,17 +88,30 @@ abstract class BalanceStoreBase with Store { } Future _onBalanceChange(Balance balance) async { - final _balance = balance as MoneroBalance; - - if (this.fullBalance != _balance.fullBalance) { - this.fullBalance = _balance.fullBalance; + if (balance is MoneroBalance) { + await _onMoneroBalanceChange(balance); } - if (this.unlockedBalance != _balance.unlockedBalance) { - this.unlockedBalance = _balance.unlockedBalance; + if (balance is BitcoinBalance) { + await _onBitcoinBalanceChange(balance); } } + Future _onMoneroBalanceChange(MoneroBalance balance) async { + if (this.fullBalance != balance.fullBalance) { + this.fullBalance = balance.fullBalance; + } + + if (this.unlockedBalance != balance.unlockedBalance) { + this.unlockedBalance = balance.unlockedBalance; + } + } + + Future _onBitcoinBalanceChange(BitcoinBalance balance) async { + fullBalance = balance.totalFormatted; + unlockedBalance = balance.totalFormatted; + } + Future _onWalletChanged(Wallet wallet) async { if (_onBalanceChangeSubscription != null) { await _onBalanceChangeSubscription.cancel(); diff --git a/lib/src/stores/wallet/wallet_keys_store.dart b/lib/src/stores/wallet/wallet_keys_store.dart index be380e29b..608e5a195 100644 --- a/lib/src/stores/wallet/wallet_keys_store.dart +++ b/lib/src/stores/wallet/wallet_keys_store.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/src/domain/services/wallet_service.dart'; @@ -15,10 +16,17 @@ abstract class WalletKeysStoreBase with Store { if (walletService.currentWallet != null) { walletService.getKeys().then((keys) { - publicViewKey = keys['publicViewKey']; - privateViewKey = keys['privateViewKey']; - publicSpendKey = keys['publicSpendKey']; - privateSpendKey = keys['privateSpendKey']; + if (walletService.getType() == WalletType.monero) { + publicViewKey = keys['publicViewKey']; + privateViewKey = keys['privateViewKey']; + publicSpendKey = keys['publicSpendKey']; + privateSpendKey = keys['privateSpendKey']; + } + + if (walletService.getType() == WalletType.bitcoin) { + publicViewKey = keys['publicKey']; + privateSpendKey = keys['privateKey']; + } }); } } diff --git a/lib/src/stores/wallet/wallet_store.dart b/lib/src/stores/wallet/wallet_store.dart index 9e2f4e88d..950caf2b2 100644 --- a/lib/src/stores/wallet/wallet_store.dart +++ b/lib/src/stores/wallet/wallet_store.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:cake_wallet/src/domain/common/node.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/src/domain/common/wallet.dart'; import 'package:cake_wallet/src/domain/monero/account.dart'; @@ -119,6 +120,7 @@ abstract class WalletStoreBase with Store { return; } + address = await wallet.getAddress(); wallet.onNameChange.listen((name) => this.name = name); wallet.onAddressChange.listen((address) => this.address = address); @@ -160,4 +162,9 @@ abstract class WalletStoreBase with Store { } Future isConnected() async => await _walletService.isConnected(); + + WalletType getType() => _walletService.getType(); + + String get getAddress => + getType() == WalletType.monero ? subaddress.address : address; } diff --git a/pubspec.lock b/pubspec.lock index ee0d1d59f..c2693ca4f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.9.3" + bech32: + dependency: transitive + description: + name: bech32 + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.2" + bip32: + dependency: transitive + description: + name: bip32 + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + bip39: + dependency: transitive + description: + name: bip39 + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" + bitcoin_flutter: + dependency: "direct main" + description: + name: bitcoin_flutter + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" boolean_selector: dependency: transitive description: @@ -64,6 +92,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.5" + bs58check: + dependency: transitive + description: + name: bs58check + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" build: dependency: transitive description: @@ -336,6 +371,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.0" + hex: + dependency: transitive + description: + name: hex + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.2" hive: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 561f9e935..5c17355aa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,6 +56,7 @@ dependencies: encrypt: ^4.0.0 password: ^1.0.0 basic_utils: ^1.0.8 + bitcoin_flutter: ^2.0.0 dev_dependencies: flutter_test: