TMP

M committed Aug 25, 2020 at 19:32 UTC 5eefd6a31bbc0dd4f037fe6c0d52f23cf02921ba
35 files changed +1275 -620
.gitignore
+3 -1
@@ -89,4 +89,6 @@ android/key.properties
89 **/tool/.secrets-prod.json
90 **/lib/.secrets.g.dart
91
92 -vendor/
\ No newline at end of file
92 +vendor/
93 +
94 +android/app/.cxx/**
ios/Runner.xcodeproj/project.pbxproj
+6 -6
@@ -373,7 +373,7 @@
373 buildSettings = {
374 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
375 CLANG_ENABLE_MODULES = YES;
376 - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
376 + CURRENT_PROJECT_VERSION = 6;
377 DEVELOPMENT_TEAM = 32J6BB6VUS;
378 ENABLE_BITCODE = NO;
379 FRAMEWORK_SEARCH_PATHS = (
@@ -387,7 +387,7 @@
387 "$(inherited)",
388 "$(PROJECT_DIR)/Flutter",
389 );
390 - MARKETING_VERSION = 3.1.28;
390 + MARKETING_VERSION = 3.2.0;
391 PRODUCT_BUNDLE_IDENTIFIER = com.cakewallet.cakewallet;
392 PRODUCT_NAME = "$(TARGET_NAME)";
393 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -509,7 +509,7 @@
509 buildSettings = {
510 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
511 CLANG_ENABLE_MODULES = YES;
512 - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
512 + CURRENT_PROJECT_VERSION = 6;
513 DEVELOPMENT_TEAM = 32J6BB6VUS;
514 ENABLE_BITCODE = NO;
515 FRAMEWORK_SEARCH_PATHS = (
@@ -523,7 +523,7 @@
523 "$(inherited)",
524 "$(PROJECT_DIR)/Flutter",
525 );
526 - MARKETING_VERSION = 3.1.28;
526 + MARKETING_VERSION = 3.2.0;
527 PRODUCT_BUNDLE_IDENTIFIER = com.cakewallet.cakewallet;
528 PRODUCT_NAME = "$(TARGET_NAME)";
529 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -540,7 +540,7 @@
540 buildSettings = {
541 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
542 CLANG_ENABLE_MODULES = YES;
543 - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
543 + CURRENT_PROJECT_VERSION = 6;
544 DEVELOPMENT_TEAM = 32J6BB6VUS;
545 ENABLE_BITCODE = NO;
546 FRAMEWORK_SEARCH_PATHS = (
@@ -554,7 +554,7 @@
554 "$(inherited)",
555 "$(PROJECT_DIR)/Flutter",
556 );
557 - MARKETING_VERSION = 3.1.28;
557 + MARKETING_VERSION = 3.2.0;
558 PRODUCT_BUNDLE_IDENTIFIER = com.cakewallet.cakewallet;
559 PRODUCT_NAME = "$(TARGET_NAME)";
560 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
ios/Runner/Info.plist
+1 -1
@@ -19,7 +19,7 @@
19 <key>CFBundleSignature</key>
20 <string>????</string>
21 <key>CFBundleVersion</key>
22 - <string>$(FLUTTER_BUILD_NUMBER)</string>
22 + <string>$(CURRENT_PROJECT_VERSION)</string>
23 <key>LSRequiresIPhoneOS</key>
24 <true/>
25 <key>UILaunchStoryboardName</key>
lib/bitcoin/bitcoin_address_record.dart
+5 -3
@@ -1,17 +1,19 @@
1 import 'dart:convert';
2
3 class BitcoinAddressRecord {
4 - BitcoinAddressRecord(this.address, {this.label});
4 + BitcoinAddressRecord(this.address, {this.label, this.index});
5
6 factory BitcoinAddressRecord.fromJSON(String jsonSource) {
7 final decoded = json.decode(jsonSource) as Map;
8
9 return BitcoinAddressRecord(decoded['address'] as String,
10 - label: decoded['label'] as String);
10 + label: decoded['label'] as String, index: decoded['index'] as int);
11 }
12
13 final String address;
14 + int index;
15 String label;
16
16 - String toJSON() => json.encode({'label': label, 'address': address});
17 + String toJSON() =>
18 + json.encode({'label': label, 'address': address, 'index': index});
19 }
lib/bitcoin/bitcoin_transaction_credentials.dart
+4 -1
@@ -1,6 +1,9 @@
1 +import 'package:cake_wallet/src/domain/common/transaction_priority.dart';
2 +
3 class BitcoinTransactionCredentials {
2 - const BitcoinTransactionCredentials(this.address, this.amount);
4 + BitcoinTransactionCredentials(this.address, this.amount, this.priority);
5
6 final String address;
7 final double amount;
8 + TransactionPriority priority;
9 }
lib/bitcoin/bitcoin_transaction_history.dart
+130 -56
@@ -6,8 +6,6 @@ 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 -import 'package:cake_wallet/src/domain/common/transaction_info.dart';
10 -import 'package:cake_wallet/src/domain/common/transaction_direction.dart';
9
10 part 'bitcoin_transaction_history.g.dart';
11
@@ -24,100 +22,176 @@ abstract class BitcoinTransactionHistoryBase
22 {this.eclient, String dirPath, @required String password})
23 : path = '$dirPath/$_transactionsHistoryFileName',
24 _password = password,
27 - _height = 0;
25 + _height = 0,
26 + _isUpdating = false {
27 + transactions = ObservableMap<String, BitcoinTransactionInfo>();
28 + }
29
30 BitcoinWalletBase wallet;
31 final ElectrumClient eclient;
32 final String path;
33 final String _password;
34 int _height;
35 + bool _isUpdating;
36
37 Future<void> init() async {
36 - final info = await _read();
37 - _height = info['height'] as int ?? _height;
38 - transactions = ObservableList.of(
39 - info['transactions'] as List<BitcoinTransactionInfo> ??
40 - <BitcoinTransactionInfo>[]);
38 + await _load();
39 }
40
41 @override
42 Future update() async {
45 - await super.update();
46 - _updateHeight();
43 + if (_isUpdating) {
44 + return;
45 + }
46 +
47 + try {
48 + _isUpdating = true;
49 + final txs = await fetchTransactions();
50 + await add(txs);
51 + _isUpdating = false;
52 + } catch (_) {
53 + _isUpdating = false;
54 + rethrow;
55 + }
56 }
57
58 @override
50 - Future<List<BitcoinTransactionInfo>> fetchTransactions() async {
51 - final addresses = wallet.addresses;
59 + Future<Map<String, BitcoinTransactionInfo>> fetchTransactions() async {
60 final histories =
53 - addresses.map((record) => eclient.getHistory(address: record.address));
61 + wallet.scriptHashes.map((scriptHash) => eclient.getHistory(scriptHash));
62 final _historiesWithDetails = await Future.wait(histories)
63 .then((histories) => histories
56 - .map((h) => h.where((tx) => (tx['height'] as int) > _height))
64 +// .map((h) => h.where((tx) {
65 +// final height = tx['height'] as int ?? 0;
66 +// // FIXME: Filter only needed transactions
67 +// final _tx = get(tx['tx_hash'] as String);
68 +//
69 +// return height == 0 || height > _height;
70 +// }))
71 .expand((i) => i)
72 .toList())
73 .then((histories) => histories.map((tx) => fetchTransactionInfo(
74 hash: tx['tx_hash'] as String, height: tx['height'] as int)));
75 final historiesWithDetails = await Future.wait(_historiesWithDetails);
76
63 - return historiesWithDetails
64 - .map((info) => BitcoinTransactionInfo.fromHexAndHeader(
65 - info['raw'] as String, info['header'] as Map<String, Object>,
66 - addresses: addresses.map((record) => record.address).toList()))
67 - .toList();
77 + return historiesWithDetails.fold<Map<String, BitcoinTransactionInfo>>(
78 + <String, BitcoinTransactionInfo>{}, (acc, tx) {
79 + acc[tx.id] = tx;
80 + return acc;
81 + });
82 }
83
70 - Future<Map<String, Object>> fetchTransactionInfo(
84 + Future<BitcoinTransactionInfo> fetchTransactionInfo(
85 {@required String hash, @required int height}) async {
72 - final rawFetching = eclient.getTransactionRaw(hash: hash);
73 - final headerFetching = eclient.getHeader(height: height);
74 - final result = await Future.wait([rawFetching, headerFetching]);
75 - final raw = result.first as String;
76 - final header = result[1] as Map<String, Object>;
77 -
78 - return {'raw': raw, 'header': header};
86 + final tx = await eclient.getTransactionExpanded(hash: hash);
87 + return BitcoinTransactionInfo.fromElectrumVerbose(tx,
88 + height: height, addresses: wallet.addresses);
89 }
90
81 - Future<void> add(List<BitcoinTransactionInfo> transactions) async {
82 - this.transactions.addAll(transactions);
91 + Future<void> add(Map<String, BitcoinTransactionInfo> transactionsList) async {
92 + transactionsList.entries.forEach((entry) {
93 + _updateOrInsert(entry.value);
94 +
95 + if (entry.value.height > _height) {
96 + _height = entry.value.height;
97 + }
98 + });
99 +
100 await save();
101 }
102
103 Future<void> addOne(BitcoinTransactionInfo tx) async {
87 - transactions.add(tx);
104 + _updateOrInsert(tx);
105 +
106 + if (tx.height > _height) {
107 + _height = tx.height;
108 + }
109 +
110 await save();
111 }
112
91 - Future<void> save() async => writeData(
92 - path: path,
93 - password: _password,
94 - data: json.encode({'height': _height, 'transactions': transactions}));
113 + BitcoinTransactionInfo get(String id) => transactions[id];
114 +
115 + Future<void> save() async {
116 + final data = json.encode({'height': _height, 'transactions': transactions});
117 +
118 + print('data');
119 + print(data);
120 +
121 + await writeData(path: path, password: _password, data: data);
122 + }
123 +
124 + @override
125 + void updateAsync({void Function() onFinished}) {
126 + fetchTransactionsAsync((transaction) => _updateOrInsert(transaction),
127 + onFinished: onFinished);
128 + }
129 +
130 + @override
131 + void fetchTransactionsAsync(
132 + void Function(BitcoinTransactionInfo transaction) onTransactionLoaded,
133 + {void Function() onFinished}) async {
134 + final histories = await Future.wait(wallet.scriptHashes
135 + .map((scriptHash) async => await eclient.getHistory(scriptHash)));
136 + final transactionsCount =
137 + histories.fold<int>(0, (acc, m) => acc + m.length);
138 + var counter = 0;
139 +
140 + final batches = histories.map((metaList) =>
141 + _fetchBatchOfTransactions(metaList, onTransactionLoaded: (transaction) {
142 + onTransactionLoaded(transaction);
143 + counter += 1;
144 +
145 + if (counter == transactionsCount) {
146 + onFinished?.call();
147 + }
148 + }));
149 +
150 + await Future.wait(batches);
151 + }
152 +
153 + Future<void> _fetchBatchOfTransactions(
154 + Iterable<Map<String, dynamic>> metaList,
155 + {void Function(BitcoinTransactionInfo tranasaction)
156 + onTransactionLoaded}) async =>
157 + metaList.forEach((txMeta) => fetchTransactionInfo(
158 + hash: txMeta['tx_hash'] as String,
159 + height: txMeta['height'] as int)
160 + .then((transaction) => onTransactionLoaded(transaction)));
161
162 Future<Map<String, Object>> _read() async {
163 + final content = await read(path: path, password: _password);
164 + return json.decode(content) as Map<String, Object>;
165 + }
166 +
167 + Future<void> _load() async {
168 try {
98 - final content = await read(path: path, password: _password);
99 - final jsoned = json.decode(content) as Map<String, Object>;
100 - final height = jsoned['height'] as int;
101 - final transactions = (jsoned['transactions'] as List<dynamic>)
102 - .map((dynamic row) {
103 - if (row is Map<String, Object>) {
104 - return BitcoinTransactionInfo.fromJson(row);
105 - }
106 -
107 - return null;
108 - })
109 - .where((el) => el != null)
110 - .toList();
111 -
112 - return {'transactions': transactions, 'height': height};
113 - } catch (_) {
114 - return {'transactions': <BitcoinTransactionInfo>[], 'height': 0};
115 - }
169 + final content = await _read();
170 + final txs = content['transactions'] as Map<String, Object> ?? {};
171 +
172 + txs.entries.forEach((entry) {
173 + final val = entry.value;
174 +
175 + if (val is Map<String, Object>) {
176 + final tx = BitcoinTransactionInfo.fromJson(val);
177 + _updateOrInsert(tx);
178 + }
179 + });
180 +
181 + _height = content['height'] as int;
182 + } catch (_) {}
183 }
184
118 - void _updateHeight() {
119 - final newHeight = transactions.fold(
120 - 0, (int acc, val) => val.height > acc ? val.height : acc);
121 - _height = newHeight > _height ? newHeight : _height;
185 + void _updateOrInsert(BitcoinTransactionInfo transaction) {
186 + if (transactions[transaction.id] == null) {
187 + transactions[transaction.id] = transaction;
188 + } else {
189 + final originalTx = transactions[transaction.id];
190 + originalTx.confirmations = transaction.confirmations;
191 + originalTx.amount = transaction.amount;
192 + originalTx.height = transaction.height;
193 + originalTx.date ??= transaction.date;
194 + originalTx.isPending = transaction.isPending;
195 + }
196 }
197 }
lib/bitcoin/bitcoin_transaction_info.dart
+81 -21
@@ -1,7 +1,9 @@
1 -import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
1 import 'package:flutter/foundation.dart';
2 import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
3 +import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart';
4 import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData;
5 +import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
6 +import 'package:cake_wallet/src/domain/bitcoin/bitcoin_amount_format.dart';
7 import 'package:cake_wallet/src/domain/common/transaction_direction.dart';
8 import 'package:cake_wallet/src/domain/common/transaction_info.dart';
9 import 'package:cake_wallet/src/domain/common/format_amount.dart';
@@ -13,7 +15,8 @@ class BitcoinTransactionInfo extends TransactionInfo {
15 @required int amount,
16 @required TransactionDirection direction,
17 @required bool isPending,
16 - @required DateTime date}) {
18 + @required DateTime date,
19 + @required this.confirmations}) {
20 this.height = height;
21 this.amount = amount;
22 this.direction = direction;
@@ -21,34 +24,87 @@ class BitcoinTransactionInfo extends TransactionInfo {
24 this.isPending = isPending;
25 }
26
24 - factory BitcoinTransactionInfo.fromHexAndHeader(
25 - String hex, Map<String, Object> header,
26 - {List<String> addresses}) {
27 + factory BitcoinTransactionInfo.fromElectrumVerbose(Map<String, Object> obj,
28 + {@required List<BitcoinAddressRecord> addresses, @required int height}) {
29 + final addressesSet = addresses.map((addr) => addr.address).toSet();
30 + final id = obj['txid'] as String;
31 + final vins = obj['vin'] as List<Object> ?? [];
32 + final vout = (obj['vout'] as List<Object> ?? []);
33 + final date = obj['time'] is int
34 + ? DateTime.fromMillisecondsSinceEpoch((obj['time'] as int) * 1000)
35 + : DateTime.now();
36 + final confirmations = obj['confirmations'] as int ?? 0;
37 + var direction = TransactionDirection.incoming;
38 +
39 + for (dynamic vin in vins) {
40 + final vout = vin['vout'] as int;
41 + final out = vin['tx']['vout'][vout] as Map;
42 + final outAddresses =
43 + (out['scriptPubKey']['addresses'] as List<Object>)?.toSet();
44 +
45 + if (outAddresses?.intersection(addressesSet)?.isNotEmpty ?? false) {
46 + direction = TransactionDirection.outgoing;
47 + break;
48 + }
49 + }
50 +
51 + final amount = vout.fold(0, (int acc, dynamic out) {
52 + final outAddresses =
53 + out['scriptPubKey']['addresses'] as List<Object> ?? [];
54 + final ntrs = outAddresses.toSet().intersection(addressesSet);
55 + var amount = acc;
56 +
57 + if ((direction == TransactionDirection.incoming && ntrs.isNotEmpty) ||
58 + (direction == TransactionDirection.outgoing && ntrs.isEmpty)) {
59 + amount += doubleToBitcoinAmount(out['value'] as double ?? 0.0);
60 + }
61 +
62 + return amount;
63 + });
64 +
65 + return BitcoinTransactionInfo(
66 + id: id,
67 + height: height,
68 + isPending: false,
69 + direction: direction,
70 + amount: amount,
71 + date: date,
72 + confirmations: confirmations);
73 + }
74 +
75 + factory BitcoinTransactionInfo.fromHexAndHeader(String hex,
76 + {List<String> addresses, int height, int timestamp, int confirmations}) {
77 final tx = bitcoin.Transaction.fromHex(hex);
78 var exist = false;
79 var amount = 0;
80
31 - tx.outs.forEach((out) {
32 - try {
33 - final p2pkh = bitcoin.P2PKH(
34 - data: PaymentData(output: out.script), network: bitcoin.bitcoin);
35 - exist = addresses.contains(p2pkh.data.address);
81 + if (addresses != null) {
82 + tx.outs.forEach((out) {
83 + try {
84 + final p2pkh = bitcoin.P2PKH(
85 + data: PaymentData(output: out.script), network: bitcoin.bitcoin);
86 + exist = addresses.contains(p2pkh.data.address);
87
37 - if (exist) {
38 - amount += out.value;
39 - }
40 - } catch (_) {}
41 - });
88 + if (exist) {
89 + amount += out.value;
90 + }
91 + } catch (_) {}
92 + });
93 + }
94 +
95 + final date = timestamp != null
96 + ? DateTime.fromMillisecondsSinceEpoch(timestamp * 1000)
97 + : DateTime.now();
98
99 // FIXME: Get transaction is pending
100 return BitcoinTransactionInfo(
101 id: tx.getId(),
46 - height: header['block_height'] as int,
102 + height: height,
103 isPending: false,
104 direction: TransactionDirection.incoming,
105 amount: amount,
50 - date: DateTime.fromMillisecondsSinceEpoch(
51 - (header['timestamp'] as int) * 1000));
106 + date: date,
107 + confirmations: confirmations);
108 }
109
110 factory BitcoinTransactionInfo.fromJson(Map<String, dynamic> data) {
@@ -58,15 +114,18 @@ class BitcoinTransactionInfo extends TransactionInfo {
114 amount: data['amount'] as int,
115 direction: parseTransactionDirectionFromInt(data['direction'] as int),
116 date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int),
61 - isPending: data['isPending'] as bool);
117 + isPending: data['isPending'] as bool,
118 + confirmations: data['confirmations'] as int);
119 }
120
121 final String id;
122 + int confirmations;
123
124 String _fiatAmount;
125
126 @override
69 - String amountFormatted() => '${formatAmount(bitcoinAmountToString(amount: amount))} BTC';
127 + String amountFormatted() =>
128 + '${formatAmount(bitcoinAmountToString(amount: amount))} BTC';
129
130 @override
131 String fiatAmount() => _fiatAmount ?? '';
@@ -75,13 +134,14 @@ class BitcoinTransactionInfo extends TransactionInfo {
134 void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
135
136 Map<String, dynamic> toJson() {
78 - final m = Map<String, dynamic>();
137 + final m = <String, dynamic>{};
138 m['id'] = id;
139 m['height'] = height;
140 m['amount'] = amount;
141 m['direction'] = direction.index;
142 m['date'] = date.millisecondsSinceEpoch;
143 m['isPending'] = isPending;
144 + m['confirmations'] = confirmations;
145 return m;
146 }
147 }
lib/bitcoin/bitcoin_transaction_no_inputs_exception.dart new
+4
@@ -0,0 +1,4 @@
1 +class BitcoinTransactionNoInputsException implements Exception {
2 + @override
3 + String toString() => 'No inputs for the transaction.';
4 +}
\ No newline at end of file
lib/bitcoin/bitcoin_transaction_wrong_balance_exception.dart new
+4
@@ -0,0 +1,4 @@
1 +class BitcoinTransactionWrongBalanceException implements Exception {
2 + @override
3 + String toString() => 'Wrong balance. Not enough BTC on your balance.';
4 +}
\ No newline at end of file
lib/bitcoin/bitcoin_unspent.dart new
+17
@@ -0,0 +1,17 @@
1 +import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart';
2 +
3 +class BitcoinUnspent {
4 + BitcoinUnspent(this.address, this.hash, this.value, this.vout);
5 +
6 + factory BitcoinUnspent.fromJSON(
7 + BitcoinAddressRecord address, Map<String, dynamic> json) =>
8 + BitcoinUnspent(address, json['tx_hash'] as String, json['value'] as int,
9 + json['tx_pos'] as int);
10 +
11 + final BitcoinAddressRecord address;
12 + final String hash;
13 + final int value;
14 + final int vout;
15 +
16 + bool get isP2wpkh => address.address.startsWith('bc1');
17 +}
lib/bitcoin/bitcoin_wallet.dart
+175 -62
@@ -1,10 +1,20 @@
1 import 'dart:typed_data';
2 import 'dart:convert';
3 import 'package:cake_wallet/bitcoin/bitcoin_transaction_credentials.dart';
4 +import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart';
5 +import 'package:cake_wallet/bitcoin/bitcoin_transaction_no_inputs_exception.dart';
6 +import 'package:cake_wallet/bitcoin/bitcoin_transaction_wrong_balance_exception.dart';
7 +import 'package:cake_wallet/bitcoin/bitcoin_unspent.dart';
8 import 'package:cake_wallet/bitcoin/bitcoin_wallet_keys.dart';
9 +import 'package:cake_wallet/bitcoin/pending_bitcoin_transaction.dart';
10 +import 'package:cake_wallet/bitcoin/script_hash.dart';
11 +import 'package:cake_wallet/bitcoin/utils.dart';
12 import 'package:cake_wallet/src/domain/bitcoin/bitcoin_amount_format.dart';
13 import 'package:cake_wallet/src/domain/common/crypto_currency.dart';
14 import 'package:cake_wallet/src/domain/common/sync_status.dart';
15 +import 'package:cake_wallet/src/domain/common/transaction_direction.dart';
16 +import 'package:cake_wallet/src/domain/common/transaction_priority.dart';
17 +import 'package:cw_monero/transaction_history.dart';
18 import 'package:flutter/cupertino.dart';
19 import 'package:mobx/mobx.dart';
20 import 'package:bip39/bip39.dart' as bip39;
@@ -20,12 +30,39 @@ import 'package:cake_wallet/bitcoin/bitcoin_balance.dart';
30 import 'package:cake_wallet/src/domain/common/node.dart';
31 import 'package:cake_wallet/core/wallet_base.dart';
32 import 'package:rxdart/rxdart.dart';
33 +import 'package:hex/hex.dart';
34
35 part 'bitcoin_wallet.g.dart';
36
37 class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet;
38
39 abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
40 + BitcoinWalletBase._internal(
41 + {@required this.eclient,
42 + @required this.path,
43 + @required String password,
44 + @required this.name,
45 + List<BitcoinAddressRecord> initialAddresses,
46 + int accountIndex = 0,
47 + this.transactionHistory,
48 + this.mnemonic,
49 + BitcoinBalance initialBalance})
50 + : balance =
51 + initialBalance ?? BitcoinBalance(confirmed: 0, unconfirmed: 0),
52 + hd = bitcoin.HDWallet.fromSeed(bip39.mnemonicToSeed(mnemonic),
53 + network: bitcoin.bitcoin),
54 + addresses = initialAddresses != null
55 + ? ObservableList<BitcoinAddressRecord>.of(initialAddresses)
56 + : ObservableList<BitcoinAddressRecord>(),
57 + syncStatus = NotConnectedSyncStatus(),
58 + _password = password,
59 + _accountIndex = accountIndex,
60 + _addressesKeys = {} {
61 + type = WalletType.bitcoin;
62 + currency = CryptoCurrency.btc;
63 + _scripthashesUpdateSubject = {};
64 + }
65 +
66 static BitcoinWallet fromJSON(
67 {@required String password,
68 @required String name,
@@ -37,12 +74,12 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
74 (data['account_index'] == 'null' || data['account_index'] == null)
75 ? 0
76 : int.parse(data['account_index'] as String);
40 - final _addresses = data['addresses'] as List;
77 + final _addresses = data['addresses'] as List ?? <Object>[];
78 final addresses = <BitcoinAddressRecord>[];
79 final balance = BitcoinBalance.fromJSON(data['balance'] as String) ??
80 BitcoinBalance(confirmed: 0, unconfirmed: 0);
81
45 - _addresses?.forEach((Object el) {
82 + _addresses.forEach((Object el) {
83 if (el is String) {
84 addresses.add(BitcoinAddressRecord.fromJSON(el));
85 }
@@ -83,34 +120,10 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
120 transactionHistory: history);
121 }
122
86 - BitcoinWalletBase._internal(
87 - {@required this.eclient,
88 - @required this.path,
89 - @required String password,
90 - @required this.name,
91 - List<BitcoinAddressRecord> initialAddresses,
92 - int accountIndex = 0,
93 - this.transactionHistory,
94 - this.mnemonic,
95 - BitcoinBalance initialBalance}) {
96 - type = WalletType.bitcoin;
97 - currency = CryptoCurrency.btc;
98 - balance = initialBalance ?? BitcoinBalance(confirmed: 0, unconfirmed: 0);
99 - hd = bitcoin.HDWallet.fromSeed(bip39.mnemonicToSeed(mnemonic),
100 - network: bitcoin.bitcoin);
101 - addresses = initialAddresses != null
102 - ? ObservableList<BitcoinAddressRecord>.of(initialAddresses)
103 - : ObservableList<BitcoinAddressRecord>();
104 - syncStatus = NotConnectedSyncStatus();
105 -
106 - _password = password;
107 - _accountIndex = accountIndex;
108 - }
109 -
123 @override
124 final BitcoinTransactionHistory transactionHistory;
125 final String path;
113 - bitcoin.HDWallet hd;
126 + final bitcoin.HDWallet hd;
127 final ElectrumClient eclient;
128 final String mnemonic;
129
@@ -131,6 +144,11 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
144
145 ObservableList<BitcoinAddressRecord> addresses;
146
147 + Map<String, bitcoin.ECPair> _addressesKeys;
148 +
149 + List<String> get scriptHashes =>
150 + addresses.map((addr) => scriptHash(addr.address)).toList();
151 +
152 String get xpub => hd.base58;
153
154 @override
@@ -142,11 +160,13 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
160
161 int _accountIndex;
162 String _password;
145 - BehaviorSubject<Object> _addressUpdateSubject;
163 + Map<String, BehaviorSubject<Object>> _scripthashesUpdateSubject;
164
165 Future<void> init() async {
166 if (addresses.isEmpty) {
149 - addresses.add(BitcoinAddressRecord(_getAddress(hd: hd, index: 0)));
167 + final index = 0;
168 + addresses
169 + .add(BitcoinAddressRecord(_getAddress(index: index), index: index));
170 }
171
172 address = addresses.first.address;
@@ -156,9 +176,8 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
176
177 Future<BitcoinAddressRecord> generateNewAddress({String label}) async {
178 _accountIndex += 1;
159 - final address = BitcoinAddressRecord(
160 - _getAddress(hd: hd, index: _accountIndex),
161 - label: label);
179 + final address = BitcoinAddressRecord(_getAddress(index: _accountIndex),
180 + index: _accountIndex, label: label);
181 addresses.add(address);
182
183 await save();
@@ -181,9 +200,8 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
200 Future<void> startSync() async {
201 try {
202 syncStatus = StartingSyncStatus();
184 - await _addressUpdateSubject?.close();
185 - _addressUpdateSubject = eclient.addressUpdate(address: address);
186 - await transactionHistory.update();
203 + transactionHistory.updateAsync(onFinished: () => print('finished!'));
204 + _subscribeForUpdates();
205 await _updateBalance();
206 syncStatus = SyncedSyncStatus();
207 } catch (e) {
@@ -197,38 +215,101 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
215 Future<void> connectToNode({@required Node node}) async {
216 try {
217 syncStatus = ConnectingSyncStatus();
200 - await eclient.connect(host: 'electrum2.hodlister.co', port: 50002);
218 + // electrum2.hodlister.co
219 + // bitcoin.electrumx.multicoin.co:50002
220 + // electrum2.taborsky.cz:5002
221 + await eclient.connect(
222 + host: 'bitcoin.electrumx.multicoin.co', port: 50002);
223 syncStatus = ConnectedSyncStatus();
224 } catch (e) {
203 - print(e.toString);
225 + print(e.toString());
226 syncStatus = FailedSyncStatus();
227 }
228 }
229
230 @override
209 - Future<void> createTransaction(Object credentials) async {
231 + Future<PendingBitcoinTransaction> createTransaction(
232 + Object credentials) async {
233 final transactionCredentials = credentials as BitcoinTransactionCredentials;
211 -
234 + final inputs = <BitcoinUnspent>[];
235 + final fee = _feeMultiplier(transactionCredentials.priority);
236 + final amount = transactionCredentials.amount != null
237 + ? doubleToBitcoinAmount(transactionCredentials.amount)
238 + : balance.total - fee;
239 + final totalAmount = amount + fee;
240 final txb = bitcoin.TransactionBuilder(network: bitcoin.bitcoin);
213 - final keyPair = bitcoin.ECPair.fromWIF(hd.wif);
214 - final transactions = transactionHistory.transactions;
215 - transactions.sort((q, w) => q.height.compareTo(w.height));
216 - final prevTx = transactions.first;
241 + var leftAmount = totalAmount;
242 + final changeAddress = address;
243 + var totalInputAmount = 0;
244 +
245 + final unspent = addresses.map((address) => eclient
246 + .getListUnspentWithAddress(address.address)
247 + .then((unspent) => unspent
248 + .map((unspent) => BitcoinUnspent.fromJSON(address, unspent))));
249 +
250 + for (final unptsFutures in unspent) {
251 + final utxs = await unptsFutures;
252 +
253 + for (final utx in utxs) {
254 + final inAmount = utx.value > totalAmount ? totalAmount : utx.value;
255 + leftAmount = leftAmount - inAmount;
256 + totalInputAmount += inAmount;
257 + inputs.add(utx);
258 +
259 + if (leftAmount <= 0) {
260 + break;
261 + }
262 + }
263 +
264 + if (leftAmount <= 0) {
265 + break;
266 + }
267 + }
268 +
269 + if (inputs.isEmpty) {
270 + throw BitcoinTransactionNoInputsException();
271 + }
272 +
273 + if (amount <= 0 || totalInputAmount < amount) {
274 + throw BitcoinTransactionWrongBalanceException();
275 + }
276 +
277 + final changeValue = totalInputAmount - amount - fee;
278
279 txb.setVersion(1);
219 - txb.addInput(prevTx, 0);
220 - txb.addOutput(transactionCredentials.address,
221 - doubleToBitcoinAmount(transactionCredentials.amount));
222 - txb.sign(vin: 0, keyPair: keyPair);
223 - final encoded = txb.build().toHex();
224 -
225 - print('Enoded transaction $encoded');
226 - await eclient.broadcastTransaction(transactionRaw: encoded);
227 - }
280
229 - @override
230 - Future<void> save() async =>
231 - await write(path: path, password: _password, data: toJSON());
281 + inputs.forEach((input) {
282 + if (input.isP2wpkh) {
283 + final p2wpkh = bitcoin
284 + .P2WPKH(
285 + data: generatePaymentData(hd: hd, index: input.address.index),
286 + network: bitcoin.bitcoin)
287 + .data;
288 +
289 + txb.addInput(input.hash, input.vout, null, p2wpkh.output);
290 + } else {
291 + txb.addInput(input.hash, input.vout);
292 + }
293 + });
294 +
295 + txb.addOutput(transactionCredentials.address, amount);
296 +
297 + if (changeValue > 0) {
298 + txb.addOutput(changeAddress, changeValue);
299 + }
300 +
301 + for (var i = 0; i < inputs.length; i++) {
302 + final input = inputs[i];
303 + final keyPair = generateKeyPair(hd: hd, index: input.address.index);
304 + final witnessValue = input.isP2wpkh ? input.value : null;
305 +
306 + txb.sign(vin: i, keyPair: keyPair, witnessValue: witnessValue);
307 + }
308 +
309 + return PendingBitcoinTransaction(txb.build(),
310 + eclient: eclient, amount: amount, fee: fee)
311 + ..addListener((transaction) => transactionHistory.addOne(transaction));
312 + }
313
314 String toJSON() => json.encode({
315 'mnemonic': mnemonic,
@@ -237,16 +318,32 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
318 'balance': balance?.toJSON()
319 });
320
240 - String _getAddress({bitcoin.HDWallet hd, int index}) => bitcoin
241 - .P2WPKH(
242 - data: PaymentData(
243 - pubkey: Uint8List.fromList(hd.derive(index).pubKey.codeUnits)))
244 - .data
245 - .address;
321 + @override
322 + double calculateEstimatedFee(TransactionPriority priority) =>
323 + bitcoinAmountToDouble(amount: _feeMultiplier(priority));
324 +
325 + @override
326 + Future<void> save() async =>
327 + await write(path: path, password: _password, data: toJSON());
328 +
329 + bitcoin.ECPair keyPairFor({@required int index}) =>
330 + generateKeyPair(hd: hd, index: index);
331 +
332 + void _subscribeForUpdates() {
333 + scriptHashes.forEach((sh) async {
334 + await _scripthashesUpdateSubject[sh]?.close();
335 + _scripthashesUpdateSubject[sh] = eclient.scripthashUpdate(sh);
336 + _scripthashesUpdateSubject[sh].listen((event) async {
337 + print('event $event');
338 + transactionHistory.updateAsync();
339 + await _updateBalance();
340 + });
341 + });
342 + }
343
344 Future<BitcoinBalance> _fetchBalances() async {
345 final balances = await Future.wait(
249 - addresses.map((record) => eclient.getBalance(address: record.address)));
346 + scriptHashes.map((sHash) => eclient.getBalance(sHash)));
347 final balance = balances.fold(
348 BitcoinBalance(confirmed: 0, unconfirmed: 0),
349 (BitcoinBalance acc, val) => BitcoinBalance(
@@ -261,4 +358,20 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
358 balance = await _fetchBalances();
359 await save();
360 }
361 +
362 + String _getAddress({@required int index}) =>
363 + generateAddress(hd: hd, index: index);
364 +
365 + int _feeMultiplier(TransactionPriority priority) {
366 + switch (priority) {
367 + case TransactionPriority.slow:
368 + return 6000;
369 + case TransactionPriority.regular:
370 + return 9000;
371 + case TransactionPriority.fast:
372 + return 15000;
373 + default:
374 + return 0;
375 + }
376 + }
377 }
lib/bitcoin/electrum.dart
+91 -24
@@ -1,17 +1,18 @@
1 import 'dart:async';
2 import 'dart:convert';
3 import 'dart:io';
4 +import 'package:cake_wallet/bitcoin/script_hash.dart';
5 import 'package:flutter/foundation.dart';
6 import 'package:rxdart/rxdart.dart';
7
8 String jsonrpcparams(List<Object> params) {
9 final _params = params?.map((val) => '"${val.toString()}"')?.join(',');
9 - return "[$_params]";
10 + return '[$_params]';
11 }
12
13 String jsonrpc(
14 {String method, List<Object> params, int id, double version = 2.0}) =>
14 - '{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${jsonrpcparams(params)}}\n';
15 + '{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${json.encode(params)}}\n';
16
17 class SocketTask {
18 SocketTask({this.completer, this.isSubscription, this.subject});
@@ -50,6 +51,7 @@ class ElectrumClient {
51 socket.listen((List<int> event) {
52 try {
53 final jsoned = json.decode(utf8.decode(event)) as Map<String, Object>;
54 +// print(jsoned);
55 final method = jsoned['method'];
56
57 if (method is String) {
@@ -93,18 +95,18 @@ class ElectrumClient {
95 return [];
96 });
97
96 - Future<Map<String, Object>> getBalance({String address}) =>
97 - call(method: 'blockchain.address.get_balance', params: [address])
98 + Future<Map<String, Object>> getBalance(String scriptHash) =>
99 + call(method: 'blockchain.scripthash.get_balance', params: [scriptHash])
100 .then((dynamic result) {
101 if (result is Map<String, Object>) {
102 return result;
103 }
104
103 - return Map<String, Object>();
105 + return <String, Object>{};
106 });
107
106 - Future<List<Map<String, dynamic>>> getHistory({String address}) =>
107 - call(method: 'blockchain.address.get_history', params: [address])
108 + Future<List<Map<String, dynamic>>> getHistory(String scriptHash) =>
109 + call(method: 'blockchain.scripthash.get_history', params: [scriptHash])
110 .then((dynamic result) {
111 if (result is List) {
112 return result.map((dynamic val) {
@@ -112,26 +114,94 @@ class ElectrumClient {
114 return val;
115 }
116
115 - return Map<String, Object>();
117 + return <String, Object>{};
118 }).toList();
119 }
120
121 return [];
122 });
123
122 - Future<String> getTransactionRaw({@required String hash}) async =>
123 - call(method: 'blockchain.transaction.get', params: [hash])
124 + Future<List<Map<String, dynamic>>> getListUnspentWithAddress(
125 + String address) =>
126 + call(
127 + method: 'blockchain.scripthash.listunspent',
128 + params: [scriptHash(address)]).then((dynamic result) {
129 + if (result is List) {
130 + return result.map((dynamic val) {
131 + if (val is Map<String, Object>) {
132 + val['address'] = address;
133 + return val;
134 + }
135 +
136 + return <String, Object>{};
137 + }).toList();
138 + }
139 +
140 + return [];
141 + });
142 +
143 + Future<List<Map<String, dynamic>>> getListUnspent(String scriptHash) =>
144 + call(method: 'blockchain.scripthash.listunspent', params: [scriptHash])
145 .then((dynamic result) {
125 - if (result is String) {
146 + if (result is List) {
147 + return result.map((dynamic val) {
148 + if (val is Map<String, Object>) {
149 + return val;
150 + }
151 +
152 + return <String, Object>{};
153 + }).toList();
154 + }
155 +
156 + return [];
157 + });
158 +
159 + Future<List<Map<String, dynamic>>> getMempool(String scriptHash) =>
160 + call(method: 'blockchain.scripthash.get_mempool', params: [scriptHash])
161 + .then((dynamic result) {
162 + if (result is List) {
163 + return result.map((dynamic val) {
164 + if (val is Map<String, Object>) {
165 + return val;
166 + }
167 +
168 + return <String, Object>{};
169 + }).toList();
170 + }
171 +
172 + return [];
173 + });
174 +
175 + Future<Map<String, Object>> getTransactionRaw(
176 + {@required String hash}) async =>
177 + call(method: 'blockchain.transaction.get', params: [hash, true])
178 + .then((dynamic result) {
179 + if (result is Map<String, Object>) {
180 return result;
181 }
182
129 - return '';
183 + return <String, Object>{};
184 });
185
132 - Future<String> broadcastTransaction({@required String transactionRaw}) async =>
186 + Future<Map<String, Object>> getTransactionExpanded(
187 + {@required String hash}) async {
188 + final originalTx = await getTransactionRaw(hash: hash);
189 + final vins = originalTx['vin'] as List<Object>;
190 +
191 + for (dynamic vin in vins) {
192 + if (vin is Map<String, Object>) {
193 + vin['tx'] = await getTransactionRaw(hash: vin['txid'] as String);
194 + }
195 + }
196 +
197 + return originalTx;
198 + }
199 +
200 + Future<String> broadcastTransaction(
201 + {@required String transactionRaw}) async =>
202 call(method: 'blockchain.transaction.broadcast', params: [transactionRaw])
203 .then((dynamic result) {
204 + print('result $result');
205 if (result is String) {
206 return result;
207 }
@@ -163,11 +233,11 @@ class ElectrumClient {
233 return 0;
234 });
235
166 - BehaviorSubject<Object> addressUpdate({@required String address}) =>
236 + BehaviorSubject<Object> scripthashUpdate(String scripthash) =>
237 subscribe<Object>(
168 - id: 'blockchain.address.subscribe:$address',
169 - method: 'blockchain.address.subscribe',
170 - params: [address]);
238 + id: 'blockchain.scripthash.subscribe:$scripthash',
239 + method: 'blockchain.scripthash.subscribe',
240 + params: [scripthash]);
241
242 BehaviorSubject<T> subscribe<T>(
243 {@required String id,
@@ -218,15 +288,12 @@ class ElectrumClient {
288 void _methodHandler(
289 {@required String method, @required Map<String, Object> request}) {
290 switch (method) {
221 - case 'blockchain.address.subscribe':
291 + case 'blockchain.scripthash.subscribe':
292 final params = request['params'] as List<dynamic>;
223 - final address = params.first as String;
224 - final id = 'blockchain.address.subscribe:$address';
225 -
226 - if (_tasks[id] != null) {
227 - _tasks[id].subject.add(params.last);
228 - }
293 + final scripthash = params.first as String;
294 + final id = 'blockchain.scripthash.subscribe:$scripthash';
295
296 + _tasks[id]?.subject?.add(params.last);
297 break;
298 default:
299 break;
lib/bitcoin/pending_bitcoin_transaction.dart new
+47
@@ -0,0 +1,47 @@
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/src/domain/common/transaction_direction.dart';
4 +import 'package:flutter/foundation.dart';
5 +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
6 +import 'package:cake_wallet/core/pending_transaction.dart';
7 +import 'package:cake_wallet/bitcoin/electrum.dart';
8 +
9 +class PendingBitcoinTransaction with PendingTransaction {
10 + PendingBitcoinTransaction(this._tx,
11 + {@required this.eclient, @required this.amount, @required this.fee})
12 + : _listeners = <void Function(BitcoinTransactionInfo transaction)>[];
13 +
14 + final bitcoin.Transaction _tx;
15 + final ElectrumClient eclient;
16 + final int amount;
17 + final int fee;
18 +
19 + String get id => _tx.getId();
20 +
21 + @override
22 + String get amountFormatted => bitcoinAmountToString(amount: amount);
23 +
24 + @override
25 + String get feeFormatted => bitcoinAmountToString(amount: fee);
26 +
27 + final List<void Function(BitcoinTransactionInfo transaction)> _listeners;
28 +
29 + @override
30 + Future<void> commit() async {
31 + await eclient.broadcastTransaction(transactionRaw: _tx.toHex());
32 + _listeners?.forEach((listener) => listener(transactionInfo()));
33 + }
34 +
35 + void addListener(
36 + void Function(BitcoinTransactionInfo transaction) listener) =>
37 + _listeners.add(listener);
38 +
39 + BitcoinTransactionInfo transactionInfo() => BitcoinTransactionInfo(
40 + id: id,
41 + height: 0,
42 + amount: amount,
43 + direction: TransactionDirection.outgoing,
44 + date: DateTime.now(),
45 + isPending: true,
46 + confirmations: 0);
47 +}
lib/bitcoin/script_hash.dart new
+18
@@ -0,0 +1,18 @@
1 +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
2 +import 'package:crypto/crypto.dart';
3 +
4 +String scriptHash(String address) {
5 + final outputScript = bitcoin.Address.addressToOutputScript(address);
6 + final splitted = sha256.convert(outputScript).toString().split('');
7 + var res = '';
8 +
9 + for (var i = splitted.length - 1; i >= 0; i--) {
10 + final char = splitted[i];
11 + i--;
12 + final nextChar = splitted[i];
13 + res += nextChar;
14 + res += char;
15 + }
16 +
17 + return res;
18 +}
\ No newline at end of file
lib/bitcoin/utils.dart new
+26
@@ -0,0 +1,26 @@
1 +import 'dart:typed_data';
2 +import 'package:flutter/foundation.dart';
3 +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
4 +import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData;
5 +import 'package:hex/hex.dart';
6 +
7 +bitcoin.PaymentData generatePaymentData(
8 + {@required bitcoin.HDWallet hd, @required int index}) =>
9 + PaymentData(
10 + pubkey: Uint8List.fromList(HEX.decode(hd.derive(index).pubKey)));
11 +
12 +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);
18 +
19 +String generateAddress({@required bitcoin.HDWallet hd, @required int index}) =>
20 + bitcoin
21 + .P2WPKH(
22 + data: PaymentData(
23 + pubkey:
24 + Uint8List.fromList(HEX.decode(hd.derive(index).pubKey))))
25 + .data
26 + .address;
lib/core/amount_validator.dart
+2 -2
@@ -13,10 +13,10 @@ class AmountValidator extends TextValidator {
13 static String _pattern(WalletType type) {
14 switch (type) {
15 case WalletType.monero:
16 - return '^([0-9]+([.][0-9]{0,12})?|[.][0-9]{1,12})\$';
16 + return '^([0-9]+([.\,][0-9]{0,12})?|[.\,][0-9]{1,12})\$';
17 case WalletType.bitcoin:
18 // FIXME: Incorrect pattern for bitcoin
19 - return '^([0-9]+([.][0-9]{0,12})?|[.][0-9]{1,12})\$';
19 + return '^([0-9]+([.\,][0-9]{0,12})?|[.\,][0-9]{1,12})\$';
20 default:
21 return '';
22 }
lib/core/pending_transaction.dart new
+6
@@ -0,0 +1,6 @@
1 +mixin PendingTransaction {
2 + String get amountFormatted;
3 + String get feeFormatted;
4 +
5 + Future<void> commit();
6 +}
\ No newline at end of file
lib/core/transaction_history.dart
+14 -3
@@ -1,3 +1,4 @@
1 +import 'package:flutter/foundation.dart';
2 import 'package:mobx/mobx.dart';
3 import 'package:cake_wallet/src/domain/common/transaction_info.dart';
4
@@ -5,7 +6,7 @@ abstract class TransactionHistoryBase<TransactionType extends TransactionInfo> {
6 TransactionHistoryBase() : _isUpdating = false;
7
8 @observable
8 - ObservableList<TransactionType> transactions;
9 + ObservableMap<String, TransactionType> transactions;
10
11 bool _isUpdating;
12
@@ -24,5 +25,15 @@ abstract class TransactionHistoryBase<TransactionType extends TransactionInfo> {
25 }
26 }
27
27 - Future<List<TransactionType>> fetchTransactions();
28 -}
\ No newline at end of file
28 + void updateAsync({void Function() onFinished}) {
29 + fetchTransactionsAsync(
30 + (transaction) => transactions[transaction.id] = transaction,
31 + onFinished: onFinished);
32 + }
33 +
34 + void fetchTransactionsAsync(
35 + void Function(TransactionType transaction) onTransactionLoaded,
36 + {void Function() onFinished});
37 +
38 + Future<Map<String, TransactionType>> fetchTransactions();
39 +}
lib/core/wallet_base.dart
+5 -1
@@ -1,5 +1,7 @@
1 import 'package:flutter/foundation.dart';
2 +import 'package:cake_wallet/core/pending_transaction.dart';
3 import 'package:cake_wallet/core/transaction_history.dart';
4 +import 'package:cake_wallet/src/domain/common/transaction_priority.dart';
5 import 'package:cake_wallet/src/domain/common/crypto_currency.dart';
6 import 'package:cake_wallet/src/domain/common/sync_status.dart';
7 import 'package:cake_wallet/src/domain/common/node.dart';
@@ -30,7 +32,9 @@ abstract class WalletBase<BalaceType> {
32
33 Future<void> startSync();
34
33 - Future<void> createTransaction(Object credentials);
35 + Future<PendingTransaction> createTransaction(Object credentials);
36 +
37 + double calculateEstimatedFee(TransactionPriority priority);
38
39 Future<void> save();
40 }
lib/di.dart
+23 -24
@@ -36,7 +36,7 @@ import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
36 import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
37 import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart';
38 import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
39 -import 'package:cake_wallet/view_model/send_view_model.dart';
39 +import 'package:cake_wallet/view_model/send/send_view_model.dart';
40 import 'package:cake_wallet/view_model/settings/settings_view_model.dart';
41 import 'package:cake_wallet/view_model/wallet_keys_view_model.dart';
42 import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart';
@@ -105,8 +105,7 @@ Future setup(
105 getIt.registerSingleton<ContactService>(
106 ContactService(contactSource, getIt.get<AppStore>().contactListStore));
107 getIt.registerSingleton<TradesStore>(TradesStore(
108 - tradesSource: tradesSource,
109 - settingsStore: getIt.get<SettingsStore>()));
108 + tradesSource: tradesSource, settingsStore: getIt.get<SettingsStore>()));
109 getIt.registerSingleton<TradeFilterStore>(
110 TradeFilterStore(wallet: getIt.get<AppStore>().wallet));
111 getIt.registerSingleton<TransactionFilterStore>(TransactionFilterStore());
@@ -143,21 +142,18 @@ Future setup(
142 getIt.registerFactory<WalletAddressListViewModel>(
143 () => WalletAddressListViewModel(wallet: getIt.get<AppStore>().wallet));
144
146 - getIt.registerFactory(
147 - () => BalanceViewModel(
148 - wallet: getIt.get<AppStore>().wallet,
149 - settingsStore: getIt.get<SettingsStore>(),
150 - fiatConvertationStore: getIt.get<FiatConvertationStore>()));
145 + getIt.registerFactory(() => BalanceViewModel(
146 + wallet: getIt.get<AppStore>().wallet,
147 + settingsStore: getIt.get<SettingsStore>(),
148 + fiatConvertationStore: getIt.get<FiatConvertationStore>()));
149
152 - getIt.registerFactory(
153 - () => DashboardViewModel(
154 - balanceViewModel: getIt.get<BalanceViewModel>(),
155 - appStore: getIt.get<AppStore>(),
156 - tradesStore: getIt.get<TradesStore>(),
157 - tradeFilterStore: getIt.get<TradeFilterStore>(),
158 - transactionFilterStore: getIt.get<TransactionFilterStore>(),
159 - pageViewStore: getIt.get<PageViewStore>()
160 - ));
150 + getIt.registerFactory(() => DashboardViewModel(
151 + balanceViewModel: getIt.get<BalanceViewModel>(),
152 + appStore: getIt.get<AppStore>(),
153 + tradesStore: getIt.get<TradesStore>(),
154 + tradeFilterStore: getIt.get<TradeFilterStore>(),
155 + transactionFilterStore: getIt.get<TransactionFilterStore>(),
156 + pageViewStore: getIt.get<PageViewStore>()));
157
158 getIt.registerFactory<AuthService>(() => AuthService(
159 secureStorage: getIt.get<FlutterSecureStorage>(),
@@ -185,10 +181,9 @@ Future setup(
181 onAuthenticationFinished: onAuthFinished,
182 closable: false));
183
188 - getIt.registerFactory<DashboardPage>(
189 - () => DashboardPage(
190 - walletViewModel: getIt.get<DashboardViewModel>(),
191 - addressListViewModel: getIt.get<WalletAddressListViewModel>()));
184 + getIt.registerFactory<DashboardPage>(() => DashboardPage(
185 + walletViewModel: getIt.get<DashboardViewModel>(),
186 + addressListViewModel: getIt.get<WalletAddressListViewModel>()));
187
188 getIt.registerFactory<ReceivePage>(() => ReceivePage(
189 addressListViewModel: getIt.get<WalletAddressListViewModel>()));
@@ -203,7 +198,9 @@ Future setup(
198 getIt.get<WalletAddressEditOrCreateViewModel>(param1: item)));
199
200 getIt.registerFactory<SendViewModel>(() => SendViewModel(
206 - getIt.get<AppStore>().wallet, getIt.get<AppStore>().settingsStore));
201 + getIt.get<AppStore>().wallet,
202 + getIt.get<AppStore>().settingsStore,
203 + getIt.get<FiatConvertationStore>()));
204
205 getIt.registerFactory(
206 () => SendPage(sendViewModel: getIt.get<SendViewModel>()));
@@ -243,8 +240,10 @@ Future setup(
240 moneroAccountCreationViewModel:
241 getIt.get<MoneroAccountEditOrCreateViewModel>()));
242
246 - getIt.registerFactory(
247 - () => SettingsViewModel(getIt.get<AppStore>().settingsStore));
243 + getIt.registerFactory(() {
244 + final appStore = getIt.get<AppStore>();
245 + return SettingsViewModel(appStore.settingsStore, appStore.wallet);
246 + });
247
248 getIt.registerFactory(() => SettingsPage(getIt.get<SettingsViewModel>()));
249
lib/monero/monero_transaction_history.dart
+20 -3
@@ -20,12 +20,29 @@ class MoneroTransactionHistory = MoneroTransactionHistoryBase
20 abstract class MoneroTransactionHistoryBase
21 extends TransactionHistoryBase<MoneroTransactionInfo> with Store {
22 MoneroTransactionHistoryBase() {
23 - transactions = ObservableList<MoneroTransactionInfo>();
23 + transactions = ObservableMap<String, MoneroTransactionInfo>();
24 }
25
26 @override
27 - Future<List<MoneroTransactionInfo>> fetchTransactions() async {
27 + Future<Map<String, MoneroTransactionInfo>> fetchTransactions() async {
28 monero_transaction_history.refreshTransactions();
29 - return _getAllTransactions(null);
29 + return _getAllTransactions(null).fold<Map<String, MoneroTransactionInfo>>(
30 + <String, MoneroTransactionInfo>{},
31 + (Map<String, MoneroTransactionInfo> acc, MoneroTransactionInfo tx) {
32 + acc[tx.id] = tx;
33 + return acc;
34 + });
35 }
36 +
37 + @override
38 + void updateAsync({void Function() onFinished}) {
39 + fetchTransactionsAsync(
40 + (transaction) => transactions[transaction.id] = transaction,
41 + onFinished: onFinished);
42 + }
43 +
44 + @override
45 + void fetchTransactionsAsync(
46 + void Function(MoneroTransactionInfo transaction) onTransactionLoaded,
47 + {void Function() onFinished}) {}
48 }
lib/monero/monero_wallet.dart
+31 -1
@@ -16,6 +16,9 @@ import 'package:cake_wallet/src/domain/monero/account.dart';
16 import 'package:cake_wallet/src/domain/monero/account_list.dart';
17 import 'package:cake_wallet/src/domain/monero/subaddress.dart';
18 import 'package:cake_wallet/src/domain/common/node.dart';
19 +import 'package:cake_wallet/core/pending_transaction.dart';
20 +import 'package:cake_wallet/src/domain/common/transaction_priority.dart';
21 +import 'package:cake_wallet/src/domain/common/calculate_fiat_amount.dart' as cfa;
22
23 part 'monero_wallet.g.dart';
24
@@ -133,7 +136,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
136 }
137
138 @override
136 - Future<void> createTransaction(Object credentials) async {
139 + Future<PendingTransaction> createTransaction(Object credentials) async {
140 // final _credentials = credentials as MoneroTransactionCreationCredentials;
141 // final transactionDescription = await transaction_history.createTransaction(
142 // address: _credentials.address,
@@ -146,6 +149,33 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
149 // transactionDescription);
150 }
151
152 + @override
153 + double calculateEstimatedFee(TransactionPriority priority) {
154 + // FIXME: hardcoded value;
155 +
156 + if (priority == TransactionPriority.slow) {
157 + return 0.00002459;
158 + }
159 +
160 + if (priority == TransactionPriority.regular) {
161 + return 0.00012305;
162 + }
163 +
164 + if (priority == TransactionPriority.medium) {
165 + return 0.00024503;
166 + }
167 +
168 + if (priority == TransactionPriority.fast) {
169 + return 0.00061453;
170 + }
171 +
172 + if (priority == TransactionPriority.fastest) {
173 + return 0.0260216;
174 + }
175 +
176 + return 0;
177 + }
178 +
179 @override
180 Future<void> save() async {
181 // if (_isSaving) {
lib/src/domain/common/transaction_info.dart
+1
@@ -1,6 +1,7 @@
1 import 'package:cake_wallet/src/domain/common/transaction_direction.dart';
2
3 abstract class TransactionInfo extends Object {
4 + String id;
5 int amount;
6 TransactionDirection direction;
7 bool isPending;
lib/src/screens/send/send_page.dart
+278 -252
@@ -1,40 +1,27 @@
1 -import 'package:cake_wallet/core/address_validator.dart';
2 -import 'package:cake_wallet/core/amount_validator.dart';
3 -import 'package:cake_wallet/src/screens/auth/auth_page.dart';
4 -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
5 -import 'package:cake_wallet/view_model/send_view_model.dart';
1 +import 'dart:ui';
2 import 'package:flutter/cupertino.dart';
3 import 'package:flutter/material.dart';
4 import 'package:flutter/services.dart';
5 import 'package:flutter_mobx/flutter_mobx.dart';
6 import 'package:mobx/mobx.dart';
11 -import 'package:provider/provider.dart';
7 import 'package:cake_wallet/palette.dart';
8 import 'package:cake_wallet/routes.dart';
9 +import 'package:cake_wallet/src/screens/auth/auth_page.dart';
10 import 'package:cake_wallet/src/widgets/address_text_field.dart';
11 import 'package:cake_wallet/src/widgets/primary_button.dart';
16 -import 'package:cake_wallet/src/stores/settings/settings_store.dart';
17 -import 'package:cake_wallet/src/stores/balance/balance_store.dart';
18 -import 'package:cake_wallet/src/stores/wallet/wallet_store.dart';
19 -import 'package:cake_wallet/src/stores/send/send_store.dart';
20 -
21 -//import 'package:cake_wallet/src/stores/send/sending_state.dart';
12 +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
13 +import 'package:cake_wallet/view_model/send/send_view_model.dart';
14 import 'package:cake_wallet/src/screens/base_page.dart';
23 -import 'package:cake_wallet/src/domain/common/crypto_currency.dart';
24 -import 'package:cake_wallet/src/domain/common/calculate_estimated_fee.dart';
15 import 'package:cake_wallet/generated/i18n.dart';
26 -import 'package:cake_wallet/src/domain/common/sync_status.dart';
27 -import 'package:cake_wallet/src/stores/sync/sync_store.dart';
16 import 'package:cake_wallet/src/widgets/top_panel.dart';
29 -import 'package:dotted_border/dotted_border.dart';
17 import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
18 import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
19 import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart';
33 -import 'package:cake_wallet/src/screens/send/widgets/sending_alert.dart';
34 -import 'package:cake_wallet/src/widgets/template_tile.dart';
35 -import 'package:cake_wallet/src/stores/send_template/send_template_store.dart';
20 +import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
21 import 'package:cake_wallet/src/widgets/trail_button.dart';
22
23 +// FIXME: Refactor this screen.
24 +
25 class SendPage extends BasePage {
26 SendPage({@required this.sendViewModel});
27
@@ -53,11 +40,8 @@ class SendPage extends BasePage {
40 bool get resizeToAvoidBottomPadding => false;
41
42 @override
56 - Widget trailing(context) {
57 -// final sendStore = Provider.of<SendStore>(context);
58 -
59 - return TrailButton(caption: S.of(context).clear, onPressed: () => null);
60 - }
43 + Widget trailing(context) => TrailButton(
44 + caption: S.of(context).clear, onPressed: () => sendViewModel.reset());
45
46 @override
47 Widget body(BuildContext context) => SendForm(sendViewModel: sendViewModel);
@@ -95,36 +79,28 @@ class SendFormState extends State<SendForm> {
79 }
80
81 Future<void> getOpenaliasRecord(BuildContext context) async {
98 - final sendStore = Provider.of<SendStore>(context);
99 - final isOpenalias =
100 - await sendStore.isOpenaliasRecord(_addressController.text);
101 -
102 - if (isOpenalias) {
103 - _addressController.text = sendStore.recordAddress;
104 -
105 - await showDialog<void>(
106 - context: context,
107 - builder: (BuildContext context) {
108 - return AlertWithOneAction(
109 - alertTitle: S.of(context).openalias_alert_title,
110 - alertContent:
111 - S.of(context).openalias_alert_content(sendStore.recordName),
112 - buttonText: S.of(context).ok,
113 - buttonAction: () => Navigator.of(context).pop());
114 - });
115 - }
82 +// final sendStore = Provider.of<SendStore>(context);
83 +// final isOpenalias =
84 +// await sendStore.isOpenaliasRecord(_addressController.text);
85 +//
86 +// if (isOpenalias) {
87 +// _addressController.text = sendStore.recordAddress;
88 +//
89 +// await showDialog<void>(
90 +// context: context,
91 +// builder: (BuildContext context) {
92 +// return AlertWithOneAction(
93 +// alertTitle: S.of(context).openalias_alert_title,
94 +// alertContent:
95 +// S.of(context).openalias_alert_content(sendStore.recordName),
96 +// buttonText: S.of(context).ok,
97 +// buttonAction: () => Navigator.of(context).pop());
98 +// });
99 +// }
100 }
101
102 @override
103 Widget build(BuildContext context) {
120 -// final settingsStore = Provider.of<SettingsStore>(context);
121 -// final sendStore = Provider.of<SendStore>(context);
122 -// sendStore.settingsStore = settingsStore;
123 -// final balanceStore = Provider.of<BalanceStore>(context);
124 -// final walletStore = Provider.of<WalletStore>(context);
125 -// final syncStore = Provider.of<SyncStore>(context);
126 -// final sendTemplateStore = Provider.of<SendTemplateStore>(context);
127 -
104 _setEffects(context);
105
106 return Container(
@@ -140,7 +116,8 @@ class SendFormState extends State<SendForm> {
116 child: Column(children: <Widget>[
117 AddressTextField(
118 controller: _addressController,
143 - placeholder: S.of(context).send_monero_address,
119 + placeholder: 'Address',
120 + //S.of(context).send_monero_address, FIXME: placeholder for btc and xmr address text field.
121 focusNode: _focusNode,
122 onURIScanned: (uri) {
123 var address = '';
@@ -163,110 +140,86 @@ class SendFormState extends State<SendForm> {
140 buttonColor: Theme.of(context).accentTextTheme.title.color,
141 validator: widget.sendViewModel.addressValidator,
142 ),
166 - Observer(builder: (_) {
167 - return Padding(
168 - padding: const EdgeInsets.only(top: 20),
169 - child: TextFormField(
170 - style: TextStyle(
171 - fontSize: 16.0,
172 - color: Theme.of(context)
173 - .primaryTextTheme
174 - .title
175 - .color),
176 - controller: _cryptoAmountController,
177 - keyboardType: TextInputType.numberWithOptions(
178 - signed: false, decimal: true),
179 - inputFormatters: [
180 - BlacklistingTextInputFormatter(
181 - RegExp('[\\-|\\ |\\,]'))
182 - ],
183 - decoration: InputDecoration(
184 - prefixIcon: Padding(
185 - padding: EdgeInsets.only(top: 12),
186 - child: Text('XMR:',
187 - style: TextStyle(
188 - fontSize: 16,
189 - fontWeight: FontWeight.w500,
143 + Padding(
144 + padding: const EdgeInsets.only(top: 20),
145 + child: TextFormField(
146 + onChanged: (value) =>
147 + widget.sendViewModel.setCryptoAmount(value),
148 + style: TextStyle(
149 + fontSize: 16.0,
150 + color:
151 + Theme.of(context).primaryTextTheme.title.color),
152 + controller: _cryptoAmountController,
153 + keyboardType: TextInputType.numberWithOptions(
154 + signed: false, decimal: true),
155 +// inputFormatters: [
156 +// BlacklistingTextInputFormatter(
157 +// RegExp('[\\-|\\ |\\,]'))
158 +// ],
159 + decoration: InputDecoration(
160 + prefixIcon: Padding(
161 + padding: EdgeInsets.only(top: 12),
162 + child: Text('${widget.sendViewModel.currency.toString()}:',
163 + style: TextStyle(
164 + fontSize: 16,
165 + fontWeight: FontWeight.w500,
166 + color: Theme.of(context)
167 + .primaryTextTheme
168 + .title
169 + .color,
170 + )),
171 + ),
172 + suffixIcon: Padding(
173 + padding: EdgeInsets.only(bottom: 5),
174 + child: Container(
175 + height: 32,
176 + width: 32,
177 + margin: EdgeInsets.only(
178 + left: 12, bottom: 7, top: 4),
179 + decoration: BoxDecoration(
180 color: Theme.of(context)
191 - .primaryTextTheme
181 + .accentTextTheme
182 .title
183 .color,
194 - )),
195 - ),
196 - suffixIcon: Padding(
197 - padding: EdgeInsets.only(bottom: 5),
198 - child: Row(
199 - mainAxisSize: MainAxisSize.min,
200 - mainAxisAlignment:
201 - MainAxisAlignment.spaceBetween,
202 - children: <Widget>[
203 - Container(
204 - width:
205 - MediaQuery.of(context).size.width / 2,
206 - alignment: Alignment.centerLeft,
207 - child: Text(
208 - ' / ' + widget.sendViewModel.balance,
209 - maxLines: 1,
210 - overflow: TextOverflow.ellipsis,
184 + borderRadius:
185 + BorderRadius.all(Radius.circular(6))),
186 + child: InkWell(
187 + onTap: () => widget.sendViewModel.setAll(),
188 + child: Center(
189 + child: Text(S.of(context).all,
190 + textAlign: TextAlign.center,
191 style: TextStyle(
212 - fontSize: 16,
192 + fontSize: 9,
193 + fontWeight: FontWeight.bold,
194 color: Theme.of(context)
195 .primaryTextTheme
196 .caption
197 .color)),
198 ),
218 - Container(
219 - height: 32,
220 - width: 32,
221 - margin: EdgeInsets.only(
222 - left: 12, bottom: 7, top: 4),
223 - decoration: BoxDecoration(
224 - color: Theme.of(context)
225 - .accentTextTheme
226 - .title
227 - .color,
228 - borderRadius: BorderRadius.all(
229 - Radius.circular(6))),
230 - child: InkWell(
231 - onTap: () => null,
232 - // widget.sendViewModel,
233 - child: Center(
234 - child: Text(S.of(context).all,
235 - textAlign: TextAlign.center,
236 - style: TextStyle(
237 - fontSize: 9,
238 - fontWeight: FontWeight.bold,
239 - color: Theme.of(context)
240 - .primaryTextTheme
241 - .caption
242 - .color)),
243 - ),
244 - ),
245 - )
246 - ],
247 - ),
248 - ),
249 - hintStyle: TextStyle(
250 - fontSize: 16.0,
251 - color: Theme.of(context)
252 - .primaryTextTheme
253 - .title
254 - .color),
255 - hintText: '0.0000',
256 - focusedBorder: UnderlineInputBorder(
257 - borderSide: BorderSide(
258 - color: Theme.of(context).dividerColor,
259 - width: 1.0)),
260 - enabledBorder: UnderlineInputBorder(
261 - borderSide: BorderSide(
262 - color: Theme.of(context).dividerColor,
263 - width: 1.0))),
264 - validator: widget.sendViewModel.amountValidator),
265 - );
266 - }),
199 + ),
200 + )),
201 + hintStyle: TextStyle(
202 + fontSize: 16.0,
203 + color: Theme.of(context)
204 + .primaryTextTheme
205 + .title
206 + .color),
207 + hintText: '0.0000',
208 + focusedBorder: UnderlineInputBorder(
209 + borderSide: BorderSide(
210 + color: Theme.of(context).dividerColor,
211 + width: 1.0)),
212 + enabledBorder: UnderlineInputBorder(
213 + borderSide: BorderSide(
214 + color: Theme.of(context).dividerColor,
215 + width: 1.0))),
216 + validator: widget.sendViewModel.amountValidator),
217 + ),
218 Padding(
219 padding: const EdgeInsets.only(top: 20),
220 child: TextFormField(
221 + onChanged: (value) =>
222 + widget.sendViewModel.setFiatAmount(value),
223 style: TextStyle(
224 fontSize: 16.0,
225 color:
@@ -274,10 +227,10 @@ class SendFormState extends State<SendForm> {
227 controller: _fiatAmountController,
228 keyboardType: TextInputType.numberWithOptions(
229 signed: false, decimal: true),
277 - inputFormatters: [
278 - BlacklistingTextInputFormatter(
279 - RegExp('[\\-|\\ |\\,]'))
280 - ],
230 +// inputFormatters: [
231 +// BlacklistingTextInputFormatter(
232 +// RegExp('[\\-|\\ |\\,]'))
233 +// ],
234 decoration: InputDecoration(
235 prefixIcon: Padding(
236 padding: EdgeInsets.only(top: 12),
@@ -426,52 +379,43 @@ class SendFormState extends State<SendForm> {
379 bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
380 bottomSection: Observer(builder: (_) {
381 return LoadingPrimaryButton(
429 - onPressed: () => null,
430 -// syncStore.status is SyncedSyncStatus
431 -// ? () async {
432 -// // Hack. Don't ask me.
433 -// FocusScope.of(context).requestFocus(FocusNode());
434 -//
435 -// if (_formKey.currentState.validate()) {
436 -// await showDialog<void>(
437 -// context: context,
438 -// builder: (dialogContext) {
439 -// return AlertWithTwoActions(
440 -// alertTitle:
441 -// S.of(context).send_creating_transaction,
442 -// alertContent: S.of(context).confirm_sending,
443 -// leftButtonText: S.of(context).send,
444 -// rightButtonText: S.of(context).cancel,
445 -// actionLeftButton: () async {
446 -// await Navigator.of(dialogContext)
447 -// .popAndPushNamed(Routes.auth, arguments:
448 -// (bool isAuthenticatedSuccessfully,
449 -// AuthPageState auth) {
450 -// if (!isAuthenticatedSuccessfully) {
451 -// return;
452 -// }
453 -//
454 -// Navigator.of(auth.context).pop();
455 -//
456 -// sendStore.createTransaction(
457 -// address: _addressController.text,
458 -// paymentId: '');
459 -// });
460 -// },
461 -// actionRightButton: () =>
462 -// Navigator.of(context).pop());
463 -// });
464 -// }
465 -// }
466 -// : null,
382 + onPressed: () async {
383 + FocusScope.of(context).requestFocus(FocusNode());
384 +
385 + if (!_formKey.currentState.validate()) {
386 + return;
387 + }
388 +
389 + await showDialog<void>(
390 + context: context,
391 + builder: (dialogContext) {
392 + return AlertWithTwoActions(
393 + alertTitle: S.of(context).send_creating_transaction,
394 + alertContent: S.of(context).confirm_sending,
395 + leftButtonText: S.of(context).send,
396 + rightButtonText: S.of(context).cancel,
397 + actionLeftButton: () async {
398 + await Navigator.of(dialogContext)
399 + .popAndPushNamed(Routes.auth, arguments:
400 + (bool isAuthenticatedSuccessfully,
401 + AuthPageState auth) {
402 + if (!isAuthenticatedSuccessfully) {
403 + return;
404 + }
405 +
406 + Navigator.of(auth.context).pop();
407 + widget.sendViewModel.createTransaction();
408 + });
409 + },
410 + actionRightButton: () => Navigator.of(context).pop());
411 + });
412 + },
413 text: S.of(context).send,
414 color: Colors.blue,
415 textColor: Colors.white,
416 isLoading: widget.sendViewModel.state is TransactionIsCreating ||
417 widget.sendViewModel.state is TransactionCommitting,
472 - isDisabled:
473 - false // FIXME !(syncStore.status is SyncedSyncStatus),
474 - );
418 + isDisabled: !widget.sendViewModel.isReadyForSend);
419 }),
420 ),
421 );
@@ -482,47 +426,42 @@ class SendFormState extends State<SendForm> {
426 return;
427 }
428
485 -// reaction((_) => widget.sendViewModel.fiatAmount, (String amount) {
486 -// if (amount != _fiatAmountController.text) {
487 -// _fiatAmountController.text = amount;
488 -// }
489 -// });
490 -//
491 -// reaction((_) => widget.sendViewModel.cryptoAmount, (String amount) {
492 -// if (amount != _cryptoAmountController.text) {
493 -// _cryptoAmountController.text = amount;
494 -// }
495 -// });
496 -//
497 -// reaction((_) => widget.sendViewModel.address, (String address) {
498 -// if (address != _addressController.text) {
499 -// _addressController.text = address;
500 -// }
501 -// });
502 -//
503 -// _addressController.addListener(() {
504 -// final address = _addressController.text;
505 -//
506 -// if (widget.sendViewModel.address != address) {
507 -// widget.sendViewModel.changeAddress(address);
508 -// }
509 -// });
429 + reaction((_) => widget.sendViewModel.all, (bool all) {
430 + if (all) {
431 + _cryptoAmountController.text = S.current.all;
432 + _fiatAmountController.text = null;
433 + }
434 + });
435
511 -// _fiatAmountController.addListener(() {
512 -// final fiatAmount = _fiatAmountController.text;
513 -//
514 -// if (sendStore.fiatAmount != fiatAmount) {
515 -// sendStore.changeFiatAmount(fiatAmount);
516 -// }
517 -// });
436 + reaction((_) => widget.sendViewModel.fiatAmount, (String amount) {
437 + if (amount != _fiatAmountController.text) {
438 + _fiatAmountController.text = amount;
439 + }
440 + });
441
519 -// _cryptoAmountController.addListener(() {
520 -// final cryptoAmount = _cryptoAmountController.text;
521 -//
522 -// if (sendStore.cryptoAmount != cryptoAmount) {
523 -// sendStore.changeCryptoAmount(cryptoAmount);
524 -// }
525 -// });
442 + reaction((_) => widget.sendViewModel.cryptoAmount, (String amount) {
443 + if (widget.sendViewModel.all && amount != S.current.all) {
444 + widget.sendViewModel.all = false;
445 + }
446 +
447 + if (amount != _cryptoAmountController.text) {
448 + _cryptoAmountController.text = amount;
449 + }
450 + });
451 +
452 + reaction((_) => widget.sendViewModel.address, (String address) {
453 + if (address != _addressController.text) {
454 + _addressController.text = address;
455 + }
456 + });
457 +
458 + _addressController.addListener(() {
459 + final address = _addressController.text;
460 +
461 + if (widget.sendViewModel.address != address) {
462 + widget.sendViewModel.address = address;
463 + }
464 + });
465
466 reaction((_) => widget.sendViewModel.state, (SendViewModelState state) {
467 if (state is SendingFailed) {
@@ -540,30 +479,117 @@ class SendFormState extends State<SendForm> {
479 }
480
481 if (state is TransactionCreatedSuccessfully) {
543 -// WidgetsBinding.instance.addPostFrameCallback((_) {
544 -// showDialog<void>(
545 -// context: context,
546 -// builder: (BuildContext context) {
547 -// return ConfirmSendingAlert(
548 -// alertTitle: S.of(context).confirm_sending,
549 -// amount: S.of(context).send_amount,
550 -// amountValue: sendStore.pendingTransaction.amount,
551 -// fee: S.of(context).send_fee,
552 -// feeValue: sendStore.pendingTransaction.fee,
553 -// leftButtonText: S.of(context).ok,
554 -// rightButtonText: S.of(context).cancel,
555 -// actionLeftButton: () {
556 -// Navigator.of(context).pop();
557 -// sendStore.commitTransaction();
558 -// showDialog<void>(
559 -// context: context,
560 -// builder: (BuildContext context) {
561 -// return SendingAlert(sendStore: sendStore);
562 -// });
563 -// },
564 -// actionRightButton: () => Navigator.of(context).pop());
565 -// });
566 -// });
482 + WidgetsBinding.instance.addPostFrameCallback((_) {
483 + showDialog<void>(
484 + context: context,
485 + builder: (BuildContext context) {
486 + return ConfirmSendingAlert(
487 + alertTitle: S.of(context).confirm_sending,
488 + amount: S.of(context).send_amount,
489 + amountValue:
490 + widget.sendViewModel.pendingTransaction.amountFormatted,
491 + fee: S.of(context).send_fee,
492 + feeValue:
493 + widget.sendViewModel.pendingTransaction.feeFormatted,
494 + leftButtonText: S.of(context).ok,
495 + rightButtonText: S.of(context).cancel,
496 + actionLeftButton: () {
497 + Navigator.of(context).pop();
498 + widget.sendViewModel.commitTransaction();
499 + showDialog<void>(
500 + context: context,
501 + builder: (BuildContext context) {
502 + return Observer(builder: (_) {
503 + final state = widget.sendViewModel.state;
504 +
505 + if (state is TransactionCommitted) {
506 + return Stack(
507 + children: <Widget>[
508 + Container(
509 + color: Theme.of(context).backgroundColor,
510 + child: Center(
511 + child: Image.asset(
512 + 'assets/images/birthday_cake.png'),
513 + ),
514 + ),
515 + Center(
516 + child: Padding(
517 + padding: EdgeInsets.only(
518 + top: 220, left: 24, right: 24),
519 + child: Text(
520 + S.of(context).send_success,
521 + textAlign: TextAlign.center,
522 + style: TextStyle(
523 + fontSize: 22,
524 + fontWeight: FontWeight.bold,
525 + color: Theme.of(context)
526 + .primaryTextTheme
527 + .title
528 + .color,
529 + decoration: TextDecoration.none,
530 + ),
531 + ),
532 + ),
533 + ),
534 + Positioned(
535 + left: 24,
536 + right: 24,
537 + bottom: 24,
538 + child: PrimaryButton(
539 + onPressed: () =>
540 + Navigator.of(context).pop(),
541 + text: S.of(context).send_got_it,
542 + color: Colors.blue,
543 + textColor: Colors.white))
544 + ],
545 + );
546 + }
547 +
548 + return Stack(
549 + children: <Widget>[
550 + Container(
551 + color: Theme.of(context).backgroundColor,
552 + child: Center(
553 + child: Image.asset(
554 + 'assets/images/birthday_cake.png'),
555 + ),
556 + ),
557 + BackdropFilter(
558 + filter: ImageFilter.blur(
559 + sigmaX: 3.0, sigmaY: 3.0),
560 + child: Container(
561 + decoration: BoxDecoration(
562 + color: Theme.of(context)
563 + .backgroundColor
564 + .withOpacity(0.25)),
565 + child: Center(
566 + child: Padding(
567 + padding: EdgeInsets.only(top: 220),
568 + child: Text(
569 + S.of(context).send_sending,
570 + textAlign: TextAlign.center,
571 + style: TextStyle(
572 + fontSize: 22,
573 + fontWeight: FontWeight.bold,
574 + color: Theme.of(context)
575 + .primaryTextTheme
576 + .title
577 + .color,
578 + decoration: TextDecoration.none,
579 + ),
580 + ),
581 + ),
582 + ),
583 + ),
584 + )
585 + ],
586 + );
587 + });
588 + });
589 + },
590 + actionRightButton: () => Navigator.of(context).pop());
591 + });
592 + });
593 }
594
595 if (state is TransactionCommitted) {
lib/src/screens/send/widgets/sending_alert.dart
+1 -1
@@ -1,6 +1,6 @@
1 import 'dart:ui';
2 -import 'package:cake_wallet/src/stores/send/sending_state.dart';
2 import 'package:flutter/material.dart';
3 +import 'package:cake_wallet/src/stores/send/sending_state.dart';
4 import 'package:cake_wallet/src/widgets/primary_button.dart';
5 import 'package:cake_wallet/src/stores/send/send_store.dart';
6 import 'package:cake_wallet/generated/i18n.dart';
lib/src/screens/settings/settings.dart
+5 -3
@@ -39,9 +39,11 @@ class SettingsPage extends BasePage {
39 if (item is PickerListItem) {
40 return Observer(builder: (_) {
41 return SettingsPickerCell<dynamic>(
42 - title: item.title,
43 - selectedItem: item.selectedItem(),
44 - items: item.items);
42 + title: item.title,
43 + selectedItem: item.selectedItem(),
44 + items: item.items,
45 + onItemSelected: (dynamic value) => item.onItemSelected(value),
46 + );
47 });
48 }
49
lib/src/screens/settings/widgets/settings_picker_cell.dart
+20 -15
@@ -4,25 +4,30 @@ import 'package:cake_wallet/src/widgets/standard_list.dart';
4 import 'package:cake_wallet/generated/i18n.dart';
5
6 class SettingsPickerCell<ItemType> extends StandardListRow {
7 - SettingsPickerCell({@required String title, this.selectedItem, this.items})
7 + SettingsPickerCell(
8 + {@required String title,
9 + this.selectedItem,
10 + this.items,
11 + this.onItemSelected})
12 : super(
9 - title: title,
10 - isSelected: false,
11 - onTap: (BuildContext context) async {
12 - final selectedAtIndex = items.indexOf(selectedItem);
13 + title: title,
14 + isSelected: false,
15 + onTap: (BuildContext context) async {
16 + final selectedAtIndex = items.indexOf(selectedItem);
17
14 - await showDialog<void>(
15 - context: context,
16 - builder: (_) => Picker(
17 - items: items,
18 - selectedAtIndex: selectedAtIndex,
19 - title: S.current.please_select,
20 - mainAxisAlignment: MainAxisAlignment.center,
21 - onItemSelected: (Object _) {}));
22 - });
18 + await showDialog<void>(
19 + context: context,
20 + builder: (_) => Picker(
21 + items: items,
22 + selectedAtIndex: selectedAtIndex,
23 + title: S.current.please_select,
24 + mainAxisAlignment: MainAxisAlignment.center,
25 + onItemSelected: (ItemType item) => onItemSelected?.call(item)));
26 + });
27
28 final ItemType selectedItem;
29 final List<ItemType> items;
30 + final void Function(ItemType item) onItemSelected;
31
32 @override
33 Widget buildTrailing(BuildContext context) {
@@ -35,4 +40,4 @@ class SettingsPickerCell<ItemType> extends StandardListRow {
40 color: Theme.of(context).primaryTextTheme.caption.color),
41 );
42 }
38 -}
\ No newline at end of file
43 +}
lib/store/settings_store.dart
+1 -7
@@ -42,12 +42,6 @@ abstract class SettingsStoreBase with Store {
42 languageCode = initialLanguageCode;
43 currentLocale = initialCurrentLocale;
44 itemHeaders = {};
45 -
46 -// actionlistDisplayMode.observe(
47 -// (dynamic _) => _sharedPreferences.setInt(displayActionListModeKey,
48 -// serializeActionlistDisplayModes(actionlistDisplayMode)),
49 -// fireImmediately: false);
50 -
45 _sharedPreferences = sharedPreferences;
46 _nodeSource = nodeSource;
47 }
@@ -120,7 +114,7 @@ abstract class SettingsStoreBase with Store {
114 sharedPreferences.getBool(shouldSaveRecipientAddressKey);
115 final allowBiometricalAuthentication =
116 sharedPreferences.getBool(allowBiometricalAuthenticationKey) ?? false;
123 - final savedDarkTheme = sharedPreferences.getBool(currentDarkTheme) ?? false;
117 + final savedDarkTheme = sharedPreferences.getBool(currentDarkTheme) ?? true;
118 final actionListDisplayMode = ObservableList<ActionListDisplayMode>();
119 actionListDisplayMode.addAll(deserializeActionlistDisplayModes(
120 sharedPreferences.getInt(displayActionListModeKey) ??
lib/view_model/dashboard/balance_view_model.dart
+4 -2
@@ -35,9 +35,11 @@ abstract class BalanceViewModelBase with Store {
35
36 if (_wallet is BitcoinWallet) {
37 return WalletBalance(
38 - unlockedBalance: _wallet.balance.confirmedFormatted,
39 - totalBalance: _wallet.balance.unconfirmedFormatted);
38 + unlockedBalance: _wallet.balance.totalFormatted,
39 + totalBalance: _wallet.balance.totalFormatted);
40 }
41 +
42 + return null;
43 }
44
45 String _getFiatBalance({double price, String cryptoAmount}) {
lib/view_model/dashboard/dashboard_view_model.dart
+22 -27
@@ -29,24 +29,24 @@ part 'dashboard_view_model.g.dart';
29 class DashboardViewModel = DashboardViewModelBase with _$DashboardViewModel;
30
31 abstract class DashboardViewModelBase with Store {
32 - DashboardViewModelBase({
33 - this.balanceViewModel,
34 - this.appStore,
35 - this.tradesStore,
36 - this.tradeFilterStore,
37 - this.transactionFilterStore,
38 - this.pageViewStore}) {
39 -
32 + DashboardViewModelBase(
33 + {this.balanceViewModel,
34 + this.appStore,
35 + this.tradesStore,
36 + this.tradeFilterStore,
37 + this.transactionFilterStore,
38 + this.pageViewStore}) {
39 name = appStore.wallet?.name;
40 wallet ??= appStore.wallet;
41 type = wallet.type;
42
44 - transactions = ObservableList.of(wallet.transactionHistory.transactions
43 + transactions = ObservableList.of(wallet
44 + .transactionHistory.transactions.values
45 .map((transaction) => TransactionListItem(
46 - transaction: transaction,
47 - price: price,
48 - fiatCurrency: appStore.settingsStore.fiatCurrency,
49 - displayMode: balanceDisplayMode)));
46 + transaction: transaction,
47 + price: price,
48 + fiatCurrency: appStore.settingsStore.fiatCurrency,
49 + displayMode: balanceDisplayMode)));
50
51 _reaction = reaction((_) => appStore.wallet, _onWalletChange);
52
@@ -83,15 +83,11 @@ abstract class DashboardViewModelBase with Store {
83 var statusText = '';
84
85 if (status is SyncingSyncStatus) {
86 - statusText = S.current
87 - .Blocks_remaining(
88 - status.toString());
86 + statusText = S.current.Blocks_remaining(status.toString());
87 }
88
89 if (status is FailedSyncStatus) {
92 - statusText = S
93 - .current
94 - .please_try_to_connect_to_another_node;
90 + statusText = S.current.please_try_to_connect_to_another_node;
91 }
92
93 return statusText;
@@ -111,8 +107,7 @@ abstract class DashboardViewModelBase with Store {
107 List<ActionListItem> get items {
108 final _items = <ActionListItem>[];
109
114 - _items
115 - .addAll(transactionFilterStore.filtered(transactions: transactions));
110 + _items.addAll(transactionFilterStore.filtered(transactions: transactions));
111 _items.addAll(tradeFilterStore.filtered(trades: trades));
112
113 return formattedItemsList(_items);
@@ -137,11 +132,11 @@ abstract class DashboardViewModelBase with Store {
132 void _onWalletChange(WalletBase wallet) {
133 name = wallet.name;
134 transactions.clear();
140 - transactions.addAll(wallet.transactionHistory.transactions
141 - .map((transaction) => TransactionListItem(
142 - transaction: transaction,
143 - price: price,
144 - fiatCurrency: appStore.settingsStore.fiatCurrency,
145 - displayMode: balanceDisplayMode)));
135 + transactions.addAll(wallet.transactionHistory.transactions.values.map(
136 + (transaction) => TransactionListItem(
137 + transaction: transaction,
138 + price: price,
139 + fiatCurrency: appStore.settingsStore.fiatCurrency,
140 + displayMode: balanceDisplayMode)));
141 }
142 }
lib/view_model/send/send_view_model.dart new
+171
@@ -0,0 +1,171 @@
1 +import 'package:cake_wallet/src/domain/common/calculate_fiat_amount.dart';
2 +import 'package:cake_wallet/store/dashboard/fiat_convertation_store.dart';
3 +import 'package:intl/intl.dart';
4 +import 'package:mobx/mobx.dart';
5 +import 'package:cake_wallet/core/address_validator.dart';
6 +import 'package:cake_wallet/core/amount_validator.dart';
7 +import 'package:cake_wallet/core/pending_transaction.dart';
8 +import 'package:cake_wallet/core/validator.dart';
9 +import 'package:cake_wallet/core/wallet_base.dart';
10 +import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
11 +import 'package:cake_wallet/monero/monero_wallet.dart';
12 +import 'package:cake_wallet/src/domain/common/sync_status.dart';
13 +import 'package:cake_wallet/src/domain/common/crypto_currency.dart';
14 +import 'package:cake_wallet/src/domain/common/fiat_currency.dart';
15 +import 'package:cake_wallet/src/domain/common/transaction_priority.dart';
16 +import 'package:cake_wallet/store/settings_store.dart';
17 +import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
18 +import 'package:cake_wallet/src/domain/common/wallet_type.dart';
19 +import 'package:cake_wallet/bitcoin/bitcoin_transaction_credentials.dart';
20 +
21 +part 'send_view_model.g.dart';
22 +
23 +class SendViewModel = SendViewModelBase with _$SendViewModel;
24 +
25 +abstract class SendViewModelBase with Store {
26 + SendViewModelBase(
27 + this._wallet, this._settingsStore, this._fiatConversationStore)
28 + : state = InitialSendViewModelState(),
29 + _cryptoNumberFormat = NumberFormat()..maximumFractionDigits = 12,
30 + all = false;
31 +
32 + @observable
33 + SendViewModelState state;
34 +
35 + @observable
36 + String fiatAmount;
37 +
38 + @observable
39 + String cryptoAmount;
40 +
41 + @observable
42 + String address;
43 +
44 + @observable
45 + bool all;
46 +
47 + FiatCurrency get fiat => _settingsStore.fiatCurrency;
48 +
49 + TransactionPriority get transactionPriority =>
50 + _settingsStore.transactionPriority;
51 +
52 + double get estimatedFee =>
53 + _wallet.calculateEstimatedFee(_settingsStore.transactionPriority);
54 +
55 + CryptoCurrency get currency => _wallet.currency;
56 +
57 + Validator get amountValidator => AmountValidator(type: _wallet.type);
58 +
59 + Validator get addressValidator => AddressValidator(type: _wallet.currency);
60 +
61 + PendingTransaction pendingTransaction;
62 +
63 + @computed
64 + String get balance {
65 + if (_wallet is MoneroWallet) {
66 + _wallet.balance.formattedUnlockedBalance;
67 + }
68 +
69 + if (_wallet is BitcoinWallet) {
70 + _wallet.balance.confirmedFormatted;
71 + }
72 +
73 + return '0.0';
74 + }
75 +
76 + @computed
77 + bool get isReadyForSend => _wallet.syncStatus is SyncedSyncStatus;
78 +
79 + final WalletBase _wallet;
80 + final SettingsStore _settingsStore;
81 + final FiatConvertationStore _fiatConversationStore;
82 + NumberFormat _cryptoNumberFormat;
83 +
84 + @action
85 + void setAll() => all = true;
86 +
87 + @action
88 + void reset() {
89 + cryptoAmount = '';
90 + fiatAmount = '';
91 + address = '';
92 + }
93 +
94 + @action
95 + Future<void> createTransaction() async {
96 + try {
97 + state = TransactionIsCreating();
98 + pendingTransaction = await _wallet.createTransaction(_credentials());
99 + state = TransactionCreatedSuccessfully();
100 + } catch (e) {
101 + state = SendingFailed(error: e.toString());
102 + }
103 + }
104 +
105 + @action
106 + Future<void> commitTransaction() async {
107 + try {
108 + state = TransactionCommitting();
109 + await pendingTransaction.commit();
110 + state = TransactionCommitted();
111 + } catch (e) {
112 + state = SendingFailed(error: e.toString());
113 + }
114 + }
115 +
116 + @action
117 + void setCryptoAmount(String amount) {
118 + cryptoAmount = amount;
119 + _updateFiatAmount();
120 + }
121 +
122 + @action
123 + void setFiatAmount(String amount) {
124 + fiatAmount = amount;
125 + _updateCryptoAmount();
126 + }
127 +
128 + @action
129 + void _updateFiatAmount() {
130 + try {
131 + final fiat = calculateFiatAmount(
132 + price: _fiatConversationStore.price, cryptoAmount: cryptoAmount);
133 + if (fiatAmount != fiat) {
134 + fiatAmount = fiat;
135 + }
136 + } catch (_) {
137 + fiatAmount = '';
138 + }
139 + }
140 +
141 + @action
142 + void _updateCryptoAmount() {
143 + try {
144 + final crypto = double.parse(fiatAmount) / _fiatConversationStore.price;
145 + final cryptoAmountTmp = _cryptoNumberFormat.format(crypto);
146 +
147 + if (cryptoAmount != cryptoAmountTmp) {
148 + cryptoAmount = cryptoAmountTmp;
149 + }
150 + } catch (e) {
151 + cryptoAmount = '';
152 + }
153 + }
154 +
155 + Object _credentials() {
156 + final amount =
157 + !all ? double.parse(cryptoAmount.replaceAll(',', '.')) : null;
158 +
159 + switch (_wallet.type) {
160 + case WalletType.bitcoin:
161 + return BitcoinTransactionCredentials(
162 + address, amount, _settingsStore.transactionPriority);
163 + case WalletType.monero:
164 + // FIXME: Wrong credentials
165 + return BitcoinTransactionCredentials(
166 + address, amount, _settingsStore.transactionPriority);
167 + default:
168 + return null;
169 + }
170 + }
171 +}
lib/view_model/send/send_view_model_state.dart new
+18
@@ -0,0 +1,18 @@
1 +import 'package:flutter/foundation.dart';
2 +
3 +abstract class SendViewModelState {}
4 +
5 +class InitialSendViewModelState extends SendViewModelState {}
6 +
7 +class TransactionIsCreating extends SendViewModelState {}
8 +class TransactionCreatedSuccessfully extends SendViewModelState {}
9 +
10 +class TransactionCommitting extends SendViewModelState {}
11 +
12 +class TransactionCommitted extends SendViewModelState {}
13 +
14 +class SendingFailed extends SendViewModelState {
15 + SendingFailed({@required this.error});
16 +
17 + String error;
18 +}
\ No newline at end of file
lib/view_model/send_view_model.dart deleted
-94
@@ -1,94 +0,0 @@
1 -import 'package:cake_wallet/core/address_validator.dart';
2 -import 'package:cake_wallet/core/amount_validator.dart';
3 -import 'package:cake_wallet/core/validator.dart';
4 -import 'package:cake_wallet/core/wallet_base.dart';
5 -import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
6 -import 'package:cake_wallet/monero/monero_wallet.dart';
7 -import 'package:cake_wallet/src/domain/common/balance.dart';
8 -import 'package:cake_wallet/src/domain/common/calculate_estimated_fee.dart';
9 -import 'package:cake_wallet/src/domain/common/crypto_currency.dart';
10 -import 'package:cake_wallet/src/domain/common/fiat_currency.dart';
11 -import 'package:cake_wallet/src/domain/common/transaction_priority.dart';
12 -import 'package:cake_wallet/store/settings_store.dart';
13 -import 'package:flutter/foundation.dart';
14 -import 'package:mobx/mobx.dart';
15 -import 'package:cake_wallet/monero/monero_wallet_service.dart';
16 -import 'package:cake_wallet/bitcoin/bitcoin_wallet_creation_credentials.dart';
17 -import 'package:cake_wallet/core/wallet_creation_service.dart';
18 -import 'package:cake_wallet/core/wallet_credentials.dart';
19 -import 'package:cake_wallet/src/domain/common/wallet_type.dart';
20 -import 'package:cake_wallet/view_model/wallet_creation_vm.dart';
21 -
22 -part 'send_view_model.g.dart';
23 -
24 -abstract class SendViewModelState {}
25 -
26 -class InitialSendViewModelState extends SendViewModelState {}
27 -
28 -class TransactionIsCreating extends SendViewModelState {}
29 -
30 -class TransactionCreatedSuccessfully extends SendViewModelState {}
31 -
32 -class TransactionCommitting extends SendViewModelState {}
33 -
34 -class TransactionCommitted extends SendViewModelState {}
35 -
36 -class SendingFailed extends SendViewModelState {
37 - SendingFailed({@required this.error});
38 -
39 - String error;
40 -}
41 -
42 -class SendViewModel = SendViewModelBase with _$SendViewModel;
43 -
44 -abstract class SendViewModelBase with Store {
45 - SendViewModelBase(this._wallet, this._settingsStore)
46 - : state = InitialSendViewModelState();
47 -
48 - @observable
49 - SendViewModelState state;
50 -
51 - @observable
52 - String fiatAmount;
53 -
54 - @observable
55 - String cryptoAmount;
56 -
57 - @observable
58 - String address;
59 -
60 - FiatCurrency get fiat => _settingsStore.fiatCurrency;
61 -
62 - TransactionPriority get transactionPriority =>
63 - _settingsStore.transactionPriority;
64 -
65 - double get estimatedFee =>
66 - calculateEstimatedFee(priority: transactionPriority);
67 -
68 - CryptoCurrency get currency => _wallet.currency;
69 -
70 - Validator get amountValidator => AmountValidator(type: _wallet.type);
71 -
72 - Validator get addressValidator => AddressValidator(type: _wallet.currency);
73 -
74 - @computed
75 - String get balance {
76 - if (_wallet is MoneroWallet) {
77 - _wallet.balance.formattedUnlockedBalance;
78 - }
79 -
80 - if (_wallet is BitcoinWallet) {
81 - _wallet.balance.confirmedFormatted;
82 - }
83 -
84 - return '0.0';
85 - }
86 -
87 - WalletBase _wallet;
88 -
89 - SettingsStore _settingsStore;
90 -
91 - Future<void> createTransaction() async {}
92 -
93 - Future<void> commitTransaction() async {}
94 -}
lib/view_model/settings/picker_list_item.dart
+12 -3
@@ -4,10 +4,19 @@ import 'package:cake_wallet/view_model/settings/settings_list_item.dart';
4 class PickerListItem<ItemType> extends SettingsListItem {
5 PickerListItem(
6 {@required String title,
7 - @required this.selectedItem,
8 - @required this.items})
9 - : super(title);
7 + @required this.selectedItem,
8 + @required this.items,
9 + void Function(ItemType item) onItemSelected})
10 + : _onItemSelected = onItemSelected,
11 + super(title);
12
13 final ItemType Function() selectedItem;
14 final List<ItemType> items;
15 + final void Function(ItemType item) _onItemSelected;
16 +
17 + void onItemSelected(dynamic item) {
18 + if (item is ItemType) {
19 + _onItemSelected?.call(item);
20 + }
21 + }
22 }
lib/view_model/settings/settings_view_model.dart
+29 -7
@@ -1,3 +1,5 @@
1 +import 'package:cake_wallet/core/wallet_base.dart';
2 +import 'package:cake_wallet/src/domain/common/wallet_type.dart';
3 import 'package:flutter/cupertino.dart';
4 import 'package:mobx/mobx.dart';
5 import 'package:cake_wallet/routes.dart';
@@ -20,7 +22,8 @@ part 'settings_view_model.g.dart';
22 class SettingsViewModel = SettingsViewModelBase with _$SettingsViewModel;
23
24 abstract class SettingsViewModelBase with Store {
23 - SettingsViewModelBase(this._settingsStore) : itemHeaders = {} {
25 + SettingsViewModelBase(this._settingsStore, WalletBase wallet)
26 + : itemHeaders = {} {
27 sections = [
28 [
29 PickerListItem(
@@ -33,8 +36,10 @@ abstract class SettingsViewModelBase with Store {
36 selectedItem: () => fiatCurrency),
37 PickerListItem(
38 title: S.current.settings_fee_priority,
36 - items: TransactionPriority.all,
37 - selectedItem: () => transactionPriority),
39 + items: _transactionPriorities(wallet.type),
40 + selectedItem: () => transactionPriority,
41 + onItemSelected: (TransactionPriority priority) =>
42 + _settingsStore.transactionPriority = priority),
43 SwitcherListItem(
44 title: S.current.settings_save_recipient_address,
45 value: () => shouldSaveRecipientAddress,
@@ -146,12 +151,9 @@ abstract class SettingsViewModelBase with Store {
151 _settingsStore.allowBiometricalAuthentication = value;
152
153 // @observable
149 -// bool isDarkTheme;
150 -//
151 -// @observable
152 -// int defaultPinLength;
154
155 // @observable
156 +
157 final Map<String, String> itemHeaders;
158 List<List<SettingsListItem>> sections;
159 final SettingsStore _settingsStore;
@@ -182,4 +184,24 @@ abstract class SettingsViewModelBase with Store {
184
185 @action
186 void _showTrades() => actionlistDisplayMode.add(ActionListDisplayMode.trades);
187 +
188 +//
189 +// @observable
190 +// int defaultPinLength;
191 +// bool isDarkTheme;
192 +
193 + static List<TransactionPriority> _transactionPriorities(WalletType type) {
194 + switch (type) {
195 + case WalletType.monero:
196 + return TransactionPriority.all;
197 + case WalletType.bitcoin:
198 + return [
199 + TransactionPriority.slow,
200 + TransactionPriority.regular,
201 + TransactionPriority.fast
202 + ];
203 + default:
204 + return [];
205 + }
206 + }
207 }