From b8987c73c0b6f5f51278c50b1de333cc3e35bbef Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 14 Feb 2024 19:47:26 -0600 Subject: [PATCH 1/2] WIP use and reuse electrum adapter channel --- lib/db/db_version_migration.dart | 1 + .../cached_electrumx_client.dart | 48 ++- lib/electrumx_rpc/electrumx_client.dart | 400 +++++++++--------- .../add_edit_node_view.dart | 1 + .../manage_nodes_views/node_details_view.dart | 1 + lib/services/notifications_service.dart | 1 + .../electrumx_interface.dart | 62 ++- lib/widgets/node_card.dart | 1 + lib/widgets/node_options_sheet.dart | 1 + pubspec.lock | 6 +- pubspec.yaml | 3 +- 11 files changed, 293 insertions(+), 232 deletions(-) diff --git a/lib/db/db_version_migration.dart b/lib/db/db_version_migration.dart index 21241f630..e81c01c76 100644 --- a/lib/db/db_version_migration.dart +++ b/lib/db/db_version_migration.dart @@ -85,6 +85,7 @@ class DbVersionMigrator with WalletDB { useSSL: node.useSSL), prefs: prefs, failovers: failovers, + coin: Coin.firo, ); try { diff --git a/lib/electrumx_rpc/cached_electrumx_client.dart b/lib/electrumx_rpc/cached_electrumx_client.dart index b0838e93d..be9f9a47c 100644 --- a/lib/electrumx_rpc/cached_electrumx_client.dart +++ b/lib/electrumx_rpc/cached_electrumx_client.dart @@ -12,29 +12,32 @@ import 'dart:convert'; import 'dart:math'; import 'package:electrum_adapter/electrum_adapter.dart' as electrum_adapter; +import 'package:electrum_adapter/electrum_adapter.dart'; +import 'package:electrum_adapter/methods/specific/firo.dart'; import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; -import 'package:stackwallet/services/tor_service.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/prefs.dart'; import 'package:string_validator/string_validator.dart'; class CachedElectrumXClient { final ElectrumXClient electrumXClient; + final ElectrumClient electrumAdapterClient; static const minCacheConfirms = 30; const CachedElectrumXClient({ required this.electrumXClient, + required this.electrumAdapterClient, }); factory CachedElectrumXClient.from({ required ElectrumXClient electrumXClient, + required ElectrumClient electrumAdapterClient, }) => CachedElectrumXClient( - electrumXClient: electrumXClient, - ); + electrumXClient: electrumXClient, + electrumAdapterClient: electrumAdapterClient); Future> getAnonymitySet({ required String groupId, @@ -59,9 +62,10 @@ class CachedElectrumXClient { set = Map.from(cachedSet); } - final newSet = await electrumXClient.getLelantusAnonymitySet( + final newSet = await (electrumAdapterClient as FiroElectrumClient) + .getLelantusAnonymitySet( groupId: groupId, - blockhash: set["blockHash"] as String, + blockHash: set["blockHash"] as String, ); // update set with new data @@ -85,7 +89,7 @@ class CachedElectrumXClient { translatedCoin.add(!isHexadecimal(newCoin[2] as String) ? base64ToHex(newCoin[2] as String) : newCoin[2]); - } catch (e, s) { + } catch (e) { translatedCoin.add(newCoin[2]); } translatedCoin.add(!isHexadecimal(newCoin[3] as String) @@ -133,7 +137,8 @@ class CachedElectrumXClient { set = Map.from(cachedSet); } - final newSet = await electrumXClient.getSparkAnonymitySet( + final newSet = await (electrumAdapterClient as FiroElectrumClient) + .getSparkAnonymitySet( coinGroupId: groupId, startBlockHash: set["blockHash"] as String, ); @@ -191,15 +196,8 @@ class CachedElectrumXClient { final cachedTx = box.get(txHash) as Map?; if (cachedTx == null) { - var channel = await electrum_adapter.connect(electrumXClient.host, - port: electrumXClient.port, - useSSL: electrumXClient.useSSL, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null); - var client = electrum_adapter.ElectrumClient( - channel, electrumXClient.host, electrumXClient.port); - final Map result = await client.getTransaction(txHash); + final Map result = + await electrumAdapterClient.getTransaction(txHash); result.remove("hex"); result.remove("lelantusData"); @@ -241,7 +239,8 @@ class CachedElectrumXClient { cachedSerials.length - 100, // 100 being some arbitrary buffer ); - final serials = await electrumXClient.getLelantusUsedCoinSerials( + final serials = await (electrumAdapterClient as FiroElectrumClient) + .getLelantusUsedCoinSerials( startNumber: startNumber, ); @@ -289,7 +288,8 @@ class CachedElectrumXClient { cachedTags.length - 100, // 100 being some arbitrary buffer ); - final tags = await electrumXClient.getSparkUsedCoinsTags( + final tags = + await (electrumAdapterClient as FiroElectrumClient).getUsedCoinsTags( startNumber: startNumber, ); @@ -297,12 +297,18 @@ class CachedElectrumXClient { // .map((e) => !isHexadecimal(e) ? base64ToHex(e) : e) // .toSet(); + // Convert the Map tags to a Set. + final newTags = (tags["tags"] as List).toSet(); + // ensure we are getting some overlap so we know we are not missing any if (cachedTags.isNotEmpty && tags.isNotEmpty) { - assert(cachedTags.intersection(tags).isNotEmpty); + assert(cachedTags.intersection(newTags).isNotEmpty); } - cachedTags.addAll(tags); + // Make newTags an Iterable. + final Iterable iterableTags = newTags.map((e) => e.toString()); + + cachedTags.addAll(iterableTags); await box.put( "tags", diff --git a/lib/electrumx_rpc/electrumx_client.dart b/lib/electrumx_rpc/electrumx_client.dart index 14f3f6bac..7b9473a7b 100644 --- a/lib/electrumx_rpc/electrumx_client.dart +++ b/lib/electrumx_rpc/electrumx_client.dart @@ -9,12 +9,12 @@ */ import 'dart:async'; -import 'dart:convert'; import 'dart:io'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:decimal/decimal.dart'; import 'package:electrum_adapter/electrum_adapter.dart' as electrum_adapter; +import 'package:electrum_adapter/electrum_adapter.dart'; import 'package:electrum_adapter/methods/specific/firo.dart'; import 'package:event_bus/event_bus.dart'; import 'package:flutter/foundation.dart'; @@ -26,9 +26,10 @@ import 'package:stackwallet/services/event_bus/events/global/tor_connection_stat import 'package:stackwallet/services/event_bus/events/global/tor_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/tor_service.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; -import 'package:uuid/uuid.dart'; +import 'package:stream_channel/stream_channel.dart'; class WifiOnlyException implements Exception {} @@ -75,6 +76,12 @@ class ElectrumXClient { JsonRPC? get rpcClient => _rpcClient; JsonRPC? _rpcClient; + StreamChannel? get electrumAdapterChannel => _electrumAdapterChannel; + StreamChannel? _electrumAdapterChannel; + + ElectrumClient? get electrumAdapterClient => _electrumAdapterClient; + ElectrumClient? _electrumAdapterClient; + late Prefs _prefs; late TorService _torService; @@ -83,6 +90,9 @@ class ElectrumXClient { final Duration connectionTimeoutForSpecialCaseJsonRPCClients; + Coin get coin => _coin; + late Coin _coin; + // add finalizer to cancel stream subscription when all references to an // instance of ElectrumX becomes inaccessible static final Finalizer _finalizer = Finalizer( @@ -103,6 +113,7 @@ class ElectrumXClient { required bool useSSL, required Prefs prefs, required List failovers, + required Coin coin, JsonRPC? client, this.connectionTimeoutForSpecialCaseJsonRPCClients = const Duration(seconds: 60), @@ -115,6 +126,7 @@ class ElectrumXClient { _port = port; _useSSL = useSSL; _rpcClient = client; + _coin = coin; final bus = globalEventBusForTesting ?? GlobalEventBus.instance; _torStatusListener = bus.on().listen( @@ -154,6 +166,8 @@ class ElectrumXClient { // setting to null should force the creation of a new json rpc client // on the next request sent through this electrumx instance _rpcClient = null; + _electrumAdapterChannel = null; + _electrumAdapterClient = null; await temp?.disconnect( reason: "Tor status changed to \"${event.status}\"", @@ -166,6 +180,7 @@ class ElectrumXClient { required ElectrumXNode node, required Prefs prefs, required List failovers, + required Coin coin, TorService? torService, EventBus? globalEventBusForTesting, }) { @@ -177,6 +192,7 @@ class ElectrumXClient { torService: torService, failovers: failovers, globalEventBusForTesting: globalEventBusForTesting, + coin: coin, ); } @@ -238,24 +254,99 @@ class ElectrumXClient { return; } } + } + + Future _checkElectrumAdapter() async { + ({InternetAddress host, int port})? proxyInfo; + + // If we're supposed to use Tor... + if (_prefs.useTor) { + // But Tor isn't running... + if (_torService.status != TorConnectionStatus.connected) { + // And the killswitch isn't set... + if (!_prefs.torKillSwitch) { + // Then we'll just proceed and connect to ElectrumX through clearnet at the bottom of this function. + Logging.instance.log( + "Tor preference set but Tor is not enabled, killswitch not set, connecting to Electrum adapter through clearnet", + level: LogLevel.Warning, + ); + } else { + // ... But if the killswitch is set, then we throw an exception. + throw Exception( + "Tor preference and killswitch set but Tor is not enabled, not connecting to Electrum adapter"); + // TODO [prio=low]: Try to start Tor. + } + } else { + // Get the proxy info from the TorService. + proxyInfo = _torService.getProxyInfo(); + } + } + + // TODO [prio=med]: Add proxyInfo to StreamChannel (or add to wrapper). + // if (_electrumAdapter!.proxyInfo != proxyInfo) { + // _electrumAdapter!.proxyInfo = proxyInfo; + // _electrumAdapter!.disconnect( + // reason: "Tor proxyInfo does not match current info", + // ); + // } if (currentFailoverIndex == -1) { - _rpcClient ??= JsonRPC( - host: host, + _electrumAdapterChannel ??= await electrum_adapter.connect( + host, port: port, + connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, + aliveTimerDuration: connectionTimeoutForSpecialCaseJsonRPCClients, + acceptUnverified: true, useSSL: useSSL, - connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, - proxyInfo: null, + proxyInfo: proxyInfo, ); + if (_coin == Coin.firo || _coin == Coin.firoTestNet) { + _electrumAdapterClient ??= FiroElectrumClient( + _electrumAdapterChannel!, + host, + port, + useSSL, + proxyInfo, + ); + } else { + _electrumAdapterClient ??= ElectrumClient( + _electrumAdapterChannel!, + host, + port, + useSSL, + proxyInfo, + ); + } } else { - _rpcClient ??= JsonRPC( - host: failovers![currentFailoverIndex].address, + _electrumAdapterChannel ??= await electrum_adapter.connect( + failovers![currentFailoverIndex].address, port: failovers![currentFailoverIndex].port, - useSSL: failovers![currentFailoverIndex].useSSL, connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, - proxyInfo: null, + aliveTimerDuration: connectionTimeoutForSpecialCaseJsonRPCClients, + acceptUnverified: true, + useSSL: failovers![currentFailoverIndex].useSSL, + proxyInfo: proxyInfo, ); + if (_coin == Coin.firo || _coin == Coin.firoTestNet) { + _electrumAdapterClient ??= FiroElectrumClient( + _electrumAdapterChannel!, + failovers![currentFailoverIndex].address, + failovers![currentFailoverIndex].port, + failovers![currentFailoverIndex].useSSL, + proxyInfo, + ); + } else { + _electrumAdapterClient ??= ElectrumClient( + _electrumAdapterChannel!, + failovers![currentFailoverIndex].address, + failovers![currentFailoverIndex].port, + failovers![currentFailoverIndex].useSSL, + proxyInfo, + ); + } } + + return; } /// Send raw rpc command @@ -271,32 +362,22 @@ class ElectrumXClient { } if (_requireMutex) { - await _torConnectingLock.protect(() async => _checkRpcClient()); + await _torConnectingLock + .protect(() async => await _checkElectrumAdapter()); } else { - _checkRpcClient(); + await _checkElectrumAdapter(); } try { - final requestId = requestID ?? const Uuid().v1(); - final jsonArgs = json.encode(args); - final jsonRequestString = '{"jsonrpc": "2.0", ' - '"id": "$requestId",' - '"method": "$command",' - '"params": $jsonArgs}'; - - // Logging.instance.log("ElectrumX jsonRequestString: $jsonRequestString"); - - final response = await _rpcClient!.request( - jsonRequestString, - requestTimeout, + final response = await _electrumAdapterClient!.request( + command, + args, ); - if (response.exception != null) { - throw response.exception!; - } - - if (response.data is Map && response.data["error"] != null) { - if (response.data["error"] + if (response is Map && + response.keys.contains("error") && + response["error"] != null) { + if (response["error"] .toString() .contains("No such mempool or blockchain transaction")) { throw NoSuchTransactionException( @@ -308,13 +389,13 @@ class ElectrumXClient { throw Exception( "JSONRPC response\n" " command: $command\n" - " error: ${response.data}" + " error: ${response["error"]}\n" " args: $args\n", ); } currentFailoverIndex = -1; - return response.data; + return response; } on WifiOnlyException { rethrow; } on SocketException { @@ -350,7 +431,7 @@ class ElectrumXClient { /// map of /// /// returns a list of json response objects if no errors were found - Future>> batchRequest({ + Future> batchRequest({ required String command, required Map> args, Duration requestTimeout = const Duration(seconds: 60), @@ -361,62 +442,34 @@ class ElectrumXClient { } if (_requireMutex) { - await _torConnectingLock.protect(() async => _checkRpcClient()); + await _torConnectingLock + .protect(() async => await _checkElectrumAdapter()); } else { - _checkRpcClient(); + await _checkElectrumAdapter(); } try { - final List requestStrings = []; - - for (final entry in args.entries) { - final jsonArgs = json.encode(entry.value); - requestStrings.add( - '{"jsonrpc": "2.0", "id": "${entry.key}","method": "$command","params": $jsonArgs}'); - } - - // combine request strings into json array - String request = "["; - for (int i = 0; i < requestStrings.length - 1; i++) { - request += "${requestStrings[i]},"; - } - request += "${requestStrings.last}]"; - - // Logging.instance.log("batch request: $request"); - - // send batch request - final jsonRpcResponse = - (await _rpcClient!.request(request, requestTimeout)); - - if (jsonRpcResponse.exception != null) { - throw jsonRpcResponse.exception!; - } - - final List response; - try { - if (jsonRpcResponse.data is Map) { - response = [jsonRpcResponse.data]; - - if (requestStrings.length > 1) { - Logging.instance.log( - "ElectrumXClient.batchRequest: Map returned instead of a list and there are ${requestStrings.length} queued.", - level: LogLevel.Error); - } - // Could throw error here. - } else { - response = jsonRpcResponse.data as List; + var futures = >[]; + List? response; + _electrumAdapterClient!.peer.withBatch(() { + for (final entry in args.entries) { + futures.add(_electrumAdapterClient!.request(command, entry.value)); } - } catch (_) { - throw Exception( - "Expected json list or map but got a ${jsonRpcResponse.data.runtimeType}: ${jsonRpcResponse.data}", - ); - } + }); + response = await Future.wait(futures); // check for errors, format and throw if there are any final List errors = []; for (int i = 0; i < response.length; i++) { - final result = response[i]; - if (result["error"] != null || result["result"] == null) { + var result = response[i]; + + if (result == null || (result is List && result.isEmpty)) { + continue; + // TODO [prio=extreme]: Figure out if this is actually an issue. + } + result = result[0]; // Unwrap the list. + if ((result is Map && result.keys.contains("error")) || + result == null) { errors.add(result.toString()); } } @@ -430,7 +483,7 @@ class ElectrumXClient { } currentFailoverIndex = -1; - return List>.from(response, growable: false); + return response; } on WifiOnlyException { rethrow; } on SocketException { @@ -471,7 +524,7 @@ class ElectrumXClient { requestTimeout: const Duration(seconds: 2), retries: retryCount, ).timeout(const Duration(seconds: 2)) as Map; - return response.keys.contains("result") && response["result"] == null; + return response.isNotEmpty; // TODO [prio=extreme]: Fix this. } catch (e) { rethrow; } @@ -492,14 +545,14 @@ class ElectrumXClient { requestID: requestID, command: 'blockchain.headers.subscribe', ); - if (response["result"] == null) { + if (response == null) { Logging.instance.log( "getBlockHeadTip returned null response", level: LogLevel.Error, ); throw 'getBlockHeadTip returned null response'; } - return Map.from(response["result"] as Map); + return Map.from(response as Map); } catch (e) { rethrow; } @@ -524,7 +577,7 @@ class ElectrumXClient { requestID: requestID, command: 'server.features', ); - return Map.from(response["result"] as Map); + return Map.from(response as Map); } catch (e) { rethrow; } @@ -545,7 +598,7 @@ class ElectrumXClient { rawTx, ], ); - return response["result"] as String; + return response as String; } catch (e) { rethrow; } @@ -572,7 +625,7 @@ class ElectrumXClient { scripthash, ], ); - return Map.from(response["result"] as Map); + return Map.from(response as Map); } catch (e) { rethrow; } @@ -609,7 +662,7 @@ class ElectrumXClient { scripthash, ], ); - result = response["result"]; + result = response; retryCount--; } @@ -619,17 +672,16 @@ class ElectrumXClient { } } - Future>>> getBatchHistory( + Future>>> getBatchHistory( {required Map> args}) async { try { final response = await batchRequest( command: 'blockchain.scripthash.get_history', args: args, ); - final Map>> result = {}; + final Map>> result = {}; for (int i = 0; i < response.length; i++) { - result[response[i]["id"] as String] = - List>.from(response[i]["result"] as List); + result[i] = List>.from(response[i] as List); } return result; } catch (e) { @@ -667,23 +719,33 @@ class ElectrumXClient { scripthash, ], ); - return List>.from(response["result"] as List); + return List>.from(response as List); } catch (e) { rethrow; } } - Future>>> getBatchUTXOs( + Future>>> getBatchUTXOs( {required Map> args}) async { try { final response = await batchRequest( command: 'blockchain.scripthash.listunspent', args: args, ); - final Map>> result = {}; + final Map>> result = {}; for (int i = 0; i < response.length; i++) { - result[response[i]["id"] as String] = - List>.from(response[i]["result"] as List); + if ((response[i] as List).isNotEmpty) { + try { + // result[i] = response[i] as List>; + result[i] = List>.from(response[i] as List); + } catch (e) { + print(response[i]); + Logging.instance.log( + "getBatchUTXOs failed to parse response", + level: LogLevel.Error, + ); + } + } } return result; } catch (e) { @@ -745,14 +807,8 @@ class ElectrumXClient { }) async { Logging.instance.log("attempting to fetch blockchain.transaction.get...", level: LogLevel.Info); - var channel = await electrum_adapter.connect(host, - port: port, - useSSL: useSSL, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null); - var client = electrum_adapter.ElectrumClient(channel, host, port); - dynamic response = await client.getTransaction(txHash); + await _checkElectrumAdapter(); + dynamic response = await _electrumAdapterClient!.getTransaction(txHash); Logging.instance.log("Fetching blockchain.transaction.get finished", level: LogLevel.Info); @@ -784,18 +840,13 @@ class ElectrumXClient { }) async { Logging.instance.log("attempting to fetch lelantus.getanonymityset...", level: LogLevel.Info); - var channel = await electrum_adapter.connect(host, - port: port, - useSSL: useSSL, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null); - var client = electrum_adapter.FiroElectrumClient(channel); - Map anonymitySet = await client.getLelantusAnonymitySet( - groupId: groupId, blockHash: blockhash); + await _checkElectrumAdapter(); + Map response = + await (_electrumAdapterClient as FiroElectrumClient)! + .getLelantusAnonymitySet(groupId: groupId, blockHash: blockhash); Logging.instance.log("Fetching lelantus.getanonymityset finished", level: LogLevel.Info); - return anonymitySet; + return response; } //TODO add example to docs @@ -808,17 +859,12 @@ class ElectrumXClient { }) async { Logging.instance.log("attempting to fetch lelantus.getmintmetadata...", level: LogLevel.Info); - var channel = await electrum_adapter.connect(host, - port: port, - useSSL: useSSL, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null); - var client = electrum_adapter.FiroElectrumClient(channel); - dynamic mintData = await client.getLelantusMintData(mints: mints); + await _checkElectrumAdapter(); + dynamic response = await (_electrumAdapterClient as FiroElectrumClient)! + .getLelantusMintData(mints: mints); Logging.instance.log("Fetching lelantus.getmintmetadata finished", level: LogLevel.Info); - return mintData; + return response; } //TODO add example to docs @@ -829,20 +875,14 @@ class ElectrumXClient { }) async { Logging.instance.log("attempting to fetch lelantus.getusedcoinserials...", level: LogLevel.Info); - var channel = await electrum_adapter.connect(host, - port: port, - useSSL: useSSL, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null); - var client = electrum_adapter.FiroElectrumClient(channel); + await _checkElectrumAdapter(); int retryCount = 3; - dynamic usedCoinSerials; + dynamic response; - while (retryCount > 0 && usedCoinSerials is! List) { - usedCoinSerials = - await client.getLelantusUsedCoinSerials(startNumber: startNumber); + while (retryCount > 0 && response is! List) { + response = await (_electrumAdapterClient as FiroElectrumClient)! + .getLelantusUsedCoinSerials(startNumber: startNumber); // TODO add 2 minute timeout. Logging.instance.log("Fetching lelantus.getusedcoinserials finished", level: LogLevel.Info); @@ -850,7 +890,7 @@ class ElectrumXClient { retryCount--; } - return Map.from(usedCoinSerials as Map); + return Map.from(response as Map); } /// Returns the latest Lelantus set id @@ -859,17 +899,12 @@ class ElectrumXClient { Future getLelantusLatestCoinId({String? requestID}) async { Logging.instance.log("attempting to fetch lelantus.getlatestcoinid...", level: LogLevel.Info); - var channel = await electrum_adapter.connect(host, - port: port, - useSSL: useSSL, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null); - var client = electrum_adapter.FiroElectrumClient(channel); - int latestCoinId = await client.getLatestCoinId(); + await _checkElectrumAdapter(); + int response = + await (_electrumAdapterClient as FiroElectrumClient).getLatestCoinId(); Logging.instance.log("Fetching lelantus.getlatestcoinid finished", level: LogLevel.Info); - return latestCoinId; + return response; } // ============== Spark ====================================================== @@ -895,18 +930,14 @@ class ElectrumXClient { try { Logging.instance.log("attempting to fetch spark.getsparkanonymityset...", level: LogLevel.Info); - var channel = await electrum_adapter.connect(host, - port: port, - useSSL: useSSL, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null); - var client = electrum_adapter.FiroElectrumClient(channel); - Map anonymitySet = await client.getSparkAnonymitySet( - coinGroupId: coinGroupId, startBlockHash: startBlockHash); + await _checkElectrumAdapter(); + Map response = + await (_electrumAdapterClient as FiroElectrumClient) + .getSparkAnonymitySet( + coinGroupId: coinGroupId, startBlockHash: startBlockHash); Logging.instance.log("Fetching spark.getsparkanonymityset finished", level: LogLevel.Info); - return anonymitySet; + return response; } catch (e) { rethrow; } @@ -922,19 +953,14 @@ class ElectrumXClient { // Use electrum_adapter package's getSparkUsedCoinsTags method. Logging.instance.log("attempting to fetch spark.getusedcoinstags...", level: LogLevel.Info); - var channel = await electrum_adapter.connect(host, - port: port, - useSSL: useSSL, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null); - var client = electrum_adapter.FiroElectrumClient(channel); - Map usedCoinsTags = - await client.getUsedCoinsTags(startNumber: startNumber); + await _checkElectrumAdapter(); + Map response = + await (_electrumAdapterClient as FiroElectrumClient) + .getUsedCoinsTags(startNumber: startNumber); // TODO: Add 2 minute timeout. Logging.instance.log("Fetching spark.getusedcoinstags finished", level: LogLevel.Info); - final map = Map.from(usedCoinsTags); + final map = Map.from(response); final set = Set.from(map["tags"] as List); return await compute(_ffiHashTagsComputeWrapper, set); } catch (e) { @@ -960,18 +986,13 @@ class ElectrumXClient { try { Logging.instance.log("attempting to fetch spark.getsparkmintmetadata...", level: LogLevel.Info); - var channel = await electrum_adapter.connect(host, - port: port, - useSSL: useSSL, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null); - var client = electrum_adapter.FiroElectrumClient(channel); - List mintMetaData = - await client.getSparkMintMetaData(sparkCoinHashes: sparkCoinHashes); + await _checkElectrumAdapter(); + List response = + await (_electrumAdapterClient as FiroElectrumClient) + .getSparkMintMetaData(sparkCoinHashes: sparkCoinHashes); Logging.instance.log("Fetching spark.getsparkmintmetadata finished", level: LogLevel.Info); - return List>.from(mintMetaData); + return List>.from(response); } catch (e) { Logging.instance.log(e, level: LogLevel.Error); rethrow; @@ -987,17 +1008,12 @@ class ElectrumXClient { try { Logging.instance.log("attempting to fetch spark.getsparklatestcoinid...", level: LogLevel.Info); - var channel = await electrum_adapter.connect(host, - port: port, - useSSL: useSSL, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null); - var client = electrum_adapter.FiroElectrumClient(channel); - int latestCoinId = await client.getSparkLatestCoinId(); + await _checkElectrumAdapter(); + int response = await (_electrumAdapterClient as FiroElectrumClient) + .getSparkLatestCoinId(); Logging.instance.log("Fetching spark.getsparklatestcoinid finished", level: LogLevel.Info); - return latestCoinId; + return response; } catch (e) { Logging.instance.log(e, level: LogLevel.Error); rethrow; @@ -1014,14 +1030,8 @@ class ElectrumXClient { /// "rate": 1000, /// } Future> getFeeRate({String? requestID}) async { - var channel = await electrum_adapter.connect(host, - port: port, - useSSL: useSSL, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null); - var client = electrum_adapter.FiroElectrumClient(channel); - return await client.getFeeRate(); + await _checkElectrumAdapter(); + return await _electrumAdapterClient!.getFeeRate(); } /// Return the estimated transaction fee per kilobyte for a transaction to be confirmed within a certain number of [blocks]. @@ -1039,10 +1049,10 @@ class ElectrumXClient { ], ); try { - return Decimal.parse(response["result"].toString()); + return Decimal.parse(response.toString()); } catch (e, s) { final String msg = "Error parsing fee rate. Response: $response" - "\nResult: ${response["result"]}\nError: $e\nStack trace: $s"; + "\nResult: ${response}\nError: $e\nStack trace: $s"; Logging.instance.log(msg, level: LogLevel.Fatal); throw Exception(msg); } @@ -1062,7 +1072,7 @@ class ElectrumXClient { requestID: requestID, command: 'blockchain.relayfee', ); - return Decimal.parse(response["result"].toString()); + return Decimal.parse(response.toString()); } catch (e) { rethrow; } 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 e03c3ab21..ddac06aca 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 @@ -177,6 +177,7 @@ class _AddEditNodeViewState extends ConsumerState { useSSL: formData.useSSL!, failovers: [], prefs: ref.read(prefsChangeNotifierProvider), + coin: coin, ); try { 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 6bf0092e8..bd974b6ec 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 @@ -154,6 +154,7 @@ class _NodeDetailsViewState extends ConsumerState { useSSL: node.useSSL, failovers: [], prefs: ref.read(prefsChangeNotifierProvider), + coin: coin, ); try { diff --git a/lib/services/notifications_service.dart b/lib/services/notifications_service.dart index 1512c12c6..e6c49f47f 100644 --- a/lib/services/notifications_service.dart +++ b/lib/services/notifications_service.dart @@ -146,6 +146,7 @@ class NotificationsService extends ChangeNotifier { node: eNode, failovers: failovers, prefs: prefs, + coin: coin, ); final tx = await client.getTransaction(txHash: txid); diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 61fc9345b..bcd0d6bfc 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -4,6 +4,8 @@ import 'dart:math'; import 'package:bip47/src/util.dart'; import 'package:bitcoindart/bitcoindart.dart' as bitcoindart; import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; +import 'package:electrum_adapter/electrum_adapter.dart' as electrum_adapter; +import 'package:electrum_adapter/electrum_adapter.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart'; import 'package:stackwallet/electrumx_rpc/electrumx_chain_height_service.dart'; @@ -15,22 +17,26 @@ import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2 import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/models/signing_data.dart'; +import 'package:stackwallet/services/tor_service.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/paynym_is_api.dart'; +import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/firo.dart'; import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_hd_currency.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/impl/bitcoin_wallet.dart'; import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; -import 'package:uuid/uuid.dart'; +import 'package:stream_channel/stream_channel.dart'; mixin ElectrumXInterface on Bip39HDWallet { late ElectrumXClient electrumXClient; + late StreamChannel electrumAdapterChannel; + late ElectrumClient electrumAdapterClient; late CachedElectrumXClient electrumXCachedClient; late SubscribableElectrumXClient subscribableElectrumXClient; @@ -889,7 +895,7 @@ mixin ElectrumXInterface on Bip39HDWallet { return transactions.length; } - Future> fetchTxCountBatched({ + Future> fetchTxCountBatched({ required Map addresses, }) async { try { @@ -901,7 +907,7 @@ mixin ElectrumXInterface on Bip39HDWallet { } final response = await electrumXClient.getBatchHistory(args: args); - final Map result = {}; + final Map result = {}; for (final entry in response.entries) { result[entry.key] = entry.value.length; } @@ -943,9 +949,40 @@ mixin ElectrumXInterface on Bip39HDWallet { node: newNode, prefs: prefs, failovers: failovers, + coin: cryptoCurrency.coin, ); + electrumAdapterChannel = await electrum_adapter.connect( + newNode.address, + port: newNode.port, + acceptUnverified: true, + useSSL: newNode.useSSL, + proxyInfo: Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, + ); + if (electrumXClient.coin == Coin.firo || + electrumXClient.coin == Coin.firoTestNet) { + electrumAdapterClient = FiroElectrumClient( + electrumAdapterChannel, + newNode.address, + newNode.port, + newNode.useSSL, + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null); + } else { + electrumAdapterClient = ElectrumClient( + electrumAdapterChannel, + newNode.address, + newNode.port, + newNode.useSSL, + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null); + } electrumXCachedClient = CachedElectrumXClient.from( electrumXClient: electrumXClient, + electrumAdapterClient: electrumAdapterClient, ); subscribableElectrumXClient = SubscribableElectrumXClient.from( node: newNode, @@ -1115,21 +1152,22 @@ mixin ElectrumXInterface on Bip39HDWallet { List> allTxHashes = []; if (serverCanBatch) { - final Map>> batches = {}; - final Map requestIdToAddressMap = {}; + final Map>> batches = {}; + final Map requestIdToAddressMap = {}; const batchSizeMax = 100; int batchNumber = 0; for (int i = 0; i < allAddresses.length; i++) { - if (batches[batchNumber] == null) { - batches[batchNumber] = {}; + if (batches["$batchNumber"] == null) { + batches["$batchNumber"] = {}; } final scriptHash = cryptoCurrency.addressToScriptHash( address: allAddresses.elementAt(i), ); - final id = Logger.isTestEnv ? "$i" : const Uuid().v1(); - requestIdToAddressMap[id] = allAddresses.elementAt(i); - batches[batchNumber]!.addAll({ - id: [scriptHash] + // final id = Logger.isTestEnv ? "$i" : const Uuid().v1(); + // TODO [prio=???]: Pass request IDs to electrum_adapter. + requestIdToAddressMap[i] = allAddresses.elementAt(i); + batches["$batchNumber"]!.addAll({ + "$i": [scriptHash] }); if (i % batchSizeMax == batchSizeMax - 1) { batchNumber++; @@ -1138,7 +1176,7 @@ mixin ElectrumXInterface on Bip39HDWallet { for (int i = 0; i < batches.length; i++) { final response = - await electrumXClient.getBatchHistory(args: batches[i]!); + await electrumXClient.getBatchHistory(args: batches["$i"]!); for (final entry in response.entries) { for (int j = 0; j < entry.value.length; j++) { entry.value[j]["address"] = requestIdToAddressMap[entry.key]; diff --git a/lib/widgets/node_card.dart b/lib/widgets/node_card.dart index 7576999de..bc7233065 100644 --- a/lib/widgets/node_card.dart +++ b/lib/widgets/node_card.dart @@ -175,6 +175,7 @@ class _NodeCardState extends ConsumerState { useSSL: node.useSSL, failovers: [], prefs: ref.read(prefsChangeNotifierProvider), + coin: widget.coin, ); try { diff --git a/lib/widgets/node_options_sheet.dart b/lib/widgets/node_options_sheet.dart index c14b6a2ec..0a2e5ac4c 100644 --- a/lib/widgets/node_options_sheet.dart +++ b/lib/widgets/node_options_sheet.dart @@ -158,6 +158,7 @@ class NodeOptionsSheet extends ConsumerWidget { failovers: [], prefs: ref.read(prefsChangeNotifierProvider), torService: ref.read(pTorService), + coin: coin, ); try { diff --git a/pubspec.lock b/pubspec.lock index 35624bfec..c0e5b16c1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -528,8 +528,8 @@ packages: dependency: "direct main" description: path: "." - ref: dd83940d73429d917f9e50b3a765adbf5e06df6d - resolved-ref: dd83940d73429d917f9e50b3a765adbf5e06df6d + ref: "51b7a60e07b0409b361e31da65d98178ee235bed" + resolved-ref: "51b7a60e07b0409b361e31da65d98178ee235bed" url: "https://github.com/cypherstack/electrum_adapter.git" source: git version: "3.0.0" @@ -1603,7 +1603,7 @@ packages: source: hosted version: "1.5.3" stream_channel: - dependency: transitive + dependency: "direct main" description: name: stream_channel sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 diff --git a/pubspec.yaml b/pubspec.yaml index ce3ac48f4..0ec9d9fa7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -176,7 +176,8 @@ dependencies: electrum_adapter: git: url: https://github.com/cypherstack/electrum_adapter.git - ref: dd83940d73429d917f9e50b3a765adbf5e06df6d + ref: 51b7a60e07b0409b361e31da65d98178ee235bed + stream_channel: ^2.1.0 dev_dependencies: flutter_test: From cbcac9bccee776f22e50c196f82ce63d2134d14a Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 14 Feb 2024 20:04:26 -0600 Subject: [PATCH 2/2] make coin optional --- lib/electrumx_rpc/electrumx_client.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/electrumx_rpc/electrumx_client.dart b/lib/electrumx_rpc/electrumx_client.dart index 7b9473a7b..ef9586a30 100644 --- a/lib/electrumx_rpc/electrumx_client.dart +++ b/lib/electrumx_rpc/electrumx_client.dart @@ -90,8 +90,8 @@ class ElectrumXClient { final Duration connectionTimeoutForSpecialCaseJsonRPCClients; - Coin get coin => _coin; - late Coin _coin; + Coin? get coin => _coin; + late Coin? _coin; // add finalizer to cancel stream subscription when all references to an // instance of ElectrumX becomes inaccessible @@ -113,7 +113,7 @@ class ElectrumXClient { required bool useSSL, required Prefs prefs, required List failovers, - required Coin coin, + Coin? coin, JsonRPC? client, this.connectionTimeoutForSpecialCaseJsonRPCClients = const Duration(seconds: 60),