diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4d7193eeb3..ca80322b13 100644 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,14 @@ { "pins" : [ + { + "identity" : "cocoalumberjack", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CocoaLumberjack/CocoaLumberjack.git", + "state" : { + "revision" : "c5f2ed4d219866117e135cf648180dc7b66739d3", + "version" : "3.9.1" + } + }, { "identity" : "dkcamera", "kind" : "remoteSourceControl", @@ -36,6 +45,23 @@ "version" : "5.21.7" } }, + { + "identity" : "svgkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SVGKit/SVGKit.git", + "state" : { + "revision" : "6002a0ff6b2d4405805395959b92b38aa24662cb" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log", + "state" : { + "revision" : "4038b6a4f74a1426f47eb1dd5b4bb5628a15d597", + "version" : "1.16.1" + } + }, { "identity" : "swiftygif", "kind" : "remoteSourceControl", diff --git a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4d7193eeb3..ca80322b13 100644 --- a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,14 @@ { "pins" : [ + { + "identity" : "cocoalumberjack", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CocoaLumberjack/CocoaLumberjack.git", + "state" : { + "revision" : "c5f2ed4d219866117e135cf648180dc7b66739d3", + "version" : "3.9.1" + } + }, { "identity" : "dkcamera", "kind" : "remoteSourceControl", @@ -36,6 +45,23 @@ "version" : "5.21.7" } }, + { + "identity" : "svgkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SVGKit/SVGKit.git", + "state" : { + "revision" : "6002a0ff6b2d4405805395959b92b38aa24662cb" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log", + "state" : { + "revision" : "4038b6a4f74a1426f47eb1dd5b4bb5628a15d597", + "version" : "1.16.1" + } + }, { "identity" : "swiftygif", "kind" : "remoteSourceControl", diff --git a/lib/electrumx_rpc/client_manager.dart b/lib/electrumx_rpc/client_manager.dart index 8cd9d53241..0a666f1d6b 100644 --- a/lib/electrumx_rpc/client_manager.dart +++ b/lib/electrumx_rpc/client_manager.dart @@ -13,6 +13,7 @@ class ClientManager { final Map _map = {}; final Map _mapNet = {}; + final Map _certificates = {}; final Map _heights = {}; final Map> _subscriptions = {}; final Map> _heightCompleters = {}; @@ -42,82 +43,75 @@ class ClientManager { ElectrumClient client, { required CryptoCurrency cryptoCurrency, required TorPlainNetworkOption netType, + String? trustedCertificatePem, }) async { final key = _keyHelper(cryptoCurrency); - if (_map[key] != null) { - 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(); + final previous = _map[key]; + final subscription = _subscriptions.remove(key); + _map[key] = client; + _mapNet[key] = netType; + _heights.remove(key); + _certificates[key] = trustedCertificatePem; + final firstHeight = _heightCompleters[key] = Completer(); _subscriptions[key] = client.subscribeHeaders().listen( (event) { + if (!identical(_map[key], client)) return; _heights[key] = event.height; - - if (!_heightCompleters[key]!.isCompleted) { - _heightCompleters[key]!.complete(event.height); + if (!firstHeight.isCompleted) { + firstHeight.complete(event.height); } }, - onError: (Object err, StackTrace s) => Logging.instance.e( - "ClientManager listen", - error: err, - stackTrace: s, - ), + onError: (Object err, StackTrace s) => + Logging.instance.e("ClientManager listen", error: err, stackTrace: s), ); + await subscription?.cancel(); + await previous?.close(); } Future getChainHeightFor(CryptoCurrency cryptoCurrency) async { final key = _keyHelper(cryptoCurrency); if (_map[key] == null) { - throw Exception( - "No managed ElectrumClient for $key found.", - ); + throw Exception("No managed ElectrumClient for $key found."); } if (_heightCompleters[key] == null) { - throw Exception( - "No managed _heightCompleters for $key found.", - ); + throw Exception("No managed _heightCompleters for $key found."); } if (Prefs.instance.useTor) { if (_mapNet[key]! == TorPlainNetworkOption.clear) { - throw Exception( - "Non-TOR only client for $key found.", - ); + throw Exception("Non-TOR only client for $key found."); } } else { if (_mapNet[key]! == TorPlainNetworkOption.tor) { - throw Exception( - "TOR only client for $key found.", - ); + throw Exception("TOR only client for $key found."); } } return _heights[key] ?? await _heightCompleters[key]!.future; } + /// The certificate the managed connection for [cryptoCurrency] was opened + /// with, or null when it relies on the platform's trusted roots. + String? trustedCertificateFor(CryptoCurrency cryptoCurrency) => + _certificates[_keyHelper(cryptoCurrency)]; + Future<(ElectrumClient?, TorPlainNetworkOption?)> remove({ required CryptoCurrency cryptoCurrency, + ElectrumClient? expectedClient, }) async { final key = _keyHelper(cryptoCurrency); - await _subscriptions[key]?.cancel(); - _subscriptions.remove(key); + if (expectedClient != null && !identical(_map[key], expectedClient)) { + return (null, null); + } + final client = _map.remove(key); + final network = _mapNet.remove(key); + final subscription = _subscriptions.remove(key); _heights.remove(key); _heightCompleters.remove(key); - - return (_map.remove(key), _mapNet.remove(key)); + _certificates.remove(key); + await subscription?.cancel(); + return (client, network); } Future closeAll() async { @@ -138,5 +132,6 @@ class ClientManager { _subscriptions.clear(); _mapNet.clear(); _map.clear(); + _certificates.clear(); } } diff --git a/lib/electrumx_rpc/electrumx_client.dart b/lib/electrumx_rpc/electrumx_client.dart index 89bac9a46f..08178ae2fc 100644 --- a/lib/electrumx_rpc/electrumx_client.dart +++ b/lib/electrumx_rpc/electrumx_client.dart @@ -18,7 +18,6 @@ import 'package:electrum_adapter/electrum_adapter.dart'; import 'package:electrum_adapter/methods/specific/firo.dart'; import 'package:event_bus/event_bus.dart'; import 'package:mutex/mutex.dart'; -import 'package:stream_channel/stream_channel.dart'; import '../app_config.dart'; import '../exceptions/electrumx/no_such_transaction.dart'; @@ -31,6 +30,7 @@ import '../utilities/amount/amount.dart'; import '../utilities/extensions/impl/string.dart'; import '../utilities/logger.dart'; import '../utilities/prefs.dart'; +import '../utilities/tls_certificate.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'; @@ -51,6 +51,7 @@ class ElectrumXNode { required this.useSSL, required this.torEnabled, required this.clearnetEnabled, + this.trustedCertificatePem, }); final String address; final int port; @@ -59,6 +60,7 @@ class ElectrumXNode { final bool useSSL; final bool torEnabled; final bool clearnetEnabled; + final String? trustedCertificatePem; factory ElectrumXNode.from(ElectrumXNode node) { return ElectrumXNode( @@ -69,6 +71,7 @@ class ElectrumXNode { useSSL: node.useSSL, torEnabled: node.torEnabled, clearnetEnabled: node.clearnetEnabled, + trustedCertificatePem: node.trustedCertificatePem, ); } @@ -90,12 +93,19 @@ class ElectrumXClient { bool get useSSL => _useSSL; late bool _useSSL; + final String? trustedCertificatePem; - // StreamChannel? get electrumAdapterChannel => _electrumAdapterChannel; - StreamChannel? _electrumAdapterChannel; + bool _closed = false; + int _connectionGeneration = 0; + ElectrumClient? _adapter; + ElectrumClient? _pendingClient; - ElectrumClient? getElectrumAdapter() => ClientManager.sharedInstance - .getClient(cryptoCurrency: cryptoCurrency, netType: netType); + ElectrumClient? getElectrumAdapter() => _closed + ? null + : ClientManager.sharedInstance.getClient( + cryptoCurrency: cryptoCurrency, + netType: netType, + ); late Prefs _prefs; late TorService _torService; @@ -105,12 +115,6 @@ class ElectrumXClient { final Duration connectionTimeoutForSpecialCaseJsonRPCClients; - // add finalizer to cancel stream subscription when all references to an - // instance of ElectrumX becomes inaccessible - static final Finalizer _finalizer = Finalizer((p0) { - p0._torPreferenceListener?.cancel(); - p0._torStatusListener?.cancel(); - }); StreamSubscription? _torPreferenceListener; StreamSubscription? _torStatusListener; @@ -132,6 +136,7 @@ class ElectrumXClient { ), TorService? torService, EventBus? globalEventBusForTesting, + this.trustedCertificatePem, }) { _prefs = prefs; _torService = torService ?? TorService.sharedInstance; @@ -146,9 +151,14 @@ class ElectrumXClient { _torStatusListener = bus.on().listen(( event, ) async { + if (_closed) return; switch (event.newStatus) { case TorConnectionStatus.connecting: await _torConnectingLock.acquire(); + if (_closed) { + _torConnectingLock.release(); + return; + } _requireMutex = true; break; @@ -166,21 +176,7 @@ class ElectrumXClient { _torPreferenceListener = bus.on().listen(( event, ) async { - // not sure if we need to do anything specific here - // switch (event.status) { - // case TorStatus.enabled: - // case TorStatus.disabled: - // } - - // setting to null should force the creation of a new json rpc client - // on the next request sent through this electrumx instance - _electrumAdapterChannel = null; - await (await ClientManager.sharedInstance.remove( - cryptoCurrency: cryptoCurrency, - )).$1?.close(); - - // Also close any chain height services that are currently open. - // await ChainHeightServiceManager.dispose(); + if (!_closed) await _resetAdapter(); }); } @@ -196,6 +192,7 @@ class ElectrumXClient { host: node.address, port: node.port, useSSL: node.useSSL, + trustedCertificatePem: node.trustedCertificatePem, prefs: prefs, torService: torService, failovers: failovers, @@ -218,120 +215,141 @@ class ElectrumXClient { } Future closeAdapter() async { - await getElectrumAdapter()?.close(); + _closed = true; + _requireMutex = false; + if (_torConnectingLock.isLocked) _torConnectingLock.release(); + await _torPreferenceListener?.cancel(); + await _torStatusListener?.cancel(); + await _resetAdapter(); } - Future checkElectrumAdapter() async { - await _adapterMutex.protect(() async { - ({InternetAddress host, int port})? proxyInfo; - - if (AppConfig.hasFeature(AppFeature.tor)) { - // 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.w( - "Tor preference set but Tor is not enabled, killswitch not set," - " connecting to Electrum adapter through clearnet", - ); - } 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 Electrum adapter", - ); - // TODO [prio=low]: Try to start Tor. - } - } else { - // 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. - if (getElectrumAdapter() != null && getElectrumAdapter()!.peer.isClosed) { - _electrumAdapterChannel = null; - await ClientManager.sharedInstance.remove( - cryptoCurrency: cryptoCurrency, - ); - } + Future _release(ElectrumClient client) async { + final removed = ClientManager.sharedInstance.remove( + cryptoCurrency: cryptoCurrency, + expectedClient: client, + ); + try { + await client.close(); + } finally { + await removed; + } + } - final String useHost; - final int usePort; - final bool useUseSSL; + Future _resetAdapter() async { + _connectionGeneration++; + final pending = _pendingClient; + final current = _adapter; + _pendingClient = _adapter = null; + if (pending != null) await _release(pending); + if (current != null && !identical(current, pending)) { + await _release(current); + } + } - if (currentFailoverIndex == -1) { - useHost = host; - usePort = port; - useUseSSL = useSSL; - } else { - _electrumAdapterChannel = null; - await ClientManager.sharedInstance.remove( - cryptoCurrency: cryptoCurrency, - ); - useHost = _failovers[currentFailoverIndex].address; - usePort = _failovers[currentFailoverIndex].port; - useUseSSL = _failovers[currentFailoverIndex].useSSL; + Future checkElectrumAdapter() => _adapterMutex.protect(() async { + final generation = _connectionGeneration; + void ensureCurrent() { + if (_closed || generation != _connectionGeneration) { + throw StateError('Electrum client changed'); } + } - _electrumAdapterChannel ??= await electrum_adapter.connect( + ensureCurrent(); + final failover = currentFailoverIndex == -1 + ? null + : _failovers[currentFailoverIndex]; + final useHost = failover?.address ?? host; + final usePort = failover?.port ?? port; + final ssl = failover?.useSSL ?? useSSL; + final pem = ssl + ? (failover == null + ? trustedCertificatePem + : failover.trustedCertificatePem) + : null; + ({InternetAddress host, int port})? proxy; + if (AppConfig.hasFeature(AppFeature.tor) && _prefs.useTor) { + if (_torService.status == TorConnectionStatus.connected) { + proxy = _torService.getProxyInfo(); + } else if (_prefs.torKillSwitch) { + throw StateError('Tor is not connected'); + } + } + bool matches( + ElectrumClient client, + String h, + int p, + bool tls, + String? pin, + ) => + client.host == h && + client.port == p && + client.useSSL == tls && + ClientManager.sharedInstance.trustedCertificateFor(cryptoCurrency) == + (tls ? pin : null); + bool shareable(ElectrumClient client) => + !client.peer.isClosed && + client.proxyInfo?.host.address == proxy?.host.address && + client.proxyInfo?.port == proxy?.port && + (failover != null + ? matches(client, useHost, usePort, ssl, pem) + : matches(client, host, port, useSSL, trustedCertificatePem) || + _failovers.any( + (n) => matches( + client, + n.address, + n.port, + n.useSSL, + n.trustedCertificatePem, + ), + )); + final existing = getElectrumAdapter(); + if (existing != null && shareable(existing)) { + _adapter = existing; + return; + } + if (existing != null) { + await _release(existing); + ensureCurrent(); + } + ElectrumClient? candidate; + bool kept = false; + try { + final channel = await electrum_adapter.connect( useHost, port: usePort, + useSSL: ssl, + proxyInfo: proxy, connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, - aliveTimerDuration: connectionTimeoutForSpecialCaseJsonRPCClients, acceptUnverified: false, - useSSL: useUseSSL, - proxyInfo: proxyInfo, + onBadCertificate: trustedCertificateCheck(pem), ); - - if (getElectrumAdapter() == null) { - final ElectrumClient newClient; - if (cryptoCurrency is Firo) { - newClient = FiroElectrumClient( - _electrumAdapterChannel!, - useHost, - usePort, - useUseSSL, - proxyInfo, - ); - } else { - newClient = ElectrumClient( - _electrumAdapterChannel!, - useHost, - usePort, - useUseSSL, - proxyInfo, - ); - } - - await newClient.request('server.version'); - + candidate = cryptoCurrency is Firo + ? FiroElectrumClient(channel, useHost, usePort, ssl, proxy) + : ElectrumClient(channel, useHost, usePort, ssl, proxy); + _pendingClient = candidate; + ensureCurrent(); + await candidate.request('server.version'); + ensureCurrent(); + final other = getElectrumAdapter(); + if (other != null && shareable(other)) { + _adapter = other; + } else { + _adapter = candidate; await ClientManager.sharedInstance.addClient( - newClient, + candidate, cryptoCurrency: cryptoCurrency, netType: netType, + trustedCertificatePem: pem, ); + ensureCurrent(); + kept = true; } - }); - } + } finally { + if (identical(_pendingClient, candidate)) _pendingClient = null; + if (!kept && candidate != null) await _release(candidate); + } + ensureCurrent(); + }); /// Send raw rpc command Future request({ @@ -354,7 +372,7 @@ class ElectrumXClient { } try { - final response = await getElectrumAdapter()!.request(command, args); + final response = await _adapter!.request(command, args); if (response is Map && response.keys.contains("error") && @@ -448,9 +466,9 @@ class ElectrumXClient { try { final futures = >[]; - getElectrumAdapter()!.peer.withBatch(() { + _adapter!.peer.withBatch(() { for (final arg in args) { - futures.add(getElectrumAdapter()!.request(command, arg)); + futures.add(_adapter!.request(command, arg)); } }); final response = await Future.wait(futures); @@ -829,7 +847,7 @@ class ElectrumXClient { }) async { Logging.instance.d("attempting to fetch blockchain.transaction.get..."); await checkElectrumAdapter(); - final dynamic response = await getElectrumAdapter()!.request( + final dynamic response = await _adapter!.request( 'blockchain.transaction.get', [txHash, verbose], ); @@ -885,9 +903,8 @@ class ElectrumXClient { }) async { Logging.instance.d("attempting to fetch lelantus.getanonymityset..."); await checkElectrumAdapter(); - final Map response = - await (getElectrumAdapter() as FiroElectrumClient) - .getLelantusAnonymitySet(groupId: groupId, blockHash: blockhash); + final Map response = await (_adapter as FiroElectrumClient) + .getLelantusAnonymitySet(groupId: groupId, blockHash: blockhash); Logging.instance.d("Fetching lelantus.getanonymityset finished"); return response; } @@ -902,7 +919,7 @@ class ElectrumXClient { }) async { Logging.instance.d("attempting to fetch lelantus.getmintmetadata..."); await checkElectrumAdapter(); - final dynamic response = await (getElectrumAdapter() as FiroElectrumClient) + final dynamic response = await (_adapter as FiroElectrumClient) .getLelantusMintData(mints: mints); Logging.instance.d("Fetching lelantus.getmintmetadata finished"); return response; @@ -921,7 +938,7 @@ class ElectrumXClient { dynamic response; while (retryCount > 0 && response is! List) { - response = await (getElectrumAdapter() as FiroElectrumClient) + response = await (_adapter as FiroElectrumClient) .getLelantusUsedCoinSerials(startNumber: startNumber); // TODO add 2 minute timeout. Logging.instance.d("Fetching lelantus.getusedcoinserials finished"); @@ -938,7 +955,7 @@ class ElectrumXClient { Future getLelantusLatestCoinId({String? requestID}) async { Logging.instance.d("attempting to fetch lelantus.getlatestcoinid..."); await checkElectrumAdapter(); - final int response = await (getElectrumAdapter() as FiroElectrumClient) + final int response = await (_adapter as FiroElectrumClient) .getLatestCoinId(); Logging.instance.d("Fetching lelantus.getlatestcoinid finished"); return response; @@ -968,11 +985,10 @@ class ElectrumXClient { final start = DateTime.now(); await checkElectrumAdapter(); final Map response = - await (getElectrumAdapter() as FiroElectrumClient) - .getSparkAnonymitySet( - coinGroupId: coinGroupId, - startBlockHash: startBlockHash, - ); + await (_adapter as FiroElectrumClient).getSparkAnonymitySet( + coinGroupId: coinGroupId, + startBlockHash: startBlockHash, + ); Logging.instance.d( "Finished ElectrumXClient.getSparkAnonymitySet(coinGroupId" "=$coinGroupId, startBlockHash=$startBlockHash). " @@ -996,7 +1012,7 @@ class ElectrumXClient { // final start = DateTime.now(); // await _checkElectrumAdapter(); // final Map response = - // await (getElectrumAdapter() as FiroElectrumClient) + // await (_adapter as FiroElectrumClient) // .getUsedCoinsTags(startNumber: startNumber); // // TODO: Add 2 minute timeout. // // Why 2 minutes? @@ -1043,7 +1059,7 @@ class ElectrumXClient { // ); // await checkElectrumAdapter(); // final List response = - // await (getElectrumAdapter() as FiroElectrumClient) + // await (_adapter as FiroElectrumClient) // .getSparkMintMetaData(sparkCoinHashes: sparkCoinHashes); // Logging.instance.log( // "Fetching spark.getsparkmintmetadata finished", @@ -1063,7 +1079,7 @@ class ElectrumXClient { try { Logging.instance.d("attempting to fetch spark.getsparklatestcoinid..."); await checkElectrumAdapter(); - final int response = await (getElectrumAdapter() as FiroElectrumClient) + final int response = await (_adapter as FiroElectrumClient) .getSparkLatestCoinId(); Logging.instance.d("Fetching spark.getsparklatestcoinid finished"); return response; @@ -1364,7 +1380,7 @@ class ElectrumXClient { /// } Future> getFeeRate({String? requestID}) async { await checkElectrumAdapter(); - return await getElectrumAdapter()!.getFeeRate(); + return await _adapter!.getFeeRate(); } /// Return the estimated transaction fee per kilobyte for a transaction to be diff --git a/lib/models/node_form_data.dart b/lib/models/node_form_data.dart new file mode 100644 index 0000000000..eff051c23d --- /dev/null +++ b/lib/models/node_form_data.dart @@ -0,0 +1,81 @@ +import '../utilities/tor_plain_net_option_enum.dart'; +import 'node_model.dart'; + +class NodeFormData { + NodeFormData(); + + // The certificate is assigned last so it is bound to the node's endpoint. + factory NodeFormData.fromNode(NodeModel node) => NodeFormData() + ..name = node.name + ..host = node.host + ..port = node.port + ..useSSL = node.useSSL + ..login = node.loginName + ..trusted = node.trusted + ..isFailover = node.isFailover + ..netOption = TorPlainNetworkOption.fromNodeData( + node.torEnabled, + node.clearnetEnabled, + ) + ..forceNoTor = node.forceNoTor + ..isPrimary = node.isPrimary + ..apiSecret = node.nodeApiSecret + ..trustedCertificatePem = node.trustedCertificatePem + ..source = node; + + String? name, host, login, password, apiSecret; + int? port; + bool? useSSL, isFailover, trusted, forceNoTor, isPrimary; + TorPlainNetworkOption? netOption; + NodeModel? source; + Future? passwordLoad; + int certificateRevision = 0; + + // A certificate approval belongs to the endpoint it was reviewed for. It + // stays with that endpoint: editing the host and restoring it keeps the + // approval, while any other host, port or SSL setting reads as unapproved. + String? _approvedPem; + String? _approvedHost; + int? _approvedPort; + bool? _approvedUseSSL; + bool _forgotten = false; + + bool get certificateForgotten => + _forgotten && + host == _approvedHost && + port == _approvedPort && + useSSL == _approvedUseSSL; + + void forgetCertificate() { + trustedCertificatePem = null; + _forgotten = true; + } + + String? get trustedCertificatePem => + _approvedPem != null && + host == _approvedHost && + port == _approvedPort && + useSSL == _approvedUseSSL + ? _approvedPem + : null; + + set trustedCertificatePem(String? value) { + certificateRevision++; + _forgotten = false; + _approvedPem = value; + _approvedHost = host; + _approvedPort = port; + _approvedUseSSL = useSSL; + } + + /// Whether the form currently names [node]'s endpoint, the identity a + /// trusted certificate is bound to. + bool matchesEndpointOf(NodeModel node) => + host == node.host && port == node.port && useSSL == node.useSSL; + + @override + String toString() { + return "{ name: $name, host: $host, port: $port, useSSL: $useSSL, " + "trusted: $trusted, netOption: $netOption, isPrimary: $isPrimary }"; + } +} diff --git a/lib/models/node_model.dart b/lib/models/node_model.dart index 85c02c48a3..796bcd9b86 100644 --- a/lib/models/node_model.dart +++ b/lib/models/node_model.dart @@ -12,6 +12,7 @@ import 'package:hive_ce/hive.dart'; import '../utilities/default_nodes.dart'; import '../utilities/flutter_secure_storage_interface.dart'; +import '../utilities/tls_certificate.dart'; part 'type_adaptors/node_model.g.dart'; @@ -49,6 +50,8 @@ class NodeModel { final bool isPrimary; // @HiveField(15) final String? nodeApiSecret; + // @HiveField(16) + final String? trustedCertificatePem; NodeModel({ required this.host, @@ -67,6 +70,7 @@ class NodeModel { this.loginName, this.trusted, this.nodeApiSecret, + this.trustedCertificatePem, }); factory NodeModel.fromStackBackup( @@ -97,9 +101,25 @@ class NodeModel { fallback: legacyPrimaryNodeIds?.contains(id) ?? false, ), nodeApiSecret: map['nodeApiSecret'] as String?, + trustedCertificatePem: _backupCertificatePem( + map['trustedCertificatePem'], + ), ); } + /// A PEM that cannot be parsed would make every connection to this node + /// throw, so a damaged backup value is dropped. The node then uses the + /// platform roots until its certificate is reviewed again. + static String? _backupCertificatePem(Object? value) { + if (value is! String) return null; + try { + certificateDer(value); + return value; + } catch (_) { + return null; + } + } + static bool _backupBool(Object? value, {required bool fallback}) => _nullableBackupBool(value) ?? fallback; @@ -117,6 +137,14 @@ class NodeModel { return null; } + /// Whether [host], [port] and [useSSL] name this node's endpoint, the + /// identity a trusted certificate is bound to. + bool sameEndpoint({ + required String host, + required int port, + required bool useSSL, + }) => this.host == host && this.port == port && this.useSSL == useSSL; + NodeModel copyWith({ String? host, int? port, @@ -133,7 +161,14 @@ class NodeModel { bool? clearnetEnabled, bool? isPrimary, String? nodeApiSecret, + String? trustedCertificatePem, + bool clearTrustedCertificate = false, }) { + final endpointChanged = !sameEndpoint( + host: host ?? this.host, + port: port ?? this.port, + useSSL: useSSL ?? this.useSSL, + ); return NodeModel( host: host ?? this.host, port: port ?? this.port, @@ -151,6 +186,10 @@ class NodeModel { forceNoTor: forceNoTor ?? this.forceNoTor, isPrimary: isPrimary ?? this.isPrimary, nodeApiSecret: nodeApiSecret ?? this.nodeApiSecret, + trustedCertificatePem: clearTrustedCertificate + ? null + : trustedCertificatePem ?? + (endpointChanged ? null : this.trustedCertificatePem), ); } @@ -177,6 +216,7 @@ class NodeModel { map['forceNoTor'] = forceNoTor; map['isPrimary'] = isPrimary; map['nodeApiSecret'] = nodeApiSecret; + map['trustedCertificatePem'] = trustedCertificatePem; return map; } diff --git a/lib/models/type_adaptors/node_model.g.dart b/lib/models/type_adaptors/node_model.g.dart index 1ab826d553..54149c38e4 100644 --- a/lib/models/type_adaptors/node_model.g.dart +++ b/lib/models/type_adaptors/node_model.g.dart @@ -1,4 +1,4 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND +// Manually maintained Hive adapter. Append fields for backwards compatibility. part of '../node_model.dart'; @@ -33,13 +33,14 @@ class NodeModelAdapter extends TypeAdapter { forceNoTor: fields[13] as bool? ?? false, isPrimary: fields[14] as bool? ?? false, nodeApiSecret: fields[15] as String?, + trustedCertificatePem: fields[16] as String?, ); } @override void write(BinaryWriter writer, NodeModel obj) { writer - ..writeByte(16) + ..writeByte(17) ..writeByte(0) ..write(obj.id) ..writeByte(1) @@ -71,7 +72,9 @@ class NodeModelAdapter extends TypeAdapter { ..writeByte(14) ..write(obj.isPrimary) ..writeByte(15) - ..write(obj.nodeApiSecret); + ..write(obj.nodeApiSecret) + ..writeByte(16) + ..write(obj.trustedCertificatePem); } @override 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 c727146756..8f4827665b 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 @@ -16,24 +16,26 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:uuid/uuid.dart'; +import '../../../../models/node_form_data.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/barcode_scanner_interface.dart'; import '../../../../utilities/constants.dart'; -import '../../../../utilities/enums/sync_type_enum.dart'; import '../../../../utilities/flutter_secure_storage_interface.dart'; import '../../../../utilities/logger.dart'; +import '../../../../utilities/node_change_notifier.dart'; import '../../../../utilities/node_uri_util.dart'; import '../../../../utilities/test_node_connection.dart'; import '../../../../utilities/text_styles.dart'; +import '../../../../utilities/tls_certificate.dart'; import '../../../../utilities/tor_plain_net_option_enum.dart'; import '../../../../utilities/util.dart'; import '../../../../wallets/crypto_currency/crypto_currency.dart'; +import '../../../../wallets/crypto_currency/interfaces/electrumx_currency_interface.dart'; import '../../../../wallets/crypto_currency/intermediate/cryptonote_currency.dart'; import '../../../../widgets/background.dart'; import '../../../../widgets/conditional_parent.dart'; @@ -48,7 +50,7 @@ import '../../../../widgets/stack_dialog.dart'; import '../../../../widgets/stack_text_field.dart'; import '../../../../widgets/textfield_icon_button.dart'; -// import 'package:web3dart/web3dart.dart'; +export '../../../../models/node_form_data.dart'; enum AddEditNodeViewType { add, edit } @@ -83,28 +85,50 @@ class _AddEditNodeViewState extends ConsumerState { late bool saveEnabled; late bool testConnectionEnabled; + late final NodeFormData _form; + bool _busy = false; + void _onTestSuccess(NodeFormData data) { - if (coin is Epiccash) { - ref.read(nodeFormDataProvider).host = data.host; - ref.read(nodeFormDataProvider).port = data.port; - ref.read(nodeFormDataProvider).useSSL = data.useSSL; - } else if (coin is Mimblewimblecoin) { - ref.read(nodeFormDataProvider).host = data.host; - ref.read(nodeFormDataProvider).port = data.port; - ref.read(nodeFormDataProvider).useSSL = data.useSSL; - } else if (coin is CryptonoteCurrency) { - ref.read(nodeFormDataProvider).host = data.host; + if (mounted && identical(data, _form)) setState(() {}); + } + + VoidCallback? _whileIdle(Future Function() action) => + _busy ? null : () => _runBusy(action); + + Future _runBusy(Future Function() action) async { + if (_busy) return; + setState(() => _busy = true); + try { + await WidgetsBinding.instance.endOfFrame; + await _form.passwordLoad; + if (mounted) await action(); + } catch (error, stack) { + Logging.instance.w('$error', error: error, stackTrace: stack); + if (mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: 'Node operation failed', + context: context, + ), + ); + } + } finally { + if (mounted) setState(() => _busy = false); } } Future attemptSave() async { + final service = ref.read(nodeServiceChangeNotifierProvider); + final notify = nodeChangeNotifier(ref, coin); final canConnect = await ref.read(testNodeConnectionProvider)( context: context, onSuccess: _onTestSuccess, cryptoCurrency: coin, - nodeFormData: ref.read(nodeFormDataProvider), + nodeFormData: _form, ); + if (!mounted || ModalRoute.of(context)?.isCurrent == false) return; bool? shouldSave; if (!canConnect && mounted) { @@ -217,12 +241,12 @@ class _AddEditNodeViewState extends ConsumerState { }); } - if (!canConnect && !shouldSave!) { + if (!mounted || (!canConnect && shouldSave != true)) { // return without saving return; } - final formData = ref.read(nodeFormDataProvider); + final formData = _form; // strip unused path String address = formData.host!; @@ -255,6 +279,7 @@ class _AddEditNodeViewState extends ConsumerState { coinName: coin.identifier, isFailover: formData.isFailover!, trusted: formData.trusted!, + trustedCertificatePem: formData.trustedCertificatePem, isDown: false, torEnabled: torEnabled, clearnetEnabled: plainEnabled, @@ -263,10 +288,8 @@ class _AddEditNodeViewState extends ConsumerState { nodeApiSecret: formData.apiSecret, ); - await ref - .read(nodeServiceChangeNotifierProvider) - .save(node, formData.password, true); - await _notifyWalletsOfUpdatedNode(); + await service.save(node, formData.password, true); + await notify(); if (mounted) { Navigator.of(context) .popUntil(ModalRoute.withName(widget.routeOnSuccessOrDelete)); @@ -284,6 +307,7 @@ class _AddEditNodeViewState extends ConsumerState { coinName: coin.identifier, isFailover: formData.isFailover!, trusted: formData.trusted!, + trustedCertificatePem: formData.trustedCertificatePem, isDown: false, torEnabled: torEnabled, clearnetEnabled: plainEnabled, @@ -292,10 +316,16 @@ class _AddEditNodeViewState extends ConsumerState { nodeApiSecret: formData.apiSecret, ); - await ref - .read(nodeServiceChangeNotifierProvider) - .save(node, formData.password, true); - await _notifyWalletsOfUpdatedNode(); + if (!await service.saveEdit( + node, + formData.password, + true, + opened: formData.source!, + forgetCertificate: formData.certificateForgotten, + )) { + return; + } + await notify(); if (mounted) { Navigator.of(context) .popUntil(ModalRoute.withName(widget.routeOnSuccessOrDelete)); @@ -304,41 +334,6 @@ 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; - } - } - bool _scanLock = false; void _scanQr() async { @@ -433,7 +428,7 @@ class _AddEditNodeViewState extends ConsumerState { @override void initState() { isDesktop = Util.isDesktop; - ref.refresh(nodeFormDataProvider); + _form = ref.refresh(nodeFormDataProvider); viewType = widget.viewType; coin = widget.coin; @@ -517,7 +512,7 @@ class _AddEditNodeViewState extends ConsumerState { .extension()! .accentColorDark, ), - onPressed: _scanQr, + onPressed: _busy ? null : _scanQr, ), ), ), @@ -553,16 +548,20 @@ class _AddEditNodeViewState extends ConsumerState { width: 20, height: 20, ), - onPressed: () async { - Navigator.popUntil( - context, - ModalRoute.withName(widget.routeOnSuccessOrDelete), - ); - - await ref - .read(nodeServiceChangeNotifierProvider) - .delete(nodeId!, true); - }, + onPressed: _busy + ? null + : () async { + Navigator.popUntil( + context, + ModalRoute.withName( + widget.routeOnSuccessOrDelete, + ), + ); + + await ref + .read(nodeServiceChangeNotifierProvider) + .delete(nodeId!, true); + }, ), ), ), @@ -601,7 +600,7 @@ class _AddEditNodeViewState extends ConsumerState { maxWidth: 580, maxHeight: double.infinity, child: Column( - mainAxisSize: MainAxisSize.min, + mainAxisSize: .min, children: [ const SizedBox(height: 8), Row( @@ -630,7 +629,7 @@ class _AddEditNodeViewState extends ConsumerState { .extension()! .background, icon: const QrCodeIcon(width: 21, height: 21), - onPressed: _scanQr, + onPressed: _busy ? null : _scanQr, ), ), ], @@ -648,7 +647,7 @@ class _AddEditNodeViewState extends ConsumerState { ), ), child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + crossAxisAlignment: .stretch, children: [ NodeForm( key: Key((node ?? _scannedResult?.$1)?.id ?? "none"), @@ -656,6 +655,8 @@ class _AddEditNodeViewState extends ConsumerState { scannedPw: _scannedResult?.$2, secureStore: ref.read(secureStoreProvider), readOnly: false, + busy: _busy, + data: _form, coin: widget.coin, onChanged: (canSave, canTest) { if (canSave != saveEnabled && @@ -682,16 +683,16 @@ class _AddEditNodeViewState extends ConsumerState { Expanded( child: SecondaryButton( label: "Test connection", - enabled: testConnectionEnabled, + enabled: testConnectionEnabled && !_busy, buttonHeight: isDesktop ? ButtonHeight.l : null, - onPressed: testConnectionEnabled - ? () async { + onPressed: testConnectionEnabled && !_busy + ? _whileIdle(() async { final testPassed = await ref.read(testNodeConnectionProvider)( context: context, onSuccess: _onTestSuccess, cryptoCurrency: coin, - nodeFormData: ref.read(nodeFormDataProvider), + nodeFormData: _form, ); if (context.mounted) { if (testPassed) { @@ -712,7 +713,7 @@ class _AddEditNodeViewState extends ConsumerState { ); } } - } + }) : null, ), ), @@ -721,9 +722,11 @@ class _AddEditNodeViewState extends ConsumerState { Expanded( child: PrimaryButton( label: "Save", - enabled: saveEnabled, + enabled: saveEnabled && !_busy, buttonHeight: ButtonHeight.l, - onPressed: saveEnabled ? attemptSave : null, + onPressed: saveEnabled && !_busy + ? _whileIdle(attemptSave) + : null, ), ), ], @@ -738,7 +741,9 @@ class _AddEditNodeViewState extends ConsumerState { : Theme.of(context) .extension()! .getPrimaryDisabledButtonStyle(context), - onPressed: saveEnabled ? attemptSave : null, + onPressed: saveEnabled && !_busy + ? _whileIdle(attemptSave) + : null, child: Text("Save", style: STextStyles.button(context)), ), ], @@ -748,24 +753,14 @@ class _AddEditNodeViewState extends ConsumerState { } } -class NodeFormData { - String? name, host, login, password, apiSecret; - int? port; - bool? useSSL, isFailover, trusted, forceNoTor, isPrimary; - TorPlainNetworkOption? netOption; - - @override - String toString() { - return "{ name: $name, host: $host, port: $port, useSSL: $useSSL, trusted: $trusted, netOption: $netOption, isPrimary: $isPrimary }"; - } -} - final nodeFormDataProvider = Provider((_) => NodeFormData()); class NodeForm extends ConsumerStatefulWidget { const NodeForm({ super.key, this.node, + this.data, + this.busy = false, this.scannedPw, required this.secureStore, required this.readOnly, @@ -774,6 +769,8 @@ class NodeForm extends ConsumerStatefulWidget { }); final NodeModel? node; + final NodeFormData? data; + final bool busy; final String? scannedPw; final SecureStorageInterface secureStore; final bool readOnly; @@ -785,6 +782,36 @@ class NodeForm extends ConsumerStatefulWidget { } class _NodeFormState extends ConsumerState { + late final NodeFormData _data; + bool _changing = false; + bool _passwordEdited = false; + bool get _inputsReadOnly => widget.readOnly || widget.busy || _changing; + String? get _certificate => widget.readOnly + ? widget.node?.trustedCertificatePem + : _data.trustedCertificatePem; + + Future _changeNode(Future Function() action) async { + if (widget.busy || _changing) return; + final notify = nodeChangeNotifier(ref, widget.coin); + setState(() => _changing = true); + try { + if (await action()) await notify(); + } catch (error, stack) { + Logging.instance.w('$error', error: error, stackTrace: stack); + if (mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: 'Node operation failed', + context: context, + ), + ); + } + } finally { + if (mounted) setState(() => _changing = false); + } + } + late final TextEditingController _nameController; late final TextEditingController _hostController; late final TextEditingController _portController; @@ -834,41 +861,45 @@ class _NodeFormState extends ConsumerState { bool enableField(TextEditingController controller) { bool enable = true; - if (widget.readOnly) { + if (_inputsReadOnly) { enable = controller.text.isNotEmpty; } return enable; } bool get shouldBeReadOnly => - widget.readOnly || widget.node?.isDefault == true; + _inputsReadOnly || widget.node?.isDefault == true; void _updateState() { + if (widget.busy || _changing) return; port = int.tryParse(_portController.text); onChanged?.call(canSave, canTestConnection); - ref.read(nodeFormDataProvider).name = _nameController.text; - ref.read(nodeFormDataProvider).host = _hostController.text; - ref.read(nodeFormDataProvider).login = _usernameController.text.isEmpty + _data.name = _nameController.text; + _data.host = _hostController.text; + _data.login = _usernameController.text.isEmpty ? null : _usernameController.text; - ref.read(nodeFormDataProvider).password = _passwordController.text.isEmpty + _data.password = _passwordController.text.isEmpty ? null : _passwordController.text; - ref.read(nodeFormDataProvider).apiSecret = _apiSecretController.text.isEmpty + _data.apiSecret = _apiSecretController.text.isEmpty ? null : _apiSecretController.text; - ref.read(nodeFormDataProvider).port = port; - ref.read(nodeFormDataProvider).useSSL = _useSSL; - ref.read(nodeFormDataProvider).isFailover = _isFailover; - ref.read(nodeFormDataProvider).trusted = _trusted; - ref.read(nodeFormDataProvider).netOption = netOption; - ref.read(nodeFormDataProvider).forceNoTor = _forceNoTor; - ref.read(nodeFormDataProvider).host; - ref.read(nodeFormDataProvider).isPrimary = _isPrimary; + _data.port = port; + _data.useSSL = _useSSL; + _data.isFailover = _isFailover; + _data.trusted = _trusted; + _data.netOption = netOption; + _data.forceNoTor = _forceNoTor; + _data.host; + _data.isPrimary = _isPrimary; } @override void initState() { + _data = widget.data ?? ref.read(nodeFormDataProvider); + _data.source = widget.node; + _data.trustedCertificatePem = null; onChanged = widget.onChanged; _nameController = TextEditingController(); _hostController = TextEditingController(); @@ -883,11 +914,16 @@ class _NodeFormState extends ConsumerState { final node = widget.node!; if (enableAuthFields) { if (widget.scannedPw == null) { - node.getPassword(widget.secureStore).then((value) { - if (value is String) { - _passwordController.text = value; - } - }); + // Save and Test await this; a failed read must not block them. + _data.passwordLoad = node.getPassword(widget.secureStore).then(( + value, + ) { + if (!mounted || value is! String || _passwordEdited) return; + _passwordController.text = value; + _data.password = value; + // Tests and saves may run before any field is edited. + _updateState(); + }, onError: (Object _) {}); } else { _passwordController.text = widget.scannedPw!; } @@ -921,8 +957,11 @@ class _NodeFormState extends ConsumerState { } WidgetsBinding.instance.addPostFrameCallback((_) { - // update provider state object so test connection works without having to modify a field in the ui first + if (!mounted) return; + // Update the form before loading its endpoint-specific approval. _updateState(); + _data.trustedCertificatePem = node.trustedCertificatePem; + if (mounted) setState(() {}); }); } else { enableSSLCheckbox = true; @@ -1230,6 +1269,8 @@ class _NodeFormState extends ConsumerState { TextFieldIconButton( child: const XIcon(), onTap: () async { + if (widget.busy || _changing) return; + _passwordEdited = true; _passwordController.text = ""; _updateState(); }, @@ -1241,6 +1282,8 @@ class _NodeFormState extends ConsumerState { : null, ), onChanged: (newValue) { + if (widget.busy || _changing) return; + _passwordEdited = true; _updateState(); setState(() {}); }, @@ -1346,11 +1389,57 @@ class _NodeFormState extends ConsumerState { ), ], ), + if ((widget.coin is ElectrumXCurrencyInterface || + widget.coin is BitcoinFrost) && + _useSSL && + _certificate != null) + Column( + crossAxisAlignment: .start, + children: [ + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: SelectableText( + 'Trusted TLS certificate\n' + 'SHA-256:\n' + '${certificatePemFingerprint(_certificate!) ?? 'Unavailable'}', + style: STextStyles.itemSubtitle12(context), + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + const Spacer(), + SecondaryButton( + label: 'Forget certificate', + buttonHeight: .s, + padding: const .symmetric(horizontal: 10), + onPressed: widget.busy || _changing + ? null + : () async { + if (!widget.readOnly) { + setState(_data.forgetCertificate); + return; + } + await _changeNode( + () => ref + .read(nodeServiceChangeNotifierProvider) + .forgetTrustedCertificate(widget.node!), + ); + }, + ), + ], + ), + ], + ), if (widget.coin is CryptonoteCurrency) Row( children: [ GestureDetector( - onTap: !widget.readOnly /*&& trustedCheckbox*/ + onTap: !_inputsReadOnly ? () { setState(() { _trusted = !_trusted; @@ -1366,7 +1455,7 @@ class _NodeFormState extends ConsumerState { width: 20, height: 20, child: Checkbox( - fillColor: !widget.readOnly + fillColor: !_inputsReadOnly ? null : MaterialStateProperty.all( Theme.of(context) @@ -1376,7 +1465,7 @@ class _NodeFormState extends ConsumerState { materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, value: _trusted, - onChanged: !widget.readOnly + onChanged: !_inputsReadOnly ? (newValue) { setState(() { _trusted = newValue!; @@ -1407,26 +1496,24 @@ class _NodeFormState extends ConsumerState { Row( children: [ GestureDetector( - onTap: () { - setState(() { - _isFailover = !_isFailover; - }); - if (widget.readOnly) { - ref - .read(nodeServiceChangeNotifierProvider) - .save( - widget.node!.copyWith( - isFailover: _isFailover, - loginName: widget.node!.loginName, - trusted: widget.node!.trusted, - ), - null, - true, - ); - } else { - _updateState(); - } - }, + onTap: widget.busy || _changing + ? null + : () { + setState(() { + _isFailover = !_isFailover; + }); + if (widget.readOnly) { + unawaited( + _changeNode( + () => ref + .read(nodeServiceChangeNotifierProvider) + .setFailover(widget.node!.id, _isFailover), + ), + ); + } else { + _updateState(); + } + }, child: Container( color: Colors.transparent, child: Row( @@ -1438,26 +1525,29 @@ class _NodeFormState extends ConsumerState { materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, value: _isFailover, - onChanged: (newValue) { - setState(() { - _isFailover = newValue!; - }); - if (widget.readOnly) { - ref - .read(nodeServiceChangeNotifierProvider) - .save( - widget.node!.copyWith( - isFailover: _isFailover, - loginName: widget.node!.loginName, - trusted: widget.node!.trusted, - ), - null, - true, - ); - } else { - _updateState(); - } - }, + onChanged: widget.busy || _changing + ? null + : (newValue) { + setState(() { + _isFailover = newValue!; + }); + if (widget.readOnly) { + unawaited( + _changeNode( + () => ref + .read( + nodeServiceChangeNotifierProvider, + ) + .setFailover( + widget.node!.id, + _isFailover, + ), + ), + ); + } else { + _updateState(); + } + }, ), ), const SizedBox(width: 12), @@ -1477,11 +1567,11 @@ class _NodeFormState extends ConsumerState { children: [ RadioTextButton( label: "Only TOR traffic", - enabled: !widget.readOnly, + enabled: !_inputsReadOnly, value: TorPlainNetworkOption.tor, groupValue: netOption, onChanged: (value) { - if (!widget.readOnly) { + if (!_inputsReadOnly) { setState(() => netOption = TorPlainNetworkOption.tor); _updateState(); } @@ -1495,11 +1585,11 @@ class _NodeFormState extends ConsumerState { children: [ RadioTextButton( label: "Only non-TOR traffic", - enabled: !widget.readOnly, + enabled: !_inputsReadOnly, value: TorPlainNetworkOption.clear, groupValue: netOption, onChanged: (value) { - if (!widget.readOnly) { + if (!_inputsReadOnly) { setState(() => netOption = TorPlainNetworkOption.clear); _updateState(); } @@ -1513,11 +1603,11 @@ class _NodeFormState extends ConsumerState { children: [ RadioTextButton( label: "Allow both", - enabled: !widget.readOnly, + enabled: !_inputsReadOnly, value: TorPlainNetworkOption.both, groupValue: netOption, onChanged: (value) { - if (!widget.readOnly) { + if (!_inputsReadOnly) { setState(() => netOption = TorPlainNetworkOption.both); _updateState(); } @@ -1534,7 +1624,7 @@ class _NodeFormState extends ConsumerState { width: 20, height: 20, child: Checkbox( - fillColor: !widget.readOnly + fillColor: !_inputsReadOnly ? null : MaterialStateProperty.all( Theme.of(context) @@ -1543,7 +1633,7 @@ class _NodeFormState extends ConsumerState { ), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, value: _forceNoTor, - onChanged: !widget.readOnly + onChanged: !_inputsReadOnly ? (newValue) { setState(() { _forceNoTor = newValue!; @@ -1598,8 +1688,8 @@ class RadioTextButton extends StatelessWidget { child: Container( color: Colors.transparent, child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: .min, + crossAxisAlignment: .start, children: [ SizedBox( width: 20, 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 233ba0c6b9..c685a668d5 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 @@ -16,14 +16,14 @@ import 'package:flutter_svg/svg.dart'; import 'package:tuple/tuple.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/enums/sync_type_enum.dart'; import '../../../../utilities/flutter_secure_storage_interface.dart'; import '../../../../utilities/test_node_connection.dart'; +import '../../../../utilities/logger.dart'; +import '../../../../utilities/node_change_notifier.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/tor_plain_net_option_enum.dart'; import '../../../../utilities/util.dart'; @@ -62,44 +62,38 @@ class _NodeDetailsViewState extends ConsumerState { late final String popRouteName; bool _desktopReadOnly = true; + bool _busy = false; + late final NodeFormData _form; - Future _notifyWalletsOfUpdatedNode() async { - final wallets = ref - .read(pWallets) - .wallets - .where((e) => e.info.coin == widget.coin); - final prefs = ref.read(prefsChangeNotifierProvider); + VoidCallback? _whileIdle(Future Function() action) => + _busy ? null : () => _runBusy(action); - 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; + Future _runBusy(Future Function() action) async { + if (_busy) return; + setState(() => _busy = true); + try { + await WidgetsBinding.instance.endOfFrame; + await _form.passwordLoad; + if (mounted) await action(); + } catch (error, stack) { + Logging.instance.w('$error', error: error, stackTrace: stack); + if (mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: 'Node operation failed', + context: context, + ), + ); + } + } finally { + if (mounted) setState(() => _busy = false); } } @override initState() { + _form = ref.refresh(nodeFormDataProvider); secureStore = ref.read(secureStoreProvider); coin = widget.coin; nodeId = widget.nodeId; @@ -129,9 +123,9 @@ class _NodeDetailsViewState extends ConsumerState { condition: !isDesktop, builder: (child) => Background( child: Scaffold( - backgroundColor: Theme.of( - context, - ).extension()!.background, + backgroundColor: Theme.of(context) + .extension()! + .background, appBar: AppBar( leading: AppBarBackButton( onPressed: () async { @@ -158,28 +152,30 @@ class _NodeDetailsViewState extends ConsumerState { key: const Key("nodeDetailsEditNodeAppBarButtonKey"), size: 36, shadows: const [], - color: Theme.of( - context, - ).extension()!.background, + color: Theme.of(context) + .extension()! + .background, icon: SvgPicture.asset( Assets.svg.pencil, - color: Theme.of( - context, - ).extension()!.accentColorDark, + color: Theme.of(context) + .extension()! + .accentColorDark, width: 20, height: 20, ), - onPressed: () { - Navigator.of(context).pushNamed( - AddEditNodeView.routeName, - arguments: Tuple4( - AddEditNodeViewType.edit, - coin, - nodeId, - popRouteName, - ), - ); - }, + onPressed: _busy + ? null + : () { + Navigator.of(context).pushNamed( + AddEditNodeView.routeName, + arguments: Tuple4( + AddEditNodeViewType.edit, + coin, + nodeId, + popRouteName, + ), + ); + }, ), ), ), @@ -238,6 +234,10 @@ class _NodeDetailsViewState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ NodeForm( + // Reload the form only when the stored node changes. + key: ValueKey(node), + data: _form, + busy: _busy, node: node, secureStore: secureStore, readOnly: isDesktop ? _desktopReadOnly : true, @@ -256,13 +256,16 @@ class _NodeDetailsViewState extends ConsumerState { child: DeleteButton( label: "Delete node", desktopMed: true, - onPressed: () async { - Navigator.of(context).pop(); + onPressed: _busy + ? null + : () async { + final service = ref.read( + nodeServiceChangeNotifierProvider, + ); + Navigator.of(context).pop(); - await ref - .read(nodeServiceChangeNotifierProvider) - .delete(node!.id, true); - }, + await service.delete(node!.id, true); + }, ), ), const SizedBox(width: 16), @@ -277,36 +280,27 @@ class _NodeDetailsViewState extends ConsumerState { Expanded( child: SecondaryButton( label: "Test connection", + enabled: !_busy, buttonHeight: isDesktop ? ButtonHeight.l : null, - onPressed: () async { + onPressed: _whileIdle(() async { + final service = ref.read( + nodeServiceChangeNotifierProvider, + ); + final notify = nodeChangeNotifier(ref, coin); final node = ref .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.clearnetEnabled, + final editing = isDesktop && !_desktopReadOnly; + final nodeFormData = editing + ? _form + : NodeFormData.fromNode(node); + if (!editing) { + nodeFormData.password = await node.getPassword( + ref.read(secureStoreProvider), ); } - final nodeFormData = NodeFormData() - ..useSSL = node.useSSL - ..trusted = node.trusted - ..name = node.name - ..host = node.host - ..login = node.loginName - ..port = node.port - ..isFailover = node.isFailover - ..netOption = netOption - ..forceNoTor = node.forceNoTor; - nodeFormData.password = await node.getPassword( - ref.read(secureStoreProvider), - ); - if (context.mounted) { final testPassed = await ref.read(testNodeConnectionProvider)( @@ -316,6 +310,14 @@ class _NodeDetailsViewState extends ConsumerState { ); if (testPassed) { + if (!editing) { + final saved = await service.saveTrustedCertificate( + node, + nodeFormData, + ); + if (saved) await notify(); + } + if (mounted) setState(() {}); if (context.mounted) { unawaited( showFloatingFlushBar( @@ -337,62 +339,58 @@ class _NodeDetailsViewState extends ConsumerState { } } } - }, + }), ), ), if (isDesktop) const SizedBox(width: 16), if (isDesktop) Expanded( - child: - // !nodeId.startsWith(DefaultNodes.defaultNodeIdPrefix) - // ? - PrimaryButton( - label: _desktopReadOnly ? "Edit" : "Save", - buttonHeight: ButtonHeight.l, - onPressed: () async { - final shouldSave = _desktopReadOnly == false; - setState(() { - _desktopReadOnly = !_desktopReadOnly; - }); - - if (shouldSave) { - final editedNode = node!.copyWith( - host: ref.read(nodeFormDataProvider).host, - port: ref.read(nodeFormDataProvider).port, - name: ref.read(nodeFormDataProvider).name, - useSSL: ref.read(nodeFormDataProvider).useSSL, - trusted: ref.read(nodeFormDataProvider).trusted, - loginName: ref.read(nodeFormDataProvider).login, - isFailover: ref - .read(nodeFormDataProvider) - .isFailover, - torEnabled: - ref.read(nodeFormDataProvider).netOption == - TorPlainNetworkOption.tor || - ref.read(nodeFormDataProvider).netOption == - TorPlainNetworkOption.both, - clearnetEnabled: - ref.read(nodeFormDataProvider).netOption == - TorPlainNetworkOption.clear || - ref.read(nodeFormDataProvider).netOption == - TorPlainNetworkOption.both, - forceNoTor: ref - .read(nodeFormDataProvider) - .forceNoTor, - ); + child: PrimaryButton( + label: _desktopReadOnly ? "Edit" : "Save", + enabled: !_busy, + buttonHeight: ButtonHeight.l, + onPressed: _whileIdle(() async { + if (_desktopReadOnly) { + setState(() => _desktopReadOnly = false); + return; + } + final service = ref.read( + nodeServiceChangeNotifierProvider, + ); + final notify = nodeChangeNotifier(ref, coin); + final editedNode = node!.copyWith( + host: _form.host, + port: _form.port, + name: _form.name, + useSSL: _form.useSSL, + trusted: _form.trusted, + trustedCertificatePem: _form.trustedCertificatePem, + clearTrustedCertificate: + _form.trustedCertificatePem == null, + loginName: _form.login, + isFailover: _form.isFailover, + torEnabled: + _form.netOption == TorPlainNetworkOption.tor || + _form.netOption == TorPlainNetworkOption.both, + clearnetEnabled: + _form.netOption == TorPlainNetworkOption.clear || + _form.netOption == TorPlainNetworkOption.both, + forceNoTor: _form.forceNoTor, + ); - await ref - .read(nodeServiceChangeNotifierProvider) - .save( - editedNode, - ref.read(nodeFormDataProvider).password, - true, - ); - await _notifyWalletsOfUpdatedNode(); - } - }, - ), - // : Container() + if (!await service.saveEdit( + editedNode, + _form.password, + true, + opened: _form.source!, + forgetCertificate: _form.certificateForgotten, + )) { + return; + } + await notify(); + if (mounted) setState(() => _desktopReadOnly = true); + }), + ), ), ], ), diff --git a/lib/services/node_service.dart b/lib/services/node_service.dart index 6a83299119..24339807f5 100644 --- a/lib/services/node_service.dart +++ b/lib/services/node_service.dart @@ -16,6 +16,7 @@ import 'package:http/http.dart'; import '../app_config.dart'; import '../db/hive/db.dart'; import '../models/epicbox_server_model.dart'; +import '../models/node_form_data.dart'; import '../models/node_model.dart'; import '../utilities/default_epicboxes.dart'; import '../utilities/default_nodes.dart'; @@ -67,11 +68,7 @@ class NodeService extends ChangeNotifier { isPrimary: false, ); - await DB.instance.put( - boxName: DB.boxNameNodeModels, - key: _id, - value: node, - ); + await _writeNode(_id, (current) => current == null ? node : null); } } } @@ -79,38 +76,33 @@ class NodeService extends ChangeNotifier { for (final defaultNode in AppConfig.coins.map( (e) => e.defaultNode(isPrimary: true), )) { - final savedNode = DB.instance.get( - boxName: DB.boxNameNodeModels, - key: defaultNode.id, - ); - if (savedNode == null) { - // save the default node to hive only if no other nodes for the specific coin exist - if (getNodesFor( - AppConfig.getCryptoCurrencyByPrettyName(defaultNode.coinName), - ).isEmpty) { - await DB.instance.put( - boxName: DB.boxNameNodeModels, - key: defaultNode.id, - value: defaultNode, - ); + await _writeNode(defaultNode.id, (savedNode) { + if (savedNode == null) { + return getNodesFor( + AppConfig.getCryptoCurrencyByPrettyName(defaultNode.coinName), + ).isEmpty + ? defaultNode + : null; } - } else { - // update all fields but copy over previously set enabled and trusted states - await DB.instance.put( - boxName: DB.boxNameNodeModels, - key: savedNode.id, - value: defaultNode.copyWith( - enabled: savedNode.enabled, - isFailover: savedNode.isFailover, - trusted: savedNode.trusted, - torEnabled: savedNode.torEnabled, - clearnetEnabled: savedNode.clearnetEnabled, - loginName: savedNode.loginName, - isPrimary: savedNode.isPrimary, - forceNoTor: savedNode.forceNoTor, - ), + return defaultNode.copyWith( + enabled: savedNode.enabled, + isFailover: savedNode.isFailover, + trusted: savedNode.trusted, + torEnabled: savedNode.torEnabled, + clearnetEnabled: savedNode.clearnetEnabled, + loginName: savedNode.loginName, + isPrimary: savedNode.isPrimary, + forceNoTor: savedNode.forceNoTor, + trustedCertificatePem: + savedNode.sameEndpoint( + host: defaultNode.host, + port: defaultNode.port, + useSSL: defaultNode.useSSL, + ) + ? savedNode.trustedCertificatePem + : null, ); - } + }); } } @@ -119,40 +111,48 @@ class NodeService extends ChangeNotifier { required NodeModel node, bool shouldNotifyListeners = false, }) async { - // current - final currentPrimaries = primaryNodes.where( - (e) => e.coinName == coin.identifier && e.id != node.id, - ); - final List toStore = []; - for (final node in currentPrimaries) { - final updated = node.copyWith( - loginName: node.loginName, - trusted: node.trusted, - isPrimary: false, + await DB.instance.mutex.protect(() async { + await _promote(getNodeById(id: node.id) ?? node); + }); + if (shouldNotifyListeners) notifyListeners(); + } + + // The caller holds DB's write lock; only primary flags change. + Future _promote(NodeModel node) async { + final box = DB.instance.hive.box(DB.boxNameNodeModels); + for (final other in primaryNodes.where( + (other) => other.coinName == node.coinName && other.id != node.id, + )) { + await box.put( + other.id, + other.copyWith( + loginName: other.loginName, + trusted: other.trusted, + isPrimary: false, + ), ); - toStore.add(updated); } - toStore.add( + await box.put( + node.id, node.copyWith( loginName: node.loginName, trusted: node.trusted, isPrimary: true, ), ); - - for (final node in toStore) { - await DB.instance.put( - boxName: DB.boxNameNodeModels, - key: node.id, - value: node, - ); - } - - if (shouldNotifyListeners) { - notifyListeners(); - } } + // Normal Hive boxes cache immutable NodeModels. Identity detects any write, + // including Forget of an empty approval, without another persisted revision. + Future _writeNode(String id, NodeModel? Function(NodeModel?) next) => + DB.instance.mutex.protect(() async { + final box = DB.instance.hive.box(DB.boxNameNodeModels); + final node = next(box.get(id)); + if (node == null) return false; + await box.put(id, node); + return true; + }); + NodeModel? getPrimaryNodeFor({required CryptoCurrency currency}) { return primaryNodes .where((e) => e.coinName == currency.identifier) @@ -198,9 +198,9 @@ class NodeService extends ChangeNotifier { } List failoverNodesFor({required CryptoCurrency currency}) { - return getNodesFor( - currency, - ).where((e) => e.isFailover && !e.isDown).toList(); + return getNodesFor(currency) + .where((e) => e.isFailover && !e.isDown) + .toList(); } /// Adds a new node to the database. @@ -218,20 +218,145 @@ class NodeService extends ChangeNotifier { value: node, ); - // Save password to secure storage. - if (password != null) { - await secureStorageInterface.write( - key: "${node.id}_nodePW", - value: password, - ); - } else { - await secureStorageInterface.delete(key: "${node.id}_nodePW"); - } + await _savePassword(node.id, password); if (shouldNotifyListeners) { notifyListeners(); } } + Future _savePassword(String id, String? password) async { + if (password == null) { + await secureStorageInterface.delete(key: "${id}_nodePW"); + } else { + await secureStorageInterface.write(key: "${id}_nodePW", value: password); + } + } + + Future saveEdit( + NodeModel node, + String? password, + bool notify, { + required NodeModel opened, + bool forgetCertificate = false, + }) async { + if (getNodeById(id: node.id) == null) return false; + await _savePassword(node.id, password); + final saved = await _writeNode(node.id, (current) { + if (current == null) return null; + final pem = identical(current, opened) || forgetCertificate + ? node.trustedCertificatePem + : node.sameEndpoint( + host: current.host, + port: current.port, + useSSL: current.useSSL, + ) + ? current.trustedCertificatePem + : null; + return node.copyWith( + loginName: node.loginName, + trusted: node.trusted, + isPrimary: current.isPrimary, + trustedCertificatePem: pem, + clearTrustedCertificate: pem == null, + ); + }); + if (saved && notify) notifyListeners(); + return saved; + } + + bool prepareCertificateTest(NodeFormData data) { + final source = data.source; + if (source == null) return true; + final current = getNodeById(id: source.id); + if (current == null) return false; + if (!identical(source, current)) { + if (!data.certificateForgotten) { + data.trustedCertificatePem = data.matchesEndpointOf(current) + ? current.trustedCertificatePem + : null; + } + data.source = current; + } + return true; + } + + Future saveTrustedCertificate(NodeModel node, NodeFormData data) async { + final pem = data.trustedCertificatePem; + final saved = await _writeNode( + node.id, + (current) => + identical(current, node) && + pem != null && + data.matchesEndpointOf(current!) && + current.trustedCertificatePem != pem + ? current.copyWith( + loginName: current.loginName, + trusted: current.trusted, + trustedCertificatePem: pem, + ) + : null, + ); + if (saved) notifyListeners(); + return saved; + } + + Future forgetTrustedCertificate(NodeModel shown) async { + final saved = await _writeNode( + shown.id, + (current) => + current != null && + shown.sameEndpoint( + host: current.host, + port: current.port, + useSSL: current.useSSL, + ) + ? current.copyWith( + loginName: current.loginName, + trusted: current.trusted, + clearTrustedCertificate: true, + ) + : null, + ); + if (saved) notifyListeners(); + return saved; + } + + Future commitConnectionTest({ + required NodeModel node, + required NodeFormData formData, + }) async { + final saved = await DB.instance.mutex.protect(() async { + if (!identical(getNodeById(id: node.id), node)) return false; + final pem = formData.matchesEndpointOf(node) + ? formData.trustedCertificatePem + : node.trustedCertificatePem; + await _promote( + node.copyWith( + loginName: node.loginName, + trusted: node.trusted, + trustedCertificatePem: pem, + clearTrustedCertificate: pem == null, + ), + ); + return true; + }); + if (saved) notifyListeners(); + return saved; + } + + Future setFailover(String id, bool value) async { + final saved = await _writeNode( + id, + (node) => node?.copyWith( + loginName: node.loginName, + trusted: node.trusted, + isFailover: value, + ), + ); + if (saved) notifyListeners(); + return saved; + } + Future delete(String id, bool shouldNotifyListeners) async { final nodeToDelete = getNodeById(id: id); @@ -271,14 +396,9 @@ class NodeService extends ChangeNotifier { bool enabled, bool shouldNotifyListeners, ) async { - final model = DB.instance.get( - boxName: DB.boxNameNodeModels, - key: id, - )!; - await DB.instance.put( - boxName: DB.boxNameNodeModels, - key: model.id, - value: model.copyWith( + await _writeNode( + id, + (model) => model?.copyWith( enabled: enabled, loginName: model.loginName, trusted: model.trusted, diff --git a/lib/services/notifications_service.dart b/lib/services/notifications_service.dart index fa7575d314..411d7a532f 100644 --- a/lib/services/notifications_service.dart +++ b/lib/services/notifications_service.dart @@ -156,11 +156,12 @@ class NotificationsService extends ChangeNotifier { name: node.name, id: node.id, useSSL: node.useSSL, + trustedCertificatePem: node.trustedCertificatePem, torEnabled: node.torEnabled, clearnetEnabled: node.clearnetEnabled, ); - final failovers = nodeService - .failoverNodesFor(currency: coin) + final fallbackNodes = nodeService.failoverNodesFor(currency: coin); + final failovers = fallbackNodes .map( (e) => ElectrumXNode( address: e.host, @@ -168,6 +169,7 @@ class NotificationsService extends ChangeNotifier { name: e.name, id: e.id, useSSL: e.useSSL, + trustedCertificatePem: e.trustedCertificatePem, torEnabled: node.torEnabled, clearnetEnabled: node.clearnetEnabled, ), @@ -180,7 +182,28 @@ class NotificationsService extends ChangeNotifier { prefs: prefs, cryptoCurrency: wallet.cryptoCurrency, ); - final tx = await client.getTransaction(txHash: txid); + // This client is not owned by a loaded wallet. Retire its pending + // setup too when an approval or node changes. + void retire() { + if (!identical( + nodeService.getPrimaryNodeFor(currency: coin), + node, + ) || + !listEquals( + nodeService.failoverNodesFor(currency: coin), + fallbackNodes, + )) { + unawaited(client.closeAdapter()); + } + } + + nodeService.addListener(retire); + late final Map tx; + try { + tx = await client.getTransaction(txHash: txid); + } finally { + nodeService.removeListener(retire); + } int confirmations = tx["confirmations"] as int? ?? 0; diff --git a/lib/utilities/connection_check/electrum_connection_check.dart b/lib/utilities/connection_check/electrum_connection_check.dart index 8d845aa01c..e25a461b6e 100644 --- a/lib/utilities/connection_check/electrum_connection_check.dart +++ b/lib/utilities/connection_check/electrum_connection_check.dart @@ -1,19 +1,81 @@ +import 'dart:async'; import 'dart:io'; import 'package:electrum_adapter/electrum_adapter.dart'; import '../../app_config.dart'; +import '../../models/node_form_data.dart'; import '../../services/event_bus/events/global/tor_connection_status_changed_event.dart'; import '../../services/tor_service.dart'; import '../logger.dart'; import '../prefs.dart'; +import '../tls_certificate.dart'; -Future checkElectrumServer({ +/// The outcome of one connection attempt. [certificate] is the server's TLS +/// certificate when the attempt failed because that certificate was rejected. +class ElectrumConnectionResult { + const ElectrumConnectionResult({ + required this.success, + this.certificate, + this.error, + }); + + final bool success; + final X509Certificate? certificate; + final Object? error; +} + +/// Connects, sends server.version and disconnects. A certificate the +/// platform rejects is accepted only when it is the approved one; any other +/// is returned for review without being accepted. +Future testElectrumConnection({ + required String host, + required int port, + required bool useSSL, + required Duration timeout, + String? trustedCertificatePem, + ({InternetAddress host, int port})? proxyInfo, +}) async { + ElectrumClient? client; + X509Certificate? rejected; + try { + final trusted = useSSL + ? trustedCertificateCheck(trustedCertificatePem) + : null; + client = await ElectrumClient.connect( + host: host, + port: port, + useSSL: useSSL, + proxyInfo: proxyInfo, + connectionTimeout: timeout, + acceptUnverified: false, + onBadCertificate: (certificate) { + if (trusted != null && trusted(certificate)) return true; + rejected = certificate; + return false; + }, + ).timeout(timeout); + await client.request('server.version').timeout(timeout); + return const ElectrumConnectionResult(success: true); + } catch (error) { + return ElectrumConnectionResult( + success: false, + certificate: rejected, + error: error, + ); + } finally { + await client?.close(); + } +} + +/// Applies the Tor preference, then runs [testElectrumConnection]. +Future checkElectrumServer({ required String host, required int port, required bool useSSL, Prefs? overridePrefs, TorService? overrideTorService, + String? trustedCertificatePem, }) async { final _prefs = overridePrefs ?? Prefs.instance; @@ -43,27 +105,75 @@ Future checkElectrumServer({ } } - final client = - await ElectrumClient.connect( - host: host, - port: port, - useSSL: useSSL && !host.endsWith('.onion'), - proxyInfo: proxyInfo, - acceptUnverified: false, - ).timeout( - Duration(seconds: (proxyInfo == null ? 5 : 30)), - onTimeout: () => throw Exception( - "The checkElectrumServer connect() call timed out.", - ), - ); - - await client - .request('server.version') - .timeout(Duration(seconds: (proxyInfo == null ? 5 : 30))); - - return true; + return await testElectrumConnection( + host: host, + port: port, + useSSL: useSSL, + trustedCertificatePem: trustedCertificatePem, + proxyInfo: proxyInfo, + timeout: Duration(seconds: proxyInfo == null ? 5 : 30), + ); } catch (e, s) { Logging.instance.e("$e\n$s", error: e, stackTrace: s); - return false; + return ElectrumConnectionResult(success: false, error: e); + } +} + +typedef ElectrumNodeTester = Future Function({ + required String host, + required int port, + required bool useSSL, + String? trustedCertificatePem, +}); + +/// Tests the node in [data]. A rejected certificate is offered for review +/// through [requestTrust]; after approval the test is retried with it, and +/// only a successful retry keeps the approval in the form. The UI supplies +/// the approval dialog. +Future testElectrumNodeConnection( + NodeFormData data, { + required ElectrumNodeTester testConnection, + required Future Function(X509Certificate, String, int) requestTrust, + required bool Function() isActive, + void Function(NodeFormData)? onSuccess, +}) async { + final host = data.host!; + final port = data.port!; + final useSSL = data.useSSL!; + final netOption = data.netOption; + final certificatePem = data.trustedCertificatePem; + final decision = data.certificateRevision; + bool unchanged() => + isActive() && + data.host == host && + data.port == port && + data.useSSL == useSSL && + data.netOption == netOption && + data.certificateRevision == decision && + data.trustedCertificatePem == certificatePem; + + var response = await testConnection( + host: host, + port: port, + useSSL: useSSL, + trustedCertificatePem: certificatePem, + ); + if (!unchanged()) return false; + final certificate = response.certificate; + if (certificate != null) { + final shouldTrust = await requestTrust(certificate, host, port); + if (!shouldTrust || !unchanged()) return false; + response = await testConnection( + host: host, + port: port, + useSSL: useSSL, + trustedCertificatePem: certificate.pem, + ); + if (!unchanged()) return false; + if (response.success) { + data.trustedCertificatePem = certificate.pem; + } } + if (response.success) onSuccess?.call(data); + return response.success; } diff --git a/lib/utilities/node_change_notifier.dart b/lib/utilities/node_change_notifier.dart new file mode 100644 index 0000000000..041320d6f3 --- /dev/null +++ b/lib/utilities/node_change_notifier.dart @@ -0,0 +1,36 @@ +import 'dart:async'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../electrumx_rpc/client_manager.dart'; +import '../providers/global/active_wallet_provider.dart'; +import '../providers/providers.dart'; +import '../wallets/crypto_currency/crypto_currency.dart'; +import 'enums/sync_type_enum.dart'; + +/// Captures providers while the view is alive, then notifies the wallets +/// still loaded when the returned callback runs. +Future Function() nodeChangeNotifier(WidgetRef ref, CryptoCurrency coin) { + final wallets = ref.read(pWallets); + final prefs = ref.read(prefsChangeNotifierProvider); + final activeWallet = ref.read(currentWalletIdProvider.notifier); + + return () async { + // Retire the coin's previous connection even when its wallet was removed + // and none of the loaded wallets ever used that connection. + final (removed, _) = await ClientManager.sharedInstance.remove( + cryptoCurrency: coin, + ); + await removed?.close(); + for (final wallet in wallets.wallets.where((e) => e.info.coin == coin)) { + final shouldRefresh = switch (prefs.syncType) { + SyncingType.currentWalletOnly => activeWallet.state == wallet.walletId, + SyncingType.selectedWalletsAtStartup => + prefs.walletIdsSyncOnStartup.contains(wallet.walletId), + SyncingType.allWalletsOnStartup => true, + }; + final update = wallet.updateNode(); + unawaited(shouldRefresh ? update.then((_) => wallet.refresh()) : update); + } + }; +} diff --git a/lib/utilities/test_monero_node_connection.dart b/lib/utilities/test_monero_node_connection.dart index 6a4fbfa455..8f544d3f71 100644 --- a/lib/utilities/test_monero_node_connection.dart +++ b/lib/utilities/test_monero_node_connection.dart @@ -30,12 +30,7 @@ class MoneroNodeConnectionResponse { final int? port; final bool success; - MoneroNodeConnectionResponse( - this.cert, - this.url, - this.port, - this.success, - ); + MoneroNodeConnectionResponse(this.cert, this.url, this.port, this.success); } Future testMoneroNodeConnection( @@ -43,10 +38,7 @@ Future testMoneroNodeConnection( String? username, String? password, bool allowBadX509Certificate, { - required ({ - InternetAddress host, - int port, - })? proxyInfo, + required ({InternetAddress host, int port})? proxyInfo, }) async { if (uri.host.endsWith(".onion")) { if (proxyInfo == null) { @@ -80,8 +72,9 @@ Future testMoneroNodeConnection( // node asking us for authentication, but we don't have any crendentials. return MoneroNodeConnectionResponse(null, null, null, false); } - authenticateHeaderValue = - line.replaceFirst('WWW-authenticate: ', '').trim(); + authenticateHeaderValue = line + .replaceFirst('WWW-authenticate: ', '') + .trim(); } } // header to authenticate was present, we need to remake the request with digest @@ -91,8 +84,12 @@ Future testMoneroNodeConnection( // generate the Authorization header for the second request. final authHeader = digestAuth.getAuthString('POST', uri.path); - final rawRequestAuthenticated = - DaemonRpc.rawRequestRpc(uri, 'get_info', {}, authHeader); + final rawRequestAuthenticated = DaemonRpc.rawRequestRpc( + uri, + 'get_info', + {}, + authHeader, + ); // resend with an authenticated request response = await socket.send(rawRequestAuthenticated); } @@ -103,7 +100,7 @@ Future testMoneroNodeConnection( return MoneroNodeConnectionResponse(null, null, null, success); } catch (e, s) { - Logging.instance.w("$e\n$s", error: e, stackTrace: s,); + Logging.instance.w("$e\n$s", error: e, stackTrace: s); return MoneroNodeConnectionResponse(null, null, null, false); } finally { await socket?.close(); @@ -114,10 +111,7 @@ Future testMoneroNodeConnection( try { if (proxyInfo != null) { SocksTCPClient.assignToHttpClient(httpClient, [ - ProxySettings( - proxyInfo.host, - proxyInfo.port, - ), + ProxySettings(proxyInfo.host, proxyInfo.port), ]); } @@ -127,8 +121,12 @@ Future testMoneroNodeConnection( } if (badCertResponse == null) { - badCertResponse = - MoneroNodeConnectionResponse(cert, url, port, false); + badCertResponse = MoneroNodeConnectionResponse( + cert, + url, + port, + false, + ); } else { return false; } @@ -150,7 +148,7 @@ Future testMoneroNodeConnection( if (badCertResponse != null) { return badCertResponse!; } else { - Logging.instance.w("$e\n$s", error: e, stackTrace: s,); + Logging.instance.w("$e\n$s", error: e, stackTrace: s); return MoneroNodeConnectionResponse(null, null, null, false); } } finally { @@ -204,7 +202,7 @@ Future showBadX509CertificateDialog( extension on SOCKSSocket { /// write the raw request to the socket and return the response as String Future send(String rawRequest) async { - write(rawRequest); + await write(rawRequest); final buffer = StringBuffer(); await for (final response in inputStream) { buffer.write(utf8.decode(response)); diff --git a/lib/utilities/test_node_connection.dart b/lib/utilities/test_node_connection.dart index 0b3504ce21..0e74a1c052 100644 --- a/lib/utilities/test_node_connection.dart +++ b/lib/utilities/test_node_connection.dart @@ -11,6 +11,7 @@ import '../app_config.dart'; import '../networking/http.dart'; import '../pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart'; import '../providers/global/prefs_provider.dart'; +import '../providers/global/node_service_provider.dart'; import '../services/tor_service.dart'; import '../wallets/api/cardano/blockfrost_http_provider.dart'; import '../wallets/api/tezos/tezos_rpc_api.dart'; @@ -20,6 +21,7 @@ import '../wallets/crypto_currency/intermediate/cryptonote_currency.dart'; import '../wallets/crypto_currency/intermediate/nano_currency.dart'; import '../wallets/wallet/impl/solana_wallet.dart'; import '../wl_gen/interfaces/lib_xelis_interface.dart'; +import '../widgets/tls_certificate_dialog.dart' as electrum_tls; import 'connection_check/electrum_connection_check.dart'; import 'logger.dart'; import 'test_epic_box_connection.dart'; @@ -220,12 +222,40 @@ Future testNodeConnection({ case ElectrumXCurrencyInterface(): case BitcoinFrost(): try { - testPassed = await checkElectrumServer( - host: formData.host!, - port: formData.port!, - useSSL: formData.useSSL!, - overridePrefs: read(prefsChangeNotifierProvider), - overrideTorService: read(pTorService), + if (!read(nodeServiceChangeNotifierProvider) + .prepareCertificateTest(formData)) { + return false; + } + final prefs = read(prefsChangeNotifierProvider); + final torService = read(pTorService); + final useTor = prefs.useTor; + final torStatus = AppConfig.hasFeature(AppFeature.tor) && useTor + ? torService.status + : null; + testPassed = await testElectrumNodeConnection( + formData, + isActive: () => + context.mounted && + ModalRoute.of(context)?.isCurrent != false && + prefs.useTor == useTor && + (torStatus == null || torService.status == torStatus), + onSuccess: onSuccess, + requestTrust: (certificate, host, port) => electrum_tls + .showBadX509CertificateDialog(certificate, host, port, context), + testConnection: + ({ + required host, + required port, + required useSSL, + trustedCertificatePem, + }) => checkElectrumServer( + host: host, + port: port, + useSSL: useSSL, + trustedCertificatePem: trustedCertificatePem, + overridePrefs: prefs, + overrideTorService: torService, + ), ); } catch (_) { testPassed = false; diff --git a/lib/utilities/tls_certificate.dart b/lib/utilities/tls_certificate.dart new file mode 100644 index 0000000000..cfdd4a4ab6 --- /dev/null +++ b/lib/utilities/tls_certificate.dart @@ -0,0 +1,42 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:crypto/crypto.dart'; + +Uint8List certificateDer(String pem) { + final match = RegExp( + r'^\s*-----BEGIN CERTIFICATE-----\s*([A-Za-z0-9+/=\s]+)-----END CERTIFICATE-----\s*$', + ).firstMatch(pem); + if (match == null || match.end != pem.length) { + throw const FormatException('Expected one PEM certificate'); + } + return base64Decode(match.group(1)!.replaceAll(RegExp(r'\s'), '')); +} + +String certificateFingerprint(List der) { + final hex = sha256.convert(der).toString().toUpperCase(); + return [for (int i = 0; i < hex.length; i += 2) hex.substring(i, i + 2)] + .join(':'); +} + +String? certificatePemFingerprint(String pem) { + try { + return certificateFingerprint(certificateDer(pem)); + } on FormatException { + return null; + } +} + +/// The check a TLS connection runs when the platform rejects the server's +/// certificate. It accepts exactly the certificate approved for the node, +/// whatever its hostname or validity, because the approval pins that +/// certificate. Null when nothing is approved, so the platform's verdict +/// stands. Throws a [FormatException] for a damaged PEM before connecting. +bool Function(X509Certificate)? trustedCertificateCheck( + String? certificatePem, +) { + if (certificatePem == null) return null; + final trusted = certificateFingerprint(certificateDer(certificatePem)); + return (certificate) => certificateFingerprint(certificate.der) == trusted; +} diff --git a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart index b499b82886..1430b3d696 100644 --- a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart @@ -1292,6 +1292,7 @@ class BitcoinFrostWallet extends Wallet port: node.port, name: node.name, useSSL: node.useSSL, + trustedCertificatePem: node.trustedCertificatePem, id: node.id, torEnabled: node.torEnabled, clearnetEnabled: node.clearnetEnabled, @@ -1309,6 +1310,7 @@ class BitcoinFrostWallet extends Wallet name: e.name, id: e.id, useSSL: e.useSSL, + trustedCertificatePem: e.trustedCertificatePem, torEnabled: e.torEnabled, clearnetEnabled: e.clearnetEnabled, ), diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index f2d5eb05b9..c92948acc2 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -1093,6 +1093,7 @@ mixin ElectrumXInterface port: node.port, name: node.name, useSSL: node.useSSL, + trustedCertificatePem: node.trustedCertificatePem, id: node.id, torEnabled: node.torEnabled, clearnetEnabled: node.clearnetEnabled, @@ -1109,6 +1110,7 @@ mixin ElectrumXInterface name: e.name, id: e.id, useSSL: e.useSSL, + trustedCertificatePem: e.trustedCertificatePem, torEnabled: e.torEnabled, clearnetEnabled: e.clearnetEnabled, ), diff --git a/lib/widgets/node_card.dart b/lib/widgets/node_card.dart index 7d7a829698..35c31c3c96 100644 --- a/lib/widgets/node_card.dart +++ b/lib/widgets/node_card.dart @@ -15,20 +15,18 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:tuple/tuple.dart'; +import '../models/node_form_data.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'; -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/default_nodes.dart'; -import '../utilities/enums/sync_type_enum.dart'; import '../utilities/test_node_connection.dart'; +import '../utilities/node_change_notifier.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'; @@ -58,41 +56,6 @@ class _NodeCardState extends ConsumerState { late final String nodeId; bool _advancedIsExpanded = false; - Future _notifyWalletsOfUpdatedNode(WidgetRef ref) 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() { nodeId = widget.nodeId; @@ -167,24 +130,13 @@ class _NodeCardState extends ConsumerState { text: "Connect", enabled: _status == "Disconnected", onTap: () async { - final TorPlainNetworkOption netOption; - if (_node.torEnabled && !_node.clearnetEnabled) { - netOption = TorPlainNetworkOption.tor; - } else if (_node.clearnetEnabled && !_node.torEnabled) { - netOption = TorPlainNetworkOption.clear; - } else { - netOption = TorPlainNetworkOption.both; - } - - final nodeFormData = NodeFormData() - ..useSSL = _node.useSSL - ..trusted = _node.trusted - ..name = _node.name - ..host = _node.host - ..login = _node.loginName - ..port = _node.port - ..netOption = netOption - ..isFailover = _node.isFailover; + // Carry the saved certificate so an approved node is + // not reviewed again, and keep a new approval below. + final service = ref.read( + nodeServiceChangeNotifierProvider, + ); + final notify = nodeChangeNotifier(ref, widget.coin); + final nodeFormData = NodeFormData.fromNode(_node); nodeFormData.password = await _node.getPassword( ref.read(secureStoreProvider), ); @@ -211,15 +163,12 @@ class _NodeCardState extends ConsumerState { return; } - await ref - .read(nodeServiceChangeNotifierProvider) - .setPrimaryNodeFor( - coin: widget.coin, - node: _node, - shouldNotifyListeners: true, - ); - - await _notifyWalletsOfUpdatedNode(ref); + if (await service.commitConnectionTest( + node: _node, + formData: nodeFormData, + )) { + await notify(); + } } }, ), @@ -251,9 +200,9 @@ class _NodeCardState extends ConsumerState { height: isDesktop ? 40 : 24, decoration: BoxDecoration( color: _node.id.startsWith(DefaultNodes.defaultNodeIdPrefix) - ? Theme.of( - context, - ).extension()!.buttonBackSecondary + ? Theme.of(context) + .extension()! + .buttonBackSecondary : Theme.of(context) .extension()! .infoItemIcons @@ -267,12 +216,12 @@ class _NodeCardState extends ConsumerState { width: isDesktop ? 20 : 14, color: _node.id.startsWith(DefaultNodes.defaultNodeIdPrefix) - ? Theme.of( - context, - ).extension()!.accentColorDark - : Theme.of( - context, - ).extension()!.infoItemIcons, + ? Theme.of(context) + .extension()! + .accentColorDark + : Theme.of(context) + .extension()! + .infoItemIcons, ), ), ), @@ -290,12 +239,12 @@ class _NodeCardState extends ConsumerState { SvgPicture.asset( Assets.svg.network, color: _status == "Connected" - ? Theme.of( - context, - ).extension()!.accentColorGreen - : Theme.of( - context, - ).extension()!.buttonBackSecondary, + ? Theme.of(context) + .extension()! + .accentColorGreen + : Theme.of(context) + .extension()! + .buttonBackSecondary, width: 20, height: 20, ), @@ -306,9 +255,9 @@ class _NodeCardState extends ConsumerState { : Assets.svg.chevronDown, width: 12, height: 6, - color: Theme.of( - context, - ).extension()!.textSubtitle1, + color: Theme.of(context) + .extension()! + .textSubtitle1, ), ], ), diff --git a/lib/widgets/node_options_sheet.dart b/lib/widgets/node_options_sheet.dart index 201b802cbe..45500becf8 100644 --- a/lib/widgets/node_options_sheet.dart +++ b/lib/widgets/node_options_sheet.dart @@ -8,26 +8,22 @@ * */ -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 '../pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart'; +import '../models/node_form_data.dart'; import '../pages/settings_views/global_settings_view/manage_nodes_views/node_details_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/constants.dart'; import '../utilities/default_nodes.dart'; -import '../utilities/enums/sync_type_enum.dart'; import '../utilities/test_node_connection.dart'; +import '../utilities/node_change_notifier.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'; @@ -43,41 +39,6 @@ class NodeOptionsSheet extends ConsumerWidget { final CryptoCurrency coin; final String popBackToRoute; - Future _notifyWalletsOfUpdatedNode(WidgetRef ref) async { - final wallets = ref - .read(pWallets) - .wallets - .where((e) => e.info.coin == 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 Widget build(BuildContext context, WidgetRef ref) { final maxHeight = MediaQuery.of(context).size.height * 0.60; @@ -121,9 +82,9 @@ class NodeOptionsSheet extends ConsumerWidget { Center( child: Container( decoration: BoxDecoration( - color: Theme.of( - context, - ).extension()!.textFieldDefaultBG, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -150,9 +111,9 @@ class NodeOptionsSheet extends ConsumerWidget { node.id.startsWith( DefaultNodes.defaultNodeIdPrefix, ) - ? Theme.of( - context, - ).extension()!.textSubtitle4 + ? Theme.of(context) + .extension()! + .textSubtitle4 : Theme.of(context) .extension()! .infoItemIcons @@ -168,12 +129,12 @@ class NodeOptionsSheet extends ConsumerWidget { node.id.startsWith( DefaultNodes.defaultNodeIdPrefix, ) - ? Theme.of( - context, - ).extension()!.accentColorDark - : Theme.of( - context, - ).extension()!.infoItemIcons, + ? Theme.of(context) + .extension()! + .accentColorDark + : Theme.of(context) + .extension()! + .infoItemIcons, ), ), ), @@ -193,12 +154,12 @@ class NodeOptionsSheet extends ConsumerWidget { SvgPicture.asset( Assets.svg.network, color: status == "Connected" - ? Theme.of( - context, - ).extension()!.accentColorGreen - : Theme.of( - context, - ).extension()!.buttonBackSecondary, + ? Theme.of(context) + .extension()! + .accentColorGreen + : Theme.of(context) + .extension()! + .buttonBackSecondary, width: 18, ), ], @@ -222,9 +183,9 @@ class NodeOptionsSheet extends ConsumerWidget { child: Text( "Details", style: STextStyles.button(context).copyWith( - color: Theme.of( - context, - ).extension()!.accentColorDark, + color: Theme.of(context) + .extension()! + .accentColorDark, ), ), ), @@ -243,50 +204,38 @@ class NodeOptionsSheet extends ConsumerWidget { onPressed: status == "Connected" ? null : () async { - final pw = await node.getPassword( + // Carry the saved certificate so an approved + // node is not reviewed again, and keep a new + // approval below. + final service = ref.read( + nodeServiceChangeNotifierProvider, + ); + final notify = nodeChangeNotifier(ref, coin); + final nodeFormData = NodeFormData.fromNode( + node, + ); + nodeFormData.password = await node.getPassword( ref.read(secureStoreProvider), ); if (context.mounted) { - final TorPlainNetworkOption netOption; - if (node.torEnabled && - !node.clearnetEnabled) { - netOption = TorPlainNetworkOption.tor; - } else if (node.clearnetEnabled && - !node.torEnabled) { - netOption = TorPlainNetworkOption.clear; - } else { - netOption = TorPlainNetworkOption.both; - } final canConnect = await ref.read( testNodeConnectionProvider, )( context: context, - nodeFormData: NodeFormData() - ..name = node.name - ..host = node.host - ..login = node.loginName - ..password = pw - ..port = node.port - ..useSSL = node.useSSL - ..isFailover = node.isFailover - ..netOption = netOption - ..trusted = node.trusted, + nodeFormData: nodeFormData, cryptoCurrency: coin, ); if (!canConnect) { return; } - await ref - .read(nodeServiceChangeNotifierProvider) - .setPrimaryNodeFor( - coin: coin, - node: node, - shouldNotifyListeners: true, - ); - - await _notifyWalletsOfUpdatedNode(ref); + if (await service.commitConnectionTest( + node: node, + formData: nodeFormData, + )) { + await notify(); + } } }, child: Text( diff --git a/lib/widgets/tls_certificate_dialog.dart b/lib/widgets/tls_certificate_dialog.dart new file mode 100644 index 0000000000..e116148bc2 --- /dev/null +++ b/lib/widgets/tls_certificate_dialog.dart @@ -0,0 +1,40 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; + +import '../utilities/tls_certificate.dart'; +import 'desktop/primary_button.dart'; +import 'desktop/secondary_button.dart'; +import 'stack_dialog.dart'; + +Future showBadX509CertificateDialog( + X509Certificate cert, + String host, + int port, + BuildContext context, +) async { + final result = await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => StackDialog( + title: 'Untrusted TLS certificate', + width: 520, + message: + 'Server: $host:$port\n\n' + 'Subject: ${cert.subject}\n' + 'Issuer: ${cert.issuer}\n\n' + 'Valid from: ${cert.startValidity.toUtc()}\n' + 'Valid until: ${cert.endValidity.toUtc()}\n\n' + 'SHA-256:\n${certificateFingerprint(cert.der)}', + leftButton: SecondaryButton( + label: 'Cancel', + onPressed: () => Navigator.of(context).pop(false), + ), + rightButton: PrimaryButton( + label: 'Trust', + onPressed: () => Navigator.of(context).pop(true), + ), + ), + ); + return result ?? false; +} diff --git a/pubspec.lock b/pubspec.lock index 1b3b953206..104cad6aad 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -889,11 +889,11 @@ packages: dependency: "direct main" description: path: "." - ref: b6fa44d015d3bfa06934b73219928c29ca48a290 - resolved-ref: b6fa44d015d3bfa06934b73219928c29ca48a290 + ref: "69de4e880c855a682b4b6539b54ddec7d80558fc" + resolved-ref: "69de4e880c855a682b4b6539b54ddec7d80558fc" url: "https://github.com/cypherstack/electrum_adapter.git" source: git - version: "3.0.2" + version: "4.1.0" emojis: dependency: "direct main" description: @@ -1232,8 +1232,8 @@ packages: dependency: "direct main" description: path: "." - ref: "14427bcbbe1e754bce4a1b93cdb0a31ce56d792b" - resolved-ref: "14427bcbbe1e754bce4a1b93cdb0a31ce56d792b" + ref: "7548bf1c6e79a82e991c3feb61c1908dfe324275" + resolved-ref: "7548bf1c6e79a82e991c3feb61c1908dfe324275" url: "https://github.com/cypherstack/fusiondart.git" source: git version: "1.0.0" @@ -2143,10 +2143,10 @@ packages: dependency: transitive description: name: socks_socket - sha256: "53bc7eae40a3aa16ea810b0e9de3bb23ba7beb0b40d09357b89190f2f44374cc" + sha256: "536e0876f94d878c09d0673d69d20f61894becce4476f0abd45a50a2ff387cf9" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "2.1.0" solana: dependency: "direct main" description: diff --git a/scripts/app_config/templates/pubspec.template.yaml b/scripts/app_config/templates/pubspec.template.yaml index 60e89c7b7f..9de97b3972 100644 --- a/scripts/app_config/templates/pubspec.template.yaml +++ b/scripts/app_config/templates/pubspec.template.yaml @@ -117,7 +117,7 @@ dependencies: fusiondart: git: url: https://github.com/cypherstack/fusiondart.git - ref: 14427bcbbe1e754bce4a1b93cdb0a31ce56d792b + ref: 7548bf1c6e79a82e991c3feb61c1908dfe324275 # Utility plugins http: ^1.6.0 @@ -229,7 +229,7 @@ dependencies: electrum_adapter: git: url: https://github.com/cypherstack/electrum_adapter.git - ref: b6fa44d015d3bfa06934b73219928c29ca48a290 + ref: 69de4e880c855a682b4b6539b54ddec7d80558fc stream_channel: ^2.1.0 solana: git: # TODO [prio=low]: Revert to official package once Tor support is merged upstream. diff --git a/test/electrumx_duplicate_endpoint_test.dart b/test/electrumx_duplicate_endpoint_test.dart new file mode 100644 index 0000000000..d03b73a68e --- /dev/null +++ b/test/electrumx_duplicate_endpoint_test.dart @@ -0,0 +1,121 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:event_bus/event_bus.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:logger/logger.dart' show Level; +import 'package:mockito/mockito.dart'; +import 'package:stackwallet/electrumx_rpc/client_manager.dart'; +import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; +import 'package:stackwallet/services/tor_service.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/prefs.dart'; +import 'package:stackwallet/utilities/tor_plain_net_option_enum.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; + +import 'utilities/tls_test_server.dart'; + +class _Prefs extends Mock implements Prefs { + @override + bool get useTor => false; + @override + bool get wifiOnly => false; +} + +class _Tor extends Mock implements TorService {} + +void main() { + test('repeated requests can keep an approved duplicate-endpoint failover', () async { + final logs = await Directory.systemTemp.createTemp('duplicate_endpoint'); + await Logging.instance.initialize(logs.path, level: Level.off); + final listener = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + final sockets = []; + int accepted = 0; + addTearDown(() async { + await ClientManager.sharedInstance.closeAll(); + for (final s in sockets) { + s.destroy(); + } + await listener.close(); + await logs.delete(recursive: true); + }); + listener.listen((raw) async { + final first = ++accepted == 1; + sockets.add(raw); + raw.done.ignore(); + final name = first ? 'node_a' : 'node_b'; + final context = SecurityContext() + ..useCertificateChainBytes( + utf8.encode(TlsElectrumServer.certificate(name)), + ) + ..usePrivateKeyBytes( + File('test/fixtures/tls/$name.key.pem').readAsBytesSync(), + ); + try { + final socket = await SecureSocket.secureServer(raw, context); + sockets.add(socket); + socket.done.ignore(); + socket + .cast>() + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((line) { + final request = jsonDecode(line) as Map; + final method = request['method']; + final Object? result = switch (method) { + 'server.version' => ['test', '1.4'], + 'blockchain.headers.subscribe' => { + 'height': 100, + 'hex': '00' * 80, + }, + 'probe' => + first ? {'error': 'retry at failover'} : {'via': 'B'}, + _ => null, + }; + socket.write( + '${jsonEncode({'jsonrpc': '2.0', 'id': request['id'], 'result': result})}\n', + ); + }, onError: (Object _) {}); + } catch (_) { + raw.destroy(); + } + }); + final bus = EventBus(); + addTearDown(bus.destroy); + final connection = ElectrumXClient( + host: '127.0.0.1', + port: listener.port, + useSSL: true, + prefs: _Prefs(), + netType: TorPlainNetworkOption.both, + cryptoCurrency: Bitcoin(CryptoCurrencyNetwork.main), + torService: _Tor(), + globalEventBusForTesting: bus, + trustedCertificatePem: TlsElectrumServer.certificate('node_a'), + connectionTimeoutForSpecialCaseJsonRPCClients: const Duration(seconds: 2), + failovers: [ + ElectrumXNode( + address: '127.0.0.1', + port: listener.port, + name: 'B', + id: 'B', + useSSL: true, + torEnabled: true, + clearnetEnabled: true, + trustedCertificatePem: TlsElectrumServer.certificate('node_b'), + ), + ], + ); + addTearDown(connection.closeAdapter); + expect(await connection.request(command: 'probe'), {'via': 'B'}); + expect(connection.currentFailoverIndex, -1); + final selected = connection.getElectrumAdapter(); + final next = await connection.ping().then( + (value) => value, + onError: (Object e) => e, + ); + expect(next, true); + expect(connection.getElectrumAdapter(), same(selected)); + }); +} diff --git a/test/electrumx_failover_test.dart b/test/electrumx_failover_test.dart new file mode 100644 index 0000000000..1ce1008298 --- /dev/null +++ b/test/electrumx_failover_test.dart @@ -0,0 +1,492 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:event_bus/event_bus.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:logger/logger.dart' show Level; +import 'package:mockito/mockito.dart'; +import 'package:stackwallet/electrumx_rpc/client_manager.dart'; +import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; +import 'package:stackwallet/services/tor_service.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/prefs.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; + +import 'utilities/tls_test_server.dart'; + +class _Prefs extends Mock implements Prefs { + @override + bool get useTor => false; + @override + bool get wifiOnly => false; + @override + bool get torKillSwitch => false; +} + +class _Tor extends Mock implements TorService {} + +class _RpcServer { + _RpcServer(this.server); + final ServerSocket server; + final sockets = []; + final ready = Completer(); + int pending = 0; + int requests = 0; + void Function()? fail; + int get port => server.port; + + static Future<_RpcServer> start({ + required bool batch, + bool holdRequests = true, + }) async { + final primary = _RpcServer( + await ServerSocket.bind(InternetAddress.loopbackIPv4, 0), + ); + primary.server.listen((socket) { + primary.sockets.add(socket); + socket.done.ignore(); + socket + .cast>() + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((line) { + final decoded = jsonDecode(line); + for (final request in decoded is List ? decoded : [decoded]) { + primary.requests++; + if (holdRequests && + (request['method'] == 'probe.a' || + request['method'] == 'probe.b')) { + primary.pending++; + if (request['method'] == 'probe.a') { + primary.fail = () { + final reply = batch + ? { + 'jsonrpc': '2.0', + 'id': request['id'], + 'error': { + 'code': -32000, + 'message': 'temporary failure', + }, + } + : { + 'jsonrpc': '2.0', + 'id': request['id'], + 'result': {'error': 'temporary failure'}, + }; + socket.write('${jsonEncode(batch ? [reply] : reply)}\n'); + }; + } + if (primary.pending == 2) primary.ready.complete(); + } else { + final Object result = switch (request['method']) { + 'server.version' => ['test', '1.4'], + 'blockchain.headers.subscribe' => { + 'height': 100, + 'hex': '00' * 80, + }, + _ => {}, + }; + final reply = { + 'jsonrpc': '2.0', + 'id': request['id'], + 'result': result, + }; + socket.write( + '${jsonEncode(decoded is List ? [reply] : reply)}\n', + ); + } + } + }, onError: (Object _) {}); + }); + return primary; + } + + Future close() async { + for (final socket in sockets) { + socket.destroy(); + } + await server.close(); + } +} + +class _Rpc { + _Rpc(this.socket, this.message); + final Socket socket; + final Map message; + String get method => message['method'] as String; + void reply(Object? value) => socket.write( + '${jsonEncode({'jsonrpc': '2.0', 'id': message['id'], 'result': value})}\n', + ); +} + +/// A plain JSON-RPC server for the publication-selection regression. It +/// answers header subscriptions, optionally auto-answers server.version, and +/// hands every request to [onRequest] so a test can error or hold it. +class _PubServer { + _PubServer(this.listener, this.autoVersion); + final ServerSocket listener; + final bool autoVersion; + final sockets = []; + final requests = <_Rpc>[]; + void Function(_Rpc)? onRequest; + int get port => listener.port; + List<_Rpc> get versions => + requests.where((e) => e.method == 'server.version').toList(); + + static Future<_PubServer> start({bool autoVersion = false}) async { + final server = _PubServer( + await ServerSocket.bind(InternetAddress.loopbackIPv4, 0), + autoVersion, + ); + server.listener.listen((socket) { + server.sockets.add(socket); + socket.done.ignore(); + socket + .cast>() + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((line) { + final decoded = jsonDecode(line); + for (final message in decoded is List ? decoded : [decoded]) { + final rpc = _Rpc(socket, message as Map); + server.requests.add(rpc); + if (rpc.method == 'blockchain.headers.subscribe') { + rpc.reply({'height': 100, 'hex': '00' * 80}); + } else if (rpc.method == 'server.version' && autoVersion) { + rpc.reply(['test', '1.4']); + } + server.onRequest?.call(rpc); + } + }, onError: (Object _) {}); + }); + return server; + } + + Future close() async { + for (final socket in sockets) { + socket.destroy(); + } + await listener.close(); + } +} + +Future _until(bool Function() ready) async { + final deadline = DateTime.now().add(const Duration(seconds: 3)); + while (!ready()) { + if (DateTime.now().isAfter(deadline)) { + throw TimeoutException('probe synchronization'); + } + await Future.delayed(const Duration(milliseconds: 5)); + } +} + +void main() { + late Directory logDirectory; + setUpAll(() async { + logDirectory = await Directory.systemTemp.createTemp('electrumx_failover'); + await Logging.instance.initialize(logDirectory.path, level: Level.off); + }); + tearDownAll(() => logDirectory.delete(recursive: true)); + final coin = Bitcoin(CryptoCurrencyNetwork.main); + final servers = []; + late EventBus bus; + + setUp(() => bus = EventBus()); + tearDown(() async { + await ClientManager.sharedInstance.closeAll(); + bus.destroy(); + for (final server in servers) { + await server.close(); + } + servers.clear(); + }); + + Future startPlain() async { + final server = await TlsElectrumServer.start(certificateName: null); + servers.add(server); + return server; + } + + /// A port nothing listens on. + Future deadPort() async { + final socket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + final port = socket.port; + await socket.close(); + return port; + } + + ElectrumXNode node(int port, String id) => ElectrumXNode( + address: '127.0.0.1', + port: port, + name: id, + id: id, + useSSL: false, + torEnabled: true, + clearnetEnabled: true, + ); + + ElectrumXClient client( + ElectrumXNode primary, { + List failovers = const [], + }) => ElectrumXClient.from( + node: primary, + prefs: _Prefs(), + torService: _Tor(), + failovers: failovers, + cryptoCurrency: coin, + globalEventBusForTesting: bus, + ); + + test( + 'requests after a successful failover request stay on the failover', + () async { + final failover = await startPlain(); + final connection = client( + node(await deadPort(), 'primary'), + failovers: [node(failover.port, 'failover')], + ); + connection.currentFailoverIndex = 0; + // A successful request resets the failover index to the primary. + expect(await connection.ping(), true); + expect(connection.currentFailoverIndex, -1); + final result = await connection.getTransaction(txHash: 'a' * 64); + expect(result['confirmations'], 3); + expect(connection.getElectrumAdapter()!.port, failover.port); + // server.version, headers.subscribe, ping, transaction.get: one session. + expect(failover.requests, 4); + expect(failover.disconnections, 0); + }, + ); + + for (final primaryShadows in [false, true]) { + test( + 'duplicate endpoint approvals do not hide a successful ${primaryShadows ? 'primary' : 'failover'} fallback', + () async { + final server = await TlsElectrumServer.start(certificateName: 'node_b'); + servers.add(server); + final a = TlsElectrumServer.certificate('node_a'); + final b = TlsElectrumServer.certificate('node_b'); + ElectrumXNode approved(String id, String pem) => ElectrumXNode( + address: '127.0.0.1', + port: server.port, + name: id, + id: id, + useSSL: true, + torEnabled: true, + clearnetEnabled: true, + trustedCertificatePem: pem, + ); + final connection = client( + primaryShadows + ? approved('primary', a) + : node(await deadPort(), 'primary'), + failovers: [ + if (!primaryShadows) approved('old approval', a), + approved('selected', b), + ], + ); + addTearDown(connection.closeAdapter); + connection.currentFailoverIndex = primaryShadows ? 0 : 1; + expect(await connection.ping(), true); + final selected = connection.getElectrumAdapter(); + expect(connection.currentFailoverIndex, -1); + expect( + (await connection.getTransaction(txHash: 'a' * 64))['confirmations'], + 3, + ); + expect(connection.getElectrumAdapter(), same(selected)); + expect(server.accepted, 1); + expect(server.disconnections, 0); + }, + ); + } + + test('another client for the coin shares the failover connection', () async { + final failover = await startPlain(); + final dead = await deadPort(); + final first = client( + node(dead, 'primary'), + failovers: [node(failover.port, 'failover')], + ); + first.currentFailoverIndex = 0; + expect(await first.ping(), true); + final second = client( + node(dead, 'primary'), + failovers: [node(failover.port, 'failover')], + ); + final result = await second.getTransaction(txHash: 'b' * 64); + expect(result['confirmations'], 3); + expect(failover.disconnections, 0); + }); + + test("a failover retry reuses another owner's healthy selected connection", () async { + // A shared failover endpoint that answers setup and holds one request. + final listener = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + final sockets = []; + var accepted = 0; + void Function()? release; + final held = Completer(); + listener.listen((socket) { + accepted++; + sockets.add(socket); + socket.done.ignore(); + socket + .cast>() + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((line) { + final request = jsonDecode(line) as Map; + void reply(Object? result) => socket.write( + '${jsonEncode({'jsonrpc': '2.0', 'id': request['id'], 'result': result})}\n', + ); + switch (request['method']) { + case 'blockchain.transaction.get': + release = () => reply({'confirmations': 7}); + if (!held.isCompleted) held.complete(); + case 'blockchain.headers.subscribe': + reply({'height': 100, 'hex': '00' * 80}); + default: + reply(['test', '1.4']); + } + }, onError: (Object _) {}); + }); + addTearDown(() async { + for (final socket in sockets) { + socket.destroy(); + } + await listener.close(); + }); + final dead = await deadPort(); + final failoverNode = node(listener.port, 'failover'); + final w1 = client(node(dead, 'primary'), failovers: [failoverNode]); + final w2 = client(node(dead, 'primary'), failovers: [failoverNode]); + addTearDown(w1.closeAdapter); + addTearDown(w2.closeAdapter); + + // W1 has already failed over to the shared node and is healthy on it. + w1.currentFailoverIndex = 0; + expect(await w1.ping(), true); + expect(w1.currentFailoverIndex, -1); + final shared = w1.getElectrumAdapter()!; + + // W1 keeps a request in flight on the shared connection. + final inflight = w1.getTransaction(txHash: 'a' * 64); + await held.future.timeout(const Duration(seconds: 2)); + + // W2's primary is also dead; its failover retry selects the same node. + w2.currentFailoverIndex = 0; + await w2.checkElectrumAdapter(); + expect( + w2.getElectrumAdapter(), + same(shared), + reason: 'the healthy selected failover is reused, not reopened', + ); + expect( + shared.peer.isClosed, + false, + reason: "another owner's connection must not be closed", + ); + expect(accepted, 1, reason: 'no spare session is opened'); + + release!(); + expect( + (await inflight.timeout(const Duration(seconds: 2)))['confirmations'], + 7, + reason: 'the in-flight RPC on the shared connection is not cut off', + ); + }); + + test( + 'a concurrent primary publication cannot replace the selected fallback', + () async { + final primary = await _PubServer.start(autoVersion: true); + final fallback = await _PubServer.start(); + addTearDown(primary.close); + addTearDown(fallback.close); + primary.onRequest = (rpc) { + if (rpc.method == 'probe') rpc.reply({'error': 'primary unavailable'}); + }; + fallback.onRequest = (rpc) { + if (rpc.method == 'probe') rpc.reply({'via': 'fallback'}); + }; + final first = client( + node(primary.port, 'primary'), + failovers: [node(fallback.port, 'fallback')], + ); + final second = client( + node(primary.port, 'primary'), + failovers: [node(fallback.port, 'fallback')], + ); + addTearDown(first.closeAdapter); + addTearDown(second.closeAdapter); + final pending = first + .request(command: 'probe') + .then((value) => value, onError: (Object e) => e); + await _until(() => fallback.versions.isNotEmpty); + // The second owner publishes a fresh primary while the fallback waits. + await second.checkElectrumAdapter(); + expect(second.getElectrumAdapter()!.port, primary.port); + // Let the primary's header subscription finish before it is replaced. + await ClientManager.sharedInstance.getChainHeightFor(coin); + fallback.versions.single.reply(['test', '1.4']); + final result = await pending.timeout(const Duration(seconds: 4)); + expect(result, { + 'via': 'fallback', + }, reason: 'the selected fallback must serve the retry, not the primary'); + expect(first.getElectrumAdapter()!.port, fallback.port); + expect( + fallback.requests.where((r) => r.method == 'probe').length, + 1, + reason: 'the probe reaches the healthy fallback', + ); + }, + ); + for (final batch in [false, true]) { + test( + 'failover survives another outstanding RPC closing (batch=$batch)', + () async { + final primary = await _RpcServer.start(batch: batch); + final failover = await _RpcServer.start( + batch: batch, + holdRequests: false, + ); + addTearDown(failover.close); + final connection = client( + node(primary.port, 'primary'), + failovers: [node(failover.port, 'failover')], + ); + final first = + (batch + ? connection.batchRequest( + command: 'probe.a', + args: [ + ['address'], + ], + ) + : connection.request(command: 'probe.a')) + .then( + (value) => value, + onError: (Object error) => error, + ); + final other = connection + .request(command: 'probe.b') + .then((value) => value, onError: (Object error) => error); + try { + await primary.ready.future.timeout(const Duration(seconds: 2)); + primary.fail!(); + final result = await first.timeout(const Duration(seconds: 3)); + expect( + result, + batch ? isA>() : isA>(), + ); + expect(failover.requests, greaterThanOrEqualTo(2)); + } finally { + await primary.close(); + await other.timeout(const Duration(seconds: 3)); + await connection.closeAdapter(); + } + }, + ); + } +} diff --git a/test/electrumx_late_approval_test.dart b/test/electrumx_late_approval_test.dart new file mode 100644 index 0000000000..313781f9cb --- /dev/null +++ b/test/electrumx_late_approval_test.dart @@ -0,0 +1,114 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:event_bus/event_bus.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:logger/logger.dart' show Level; +import 'package:mockito/mockito.dart'; +import 'package:stackwallet/electrumx_rpc/client_manager.dart'; +import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; +import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart'; +import 'package:stackwallet/services/tor_service.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/prefs.dart'; +import 'package:stackwallet/utilities/tor_plain_net_option_enum.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; + +import 'utilities/tls_test_server.dart'; + +class _Prefs extends Mock implements Prefs { + _Prefs(this.tor); + final bool tor; + @override + bool get useTor => tor; + @override + bool get wifiOnly => false; + @override + bool get torKillSwitch => true; +} + +class _Tor extends Mock implements TorService { + _Tor(this.proxyInfo); + final ({InternetAddress host, int port}) proxyInfo; + final requested = Completer(); + @override + TorConnectionStatus get status => TorConnectionStatus.connected; + @override + ({InternetAddress host, int port}) getProxyInfo() { + if (!requested.isCompleted) requested.complete(); + return proxyInfo; + } +} + +void main() { + test( + 'late adapter completion cannot republish a retired certificate approval', + () async { + final logs = await Directory.systemTemp.createTemp( + 'tls_only_late_approval', + ); + await Logging.instance.initialize(logs.path, level: Level.off); + final oldServer = await TlsElectrumServer.start(); + final newServer = await TlsElectrumServer.start(certificateName: null); + final proxy = await TlsTestProxy.start( + oldServer.port, + greetingDelay: const Duration(milliseconds: 200), + ); + final tor = _Tor(proxy.info); + final coin = Bitcoin(CryptoCurrencyNetwork.main); + final bus = EventBus(); + final oldClient = ElectrumXClient( + host: 'old.example', + port: oldServer.port, + useSSL: true, + trustedCertificatePem: TlsElectrumServer.certificate('node_a'), + prefs: _Prefs(true), + netType: TorPlainNetworkOption.both, + failovers: [], + cryptoCurrency: coin, + torService: tor, + globalEventBusForTesting: bus, + ); + final newClient = ElectrumXClient( + host: '127.0.0.1', + port: newServer.port, + useSSL: false, + prefs: _Prefs(false), + netType: TorPlainNetworkOption.both, + failovers: [], + cryptoCurrency: coin, + torService: tor, + globalEventBusForTesting: bus, + ); + addTearDown(() async { + await oldClient.closeAdapter(); + await newClient.closeAdapter(); + await ClientManager.sharedInstance.closeAll(); + bus.destroy(); + await proxy.close(); + await oldServer.close(); + await newServer.close(); + await logs.delete(recursive: true); + }); + + final oldOutcome = oldClient.ping().then( + (value) => value, + onError: (Object error) => error, + ); + await tor.requested.future.timeout(const Duration(seconds: 2)); + await oldClient.closeAdapter(); + expect(await newClient.ping(), true); + final replacement = newClient.getElectrumAdapter(); + expect(replacement, isNotNull); + + expect( + await oldOutcome.timeout(const Duration(seconds: 3)), + isA(), + ); + expect(proxy.destinations, ['old.example']); + expect(oldServer.requests, 0); + expect(newClient.getElectrumAdapter(), same(replacement)); + expect(await newClient.ping(), true); + }, + ); +} diff --git a/test/electrumx_test.dart b/test/electrumx_test.dart index 171b55cd25..7d0412a001 100644 --- a/test/electrumx_test.dart +++ b/test/electrumx_test.dart @@ -111,7 +111,7 @@ void main() { TorPlainNetworkOption netType = TorPlainNetworkOption.both, }) { return ManagedElectrumXClient( - host: 'mock.stackwallet.dev', + host: clearServer.host, port: 50002, useSSL: true, prefs: prefs, diff --git a/test/electrumx_tls_test.dart b/test/electrumx_tls_test.dart new file mode 100644 index 0000000000..6d1c3d4d43 --- /dev/null +++ b/test/electrumx_tls_test.dart @@ -0,0 +1,216 @@ +import 'dart:io'; + +import 'package:event_bus/event_bus.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:logger/logger.dart' show Level; +import 'package:stackwallet/electrumx_rpc/client_manager.dart'; +import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; +import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/services/node_service.dart'; +import 'package:stackwallet/utilities/prefs.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/services/tor_service.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_wallet.dart'; + +import 'utilities/tls_test_server.dart'; + +class _Prefs extends Mock implements Prefs { + @override + bool get useTor => false; + @override + bool get wifiOnly => false; + @override + bool get torKillSwitch => false; +} + +class _Tor extends Mock implements TorService {} + +/// Serves one primary node and no failovers to a wallet under test. +class _Nodes extends NodeService { + _Nodes(this.node) : super(secureStorageInterface: FakeSecureStorage()); + final NodeModel node; + @override + NodeModel? getPrimaryNodeFor({required CryptoCurrency currency}) => node; + @override + List failoverNodesFor({required CryptoCurrency currency}) => []; +} + +void main() { + late Directory logDirectory; + setUpAll(() async { + logDirectory = await Directory.systemTemp.createTemp('electrum_tls_logs'); + await Logging.instance.initialize(logDirectory.path, level: Level.off); + }); + tearDownAll(() => logDirectory.delete(recursive: true)); + final coin = Bitcoin(CryptoCurrencyNetwork.main); + final servers = []; + late EventBus bus; + final prefs = _Prefs(); + + setUp(() => bus = EventBus()); + tearDown(() async { + await ClientManager.sharedInstance.closeAll(); + bus.destroy(); + for (final server in servers) { + await server.close(); + } + servers.clear(); + }); + + Future start(String certificateName) async { + final server = await TlsElectrumServer.start( + certificateName: certificateName, + ); + servers.add(server); + return server; + } + + ElectrumXNode node(TlsElectrumServer server, {String? certificateName}) => + ElectrumXNode( + address: '127.0.0.1', + port: server.port, + name: 'TLS node', + id: '${server.port}', + useSSL: true, + torEnabled: true, + clearnetEnabled: true, + trustedCertificatePem: certificateName == null + ? null + : TlsElectrumServer.certificate(certificateName), + ); + + ElectrumXClient client( + ElectrumXNode node, { + List failovers = const [], + }) => ElectrumXClient.from( + node: node, + prefs: prefs, + torService: _Tor(), + failovers: failovers, + cryptoCurrency: coin, + globalEventBusForTesting: bus, + ); + + test('live client connects using the approved primary certificate', () async { + final server = await start('node_a'); + final primary = node(server, certificateName: 'node_a'); + expect( + ElectrumXNode.from(primary).trustedCertificatePem, + primary.trustedCertificatePem, + ); + final connection = client(primary); + await connection.checkElectrumAdapter(); + final result = await connection.getTransaction(txHash: 'a' * 64); + expect(result['confirmations'], 3); + expect(connection.trustedCertificatePem, primary.trustedCertificatePem); + }); + + test('live client rejects an unapproved primary certificate', () async { + final server = await start('node_a'); + await expectLater( + client(node(server)).checkElectrumAdapter(), + throwsA(isA()), + ); + expect(server.requests, 0); + }); + + test('failover uses its own approved certificate', () async { + final primary = await start('node_a'); + final failover = await start('node_b'); + final connection = client( + node(primary, certificateName: 'node_a'), + failovers: [node(failover, certificateName: 'node_b')], + ); + connection.currentFailoverIndex = 0; + await connection.checkElectrumAdapter(); + await connection.getElectrumAdapter()!.request('server.version'); + expect(connection.getElectrumAdapter()!.port, failover.port); + }); + + test('primary approval is not inherited by an unapproved failover', () async { + final primary = await start('node_a'); + final failover = await start('node_b'); + final connection = client( + node(primary, certificateName: 'node_a'), + failovers: [node(failover)], + ); + connection.currentFailoverIndex = 0; + await expectLater( + connection.checkElectrumAdapter(), + throwsA(isA()), + ); + expect(failover.requests, 0); + }); + + test('a client with the same approval shares the connection', () async { + final server = await start('node_a'); + final first = client(node(server, certificateName: 'node_a')); + await first.checkElectrumAdapter(); + // Let the header subscription settle before another client joins. + await first.getElectrumAdapter()!.request('server.version'); + final second = client(node(server, certificateName: 'node_a')); + await second.checkElectrumAdapter(); + expect(second.getElectrumAdapter(), same(first.getElectrumAdapter())); + expect(server.disconnections, 0); + }); + + test( + 'revoking approval cannot reuse a previously trusted connection', + () async { + final server = await start('node_a'); + final trusted = client(node(server, certificateName: 'node_a')); + await trusted.checkElectrumAdapter(); + await trusted.getElectrumAdapter()!.request('server.version'); + final peer = trusted.getElectrumAdapter()!.peer; + // No wallet closed the connection; the next client for this endpoint + // carries the revocation itself. + final revoked = client(node(server)); + await expectLater( + revoked.checkElectrumAdapter(), + throwsA(isA()), + ); + expect(peer.isClosed, true); + expect(revoked.getElectrumAdapter(), null); + }, + ); + + test('a fresh wallet applies a revocation no loaded wallet could', () async { + final server = await start('node_a'); + // A wallet connected under the approval and was then deleted, which + // leaves the coin-wide connection open. With no wallet of the coin + // loaded, saving the revocation had no wallet to notify. + final trusted = client(node(server, certificateName: 'node_a')); + await trusted.checkElectrumAdapter(); + await trusted.getElectrumAdapter()!.request('server.version'); + final peer = trusted.getElectrumAdapter()!.peer; + final fresh = BitcoinWallet(CryptoCurrencyNetwork.main) + ..prefs = prefs + ..nodeService = _Nodes( + NodeModel( + host: '127.0.0.1', + port: server.port, + name: 'TLS node', + id: 'tls-node', + useSSL: true, + enabled: true, + coinName: 'bitcoin', + isFailover: false, + isDown: false, + trusted: false, + torEnabled: true, + clearnetEnabled: true, + isPrimary: true, + ), + ); + // Wallet.load() calls this before the wallet has any client to close. + await fresh.updateNode(); + await expectLater( + fresh.electrumXClient.request(command: 'server.version'), + throwsA(isA()), + ); + expect(peer.isClosed, true); + }); +} diff --git a/test/fixtures/tls/expired.key.pem b/test/fixtures/tls/expired.key.pem new file mode 100644 index 0000000000..5af3ba016d --- /dev/null +++ b/test/fixtures/tls/expired.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC2Y1jMxcqqPHby ++c9On7aAdWHUy9cAaKGMzcmEDK/kuo8mjpZeVT7NnXGot65j4WWeWfo3coIYuC3+ +aA5obDMj04cAv9VNFPtJGyFVFt/UyM7kE8dVZc3/lkv+IyUMs0mBs/b09NhMHsZE +P+H4bzZuF75Sb6LJtCcl9rpztROp2GklpEnsynIuAFZA8jigUnpmvMjptPCAQhyq +QMiTyFqT2zJVfWPuuQkYa/1YQdwfjAv4+JsnQhjtYgk9eg6m/r24RZK0hp2cDTQh +Oc6juSwUgG6zKh56qo9L89g0+IH3dKk2hYOc50p9Gs5N81ohH3LbeNktjGAzWp8y +rHhe/sAxAgMBAAECggEBAInVJEuEAjeKgD35CXCahDop+yfsb+vQJp4Y+nw/dkG9 +tublhL9/XnnWzQk63qB+uuPjjZOPhr+VmLSVAKZrdm5RscmgL1sUSx6hURxTsA3H +P4rfY5mm2PmZJI7HEn25lAhf73aI/ljwPNdhC61KpnCmot77nKMf8SHghmFg655D +NezEYaUemR3W1yQAdkk/5p+Bzd+mLBj7MRY+BAvlz3WbBk7n3hvPqB6XjvPDhhMZ +aVjsnzLKsHqxiaZn/0VepR//bkVlThGNUnUkuzz/NZvdNVXu1e9gkBCN2mS2huAX +laPr73/9EjaE6XQRDWfMiItzswXCQCsBrt25ZrPh7G0CgYEA3RMDFRfMSS05aepe +432pnCSI8gcVQWamPSdguobPBo74ZwhmeRo5cLUocOW+HmbjrKCyfisQCBuEgd1g +9zMo5X/cwFTSmzurWFNpK+tC2P9u70SSou/lrjIFDW9bQGjvoux23XYlnGoQznPP +/7dWYGgzU238/K+zihYTMe01CosCgYEA0zO8I63Tqlo5nF2mDDLVCCvMXHvAi0IT +HoSR8OYG66l8qY8VyQWf9UWEvbnWeFtODdG48zKX+BhP6iyKQtlBNIno5oc8sXab +Q6upwNIJHOlwSga2JSncKGE6Uf4XDuCtSAUAwLryJyHPpdtFy9hvtIliAs6xd7fZ +Vcxhl/YyQ7MCgYEAz83GMPaMtSBIfpGqngYJ/1asT2xvA2ncy1EPcGW1hkGvpxHT +WPRYl+QBXpXl7qfiTOtEvKn3yNcs5/FS6ypwv8E/nJ5e1UHyXvUQwBLFZDo+abQ4 +beY4LFSiQS2yRcodrzS8ypD665sbryRXmNQZ4kcwL7UMf+0cQ6laY6zIFd8CgYAl +db+g9GvegzJEsbeJ4lo2oM4WeTyldoq9lvXrrGD924no6SAMD5tXfYUVwahwoIEv +oksTl52su2MxPcUoJjCSiQNuH/5723WegwwQzzAemKj5+EbGQK5z2wrlEBQ251LU +u05u4LmihzS1TjcK+NCXTOa9aLvBaDJ8LUwCEADKUQKBgQCWcdXV4zA9KrPd8pzn +U20x3RXYTLLDYygsQm0wWBOUw+aQJBJPBJuw1PzkYUoQI2icXPtjAMeMykIU4LkY +UKkZokaRmlAdddE9dTlYyaVOjVrEOdcExN0mJisoS2UDxmJG77mZ5Bb573dfsm3K +WLCf3QE5JJ/7uYfxVKuKfA/dmw== +-----END PRIVATE KEY----- diff --git a/test/fixtures/tls/expired.pem b/test/fixtures/tls/expired.pem new file mode 100644 index 0000000000..accca0526e --- /dev/null +++ b/test/fixtures/tls/expired.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC5zCCAc+gAwIBAgIBATANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAlsb2Nh +bGhvc3QwHhcNMjAwMTAxMDAwMDAwWhcNMjEwMTAxMDAwMDAwWjAUMRIwEAYDVQQD +DAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2Y1jM +xcqqPHby+c9On7aAdWHUy9cAaKGMzcmEDK/kuo8mjpZeVT7NnXGot65j4WWeWfo3 +coIYuC3+aA5obDMj04cAv9VNFPtJGyFVFt/UyM7kE8dVZc3/lkv+IyUMs0mBs/b0 +9NhMHsZEP+H4bzZuF75Sb6LJtCcl9rpztROp2GklpEnsynIuAFZA8jigUnpmvMjp +tPCAQhyqQMiTyFqT2zJVfWPuuQkYa/1YQdwfjAv4+JsnQhjtYgk9eg6m/r24RZK0 +hp2cDTQhOc6juSwUgG6zKh56qo9L89g0+IH3dKk2hYOc50p9Gs5N81ohH3LbeNkt +jGAzWp8yrHhe/sAxAgMBAAGjRDBCMA8GA1UdEwEB/wQFMAMBAf8wGgYDVR0RBBMw +EYIJbG9jYWxob3N0hwR/AAABMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA0GCSqGSIb3 +DQEBCwUAA4IBAQCZiHHwkDZtLK9qSaoKaHL5voxvbH00ggjIS7FDGUUQFpSwbYgy +kzm23UID7sQwBzWh3KVfCOAeYmNL6mEV61gcoIKd8u4W/MArklQH6gX+jJENVgRl +N/hd2TXvGV625S8/2jksNCYsHcWVYkB7hJ3IGEqSFVFtOt6HkxX2s3VSOAeoRNNa +RjPRF+H2yx6K0kw6PdLvWXw3vY399XORkmXEN+CNPXqjARgee8jgR9Byf+LJZ/Wp +mUelvexeQhhYQh6qBSDfvNUDWqM1MUwEYJ3ZwePhRivR2t4k0b+MyeuyxyLsDs5t +lUGqkNlu6y7G24L7pDYPMAUjO1HnfB3kdv3x +-----END CERTIFICATE----- diff --git a/test/fixtures/tls/node_a.key.pem b/test/fixtures/tls/node_a.key.pem new file mode 100644 index 0000000000..2e446b4b86 --- /dev/null +++ b/test/fixtures/tls/node_a.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC+BqJm1wyIEii2 +9MO2qtBr0Ne5I0bFf1aNA5b1hz7QapEQRO8gdL93DlpAJw0pAGa5rSEdAV7A2MgV +u35rfjG8MWoZblANv5rlHprkAnMl4oIRCXt3voIyVNUM4eZJu2pifEjT5GwDiHR3 +HsovdgPM7bzUjVzdBIOhhNXRp8Fkayo6IyChzA/3Gj6ehbOhHwOW05B4o3Pzj9lr +9gf4l9X/cwihuuvL+n0ZUrBMScQtiZ73LhyqcqQvcS7azy4FFzrdzrXFwtO6nxUN +mG6C/sPAZwY1ml6Dnadroc61eF4gLFIQyL4wDpuzLb7z3ENONO2b4yjenstc6Xkz +xCJgBFulAgMBAAECggEARpMINdGgodi8rlZxaaI/DgveUrA7VJZaerzekAnO1nD9 +t2EivpcdlQYLls/qH1Q60rLuHWuo2U6p4Qhlh16y8gYYNaG2saR7zqRzOkN+qI9b +ZyBybqKekDrZdZNgh/nHJRSrp6aqRxWiy6rvxHi8KJwTCY4G8z799Dr1fowjUzul +4omUkQBA81qfHWzAU1PRQ9FyQtzM+/Y+7JIA5j7zo06GgU+NjJx+SC6jd8Ym8s5p +xdKw1JrrNcWn6Z1n/nV1WamqDI/uodFij97nKIuR2839hb4zE6rxMC4o8EYxxXP/ +VIcKQqTWysBChA6Bg0kqEB6X6yaSU51Pjl23VOylcQKBgQDkIEPBZjs8efoxU3ao +UnohQ4djZXzksdWh4GfNtT5Z9LhG8wrAiTLaPcl4bgt9wmfY8294i7S/fsvUFeMt +qfAziogi/IqyOaM42R/iRoKMSDsVhHrg/2hC24hbfpMDiPEJjEogAJJbC+DpgGC3 +cmrYxNDJXzDNgHYuoxU47wMucwKBgQDVPpsZyfdftuSJvNx/t0W2yTw1nKW9pxVw +Gb6irgrVq7YRUukPfhP2yAwwGgz7T/Cg/W3hLAnqva3AQFflTlkCtO6bTY2AL77F +AsUgajq6vIm+a5VHj+zP2jFVnPftdpDuUAHu5sEw9ykEsXEja5nP6jT/Lf8pdyXj +1P4c80FvhwKBgQClSg9tyOHwvryeEshty1y8T06AoV8t4QBWSt9LeCAWU69VkPHg +cUhMQuqAfNOMqlhOJIxG/orXGUscf0Ncqb3lymE7YFoqKoLuRLvKWqZqeEd/Hr9d +2RE1KBWTnZFtxUJIVcK7j057845iAWeSY6+xImHZFkSpEYQP5JAS8XMPqwKBgF6B +uZGLXfPwhYa5yetcWSwyMVn9S8Mqqqic8Ez+31RlHnK9EQdJtCVZ8XTcBsbWXemr +h6gUsWx0le2vVoUM3QwDDgCMNVsLTfvYR28fRVn/NvjKi0VE9woDRHwR7hsywfas +RL8RS6ucznQDBAZTqL5pqQUy6HQDFyrK+bfw4x4/AoGAI/IdlKo7jILyis9ASL14 +ckSUcXjuiyI2ha4ap1+EpDOTIpkAsbCEXDqnDfpbpjL847vs4mxsJP7+ZF50j0F/ +XQr1ZCKWYc2CXjYxdq7udM2rfWrT2WX9goj44iftF2jK/HzgzEEKhk3ldXMLDeSO +vlfmIeCu1QiTx7TY4WamPWA= +-----END PRIVATE KEY----- diff --git a/test/fixtures/tls/node_b.key.pem b/test/fixtures/tls/node_b.key.pem new file mode 100644 index 0000000000..81ebe82e2a --- /dev/null +++ b/test/fixtures/tls/node_b.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDqOJsCQTKfiveh +k1in+TEkQndzNDuzKFSAzyZ+nWthUAvp1XxFdlcOiWBL5mwLkC0zqRrasBTW4bQu +w+Kbw4n7NzBjcs1FSgz86PBJ/5R1UznEGe3G1+83U7noxKqdrPWkwVt14Qolp9At +BvhRBciH/VjvwRfuwSMkCb6aGZgYLiFcGDvk+nvlpE0BYjq61zpd8p+U3EoFwTr4 +5mvG7uNFFC08wQzXKcU5CHNneq1vg42quUHn503q1l/RH4D9wD/sVb3UeqD6KeXp +VrOQ63dh85gdgDf39VNp23oy7BmbiUzi2h9Ir5FQejr+oAq25lIUPc2OsU1ofrKh +gBa6wKDxAgMBAAECggEACCCCsGdWvFZ0FTUppFpvLBTqVCMB10uU8ws9CLOpoCzI +3Qo9T6O1jZmSr8+E/Dm6d8psVh45+uLF/8FoQYNdbEwT779yA+931+a1P7wQiKmk +nXtQ1s9zS3YqQztfuCziMR9MbyCp23ihLo7JWvZaaO2PgnIuxGgLMhrsdChjMKOI +InI2SCAt3tnx6uRDJbgRDFsMM3zZepqZSFa7aoeSiT9QvZakCOqqwZUbv4oUdNwf +QuLeURmaFv/xJR1TbzEm7qenAZj2WxBYsDD6WP5mu0EMT6OPFOAQPTkJey4mOyeq +3vmy+q6/iZJeeTcQsnUmKTQBfsBoEuzd3gULRepYyQKBgQD2mOkVzQN0x2Dukcsq +3FIkD0LQVLG7xhECsXfqrtb3j/FSD9IA9n0b7U04Hus9A7sMAIATSnoEWYFBro/s +FyozOioPhWAdXz0/isxM4HRlkcZvg5Ypq4PRthCKBa+ZrZkjAPyZod1dKfEFgxXN +QFYO7tCD9XMUdOu/3MpMcs681wKBgQDzJuNqRvhhEFMLCOZP0KYZL9RybU3Lxv9C ++xpTBUtcatoh/0wTzs7r6c3+pOSMO/xHPI8OMkNGWQpZro2392garWBnsfMeDsy0 +t/bdWT8JhXM6/IvcQ/vZ0V6PVgfhRjW7tavEptoPMHQKEx7Z/7MXbTtzKrQcNPzo +Fhz9ghPPdwKBgQCZG9nk2lTiBgUOO2GM7BzoHL2cQmnUL6dSJ8RP51y7exkXnZED +XgfR9iXyyWe/i3hwfEMJNz3JQ5V4pZzgtoVSJzQETQifVAngURqGRPqgMcGmbWon +10FSTRu5xsv/vASP8kXkUdbyyF7I0FoSBXcpcGfw9s4MDohtqPXGP6ZTIwKBgBoS +OmIj1+/4Yx8UxjUapySeN0ZJqATIgog5ClqKf8kZ/+eTSyH4k8FB4X+DSKosqGEv +AhvAkewSEgUJrVJjabsqieK0dt/rMFHUQ9j5/8NSlsdlWs20UGXLiDsQO2oRMCO/ +V/qO9WSZimemIzG2tAo4AR7ZHlTD7bYo4NThxKQDAoGBAM34F/m4W6KqOUJ+Jq+k +pkmx3rBMIE+Qnr2pU8z9hwemWDPmZc/R5eVoOACaoMjA8+GN5hQa/FWBMxzgZZRA +lDXrzlFXv7MMTQpKl6YPNHhgfy1IHwWqtNGFCwVqQsFrbmolILuZkjixfUbrh9pl +2pqXZd1YKFsdGSLym41sGkiu +-----END PRIVATE KEY----- diff --git a/test/models/node_model_backup_test.dart b/test/models/node_model_backup_test.dart index d0259c1541..892785344d 100644 --- a/test/models/node_model_backup_test.dart +++ b/test/models/node_model_backup_test.dart @@ -1,7 +1,48 @@ +import 'dart:io'; + import 'package:flutter_test/flutter_test.dart'; import 'package:stackwallet/models/node_model.dart'; void main() { + test('keeps a PEM whose contents are not a certificate', () { + // It cannot match any server certificate, so the node is simply not + // approved until its certificate is reviewed again. + for (final body in ['aGVsbG8=', 'MAA=']) { + final pem = + '-----BEGIN CERTIFICATE-----\n$body\n-----END CERTIFICATE-----\n'; + final node = NodeModel.fromStackBackup({ + 'host': 'node.example.com', + 'port': 50002, + 'name': 'Node', + 'id': 'tls', + 'coinName': 'bitcoin', + 'trustedCertificatePem': pem, + }); + expect(node.trustedCertificatePem, pem); + } + }); + + test('restores a trusted certificate and drops a damaged one', () { + final pem = File('test/fixtures/tls/expired.pem').readAsStringSync(); + final map = { + 'host': 'node.example.com', + 'port': 50002, + 'name': 'Node', + 'id': 'tls', + 'coinName': 'bitcoin', + 'trustedCertificatePem': pem, + }; + expect(NodeModel.fromStackBackup(map).trustedCertificatePem, pem); + for (final damaged in ['', 'not a certificate', 42, pem.substring(40)]) { + map['trustedCertificatePem'] = damaged; + expect( + NodeModel.fromStackBackup(map).trustedCertificatePem, + null, + reason: '$damaged', + ); + } + }); + test('restores current and legacy node backup fields', () { final source = NodeModel( host: 'node.example.com', diff --git a/test/models/node_tls_trust_test.dart b/test/models/node_tls_trust_test.dart new file mode 100644 index 0000000000..2a40724ac0 --- /dev/null +++ b/test/models/node_tls_trust_test.dart @@ -0,0 +1,198 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:hive_ce/hive.dart'; +import 'package:stackwallet/models/node_form_data.dart'; +import 'package:stackwallet/models/node_model.dart'; + +import '../utilities/tls_test_server.dart'; + +NodeModel tlsNode({String? certificatePem}) => NodeModel( + host: '127.0.0.1', + port: 50002, + name: 'TLS node', + id: 'tls-node', + useSSL: true, + enabled: true, + coinName: 'bitcoin', + isFailover: false, + isDown: false, + trusted: false, + torEnabled: true, + clearnetEnabled: true, + isPrimary: false, + trustedCertificatePem: certificatePem, +); + +void main() { + final pem = TlsElectrumServer.certificate('node_a'); + + test( + 'backup round trip preserves TLS approval independently of daemon trust', + () { + final node = tlsNode(certificatePem: pem); + final restored = NodeModel.fromStackBackup(node.toMap()); + expect(restored.trustedCertificatePem, pem); + expect(restored.trusted, false); + final legacy = node.toMap()..remove('trustedCertificatePem'); + expect(NodeModel.fromStackBackup(legacy).trustedCertificatePem, null); + }, + ); + + test( + 'node edits preserve approval for the same endpoint and can revoke it', + () { + final node = tlsNode(certificatePem: pem); + expect( + node + .copyWith(name: 'Renamed', loginName: null, trusted: false) + .trustedCertificatePem, + pem, + ); + expect( + node + .copyWith( + loginName: null, + trusted: false, + clearTrustedCertificate: true, + ) + .trustedCertificatePem, + null, + ); + expect( + node + .copyWith(host: 'another.example', loginName: null, trusted: false) + .trustedCertificatePem, + null, + ); + expect( + node + .copyWith(port: 50003, loginName: null, trusted: false) + .trustedCertificatePem, + null, + ); + expect( + node + .copyWith(useSSL: false, loginName: null, trusted: false) + .trustedCertificatePem, + null, + ); + }, + ); + + test('form edits clear approval only when the endpoint changes', () { + final data = NodeFormData.fromNode(tlsNode(certificatePem: pem)); + data.name = 'Renamed'; + data.host = data.host; + data.port = data.port; + data.useSSL = data.useSSL; + expect(data.trustedCertificatePem, pem); + data.host = 'another.example'; + expect(data.trustedCertificatePem, null); + data.trustedCertificatePem = pem; + data.port = 50003; + expect(data.trustedCertificatePem, null); + data.trustedCertificatePem = pem; + data.useSSL = false; + expect(data.trustedCertificatePem, null); + }); + + test('restoring an edited endpoint restores its approval', () { + final data = NodeFormData.fromNode(tlsNode(certificatePem: pem)); + data.host = 'another.example'; + expect(data.trustedCertificatePem, null); + data.host = '127.0.0.1'; + expect(data.trustedCertificatePem, pem); + data.port = 50003; + data.port = 50002; + data.useSSL = false; + data.useSSL = true; + expect(data.trustedCertificatePem, pem); + expect(data.matchesEndpointOf(tlsNode()), true); + data.trustedCertificatePem = null; + data.host = 'another.example'; + data.host = '127.0.0.1'; + expect(data.trustedCertificatePem, null); + }); + + test( + 'manual Hive adapter persists approval across close and reopen', + () async { + final directory = await Directory.systemTemp.createTemp('node_tls_hive'); + Hive.init(directory.path); + Hive.resetAdapters(); + Hive.registerAdapter(NodeModelAdapter()); + try { + var box = await Hive.openBox('nodes'); + await box.put('node', tlsNode(certificatePem: pem)); + await box.close(); + box = await Hive.openBox('nodes'); + expect(box.get('node')!.trustedCertificatePem, pem); + expect(box.get('node')!.trusted, false); + await box.close(); + } finally { + await Hive.close(); + Hive.resetAdapters(); + await directory.delete(recursive: true); + } + }, + ); + + test('manual Hive adapter reads legacy nodes without migration', () async { + final directory = await Directory.systemTemp.createTemp('node_tls_legacy'); + Hive.init(directory.path); + Hive.registerAdapter(_LegacyNodeAdapter(), override: true); + try { + var box = await Hive.openBox('nodes'); + await box.put('node', tlsNode()); + await box.close(); + Hive.resetAdapters(); + Hive.registerAdapter(NodeModelAdapter()); + box = await Hive.openBox('nodes'); + expect(box.get('node')!.trustedCertificatePem, null); + expect(box.get('node')!.host, '127.0.0.1'); + await box.close(); + } finally { + await Hive.close(); + Hive.resetAdapters(); + await directory.delete(recursive: true); + } + }); +} + +/// The sixteen fields written before TLS trust was introduced. +class _LegacyNodeAdapter extends TypeAdapter { + @override + int get typeId => 12; + + @override + NodeModel read(BinaryReader reader) => NodeModelAdapter().read(reader); + + @override + void write(BinaryWriter writer, NodeModel node) { + final fields = [ + node.id, + node.host, + node.port, + node.name, + node.useSSL, + node.loginName, + node.enabled, + node.coinName, + node.isFailover, + node.isDown, + node.trusted, + node.torEnabled, + node.clearnetEnabled, + node.forceNoTor, + node.isPrimary, + node.nodeApiSecret, + ]; + writer.writeByte(fields.length); + for (int i = 0; i < fields.length; i++) { + writer + ..writeByte(i) + ..write(fields[i]); + } + } +} diff --git a/test/pages/send_view/send_view_test.mocks.dart b/test/pages/send_view/send_view_test.mocks.dart index 8df86739df..dce6c95f8a 100644 --- a/test/pages/send_view/send_view_test.mocks.dart +++ b/test/pages/send_view/send_view_test.mocks.dart @@ -4,24 +4,25 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i10; -import 'dart:typed_data' as _i20; -import 'dart:ui' as _i15; +import 'dart:typed_data' as _i21; +import 'dart:ui' as _i16; -import 'package:logger/logger.dart' as _i23; +import 'package:logger/logger.dart' as _i24; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i17; +import 'package:mockito/src/dummies.dart' as _i18; import 'package:stackwallet/db/isar/main_db.dart' as _i3; -import 'package:stackwallet/models/epicbox_server_model.dart' as _i14; -import 'package:stackwallet/models/isar/stack_theme.dart' as _i19; +import 'package:stackwallet/models/epicbox_server_model.dart' as _i15; +import 'package:stackwallet/models/isar/stack_theme.dart' as _i20; +import 'package:stackwallet/models/node_form_data.dart' as _i14; import 'package:stackwallet/models/node_model.dart' as _i13; import 'package:stackwallet/networking/http.dart' as _i7; -import 'package:stackwallet/services/locale_service.dart' as _i16; +import 'package:stackwallet/services/locale_service.dart' as _i17; import 'package:stackwallet/services/node_service.dart' as _i2; import 'package:stackwallet/services/wallets.dart' as _i9; -import 'package:stackwallet/themes/theme_service.dart' as _i18; -import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i24; -import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i22; -import 'package:stackwallet/utilities/enums/sync_type_enum.dart' as _i21; +import 'package:stackwallet/themes/theme_service.dart' as _i19; +import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i25; +import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i23; +import 'package:stackwallet/utilities/enums/sync_type_enum.dart' as _i22; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' as _i6; import 'package:stackwallet/utilities/prefs.dart' as _i12; @@ -88,31 +89,22 @@ class MockWallets extends _i1.Mock implements _i9.Wallets { } @override - _i2.NodeService get nodeService => - (super.noSuchMethod( - Invocation.getter(#nodeService), - returnValue: _FakeNodeService_0( - this, - Invocation.getter(#nodeService), - ), - ) - as _i2.NodeService); + _i2.NodeService get nodeService => (super.noSuchMethod( + Invocation.getter(#nodeService), + returnValue: _FakeNodeService_0(this, Invocation.getter(#nodeService)), + ) as _i2.NodeService); @override - _i3.MainDB get mainDB => - (super.noSuchMethod( - Invocation.getter(#mainDB), - returnValue: _FakeMainDB_1(this, Invocation.getter(#mainDB)), - ) - as _i3.MainDB); + _i3.MainDB get mainDB => (super.noSuchMethod( + Invocation.getter(#mainDB), + returnValue: _FakeMainDB_1(this, Invocation.getter(#mainDB)), + ) as _i3.MainDB); @override - List<_i5.Wallet<_i4.CryptoCurrency>> get wallets => - (super.noSuchMethod( - Invocation.getter(#wallets), - returnValue: <_i5.Wallet<_i4.CryptoCurrency>>[], - ) - as List<_i5.Wallet<_i4.CryptoCurrency>>); + List<_i5.Wallet<_i4.CryptoCurrency>> get wallets => (super.noSuchMethod( + Invocation.getter(#wallets), + returnValue: <_i5.Wallet<_i4.CryptoCurrency>>[], + ) as List<_i5.Wallet<_i4.CryptoCurrency>>); @override set nodeService(_i2.NodeService? value) => super.noSuchMethod( @@ -129,13 +121,12 @@ class MockWallets extends _i1.Mock implements _i9.Wallets { @override _i5.Wallet<_i4.CryptoCurrency> getWallet(String? walletId) => (super.noSuchMethod( - Invocation.method(#getWallet, [walletId]), - returnValue: _FakeWallet_2<_i4.CryptoCurrency>( - this, - Invocation.method(#getWallet, [walletId]), - ), - ) - as _i5.Wallet<_i4.CryptoCurrency>); + Invocation.method(#getWallet, [walletId]), + returnValue: _FakeWallet_2<_i4.CryptoCurrency>( + this, + Invocation.method(#getWallet, [walletId]), + ), + ) as _i5.Wallet<_i4.CryptoCurrency>); @override void addWallet(_i5.Wallet<_i4.CryptoCurrency>? wallet) => super.noSuchMethod( @@ -147,43 +138,33 @@ class MockWallets extends _i1.Mock implements _i9.Wallets { _i10.Future deleteWallet( _i11.WalletInfo? info, _i6.SecureStorageInterface? secureStorage, - ) => - (super.noSuchMethod( - Invocation.method(#deleteWallet, [info, secureStorage]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + ) => (super.noSuchMethod( + Invocation.method(#deleteWallet, [info, secureStorage]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override _i10.Future load( _i12.Prefs? prefs, _i3.MainDB? mainDB, bool? isDuress, - ) => - (super.noSuchMethod( - Invocation.method(#load, [prefs, mainDB, isDuress]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + ) => (super.noSuchMethod( + Invocation.method(#load, [prefs, mainDB, isDuress]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override _i10.Future loadAfterStackRestore( _i12.Prefs? prefs, List<_i5.Wallet<_i4.CryptoCurrency>>? wallets, bool? isDesktop, - ) => - (super.noSuchMethod( - Invocation.method(#loadAfterStackRestore, [ - prefs, - wallets, - isDesktop, - ]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + ) => (super.noSuchMethod( + Invocation.method(#loadAfterStackRestore, [prefs, wallets, isDesktop]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); } /// A class which mocks [NodeService]. @@ -195,31 +176,25 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { } @override - _i6.SecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( - Invocation.getter(#secureStorageInterface), - returnValue: _FakeSecureStorageInterface_3( - this, - Invocation.getter(#secureStorageInterface), - ), - ) - as _i6.SecureStorageInterface); + _i6.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( + Invocation.getter(#secureStorageInterface), + returnValue: _FakeSecureStorageInterface_3( + this, + Invocation.getter(#secureStorageInterface), + ), + ) as _i6.SecureStorageInterface); @override - List<_i13.NodeModel> get primaryNodes => - (super.noSuchMethod( - Invocation.getter(#primaryNodes), - returnValue: <_i13.NodeModel>[], - ) - as List<_i13.NodeModel>); + List<_i13.NodeModel> get primaryNodes => (super.noSuchMethod( + Invocation.getter(#primaryNodes), + returnValue: <_i13.NodeModel>[], + ) as List<_i13.NodeModel>); @override - List<_i13.NodeModel> get nodes => - (super.noSuchMethod( - Invocation.getter(#nodes), - returnValue: <_i13.NodeModel>[], - ) - as List<_i13.NodeModel>); + List<_i13.NodeModel> get nodes => (super.noSuchMethod( + Invocation.getter(#nodes), + returnValue: <_i13.NodeModel>[], + ) as List<_i13.NodeModel>); @override bool get hasListeners => @@ -227,45 +202,39 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { as bool); @override - _i10.Future updateDefaults() => - (super.noSuchMethod( - Invocation.method(#updateDefaults, []), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + _i10.Future updateDefaults() => (super.noSuchMethod( + Invocation.method(#updateDefaults, []), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override _i10.Future setPrimaryNodeFor({ required _i4.CryptoCurrency? coin, required _i13.NodeModel? node, bool? shouldNotifyListeners = false, - }) => - (super.noSuchMethod( - Invocation.method(#setPrimaryNodeFor, [], { - #coin: coin, - #node: node, - #shouldNotifyListeners: shouldNotifyListeners, - }), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + }) => (super.noSuchMethod( + Invocation.method(#setPrimaryNodeFor, [], { + #coin: coin, + #node: node, + #shouldNotifyListeners: shouldNotifyListeners, + }), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override _i13.NodeModel? getPrimaryNodeFor({required _i4.CryptoCurrency? currency}) => (super.noSuchMethod( - Invocation.method(#getPrimaryNodeFor, [], {#currency: currency}), - ) - as _i13.NodeModel?); + Invocation.method(#getPrimaryNodeFor, [], {#currency: currency}), + ) as _i13.NodeModel?); @override List<_i13.NodeModel> getNodesFor(_i4.CryptoCurrency? coin) => (super.noSuchMethod( - Invocation.method(#getNodesFor, [coin]), - returnValue: <_i13.NodeModel>[], - ) - as List<_i13.NodeModel>); + Invocation.method(#getNodesFor, [coin]), + returnValue: <_i13.NodeModel>[], + ) as List<_i13.NodeModel>); @override _i13.NodeModel? getNodeById({required String? id}) => @@ -275,127 +244,161 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { @override List<_i13.NodeModel> failoverNodesFor({ required _i4.CryptoCurrency? currency, - }) => - (super.noSuchMethod( - Invocation.method(#failoverNodesFor, [], {#currency: currency}), - returnValue: <_i13.NodeModel>[], - ) - as List<_i13.NodeModel>); + }) => (super.noSuchMethod( + Invocation.method(#failoverNodesFor, [], {#currency: currency}), + returnValue: <_i13.NodeModel>[], + ) as List<_i13.NodeModel>); @override _i10.Future save( _i13.NodeModel? node, String? password, bool? shouldNotifyListeners, - ) => + ) => (super.noSuchMethod( + Invocation.method(#save, [node, password, shouldNotifyListeners]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); + + @override + _i10.Future saveEdit( + _i13.NodeModel? node, + String? password, + bool? notify, { + required _i13.NodeModel? opened, + bool? forgetCertificate = false, + }) => (super.noSuchMethod( + Invocation.method( + #saveEdit, + [node, password, notify], + {#opened: opened, #forgetCertificate: forgetCertificate}, + ), + returnValue: _i10.Future.value(false), + ) as _i10.Future); + + @override + bool prepareCertificateTest(_i14.NodeFormData? data) => (super.noSuchMethod( + Invocation.method(#prepareCertificateTest, [data]), + returnValue: false, + ) as bool); + + @override + _i10.Future saveTrustedCertificate( + _i13.NodeModel? node, + _i14.NodeFormData? data, + ) => (super.noSuchMethod( + Invocation.method(#saveTrustedCertificate, [node, data]), + returnValue: _i10.Future.value(false), + ) as _i10.Future); + + @override + _i10.Future forgetTrustedCertificate(_i13.NodeModel? shown) => (super.noSuchMethod( - Invocation.method(#save, [node, password, shouldNotifyListeners]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + Invocation.method(#forgetTrustedCertificate, [shown]), + returnValue: _i10.Future.value(false), + ) as _i10.Future); + + @override + _i10.Future commitConnectionTest({ + required _i13.NodeModel? node, + required _i14.NodeFormData? formData, + }) => (super.noSuchMethod( + Invocation.method(#commitConnectionTest, [], { + #node: node, + #formData: formData, + }), + returnValue: _i10.Future.value(false), + ) as _i10.Future); + + @override + _i10.Future setFailover(String? id, bool? value) => (super.noSuchMethod( + Invocation.method(#setFailover, [id, value]), + returnValue: _i10.Future.value(false), + ) as _i10.Future); @override _i10.Future delete(String? id, bool? shouldNotifyListeners) => (super.noSuchMethod( - Invocation.method(#delete, [id, shouldNotifyListeners]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + Invocation.method(#delete, [id, shouldNotifyListeners]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override _i10.Future setEnabledState( String? id, bool? enabled, bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method(#setEnabledState, [ - id, - enabled, - shouldNotifyListeners, - ]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); - - @override - _i10.Future updateDefaultEpicBoxes() => - (super.noSuchMethod( - Invocation.method(#updateDefaultEpicBoxes, []), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + ) => (super.noSuchMethod( + Invocation.method(#setEnabledState, [id, enabled, shouldNotifyListeners]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); + + @override + _i10.Future updateDefaultEpicBoxes() => (super.noSuchMethod( + Invocation.method(#updateDefaultEpicBoxes, []), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override _i10.Future setPrimaryEpicBox({ - required _i14.EpicBoxServerModel? epicBox, + required _i15.EpicBoxServerModel? epicBox, bool? shouldNotifyListeners = false, - }) => - (super.noSuchMethod( - Invocation.method(#setPrimaryEpicBox, [], { - #epicBox: epicBox, - #shouldNotifyListeners: shouldNotifyListeners, - }), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + }) => (super.noSuchMethod( + Invocation.method(#setPrimaryEpicBox, [], { + #epicBox: epicBox, + #shouldNotifyListeners: shouldNotifyListeners, + }), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - List<_i14.EpicBoxServerModel> getEpicBoxes() => - (super.noSuchMethod( - Invocation.method(#getEpicBoxes, []), - returnValue: <_i14.EpicBoxServerModel>[], - ) - as List<_i14.EpicBoxServerModel>); + List<_i15.EpicBoxServerModel> getEpicBoxes() => (super.noSuchMethod( + Invocation.method(#getEpicBoxes, []), + returnValue: <_i15.EpicBoxServerModel>[], + ) as List<_i15.EpicBoxServerModel>); @override - _i14.EpicBoxServerModel? getEpicBoxById({required String? id}) => + _i15.EpicBoxServerModel? getEpicBoxById({required String? id}) => (super.noSuchMethod(Invocation.method(#getEpicBoxById, [], {#id: id})) - as _i14.EpicBoxServerModel?); + as _i15.EpicBoxServerModel?); @override _i10.Future addEpicBox( - _i14.EpicBoxServerModel? epicBox, + _i15.EpicBoxServerModel? epicBox, bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method(#addEpicBox, [epicBox, shouldNotifyListeners]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + ) => (super.noSuchMethod( + Invocation.method(#addEpicBox, [epicBox, shouldNotifyListeners]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override _i10.Future deleteEpicBox(String? id, bool? shouldNotifyListeners) => (super.noSuchMethod( - Invocation.method(#deleteEpicBox, [id, shouldNotifyListeners]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + Invocation.method(#deleteEpicBox, [id, shouldNotifyListeners]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i10.Future updateCommunityNodes() => - (super.noSuchMethod( - Invocation.method(#updateCommunityNodes, []), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + _i10.Future updateCommunityNodes() => (super.noSuchMethod( + Invocation.method(#updateCommunityNodes, []), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - void addListener(_i15.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i16.VoidCallback? listener) => super.noSuchMethod( Invocation.method(#addListener, [listener]), returnValueForMissingStub: null, ); @override - void removeListener(_i15.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i16.VoidCallback? listener) => super.noSuchMethod( Invocation.method(#removeListener, [listener]), returnValueForMissingStub: null, ); @@ -416,21 +419,16 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { /// A class which mocks [LocaleService]. /// /// See the documentation for Mockito's code generation for more information. -class MockLocaleService extends _i1.Mock implements _i16.LocaleService { +class MockLocaleService extends _i1.Mock implements _i17.LocaleService { MockLocaleService() { _i1.throwOnMissingStub(this); } @override - String get locale => - (super.noSuchMethod( - Invocation.getter(#locale), - returnValue: _i17.dummyValue( - this, - Invocation.getter(#locale), - ), - ) - as String); + String get locale => (super.noSuchMethod( + Invocation.getter(#locale), + returnValue: _i18.dummyValue(this, Invocation.getter(#locale)), + ) as String); @override bool get hasListeners => @@ -438,22 +436,20 @@ class MockLocaleService extends _i1.Mock implements _i16.LocaleService { as bool); @override - _i10.Future loadLocale({bool? notify = true}) => - (super.noSuchMethod( - Invocation.method(#loadLocale, [], {#notify: notify}), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + _i10.Future loadLocale({bool? notify = true}) => (super.noSuchMethod( + Invocation.method(#loadLocale, [], {#notify: notify}), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - void addListener(_i15.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i16.VoidCallback? listener) => super.noSuchMethod( Invocation.method(#addListener, [listener]), returnValueForMissingStub: null, ); @override - void removeListener(_i15.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i16.VoidCallback? listener) => super.noSuchMethod( Invocation.method(#removeListener, [listener]), returnValueForMissingStub: null, ); @@ -474,34 +470,28 @@ class MockLocaleService extends _i1.Mock implements _i16.LocaleService { /// A class which mocks [ThemeService]. /// /// See the documentation for Mockito's code generation for more information. -class MockThemeService extends _i1.Mock implements _i18.ThemeService { +class MockThemeService extends _i1.Mock implements _i19.ThemeService { MockThemeService() { _i1.throwOnMissingStub(this); } @override - _i3.MainDB get db => - (super.noSuchMethod( - Invocation.getter(#db), - returnValue: _FakeMainDB_1(this, Invocation.getter(#db)), - ) - as _i3.MainDB); + _i3.MainDB get db => (super.noSuchMethod( + Invocation.getter(#db), + returnValue: _FakeMainDB_1(this, Invocation.getter(#db)), + ) as _i3.MainDB); @override - _i7.HTTP get client => - (super.noSuchMethod( - Invocation.getter(#client), - returnValue: _FakeHTTP_4(this, Invocation.getter(#client)), - ) - as _i7.HTTP); + _i7.HTTP get client => (super.noSuchMethod( + Invocation.getter(#client), + returnValue: _FakeHTTP_4(this, Invocation.getter(#client)), + ) as _i7.HTTP); @override - List<_i19.StackTheme> get installedThemes => - (super.noSuchMethod( - Invocation.getter(#installedThemes), - returnValue: <_i19.StackTheme>[], - ) - as List<_i19.StackTheme>); + List<_i20.StackTheme> get installedThemes => (super.noSuchMethod( + Invocation.getter(#installedThemes), + returnValue: <_i20.StackTheme>[], + ) as List<_i20.StackTheme>); @override set client(_i7.HTTP? value) => super.noSuchMethod( @@ -516,66 +506,55 @@ class MockThemeService extends _i1.Mock implements _i18.ThemeService { ); @override - _i10.Future install({required _i20.Uint8List? themeArchiveData}) => + _i10.Future install({required _i21.Uint8List? themeArchiveData}) => (super.noSuchMethod( - Invocation.method(#install, [], { - #themeArchiveData: themeArchiveData, - }), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + Invocation.method(#install, [], {#themeArchiveData: themeArchiveData}), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i10.Future remove({required String? themeId}) => - (super.noSuchMethod( - Invocation.method(#remove, [], {#themeId: themeId}), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + _i10.Future remove({required String? themeId}) => (super.noSuchMethod( + Invocation.method(#remove, [], {#themeId: themeId}), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i10.Future checkDefaultThemesOnStartup() => - (super.noSuchMethod( - Invocation.method(#checkDefaultThemesOnStartup, []), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + _i10.Future checkDefaultThemesOnStartup() => (super.noSuchMethod( + Invocation.method(#checkDefaultThemesOnStartup, []), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override _i10.Future verifyInstalled({required String? themeId}) => (super.noSuchMethod( - Invocation.method(#verifyInstalled, [], {#themeId: themeId}), - returnValue: _i10.Future.value(false), - ) - as _i10.Future); + Invocation.method(#verifyInstalled, [], {#themeId: themeId}), + returnValue: _i10.Future.value(false), + ) as _i10.Future); @override - _i10.Future> fetchThemes() => + _i10.Future> fetchThemes() => (super.noSuchMethod( - Invocation.method(#fetchThemes, []), - returnValue: _i10.Future>.value( - <_i18.StackThemeMetaData>[], - ), - ) - as _i10.Future>); + Invocation.method(#fetchThemes, []), + returnValue: _i10.Future>.value( + <_i19.StackThemeMetaData>[], + ), + ) as _i10.Future>); @override - _i10.Future<_i20.Uint8List> fetchTheme({ - required _i18.StackThemeMetaData? themeMetaData, - }) => - (super.noSuchMethod( - Invocation.method(#fetchTheme, [], {#themeMetaData: themeMetaData}), - returnValue: _i10.Future<_i20.Uint8List>.value(_i20.Uint8List(0)), - ) - as _i10.Future<_i20.Uint8List>); + _i10.Future<_i21.Uint8List> fetchTheme({ + required _i19.StackThemeMetaData? themeMetaData, + }) => (super.noSuchMethod( + Invocation.method(#fetchTheme, [], {#themeMetaData: themeMetaData}), + returnValue: _i10.Future<_i21.Uint8List>.value(_i21.Uint8List(0)), + ) as _i10.Future<_i21.Uint8List>); @override - _i19.StackTheme? getTheme({required String? themeId}) => + _i20.StackTheme? getTheme({required String? themeId}) => (super.noSuchMethod(Invocation.method(#getTheme, [], {#themeId: themeId})) - as _i19.StackTheme?); + as _i20.StackTheme?); } /// A class which mocks [Prefs]. @@ -592,12 +571,10 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { as bool); @override - int get lastUnlockedTimeout => - (super.noSuchMethod( - Invocation.getter(#lastUnlockedTimeout), - returnValue: 0, - ) - as int); + int get lastUnlockedTimeout => (super.noSuchMethod( + Invocation.getter(#lastUnlockedTimeout), + returnValue: 0, + ) as int); @override int get lastUnlocked => @@ -605,28 +582,22 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { as int); @override - int get currentNotificationId => - (super.noSuchMethod( - Invocation.getter(#currentNotificationId), - returnValue: 0, - ) - as int); + int get currentNotificationId => (super.noSuchMethod( + Invocation.getter(#currentNotificationId), + returnValue: 0, + ) as int); @override - List get walletIdsSyncOnStartup => - (super.noSuchMethod( - Invocation.getter(#walletIdsSyncOnStartup), - returnValue: [], - ) - as List); + List get walletIdsSyncOnStartup => (super.noSuchMethod( + Invocation.getter(#walletIdsSyncOnStartup), + returnValue: [], + ) as List); @override - _i21.SyncingType get syncType => - (super.noSuchMethod( - Invocation.getter(#syncType), - returnValue: _i21.SyncingType.currentWalletOnly, - ) - as _i21.SyncingType); + _i22.SyncingType get syncType => (super.noSuchMethod( + Invocation.getter(#syncType), + returnValue: _i22.SyncingType.currentWalletOnly, + ) as _i22.SyncingType); @override bool get wifiOnly => @@ -634,34 +605,22 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { as bool); @override - bool get showFavoriteWallets => - (super.noSuchMethod( - Invocation.getter(#showFavoriteWallets), - returnValue: false, - ) - as bool); + bool get showFavoriteWallets => (super.noSuchMethod( + Invocation.getter(#showFavoriteWallets), + returnValue: false, + ) as bool); @override - String get language => - (super.noSuchMethod( - Invocation.getter(#language), - returnValue: _i17.dummyValue( - this, - Invocation.getter(#language), - ), - ) - as String); + String get language => (super.noSuchMethod( + Invocation.getter(#language), + returnValue: _i18.dummyValue(this, Invocation.getter(#language)), + ) as String); @override - String get currency => - (super.noSuchMethod( - Invocation.getter(#currency), - returnValue: _i17.dummyValue( - this, - Invocation.getter(#currency), - ), - ) - as String); + String get currency => (super.noSuchMethod( + Invocation.getter(#currency), + returnValue: _i18.dummyValue(this, Invocation.getter(#currency)), + ) as String); @override bool get randomizePIN => @@ -684,12 +643,10 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { as bool); @override - bool get biometricsDuress => - (super.noSuchMethod( - Invocation.getter(#biometricsDuress), - returnValue: false, - ) - as bool); + bool get biometricsDuress => (super.noSuchMethod( + Invocation.getter(#biometricsDuress), + returnValue: false, + ) as bool); @override int get familiarity => @@ -702,44 +659,34 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { as bool); @override - bool get showTestNetCoins => - (super.noSuchMethod( - Invocation.getter(#showTestNetCoins), - returnValue: false, - ) - as bool); + bool get showTestNetCoins => (super.noSuchMethod( + Invocation.getter(#showTestNetCoins), + returnValue: false, + ) as bool); @override - bool get isAutoBackupEnabled => - (super.noSuchMethod( - Invocation.getter(#isAutoBackupEnabled), - returnValue: false, - ) - as bool); + bool get isAutoBackupEnabled => (super.noSuchMethod( + Invocation.getter(#isAutoBackupEnabled), + returnValue: false, + ) as bool); @override - _i22.BackupFrequencyType get backupFrequencyType => - (super.noSuchMethod( - Invocation.getter(#backupFrequencyType), - returnValue: _i22.BackupFrequencyType.everyTenMinutes, - ) - as _i22.BackupFrequencyType); + _i23.BackupFrequencyType get backupFrequencyType => (super.noSuchMethod( + Invocation.getter(#backupFrequencyType), + returnValue: _i23.BackupFrequencyType.everyTenMinutes, + ) as _i23.BackupFrequencyType); @override - bool get hideBlockExplorerWarning => - (super.noSuchMethod( - Invocation.getter(#hideBlockExplorerWarning), - returnValue: false, - ) - as bool); + bool get hideBlockExplorerWarning => (super.noSuchMethod( + Invocation.getter(#hideBlockExplorerWarning), + returnValue: false, + ) as bool); @override - bool get gotoWalletOnStartup => - (super.noSuchMethod( - Invocation.getter(#gotoWalletOnStartup), - returnValue: false, - ) - as bool); + bool get gotoWalletOnStartup => (super.noSuchMethod( + Invocation.getter(#gotoWalletOnStartup), + returnValue: false, + ) as bool); @override bool get externalCalls => @@ -747,53 +694,40 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { as bool); @override - bool get enableCoinControl => - (super.noSuchMethod( - Invocation.getter(#enableCoinControl), - returnValue: false, - ) - as bool); + bool get enableCoinControl => (super.noSuchMethod( + Invocation.getter(#enableCoinControl), + returnValue: false, + ) as bool); @override - bool get enableSystemBrightness => - (super.noSuchMethod( - Invocation.getter(#enableSystemBrightness), - returnValue: false, - ) - as bool); + bool get enableSystemBrightness => (super.noSuchMethod( + Invocation.getter(#enableSystemBrightness), + returnValue: false, + ) as bool); @override - String get themeId => - (super.noSuchMethod( - Invocation.getter(#themeId), - returnValue: _i17.dummyValue( - this, - Invocation.getter(#themeId), - ), - ) - as String); + String get themeId => (super.noSuchMethod( + Invocation.getter(#themeId), + returnValue: _i18.dummyValue(this, Invocation.getter(#themeId)), + ) as String); @override - String get systemBrightnessLightThemeId => - (super.noSuchMethod( - Invocation.getter(#systemBrightnessLightThemeId), - returnValue: _i17.dummyValue( - this, - Invocation.getter(#systemBrightnessLightThemeId), - ), - ) - as String); + String get systemBrightnessLightThemeId => (super.noSuchMethod( + Invocation.getter(#systemBrightnessLightThemeId), + returnValue: _i18.dummyValue( + this, + Invocation.getter(#systemBrightnessLightThemeId), + ), + ) as String); @override - String get systemBrightnessDarkThemeId => - (super.noSuchMethod( - Invocation.getter(#systemBrightnessDarkThemeId), - returnValue: _i17.dummyValue( - this, - Invocation.getter(#systemBrightnessDarkThemeId), - ), - ) - as String); + String get systemBrightnessDarkThemeId => (super.noSuchMethod( + Invocation.getter(#systemBrightnessDarkThemeId), + returnValue: _i18.dummyValue( + this, + Invocation.getter(#systemBrightnessDarkThemeId), + ), + ) as String); @override bool get useTor => @@ -806,36 +740,28 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { as bool); @override - bool get enableExchange => - (super.noSuchMethod( - Invocation.getter(#enableExchange), - returnValue: false, - ) - as bool); + bool get enableExchange => (super.noSuchMethod( + Invocation.getter(#enableExchange), + returnValue: false, + ) as bool); @override - bool get advancedFiroFeatures => - (super.noSuchMethod( - Invocation.getter(#advancedFiroFeatures), - returnValue: false, - ) - as bool); + bool get advancedFiroFeatures => (super.noSuchMethod( + Invocation.getter(#advancedFiroFeatures), + returnValue: false, + ) as bool); @override - _i23.Level get logLevel => - (super.noSuchMethod( - Invocation.getter(#logLevel), - returnValue: _i23.Level.all, - ) - as _i23.Level); + _i24.Level get logLevel => (super.noSuchMethod( + Invocation.getter(#logLevel), + returnValue: _i24.Level.all, + ) as _i24.Level); @override - ({bool enabled, int minutes}) get autoLockInfo => - (super.noSuchMethod( - Invocation.getter(#autoLockInfo), - returnValue: (enabled: false, minutes: 0), - ) - as ({bool enabled, int minutes})); + ({bool enabled, int minutes}) get autoLockInfo => (super.noSuchMethod( + Invocation.getter(#autoLockInfo), + returnValue: (enabled: false, minutes: 0), + ) as ({bool enabled, int minutes})); @override bool get privacyScreen => @@ -843,12 +769,10 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { as bool); @override - bool get disableScreenShots => - (super.noSuchMethod( - Invocation.getter(#disableScreenShots), - returnValue: false, - ) - as bool); + bool get disableScreenShots => (super.noSuchMethod( + Invocation.getter(#disableScreenShots), + returnValue: false, + ) as bool); @override set lastUnlockedTimeout(int? lastUnlockedTimeout) => super.noSuchMethod( @@ -870,7 +794,7 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { ); @override - set syncType(_i21.SyncingType? syncType) => super.noSuchMethod( + set syncType(_i22.SyncingType? syncType) => super.noSuchMethod( Invocation.setter(#syncType, syncType), returnValueForMissingStub: null, ); @@ -960,7 +884,7 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { ); @override - set backupFrequencyType(_i22.BackupFrequencyType? backupFrequencyType) => + set backupFrequencyType(_i23.BackupFrequencyType? backupFrequencyType) => super.noSuchMethod( Invocation.setter(#backupFrequencyType, backupFrequencyType), returnValueForMissingStub: null, @@ -1067,7 +991,7 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { ); @override - set logLevel(_i23.Level? logLevel) => super.noSuchMethod( + set logLevel(_i24.Level? logLevel) => super.noSuchMethod( Invocation.setter(#logLevel, logLevel), returnValueForMissingStub: null, ); @@ -1097,60 +1021,48 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { as bool); @override - _i10.Future init() => - (super.noSuchMethod( - Invocation.method(#init, []), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + _i10.Future init() => (super.noSuchMethod( + Invocation.method(#init, []), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i10.Future incrementCurrentNotificationIndex() => - (super.noSuchMethod( - Invocation.method(#incrementCurrentNotificationIndex, []), - returnValue: _i10.Future.value(0), - ) - as _i10.Future); + _i10.Future incrementCurrentNotificationIndex() => (super.noSuchMethod( + Invocation.method(#incrementCurrentNotificationIndex, []), + returnValue: _i10.Future.value(0), + ) as _i10.Future); @override - _i10.Future isExternalCallsSet() => - (super.noSuchMethod( - Invocation.method(#isExternalCallsSet, []), - returnValue: _i10.Future.value(false), - ) - as _i10.Future); + _i10.Future isExternalCallsSet() => (super.noSuchMethod( + Invocation.method(#isExternalCallsSet, []), + returnValue: _i10.Future.value(false), + ) as _i10.Future); @override - _i10.Future saveUserID(String? userId) => - (super.noSuchMethod( - Invocation.method(#saveUserID, [userId]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + _i10.Future saveUserID(String? userId) => (super.noSuchMethod( + Invocation.method(#saveUserID, [userId]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i10.Future saveSignupEpoch(int? signupEpoch) => - (super.noSuchMethod( - Invocation.method(#saveSignupEpoch, [signupEpoch]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + _i10.Future saveSignupEpoch(int? signupEpoch) => (super.noSuchMethod( + Invocation.method(#saveSignupEpoch, [signupEpoch]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i24.AmountUnit amountUnit(_i4.CryptoCurrency? coin) => - (super.noSuchMethod( - Invocation.method(#amountUnit, [coin]), - returnValue: _i24.AmountUnit.normal, - ) - as _i24.AmountUnit); + _i25.AmountUnit amountUnit(_i4.CryptoCurrency? coin) => (super.noSuchMethod( + Invocation.method(#amountUnit, [coin]), + returnValue: _i25.AmountUnit.normal, + ) as _i25.AmountUnit); @override void updateAmountUnit({ required _i4.CryptoCurrency? coin, - required _i24.AmountUnit? amountUnit, + required _i25.AmountUnit? amountUnit, }) => super.noSuchMethod( Invocation.method(#updateAmountUnit, [], { #coin: coin, @@ -1160,12 +1072,10 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { ); @override - int maxDecimals(_i4.CryptoCurrency? coin) => - (super.noSuchMethod( - Invocation.method(#maxDecimals, [coin]), - returnValue: 0, - ) - as int); + int maxDecimals(_i4.CryptoCurrency? coin) => (super.noSuchMethod( + Invocation.method(#maxDecimals, [coin]), + returnValue: 0, + ) as int); @override void updateMaxDecimals({ @@ -1182,13 +1092,12 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { @override _i8.FusionInfo getFusionServerInfo(_i4.CryptoCurrency? coin) => (super.noSuchMethod( - Invocation.method(#getFusionServerInfo, [coin]), - returnValue: _FakeFusionInfo_5( - this, - Invocation.method(#getFusionServerInfo, [coin]), - ), - ) - as _i8.FusionInfo); + Invocation.method(#getFusionServerInfo, [coin]), + returnValue: _FakeFusionInfo_5( + this, + Invocation.method(#getFusionServerInfo, [coin]), + ), + ) as _i8.FusionInfo); @override void setFusionServerInfo( @@ -1200,13 +1109,13 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { ); @override - void addListener(_i15.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i16.VoidCallback? listener) => super.noSuchMethod( Invocation.method(#addListener, [listener]), returnValueForMissingStub: null, ); @override - void removeListener(_i15.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i16.VoidCallback? listener) => super.noSuchMethod( Invocation.method(#removeListener, [listener]), returnValueForMissingStub: null, ); diff --git a/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart b/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart index 4f133dc983..959ee35177 100644 --- a/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart @@ -8,7 +8,8 @@ import 'dart:ui' as _i7; import 'package:flutter/material.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/models/epicbox_server_model.dart' as _i11; +import 'package:stackwallet/models/epicbox_server_model.dart' as _i12; +import 'package:stackwallet/models/node_form_data.dart' as _i11; import 'package:stackwallet/models/node_model.dart' as _i9; import 'package:stackwallet/services/node_service.dart' as _i8; import 'package:stackwallet/services/wallets_service.dart' as _i6; @@ -56,15 +57,14 @@ class MockBarcodeScannerWrapper extends _i1.Mock @override _i4.Future<_i2.ScanResult> scan({required _i5.BuildContext? context}) => (super.noSuchMethod( + Invocation.method(#scan, [], {#context: context}), + returnValue: _i4.Future<_i2.ScanResult>.value( + _FakeScanResult_0( + this, Invocation.method(#scan, [], {#context: context}), - returnValue: _i4.Future<_i2.ScanResult>.value( - _FakeScanResult_0( - this, - Invocation.method(#scan, [], {#context: context}), - ), - ), - ) - as _i4.Future<_i2.ScanResult>); + ), + ), + ) as _i4.Future<_i2.ScanResult>); } /// A class which mocks [WalletsService]. @@ -78,12 +78,11 @@ class MockWalletsService extends _i1.Mock implements _i6.WalletsService { @override _i4.Future> get walletNames => (super.noSuchMethod( - Invocation.getter(#walletNames), - returnValue: _i4.Future>.value( - {}, - ), - ) - as _i4.Future>); + Invocation.getter(#walletNames), + returnValue: _i4.Future>.value( + {}, + ), + ) as _i4.Future>); @override bool get hasListeners => @@ -124,31 +123,25 @@ class MockNodeService extends _i1.Mock implements _i8.NodeService { } @override - _i3.SecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( - Invocation.getter(#secureStorageInterface), - returnValue: _FakeSecureStorageInterface_1( - this, - Invocation.getter(#secureStorageInterface), - ), - ) - as _i3.SecureStorageInterface); + _i3.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( + Invocation.getter(#secureStorageInterface), + returnValue: _FakeSecureStorageInterface_1( + this, + Invocation.getter(#secureStorageInterface), + ), + ) as _i3.SecureStorageInterface); @override - List<_i9.NodeModel> get primaryNodes => - (super.noSuchMethod( - Invocation.getter(#primaryNodes), - returnValue: <_i9.NodeModel>[], - ) - as List<_i9.NodeModel>); + List<_i9.NodeModel> get primaryNodes => (super.noSuchMethod( + Invocation.getter(#primaryNodes), + returnValue: <_i9.NodeModel>[], + ) as List<_i9.NodeModel>); @override - List<_i9.NodeModel> get nodes => - (super.noSuchMethod( - Invocation.getter(#nodes), - returnValue: <_i9.NodeModel>[], - ) - as List<_i9.NodeModel>); + List<_i9.NodeModel> get nodes => (super.noSuchMethod( + Invocation.getter(#nodes), + returnValue: <_i9.NodeModel>[], + ) as List<_i9.NodeModel>); @override bool get hasListeners => @@ -156,45 +149,39 @@ class MockNodeService extends _i1.Mock implements _i8.NodeService { as bool); @override - _i4.Future updateDefaults() => - (super.noSuchMethod( - Invocation.method(#updateDefaults, []), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) - as _i4.Future); + _i4.Future updateDefaults() => (super.noSuchMethod( + Invocation.method(#updateDefaults, []), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override _i4.Future setPrimaryNodeFor({ required _i10.CryptoCurrency? coin, required _i9.NodeModel? node, bool? shouldNotifyListeners = false, - }) => - (super.noSuchMethod( - Invocation.method(#setPrimaryNodeFor, [], { - #coin: coin, - #node: node, - #shouldNotifyListeners: shouldNotifyListeners, - }), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) - as _i4.Future); + }) => (super.noSuchMethod( + Invocation.method(#setPrimaryNodeFor, [], { + #coin: coin, + #node: node, + #shouldNotifyListeners: shouldNotifyListeners, + }), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override _i9.NodeModel? getPrimaryNodeFor({required _i10.CryptoCurrency? currency}) => (super.noSuchMethod( - Invocation.method(#getPrimaryNodeFor, [], {#currency: currency}), - ) - as _i9.NodeModel?); + Invocation.method(#getPrimaryNodeFor, [], {#currency: currency}), + ) as _i9.NodeModel?); @override List<_i9.NodeModel> getNodesFor(_i10.CryptoCurrency? coin) => (super.noSuchMethod( - Invocation.method(#getNodesFor, [coin]), - returnValue: <_i9.NodeModel>[], - ) - as List<_i9.NodeModel>); + Invocation.method(#getNodesFor, [coin]), + returnValue: <_i9.NodeModel>[], + ) as List<_i9.NodeModel>); @override _i9.NodeModel? getNodeById({required String? id}) => @@ -204,118 +191,152 @@ class MockNodeService extends _i1.Mock implements _i8.NodeService { @override List<_i9.NodeModel> failoverNodesFor({ required _i10.CryptoCurrency? currency, - }) => - (super.noSuchMethod( - Invocation.method(#failoverNodesFor, [], {#currency: currency}), - returnValue: <_i9.NodeModel>[], - ) - as List<_i9.NodeModel>); + }) => (super.noSuchMethod( + Invocation.method(#failoverNodesFor, [], {#currency: currency}), + returnValue: <_i9.NodeModel>[], + ) as List<_i9.NodeModel>); @override _i4.Future save( _i9.NodeModel? node, String? password, bool? shouldNotifyListeners, - ) => + ) => (super.noSuchMethod( + Invocation.method(#save, [node, password, shouldNotifyListeners]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + + @override + _i4.Future saveEdit( + _i9.NodeModel? node, + String? password, + bool? notify, { + required _i9.NodeModel? opened, + bool? forgetCertificate = false, + }) => (super.noSuchMethod( + Invocation.method( + #saveEdit, + [node, password, notify], + {#opened: opened, #forgetCertificate: forgetCertificate}, + ), + returnValue: _i4.Future.value(false), + ) as _i4.Future); + + @override + bool prepareCertificateTest(_i11.NodeFormData? data) => (super.noSuchMethod( + Invocation.method(#prepareCertificateTest, [data]), + returnValue: false, + ) as bool); + + @override + _i4.Future saveTrustedCertificate( + _i9.NodeModel? node, + _i11.NodeFormData? data, + ) => (super.noSuchMethod( + Invocation.method(#saveTrustedCertificate, [node, data]), + returnValue: _i4.Future.value(false), + ) as _i4.Future); + + @override + _i4.Future forgetTrustedCertificate(_i9.NodeModel? shown) => (super.noSuchMethod( - Invocation.method(#save, [node, password, shouldNotifyListeners]), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) - as _i4.Future); + Invocation.method(#forgetTrustedCertificate, [shown]), + returnValue: _i4.Future.value(false), + ) as _i4.Future); + + @override + _i4.Future commitConnectionTest({ + required _i9.NodeModel? node, + required _i11.NodeFormData? formData, + }) => (super.noSuchMethod( + Invocation.method(#commitConnectionTest, [], { + #node: node, + #formData: formData, + }), + returnValue: _i4.Future.value(false), + ) as _i4.Future); + + @override + _i4.Future setFailover(String? id, bool? value) => (super.noSuchMethod( + Invocation.method(#setFailover, [id, value]), + returnValue: _i4.Future.value(false), + ) as _i4.Future); @override _i4.Future delete(String? id, bool? shouldNotifyListeners) => (super.noSuchMethod( - Invocation.method(#delete, [id, shouldNotifyListeners]), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) - as _i4.Future); + Invocation.method(#delete, [id, shouldNotifyListeners]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override _i4.Future setEnabledState( String? id, bool? enabled, bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method(#setEnabledState, [ - id, - enabled, - shouldNotifyListeners, - ]), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) - as _i4.Future); - - @override - _i4.Future updateDefaultEpicBoxes() => - (super.noSuchMethod( - Invocation.method(#updateDefaultEpicBoxes, []), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) - as _i4.Future); + ) => (super.noSuchMethod( + Invocation.method(#setEnabledState, [id, enabled, shouldNotifyListeners]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + + @override + _i4.Future updateDefaultEpicBoxes() => (super.noSuchMethod( + Invocation.method(#updateDefaultEpicBoxes, []), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override _i4.Future setPrimaryEpicBox({ - required _i11.EpicBoxServerModel? epicBox, + required _i12.EpicBoxServerModel? epicBox, bool? shouldNotifyListeners = false, - }) => - (super.noSuchMethod( - Invocation.method(#setPrimaryEpicBox, [], { - #epicBox: epicBox, - #shouldNotifyListeners: shouldNotifyListeners, - }), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) - as _i4.Future); + }) => (super.noSuchMethod( + Invocation.method(#setPrimaryEpicBox, [], { + #epicBox: epicBox, + #shouldNotifyListeners: shouldNotifyListeners, + }), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - List<_i11.EpicBoxServerModel> getEpicBoxes() => - (super.noSuchMethod( - Invocation.method(#getEpicBoxes, []), - returnValue: <_i11.EpicBoxServerModel>[], - ) - as List<_i11.EpicBoxServerModel>); + List<_i12.EpicBoxServerModel> getEpicBoxes() => (super.noSuchMethod( + Invocation.method(#getEpicBoxes, []), + returnValue: <_i12.EpicBoxServerModel>[], + ) as List<_i12.EpicBoxServerModel>); @override - _i11.EpicBoxServerModel? getEpicBoxById({required String? id}) => + _i12.EpicBoxServerModel? getEpicBoxById({required String? id}) => (super.noSuchMethod(Invocation.method(#getEpicBoxById, [], {#id: id})) - as _i11.EpicBoxServerModel?); + as _i12.EpicBoxServerModel?); @override _i4.Future addEpicBox( - _i11.EpicBoxServerModel? epicBox, + _i12.EpicBoxServerModel? epicBox, bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method(#addEpicBox, [epicBox, shouldNotifyListeners]), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) - as _i4.Future); + ) => (super.noSuchMethod( + Invocation.method(#addEpicBox, [epicBox, shouldNotifyListeners]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override _i4.Future deleteEpicBox(String? id, bool? shouldNotifyListeners) => (super.noSuchMethod( - Invocation.method(#deleteEpicBox, [id, shouldNotifyListeners]), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) - as _i4.Future); + Invocation.method(#deleteEpicBox, [id, shouldNotifyListeners]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i4.Future updateCommunityNodes() => - (super.noSuchMethod( - Invocation.method(#updateCommunityNodes, []), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) - as _i4.Future); + _i4.Future updateCommunityNodes() => (super.noSuchMethod( + Invocation.method(#updateCommunityNodes, []), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override void addListener(_i7.VoidCallback? listener) => super.noSuchMethod( diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart index 2e86d9e57e..3960840952 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart @@ -4,10 +4,11 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i5; -import 'dart:ui' as _i8; +import 'dart:ui' as _i9; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/models/epicbox_server_model.dart' as _i7; +import 'package:stackwallet/models/epicbox_server_model.dart' as _i8; +import 'package:stackwallet/models/node_form_data.dart' as _i7; import 'package:stackwallet/models/node_model.dart' as _i4; import 'package:stackwallet/services/node_service.dart' as _i3; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' @@ -45,31 +46,25 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { } @override - _i2.SecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( - Invocation.getter(#secureStorageInterface), - returnValue: _FakeSecureStorageInterface_0( - this, - Invocation.getter(#secureStorageInterface), - ), - ) - as _i2.SecureStorageInterface); + _i2.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( + Invocation.getter(#secureStorageInterface), + returnValue: _FakeSecureStorageInterface_0( + this, + Invocation.getter(#secureStorageInterface), + ), + ) as _i2.SecureStorageInterface); @override - List<_i4.NodeModel> get primaryNodes => - (super.noSuchMethod( - Invocation.getter(#primaryNodes), - returnValue: <_i4.NodeModel>[], - ) - as List<_i4.NodeModel>); + List<_i4.NodeModel> get primaryNodes => (super.noSuchMethod( + Invocation.getter(#primaryNodes), + returnValue: <_i4.NodeModel>[], + ) as List<_i4.NodeModel>); @override - List<_i4.NodeModel> get nodes => - (super.noSuchMethod( - Invocation.getter(#nodes), - returnValue: <_i4.NodeModel>[], - ) - as List<_i4.NodeModel>); + List<_i4.NodeModel> get nodes => (super.noSuchMethod( + Invocation.getter(#nodes), + returnValue: <_i4.NodeModel>[], + ) as List<_i4.NodeModel>); @override bool get hasListeners => @@ -77,45 +72,39 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { as bool); @override - _i5.Future updateDefaults() => - (super.noSuchMethod( - Invocation.method(#updateDefaults, []), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + _i5.Future updateDefaults() => (super.noSuchMethod( + Invocation.method(#updateDefaults, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future setPrimaryNodeFor({ required _i6.CryptoCurrency? coin, required _i4.NodeModel? node, bool? shouldNotifyListeners = false, - }) => - (super.noSuchMethod( - Invocation.method(#setPrimaryNodeFor, [], { - #coin: coin, - #node: node, - #shouldNotifyListeners: shouldNotifyListeners, - }), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + }) => (super.noSuchMethod( + Invocation.method(#setPrimaryNodeFor, [], { + #coin: coin, + #node: node, + #shouldNotifyListeners: shouldNotifyListeners, + }), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i4.NodeModel? getPrimaryNodeFor({required _i6.CryptoCurrency? currency}) => (super.noSuchMethod( - Invocation.method(#getPrimaryNodeFor, [], {#currency: currency}), - ) - as _i4.NodeModel?); + Invocation.method(#getPrimaryNodeFor, [], {#currency: currency}), + ) as _i4.NodeModel?); @override List<_i4.NodeModel> getNodesFor(_i6.CryptoCurrency? coin) => (super.noSuchMethod( - Invocation.method(#getNodesFor, [coin]), - returnValue: <_i4.NodeModel>[], - ) - as List<_i4.NodeModel>); + Invocation.method(#getNodesFor, [coin]), + returnValue: <_i4.NodeModel>[], + ) as List<_i4.NodeModel>); @override _i4.NodeModel? getNodeById({required String? id}) => @@ -125,127 +114,161 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { @override List<_i4.NodeModel> failoverNodesFor({ required _i6.CryptoCurrency? currency, - }) => - (super.noSuchMethod( - Invocation.method(#failoverNodesFor, [], {#currency: currency}), - returnValue: <_i4.NodeModel>[], - ) - as List<_i4.NodeModel>); + }) => (super.noSuchMethod( + Invocation.method(#failoverNodesFor, [], {#currency: currency}), + returnValue: <_i4.NodeModel>[], + ) as List<_i4.NodeModel>); @override _i5.Future save( _i4.NodeModel? node, String? password, bool? shouldNotifyListeners, - ) => + ) => (super.noSuchMethod( + Invocation.method(#save, [node, password, shouldNotifyListeners]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future saveEdit( + _i4.NodeModel? node, + String? password, + bool? notify, { + required _i4.NodeModel? opened, + bool? forgetCertificate = false, + }) => (super.noSuchMethod( + Invocation.method( + #saveEdit, + [node, password, notify], + {#opened: opened, #forgetCertificate: forgetCertificate}, + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + + @override + bool prepareCertificateTest(_i7.NodeFormData? data) => (super.noSuchMethod( + Invocation.method(#prepareCertificateTest, [data]), + returnValue: false, + ) as bool); + + @override + _i5.Future saveTrustedCertificate( + _i4.NodeModel? node, + _i7.NodeFormData? data, + ) => (super.noSuchMethod( + Invocation.method(#saveTrustedCertificate, [node, data]), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + + @override + _i5.Future forgetTrustedCertificate(_i4.NodeModel? shown) => (super.noSuchMethod( - Invocation.method(#save, [node, password, shouldNotifyListeners]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + Invocation.method(#forgetTrustedCertificate, [shown]), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + + @override + _i5.Future commitConnectionTest({ + required _i4.NodeModel? node, + required _i7.NodeFormData? formData, + }) => (super.noSuchMethod( + Invocation.method(#commitConnectionTest, [], { + #node: node, + #formData: formData, + }), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + + @override + _i5.Future setFailover(String? id, bool? value) => (super.noSuchMethod( + Invocation.method(#setFailover, [id, value]), + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override _i5.Future delete(String? id, bool? shouldNotifyListeners) => (super.noSuchMethod( - Invocation.method(#delete, [id, shouldNotifyListeners]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + Invocation.method(#delete, [id, shouldNotifyListeners]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future setEnabledState( String? id, bool? enabled, bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method(#setEnabledState, [ - id, - enabled, - shouldNotifyListeners, - ]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + ) => (super.noSuchMethod( + Invocation.method(#setEnabledState, [id, enabled, shouldNotifyListeners]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i5.Future updateDefaultEpicBoxes() => - (super.noSuchMethod( - Invocation.method(#updateDefaultEpicBoxes, []), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + _i5.Future updateDefaultEpicBoxes() => (super.noSuchMethod( + Invocation.method(#updateDefaultEpicBoxes, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future setPrimaryEpicBox({ - required _i7.EpicBoxServerModel? epicBox, + required _i8.EpicBoxServerModel? epicBox, bool? shouldNotifyListeners = false, - }) => - (super.noSuchMethod( - Invocation.method(#setPrimaryEpicBox, [], { - #epicBox: epicBox, - #shouldNotifyListeners: shouldNotifyListeners, - }), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + }) => (super.noSuchMethod( + Invocation.method(#setPrimaryEpicBox, [], { + #epicBox: epicBox, + #shouldNotifyListeners: shouldNotifyListeners, + }), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - List<_i7.EpicBoxServerModel> getEpicBoxes() => - (super.noSuchMethod( - Invocation.method(#getEpicBoxes, []), - returnValue: <_i7.EpicBoxServerModel>[], - ) - as List<_i7.EpicBoxServerModel>); + List<_i8.EpicBoxServerModel> getEpicBoxes() => (super.noSuchMethod( + Invocation.method(#getEpicBoxes, []), + returnValue: <_i8.EpicBoxServerModel>[], + ) as List<_i8.EpicBoxServerModel>); @override - _i7.EpicBoxServerModel? getEpicBoxById({required String? id}) => + _i8.EpicBoxServerModel? getEpicBoxById({required String? id}) => (super.noSuchMethod(Invocation.method(#getEpicBoxById, [], {#id: id})) - as _i7.EpicBoxServerModel?); + as _i8.EpicBoxServerModel?); @override _i5.Future addEpicBox( - _i7.EpicBoxServerModel? epicBox, + _i8.EpicBoxServerModel? epicBox, bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method(#addEpicBox, [epicBox, shouldNotifyListeners]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + ) => (super.noSuchMethod( + Invocation.method(#addEpicBox, [epicBox, shouldNotifyListeners]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future deleteEpicBox(String? id, bool? shouldNotifyListeners) => (super.noSuchMethod( - Invocation.method(#deleteEpicBox, [id, shouldNotifyListeners]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + Invocation.method(#deleteEpicBox, [id, shouldNotifyListeners]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i5.Future updateCommunityNodes() => - (super.noSuchMethod( - Invocation.method(#updateCommunityNodes, []), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + _i5.Future updateCommunityNodes() => (super.noSuchMethod( + Invocation.method(#updateCommunityNodes, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - void addListener(_i8.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i9.VoidCallback? listener) => super.noSuchMethod( Invocation.method(#addListener, [listener]), returnValueForMissingStub: null, ); @override - void removeListener(_i8.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i9.VoidCallback? listener) => super.noSuchMethod( Invocation.method(#removeListener, [listener]), returnValueForMissingStub: null, ); diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart index eeb6f02923..5cdf46ca75 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart @@ -4,10 +4,11 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i5; -import 'dart:ui' as _i8; +import 'dart:ui' as _i9; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/models/epicbox_server_model.dart' as _i7; +import 'package:stackwallet/models/epicbox_server_model.dart' as _i8; +import 'package:stackwallet/models/node_form_data.dart' as _i7; import 'package:stackwallet/models/node_model.dart' as _i4; import 'package:stackwallet/services/node_service.dart' as _i3; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' @@ -45,31 +46,25 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { } @override - _i2.SecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( - Invocation.getter(#secureStorageInterface), - returnValue: _FakeSecureStorageInterface_0( - this, - Invocation.getter(#secureStorageInterface), - ), - ) - as _i2.SecureStorageInterface); + _i2.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( + Invocation.getter(#secureStorageInterface), + returnValue: _FakeSecureStorageInterface_0( + this, + Invocation.getter(#secureStorageInterface), + ), + ) as _i2.SecureStorageInterface); @override - List<_i4.NodeModel> get primaryNodes => - (super.noSuchMethod( - Invocation.getter(#primaryNodes), - returnValue: <_i4.NodeModel>[], - ) - as List<_i4.NodeModel>); + List<_i4.NodeModel> get primaryNodes => (super.noSuchMethod( + Invocation.getter(#primaryNodes), + returnValue: <_i4.NodeModel>[], + ) as List<_i4.NodeModel>); @override - List<_i4.NodeModel> get nodes => - (super.noSuchMethod( - Invocation.getter(#nodes), - returnValue: <_i4.NodeModel>[], - ) - as List<_i4.NodeModel>); + List<_i4.NodeModel> get nodes => (super.noSuchMethod( + Invocation.getter(#nodes), + returnValue: <_i4.NodeModel>[], + ) as List<_i4.NodeModel>); @override bool get hasListeners => @@ -77,45 +72,39 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { as bool); @override - _i5.Future updateDefaults() => - (super.noSuchMethod( - Invocation.method(#updateDefaults, []), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + _i5.Future updateDefaults() => (super.noSuchMethod( + Invocation.method(#updateDefaults, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future setPrimaryNodeFor({ required _i6.CryptoCurrency? coin, required _i4.NodeModel? node, bool? shouldNotifyListeners = false, - }) => - (super.noSuchMethod( - Invocation.method(#setPrimaryNodeFor, [], { - #coin: coin, - #node: node, - #shouldNotifyListeners: shouldNotifyListeners, - }), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + }) => (super.noSuchMethod( + Invocation.method(#setPrimaryNodeFor, [], { + #coin: coin, + #node: node, + #shouldNotifyListeners: shouldNotifyListeners, + }), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i4.NodeModel? getPrimaryNodeFor({required _i6.CryptoCurrency? currency}) => (super.noSuchMethod( - Invocation.method(#getPrimaryNodeFor, [], {#currency: currency}), - ) - as _i4.NodeModel?); + Invocation.method(#getPrimaryNodeFor, [], {#currency: currency}), + ) as _i4.NodeModel?); @override List<_i4.NodeModel> getNodesFor(_i6.CryptoCurrency? coin) => (super.noSuchMethod( - Invocation.method(#getNodesFor, [coin]), - returnValue: <_i4.NodeModel>[], - ) - as List<_i4.NodeModel>); + Invocation.method(#getNodesFor, [coin]), + returnValue: <_i4.NodeModel>[], + ) as List<_i4.NodeModel>); @override _i4.NodeModel? getNodeById({required String? id}) => @@ -125,127 +114,161 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { @override List<_i4.NodeModel> failoverNodesFor({ required _i6.CryptoCurrency? currency, - }) => - (super.noSuchMethod( - Invocation.method(#failoverNodesFor, [], {#currency: currency}), - returnValue: <_i4.NodeModel>[], - ) - as List<_i4.NodeModel>); + }) => (super.noSuchMethod( + Invocation.method(#failoverNodesFor, [], {#currency: currency}), + returnValue: <_i4.NodeModel>[], + ) as List<_i4.NodeModel>); @override _i5.Future save( _i4.NodeModel? node, String? password, bool? shouldNotifyListeners, - ) => + ) => (super.noSuchMethod( + Invocation.method(#save, [node, password, shouldNotifyListeners]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future saveEdit( + _i4.NodeModel? node, + String? password, + bool? notify, { + required _i4.NodeModel? opened, + bool? forgetCertificate = false, + }) => (super.noSuchMethod( + Invocation.method( + #saveEdit, + [node, password, notify], + {#opened: opened, #forgetCertificate: forgetCertificate}, + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + + @override + bool prepareCertificateTest(_i7.NodeFormData? data) => (super.noSuchMethod( + Invocation.method(#prepareCertificateTest, [data]), + returnValue: false, + ) as bool); + + @override + _i5.Future saveTrustedCertificate( + _i4.NodeModel? node, + _i7.NodeFormData? data, + ) => (super.noSuchMethod( + Invocation.method(#saveTrustedCertificate, [node, data]), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + + @override + _i5.Future forgetTrustedCertificate(_i4.NodeModel? shown) => (super.noSuchMethod( - Invocation.method(#save, [node, password, shouldNotifyListeners]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + Invocation.method(#forgetTrustedCertificate, [shown]), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + + @override + _i5.Future commitConnectionTest({ + required _i4.NodeModel? node, + required _i7.NodeFormData? formData, + }) => (super.noSuchMethod( + Invocation.method(#commitConnectionTest, [], { + #node: node, + #formData: formData, + }), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + + @override + _i5.Future setFailover(String? id, bool? value) => (super.noSuchMethod( + Invocation.method(#setFailover, [id, value]), + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override _i5.Future delete(String? id, bool? shouldNotifyListeners) => (super.noSuchMethod( - Invocation.method(#delete, [id, shouldNotifyListeners]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + Invocation.method(#delete, [id, shouldNotifyListeners]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future setEnabledState( String? id, bool? enabled, bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method(#setEnabledState, [ - id, - enabled, - shouldNotifyListeners, - ]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + ) => (super.noSuchMethod( + Invocation.method(#setEnabledState, [id, enabled, shouldNotifyListeners]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i5.Future updateDefaultEpicBoxes() => - (super.noSuchMethod( - Invocation.method(#updateDefaultEpicBoxes, []), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + _i5.Future updateDefaultEpicBoxes() => (super.noSuchMethod( + Invocation.method(#updateDefaultEpicBoxes, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future setPrimaryEpicBox({ - required _i7.EpicBoxServerModel? epicBox, + required _i8.EpicBoxServerModel? epicBox, bool? shouldNotifyListeners = false, - }) => - (super.noSuchMethod( - Invocation.method(#setPrimaryEpicBox, [], { - #epicBox: epicBox, - #shouldNotifyListeners: shouldNotifyListeners, - }), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + }) => (super.noSuchMethod( + Invocation.method(#setPrimaryEpicBox, [], { + #epicBox: epicBox, + #shouldNotifyListeners: shouldNotifyListeners, + }), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - List<_i7.EpicBoxServerModel> getEpicBoxes() => - (super.noSuchMethod( - Invocation.method(#getEpicBoxes, []), - returnValue: <_i7.EpicBoxServerModel>[], - ) - as List<_i7.EpicBoxServerModel>); + List<_i8.EpicBoxServerModel> getEpicBoxes() => (super.noSuchMethod( + Invocation.method(#getEpicBoxes, []), + returnValue: <_i8.EpicBoxServerModel>[], + ) as List<_i8.EpicBoxServerModel>); @override - _i7.EpicBoxServerModel? getEpicBoxById({required String? id}) => + _i8.EpicBoxServerModel? getEpicBoxById({required String? id}) => (super.noSuchMethod(Invocation.method(#getEpicBoxById, [], {#id: id})) - as _i7.EpicBoxServerModel?); + as _i8.EpicBoxServerModel?); @override _i5.Future addEpicBox( - _i7.EpicBoxServerModel? epicBox, + _i8.EpicBoxServerModel? epicBox, bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method(#addEpicBox, [epicBox, shouldNotifyListeners]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + ) => (super.noSuchMethod( + Invocation.method(#addEpicBox, [epicBox, shouldNotifyListeners]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future deleteEpicBox(String? id, bool? shouldNotifyListeners) => (super.noSuchMethod( - Invocation.method(#deleteEpicBox, [id, shouldNotifyListeners]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + Invocation.method(#deleteEpicBox, [id, shouldNotifyListeners]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i5.Future updateCommunityNodes() => - (super.noSuchMethod( - Invocation.method(#updateCommunityNodes, []), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + _i5.Future updateCommunityNodes() => (super.noSuchMethod( + Invocation.method(#updateCommunityNodes, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - void addListener(_i8.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i9.VoidCallback? listener) => super.noSuchMethod( Invocation.method(#addListener, [listener]), returnValueForMissingStub: null, ); @override - void removeListener(_i8.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i9.VoidCallback? listener) => super.noSuchMethod( Invocation.method(#removeListener, [listener]), returnValueForMissingStub: null, ); diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_view_screen_test.mocks.dart index 0d75015217..af0d5eb6e7 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_view_screen_test.mocks.dart @@ -4,10 +4,11 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i5; -import 'dart:ui' as _i8; +import 'dart:ui' as _i9; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/models/epicbox_server_model.dart' as _i7; +import 'package:stackwallet/models/epicbox_server_model.dart' as _i8; +import 'package:stackwallet/models/node_form_data.dart' as _i7; import 'package:stackwallet/models/node_model.dart' as _i4; import 'package:stackwallet/services/node_service.dart' as _i3; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' @@ -45,31 +46,25 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { } @override - _i2.SecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( - Invocation.getter(#secureStorageInterface), - returnValue: _FakeSecureStorageInterface_0( - this, - Invocation.getter(#secureStorageInterface), - ), - ) - as _i2.SecureStorageInterface); + _i2.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( + Invocation.getter(#secureStorageInterface), + returnValue: _FakeSecureStorageInterface_0( + this, + Invocation.getter(#secureStorageInterface), + ), + ) as _i2.SecureStorageInterface); @override - List<_i4.NodeModel> get primaryNodes => - (super.noSuchMethod( - Invocation.getter(#primaryNodes), - returnValue: <_i4.NodeModel>[], - ) - as List<_i4.NodeModel>); + List<_i4.NodeModel> get primaryNodes => (super.noSuchMethod( + Invocation.getter(#primaryNodes), + returnValue: <_i4.NodeModel>[], + ) as List<_i4.NodeModel>); @override - List<_i4.NodeModel> get nodes => - (super.noSuchMethod( - Invocation.getter(#nodes), - returnValue: <_i4.NodeModel>[], - ) - as List<_i4.NodeModel>); + List<_i4.NodeModel> get nodes => (super.noSuchMethod( + Invocation.getter(#nodes), + returnValue: <_i4.NodeModel>[], + ) as List<_i4.NodeModel>); @override bool get hasListeners => @@ -77,45 +72,39 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { as bool); @override - _i5.Future updateDefaults() => - (super.noSuchMethod( - Invocation.method(#updateDefaults, []), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + _i5.Future updateDefaults() => (super.noSuchMethod( + Invocation.method(#updateDefaults, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future setPrimaryNodeFor({ required _i6.CryptoCurrency? coin, required _i4.NodeModel? node, bool? shouldNotifyListeners = false, - }) => - (super.noSuchMethod( - Invocation.method(#setPrimaryNodeFor, [], { - #coin: coin, - #node: node, - #shouldNotifyListeners: shouldNotifyListeners, - }), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + }) => (super.noSuchMethod( + Invocation.method(#setPrimaryNodeFor, [], { + #coin: coin, + #node: node, + #shouldNotifyListeners: shouldNotifyListeners, + }), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i4.NodeModel? getPrimaryNodeFor({required _i6.CryptoCurrency? currency}) => (super.noSuchMethod( - Invocation.method(#getPrimaryNodeFor, [], {#currency: currency}), - ) - as _i4.NodeModel?); + Invocation.method(#getPrimaryNodeFor, [], {#currency: currency}), + ) as _i4.NodeModel?); @override List<_i4.NodeModel> getNodesFor(_i6.CryptoCurrency? coin) => (super.noSuchMethod( - Invocation.method(#getNodesFor, [coin]), - returnValue: <_i4.NodeModel>[], - ) - as List<_i4.NodeModel>); + Invocation.method(#getNodesFor, [coin]), + returnValue: <_i4.NodeModel>[], + ) as List<_i4.NodeModel>); @override _i4.NodeModel? getNodeById({required String? id}) => @@ -125,127 +114,161 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { @override List<_i4.NodeModel> failoverNodesFor({ required _i6.CryptoCurrency? currency, - }) => - (super.noSuchMethod( - Invocation.method(#failoverNodesFor, [], {#currency: currency}), - returnValue: <_i4.NodeModel>[], - ) - as List<_i4.NodeModel>); + }) => (super.noSuchMethod( + Invocation.method(#failoverNodesFor, [], {#currency: currency}), + returnValue: <_i4.NodeModel>[], + ) as List<_i4.NodeModel>); @override _i5.Future save( _i4.NodeModel? node, String? password, bool? shouldNotifyListeners, - ) => + ) => (super.noSuchMethod( + Invocation.method(#save, [node, password, shouldNotifyListeners]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future saveEdit( + _i4.NodeModel? node, + String? password, + bool? notify, { + required _i4.NodeModel? opened, + bool? forgetCertificate = false, + }) => (super.noSuchMethod( + Invocation.method( + #saveEdit, + [node, password, notify], + {#opened: opened, #forgetCertificate: forgetCertificate}, + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + + @override + bool prepareCertificateTest(_i7.NodeFormData? data) => (super.noSuchMethod( + Invocation.method(#prepareCertificateTest, [data]), + returnValue: false, + ) as bool); + + @override + _i5.Future saveTrustedCertificate( + _i4.NodeModel? node, + _i7.NodeFormData? data, + ) => (super.noSuchMethod( + Invocation.method(#saveTrustedCertificate, [node, data]), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + + @override + _i5.Future forgetTrustedCertificate(_i4.NodeModel? shown) => (super.noSuchMethod( - Invocation.method(#save, [node, password, shouldNotifyListeners]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + Invocation.method(#forgetTrustedCertificate, [shown]), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + + @override + _i5.Future commitConnectionTest({ + required _i4.NodeModel? node, + required _i7.NodeFormData? formData, + }) => (super.noSuchMethod( + Invocation.method(#commitConnectionTest, [], { + #node: node, + #formData: formData, + }), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + + @override + _i5.Future setFailover(String? id, bool? value) => (super.noSuchMethod( + Invocation.method(#setFailover, [id, value]), + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override _i5.Future delete(String? id, bool? shouldNotifyListeners) => (super.noSuchMethod( - Invocation.method(#delete, [id, shouldNotifyListeners]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + Invocation.method(#delete, [id, shouldNotifyListeners]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future setEnabledState( String? id, bool? enabled, bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method(#setEnabledState, [ - id, - enabled, - shouldNotifyListeners, - ]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + ) => (super.noSuchMethod( + Invocation.method(#setEnabledState, [id, enabled, shouldNotifyListeners]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i5.Future updateDefaultEpicBoxes() => - (super.noSuchMethod( - Invocation.method(#updateDefaultEpicBoxes, []), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + _i5.Future updateDefaultEpicBoxes() => (super.noSuchMethod( + Invocation.method(#updateDefaultEpicBoxes, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future setPrimaryEpicBox({ - required _i7.EpicBoxServerModel? epicBox, + required _i8.EpicBoxServerModel? epicBox, bool? shouldNotifyListeners = false, - }) => - (super.noSuchMethod( - Invocation.method(#setPrimaryEpicBox, [], { - #epicBox: epicBox, - #shouldNotifyListeners: shouldNotifyListeners, - }), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + }) => (super.noSuchMethod( + Invocation.method(#setPrimaryEpicBox, [], { + #epicBox: epicBox, + #shouldNotifyListeners: shouldNotifyListeners, + }), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - List<_i7.EpicBoxServerModel> getEpicBoxes() => - (super.noSuchMethod( - Invocation.method(#getEpicBoxes, []), - returnValue: <_i7.EpicBoxServerModel>[], - ) - as List<_i7.EpicBoxServerModel>); + List<_i8.EpicBoxServerModel> getEpicBoxes() => (super.noSuchMethod( + Invocation.method(#getEpicBoxes, []), + returnValue: <_i8.EpicBoxServerModel>[], + ) as List<_i8.EpicBoxServerModel>); @override - _i7.EpicBoxServerModel? getEpicBoxById({required String? id}) => + _i8.EpicBoxServerModel? getEpicBoxById({required String? id}) => (super.noSuchMethod(Invocation.method(#getEpicBoxById, [], {#id: id})) - as _i7.EpicBoxServerModel?); + as _i8.EpicBoxServerModel?); @override _i5.Future addEpicBox( - _i7.EpicBoxServerModel? epicBox, + _i8.EpicBoxServerModel? epicBox, bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method(#addEpicBox, [epicBox, shouldNotifyListeners]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + ) => (super.noSuchMethod( + Invocation.method(#addEpicBox, [epicBox, shouldNotifyListeners]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future deleteEpicBox(String? id, bool? shouldNotifyListeners) => (super.noSuchMethod( - Invocation.method(#deleteEpicBox, [id, shouldNotifyListeners]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + Invocation.method(#deleteEpicBox, [id, shouldNotifyListeners]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i5.Future updateCommunityNodes() => - (super.noSuchMethod( - Invocation.method(#updateCommunityNodes, []), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + _i5.Future updateCommunityNodes() => (super.noSuchMethod( + Invocation.method(#updateCommunityNodes, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - void addListener(_i8.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i9.VoidCallback? listener) => super.noSuchMethod( Invocation.method(#addListener, [listener]), returnValueForMissingStub: null, ); @override - void removeListener(_i8.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i9.VoidCallback? listener) => super.noSuchMethod( Invocation.method(#removeListener, [listener]), returnValueForMissingStub: null, ); diff --git a/test/services/node_certificate_writes_test.dart b/test/services/node_certificate_writes_test.dart new file mode 100644 index 0000000000..56dad9f4fd --- /dev/null +++ b/test/services/node_certificate_writes_test.dart @@ -0,0 +1,293 @@ +import 'dart:async'; + +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/db/hive/db.dart'; +import 'package:stackwallet/models/node_form_data.dart'; +import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/services/node_service.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; + +import '../hive/hive_ce_test_utils.dart'; +import '../utilities/tls_test_server.dart'; + +class _PausedPassword extends FakeSecureStorage { + final started = Completer(); + final release = Completer(); + @override + Future delete({ + required String key, + IOSOptions? iOptions, + AndroidOptions? aOptions, + LinuxOptions? lOptions, + WebOptions? webOptions, + MacOsOptions? mOptions, + WindowsOptions? wOptions, + }) async { + started.complete(); + await release.future; + await super.delete(key: key); + } +} + +void main() { + final coin = Bitcoin(CryptoCurrencyNetwork.main); + late NodeService service; + late FakeSecureStorage storage; + late NodeModel original; + late String pem; + NodeModel current() => service.getNodeById(id: original.id)!; + NodeFormData approved(NodeModel node) => + NodeFormData.fromNode(node)..trustedCertificatePem = pem; + + setUp(() async { + await setUpHiveCeTest(); + if (!DB.instance.hive.isAdapterRegistered(12)) { + DB.instance.hive.registerAdapter(NodeModelAdapter()); + } + final box = await DB.instance.hive.openBox(DB.boxNameNodeModels); + storage = FakeSecureStorage(); + service = NodeService(secureStorageInterface: storage); + original = NodeModel( + host: 'node.example', + port: 50002, + name: 'Node', + id: 'n', + useSSL: true, + enabled: true, + coinName: coin.identifier, + isFailover: true, + isDown: false, + torEnabled: true, + clearnetEnabled: true, + isPrimary: false, + ); + await box.put(original.id, original); + pem = TlsElectrumServer.certificate('node_a'); + }); + tearDown(tearDownHiveCeTest); + + test('a certificate-only write keeps node metadata and never reads or changes credentials', () async { + expect(identical(current(), current()), true); + expect( + await service.saveTrustedCertificate(original, approved(original)), + true, + ); + expect(current().trustedCertificatePem, pem); + expect(current().name, original.name); + expect(current().isPrimary, false); + expect(storage.interactions, 0); + }); + + for (final change in [ + 'forget empty', + 'forget approved', + 'replace', + 'endpoint', + 'delete', + ]) { + test('a late approval cannot undo $change', () async { + if (change == 'forget approved') { + await service.saveTrustedCertificate(original, approved(original)); + original = current(); + } + final result = approved(original); + switch (change) { + case 'forget empty': + case 'forget approved': + await service.forgetTrustedCertificate(original); + case 'replace': + final replacement = approved(original) + ..trustedCertificatePem = TlsElectrumServer.certificate('node_b'); + await service.saveTrustedCertificate(original, replacement); + case 'endpoint': + await DB.instance.put( + boxName: DB.boxNameNodeModels, + key: original.id, + value: original.copyWith( + host: 'elsewhere.example', + loginName: null, + trusted: null, + ), + ); + case 'delete': + await DB.instance.delete( + boxName: DB.boxNameNodeModels, + key: original.id, + ); + } + final before = service.getNodeById(id: original.id); + expect(await service.saveTrustedCertificate(original, result), false); + expect(identical(service.getNodeById(id: original.id), before), true); + }); + } + + test( + 'a queued approval checks the row after an earlier queued Forget', + () async { + await DB.instance.mutex.acquire(); + final forgotten = service.forgetTrustedCertificate(original); + final late = service.saveTrustedCertificate(original, approved(original)); + DB.instance.mutex.release(); + expect(await forgotten, true); + expect(await late, false); + expect(current().trustedCertificatePem, null); + }, + ); + + test( + 'a Save waiting for the password cannot restore a forgotten certificate', + () async { + await service.saveTrustedCertificate(original, approved(original)); + final opened = current(); + final paused = _PausedPassword(); + final editor = NodeService(secureStorageInterface: paused); + final saving = editor.saveEdit( + opened.copyWith(name: 'Edited', loginName: null, trusted: null), + null, + true, + opened: opened, + ); + await paused.started.future; + await service.forgetTrustedCertificate(opened); + paused.release.complete(); + expect(await saving, true); + expect(current().name, 'Edited'); + expect(current().trustedCertificatePem, null); + }, + ); + + test('an explicit staged Forget wins an approval made before Save', () async { + await service.saveTrustedCertificate(original, approved(original)); + final edited = original.copyWith( + loginName: null, + trusted: null, + clearTrustedCertificate: true, + ); + expect( + await service.saveEdit( + edited, + null, + true, + opened: original, + forgetCertificate: true, + ), + true, + ); + expect(current().trustedCertificatePem, null); + }); + + test( + 'a fresh test discards stale form trust and can approve after Forget', + () async { + await service.saveTrustedCertificate(original, approved(original)); + final form = NodeFormData.fromNode(current()); + await service.forgetTrustedCertificate(current()); + expect(service.prepareCertificateTest(form), true); + expect(form.trustedCertificatePem, null); + form.trustedCertificatePem = pem; + final edit = current().copyWith( + loginName: null, + trusted: null, + trustedCertificatePem: form.trustedCertificatePem, + ); + expect( + await service.saveEdit(edit, null, true, opened: form.source!), + true, + ); + expect(current().trustedCertificatePem, pem); + }, + ); + + test('a staged Forget survives preparing another test after an external approval', () async { + final form = NodeFormData.fromNode(original)..forgetCertificate(); + await service.saveTrustedCertificate(original, approved(original)); + expect(service.prepareCertificateTest(form), true); + expect(form.certificateForgotten, true); + expect(form.trustedCertificatePem, null); + }); + + for (final flag in ['primary', 'enabled', 'failover']) { + test( + 'updating $flag keeps the latest certificate and credentials', + () async { + await service.saveTrustedCertificate(original, approved(original)); + switch (flag) { + case 'primary': + await service.setPrimaryNodeFor(coin: coin, node: original); + case 'enabled': + await service.setEnabledState(original.id, false, true); + case 'failover': + await service.setFailover(original.id, false); + } + expect(current().trustedCertificatePem, pem); + expect(storage.interactions, 0); + }, + ); + } + + test('Connect stores its approval and primary flag together without touching credentials', () async { + expect( + await service.commitConnectionTest( + node: original, + formData: approved(original), + ), + true, + ); + expect(current().trustedCertificatePem, pem); + expect(current().isPrimary, true); + expect(storage.interactions, 0); + }); + + for (final change in ['Forget', 'edit', 'delete']) { + test('Connect cannot promote or restore trust after $change', () async { + final result = approved(original); + switch (change) { + case 'Forget': + await service.forgetTrustedCertificate(original); + case 'edit': + await DB.instance.put( + boxName: DB.boxNameNodeModels, + key: original.id, + value: original.copyWith( + host: 'new.example', + loginName: null, + trusted: null, + ), + ); + case 'delete': + await DB.instance.delete( + boxName: DB.boxNameNodeModels, + key: original.id, + ); + } + expect( + await service.commitConnectionTest(node: original, formData: result), + false, + ); + expect(service.getPrimaryNodeFor(currency: coin), null); + }); + } + + test( + 'an edited endpoint keeps only the approval tested for that endpoint', + () async { + final form = approved(original)..host = 'new.example'; + expect(form.trustedCertificatePem, null); + form.trustedCertificatePem = pem; + final edited = original.copyWith( + host: form.host, + loginName: null, + trusted: null, + trustedCertificatePem: form.trustedCertificatePem, + ); + expect( + await service.saveEdit(edited, null, true, opened: original), + true, + ); + expect(current().host, 'new.example'); + expect(current().trustedCertificatePem, pem); + }, + ); +} diff --git a/test/services/notification_certificate_revocation_test.dart b/test/services/notification_certificate_revocation_test.dart new file mode 100644 index 0000000000..dbe1dd0a34 --- /dev/null +++ b/test/services/notification_certificate_revocation_test.dart @@ -0,0 +1,156 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:logger/logger.dart' show Level; +import 'package:stackwallet/db/hive/db.dart'; +import 'package:stackwallet/electrumx_rpc/client_manager.dart'; +import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/models/notification_model.dart'; +import 'package:stackwallet/services/node_service.dart'; +import 'package:stackwallet/services/notifications_service.dart'; +import 'package:stackwallet/services/trade_service.dart'; +import 'package:stackwallet/services/wallets.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/prefs.dart'; +import 'package:stackwallet/utilities/tor_plain_net_option_enum.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; + +import '../hive/hive_ce_test_utils.dart'; +import '../utilities/tls_test_server.dart'; + +class _Wallet extends Fake implements ElectrumXInterface { + @override + String get walletId => 'watched-wallet'; + @override + Bitcoin get cryptoCurrency => Bitcoin(CryptoCurrencyNetwork.main); +} + +class _Prefs extends Fake implements Prefs { + @override + bool get useTor => false; + @override + bool get wifiOnly => false; + @override + bool get externalCalls => false; +} + +class _Trades extends Fake implements TradesService {} + +Future _until(bool Function() done, String waitingFor) async { + final watch = Stopwatch()..start(); + while (!done()) { + if (watch.elapsed > const Duration(seconds: 2)) { + fail('Timed out waiting for $waitingFor'); + } + await Future.delayed(const Duration(milliseconds: 5)); + } +} + +void main() { + test('Forget retires a pending background notification connection', () async { + await setUpHiveCeTest(); + final logs = await Directory.systemTemp.createTemp('notification_tls'); + await Logging.instance.initialize(logs.path, level: Level.off); + DB.instance.hive.registerAdapter(NodeModelAdapter()); + DB.instance.hive.registerAdapter(NotificationModelAdapter()); + final nodes = await DB.instance.hive.openBox( + DB.boxNameNodeModels, + ); + final watched = await DB.instance.hive.openBox( + DB.boxNameWatchedTransactions, + ); + final server = await TlsElectrumServer.start(respond: false); + final wallet = _Wallet(); + final coin = wallet.cryptoCurrency; + final node = NodeModel( + id: 'n', + host: '127.0.0.1', + port: server.port, + name: 'Node', + useSSL: true, + enabled: true, + coinName: coin.identifier, + isFailover: false, + isDown: false, + torEnabled: true, + clearnetEnabled: true, + isPrimary: true, + trustedCertificatePem: TlsElectrumServer.certificate('node_a'), + ); + await nodes.put(node.id, node); + await watched.put( + 0, + NotificationModel( + id: 0, + title: 'Transaction (0/${coin.minConfirms})', + description: '', + iconAssetName: '', + date: DateTime.now(), + walletId: wallet.walletId, + read: false, + shouldWatchForUpdates: true, + txid: 'a' * 64, + coinName: coin.identifier, + ), + ); + Wallets.sharedInstance.addWallet(wallet); + final service = NodeService(secureStorageInterface: FakeSecureStorage()); + final notifications = NotificationsService.instance; + await notifications.init( + nodeService: service, + tradesService: _Trades(), + prefs: _Prefs(), + ); + final interval = NotificationsService.notificationRefreshInterval; + addTearDown(() async { + notifications.stopCheckingWatchedTransactions(); + NotificationsService.notificationRefreshInterval = interval; + await ClientManager.sharedInstance.closeAll(); + await server.close(); + await tearDownHiveCeTest(); + await logs.delete(recursive: true); + }); + NotificationsService.notificationRefreshInterval = const Duration( + milliseconds: 50, + ); + notifications.startCheckingWatchedNotifications(); + await _until( + () => server.methods.contains('server.version'), + 'notification RPC setup', + ); + notifications.stopCheckingWatchedTransactions(); + expect(server.disconnections, 0); + await service.save( + NodeModel.fromStackBackup({ + ...node.toMap(), + 'id': 'other-coin', + 'coinName': 'litecoin', + }), + null, + true, + ); + await Future.delayed(const Duration(milliseconds: 20)); + expect( + server.disconnections, + 0, + reason: 'unrelated node changes keep the connection', + ); + await service.forgetTrustedCertificate(node); + await _until( + () => server.disconnections > 0, + 'Forget to disconnect the notification', + ); + expect(nodes.get(node.id)!.trustedCertificatePem, null); + expect( + ClientManager.sharedInstance.getClient( + cryptoCurrency: coin, + netType: TorPlainNetworkOption.both, + ), + null, + ); + expect(server.methods, isNot(contains('blockchain.transaction.get'))); + }); +} diff --git a/test/utilities/electrum_connection_check_test.dart b/test/utilities/electrum_connection_check_test.dart new file mode 100644 index 0000000000..c29f0387ba --- /dev/null +++ b/test/utilities/electrum_connection_check_test.dart @@ -0,0 +1,96 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:logger/logger.dart' show Level; +import 'package:mockito/mockito.dart'; +import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart'; +import 'package:stackwallet/services/tor_service.dart'; +import 'package:stackwallet/utilities/connection_check/electrum_connection_check.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/prefs.dart'; + +import 'tls_test_server.dart'; + +class _TorPrefs extends Mock implements Prefs { + @override + bool get useTor => true; + @override + bool get torKillSwitch => false; +} + +class _ConnectedTor extends Mock implements TorService { + _ConnectedTor(this.proxy); + final ({InternetAddress host, int port}) proxy; + @override + TorConnectionStatus get status => TorConnectionStatus.connected; + @override + ({InternetAddress host, int port}) getProxyInfo() => proxy; +} + +void main() { + late Directory logDirectory; + setUpAll(() async { + logDirectory = await Directory.systemTemp.createTemp('electrum_check'); + await Logging.instance.initialize(logDirectory.path, level: Level.off); + }); + tearDownAll(() => logDirectory.delete(recursive: true)); + + // The test server and the test SOCKS proxy both listen on 127.0.0.1; the + // onion name is only ever sent to that proxy, never resolved. + Future withOnionServer( + String? certificateName, + Future Function(TlsElectrumServer server, TlsTestProxy proxy) body, + ) async { + final server = await TlsElectrumServer.start( + certificateName: certificateName, + ); + final proxy = await TlsTestProxy.start(server.port); + try { + await body(server, proxy); + } finally { + await proxy.close(); + await server.close(); + } + } + + test('an onion node with SSL is tested over TLS and can be trusted', () { + return withOnionServer('node_a', (server, proxy) async { + final rejected = await checkElectrumServer( + host: 'tlsx.test.onion', + port: server.port, + useSSL: true, + overridePrefs: _TorPrefs(), + overrideTorService: _ConnectedTor(proxy.info), + ); + expect(rejected.success, false); + expect(rejected.certificate, isNotNull); + expect(server.requests, 0); + final approved = await checkElectrumServer( + host: 'tlsx.test.onion', + port: server.port, + useSSL: true, + trustedCertificatePem: rejected.certificate!.pem, + overridePrefs: _TorPrefs(), + overrideTorService: _ConnectedTor(proxy.info), + ); + expect(approved.success, true, reason: approved.error?.toString()); + expect(server.requests, 1); + expect(proxy.destinations, everyElement('tlsx.test.onion')); + }); + }); + + test('an onion node without SSL is tested in plain text', () { + return withOnionServer(null, (server, proxy) async { + final result = await checkElectrumServer( + host: 'tlsx.test.onion', + port: server.port, + useSSL: false, + overridePrefs: _TorPrefs(), + overrideTorService: _ConnectedTor(proxy.info), + ); + expect(result.success, true, reason: result.error?.toString()); + expect(result.certificate, null); + expect(proxy.destinations, ['tlsx.test.onion']); + }); + }); +} diff --git a/test/utilities/electrum_tls_test.dart b/test/utilities/electrum_tls_test.dart new file mode 100644 index 0000000000..275edd8433 --- /dev/null +++ b/test/utilities/electrum_tls_test.dart @@ -0,0 +1,229 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/utilities/connection_check/electrum_connection_check.dart'; +import 'package:stackwallet/utilities/tls_certificate.dart'; + +import 'tls_test_server.dart'; + +void main() { + final servers = []; + final proxies = []; + + Future start({ + String? certificateName = 'node_a', + bool respond = true, + }) async { + final server = await TlsElectrumServer.start( + certificateName: certificateName, + respond: respond, + ); + servers.add(server); + return server; + } + + Future check( + TlsElectrumServer server, { + String? trustedCertificatePem, + String host = '127.0.0.1', + bool useSSL = true, + ({InternetAddress host, int port})? proxyInfo, + Duration timeout = const Duration(seconds: 2), + }) => testElectrumConnection( + host: host, + port: server.port, + useSSL: useSSL, + trustedCertificatePem: trustedCertificatePem, + proxyInfo: proxyInfo, + timeout: timeout, + ); + + tearDown(() async { + for (final proxy in proxies) { + await proxy.close(); + } + for (final server in servers) { + await server.close(); + } + proxies.clear(); + servers.clear(); + }); + + test('untrusted TLS returns a certificate without sending an RPC', () async { + final server = await start(); + final result = await check(server); + expect(result.success, false); + expect(result.certificate, isNotNull); + expect( + certificateDer(result.certificate!.pem), + certificateDer(TlsElectrumServer.certificate('node_a')), + ); + expect( + trustedCertificateCheck(TlsElectrumServer.certificate('node_a'))!( + result.certificate!, + ), + true, + ); + expect( + trustedCertificateCheck(TlsElectrumServer.certificate('node_b'))!( + result.certificate!, + ), + false, + ); + expect(server.requests, 0); + }); + + test('approval enables RPC and closes the test connection', () async { + final server = await start(); + final rejected = await check(server); + final approved = await check( + server, + trustedCertificatePem: rejected.certificate!.pem, + ); + expect(approved.success, true, reason: approved.error?.toString()); + expect(server.requests, 1); + await Future.delayed(const Duration(milliseconds: 30)); + expect(server.disconnections, 1); + }); + + test('approval does not change the default trust store', () async { + final server = await start(); + expect( + (await check( + server, + trustedCertificatePem: TlsElectrumServer.certificate('node_a'), + )).success, + true, + ); + expect((await check(server)).success, false); + }); + + test('another certificate is rejected and returned for review', () async { + final server = await start(certificateName: 'node_b'); + final result = await check( + server, + trustedCertificatePem: TlsElectrumServer.certificate('node_a'), + ); + expect(result.success, false); + expect( + certificateDer(result.certificate!.pem), + certificateDer(TlsElectrumServer.certificate('node_b')), + ); + expect(server.requests, 0); + }); + + test('invalid saved PEM fails without connecting', () async { + final server = await start(); + final result = await check(server, trustedCertificatePem: 'invalid'); + expect(result.success, false); + expect(result.error, isA()); + expect(server.requests, 0); + }); + + test('approval pins the certificate even after it expired', () async { + final server = await start(certificateName: 'expired'); + final rejected = await check(server); + expect(rejected.success, false); + expect(rejected.certificate, isNotNull); + final approved = await check( + server, + trustedCertificatePem: TlsElectrumServer.certificate('expired'), + ); + expect(approved.success, true, reason: approved.error?.toString()); + expect(server.requests, 1); + }); + + test('plain TCP can be tested without a certificate', () async { + final server = await start(certificateName: null); + final result = await check(server, useSSL: false); + expect(result.success, true); + expect(result.certificate, null); + }); + + test('RPC timeout fails and closes the connection', () async { + final server = await start(respond: false); + final result = await check( + server, + trustedCertificatePem: TlsElectrumServer.certificate('node_a'), + timeout: const Duration(milliseconds: 200), + ); + expect(result.success, false); + expect(result.certificate, null); + await Future.delayed(const Duration(milliseconds: 30)); + expect(server.disconnections, 1); + }); + + test('SOCKS TLS rejection and approval use the same proxy', () async { + final server = await start(); + final proxy = await TlsTestProxy.start(server.port); + proxies.add(proxy); + final rejected = await check( + server, + host: 'tlsx.test.onion', + proxyInfo: proxy.info, + ); + expect(rejected.success, false); + expect(rejected.certificate, isNotNull); + expect(server.requests, 0); + final approved = await check( + server, + host: 'tlsx.test.onion', + proxyInfo: proxy.info, + trustedCertificatePem: rejected.certificate!.pem, + ); + expect(approved.success, true, reason: approved.error?.toString()); + expect(proxy.destinations, everyElement('tlsx.test.onion')); + expect(proxy.destinations.length, 2); + }); + + test('approved certificate does not have to match the hostname', () async { + final server = await start(); + final proxy = await TlsTestProxy.start(server.port); + proxies.add(proxy); + final result = await check( + server, + host: 'wrong.example', + proxyInfo: proxy.info, + trustedCertificatePem: TlsElectrumServer.certificate('node_a'), + ); + expect(result.success, true, reason: result.error?.toString()); + expect(server.requests, 1); + }); + + test('a stalled handshake fails when the timeout fires', () async { + final stalled = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + final accepted = []; + // Accept, then never answer the handshake. + stalled.listen((socket) { + accepted.add(socket); + socket.listen((_) {}, onError: (Object _) {}); + }); + final stopwatch = Stopwatch()..start(); + final result = await testElectrumConnection( + host: '127.0.0.1', + port: stalled.port, + useSSL: true, + timeout: const Duration(milliseconds: 300), + ); + expect(result.success, false); + expect(result.error, isA()); + expect(result.certificate, null); + expect(stopwatch.elapsed, lessThan(const Duration(seconds: 3))); + for (final socket in accepted) { + socket.destroy(); + } + await stalled.close(); + }); + + test('PEM parsing accepts one certificate and rejects bundles', () { + final pem = TlsElectrumServer.certificate('node_a'); + expect( + certificateFingerprint(certificateDer(pem)), + matches(RegExp(r'^(?:[0-9A-F]{2}:){31}[0-9A-F]{2}$')), + ); + expect(() => certificateDer('$pem$pem'), throwsFormatException); + expect(trustedCertificateCheck(null), null); + expect(() => trustedCertificateCheck('invalid'), throwsFormatException); + }); +} diff --git a/test/utilities/mock_electrum_server.dart b/test/utilities/mock_electrum_server.dart index 6802c0cec6..8bd3c62a6d 100644 --- a/test/utilities/mock_electrum_server.dart +++ b/test/utilities/mock_electrum_server.dart @@ -166,6 +166,7 @@ class ManagedElectrumXClient extends ElectrumXClient { final existing = getElectrumAdapter(); if (existing != null && !existing.peer.isClosed) { + await super.checkElectrumAdapter(); return; } if (existing != null) { @@ -184,6 +185,7 @@ class ManagedElectrumXClient extends ElectrumXClient { cryptoCurrency: cryptoCurrency, netType: netType, ); + await super.checkElectrumAdapter(); } } diff --git a/test/utilities/monero_socks_write_test.dart b/test/utilities/monero_socks_write_test.dart new file mode 100644 index 0000000000..227b9526e2 --- /dev/null +++ b/test/utilities/monero_socks_write_test.dart @@ -0,0 +1,116 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:async/async.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:logger/logger.dart' show Level; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/test_monero_node_connection.dart'; + +class _Reader { + _Reader(Socket socket) : input = StreamQueue(socket); + final StreamQueue> input; + final bytes = []; + Future> take(int count) async { + while (bytes.length < count) { + bytes.addAll(await input.next); + } + final result = bytes.sublist(0, count); + bytes.removeRange(0, count); + return result; + } +} + +void main() { + test('Monero handles SOCKS resets without uncaught write errors', () async { + final logs = await Directory.systemTemp.createTemp( + 'monero_dependency_probe', + ); + await Logging.instance.initialize(logs.path, level: Level.off); + final proxy = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + final peers = []; + final handlers = >[]; + var accepted = 0; + final subscription = proxy.listen((socket) { + peers.add(socket); + final ordinal = accepted++; + final handler = () async { + final reader = _Reader(socket); + try { + final greeting = await reader.take(2); + await reader.take(greeting[1]); + socket.add([5, 0]); + final connect = await reader.take(4); + if (connect[3] == 3) { + await reader.take((await reader.take(1)).single); + } else { + await reader.take(4); + } + await reader.take(2); + socket.add([5, 0, 0, 1, 127, 0, 0, 1, 0, 0]); + await socket.flush(); + if (ordinal % 3 != 0) { + await Future.delayed(Duration(milliseconds: ordinal % 3)); + } + final linger = Int32List.fromList([1, 0]); + socket.setRawOption( + RawSocketOption( + RawSocketOption.levelSocket, + Platform.isMacOS ? 0x0080 : 13, + linger.buffer.asUint8List(), + ), + ); + socket.destroy(); + } catch (_) { + socket.destroy(); + } finally { + await reader.input.cancel(); + } + }(); + handlers.add(handler); + }); + addTearDown(() async { + for (final peer in peers) { + peer.destroy(); + } + await subscription.cancel(); + await proxy.close(); + await Future.wait(handlers); + await logs.delete(recursive: true); + }); + + final results = >[]; + for (var i = 0; i < 15; i++) { + final uncaught = []; + final completed = Completer>(); + runZonedGuarded( + () async { + Object? outcome; + try { + outcome = (await testMoneroNodeConnection( + Uri.parse('http://node.example.onion:18081/json_rpc'), + null, + null, + false, + proxyInfo: (host: InternetAddress.loopbackIPv4, port: proxy.port), + ).timeout(const Duration(milliseconds: 500))).success; + } catch (error) { + outcome = error.runtimeType.toString(); + } + await Future.delayed(const Duration(milliseconds: 20)); + completed.complete({'outcome': outcome, 'uncaught': uncaught}); + }, + (Object error, StackTrace stack) { + uncaught.add('${error.runtimeType}: $error'); + }, + ); + results.add(await completed.future.timeout(const Duration(seconds: 2))); + } + expect(results, hasLength(15)); + expect( + results.expand((result) => result['uncaught'] as List), + isEmpty, + ); + }); +} diff --git a/test/utilities/test_electrum_node_connection_test.dart b/test/utilities/test_electrum_node_connection_test.dart new file mode 100644 index 0000000000..c4de700913 --- /dev/null +++ b/test/utilities/test_electrum_node_connection_test.dart @@ -0,0 +1,205 @@ +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/models/node_form_data.dart'; +import 'package:stackwallet/utilities/connection_check/electrum_connection_check.dart'; +import 'package:stackwallet/utilities/tor_plain_net_option_enum.dart'; + +import 'tls_test_server.dart'; + +void main() { + late TlsElectrumServer server; + late NodeFormData data; + int approvals = 0; + int successes = 0; + + setUp(() async { + server = await TlsElectrumServer.start(); + data = NodeFormData() + ..host = '127.0.0.1' + ..port = server.port + ..useSSL = true; + approvals = 0; + successes = 0; + }); + tearDown(() => server.close()); + + Future testNode({ + bool approve = true, + Future Function()? approval, + ElectrumNodeTester? tester, + bool Function()? isActive, + }) => testElectrumNodeConnection( + data, + isActive: isActive ?? () => true, + onSuccess: (_) => successes++, + requestTrust: (certificate, host, port) async { + approvals++; + expect(host, '127.0.0.1'); + expect(port, server.port); + return approval == null ? approve : await approval(); + }, + testConnection: + tester ?? + ({ + required host, + required port, + required useSSL, + trustedCertificatePem, + }) => testElectrumConnection( + host: host, + port: port, + useSSL: useSSL, + trustedCertificatePem: trustedCertificatePem, + timeout: const Duration(seconds: 2), + ), + ); + + test('Cancel leaves the form untrusted and sends no RPC', () async { + expect(await testNode(approve: false), false); + expect(data.trustedCertificatePem, null); + expect(approvals, 1); + expect(successes, 0); + expect(server.requests, 0); + }); + + test( + 'Trust saves the reviewed certificate after a successful RPC retry', + () async { + expect(await testNode(), true); + expect( + data.trustedCertificatePem, + TlsElectrumServer.certificate('node_a'), + ); + expect(approvals, 1); + expect(successes, 1); + expect(server.requests, 1); + expect(await testNode(), true); + expect(approvals, 1); + expect(successes, 2); + }, + ); + + test('a failed retry saves no approval', () async { + final rejected = await testElectrumConnection( + host: data.host!, + port: data.port!, + useSSL: true, + timeout: const Duration(seconds: 2), + ); + expect( + await testNode( + tester: + ({ + required host, + required port, + required useSSL, + trustedCertificatePem, + }) async => trustedCertificatePem == null + ? rejected + : const ElectrumConnectionResult( + success: false, + error: 'still rejected', + ), + ), + false, + ); + expect(data.trustedCertificatePem, null); + expect(successes, 0); + }); + + test('editing the endpoint during review cancels the retry', () async { + expect( + await testNode( + approval: () async { + data.host = 'another.example'; + return true; + }, + ), + false, + ); + expect(data.trustedCertificatePem, null); + expect(server.requests, 0); + }); + + test('leaving the screen during review cancels the retry', () async { + bool active = true; + expect( + await testNode( + isActive: () => active, + approval: () async { + active = false; + return true; + }, + ), + false, + ); + expect(data.trustedCertificatePem, null); + expect(server.requests, 0); + }); + + test('changing the network option during review cancels approval', () async { + expect( + await testNode( + approval: () async { + data.netOption = TorPlainNetworkOption.tor; + return true; + }, + ), + false, + ); + expect(data.trustedCertificatePem, null); + expect(server.requests, 0); + }); + + test('a late successful retry cannot trust a changed endpoint', () async { + final retry = Completer(); + final retryStarted = Completer(); + final rejected = await testElectrumConnection( + host: data.host!, + port: data.port!, + useSSL: true, + timeout: const Duration(seconds: 2), + ); + final result = testNode( + tester: + ({ + required host, + required port, + required useSSL, + trustedCertificatePem, + }) async { + if (trustedCertificatePem == null) return rejected; + retryStarted.complete(); + return retry.future; + }, + ); + await retryStarted.future; + data.port = data.port! + 1; + retry.complete(const ElectrumConnectionResult(success: true)); + expect(await result, false); + expect(data.trustedCertificatePem, null); + expect(successes, 0); + }); + for (final clearThenRestore in [false, true]) { + test( + 'an intervening certificate decision cancels review ($clearThenRestore)', + () async { + final before = data.trustedCertificatePem; + expect( + await testNode( + approval: () async { + data.forgetCertificate(); + if (clearThenRestore) data.trustedCertificatePem = before; + return true; + }, + ), + false, + ); + expect(data.trustedCertificatePem, null); + expect(server.requests, 0); + expect(successes, 0); + }, + ); + } +} diff --git a/test/utilities/tls_test_server.dart b/test/utilities/tls_test_server.dart new file mode 100644 index 0000000000..a046cbb7c7 --- /dev/null +++ b/test/utilities/tls_test_server.dart @@ -0,0 +1,313 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:async/async.dart'; +import 'package:basic_utils/basic_utils.dart'; +import 'package:pointycastle/asn1.dart'; +import 'package:pointycastle/export.dart'; + +/// Certificates for the test servers, built in Dart so the tests need no +/// native tooling. The 'expired' certificate is a committed fixture; the +/// others are generated once per test process from the committed test keys. +final _certificates = {}; + +String _keyPem(String name) => + File('test/fixtures/tls/$name.key.pem').readAsStringSync(); + +String _certificatePem(String name) { + if (name == 'expired') { + return File('test/fixtures/tls/expired.pem').readAsStringSync(); + } + return _certificates[name] ??= generateServerCertificatePem( + privateKeyPem: _keyPem(name), + commonName: 'localhost', + validity: const Duration(days: 1), + ); +} + +/// A self-signed X.509 server certificate, SHA-256 with RSA, in PEM. +String generateServerCertificatePem({ + required String privateKeyPem, + required String commonName, + required Duration validity, +}) { + final privateKey = CryptoUtils.rsaPrivateKeyFromPem(privateKeyPem); + final now = DateTime.now().toUtc(); + + ASN1Sequence algorithm(String oid) => ASN1Sequence( + elements: [ASN1ObjectIdentifier.fromIdentifierString(oid), ASN1Null()], + ); + final name = ASN1Sequence( + elements: [ + ASN1Set( + elements: [ + ASN1Sequence( + elements: [ + ASN1ObjectIdentifier.fromIdentifierString('2.5.4.3'), + ASN1UTF8String(utf8StringValue: commonName), + ], + ), + ], + ), + ], + ); + final publicKeyInfo = ASN1Sequence( + elements: [ + algorithm('1.2.840.113549.1.1.1'), + ASN1BitString( + stringValues: ASN1Sequence( + elements: [ + ASN1Integer(privateKey.modulus!), + ASN1Integer(privateKey.publicExponent!), + ], + ).encode(), + ), + ], + ); + final random = Random.secure(); + final serial = + (BigInt.from(random.nextInt(1 << 31)) << 32) + + BigInt.from(random.nextInt(1 << 32)); + final tbsCertificate = ASN1Sequence( + elements: [ + ASN1Integer(serial), + algorithm('1.2.840.113549.1.1.11'), + name, + ASN1Sequence( + elements: [ + ASN1UtcTime(now.subtract(const Duration(hours: 1))), + ASN1UtcTime(now.add(validity)), + ], + ), + name, + publicKeyInfo, + ], + ); + final signer = RSASigner(SHA256Digest(), '0609608648016503040201') + ..init(true, PrivateKeyParameter(privateKey)); + final signature = signer.generateSignature(tbsCertificate.encode()); + final certificate = ASN1Sequence( + elements: [ + tbsCertificate, + algorithm('1.2.840.113549.1.1.11'), + ASN1BitString(stringValues: signature.bytes), + ], + ); + final body = base64Encode(certificate.encode()); + final lines = [ + for (var i = 0; i < body.length; i += 64) + body.substring(i, min(i + 64, body.length)), + ]; + return '-----BEGIN CERTIFICATE-----\n${lines.join('\n')}\n' + '-----END CERTIFICATE-----\n'; +} + +/// Loopback fixtures only; the private keys under fixtures/tls are test keys. +class TlsElectrumServer { + TlsElectrumServer._(this._server); + + final ServerSocket _server; + final _sockets = []; + late final StreamSubscription _subscription; + int requests = 0; + final methods = []; + int disconnections = 0; + int accepted = 0; + int get port => _server.port; + + static String certificate(String name) => _certificatePem(name); + + /// With [versionError], server.version is answered with a JSON-RPC error, + /// as a server that rejects the client would. + static Future start({ + String? certificateName = 'node_a', + bool respond = true, + bool versionError = false, + bool stallHandshake = false, + }) async { + final server = TlsElectrumServer._( + await ServerSocket.bind(InternetAddress.loopbackIPv4, 0), + ); + final context = certificateName == null + ? null + : (SecurityContext() + ..useCertificateChainBytes( + utf8.encode(_certificatePem(certificateName)), + ) + ..usePrivateKeyBytes(utf8.encode(_keyPem(certificateName)))); + server._subscription = server._server.listen((raw) async { + server.accepted++; + server._sockets.add(raw); + raw.done.ignore(); + if (stallHandshake) { + raw.listen( + (_) {}, + onError: (Object _) {}, + onDone: () => server.disconnections++, + ); + return; + } + try { + final socket = context == null + ? raw + : await SecureSocket.secureServer(raw, context); + server._sockets.add(socket); + socket.done.ignore(); + socket + .cast>() + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen( + (line) { + final request = jsonDecode(line) as Map; + server.requests++; + server.methods.add(request['method'] as String); + if (!respond) return; + if (versionError && request['method'] == 'server.version') { + socket.write( + '${jsonEncode({ + 'jsonrpc': '2.0', + 'id': request['id'], + 'error': {'code': -32601, 'message': 'unsupported'}, + })}\n', + ); + return; + } + final Object result; + switch (request['method']) { + case 'server.version': + result = ['ElectrumX TLS test', '1.4']; + case 'blockchain.headers.subscribe': + result = {'height': 100, 'hex': '00' * 80}; + case 'blockchain.transaction.get': + result = {'txid': request['params'][0], 'confirmations': 3}; + default: + result = {}; + } + final reply = { + 'jsonrpc': '2.0', + 'id': request['id'], + 'result': result, + }; + socket.write('${jsonEncode(reply)}\n'); + }, + onError: (Object _) {}, + onDone: () { + server.disconnections++; + }, + ); + } catch (_) { + raw.destroy(); + } + }); + return server; + } + + void disconnectClients() { + for (final socket in _sockets) { + socket.destroy(); + } + _sockets.clear(); + } + + Future close() async { + disconnectClients(); + await _subscription.cancel(); + await _server.close(); + } +} + +/// A local SOCKS5 tunnel that records destination names without DNS lookups. +class TlsTestProxy { + TlsTestProxy._(this._server, this.destinationPort, this.greetingDelay); + + final ServerSocket _server; + final int destinationPort; + final Duration greetingDelay; + final destinations = []; + final _sockets = []; + late final StreamSubscription _subscription; + ({InternetAddress host, int port}) get info => + (host: InternetAddress.loopbackIPv4, port: _server.port); + + static Future start( + int destinationPort, { + Duration greetingDelay = Duration.zero, + }) async { + final proxy = TlsTestProxy._( + await ServerSocket.bind(InternetAddress.loopbackIPv4, 0), + destinationPort, + greetingDelay, + ); + proxy._subscription = proxy._server.listen(proxy._forward); + return proxy; + } + + Future _forward(Socket client) async { + _sockets.add(client); + final input = _SocketReader(client); + bool inputForwarded = false; + try { + final greeting = await input.read(2); + await input.read(greeting[1]); + if (greetingDelay != Duration.zero) { + await Future.delayed(greetingDelay); + } + client.add([5, 0]); + final header = await input.read(4); + final String host; + switch (header[3]) { + case 3: + final length = (await input.read(1)).single; + host = utf8.decode(await input.read(length)); + case 1: + host = (await input.read(4)).join('.'); + default: + throw StateError('Unexpected SOCKS destination type'); + } + await input.read(2); + destinations.add(host); + final upstream = await Socket.connect( + InternetAddress.loopbackIPv4, + destinationPort, + ); + _sockets.add(upstream); + client.add([5, 0, 0, 1, 127, 0, 0, 1, 0, 0]); + if (input.pending.isNotEmpty) upstream.add(input.pending); + final remainingInput = input.queue.rest; + inputForwarded = true; + await Future.wait([ + remainingInput.pipe(upstream), + upstream.cast>().pipe(client), + ]); + } catch (_) { + client.destroy(); + if (!inputForwarded) await input.queue.cancel(); + } + } + + Future close() async { + for (final socket in _sockets) { + socket.destroy(); + } + await _subscription.cancel(); + await _server.close(); + } +} + +class _SocketReader { + _SocketReader(Socket socket) : queue = StreamQueue(socket.cast>()); + final StreamQueue> queue; + List pending = []; + + Future> read(int count) async { + while (pending.length < count) { + pending.addAll(await queue.next); + } + final value = pending.sublist(0, count); + pending = pending.sublist(count); + return value; + } +} diff --git a/test/widget_tests/managed_favorite_test.mocks.dart b/test/widget_tests/managed_favorite_test.mocks.dart index f354ad9329..165249a767 100644 --- a/test/widget_tests/managed_favorite_test.mocks.dart +++ b/test/widget_tests/managed_favorite_test.mocks.dart @@ -11,8 +11,9 @@ import 'package:logger/logger.dart' as _i19; import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/src/dummies.dart' as _i17; import 'package:stackwallet/db/isar/main_db.dart' as _i3; -import 'package:stackwallet/models/epicbox_server_model.dart' as _i24; +import 'package:stackwallet/models/epicbox_server_model.dart' as _i25; import 'package:stackwallet/models/isar/stack_theme.dart' as _i14; +import 'package:stackwallet/models/node_form_data.dart' as _i24; import 'package:stackwallet/models/node_model.dart' as _i23; import 'package:stackwallet/networking/http.dart' as _i6; import 'package:stackwallet/services/locale_service.dart' as _i22; @@ -88,31 +89,22 @@ class MockWallets extends _i1.Mock implements _i9.Wallets { } @override - _i2.NodeService get nodeService => - (super.noSuchMethod( - Invocation.getter(#nodeService), - returnValue: _FakeNodeService_0( - this, - Invocation.getter(#nodeService), - ), - ) - as _i2.NodeService); + _i2.NodeService get nodeService => (super.noSuchMethod( + Invocation.getter(#nodeService), + returnValue: _FakeNodeService_0(this, Invocation.getter(#nodeService)), + ) as _i2.NodeService); @override - _i3.MainDB get mainDB => - (super.noSuchMethod( - Invocation.getter(#mainDB), - returnValue: _FakeMainDB_1(this, Invocation.getter(#mainDB)), - ) - as _i3.MainDB); + _i3.MainDB get mainDB => (super.noSuchMethod( + Invocation.getter(#mainDB), + returnValue: _FakeMainDB_1(this, Invocation.getter(#mainDB)), + ) as _i3.MainDB); @override - List<_i5.Wallet<_i4.CryptoCurrency>> get wallets => - (super.noSuchMethod( - Invocation.getter(#wallets), - returnValue: <_i5.Wallet<_i4.CryptoCurrency>>[], - ) - as List<_i5.Wallet<_i4.CryptoCurrency>>); + List<_i5.Wallet<_i4.CryptoCurrency>> get wallets => (super.noSuchMethod( + Invocation.getter(#wallets), + returnValue: <_i5.Wallet<_i4.CryptoCurrency>>[], + ) as List<_i5.Wallet<_i4.CryptoCurrency>>); @override set nodeService(_i2.NodeService? value) => super.noSuchMethod( @@ -129,13 +121,12 @@ class MockWallets extends _i1.Mock implements _i9.Wallets { @override _i5.Wallet<_i4.CryptoCurrency> getWallet(String? walletId) => (super.noSuchMethod( - Invocation.method(#getWallet, [walletId]), - returnValue: _FakeWallet_2<_i4.CryptoCurrency>( - this, - Invocation.method(#getWallet, [walletId]), - ), - ) - as _i5.Wallet<_i4.CryptoCurrency>); + Invocation.method(#getWallet, [walletId]), + returnValue: _FakeWallet_2<_i4.CryptoCurrency>( + this, + Invocation.method(#getWallet, [walletId]), + ), + ) as _i5.Wallet<_i4.CryptoCurrency>); @override void addWallet(_i5.Wallet<_i4.CryptoCurrency>? wallet) => super.noSuchMethod( @@ -147,43 +138,33 @@ class MockWallets extends _i1.Mock implements _i9.Wallets { _i10.Future deleteWallet( _i11.WalletInfo? info, _i8.SecureStorageInterface? secureStorage, - ) => - (super.noSuchMethod( - Invocation.method(#deleteWallet, [info, secureStorage]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + ) => (super.noSuchMethod( + Invocation.method(#deleteWallet, [info, secureStorage]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override _i10.Future load( _i12.Prefs? prefs, _i3.MainDB? mainDB, bool? isDuress, - ) => - (super.noSuchMethod( - Invocation.method(#load, [prefs, mainDB, isDuress]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + ) => (super.noSuchMethod( + Invocation.method(#load, [prefs, mainDB, isDuress]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override _i10.Future loadAfterStackRestore( _i12.Prefs? prefs, List<_i5.Wallet<_i4.CryptoCurrency>>? wallets, bool? isDesktop, - ) => - (super.noSuchMethod( - Invocation.method(#loadAfterStackRestore, [ - prefs, - wallets, - isDesktop, - ]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + ) => (super.noSuchMethod( + Invocation.method(#loadAfterStackRestore, [prefs, wallets, isDesktop]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); } /// A class which mocks [ThemeService]. @@ -195,28 +176,22 @@ class MockThemeService extends _i1.Mock implements _i13.ThemeService { } @override - _i3.MainDB get db => - (super.noSuchMethod( - Invocation.getter(#db), - returnValue: _FakeMainDB_1(this, Invocation.getter(#db)), - ) - as _i3.MainDB); + _i3.MainDB get db => (super.noSuchMethod( + Invocation.getter(#db), + returnValue: _FakeMainDB_1(this, Invocation.getter(#db)), + ) as _i3.MainDB); @override - _i6.HTTP get client => - (super.noSuchMethod( - Invocation.getter(#client), - returnValue: _FakeHTTP_3(this, Invocation.getter(#client)), - ) - as _i6.HTTP); + _i6.HTTP get client => (super.noSuchMethod( + Invocation.getter(#client), + returnValue: _FakeHTTP_3(this, Invocation.getter(#client)), + ) as _i6.HTTP); @override - List<_i14.StackTheme> get installedThemes => - (super.noSuchMethod( - Invocation.getter(#installedThemes), - returnValue: <_i14.StackTheme>[], - ) - as List<_i14.StackTheme>); + List<_i14.StackTheme> get installedThemes => (super.noSuchMethod( + Invocation.getter(#installedThemes), + returnValue: <_i14.StackTheme>[], + ) as List<_i14.StackTheme>); @override set client(_i6.HTTP? value) => super.noSuchMethod( @@ -233,59 +208,48 @@ class MockThemeService extends _i1.Mock implements _i13.ThemeService { @override _i10.Future install({required _i15.Uint8List? themeArchiveData}) => (super.noSuchMethod( - Invocation.method(#install, [], { - #themeArchiveData: themeArchiveData, - }), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + Invocation.method(#install, [], {#themeArchiveData: themeArchiveData}), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i10.Future remove({required String? themeId}) => - (super.noSuchMethod( - Invocation.method(#remove, [], {#themeId: themeId}), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + _i10.Future remove({required String? themeId}) => (super.noSuchMethod( + Invocation.method(#remove, [], {#themeId: themeId}), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i10.Future checkDefaultThemesOnStartup() => - (super.noSuchMethod( - Invocation.method(#checkDefaultThemesOnStartup, []), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + _i10.Future checkDefaultThemesOnStartup() => (super.noSuchMethod( + Invocation.method(#checkDefaultThemesOnStartup, []), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override _i10.Future verifyInstalled({required String? themeId}) => (super.noSuchMethod( - Invocation.method(#verifyInstalled, [], {#themeId: themeId}), - returnValue: _i10.Future.value(false), - ) - as _i10.Future); + Invocation.method(#verifyInstalled, [], {#themeId: themeId}), + returnValue: _i10.Future.value(false), + ) as _i10.Future); @override _i10.Future> fetchThemes() => (super.noSuchMethod( - Invocation.method(#fetchThemes, []), - returnValue: _i10.Future>.value( - <_i13.StackThemeMetaData>[], - ), - ) - as _i10.Future>); + Invocation.method(#fetchThemes, []), + returnValue: _i10.Future>.value( + <_i13.StackThemeMetaData>[], + ), + ) as _i10.Future>); @override _i10.Future<_i15.Uint8List> fetchTheme({ required _i13.StackThemeMetaData? themeMetaData, - }) => - (super.noSuchMethod( - Invocation.method(#fetchTheme, [], {#themeMetaData: themeMetaData}), - returnValue: _i10.Future<_i15.Uint8List>.value(_i15.Uint8List(0)), - ) - as _i10.Future<_i15.Uint8List>); + }) => (super.noSuchMethod( + Invocation.method(#fetchTheme, [], {#themeMetaData: themeMetaData}), + returnValue: _i10.Future<_i15.Uint8List>.value(_i15.Uint8List(0)), + ) as _i10.Future<_i15.Uint8List>); @override _i14.StackTheme? getTheme({required String? themeId}) => @@ -307,12 +271,10 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { as bool); @override - int get lastUnlockedTimeout => - (super.noSuchMethod( - Invocation.getter(#lastUnlockedTimeout), - returnValue: 0, - ) - as int); + int get lastUnlockedTimeout => (super.noSuchMethod( + Invocation.getter(#lastUnlockedTimeout), + returnValue: 0, + ) as int); @override int get lastUnlocked => @@ -320,28 +282,22 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { as int); @override - int get currentNotificationId => - (super.noSuchMethod( - Invocation.getter(#currentNotificationId), - returnValue: 0, - ) - as int); + int get currentNotificationId => (super.noSuchMethod( + Invocation.getter(#currentNotificationId), + returnValue: 0, + ) as int); @override - List get walletIdsSyncOnStartup => - (super.noSuchMethod( - Invocation.getter(#walletIdsSyncOnStartup), - returnValue: [], - ) - as List); + List get walletIdsSyncOnStartup => (super.noSuchMethod( + Invocation.getter(#walletIdsSyncOnStartup), + returnValue: [], + ) as List); @override - _i16.SyncingType get syncType => - (super.noSuchMethod( - Invocation.getter(#syncType), - returnValue: _i16.SyncingType.currentWalletOnly, - ) - as _i16.SyncingType); + _i16.SyncingType get syncType => (super.noSuchMethod( + Invocation.getter(#syncType), + returnValue: _i16.SyncingType.currentWalletOnly, + ) as _i16.SyncingType); @override bool get wifiOnly => @@ -349,34 +305,22 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { as bool); @override - bool get showFavoriteWallets => - (super.noSuchMethod( - Invocation.getter(#showFavoriteWallets), - returnValue: false, - ) - as bool); + bool get showFavoriteWallets => (super.noSuchMethod( + Invocation.getter(#showFavoriteWallets), + returnValue: false, + ) as bool); @override - String get language => - (super.noSuchMethod( - Invocation.getter(#language), - returnValue: _i17.dummyValue( - this, - Invocation.getter(#language), - ), - ) - as String); + String get language => (super.noSuchMethod( + Invocation.getter(#language), + returnValue: _i17.dummyValue(this, Invocation.getter(#language)), + ) as String); @override - String get currency => - (super.noSuchMethod( - Invocation.getter(#currency), - returnValue: _i17.dummyValue( - this, - Invocation.getter(#currency), - ), - ) - as String); + String get currency => (super.noSuchMethod( + Invocation.getter(#currency), + returnValue: _i17.dummyValue(this, Invocation.getter(#currency)), + ) as String); @override bool get randomizePIN => @@ -399,12 +343,10 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { as bool); @override - bool get biometricsDuress => - (super.noSuchMethod( - Invocation.getter(#biometricsDuress), - returnValue: false, - ) - as bool); + bool get biometricsDuress => (super.noSuchMethod( + Invocation.getter(#biometricsDuress), + returnValue: false, + ) as bool); @override int get familiarity => @@ -417,44 +359,34 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { as bool); @override - bool get showTestNetCoins => - (super.noSuchMethod( - Invocation.getter(#showTestNetCoins), - returnValue: false, - ) - as bool); + bool get showTestNetCoins => (super.noSuchMethod( + Invocation.getter(#showTestNetCoins), + returnValue: false, + ) as bool); @override - bool get isAutoBackupEnabled => - (super.noSuchMethod( - Invocation.getter(#isAutoBackupEnabled), - returnValue: false, - ) - as bool); + bool get isAutoBackupEnabled => (super.noSuchMethod( + Invocation.getter(#isAutoBackupEnabled), + returnValue: false, + ) as bool); @override - _i18.BackupFrequencyType get backupFrequencyType => - (super.noSuchMethod( - Invocation.getter(#backupFrequencyType), - returnValue: _i18.BackupFrequencyType.everyTenMinutes, - ) - as _i18.BackupFrequencyType); + _i18.BackupFrequencyType get backupFrequencyType => (super.noSuchMethod( + Invocation.getter(#backupFrequencyType), + returnValue: _i18.BackupFrequencyType.everyTenMinutes, + ) as _i18.BackupFrequencyType); @override - bool get hideBlockExplorerWarning => - (super.noSuchMethod( - Invocation.getter(#hideBlockExplorerWarning), - returnValue: false, - ) - as bool); + bool get hideBlockExplorerWarning => (super.noSuchMethod( + Invocation.getter(#hideBlockExplorerWarning), + returnValue: false, + ) as bool); @override - bool get gotoWalletOnStartup => - (super.noSuchMethod( - Invocation.getter(#gotoWalletOnStartup), - returnValue: false, - ) - as bool); + bool get gotoWalletOnStartup => (super.noSuchMethod( + Invocation.getter(#gotoWalletOnStartup), + returnValue: false, + ) as bool); @override bool get externalCalls => @@ -462,53 +394,40 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { as bool); @override - bool get enableCoinControl => - (super.noSuchMethod( - Invocation.getter(#enableCoinControl), - returnValue: false, - ) - as bool); + bool get enableCoinControl => (super.noSuchMethod( + Invocation.getter(#enableCoinControl), + returnValue: false, + ) as bool); @override - bool get enableSystemBrightness => - (super.noSuchMethod( - Invocation.getter(#enableSystemBrightness), - returnValue: false, - ) - as bool); + bool get enableSystemBrightness => (super.noSuchMethod( + Invocation.getter(#enableSystemBrightness), + returnValue: false, + ) as bool); @override - String get themeId => - (super.noSuchMethod( - Invocation.getter(#themeId), - returnValue: _i17.dummyValue( - this, - Invocation.getter(#themeId), - ), - ) - as String); + String get themeId => (super.noSuchMethod( + Invocation.getter(#themeId), + returnValue: _i17.dummyValue(this, Invocation.getter(#themeId)), + ) as String); @override - String get systemBrightnessLightThemeId => - (super.noSuchMethod( - Invocation.getter(#systemBrightnessLightThemeId), - returnValue: _i17.dummyValue( - this, - Invocation.getter(#systemBrightnessLightThemeId), - ), - ) - as String); + String get systemBrightnessLightThemeId => (super.noSuchMethod( + Invocation.getter(#systemBrightnessLightThemeId), + returnValue: _i17.dummyValue( + this, + Invocation.getter(#systemBrightnessLightThemeId), + ), + ) as String); @override - String get systemBrightnessDarkThemeId => - (super.noSuchMethod( - Invocation.getter(#systemBrightnessDarkThemeId), - returnValue: _i17.dummyValue( - this, - Invocation.getter(#systemBrightnessDarkThemeId), - ), - ) - as String); + String get systemBrightnessDarkThemeId => (super.noSuchMethod( + Invocation.getter(#systemBrightnessDarkThemeId), + returnValue: _i17.dummyValue( + this, + Invocation.getter(#systemBrightnessDarkThemeId), + ), + ) as String); @override bool get useTor => @@ -521,36 +440,28 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { as bool); @override - bool get enableExchange => - (super.noSuchMethod( - Invocation.getter(#enableExchange), - returnValue: false, - ) - as bool); + bool get enableExchange => (super.noSuchMethod( + Invocation.getter(#enableExchange), + returnValue: false, + ) as bool); @override - bool get advancedFiroFeatures => - (super.noSuchMethod( - Invocation.getter(#advancedFiroFeatures), - returnValue: false, - ) - as bool); + bool get advancedFiroFeatures => (super.noSuchMethod( + Invocation.getter(#advancedFiroFeatures), + returnValue: false, + ) as bool); @override - _i19.Level get logLevel => - (super.noSuchMethod( - Invocation.getter(#logLevel), - returnValue: _i19.Level.all, - ) - as _i19.Level); + _i19.Level get logLevel => (super.noSuchMethod( + Invocation.getter(#logLevel), + returnValue: _i19.Level.all, + ) as _i19.Level); @override - ({bool enabled, int minutes}) get autoLockInfo => - (super.noSuchMethod( - Invocation.getter(#autoLockInfo), - returnValue: (enabled: false, minutes: 0), - ) - as ({bool enabled, int minutes})); + ({bool enabled, int minutes}) get autoLockInfo => (super.noSuchMethod( + Invocation.getter(#autoLockInfo), + returnValue: (enabled: false, minutes: 0), + ) as ({bool enabled, int minutes})); @override bool get privacyScreen => @@ -558,12 +469,10 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { as bool); @override - bool get disableScreenShots => - (super.noSuchMethod( - Invocation.getter(#disableScreenShots), - returnValue: false, - ) - as bool); + bool get disableScreenShots => (super.noSuchMethod( + Invocation.getter(#disableScreenShots), + returnValue: false, + ) as bool); @override set lastUnlockedTimeout(int? lastUnlockedTimeout) => super.noSuchMethod( @@ -812,55 +721,43 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { as bool); @override - _i10.Future init() => - (super.noSuchMethod( - Invocation.method(#init, []), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + _i10.Future init() => (super.noSuchMethod( + Invocation.method(#init, []), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i10.Future incrementCurrentNotificationIndex() => - (super.noSuchMethod( - Invocation.method(#incrementCurrentNotificationIndex, []), - returnValue: _i10.Future.value(0), - ) - as _i10.Future); + _i10.Future incrementCurrentNotificationIndex() => (super.noSuchMethod( + Invocation.method(#incrementCurrentNotificationIndex, []), + returnValue: _i10.Future.value(0), + ) as _i10.Future); @override - _i10.Future isExternalCallsSet() => - (super.noSuchMethod( - Invocation.method(#isExternalCallsSet, []), - returnValue: _i10.Future.value(false), - ) - as _i10.Future); + _i10.Future isExternalCallsSet() => (super.noSuchMethod( + Invocation.method(#isExternalCallsSet, []), + returnValue: _i10.Future.value(false), + ) as _i10.Future); @override - _i10.Future saveUserID(String? userId) => - (super.noSuchMethod( - Invocation.method(#saveUserID, [userId]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + _i10.Future saveUserID(String? userId) => (super.noSuchMethod( + Invocation.method(#saveUserID, [userId]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i10.Future saveSignupEpoch(int? signupEpoch) => - (super.noSuchMethod( - Invocation.method(#saveSignupEpoch, [signupEpoch]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + _i10.Future saveSignupEpoch(int? signupEpoch) => (super.noSuchMethod( + Invocation.method(#saveSignupEpoch, [signupEpoch]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i20.AmountUnit amountUnit(_i4.CryptoCurrency? coin) => - (super.noSuchMethod( - Invocation.method(#amountUnit, [coin]), - returnValue: _i20.AmountUnit.normal, - ) - as _i20.AmountUnit); + _i20.AmountUnit amountUnit(_i4.CryptoCurrency? coin) => (super.noSuchMethod( + Invocation.method(#amountUnit, [coin]), + returnValue: _i20.AmountUnit.normal, + ) as _i20.AmountUnit); @override void updateAmountUnit({ @@ -875,12 +772,10 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { ); @override - int maxDecimals(_i4.CryptoCurrency? coin) => - (super.noSuchMethod( - Invocation.method(#maxDecimals, [coin]), - returnValue: 0, - ) - as int); + int maxDecimals(_i4.CryptoCurrency? coin) => (super.noSuchMethod( + Invocation.method(#maxDecimals, [coin]), + returnValue: 0, + ) as int); @override void updateMaxDecimals({ @@ -897,13 +792,12 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { @override _i7.FusionInfo getFusionServerInfo(_i4.CryptoCurrency? coin) => (super.noSuchMethod( - Invocation.method(#getFusionServerInfo, [coin]), - returnValue: _FakeFusionInfo_4( - this, - Invocation.method(#getFusionServerInfo, [coin]), - ), - ) - as _i7.FusionInfo); + Invocation.method(#getFusionServerInfo, [coin]), + returnValue: _FakeFusionInfo_4( + this, + Invocation.method(#getFusionServerInfo, [coin]), + ), + ) as _i7.FusionInfo); @override void setFusionServerInfo( @@ -948,15 +842,10 @@ class MockLocaleService extends _i1.Mock implements _i22.LocaleService { } @override - String get locale => - (super.noSuchMethod( - Invocation.getter(#locale), - returnValue: _i17.dummyValue( - this, - Invocation.getter(#locale), - ), - ) - as String); + String get locale => (super.noSuchMethod( + Invocation.getter(#locale), + returnValue: _i17.dummyValue(this, Invocation.getter(#locale)), + ) as String); @override bool get hasListeners => @@ -964,13 +853,11 @@ class MockLocaleService extends _i1.Mock implements _i22.LocaleService { as bool); @override - _i10.Future loadLocale({bool? notify = true}) => - (super.noSuchMethod( - Invocation.method(#loadLocale, [], {#notify: notify}), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + _i10.Future loadLocale({bool? notify = true}) => (super.noSuchMethod( + Invocation.method(#loadLocale, [], {#notify: notify}), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override void addListener(_i21.VoidCallback? listener) => super.noSuchMethod( @@ -1006,31 +893,25 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { } @override - _i8.SecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( - Invocation.getter(#secureStorageInterface), - returnValue: _FakeSecureStorageInterface_5( - this, - Invocation.getter(#secureStorageInterface), - ), - ) - as _i8.SecureStorageInterface); + _i8.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( + Invocation.getter(#secureStorageInterface), + returnValue: _FakeSecureStorageInterface_5( + this, + Invocation.getter(#secureStorageInterface), + ), + ) as _i8.SecureStorageInterface); @override - List<_i23.NodeModel> get primaryNodes => - (super.noSuchMethod( - Invocation.getter(#primaryNodes), - returnValue: <_i23.NodeModel>[], - ) - as List<_i23.NodeModel>); + List<_i23.NodeModel> get primaryNodes => (super.noSuchMethod( + Invocation.getter(#primaryNodes), + returnValue: <_i23.NodeModel>[], + ) as List<_i23.NodeModel>); @override - List<_i23.NodeModel> get nodes => - (super.noSuchMethod( - Invocation.getter(#nodes), - returnValue: <_i23.NodeModel>[], - ) - as List<_i23.NodeModel>); + List<_i23.NodeModel> get nodes => (super.noSuchMethod( + Invocation.getter(#nodes), + returnValue: <_i23.NodeModel>[], + ) as List<_i23.NodeModel>); @override bool get hasListeners => @@ -1038,45 +919,39 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { as bool); @override - _i10.Future updateDefaults() => - (super.noSuchMethod( - Invocation.method(#updateDefaults, []), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + _i10.Future updateDefaults() => (super.noSuchMethod( + Invocation.method(#updateDefaults, []), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override _i10.Future setPrimaryNodeFor({ required _i4.CryptoCurrency? coin, required _i23.NodeModel? node, bool? shouldNotifyListeners = false, - }) => - (super.noSuchMethod( - Invocation.method(#setPrimaryNodeFor, [], { - #coin: coin, - #node: node, - #shouldNotifyListeners: shouldNotifyListeners, - }), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + }) => (super.noSuchMethod( + Invocation.method(#setPrimaryNodeFor, [], { + #coin: coin, + #node: node, + #shouldNotifyListeners: shouldNotifyListeners, + }), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override _i23.NodeModel? getPrimaryNodeFor({required _i4.CryptoCurrency? currency}) => (super.noSuchMethod( - Invocation.method(#getPrimaryNodeFor, [], {#currency: currency}), - ) - as _i23.NodeModel?); + Invocation.method(#getPrimaryNodeFor, [], {#currency: currency}), + ) as _i23.NodeModel?); @override List<_i23.NodeModel> getNodesFor(_i4.CryptoCurrency? coin) => (super.noSuchMethod( - Invocation.method(#getNodesFor, [coin]), - returnValue: <_i23.NodeModel>[], - ) - as List<_i23.NodeModel>); + Invocation.method(#getNodesFor, [coin]), + returnValue: <_i23.NodeModel>[], + ) as List<_i23.NodeModel>); @override _i23.NodeModel? getNodeById({required String? id}) => @@ -1086,118 +961,152 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { @override List<_i23.NodeModel> failoverNodesFor({ required _i4.CryptoCurrency? currency, - }) => - (super.noSuchMethod( - Invocation.method(#failoverNodesFor, [], {#currency: currency}), - returnValue: <_i23.NodeModel>[], - ) - as List<_i23.NodeModel>); + }) => (super.noSuchMethod( + Invocation.method(#failoverNodesFor, [], {#currency: currency}), + returnValue: <_i23.NodeModel>[], + ) as List<_i23.NodeModel>); @override _i10.Future save( _i23.NodeModel? node, String? password, bool? shouldNotifyListeners, - ) => + ) => (super.noSuchMethod( + Invocation.method(#save, [node, password, shouldNotifyListeners]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); + + @override + _i10.Future saveEdit( + _i23.NodeModel? node, + String? password, + bool? notify, { + required _i23.NodeModel? opened, + bool? forgetCertificate = false, + }) => (super.noSuchMethod( + Invocation.method( + #saveEdit, + [node, password, notify], + {#opened: opened, #forgetCertificate: forgetCertificate}, + ), + returnValue: _i10.Future.value(false), + ) as _i10.Future); + + @override + bool prepareCertificateTest(_i24.NodeFormData? data) => (super.noSuchMethod( + Invocation.method(#prepareCertificateTest, [data]), + returnValue: false, + ) as bool); + + @override + _i10.Future saveTrustedCertificate( + _i23.NodeModel? node, + _i24.NodeFormData? data, + ) => (super.noSuchMethod( + Invocation.method(#saveTrustedCertificate, [node, data]), + returnValue: _i10.Future.value(false), + ) as _i10.Future); + + @override + _i10.Future forgetTrustedCertificate(_i23.NodeModel? shown) => (super.noSuchMethod( - Invocation.method(#save, [node, password, shouldNotifyListeners]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + Invocation.method(#forgetTrustedCertificate, [shown]), + returnValue: _i10.Future.value(false), + ) as _i10.Future); + + @override + _i10.Future commitConnectionTest({ + required _i23.NodeModel? node, + required _i24.NodeFormData? formData, + }) => (super.noSuchMethod( + Invocation.method(#commitConnectionTest, [], { + #node: node, + #formData: formData, + }), + returnValue: _i10.Future.value(false), + ) as _i10.Future); + + @override + _i10.Future setFailover(String? id, bool? value) => (super.noSuchMethod( + Invocation.method(#setFailover, [id, value]), + returnValue: _i10.Future.value(false), + ) as _i10.Future); @override _i10.Future delete(String? id, bool? shouldNotifyListeners) => (super.noSuchMethod( - Invocation.method(#delete, [id, shouldNotifyListeners]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + Invocation.method(#delete, [id, shouldNotifyListeners]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override _i10.Future setEnabledState( String? id, bool? enabled, bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method(#setEnabledState, [ - id, - enabled, - shouldNotifyListeners, - ]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); - - @override - _i10.Future updateDefaultEpicBoxes() => - (super.noSuchMethod( - Invocation.method(#updateDefaultEpicBoxes, []), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + ) => (super.noSuchMethod( + Invocation.method(#setEnabledState, [id, enabled, shouldNotifyListeners]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); + + @override + _i10.Future updateDefaultEpicBoxes() => (super.noSuchMethod( + Invocation.method(#updateDefaultEpicBoxes, []), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override _i10.Future setPrimaryEpicBox({ - required _i24.EpicBoxServerModel? epicBox, + required _i25.EpicBoxServerModel? epicBox, bool? shouldNotifyListeners = false, - }) => - (super.noSuchMethod( - Invocation.method(#setPrimaryEpicBox, [], { - #epicBox: epicBox, - #shouldNotifyListeners: shouldNotifyListeners, - }), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + }) => (super.noSuchMethod( + Invocation.method(#setPrimaryEpicBox, [], { + #epicBox: epicBox, + #shouldNotifyListeners: shouldNotifyListeners, + }), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - List<_i24.EpicBoxServerModel> getEpicBoxes() => - (super.noSuchMethod( - Invocation.method(#getEpicBoxes, []), - returnValue: <_i24.EpicBoxServerModel>[], - ) - as List<_i24.EpicBoxServerModel>); + List<_i25.EpicBoxServerModel> getEpicBoxes() => (super.noSuchMethod( + Invocation.method(#getEpicBoxes, []), + returnValue: <_i25.EpicBoxServerModel>[], + ) as List<_i25.EpicBoxServerModel>); @override - _i24.EpicBoxServerModel? getEpicBoxById({required String? id}) => + _i25.EpicBoxServerModel? getEpicBoxById({required String? id}) => (super.noSuchMethod(Invocation.method(#getEpicBoxById, [], {#id: id})) - as _i24.EpicBoxServerModel?); + as _i25.EpicBoxServerModel?); @override _i10.Future addEpicBox( - _i24.EpicBoxServerModel? epicBox, + _i25.EpicBoxServerModel? epicBox, bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method(#addEpicBox, [epicBox, shouldNotifyListeners]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + ) => (super.noSuchMethod( + Invocation.method(#addEpicBox, [epicBox, shouldNotifyListeners]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override _i10.Future deleteEpicBox(String? id, bool? shouldNotifyListeners) => (super.noSuchMethod( - Invocation.method(#deleteEpicBox, [id, shouldNotifyListeners]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + Invocation.method(#deleteEpicBox, [id, shouldNotifyListeners]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i10.Future updateCommunityNodes() => - (super.noSuchMethod( - Invocation.method(#updateCommunityNodes, []), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + _i10.Future updateCommunityNodes() => (super.noSuchMethod( + Invocation.method(#updateCommunityNodes, []), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override void addListener(_i21.VoidCallback? listener) => super.noSuchMethod( diff --git a/test/widget_tests/node_card_test.dart b/test/widget_tests/node_card_test.dart index f159dab708..f57a62b60b 100644 --- a/test/widget_tests/node_card_test.dart +++ b/test/widget_tests/node_card_test.dart @@ -77,9 +77,8 @@ void main() { when( nodeService.getPrimaryNodeFor(currency: bitcoin), ).thenAnswer((_) => buildNode(id: 'other node id', name: 'Stack Default')); - when( - nodeService.getNodeById(id: 'node id'), - ).thenAnswer((_) => buildNode(id: 'node id', name: 'some other name')); + when(nodeService.getNodeById(id: 'node id')) + .thenAnswer((_) => buildNode(id: 'node id', name: 'some other name')); await pumpSubject( tester, @@ -101,9 +100,8 @@ void main() { final nodeService = MockNodeService(); final activeNode = buildNode(id: 'node id', name: 'Some other node name'); - when( - nodeService.getPrimaryNodeFor(currency: bitcoin), - ).thenAnswer((_) => activeNode); + when(nodeService.getPrimaryNodeFor(currency: bitcoin)) + .thenAnswer((_) => activeNode); when(nodeService.getNodeById(id: 'node id')).thenAnswer((_) => activeNode); await pumpSubject( @@ -127,9 +125,8 @@ void main() { final nodeService = MockNodeService(); final activeNode = buildNode(id: 'node id', name: 'Stack Default'); - when( - nodeService.getPrimaryNodeFor(currency: bitcoin), - ).thenAnswer((_) => activeNode); + when(nodeService.getPrimaryNodeFor(currency: bitcoin)) + .thenAnswer((_) => activeNode); when(nodeService.getNodeById(id: 'node id')).thenAnswer((_) => activeNode); await pumpSubject( @@ -178,9 +175,8 @@ void main() { when(nodeService.getPrimaryNodeFor(currency: bitcoin)).thenAnswer( (_) => buildNode(id: 'other node id', name: 'Some other node name'), ); - when( - nodeService.getNodeById(id: 'node id'), - ).thenAnswer((_) => disconnectedNode); + when(nodeService.getNodeById(id: 'node id')) + .thenAnswer((_) => disconnectedNode); await pumpSubject( tester, @@ -211,10 +207,9 @@ void main() { expect(platformOverrides.connectionInvocations.single.host, '127.0.0.1'); verifyNever( - nodeService.setPrimaryNodeFor( - coin: bitcoin, + nodeService.commitConnectionTest( node: anyNamed('node'), - shouldNotifyListeners: anyNamed('shouldNotifyListeners'), + formData: anyNamed('formData'), ), ); }, diff --git a/test/widget_tests/node_card_test.mocks.dart b/test/widget_tests/node_card_test.mocks.dart index 73db103f60..d1f39800cd 100644 --- a/test/widget_tests/node_card_test.mocks.dart +++ b/test/widget_tests/node_card_test.mocks.dart @@ -4,10 +4,11 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i5; -import 'dart:ui' as _i8; +import 'dart:ui' as _i9; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/models/epicbox_server_model.dart' as _i7; +import 'package:stackwallet/models/epicbox_server_model.dart' as _i8; +import 'package:stackwallet/models/node_form_data.dart' as _i7; import 'package:stackwallet/models/node_model.dart' as _i4; import 'package:stackwallet/services/node_service.dart' as _i3; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' @@ -45,31 +46,25 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { } @override - _i2.SecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( - Invocation.getter(#secureStorageInterface), - returnValue: _FakeSecureStorageInterface_0( - this, - Invocation.getter(#secureStorageInterface), - ), - ) - as _i2.SecureStorageInterface); + _i2.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( + Invocation.getter(#secureStorageInterface), + returnValue: _FakeSecureStorageInterface_0( + this, + Invocation.getter(#secureStorageInterface), + ), + ) as _i2.SecureStorageInterface); @override - List<_i4.NodeModel> get primaryNodes => - (super.noSuchMethod( - Invocation.getter(#primaryNodes), - returnValue: <_i4.NodeModel>[], - ) - as List<_i4.NodeModel>); + List<_i4.NodeModel> get primaryNodes => (super.noSuchMethod( + Invocation.getter(#primaryNodes), + returnValue: <_i4.NodeModel>[], + ) as List<_i4.NodeModel>); @override - List<_i4.NodeModel> get nodes => - (super.noSuchMethod( - Invocation.getter(#nodes), - returnValue: <_i4.NodeModel>[], - ) - as List<_i4.NodeModel>); + List<_i4.NodeModel> get nodes => (super.noSuchMethod( + Invocation.getter(#nodes), + returnValue: <_i4.NodeModel>[], + ) as List<_i4.NodeModel>); @override bool get hasListeners => @@ -77,45 +72,39 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { as bool); @override - _i5.Future updateDefaults() => - (super.noSuchMethod( - Invocation.method(#updateDefaults, []), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + _i5.Future updateDefaults() => (super.noSuchMethod( + Invocation.method(#updateDefaults, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future setPrimaryNodeFor({ required _i6.CryptoCurrency? coin, required _i4.NodeModel? node, bool? shouldNotifyListeners = false, - }) => - (super.noSuchMethod( - Invocation.method(#setPrimaryNodeFor, [], { - #coin: coin, - #node: node, - #shouldNotifyListeners: shouldNotifyListeners, - }), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + }) => (super.noSuchMethod( + Invocation.method(#setPrimaryNodeFor, [], { + #coin: coin, + #node: node, + #shouldNotifyListeners: shouldNotifyListeners, + }), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i4.NodeModel? getPrimaryNodeFor({required _i6.CryptoCurrency? currency}) => (super.noSuchMethod( - Invocation.method(#getPrimaryNodeFor, [], {#currency: currency}), - ) - as _i4.NodeModel?); + Invocation.method(#getPrimaryNodeFor, [], {#currency: currency}), + ) as _i4.NodeModel?); @override List<_i4.NodeModel> getNodesFor(_i6.CryptoCurrency? coin) => (super.noSuchMethod( - Invocation.method(#getNodesFor, [coin]), - returnValue: <_i4.NodeModel>[], - ) - as List<_i4.NodeModel>); + Invocation.method(#getNodesFor, [coin]), + returnValue: <_i4.NodeModel>[], + ) as List<_i4.NodeModel>); @override _i4.NodeModel? getNodeById({required String? id}) => @@ -125,127 +114,161 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { @override List<_i4.NodeModel> failoverNodesFor({ required _i6.CryptoCurrency? currency, - }) => - (super.noSuchMethod( - Invocation.method(#failoverNodesFor, [], {#currency: currency}), - returnValue: <_i4.NodeModel>[], - ) - as List<_i4.NodeModel>); + }) => (super.noSuchMethod( + Invocation.method(#failoverNodesFor, [], {#currency: currency}), + returnValue: <_i4.NodeModel>[], + ) as List<_i4.NodeModel>); @override _i5.Future save( _i4.NodeModel? node, String? password, bool? shouldNotifyListeners, - ) => + ) => (super.noSuchMethod( + Invocation.method(#save, [node, password, shouldNotifyListeners]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future saveEdit( + _i4.NodeModel? node, + String? password, + bool? notify, { + required _i4.NodeModel? opened, + bool? forgetCertificate = false, + }) => (super.noSuchMethod( + Invocation.method( + #saveEdit, + [node, password, notify], + {#opened: opened, #forgetCertificate: forgetCertificate}, + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + + @override + bool prepareCertificateTest(_i7.NodeFormData? data) => (super.noSuchMethod( + Invocation.method(#prepareCertificateTest, [data]), + returnValue: false, + ) as bool); + + @override + _i5.Future saveTrustedCertificate( + _i4.NodeModel? node, + _i7.NodeFormData? data, + ) => (super.noSuchMethod( + Invocation.method(#saveTrustedCertificate, [node, data]), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + + @override + _i5.Future forgetTrustedCertificate(_i4.NodeModel? shown) => (super.noSuchMethod( - Invocation.method(#save, [node, password, shouldNotifyListeners]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + Invocation.method(#forgetTrustedCertificate, [shown]), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + + @override + _i5.Future commitConnectionTest({ + required _i4.NodeModel? node, + required _i7.NodeFormData? formData, + }) => (super.noSuchMethod( + Invocation.method(#commitConnectionTest, [], { + #node: node, + #formData: formData, + }), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + + @override + _i5.Future setFailover(String? id, bool? value) => (super.noSuchMethod( + Invocation.method(#setFailover, [id, value]), + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override _i5.Future delete(String? id, bool? shouldNotifyListeners) => (super.noSuchMethod( - Invocation.method(#delete, [id, shouldNotifyListeners]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + Invocation.method(#delete, [id, shouldNotifyListeners]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future setEnabledState( String? id, bool? enabled, bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method(#setEnabledState, [ - id, - enabled, - shouldNotifyListeners, - ]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + ) => (super.noSuchMethod( + Invocation.method(#setEnabledState, [id, enabled, shouldNotifyListeners]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i5.Future updateDefaultEpicBoxes() => - (super.noSuchMethod( - Invocation.method(#updateDefaultEpicBoxes, []), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + _i5.Future updateDefaultEpicBoxes() => (super.noSuchMethod( + Invocation.method(#updateDefaultEpicBoxes, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future setPrimaryEpicBox({ - required _i7.EpicBoxServerModel? epicBox, + required _i8.EpicBoxServerModel? epicBox, bool? shouldNotifyListeners = false, - }) => - (super.noSuchMethod( - Invocation.method(#setPrimaryEpicBox, [], { - #epicBox: epicBox, - #shouldNotifyListeners: shouldNotifyListeners, - }), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + }) => (super.noSuchMethod( + Invocation.method(#setPrimaryEpicBox, [], { + #epicBox: epicBox, + #shouldNotifyListeners: shouldNotifyListeners, + }), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - List<_i7.EpicBoxServerModel> getEpicBoxes() => - (super.noSuchMethod( - Invocation.method(#getEpicBoxes, []), - returnValue: <_i7.EpicBoxServerModel>[], - ) - as List<_i7.EpicBoxServerModel>); + List<_i8.EpicBoxServerModel> getEpicBoxes() => (super.noSuchMethod( + Invocation.method(#getEpicBoxes, []), + returnValue: <_i8.EpicBoxServerModel>[], + ) as List<_i8.EpicBoxServerModel>); @override - _i7.EpicBoxServerModel? getEpicBoxById({required String? id}) => + _i8.EpicBoxServerModel? getEpicBoxById({required String? id}) => (super.noSuchMethod(Invocation.method(#getEpicBoxById, [], {#id: id})) - as _i7.EpicBoxServerModel?); + as _i8.EpicBoxServerModel?); @override _i5.Future addEpicBox( - _i7.EpicBoxServerModel? epicBox, + _i8.EpicBoxServerModel? epicBox, bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method(#addEpicBox, [epicBox, shouldNotifyListeners]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + ) => (super.noSuchMethod( + Invocation.method(#addEpicBox, [epicBox, shouldNotifyListeners]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future deleteEpicBox(String? id, bool? shouldNotifyListeners) => (super.noSuchMethod( - Invocation.method(#deleteEpicBox, [id, shouldNotifyListeners]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + Invocation.method(#deleteEpicBox, [id, shouldNotifyListeners]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i5.Future updateCommunityNodes() => - (super.noSuchMethod( - Invocation.method(#updateCommunityNodes, []), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + _i5.Future updateCommunityNodes() => (super.noSuchMethod( + Invocation.method(#updateCommunityNodes, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - void addListener(_i8.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i9.VoidCallback? listener) => super.noSuchMethod( Invocation.method(#addListener, [listener]), returnValueForMissingStub: null, ); @override - void removeListener(_i8.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i9.VoidCallback? listener) => super.noSuchMethod( Invocation.method(#removeListener, [listener]), returnValueForMissingStub: null, ); diff --git a/test/widget_tests/node_options_sheet_test.dart b/test/widget_tests/node_options_sheet_test.dart index 26cfffb339..afe94d5b90 100644 --- a/test/widget_tests/node_options_sheet_test.dart +++ b/test/widget_tests/node_options_sheet_test.dart @@ -62,9 +62,8 @@ void main() { when(wallets.wallets).thenReturn([]); when(prefs.syncType).thenReturn(SyncingType.currentWalletOnly); when(nodeService.getNodeById(id: node.id)).thenAnswer((_) => node); - when( - nodeService.getPrimaryNodeFor(currency: bitcoin), - ).thenAnswer((_) => primaryNode); + when(nodeService.getPrimaryNodeFor(currency: bitcoin)) + .thenAnswer((_) => primaryNode); } Future pumpSubject( @@ -209,12 +208,11 @@ void main() { primaryNode: otherPrimary, ); when( - mockNodeService.setPrimaryNodeFor( - coin: bitcoin, + mockNodeService.commitConnectionTest( node: node, - shouldNotifyListeners: true, + formData: anyNamed('formData'), ), - ).thenAnswer((_) async {}); + ).thenAnswer((_) async => true); await pumpSubject( tester, @@ -238,10 +236,9 @@ void main() { expect(platformOverrides.connectionInvocations.single.host, '127.0.0.1'); verify( - mockNodeService.setPrimaryNodeFor( - coin: bitcoin, + mockNodeService.commitConnectionTest( node: node, - shouldNotifyListeners: true, + formData: anyNamed('formData'), ), ).called(1); }); @@ -285,10 +282,9 @@ void main() { expect(platformOverrides.connectionInvocations.single.password, isNull); verifyNever( - mockNodeService.setPrimaryNodeFor( - coin: bitcoin, + mockNodeService.commitConnectionTest( node: anyNamed('node'), - shouldNotifyListeners: anyNamed('shouldNotifyListeners'), + formData: anyNamed('formData'), ), ); }, diff --git a/test/widget_tests/node_options_sheet_test.mocks.dart b/test/widget_tests/node_options_sheet_test.mocks.dart index 3b2ce90f4e..8e2b8249bf 100644 --- a/test/widget_tests/node_options_sheet_test.mocks.dart +++ b/test/widget_tests/node_options_sheet_test.mocks.dart @@ -11,12 +11,13 @@ import 'package:logger/logger.dart' as _i16; import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/src/dummies.dart' as _i14; import 'package:stackwallet/db/isar/main_db.dart' as _i3; -import 'package:stackwallet/models/epicbox_server_model.dart' as _i20; +import 'package:stackwallet/models/epicbox_server_model.dart' as _i21; +import 'package:stackwallet/models/node_form_data.dart' as _i20; import 'package:stackwallet/models/node_model.dart' as _i19; import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart' - as _i22; + as _i23; import 'package:stackwallet/services/node_service.dart' as _i2; -import 'package:stackwallet/services/tor_service.dart' as _i21; +import 'package:stackwallet/services/tor_service.dart' as _i22; import 'package:stackwallet/services/wallets.dart' as _i9; import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i17; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i15; @@ -88,31 +89,22 @@ class MockWallets extends _i1.Mock implements _i9.Wallets { } @override - _i2.NodeService get nodeService => - (super.noSuchMethod( - Invocation.getter(#nodeService), - returnValue: _FakeNodeService_0( - this, - Invocation.getter(#nodeService), - ), - ) - as _i2.NodeService); + _i2.NodeService get nodeService => (super.noSuchMethod( + Invocation.getter(#nodeService), + returnValue: _FakeNodeService_0(this, Invocation.getter(#nodeService)), + ) as _i2.NodeService); @override - _i3.MainDB get mainDB => - (super.noSuchMethod( - Invocation.getter(#mainDB), - returnValue: _FakeMainDB_1(this, Invocation.getter(#mainDB)), - ) - as _i3.MainDB); + _i3.MainDB get mainDB => (super.noSuchMethod( + Invocation.getter(#mainDB), + returnValue: _FakeMainDB_1(this, Invocation.getter(#mainDB)), + ) as _i3.MainDB); @override - List<_i5.Wallet<_i4.CryptoCurrency>> get wallets => - (super.noSuchMethod( - Invocation.getter(#wallets), - returnValue: <_i5.Wallet<_i4.CryptoCurrency>>[], - ) - as List<_i5.Wallet<_i4.CryptoCurrency>>); + List<_i5.Wallet<_i4.CryptoCurrency>> get wallets => (super.noSuchMethod( + Invocation.getter(#wallets), + returnValue: <_i5.Wallet<_i4.CryptoCurrency>>[], + ) as List<_i5.Wallet<_i4.CryptoCurrency>>); @override set nodeService(_i2.NodeService? value) => super.noSuchMethod( @@ -129,13 +121,12 @@ class MockWallets extends _i1.Mock implements _i9.Wallets { @override _i5.Wallet<_i4.CryptoCurrency> getWallet(String? walletId) => (super.noSuchMethod( - Invocation.method(#getWallet, [walletId]), - returnValue: _FakeWallet_2<_i4.CryptoCurrency>( - this, - Invocation.method(#getWallet, [walletId]), - ), - ) - as _i5.Wallet<_i4.CryptoCurrency>); + Invocation.method(#getWallet, [walletId]), + returnValue: _FakeWallet_2<_i4.CryptoCurrency>( + this, + Invocation.method(#getWallet, [walletId]), + ), + ) as _i5.Wallet<_i4.CryptoCurrency>); @override void addWallet(_i5.Wallet<_i4.CryptoCurrency>? wallet) => super.noSuchMethod( @@ -147,43 +138,33 @@ class MockWallets extends _i1.Mock implements _i9.Wallets { _i10.Future deleteWallet( _i11.WalletInfo? info, _i7.SecureStorageInterface? secureStorage, - ) => - (super.noSuchMethod( - Invocation.method(#deleteWallet, [info, secureStorage]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + ) => (super.noSuchMethod( + Invocation.method(#deleteWallet, [info, secureStorage]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override _i10.Future load( _i12.Prefs? prefs, _i3.MainDB? mainDB, bool? isDuress, - ) => - (super.noSuchMethod( - Invocation.method(#load, [prefs, mainDB, isDuress]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + ) => (super.noSuchMethod( + Invocation.method(#load, [prefs, mainDB, isDuress]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override _i10.Future loadAfterStackRestore( _i12.Prefs? prefs, List<_i5.Wallet<_i4.CryptoCurrency>>? wallets, bool? isDesktop, - ) => - (super.noSuchMethod( - Invocation.method(#loadAfterStackRestore, [ - prefs, - wallets, - isDesktop, - ]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + ) => (super.noSuchMethod( + Invocation.method(#loadAfterStackRestore, [prefs, wallets, isDesktop]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); } /// A class which mocks [Prefs]. @@ -200,12 +181,10 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { as bool); @override - int get lastUnlockedTimeout => - (super.noSuchMethod( - Invocation.getter(#lastUnlockedTimeout), - returnValue: 0, - ) - as int); + int get lastUnlockedTimeout => (super.noSuchMethod( + Invocation.getter(#lastUnlockedTimeout), + returnValue: 0, + ) as int); @override int get lastUnlocked => @@ -213,28 +192,22 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { as int); @override - int get currentNotificationId => - (super.noSuchMethod( - Invocation.getter(#currentNotificationId), - returnValue: 0, - ) - as int); + int get currentNotificationId => (super.noSuchMethod( + Invocation.getter(#currentNotificationId), + returnValue: 0, + ) as int); @override - List get walletIdsSyncOnStartup => - (super.noSuchMethod( - Invocation.getter(#walletIdsSyncOnStartup), - returnValue: [], - ) - as List); + List get walletIdsSyncOnStartup => (super.noSuchMethod( + Invocation.getter(#walletIdsSyncOnStartup), + returnValue: [], + ) as List); @override - _i13.SyncingType get syncType => - (super.noSuchMethod( - Invocation.getter(#syncType), - returnValue: _i13.SyncingType.currentWalletOnly, - ) - as _i13.SyncingType); + _i13.SyncingType get syncType => (super.noSuchMethod( + Invocation.getter(#syncType), + returnValue: _i13.SyncingType.currentWalletOnly, + ) as _i13.SyncingType); @override bool get wifiOnly => @@ -242,34 +215,22 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { as bool); @override - bool get showFavoriteWallets => - (super.noSuchMethod( - Invocation.getter(#showFavoriteWallets), - returnValue: false, - ) - as bool); + bool get showFavoriteWallets => (super.noSuchMethod( + Invocation.getter(#showFavoriteWallets), + returnValue: false, + ) as bool); @override - String get language => - (super.noSuchMethod( - Invocation.getter(#language), - returnValue: _i14.dummyValue( - this, - Invocation.getter(#language), - ), - ) - as String); + String get language => (super.noSuchMethod( + Invocation.getter(#language), + returnValue: _i14.dummyValue(this, Invocation.getter(#language)), + ) as String); @override - String get currency => - (super.noSuchMethod( - Invocation.getter(#currency), - returnValue: _i14.dummyValue( - this, - Invocation.getter(#currency), - ), - ) - as String); + String get currency => (super.noSuchMethod( + Invocation.getter(#currency), + returnValue: _i14.dummyValue(this, Invocation.getter(#currency)), + ) as String); @override bool get randomizePIN => @@ -292,12 +253,10 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { as bool); @override - bool get biometricsDuress => - (super.noSuchMethod( - Invocation.getter(#biometricsDuress), - returnValue: false, - ) - as bool); + bool get biometricsDuress => (super.noSuchMethod( + Invocation.getter(#biometricsDuress), + returnValue: false, + ) as bool); @override int get familiarity => @@ -310,44 +269,34 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { as bool); @override - bool get showTestNetCoins => - (super.noSuchMethod( - Invocation.getter(#showTestNetCoins), - returnValue: false, - ) - as bool); + bool get showTestNetCoins => (super.noSuchMethod( + Invocation.getter(#showTestNetCoins), + returnValue: false, + ) as bool); @override - bool get isAutoBackupEnabled => - (super.noSuchMethod( - Invocation.getter(#isAutoBackupEnabled), - returnValue: false, - ) - as bool); + bool get isAutoBackupEnabled => (super.noSuchMethod( + Invocation.getter(#isAutoBackupEnabled), + returnValue: false, + ) as bool); @override - _i15.BackupFrequencyType get backupFrequencyType => - (super.noSuchMethod( - Invocation.getter(#backupFrequencyType), - returnValue: _i15.BackupFrequencyType.everyTenMinutes, - ) - as _i15.BackupFrequencyType); + _i15.BackupFrequencyType get backupFrequencyType => (super.noSuchMethod( + Invocation.getter(#backupFrequencyType), + returnValue: _i15.BackupFrequencyType.everyTenMinutes, + ) as _i15.BackupFrequencyType); @override - bool get hideBlockExplorerWarning => - (super.noSuchMethod( - Invocation.getter(#hideBlockExplorerWarning), - returnValue: false, - ) - as bool); + bool get hideBlockExplorerWarning => (super.noSuchMethod( + Invocation.getter(#hideBlockExplorerWarning), + returnValue: false, + ) as bool); @override - bool get gotoWalletOnStartup => - (super.noSuchMethod( - Invocation.getter(#gotoWalletOnStartup), - returnValue: false, - ) - as bool); + bool get gotoWalletOnStartup => (super.noSuchMethod( + Invocation.getter(#gotoWalletOnStartup), + returnValue: false, + ) as bool); @override bool get externalCalls => @@ -355,53 +304,40 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { as bool); @override - bool get enableCoinControl => - (super.noSuchMethod( - Invocation.getter(#enableCoinControl), - returnValue: false, - ) - as bool); + bool get enableCoinControl => (super.noSuchMethod( + Invocation.getter(#enableCoinControl), + returnValue: false, + ) as bool); @override - bool get enableSystemBrightness => - (super.noSuchMethod( - Invocation.getter(#enableSystemBrightness), - returnValue: false, - ) - as bool); + bool get enableSystemBrightness => (super.noSuchMethod( + Invocation.getter(#enableSystemBrightness), + returnValue: false, + ) as bool); @override - String get themeId => - (super.noSuchMethod( - Invocation.getter(#themeId), - returnValue: _i14.dummyValue( - this, - Invocation.getter(#themeId), - ), - ) - as String); + String get themeId => (super.noSuchMethod( + Invocation.getter(#themeId), + returnValue: _i14.dummyValue(this, Invocation.getter(#themeId)), + ) as String); @override - String get systemBrightnessLightThemeId => - (super.noSuchMethod( - Invocation.getter(#systemBrightnessLightThemeId), - returnValue: _i14.dummyValue( - this, - Invocation.getter(#systemBrightnessLightThemeId), - ), - ) - as String); + String get systemBrightnessLightThemeId => (super.noSuchMethod( + Invocation.getter(#systemBrightnessLightThemeId), + returnValue: _i14.dummyValue( + this, + Invocation.getter(#systemBrightnessLightThemeId), + ), + ) as String); @override - String get systemBrightnessDarkThemeId => - (super.noSuchMethod( - Invocation.getter(#systemBrightnessDarkThemeId), - returnValue: _i14.dummyValue( - this, - Invocation.getter(#systemBrightnessDarkThemeId), - ), - ) - as String); + String get systemBrightnessDarkThemeId => (super.noSuchMethod( + Invocation.getter(#systemBrightnessDarkThemeId), + returnValue: _i14.dummyValue( + this, + Invocation.getter(#systemBrightnessDarkThemeId), + ), + ) as String); @override bool get useTor => @@ -414,36 +350,28 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { as bool); @override - bool get enableExchange => - (super.noSuchMethod( - Invocation.getter(#enableExchange), - returnValue: false, - ) - as bool); + bool get enableExchange => (super.noSuchMethod( + Invocation.getter(#enableExchange), + returnValue: false, + ) as bool); @override - bool get advancedFiroFeatures => - (super.noSuchMethod( - Invocation.getter(#advancedFiroFeatures), - returnValue: false, - ) - as bool); + bool get advancedFiroFeatures => (super.noSuchMethod( + Invocation.getter(#advancedFiroFeatures), + returnValue: false, + ) as bool); @override - _i16.Level get logLevel => - (super.noSuchMethod( - Invocation.getter(#logLevel), - returnValue: _i16.Level.all, - ) - as _i16.Level); + _i16.Level get logLevel => (super.noSuchMethod( + Invocation.getter(#logLevel), + returnValue: _i16.Level.all, + ) as _i16.Level); @override - ({bool enabled, int minutes}) get autoLockInfo => - (super.noSuchMethod( - Invocation.getter(#autoLockInfo), - returnValue: (enabled: false, minutes: 0), - ) - as ({bool enabled, int minutes})); + ({bool enabled, int minutes}) get autoLockInfo => (super.noSuchMethod( + Invocation.getter(#autoLockInfo), + returnValue: (enabled: false, minutes: 0), + ) as ({bool enabled, int minutes})); @override bool get privacyScreen => @@ -451,12 +379,10 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { as bool); @override - bool get disableScreenShots => - (super.noSuchMethod( - Invocation.getter(#disableScreenShots), - returnValue: false, - ) - as bool); + bool get disableScreenShots => (super.noSuchMethod( + Invocation.getter(#disableScreenShots), + returnValue: false, + ) as bool); @override set lastUnlockedTimeout(int? lastUnlockedTimeout) => super.noSuchMethod( @@ -705,55 +631,43 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { as bool); @override - _i10.Future init() => - (super.noSuchMethod( - Invocation.method(#init, []), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + _i10.Future init() => (super.noSuchMethod( + Invocation.method(#init, []), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i10.Future incrementCurrentNotificationIndex() => - (super.noSuchMethod( - Invocation.method(#incrementCurrentNotificationIndex, []), - returnValue: _i10.Future.value(0), - ) - as _i10.Future); + _i10.Future incrementCurrentNotificationIndex() => (super.noSuchMethod( + Invocation.method(#incrementCurrentNotificationIndex, []), + returnValue: _i10.Future.value(0), + ) as _i10.Future); @override - _i10.Future isExternalCallsSet() => - (super.noSuchMethod( - Invocation.method(#isExternalCallsSet, []), - returnValue: _i10.Future.value(false), - ) - as _i10.Future); + _i10.Future isExternalCallsSet() => (super.noSuchMethod( + Invocation.method(#isExternalCallsSet, []), + returnValue: _i10.Future.value(false), + ) as _i10.Future); @override - _i10.Future saveUserID(String? userId) => - (super.noSuchMethod( - Invocation.method(#saveUserID, [userId]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + _i10.Future saveUserID(String? userId) => (super.noSuchMethod( + Invocation.method(#saveUserID, [userId]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i10.Future saveSignupEpoch(int? signupEpoch) => - (super.noSuchMethod( - Invocation.method(#saveSignupEpoch, [signupEpoch]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + _i10.Future saveSignupEpoch(int? signupEpoch) => (super.noSuchMethod( + Invocation.method(#saveSignupEpoch, [signupEpoch]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i17.AmountUnit amountUnit(_i4.CryptoCurrency? coin) => - (super.noSuchMethod( - Invocation.method(#amountUnit, [coin]), - returnValue: _i17.AmountUnit.normal, - ) - as _i17.AmountUnit); + _i17.AmountUnit amountUnit(_i4.CryptoCurrency? coin) => (super.noSuchMethod( + Invocation.method(#amountUnit, [coin]), + returnValue: _i17.AmountUnit.normal, + ) as _i17.AmountUnit); @override void updateAmountUnit({ @@ -768,12 +682,10 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { ); @override - int maxDecimals(_i4.CryptoCurrency? coin) => - (super.noSuchMethod( - Invocation.method(#maxDecimals, [coin]), - returnValue: 0, - ) - as int); + int maxDecimals(_i4.CryptoCurrency? coin) => (super.noSuchMethod( + Invocation.method(#maxDecimals, [coin]), + returnValue: 0, + ) as int); @override void updateMaxDecimals({ @@ -790,13 +702,12 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { @override _i6.FusionInfo getFusionServerInfo(_i4.CryptoCurrency? coin) => (super.noSuchMethod( - Invocation.method(#getFusionServerInfo, [coin]), - returnValue: _FakeFusionInfo_3( - this, - Invocation.method(#getFusionServerInfo, [coin]), - ), - ) - as _i6.FusionInfo); + Invocation.method(#getFusionServerInfo, [coin]), + returnValue: _FakeFusionInfo_3( + this, + Invocation.method(#getFusionServerInfo, [coin]), + ), + ) as _i6.FusionInfo); @override void setFusionServerInfo( @@ -841,31 +752,25 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { } @override - _i7.SecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( - Invocation.getter(#secureStorageInterface), - returnValue: _FakeSecureStorageInterface_4( - this, - Invocation.getter(#secureStorageInterface), - ), - ) - as _i7.SecureStorageInterface); + _i7.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( + Invocation.getter(#secureStorageInterface), + returnValue: _FakeSecureStorageInterface_4( + this, + Invocation.getter(#secureStorageInterface), + ), + ) as _i7.SecureStorageInterface); @override - List<_i19.NodeModel> get primaryNodes => - (super.noSuchMethod( - Invocation.getter(#primaryNodes), - returnValue: <_i19.NodeModel>[], - ) - as List<_i19.NodeModel>); + List<_i19.NodeModel> get primaryNodes => (super.noSuchMethod( + Invocation.getter(#primaryNodes), + returnValue: <_i19.NodeModel>[], + ) as List<_i19.NodeModel>); @override - List<_i19.NodeModel> get nodes => - (super.noSuchMethod( - Invocation.getter(#nodes), - returnValue: <_i19.NodeModel>[], - ) - as List<_i19.NodeModel>); + List<_i19.NodeModel> get nodes => (super.noSuchMethod( + Invocation.getter(#nodes), + returnValue: <_i19.NodeModel>[], + ) as List<_i19.NodeModel>); @override bool get hasListeners => @@ -873,45 +778,39 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { as bool); @override - _i10.Future updateDefaults() => - (super.noSuchMethod( - Invocation.method(#updateDefaults, []), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + _i10.Future updateDefaults() => (super.noSuchMethod( + Invocation.method(#updateDefaults, []), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override _i10.Future setPrimaryNodeFor({ required _i4.CryptoCurrency? coin, required _i19.NodeModel? node, bool? shouldNotifyListeners = false, - }) => - (super.noSuchMethod( - Invocation.method(#setPrimaryNodeFor, [], { - #coin: coin, - #node: node, - #shouldNotifyListeners: shouldNotifyListeners, - }), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + }) => (super.noSuchMethod( + Invocation.method(#setPrimaryNodeFor, [], { + #coin: coin, + #node: node, + #shouldNotifyListeners: shouldNotifyListeners, + }), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override _i19.NodeModel? getPrimaryNodeFor({required _i4.CryptoCurrency? currency}) => (super.noSuchMethod( - Invocation.method(#getPrimaryNodeFor, [], {#currency: currency}), - ) - as _i19.NodeModel?); + Invocation.method(#getPrimaryNodeFor, [], {#currency: currency}), + ) as _i19.NodeModel?); @override List<_i19.NodeModel> getNodesFor(_i4.CryptoCurrency? coin) => (super.noSuchMethod( - Invocation.method(#getNodesFor, [coin]), - returnValue: <_i19.NodeModel>[], - ) - as List<_i19.NodeModel>); + Invocation.method(#getNodesFor, [coin]), + returnValue: <_i19.NodeModel>[], + ) as List<_i19.NodeModel>); @override _i19.NodeModel? getNodeById({required String? id}) => @@ -921,118 +820,152 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { @override List<_i19.NodeModel> failoverNodesFor({ required _i4.CryptoCurrency? currency, - }) => - (super.noSuchMethod( - Invocation.method(#failoverNodesFor, [], {#currency: currency}), - returnValue: <_i19.NodeModel>[], - ) - as List<_i19.NodeModel>); + }) => (super.noSuchMethod( + Invocation.method(#failoverNodesFor, [], {#currency: currency}), + returnValue: <_i19.NodeModel>[], + ) as List<_i19.NodeModel>); @override _i10.Future save( _i19.NodeModel? node, String? password, bool? shouldNotifyListeners, - ) => + ) => (super.noSuchMethod( + Invocation.method(#save, [node, password, shouldNotifyListeners]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); + + @override + _i10.Future saveEdit( + _i19.NodeModel? node, + String? password, + bool? notify, { + required _i19.NodeModel? opened, + bool? forgetCertificate = false, + }) => (super.noSuchMethod( + Invocation.method( + #saveEdit, + [node, password, notify], + {#opened: opened, #forgetCertificate: forgetCertificate}, + ), + returnValue: _i10.Future.value(false), + ) as _i10.Future); + + @override + bool prepareCertificateTest(_i20.NodeFormData? data) => (super.noSuchMethod( + Invocation.method(#prepareCertificateTest, [data]), + returnValue: false, + ) as bool); + + @override + _i10.Future saveTrustedCertificate( + _i19.NodeModel? node, + _i20.NodeFormData? data, + ) => (super.noSuchMethod( + Invocation.method(#saveTrustedCertificate, [node, data]), + returnValue: _i10.Future.value(false), + ) as _i10.Future); + + @override + _i10.Future forgetTrustedCertificate(_i19.NodeModel? shown) => (super.noSuchMethod( - Invocation.method(#save, [node, password, shouldNotifyListeners]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + Invocation.method(#forgetTrustedCertificate, [shown]), + returnValue: _i10.Future.value(false), + ) as _i10.Future); + + @override + _i10.Future commitConnectionTest({ + required _i19.NodeModel? node, + required _i20.NodeFormData? formData, + }) => (super.noSuchMethod( + Invocation.method(#commitConnectionTest, [], { + #node: node, + #formData: formData, + }), + returnValue: _i10.Future.value(false), + ) as _i10.Future); + + @override + _i10.Future setFailover(String? id, bool? value) => (super.noSuchMethod( + Invocation.method(#setFailover, [id, value]), + returnValue: _i10.Future.value(false), + ) as _i10.Future); @override _i10.Future delete(String? id, bool? shouldNotifyListeners) => (super.noSuchMethod( - Invocation.method(#delete, [id, shouldNotifyListeners]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + Invocation.method(#delete, [id, shouldNotifyListeners]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override _i10.Future setEnabledState( String? id, bool? enabled, bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method(#setEnabledState, [ - id, - enabled, - shouldNotifyListeners, - ]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); - - @override - _i10.Future updateDefaultEpicBoxes() => - (super.noSuchMethod( - Invocation.method(#updateDefaultEpicBoxes, []), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + ) => (super.noSuchMethod( + Invocation.method(#setEnabledState, [id, enabled, shouldNotifyListeners]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); + + @override + _i10.Future updateDefaultEpicBoxes() => (super.noSuchMethod( + Invocation.method(#updateDefaultEpicBoxes, []), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override _i10.Future setPrimaryEpicBox({ - required _i20.EpicBoxServerModel? epicBox, + required _i21.EpicBoxServerModel? epicBox, bool? shouldNotifyListeners = false, - }) => - (super.noSuchMethod( - Invocation.method(#setPrimaryEpicBox, [], { - #epicBox: epicBox, - #shouldNotifyListeners: shouldNotifyListeners, - }), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + }) => (super.noSuchMethod( + Invocation.method(#setPrimaryEpicBox, [], { + #epicBox: epicBox, + #shouldNotifyListeners: shouldNotifyListeners, + }), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - List<_i20.EpicBoxServerModel> getEpicBoxes() => - (super.noSuchMethod( - Invocation.method(#getEpicBoxes, []), - returnValue: <_i20.EpicBoxServerModel>[], - ) - as List<_i20.EpicBoxServerModel>); + List<_i21.EpicBoxServerModel> getEpicBoxes() => (super.noSuchMethod( + Invocation.method(#getEpicBoxes, []), + returnValue: <_i21.EpicBoxServerModel>[], + ) as List<_i21.EpicBoxServerModel>); @override - _i20.EpicBoxServerModel? getEpicBoxById({required String? id}) => + _i21.EpicBoxServerModel? getEpicBoxById({required String? id}) => (super.noSuchMethod(Invocation.method(#getEpicBoxById, [], {#id: id})) - as _i20.EpicBoxServerModel?); + as _i21.EpicBoxServerModel?); @override _i10.Future addEpicBox( - _i20.EpicBoxServerModel? epicBox, + _i21.EpicBoxServerModel? epicBox, bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method(#addEpicBox, [epicBox, shouldNotifyListeners]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + ) => (super.noSuchMethod( + Invocation.method(#addEpicBox, [epicBox, shouldNotifyListeners]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override _i10.Future deleteEpicBox(String? id, bool? shouldNotifyListeners) => (super.noSuchMethod( - Invocation.method(#deleteEpicBox, [id, shouldNotifyListeners]), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + Invocation.method(#deleteEpicBox, [id, shouldNotifyListeners]), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i10.Future updateCommunityNodes() => - (super.noSuchMethod( - Invocation.method(#updateCommunityNodes, []), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + _i10.Future updateCommunityNodes() => (super.noSuchMethod( + Invocation.method(#updateCommunityNodes, []), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override void addListener(_i18.VoidCallback? listener) => super.noSuchMethod( @@ -1062,32 +995,25 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { /// A class which mocks [TorService]. /// /// See the documentation for Mockito's code generation for more information. -class MockTorService extends _i1.Mock implements _i21.TorService { +class MockTorService extends _i1.Mock implements _i22.TorService { MockTorService() { _i1.throwOnMissingStub(this); } @override - _i22.TorConnectionStatus get status => - (super.noSuchMethod( - Invocation.getter(#status), - returnValue: _i22.TorConnectionStatus.disconnected, - ) - as _i22.TorConnectionStatus); + _i23.TorConnectionStatus get status => (super.noSuchMethod( + Invocation.getter(#status), + returnValue: _i23.TorConnectionStatus.disconnected, + ) as _i23.TorConnectionStatus); @override - ({_i8.InternetAddress host, int port}) getProxyInfo() => - (super.noSuchMethod( - Invocation.method(#getProxyInfo, []), - returnValue: ( - host: _FakeInternetAddress_5( - this, - Invocation.method(#getProxyInfo, []), - ), - port: 0, - ), - ) - as ({_i8.InternetAddress host, int port})); + ({_i8.InternetAddress host, int port}) getProxyInfo() => (super.noSuchMethod( + Invocation.method(#getProxyInfo, []), + returnValue: ( + host: _FakeInternetAddress_5(this, Invocation.method(#getProxyInfo, [])), + port: 0, + ), + ) as ({_i8.InternetAddress host, int port})); @override void init({required String? torDataDirPath}) => super.noSuchMethod( @@ -1096,20 +1022,16 @@ class MockTorService extends _i1.Mock implements _i21.TorService { ); @override - _i10.Future start() => - (super.noSuchMethod( - Invocation.method(#start, []), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + _i10.Future start() => (super.noSuchMethod( + Invocation.method(#start, []), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i10.Future disable() => - (super.noSuchMethod( - Invocation.method(#disable, []), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) - as _i10.Future); + _i10.Future disable() => (super.noSuchMethod( + Invocation.method(#disable, []), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); } diff --git a/test/widget_tests/node_tls_trust_test.dart b/test/widget_tests/node_tls_trust_test.dart new file mode 100644 index 0000000000..abf4b618ae --- /dev/null +++ b/test/widget_tests/node_tls_trust_test.dart @@ -0,0 +1,386 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:logger/logger.dart' show Level; +import 'package:mockito/mockito.dart'; +import 'package:platform/platform.dart'; +import 'package:stackwallet/db/hive/db.dart'; +import 'package:stackwallet/electrumx_rpc/client_manager.dart'; +import 'package:stackwallet/models/isar/stack_theme.dart'; +import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; +import 'package:stackwallet/services/node_service.dart'; +import 'package:stackwallet/services/wallets.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/enums/sync_type_enum.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/prefs.dart'; +import 'package:stackwallet/utilities/test_node_connection.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/wallet/wallet.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/node_card.dart'; +import 'package:stackwallet/widgets/node_options_sheet.dart'; + +import '../sample_data/theme_json.dart'; +import '../utilities/tls_test_server.dart'; + +class _Wallets extends Fake implements Wallets { + @override + List get wallets => []; +} + +class _Prefs extends Mock implements Prefs { + @override + bool get useTor => false; + @override + SyncingType get syncType => SyncingType.allWalletsOnStartup; +} + +void main() { + late CryptoCurrency coin; + late NodeService nodes; + late FakeSecureStorage storage; + late String pem; + late Directory logs; + String? testedHost, testedCertificate; + Completer? testGate; + int tests = 0; + NodeModel node() => nodes.getNodeById(id: 'custom-node')!; + NodeModel makeNode(String id, {bool primary = false}) => NodeModel( + host: id == 'custom-node' ? 'node.example' : 'other.example', + port: 50002, + name: id == 'custom-node' ? 'Node' : 'Other', + id: id, + useSSL: true, + enabled: true, + coinName: coin.identifier, + isFailover: false, + isDown: false, + torEnabled: true, + clearnetEnabled: true, + isPrimary: primary, + ); + + setUpAll(() async { + logs = await Directory.systemTemp.createTemp('minimal_tls_widgets'); + await Logging.instance.initialize(logs.path, level: Level.off); + }); + tearDownAll(() => logs.delete(recursive: true)); + setUp(() async { + coin = Bitcoin(CryptoCurrencyNetwork.main); + DB.instance.hive.init(logs.path); + if (!DB.instance.hive.isAdapterRegistered(12)) { + DB.instance.hive.registerAdapter(NodeModelAdapter()); + } + final box = await DB.instance.hive.openBox( + DB.boxNameNodeModels, + bytes: Uint8List(0), + ); + await box.put('custom-node', makeNode('custom-node')); + await box.put('other-node', makeNode('other-node', primary: true)); + storage = FakeSecureStorage(); + nodes = NodeService(secureStorageInterface: storage); + pem = TlsElectrumServer.certificate('node_a'); + testedHost = testedCertificate = null; + testGate = null; + tests = 0; + Util.layoutPlatform = FakePlatform(operatingSystem: 'macos'); + }); + tearDown(() async { + await ClientManager.sharedInstance.closeAll(); + await DB.instance.hive.close(); + Util.layoutPlatform = const LocalPlatform(); + }); + + Future pump(WidgetTester tester, Widget child) async { + tester.view.physicalSize = const Size(1200, 1800); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + await tester.pumpWidget( + ProviderScope( + overrides: [ + nodeServiceChangeNotifierProvider.overrideWithValue(nodes), + secureStoreProvider.overrideWithValue(storage), + prefsChangeNotifierProvider.overrideWithValue(_Prefs()), + pWallets.overrideWithValue(_Wallets()), + testNodeConnectionProvider.overrideWithValue(({ + required context, + required nodeFormData, + required cryptoCurrency, + onSuccess, + }) async { + if (!nodes.prepareCertificateTest(nodeFormData)) return false; + testedHost = nodeFormData.host; + testedCertificate = nodeFormData.trustedCertificatePem; + tests++; + await testGate?.future; + nodeFormData.trustedCertificatePem = pem; + onSuccess?.call(nodeFormData); + return true; + }), + ], + child: MaterialApp( + theme: ThemeData( + extensions: [ + StackColors.fromStackColorTheme( + StackTheme.fromJson(json: lightThemeJsonMap), + ), + ], + ), + builder: (context, child) => MediaQuery( + data: MediaQuery.of(context) + .copyWith(textScaler: const TextScaler.linear(.8)), + child: child!, + ), + home: child, + ), + ), + ); + await tester.pumpAndSettle(); + } + + Widget details() => + NodeDetailsView(coin: coin, nodeId: 'custom-node', popRouteName: '/'); + Future approveStored() async { + final opened = node(); + await nodes.saveTrustedCertificate( + opened, + NodeFormData.fromNode(opened)..trustedCertificatePem = pem, + ); + } + + testWidgets( + 'Details Test saves approval without rewriting the node password', + (tester) async { + await storage.write(key: 'custom-node_nodePW', value: 'secret'); + await pump(tester, details()); + final writes = storage.writes; + final deletes = storage.deletes; + await tester.tap(find.text('Test connection')); + await tester.pumpAndSettle(); + expect(node().trustedCertificatePem, pem); + expect(testedHost, 'node.example'); + expect(storage.writes, writes); + expect(storage.deletes, deletes); + expect(find.textContaining('Trusted TLS certificate'), findsOneWidget); + }, + ); + + testWidgets('desktop Test stages an edited endpoint approval until Save', ( + tester, + ) async { + await pump(tester, details()); + await tester.tap(find.text('Edit')); + await tester.pumpAndSettle(); + await tester.enterText(find.byType(TextField).at(1), 'edited.example'); + await tester.tap(find.text('Test connection')); + await tester.pumpAndSettle(); + expect(testedHost, 'edited.example'); + expect(node().host, 'node.example'); + expect(node().trustedCertificatePem, null); + await tester.tap(find.text('Save')); + await tester.pumpAndSettle(); + expect(node().host, 'edited.example'); + expect(node().trustedCertificatePem, pem); + }); + + testWidgets( + 'desktop Save uses its own form after another view refreshes the provider', + (tester) async { + await pump(tester, details()); + await tester.tap(find.text('Edit')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Test connection')); + await tester.pumpAndSettle(); + final container = ProviderScope.containerOf( + tester.element(find.byType(NodeDetailsView)), + ); + container.refresh(nodeFormDataProvider) + ..host = 'different.example' + ..port = 60002 + ..useSSL = true + ..trustedCertificatePem = TlsElectrumServer.certificate('node_b'); + await tester.tap(find.text('Save')); + await tester.pumpAndSettle(); + expect(node().host, 'node.example'); + expect(node().trustedCertificatePem, pem); + }, + ); + + testWidgets('repeated desktop edits keep the saved RPC password', ( + tester, + ) async { + coin = Ethereum(CryptoCurrencyNetwork.main); + await nodes.save(makeNode('custom-node'), 'secret', false); + await pump(tester, details()); + Finder field(String text) => find.byWidgetPredicate( + (widget) => widget is TextField && widget.controller?.text == text, + ); + await tester.tap(find.text('Edit')); + await tester.pumpAndSettle(); + await tester.enterText(field('secret'), 'changed-secret'); + await tester.tap(find.text('Save')); + await tester.pumpAndSettle(); + expect(await storage.read(key: 'custom-node_nodePW'), 'changed-secret'); + await tester.tap(find.text('Edit')); + await tester.pumpAndSettle(); + expect(field('changed-secret'), findsOneWidget); + await tester.enterText(field('Node'), 'Renamed'); + await tester.tap(find.text('Save')); + await tester.pumpAndSettle(); + expect(node().name, 'Renamed'); + expect(await storage.read(key: 'custom-node_nodePW'), 'changed-secret'); + }); + + testWidgets('options Connect commits approval and primary together', ( + tester, + ) async { + await pump( + tester, + NodeOptionsSheet(nodeId: 'custom-node', coin: coin, popBackToRoute: '/'), + ); + await tester.tap(find.text('Connect')); + await tester.pumpAndSettle(); + expect(testedCertificate, null); + expect(node().trustedCertificatePem, pem); + expect(node().isPrimary, true); + expect(nodes.getNodeById(id: 'other-node')!.isPrimary, false); + }); + + testWidgets('node card Connect carries an already approved certificate', ( + tester, + ) async { + await approveStored(); + await pump( + tester, + NodeCard(nodeId: 'custom-node', coin: coin, popBackToRoute: '/'), + ); + await tester.tap(find.text('Node')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Connect', findRichText: true)); + await tester.pumpAndSettle(); + expect(testedCertificate, pem); + expect(node().trustedCertificatePem, pem); + expect(node().isPrimary, true); + }); + + testWidgets('add/edit Save includes the tested certificate', (tester) async { + await pump( + tester, + AddEditNodeView( + viewType: AddEditNodeViewType.edit, + coin: coin, + nodeId: 'custom-node', + routeOnSuccessOrDelete: '/', + ), + ); + await tester.tap(find.text('Save')); + await tester.pumpAndSettle(); + expect(node().trustedCertificatePem, pem); + }); + + testWidgets( + 'Forget in read-only Details is an immediate certificate-only write', + (tester) async { + await approveStored(); + await storage.write(key: 'custom-node_nodePW', value: 'secret'); + await pump(tester, details()); + final writes = storage.writes; + final deletes = storage.deletes; + await tester.tap(find.text('Forget certificate')); + await tester.pumpAndSettle(); + expect(node().trustedCertificatePem, null); + expect(node().host, 'node.example'); + expect(storage.writes, writes); + expect(storage.deletes, deletes); + }, + ); + + testWidgets('an editing Forget is staged until Save', (tester) async { + await approveStored(); + await pump(tester, details()); + await tester.tap(find.text('Edit')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Forget certificate')); + await tester.pumpAndSettle(); + expect(node().trustedCertificatePem, pem); + await tester.tap(find.text('Save')); + await tester.pumpAndSettle(); + expect(node().trustedCertificatePem, null); + }); + + testWidgets('desktop Save cannot overtake a running Test', (tester) async { + await pump(tester, details()); + await tester.tap(find.text('Edit')); + await tester.pumpAndSettle(); + testGate = Completer(); + final save = tester + .widget(find.widgetWithText(PrimaryButton, 'Save')) + .onPressed!; + await tester.tap(find.text('Test connection')); + await tester.pump(); + save(); + await tester.pump(); + expect(tests, 1); + expect(node().trustedCertificatePem, null); + expect( + tester + .widget(find.widgetWithText(PrimaryButton, 'Save')) + .onPressed, + null, + ); + testGate!.complete(); + await tester.pumpAndSettle(); + await tester.tap(find.text('Save')); + await tester.pumpAndSettle(); + expect(node().trustedCertificatePem, pem); + }); + + testWidgets('a readonly test cannot undo an intervening empty Forget', ( + tester, + ) async { + await pump(tester, details()); + testGate = Completer(); + await tester.tap(find.text('Test connection')); + await tester.pump(); + await nodes.forgetTrustedCertificate(node()); + testGate!.complete(); + await tester.pumpAndSettle(); + expect(node().trustedCertificatePem, null); + }); + + testWidgets('Connect ignores a node changed during its test', (tester) async { + await pump( + tester, + NodeOptionsSheet(nodeId: 'custom-node', coin: coin, popBackToRoute: '/'), + ); + testGate = Completer(); + await tester.tap(find.text('Connect')); + await tester.pump(); + final before = node(); + await DB.instance.put( + boxName: DB.boxNameNodeModels, + key: before.id, + value: before.copyWith( + host: 'new.example', + loginName: null, + trusted: null, + ), + ); + testGate!.complete(); + await tester.pumpAndSettle(); + expect(node().isPrimary, false); + expect(node().trustedCertificatePem, null); + }); +} diff --git a/test/widget_tests/tls_certificate_dialog_test.dart b/test/widget_tests/tls_certificate_dialog_test.dart new file mode 100644 index 0000000000..4257761809 --- /dev/null +++ b/test/widget_tests/tls_certificate_dialog_test.dart @@ -0,0 +1,132 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/models/isar/stack_theme.dart'; +import 'package:stackwallet/models/node_form_data.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/connection_check/electrum_connection_check.dart'; +import 'package:stackwallet/utilities/tls_certificate.dart'; +import 'package:stackwallet/widgets/tls_certificate_dialog.dart'; + +import '../sample_data/theme_json.dart'; +import '../utilities/tls_test_server.dart'; + +void main() { + late X509Certificate certificate; + setUpAll(() async { + final server = await TlsElectrumServer.start(); + try { + certificate = (await testElectrumConnection( + host: '127.0.0.1', + port: server.port, + useSSL: true, + timeout: const Duration(seconds: 2), + )).certificate!; + } finally { + await server.close(); + } + }); + + for (final choice in ['Cancel', 'Trust']) { + testWidgets('shows certificate details and returns $choice', ( + tester, + ) async { + bool? approved; + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + extensions: [ + StackColors.fromStackColorTheme( + StackTheme.fromJson(json: lightThemeJsonMap), + ), + ], + ), + home: Builder( + builder: (context) => Scaffold( + body: TextButton( + onPressed: () async { + approved = await showBadX509CertificateDialog( + certificate, + 'node.example', + 50002, + context, + ); + }, + child: const Text('Review'), + ), + ), + ), + ), + ); + await tester.tap(find.text('Review')); + await tester.pumpAndSettle(); + final message = tester + .widgetList(find.byType(SelectableText)) + .map((text) => text.data) + .join('\n'); + expect(message, contains('node.example:50002')); + expect(message, contains(certificate.subject)); + expect(message, contains(certificate.issuer)); + expect(message, contains('${certificate.endValidity.toUtc()}')); + expect(message, contains(certificateFingerprint(certificate.der))); + await tester.tap(find.text(choice)); + await tester.pumpAndSettle(); + expect(approved, choice == 'Trust'); + }); + } + testWidgets('Trust resumes the test on its current route', (tester) async { + final data = NodeFormData() + ..host = 'node.example' + ..port = 50002 + ..useSSL = true; + bool? success; + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + extensions: [ + StackColors.fromStackColorTheme( + StackTheme.fromJson(json: lightThemeJsonMap), + ), + ], + ), + home: Builder( + builder: (context) => Scaffold( + body: TextButton( + child: const Text('Test'), + onPressed: () async { + success = await testElectrumNodeConnection( + data, + isActive: () => + context.mounted && + ModalRoute.of(context)?.isCurrent != false, + requestTrust: (cert, host, port) => + showBadX509CertificateDialog(cert, host, port, context), + testConnection: + ({ + required host, + required port, + required useSSL, + trustedCertificatePem, + }) async => trustedCertificatePem == null + ? ElectrumConnectionResult( + success: false, + certificate: certificate, + ) + : const ElectrumConnectionResult(success: true), + ); + }, + ), + ), + ), + ), + ); + await tester.tap(find.text('Test')); + await tester.pumpAndSettle(); + expect(success, null); + await tester.tap(find.text('Trust')); + await tester.pumpAndSettle(); + expect(success, true); + expect(data.trustedCertificatePem, certificate.pem); + }); +} diff --git a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart index ecc2874ec6..e1399a1bcc 100644 --- a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart @@ -4,11 +4,12 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i8; -import 'dart:ui' as _i13; +import 'dart:ui' as _i14; import 'package:mockito/mockito.dart' as _i1; import 'package:stackwallet/db/isar/main_db.dart' as _i3; -import 'package:stackwallet/models/epicbox_server_model.dart' as _i12; +import 'package:stackwallet/models/epicbox_server_model.dart' as _i13; +import 'package:stackwallet/models/node_form_data.dart' as _i12; import 'package:stackwallet/models/node_model.dart' as _i11; import 'package:stackwallet/services/node_service.dart' as _i2; import 'package:stackwallet/services/wallets.dart' as _i7; @@ -66,31 +67,22 @@ class MockWallets extends _i1.Mock implements _i7.Wallets { } @override - _i2.NodeService get nodeService => - (super.noSuchMethod( - Invocation.getter(#nodeService), - returnValue: _FakeNodeService_0( - this, - Invocation.getter(#nodeService), - ), - ) - as _i2.NodeService); + _i2.NodeService get nodeService => (super.noSuchMethod( + Invocation.getter(#nodeService), + returnValue: _FakeNodeService_0(this, Invocation.getter(#nodeService)), + ) as _i2.NodeService); @override - _i3.MainDB get mainDB => - (super.noSuchMethod( - Invocation.getter(#mainDB), - returnValue: _FakeMainDB_1(this, Invocation.getter(#mainDB)), - ) - as _i3.MainDB); + _i3.MainDB get mainDB => (super.noSuchMethod( + Invocation.getter(#mainDB), + returnValue: _FakeMainDB_1(this, Invocation.getter(#mainDB)), + ) as _i3.MainDB); @override - List<_i5.Wallet<_i4.CryptoCurrency>> get wallets => - (super.noSuchMethod( - Invocation.getter(#wallets), - returnValue: <_i5.Wallet<_i4.CryptoCurrency>>[], - ) - as List<_i5.Wallet<_i4.CryptoCurrency>>); + List<_i5.Wallet<_i4.CryptoCurrency>> get wallets => (super.noSuchMethod( + Invocation.getter(#wallets), + returnValue: <_i5.Wallet<_i4.CryptoCurrency>>[], + ) as List<_i5.Wallet<_i4.CryptoCurrency>>); @override set nodeService(_i2.NodeService? value) => super.noSuchMethod( @@ -107,13 +99,12 @@ class MockWallets extends _i1.Mock implements _i7.Wallets { @override _i5.Wallet<_i4.CryptoCurrency> getWallet(String? walletId) => (super.noSuchMethod( - Invocation.method(#getWallet, [walletId]), - returnValue: _FakeWallet_2<_i4.CryptoCurrency>( - this, - Invocation.method(#getWallet, [walletId]), - ), - ) - as _i5.Wallet<_i4.CryptoCurrency>); + Invocation.method(#getWallet, [walletId]), + returnValue: _FakeWallet_2<_i4.CryptoCurrency>( + this, + Invocation.method(#getWallet, [walletId]), + ), + ) as _i5.Wallet<_i4.CryptoCurrency>); @override void addWallet(_i5.Wallet<_i4.CryptoCurrency>? wallet) => super.noSuchMethod( @@ -125,43 +116,33 @@ class MockWallets extends _i1.Mock implements _i7.Wallets { _i8.Future deleteWallet( _i9.WalletInfo? info, _i6.SecureStorageInterface? secureStorage, - ) => - (super.noSuchMethod( - Invocation.method(#deleteWallet, [info, secureStorage]), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) - as _i8.Future); + ) => (super.noSuchMethod( + Invocation.method(#deleteWallet, [info, secureStorage]), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); @override _i8.Future load( _i10.Prefs? prefs, _i3.MainDB? mainDB, bool? isDuress, - ) => - (super.noSuchMethod( - Invocation.method(#load, [prefs, mainDB, isDuress]), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) - as _i8.Future); + ) => (super.noSuchMethod( + Invocation.method(#load, [prefs, mainDB, isDuress]), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); @override _i8.Future loadAfterStackRestore( _i10.Prefs? prefs, List<_i5.Wallet<_i4.CryptoCurrency>>? wallets, bool? isDesktop, - ) => - (super.noSuchMethod( - Invocation.method(#loadAfterStackRestore, [ - prefs, - wallets, - isDesktop, - ]), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) - as _i8.Future); + ) => (super.noSuchMethod( + Invocation.method(#loadAfterStackRestore, [prefs, wallets, isDesktop]), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); } /// A class which mocks [NodeService]. @@ -173,31 +154,25 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { } @override - _i6.SecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( - Invocation.getter(#secureStorageInterface), - returnValue: _FakeSecureStorageInterface_3( - this, - Invocation.getter(#secureStorageInterface), - ), - ) - as _i6.SecureStorageInterface); + _i6.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( + Invocation.getter(#secureStorageInterface), + returnValue: _FakeSecureStorageInterface_3( + this, + Invocation.getter(#secureStorageInterface), + ), + ) as _i6.SecureStorageInterface); @override - List<_i11.NodeModel> get primaryNodes => - (super.noSuchMethod( - Invocation.getter(#primaryNodes), - returnValue: <_i11.NodeModel>[], - ) - as List<_i11.NodeModel>); + List<_i11.NodeModel> get primaryNodes => (super.noSuchMethod( + Invocation.getter(#primaryNodes), + returnValue: <_i11.NodeModel>[], + ) as List<_i11.NodeModel>); @override - List<_i11.NodeModel> get nodes => - (super.noSuchMethod( - Invocation.getter(#nodes), - returnValue: <_i11.NodeModel>[], - ) - as List<_i11.NodeModel>); + List<_i11.NodeModel> get nodes => (super.noSuchMethod( + Invocation.getter(#nodes), + returnValue: <_i11.NodeModel>[], + ) as List<_i11.NodeModel>); @override bool get hasListeners => @@ -205,45 +180,39 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { as bool); @override - _i8.Future updateDefaults() => - (super.noSuchMethod( - Invocation.method(#updateDefaults, []), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) - as _i8.Future); + _i8.Future updateDefaults() => (super.noSuchMethod( + Invocation.method(#updateDefaults, []), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); @override _i8.Future setPrimaryNodeFor({ required _i4.CryptoCurrency? coin, required _i11.NodeModel? node, bool? shouldNotifyListeners = false, - }) => - (super.noSuchMethod( - Invocation.method(#setPrimaryNodeFor, [], { - #coin: coin, - #node: node, - #shouldNotifyListeners: shouldNotifyListeners, - }), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) - as _i8.Future); + }) => (super.noSuchMethod( + Invocation.method(#setPrimaryNodeFor, [], { + #coin: coin, + #node: node, + #shouldNotifyListeners: shouldNotifyListeners, + }), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); @override _i11.NodeModel? getPrimaryNodeFor({required _i4.CryptoCurrency? currency}) => (super.noSuchMethod( - Invocation.method(#getPrimaryNodeFor, [], {#currency: currency}), - ) - as _i11.NodeModel?); + Invocation.method(#getPrimaryNodeFor, [], {#currency: currency}), + ) as _i11.NodeModel?); @override List<_i11.NodeModel> getNodesFor(_i4.CryptoCurrency? coin) => (super.noSuchMethod( - Invocation.method(#getNodesFor, [coin]), - returnValue: <_i11.NodeModel>[], - ) - as List<_i11.NodeModel>); + Invocation.method(#getNodesFor, [coin]), + returnValue: <_i11.NodeModel>[], + ) as List<_i11.NodeModel>); @override _i11.NodeModel? getNodeById({required String? id}) => @@ -253,127 +222,161 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { @override List<_i11.NodeModel> failoverNodesFor({ required _i4.CryptoCurrency? currency, - }) => - (super.noSuchMethod( - Invocation.method(#failoverNodesFor, [], {#currency: currency}), - returnValue: <_i11.NodeModel>[], - ) - as List<_i11.NodeModel>); + }) => (super.noSuchMethod( + Invocation.method(#failoverNodesFor, [], {#currency: currency}), + returnValue: <_i11.NodeModel>[], + ) as List<_i11.NodeModel>); @override _i8.Future save( _i11.NodeModel? node, String? password, bool? shouldNotifyListeners, - ) => + ) => (super.noSuchMethod( + Invocation.method(#save, [node, password, shouldNotifyListeners]), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + + @override + _i8.Future saveEdit( + _i11.NodeModel? node, + String? password, + bool? notify, { + required _i11.NodeModel? opened, + bool? forgetCertificate = false, + }) => (super.noSuchMethod( + Invocation.method( + #saveEdit, + [node, password, notify], + {#opened: opened, #forgetCertificate: forgetCertificate}, + ), + returnValue: _i8.Future.value(false), + ) as _i8.Future); + + @override + bool prepareCertificateTest(_i12.NodeFormData? data) => (super.noSuchMethod( + Invocation.method(#prepareCertificateTest, [data]), + returnValue: false, + ) as bool); + + @override + _i8.Future saveTrustedCertificate( + _i11.NodeModel? node, + _i12.NodeFormData? data, + ) => (super.noSuchMethod( + Invocation.method(#saveTrustedCertificate, [node, data]), + returnValue: _i8.Future.value(false), + ) as _i8.Future); + + @override + _i8.Future forgetTrustedCertificate(_i11.NodeModel? shown) => (super.noSuchMethod( - Invocation.method(#save, [node, password, shouldNotifyListeners]), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) - as _i8.Future); + Invocation.method(#forgetTrustedCertificate, [shown]), + returnValue: _i8.Future.value(false), + ) as _i8.Future); + + @override + _i8.Future commitConnectionTest({ + required _i11.NodeModel? node, + required _i12.NodeFormData? formData, + }) => (super.noSuchMethod( + Invocation.method(#commitConnectionTest, [], { + #node: node, + #formData: formData, + }), + returnValue: _i8.Future.value(false), + ) as _i8.Future); + + @override + _i8.Future setFailover(String? id, bool? value) => (super.noSuchMethod( + Invocation.method(#setFailover, [id, value]), + returnValue: _i8.Future.value(false), + ) as _i8.Future); @override _i8.Future delete(String? id, bool? shouldNotifyListeners) => (super.noSuchMethod( - Invocation.method(#delete, [id, shouldNotifyListeners]), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) - as _i8.Future); + Invocation.method(#delete, [id, shouldNotifyListeners]), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); @override _i8.Future setEnabledState( String? id, bool? enabled, bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method(#setEnabledState, [ - id, - enabled, - shouldNotifyListeners, - ]), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) - as _i8.Future); - - @override - _i8.Future updateDefaultEpicBoxes() => - (super.noSuchMethod( - Invocation.method(#updateDefaultEpicBoxes, []), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) - as _i8.Future); + ) => (super.noSuchMethod( + Invocation.method(#setEnabledState, [id, enabled, shouldNotifyListeners]), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + + @override + _i8.Future updateDefaultEpicBoxes() => (super.noSuchMethod( + Invocation.method(#updateDefaultEpicBoxes, []), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); @override _i8.Future setPrimaryEpicBox({ - required _i12.EpicBoxServerModel? epicBox, + required _i13.EpicBoxServerModel? epicBox, bool? shouldNotifyListeners = false, - }) => - (super.noSuchMethod( - Invocation.method(#setPrimaryEpicBox, [], { - #epicBox: epicBox, - #shouldNotifyListeners: shouldNotifyListeners, - }), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) - as _i8.Future); + }) => (super.noSuchMethod( + Invocation.method(#setPrimaryEpicBox, [], { + #epicBox: epicBox, + #shouldNotifyListeners: shouldNotifyListeners, + }), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); @override - List<_i12.EpicBoxServerModel> getEpicBoxes() => - (super.noSuchMethod( - Invocation.method(#getEpicBoxes, []), - returnValue: <_i12.EpicBoxServerModel>[], - ) - as List<_i12.EpicBoxServerModel>); + List<_i13.EpicBoxServerModel> getEpicBoxes() => (super.noSuchMethod( + Invocation.method(#getEpicBoxes, []), + returnValue: <_i13.EpicBoxServerModel>[], + ) as List<_i13.EpicBoxServerModel>); @override - _i12.EpicBoxServerModel? getEpicBoxById({required String? id}) => + _i13.EpicBoxServerModel? getEpicBoxById({required String? id}) => (super.noSuchMethod(Invocation.method(#getEpicBoxById, [], {#id: id})) - as _i12.EpicBoxServerModel?); + as _i13.EpicBoxServerModel?); @override _i8.Future addEpicBox( - _i12.EpicBoxServerModel? epicBox, + _i13.EpicBoxServerModel? epicBox, bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method(#addEpicBox, [epicBox, shouldNotifyListeners]), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) - as _i8.Future); + ) => (super.noSuchMethod( + Invocation.method(#addEpicBox, [epicBox, shouldNotifyListeners]), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); @override _i8.Future deleteEpicBox(String? id, bool? shouldNotifyListeners) => (super.noSuchMethod( - Invocation.method(#deleteEpicBox, [id, shouldNotifyListeners]), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) - as _i8.Future); + Invocation.method(#deleteEpicBox, [id, shouldNotifyListeners]), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); @override - _i8.Future updateCommunityNodes() => - (super.noSuchMethod( - Invocation.method(#updateCommunityNodes, []), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) - as _i8.Future); + _i8.Future updateCommunityNodes() => (super.noSuchMethod( + Invocation.method(#updateCommunityNodes, []), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); @override - void addListener(_i13.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i14.VoidCallback? listener) => super.noSuchMethod( Invocation.method(#addListener, [listener]), returnValueForMissingStub: null, ); @override - void removeListener(_i13.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i14.VoidCallback? listener) => super.noSuchMethod( Invocation.method(#removeListener, [listener]), returnValueForMissingStub: null, ); diff --git a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart index c2139bdfd6..67e34b0e04 100644 --- a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart @@ -5,12 +5,13 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i9; import 'dart:typed_data' as _i14; -import 'dart:ui' as _i17; +import 'dart:ui' as _i18; import 'package:mockito/mockito.dart' as _i1; import 'package:stackwallet/db/isar/main_db.dart' as _i3; -import 'package:stackwallet/models/epicbox_server_model.dart' as _i16; +import 'package:stackwallet/models/epicbox_server_model.dart' as _i17; import 'package:stackwallet/models/isar/stack_theme.dart' as _i13; +import 'package:stackwallet/models/node_form_data.dart' as _i16; import 'package:stackwallet/models/node_model.dart' as _i15; import 'package:stackwallet/networking/http.dart' as _i6; import 'package:stackwallet/services/node_service.dart' as _i2; @@ -75,31 +76,22 @@ class MockWallets extends _i1.Mock implements _i8.Wallets { } @override - _i2.NodeService get nodeService => - (super.noSuchMethod( - Invocation.getter(#nodeService), - returnValue: _FakeNodeService_0( - this, - Invocation.getter(#nodeService), - ), - ) - as _i2.NodeService); + _i2.NodeService get nodeService => (super.noSuchMethod( + Invocation.getter(#nodeService), + returnValue: _FakeNodeService_0(this, Invocation.getter(#nodeService)), + ) as _i2.NodeService); @override - _i3.MainDB get mainDB => - (super.noSuchMethod( - Invocation.getter(#mainDB), - returnValue: _FakeMainDB_1(this, Invocation.getter(#mainDB)), - ) - as _i3.MainDB); + _i3.MainDB get mainDB => (super.noSuchMethod( + Invocation.getter(#mainDB), + returnValue: _FakeMainDB_1(this, Invocation.getter(#mainDB)), + ) as _i3.MainDB); @override - List<_i5.Wallet<_i4.CryptoCurrency>> get wallets => - (super.noSuchMethod( - Invocation.getter(#wallets), - returnValue: <_i5.Wallet<_i4.CryptoCurrency>>[], - ) - as List<_i5.Wallet<_i4.CryptoCurrency>>); + List<_i5.Wallet<_i4.CryptoCurrency>> get wallets => (super.noSuchMethod( + Invocation.getter(#wallets), + returnValue: <_i5.Wallet<_i4.CryptoCurrency>>[], + ) as List<_i5.Wallet<_i4.CryptoCurrency>>); @override set nodeService(_i2.NodeService? value) => super.noSuchMethod( @@ -116,13 +108,12 @@ class MockWallets extends _i1.Mock implements _i8.Wallets { @override _i5.Wallet<_i4.CryptoCurrency> getWallet(String? walletId) => (super.noSuchMethod( - Invocation.method(#getWallet, [walletId]), - returnValue: _FakeWallet_2<_i4.CryptoCurrency>( - this, - Invocation.method(#getWallet, [walletId]), - ), - ) - as _i5.Wallet<_i4.CryptoCurrency>); + Invocation.method(#getWallet, [walletId]), + returnValue: _FakeWallet_2<_i4.CryptoCurrency>( + this, + Invocation.method(#getWallet, [walletId]), + ), + ) as _i5.Wallet<_i4.CryptoCurrency>); @override void addWallet(_i5.Wallet<_i4.CryptoCurrency>? wallet) => super.noSuchMethod( @@ -134,43 +125,33 @@ class MockWallets extends _i1.Mock implements _i8.Wallets { _i9.Future deleteWallet( _i10.WalletInfo? info, _i7.SecureStorageInterface? secureStorage, - ) => - (super.noSuchMethod( - Invocation.method(#deleteWallet, [info, secureStorage]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + ) => (super.noSuchMethod( + Invocation.method(#deleteWallet, [info, secureStorage]), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override _i9.Future load( _i11.Prefs? prefs, _i3.MainDB? mainDB, bool? isDuress, - ) => - (super.noSuchMethod( - Invocation.method(#load, [prefs, mainDB, isDuress]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + ) => (super.noSuchMethod( + Invocation.method(#load, [prefs, mainDB, isDuress]), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override _i9.Future loadAfterStackRestore( _i11.Prefs? prefs, List<_i5.Wallet<_i4.CryptoCurrency>>? wallets, bool? isDesktop, - ) => - (super.noSuchMethod( - Invocation.method(#loadAfterStackRestore, [ - prefs, - wallets, - isDesktop, - ]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + ) => (super.noSuchMethod( + Invocation.method(#loadAfterStackRestore, [prefs, wallets, isDesktop]), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); } /// A class which mocks [ThemeService]. @@ -182,28 +163,22 @@ class MockThemeService extends _i1.Mock implements _i12.ThemeService { } @override - _i3.MainDB get db => - (super.noSuchMethod( - Invocation.getter(#db), - returnValue: _FakeMainDB_1(this, Invocation.getter(#db)), - ) - as _i3.MainDB); + _i3.MainDB get db => (super.noSuchMethod( + Invocation.getter(#db), + returnValue: _FakeMainDB_1(this, Invocation.getter(#db)), + ) as _i3.MainDB); @override - _i6.HTTP get client => - (super.noSuchMethod( - Invocation.getter(#client), - returnValue: _FakeHTTP_3(this, Invocation.getter(#client)), - ) - as _i6.HTTP); + _i6.HTTP get client => (super.noSuchMethod( + Invocation.getter(#client), + returnValue: _FakeHTTP_3(this, Invocation.getter(#client)), + ) as _i6.HTTP); @override - List<_i13.StackTheme> get installedThemes => - (super.noSuchMethod( - Invocation.getter(#installedThemes), - returnValue: <_i13.StackTheme>[], - ) - as List<_i13.StackTheme>); + List<_i13.StackTheme> get installedThemes => (super.noSuchMethod( + Invocation.getter(#installedThemes), + returnValue: <_i13.StackTheme>[], + ) as List<_i13.StackTheme>); @override set client(_i6.HTTP? value) => super.noSuchMethod( @@ -220,59 +195,48 @@ class MockThemeService extends _i1.Mock implements _i12.ThemeService { @override _i9.Future install({required _i14.Uint8List? themeArchiveData}) => (super.noSuchMethod( - Invocation.method(#install, [], { - #themeArchiveData: themeArchiveData, - }), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + Invocation.method(#install, [], {#themeArchiveData: themeArchiveData}), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override - _i9.Future remove({required String? themeId}) => - (super.noSuchMethod( - Invocation.method(#remove, [], {#themeId: themeId}), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + _i9.Future remove({required String? themeId}) => (super.noSuchMethod( + Invocation.method(#remove, [], {#themeId: themeId}), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override - _i9.Future checkDefaultThemesOnStartup() => - (super.noSuchMethod( - Invocation.method(#checkDefaultThemesOnStartup, []), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + _i9.Future checkDefaultThemesOnStartup() => (super.noSuchMethod( + Invocation.method(#checkDefaultThemesOnStartup, []), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override _i9.Future verifyInstalled({required String? themeId}) => (super.noSuchMethod( - Invocation.method(#verifyInstalled, [], {#themeId: themeId}), - returnValue: _i9.Future.value(false), - ) - as _i9.Future); + Invocation.method(#verifyInstalled, [], {#themeId: themeId}), + returnValue: _i9.Future.value(false), + ) as _i9.Future); @override _i9.Future> fetchThemes() => (super.noSuchMethod( - Invocation.method(#fetchThemes, []), - returnValue: _i9.Future>.value( - <_i12.StackThemeMetaData>[], - ), - ) - as _i9.Future>); + Invocation.method(#fetchThemes, []), + returnValue: _i9.Future>.value( + <_i12.StackThemeMetaData>[], + ), + ) as _i9.Future>); @override _i9.Future<_i14.Uint8List> fetchTheme({ required _i12.StackThemeMetaData? themeMetaData, - }) => - (super.noSuchMethod( - Invocation.method(#fetchTheme, [], {#themeMetaData: themeMetaData}), - returnValue: _i9.Future<_i14.Uint8List>.value(_i14.Uint8List(0)), - ) - as _i9.Future<_i14.Uint8List>); + }) => (super.noSuchMethod( + Invocation.method(#fetchTheme, [], {#themeMetaData: themeMetaData}), + returnValue: _i9.Future<_i14.Uint8List>.value(_i14.Uint8List(0)), + ) as _i9.Future<_i14.Uint8List>); @override _i13.StackTheme? getTheme({required String? themeId}) => @@ -289,31 +253,25 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { } @override - _i7.SecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( - Invocation.getter(#secureStorageInterface), - returnValue: _FakeSecureStorageInterface_4( - this, - Invocation.getter(#secureStorageInterface), - ), - ) - as _i7.SecureStorageInterface); + _i7.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( + Invocation.getter(#secureStorageInterface), + returnValue: _FakeSecureStorageInterface_4( + this, + Invocation.getter(#secureStorageInterface), + ), + ) as _i7.SecureStorageInterface); @override - List<_i15.NodeModel> get primaryNodes => - (super.noSuchMethod( - Invocation.getter(#primaryNodes), - returnValue: <_i15.NodeModel>[], - ) - as List<_i15.NodeModel>); + List<_i15.NodeModel> get primaryNodes => (super.noSuchMethod( + Invocation.getter(#primaryNodes), + returnValue: <_i15.NodeModel>[], + ) as List<_i15.NodeModel>); @override - List<_i15.NodeModel> get nodes => - (super.noSuchMethod( - Invocation.getter(#nodes), - returnValue: <_i15.NodeModel>[], - ) - as List<_i15.NodeModel>); + List<_i15.NodeModel> get nodes => (super.noSuchMethod( + Invocation.getter(#nodes), + returnValue: <_i15.NodeModel>[], + ) as List<_i15.NodeModel>); @override bool get hasListeners => @@ -321,45 +279,39 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { as bool); @override - _i9.Future updateDefaults() => - (super.noSuchMethod( - Invocation.method(#updateDefaults, []), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + _i9.Future updateDefaults() => (super.noSuchMethod( + Invocation.method(#updateDefaults, []), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override _i9.Future setPrimaryNodeFor({ required _i4.CryptoCurrency? coin, required _i15.NodeModel? node, bool? shouldNotifyListeners = false, - }) => - (super.noSuchMethod( - Invocation.method(#setPrimaryNodeFor, [], { - #coin: coin, - #node: node, - #shouldNotifyListeners: shouldNotifyListeners, - }), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + }) => (super.noSuchMethod( + Invocation.method(#setPrimaryNodeFor, [], { + #coin: coin, + #node: node, + #shouldNotifyListeners: shouldNotifyListeners, + }), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override _i15.NodeModel? getPrimaryNodeFor({required _i4.CryptoCurrency? currency}) => (super.noSuchMethod( - Invocation.method(#getPrimaryNodeFor, [], {#currency: currency}), - ) - as _i15.NodeModel?); + Invocation.method(#getPrimaryNodeFor, [], {#currency: currency}), + ) as _i15.NodeModel?); @override List<_i15.NodeModel> getNodesFor(_i4.CryptoCurrency? coin) => (super.noSuchMethod( - Invocation.method(#getNodesFor, [coin]), - returnValue: <_i15.NodeModel>[], - ) - as List<_i15.NodeModel>); + Invocation.method(#getNodesFor, [coin]), + returnValue: <_i15.NodeModel>[], + ) as List<_i15.NodeModel>); @override _i15.NodeModel? getNodeById({required String? id}) => @@ -369,127 +321,161 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { @override List<_i15.NodeModel> failoverNodesFor({ required _i4.CryptoCurrency? currency, - }) => - (super.noSuchMethod( - Invocation.method(#failoverNodesFor, [], {#currency: currency}), - returnValue: <_i15.NodeModel>[], - ) - as List<_i15.NodeModel>); + }) => (super.noSuchMethod( + Invocation.method(#failoverNodesFor, [], {#currency: currency}), + returnValue: <_i15.NodeModel>[], + ) as List<_i15.NodeModel>); @override _i9.Future save( _i15.NodeModel? node, String? password, bool? shouldNotifyListeners, - ) => + ) => (super.noSuchMethod( + Invocation.method(#save, [node, password, shouldNotifyListeners]), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future saveEdit( + _i15.NodeModel? node, + String? password, + bool? notify, { + required _i15.NodeModel? opened, + bool? forgetCertificate = false, + }) => (super.noSuchMethod( + Invocation.method( + #saveEdit, + [node, password, notify], + {#opened: opened, #forgetCertificate: forgetCertificate}, + ), + returnValue: _i9.Future.value(false), + ) as _i9.Future); + + @override + bool prepareCertificateTest(_i16.NodeFormData? data) => (super.noSuchMethod( + Invocation.method(#prepareCertificateTest, [data]), + returnValue: false, + ) as bool); + + @override + _i9.Future saveTrustedCertificate( + _i15.NodeModel? node, + _i16.NodeFormData? data, + ) => (super.noSuchMethod( + Invocation.method(#saveTrustedCertificate, [node, data]), + returnValue: _i9.Future.value(false), + ) as _i9.Future); + + @override + _i9.Future forgetTrustedCertificate(_i15.NodeModel? shown) => (super.noSuchMethod( - Invocation.method(#save, [node, password, shouldNotifyListeners]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + Invocation.method(#forgetTrustedCertificate, [shown]), + returnValue: _i9.Future.value(false), + ) as _i9.Future); + + @override + _i9.Future commitConnectionTest({ + required _i15.NodeModel? node, + required _i16.NodeFormData? formData, + }) => (super.noSuchMethod( + Invocation.method(#commitConnectionTest, [], { + #node: node, + #formData: formData, + }), + returnValue: _i9.Future.value(false), + ) as _i9.Future); + + @override + _i9.Future setFailover(String? id, bool? value) => (super.noSuchMethod( + Invocation.method(#setFailover, [id, value]), + returnValue: _i9.Future.value(false), + ) as _i9.Future); @override _i9.Future delete(String? id, bool? shouldNotifyListeners) => (super.noSuchMethod( - Invocation.method(#delete, [id, shouldNotifyListeners]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + Invocation.method(#delete, [id, shouldNotifyListeners]), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override _i9.Future setEnabledState( String? id, bool? enabled, bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method(#setEnabledState, [ - id, - enabled, - shouldNotifyListeners, - ]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); - - @override - _i9.Future updateDefaultEpicBoxes() => - (super.noSuchMethod( - Invocation.method(#updateDefaultEpicBoxes, []), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + ) => (super.noSuchMethod( + Invocation.method(#setEnabledState, [id, enabled, shouldNotifyListeners]), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future updateDefaultEpicBoxes() => (super.noSuchMethod( + Invocation.method(#updateDefaultEpicBoxes, []), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override _i9.Future setPrimaryEpicBox({ - required _i16.EpicBoxServerModel? epicBox, + required _i17.EpicBoxServerModel? epicBox, bool? shouldNotifyListeners = false, - }) => - (super.noSuchMethod( - Invocation.method(#setPrimaryEpicBox, [], { - #epicBox: epicBox, - #shouldNotifyListeners: shouldNotifyListeners, - }), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + }) => (super.noSuchMethod( + Invocation.method(#setPrimaryEpicBox, [], { + #epicBox: epicBox, + #shouldNotifyListeners: shouldNotifyListeners, + }), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override - List<_i16.EpicBoxServerModel> getEpicBoxes() => - (super.noSuchMethod( - Invocation.method(#getEpicBoxes, []), - returnValue: <_i16.EpicBoxServerModel>[], - ) - as List<_i16.EpicBoxServerModel>); + List<_i17.EpicBoxServerModel> getEpicBoxes() => (super.noSuchMethod( + Invocation.method(#getEpicBoxes, []), + returnValue: <_i17.EpicBoxServerModel>[], + ) as List<_i17.EpicBoxServerModel>); @override - _i16.EpicBoxServerModel? getEpicBoxById({required String? id}) => + _i17.EpicBoxServerModel? getEpicBoxById({required String? id}) => (super.noSuchMethod(Invocation.method(#getEpicBoxById, [], {#id: id})) - as _i16.EpicBoxServerModel?); + as _i17.EpicBoxServerModel?); @override _i9.Future addEpicBox( - _i16.EpicBoxServerModel? epicBox, + _i17.EpicBoxServerModel? epicBox, bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method(#addEpicBox, [epicBox, shouldNotifyListeners]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + ) => (super.noSuchMethod( + Invocation.method(#addEpicBox, [epicBox, shouldNotifyListeners]), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override _i9.Future deleteEpicBox(String? id, bool? shouldNotifyListeners) => (super.noSuchMethod( - Invocation.method(#deleteEpicBox, [id, shouldNotifyListeners]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + Invocation.method(#deleteEpicBox, [id, shouldNotifyListeners]), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override - _i9.Future updateCommunityNodes() => - (super.noSuchMethod( - Invocation.method(#updateCommunityNodes, []), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + _i9.Future updateCommunityNodes() => (super.noSuchMethod( + Invocation.method(#updateCommunityNodes, []), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override - void addListener(_i17.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i18.VoidCallback? listener) => super.noSuchMethod( Invocation.method(#addListener, [listener]), returnValueForMissingStub: null, ); @override - void removeListener(_i17.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i18.VoidCallback? listener) => super.noSuchMethod( Invocation.method(#removeListener, [listener]), returnValueForMissingStub: null, );