Fixed updating of transactions history. Added support for part formatted electrum server response

M committed Dec 16, 2020 at 21:16 UTC 02ebc54a38002f57d353108d3e8e254b8c685935
7 files changed +153 -86
lib/bitcoin/bitcoin_wallet.dart
+3 -3
@@ -348,8 +348,8 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
348 }
349
350 @override
351 - void close() {
352 -
351 + void close() async{
352 + await eclient.close();
353 }
354
355 void _subscribeForUpdates() {
@@ -357,8 +357,8 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
357 await _scripthashesUpdateSubject[sh]?.close();
358 _scripthashesUpdateSubject[sh] = eclient.scripthashUpdate(sh);
359 _scripthashesUpdateSubject[sh].listen((event) async {
360 - transactionHistory.updateAsync();
360 await _updateBalance();
361 + transactionHistory.updateAsync();
362 });
363 });
364 }
lib/bitcoin/electrum.dart
+63 -28
@@ -22,8 +22,9 @@ String jsonrpcparams(List<Object> params) {
22 }
23
24 String jsonrpc(
25 - {String method, List<Object> params, int id, double version = 2.0}) =>
26 - '{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${json.encode(params)}}\n';
25 + {String method, List<Object> params, int id, double version = 2.0}) =>
26 + '{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${json
27 + .encode(params)}}\n';
28
29 class SocketTask {
30 SocketTask({this.completer, this.isSubscription, this.subject});
@@ -49,6 +50,7 @@ class ElectrumClient {
50 final Map<String, SocketTask> _tasks;
51 bool _isConnected;
52 Timer _aliveTimer;
53 + String unterminatedString;
54
55 Future<void> connectToUri(String uri) async {
56 final splittedUri = uri.split(':');
@@ -73,19 +75,22 @@ class ElectrumClient {
75
76 socket.listen((Uint8List event) {
77 try {
76 - final jsoned =
77 - json.decode(utf8.decode(event.toList())) as Map<String, Object>;
78 - // print(jsoned);
79 - final method = jsoned['method'];
80 - final id = jsoned['id'] as String;
81 - final result = jsoned['result'];
82 -
83 - if (method is String) {
84 - _methodHandler(method: method, request: jsoned);
85 - return;
78 + _handleResponse(utf8.decode(event.toList()));
79 + } on FormatException catch (e) {
80 + final msg = e.message.toLowerCase();
81 +
82 + if (msg == 'Unterminated string'.toLowerCase()) {
83 + unterminatedString = e.source as String;
84 + }
85 +
86 + if (msg == 'Unexpected character'.toLowerCase()) {
87 + unterminatedString += e.source as String;
88 }
89
88 - _finish(id, result);
90 + if (isJSONStringCorrect(unterminatedString)) {
91 + _handleResponse(unterminatedString);
92 + unterminatedString = null;
93 + }
94 } catch (e) {
95 print(e);
96 }
@@ -148,7 +153,7 @@ class ElectrumClient {
153 });
154
155 Future<List<Map<String, dynamic>>> getListUnspentWithAddress(
151 - String address) =>
156 + String address) =>
157 call(
158 method: 'blockchain.scripthash.listunspent',
159 params: [scriptHash(address)]).then((dynamic result) {
@@ -199,7 +204,7 @@ class ElectrumClient {
204 });
205
206 Future<Map<String, Object>> getTransactionRaw(
202 - {@required String hash}) async =>
207 + {@required String hash}) async =>
208 call(method: 'blockchain.transaction.get', params: [hash, true])
209 .then((dynamic result) {
210 if (result is Map<String, Object>) {
@@ -228,7 +233,7 @@ class ElectrumClient {
233 }
234
235 Future<String> broadcastTransaction(
231 - {@required String transactionRaw}) async =>
236 + {@required String transactionRaw}) async =>
237 call(method: 'blockchain.transaction.broadcast', params: [transactionRaw])
238 .then((dynamic result) {
239 if (result is String) {
@@ -239,14 +244,14 @@ class ElectrumClient {
244 });
245
246 Future<Map<String, dynamic>> getMerkle(
242 - {@required String hash, @required int height}) async =>
247 + {@required String hash, @required int height}) async =>
248 await call(
249 method: 'blockchain.transaction.get_merkle',
250 params: [hash, height]) as Map<String, dynamic>;
251
252 Future<Map<String, dynamic>> getHeader({@required int height}) async =>
253 await call(method: 'blockchain.block.get_header', params: [height])
249 - as Map<String, dynamic>;
254 + as Map<String, dynamic>;
255
256 Future<double> estimatefee({@required int p}) =>
257 call(method: 'blockchain.estimatefee', params: [p])
@@ -270,10 +275,9 @@ class ElectrumClient {
275 params: [scripthash]);
276 }
277
273 - BehaviorSubject<T> subscribe<T>(
274 - {@required String id,
275 - @required String method,
276 - List<Object> params = const []}) {
278 + BehaviorSubject<T> subscribe<T>({@required String id,
279 + @required String method,
280 + List<Object> params = const []}) {
281 final subscription = BehaviorSubject<T>();
282 _regisrySubscription(id, subscription);
283 socket.write(jsonrpc(method: method, id: _id, params: params));
@@ -292,10 +296,9 @@ class ElectrumClient {
296 return completer.future;
297 }
298
295 - Future<dynamic> callWithTimeout(
296 - {String method,
297 - List<Object> params = const [],
298 - int timeout = 2000}) async {
299 + Future<dynamic> callWithTimeout({String method,
300 + List<Object> params = const [],
301 + int timeout = 2000}) async {
302 final completer = Completer<dynamic>();
303 _id += 1;
304 final id = _id;
@@ -316,8 +319,15 @@ class ElectrumClient {
319 socket.write(jsonrpc(method: method, id: _id, params: params));
320 }
321
319 - void _regisryTask(int id, Completer completer) => _tasks[id.toString()] =
320 - SocketTask(completer: completer, isSubscription: false);
322 + Future<void> close() async {
323 + _aliveTimer.cancel();
324 + await socket.close();
325 + onConnectionStatusChange = null;
326 + }
327 +
328 + void _regisryTask(int id, Completer completer) =>
329 + _tasks[id.toString()] =
330 + SocketTask(completer: completer, isSubscription: false);
331
332 void _regisrySubscription(String id, BehaviorSubject subject) =>
333 _tasks[id] = SocketTask(subject: subject, isSubscription: true);
@@ -360,6 +370,31 @@ class ElectrumClient {
370
371 _isConnected = isConnected;
372 }
373 +
374 + void _handleResponse(String response) {
375 + print('Response: $response');
376 + final jsoned = json.decode(response) as Map<String, Object>;
377 + // print(jsoned);
378 + final method = jsoned['method'];
379 + final id = jsoned['id'] as String;
380 + final result = jsoned['result'];
381 +
382 + if (method is String) {
383 + _methodHandler(method: method, request: jsoned);
384 + return;
385 + }
386 +
387 + _finish(id, result);
388 + }
389 +}
390 +// FIXME: move me
391 +bool isJSONStringCorrect(String source) {
392 + try {
393 + json.decode(source);
394 + return true;
395 + } catch (_) {
396 + return false;
397 + }
398 }
399
400 class RequestFailedTimeoutException implements Exception {
lib/bitcoin/pending_bitcoin_transaction.dart
+1
@@ -16,6 +16,7 @@ class PendingBitcoinTransaction with PendingTransaction {
16 final int amount;
17 final int fee;
18
19 + @override
20 String get id => _tx.getId();
21
22 @override
lib/src/screens/dashboard/wallet_menu.dart
+42 -33
@@ -9,44 +9,53 @@ import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
9 // FIXME: terrible design
10
11 class WalletMenu {
12 - WalletMenu(this.context, this.reconnect);
13 -
14 - final List<WalletMenuItem> items = [
15 - WalletMenuItem(
16 - title: S.current.reconnect,
17 - image: Image.asset('assets/images/reconnect_menu.png',
18 - height: 16, width: 16)),
19 - WalletMenuItem(
20 - title: S.current.rescan,
21 - image: Image.asset('assets/images/filter_icon.png',
22 - height: 16, width: 16)),
23 - WalletMenuItem(
24 - title: S.current.wallets,
25 - image: Image.asset('assets/images/wallet_menu.png',
26 - height: 16, width: 16)),
27 - WalletMenuItem(
28 - title: S.current.nodes,
29 - image:
30 - Image.asset('assets/images/nodes_menu.png', height: 16, width: 16)),
31 - WalletMenuItem(
32 - title: S.current.show_keys,
33 - image:
34 - Image.asset('assets/images/key_menu.png', height: 16, width: 16)),
35 - WalletMenuItem(
36 - title: S.current.address_book_menu,
37 - image: Image.asset('assets/images/open_book_menu.png',
38 - height: 16, width: 16)),
39 - WalletMenuItem(
40 - title: S.current.settings_title,
41 - image: Image.asset('assets/images/settings_menu.png',
42 - height: 16, width: 16)),
43 - ];
12 + WalletMenu(this.context, this.reconnect, this.hasRescan) : items = [] {
13 + items.addAll([
14 + WalletMenuItem(
15 + title: S.current.reconnect,
16 + image: Image.asset('assets/images/reconnect_menu.png',
17 + height: 16, width: 16)),
18 + if (hasRescan)
19 + WalletMenuItem(
20 + title: S.current.rescan,
21 + image: Image.asset('assets/images/filter_icon.png',
22 + height: 16, width: 16)),
23 + WalletMenuItem(
24 + title: S.current.wallets,
25 + image: Image.asset('assets/images/wallet_menu.png',
26 + height: 16, width: 16)),
27 + WalletMenuItem(
28 + title: S.current.nodes,
29 + image: Image.asset('assets/images/nodes_menu.png',
30 + height: 16, width: 16)),
31 + WalletMenuItem(
32 + title: S.current.show_keys,
33 + image:
34 + Image.asset('assets/images/key_menu.png', height: 16, width: 16)),
35 + WalletMenuItem(
36 + title: S.current.address_book_menu,
37 + image: Image.asset('assets/images/open_book_menu.png',
38 + height: 16, width: 16)),
39 + WalletMenuItem(
40 + title: S.current.settings_title,
41 + image: Image.asset('assets/images/settings_menu.png',
42 + height: 16, width: 16)),
43 + ]);
44 + }
45
46 + final List<WalletMenuItem> items;
47 final BuildContext context;
48 final Future<void> Function() reconnect;
49 + final bool hasRescan;
50
51 void action(int index) {
49 - switch (index) {
52 + var indx = index;
53 +
54 + if (index > 0 && !hasRescan) {
55 + indx += 1;
56 + }
57 +
58 + switch (indx) {
59 case 0:
60 _presentReconnectAlert(context);
61 break;
lib/src/screens/dashboard/widgets/menu_widget.dart
+17 -12
@@ -66,8 +66,10 @@ class MenuWidgetState extends State<MenuWidget> {
66
67 @override
68 Widget build(BuildContext context) {
69 - final walletMenu =
70 - WalletMenu(context, () async => widget.dashboardViewModel.reconnect());
69 + final walletMenu = WalletMenu(
70 + context,
71 + () async => widget.dashboardViewModel.reconnect(),
72 + widget.dashboardViewModel.hasRescan);
73 final itemCount = walletMenu.items.length;
74
75 moneroIcon = Image.asset('assets/images/monero_menu.png',
@@ -148,16 +150,19 @@ class MenuWidgetState extends State<MenuWidget> {
150 ),
151 if (widget.dashboardViewModel.subname !=
152 null)
151 - Observer(builder: (_) => Text(
152 - widget.dashboardViewModel.subname,
153 - style: TextStyle(
154 - color: Theme.of(context)
155 - .accentTextTheme
156 - .overline
157 - .decorationColor,
158 - fontWeight: FontWeight.w500,
159 - fontSize: 12),
160 - ))
153 + Observer(
154 + builder: (_) => Text(
155 + widget.dashboardViewModel
156 + .subname,
157 + style: TextStyle(
158 + color: Theme.of(context)
159 + .accentTextTheme
160 + .overline
161 + .decorationColor,
162 + fontWeight:
163 + FontWeight.w500,
164 + fontSize: 12),
165 + ))
166 ],
167 ),
168 ))
lib/src/screens/transaction_details/transaction_details_page.dart
+10 -10
@@ -35,16 +35,6 @@ class TransactionDetailsPage extends BasePage {
35 value: tx.feeFormatted())
36 ];
37
38 - if (showRecipientAddress) {
39 - final recipientAddress = transactionDescriptionBox.values.firstWhere((val) => val.id == transactionInfo.id, orElse: () => null)?.recipientAddress;
40 -
41 - if (recipientAddress?.isNotEmpty ?? false) {
42 - items.add(StandartListItem(
43 - title: S.current.transaction_details_recipient_address,
44 - value: recipientAddress));
45 - }
46 - }
47 -
38 if (tx.key?.isNotEmpty ?? null) {
39 // FIXME: add translation
40 items.add(StandartListItem(title: 'Transaction Key', value: tx.key));
@@ -71,6 +61,16 @@ class TransactionDetailsPage extends BasePage {
61
62 _items.addAll(items);
63 }
64 +
65 + if (showRecipientAddress) {
66 + final recipientAddress = transactionDescriptionBox.values.firstWhere((val) => val.id == transactionInfo.id, orElse: () => null)?.recipientAddress;
67 +
68 + if (recipientAddress?.isNotEmpty ?? false) {
69 + _items.add(StandartListItem(
70 + title: S.current.transaction_details_recipient_address,
71 + value: recipientAddress));
72 + }
73 + }
74 }
75
76 @override
lib/view_model/dashboard/dashboard_view_model.dart
+17
@@ -186,6 +186,8 @@ abstract class DashboardViewModelBase with Store {
186 @observable
187 WalletBase wallet;
188
189 + bool get hasRescan => wallet.type == WalletType.monero;
190 +
191 BalanceViewModel balanceViewModel;
192
193 AppStore appStore;
@@ -237,6 +239,21 @@ abstract class DashboardViewModelBase with Store {
239 balanceViewModel: balanceViewModel,
240 settingsStore: appStore.settingsStore)));
241 }
242 +
243 + connectMapToListWithTransform(
244 + appStore.wallet.transactionHistory.transactions,
245 + transactions,
246 + (TransactionInfo val) => TransactionListItem(
247 + transaction: val,
248 + balanceViewModel: balanceViewModel,
249 + settingsStore: appStore.settingsStore),
250 + filter: (TransactionInfo tx) {
251 + if (tx is MoneroTransactionInfo && wallet is MoneroWallet) {
252 + return tx.accountIndex == wallet.account.id;
253 + }
254 +
255 + return true;
256 + });
257 }
258
259 @action