From 02ebc54a38002f57d353108d3e8e254b8c685935 Mon Sep 17 00:00:00 2001 From: M Date: Wed, 16 Dec 2020 21:16:47 +0200 Subject: [PATCH] Fixed updating of transactions history. Added support for part formatted electrum server response --- lib/bitcoin/bitcoin_wallet.dart | 6 +- lib/bitcoin/electrum.dart | 91 +++++++++++++------ lib/bitcoin/pending_bitcoin_transaction.dart | 1 + lib/src/screens/dashboard/wallet_menu.dart | 75 ++++++++------- .../dashboard/widgets/menu_widget.dart | 29 +++--- .../transaction_details_page.dart | 20 ++-- .../dashboard/dashboard_view_model.dart | 17 ++++ 7 files changed, 153 insertions(+), 86 deletions(-) diff --git a/lib/bitcoin/bitcoin_wallet.dart b/lib/bitcoin/bitcoin_wallet.dart index 5d2b7209..0eca71b5 100644 --- a/lib/bitcoin/bitcoin_wallet.dart +++ b/lib/bitcoin/bitcoin_wallet.dart @@ -348,8 +348,8 @@ abstract class BitcoinWalletBase extends WalletBase with Store { } @override - void close() { - + void close() async{ + await eclient.close(); } void _subscribeForUpdates() { @@ -357,8 +357,8 @@ abstract class BitcoinWalletBase extends WalletBase with Store { await _scripthashesUpdateSubject[sh]?.close(); _scripthashesUpdateSubject[sh] = eclient.scripthashUpdate(sh); _scripthashesUpdateSubject[sh].listen((event) async { - transactionHistory.updateAsync(); await _updateBalance(); + transactionHistory.updateAsync(); }); }); } diff --git a/lib/bitcoin/electrum.dart b/lib/bitcoin/electrum.dart index 9a51a2c6..20dc2968 100644 --- a/lib/bitcoin/electrum.dart +++ b/lib/bitcoin/electrum.dart @@ -22,8 +22,9 @@ String jsonrpcparams(List params) { } String jsonrpc( - {String method, List params, int id, double version = 2.0}) => - '{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${json.encode(params)}}\n'; + {String method, List params, int id, double version = 2.0}) => + '{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${json + .encode(params)}}\n'; class SocketTask { SocketTask({this.completer, this.isSubscription, this.subject}); @@ -49,6 +50,7 @@ class ElectrumClient { final Map _tasks; bool _isConnected; Timer _aliveTimer; + String unterminatedString; Future connectToUri(String uri) async { final splittedUri = uri.split(':'); @@ -73,19 +75,22 @@ class ElectrumClient { socket.listen((Uint8List event) { try { - final jsoned = - json.decode(utf8.decode(event.toList())) as Map; - // print(jsoned); - final method = jsoned['method']; - final id = jsoned['id'] as String; - final result = jsoned['result']; - - if (method is String) { - _methodHandler(method: method, request: jsoned); - return; + _handleResponse(utf8.decode(event.toList())); + } on FormatException catch (e) { + final msg = e.message.toLowerCase(); + + if (msg == 'Unterminated string'.toLowerCase()) { + unterminatedString = e.source as String; + } + + if (msg == 'Unexpected character'.toLowerCase()) { + unterminatedString += e.source as String; } - _finish(id, result); + if (isJSONStringCorrect(unterminatedString)) { + _handleResponse(unterminatedString); + unterminatedString = null; + } } catch (e) { print(e); } @@ -148,7 +153,7 @@ class ElectrumClient { }); Future>> getListUnspentWithAddress( - String address) => + String address) => call( method: 'blockchain.scripthash.listunspent', params: [scriptHash(address)]).then((dynamic result) { @@ -199,7 +204,7 @@ class ElectrumClient { }); Future> getTransactionRaw( - {@required String hash}) async => + {@required String hash}) async => call(method: 'blockchain.transaction.get', params: [hash, true]) .then((dynamic result) { if (result is Map) { @@ -228,7 +233,7 @@ class ElectrumClient { } Future broadcastTransaction( - {@required String transactionRaw}) async => + {@required String transactionRaw}) async => call(method: 'blockchain.transaction.broadcast', params: [transactionRaw]) .then((dynamic result) { if (result is String) { @@ -239,14 +244,14 @@ class ElectrumClient { }); Future> getMerkle( - {@required String hash, @required int height}) async => + {@required String hash, @required int height}) async => await call( method: 'blockchain.transaction.get_merkle', params: [hash, height]) as Map; Future> getHeader({@required int height}) async => await call(method: 'blockchain.block.get_header', params: [height]) - as Map; + as Map; Future estimatefee({@required int p}) => call(method: 'blockchain.estimatefee', params: [p]) @@ -270,10 +275,9 @@ class ElectrumClient { params: [scripthash]); } - BehaviorSubject subscribe( - {@required String id, - @required String method, - List params = const []}) { + BehaviorSubject subscribe({@required String id, + @required String method, + List params = const []}) { final subscription = BehaviorSubject(); _regisrySubscription(id, subscription); socket.write(jsonrpc(method: method, id: _id, params: params)); @@ -292,10 +296,9 @@ class ElectrumClient { return completer.future; } - Future callWithTimeout( - {String method, - List params = const [], - int timeout = 2000}) async { + Future callWithTimeout({String method, + List params = const [], + int timeout = 2000}) async { final completer = Completer(); _id += 1; final id = _id; @@ -316,8 +319,15 @@ class ElectrumClient { socket.write(jsonrpc(method: method, id: _id, params: params)); } - void _regisryTask(int id, Completer completer) => _tasks[id.toString()] = - SocketTask(completer: completer, isSubscription: false); + Future close() async { + _aliveTimer.cancel(); + await socket.close(); + onConnectionStatusChange = null; + } + + void _regisryTask(int id, Completer completer) => + _tasks[id.toString()] = + SocketTask(completer: completer, isSubscription: false); void _regisrySubscription(String id, BehaviorSubject subject) => _tasks[id] = SocketTask(subject: subject, isSubscription: true); @@ -360,6 +370,31 @@ class ElectrumClient { _isConnected = isConnected; } + + void _handleResponse(String response) { + print('Response: $response'); + final jsoned = json.decode(response) as Map; + // print(jsoned); + final method = jsoned['method']; + final id = jsoned['id'] as String; + final result = jsoned['result']; + + if (method is String) { + _methodHandler(method: method, request: jsoned); + return; + } + + _finish(id, result); + } +} +// FIXME: move me +bool isJSONStringCorrect(String source) { + try { + json.decode(source); + return true; + } catch (_) { + return false; + } } class RequestFailedTimeoutException implements Exception { diff --git a/lib/bitcoin/pending_bitcoin_transaction.dart b/lib/bitcoin/pending_bitcoin_transaction.dart index d2c018a3..edd5a045 100644 --- a/lib/bitcoin/pending_bitcoin_transaction.dart +++ b/lib/bitcoin/pending_bitcoin_transaction.dart @@ -16,6 +16,7 @@ class PendingBitcoinTransaction with PendingTransaction { final int amount; final int fee; + @override String get id => _tx.getId(); @override diff --git a/lib/src/screens/dashboard/wallet_menu.dart b/lib/src/screens/dashboard/wallet_menu.dart index a4142d42..273cdd9b 100644 --- a/lib/src/screens/dashboard/wallet_menu.dart +++ b/lib/src/screens/dashboard/wallet_menu.dart @@ -9,44 +9,53 @@ import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; // FIXME: terrible design class WalletMenu { - WalletMenu(this.context, this.reconnect); - - final List items = [ - WalletMenuItem( - title: S.current.reconnect, - image: Image.asset('assets/images/reconnect_menu.png', - height: 16, width: 16)), - WalletMenuItem( - title: S.current.rescan, - image: Image.asset('assets/images/filter_icon.png', - height: 16, width: 16)), - WalletMenuItem( - title: S.current.wallets, - image: Image.asset('assets/images/wallet_menu.png', - height: 16, width: 16)), - WalletMenuItem( - title: S.current.nodes, - image: - Image.asset('assets/images/nodes_menu.png', height: 16, width: 16)), - WalletMenuItem( - title: S.current.show_keys, - image: - Image.asset('assets/images/key_menu.png', height: 16, width: 16)), - WalletMenuItem( - title: S.current.address_book_menu, - image: Image.asset('assets/images/open_book_menu.png', - height: 16, width: 16)), - WalletMenuItem( - title: S.current.settings_title, - image: Image.asset('assets/images/settings_menu.png', - height: 16, width: 16)), - ]; + WalletMenu(this.context, this.reconnect, this.hasRescan) : items = [] { + items.addAll([ + WalletMenuItem( + title: S.current.reconnect, + image: Image.asset('assets/images/reconnect_menu.png', + height: 16, width: 16)), + if (hasRescan) + WalletMenuItem( + title: S.current.rescan, + image: Image.asset('assets/images/filter_icon.png', + height: 16, width: 16)), + WalletMenuItem( + title: S.current.wallets, + image: Image.asset('assets/images/wallet_menu.png', + height: 16, width: 16)), + WalletMenuItem( + title: S.current.nodes, + image: Image.asset('assets/images/nodes_menu.png', + height: 16, width: 16)), + WalletMenuItem( + title: S.current.show_keys, + image: + Image.asset('assets/images/key_menu.png', height: 16, width: 16)), + WalletMenuItem( + title: S.current.address_book_menu, + image: Image.asset('assets/images/open_book_menu.png', + height: 16, width: 16)), + WalletMenuItem( + title: S.current.settings_title, + image: Image.asset('assets/images/settings_menu.png', + height: 16, width: 16)), + ]); + } + final List items; final BuildContext context; final Future Function() reconnect; + final bool hasRescan; void action(int index) { - switch (index) { + var indx = index; + + if (index > 0 && !hasRescan) { + indx += 1; + } + + switch (indx) { case 0: _presentReconnectAlert(context); break; diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index f8de5ba8..ed90f9be 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -66,8 +66,10 @@ class MenuWidgetState extends State { @override Widget build(BuildContext context) { - final walletMenu = - WalletMenu(context, () async => widget.dashboardViewModel.reconnect()); + final walletMenu = WalletMenu( + context, + () async => widget.dashboardViewModel.reconnect(), + widget.dashboardViewModel.hasRescan); final itemCount = walletMenu.items.length; moneroIcon = Image.asset('assets/images/monero_menu.png', @@ -148,16 +150,19 @@ class MenuWidgetState extends State { ), if (widget.dashboardViewModel.subname != null) - Observer(builder: (_) => Text( - widget.dashboardViewModel.subname, - style: TextStyle( - color: Theme.of(context) - .accentTextTheme - .overline - .decorationColor, - fontWeight: FontWeight.w500, - fontSize: 12), - )) + Observer( + builder: (_) => Text( + widget.dashboardViewModel + .subname, + style: TextStyle( + color: Theme.of(context) + .accentTextTheme + .overline + .decorationColor, + fontWeight: + FontWeight.w500, + fontSize: 12), + )) ], ), )) diff --git a/lib/src/screens/transaction_details/transaction_details_page.dart b/lib/src/screens/transaction_details/transaction_details_page.dart index 6745e05a..0ded4cd6 100644 --- a/lib/src/screens/transaction_details/transaction_details_page.dart +++ b/lib/src/screens/transaction_details/transaction_details_page.dart @@ -35,16 +35,6 @@ class TransactionDetailsPage extends BasePage { value: tx.feeFormatted()) ]; - if (showRecipientAddress) { - final recipientAddress = transactionDescriptionBox.values.firstWhere((val) => val.id == transactionInfo.id, orElse: () => null)?.recipientAddress; - - if (recipientAddress?.isNotEmpty ?? false) { - items.add(StandartListItem( - title: S.current.transaction_details_recipient_address, - value: recipientAddress)); - } - } - if (tx.key?.isNotEmpty ?? null) { // FIXME: add translation items.add(StandartListItem(title: 'Transaction Key', value: tx.key)); @@ -71,6 +61,16 @@ class TransactionDetailsPage extends BasePage { _items.addAll(items); } + + if (showRecipientAddress) { + final recipientAddress = transactionDescriptionBox.values.firstWhere((val) => val.id == transactionInfo.id, orElse: () => null)?.recipientAddress; + + if (recipientAddress?.isNotEmpty ?? false) { + _items.add(StandartListItem( + title: S.current.transaction_details_recipient_address, + value: recipientAddress)); + } + } } @override diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 3b7471cf..42058699 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -186,6 +186,8 @@ abstract class DashboardViewModelBase with Store { @observable WalletBase wallet; + bool get hasRescan => wallet.type == WalletType.monero; + BalanceViewModel balanceViewModel; AppStore appStore; @@ -237,6 +239,21 @@ abstract class DashboardViewModelBase with Store { balanceViewModel: balanceViewModel, settingsStore: appStore.settingsStore))); } + + connectMapToListWithTransform( + appStore.wallet.transactionHistory.transactions, + transactions, + (TransactionInfo val) => TransactionListItem( + transaction: val, + balanceViewModel: balanceViewModel, + settingsStore: appStore.settingsStore), + filter: (TransactionInfo tx) { + if (tx is MoneroTransactionInfo && wallet is MoneroWallet) { + return tx.accountIndex == wallet.account.id; + } + + return true; + }); } @action