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: