diff --git a/lib/db/db_version_migration.dart b/lib/db/db_version_migration.dart index 1415803e4..606996a7f 100644 --- a/lib/db/db_version_migration.dart +++ b/lib/db/db_version_migration.dart @@ -77,6 +77,8 @@ class DbVersionMigrator with WalletDB { name: e.name, id: e.id, useSSL: e.useSSL, + torEnabled: e.torEnabled, + clearEnabled: e.plainEnabled, ), ) .toList(); @@ -88,6 +90,8 @@ class DbVersionMigrator with WalletDB { name: node.name, id: node.id, useSSL: node.useSSL, + torEnabled: node.torEnabled, + clearEnabled: node.plainEnabled, ), prefs: prefs, failovers: failovers, diff --git a/lib/electrumx_rpc/client_manager.dart b/lib/electrumx_rpc/client_manager.dart index fb8b920cc..aea7e34e6 100644 --- a/lib/electrumx_rpc/client_manager.dart +++ b/lib/electrumx_rpc/client_manager.dart @@ -3,6 +3,8 @@ import 'dart:async'; import 'package:electrum_adapter/electrum_adapter.dart'; import '../utilities/logger.dart'; +import '../utilities/prefs.dart'; +import '../utilities/tor_plain_net_option_enum.dart'; import '../wallets/crypto_currency/crypto_currency.dart'; class ClientManager { @@ -10,6 +12,7 @@ class ClientManager { static final ClientManager sharedInstance = ClientManager._(); final Map _map = {}; + final Map _mapNet = {}; final Map _heights = {}; final Map> _subscriptions = {}; final Map> _heightCompleters = {}; @@ -24,18 +27,37 @@ class ClientManager { ElectrumClient? getClient({ required CryptoCurrency cryptoCurrency, - }) => - _map[_keyHelper(cryptoCurrency)]; + required TorPlainNetworkOption netType, + }) { + final _key = _keyHelper(cryptoCurrency); - void addClient( + if (netType == _mapNet[_key]) { + return _map[_key]; + } else { + return null; + } + } + + Future addClient( ElectrumClient client, { required CryptoCurrency cryptoCurrency, - }) { + required TorPlainNetworkOption netType, + }) async { final key = _keyHelper(cryptoCurrency); if (_map[key] != null) { - throw Exception("ElectrumX Client for $key already exists."); + if (_mapNet[key] == netType) { + throw Exception( + "ElectrumX Client for $key and $netType already exists.", + ); + } + + await remove(cryptoCurrency: cryptoCurrency); + + _map[key] = client; + _mapNet[key] = netType; } else { _map[key] = client; + _mapNet[key] = netType; } _heightCompleters[key] = Completer(); @@ -68,10 +90,24 @@ class ClientManager { ); } + if (Prefs.instance.useTor) { + if (_mapNet[key]! == TorPlainNetworkOption.clear) { + throw Exception( + "Non-TOR only client for $key found.", + ); + } + } else { + if (_mapNet[key]! == TorPlainNetworkOption.tor) { + throw Exception( + "TOR only client for $key found.", + ); + } + } + return _heights[key] ?? await _heightCompleters[key]!.future; } - Future remove({ + Future<(ElectrumClient?, TorPlainNetworkOption?)> remove({ required CryptoCurrency cryptoCurrency, }) async { final key = _keyHelper(cryptoCurrency); @@ -80,7 +116,7 @@ class ClientManager { _heights.remove(key); _heightCompleters.remove(key); - return _map.remove(key); + return (_map.remove(key), _mapNet.remove(key)); } Future closeAll() async { @@ -99,6 +135,7 @@ class ClientManager { _heightCompleters.clear(); _heights.clear(); _subscriptions.clear(); + _mapNet.clear(); _map.clear(); } } diff --git a/lib/electrumx_rpc/electrumx_client.dart b/lib/electrumx_rpc/electrumx_client.dart index bceb03fac..986fad95c 100644 --- a/lib/electrumx_rpc/electrumx_client.dart +++ b/lib/electrumx_rpc/electrumx_client.dart @@ -29,6 +29,7 @@ import '../utilities/amount/amount.dart'; import '../utilities/extensions/impl/string.dart'; import '../utilities/logger.dart'; import '../utilities/prefs.dart'; +import '../utilities/tor_plain_net_option_enum.dart'; import '../wallets/crypto_currency/crypto_currency.dart'; import '../wallets/crypto_currency/interfaces/electrumx_currency_interface.dart'; import 'client_manager.dart'; @@ -42,6 +43,10 @@ typedef SparkMempoolData = ({ class WifiOnlyException implements Exception {} +class TorOnlyException implements Exception {} + +class ClearnetOnlyException implements Exception {} + class ElectrumXNode { ElectrumXNode({ required this.address, @@ -49,12 +54,16 @@ class ElectrumXNode { required this.name, required this.id, required this.useSSL, + required this.torEnabled, + required this.clearEnabled, }); final String address; final int port; final String name; final String id; final bool useSSL; + final bool torEnabled; + final bool clearEnabled; factory ElectrumXNode.from(ElectrumXNode node) { return ElectrumXNode( @@ -63,6 +72,8 @@ class ElectrumXNode { name: node.name, id: node.id, useSSL: node.useSSL, + torEnabled: node.torEnabled, + clearEnabled: node.clearEnabled, ); } @@ -74,6 +85,7 @@ class ElectrumXNode { class ElectrumXClient { final CryptoCurrency cryptoCurrency; + final TorPlainNetworkOption netType; String get host => _host; late String _host; @@ -90,6 +102,7 @@ class ElectrumXClient { ElectrumClient? getElectrumAdapter() => ClientManager.sharedInstance.getClient( cryptoCurrency: cryptoCurrency, + netType: netType, ); late Prefs _prefs; @@ -119,6 +132,7 @@ class ElectrumXClient { required int port, required bool useSSL, required Prefs prefs, + required this.netType, required List failovers, required this.cryptoCurrency, this.connectionTimeoutForSpecialCaseJsonRPCClients = @@ -168,6 +182,7 @@ class ElectrumXClient { _electrumAdapterChannel = null; await (await ClientManager.sharedInstance .remove(cryptoCurrency: cryptoCurrency)) + .$1 ?.close(); // Also close any chain height services that are currently open. @@ -193,6 +208,10 @@ class ElectrumXClient { failovers: failovers, globalEventBusForTesting: globalEventBusForTesting, cryptoCurrency: cryptoCurrency, + netType: TorPlainNetworkOption.fromNodeData( + node.torEnabled, + node.clearEnabled, + ), ); } @@ -236,6 +255,18 @@ class ElectrumXClient { // Get the proxy info from the TorService. proxyInfo = _torService.getProxyInfo(); } + + if (netType == TorPlainNetworkOption.clear) { + _electrumAdapterChannel = null; + await ClientManager.sharedInstance + .remove(cryptoCurrency: cryptoCurrency); + } + } else { + if (netType == TorPlainNetworkOption.tor) { + _electrumAdapterChannel = null; + await ClientManager.sharedInstance + .remove(cryptoCurrency: cryptoCurrency); + } } // If the current ElectrumAdapterClient is closed, create a new one. @@ -288,9 +319,10 @@ class ElectrumXClient { ); } - ClientManager.sharedInstance.addClient( + await ClientManager.sharedInstance.addClient( newClient, cryptoCurrency: cryptoCurrency, + netType: netType, ); } @@ -352,6 +384,10 @@ class ElectrumXClient { return response; } on WifiOnlyException { rethrow; + } on ClearnetOnlyException { + rethrow; + } on TorOnlyException { + rethrow; } on SocketException { // likely timed out so then retry if (retries > 0) { @@ -442,6 +478,10 @@ class ElectrumXClient { return response; } on WifiOnlyException { rethrow; + } on ClearnetOnlyException { + rethrow; + } on TorOnlyException { + rethrow; } on SocketException { // likely timed out so then retry if (retries > 0) { @@ -488,10 +528,10 @@ class ElectrumXClient { return await request( requestID: requestID, command: 'server.ping', - requestTimeout: const Duration(seconds: 2), + requestTimeout: const Duration(seconds: 3), retries: retryCount, ).timeout( - const Duration(seconds: 2), + const Duration(seconds: 3), onTimeout: () { Logging.instance.log( "ElectrumxClient.ping timed out with retryCount=$retryCount, host=$_host", diff --git a/lib/exceptions/wallet/node_tor_mismatch_config_exception.dart b/lib/exceptions/wallet/node_tor_mismatch_config_exception.dart new file mode 100644 index 000000000..38fe0638b --- /dev/null +++ b/lib/exceptions/wallet/node_tor_mismatch_config_exception.dart @@ -0,0 +1 @@ +class NodeTorMismatchConfigException implements Exception {} diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index 7888635b7..6480c52f0 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -18,14 +18,17 @@ import 'package:uuid/uuid.dart'; import '../../../../models/node_model.dart'; import '../../../../notifications/show_flush_bar.dart'; +import '../../../../providers/global/active_wallet_provider.dart'; import '../../../../providers/global/secure_store_provider.dart'; import '../../../../providers/providers.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/assets.dart'; import '../../../../utilities/constants.dart'; +import '../../../../utilities/enums/sync_type_enum.dart'; import '../../../../utilities/flutter_secure_storage_interface.dart'; import '../../../../utilities/test_node_connection.dart'; import '../../../../utilities/text_styles.dart'; +import '../../../../utilities/tor_plain_net_option_enum.dart'; import '../../../../utilities/util.dart'; import '../../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../../wallets/crypto_currency/intermediate/cryptonote_currency.dart'; @@ -229,6 +232,11 @@ class _AddEditNodeViewState extends ConsumerState { } } + final torEnabled = formData.netOption == TorPlainNetworkOption.tor || + formData.netOption == TorPlainNetworkOption.both; + final plainEnabled = formData.netOption == TorPlainNetworkOption.clear || + formData.netOption == TorPlainNetworkOption.both; + switch (viewType) { case AddEditNodeViewType.add: final NodeModel node = NodeModel( @@ -243,6 +251,8 @@ class _AddEditNodeViewState extends ConsumerState { isFailover: formData.isFailover!, trusted: formData.trusted!, isDown: false, + torEnabled: torEnabled, + plainEnabled: plainEnabled, ); await ref.read(nodeServiceChangeNotifierProvider).add( @@ -250,6 +260,7 @@ class _AddEditNodeViewState extends ConsumerState { formData.password, true, ); + await _notifyWalletsOfUpdatedNode(); if (mounted) { Navigator.of(context) .popUntil(ModalRoute.withName(widget.routeOnSuccessOrDelete)); @@ -268,6 +279,8 @@ class _AddEditNodeViewState extends ConsumerState { isFailover: formData.isFailover!, trusted: formData.trusted!, isDown: false, + torEnabled: torEnabled, + plainEnabled: plainEnabled, ); await ref.read(nodeServiceChangeNotifierProvider).add( @@ -275,6 +288,7 @@ class _AddEditNodeViewState extends ConsumerState { formData.password, true, ); + await _notifyWalletsOfUpdatedNode(); if (mounted) { Navigator.of(context) .popUntil(ModalRoute.withName(widget.routeOnSuccessOrDelete)); @@ -283,6 +297,39 @@ class _AddEditNodeViewState extends ConsumerState { } } + Future _notifyWalletsOfUpdatedNode() async { + final wallets = + ref.read(pWallets).wallets.where((e) => e.info.coin == widget.coin); + final prefs = ref.read(prefsChangeNotifierProvider); + + switch (prefs.syncType) { + case SyncingType.currentWalletOnly: + for (final wallet in wallets) { + if (ref.read(currentWalletIdProvider) == wallet.walletId) { + unawaited(wallet.updateNode().then((value) => wallet.refresh())); + } else { + unawaited(wallet.updateNode()); + } + } + break; + case SyncingType.selectedWalletsAtStartup: + final List walletIdsToSync = prefs.walletIdsSyncOnStartup; + for (final wallet in wallets) { + if (walletIdsToSync.contains(wallet.walletId)) { + unawaited(wallet.updateNode().then((value) => wallet.refresh())); + } else { + unawaited(wallet.updateNode()); + } + } + break; + case SyncingType.allWalletsOnStartup: + for (final wallet in wallets) { + unawaited(wallet.updateNode().then((value) => wallet.refresh())); + } + break; + } + } + @override void initState() { isDesktop = Util.isDesktop; @@ -568,10 +615,11 @@ class NodeFormData { String? name, host, login, password; int? port; bool? useSSL, isFailover, trusted; + TorPlainNetworkOption? netOption; @override String toString() { - return "{ name: $name, host: $host, port: $port, useSSL: $useSSL, trusted: $trusted }"; + return "{ name: $name, host: $host, port: $port, useSSL: $useSSL, trusted: $trusted, netOption: $netOption }"; } } @@ -615,6 +663,7 @@ class _NodeFormState extends ConsumerState { bool _trusted = false; int? port; late bool enableSSLCheckbox; + late TorPlainNetworkOption netOption; late final bool enableAuthFields; @@ -672,6 +721,7 @@ class _NodeFormState extends ConsumerState { ref.read(nodeFormDataProvider).useSSL = _useSSL; ref.read(nodeFormDataProvider).isFailover = _isFailover; ref.read(nodeFormDataProvider).trusted = _trusted; + ref.read(nodeFormDataProvider).netOption = netOption; } @override @@ -704,6 +754,15 @@ class _NodeFormState extends ConsumerState { _useSSL = node.useSSL; _isFailover = node.isFailover; _trusted = node.trusted ?? false; + + if (node.torEnabled && !node.plainEnabled) { + netOption = TorPlainNetworkOption.tor; + } else if (node.plainEnabled && !node.torEnabled) { + netOption = TorPlainNetworkOption.clear; + } else { + netOption = TorPlainNetworkOption.both; + } + if (widget.coin is Epiccash) { enableSSLCheckbox = !node.host.startsWith("http"); } else { @@ -716,6 +775,7 @@ class _NodeFormState extends ConsumerState { }); } else { enableSSLCheckbox = true; + netOption = TorPlainNetworkOption.both; // default to port 3413 // _portController.text = "3413"; } @@ -1168,7 +1228,139 @@ class _NodeFormState extends ConsumerState { ), ], ), + const SizedBox( + height: 16, + ), + Row( + children: [ + RadioTextButton( + label: "Only TOR traffic", + enabled: !widget.readOnly, + value: TorPlainNetworkOption.tor, + groupValue: netOption, + onChanged: (value) { + if (!widget.readOnly) { + setState( + () => netOption = TorPlainNetworkOption.tor, + ); + _updateState(); + } + }, + ), + ], + ), + const SizedBox( + height: 8, + ), + Row( + children: [ + RadioTextButton( + label: "Only non-TOR traffic", + enabled: !widget.readOnly, + value: TorPlainNetworkOption.clear, + groupValue: netOption, + onChanged: (value) { + if (!widget.readOnly) { + setState( + () => netOption = TorPlainNetworkOption.clear, + ); + _updateState(); + } + }, + ), + ], + ), + const SizedBox( + height: 8, + ), + Row( + children: [ + RadioTextButton( + label: "Allow both", + enabled: !widget.readOnly, + value: TorPlainNetworkOption.both, + groupValue: netOption, + onChanged: (value) { + if (!widget.readOnly) { + setState( + () => netOption = TorPlainNetworkOption.both, + ); + _updateState(); + } + }, + ), + ], + ), ], ); } } + +class RadioTextButton extends StatelessWidget { + const RadioTextButton({ + super.key, + required this.value, + required this.label, + required this.groupValue, + required this.onChanged, + this.enabled = true, + }); + + final T value; + final String label; + final T groupValue; + final bool enabled; + final void Function(T) onChanged; + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) => MouseRegion( + cursor: SystemMouseCursors.click, + child: child, + ), + child: GestureDetector( + onTap: () { + if (value != groupValue) { + onChanged.call(value); + } + }, + child: Container( + color: Colors.transparent, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension()! + .radioButtonIconEnabled, + value: value, + groupValue: groupValue, + onChanged: !enabled + ? null + : (_) { + if (value != groupValue) { + onChanged.call(value); + } + }, + ), + ), + const SizedBox( + width: 14, + ), + Text( + label, + style: STextStyles.w500_14(context), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index da7f0cfcc..3b83d5690 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -13,15 +13,19 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:tuple/tuple.dart'; + import '../../../../notifications/show_flush_bar.dart'; -import 'add_edit_node_view.dart'; +import '../../../../providers/global/active_wallet_provider.dart'; import '../../../../providers/global/secure_store_provider.dart'; import '../../../../providers/providers.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/assets.dart'; +import '../../../../utilities/enums/sync_type_enum.dart'; import '../../../../utilities/flutter_secure_storage_interface.dart'; import '../../../../utilities/test_node_connection.dart'; import '../../../../utilities/text_styles.dart'; +import '../../../../utilities/tor_plain_net_option_enum.dart'; import '../../../../utilities/util.dart'; import '../../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../../widgets/background.dart'; @@ -31,7 +35,7 @@ import '../../../../widgets/desktop/delete_button.dart'; import '../../../../widgets/desktop/desktop_dialog.dart'; import '../../../../widgets/desktop/primary_button.dart'; import '../../../../widgets/desktop/secondary_button.dart'; -import 'package:tuple/tuple.dart'; +import 'add_edit_node_view.dart'; class NodeDetailsView extends ConsumerStatefulWidget { const NodeDetailsView({ @@ -59,6 +63,39 @@ class _NodeDetailsViewState extends ConsumerState { bool _desktopReadOnly = true; + Future _notifyWalletsOfUpdatedNode() async { + final wallets = + ref.read(pWallets).wallets.where((e) => e.info.coin == widget.coin); + final prefs = ref.read(prefsChangeNotifierProvider); + + switch (prefs.syncType) { + case SyncingType.currentWalletOnly: + for (final wallet in wallets) { + if (ref.read(currentWalletIdProvider) == wallet.walletId) { + unawaited(wallet.updateNode().then((value) => wallet.refresh())); + } else { + unawaited(wallet.updateNode()); + } + } + break; + case SyncingType.selectedWalletsAtStartup: + final List walletIdsToSync = prefs.walletIdsSyncOnStartup; + for (final wallet in wallets) { + if (walletIdsToSync.contains(wallet.walletId)) { + unawaited(wallet.updateNode().then((value) => wallet.refresh())); + } else { + unawaited(wallet.updateNode()); + } + } + break; + case SyncingType.allWalletsOnStartup: + for (final wallet in wallets) { + unawaited(wallet.updateNode().then((value) => wallet.refresh())); + } + break; + } + } + @override initState() { secureStore = ref.read(secureStoreProvider); @@ -265,6 +302,16 @@ class _NodeDetailsViewState extends ConsumerState { .read(nodeServiceChangeNotifierProvider) .getNodeById(id: nodeId)!; + final TorPlainNetworkOption netOption; + if (ref.read(nodeFormDataProvider).netOption != null) { + netOption = ref.read(nodeFormDataProvider).netOption!; + } else { + netOption = TorPlainNetworkOption.fromNodeData( + node.torEnabled, + node.plainEnabled, + ); + } + final nodeFormData = NodeFormData() ..useSSL = node.useSSL ..trusted = node.trusted @@ -272,7 +319,8 @@ class _NodeDetailsViewState extends ConsumerState { ..host = node.host ..login = node.loginName ..port = node.port - ..isFailover = node.isFailover; + ..isFailover = node.isFailover + ..netOption = netOption; nodeFormData.password = await node.getPassword( ref.read(secureStoreProvider), ); @@ -338,6 +386,16 @@ class _NodeDetailsViewState extends ConsumerState { loginName: ref.read(nodeFormDataProvider).login, isFailover: ref.read(nodeFormDataProvider).isFailover, + torEnabled: + ref.read(nodeFormDataProvider).netOption == + TorPlainNetworkOption.tor || + ref.read(nodeFormDataProvider).netOption == + TorPlainNetworkOption.both, + plainEnabled: + ref.read(nodeFormDataProvider).netOption == + TorPlainNetworkOption.clear || + ref.read(nodeFormDataProvider).netOption == + TorPlainNetworkOption.both, ); await ref @@ -347,6 +405,7 @@ class _NodeDetailsViewState extends ConsumerState { ref.read(nodeFormDataProvider).password, true, ); + await _notifyWalletsOfUpdatedNode(); } }, ) diff --git a/lib/services/node_service.dart b/lib/services/node_service.dart index 306cb5d6a..a0f5c8065 100644 --- a/lib/services/node_service.dart +++ b/lib/services/node_service.dart @@ -59,6 +59,8 @@ class NodeService extends ChangeNotifier { enabled: savedNode.enabled, isFailover: savedNode.isFailover, trusted: savedNode.trusted, + torEnabled: savedNode.torEnabled, + plainEnabled: savedNode.plainEnabled, ), ); } @@ -75,6 +77,8 @@ class NodeService extends ChangeNotifier { enabled: primaryNode.enabled, isFailover: primaryNode.isFailover, trusted: primaryNode.trusted, + torEnabled: primaryNode.torEnabled, + plainEnabled: primaryNode.plainEnabled, ), ); } diff --git a/lib/services/notifications_service.dart b/lib/services/notifications_service.dart index 5df8cad4d..3748791d9 100644 --- a/lib/services/notifications_service.dart +++ b/lib/services/notifications_service.dart @@ -11,23 +11,23 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; + import '../app_config.dart'; import '../db/hive/db.dart'; import '../electrumx_rpc/electrumx_client.dart'; import '../exceptions/electrumx/no_such_transaction.dart'; import '../models/exchange/response_objects/trade.dart'; import '../models/notification_model.dart'; +import '../utilities/logger.dart'; +import '../utilities/prefs.dart'; +import '../wallets/crypto_currency/crypto_currency.dart'; +import '../wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; +import 'exchange/exchange.dart'; import 'exchange/exchange_response.dart'; import 'node_service.dart'; import 'notifications_api.dart'; import 'trade_service.dart'; import 'wallets.dart'; -import '../utilities/logger.dart'; -import '../utilities/prefs.dart'; -import '../wallets/crypto_currency/crypto_currency.dart'; -import '../wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; - -import 'exchange/exchange.dart'; class NotificationsService extends ChangeNotifier { late NodeService nodeService; @@ -136,12 +136,26 @@ class NotificationsService extends ChangeNotifier { final node = nodeService.getPrimaryNodeFor(currency: coin); if (node != null) { if (wallet is ElectrumXInterface) { + if (prefs.useTor) { + if (node.plainEnabled && !node.torEnabled) { + // just ignore I guess?? + return; + } + } else { + if (node.torEnabled && !node.plainEnabled) { + // just ignore I guess?? + return; + } + } + final eNode = ElectrumXNode( address: node.host, port: node.port, name: node.name, id: node.id, useSSL: node.useSSL, + torEnabled: node.torEnabled, + clearEnabled: node.plainEnabled, ); final failovers = nodeService .failoverNodesFor(currency: coin) @@ -152,6 +166,8 @@ class NotificationsService extends ChangeNotifier { name: e.name, id: e.id, useSSL: e.useSSL, + torEnabled: node.torEnabled, + clearEnabled: node.plainEnabled, ), ) .toList(); diff --git a/lib/utilities/test_node_connection.dart b/lib/utilities/test_node_connection.dart index 458e04e4d..127702b14 100644 --- a/lib/utilities/test_node_connection.dart +++ b/lib/utilities/test_node_connection.dart @@ -5,7 +5,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:on_chain/ada/ada.dart'; -import 'package:on_chain/ada/src/provider/provider/provider.dart'; import 'package:socks5_proxy/socks.dart'; import '../networking/http.dart'; @@ -25,6 +24,7 @@ import 'test_epic_box_connection.dart'; import 'test_eth_node_connection.dart'; import 'test_monero_node_connection.dart'; import 'test_stellar_node_connection.dart'; +import 'tor_plain_net_option_enum.dart'; Future _xmrHelper( NodeFormData nodeFormData, @@ -45,7 +45,6 @@ Future _xmrHelper( final uriString = "${uri.scheme}://${uri.host}:${port ?? 0}$path"; - if (proxyInfo == null && uri.host.endsWith(".onion")) { return false; } @@ -93,6 +92,24 @@ Future testNodeConnection({ }) async { final formData = nodeFormData; + if (ref.read(prefsChangeNotifierProvider).useTor) { + if (formData.netOption! == TorPlainNetworkOption.clear) { + Logging.instance.log( + "This node is configured for non-TOR only but TOR is enabled", + level: LogLevel.Warning, + ); + return false; + } + } else { + if (formData.netOption! == TorPlainNetworkOption.tor) { + Logging.instance.log( + "This node is configured for TOR only but TOR is disabled", + level: LogLevel.Warning, + ); + return false; + } + } + bool testPassed = false; switch (cryptoCurrency) { @@ -111,9 +128,7 @@ Future testNodeConnection({ case CryptonoteCurrency(): try { - final proxyInfo = ref - .read(prefsChangeNotifierProvider) - .useTor + final proxyInfo = ref.read(prefsChangeNotifierProvider).useTor ? ref.read(pTorService).getProxyInfo() : null; @@ -186,7 +201,7 @@ Future testNodeConnection({ case Stellar(): try { testPassed = - await testStellarNodeConnection(formData.host!, formData.port!); + await testStellarNodeConnection(formData.host!, formData.port!); } catch (_) {} break; @@ -202,9 +217,7 @@ Future testNodeConnection({ "action": "version", }, ), - proxyInfo: ref - .read(prefsChangeNotifierProvider) - .useTor + proxyInfo: ref.read(prefsChangeNotifierProvider).useTor ? ref.read(pTorService).getProxyInfo() : null, ); @@ -245,9 +258,7 @@ Future testNodeConnection({ case Cardano(): try { final client = HttpClient(); - if (ref - .read(prefsChangeNotifierProvider) - .useTor) { + if (ref.read(prefsChangeNotifierProvider).useTor) { final proxyInfo = TorService.sharedInstance.getProxyInfo(); final proxySettings = ProxySettings( proxyInfo.host, diff --git a/lib/utilities/tor_plain_net_option_enum.dart b/lib/utilities/tor_plain_net_option_enum.dart new file mode 100644 index 000000000..26b5604d9 --- /dev/null +++ b/lib/utilities/tor_plain_net_option_enum.dart @@ -0,0 +1,23 @@ +enum TorPlainNetworkOption { + tor, + clear, + both; + + bool allowsTor() => this == tor || this == both; + bool allowsClear() => this == clear || this == both; + + static TorPlainNetworkOption fromNodeData( + bool torEnabled, + bool clearEnabled, + ) { + if (clearEnabled && torEnabled) { + return TorPlainNetworkOption.both; + } else if (torEnabled) { + return TorPlainNetworkOption.tor; + } else if (clearEnabled) { + return TorPlainNetworkOption.clear; + } else { + return TorPlainNetworkOption.both; + } + } +} diff --git a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart index 9d4a4b739..aa1bab4e7 100644 --- a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart @@ -1338,6 +1338,8 @@ class BitcoinFrostWallet extends Wallet name: node.name, useSSL: node.useSSL, id: node.id, + torEnabled: node.torEnabled, + clearEnabled: node.plainEnabled, ); } @@ -1352,6 +1354,8 @@ class BitcoinFrostWallet extends Wallet name: e.name, id: e.id, useSSL: e.useSSL, + torEnabled: e.torEnabled, + clearEnabled: e.plainEnabled, ), ) .toList(); diff --git a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart index f1ac98c80..e83ba744b 100644 --- a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart @@ -467,7 +467,22 @@ abstract class LibMoneroWallet final host = Uri.parse(node.host).host; ({InternetAddress host, int port})? proxy; if (prefs.useTor) { + if (node.plainEnabled && !node.torEnabled) { + libMoneroWallet?.stopAutoSaving(); + libMoneroWallet?.stopListeners(); + libMoneroWallet?.stopSyncing(); + _setSyncStatus(lib_monero_compat.FailedSyncStatus()); + throw Exception("TOR - clearnet mismatch"); + } proxy = TorService.sharedInstance.getProxyInfo(); + } else { + if (!node.plainEnabled && node.torEnabled) { + libMoneroWallet?.stopAutoSaving(); + libMoneroWallet?.stopListeners(); + libMoneroWallet?.stopSyncing(); + _setSyncStatus(lib_monero_compat.FailedSyncStatus()); + throw Exception("TOR - clearnet mismatch"); + } } _setSyncStatus(lib_monero_compat.ConnectingSyncStatus()); @@ -495,6 +510,9 @@ abstract class LibMoneroWallet proxy == null ? null : "${proxy.host.address}:${proxy.port}", ); } + libMoneroWallet?.startSyncing(); + libMoneroWallet?.startListeners(); + libMoneroWallet?.startAutoSaving(); _setSyncStatus(lib_monero_compat.ConnectedSyncStatus()); } catch (e, s) { @@ -1020,6 +1038,26 @@ abstract class LibMoneroWallet return; } + final node = getCurrentNode(); + + if (prefs.useTor) { + if (node.plainEnabled && !node.torEnabled) { + libMoneroWallet?.stopAutoSaving(); + libMoneroWallet?.stopListeners(); + libMoneroWallet?.stopSyncing(); + _setSyncStatus(lib_monero_compat.FailedSyncStatus()); + throw Exception("TOR - clearnet mismatch"); + } + } else { + if (!node.plainEnabled && node.torEnabled) { + libMoneroWallet?.stopAutoSaving(); + libMoneroWallet?.stopListeners(); + libMoneroWallet?.stopSyncing(); + _setSyncStatus(lib_monero_compat.FailedSyncStatus()); + throw Exception("TOR - clearnet mismatch"); + } + } + // this acquire should be almost instant due to above check. // Slight possibility of race but should be irrelevant await refreshMutex.acquire(); diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 62f0e2255..e764dee54 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -906,6 +906,8 @@ mixin ElectrumXInterface name: node.name, useSSL: node.useSSL, id: node.id, + torEnabled: node.torEnabled, + clearEnabled: node.plainEnabled, ); } @@ -919,6 +921,8 @@ mixin ElectrumXInterface name: e.name, id: e.id, useSSL: e.useSSL, + torEnabled: e.torEnabled, + clearEnabled: e.plainEnabled, ), ) .toList(); diff --git a/lib/widgets/node_card.dart b/lib/widgets/node_card.dart index b8a6f9cad..6099ff242 100644 --- a/lib/widgets/node_card.dart +++ b/lib/widgets/node_card.dart @@ -13,6 +13,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:tuple/tuple.dart'; + import '../notifications/show_flush_bar.dart'; import '../pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart'; import '../pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart'; @@ -26,6 +28,7 @@ import '../utilities/default_nodes.dart'; import '../utilities/enums/sync_type_enum.dart'; import '../utilities/test_node_connection.dart'; import '../utilities/text_styles.dart'; +import '../utilities/tor_plain_net_option_enum.dart'; import '../utilities/util.dart'; import '../wallets/crypto_currency/crypto_currency.dart'; import 'conditional_parent.dart'; @@ -33,7 +36,6 @@ import 'custom_buttons/blue_text_button.dart'; import 'expandable.dart'; import 'node_options_sheet.dart'; import 'rounded_white_container.dart'; -import 'package:tuple/tuple.dart'; class NodeCard extends ConsumerStatefulWidget { const NodeCard({ @@ -165,6 +167,15 @@ class _NodeCardState extends ConsumerState { text: "Connect", enabled: _status == "Disconnected", onTap: () async { + final TorPlainNetworkOption netOption; + if (_node.torEnabled && !_node.plainEnabled) { + netOption = TorPlainNetworkOption.tor; + } else if (_node.plainEnabled && !_node.torEnabled) { + netOption = TorPlainNetworkOption.clear; + } else { + netOption = TorPlainNetworkOption.both; + } + final nodeFormData = NodeFormData() ..useSSL = _node.useSSL ..trusted = _node.trusted @@ -172,6 +183,7 @@ class _NodeCardState extends ConsumerState { ..host = _node.host ..login = _node.loginName ..port = _node.port + ..netOption = netOption ..isFailover = _node.isFailover; nodeFormData.password = await _node.getPassword( ref.read(secureStoreProvider), diff --git a/lib/widgets/node_options_sheet.dart b/lib/widgets/node_options_sheet.dart index 553f60c05..8273401b1 100644 --- a/lib/widgets/node_options_sheet.dart +++ b/lib/widgets/node_options_sheet.dart @@ -27,6 +27,7 @@ import '../utilities/default_nodes.dart'; import '../utilities/enums/sync_type_enum.dart'; import '../utilities/test_node_connection.dart'; import '../utilities/text_styles.dart'; +import '../utilities/tor_plain_net_option_enum.dart'; import '../wallets/crypto_currency/crypto_currency.dart'; import 'rounded_white_container.dart'; @@ -256,6 +257,15 @@ class NodeOptionsSheet extends ConsumerWidget { ref.read(secureStoreProvider), ); if (context.mounted) { + final TorPlainNetworkOption netOption; + if (node.torEnabled && !node.plainEnabled) { + netOption = TorPlainNetworkOption.tor; + } else if (node.plainEnabled && + !node.torEnabled) { + netOption = TorPlainNetworkOption.clear; + } else { + netOption = TorPlainNetworkOption.both; + } final canConnect = await testNodeConnection( context: context, nodeFormData: NodeFormData() @@ -266,6 +276,7 @@ class NodeOptionsSheet extends ConsumerWidget { ..port = node.port ..useSSL = node.useSSL ..isFailover = node.isFailover + ..netOption = netOption ..trusted = node.trusted, cryptoCurrency: coin, ref: ref, diff --git a/test/cached_electrumx_test.dart b/test/cached_electrumx_test.dart index 26213175f..2d5b9b418 100644 --- a/test/cached_electrumx_test.dart +++ b/test/cached_electrumx_test.dart @@ -168,6 +168,8 @@ void main() { name: "some name", id: "some ID", useSSL: true, + torEnabled: true, + clearEnabled: true, ); final client = diff --git a/test/services/node_service_test.dart b/test/services/node_service_test.dart index 5b4f1e635..d5ce1b84c 100644 --- a/test/services/node_service_test.dart +++ b/test/services/node_service_test.dart @@ -1,10 +1,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hive/hive.dart'; import 'package:hive_test/hive_test.dart'; +import 'package:stackwallet/app_config.dart'; import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/services/node_service.dart'; -import 'package:stackwallet/app_config.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; @@ -48,6 +48,8 @@ void main() { coinName: "bitcoin", isFailover: true, isDown: false, + torEnabled: true, + plainEnabled: true, ); await service.setPrimaryNodeFor( coin: Bitcoin(CryptoCurrencyNetwork.main), @@ -129,6 +131,8 @@ void main() { coinName: "bitcoin", isFailover: true, isDown: false, + torEnabled: true, + plainEnabled: true, ); final nodeB = NodeModel( host: "host2", @@ -140,6 +144,8 @@ void main() { coinName: "monero", isFailover: true, isDown: false, + torEnabled: true, + plainEnabled: true, ); final nodeC = NodeModel( host: "host3", @@ -151,6 +157,8 @@ void main() { coinName: "epicCash", isFailover: true, isDown: false, + torEnabled: true, + plainEnabled: true, ); setUp(() async { diff --git a/test/widget_tests/node_card_test.dart b/test/widget_tests/node_card_test.dart index 45c0dbe12..374991fab 100644 --- a/test/widget_tests/node_card_test.dart +++ b/test/widget_tests/node_card_test.dart @@ -37,6 +37,8 @@ void main() { coinName: "Bitcoin", isFailover: false, isDown: false, + torEnabled: true, + plainEnabled: true, ), ); @@ -51,6 +53,8 @@ void main() { coinName: "Bitcoin", isFailover: false, isDown: false, + torEnabled: true, + plainEnabled: true, ), ); @@ -112,6 +116,8 @@ void main() { coinName: "Bitcoin", isFailover: false, isDown: false, + torEnabled: true, + plainEnabled: true, ), ); @@ -126,6 +132,8 @@ void main() { coinName: "Bitcoin", isFailover: false, isDown: false, + torEnabled: true, + plainEnabled: true, ), ); @@ -188,6 +196,8 @@ void main() { coinName: "Bitcoin", isFailover: false, isDown: false, + torEnabled: true, + plainEnabled: true, ), ); @@ -202,6 +212,8 @@ void main() { coinName: "Bitcoin", isFailover: false, isDown: false, + torEnabled: true, + plainEnabled: true, ), ); diff --git a/test/widget_tests/node_options_sheet_test.dart b/test/widget_tests/node_options_sheet_test.dart index c289b29b5..6070e2473 100644 --- a/test/widget_tests/node_options_sheet_test.dart +++ b/test/widget_tests/node_options_sheet_test.dart @@ -25,17 +25,20 @@ void main() { final mockPrefs = MockPrefs(); final mockNodeService = MockNodeService(); - when(mockNodeService.getNodeById(id: "node id")).thenAnswer( - (realInvocation) => NodeModel( - host: "127.0.0.1", - port: 2000, - name: "Some other name", - id: "node id", - useSSL: true, - enabled: true, - coinName: "Bitcoin", - isFailover: false, - isDown: false)); + when(mockNodeService.getNodeById(id: "node id")) + .thenAnswer((realInvocation) => NodeModel( + host: "127.0.0.1", + port: 2000, + name: "Some other name", + id: "node id", + useSSL: true, + enabled: true, + coinName: "Bitcoin", + isFailover: false, + isDown: false, + torEnabled: true, + plainEnabled: true, + )); when(mockNodeService.getPrimaryNodeFor( currency: Bitcoin(CryptoCurrencyNetwork.main))) @@ -48,6 +51,8 @@ void main() { enabled: true, coinName: "Bitcoin", isFailover: false, + torEnabled: true, + plainEnabled: true, isDown: false)); await tester.pumpWidget( @@ -109,6 +114,8 @@ void main() { coinName: "Bitcoin", isFailover: false, isDown: false, + torEnabled: true, + plainEnabled: true, ), ); @@ -125,6 +132,8 @@ void main() { coinName: "Bitcoin", isFailover: false, isDown: false, + torEnabled: true, + plainEnabled: true, ), ); @@ -186,6 +195,8 @@ void main() { coinName: "Bitcoin", isFailover: false, isDown: false, + torEnabled: true, + plainEnabled: true, ), ); @@ -202,6 +213,8 @@ void main() { coinName: "Bitcoin", isFailover: false, isDown: false, + torEnabled: true, + plainEnabled: true, ), );