BTC

M committed May 12, 2020 at 15:04 UTC 1d793ab284a6a5cbae241b4b90236fed4595fdf5
44 files changed +1504 -226
android/.project new
+17
@@ -0,0 +1,17 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<projectDescription>
3 + <name>android</name>
4 + <comment>Project android_ created by Buildship.</comment>
5 + <projects>
6 + </projects>
7 + <buildSpec>
8 + <buildCommand>
9 + <name>org.eclipse.buildship.core.gradleprojectbuilder</name>
10 + <arguments>
11 + </arguments>
12 + </buildCommand>
13 + </buildSpec>
14 + <natures>
15 + <nature>org.eclipse.buildship.core.gradleprojectnature</nature>
16 + </natures>
17 +</projectDescription>
android/.settings/org.eclipse.buildship.core.prefs new
+2
@@ -0,0 +1,2 @@
1 +connection.project.dir=
2 +eclipse.preferences.version=1
android/app/.classpath new
+6
@@ -0,0 +1,6 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<classpath>
3 + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11/"/>
4 + <classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
5 + <classpathentry kind="output" path="bin/default"/>
6 +</classpath>
android/app/.project new
+23
@@ -0,0 +1,23 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<projectDescription>
3 + <name>app</name>
4 + <comment>Project app created by Buildship.</comment>
5 + <projects>
6 + </projects>
7 + <buildSpec>
8 + <buildCommand>
9 + <name>org.eclipse.jdt.core.javabuilder</name>
10 + <arguments>
11 + </arguments>
12 + </buildCommand>
13 + <buildCommand>
14 + <name>org.eclipse.buildship.core.gradleprojectbuilder</name>
15 + <arguments>
16 + </arguments>
17 + </buildCommand>
18 + </buildSpec>
19 + <natures>
20 + <nature>org.eclipse.jdt.core.javanature</nature>
21 + <nature>org.eclipse.buildship.core.gradleprojectnature</nature>
22 + </natures>
23 +</projectDescription>
android/app/.settings/org.eclipse.buildship.core.prefs new
+2
@@ -0,0 +1,2 @@
1 +connection.project.dir=..
2 +eclipse.preferences.version=1
cw_monero/android/.classpath new
+6
@@ -0,0 +1,6 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<classpath>
3 + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11/"/>
4 + <classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
5 + <classpathentry kind="output" path="bin/default"/>
6 +</classpath>
cw_monero/android/.project new
+23
@@ -0,0 +1,23 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<projectDescription>
3 + <name>cw_monero</name>
4 + <comment>Project android created by Buildship.</comment>
5 + <projects>
6 + </projects>
7 + <buildSpec>
8 + <buildCommand>
9 + <name>org.eclipse.jdt.core.javabuilder</name>
10 + <arguments>
11 + </arguments>
12 + </buildCommand>
13 + <buildCommand>
14 + <name>org.eclipse.buildship.core.gradleprojectbuilder</name>
15 + <arguments>
16 + </arguments>
17 + </buildCommand>
18 + </buildSpec>
19 + <natures>
20 + <nature>org.eclipse.jdt.core.javanature</nature>
21 + <nature>org.eclipse.buildship.core.gradleprojectnature</nature>
22 + </natures>
23 +</projectDescription>
cw_monero/android/.settings/org.eclipse.buildship.core.prefs new
+13
@@ -0,0 +1,13 @@
1 +arguments=
2 +auto.sync=false
3 +build.scans.enabled=false
4 +connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(6.0-20191016123526+0000))
5 +connection.project.dir=../../android
6 +eclipse.preferences.version=1
7 +gradle.user.home=
8 +java.home=
9 +jvm.arguments=
10 +offline.mode=false
11 +override.workspace.settings=true
12 +show.console.view=true
13 +show.executions.view=true
cw_monero/ios/cw_monero.podspec
+5
@@ -48,4 +48,9 @@ A new flutter plugin project.
48 sodium.libraries = 'sodium'
49 sodium.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/ios/libs/sodium/include/**" }
50 end
51 +
52 + s.subspec 'lmdb' do |lmdb|
53 + lmdb.vendored_libraries = 'External/ios/libs/lmdb/liblmdb.a'
54 + lmdb.libraries = 'lmdb'
55 + end
56 end
\ No newline at end of file
ios/Podfile.lock
+25 -4
@@ -4,24 +4,33 @@ PODS:
4 - MTBBarcodeScanner
5 - cw_monero (0.0.2):
6 - cw_monero/Boost (= 0.0.2)
7 + - cw_monero/lmdb (= 0.0.2)
8 - cw_monero/Monero (= 0.0.2)
9 - cw_monero/OpenSSL (= 0.0.2)
10 - cw_monero/Sodium (= 0.0.2)
11 - Flutter
12 - cw_monero/Boost (0.0.2):
13 - Flutter
14 + - cw_monero/lmdb (0.0.2):
15 + - Flutter
16 - cw_monero/Monero (0.0.2):
17 - Flutter
18 - cw_monero/OpenSSL (0.0.2):
19 - Flutter
20 - cw_monero/Sodium (0.0.2):
21 - Flutter
22 + - devicelocale (0.0.1):
23 + - Flutter
24 - esys_flutter_share (0.0.1):
25 - Flutter
26 - Flutter (1.0.0)
27 - flutter_secure_storage (3.3.1):
28 - Flutter
29 + - local_auth (0.0.1):
30 + - Flutter
31 - MTBBarcodeScanner (5.0.11)
32 + - package_info (0.0.1):
33 + - Flutter
34 - path_provider (0.0.1):
35 - Flutter
36 - share (0.5.2):
@@ -36,9 +45,12 @@ PODS:
45 DEPENDENCIES:
46 - barcode_scan (from `.symlinks/plugins/barcode_scan/ios`)
47 - cw_monero (from `.symlinks/plugins/cw_monero/ios`)
48 + - devicelocale (from `.symlinks/plugins/devicelocale/ios`)
49 - esys_flutter_share (from `.symlinks/plugins/esys_flutter_share/ios`)
40 - - Flutter (from `.symlinks/flutter/ios-release`)
50 + - Flutter (from `.symlinks/flutter/ios`)
51 - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
52 + - local_auth (from `.symlinks/plugins/local_auth/ios`)
53 + - package_info (from `.symlinks/plugins/package_info/ios`)
54 - path_provider (from `.symlinks/plugins/path_provider/ios`)
55 - share (from `.symlinks/plugins/share/ios`)
56 - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`)
@@ -54,12 +66,18 @@ EXTERNAL SOURCES:
66 :path: ".symlinks/plugins/barcode_scan/ios"
67 cw_monero:
68 :path: ".symlinks/plugins/cw_monero/ios"
69 + devicelocale:
70 + :path: ".symlinks/plugins/devicelocale/ios"
71 esys_flutter_share:
72 :path: ".symlinks/plugins/esys_flutter_share/ios"
73 Flutter:
60 - :path: ".symlinks/flutter/ios-release"
74 + :path: ".symlinks/flutter/ios"
75 flutter_secure_storage:
76 :path: ".symlinks/plugins/flutter_secure_storage/ios"
77 + local_auth:
78 + :path: ".symlinks/plugins/local_auth/ios"
79 + package_info:
80 + :path: ".symlinks/plugins/package_info/ios"
81 path_provider:
82 :path: ".symlinks/plugins/path_provider/ios"
83 share:
@@ -73,11 +91,14 @@ EXTERNAL SOURCES:
91
92 SPEC CHECKSUMS:
93 barcode_scan: 33f586d02270046fc6559135038b34b5754eaa4f
76 - cw_monero: ca40a57b99f7753ed93d3b50af671a637277eb89
94 + cw_monero: 2e1f79929880cc2293b5bc1b25e28152e4d84649
95 + devicelocale: feebbe5e7a30adb8c4f83185de1b50ff19b44f00
96 esys_flutter_share: 403498dab005b36ce1f8d7aff377e81f0621b0b4
97 Flutter: 0e3d915762c693b495b44d77113d4970485de6ec
98 flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
99 + local_auth: 2571c49920ae469f46d5557435fad8fa473a5e88
100 MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
101 + package_info: 48b108e75b8802c2d5e126f208ef540561c98aef
102 path_provider: fb74bd0465e96b594bb3b5088ee4a4e7bb1f2a9d
103 share: bae0a282aab4483288913fc4dc0b935d4b491f2e
104 shared_preferences: 430726339841afefe5142b9c1f50cb6bd7793e01
@@ -86,4 +107,4 @@ SPEC CHECKSUMS:
107
108 PODFILE CHECKSUM: f1916a43bb28badbd408be80e8e4b8652a74e93e
109
89 -COCOAPODS: 1.8.4
110 +COCOAPODS: 1.9.1
ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new
+8
@@ -0,0 +1,8 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3 +<plist version="1.0">
4 +<dict>
5 + <key>PreviewsEnabled</key>
6 + <false/>
7 +</dict>
8 +</plist>
lib/bitcoin/api.dart new
+21
@@ -0,0 +1,21 @@
1 +import 'dart:convert';
2 +import 'package:http/http.dart';
3 +
4 +const blockchainInfoBaseURI = 'https://blockchain.info';
5 +const multiAddressURI = '$blockchainInfoBaseURI/multiaddr';
6 +
7 +Future<List<String>> fetchAllAddresses({String xpub}) async {
8 + final uri = '$multiAddressURI?active=$xpub';
9 + final response = await get(uri);
10 + final responseJSON = json.decode(response.body) as Map<String, dynamic>;
11 +
12 + print(responseJSON);
13 +
14 + return (responseJSON['addresses'] as List<dynamic>).map((dynamic row) {
15 + if (row is Map<String, Object>) {
16 + return row['address'] as String;
17 + }
18 +
19 + return '';
20 + }).toList();
21 +}
lib/bitcoin/bitcoin_amount_format.dart new
+13
@@ -0,0 +1,13 @@
1 +import 'package:intl/intl.dart';
2 +import 'package:cake_wallet/src/domain/common/crypto_amount_format.dart';
3 +
4 +const bitcoinAmountLength = 8;
5 +const bitcoinAmountDivider = 100000000;
6 +final bitcoinAmountFormat = NumberFormat()
7 + ..maximumFractionDigits = bitcoinAmountLength
8 + ..minimumFractionDigits = 1;
9 +
10 +String bitcoinAmountToString({int amount}) =>
11 + bitcoinAmountFormat.format(cryptoAmountToDouble(amount: amount, divider: bitcoinAmountDivider));
12 +
13 +double bitcoinAmountToDouble({int amount}) => cryptoAmountToDouble(amount: amount, divider: bitcoinAmountDivider);
lib/bitcoin/bitcoin_balance.dart new
+14
@@ -0,0 +1,14 @@
1 +import 'package:flutter/foundation.dart';
2 +import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
3 +import 'package:cake_wallet/src/domain/common/balance.dart';
4 +
5 +class BitcoinBalance extends Balance {
6 + BitcoinBalance({@required this.confirmed, @required this.unconfirmed});
7 +
8 + final int confirmed;
9 + final int unconfirmed;
10 + int get total => confirmed + unconfirmed;
11 + String get confirmedFormatted => bitcoinAmountToString(amount: confirmed);
12 + String get unconfirmedFormatted => bitcoinAmountToString(amount: unconfirmed);
13 + String get totalFormatted => bitcoinAmountToString(amount: total);
14 +}
lib/bitcoin/bitcoin_transaction_history.dart new
+145
@@ -0,0 +1,145 @@
1 +import 'dart:convert';
2 +import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
3 +import 'package:flutter/foundation.dart';
4 +import 'package:rxdart/rxdart.dart';
5 +import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart';
6 +import 'package:cake_wallet/bitcoin/electrum.dart';
7 +import 'package:cake_wallet/src/domain/common/transaction_history.dart';
8 +import 'package:cake_wallet/src/domain/common/transaction_info.dart';
9 +import 'package:cake_wallet/bitcoin/file.dart';
10 +
11 +class BitcoinTransactionHistory extends TransactionHistory {
12 + BitcoinTransactionHistory(
13 + {@required this.eclient,
14 + @required this.path,
15 + @required String password,
16 + @required this.wallet})
17 + : _transactions = BehaviorSubject<List<TransactionInfo>>.seeded([]),
18 + _password = password,
19 + _height = 0;
20 +
21 + final BitcoinWallet wallet;
22 + final ElectrumClient eclient;
23 + final String path;
24 + final String _password;
25 + int _height;
26 +
27 + @override
28 + Observable<List<TransactionInfo>> get transactions => _transactions.stream;
29 + List<TransactionInfo> get transactionsAll => _transactions.value;
30 + final BehaviorSubject<List<TransactionInfo>> _transactions;
31 + bool _isUpdating = false;
32 +
33 + Future<void> init() async {
34 + final info = await _read();
35 + _height = (info['height'] as int) ?? _height;
36 + _transactions.value = info['transactions'] as List<TransactionInfo>;
37 + }
38 +
39 + @override
40 + Future<List<TransactionInfo>> getAll() async => _transactions.value;
41 +
42 + @override
43 + Future update() async {
44 + if (_isUpdating) {
45 + return;
46 + }
47 +
48 + try {
49 + _isUpdating = true;
50 + final newTransasctions = await fetchTransactions();
51 + _transactions.value = _transactions.value + newTransasctions;
52 + _updateHeight();
53 + await save();
54 + _isUpdating = false;
55 + } catch (e) {
56 + _isUpdating = false;
57 + rethrow;
58 + }
59 + }
60 +
61 + Future<Map<String, Object>> fetchTransactionInfo(
62 + {@required String hash, @required int height}) async {
63 + final rawFetching = eclient.getTransactionRaw(hash: hash);
64 + final headerFetching = eclient.getHeader(height: height);
65 + final result = await Future.wait([rawFetching, headerFetching]);
66 + final raw = result.first as String;
67 + final header = result[1] as Map<String, Object>;
68 +
69 + return {'raw': raw, 'header': header};
70 + }
71 +
72 + Future<List<BitcoinTransactionInfo>> fetchTransactions() async {
73 + final addresses = wallet.getAddresses();
74 + final histories =
75 + addresses.map((address) => eclient.getHistory(address: address));
76 + final _historiesWithDetails = await Future.wait(histories)
77 + .then((histories) => histories
78 + .map((h) => h.where((tx) => (tx['height'] as int) > _height))
79 + .expand((i) => i)
80 + .toList())
81 + .then((histories) => histories.map((tx) => fetchTransactionInfo(
82 + hash: tx['tx_hash'] as String, height: tx['height'] as int)));
83 + final historiesWithDetails = await Future.wait(_historiesWithDetails);
84 +
85 + return historiesWithDetails
86 + .map((info) => BitcoinTransactionInfo.fromHexAndHeader(
87 + info['raw'] as String, info['header'] as Map<String, Object>,
88 + addresses: addresses))
89 + .toList();
90 + }
91 +
92 + Future<void> add(List<BitcoinTransactionInfo> transactions) async {
93 + final txs = await getAll()
94 + ..addAll(transactions);
95 + await writeData(
96 + path: path,
97 + password: _password,
98 + data: json
99 + .encode(txs.map((tx) => (tx as BitcoinTransactionInfo).toJson())));
100 + }
101 +
102 + Future<void> addOne(BitcoinTransactionInfo tx) async {
103 + final txs = await getAll()
104 + ..add(tx);
105 + await writeData(
106 + path: path,
107 + password: _password,
108 + data: json
109 + .encode(txs.map((tx) => (tx as BitcoinTransactionInfo).toJson())));
110 + }
111 +
112 + Future<void> save() async => writeData(
113 + path: path,
114 + password: _password,
115 + data: json
116 + .encode({'height': _height, 'transactions': _transactions.value}));
117 +
118 + Future<Map<String, Object>> _read() async {
119 + try {
120 + final content = await read(path: path, password: _password);
121 + final jsoned = json.decode(content) as Map<String, Object>;
122 + final height = jsoned['height'] as int;
123 + final transactions = (jsoned['transactions'] as List<dynamic>)
124 + .map((dynamic row) {
125 + if (row is Map<String, Object>) {
126 + return BitcoinTransactionInfo.fromJson(row);
127 + }
128 +
129 + return null;
130 + })
131 + .where((el) => el != null)
132 + .toList();
133 +
134 + return {'transactions': transactions, 'height': height};
135 + } catch (_) {
136 + return {'transactions': List<TransactionInfo>(), 'height': 0};
137 + }
138 + }
139 +
140 + void _updateHeight() {
141 + final int newHeight = _transactions.value
142 + .fold(0, (acc, val) => val.height > acc ? val.height : acc);
143 + _height = newHeight > _height ? newHeight : _height;
144 + }
145 +}
lib/bitcoin/bitcoin_transaction_info.dart new
+81
@@ -0,0 +1,81 @@
1 +import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
2 +import 'package:flutter/foundation.dart';
3 +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
4 +import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData;
5 +import 'package:cake_wallet/src/domain/common/transaction_direction.dart';
6 +import 'package:cake_wallet/src/domain/common/transaction_info.dart';
7 +
8 +class BitcoinTransactionInfo extends TransactionInfo {
9 + BitcoinTransactionInfo(
10 + {@required this.id,
11 + @required int height,
12 + @required int amount,
13 + @required TransactionDirection direction,
14 + @required bool isPending,
15 + @required DateTime date}) {
16 + this.height = height;
17 + this.amount = amount;
18 + this.direction = direction;
19 + this.date = date;
20 + this.isPending = isPending;
21 + }
22 +
23 + factory BitcoinTransactionInfo.fromHexAndHeader(
24 + String hex, Map<String, Object> header,
25 + {List<String> addresses}) {
26 + final tx = bitcoin.Transaction.fromHex(hex);
27 + var exist = false;
28 + var amount = 0;
29 +
30 + tx.outs.forEach((out) {
31 + try {
32 + final p2pkh = bitcoin.P2PKH(
33 + data: PaymentData(output: out.script), network: bitcoin.bitcoin);
34 + exist = addresses.contains(p2pkh.data.address);
35 +
36 + if (exist) {
37 + amount += out.value;
38 + }
39 + } catch (_) {}
40 + });
41 +
42 + // FIXME: Get transaction is pending
43 + return BitcoinTransactionInfo(
44 + id: tx.getId(),
45 + height: header['block_height'] as int,
46 + isPending: false,
47 + direction: TransactionDirection.incoming,
48 + amount: amount,
49 + date: DateTime.fromMillisecondsSinceEpoch(
50 + (header['timestamp'] as int) * 1000));
51 + }
52 +
53 + factory BitcoinTransactionInfo.fromJson(Map<String, dynamic> data) {
54 + return BitcoinTransactionInfo(
55 + id: data['id'] as String,
56 + height: data['height'] as int,
57 + amount: data['amount'] as int,
58 + direction: parseTransactionDirectionFromInt(data['direction'] as int),
59 + date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int),
60 + isPending: data['isPending'] as bool);
61 + }
62 +
63 + final String id;
64 +
65 + @override
66 + String amountFormatted() => bitcoinAmountToString(amount: amount);
67 +
68 + @override
69 + String fiatAmount() => '';
70 +
71 + Map<String, dynamic> toJson() {
72 + final m = Map<String, dynamic>();
73 + m['id'] = id;
74 + m['height'] = height;
75 + m['amount'] = amount;
76 + m['direction'] = direction.index;
77 + m['date'] = date.millisecondsSinceEpoch;
78 + m['isPending'] = isPending;
79 + return m;
80 + }
81 +}
lib/bitcoin/bitcoin_wallet.dart new
+274
@@ -0,0 +1,274 @@
1 +import 'dart:async';
2 +import 'dart:convert';
3 +import 'dart:typed_data';
4 +import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
5 +import 'package:cake_wallet/bitcoin/bitcoin_balance.dart';
6 +import 'package:cake_wallet/src/domain/common/sync_status.dart';
7 +import 'package:flutter/foundation.dart';
8 +import 'package:rxdart/rxdart.dart';
9 +import 'package:bip39/bip39.dart' as bip39;
10 +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
11 +import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData;
12 +import 'package:cake_wallet/bitcoin/file.dart';
13 +import 'package:cake_wallet/bitcoin/electrum.dart';
14 +import 'package:cake_wallet/bitcoin/bitcoin_transaction_history.dart';
15 +import 'package:cake_wallet/src/domain/common/pathForWallet.dart';
16 +import 'package:cake_wallet/src/domain/common/node.dart';
17 +import 'package:cake_wallet/src/domain/common/pending_transaction.dart';
18 +import 'package:cake_wallet/src/domain/common/transaction_creation_credentials.dart';
19 +import 'package:cake_wallet/src/domain/common/transaction_history.dart';
20 +import 'package:cake_wallet/src/domain/common/wallet.dart';
21 +import 'package:cake_wallet/src/domain/common/wallet_type.dart';
22 +
23 +class BitcoinWallet extends Wallet {
24 + BitcoinWallet(
25 + {@required this.hdwallet,
26 + @required this.eclient,
27 + @required this.path,
28 + @required String password,
29 + int accountIndex = 0,
30 + this.mnemonic})
31 + : _accountIndex = accountIndex,
32 + _password = password,
33 + _syncStatus = BehaviorSubject<SyncStatus>(),
34 + _onBalanceChange = BehaviorSubject<BitcoinBalance>(),
35 + _onAddressChange = BehaviorSubject<String>(),
36 + _onNameChange = BehaviorSubject<String>();
37 +
38 + @override
39 + Observable<BitcoinBalance> get onBalanceChange => _onBalanceChange.stream;
40 +
41 + @override
42 + Observable<SyncStatus> get syncStatus => _syncStatus.stream;
43 +
44 + @override
45 + String get name => path.split('/').last ?? '';
46 + @override
47 + String get address => hdwallet.address;
48 + String get xpub => hdwallet.base58;
49 +
50 + final String path;
51 + final bitcoin.HDWallet hdwallet;
52 + final ElectrumClient eclient;
53 + final String mnemonic;
54 + BitcoinTransactionHistory history;
55 +
56 + final BehaviorSubject<SyncStatus> _syncStatus;
57 + final BehaviorSubject<BitcoinBalance> _onBalanceChange;
58 + final BehaviorSubject<String> _onAddressChange;
59 + final BehaviorSubject<String> _onNameChange;
60 + BehaviorSubject<Object> _addressUpdatesSubject;
61 + StreamSubscription<Object> _addressUpdatesSubscription;
62 + final String _password;
63 + int _accountIndex;
64 +
65 + static Future<BitcoinWallet> load(
66 + {@required String name, @required String password}) async {
67 + final walletDirPath =
68 + await pathForWalletDir(name: name, type: WalletType.bitcoin);
69 + final walletPath = '$walletDirPath/$name';
70 + final walletJSONRaw = await read(path: walletPath, password: password);
71 + final jsoned = json.decode(walletJSONRaw) as Map<String, Object>;
72 + final mnemonic = jsoned['mnemonic'] as String;
73 + final accountIndex =
74 + (jsoned['account_index'] == "null" || jsoned['account_index'] == null)
75 + ? 0
76 + : int.parse(jsoned['account_index'] as String);
77 +
78 + return await build(
79 + mnemonic: mnemonic,
80 + password: password,
81 + name: name,
82 + accountIndex: accountIndex);
83 + }
84 +
85 + static Future<BitcoinWallet> build(
86 + {@required String mnemonic,
87 + @required String password,
88 + @required String name,
89 + int accountIndex = 0}) async {
90 + final hd = bitcoin.HDWallet.fromSeed(bip39.mnemonicToSeed(mnemonic),
91 + network: bitcoin.bitcoin);
92 + final walletDirPath =
93 + await pathForWalletDir(name: name, type: WalletType.bitcoin);
94 + final walletPath = '$walletDirPath/$name';
95 + final historyPath = '$walletDirPath/transactions.json';
96 + final eclient = ElectrumClient();
97 + final wallet = BitcoinWallet(
98 + hdwallet: hd,
99 + eclient: eclient,
100 + path: walletPath,
101 + mnemonic: mnemonic,
102 + password: password,
103 + accountIndex: accountIndex);
104 + final history = BitcoinTransactionHistory(
105 + eclient: eclient,
106 + path: historyPath,
107 + password: password,
108 + wallet: wallet);
109 + wallet.history = history;
110 + await history.init();
111 +
112 + // await wallet.connectToNode(
113 + // node: Node(uri: 'https://electrum2.hodlister.co:50002'));
114 +
115 + // final transactions = await history.fetchTransactions();
116 +
117 + // final balance = await wallet.fetchBalance();
118 +
119 + // print('balance\n$balance');
120 +
121 + // transactions.forEach((tx) => print(tx.id));
122 +
123 + await wallet.updateInfo();
124 +
125 + return wallet;
126 + }
127 +
128 + List<String> getAddresses() => _accountIndex == 0
129 + ? [address]
130 + : List<String>.generate(
131 + _accountIndex, (i) => _getAddress(hd: hdwallet, index: i));
132 +
133 + Future<String> newAddress() async {
134 + _accountIndex += 1;
135 + final address = _getAddress(hd: hdwallet, index: _accountIndex);
136 + await save();
137 +
138 + return address;
139 + }
140 +
141 + @override
142 + Future close() async {
143 + await _addressUpdatesSubscription?.cancel();
144 + }
145 +
146 + @override
147 + Future connectToNode(
148 + {Node node, bool useSSL = false, bool isLightWallet = false}) async {
149 + try {
150 + // FIXME: Hardcoded server address
151 + // final uri = Uri.parse(node.uri);
152 + // https://electrum2.hodlister.co:50002
153 + await eclient.connect(host: 'electrum2.hodlister.co', port: 50002);
154 + _syncStatus.value = ConnectedSyncStatus();
155 + } catch (e) {
156 + print(e.toString());
157 + _syncStatus.value = FailedSyncStatus();
158 + }
159 + }
160 +
161 + @override
162 + Future<PendingTransaction> createTransaction(
163 + TransactionCreationCredentials credentials) {
164 + final txb = TransactionBuilder(network: bitcoin.bitcoin);
165 +
166 + // TODO: implement createTransaction
167 + return null;
168 + }
169 +
170 + @override
171 + Future<String> getAddress() async => address;
172 +
173 + @override
174 + Future<int> getCurrentHeight() async => 0;
175 +
176 + @override
177 + Future<String> getFilename() async => path.split('/').last ?? '';
178 +
179 + @override
180 + Future<String> getFullBalance() async =>
181 + bitcoinAmountToString(amount: _onBalanceChange.value.total);
182 +
183 + @override
184 + TransactionHistory getHistory() => history;
185 +
186 + @override
187 + Future<Map<String, String>> getKeys() async =>
188 + {'publicKey': hdwallet.pubKey, 'privateKey': hdwallet.privKey};
189 +
190 + @override
191 + Future<String> getName() async => path.split('/').last ?? '';
192 +
193 + @override
194 + Future<int> getNodeHeight() async => 0;
195 +
196 + @override
197 + Future<String> getSeed() async => mnemonic;
198 +
199 + @override
200 + WalletType getType() => WalletType.bitcoin;
201 +
202 + @override
203 + Future<String> getUnlockedBalance() async =>
204 + bitcoinAmountToString(amount: _onBalanceChange.value.total);
205 +
206 + @override
207 + Future<bool> isConnected() async => eclient.isConnected;
208 +
209 + @override
210 + Observable<String> get onAddressChange => _onAddressChange.stream;
211 +
212 + @override
213 + Observable<String> get onNameChange => _onNameChange.stream;
214 +
215 + @override
216 + Future rescan({int restoreHeight = 0}) {
217 + // TODO: implement rescan
218 + return null;
219 + }
220 +
221 + @override
222 + Future startSync() async {
223 + _addressUpdatesSubject = eclient.addressUpdate(address: address);
224 + _addressUpdatesSubscription =
225 + _addressUpdatesSubject.listen((obj) => print('new obj: $obj'));
226 + _onBalanceChange.value = await fetchBalance();
227 + getHistory().update();
228 + }
229 +
230 + @override
231 + Future updateInfo() async {
232 + _onNameChange.value = await getName();
233 + // _addressUpdatesSubject = eclient.addressUpdate(address: address);
234 + // _addressUpdatesSubscription =
235 + // _addressUpdatesSubject.listen((obj) => print('new obj: $obj'));
236 + _onBalanceChange.value = BitcoinBalance(confirmed: 0, unconfirmed: 0);
237 + print(await getKeys());
238 + }
239 +
240 + Future<BitcoinBalance> fetchBalance() async {
241 + final balance = await _fetchBalances();
242 +
243 + return BitcoinBalance(
244 + confirmed: balance['confirmed'], unconfirmed: balance['unconfirmed']);
245 + }
246 +
247 + Future<void> save() async => await write(
248 + path: path,
249 + password: _password,
250 + obj: {'mnemonic': mnemonic, 'account_index': _accountIndex.toString()});
251 +
252 + String _getAddress({bitcoin.HDWallet hd, int index}) => bitcoin
253 + .P2PKH(
254 + data: PaymentData(
255 + pubkey: Uint8List.fromList(hd.derive(index).pubKey.codeUnits)))
256 + .data
257 + .address;
258 +
259 + Future<Map<String, int>> _fetchBalances() async {
260 + final balances = await Future.wait(
261 + getAddresses().map((address) => eclient.getBalance(address: address)));
262 + final balance =
263 + balances.fold(Map<String, int>(), (Map<String, int> acc, val) {
264 + acc['confirmed'] =
265 + (val['confirmed'] as int ?? 0) + (acc['confirmed'] ?? 0);
266 + acc['unconfirmed'] =
267 + (val['unconfirmed'] as int ?? 0) + (acc['unconfirmed'] ?? 0);
268 +
269 + return acc;
270 + });
271 +
272 + return balance;
273 + }
274 +}
lib/bitcoin/bitcoin_wallet.manager.dart new
+59
@@ -0,0 +1,59 @@
1 +import 'dart:io';
2 +import 'package:bip39/bip39.dart' as bip39;
3 +import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
4 +import 'package:cake_wallet/src/domain/common/pathForWallet.dart';
5 +import 'package:cake_wallet/src/domain/common/wallet.dart';
6 +import 'package:cake_wallet/src/domain/common/wallet_description.dart';
7 +import 'package:cake_wallet/src/domain/common/wallet_type.dart';
8 +import 'package:cake_wallet/src/domain/common/wallets_manager.dart';
9 +
10 +class BitcoinWalletManager extends WalletsManager {
11 + @override
12 + Future<Wallet> create(String name, String password, String language) async {
13 + final wallet = await BitcoinWallet.build(
14 + mnemonic: bip39.generateMnemonic(), password: password, name: name);
15 + await wallet.save();
16 +
17 + return wallet;
18 + }
19 +
20 + @override
21 + Future<bool> isWalletExit(String name) async =>
22 + File(await pathForWallet(name: name, type: WalletType.bitcoin))
23 + .existsSync();
24 +
25 + @override
26 + Future<Wallet> openWallet(String name, String password) async {
27 + return BitcoinWallet.load(
28 + name: name, password: password);
29 + }
30 +
31 + @override
32 + Future remove(WalletDescription wallet) async {
33 + final path = await pathForWalletDir(name: wallet.name, type: wallet.type);
34 + final f = File(path);
35 +
36 + if (!f.existsSync()) {
37 + return;
38 + }
39 +
40 + f.deleteSync();
41 + }
42 +
43 + @override
44 + Future<Wallet> restoreFromKeys(String name, String password, String language,
45 + int restoreHeight, String address, String viewKey, String spendKey) {
46 + // TODO: implement restoreFromKeys
47 + return null;
48 + }
49 +
50 + @override
51 + Future<Wallet> restoreFromSeed(
52 + String name, String password, String seed, int restoreHeight) async {
53 + final wallet = await BitcoinWallet.build(
54 + name: name, password: password, mnemonic: seed);
55 + await wallet.save();
56 +
57 + return wallet;
58 + }
59 +}
lib/bitcoin/electrum.dart new
+218
@@ -0,0 +1,218 @@
1 +import 'dart:async';
2 +import 'dart:convert';
3 +import 'dart:io';
4 +import 'package:flutter/foundation.dart';
5 +import 'package:rxdart/rxdart.dart';
6 +
7 +String jsonrpcparams(List<Object> params) {
8 + final _params = params?.map((val) => '"${val.toString()}"')?.join(',');
9 + return "[$_params]";
10 +}
11 +
12 +String jsonrpc(
13 + {String method, List<Object> params, int id, double version = 2.0}) =>
14 + '{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${jsonrpcparams(params)}}\n';
15 +
16 +class SocketTask {
17 + SocketTask({this.completer, this.isSubscription, this.subject});
18 +
19 + final Completer completer;
20 + final BehaviorSubject subject;
21 + final bool isSubscription;
22 +}
23 +
24 +class ElectrumClient {
25 + ElectrumClient()
26 + : _id = 0,
27 + _isConnected = false,
28 + _tasks = {};
29 +
30 + static const connectionTimeout = Duration(seconds: 5);
31 +
32 + bool get isConnected => _isConnected;
33 + Socket socket;
34 + int _id;
35 + final Map<String, SocketTask> _tasks;
36 + bool _isConnected;
37 +
38 + Future<void> connect({@required String host, @required int port}) async {
39 + if (socket != null) {
40 + await socket.close();
41 + }
42 +
43 + final start = DateTime.now();
44 +
45 + socket = await SecureSocket.connect(host, port, timeout: connectionTimeout);
46 +
47 + _isConnected = true;
48 +
49 + socket.listen((List<int> event) {
50 + try {
51 + final Map<String, Object> jsoned =
52 + json.decode(utf8.decode(event)) as Map<String, Object>;
53 + final method = jsoned['method'];
54 +
55 + if (method is String) {
56 + _methodHandler(method: method, request: jsoned);
57 + return;
58 + }
59 +
60 + final id = jsoned['id'] as String;
61 + final params = jsoned['result'];
62 +
63 + _finish(id, params);
64 + } catch (e) {
65 + print(e);
66 + }
67 + }, onError: (Object error) {
68 + print('ElectrumClient error: ${error.toString()}');
69 + }, onDone: () {
70 + final end = DateTime.now();
71 + final diff = end.millisecondsSinceEpoch - start.millisecondsSinceEpoch;
72 + print('On done: $diff');
73 + });
74 +
75 + print('Connected to ${socket.remoteAddress}');
76 + }
77 +
78 + Future<void> ping() => call(method: 'server.ping');
79 +
80 + Future<List<String>> version() =>
81 + call(method: 'server.version').then((dynamic result) {
82 + if (result is List) {
83 + return result.map((dynamic val) => val.toString()).toList();
84 + }
85 +
86 + return [];
87 + });
88 +
89 + Future<Map<String, Object>> getBalance({String address}) =>
90 + call(method: 'blockchain.address.get_balance', params: [address])
91 + .then((dynamic result) {
92 + if (result is Map<String, Object>) {
93 + return result;
94 + }
95 +
96 + return Map<String, Object>();
97 + });
98 +
99 + Future<List<Map<String, dynamic>>> getHistory({String address}) =>
100 + call(method: 'blockchain.address.get_history', params: [address])
101 + .then((dynamic result) {
102 + if (result is List) {
103 + return result.map((dynamic val) {
104 + if (val is Map<String, Object>) {
105 + return val;
106 + }
107 +
108 + return Map<String, Object>();
109 + }).toList();
110 + }
111 +
112 + return [];
113 + });
114 +
115 + Future<String> getTransactionRaw({@required String hash}) async =>
116 + call(method: 'blockchain.transaction.get', params: [hash])
117 + .then((dynamic result) {
118 + if (result is String) {
119 + return result;
120 + }
121 +
122 + return '';
123 + });
124 +
125 + Future<Map<String, dynamic>> getMerkle(
126 + {@required String hash, @required int height}) async =>
127 + await call(
128 + method: 'blockchain.transaction.get_merkle',
129 + params: [hash, height]) as Map<String, dynamic>;
130 +
131 + Future<Map<String, dynamic>> getHeader({@required int height}) async =>
132 + await call(method: 'blockchain.block.get_header', params: [height])
133 + as Map<String, dynamic>;
134 +
135 + Future<double> estimatefee({@required int p}) =>
136 + call(method: 'blockchain.estimatefee', params: [p])
137 + .then((dynamic result) {
138 + if (result is double) {
139 + return result;
140 + }
141 +
142 + if (result is String) {
143 + return double.parse(result);
144 + }
145 +
146 + return 0;
147 + });
148 +
149 + BehaviorSubject<Object> addressUpdate({@required String address}) =>
150 + subscribe<Object>(
151 + id: 'blockchain.address.subscribe:$address',
152 + method: 'blockchain.address.subscribe',
153 + params: [address]);
154 +
155 + BehaviorSubject<T> subscribe<T>(
156 + {@required String id,
157 + @required String method,
158 + List<Object> params = const []}) {
159 + final subscription = BehaviorSubject<T>();
160 + _regisrySubscription(id, subscription);
161 + socket.write(jsonrpc(method: method, id: _id, params: params));
162 +
163 + return subscription;
164 + }
165 +
166 + Future<dynamic> call({String method, List<Object> params = const []}) {
167 + final completer = Completer<dynamic>();
168 + _id += 1;
169 + final id = _id;
170 + _regisryTask(id, completer);
171 + socket.write(jsonrpc(method: method, id: _id, params: params));
172 +
173 + return completer.future;
174 + }
175 +
176 + void request({String method, List<Object> params = const []}) {
177 + _id += 1;
178 + socket.write(jsonrpc(method: method, id: _id, params: params));
179 + }
180 +
181 + void _regisryTask(int id, Completer completer) => _tasks[id.toString()] =
182 + SocketTask(completer: completer, isSubscription: false);
183 +
184 + void _regisrySubscription(String id, BehaviorSubject subject) =>
185 + _tasks[id] = SocketTask(subject: subject, isSubscription: true);
186 +
187 + void _finish(String id, Object data) {
188 + if (_tasks[id] == null) {
189 + return;
190 + }
191 +
192 + _tasks[id]?.completer?.complete(data);
193 +
194 + if (!(_tasks[id]?.isSubscription ?? false)) {
195 + _tasks[id] = null;
196 + } else {
197 + _tasks[id].subject.add(data);
198 + }
199 + }
200 +
201 + void _methodHandler(
202 + {@required String method, @required Map<String, Object> request}) {
203 + switch (method) {
204 + case 'blockchain.address.subscribe':
205 + final params = request['params'] as List<dynamic>;
206 + final address = params.first as String;
207 + final id = 'blockchain.address.subscribe:$address';
208 +
209 + if (_tasks[id] != null) {
210 + _tasks[id].subject.add(params.last);
211 + }
212 +
213 + break;
214 + default:
215 + break;
216 + }
217 + }
218 +}
lib/bitcoin/file.dart new
+43
@@ -0,0 +1,43 @@
1 +import 'dart:io';
2 +import 'dart:convert';
3 +import 'package:cake_wallet/bitcoin/key.dart';
4 +import 'package:encrypt/encrypt.dart' as encrypt;
5 +import 'package:flutter/foundation.dart';
6 +
7 +Future<void> write(
8 + {@required String path,
9 + @required String password,
10 + @required Map<String, String> obj}) async {
11 + final jsoned = json.encode(obj);
12 + final keys = extractKeys(password);
13 + final key = encrypt.Key.fromBase64(keys.first);
14 + final iv = encrypt.IV.fromBase64(keys.last);
15 + final encrypted = await encode(key: key, iv: iv, data: jsoned);
16 + final f = File(path);
17 + f.writeAsStringSync(encrypted);
18 +}
19 +
20 +Future<void> writeData(
21 + {@required String path,
22 + @required String password,
23 + @required String data}) async {
24 + final keys = extractKeys(password);
25 + final key = encrypt.Key.fromBase64(keys.first);
26 + final iv = encrypt.IV.fromBase64(keys.last);
27 + final encrypted = await encode(key: key, iv: iv, data: data);
28 + final f = File(path);
29 + f.writeAsStringSync(encrypted);
30 +}
31 +
32 +Future<String> read(
33 + {@required String path, @required String password}) async {
34 + final file = File(path);
35 +
36 + if (!file.existsSync()) {
37 + file.createSync();
38 + }
39 +
40 + final encrypted = file.readAsStringSync();
41 +
42 + return decode(password: password, data: encrypted);
43 +}
lib/bitcoin/key.dart new
+34
@@ -0,0 +1,34 @@
1 +import 'package:encrypt/encrypt.dart' as encrypt;
2 +
3 +const ivEncodedStringLength = 12;
4 +
5 +String generateKey() {
6 + final key = encrypt.Key.fromSecureRandom(512);
7 + final iv = encrypt.IV.fromSecureRandom(8);
8 +
9 + return key.base64 + iv.base64;
10 +}
11 +
12 +List<String> extractKeys(String key) {
13 + final _key = key.substring(0, key.length - ivEncodedStringLength);
14 + final iv = key.substring(key.length - ivEncodedStringLength);
15 +
16 + return [_key, iv];
17 +}
18 +
19 +Future<String> encode({encrypt.Key key, encrypt.IV iv, String data}) async {
20 + final encrypter = encrypt.Encrypter(encrypt.Salsa20(key));
21 + final encrypted = encrypter.encrypt(data, iv: iv);
22 +
23 + return encrypted.base64;
24 +}
25 +
26 +Future<String> decode({String password, String data}) async {
27 + final keys = extractKeys(password);
28 + final key = encrypt.Key.fromBase64(keys.first);
29 + final iv = encrypt.IV.fromBase64(keys.last);
30 + final encrypter = encrypt.Encrypter(encrypt.Salsa20(key));
31 + final encrypted = encrypter.decrypt64(data, iv: iv);
32 +
33 + return encrypted;
34 +}
lib/main.dart
+43 -1
@@ -1,3 +1,7 @@
1 +import 'package:cake_wallet/bitcoin/api.dart';
2 +import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
3 +import 'package:cake_wallet/bitcoin/bitcoin_wallet.manager.dart';
4 +import 'package:cake_wallet/bitcoin/key.dart';
5 import 'package:flutter_localizations/flutter_localizations.dart';
6 import 'package:path_provider/path_provider.dart';
7 import 'package:shared_preferences/shared_preferences.dart';
@@ -36,6 +40,8 @@ import 'package:cake_wallet/src/domain/services/wallet_service.dart';
40 import 'package:cake_wallet/generated/i18n.dart';
41 import 'package:cake_wallet/src/domain/common/language.dart';
42 import 'package:cake_wallet/src/stores/seed_language/seed_language_store.dart';
43 +import 'package:cake_wallet/bitcoin/electrum.dart';
44 +
45
46 void main() async {
47 WidgetsFlutterBinding.ensureInitialized();
@@ -111,6 +117,42 @@ void main() async {
117 authenticationStore: authenticationStore,
118 loginStore: loginStore);
119
120 + // final addresses = await fetchAllAddresses(xpub: '6CUGRUonZSQ4TWtTMmzXdrXDtypWKiKrhko4egpiMZbpiaQL2jkwSB1icqYh2cfDfVxdx4df189oLKnC5fSwqPfgyP3hooxujYzAu3fDVmz');
121 + // print(addresses);
122 +
123 + final eclient = ElectrumClient();
124 + await eclient.connect(host: "electrum2.hodlister.co", port: 50002);
125 + await eclient.getMerkle(hash: '3780302c523831311afaccd883f04c814bc13c3ad7c2c13810bb5bfe2c3fa621', height: 629341);
126 + print(await eclient.getHeader(height: 629341));
127 + // final version = await eclient.version();
128 + // print('version $version');
129 +
130 + // eclient.banner();
131 + // eclient.headersSubscribe();
132 +
133 + // final history = await eclient.getHistory(address: '1QEWnc4mSxUoP1fWPs32eZNRtc9zX55TwE');
134 + // print('history $history');
135 +
136 + // final balance = await eclient.getBalance(address: '1QEWnc4mSxUoP1fWPs32eZNRtc9zX55TwE');
137 + // print('balance $balance');
138 +
139 + // final estimateFee = await eclient.estimatefee(p: 6);
140 + // print('estimateFee $estimateFee');
141 +
142 + // final walletManager = BitcoinWalletManager();
143 + // final key = "zLEjJ96r4WPzlc8rWbuaP2HgxoNfec6sbWjKAcNwqTjzBfiE62A8/0Wp5P3a8Ryo3GUIs/GDG7KfwkoI1FpyuhzWZNU1P8sMN/fp88sB9ktffU5V4B9GZJU5ufSblQOKsvZxqxLJWA8nhL7iaUGcifr9TkwbpqHxBTZDlQxlZXAf/DlRUFEF2LLwo8EJ0HcCn+iPVsnqeGtgtjmOG6l7puP31AErKzaLX4yEgXaKxrdqo0ljS4g7fn4UUXpipv7ry83ZId9ZhpkcdqMRnzu84Msyg/UGGg3BX7VTtbO/ko7ojIBoyzEaF355Tg+sgbfwYAY0CNvOJqPpIhDwu+sq4mhb5H592JP426rDTcy9KV1JbZWbnbbWcqcb04vE2zvXN0x37bd4WfO77qkdoGN5m1XZB2+F2wzNUxvf25WPp5L/nvPZFk/rJGGFoy6X8mASnmIXcq5bRzwC+F2zkZSbXoRFx3yXxlaRnzltVDjWlLUrh8S01TV2llUJEFQhefzR3Xz7mgmHRXANIqRztb1AmjD7eVZid84OfedhD2Lfg9rzFcXeMTcBlaKR36ChIY5zw+ljpnqAm86pSwcJXOAJVKcQ0fJLT6dYbHYkOQqdiSs4cJQMdr/xshrkFd1raVDyL8CTNznfxSvWqSrCUqxbuvylGfrWgHzJfK5CB0oLZnA=WBZ/TzHfP4A=";
144 + // await walletManager.openWallet('name', password)
145 + // print(await walletManager.isWalletExit('qwerty'));
146 + // final wallet = await walletManager.openWallet('green', key);
147 + // final keys = await wallet.getKeys();
148 + // final seed = await wallet.getSeed();
149 + // final address = await wallet.getAddress();
150 +
151 + // print('key $key');
152 + // print('keys $keys');
153 + // print('seed $seed');
154 + // print('address $address');
155 +
156 runApp(MultiProvider(providers: [
157 Provider(create: (_) => sharedPreferences),
158 Provider(create: (_) => walletService),
@@ -136,7 +178,7 @@ Future<void> initialSetup(
178 Box<Node> nodes,
179 AuthenticationStore authStore,
180 int initialMigrationVersion = 1,
139 - WalletType initialWalletType = WalletType.monero}) async {
181 + WalletType initialWalletType = WalletType.bitcoin}) async {
182 await walletListService.changeWalletManger(walletType: initialWalletType);
183 await defaultSettingsMigration(
184 version: initialMigrationVersion,
lib/router.dart
+31 -9
@@ -1,3 +1,4 @@
1 +import 'package:cake_wallet/src/domain/common/wallet_type.dart';
2 import 'package:flutter/cupertino.dart';
3 import 'package:flutter/material.dart';
4 import 'package:flutter_secure_storage/flutter_secure_storage.dart';
@@ -109,6 +110,9 @@ class Router {
110 return MaterialPageRoute<void>(builder: (_) => createWelcomePage());
111
112 case Routes.newWalletFromWelcome:
113 + final type = settings.arguments as WalletType;
114 + walletListService.changeWalletManger(walletType: type);
115 +
116 return CupertinoPageRoute<void>(
117 builder: (_) => Provider(
118 create: (_) => UserStore(
@@ -116,10 +120,14 @@ class Router {
120 secureStorage: FlutterSecureStorage(),
121 sharedPreferences: sharedPreferences)),
122 child: SetupPinCodePage(
119 - onPinCodeSetup: (context, _) =>
120 - Navigator.pushNamed(context, Routes.newWallet))));
123 + onPinCodeSetup: (context, _) => Navigator.pushNamed(
124 + context, Routes.newWallet,
125 + arguments: type))));
126
127 case Routes.newWallet:
128 + final type = settings.arguments as WalletType;
129 + walletListService.changeWalletManger(walletType: type);
130 +
131 return CupertinoPageRoute<void>(
132 builder:
133 (_) =>
@@ -152,11 +160,18 @@ class Router {
160 fullscreenDialog: true);
161
162 case Routes.restoreOptions:
155 - return CupertinoPageRoute<void>(builder: (_) => RestoreOptionsPage());
163 + final type = settings.arguments as WalletType;
164 + walletListService.changeWalletManger(walletType: type);
165 +
166 + return CupertinoPageRoute<void>(
167 + builder: (_) => RestoreOptionsPage(type: type));
168
169 case Routes.restoreWalletOptions:
170 + final type = settings.arguments as WalletType;
171 + walletListService.changeWalletManger(walletType: type);
172 +
173 return CupertinoPageRoute<void>(
159 - builder: (_) => RestoreWalletOptionsPage());
174 + builder: (_) => RestoreWalletOptionsPage(type: type));
175
176 case Routes.restoreWalletOptionsFromWelcome:
177 return CupertinoPageRoute<void>(
@@ -177,6 +192,9 @@ class Router {
192 callback: settings.arguments as void Function()));
193
194 case Routes.restoreWalletFromSeed:
195 + final type = settings.arguments as WalletType;
196 + walletListService.changeWalletManger(walletType: type);
197 +
198 return CupertinoPageRoute<void>(
199 builder: (_) =>
200 ProxyProvider<AuthenticationStore, WalletRestorationStore>(
@@ -235,11 +253,15 @@ class Router {
253 case Routes.receive:
254 return CupertinoPageRoute<void>(
255 fullscreenDialog: true,
238 - builder: (_) => MultiProvider(providers: [
239 - Provider(
240 - create: (_) =>
241 - SubaddressListStore(walletService: walletService))
242 - ], child: ReceivePage()));
256 + builder: (_) => MultiProvider(
257 + providers: walletService.getType() == WalletType.monero
258 + ? [
259 + Provider(
260 + create: (_) => SubaddressListStore(
261 + walletService: walletService))
262 + ]
263 + : [],
264 + child: ReceivePage()));
265
266 case Routes.transactionDetails:
267 return CupertinoPageRoute<void>(
lib/src/domain/common/pathForWallet.dart new
+21
@@ -0,0 +1,21 @@
1 +import 'dart:io';
2 +import 'package:cake_wallet/src/domain/common/wallet_type.dart';
3 +import 'package:flutter/foundation.dart';
4 +import 'package:path_provider/path_provider.dart';
5 +
6 +Future<String> pathForWalletDir({@required String name, @required WalletType type}) async {
7 + final root = await getApplicationDocumentsDirectory();
8 + final prefix = walletTypeToString(type).toLowerCase();
9 + final walletsDir = Directory('${root.path}/wallets');
10 + final walletDire = Directory('${walletsDir.path}/$prefix/$name');
11 +
12 + if (!walletDire.existsSync()) {
13 + walletDire.createSync(recursive: true);
14 + }
15 +
16 + return walletDire.path;
17 +}
18 +
19 +Future<String> pathForWallet({@required String name, @required WalletType type}) async =>
20 + await pathForWalletDir(name: name, type: type)
21 + .then((path) => path + '/$name');
lib/src/domain/common/transaction_history.dart
-2
@@ -4,7 +4,5 @@ import 'package:cake_wallet/src/domain/common/transaction_info.dart';
4 abstract class TransactionHistory {
5 Observable<List<TransactionInfo>> transactions;
6 Future<List<TransactionInfo>> getAll();
7 - Future<int> count();
8 - Future refresh();
7 Future update();
8 }
lib/src/domain/common/transaction_info.dart
+9 -47
@@ -1,49 +1,11 @@
1 -import 'package:cake_wallet/src/domain/monero/monero_amount_format.dart';
2 -import 'package:cw_monero/structs/transaction_info_row.dart';
3 -import 'package:cake_wallet/src/domain/common/parseBoolFromString.dart';
1 import 'package:cake_wallet/src/domain/common/transaction_direction.dart';
5 -import 'package:cake_wallet/src/domain/common/format_amount.dart';
2
7 -class TransactionInfo {
8 - TransactionInfo(this.id, this.height, this.direction, this.date,
9 - this.isPending, this.amount, this.accountIndex);
10 -
11 - TransactionInfo.fromMap(Map map)
12 - : id = (map['hash'] ?? '') as String,
13 - height = (map['height'] ?? 0) as int,
14 - direction =
15 - parseTransactionDirectionFromNumber(map['direction'] as String) ??
16 - TransactionDirection.incoming,
17 - date = DateTime.fromMillisecondsSinceEpoch(
18 - (int.parse(map['timestamp'] as String) ?? 0) * 1000),
19 - isPending = parseBoolFromString(map['isPending'] as String),
20 - amount = map['amount'] as int,
21 - accountIndex = int.parse(map['accountIndex'] as String);
22 -
23 - TransactionInfo.fromRow(TransactionInfoRow row)
24 - : id = row.getHash(),
25 - height = row.blockHeight,
26 - direction = parseTransactionDirectionFromInt(row.direction) ??
27 - TransactionDirection.incoming,
28 - date = DateTime.fromMillisecondsSinceEpoch(row.getDatetime() * 1000),
29 - isPending = row.isPending != 0,
30 - amount = row.getAmount(),
31 - accountIndex = row.subaddrAccount;
32 -
33 - final String id;
34 - final int height;
35 - final TransactionDirection direction;
36 - final DateTime date;
37 - final int accountIndex;
38 - final bool isPending;
39 - final int amount;
40 - String recipientAddress;
41 -
42 - String _fiatAmount;
43 -
44 - String amountFormatted() => '${formatAmount(moneroAmountToString(amount: amount))} XMR';
45 -
46 - String fiatAmount() => _fiatAmount ?? '';
47 -
48 - void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
49 -}
3 +abstract class TransactionInfo extends Object {
4 + int amount;
5 + TransactionDirection direction;
6 + bool isPending;
7 + DateTime date;
8 + int height;
9 + String amountFormatted();
10 + String fiatAmount();
11 +}
\ No newline at end of file
lib/src/domain/common/wallet_type.dart
+9 -2
@@ -2,13 +2,18 @@ import 'package:hive/hive.dart';
2
3 part 'wallet_type.g.dart';
4
5 +const walletTypes = [WalletType.monero, WalletType.bitcoin];
6 +
7 @HiveType()
8 enum WalletType {
9 @HiveField(0)
10 monero,
11
12 @HiveField(1)
11 - none
13 + none,
14 +
15 + @HiveField(2)
16 + bitcoin
17 }
18
19 int serializeToInt(WalletType type) {
@@ -33,7 +38,9 @@ String walletTypeToString(WalletType type) {
38 switch (type) {
39 case WalletType.monero:
40 return 'Monero';
41 + case WalletType.bitcoin:
42 + return 'Bitcoin';
43 default:
44 return '';
45 }
39 -}
\ No newline at end of file
46 +}
lib/src/domain/monero/monero_transaction_history.dart
+10 -11
@@ -1,4 +1,5 @@
1 import 'dart:core';
2 +import 'package:cake_wallet/src/domain/monero/monero_transaction_info.dart';
3 import 'package:flutter/services.dart';
4 import 'package:rxdart/rxdart.dart';
5 import 'package:cw_monero/transaction_history.dart'
@@ -6,10 +7,11 @@ import 'package:cw_monero/transaction_history.dart'
7 import 'package:cake_wallet/src/domain/common/transaction_history.dart';
8 import 'package:cake_wallet/src/domain/common/transaction_info.dart';
9
9 -List<TransactionInfo> _getAllTransactions(dynamic _) => monero_transaction_history
10 - .getAllTransations()
11 - .map((row) => TransactionInfo.fromRow(row))
12 - .toList();
10 +List<TransactionInfo> _getAllTransactions(dynamic _) =>
11 + monero_transaction_history
12 + .getAllTransations()
13 + .map((row) => MoneroTransactionInfo.fromRow(row))
14 + .toList();
15
16 class MoneroTransactionHistory extends TransactionHistory {
17 MoneroTransactionHistory()
@@ -31,7 +33,6 @@ class MoneroTransactionHistory extends TransactionHistory {
33
34 try {
35 _isUpdating = true;
34 - await refresh();
36 _transactions.value = await getAll(force: true);
37 _isUpdating = false;
38
@@ -46,13 +47,11 @@ class MoneroTransactionHistory extends TransactionHistory {
47 }
48
49 @override
49 - Future<List<TransactionInfo>> getAll({bool force = false}) async =>
50 - _getAllTransactions(null);
51 -
52 - @override
53 - Future<int> count() async => monero_transaction_history.countOfTransactions();
50 + Future<List<TransactionInfo>> getAll({bool force = false}) async {
51 + await refresh();
52 + return _getAllTransactions(null);
53 + }
54
55 - @override
55 Future refresh() async {
56 if (_isRefreshing) {
57 return;
lib/src/domain/monero/monero_transaction_info.dart new
+50
@@ -0,0 +1,50 @@
1 +import 'package:cake_wallet/src/domain/common/transaction_info.dart';
2 +import 'package:cake_wallet/src/domain/monero/monero_amount_format.dart';
3 +import 'package:cw_monero/structs/transaction_info_row.dart';
4 +import 'package:cake_wallet/src/domain/common/parseBoolFromString.dart';
5 +import 'package:cake_wallet/src/domain/common/transaction_direction.dart';
6 +import 'package:cake_wallet/src/domain/common/format_amount.dart';
7 +
8 +class MoneroTransactionInfo extends TransactionInfo {
9 + MoneroTransactionInfo(this.id, this.height, this.direction, this.date,
10 + this.isPending, this.amount, this.accountIndex);
11 +
12 + MoneroTransactionInfo.fromMap(Map map)
13 + : id = (map['hash'] ?? '') as String,
14 + height = (map['height'] ?? 0) as int,
15 + direction =
16 + parseTransactionDirectionFromNumber(map['direction'] as String) ??
17 + TransactionDirection.incoming,
18 + date = DateTime.fromMillisecondsSinceEpoch(
19 + (int.parse(map['timestamp'] as String) ?? 0) * 1000),
20 + isPending = parseBoolFromString(map['isPending'] as String),
21 + amount = map['amount'] as int,
22 + accountIndex = int.parse(map['accountIndex'] as String);
23 +
24 + MoneroTransactionInfo.fromRow(TransactionInfoRow row)
25 + : id = row.getHash(),
26 + height = row.blockHeight,
27 + direction = parseTransactionDirectionFromInt(row.direction) ??
28 + TransactionDirection.incoming,
29 + date = DateTime.fromMillisecondsSinceEpoch(row.getDatetime() * 1000),
30 + isPending = row.isPending != 0,
31 + amount = row.getAmount(),
32 + accountIndex = row.subaddrAccount;
33 +
34 + final String id;
35 + final int height;
36 + final TransactionDirection direction;
37 + final DateTime date;
38 + final int accountIndex;
39 + final bool isPending;
40 + final int amount;
41 + String recipientAddress;
42 +
43 + String _fiatAmount;
44 +
45 + String amountFormatted() => '${formatAmount(moneroAmountToString(amount: amount))} XMR';
46 +
47 + String fiatAmount() => _fiatAmount ?? '';
48 +
49 + void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
50 +}
lib/src/domain/monero/monero_wallets_manager.dart
+2 -11
@@ -1,5 +1,6 @@
1 import 'dart:async';
2 import 'dart:io';
3 +import 'package:cake_wallet/src/domain/common/pathForWallet.dart';
4 import 'package:flutter/foundation.dart';
5 import 'package:hive/hive.dart';
6 import 'package:path_provider/path_provider.dart';
@@ -11,17 +12,7 @@ import 'package:cake_wallet/src/domain/common/wallet.dart';
12 import 'package:cake_wallet/src/domain/monero/monero_wallet.dart';
13 import 'package:cake_wallet/src/domain/common/wallet_description.dart';
14
14 -Future<String> pathForWallet({String name}) async {
15 - final directory = await getApplicationDocumentsDirectory();
16 - final pathDir = directory.path + '/$name';
17 - final dir = Directory(pathDir);
15
19 - if (!await dir.exists()) {
20 - await dir.create();
21 - }
22 -
23 - return pathDir + '/$name';
24 -}
16
17 class MoneroWalletsManager extends WalletsManager {
18 MoneroWalletsManager({@required this.walletInfoSource});
@@ -34,7 +25,7 @@ class MoneroWalletsManager extends WalletsManager {
25 Future<Wallet> create(String name, String password, String language) async {
26 try {
27 const isRecovery = false;
37 - final path = await pathForWallet(name: name);
28 + final path = await pathForWallet(name: name, type: WalletType.monero);
29
30 await monero_wallet_manager.createWallet(path: path, password: password, language: language);
31
lib/src/domain/services/wallet_list_service.dart
+25 -6
@@ -1,4 +1,6 @@
1 import 'dart:async';
2 +import 'package:cake_wallet/bitcoin/bitcoin_wallet.manager.dart';
3 +import 'package:cake_wallet/bitcoin/key.dart';
4 import 'package:cake_wallet/src/domain/common/wallet_info.dart';
5 import 'package:flutter/foundation.dart';
6 import 'package:hive/hive.dart';
@@ -29,13 +31,15 @@ class WalletListService {
31 this.walletInfoSource,
32 this.walletsManager,
33 @required this.walletService,
32 - @required this.sharedPreferences});
34 + @required this.sharedPreferences})
35 + : _type = WalletType.monero;
36
37 final FlutterSecureStorage secureStorage;
38 final WalletService walletService;
39 final Box<WalletInfo> walletInfoSource;
40 final SharedPreferences sharedPreferences;
41 WalletsManager walletsManager;
42 + WalletType _type;
43
44 Future<List<WalletDescription>> getAll() async => walletInfoSource.values
45 .map((info) => WalletDescription(name: info.name, type: info.type))
@@ -50,7 +54,7 @@ class WalletListService {
54 await walletService.close();
55 }
56
53 - final password = Uuid().v4();
57 + final password = _generatePassword();
58 await saveWalletPassword(password: password, walletName: name);
59
60 final wallet = await walletsManager.create(name, password, language);
@@ -67,7 +71,7 @@ class WalletListService {
71 await walletService.close();
72 }
73
70 - final password = Uuid().v4();
74 + final password = _generatePassword();
75 await saveWalletPassword(password: password, walletName: name);
76
77 final wallet = await walletsManager.restoreFromSeed(
@@ -76,8 +80,8 @@ class WalletListService {
80 await onWalletChange(wallet);
81 }
82
79 - Future restoreFromKeys(String name, String language, int restoreHeight, String address,
80 - String viewKey, String spendKey) async {
83 + Future restoreFromKeys(String name, String language, int restoreHeight,
84 + String address, String viewKey, String spendKey) async {
85 if (await walletsManager.isWalletExit(name)) {
86 throw WalletIsExistException(name);
87 }
@@ -86,7 +90,7 @@ class WalletListService {
90 await walletService.close();
91 }
92
89 - final password = Uuid().v4();
93 + final password = _generatePassword();
94 await saveWalletPassword(password: password, walletName: name);
95
96 final wallet = await walletsManager.restoreFromKeys(
@@ -107,11 +111,16 @@ class WalletListService {
111 }
112
113 Future changeWalletManger({WalletType walletType}) async {
114 + _type = walletType;
115 +
116 switch (walletType) {
117 case WalletType.monero:
118 walletsManager =
119 MoneroWalletsManager(walletInfoSource: walletInfoSource);
120 break;
121 + case WalletType.bitcoin:
122 + walletsManager = BitcoinWalletManager();
123 + break;
124 case WalletType.none:
125 walletsManager = null;
126 break;
@@ -121,6 +130,7 @@ class WalletListService {
130 Future onWalletChange(Wallet wallet) async {
131 walletService.currentWallet = wallet;
132 final walletName = await wallet.getName();
133 + print('walletName $walletName ');
134 await sharedPreferences.setString('current_wallet_name', walletName);
135 }
136
@@ -142,4 +152,13 @@ class WalletListService {
152
153 await secureStorage.write(key: key, value: encodedPassword);
154 }
155 +
156 + String _generatePassword() {
157 + switch (_type) {
158 + case WalletType.bitcoin:
159 + return generateKey();
160 + default:
161 + return Uuid().v4();
162 + }
163 + }
164 }
lib/src/domain/services/wallet_service.dart
+1 -1
@@ -70,7 +70,7 @@ class WalletService extends Wallet {
70 WalletDescription description;
71
72 @override
73 - WalletType getType() => WalletType.monero;
73 + WalletType getType() => _currentWallet.getType();
74
75 @override
76 Future<String> getFilename() => _currentWallet.getFilename();
lib/src/screens/receive/receive_page.dart
+89 -80
@@ -33,7 +33,7 @@ class ReceivePage extends BasePage {
33 splashColor: Colors.transparent,
34 padding: EdgeInsets.all(0),
35 onPressed: () => Share.text(
36 - 'Share address', walletStore.subaddress.address, 'text/plain'),
36 + 'Share address', walletStore.getAddress, 'text/plain'),
37 child: Icon(
38 Icons.share,
39 size: 30.0,
@@ -65,7 +65,11 @@ class ReceiveBodyState extends State<ReceiveBody> {
65 @override
66 Widget build(BuildContext context) {
67 final walletStore = Provider.of<WalletStore>(context);
68 - final subaddressListStore = Provider.of<SubaddressListStore>(context);
68 + SubaddressListStore subaddressListStore;
69 +
70 + try {
71 + subaddressListStore = Provider.of<SubaddressListStore>(context);
72 + } catch (_) {}
73
74 final currentColor = Theme.of(context).selectedRowColor;
75 final notCurrentColor = Theme.of(context).scaffoldBackgroundColor;
@@ -101,7 +105,7 @@ class ReceiveBodyState extends State<ReceiveBody> {
105 padding: EdgeInsets.all(5),
106 color: Colors.white,
107 child: QrImage(
104 - data: walletStore.subaddress.address +
108 + data: walletStore.getAddress +
109 walletStore.amountValue,
110 backgroundColor: Colors.transparent,
111 ),
@@ -122,8 +126,8 @@ class ReceiveBodyState extends State<ReceiveBody> {
126 child: Center(
127 child: GestureDetector(
128 onTap: () {
125 - Clipboard.setData(ClipboardData(
126 - text: walletStore.subaddress.address));
129 + Clipboard.setData(
130 + ClipboardData(text: walletStore.getAddress));
131 Scaffold.of(context).showSnackBar(SnackBar(
132 content: Text(
133 S.of(context).copied_to_clipboard,
@@ -133,7 +137,7 @@ class ReceiveBodyState extends State<ReceiveBody> {
137 ));
138 },
139 child: Text(
136 - walletStore.subaddress.address,
140 + walletStore.getAddress,
141 textAlign: TextAlign.center,
142 style: TextStyle(
143 fontSize: 14.0,
@@ -187,78 +191,17 @@ class ReceiveBodyState extends State<ReceiveBody> {
191 ],
192 ),
193 ),
190 - Row(
191 - children: <Widget>[
192 - Expanded(
193 - child: Container(
194 - color: Theme.of(context).accentTextTheme.headline.color,
195 - child: Column(
194 + subaddressListStore != null
195 + ? Row(
196 children: <Widget>[
197 - ListTile(
198 - title: Text(
199 - S.of(context).subaddresses,
200 - style: TextStyle(
201 - fontSize: 16.0,
202 - color: Theme.of(context)
203 - .primaryTextTheme
204 - .headline
205 - .color),
206 - ),
207 - trailing: Container(
208 - width: 28.0,
209 - height: 28.0,
210 - decoration: BoxDecoration(
211 - color: Theme.of(context).selectedRowColor,
212 - shape: BoxShape.circle),
213 - child: InkWell(
214 - onTap: () => Navigator.of(context)
215 - .pushNamed(Routes.newSubaddress),
216 - borderRadius: BorderRadius.all(Radius.circular(14.0)),
217 - child: Icon(
218 - Icons.add,
219 - color: Palette.violet,
220 - size: 22.0,
221 - ),
222 - ),
223 - ),
224 - ),
225 - Divider(
226 - color: Theme.of(context).dividerTheme.color,
227 - height: 1.0,
228 - )
229 - ],
230 - ),
231 - ))
232 - ],
233 - ),
234 - Observer(builder: (_) {
235 - return ListView.separated(
236 - shrinkWrap: true,
237 - physics: NeverScrollableScrollPhysics(),
238 - itemCount: subaddressListStore.subaddresses.length,
239 - separatorBuilder: (context, i) {
240 - return Divider(
241 - color: Theme.of(context).dividerTheme.color,
242 - height: 1.0,
243 - );
244 - },
245 - itemBuilder: (context, i) {
246 - return Observer(builder: (_) {
247 - final subaddress = subaddressListStore.subaddresses[i];
248 - final isCurrent =
249 - walletStore.subaddress.address == subaddress.address;
250 - final label = subaddress.label.isNotEmpty
251 - ? subaddress.label
252 - : subaddress.address;
253 -
254 - return InkWell(
255 - onTap: () => walletStore.setSubaddress(subaddress),
256 - child: Container(
257 - color: isCurrent ? currentColor : notCurrentColor,
258 - child: Column(children: <Widget>[
197 + Expanded(
198 + child: Container(
199 + color: Theme.of(context).accentTextTheme.headline.color,
200 + child: Column(
201 + children: <Widget>[
202 ListTile(
203 title: Text(
261 - label,
204 + S.of(context).subaddresses,
205 style: TextStyle(
206 fontSize: 16.0,
207 color: Theme.of(context)
@@ -266,13 +209,79 @@ class ReceiveBodyState extends State<ReceiveBody> {
209 .headline
210 .color),
211 ),
212 + trailing: Container(
213 + width: 28.0,
214 + height: 28.0,
215 + decoration: BoxDecoration(
216 + color: Theme.of(context).selectedRowColor,
217 + shape: BoxShape.circle),
218 + child: InkWell(
219 + onTap: () => Navigator.of(context)
220 + .pushNamed(Routes.newSubaddress),
221 + borderRadius:
222 + BorderRadius.all(Radius.circular(14.0)),
223 + child: Icon(
224 + Icons.add,
225 + color: Palette.violet,
226 + size: 22.0,
227 + ),
228 + ),
229 + ),
230 + ),
231 + Divider(
232 + color: Theme.of(context).dividerTheme.color,
233 + height: 1.0,
234 )
270 - ]),
235 + ],
236 ),
272 - );
273 - });
274 - });
275 - })
237 + ))
238 + ],
239 + )
240 + : Container(),
241 + subaddressListStore != null
242 + ? Observer(builder: (_) {
243 + return ListView.separated(
244 + shrinkWrap: true,
245 + physics: NeverScrollableScrollPhysics(),
246 + itemCount: subaddressListStore.subaddresses.length,
247 + separatorBuilder: (context, i) {
248 + return Divider(
249 + color: Theme.of(context).dividerTheme.color,
250 + height: 1.0,
251 + );
252 + },
253 + itemBuilder: (context, i) {
254 + return Observer(builder: (_) {
255 + final subaddress = subaddressListStore.subaddresses[i];
256 + final isCurrent =
257 + walletStore.getAddress == subaddress.address;
258 + final label = subaddress.label.isNotEmpty
259 + ? subaddress.label
260 + : subaddress.address;
261 +
262 + return InkWell(
263 + onTap: () => walletStore.setSubaddress(subaddress),
264 + child: Container(
265 + color: isCurrent ? currentColor : notCurrentColor,
266 + child: Column(children: <Widget>[
267 + ListTile(
268 + title: Text(
269 + label,
270 + style: TextStyle(
271 + fontSize: 16.0,
272 + color: Theme.of(context)
273 + .primaryTextTheme
274 + .headline
275 + .color),
276 + ),
277 + )
278 + ]),
279 + ),
280 + );
281 + });
282 + });
283 + })
284 + : Container()
285 ],
286 )));
287 }
lib/src/screens/restore/restore_options_page.dart
+4
@@ -1,3 +1,4 @@
1 +import 'package:cake_wallet/src/domain/common/wallet_type.dart';
2 import 'package:flutter/material.dart';
3 import 'package:cake_wallet/palette.dart';
4 import 'package:cake_wallet/routes.dart';
@@ -9,7 +10,10 @@ import 'package:cake_wallet/src/screens/base_page.dart';
10 import 'package:cake_wallet/generated/i18n.dart';
11
12 class RestoreOptionsPage extends BasePage {
13 + RestoreOptionsPage({@required this.type});
14 +
15 static const _aspectRatioImage = 2.086;
16 + final WalletType type;
17
18 @override
19 String get title => S.current.restore_restore_wallet;
lib/src/screens/restore/restore_wallet_options_page.dart
+6 -2
@@ -6,12 +6,16 @@ import 'package:cake_wallet/src/screens/restore/widgets/restore_button.dart';
6 import 'package:cake_wallet/src/screens/restore/widgets/image_widget.dart';
7 import 'package:cake_wallet/src/screens/restore/widgets/base_restore_widget.dart';
8 import 'package:cake_wallet/src/screens/base_page.dart';
9 +import 'package:cake_wallet/src/domain/common/wallet_type.dart';
10 import 'package:cake_wallet/generated/i18n.dart';
11 import 'package:cake_wallet/src/stores/seed_language/seed_language_store.dart';
12 import 'package:provider/provider.dart';
13
14 class RestoreWalletOptionsPage extends BasePage {
15 + RestoreWalletOptionsPage({@required this.type});
16 +
17 static const _aspectRatioImage = 2.086;
18 + final WalletType type;
19
20 @override
21 String get title => S.current.restore_seed_keys_restore;
@@ -31,7 +35,7 @@ class RestoreWalletOptionsPage extends BasePage {
35 firstRestoreButton: RestoreButton(
36 onPressed: () {
37 seedLanguageStore.setCurrentRoute(Routes.restoreWalletFromSeed);
34 - Navigator.pushNamed(context, Routes.seedLanguage);
38 + Navigator.pushNamed(context, Routes.seedLanguage, arguments: type);
39 },
40 imageWidget: ImageWidget(
41 image: _imageSeed,
@@ -46,7 +50,7 @@ class RestoreWalletOptionsPage extends BasePage {
50 secondRestoreButton: RestoreButton(
51 onPressed: () {
52 seedLanguageStore.setCurrentRoute(Routes.restoreWalletFromKeys);
49 - Navigator.pushNamed(context, Routes.seedLanguage);
53 + Navigator.pushNamed(context, Routes.seedLanguage, arguments: type);
54 },
55 imageWidget: ImageWidget(
56 image: _imageKeys,
lib/src/screens/show_keys/show_keys_page.dart
+10 -2
@@ -25,11 +25,19 @@ class ShowKeysPage extends BasePage {
25 builder: (_) {
26 final keysMap = {
27 S.of(context).view_key_public: walletKeysStore.publicViewKey,
28 - S.of(context).view_key_private: walletKeysStore.privateViewKey,
29 - S.of(context).spend_key_public: walletKeysStore.publicSpendKey,
28 S.of(context).spend_key_private: walletKeysStore.privateSpendKey
29 };
30
31 + if (walletKeysStore.privateViewKey.isNotEmpty) {
32 + keysMap[S.of(context).view_key_private] =
33 + walletKeysStore.privateViewKey;
34 + }
35 +
36 + if (walletKeysStore.publicSpendKey.isNotEmpty) {
37 + keysMap[S.of(context).spend_key_public] =
38 + walletKeysStore.publicSpendKey;
39 + }
40 +
41 return ListView.separated(
42 separatorBuilder: (_, __) => Container(
43 padding: EdgeInsets.only(left: 30.0, right: 20.0),
lib/src/screens/transaction_details/transaction_details_page.dart
+26 -23
@@ -1,3 +1,4 @@
1 +import 'package:cake_wallet/src/domain/monero/monero_transaction_info.dart';
2 import 'package:provider/provider.dart';
3 import 'package:flutter/material.dart';
4 import 'package:flutter/cupertino.dart';
@@ -46,31 +47,33 @@ class TransactionDetailsFormState extends State<TransactionDetailsForm> {
47 @override
48 void initState() {
49 final _dateFormat = widget.settingsStore.getCurrentDateFormat(
49 - formatUSA: "yyyy.MM.dd, HH:mm",
50 - formatDefault: "dd.MM.yyyy, HH:mm");
51 - final items = [
52 - StandartListItem(
53 - title: S.current.transaction_details_transaction_id,
54 - value: widget.transactionInfo.id),
55 - StandartListItem(
56 - title: S.current.transaction_details_date,
57 - value: _dateFormat.format(widget.transactionInfo.date)),
58 - StandartListItem(
59 - title: S.current.transaction_details_height,
60 - value: '${widget.transactionInfo.height}'),
61 - StandartListItem(
62 - title: S.current.transaction_details_amount,
63 - value: widget.transactionInfo.amountFormatted())
64 - ];
65 -
66 - if (widget.settingsStore.shouldSaveRecipientAddress &&
67 - widget.transactionInfo.recipientAddress != null) {
68 - items.add(StandartListItem(
69 - title: S.current.transaction_details_recipient_address,
70 - value: widget.transactionInfo.recipientAddress));
50 + formatUSA: "yyyy.MM.dd, HH:mm", formatDefault: "dd.MM.yyyy, HH:mm");
51 + final tx = widget.transactionInfo;
52 +
53 + if (tx is MoneroTransactionInfo) {
54 + final items = [
55 + StandartListItem(
56 + title: S.current.transaction_details_transaction_id, value: tx.id),
57 + StandartListItem(
58 + title: S.current.transaction_details_date,
59 + value: _dateFormat.format(tx.date)),
60 + StandartListItem(
61 + title: S.current.transaction_details_height, value: '${tx.height}'),
62 + StandartListItem(
63 + title: S.current.transaction_details_amount,
64 + value: tx.amountFormatted())
65 + ];
66 +
67 + if (widget.settingsStore.shouldSaveRecipientAddress &&
68 + tx.recipientAddress != null) {
69 + items.add(StandartListItem(
70 + title: S.current.transaction_details_recipient_address,
71 + value: tx.recipientAddress));
72 + }
73 +
74 + _items.addAll(items);
75 }
76
73 - _items.addAll(items);
77 super.initState();
78 }
79
lib/src/screens/welcome/welcome_page.dart
+30 -2
@@ -1,3 +1,5 @@
1 +import 'package:cake_wallet/src/domain/common/wallet_type.dart';
2 +import 'package:cake_wallet/src/widgets/picker.dart';
3 import 'package:flutter/material.dart';
4 import 'package:cake_wallet/palette.dart';
5 import 'package:cake_wallet/routes.dart';
@@ -5,14 +7,38 @@ import 'package:cake_wallet/src/widgets/primary_button.dart';
7 import 'package:cake_wallet/src/screens/base_page.dart';
8 import 'package:cake_wallet/generated/i18n.dart';
9
10 +WalletType _selectedType;
11 +
12 class WelcomePage extends BasePage {
13 static const _aspectRatioImage = 1.26;
14 static const _baseWidth = 411.43;
15 final _image = Image.asset('assets/images/welcomeImg.png');
16 final _cakeLogo = Image.asset('assets/images/cake_logo.png');
17 + final Map<String, WalletType> _picker =
18 + walletTypes.fold(Map<String, WalletType>(), (acc, item) {
19 + acc[walletTypeToString(item)] = item;
20 + return acc;
21 + });
22
23 @override
24 Widget build(BuildContext context) {
25 + WidgetsBinding.instance.addPostFrameCallback((_) => _selectedType == null
26 + ? showDialog<void>(
27 + builder: (_) => Picker(
28 + items: walletTypes
29 + .map((item) => walletTypeToString(item))
30 + .toList(),
31 + selectedAtIndex: -1,
32 + title: 'Select wallet type',
33 + pickerHeight: 510,
34 + onItemSelected: (String item) {
35 + print('before $_selectedType');
36 + _selectedType = _picker[item];
37 + print('after $_selectedType');
38 + }),
39 + context: context)
40 + : null);
41 +
42 return Scaffold(
43 backgroundColor: Theme.of(context).backgroundColor,
44 resizeToAvoidBottomPadding: false,
@@ -72,7 +98,8 @@ class WelcomePage extends BasePage {
98 child: Column(children: <Widget>[
99 PrimaryButton(
100 onPressed: () {
75 - Navigator.pushNamed(context, Routes.newWalletFromWelcome);
101 + Navigator.pushNamed(context, Routes.newWalletFromWelcome,
102 + arguments: _selectedType);
103 },
104 text: S.of(context).create_new,
105 color:
@@ -82,7 +109,8 @@ class WelcomePage extends BasePage {
109 SizedBox(height: 10),
110 PrimaryButton(
111 onPressed: () {
85 - Navigator.pushNamed(context, Routes.restoreOptions);
112 + Navigator.pushNamed(context, Routes.restoreOptions,
113 + arguments: _selectedType);
114 },
115 color: Theme.of(context).accentTextTheme.caption.backgroundColor,
116 borderColor:
lib/src/stores/action_list/action_list_store.dart
+25 -14
@@ -1,5 +1,6 @@
1 import 'dart:async';
2 import 'package:cake_wallet/src/domain/exchange/trade.dart';
3 +import 'package:cake_wallet/src/domain/monero/monero_transaction_info.dart';
4 import 'package:hive/hive.dart';
5 import 'package:mobx/mobx.dart';
6 import 'package:flutter/foundation.dart';
@@ -98,10 +99,14 @@ abstract class ActionListBase with Store {
99 final price = _priceStore.prices[symbol];
100
101 _transactions.forEach((item) {
101 - final amount = calculateFiatAmountRaw(
102 - cryptoAmount: moneroAmountToDouble(amount: item.transaction.amount),
103 - price: price);
104 - item.transaction.changeFiatAmount(amount);
102 + final tx = item.transaction;
103 +
104 + if (tx is MoneroTransactionInfo) {
105 + final amount = calculateFiatAmountRaw(
106 + cryptoAmount: moneroAmountToDouble(amount: tx.amount),
107 + price: price);
108 + tx.changeFiatAmount(amount);
109 + }
110 });
111
112 return _transactions;
@@ -171,7 +176,6 @@ abstract class ActionListBase with Store {
176 tradesSource.values.map((trade) => TradeListItem(trade: trade)).toList();
177
178 Future _updateTransactionsList() async {
174 - await _history.refresh();
179 final _transactions = await _history.getAll();
180 await _setTransactions(_transactions);
181 }
@@ -203,22 +207,29 @@ abstract class ActionListBase with Store {
207 Future _setTransactions(List<TransactionInfo> transactions) async {
208 final wallet = _walletService.currentWallet;
209 List<TransactionInfo> sortedTransactions = transactions.map((transaction) {
206 - if (transactionDescriptions.values.isNotEmpty) {
207 - final description = transactionDescriptions.values.firstWhere(
208 - (desc) => desc.id == transaction.id,
209 - orElse: () => null);
210 -
211 - if (description != null && description.recipientAddress != null) {
212 - transaction.recipientAddress = description.recipientAddress;
210 + if (transaction is MoneroTransactionInfo) {
211 + if (transactionDescriptions.values.isNotEmpty) {
212 + final description = transactionDescriptions.values.firstWhere(
213 + (desc) => desc.id == transaction.id,
214 + orElse: () => null);
215 +
216 + if (description != null && description.recipientAddress != null) {
217 + transaction.recipientAddress = description.recipientAddress;
218 + }
219 }
220 +
221 + return transaction;
222 }
223
224 return transaction;
225 }).toList();
226
227 if (wallet is MoneroWallet) {
220 - sortedTransactions =
221 - transactions.where((tx) => tx.accountIndex == _account.id).toList();
228 + sortedTransactions = transactions
229 + .where((tx) => tx is MoneroTransactionInfo
230 + ? tx.accountIndex == _account.id
231 + : false)
232 + .toList();
233 }
234
235 this._transactions = sortedTransactions
lib/src/stores/balance/balance_store.dart
+19 -5
@@ -1,4 +1,5 @@
1 import 'dart:async';
2 +import 'package:cake_wallet/bitcoin/bitcoin_balance.dart';
3 import 'package:mobx/mobx.dart';
4 import 'package:flutter/foundation.dart';
5 import 'package:cake_wallet/src/domain/common/wallet.dart';
@@ -87,17 +88,30 @@ abstract class BalanceStoreBase with Store {
88 }
89
90 Future _onBalanceChange(Balance balance) async {
90 - final _balance = balance as MoneroBalance;
91 + if (balance is MoneroBalance) {
92 + await _onMoneroBalanceChange(balance);
93 + }
94 +
95 + if (balance is BitcoinBalance) {
96 + await _onBitcoinBalanceChange(balance);
97 + }
98 + }
99
92 - if (this.fullBalance != _balance.fullBalance) {
93 - this.fullBalance = _balance.fullBalance;
100 + Future _onMoneroBalanceChange(MoneroBalance balance) async {
101 + if (this.fullBalance != balance.fullBalance) {
102 + this.fullBalance = balance.fullBalance;
103 }
104
96 - if (this.unlockedBalance != _balance.unlockedBalance) {
97 - this.unlockedBalance = _balance.unlockedBalance;
105 + if (this.unlockedBalance != balance.unlockedBalance) {
106 + this.unlockedBalance = balance.unlockedBalance;
107 }
108 }
109
110 + Future _onBitcoinBalanceChange(BitcoinBalance balance) async {
111 + fullBalance = balance.totalFormatted;
112 + unlockedBalance = balance.totalFormatted;
113 + }
114 +
115 Future _onWalletChanged(Wallet wallet) async {
116 if (_onBalanceChangeSubscription != null) {
117 await _onBalanceChangeSubscription.cancel();
lib/src/stores/wallet/wallet_keys_store.dart
+12 -4
@@ -1,3 +1,4 @@
1 +import 'package:cake_wallet/src/domain/common/wallet_type.dart';
2 import 'package:flutter/foundation.dart';
3 import 'package:mobx/mobx.dart';
4 import 'package:cake_wallet/src/domain/services/wallet_service.dart';
@@ -15,10 +16,17 @@ abstract class WalletKeysStoreBase with Store {
16
17 if (walletService.currentWallet != null) {
18 walletService.getKeys().then((keys) {
18 - publicViewKey = keys['publicViewKey'];
19 - privateViewKey = keys['privateViewKey'];
20 - publicSpendKey = keys['publicSpendKey'];
21 - privateSpendKey = keys['privateSpendKey'];
19 + if (walletService.getType() == WalletType.monero) {
20 + publicViewKey = keys['publicViewKey'];
21 + privateViewKey = keys['privateViewKey'];
22 + publicSpendKey = keys['publicSpendKey'];
23 + privateSpendKey = keys['privateSpendKey'];
24 + }
25 +
26 + if (walletService.getType() == WalletType.bitcoin) {
27 + publicViewKey = keys['publicKey'];
28 + privateSpendKey = keys['privateKey'];
29 + }
30 });
31 }
32 }
lib/src/stores/wallet/wallet_store.dart
+7
@@ -1,5 +1,6 @@
1 import 'dart:async';
2 import 'package:cake_wallet/src/domain/common/node.dart';
3 +import 'package:cake_wallet/src/domain/common/wallet_type.dart';
4 import 'package:mobx/mobx.dart';
5 import 'package:cake_wallet/src/domain/common/wallet.dart';
6 import 'package:cake_wallet/src/domain/monero/account.dart';
@@ -119,6 +120,7 @@ abstract class WalletStoreBase with Store {
120 return;
121 }
122
123 + address = await wallet.getAddress();
124 wallet.onNameChange.listen((name) => this.name = name);
125 wallet.onAddressChange.listen((address) => this.address = address);
126
@@ -160,4 +162,9 @@ abstract class WalletStoreBase with Store {
162 }
163
164 Future<bool> isConnected() async => await _walletService.isConnected();
165 +
166 + WalletType getType() => _walletService.getType();
167 +
168 + String get getAddress =>
169 + getType() == WalletType.monero ? subaddress.address : address;
170 }
pubspec.lock
+42
@@ -57,6 +57,34 @@ packages:
57 url: "https://pub.dartlang.org"
58 source: hosted
59 version: "1.9.3"
60 + bech32:
61 + dependency: transitive
62 + description:
63 + name: bech32
64 + url: "https://pub.dartlang.org"
65 + source: hosted
66 + version: "0.1.2"
67 + bip32:
68 + dependency: transitive
69 + description:
70 + name: bip32
71 + url: "https://pub.dartlang.org"
72 + source: hosted
73 + version: "1.0.5"
74 + bip39:
75 + dependency: transitive
76 + description:
77 + name: bip39
78 + url: "https://pub.dartlang.org"
79 + source: hosted
80 + version: "1.0.3"
81 + bitcoin_flutter:
82 + dependency: "direct main"
83 + description:
84 + name: bitcoin_flutter
85 + url: "https://pub.dartlang.org"
86 + source: hosted
87 + version: "2.0.0"
88 boolean_selector:
89 dependency: transitive
90 description:
@@ -64,6 +92,13 @@ packages:
92 url: "https://pub.dartlang.org"
93 source: hosted
94 version: "1.0.5"
95 + bs58check:
96 + dependency: transitive
97 + description:
98 + name: bs58check
99 + url: "https://pub.dartlang.org"
100 + source: hosted
101 + version: "1.0.1"
102 build:
103 dependency: transitive
104 description:
@@ -336,6 +371,13 @@ packages:
371 url: "https://pub.dartlang.org"
372 source: hosted
373 version: "0.2.0"
374 + hex:
375 + dependency: transitive
376 + description:
377 + name: hex
378 + url: "https://pub.dartlang.org"
379 + source: hosted
380 + version: "0.1.2"
381 hive:
382 dependency: "direct main"
383 description:
pubspec.yaml
+1
@@ -56,6 +56,7 @@ dependencies:
56 encrypt: ^4.0.0
57 password: ^1.0.0
58 basic_utils: ^1.0.8
59 + bitcoin_flutter: ^2.0.0
60
61 dev_dependencies:
62 flutter_test: