Reworked electrum wallet. Added Litecoin wallet.

M committed May 7, 2021 at 10:36 UTC 13309667940b7ebd7c9fd44098ae088a834e0492
71 files changed +1510 -1078
.gitignore
+2
@@ -96,3 +96,5 @@ vendor/
96 android/app/.cxx/**
97 ios/Flutter/.last_build_id
98 /lib/generated/**
99 +#**#
100 +/**/#**#
\ No newline at end of file
analysis_options.yaml
-1
@@ -25,7 +25,6 @@ linter:
25 - empty_constructor_bodies
26 - empty_statements
27 - hash_and_equals
28 - - implementation_imports
28 - invariant_booleans
29 - iterable_contains_unrelated_type
30 - library_names
assets/bitcoin_electrum_server_list.yml renamed
assets/images/litecoin_icon.png
Binary files /dev/null and b/assets/images/litecoin_icon.png differ
assets/images/litecoin_menu.png
Binary files /dev/null and b/assets/images/litecoin_menu.png differ
assets/images/monero_menu.png
Binary files a/assets/images/monero_menu.png and b/assets/images/monero_menu.png differ
assets/litecoin_electrum_server_list.yml new
+2
@@ -0,0 +1,2 @@
1 +-
2 + uri: 128.199.34.116:50002
\ No newline at end of file
cw_monero/ios/Classes/monero_api.cpp
-4
@@ -179,8 +179,6 @@ extern "C"
179 Monero::SubaddressAccount *m_account;
180 uint64_t m_last_known_wallet_height;
181 uint64_t m_cached_syncing_blockchain_height = 0;
182 - std::mutex store_mutex;
183 -
182
183 void change_current_wallet(Monero::Wallet *wallet)
184 {
@@ -451,9 +449,7 @@ extern "C"
449
450 void store(char *path)
451 {
454 - store_mutex.lock();
452 get_current_wallet()->store(std::string(path));
456 - store_mutex.unlock();
453 }
454
455 bool transaction_create(char *address, char *payment_id, char *amount,
cw_monero/lib/wallet_manager.dart
+2 -2
@@ -1,12 +1,12 @@
1 import 'dart:ffi';
2 -import 'package:cw_monero/exceptions/wallet_opening_exception.dart';
3 -import 'package:cw_monero/wallet.dart';
2 import 'package:ffi/ffi.dart';
3 import 'package:flutter/foundation.dart';
4 import 'package:cw_monero/convert_utf8_to_string.dart';
5 import 'package:cw_monero/signatures.dart';
6 import 'package:cw_monero/types.dart';
7 import 'package:cw_monero/monero_api.dart';
8 +import 'package:cw_monero/wallet.dart';
9 +import 'package:cw_monero/exceptions/wallet_opening_exception.dart';
10 import 'package:cw_monero/exceptions/wallet_creation_exception.dart';
11 import 'package:cw_monero/exceptions/wallet_restore_from_keys_exception.dart';
12 import 'package:cw_monero/exceptions/wallet_restore_from_seed_exception.dart';
cw_monero/pubspec.lock
+20 -20
@@ -7,49 +7,49 @@ packages:
7 name: async
8 url: "https://pub.dartlang.org"
9 source: hosted
10 - version: "2.4.2"
10 + version: "2.5.0"
11 boolean_selector:
12 dependency: transitive
13 description:
14 name: boolean_selector
15 url: "https://pub.dartlang.org"
16 source: hosted
17 - version: "2.0.0"
17 + version: "2.1.0"
18 characters:
19 dependency: transitive
20 description:
21 name: characters
22 url: "https://pub.dartlang.org"
23 source: hosted
24 - version: "1.0.0"
24 + version: "1.1.0"
25 charcode:
26 dependency: transitive
27 description:
28 name: charcode
29 url: "https://pub.dartlang.org"
30 source: hosted
31 - version: "1.1.3"
31 + version: "1.2.0"
32 clock:
33 dependency: transitive
34 description:
35 name: clock
36 url: "https://pub.dartlang.org"
37 source: hosted
38 - version: "1.0.1"
38 + version: "1.1.0"
39 collection:
40 dependency: transitive
41 description:
42 name: collection
43 url: "https://pub.dartlang.org"
44 source: hosted
45 - version: "1.14.13"
45 + version: "1.15.0"
46 fake_async:
47 dependency: transitive
48 description:
49 name: fake_async
50 url: "https://pub.dartlang.org"
51 source: hosted
52 - version: "1.1.0"
52 + version: "1.2.0"
53 ffi:
54 dependency: "direct main"
55 description:
@@ -73,21 +73,21 @@ packages:
73 name: matcher
74 url: "https://pub.dartlang.org"
75 source: hosted
76 - version: "0.12.8"
76 + version: "0.12.10"
77 meta:
78 dependency: transitive
79 description:
80 name: meta
81 url: "https://pub.dartlang.org"
82 source: hosted
83 - version: "1.1.8"
83 + version: "1.3.0"
84 path:
85 dependency: transitive
86 description:
87 name: path
88 url: "https://pub.dartlang.org"
89 source: hosted
90 - version: "1.7.0"
90 + version: "1.8.0"
91 path_provider:
92 dependency: "direct main"
93 description:
@@ -113,56 +113,56 @@ packages:
113 name: source_span
114 url: "https://pub.dartlang.org"
115 source: hosted
116 - version: "1.7.0"
116 + version: "1.8.0"
117 stack_trace:
118 dependency: transitive
119 description:
120 name: stack_trace
121 url: "https://pub.dartlang.org"
122 source: hosted
123 - version: "1.9.5"
123 + version: "1.10.0"
124 stream_channel:
125 dependency: transitive
126 description:
127 name: stream_channel
128 url: "https://pub.dartlang.org"
129 source: hosted
130 - version: "2.0.0"
130 + version: "2.1.0"
131 string_scanner:
132 dependency: transitive
133 description:
134 name: string_scanner
135 url: "https://pub.dartlang.org"
136 source: hosted
137 - version: "1.0.5"
137 + version: "1.1.0"
138 term_glyph:
139 dependency: transitive
140 description:
141 name: term_glyph
142 url: "https://pub.dartlang.org"
143 source: hosted
144 - version: "1.1.0"
144 + version: "1.2.0"
145 test_api:
146 dependency: transitive
147 description:
148 name: test_api
149 url: "https://pub.dartlang.org"
150 source: hosted
151 - version: "0.2.17"
151 + version: "0.2.19"
152 typed_data:
153 dependency: transitive
154 description:
155 name: typed_data
156 url: "https://pub.dartlang.org"
157 source: hosted
158 - version: "1.2.0"
158 + version: "1.3.0"
159 vector_math:
160 dependency: transitive
161 description:
162 name: vector_math
163 url: "https://pub.dartlang.org"
164 source: hosted
165 - version: "2.0.8"
165 + version: "2.1.0"
166 sdks:
167 - dart: ">=2.9.0-14.0.dev <3.0.0"
168 - flutter: ">=0.1.4 <2.0.0"
167 + dart: ">=2.12.0-0.0 <3.0.0"
168 + flutter: ">=0.1.4"
lib/bitcoin/address_to_output_script.dart
+10 -6
@@ -1,25 +1,29 @@
1 import 'dart:typed_data';
2 +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
3 import 'package:bs58check/bs58check.dart' as bs58check;
4 import 'package:bitcoin_flutter/src/utils/constants/op.dart';
5 import 'package:bitcoin_flutter/src/utils/script.dart' as bscript;
6 import 'package:bitcoin_flutter/src/address.dart';
7
7 -
8 Uint8List p2shAddressToOutputScript(String address) {
9 final decodeBase58 = bs58check.decode(address);
10 final hash = decodeBase58.sublist(1);
11 return bscript.compile(<dynamic>[OPS['OP_HASH160'], hash, OPS['OP_EQUAL']]);
12 }
13
14 -Uint8List addressToOutputScript(String address) {
14 +Uint8List addressToOutputScript(
15 + String address, bitcoin.NetworkType networkType) {
16 try {
17 // FIXME: improve validation for p2sh addresses
17 - if (address.startsWith('3')) {
18 + // 3 for bitcoin
19 + // m for litecoin
20 + if (address.startsWith('3') || address.toLowerCase().startsWith('m')) {
21 return p2shAddressToOutputScript(address);
22 }
23
21 - return Address.addressToOutputScript(address);
22 - } catch (_) {
24 + return Address.addressToOutputScript(address, networkType);
25 + } catch (err) {
26 + print(err);
27 return Uint8List(0);
28 }
25 -}
\ No newline at end of file
29 +}
lib/bitcoin/bitcoin_address_record.dart
+7 -4
@@ -1,14 +1,14 @@
1 import 'dart:convert';
2 -import 'package:quiver/core.dart';
2
3 class BitcoinAddressRecord {
5 - BitcoinAddressRecord(this.address, {this.index});
4 + BitcoinAddressRecord(this.address, {this.index, bool isHidden})
5 + : _isHidden = isHidden;
6
7 factory BitcoinAddressRecord.fromJSON(String jsonSource) {
8 final decoded = json.decode(jsonSource) as Map;
9
10 return BitcoinAddressRecord(decoded['address'] as String,
11 - index: decoded['index'] as int);
11 + index: decoded['index'] as int, isHidden: decoded['isHidden'] as bool);
12 }
13
14 @override
@@ -16,10 +16,13 @@ class BitcoinAddressRecord {
16 o is BitcoinAddressRecord && address == o.address;
17
18 final String address;
19 + bool get isHidden => _isHidden ?? false;
20 int index;
21 + final bool _isHidden;
22
23 @override
24 int get hashCode => address.hashCode;
25
24 - String toJSON() => json.encode({'address': address, 'index': index});
26 + String toJSON() =>
27 + json.encode({'address': address, 'index': index, 'isHidden': isHidden});
28 }
lib/bitcoin/bitcoin_transaction_history.dart deleted
-192
@@ -1,192 +0,0 @@
1 -import 'dart:convert';
2 -import 'package:flutter/foundation.dart';
3 -import 'package:mobx/mobx.dart';
4 -import 'package:cake_wallet/core/transaction_history.dart';
5 -import 'package:cake_wallet/bitcoin/file.dart';
6 -import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
7 -import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart';
8 -import 'package:cake_wallet/bitcoin/electrum.dart';
9 -
10 -part 'bitcoin_transaction_history.g.dart';
11 -
12 -const _transactionsHistoryFileName = 'transactions.json';
13 -
14 -class BitcoinTransactionHistory = BitcoinTransactionHistoryBase
15 - with _$BitcoinTransactionHistory;
16 -
17 -abstract class BitcoinTransactionHistoryBase
18 - extends TransactionHistoryBase<BitcoinTransactionInfo> with Store {
19 - BitcoinTransactionHistoryBase(
20 - {this.eclient, String dirPath, @required String password})
21 - : path = '$dirPath/$_transactionsHistoryFileName',
22 - _password = password,
23 - _height = 0,
24 - _isUpdating = false {
25 - transactions = ObservableMap<String, BitcoinTransactionInfo>();
26 - }
27 -
28 - BitcoinWalletBase wallet;
29 - final ElectrumClient eclient;
30 - final String path;
31 - final String _password;
32 - int _height;
33 - bool _isUpdating;
34 -
35 - Future<void> init() async {
36 - await _load();
37 - }
38 -
39 - @override
40 - Future update() async {
41 - if (_isUpdating) {
42 - return;
43 - }
44 -
45 - try {
46 - _isUpdating = true;
47 - final txs = await fetchTransactions();
48 - await add(txs);
49 - _isUpdating = false;
50 - } catch (_) {
51 - _isUpdating = false;
52 - rethrow;
53 - }
54 - }
55 -
56 - @override
57 - Future<Map<String, BitcoinTransactionInfo>> fetchTransactions() async {
58 - final histories =
59 - wallet.scriptHashes.map((scriptHash) => eclient.getHistory(scriptHash));
60 - final _historiesWithDetails = await Future.wait(histories)
61 - .then((histories) => histories.expand((i) => i).toList())
62 - .then((histories) => histories.map((tx) => fetchTransactionInfo(
63 - hash: tx['tx_hash'] as String, height: tx['height'] as int)));
64 - final historiesWithDetails = await Future.wait(_historiesWithDetails);
65 -
66 - return historiesWithDetails.fold<Map<String, BitcoinTransactionInfo>>(
67 - <String, BitcoinTransactionInfo>{}, (acc, tx) {
68 - acc[tx.id] = acc[tx.id]?.updated(tx) ?? tx;
69 - return acc;
70 - });
71 - }
72 -
73 - Future<BitcoinTransactionInfo> fetchTransactionInfo(
74 - {@required String hash, @required int height}) async {
75 - final tx = await eclient.getTransactionExpanded(hash: hash);
76 - return BitcoinTransactionInfo.fromElectrumVerbose(tx,
77 - height: height, addresses: wallet.addresses);
78 - }
79 -
80 - Future<void> add(Map<String, BitcoinTransactionInfo> transactionsList) async {
81 - transactionsList.entries.forEach((entry) {
82 - _updateOrInsert(entry.value);
83 -
84 - if (entry.value.height > _height) {
85 - _height = entry.value.height;
86 - }
87 - });
88 -
89 - await save();
90 - }
91 -
92 - Future<void> addOne(BitcoinTransactionInfo tx) async {
93 - _updateOrInsert(tx);
94 -
95 - if (tx.height > _height) {
96 - _height = tx.height;
97 - }
98 -
99 - await save();
100 - }
101 -
102 - BitcoinTransactionInfo get(String id) => transactions[id];
103 -
104 - Future<void> save() async {
105 - try {
106 - final data = json.encode({'height': _height, 'transactions': transactions});
107 - await writeData(path: path, password: _password, data: data);
108 - } catch(e) {
109 - print('Error while save bitcoin transaction history: ${e.toString()}');
110 - }
111 - }
112 -
113 - @override
114 - void updateAsync({void Function() onFinished}) {
115 - fetchTransactionsAsync((transaction) => _updateOrInsert(transaction),
116 - onFinished: onFinished);
117 - }
118 -
119 - @override
120 - void fetchTransactionsAsync(
121 - void Function(BitcoinTransactionInfo transaction) onTransactionLoaded,
122 - {void Function() onFinished}) async {
123 - final histories = await Future.wait(wallet.scriptHashes
124 - .map((scriptHash) async => await eclient.getHistory(scriptHash)));
125 - final transactionsCount =
126 - histories.fold<int>(0, (acc, m) => acc + m.length);
127 - var counter = 0;
128 -
129 - final batches = histories.map((metaList) =>
130 - _fetchBatchOfTransactions(metaList, onTransactionLoaded: (transaction) {
131 - onTransactionLoaded(transaction);
132 - counter += 1;
133 -
134 - if (counter == transactionsCount) {
135 - onFinished?.call();
136 - }
137 - }));
138 -
139 - await Future.wait(batches);
140 - }
141 -
142 - Future<void> _fetchBatchOfTransactions(
143 - Iterable<Map<String, dynamic>> metaList,
144 - {void Function(BitcoinTransactionInfo tranasaction)
145 - onTransactionLoaded}) async =>
146 - metaList.forEach((txMeta) => fetchTransactionInfo(
147 - hash: txMeta['tx_hash'] as String,
148 - height: txMeta['height'] as int)
149 - .then((transaction) => onTransactionLoaded(transaction)));
150 -
151 - Future<Map<String, Object>> _read() async {
152 - final content = await read(path: path, password: _password);
153 - return json.decode(content) as Map<String, Object>;
154 - }
155 -
156 - Future<void> _load() async {
157 - try {
158 - final content = await _read();
159 - final txs = content['transactions'] as Map<String, Object> ?? {};
160 -
161 - txs.entries.forEach((entry) {
162 - final val = entry.value;
163 -
164 - if (val is Map<String, Object>) {
165 - final tx = BitcoinTransactionInfo.fromJson(val);
166 - _updateOrInsert(tx);
167 - }
168 - });
169 -
170 - _height = content['height'] as int;
171 - } catch (e) {
172 - print(e);
173 - }
174 - }
175 -
176 - void _updateOrInsert(BitcoinTransactionInfo transaction) {
177 - if (transaction.id == null) {
178 - return;
179 - }
180 -
181 - if (transactions[transaction.id] == null) {
182 - transactions[transaction.id] = transaction;
183 - } else {
184 - final originalTx = transactions[transaction.id];
185 - originalTx.confirmations = transaction.confirmations;
186 - originalTx.amount = transaction.amount;
187 - originalTx.height = transaction.height;
188 - originalTx.date ??= transaction.date;
189 - originalTx.isPending = transaction.isPending;
190 - }
191 - }
192 -}
lib/bitcoin/bitcoin_unspent.dart
+2 -1
@@ -13,5 +13,6 @@ class BitcoinUnspent {
13 final int value;
14 final int vout;
15
16 - bool get isP2wpkh => address.address.startsWith('bc1');
16 + bool get isP2wpkh =>
17 + address.address.startsWith('bc') || address.address.startsWith('ltc');
18 }
lib/bitcoin/bitcoin_wallet.dart
+31 -452
@@ -1,472 +1,51 @@
1 -import 'dart:async';
2 -import 'dart:convert';
3 -import 'package:cake_wallet/bitcoin/address_to_output_script.dart';
4 -import 'package:cake_wallet/bitcoin/bitcoin_mnemonic.dart';
5 -import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart';
6 -import 'package:cake_wallet/entities/transaction_priority.dart';
1 import 'package:mobx/mobx.dart';
2 import 'package:flutter/foundation.dart';
9 -import 'package:rxdart/rxdart.dart';
3 import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
11 -import 'package:cake_wallet/bitcoin/bitcoin_transaction_credentials.dart';
12 -import 'package:cake_wallet/bitcoin/bitcoin_transaction_no_inputs_exception.dart';
13 -import 'package:cake_wallet/bitcoin/bitcoin_transaction_wrong_balance_exception.dart';
14 -import 'package:cake_wallet/bitcoin/bitcoin_unspent.dart';
15 -import 'package:cake_wallet/bitcoin/bitcoin_wallet_keys.dart';
16 -import 'package:cake_wallet/bitcoin/electrum.dart';
17 -import 'package:cake_wallet/bitcoin/pending_bitcoin_transaction.dart';
18 -import 'package:cake_wallet/bitcoin/script_hash.dart';
4 import 'package:cake_wallet/bitcoin/utils.dart';
20 -import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
21 -import 'package:cake_wallet/entities/sync_status.dart';
5 +import 'package:cake_wallet/bitcoin/electrum_wallet_snapshot.dart';
6 +import 'package:cake_wallet/bitcoin/electrum_wallet.dart';
7 import 'package:cake_wallet/entities/wallet_info.dart';
23 -import 'package:cake_wallet/bitcoin/bitcoin_transaction_history.dart';
8 import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart';
25 -import 'package:cake_wallet/bitcoin/file.dart';
26 -import 'package:cake_wallet/bitcoin/bitcoin_balance.dart';
27 -import 'package:cake_wallet/entities/node.dart';
28 -import 'package:cake_wallet/core/wallet_base.dart';
9 +import 'package:cake_wallet/bitcoin/electrum_balance.dart';
10
11 part 'bitcoin_wallet.g.dart';
12
13 class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet;
14
34 -abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
35 - BitcoinWalletBase._internal(
36 - {@required this.eclient,
37 - @required this.path,
38 - @required String password,
39 - @required WalletInfo walletInfo,
40 - @required List<BitcoinAddressRecord> initialAddresses,
41 - int accountIndex = 0,
42 - this.transactionHistory,
43 - this.mnemonic,
44 - BitcoinBalance initialBalance})
45 - : balance =
46 - initialBalance ?? BitcoinBalance(confirmed: 0, unconfirmed: 0),
47 - hd = bitcoin.HDWallet.fromSeed(mnemonicToSeedBytes(mnemonic),
48 - network: bitcoin.bitcoin)
49 - .derivePath("m/0'/0"),
50 - addresses = initialAddresses != null
51 - ? ObservableList<BitcoinAddressRecord>.of(initialAddresses.toSet())
52 - : ObservableList<BitcoinAddressRecord>(),
53 - syncStatus = NotConnectedSyncStatus(),
54 - _password = password,
55 - _accountIndex = accountIndex,
56 - _feeRates = <int>[],
57 - super(walletInfo) {
58 - _unspent = [];
59 - _scripthashesUpdateSubject = {};
60 - }
61 -
62 - static BitcoinWallet fromJSON(
63 - {@required String password,
64 - @required String name,
65 - @required String dirPath,
66 - @required WalletInfo walletInfo,
67 - String jsonSource}) {
68 - final data = json.decode(jsonSource) as Map;
69 - final mnemonic = data['mnemonic'] as String;
70 - final accountIndex =
71 - (data['account_index'] == 'null' || data['account_index'] == null)
72 - ? 0
73 - : int.parse(data['account_index'] as String);
74 - final _addresses = data['addresses'] as List ?? <Object>[];
75 - final addresses = <BitcoinAddressRecord>[];
76 - final balance = BitcoinBalance.fromJSON(data['balance'] as String) ??
77 - BitcoinBalance(confirmed: 0, unconfirmed: 0);
78 -
79 - _addresses.forEach((Object el) {
80 - if (el is String) {
81 - addresses.add(BitcoinAddressRecord.fromJSON(el));
82 - }
83 - });
84 -
85 - return BitcoinWalletBase.build(
86 - dirPath: dirPath,
87 - mnemonic: mnemonic,
88 - password: password,
89 - name: name,
90 - accountIndex: accountIndex,
91 - initialAddresses: addresses,
92 - initialBalance: balance,
93 - walletInfo: walletInfo);
94 - }
95 -
96 - static BitcoinWallet build(
15 +abstract class BitcoinWalletBase extends ElectrumWallet with Store {
16 + BitcoinWalletBase(
17 {@required String mnemonic,
18 @required String password,
99 - @required String name,
100 - @required String dirPath,
19 @required WalletInfo walletInfo,
20 List<BitcoinAddressRecord> initialAddresses,
103 - BitcoinBalance initialBalance,
104 - int accountIndex = 0}) {
105 - final walletPath = '$dirPath/$name';
106 - final eclient = ElectrumClient();
107 - final history = BitcoinTransactionHistory(
108 - eclient: eclient, dirPath: dirPath, password: password);
109 -
110 - return BitcoinWallet._internal(
111 - eclient: eclient,
112 - path: walletPath,
113 - mnemonic: mnemonic,
21 + ElectrumBalance initialBalance,
22 + int accountIndex = 0})
23 + : super(
24 + mnemonic: mnemonic,
25 + password: password,
26 + walletInfo: walletInfo,
27 + networkType: bitcoin.bitcoin,
28 + initialAddresses: initialAddresses,
29 + initialBalance: initialBalance,
30 + accountIndex: accountIndex);
31 +
32 + static Future<BitcoinWallet> open({
33 + @required String name,
34 + @required WalletInfo walletInfo,
35 + @required String password,
36 + }) async {
37 + final snp = ElectrumWallletSnapshot(name, walletInfo.type, password);
38 + await snp.load();
39 + return BitcoinWallet(
40 + mnemonic: snp.mnemonic,
41 password: password,
115 - accountIndex: accountIndex,
116 - initialAddresses: initialAddresses,
117 - initialBalance: initialBalance,
118 - transactionHistory: history,
119 - walletInfo: walletInfo);
120 - }
121 -
122 - static int estimatedTransactionSize(int inputsCount, int outputsCounts) =>
123 - inputsCount * 146 + outputsCounts * 33 + 8;
124 -
125 - @override
126 - final BitcoinTransactionHistory transactionHistory;
127 - final String path;
128 - final bitcoin.HDWallet hd;
129 - final ElectrumClient eclient;
130 - final String mnemonic;
131 -
132 - List<BitcoinUnspent> _unspent;
133 -
134 - @override
135 - @observable
136 - String address;
137 -
138 - @override
139 - @observable
140 - BitcoinBalance balance;
141 -
142 - @override
143 - @observable
144 - SyncStatus syncStatus;
145 -
146 - ObservableList<BitcoinAddressRecord> addresses;
147 -
148 - List<String> get scriptHashes =>
149 - addresses.map((addr) => scriptHash(addr.address)).toList();
150 -
151 - String get xpub => hd.base58;
152 -
153 - @override
154 - String get seed => mnemonic;
155 -
156 - @override
157 - BitcoinWalletKeys get keys => BitcoinWalletKeys(
158 - wif: hd.wif, privateKey: hd.privKey, publicKey: hd.pubKey);
159 -
160 - final String _password;
161 - List<int> _feeRates;
162 - int _accountIndex;
163 - Map<String, BehaviorSubject<Object>> _scripthashesUpdateSubject;
164 -
165 - Future<void> init() async {
166 - if (addresses.isEmpty || addresses.length < 33) {
167 - final addressesCount = 33 - addresses.length;
168 - await generateNewAddresses(addressesCount, startIndex: addresses.length);
169 - }
170 -
171 - address = addresses[_accountIndex].address;
172 - transactionHistory.wallet = this;
173 - await transactionHistory.init();
174 - }
175 -
176 - @action
177 - Future<void> nextAddress() async {
178 - _accountIndex += 1;
179 -
180 - if (_accountIndex >= addresses.length) {
181 - _accountIndex = 0;
182 - }
183 -
184 - address = addresses[_accountIndex].address;
185 -
186 - await save();
187 - }
188 -
189 - Future<BitcoinAddressRecord> generateNewAddress() async {
190 - _accountIndex += 1;
191 - final address = BitcoinAddressRecord(_getAddress(index: _accountIndex),
192 - index: _accountIndex);
193 - addresses.add(address);
194 -
195 - await save();
196 -
197 - return address;
198 - }
199 -
200 - Future<List<BitcoinAddressRecord>> generateNewAddresses(int count,
201 - {int startIndex = 0}) async {
202 - final list = <BitcoinAddressRecord>[];
203 -
204 - for (var i = startIndex; i < count + startIndex; i++) {
205 - final address = BitcoinAddressRecord(_getAddress(index: i), index: i);
206 - list.add(address);
207 - }
208 -
209 - addresses.addAll(list);
210 - await save();
211 -
212 - return list;
213 - }
214 -
215 - Future<void> updateAddress(String address) async {
216 - for (final addr in addresses) {
217 - if (addr.address == address) {
218 - await save();
219 - break;
220 - }
221 - }
222 - }
223 -
224 - @action
225 - @override
226 - Future<void> startSync() async {
227 - try {
228 - syncStatus = StartingSyncStatus();
229 - transactionHistory.updateAsync(onFinished: () {
230 - print('transactionHistory update finished!');
231 - transactionHistory.save();
232 - });
233 - _subscribeForUpdates();
234 - await _updateBalance();
235 - await _updateUnspent();
236 - _feeRates = await eclient.feeRates();
237 -
238 - Timer.periodic(const Duration(minutes: 1),
239 - (timer) async => _feeRates = await eclient.feeRates());
240 -
241 - syncStatus = SyncedSyncStatus();
242 - } catch (e) {
243 - print(e.toString());
244 - syncStatus = FailedSyncStatus();
245 - }
42 + walletInfo: walletInfo,
43 + initialAddresses: snp.addresses,
44 + initialBalance: snp.balance,
45 + accountIndex: snp.accountIndex);
46 }
47
248 - @action
48 @override
250 - Future<void> connectToNode({@required Node node}) async {
251 - try {
252 - syncStatus = ConnectingSyncStatus();
253 - await eclient.connectToUri(node.uri);
254 - eclient.onConnectionStatusChange = (bool isConnected) {
255 - if (!isConnected) {
256 - syncStatus = LostConnectionSyncStatus();
257 - }
258 - };
259 - syncStatus = ConnectedSyncStatus();
260 - } catch (e) {
261 - print(e.toString());
262 - syncStatus = FailedSyncStatus();
263 - }
264 - }
265 -
266 - @override
267 - Future<PendingBitcoinTransaction> createTransaction(
268 - Object credentials) async {
269 - const minAmount = 546;
270 - final transactionCredentials = credentials as BitcoinTransactionCredentials;
271 - final inputs = <BitcoinUnspent>[];
272 - final allAmountFee =
273 - calculateEstimatedFee(transactionCredentials.priority, null);
274 - final allAmount = balance.confirmed - allAmountFee;
275 - var fee = 0;
276 - final credentialsAmount = transactionCredentials.amount != null
277 - ? stringDoubleToBitcoinAmount(transactionCredentials.amount)
278 - : 0;
279 - final amount = transactionCredentials.amount == null ||
280 - allAmount - credentialsAmount < minAmount
281 - ? allAmount
282 - : credentialsAmount;
283 - final txb = bitcoin.TransactionBuilder(network: bitcoin.bitcoin);
284 - final changeAddress = address;
285 - var leftAmount = amount;
286 - var totalInputAmount = 0;
287 -
288 - if (_unspent.isEmpty) {
289 - await _updateUnspent();
290 - }
291 -
292 - for (final utx in _unspent) {
293 - leftAmount = leftAmount - utx.value;
294 - totalInputAmount += utx.value;
295 - inputs.add(utx);
296 -
297 - if (leftAmount <= 0) {
298 - break;
299 - }
300 - }
301 -
302 - if (inputs.isEmpty) {
303 - throw BitcoinTransactionNoInputsException();
304 - }
305 -
306 - final totalAmount = amount + fee;
307 - fee = transactionCredentials.amount != null
308 - ? feeAmountForPriority(transactionCredentials.priority, inputs.length,
309 - amount == allAmount ? 1 : 2)
310 - : allAmountFee;
311 -
312 - if (totalAmount > balance.confirmed) {
313 - throw BitcoinTransactionWrongBalanceException();
314 - }
315 -
316 - if (amount <= 0 || totalInputAmount < amount) {
317 - throw BitcoinTransactionWrongBalanceException();
318 - }
319 -
320 - txb.setVersion(1);
321 -
322 - inputs.forEach((input) {
323 - if (input.isP2wpkh) {
324 - final p2wpkh = bitcoin
325 - .P2WPKH(
326 - data: generatePaymentData(hd: hd, index: input.address.index),
327 - network: bitcoin.bitcoin)
328 - .data;
329 -
330 - txb.addInput(input.hash, input.vout, null, p2wpkh.output);
331 - } else {
332 - txb.addInput(input.hash, input.vout);
333 - }
334 - });
335 -
336 - txb.addOutput(
337 - addressToOutputScript(transactionCredentials.address), amount);
338 -
339 - final estimatedSize = estimatedTransactionSize(inputs.length, 2);
340 - final feeAmount = feeRate(transactionCredentials.priority) * estimatedSize;
341 - final changeValue = totalInputAmount - amount - feeAmount;
342 -
343 - if (changeValue > minAmount) {
344 - txb.addOutput(changeAddress, changeValue);
345 - }
346 -
347 - for (var i = 0; i < inputs.length; i++) {
348 - final input = inputs[i];
349 - final keyPair = generateKeyPair(hd: hd, index: input.address.index);
350 - final witnessValue = input.isP2wpkh ? input.value : null;
351 -
352 - txb.sign(vin: i, keyPair: keyPair, witnessValue: witnessValue);
353 - }
354 -
355 - return PendingBitcoinTransaction(txb.build(),
356 - eclient: eclient, amount: amount, fee: fee)
357 - ..addListener((transaction) async {
358 - transactionHistory.addOne(transaction);
359 - await _updateBalance();
360 - });
361 - }
362 -
363 - String toJSON() => json.encode({
364 - 'mnemonic': mnemonic,
365 - 'account_index': _accountIndex.toString(),
366 - 'addresses': addresses.map((addr) => addr.toJSON()).toList(),
367 - 'balance': balance?.toJSON()
368 - });
369 -
370 - int feeRate(TransactionPriority priority) {
371 - if (priority is BitcoinTransactionPriority) {
372 - return _feeRates[priority.raw];
373 - }
374 -
375 - return 0;
376 - }
377 -
378 - int feeAmountForPriority(BitcoinTransactionPriority priority, int inputsCount,
379 - int outputsCount) =>
380 - feeRate(priority) * estimatedTransactionSize(inputsCount, outputsCount);
381 -
382 - @override
383 - int calculateEstimatedFee(TransactionPriority priority, int amount) {
384 - if (priority is BitcoinTransactionPriority) {
385 - int inputsCount = 0;
386 -
387 - if (amount != null) {
388 - int totalValue = 0;
389 -
390 - for (final input in _unspent) {
391 - if (totalValue >= amount) {
392 - break;
393 - }
394 -
395 - totalValue += input.value;
396 - inputsCount += 1;
397 - }
398 - } else {
399 - inputsCount = _unspent.length;
400 - }
401 - // If send all, then we have no change value
402 - return feeAmountForPriority(
403 - priority, inputsCount, amount != null ? 2 : 1);
404 - }
405 -
406 - return 0;
407 - }
408 -
409 - @override
410 - Future<void> save() async {
411 - await write(path: path, password: _password, data: toJSON());
412 - await transactionHistory.save();
413 - }
414 -
415 - bitcoin.ECPair keyPairFor({@required int index}) =>
416 - generateKeyPair(hd: hd, index: index);
417 -
418 - @override
419 - Future<void> rescan({int height}) async {
420 - // FIXME: Unimplemented
421 - }
422 -
423 - @override
424 - void close() async {
425 - await eclient.close();
426 - }
427 -
428 - Future<void> _updateUnspent() async {
429 - final unspent = await Future.wait(addresses.map((address) => eclient
430 - .getListUnspentWithAddress(address.address)
431 - .then((unspent) => unspent
432 - .map((unspent) => BitcoinUnspent.fromJSON(address, unspent)))));
433 - _unspent = unspent.expand((e) => e).toList();
434 - }
435 -
436 - void _subscribeForUpdates() {
437 - scriptHashes.forEach((sh) async {
438 - await _scripthashesUpdateSubject[sh]?.close();
439 - _scripthashesUpdateSubject[sh] = eclient.scripthashUpdate(sh);
440 - _scripthashesUpdateSubject[sh].listen((event) async {
441 - try {
442 - await _updateBalance();
443 - await _updateUnspent();
444 - transactionHistory.updateAsync();
445 - } catch (e) {
446 - print(e.toString());
447 - }
448 - });
449 - });
450 - }
451 -
452 - Future<BitcoinBalance> _fetchBalances() async {
453 - final balances = await Future.wait(
454 - scriptHashes.map((sHash) => eclient.getBalance(sHash)));
455 - final balance = balances.fold(
456 - BitcoinBalance(confirmed: 0, unconfirmed: 0),
457 - (BitcoinBalance acc, val) => BitcoinBalance(
458 - confirmed: (val['confirmed'] as int ?? 0) + (acc.confirmed ?? 0),
459 - unconfirmed:
460 - (val['unconfirmed'] as int ?? 0) + (acc.unconfirmed ?? 0)));
461 -
462 - return balance;
463 - }
464 -
465 - Future<void> _updateBalance() async {
466 - balance = await _fetchBalances();
467 - await save();
468 - }
469 -
470 - String _getAddress({@required int index}) =>
471 - generateAddress(hd: hd, index: index);
49 + String getAddress({@required int index, @required bitcoin.HDWallet hd}) =>
50 + generateP2WPKHAddress(hd: hd, index: index, networkType: networkType);
51 }
lib/bitcoin/bitcoin_wallet_service.dart
+11 -31
@@ -1,7 +1,6 @@
1 import 'dart:io';
2 import 'package:cake_wallet/bitcoin/bitcoin_mnemonic.dart';
3 import 'package:cake_wallet/bitcoin/bitcoin_mnemonic_is_incorrect_exception.dart';
4 -import 'package:cake_wallet/bitcoin/file.dart';
4 import 'package:cake_wallet/bitcoin/bitcoin_wallet_creation_credentials.dart';
5 import 'package:cake_wallet/core/wallet_base.dart';
6 import 'package:cake_wallet/core/wallet_service.dart';
@@ -19,44 +18,32 @@ class BitcoinWalletService extends WalletService<
18
19 final Box<WalletInfo> walletInfoSource;
20
21 + @override
22 + WalletType getType() => WalletType.bitcoin;
23 +
24 @override
25 Future<BitcoinWallet> create(BitcoinNewWalletCredentials credentials) async {
24 - final dirPath = await pathForWalletDir(
25 - type: WalletType.bitcoin, name: credentials.name);
26 - final wallet = BitcoinWalletBase.build(
27 - dirPath: dirPath,
26 + final wallet = BitcoinWallet(
27 mnemonic: generateMnemonic(),
28 password: credentials.password,
30 - name: credentials.name,
29 walletInfo: credentials.walletInfo);
30 await wallet.save();
31 await wallet.init();
34 -
32 return wallet;
33 }
34
35 @override
36 Future<bool> isWalletExit(String name) async =>
40 - File(await pathForWallet(name: name, type: WalletType.bitcoin))
41 - .existsSync();
37 + File(await pathForWallet(name: name, type: getType())).existsSync();
38
39 @override
40 Future<BitcoinWallet> openWallet(String name, String password) async {
45 - final walletDirPath =
46 - await pathForWalletDir(name: name, type: WalletType.bitcoin);
47 - final walletPath = '$walletDirPath/$name';
48 - final walletJSONRaw = await read(path: walletPath, password: password);
41 final walletInfo = walletInfoSource.values.firstWhere(
50 - (info) => info.id == WalletBase.idFor(name, WalletType.bitcoin),
42 + (info) => info.id == WalletBase.idFor(name, getType()),
43 orElse: () => null);
52 - final wallet = BitcoinWalletBase.fromJSON(
53 - password: password,
54 - name: name,
55 - dirPath: walletDirPath,
56 - jsonSource: walletJSONRaw,
57 - walletInfo: walletInfo);
44 + final wallet = await BitcoinWalletBase.open(
45 + password: password, name: name, walletInfo: walletInfo);
46 await wallet.init();
59 -
47 return wallet;
48 }
49
@@ -67,10 +54,8 @@ class BitcoinWalletService extends WalletService<
54
55 @override
56 Future<BitcoinWallet> restoreFromKeys(
70 - BitcoinRestoreWalletFromWIFCredentials credentials) async {
71 - // TODO: implement restoreFromKeys
72 - throw UnimplementedError();
73 - }
57 + BitcoinRestoreWalletFromWIFCredentials credentials) async =>
58 + throw UnimplementedError();
59
60 @override
61 Future<BitcoinWallet> restoreFromSeed(
@@ -79,17 +64,12 @@ class BitcoinWalletService extends WalletService<
64 throw BitcoinMnemonicIsIncorrectException();
65 }
66
82 - final dirPath = await pathForWalletDir(
83 - type: WalletType.bitcoin, name: credentials.name);
84 - final wallet = BitcoinWalletBase.build(
85 - dirPath: dirPath,
86 - name: credentials.name,
67 + final wallet = BitcoinWallet(
68 password: credentials.password,
69 mnemonic: credentials.mnemonic,
70 walletInfo: credentials.walletInfo);
71 await wallet.save();
72 await wallet.init();
92 -
73 return wallet;
74 }
75 }
lib/bitcoin/electrum.dart
+25 -12
@@ -2,6 +2,7 @@ import 'dart:async';
2 import 'dart:convert';
3 import 'dart:io';
4 import 'dart:typed_data';
5 +import 'package:bitcoin_flutter/bitcoin_flutter.dart';
6 import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
7 import 'package:cake_wallet/bitcoin/script_hash.dart';
8 import 'package:flutter/foundation.dart';
@@ -173,10 +174,11 @@ class ElectrumClient {
174 });
175
176 Future<List<Map<String, dynamic>>> getListUnspentWithAddress(
176 - String address) =>
177 + String address, NetworkType networkType) =>
178 call(
178 - method: 'blockchain.scripthash.listunspent',
179 - params: [scriptHash(address)]).then((dynamic result) {
179 + method: 'blockchain.scripthash.listunspent',
180 + params: [scriptHash(address, networkType: networkType)])
181 + .then((dynamic result) {
182 if (result is List) {
183 return result.map((dynamic val) {
184 if (val is Map<String, Object>) {
@@ -259,7 +261,7 @@ class ElectrumClient {
261 if (result is String) {
262 return result;
263 }
262 - print(result);
264 +
265 return '';
266 });
267
@@ -303,14 +305,25 @@ class ElectrumClient {
305 });
306
307 Future<List<int>> feeRates() async {
306 - final topDoubleString = await estimatefee(p: 1);
307 - final middleDoubleString = await estimatefee(p: 20);
308 - final bottomDoubleString = await estimatefee(p: 150);
309 - final top = (stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000).round();
310 - final middle = (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000).round();
311 - final bottom = (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000).round();
312 -
313 - return [bottom, middle, top];
308 + try {
309 + final topDoubleString = await estimatefee(p: 1);
310 + final middleDoubleString = await estimatefee(p: 20);
311 + final bottomDoubleString = await estimatefee(p: 150);
312 + final top =
313 + (stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000)
314 + .round();
315 + final middle =
316 + (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000)
317 + .round();
318 + final bottom =
319 + (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000)
320 + .round();
321 +
322 + final res = [bottom, middle, top];
323 + return res;
324 + } catch (_) {
325 + return [];
326 + }
327 }
328
329 BehaviorSubject<Object> scripthashUpdate(String scripthash) {
lib/bitcoin/electrum_balance.dart renamed
+6 -6
@@ -1,21 +1,20 @@
1 import 'dart:convert';
2 -
2 import 'package:flutter/foundation.dart';
3 import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
4 import 'package:cake_wallet/entities/balance.dart';
5
7 -class BitcoinBalance extends Balance {
8 - const BitcoinBalance({@required this.confirmed, @required this.unconfirmed})
6 +class ElectrumBalance extends Balance {
7 + const ElectrumBalance({@required this.confirmed, @required this.unconfirmed})
8 : super(confirmed, unconfirmed);
9
11 - factory BitcoinBalance.fromJSON(String jsonSource) {
10 + factory ElectrumBalance.fromJSON(String jsonSource) {
11 if (jsonSource == null) {
12 return null;
13 }
14
15 final decoded = json.decode(jsonSource) as Map;
16
18 - return BitcoinBalance(
17 + return ElectrumBalance(
18 confirmed: decoded['confirmed'] as int ?? 0,
19 unconfirmed: decoded['unconfirmed'] as int ?? 0);
20 }
@@ -24,7 +23,8 @@ class BitcoinBalance extends Balance {
23 final int unconfirmed;
24
25 @override
27 - String get formattedAvailableBalance => bitcoinAmountToString(amount: confirmed);
26 + String get formattedAvailableBalance =>
27 + bitcoinAmountToString(amount: confirmed);
28
29 @override
30 String get formattedAdditionalBalance =>
lib/bitcoin/electrum_transaction_history.dart new
+98
@@ -0,0 +1,98 @@
1 +import 'dart:convert';
2 +import 'package:cake_wallet/entities/pathForWallet.dart';
3 +import 'package:cake_wallet/entities/wallet_info.dart';
4 +import 'package:flutter/foundation.dart';
5 +import 'package:mobx/mobx.dart';
6 +import 'package:cake_wallet/core/transaction_history.dart';
7 +import 'package:cake_wallet/bitcoin/file.dart';
8 +import 'package:cake_wallet/bitcoin/electrum_transaction_info.dart';
9 +
10 +part 'electrum_transaction_history.g.dart';
11 +
12 +const _transactionsHistoryFileName = 'transactions.json';
13 +
14 +class ElectrumTransactionHistory = ElectrumTransactionHistoryBase
15 + with _$ElectrumTransactionHistory;
16 +
17 +abstract class ElectrumTransactionHistoryBase
18 + extends TransactionHistoryBase<ElectrumTransactionInfo> with Store {
19 + ElectrumTransactionHistoryBase(
20 + {@required this.walletInfo, @required String password})
21 + : _password = password,
22 + _height = 0 {
23 + transactions = ObservableMap<String, ElectrumTransactionInfo>();
24 + }
25 +
26 + final WalletInfo walletInfo;
27 + final String _password;
28 + int _height;
29 +
30 + Future<void> init() async => await _load();
31 +
32 + @override
33 + void addOne(ElectrumTransactionInfo transaction) =>
34 + transactions[transaction.id] = transaction;
35 +
36 + @override
37 + void addMany(Map<String, ElectrumTransactionInfo> transactions) =>
38 + this.transactions.addAll(transactions);
39 +
40 + @override
41 + Future<void> save() async {
42 + try {
43 + final dirPath =
44 + await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
45 + final path = '$dirPath/$_transactionsHistoryFileName';
46 + final data =
47 + json.encode({'height': _height, 'transactions': transactions});
48 + await writeData(path: path, password: _password, data: data);
49 + } catch (e) {
50 + print('Error while save bitcoin transaction history: ${e.toString()}');
51 + }
52 + }
53 +
54 + Future<Map<String, Object>> _read() async {
55 + final dirPath =
56 + await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
57 + final path = '$dirPath/$_transactionsHistoryFileName';
58 + final content = await read(path: path, password: _password);
59 + return json.decode(content) as Map<String, Object>;
60 + }
61 +
62 + Future<void> _load() async {
63 + try {
64 + final content = await _read();
65 + final txs = content['transactions'] as Map<String, Object> ?? {};
66 +
67 + txs.entries.forEach((entry) {
68 + final val = entry.value;
69 +
70 + if (val is Map<String, Object>) {
71 + final tx = ElectrumTransactionInfo.fromJson(val, walletInfo.type);
72 + _updateOrInsert(tx);
73 + }
74 + });
75 +
76 + _height = content['height'] as int;
77 + } catch (e) {
78 + print(e);
79 + }
80 + }
81 +
82 + void _updateOrInsert(ElectrumTransactionInfo transaction) {
83 + if (transaction.id == null) {
84 + return;
85 + }
86 +
87 + if (transactions[transaction.id] == null) {
88 + transactions[transaction.id] = transaction;
89 + } else {
90 + final originalTx = transactions[transaction.id];
91 + originalTx.confirmations = transaction.confirmations;
92 + originalTx.amount = transaction.amount;
93 + originalTx.height = transaction.height;
94 + originalTx.date ??= transaction.date;
95 + originalTx.isPending = transaction.isPending;
96 + }
97 + }
98 +}
lib/bitcoin/electrum_transaction_info.dart renamed
+19 -12
@@ -6,9 +6,10 @@ import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
6 import 'package:cake_wallet/entities/transaction_direction.dart';
7 import 'package:cake_wallet/entities/transaction_info.dart';
8 import 'package:cake_wallet/entities/format_amount.dart';
9 +import 'package:cake_wallet/entities/wallet_type.dart';
10
10 -class BitcoinTransactionInfo extends TransactionInfo {
11 - BitcoinTransactionInfo(
11 +class ElectrumTransactionInfo extends TransactionInfo {
12 + ElectrumTransactionInfo(this.type,
13 {@required String id,
14 @required int height,
15 @required int amount,
@@ -27,7 +28,8 @@ class BitcoinTransactionInfo extends TransactionInfo {
28 this.confirmations = confirmations;
29 }
30
30 - factory BitcoinTransactionInfo.fromElectrumVerbose(Map<String, Object> obj,
31 + factory ElectrumTransactionInfo.fromElectrumVerbose(
32 + Map<String, Object> obj, WalletType type,
33 {@required List<BitcoinAddressRecord> addresses, @required int height}) {
34 final addressesSet = addresses.map((addr) => addr.address).toSet();
35 final id = obj['txid'] as String;
@@ -47,7 +49,8 @@ class BitcoinTransactionInfo extends TransactionInfo {
49 final out = vin['tx']['vout'][vout] as Map;
50 final outAddresses =
51 (out['scriptPubKey']['addresses'] as List<Object>)?.toSet();
50 - inputsAmount += stringDoubleToBitcoinAmount((out['value'] as double ?? 0).toString());
52 + inputsAmount +=
53 + stringDoubleToBitcoinAmount((out['value'] as double ?? 0).toString());
54
55 if (outAddresses?.intersection(addressesSet)?.isNotEmpty ?? false) {
56 direction = TransactionDirection.outgoing;
@@ -58,7 +61,8 @@ class BitcoinTransactionInfo extends TransactionInfo {
61 final outAddresses =
62 out['scriptPubKey']['addresses'] as List<Object> ?? [];
63 final ntrs = outAddresses.toSet().intersection(addressesSet);
61 - final value = stringDoubleToBitcoinAmount((out['value'] as double ?? 0.0).toString());
64 + final value = stringDoubleToBitcoinAmount(
65 + (out['value'] as double ?? 0.0).toString());
66 totalOutAmount += value;
67
68 if ((direction == TransactionDirection.incoming && ntrs.isNotEmpty) ||
@@ -69,7 +73,7 @@ class BitcoinTransactionInfo extends TransactionInfo {
73
74 final fee = inputsAmount - totalOutAmount;
75
72 - return BitcoinTransactionInfo(
76 + return ElectrumTransactionInfo(type,
77 id: id,
78 height: height,
79 isPending: false,
@@ -80,7 +84,7 @@ class BitcoinTransactionInfo extends TransactionInfo {
84 confirmations: confirmations);
85 }
86
83 - factory BitcoinTransactionInfo.fromHexAndHeader(String hex,
87 + factory ElectrumTransactionInfo.fromHexAndHeader(WalletType type, String hex,
88 {List<String> addresses, int height, int timestamp, int confirmations}) {
89 final tx = bitcoin.Transaction.fromHex(hex);
90 var exist = false;
@@ -104,7 +108,7 @@ class BitcoinTransactionInfo extends TransactionInfo {
108 ? DateTime.fromMillisecondsSinceEpoch(timestamp * 1000)
109 : DateTime.now();
110
107 - return BitcoinTransactionInfo(
111 + return ElectrumTransactionInfo(type,
112 id: tx.getId(),
113 height: height,
114 isPending: false,
@@ -115,8 +119,9 @@ class BitcoinTransactionInfo extends TransactionInfo {
119 confirmations: confirmations);
120 }
121
118 - factory BitcoinTransactionInfo.fromJson(Map<String, dynamic> data) {
119 - return BitcoinTransactionInfo(
122 + factory ElectrumTransactionInfo.fromJson(
123 + Map<String, dynamic> data, WalletType type) {
124 + return ElectrumTransactionInfo(type,
125 id: data['id'] as String,
126 height: data['height'] as int,
127 amount: data['amount'] as int,
@@ -127,6 +132,8 @@ class BitcoinTransactionInfo extends TransactionInfo {
132 confirmations: data['confirmations'] as int);
133 }
134
135 + final WalletType type;
136 +
137 String _fiatAmount;
138
139 @override
@@ -144,8 +151,8 @@ class BitcoinTransactionInfo extends TransactionInfo {
151 @override
152 void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
153
147 - BitcoinTransactionInfo updated(BitcoinTransactionInfo info) {
148 - return BitcoinTransactionInfo(
154 + ElectrumTransactionInfo updated(ElectrumTransactionInfo info) {
155 + return ElectrumTransactionInfo(info.type,
156 id: id,
157 height: info.height,
158 amount: info.amount,
lib/bitcoin/electrum_wallet.dart new
+467
@@ -0,0 +1,467 @@
1 +import 'dart:async';
2 +import 'dart:convert';
3 +import 'package:mobx/mobx.dart';
4 +import 'package:rxdart/subjects.dart';
5 +import 'package:flutter/foundation.dart';
6 +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
7 +import 'package:cake_wallet/bitcoin/electrum_transaction_info.dart';
8 +import 'package:cake_wallet/entities/pathForWallet.dart';
9 +import 'package:cake_wallet/bitcoin/address_to_output_script.dart';
10 +import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart';
11 +import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
12 +import 'package:cake_wallet/bitcoin/electrum_balance.dart';
13 +import 'package:cake_wallet/bitcoin/bitcoin_mnemonic.dart';
14 +import 'package:cake_wallet/bitcoin/bitcoin_transaction_credentials.dart';
15 +import 'package:cake_wallet/bitcoin/electrum_transaction_history.dart';
16 +import 'package:cake_wallet/bitcoin/bitcoin_transaction_no_inputs_exception.dart';
17 +import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart';
18 +import 'package:cake_wallet/bitcoin/bitcoin_transaction_wrong_balance_exception.dart';
19 +import 'package:cake_wallet/bitcoin/bitcoin_unspent.dart';
20 +import 'package:cake_wallet/bitcoin/bitcoin_wallet_keys.dart';
21 +import 'package:cake_wallet/bitcoin/file.dart';
22 +import 'package:cake_wallet/bitcoin/pending_bitcoin_transaction.dart';
23 +import 'package:cake_wallet/bitcoin/script_hash.dart';
24 +import 'package:cake_wallet/bitcoin/utils.dart';
25 +import 'package:cake_wallet/core/wallet_base.dart';
26 +import 'package:cake_wallet/entities/node.dart';
27 +import 'package:cake_wallet/entities/sync_status.dart';
28 +import 'package:cake_wallet/entities/transaction_priority.dart';
29 +import 'package:cake_wallet/entities/wallet_info.dart';
30 +import 'package:cake_wallet/bitcoin/electrum.dart';
31 +
32 +part 'electrum_wallet.g.dart';
33 +
34 +class ElectrumWallet = ElectrumWalletBase with _$ElectrumWallet;
35 +
36 +abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
37 + ElectrumTransactionHistory, ElectrumTransactionInfo> with Store {
38 + ElectrumWalletBase(
39 + {@required String password,
40 + @required WalletInfo walletInfo,
41 + @required List<BitcoinAddressRecord> initialAddresses,
42 + @required this.networkType,
43 + @required this.mnemonic,
44 + ElectrumClient electrumClient,
45 + int accountIndex = 0,
46 + ElectrumBalance initialBalance})
47 + : balance = initialBalance ??
48 + const ElectrumBalance(confirmed: 0, unconfirmed: 0),
49 + hd = bitcoin.HDWallet.fromSeed(mnemonicToSeedBytes(mnemonic),
50 + network: networkType)
51 + .derivePath("m/0'/0"),
52 + addresses = ObservableList<BitcoinAddressRecord>.of(
53 + (initialAddresses ?? []).toSet()),
54 + syncStatus = NotConnectedSyncStatus(),
55 + _password = password,
56 + _accountIndex = accountIndex,
57 + _feeRates = <int>[],
58 + _isTransactionUpdating = false,
59 + super(walletInfo) {
60 + this.electrumClient = electrumClient ?? ElectrumClient();
61 + this.walletInfo = walletInfo;
62 + transactionHistory =
63 + ElectrumTransactionHistory(walletInfo: walletInfo, password: password);
64 + _unspent = [];
65 + _scripthashesUpdateSubject = {};
66 + }
67 +
68 + static int estimatedTransactionSize(int inputsCount, int outputsCounts) =>
69 + inputsCount * 146 + outputsCounts * 33 + 8;
70 +
71 + final bitcoin.HDWallet hd;
72 + final String mnemonic;
73 +
74 + ElectrumClient electrumClient;
75 +
76 + @override
77 + @observable
78 + String address;
79 +
80 + @override
81 + @observable
82 + ElectrumBalance balance;
83 +
84 + @override
85 + @observable
86 + SyncStatus syncStatus;
87 +
88 + ObservableList<BitcoinAddressRecord> addresses;
89 +
90 + List<String> get scriptHashes => addresses
91 + .map((addr) => scriptHash(addr.address, networkType: networkType))
92 + .toList();
93 +
94 + String get xpub => hd.base58;
95 +
96 + @override
97 + String get seed => mnemonic;
98 +
99 + bitcoin.NetworkType networkType;
100 +
101 + @override
102 + BitcoinWalletKeys get keys => BitcoinWalletKeys(
103 + wif: hd.wif, privateKey: hd.privKey, publicKey: hd.pubKey);
104 +
105 + final String _password;
106 + List<BitcoinUnspent> _unspent;
107 + List<int> _feeRates;
108 + int _accountIndex;
109 + Map<String, BehaviorSubject<Object>> _scripthashesUpdateSubject;
110 + bool _isTransactionUpdating;
111 +
112 + Future<void> init() async {
113 + await generateAddresses();
114 + address = addresses[_accountIndex].address;
115 + await transactionHistory.init();
116 + }
117 +
118 + @action
119 + Future<void> nextAddress() async {
120 + _accountIndex += 1;
121 +
122 + if (_accountIndex >= addresses.length) {
123 + _accountIndex = 0;
124 + }
125 +
126 + address = addresses[_accountIndex].address;
127 +
128 + await save();
129 + }
130 +
131 + Future<void> generateAddresses() async {
132 + if (addresses.length < 33) {
133 + final addressesCount = 33 - addresses.length;
134 + await generateNewAddresses(addressesCount,
135 + startIndex: addresses.length, hd: hd);
136 + }
137 + }
138 +
139 + Future<BitcoinAddressRecord> generateNewAddress(
140 + {bool isHidden = false, bitcoin.HDWallet hd}) async {
141 + _accountIndex += 1;
142 + final _hd = hd ?? this.hd;
143 + final address = BitcoinAddressRecord(
144 + getAddress(index: _accountIndex, hd: _hd),
145 + index: _accountIndex,
146 + isHidden: isHidden);
147 + addresses.add(address);
148 + await save();
149 + return address;
150 + }
151 +
152 + Future<List<BitcoinAddressRecord>> generateNewAddresses(int count,
153 + {int startIndex = 0, bitcoin.HDWallet hd, bool isHidden = false}) async {
154 + final list = <BitcoinAddressRecord>[];
155 +
156 + for (var i = startIndex; i < count + startIndex; i++) {
157 + final address = BitcoinAddressRecord(getAddress(index: i, hd: hd),
158 + index: i, isHidden: isHidden);
159 + list.add(address);
160 + }
161 +
162 + addresses.addAll(list);
163 + await save();
164 + return list;
165 + }
166 +
167 + Future<void> updateAddress(String address) async {
168 + for (final addr in addresses) {
169 + if (addr.address == address) {
170 + await save();
171 + break;
172 + }
173 + }
174 + }
175 +
176 + @action
177 + @override
178 + Future<void> startSync() async {
179 + try {
180 + syncStatus = StartingSyncStatus();
181 + updateTransactions();
182 + _subscribeForUpdates();
183 + await _updateBalance();
184 + await _updateUnspent();
185 + _feeRates = await electrumClient.feeRates();
186 +
187 + Timer.periodic(const Duration(minutes: 1),
188 + (timer) async => _feeRates = await electrumClient.feeRates());
189 +
190 + syncStatus = SyncedSyncStatus();
191 + } catch (e) {
192 + print(e.toString());
193 + syncStatus = FailedSyncStatus();
194 + }
195 + }
196 +
197 + @action
198 + @override
199 + Future<void> connectToNode({@required Node node}) async {
200 + try {
201 + syncStatus = ConnectingSyncStatus();
202 + await electrumClient.connectToUri(node.uri);
203 + electrumClient.onConnectionStatusChange = (bool isConnected) {
204 + if (!isConnected) {
205 + syncStatus = LostConnectionSyncStatus();
206 + }
207 + };
208 + syncStatus = ConnectedSyncStatus();
209 + } catch (e) {
210 + print(e.toString());
211 + syncStatus = FailedSyncStatus();
212 + }
213 + }
214 +
215 + @override
216 + Future<PendingBitcoinTransaction> createTransaction(
217 + Object credentials) async {
218 + const minAmount = 546;
219 + final transactionCredentials = credentials as BitcoinTransactionCredentials;
220 + final inputs = <BitcoinUnspent>[];
221 + final allAmountFee =
222 + calculateEstimatedFee(transactionCredentials.priority, null);
223 + final allAmount = balance.confirmed - allAmountFee;
224 + var fee = 0;
225 + final credentialsAmount = transactionCredentials.amount != null
226 + ? stringDoubleToBitcoinAmount(transactionCredentials.amount)
227 + : 0;
228 + final amount = transactionCredentials.amount == null ||
229 + allAmount - credentialsAmount < minAmount
230 + ? allAmount
231 + : credentialsAmount;
232 + final txb = bitcoin.TransactionBuilder(network: networkType);
233 + final changeAddress = address;
234 + var leftAmount = amount;
235 + var totalInputAmount = 0;
236 +
237 + if (_unspent.isEmpty) {
238 + await _updateUnspent();
239 + }
240 +
241 + for (final utx in _unspent) {
242 + leftAmount = leftAmount - utx.value;
243 + totalInputAmount += utx.value;
244 + inputs.add(utx);
245 +
246 + if (leftAmount <= 0) {
247 + break;
248 + }
249 + }
250 +
251 + if (inputs.isEmpty) {
252 + throw BitcoinTransactionNoInputsException();
253 + }
254 +
255 + final totalAmount = amount + fee;
256 + fee = transactionCredentials.amount != null
257 + ? feeAmountForPriority(transactionCredentials.priority, inputs.length,
258 + amount == allAmount ? 1 : 2)
259 + : allAmountFee;
260 +
261 + if (totalAmount > balance.confirmed) {
262 + throw BitcoinTransactionWrongBalanceException();
263 + }
264 +
265 + if (amount <= 0 || totalInputAmount < amount) {
266 + throw BitcoinTransactionWrongBalanceException();
267 + }
268 +
269 + txb.setVersion(1);
270 +
271 + inputs.forEach((input) {
272 + if (input.isP2wpkh) {
273 + final p2wpkh = bitcoin
274 + .P2WPKH(
275 + data: generatePaymentData(hd: hd, index: input.address.index),
276 + network: networkType)
277 + .data;
278 +
279 + txb.addInput(input.hash, input.vout, null, p2wpkh.output);
280 + } else {
281 + txb.addInput(input.hash, input.vout);
282 + }
283 + });
284 +
285 + txb.addOutput(
286 + addressToOutputScript(transactionCredentials.address, networkType),
287 + amount);
288 +
289 + final estimatedSize = estimatedTransactionSize(inputs.length, 2);
290 + final feeAmount = feeRate(transactionCredentials.priority) * estimatedSize;
291 + final changeValue = totalInputAmount - amount - feeAmount;
292 +
293 + if (changeValue > minAmount) {
294 + txb.addOutput(changeAddress, changeValue);
295 + }
296 +
297 + for (var i = 0; i < inputs.length; i++) {
298 + final input = inputs[i];
299 + final keyPair = generateKeyPair(
300 + hd: hd, index: input.address.index, network: networkType);
301 + final witnessValue = input.isP2wpkh ? input.value : null;
302 +
303 + txb.sign(vin: i, keyPair: keyPair, witnessValue: witnessValue);
304 + }
305 +
306 + return PendingBitcoinTransaction(txb.build(), type,
307 + electrumClient: electrumClient, amount: amount, fee: fee)
308 + ..addListener((transaction) async {
309 + transactionHistory.addOne(transaction);
310 + await _updateBalance();
311 + });
312 + }
313 +
314 + String toJSON() => json.encode({
315 + 'mnemonic': mnemonic,
316 + 'account_index': _accountIndex.toString(),
317 + 'addresses': addresses.map((addr) => addr.toJSON()).toList(),
318 + 'balance': balance?.toJSON()
319 + });
320 +
321 + int feeRate(TransactionPriority priority) {
322 + if (priority is BitcoinTransactionPriority) {
323 + return _feeRates[priority.raw];
324 + }
325 +
326 + return 0;
327 + }
328 +
329 + int feeAmountForPriority(BitcoinTransactionPriority priority, int inputsCount,
330 + int outputsCount) =>
331 + feeRate(priority) * estimatedTransactionSize(inputsCount, outputsCount);
332 +
333 + @override
334 + int calculateEstimatedFee(TransactionPriority priority, int amount) {
335 + if (priority is BitcoinTransactionPriority) {
336 + int inputsCount = 0;
337 +
338 + if (amount != null) {
339 + int totalValue = 0;
340 +
341 + for (final input in _unspent) {
342 + if (totalValue >= amount) {
343 + break;
344 + }
345 +
346 + totalValue += input.value;
347 + inputsCount += 1;
348 + }
349 + } else {
350 + inputsCount = _unspent.length;
351 + }
352 + // If send all, then we have no change value
353 + return feeAmountForPriority(
354 + priority, inputsCount, amount != null ? 2 : 1);
355 + }
356 +
357 + return 0;
358 + }
359 +
360 + @override
361 + Future<void> save() async {
362 + final path = await makePath();
363 + await write(path: path, password: _password, data: toJSON());
364 + await transactionHistory.save();
365 + }
366 +
367 + bitcoin.ECPair keyPairFor({@required int index}) =>
368 + generateKeyPair(hd: hd, index: index, network: networkType);
369 +
370 + @override
371 + Future<void> rescan({int height}) async => throw UnimplementedError();
372 +
373 + @override
374 + Future<void> close() async {
375 + try {
376 + await electrumClient?.close();
377 + } catch (_) {}
378 + }
379 +
380 + String getAddress({@required int index, @required bitcoin.HDWallet hd}) => '';
381 +
382 + Future<String> makePath() async =>
383 + pathForWallet(name: walletInfo.name, type: walletInfo.type);
384 +
385 + Future<void> _updateUnspent() async {
386 + final unspent = await Future.wait(addresses.map((address) => electrumClient
387 + .getListUnspentWithAddress(address.address, networkType)
388 + .then((unspent) => unspent
389 + .map((unspent) => BitcoinUnspent.fromJSON(address, unspent)))));
390 + _unspent = unspent.expand((e) => e).toList();
391 + }
392 +
393 + Future<ElectrumTransactionInfo> fetchTransactionInfo(
394 + {@required String hash, @required int height}) async {
395 + final tx = await electrumClient.getTransactionExpanded(hash: hash);
396 + return ElectrumTransactionInfo.fromElectrumVerbose(tx, walletInfo.type,
397 + height: height, addresses: addresses);
398 + }
399 +
400 + @override
401 + Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
402 + final histories =
403 + scriptHashes.map((scriptHash) => electrumClient.getHistory(scriptHash));
404 + final _historiesWithDetails = await Future.wait(histories)
405 + .then((histories) => histories.expand((i) => i).toList())
406 + .then((histories) => histories.map((tx) => fetchTransactionInfo(
407 + hash: tx['tx_hash'] as String, height: tx['height'] as int)));
408 + final historiesWithDetails = await Future.wait(_historiesWithDetails);
409 +
410 + return historiesWithDetails.fold<Map<String, ElectrumTransactionInfo>>(
411 + <String, ElectrumTransactionInfo>{}, (acc, tx) {
412 + acc[tx.id] = acc[tx.id]?.updated(tx) ?? tx;
413 + return acc;
414 + });
415 + }
416 +
417 + Future<void> updateTransactions() async {
418 + try {
419 + if (_isTransactionUpdating) {
420 + return;
421 + }
422 +
423 + _isTransactionUpdating = true;
424 + final transactions = await fetchTransactions();
425 + transactionHistory.addMany(transactions);
426 + await transactionHistory.save();
427 + _isTransactionUpdating = false;
428 + } catch (e) {
429 + print(e);
430 + _isTransactionUpdating = false;
431 + }
432 + }
433 +
434 + void _subscribeForUpdates() {
435 + scriptHashes.forEach((sh) async {
436 + await _scripthashesUpdateSubject[sh]?.close();
437 + _scripthashesUpdateSubject[sh] = electrumClient.scripthashUpdate(sh);
438 + _scripthashesUpdateSubject[sh].listen((event) async {
439 + try {
440 + await _updateBalance();
441 + await _updateUnspent();
442 + await updateTransactions();
443 + } catch (e) {
444 + print(e.toString());
445 + }
446 + });
447 + });
448 + }
449 +
450 + Future<ElectrumBalance> _fetchBalances() async {
451 + final balances = await Future.wait(
452 + scriptHashes.map((sh) => electrumClient.getBalance(sh)));
453 + final balance = balances.fold(
454 + ElectrumBalance(confirmed: 0, unconfirmed: 0),
455 + (ElectrumBalance acc, val) => ElectrumBalance(
456 + confirmed: (val['confirmed'] as int ?? 0) + (acc.confirmed ?? 0),
457 + unconfirmed:
458 + (val['unconfirmed'] as int ?? 0) + (acc.unconfirmed ?? 0)));
459 +
460 + return balance;
461 + }
462 +
463 + Future<void> _updateBalance() async {
464 + balance = await _fetchBalances();
465 + await save();
466 + }
467 +}
lib/bitcoin/electrum_wallet_snapshot.dart new
+42
@@ -0,0 +1,42 @@
1 +import 'dart:convert';
2 +import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart';
3 +import 'package:cake_wallet/bitcoin/electrum_balance.dart';
4 +import 'package:cake_wallet/bitcoin/file.dart';
5 +import 'package:cake_wallet/entities/pathForWallet.dart';
6 +import 'package:cake_wallet/entities/wallet_type.dart';
7 +
8 +class ElectrumWallletSnapshot {
9 + ElectrumWallletSnapshot(this.name, this.type, this.password);
10 +
11 + final String name;
12 + final String password;
13 + final WalletType type;
14 +
15 + String mnemonic;
16 + List<BitcoinAddressRecord> addresses;
17 + ElectrumBalance balance;
18 + int accountIndex;
19 +
20 + Future<void> load() async {
21 + try {
22 + final path = await pathForWallet(name: name, type: type);
23 + final jsonSource = await read(path: path, password: password);
24 + final data = json.decode(jsonSource) as Map;
25 + final addressesTmp = data['addresses'] as List ?? <Object>[];
26 + mnemonic = data['mnemonic'] as String;
27 + addresses = addressesTmp
28 + .whereType<String>()
29 + .map((addr) => BitcoinAddressRecord.fromJSON(addr))
30 + .toList();
31 + balance = ElectrumBalance.fromJSON(data['balance'] as String) ??
32 + ElectrumBalance(confirmed: 0, unconfirmed: 0);
33 + accountIndex = 0;
34 +
35 + try {
36 + accountIndex = int.parse(data['account_index'] as String);
37 + } catch (_) {}
38 + } catch (e) {
39 + print(e);
40 + }
41 + }
42 +}
lib/bitcoin/litecoin_network.dart new
+9
@@ -0,0 +1,9 @@
1 +import 'package:bitcoin_flutter/bitcoin_flutter.dart';
2 +
3 +final litecoinNetwork = NetworkType(
4 + messagePrefix: '\x19Litecoin Signed Message:\n',
5 + bech32: 'ltc',
6 + bip32: Bip32Type(public: 0x0488b21e, private: 0x0488ade4),
7 + pubKeyHash: 0x30,
8 + scriptHash: 0x32,
9 + wif: 0xb0);
lib/bitcoin/litecoin_wallet.dart new
+88
@@ -0,0 +1,88 @@
1 +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
2 +import 'package:cake_wallet/bitcoin/bitcoin_mnemonic.dart';
3 +import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart';
4 +import 'package:cake_wallet/entities/transaction_priority.dart';
5 +import 'package:flutter/foundation.dart';
6 +import 'package:mobx/mobx.dart';
7 +import 'package:cake_wallet/entities/wallet_info.dart';
8 +import 'package:cake_wallet/bitcoin/electrum_wallet_snapshot.dart';
9 +import 'package:cake_wallet/bitcoin/electrum_wallet.dart';
10 +import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart';
11 +import 'package:cake_wallet/bitcoin/electrum_balance.dart';
12 +import 'package:cake_wallet/bitcoin/litecoin_network.dart';
13 +import 'package:cake_wallet/bitcoin/utils.dart';
14 +
15 +part 'litecoin_wallet.g.dart';
16 +
17 +class LitecoinWallet = LitecoinWalletBase with _$LitecoinWallet;
18 +
19 +abstract class LitecoinWalletBase extends ElectrumWallet with Store {
20 + LitecoinWalletBase(
21 + {@required String mnemonic,
22 + @required String password,
23 + @required WalletInfo walletInfo,
24 + List<BitcoinAddressRecord> initialAddresses,
25 + ElectrumBalance initialBalance,
26 + int accountIndex = 0})
27 + : super(
28 + mnemonic: mnemonic,
29 + password: password,
30 + walletInfo: walletInfo,
31 + networkType: litecoinNetwork,
32 + initialAddresses: initialAddresses,
33 + initialBalance: initialBalance,
34 + accountIndex: accountIndex);
35 +
36 + static Future<LitecoinWallet> open({
37 + @required String name,
38 + @required WalletInfo walletInfo,
39 + @required String password,
40 + }) async {
41 + final snp = ElectrumWallletSnapshot(name, walletInfo.type, password);
42 + await snp.load();
43 + return LitecoinWallet(
44 + mnemonic: snp.mnemonic,
45 + password: password,
46 + walletInfo: walletInfo,
47 + initialAddresses: snp.addresses,
48 + initialBalance: snp.balance,
49 + accountIndex: snp.accountIndex);
50 + }
51 +
52 + @override
53 + String getAddress({@required int index, @required bitcoin.HDWallet hd}) =>
54 + generateP2WPKHAddress(hd: hd, index: index, networkType: networkType);
55 +
56 + @override
57 + Future<void> generateAddresses() async {
58 + if (addresses.length < 33) {
59 + final addressesCount = 22 - addresses.length;
60 + await generateNewAddresses(addressesCount,
61 + hd: hd, startIndex: addresses.length);
62 +
63 + final changeRoot = bitcoin.HDWallet.fromSeed(
64 + mnemonicToSeedBytes(mnemonic),
65 + network: networkType)
66 + .derivePath("m/0'/1");
67 +
68 + await generateNewAddresses(11,
69 + startIndex: 0, hd: changeRoot, isHidden: true);
70 + }
71 + }
72 +
73 + @override
74 + int feeRate(TransactionPriority priority) {
75 + if (priority is BitcoinTransactionPriority) {
76 + switch (priority) {
77 + case BitcoinTransactionPriority.slow:
78 + return 1;
79 + case BitcoinTransactionPriority.medium:
80 + return 2;
81 + case BitcoinTransactionPriority.fast:
82 + return 3;
83 + }
84 + }
85 +
86 + return 0;
87 + }
88 +}
lib/bitcoin/litecoin_wallet_service.dart new
+76
@@ -0,0 +1,76 @@
1 +import 'dart:io';
2 +import 'package:hive/hive.dart';
3 +import 'package:cake_wallet/bitcoin/bitcoin_mnemonic.dart';
4 +import 'package:cake_wallet/bitcoin/bitcoin_mnemonic_is_incorrect_exception.dart';
5 +import 'package:cake_wallet/bitcoin/bitcoin_wallet_creation_credentials.dart';
6 +import 'package:cake_wallet/bitcoin/litecoin_wallet.dart';
7 +import 'package:cake_wallet/core/wallet_service.dart';
8 +import 'package:cake_wallet/entities/pathForWallet.dart';
9 +import 'package:cake_wallet/entities/wallet_type.dart';
10 +import 'package:cake_wallet/entities/wallet_info.dart';
11 +import 'package:cake_wallet/core/wallet_base.dart';
12 +
13 +class LitecoinWalletService extends WalletService<
14 + BitcoinNewWalletCredentials,
15 + BitcoinRestoreWalletFromSeedCredentials,
16 + BitcoinRestoreWalletFromWIFCredentials> {
17 + LitecoinWalletService(this.walletInfoSource);
18 +
19 + final Box<WalletInfo> walletInfoSource;
20 +
21 + @override
22 + WalletType getType() => WalletType.litecoin;
23 +
24 + @override
25 + Future<LitecoinWallet> create(BitcoinNewWalletCredentials credentials) async {
26 + final wallet = LitecoinWallet(
27 + mnemonic: generateMnemonic(),
28 + password: credentials.password,
29 + walletInfo: credentials.walletInfo);
30 + await wallet.save();
31 + await wallet.init();
32 +
33 + return wallet;
34 + }
35 +
36 + @override
37 + Future<bool> isWalletExit(String name) async =>
38 + File(await pathForWallet(name: name, type: getType())).existsSync();
39 +
40 + @override
41 + Future<LitecoinWallet> openWallet(String name, String password) async {
42 + final walletInfo = walletInfoSource.values.firstWhere(
43 + (info) => info.id == WalletBase.idFor(name, getType()),
44 + orElse: () => null);
45 + final wallet = await LitecoinWalletBase.open(
46 + password: password, name: name, walletInfo: walletInfo);
47 + await wallet.init();
48 + return wallet;
49 + }
50 +
51 + @override
52 + Future<void> remove(String wallet) async =>
53 + File(await pathForWalletDir(name: wallet, type: getType()))
54 + .delete(recursive: true);
55 +
56 + @override
57 + Future<LitecoinWallet> restoreFromKeys(
58 + BitcoinRestoreWalletFromWIFCredentials credentials) async =>
59 + throw UnimplementedError();
60 +
61 + @override
62 + Future<LitecoinWallet> restoreFromSeed(
63 + BitcoinRestoreWalletFromSeedCredentials credentials) async {
64 + if (!validateMnemonic(credentials.mnemonic)) {
65 + throw BitcoinMnemonicIsIncorrectException();
66 + }
67 +
68 + final wallet = LitecoinWallet(
69 + password: credentials.password,
70 + mnemonic: credentials.mnemonic,
71 + walletInfo: credentials.walletInfo);
72 + await wallet.save();
73 + await wallet.init();
74 + return wallet;
75 + }
76 +}
lib/bitcoin/pending_bitcoin_transaction.dart
+17 -12
@@ -1,18 +1,22 @@
1 -import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
2 -import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart';
3 -import 'package:cake_wallet/entities/transaction_direction.dart';
1 import 'package:flutter/foundation.dart';
2 import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
3 import 'package:cake_wallet/core/pending_transaction.dart';
4 import 'package:cake_wallet/bitcoin/electrum.dart';
5 +import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
6 +import 'package:cake_wallet/bitcoin/electrum_transaction_info.dart';
7 +import 'package:cake_wallet/entities/transaction_direction.dart';
8 +import 'package:cake_wallet/entities/wallet_type.dart';
9
10 class PendingBitcoinTransaction with PendingTransaction {
10 - PendingBitcoinTransaction(this._tx,
11 - {@required this.eclient, @required this.amount, @required this.fee})
12 - : _listeners = <void Function(BitcoinTransactionInfo transaction)>[];
11 + PendingBitcoinTransaction(this._tx, this.type,
12 + {@required this.electrumClient,
13 + @required this.amount,
14 + @required this.fee})
15 + : _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
16
17 + final WalletType type;
18 final bitcoin.Transaction _tx;
15 - final ElectrumClient eclient;
19 + final ElectrumClient electrumClient;
20 final int amount;
21 final int fee;
22
@@ -25,24 +29,25 @@ class PendingBitcoinTransaction with PendingTransaction {
29 @override
30 String get feeFormatted => bitcoinAmountToString(amount: fee);
31
28 - final List<void Function(BitcoinTransactionInfo transaction)> _listeners;
32 + final List<void Function(ElectrumTransactionInfo transaction)> _listeners;
33
34 @override
35 Future<void> commit() async {
32 - await eclient.broadcastTransaction(transactionRaw: _tx.toHex());
36 + await electrumClient.broadcastTransaction(transactionRaw: _tx.toHex());
37 _listeners?.forEach((listener) => listener(transactionInfo()));
38 }
39
40 void addListener(
37 - void Function(BitcoinTransactionInfo transaction) listener) =>
41 + void Function(ElectrumTransactionInfo transaction) listener) =>
42 _listeners.add(listener);
43
40 - BitcoinTransactionInfo transactionInfo() => BitcoinTransactionInfo(
44 + ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type,
45 id: id,
46 height: 0,
47 amount: amount,
48 direction: TransactionDirection.outgoing,
49 date: DateTime.now(),
50 isPending: true,
47 - confirmations: 0);
51 + confirmations: 0,
52 + fee: fee);
53 }
lib/bitcoin/script_hash.dart
+9 -7
@@ -1,18 +1,20 @@
1 +import 'package:flutter/foundation.dart';
2 import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
3 import 'package:crypto/crypto.dart';
4
4 -String scriptHash(String address) {
5 - final outputScript = bitcoin.Address.addressToOutputScript(address);
6 - final splitted = sha256.convert(outputScript).toString().split('');
5 +String scriptHash(String address, {@required bitcoin.NetworkType networkType}) {
6 + final outputScript =
7 + bitcoin.Address.addressToOutputScript(address, networkType);
8 + final parts = sha256.convert(outputScript).toString().split('');
9 var res = '';
10
9 - for (var i = splitted.length - 1; i >= 0; i--) {
10 - final char = splitted[i];
11 + for (var i = parts.length - 1; i >= 0; i--) {
12 + final char = parts[i];
13 i--;
12 - final nextChar = splitted[i];
14 + final nextChar = parts[i];
15 res += nextChar;
16 res += char;
17 }
18
19 return res;
18 -}
\ No newline at end of file
20 +}
lib/bitcoin/utils.dart
+33 -4
@@ -13,14 +13,43 @@ bitcoin.ECPair generateKeyPair(
13 {@required bitcoin.HDWallet hd,
14 @required int index,
15 bitcoin.NetworkType network}) =>
16 - bitcoin.ECPair.fromWIF(hd.derive(index).wif,
17 - network: network ?? bitcoin.bitcoin);
16 + bitcoin.ECPair.fromWIF(hd.derive(index).wif, network: network);
17
19 -String generateAddress({@required bitcoin.HDWallet hd, @required int index}) =>
18 +String generateP2WPKHAddress(
19 + {@required bitcoin.HDWallet hd,
20 + @required int index,
21 + bitcoin.NetworkType networkType}) =>
22 + bitcoin
23 + .P2WPKH(
24 + data: PaymentData(
25 + pubkey:
26 + Uint8List.fromList(HEX.decode(hd.derive(index).pubKey))),
27 + network: networkType)
28 + .data
29 + .address;
30 +
31 +String generateP2WPKHAddressByPath(
32 + {@required bitcoin.HDWallet hd,
33 + @required String path,
34 + bitcoin.NetworkType networkType}) =>
35 bitcoin
36 .P2WPKH(
37 data: PaymentData(
38 pubkey:
24 - Uint8List.fromList(HEX.decode(hd.derive(index).pubKey))))
39 + Uint8List.fromList(HEX.decode(hd.derivePath(path).pubKey))),
40 + network: networkType)
41 + .data
42 + .address;
43 +
44 +String generateP2PKHAddress(
45 + {@required bitcoin.HDWallet hd,
46 + @required int index,
47 + bitcoin.NetworkType networkType}) =>
48 + bitcoin
49 + .P2PKH(
50 + data: PaymentData(
51 + pubkey:
52 + Uint8List.fromList(HEX.decode(hd.derive(index).pubKey))),
53 + network: networkType)
54 .data
55 .address;
lib/core/address_validator.dart
+1 -1
@@ -72,7 +72,7 @@ class AddressValidator extends TextValidator {
72 case CryptoCurrency.eth:
73 return [42];
74 case CryptoCurrency.ltc:
75 - return [34];
75 + return [34, 43];
76 case CryptoCurrency.nano:
77 return [64, 65];
78 case CryptoCurrency.trx:
lib/core/backup_service.dart
-1
@@ -8,7 +8,6 @@ import 'package:path_provider/path_provider.dart';
8 import 'package:cryptography/cryptography.dart';
9 import 'package:shared_preferences/shared_preferences.dart';
10 import 'package:archive/archive_io.dart';
11 -import 'package:cake_wallet/core/auth_service.dart';
11 import 'package:cake_wallet/core/key_service.dart';
12 import 'package:cake_wallet/entities/encrypt.dart';
13 import 'package:cake_wallet/entities/preferences_key.dart';
lib/core/fiat_conversion_service.dart
+5 -6
@@ -3,7 +3,6 @@ import 'package:cake_wallet/entities/fiat_currency.dart';
3 import 'dart:convert';
4 import 'package:flutter/foundation.dart';
5 import 'package:http/http.dart';
6 -import 'package:cake_wallet/entities/currency_formatter.dart';
6
7 const fiatApiAuthority = 'fiat-api.cakewallet.com';
8 const fiatApiPath = '/v1/rates';
@@ -15,9 +14,8 @@ Future<double> _fetchPrice(Map<String, dynamic> args) async {
14
15 try {
16 final fiatStringified = fiat.toString();
18 - final uri =
19 - Uri.https(fiatApiAuthority, fiatApiPath,
20 - <String, String> {'convert': fiatStringified});
17 + final uri = Uri.https(fiatApiAuthority, fiatApiPath,
18 + <String, String>{'convert': fiatStringified});
19 final response = await get(uri.toString());
20
21 if (response.statusCode != 200) {
@@ -28,7 +26,7 @@ Future<double> _fetchPrice(Map<String, dynamic> args) async {
26 final data = responseJSON['data'] as List<dynamic>;
27
28 for (final item in data) {
31 - if (item['symbol'] == cryptoToString(crypto)) {
29 + if (item['symbol'] == crypto.title) {
30 price = item['quote'][fiatStringified]['price'] as double;
31 break;
32 }
@@ -45,6 +43,7 @@ Future<double> _fetchPriceAsync(
43 compute(_fetchPrice, {'fiat': fiat, 'crypto': crypto});
44
45 class FiatConversionService {
48 - static Future<double> fetchPrice(CryptoCurrency crypto, FiatCurrency fiat) async =>
46 + static Future<double> fetchPrice(
47 + CryptoCurrency crypto, FiatCurrency fiat) async =>
48 await _fetchPriceAsync(crypto, fiat);
49 }
lib/core/generate_wallet_password.dart
+3 -3
@@ -4,9 +4,9 @@ import 'package:cake_wallet/entities/wallet_type.dart';
4
5 String generateWalletPassword(WalletType type) {
6 switch (type) {
7 - case WalletType.bitcoin:
8 - return generateKey();
9 - default:
7 + case WalletType.monero:
8 return Uuid().v4();
9 + default:
10 + return generateKey();
11 }
12 }
lib/core/node_port_validator.dart
-1
@@ -1,4 +1,3 @@
1 -import 'package:flutter/foundation.dart';
1 import 'package:cake_wallet/generated/i18n.dart';
2 import 'package:cake_wallet/core/validator.dart';
3
lib/core/transaction_history.dart
+42 -35
@@ -3,43 +3,50 @@ import 'package:mobx/mobx.dart';
3 import 'package:cake_wallet/entities/transaction_info.dart';
4
5 abstract class TransactionHistoryBase<TransactionType extends TransactionInfo> {
6 - TransactionHistoryBase() : _isUpdating = false;
6 + TransactionHistoryBase();
7 + // : _isUpdating = false;
8
9 @observable
10 ObservableMap<String, TransactionType> transactions;
11
11 - bool _isUpdating;
12 -
13 - @action
14 - Future<void> update() async {
15 - if (_isUpdating) {
16 - return;
17 - }
18 -
19 - try {
20 - _isUpdating = true;
21 - final _transactions = await fetchTransactions();
22 - transactions.keys
23 - .toSet()
24 - .difference(_transactions.keys.toSet())
25 - .forEach((k) => transactions.remove(k));
26 - _transactions.forEach((key, value) => transactions[key] = value);
27 - _isUpdating = false;
28 - } catch (e) {
29 - _isUpdating = false;
30 - rethrow;
31 - }
32 - }
33 -
34 - void updateAsync({void Function() onFinished}) {
35 - fetchTransactionsAsync(
36 - (transaction) => transactions[transaction.id] = transaction,
37 - onFinished: onFinished);
38 - }
39 -
40 - void fetchTransactionsAsync(
41 - void Function(TransactionType transaction) onTransactionLoaded,
42 - {void Function() onFinished});
43 -
44 - Future<Map<String, TransactionType>> fetchTransactions();
12 + Future<void> save();
13 +
14 + void addOne(TransactionType transaction);
15 +
16 + void addMany(Map<String, TransactionType> transactions);
17 +
18 + // bool _isUpdating;
19 +
20 + // @action
21 + // Future<void> update() async {
22 + // if (_isUpdating) {
23 + // return;
24 + // }
25 +
26 + // try {
27 + // _isUpdating = true;
28 + // final _transactions = await fetchTransactions();
29 + // transactions.keys
30 + // .toSet()
31 + // .difference(_transactions.keys.toSet())
32 + // .forEach((k) => transactions.remove(k));
33 + // _transactions.forEach((key, value) => transactions[key] = value);
34 + // _isUpdating = false;
35 + // } catch (e) {
36 + // _isUpdating = false;
37 + // rethrow;
38 + // }
39 + // }
40 +
41 + // void updateAsync({void Function() onFinished}) {
42 + // fetchTransactionsAsync(
43 + // (transaction) => transactions[transaction.id] = transaction,
44 + // onFinished: onFinished);
45 + // }
46 +
47 + // void fetchTransactionsAsync(
48 + // void Function(TransactionType transaction) onTransactionLoaded,
49 + // {void Function() onFinished});
50 +
51 + // Future<Map<String, TransactionType>> fetchTransactions();
52 }
lib/core/wallet_base.dart
+12 -2
@@ -1,4 +1,5 @@
1 import 'package:cake_wallet/entities/balance.dart';
2 +import 'package:cake_wallet/entities/transaction_info.dart';
3 import 'package:cake_wallet/entities/transaction_priority.dart';
4 import 'package:flutter/foundation.dart';
5 import 'package:cake_wallet/entities/wallet_info.dart';
@@ -11,7 +12,10 @@ import 'package:cake_wallet/entities/sync_status.dart';
12 import 'package:cake_wallet/entities/node.dart';
13 import 'package:cake_wallet/entities/wallet_type.dart';
14
14 -abstract class WalletBase<BalanceType extends Balance> {
15 +abstract class WalletBase<
16 + BalanceType extends Balance,
17 + HistoryType extends TransactionHistoryBase,
18 + TransactionType extends TransactionInfo> {
19 WalletBase(this.walletInfo);
20
21 static String idFor(String name, WalletType type) =>
@@ -41,7 +45,7 @@ abstract class WalletBase<BalanceType extends Balance> {
45
46 Object get keys;
47
44 - TransactionHistoryBase transactionHistory;
48 + HistoryType transactionHistory;
49
50 Future<void> connectToNode({@required Node node});
51
@@ -51,6 +55,12 @@ abstract class WalletBase<BalanceType extends Balance> {
55
56 int calculateEstimatedFee(TransactionPriority priority, int amount);
57
58 + // void fetchTransactionsAsync(
59 + // void Function(TransactionType transaction) onTransactionLoaded,
60 + // {void Function() onFinished});
61 +
62 + Future<Map<String, TransactionType>> fetchTransactions();
63 +
64 Future<void> save();
65
66 Future<void> rescan({int height});
lib/core/wallet_service.dart
+3
@@ -1,8 +1,11 @@
1 import 'package:cake_wallet/core/wallet_base.dart';
2 import 'package:cake_wallet/core/wallet_credentials.dart';
3 +import 'package:cake_wallet/entities/wallet_type.dart';
4
5 abstract class WalletService<N extends WalletCredentials,
6 RFS extends WalletCredentials, RFK extends WalletCredentials> {
7 + WalletType getType();
8 +
9 Future<WalletBase> create(N credentials);
10
11 Future<WalletBase> restoreFromSeed(RFS credentials);
lib/di.dart
+5
@@ -1,4 +1,5 @@
1 import 'package:cake_wallet/bitcoin/bitcoin_wallet_service.dart';
2 +import 'package:cake_wallet/bitcoin/litecoin_wallet_service.dart';
3 import 'package:cake_wallet/core/backup_service.dart';
4 import 'package:cake_wallet/core/wallet_service.dart';
5 import 'package:cake_wallet/entities/biometric_auth.dart';
@@ -443,6 +444,8 @@ Future setup(
444
445 getIt.registerFactory(() => BitcoinWalletService(_walletInfoSource));
446
447 + getIt.registerFactory(() => LitecoinWalletService(_walletInfoSource));
448 +
449 getIt.registerFactoryParam<WalletService, WalletType, void>(
450 (WalletType param1, __) {
451 switch (param1) {
@@ -450,6 +453,8 @@ Future setup(
453 return getIt.get<MoneroWalletService>();
454 case WalletType.bitcoin:
455 return getIt.get<BitcoinWalletService>();
456 + case WalletType.litecoin:
457 + return getIt.get<LitecoinWalletService>();
458 default:
459 return null;
460 }
lib/entities/currency_for_wallet_type.dart
+3 -1
@@ -7,7 +7,9 @@ CryptoCurrency currencyForWalletType(WalletType type) {
7 return CryptoCurrency.btc;
8 case WalletType.monero:
9 return CryptoCurrency.xmr;
10 + case WalletType.litecoin:
11 + return CryptoCurrency.ltc;
12 default:
13 return null;
14 }
13 -}
\ No newline at end of file
15 +}
lib/entities/currency_formatter.dart deleted
-12
@@ -1,12 +0,0 @@
1 -import 'package:cake_wallet/entities/crypto_currency.dart';
2 -
3 -String cryptoToString(CryptoCurrency crypto) {
4 - switch (crypto) {
5 - case CryptoCurrency.xmr:
6 - return 'XMR';
7 - case CryptoCurrency.btc:
8 - return 'BTC';
9 - default:
10 - return '';
11 - }
12 -}
\ No newline at end of file
lib/entities/default_settings_migration.dart
+102 -32
@@ -1,11 +1,7 @@
1 import 'dart:io' show File, Platform;
2 import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart';
3 -import 'package:cake_wallet/core/generate_wallet_password.dart';
4 -import 'package:cake_wallet/core/key_service.dart';
5 -import 'package:cake_wallet/di.dart';
3 import 'package:cake_wallet/entities/pathForWallet.dart';
4 import 'package:cake_wallet/entities/secret_store_key.dart';
8 -import 'package:cake_wallet/monero/monero_wallet_service.dart';
5 import 'package:flutter/foundation.dart';
6 import 'package:flutter_secure_storage/flutter_secure_storage.dart';
7 import 'package:hive/hive.dart';
@@ -24,7 +20,8 @@ import 'package:cake_wallet/exchange/trade.dart';
20 import 'package:encrypt/encrypt.dart' as encrypt;
21
22 const newCakeWalletMoneroUri = 'xmr-node.cakewallet.com:18081';
27 -const cakeWalletElectrumUri = 'electrum.cakewallet.com:50002';
23 +const cakeWalletBitcoinElectrumUri = 'electrum.cakewallet.com:50002';
24 +const cakeWalletLitecoinElectrumUri = '128.199.34.116:50002';
25
26 Future defaultSettingsMigration(
27 {@required int version,
@@ -68,6 +65,8 @@ Future defaultSettingsMigration(
65 sharedPreferences: sharedPreferences, nodes: nodes);
66 await changeBitcoinCurrentElectrumServerToDefault(
67 sharedPreferences: sharedPreferences, nodes: nodes);
68 + await changeLitecoinCurrentElectrumServerToDefault(
69 + sharedPreferences: sharedPreferences, nodes: nodes);
70
71 break;
72 case 2:
@@ -97,6 +96,7 @@ Future defaultSettingsMigration(
96 case 9:
97 await generateBackupPassword(secureStorage);
98 break;
99 +
100 case 10:
101 await changeTransactionPriorityAndFeeRateKeys(sharedPreferences);
102 break;
@@ -110,7 +110,14 @@ Future defaultSettingsMigration(
110 break;
111
112 case 13:
113 - await resetElectrumServer(nodes, sharedPreferences);
113 + await resetBitcoinElectrumServer(nodes, sharedPreferences);
114 + break;
115 +
116 + case 15:
117 + await addLitecoinElectrumServerList(nodes: nodes);
118 + await changeLitecoinCurrentElectrumServerToDefault(
119 + sharedPreferences: sharedPreferences, nodes: nodes);
120 + await checkCurrentNodes(nodes, sharedPreferences);
121 break;
122
123 default:
@@ -160,12 +167,21 @@ Future<void> changeMoneroCurrentNodeToDefault(
167 }
168
169 Node getBitcoinDefaultElectrumServer({@required Box<Node> nodes}) {
163 - return nodes.values
164 - .firstWhere((Node node) => node.uri == cakeWalletElectrumUri, orElse: () => null) ??
170 + return nodes.values.firstWhere(
171 + (Node node) => node.uri == cakeWalletBitcoinElectrumUri,
172 + orElse: () => null) ??
173 nodes.values.firstWhere((node) => node.type == WalletType.bitcoin,
174 orElse: () => null);
175 }
176
177 +Node getLitecoinDefaultElectrumServer({@required Box<Node> nodes}) {
178 + return nodes.values.firstWhere(
179 + (Node node) => node.uri == cakeWalletLitecoinElectrumUri,
180 + orElse: () => null) ??
181 + nodes.values.firstWhere((node) => node.type == WalletType.litecoin,
182 + orElse: () => null);
183 +}
184 +
185 Node getMoneroDefaultNode({@required Box<Node> nodes}) {
186 final timeZone = DateTime.now().timeZoneOffset.inHours;
187 var nodeUri = '';
@@ -192,6 +208,15 @@ Future<void> changeBitcoinCurrentElectrumServerToDefault(
208 await sharedPreferences.setInt('current_node_id_btc', serverId);
209 }
210
211 +Future<void> changeLitecoinCurrentElectrumServerToDefault(
212 + {@required SharedPreferences sharedPreferences,
213 + @required Box<Node> nodes}) async {
214 + final server = getLitecoinDefaultElectrumServer(nodes: nodes);
215 + final serverId = server?.key as int ?? 0;
216 +
217 + await sharedPreferences.setInt('current_node_id_ltc', serverId);
218 +}
219 +
220 Future<void> replaceDefaultNode(
221 {@required SharedPreferences sharedPreferences,
222 @required Box<Node> nodes}) async {
@@ -224,7 +249,12 @@ Future<void> updateNodeTypes({@required Box<Node> nodes}) async {
249 }
250
251 Future<void> addBitcoinElectrumServerList({@required Box<Node> nodes}) async {
227 - final serverList = await loadElectrumServerList();
252 + final serverList = await loadBitcoinElectrumServerList();
253 + await nodes.addAll(serverList);
254 +}
255 +
256 +Future<void> addLitecoinElectrumServerList({@required Box<Node> nodes}) async {
257 + final serverList = await loadLitecoinElectrumServerList();
258 await nodes.addAll(serverList);
259 }
260
@@ -284,57 +314,97 @@ Future<void> changeTransactionPriorityAndFeeRateKeys(
314 Future<void> changeDefaultMoneroNode(
315 Box<Node> nodeSource, SharedPreferences sharedPreferences) async {
316 const cakeWalletMoneroNodeUriPattern = '.cakewallet.com';
287 - final currentMoneroNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey);
288 - final currentMoneroNode = nodeSource.values.firstWhere((node) => node.key == currentMoneroNodeId);
289 - final needToReplaceCurrentMoneroNode = currentMoneroNode.uri.contains(cakeWalletMoneroNodeUriPattern);
317 + final currentMoneroNodeId =
318 + sharedPreferences.getInt(PreferencesKey.currentNodeIdKey);
319 + final currentMoneroNode =
320 + nodeSource.values.firstWhere((node) => node.key == currentMoneroNodeId);
321 + final needToReplaceCurrentMoneroNode =
322 + currentMoneroNode.uri.contains(cakeWalletMoneroNodeUriPattern);
323
324 nodeSource.values.forEach((node) async {
292 - if (node.type == WalletType.monero && node.uri.contains(cakeWalletMoneroNodeUriPattern)) {
325 + if (node.type == WalletType.monero &&
326 + node.uri.contains(cakeWalletMoneroNodeUriPattern)) {
327 await node.delete();
328 }
329 });
330
297 - final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero);
331 + final newCakeWalletNode =
332 + Node(uri: newCakeWalletMoneroUri, type: WalletType.monero);
333
334 await nodeSource.add(newCakeWalletNode);
335
336 if (needToReplaceCurrentMoneroNode) {
302 - await sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, newCakeWalletNode.key as int);
337 + await sharedPreferences.setInt(
338 + PreferencesKey.currentNodeIdKey, newCakeWalletNode.key as int);
339 }
340 }
341
306 -Future<void> checkCurrentNodes(Box<Node> nodeSource, SharedPreferences sharedPreferences) async {
307 - final currentMoneroNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey);
308 - final currentElectrumSeverId = await sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey);
309 - final currentMoneroNode = nodeSource.values.firstWhere((node) => node.key == currentMoneroNodeId, orElse: () => null);
310 - final currentElectrumServer = nodeSource.values.firstWhere((node) => node.key == currentElectrumSeverId, orElse: () => null);
342 +Future<void> checkCurrentNodes(
343 + Box<Node> nodeSource, SharedPreferences sharedPreferences) async {
344 + final currentMoneroNodeId =
345 + sharedPreferences.getInt(PreferencesKey.currentNodeIdKey);
346 + final currentBitcoinElectrumSeverId =
347 + sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey);
348 + final currentLitecoinElectrumSeverId = sharedPreferences
349 + .getInt(PreferencesKey.currentLitecoinElectrumSererIdKey);
350 + final currentMoneroNode = nodeSource.values.firstWhere(
351 + (node) => node.key == currentMoneroNodeId,
352 + orElse: () => null);
353 + final currentBitcoinElectrumServer = nodeSource.values.firstWhere(
354 + (node) => node.key == currentBitcoinElectrumSeverId,
355 + orElse: () => null);
356 + final currentLitecoinElectrumServer = nodeSource.values.firstWhere(
357 + (node) => node.key == currentLitecoinElectrumSeverId,
358 + orElse: () => null);
359
360 if (currentMoneroNode == null) {
313 - final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero);
361 + final newCakeWalletNode =
362 + Node(uri: newCakeWalletMoneroUri, type: WalletType.monero);
363 await nodeSource.add(newCakeWalletNode);
315 - await sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, newCakeWalletNode.key as int);
364 + await sharedPreferences.setInt(
365 + PreferencesKey.currentNodeIdKey, newCakeWalletNode.key as int);
366 }
367
318 - if (currentElectrumServer == null) {
319 - final cakeWalletElectrum = Node(uri: cakeWalletElectrumUri, type: WalletType.bitcoin);
368 + if (currentBitcoinElectrumServer == null) {
369 + final cakeWalletElectrum =
370 + Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin);
371 await nodeSource.add(cakeWalletElectrum);
321 - await sharedPreferences.setInt(PreferencesKey.currentBitcoinElectrumSererIdKey, cakeWalletElectrum.key as int);
372 + await sharedPreferences.setInt(
373 + PreferencesKey.currentBitcoinElectrumSererIdKey,
374 + cakeWalletElectrum.key as int);
375 }
323 -}
376
377 + if (currentLitecoinElectrumServer == null) {
378 + final cakeWalletElectrum =
379 + Node(uri: cakeWalletLitecoinElectrumUri, type: WalletType.litecoin);
380 + await nodeSource.add(cakeWalletElectrum);
381 + await sharedPreferences.setInt(
382 + PreferencesKey.currentLitecoinElectrumSererIdKey,
383 + cakeWalletElectrum.key as int);
384 + }
385 +}
386
326 -Future<void> resetElectrumServer(Box<Node> nodeSource, SharedPreferences sharedPreferences) async {
327 - final currentElectrumSeverId = sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey);
328 - final oldElectrumServer = nodeSource.values.firstWhere((node) => node.uri.contains('electrumx.cakewallet.com'), orElse: () => null);
329 - var cakeWalletNode = nodeSource.values.firstWhere((node) => node.uri == cakeWalletElectrumUri, orElse: () => null);
387 +Future<void> resetBitcoinElectrumServer(
388 + Box<Node> nodeSource, SharedPreferences sharedPreferences) async {
389 + final currentElectrumSeverId =
390 + sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey);
391 + final oldElectrumServer = nodeSource.values.firstWhere(
392 + (node) => node.uri.contains('electrumx.cakewallet.com'),
393 + orElse: () => null);
394 + var cakeWalletNode = nodeSource.values.firstWhere(
395 + (node) => node.uri == cakeWalletBitcoinElectrumUri,
396 + orElse: () => null);
397
398 if (cakeWalletNode == null) {
332 - cakeWalletNode = Node(uri: cakeWalletElectrumUri, type: WalletType.bitcoin);
399 + cakeWalletNode =
400 + Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin);
401 await nodeSource.add(cakeWalletNode);
402 }
403
404 if (currentElectrumSeverId == oldElectrumServer?.key) {
337 - await sharedPreferences.setInt(PreferencesKey.currentBitcoinElectrumSererIdKey, cakeWalletNode.key as int);
405 + await sharedPreferences.setInt(
406 + PreferencesKey.currentBitcoinElectrumSererIdKey,
407 + cakeWalletNode.key as int);
408 }
409
410 await oldElectrumServer?.delete();
lib/entities/fs_migration.dart
+6 -8
@@ -1,7 +1,10 @@
1 import 'dart:io';
2 import 'dart:convert';
3 +import 'package:flutter_secure_storage/flutter_secure_storage.dart';
4 +import 'package:shared_preferences/shared_preferences.dart';
5 +import 'package:hive/hive.dart';
6 +import 'package:path_provider/path_provider.dart';
7 import 'package:cake_wallet/core/key_service.dart';
4 -import 'package:cake_wallet/di.dart';
8 import 'package:cake_wallet/entities/contact.dart';
9 import 'package:cake_wallet/entities/crypto_currency.dart';
10 import 'package:cake_wallet/entities/encrypt.dart';
@@ -14,11 +17,6 @@ import 'package:cake_wallet/entities/wallet_info.dart';
17 import 'package:cake_wallet/entities/wallet_type.dart';
18 import 'package:cake_wallet/exchange/exchange_provider_description.dart';
19 import 'package:cake_wallet/exchange/trade.dart';
17 -import 'package:flutter_secure_storage/flutter_secure_storage.dart';
18 -import 'package:shared_preferences/shared_preferences.dart';
19 -import 'package:flutter/foundation.dart';
20 -import 'package:hive/hive.dart';
21 -import 'package:path_provider/path_provider.dart';
20 import 'package:cake_wallet/.secrets.g.dart' as secrets;
21
22 const reservedNames = ["flutter_assets", "wallets", "db"];
@@ -407,7 +405,7 @@ Future<void> ios_migrate_address_book(Box<Contact> contactSource) async {
405 }
406
407 final List<dynamic> addresses =
410 - json.decode(addressBookJSON.readAsStringSync()) as List<dynamic>;
408 + json.decode(addressBookJSON.readAsStringSync()) as List<dynamic>;
409 final contacts = addresses.map((dynamic item) {
410 final _item = item as Map<String, dynamic>;
411 final type = _item["type"] as String;
@@ -420,7 +418,7 @@ Future<void> ios_migrate_address_book(Box<Contact> contactSource) async {
418
419 await contactSource.addAll(contacts);
420 await prefs.setBool('ios_migration_address_book_completed', true);
423 - } catch(e) {
421 + } catch (e) {
422 print(e.toString());
423 }
424 }
lib/entities/node_list.dart
+23 -4
@@ -20,9 +20,9 @@ Future<List<Node>> loadDefaultNodes() async {
20 }).toList();
21 }
22
23 -Future<List<Node>> loadElectrumServerList() async {
23 +Future<List<Node>> loadBitcoinElectrumServerList() async {
24 final serverListRaw =
25 - await rootBundle.loadString('assets/electrum_server_list.yml');
25 + await rootBundle.loadString('assets/bitcoin_electrum_server_list.yml');
26 final serverList = loadYaml(serverListRaw) as YamlList;
27
28 return serverList.map((dynamic raw) {
@@ -37,10 +37,29 @@ Future<List<Node>> loadElectrumServerList() async {
37 }).toList();
38 }
39
40 +Future<List<Node>> loadLitecoinElectrumServerList() async {
41 + final serverListRaw =
42 + await rootBundle.loadString('assets/litecoin_electrum_server_list.yml');
43 + final serverList = loadYaml(serverListRaw) as YamlList;
44 +
45 + return serverList.map((dynamic raw) {
46 + if (raw is Map) {
47 + final node = Node.fromMap(raw);
48 + node?.type = WalletType.litecoin;
49 +
50 + return node;
51 + }
52 +
53 + return null;
54 + }).toList();
55 +}
56 +
57 Future resetToDefault(Box<Node> nodeSource) async {
58 final moneroNodes = await loadDefaultNodes();
42 - final bitcoinElectrumServerList = await loadElectrumServerList();
43 - final nodes = moneroNodes + bitcoinElectrumServerList;
59 + final bitcoinElectrumServerList = await loadBitcoinElectrumServerList();
60 + final litecoinElectrumServerList = await loadLitecoinElectrumServerList();
61 + final nodes =
62 + moneroNodes + bitcoinElectrumServerList + litecoinElectrumServerList;
63
64 await nodeSource.clear();
65 await nodeSource.addAll(nodes);
lib/entities/preferences_key.dart
+6 -4
@@ -1,8 +1,9 @@
1 class PreferencesKey {
2 - static const currentWalletType ='current_wallet_type';
3 - static const currentWalletName ='current_wallet_name';
2 + static const currentWalletType = 'current_wallet_type';
3 + static const currentWalletName = 'current_wallet_name';
4 static const currentNodeIdKey = 'current_node_id';
5 static const currentBitcoinElectrumSererIdKey = 'current_node_id_btc';
6 + static const currentLitecoinElectrumSererIdKey = 'current_node_id_ltc';
7 static const currentFiatCurrencyKey = 'current_fiat_currency';
8 static const currentTransactionPriorityKeyLegacy = 'current_fee_priority';
9 static const currentBalanceDisplayModeKey = 'current_balance_display_mode';
@@ -14,7 +15,8 @@ class PreferencesKey {
15 static const displayActionListModeKey = 'display_list_mode';
16 static const currentPinLength = 'current_pin_length';
17 static const currentLanguageCode = 'language_code';
17 - static const currentDefaultSettingsMigrationVersion = 'current_default_settings_migration_version';
18 + static const currentDefaultSettingsMigrationVersion =
19 + 'current_default_settings_migration_version';
20 static const moneroTransactionPriority = 'current_fee_priority_monero';
21 static const bitcoinTransactionPriority = 'current_fee_priority_bitcoin';
20 -}
\ No newline at end of file
22 +}
lib/entities/wallet_type.dart
+19 -2
@@ -3,7 +3,11 @@ import 'package:hive/hive.dart';
3
4 part 'wallet_type.g.dart';
5
6 -const walletTypes = [WalletType.monero, WalletType.bitcoin];
6 +const walletTypes = [
7 + WalletType.monero,
8 + WalletType.bitcoin,
9 + WalletType.litecoin
10 +];
11 const walletTypeTypeId = 5;
12
13 @HiveType(typeId: walletTypeTypeId)
@@ -15,7 +19,10 @@ enum WalletType {
19 none,
20
21 @HiveField(2)
18 - bitcoin
22 + bitcoin,
23 +
24 + @HiveField(3)
25 + litecoin
26 }
27
28 int serializeToInt(WalletType type) {
@@ -24,6 +31,8 @@ int serializeToInt(WalletType type) {
31 return 0;
32 case WalletType.bitcoin:
33 return 1;
34 + case WalletType.litecoin:
35 + return 2;
36 default:
37 return -1;
38 }
@@ -35,6 +44,8 @@ WalletType deserializeFromInt(int raw) {
44 return WalletType.monero;
45 case 1:
46 return WalletType.bitcoin;
47 + case 2:
48 + return WalletType.litecoin;
49 default:
50 return null;
51 }
@@ -46,6 +57,8 @@ String walletTypeToString(WalletType type) {
57 return 'Monero';
58 case WalletType.bitcoin:
59 return 'Bitcoin';
60 + case WalletType.litecoin:
61 + return 'Litecoin';
62 default:
63 return '';
64 }
@@ -57,6 +70,8 @@ String walletTypeToDisplayName(WalletType type) {
70 return 'Monero';
71 case WalletType.bitcoin:
72 return 'Bitcoin (Electrum)';
73 + case WalletType.litecoin:
74 + return 'Litecoin';
75 default:
76 return '';
77 }
@@ -68,6 +83,8 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type) {
83 return CryptoCurrency.xmr;
84 case WalletType.bitcoin:
85 return CryptoCurrency.btc;
86 + case WalletType.litecoin:
87 + return CryptoCurrency.ltc;
88 default:
89 return null;
90 }
lib/main.dart
+2 -2
@@ -107,7 +107,7 @@ Future<void> main() async {
107 exchangeTemplates: exchangeTemplates,
108 transactionDescriptions: transactionDescriptions,
109 secureStorage: secureStorage,
110 - initialMigrationVersion: 13);
110 + initialMigrationVersion: 15);
111 runApp(App());
112 } catch (e) {
113 runApp(MaterialApp(
@@ -135,7 +135,7 @@ Future<void> initialSetup(
135 @required Box<ExchangeTemplate> exchangeTemplates,
136 @required Box<TransactionDescription> transactionDescriptions,
137 FlutterSecureStorage secureStorage,
138 - int initialMigrationVersion = 13}) async {
138 + int initialMigrationVersion = 15}) async {
139 LanguageService.loadLocaleList();
140 await defaultSettingsMigration(
141 secureStorage: secureStorage,
lib/monero/monero_account_list.dart
+2 -2
@@ -49,13 +49,13 @@ abstract class MoneroAccountListBase with Store {
49
50 Future addAccount({String label}) async {
51 await account_list.addAccount(label: label);
52 - await update();
52 + update();
53 }
54
55 Future setLabelAccount({int accountIndex, String label}) async {
56 await account_list.setLabelForAccount(
57 accountIndex: accountIndex, label: label);
58 - await update();
58 + update();
59 }
60
61 void refresh() {
lib/monero/monero_transaction_history.dart
+5 -30
@@ -1,18 +1,10 @@
1 import 'dart:core';
2 import 'package:mobx/mobx.dart';
3 -import 'package:cw_monero/transaction_history.dart'
4 - as monero_transaction_history;
3 import 'package:cake_wallet/core/transaction_history.dart';
4 import 'package:cake_wallet/monero/monero_transaction_info.dart';
5
6 part 'monero_transaction_history.g.dart';
7
10 -List<MoneroTransactionInfo> _getAllTransactions(dynamic _) =>
11 - monero_transaction_history
12 - .getAllTransations()
13 - .map((row) => MoneroTransactionInfo.fromRow(row))
14 - .toList();
15 -
8 class MoneroTransactionHistory = MoneroTransactionHistoryBase
9 with _$MoneroTransactionHistory;
10
@@ -23,30 +15,13 @@ abstract class MoneroTransactionHistoryBase
15 }
16
17 @override
26 - Future<Map<String, MoneroTransactionInfo>> fetchTransactions() async {
27 - monero_transaction_history.refreshTransactions();
28 - return _getAllTransactions(null).fold<Map<String, MoneroTransactionInfo>>(
29 - <String, MoneroTransactionInfo>{},
30 - (Map<String, MoneroTransactionInfo> acc, MoneroTransactionInfo tx) {
31 - acc[tx.id] = tx;
32 - return acc;
33 - });
34 - }
18 + Future<void> save() async {}
19
20 @override
37 - @action
38 - void updateAsync({void Function() onFinished}) {
39 - fetchTransactionsAsync(
40 - (transaction) => transactions[transaction.id] = transaction,
41 - onFinished: onFinished);
42 - }
21 + void addOne(MoneroTransactionInfo transaction) =>
22 + transactions[transaction.id] = transaction;
23
24 @override
45 - void fetchTransactionsAsync(
46 - void Function(MoneroTransactionInfo transaction) onTransactionLoaded,
47 - {void Function() onFinished}) async {
48 - final transactions = await fetchTransactions();
49 - transactions.values.forEach((tx) => onTransactionLoaded(tx));
50 - onFinished?.call();
51 - }
25 + void addMany(Map<String, MoneroTransactionInfo> transactions) =>
26 + this.transactions.addAll(transactions);
27 }
lib/monero/monero_transaction_info.dart
+1
@@ -59,6 +59,7 @@ class MoneroTransactionInfo extends TransactionInfo {
59 @override
60 void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
61
62 + @override
63 String feeFormatted() =>
64 '${formatAmount(moneroAmountToString(amount: fee))} XMR';
65 }
lib/monero/monero_wallet.dart
+59 -15
@@ -1,10 +1,12 @@
1 import 'dart:async';
2 -
2 import 'package:cake_wallet/entities/transaction_priority.dart';
3 import 'package:cake_wallet/monero/monero_amount_format.dart';
4 import 'package:cake_wallet/monero/monero_transaction_creation_exception.dart';
5 +import 'package:cake_wallet/monero/monero_transaction_info.dart';
6 import 'package:flutter/foundation.dart';
7 import 'package:mobx/mobx.dart';
8 +import 'package:cw_monero/transaction_history.dart'
9 + as monero_transaction_history;
10 import 'package:cw_monero/wallet.dart';
11 import 'package:cw_monero/wallet.dart' as monero_wallet;
12 import 'package:cw_monero/transaction_history.dart' as transaction_history;
@@ -30,19 +32,21 @@ const moneroBlockSize = 1000;
32
33 class MoneroWallet = MoneroWalletBase with _$MoneroWallet;
34
33 -abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
34 - MoneroWalletBase({String filename, WalletInfo walletInfo})
35 - : transactionHistory = MoneroTransactionHistory(),
36 - accountList = MoneroAccountList(),
35 +abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
36 + MoneroTransactionHistory, MoneroTransactionInfo> with Store {
37 + MoneroWalletBase({WalletInfo walletInfo})
38 + : accountList = MoneroAccountList(),
39 subaddressList = MoneroSubaddressList(),
40 super(walletInfo) {
39 - _filename = filename;
41 + transactionHistory = MoneroTransactionHistory();
42 balance = MoneroBalance(
43 fullBalance: monero_wallet.getFullBalance(accountIndex: 0),
44 unlockedBalance: monero_wallet.getFullBalance(accountIndex: 0));
45 _lastAutosaveTimestamp = 0;
46 + _lastSaveTimestamp = 0;
47 _isSavingAfterSync = false;
48 _isSavingAfterNewTransaction = false;
49 + _isTransactionUpdating = false;
50 _onAccountChangeReaction = reaction((_) => account, (Account account) {
51 balance = MoneroBalance(
52 fullBalance: monero_wallet.getFullBalance(accountIndex: account.id),
@@ -56,9 +60,6 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
60
61 static const int _autoAfterSyncSaveInterval = 60000;
62
59 - @override
60 - final MoneroTransactionHistory transactionHistory;
61 -
63 @observable
64 Account account;
65
@@ -91,12 +92,13 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
92
93 final MoneroAccountList accountList;
94
94 - String _filename;
95 SyncListener _listener;
96 ReactionDisposer _onAccountChangeReaction;
97 int _lastAutosaveTimestamp;
98 bool _isSavingAfterSync;
99 bool _isSavingAfterNewTransaction;
100 + bool _isTransactionUpdating;
101 + int _lastSaveTimestamp;
102
103 Future<void> init() async {
104 accountList.update();
@@ -111,7 +113,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
113 monero_wallet.getUnlockedBalance(accountIndex: account.id));
114 address = subaddress.address;
115 _setListeners();
114 - await transactionHistory.update();
116 + await updateTransactions();
117
118 if (walletInfo.isRecovery) {
119 monero_wallet.setRecoveringFromSeed(isRecovery: walletInfo.isRecovery);
@@ -238,6 +240,13 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
240
241 @override
242 Future<void> save() async {
243 + final now = DateTime.now().millisecondsSinceEpoch;
244 +
245 + if (now - _lastSaveTimestamp < Duration(seconds: 10).inMilliseconds) {
246 + return;
247 + }
248 +
249 + _lastSaveTimestamp = now;
250 await monero_wallet.store();
251 }
252
@@ -264,6 +273,40 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
273 await walletInfo.save();
274 }
275
276 + @override
277 + Future<Map<String, MoneroTransactionInfo>> fetchTransactions() async {
278 + monero_transaction_history.refreshTransactions();
279 + return _getAllTransactions(null).fold<Map<String, MoneroTransactionInfo>>(
280 + <String, MoneroTransactionInfo>{},
281 + (Map<String, MoneroTransactionInfo> acc, MoneroTransactionInfo tx) {
282 + acc[tx.id] = tx;
283 + return acc;
284 + });
285 + }
286 +
287 + Future<void> updateTransactions() async {
288 + try {
289 + if (_isTransactionUpdating) {
290 + return;
291 + }
292 +
293 + _isTransactionUpdating = true;
294 + final transactions = await fetchTransactions();
295 + transactionHistory.addMany(transactions);
296 + await transactionHistory.save();
297 + _isTransactionUpdating = false;
298 + } catch (e) {
299 + print(e);
300 + _isTransactionUpdating = false;
301 + }
302 + }
303 +
304 + List<MoneroTransactionInfo> _getAllTransactions(dynamic _) =>
305 + monero_transaction_history
306 + .getAllTransations()
307 + .map((row) => MoneroTransactionInfo.fromRow(row))
308 + .toList();
309 +
310 void _setListeners() {
311 _listener?.stop();
312 _listener = monero_wallet.setListeners(_onNewBlock, _onNewTransaction);
@@ -315,7 +358,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
358 }
359
360 Future<void> _askForUpdateTransactionHistory() async =>
318 - await transactionHistory.update();
361 + await updateTransactions();
362
363 int _getFullBalance() =>
364 monero_wallet.getFullBalance(accountIndex: account.id);
@@ -389,11 +432,12 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
432 }
433 }
434
392 - void _onNewTransaction() {
435 + void _onNewTransaction() async {
436 try {
394 - _askForUpdateTransactionHistory();
437 + await _askForUpdateTransactionHistory();
438 _askForUpdateBalance();
396 - Timer(Duration(seconds: 1), () => _afterNewTransactionSave());
439 + await Future<void>.delayed(Duration(seconds: 1));
440 + await _afterNewTransactionSave();
441 } catch (e) {
442 print(e.toString());
443 }
lib/monero/monero_wallet_service.dart
+15 -22
@@ -68,18 +68,18 @@ class MoneroWalletService extends WalletService<
68 static bool walletFilesExist(String path) =>
69 !File(path).existsSync() && !File('$path.keys').existsSync();
70
71 + @override
72 + WalletType getType() => WalletType.monero;
73 +
74 @override
75 Future<MoneroWallet> create(MoneroNewWalletCredentials credentials) async {
76 try {
74 - final path =
75 - await pathForWallet(name: credentials.name, type: WalletType.monero);
77 + final path = await pathForWallet(name: credentials.name, type: getType());
78 await monero_wallet_manager.createWallet(
79 path: path,
80 password: credentials.password,
81 language: credentials.language);
80 - final wallet = MoneroWallet(
81 - filename: monero_wallet.getFilename(),
82 - walletInfo: credentials.walletInfo);
82 + final wallet = MoneroWallet(walletInfo: credentials.walletInfo);
83 await wallet.init();
84
85 return wallet;
@@ -93,7 +93,7 @@ class MoneroWalletService extends WalletService<
93 @override
94 Future<bool> isWalletExit(String name) async {
95 try {
96 - final path = await pathForWallet(name: name, type: WalletType.monero);
96 + final path = await pathForWallet(name: name, type: getType());
97 return monero_wallet_manager.isWalletExist(path: path);
98 } catch (e) {
99 // TODO: Implement Exception for wallet list service.
@@ -105,7 +105,7 @@ class MoneroWalletService extends WalletService<
105 @override
106 Future<MoneroWallet> openWallet(String name, String password) async {
107 try {
108 - final path = await pathForWallet(name: name, type: WalletType.monero);
108 + final path = await pathForWallet(name: name, type: getType());
109
110 if (walletFilesExist(path)) {
111 await repairOldAndroidWallet(name);
@@ -114,10 +114,9 @@ class MoneroWalletService extends WalletService<
114 await monero_wallet_manager
115 .openWalletAsync({'path': path, 'password': password});
116 final walletInfo = walletInfoSource.values.firstWhere(
117 - (info) => info.id == WalletBase.idFor(name, WalletType.monero),
117 + (info) => info.id == WalletBase.idFor(name, getType()),
118 orElse: () => null);
119 - final wallet = MoneroWallet(
120 - filename: monero_wallet.getFilename(), walletInfo: walletInfo);
119 + final wallet = MoneroWallet(walletInfo: walletInfo);
120 final isValid = wallet.validate();
121
122 if (!isValid) {
@@ -146,7 +145,7 @@ class MoneroWalletService extends WalletService<
145
146 @override
147 Future<void> remove(String wallet) async {
149 - final path = await pathForWalletDir(name: wallet, type: WalletType.monero);
148 + final path = await pathForWalletDir(name: wallet, type: getType());
149 final file = Directory(path);
150 final isExist = file.existsSync();
151
@@ -159,8 +158,7 @@ class MoneroWalletService extends WalletService<
158 Future<MoneroWallet> restoreFromKeys(
159 MoneroRestoreWalletFromKeysCredentials credentials) async {
160 try {
162 - final path =
163 - await pathForWallet(name: credentials.name, type: WalletType.monero);
161 + final path = await pathForWallet(name: credentials.name, type: getType());
162 await monero_wallet_manager.restoreFromKeys(
163 path: path,
164 password: credentials.password,
@@ -169,9 +167,7 @@ class MoneroWalletService extends WalletService<
167 address: credentials.address,
168 viewKey: credentials.viewKey,
169 spendKey: credentials.spendKey);
172 - final wallet = MoneroWallet(
173 - filename: monero_wallet.getFilename(),
174 - walletInfo: credentials.walletInfo);
170 + final wallet = MoneroWallet(walletInfo: credentials.walletInfo);
171 await wallet.init();
172
173 return wallet;
@@ -186,16 +182,13 @@ class MoneroWalletService extends WalletService<
182 Future<MoneroWallet> restoreFromSeed(
183 MoneroRestoreWalletFromSeedCredentials credentials) async {
184 try {
189 - final path =
190 - await pathForWallet(name: credentials.name, type: WalletType.monero);
185 + final path = await pathForWallet(name: credentials.name, type: getType());
186 await monero_wallet_manager.restoreFromSeed(
187 path: path,
188 password: credentials.password,
189 seed: credentials.mnemonic,
190 restoreHeight: credentials.height);
196 - final wallet = MoneroWallet(
197 - filename: monero_wallet.getFilename(),
198 - walletInfo: credentials.walletInfo);
191 + final wallet = MoneroWallet(walletInfo: credentials.walletInfo);
192 await wallet.init();
193
194 return wallet;
@@ -221,7 +214,7 @@ class MoneroWalletService extends WalletService<
214 }
215
216 final newWalletDirPath =
224 - await pathForWalletDir(name: name, type: WalletType.monero);
217 + await pathForWalletDir(name: name, type: getType());
218
219 dir.listSync().forEach((f) {
220 final file = File(f.path);
lib/reactions/on_current_wallet_change.dart
+9 -3
@@ -1,4 +1,6 @@
1 +import 'package:cake_wallet/core/transaction_history.dart';
2 import 'package:cake_wallet/entities/balance.dart';
3 +import 'package:cake_wallet/entities/transaction_info.dart';
4 import 'package:mobx/mobx.dart';
5 import 'package:shared_preferences/shared_preferences.dart';
6 import 'package:cake_wallet/di.dart';
@@ -18,9 +20,11 @@ ReactionDisposer _onCurrentWalletChangeFiatRateUpdateReaction;
20 void startCurrentWalletChangeReaction(AppStore appStore,
21 SettingsStore settingsStore, FiatConversionStore fiatConversionStore) {
22 _onCurrentWalletChangeReaction?.reaction?.dispose();
23 + _onCurrentWalletChangeFiatRateUpdateReaction?.reaction?.dispose();
24
22 - _onCurrentWalletChangeReaction =
23 - reaction((_) => appStore.wallet, (WalletBase<Balance> wallet) async {
25 + _onCurrentWalletChangeReaction = reaction((_) => appStore.wallet, (WalletBase<
26 + Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>
27 + wallet) async {
28 try {
29 final node = settingsStore.getCurrentNode(wallet.type);
30 startWalletSyncStatusChangeReaction(wallet);
@@ -45,7 +49,9 @@ void startCurrentWalletChangeReaction(AppStore appStore,
49 });
50
51 _onCurrentWalletChangeFiatRateUpdateReaction =
48 - reaction((_) => appStore.wallet, (WalletBase<Balance> wallet) async {
52 + reaction((_) => appStore.wallet, (WalletBase<Balance,
53 + TransactionHistoryBase<TransactionInfo>, TransactionInfo>
54 + wallet) async {
55 try {
56 fiatConversionStore.prices[wallet.currency] = 0;
57 fiatConversionStore.prices[wallet.currency] =
lib/reactions/on_wallet_sync_status_change.dart
+12 -7
@@ -1,16 +1,21 @@
1 -import 'package:cake_wallet/entities/balance.dart';
1 import 'package:mobx/mobx.dart';
2 +import 'package:cake_wallet/core/transaction_history.dart';
3 import 'package:cake_wallet/core/wallet_base.dart';
4 +import 'package:cake_wallet/entities/balance.dart';
5 +import 'package:cake_wallet/entities/transaction_info.dart';
6 import 'package:cake_wallet/entities/sync_status.dart';
7
8 ReactionDisposer _onWalletSyncStatusChangeReaction;
9
8 -void startWalletSyncStatusChangeReaction(WalletBase<Balance> wallet) {
10 +void startWalletSyncStatusChangeReaction(
11 + WalletBase<Balance, TransactionHistoryBase<TransactionInfo>,
12 + TransactionInfo>
13 + wallet) {
14 _onWalletSyncStatusChangeReaction?.reaction?.dispose();
15 _onWalletSyncStatusChangeReaction =
16 reaction((_) => wallet.syncStatus, (SyncStatus status) async {
12 - if (status is ConnectedSyncStatus) {
13 - await wallet.startSync();
14 - }
15 - });
16 -}
\ No newline at end of file
17 + if (status is ConnectedSyncStatus) {
18 + await wallet.startSync();
19 + }
20 + });
21 +}
lib/src/screens/dashboard/widgets/menu_widget.dart
+4
@@ -21,6 +21,7 @@ class MenuWidget extends StatefulWidget {
21 class MenuWidgetState extends State<MenuWidget> {
22 Image moneroIcon;
23 Image bitcoinIcon;
24 + Image litecoinIcon;
25 final largeScreen = 731;
26
27 double menuWidth;
@@ -76,6 +77,7 @@ class MenuWidgetState extends State<MenuWidget> {
77 color: Theme.of(context).accentTextTheme.overline.decorationColor);
78 bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png',
79 color: Theme.of(context).accentTextTheme.overline.decorationColor);
80 + litecoinIcon = Image.asset('assets/images/litecoin_menu.png');
81
82 return Row(
83 mainAxisSize: MainAxisSize.max,
@@ -238,6 +240,8 @@ class MenuWidgetState extends State<MenuWidget> {
240 return moneroIcon;
241 case WalletType.bitcoin:
242 return bitcoinIcon;
243 + case WalletType.litecoin:
244 + return litecoinIcon;
245 default:
246 return null;
247 }
lib/src/screens/new_wallet/new_wallet_type_page.dart
+13 -10
@@ -59,6 +59,8 @@ class WalletTypeFormState extends State<WalletTypeForm> {
59 Image.asset('assets/images/monero_logo.png', height: 24, width: 24);
60 final bitcoinIcon =
61 Image.asset('assets/images/bitcoin.png', height: 24, width: 24);
62 + final litecoinIcon =
63 + Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24);
64 final walletTypeImage = Image.asset('assets/images/wallet_type.png');
65 final walletTypeLightImage =
66 Image.asset('assets/images/wallet_type_light.png');
@@ -69,7 +71,7 @@ class WalletTypeFormState extends State<WalletTypeForm> {
71
72 @override
73 void initState() {
72 - types = [WalletType.bitcoin, WalletType.monero];
74 + types = [WalletType.bitcoin, WalletType.monero, WalletType.litecoin];
75 super.initState();
76 }
77
@@ -84,8 +86,7 @@ class WalletTypeFormState extends State<WalletTypeForm> {
86 padding: EdgeInsets.only(left: 12, right: 12),
87 child: AspectRatio(
88 aspectRatio: aspectRatioImage,
87 - child:
88 - FittedBox(child: widget.walletImage, fit: BoxFit.fill)),
89 + child: FittedBox(child: widget.walletImage, fit: BoxFit.fill)),
90 ),
91 Padding(
92 padding: EdgeInsets.only(top: 48),
@@ -99,13 +100,13 @@ class WalletTypeFormState extends State<WalletTypeForm> {
100 ),
101 ),
102 ...types.map((type) => Padding(
102 - padding: EdgeInsets.only(top: 24),
103 - child: SelectButton(
104 - image: _iconFor(type),
105 - text: walletTypeToDisplayName(type),
106 - isSelected: selected == type,
107 - onTap: () => setState(() => selected = type)),
108 - ))
103 + padding: EdgeInsets.only(top: 24),
104 + child: SelectButton(
105 + image: _iconFor(type),
106 + text: walletTypeToDisplayName(type),
107 + isSelected: selected == type,
108 + onTap: () => setState(() => selected = type)),
109 + ))
110 ],
111 ),
112 bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
@@ -125,6 +126,8 @@ class WalletTypeFormState extends State<WalletTypeForm> {
126 return moneroIcon;
127 case WalletType.bitcoin:
128 return bitcoinIcon;
129 + case WalletType.litecoin:
130 + return litecoinIcon;
131 default:
132 return null;
133 }
lib/src/screens/wallet_list/wallet_list_page.dart
+4
@@ -40,6 +40,8 @@ class WalletListBodyState extends State<WalletListBody> {
40 Image.asset('assets/images/monero_logo.png', height: 24, width: 24);
41 final bitcoinIcon =
42 Image.asset('assets/images/bitcoin.png', height: 24, width: 24);
43 + final litecoinIcon =
44 + Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24);
45 final scrollController = ScrollController();
46 final double tileHeight = 60;
47 Flushbar<void> _progressBar;
@@ -193,6 +195,8 @@ class WalletListBodyState extends State<WalletListBody> {
195 return bitcoinIcon;
196 case WalletType.monero:
197 return moneroIcon;
198 + case WalletType.litecoin:
199 + return litecoinIcon;
200 default:
201 return null;
202 }
lib/store/app_store.dart
+9 -3
@@ -1,6 +1,8 @@
1 -import 'package:cake_wallet/entities/balance.dart';
1 +import 'package:cake_wallet/entities/transaction_info.dart';
2 import 'package:mobx/mobx.dart';
3 +import 'package:cake_wallet/entities/balance.dart';
4 import 'package:cake_wallet/core/wallet_base.dart';
5 +import 'package:cake_wallet/core/transaction_history.dart';
6 import 'package:cake_wallet/store/wallet_list_store.dart';
7 import 'package:cake_wallet/store/authentication_store.dart';
8 import 'package:cake_wallet/store/settings_store.dart';
@@ -20,7 +22,8 @@ abstract class AppStoreBase with Store {
22 AuthenticationStore authenticationStore;
23
24 @observable
23 - WalletBase<Balance> wallet;
25 + WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>
26 + wallet;
27
28 WalletListStore walletList;
29
@@ -29,7 +32,10 @@ abstract class AppStoreBase with Store {
32 NodeListStore nodeListStore;
33
34 @action
32 - void changeCurrentWallet(WalletBase wallet) {
35 + void changeCurrentWallet(
36 + WalletBase<Balance, TransactionHistoryBase<TransactionInfo>,
37 + TransactionInfo>
38 + wallet) {
39 this.wallet?.close();
40 this.wallet = wallet;
41 }
lib/store/settings_store.dart
+10 -2
@@ -149,7 +149,7 @@ abstract class SettingsStoreBase with Store {
149
150 static Future<SettingsStore> load(
151 {@required Box<Node> nodeSource,
152 - @required bool isBitcoinBuyEnabled,
152 + @required bool isBitcoinBuyEnabled,
153 FiatCurrency initialFiatCurrency = FiatCurrency.usd,
154 MoneroTransactionPriority initialMoneroTransactionPriority =
155 MoneroTransactionPriority.slow,
@@ -205,15 +205,19 @@ abstract class SettingsStoreBase with Store {
205 final nodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey);
206 final bitcoinElectrumServerId = sharedPreferences
207 .getInt(PreferencesKey.currentBitcoinElectrumSererIdKey);
208 + final litecoinElectrumServerId = sharedPreferences
209 + .getInt(PreferencesKey.currentLitecoinElectrumSererIdKey);
210 final moneroNode = nodeSource.get(nodeId);
211 final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId);
212 + final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId);
213 final packageInfo = await PackageInfo.fromPlatform();
214
215 return SettingsStore(
216 sharedPreferences: sharedPreferences,
217 nodes: {
218 WalletType.monero: moneroNode,
216 - WalletType.bitcoin: bitcoinElectrumServer
219 + WalletType.bitcoin: bitcoinElectrumServer,
220 + WalletType.litecoin: litecoinElectrumServer
221 },
222 appVersion: packageInfo.version,
223 isBitcoinBuyEnabled: isBitcoinBuyEnabled,
@@ -263,6 +267,10 @@ abstract class SettingsStoreBase with Store {
267 await _sharedPreferences.setInt(
268 PreferencesKey.currentBitcoinElectrumSererIdKey, node.key as int);
269 break;
270 + case WalletType.litecoin:
271 + await _sharedPreferences.setInt(
272 + PreferencesKey.currentLitecoinElectrumSererIdKey, node.key as int);
273 + break;
274 case WalletType.monero:
275 await _sharedPreferences.setInt(
276 PreferencesKey.currentNodeIdKey, node.key as int);
lib/view_model/dashboard/balance_view_model.dart
+12 -11
@@ -1,7 +1,9 @@
1 import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
2 +import 'package:cake_wallet/core/transaction_history.dart';
3 import 'package:cake_wallet/core/wallet_base.dart';
4 import 'package:cake_wallet/entities/balance.dart';
5 import 'package:cake_wallet/entities/crypto_currency.dart';
6 +import 'package:cake_wallet/entities/transaction_info.dart';
7 import 'package:cake_wallet/entities/wallet_type.dart';
8 import 'package:cake_wallet/generated/i18n.dart';
9 import 'package:cake_wallet/monero/monero_wallet.dart';
@@ -23,11 +25,7 @@ abstract class BalanceViewModelBase with Store {
25 @required this.settingsStore,
26 @required this.fiatConvertationStore}) {
27 isReversing = false;
26 -
28 wallet ??= appStore.wallet;
28 -
29 - _reaction = reaction((_) => appStore.wallet, _onWalletChange);
30 -
29 final _wallet = wallet;
30
31 if (_wallet is MoneroWallet) {
@@ -38,6 +36,8 @@ abstract class BalanceViewModelBase with Store {
36 balance = _wallet.balance;
37 }
38
39 + reaction((_) => appStore.wallet, _onWalletChange);
40 +
41 _onCurrentWalletChangeReaction =
42 reaction<void>((_) => wallet.balance, (dynamic balance) {
43 if (balance is Balance) {
@@ -59,7 +59,8 @@ abstract class BalanceViewModelBase with Store {
59 Balance balance;
60
61 @observable
62 - WalletBase<Balance> wallet;
62 + WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>
63 + wallet;
64
65 @computed
66 double get price => fiatConvertationStore.prices[appStore.wallet.currency];
@@ -70,8 +71,8 @@ abstract class BalanceViewModelBase with Store {
71 @computed
72 BalanceDisplayMode get displayMode => isReversing
73 ? savedDisplayMode == BalanceDisplayMode.hiddenBalance
73 - ? BalanceDisplayMode.displayableBalance
74 - : savedDisplayMode
74 + ? BalanceDisplayMode.displayableBalance
75 + : savedDisplayMode
76 : savedDisplayMode;
77
78 @computed
@@ -153,14 +154,14 @@ abstract class BalanceViewModelBase with Store {
154 CryptoCurrency get currency => appStore.wallet.currency;
155
156 ReactionDisposer _onCurrentWalletChangeReaction;
156 - ReactionDisposer _reaction;
157
158 @action
159 - void _onWalletChange(WalletBase<Balance> wallet) {
159 + void _onWalletChange(
160 + WalletBase<Balance, TransactionHistoryBase<TransactionInfo>,
161 + TransactionInfo>
162 + wallet) {
163 this.wallet = wallet;
161 -
164 balance = wallet.balance;
163 -
165 _onCurrentWalletChangeReaction?.reaction?.dispose();
166 _onCurrentWalletChangeReaction = reaction<Balance>(
167 (_) => wallet.balance, (Balance balance) => this.balance = balance);
lib/view_model/dashboard/dashboard_view_model.dart
+15 -32
@@ -1,23 +1,13 @@
1 -import 'dart:convert';
2 -import 'dart:io';
3 -
4 -import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart';
5 -import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
1 +import 'package:cake_wallet/core/transaction_history.dart';
2 import 'package:cake_wallet/entities/balance.dart';
3 import 'package:cake_wallet/entities/order.dart';
8 -import 'package:cake_wallet/entities/transaction_history.dart';
9 -import 'package:cake_wallet/exchange/trade_state.dart';
4 import 'package:cake_wallet/monero/account.dart';
5 import 'package:cake_wallet/monero/monero_balance.dart';
12 -import 'package:cake_wallet/monero/monero_transaction_history.dart';
6 import 'package:cake_wallet/monero/monero_transaction_info.dart';
7 import 'package:cake_wallet/monero/monero_wallet.dart';
8 import 'package:cake_wallet/entities/balance_display_mode.dart';
16 -import 'package:cake_wallet/entities/crypto_currency.dart';
17 -import 'package:cake_wallet/entities/transaction_direction.dart';
9 import 'package:cake_wallet/entities/transaction_info.dart';
10 import 'package:cake_wallet/exchange/exchange_provider_description.dart';
20 -import 'package:cake_wallet/exchange/trade.dart';
11 import 'package:cake_wallet/store/settings_store.dart';
12 import 'package:cake_wallet/store/dashboard/orders_store.dart';
13 import 'package:cake_wallet/utils/mobx.dart';
@@ -27,12 +17,8 @@ import 'package:cake_wallet/view_model/dashboard/order_list_item.dart';
17 import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart';
18 import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart';
19 import 'package:cake_wallet/view_model/dashboard/action_list_item.dart';
30 -import 'package:cake_wallet/view_model/dashboard/action_list_display_mode.dart';
20 import 'package:cake_wallet/view_model/wyre_view_model.dart';
32 -import 'package:crypto/crypto.dart';
33 -import 'package:flutter/services.dart';
21 import 'package:hive/hive.dart';
35 -import 'package:http/http.dart';
22 import 'package:mobx/mobx.dart';
23 import 'package:cake_wallet/core/wallet_base.dart';
24 import 'package:cake_wallet/entities/sync_status.dart';
@@ -43,8 +29,6 @@ import 'package:cake_wallet/store/dashboard/trades_store.dart';
29 import 'package:cake_wallet/store/dashboard/trade_filter_store.dart';
30 import 'package:cake_wallet/store/dashboard/transaction_filter_store.dart';
31 import 'package:cake_wallet/view_model/dashboard/formatted_item_list.dart';
46 -import 'package:url_launcher/url_launcher.dart';
47 -import 'package:convert/convert.dart';
32
33 part 'dashboard_view_model.g.dart';
34
@@ -100,9 +84,6 @@ abstract class DashboardViewModelBase with Store {
84 name = appStore.wallet?.name;
85 wallet ??= appStore.wallet;
86 type = wallet.type;
103 -
104 - _reaction = reaction((_) => appStore.wallet, _onWalletChange);
105 -
87 final _wallet = wallet;
88
89 if (_wallet is MoneroWallet) {
@@ -133,6 +114,8 @@ abstract class DashboardViewModelBase with Store {
114 settingsStore: appStore.settingsStore)));
115 }
116
117 + reaction((_) => appStore.wallet, _onWalletChange);
118 +
119 connectMapToListWithTransform(
120 appStore.wallet.transactionHistory.transactions,
121 transactions,
@@ -215,7 +198,8 @@ abstract class DashboardViewModelBase with Store {
198 }
199
200 @observable
218 - WalletBase<Balance> wallet;
201 + WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>
202 + wallet;
203
204 bool get hasRescan => wallet.type == WalletType.monero;
205
@@ -241,8 +225,6 @@ abstract class DashboardViewModelBase with Store {
225
226 bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled;
227
244 - ReactionDisposer _reaction;
245 -
228 ReactionDisposer _onMoneroAccountChangeReaction;
229
230 ReactionDisposer _onMoneroBalanceChangeReaction;
@@ -253,7 +235,10 @@ abstract class DashboardViewModelBase with Store {
235 }
236
237 @action
256 - void _onWalletChange(WalletBase<Balance> wallet) {
238 + void _onWalletChange(
239 + WalletBase<Balance, TransactionHistoryBase<TransactionInfo>,
240 + TransactionInfo>
241 + wallet) {
242 this.wallet = wallet;
243 type = wallet.type;
244 name = wallet.name;
@@ -286,17 +271,17 @@ abstract class DashboardViewModelBase with Store {
271 connectMapToListWithTransform(
272 appStore.wallet.transactionHistory.transactions,
273 transactions,
289 - (TransactionInfo val) => TransactionListItem(
274 + (TransactionInfo val) => TransactionListItem(
275 transaction: val,
276 balanceViewModel: balanceViewModel,
277 settingsStore: appStore.settingsStore),
278 filter: (TransactionInfo tx) {
294 - if (tx is MoneroTransactionInfo && wallet is MoneroWallet) {
295 - return tx.accountIndex == wallet.account.id;
296 - }
279 + if (tx is MoneroTransactionInfo && wallet is MoneroWallet) {
280 + return tx.accountIndex == wallet.account.id;
281 + }
282
298 - return true;
299 - });
283 + return true;
284 + });
285 }
286
287 @action
@@ -319,6 +304,4 @@ abstract class DashboardViewModelBase with Store {
304 balanceViewModel: balanceViewModel,
305 settingsStore: appStore.settingsStore)));
306 }
322 -
323 -
307 }
lib/view_model/dashboard/transaction_list_item.dart
+3 -3
@@ -4,7 +4,7 @@ import 'package:cake_wallet/entities/transaction_info.dart';
4 import 'package:cake_wallet/store/settings_store.dart';
5 import 'package:cake_wallet/utils/mobx.dart';
6 import 'package:cake_wallet/view_model/dashboard/action_list_item.dart';
7 -import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart';
7 +import 'package:cake_wallet/bitcoin/electrum_transaction_info.dart';
8 import 'package:cake_wallet/monero/monero_transaction_info.dart';
9 import 'package:cake_wallet/monero/monero_amount_format.dart';
10 import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
@@ -42,7 +42,7 @@ class TransactionListItem extends ActionListItem with Keyable {
42 transaction.changeFiatAmount(amount);
43 }
44
45 - if (transaction is BitcoinTransactionInfo) {
45 + if (transaction is ElectrumTransactionInfo) {
46 final amount = calculateFiatAmountRaw(
47 cryptoAmount: bitcoinAmountToDouble(amount: transaction.amount),
48 price: price);
@@ -56,4 +56,4 @@ class TransactionListItem extends ActionListItem with Keyable {
56
57 @override
58 DateTime get date => transaction.date;
59 -}
\ No newline at end of file
59 +}
lib/view_model/exchange/exchange_view_model.dart
+18 -11
@@ -33,9 +33,7 @@ class ExchangeViewModel = ExchangeViewModelBase with _$ExchangeViewModel;
33 abstract class ExchangeViewModelBase with Store {
34 ExchangeViewModelBase(this.wallet, this.trades, this._exchangeTemplateStore,
35 this.tradesStore, this._settingsStore) {
36 - providerList = [
37 - ChangeNowExchangeProvider()
38 - ];
36 + providerList = [ChangeNowExchangeProvider()];
37
38 _initialPairBasedOnWallet();
39 isDepositAddressEnabled = !(depositCurrency == wallet.currency);
@@ -57,9 +55,11 @@ abstract class ExchangeViewModelBase with Store {
55 _onPairChange();
56 }
57 });
60 - receiveCurrencies = CryptoCurrency.all.where((cryptoCurrency) =>
61 - (cryptoCurrency != CryptoCurrency.xlm)&&
62 - (cryptoCurrency != CryptoCurrency.xrp)).toList();
58 + receiveCurrencies = CryptoCurrency.all
59 + .where((cryptoCurrency) =>
60 + (cryptoCurrency != CryptoCurrency.xlm) &&
61 + (cryptoCurrency != CryptoCurrency.xrp))
62 + .toList();
63 _defineIsReceiveAmountEditable();
64 isFixedRateMode = false;
65 isReceiveAmountEntered = false;
@@ -218,8 +218,10 @@ abstract class ExchangeViewModelBase with Store {
218 limitsState = LimitsIsLoading();
219
220 try {
221 - limits = await provider.fetchLimits(from: depositCurrency,
222 - to: receiveCurrency, isFixedRateMode: isFixedRateMode);
221 + limits = await provider.fetchLimits(
222 + from: depositCurrency,
223 + to: receiveCurrency,
224 + isFixedRateMode: isFixedRateMode);
225 limitsState = LimitsLoadedSuccessfully(limits: limits);
226 } catch (e) {
227 limitsState = LimitsLoadedFailure(error: e.toString());
@@ -283,8 +285,8 @@ abstract class ExchangeViewModelBase with Store {
285 } else {
286 try {
287 tradeState = TradeIsCreating();
286 - final trade = await provider.createTrade(request: request,
287 - isFixedRateMode: isFixedRateMode);
288 + final trade = await provider.createTrade(
289 + request: request, isFixedRateMode: isFixedRateMode);
290 trade.walletId = wallet.id;
291 tradesStore.setTrade(trade);
292 await trades.add(trade);
@@ -320,7 +322,8 @@ abstract class ExchangeViewModelBase with Store {
322 void calculateDepositAllAmount() {
323 if (wallet is BitcoinWallet) {
324 final availableBalance = wallet.balance.available;
323 - final priority = _settingsStore.priority[wallet.type] as BitcoinTransactionPriority;
325 + final priority =
326 + _settingsStore.priority[wallet.type] as BitcoinTransactionPriority;
327 final fee = wallet.calculateEstimatedFee(priority, null);
328
329 if (availableBalance < fee || availableBalance == 0) {
@@ -402,6 +405,10 @@ abstract class ExchangeViewModelBase with Store {
405 depositCurrency = CryptoCurrency.btc;
406 receiveCurrency = CryptoCurrency.xmr;
407 break;
408 + case WalletType.litecoin:
409 + depositCurrency = CryptoCurrency.ltc;
410 + receiveCurrency = CryptoCurrency.xmr;
411 + break;
412 default:
413 break;
414 }
lib/view_model/node_list/node_list_view_model.dart
+3
@@ -39,6 +39,9 @@ abstract class NodeListViewModelBase with Store {
39 case WalletType.monero:
40 node = getMoneroDefaultNode(nodes: _nodeSource);
41 break;
42 + case WalletType.litecoin:
43 + node = getLitecoinDefaultElectrumServer(nodes: _nodeSource);
44 + break;
45 default:
46 break;
47 }
lib/view_model/send/send_view_model.dart
+15 -2
@@ -1,5 +1,6 @@
1 import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
2 import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart';
3 +import 'package:cake_wallet/bitcoin/electrum_wallet.dart';
4 import 'package:cake_wallet/entities/balance_display_mode.dart';
5 import 'package:cake_wallet/entities/calculate_fiat_amount_raw.dart';
6 import 'package:cake_wallet/entities/transaction_description.dart';
@@ -90,6 +91,9 @@ abstract class SendViewModelBase with Store {
91 case WalletType.bitcoin:
92 _amount = stringDoubleToBitcoinAmount(_cryptoAmount);
93 break;
94 + case WalletType.litecoin:
95 + _amount = stringDoubleToBitcoinAmount(_cryptoAmount);
96 + break;
97 default:
98 break;
99 }
@@ -102,7 +106,7 @@ abstract class SendViewModelBase with Store {
106 final fee = _wallet.calculateEstimatedFee(
107 _settingsStore.priority[_wallet.type], amount);
108
105 - if (_wallet is BitcoinWallet) {
109 + if (_wallet is ElectrumWallet) {
110 return bitcoinAmountToDouble(amount: fee);
111 }
112
@@ -304,6 +308,12 @@ abstract class SendViewModelBase with Store {
308 final amount = !sendAll ? _amount : null;
309 final priority = _settingsStore.priority[_wallet.type];
310
311 + return BitcoinTransactionCredentials(
312 + address, amount, priority as BitcoinTransactionPriority);
313 + case WalletType.litecoin:
314 + final amount = !sendAll ? _amount : null;
315 + final priority = _settingsStore.priority[_wallet.type];
316 +
317 return BitcoinTransactionCredentials(
318 address, amount, priority as BitcoinTransactionPriority);
319 case WalletType.monero:
@@ -330,6 +340,9 @@ abstract class SendViewModelBase with Store {
340 case WalletType.bitcoin:
341 maximumFractionDigits = 8;
342 break;
343 + case WalletType.litecoin:
344 + maximumFractionDigits = 8;
345 + break;
346 default:
347 break;
348 }
@@ -357,7 +370,7 @@ abstract class SendViewModelBase with Store {
370 final _priority = priority as TransactionPriority;
371 final wallet = _wallet;
372
360 - if (wallet is BitcoinWallet) {
373 + if (wallet is ElectrumWallet) {
374 final rate = wallet.feeRate(_priority);
375 return '${priority.toString()} ($rate sat/byte)';
376 }
lib/view_model/settings/settings_view_model.dart
+17 -9
@@ -1,10 +1,3 @@
1 -import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart';
2 -import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
3 -import 'package:cake_wallet/entities/balance.dart';
4 -import 'package:cake_wallet/entities/transaction_priority.dart';
5 -import 'package:cake_wallet/themes/theme_base.dart';
6 -import 'package:cake_wallet/themes/theme_list.dart';
7 -import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart';
1 import 'package:flutter/cupertino.dart';
2 import 'package:mobx/mobx.dart';
3 import 'package:package_info/package_info.dart';
@@ -25,6 +18,15 @@ import 'package:cake_wallet/view_model/settings/regular_list_item.dart';
18 import 'package:cake_wallet/view_model/settings/settings_list_item.dart';
19 import 'package:cake_wallet/view_model/settings/switcher_list_item.dart';
20 import 'package:cake_wallet/src/screens/auth/auth_page.dart';
21 +import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart';
22 +import 'package:cake_wallet/bitcoin/electrum_wallet.dart';
23 +import 'package:cake_wallet/core/transaction_history.dart';
24 +import 'package:cake_wallet/entities/balance.dart';
25 +import 'package:cake_wallet/entities/transaction_info.dart';
26 +import 'package:cake_wallet/entities/transaction_priority.dart';
27 +import 'package:cake_wallet/themes/theme_base.dart';
28 +import 'package:cake_wallet/themes/theme_list.dart';
29 +import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart';
30
31 part 'settings_view_model.g.dart';
32
@@ -36,13 +38,19 @@ List<TransactionPriority> priorityForWalletType(WalletType type) {
38 return MoneroTransactionPriority.all;
39 case WalletType.bitcoin:
40 return BitcoinTransactionPriority.all;
41 + case WalletType.litecoin:
42 + return BitcoinTransactionPriority.all;
43 default:
44 return [];
45 }
46 }
47
48 abstract class SettingsViewModelBase with Store {
45 - SettingsViewModelBase(this._settingsStore, WalletBase<Balance> wallet)
49 + SettingsViewModelBase(
50 + this._settingsStore,
51 + WalletBase<Balance, TransactionHistoryBase<TransactionInfo>,
52 + TransactionInfo>
53 + wallet)
54 : itemHeaders = {},
55 _walletType = wallet.type,
56 _biometricAuth = BiometricAuth() {
@@ -77,7 +85,7 @@ abstract class SettingsViewModelBase with Store {
85 displayItem: (dynamic priority) {
86 final _priority = priority as TransactionPriority;
87
80 - if (wallet is BitcoinWallet) {
88 + if (wallet is ElectrumWallet) {
89 final rate = wallet.feeRate(_priority);
90 return '${priority.toString()} ($rate sat/byte)';
91 }
lib/view_model/transaction_details_view_model.dart
+42 -14
@@ -1,5 +1,6 @@
1 -import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart';
1 +import 'package:cake_wallet/bitcoin/electrum_transaction_info.dart';
2 import 'package:cake_wallet/entities/transaction_info.dart';
3 +import 'package:cake_wallet/entities/wallet_type.dart';
4 import 'package:cake_wallet/monero/monero_transaction_info.dart';
5 import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart';
6 import 'package:cake_wallet/src/screens/transaction_details/textfield_list_item.dart';
@@ -43,12 +44,6 @@ abstract class TransactionDetailsViewModelBase with Store {
44 value: tx.amountFormatted()),
45 StandartListItem(
46 title: S.current.transaction_details_fee, value: tx.feeFormatted()),
46 - BlockExplorerListItem(
47 - title: "View in Block Explorer",
48 - value: "View Transaction on XMRChain.net",
49 - onTap: () {
50 - launch("https://xmrchain.net/search?value=${tx.id}");
51 - })
47 ];
48
49 if (tx.key?.isNotEmpty ?? null) {
@@ -59,7 +54,7 @@ abstract class TransactionDetailsViewModelBase with Store {
54 items.addAll(_items);
55 }
56
62 - if (tx is BitcoinTransactionInfo) {
57 + if (tx is ElectrumTransactionInfo) {
58 final _items = [
59 StandartListItem(
60 title: S.current.transaction_details_transaction_id, value: tx.id),
@@ -78,12 +73,6 @@ abstract class TransactionDetailsViewModelBase with Store {
73 StandartListItem(
74 title: S.current.transaction_details_fee,
75 value: tx.feeFormatted()),
81 - BlockExplorerListItem(
82 - title: "View in Block Explorer",
83 - value: "View Transaction on Blockchain.com",
84 - onTap: () {
85 - launch("https://www.blockchain.com/btc/tx/${tx.id}");
86 - })
76 ];
77
78 items.addAll(_items);
@@ -101,6 +90,19 @@ abstract class TransactionDetailsViewModelBase with Store {
90 }
91 }
92
93 + WalletType type;
94 +
95 + if (tx is MoneroTransactionInfo) {
96 + type = WalletType.monero;
97 + } else if (tx is ElectrumTransactionInfo) {
98 + type = tx.type;
99 + }
100 +
101 + items.add(BlockExplorerListItem(
102 + title: "View in Block Explorer",
103 + value: _explorerDescription(type),
104 + onTap: () => launch(_explorerUrl(type, tx.id))));
105 +
106 final description = transactionDescriptionBox.values.firstWhere(
107 (val) => val.id == transactionInfo.id,
108 orElse: () => TransactionDescription(id: transactionInfo.id));
@@ -125,4 +127,30 @@ abstract class TransactionDetailsViewModelBase with Store {
127
128 final List<TransactionDetailsListItem> items;
129 bool showRecipientAddress;
130 +
131 + String _explorerUrl(WalletType type, String txId) {
132 + switch (type) {
133 + case WalletType.monero:
134 + return 'https://xmrchain.net/search?value=${txId}';
135 + case WalletType.bitcoin:
136 + return 'https://www.blockchain.com/btc/tx/${txId}';
137 + case WalletType.litecoin:
138 + return 'https://blockchair.com/litecoin/transaction/${txId}';
139 + default:
140 + return '';
141 + }
142 + }
143 +
144 + String _explorerDescription(WalletType type) {
145 + switch (type) {
146 + case WalletType.monero:
147 + return 'View Transaction on XMRChain.net';
148 + case WalletType.bitcoin:
149 + return 'View Transaction on Blockchain.com';
150 + case WalletType.litecoin:
151 + return 'View Transaction on Blockchair.com';
152 + default:
153 + return '';
154 + }
155 + }
156 }
lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart
+2 -1
@@ -1,6 +1,7 @@
1 import 'package:flutter/foundation.dart';
2 import 'package:mobx/mobx.dart';
3 import 'package:cake_wallet/core/wallet_base.dart';
4 +import 'package:cake_wallet/bitcoin/electrum_wallet.dart';
5 import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
6 import 'package:cake_wallet/monero/monero_wallet.dart';
7
@@ -62,7 +63,7 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store {
63 Future<void> _createNew() async {
64 final wallet = _wallet;
65
65 - if (wallet is BitcoinWallet) {
66 + if (wallet is ElectrumWallet) {
67 await wallet.generateNewAddress();
68 }
69
lib/view_model/wallet_address_list/wallet_address_list_view_model.dart
+9 -8
@@ -1,4 +1,6 @@
1 +import 'package:cake_wallet/core/transaction_history.dart';
2 import 'package:cake_wallet/entities/balance.dart';
3 +import 'package:cake_wallet/entities/transaction_info.dart';
4 import 'package:cake_wallet/store/app_store.dart';
5 import 'package:flutter/foundation.dart';
6 import 'package:mobx/mobx.dart';
@@ -56,12 +58,13 @@ class BitcoinURI extends PaymentURI {
58 }
59
60 abstract class WalletAddressListViewModelBase with Store {
59 - WalletAddressListViewModelBase(
60 - {@required AppStore appStore}) {
61 + WalletAddressListViewModelBase({@required AppStore appStore}) {
62 _appStore = appStore;
63 _wallet = _appStore.wallet;
64 hasAccounts = _wallet?.type == WalletType.monero;
64 - _onWalletChangeReaction = reaction((_) => _appStore.wallet, (WalletBase<Balance> wallet) {
65 + _onWalletChangeReaction = reaction((_) => _appStore.wallet, (WalletBase<
66 + Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>
67 + wallet) {
68 _wallet = wallet;
69 hasAccounts = _wallet.type == WalletType.monero;
70 });
@@ -119,9 +122,7 @@ abstract class WalletAddressListViewModelBase with Store {
122 final isPrimary = addr == primaryAddress;
123
124 return WalletAddressListItem(
122 - isPrimary: isPrimary,
123 - name: null,
124 - address: addr.address);
125 + isPrimary: isPrimary, name: null, address: addr.address);
126 });
127 addressList.addAll(bitcoinAddresses);
128 }
@@ -147,7 +148,8 @@ abstract class WalletAddressListViewModelBase with Store {
148 bool get hasAddressList => _wallet.type == WalletType.monero;
149
150 @observable
150 - WalletBase<Balance> _wallet;
151 + WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>
152 + _wallet;
153
154 List<ListItem> _baseItems;
155
@@ -155,7 +157,6 @@ abstract class WalletAddressListViewModelBase with Store {
157
158 ReactionDisposer _onWalletChangeReaction;
159
158 -
160 @action
161 void setAddress(WalletAddressListItem address) =>
162 _wallet.address = address.address;
lib/view_model/wallet_keys_view_model.dart
+3 -4
@@ -2,7 +2,7 @@ import 'package:mobx/mobx.dart';
2 import 'package:cake_wallet/generated/i18n.dart';
3 import 'package:cake_wallet/core/wallet_base.dart';
4 import 'package:cake_wallet/monero/monero_wallet.dart';
5 -import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
5 +import 'package:cake_wallet/bitcoin/electrum_wallet.dart';
6 import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart';
7
8 part 'wallet_keys_view_model.g.dart';
@@ -24,12 +24,11 @@ abstract class WalletKeysViewModelBase with Store {
24 title: S.current.view_key_public, value: keys.publicViewKey),
25 StandartListItem(
26 title: S.current.view_key_private, value: keys.privateViewKey),
27 - StandartListItem(
28 - title: S.current.wallet_seed, value: wallet.seed),
27 + StandartListItem(title: S.current.wallet_seed, value: wallet.seed),
28 ]);
29 }
30
32 - if (wallet is BitcoinWallet) {
31 + if (wallet is ElectrumWallet) {
32 final keys = wallet.keys;
33
34 items.addAll([
lib/view_model/wallet_new_vm.dart
+2
@@ -37,6 +37,8 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store {
37 name: name, language: options as String);
38 case WalletType.bitcoin:
39 return BitcoinNewWalletCredentials(name: name);
40 + case WalletType.litecoin:
41 + return BitcoinNewWalletCredentials(name: name);
42 default:
43 return null;
44 }
pubspec.lock
+11 -7
@@ -67,10 +67,12 @@ packages:
67 bech32:
68 dependency: transitive
69 description:
70 - name: bech32
71 - url: "https://pub.dartlang.org"
72 - source: hosted
73 - version: "0.1.2"
70 + path: "."
71 + ref: cake
72 + resolved-ref: "02fef082f20af13de00b4e64efb93a2c1e5e1cf2"
73 + url: "git@github.com:cake-tech/bech32.git"
74 + source: git
75 + version: "0.2.0"
76 bip32:
77 dependency: transitive
78 description:
@@ -88,9 +90,11 @@ packages:
90 bitcoin_flutter:
91 dependency: "direct main"
92 description:
91 - name: bitcoin_flutter
92 - url: "https://pub.dartlang.org"
93 - source: hosted
93 + path: "."
94 + ref: cake
95 + resolved-ref: b3ab2926c665f0e68b74a4a5f31059f7fcd817b7
96 + url: "git@github.com:cake-tech/bitcoin_flutter.git"
97 + source: git
98 version: "2.0.2"
99 boolean_selector:
100 dependency: transitive
pubspec.yaml
+7 -2
@@ -12,6 +12,7 @@ description: Cake Wallet.
12 # Read more about iOS versioning at
13 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
14 version: 4.1.4+43
15 +publish_to: none
16
17 environment:
18 sdk: ">=2.7.0 <3.0.0"
@@ -65,7 +66,10 @@ dependencies:
66 crypto: ^2.1.5
67 password: ^1.0.0
68 basic_utils: ^2.0.3
68 - bitcoin_flutter: ^2.0.0
69 + bitcoin_flutter:
70 + git:
71 + url: git@github.com:cake-tech/bitcoin_flutter.git
72 + ref: cake
73 get_it: ^6.0.0
74 connectivity: ^3.0.3
75 keyboard_actions: ^3.3.0
@@ -105,7 +109,8 @@ flutter:
109 assets:
110 - assets/images/
111 - assets/node_list.yml
108 - - assets/electrum_server_list.yml
112 + - assets/bitcoin_electrum_server_list.yml
113 + - assets/litecoin_electrum_server_list.yml
114 - assets/text/
115 - assets/faq/
116