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
}