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