From 75ca3d489bfef2c240b9432cf89e616a0f44244d Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 16 Feb 2024 11:25:52 -0600 Subject: [PATCH 1/7] cleanup --- .../wallet/wallet_mixin_interfaces/electrumx_interface.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 91b227792..49c3c9ae8 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -16,6 +16,8 @@ import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2 import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/models/signing_data.dart'; +import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/tor_status_changed_event.dart'; import 'package:stackwallet/services/tor_service.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -32,9 +34,6 @@ import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; import 'package:stream_channel/stream_channel.dart'; -import '../../../services/event_bus/events/global/tor_connection_status_changed_event.dart'; -import '../../../services/event_bus/events/global/tor_status_changed_event.dart'; - mixin ElectrumXInterface on Bip39HDWallet { late ElectrumXClient electrumXClient; late StreamChannel electrumAdapterChannel; From a807303eba3d953dca14ba67772165b3948faa0f Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 16 Feb 2024 16:33:19 -0600 Subject: [PATCH 2/7] listen to tor and preferences changes and handle connections accordingly --- .../electrumx_interface.dart | 93 ++++++++++++++++++- 1 file changed, 88 insertions(+), 5 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 49c3c9ae8..245dd9ed5 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -7,6 +7,7 @@ import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; import 'package:electrum_adapter/electrum_adapter.dart' as electrum_adapter; import 'package:electrum_adapter/electrum_adapter.dart'; import 'package:isar/isar.dart'; +import 'package:mutex/mutex.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart'; import 'package:stackwallet/electrumx_rpc/electrumx_chain_height_service.dart'; import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; @@ -18,6 +19,7 @@ import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/models/signing_data.dart'; import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/tor_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/tor_service.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -45,8 +47,15 @@ mixin ElectrumXInterface on Bip39HDWallet { int? _latestHeight; + late Prefs _prefs; + late TorService _torService; StreamSubscription? _torPreferenceListener; StreamSubscription? _torStatusListener; + final Mutex _torConnectingLock = Mutex(); + bool _requireMutex = false; + Timer? _aliveTimer; + static const Duration _keepAlive = Duration(minutes: 1); + bool _isConnected = false; static const _kServerBatchCutoffVersion = [1, 6]; List? _serverVersion; @@ -814,9 +823,6 @@ mixin ElectrumXInterface on Bip39HDWallet { Future fetchChainHeight() async { try { - // _checkChainHeightSubscription(); - // TODO above. Make sure that the subscription/stream is alive. - // Don't set a stream subscription if one already exists. if (ElectrumxChainHeightService.subscriptions[cryptoCurrency.coin] == null) { @@ -825,11 +831,24 @@ mixin ElectrumXInterface on Bip39HDWallet { // Make sure we only complete once. final isFirstResponse = _latestHeight == null; + // Check Electrum and update internal and cached versions if necessary. await electrumXClient.checkElectrumAdapter(); - // TODO [prio=extreme]: Does this update anything in this file?? Thinking no. + if (electrumAdapterChannel != electrumXClient.electrumAdapterChannel && + electrumXClient.electrumAdapterChannel != null) { + electrumAdapterChannel = electrumXClient.electrumAdapterChannel!; + } + if (electrumAdapterClient != electrumXClient.electrumAdapterClient && + electrumXClient.electrumAdapterClient != null) { + electrumAdapterClient = electrumXClient.electrumAdapterClient!; + } + // electrumXCachedClient.electrumAdapterChannel = electrumAdapterChannel; + if (electrumXCachedClient.electrumAdapterClient != + electrumAdapterClient) { + electrumXCachedClient.electrumAdapterClient = electrumAdapterClient; + } + // Subscribe to and listen for new block headers. final stream = electrumAdapterClient.subscribeHeaders(); - ElectrumxChainHeightService.subscriptions[cryptoCurrency.coin] = stream.asBroadcastStream().listen((response) { final int chainHeight = response.height; @@ -842,6 +861,61 @@ mixin ElectrumXInterface on Bip39HDWallet { completer.complete(chainHeight); } }); + + // If we're testing, use the global event bus for testing. + // final bus = globalEventBusForTesting ?? GlobalEventBus.instance; + // No constructors for mixins, so no globalEventBusForTesting is passed in. + final bus = GlobalEventBus.instance; + + // Listen to global event bus for Tor status changes. + _torStatusListener ??= bus.on().listen( + (event) async { + try { + switch (event.newStatus) { + case TorConnectionStatus.connecting: + // If Tor is connecting, we need to wait. + await _torConnectingLock.acquire(); + _requireMutex = true; + break; + + case TorConnectionStatus.connected: + case TorConnectionStatus.disconnected: + // If Tor is connected or disconnected, we can release the lock. + if (_torConnectingLock.isLocked) { + _torConnectingLock.release(); + } + _requireMutex = false; + break; + } + } finally { + // Ensure the lock is released. + if (_torConnectingLock.isLocked) { + _torConnectingLock.release(); + } + } + }, + ); + + // Listen to global event bus for Tor preference changes. + _torPreferenceListener ??= bus.on().listen( + (event) async { + // Close any open subscriptions. + for (final coinSub + in ElectrumxChainHeightService.subscriptions.entries) { + await coinSub.value?.cancel(); + } + + // Cancel alive timer + _aliveTimer?.cancel(); + }, + ); + + // Set a timer to check if the subscription is still alive. + _aliveTimer?.cancel(); + _aliveTimer = Timer.periodic( + _keepAlive, + (_) async => _updateConnectionStatus(await electrumXClient.ping()), + ); } else { // Don't set a stream subscription if one already exists. @@ -977,6 +1051,15 @@ mixin ElectrumXInterface on Bip39HDWallet { // host: newNode.address, port: newNode.port); } + /// Update the connection status and call the onConnectionStatusChanged callback if it exists. + void _updateConnectionStatus(bool connectionStatus) { + // TODO [prio=low]: Set onConnectionStatusChanged callback. + // if (_isConnected != connectionStatus && onConnectionStatusChanged != null) { + // onConnectionStatusChanged!(connectionStatus); + // } + _isConnected = connectionStatus; + } + //============================================================================ Future<({List
addresses, int index})> checkGapsBatched( From e2d8e80f66308be4244a24bd6fb59126bd87aa0e Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 16 Feb 2024 16:33:51 -0600 Subject: [PATCH 3/7] close old electrum client when updating to a new one and ignore late initialization errors --- .../wallet_mixin_interfaces/electrumx_interface.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 245dd9ed5..407bfa3c0 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -1001,6 +1001,16 @@ mixin ElectrumXInterface on Bip39HDWallet { .toList(); final newNode = await getCurrentElectrumXNode(); + try { + await electrumXClient.electrumAdapterClient?.close(); + } catch (e, s) { + if (e.toString().contains("initialized")) { + // Ignore. This should happen every first time the wallet is opened. + } else { + Logging.instance + .log("Error closing electrumXClient: $e", level: LogLevel.Error); + } + } electrumXClient = ElectrumXClient.from( node: newNode, prefs: prefs, From c4cbf6eb5a2322721005d03ce68b7005f334c9e3 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 16 Feb 2024 16:46:24 -0600 Subject: [PATCH 4/7] add electrum_adapter ping note --- lib/electrumx_rpc/electrumx_client.dart | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/electrumx_rpc/electrumx_client.dart b/lib/electrumx_rpc/electrumx_client.dart index bddce3289..719f4bb59 100644 --- a/lib/electrumx_rpc/electrumx_client.dart +++ b/lib/electrumx_rpc/electrumx_client.dart @@ -525,13 +525,21 @@ class ElectrumXClient { /// Returns true if ping succeeded Future ping({String? requestID, int retryCount = 1}) async { try { + // This doesn't work because electrum_adapter only returns the result + // (which is always `null`). + // await checkElectrumAdapter(); + // final response = await electrumAdapterClient! + // .ping() + // .timeout(const Duration(seconds: 2)); + // return (response as Map).isNotEmpty; + final response = await request( requestID: requestID, command: 'server.ping', requestTimeout: const Duration(seconds: 2), retries: retryCount, ).timeout(const Duration(seconds: 2)) as Map; - return response.isNotEmpty; // TODO [prio=extreme]: Fix this. + return response.isNotEmpty; } catch (e) { rethrow; } From 9ac8a32821664a0134be86fdc00ed70ab30f8429 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 16 Feb 2024 16:55:24 -0600 Subject: [PATCH 5/7] update ping and request functions --- lib/electrumx_rpc/electrumx_client.dart | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/electrumx_rpc/electrumx_client.dart b/lib/electrumx_rpc/electrumx_client.dart index 719f4bb59..340d69535 100644 --- a/lib/electrumx_rpc/electrumx_client.dart +++ b/lib/electrumx_rpc/electrumx_client.dart @@ -402,6 +402,12 @@ class ElectrumXClient { } currentFailoverIndex = -1; + + // If the command is a ping, a good return should always be null. + if (command.contains("ping")) { + return true; + } + return response; } on WifiOnlyException { rethrow; @@ -525,7 +531,7 @@ class ElectrumXClient { /// Returns true if ping succeeded Future ping({String? requestID, int retryCount = 1}) async { try { - // This doesn't work because electrum_adapter only returns the result + // This doesn't work because electrum_adapter only returns the result: // (which is always `null`). // await checkElectrumAdapter(); // final response = await electrumAdapterClient! @@ -533,13 +539,15 @@ class ElectrumXClient { // .timeout(const Duration(seconds: 2)); // return (response as Map).isNotEmpty; - final response = await request( + // Because request() has been updated to use electrum_adapter, and because + // electrum_adapter returns the result of the request, request() has been + // updated to return a bool on a server.ping command as a special case. + return await request( requestID: requestID, command: 'server.ping', requestTimeout: const Duration(seconds: 2), retries: retryCount, - ).timeout(const Duration(seconds: 2)) as Map; - return response.isNotEmpty; + ).timeout(const Duration(seconds: 2)) as bool; } catch (e) { rethrow; } From 8e2ca6a6c99645381b8986ae06ac4e61bd749276 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 16 Feb 2024 17:05:13 -0600 Subject: [PATCH 6/7] remove old rpc client references --- lib/electrumx_rpc/electrumx_client.dart | 67 ------------------------- 1 file changed, 67 deletions(-) diff --git a/lib/electrumx_rpc/electrumx_client.dart b/lib/electrumx_rpc/electrumx_client.dart index 340d69535..d8c029f36 100644 --- a/lib/electrumx_rpc/electrumx_client.dart +++ b/lib/electrumx_rpc/electrumx_client.dart @@ -114,7 +114,6 @@ class ElectrumXClient { required Prefs prefs, required List failovers, Coin? coin, - JsonRPC? client, this.connectionTimeoutForSpecialCaseJsonRPCClients = const Duration(seconds: 60), TorService? torService, @@ -125,7 +124,6 @@ class ElectrumXClient { _host = host; _port = port; _useSSL = useSSL; - _rpcClient = client; _coin = coin; final bus = globalEventBusForTesting ?? GlobalEventBus.instance; @@ -155,23 +153,10 @@ class ElectrumXClient { // case TorStatus.disabled: // } - // might be ok to just reset/kill the current _jsonRpcClient - - // since disconnecting is async and we want to ensure instant change over - // we will keep temp reference to current rpc client to call disconnect - // on before awaiting the disconnection future - - final temp = _rpcClient; - // setting to null should force the creation of a new json rpc client // on the next request sent through this electrumx instance - _rpcClient = null; _electrumAdapterChannel = null; _electrumAdapterClient = null; - - await temp?.disconnect( - reason: "Tor status changed to \"${event.status}\"", - ); }, ); } @@ -204,58 +189,6 @@ class ElectrumXClient { return true; } - void _checkRpcClient() { - // If we're supposed to use Tor... - if (_prefs.useTor) { - // But Tor isn't running... - if (_torService.status != TorConnectionStatus.connected) { - // And the killswitch isn't set... - if (!_prefs.torKillSwitch) { - // Then we'll just proceed and connect to ElectrumX through clearnet at the bottom of this function. - Logging.instance.log( - "Tor preference set but Tor is not enabled, killswitch not set, connecting to ElectrumX through clearnet", - level: LogLevel.Warning, - ); - } else { - // ... But if the killswitch is set, then we throw an exception. - throw Exception( - "Tor preference and killswitch set but Tor is not enabled, not connecting to ElectrumX"); - // TODO [prio=low]: Try to start Tor. - } - } else { - // Get the proxy info from the TorService. - final proxyInfo = _torService.getProxyInfo(); - - if (currentFailoverIndex == -1) { - _rpcClient ??= JsonRPC( - host: host, - port: port, - useSSL: useSSL, - connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, - proxyInfo: proxyInfo, - ); - } else { - _rpcClient ??= JsonRPC( - host: failovers![currentFailoverIndex].address, - port: failovers![currentFailoverIndex].port, - useSSL: failovers![currentFailoverIndex].useSSL, - connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, - proxyInfo: proxyInfo, - ); - } - - if (_rpcClient!.proxyInfo != proxyInfo) { - _rpcClient!.proxyInfo = proxyInfo; - _rpcClient!.disconnect( - reason: "Tor proxyInfo does not match current info", - ); - } - - return; - } - } - } - Future checkElectrumAdapter() async { ({InternetAddress host, int port})? proxyInfo; From 6421a2ce74600c633d92c55eefe83353c54e627a Mon Sep 17 00:00:00 2001 From: Diego Salazar Date: Fri, 16 Feb 2024 16:41:11 -0700 Subject: [PATCH 7/7] Update pubspec.yaml --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 0ec9d9fa7..764c2bd57 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.9.3+203 +version: 1.10.0+206 environment: sdk: ">=3.0.2 <4.0.0"