Merge pull request #760 from cypherstack/electrum_adapter

Use electrum_adapter package for Electrum calls
This commit is contained in:
Diego Salazar 2024-02-16 16:38:57 -07:00 committed by GitHub
commit c75b819157
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1388 additions and 1228 deletions

View file

@ -85,6 +85,7 @@ class DbVersionMigrator with WalletDB {
useSSL: node.useSSL), useSSL: node.useSSL),
prefs: prefs, prefs: prefs,
failovers: failovers, failovers: failovers,
coin: Coin.firo,
); );
try { try {

View file

@ -11,6 +11,9 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math'; 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/db/hive/db.dart';
import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; import 'package:stackwallet/electrumx_rpc/electrumx_client.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart';
@ -19,20 +22,41 @@ import 'package:string_validator/string_validator.dart';
class CachedElectrumXClient { class CachedElectrumXClient {
final ElectrumXClient electrumXClient; final ElectrumXClient electrumXClient;
ElectrumClient electrumAdapterClient;
final Future<ElectrumClient> Function() electrumAdapterUpdateCallback;
static const minCacheConfirms = 30; static const minCacheConfirms = 30;
const CachedElectrumXClient({ CachedElectrumXClient({
required this.electrumXClient, required this.electrumXClient,
required this.electrumAdapterClient,
required this.electrumAdapterUpdateCallback,
}); });
factory CachedElectrumXClient.from({ factory CachedElectrumXClient.from({
required ElectrumXClient electrumXClient, required ElectrumXClient electrumXClient,
required ElectrumClient electrumAdapterClient,
required Future<ElectrumClient> Function() electrumAdapterUpdateCallback,
}) => }) =>
CachedElectrumXClient( CachedElectrumXClient(
electrumXClient: electrumXClient, electrumXClient: electrumXClient,
electrumAdapterClient: electrumAdapterClient,
electrumAdapterUpdateCallback: electrumAdapterUpdateCallback,
); );
/// If the client is closed, use the callback to update it.
_checkElectrumAdapterClient() async {
if (electrumAdapterClient.peer.isClosed) {
Logging.instance.log(
"ElectrumAdapterClient is closed, reopening it...",
level: LogLevel.Info,
);
ElectrumClient? _electrumAdapterClient =
await electrumAdapterUpdateCallback.call();
electrumAdapterClient = _electrumAdapterClient;
}
}
Future<Map<String, dynamic>> getAnonymitySet({ Future<Map<String, dynamic>> getAnonymitySet({
required String groupId, required String groupId,
String blockhash = "", String blockhash = "",
@ -56,9 +80,12 @@ class CachedElectrumXClient {
set = Map<String, dynamic>.from(cachedSet); set = Map<String, dynamic>.from(cachedSet);
} }
final newSet = await electrumXClient.getLelantusAnonymitySet( await _checkElectrumAdapterClient();
final newSet = await (electrumAdapterClient as FiroElectrumClient)
.getLelantusAnonymitySet(
groupId: groupId, groupId: groupId,
blockhash: set["blockHash"] as String, blockHash: set["blockHash"] as String,
); );
// update set with new data // update set with new data
@ -82,7 +109,7 @@ class CachedElectrumXClient {
translatedCoin.add(!isHexadecimal(newCoin[2] as String) translatedCoin.add(!isHexadecimal(newCoin[2] as String)
? base64ToHex(newCoin[2] as String) ? base64ToHex(newCoin[2] as String)
: newCoin[2]); : newCoin[2]);
} catch (e, s) { } catch (e) {
translatedCoin.add(newCoin[2]); translatedCoin.add(newCoin[2]);
} }
translatedCoin.add(!isHexadecimal(newCoin[3] as String) translatedCoin.add(!isHexadecimal(newCoin[3] as String)
@ -130,7 +157,10 @@ class CachedElectrumXClient {
set = Map<String, dynamic>.from(cachedSet); set = Map<String, dynamic>.from(cachedSet);
} }
final newSet = await electrumXClient.getSparkAnonymitySet( await _checkElectrumAdapterClient();
final newSet = await (electrumAdapterClient as FiroElectrumClient)
.getSparkAnonymitySet(
coinGroupId: groupId, coinGroupId: groupId,
startBlockHash: set["blockHash"] as String, startBlockHash: set["blockHash"] as String,
); );
@ -188,8 +218,10 @@ class CachedElectrumXClient {
final cachedTx = box.get(txHash) as Map?; final cachedTx = box.get(txHash) as Map?;
if (cachedTx == null) { if (cachedTx == null) {
final Map<String, dynamic> result = await electrumXClient await _checkElectrumAdapterClient();
.getTransaction(txHash: txHash, verbose: verbose);
final Map<String, dynamic> result =
await electrumAdapterClient.getTransaction(txHash);
result.remove("hex"); result.remove("hex");
result.remove("lelantusData"); result.remove("lelantusData");
@ -231,7 +263,10 @@ class CachedElectrumXClient {
cachedSerials.length - 100, // 100 being some arbitrary buffer cachedSerials.length - 100, // 100 being some arbitrary buffer
); );
final serials = await electrumXClient.getLelantusUsedCoinSerials( await _checkElectrumAdapterClient();
final serials = await (electrumAdapterClient as FiroElectrumClient)
.getLelantusUsedCoinSerials(
startNumber: startNumber, startNumber: startNumber,
); );
@ -279,7 +314,10 @@ class CachedElectrumXClient {
cachedTags.length - 100, // 100 being some arbitrary buffer cachedTags.length - 100, // 100 being some arbitrary buffer
); );
final tags = await electrumXClient.getSparkUsedCoinsTags( await _checkElectrumAdapterClient();
final tags =
await (electrumAdapterClient as FiroElectrumClient).getUsedCoinsTags(
startNumber: startNumber, startNumber: startNumber,
); );
@ -287,12 +325,18 @@ class CachedElectrumXClient {
// .map((e) => !isHexadecimal(e) ? base64ToHex(e) : e) // .map((e) => !isHexadecimal(e) ? base64ToHex(e) : e)
// .toSet(); // .toSet();
// Convert the Map<String, dynamic> tags to a Set<Object?>.
final newTags = (tags["tags"] as List).toSet();
// ensure we are getting some overlap so we know we are not missing any // ensure we are getting some overlap so we know we are not missing any
if (cachedTags.isNotEmpty && tags.isNotEmpty) { if (cachedTags.isNotEmpty && tags.isNotEmpty) {
assert(cachedTags.intersection(tags).isNotEmpty); assert(cachedTags.intersection(newTags).isNotEmpty);
} }
cachedTags.addAll(tags); // Make newTags an Iterable<String>.
final Iterable<String> iterableTags = newTags.map((e) => e.toString());
cachedTags.addAll(iterableTags);
await box.put( await box.put(
"tags", "tags",

View file

@ -9,11 +9,13 @@
*/ */
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:decimal/decimal.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:event_bus/event_bus.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart';
@ -24,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/events/global/tor_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/tor_service.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/logger.dart';
import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/prefs.dart';
import 'package:uuid/uuid.dart'; import 'package:stream_channel/stream_channel.dart';
class WifiOnlyException implements Exception {} class WifiOnlyException implements Exception {}
@ -73,6 +76,12 @@ class ElectrumXClient {
JsonRPC? get rpcClient => _rpcClient; JsonRPC? get rpcClient => _rpcClient;
JsonRPC? _rpcClient; JsonRPC? _rpcClient;
StreamChannel<dynamic>? get electrumAdapterChannel => _electrumAdapterChannel;
StreamChannel<dynamic>? _electrumAdapterChannel;
ElectrumClient? get electrumAdapterClient => _electrumAdapterClient;
ElectrumClient? _electrumAdapterClient;
late Prefs _prefs; late Prefs _prefs;
late TorService _torService; late TorService _torService;
@ -81,6 +90,9 @@ class ElectrumXClient {
final Duration connectionTimeoutForSpecialCaseJsonRPCClients; final Duration connectionTimeoutForSpecialCaseJsonRPCClients;
Coin? get coin => _coin;
late Coin? _coin;
// add finalizer to cancel stream subscription when all references to an // add finalizer to cancel stream subscription when all references to an
// instance of ElectrumX becomes inaccessible // instance of ElectrumX becomes inaccessible
static final Finalizer<ElectrumXClient> _finalizer = Finalizer( static final Finalizer<ElectrumXClient> _finalizer = Finalizer(
@ -101,7 +113,7 @@ class ElectrumXClient {
required bool useSSL, required bool useSSL,
required Prefs prefs, required Prefs prefs,
required List<ElectrumXNode> failovers, required List<ElectrumXNode> failovers,
JsonRPC? client, Coin? coin,
this.connectionTimeoutForSpecialCaseJsonRPCClients = this.connectionTimeoutForSpecialCaseJsonRPCClients =
const Duration(seconds: 60), const Duration(seconds: 60),
TorService? torService, TorService? torService,
@ -112,7 +124,7 @@ class ElectrumXClient {
_host = host; _host = host;
_port = port; _port = port;
_useSSL = useSSL; _useSSL = useSSL;
_rpcClient = client; _coin = coin;
final bus = globalEventBusForTesting ?? GlobalEventBus.instance; final bus = globalEventBusForTesting ?? GlobalEventBus.instance;
_torStatusListener = bus.on<TorConnectionStatusChangedEvent>().listen( _torStatusListener = bus.on<TorConnectionStatusChangedEvent>().listen(
@ -141,21 +153,10 @@ class ElectrumXClient {
// case TorStatus.disabled: // case TorStatus.disabled:
// } // }
// might be ok to just reset/kill the current _jsonRpcClient
// since disconnecting is async and we want to ensure instant change over
// we will keep temp reference to current rpc client to call disconnect
// on before awaiting the disconnection future
final temp = _rpcClient;
// setting to null should force the creation of a new json rpc client // setting to null should force the creation of a new json rpc client
// on the next request sent through this electrumx instance // 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}\"",
);
}, },
); );
} }
@ -164,6 +165,7 @@ class ElectrumXClient {
required ElectrumXNode node, required ElectrumXNode node,
required Prefs prefs, required Prefs prefs,
required List<ElectrumXNode> failovers, required List<ElectrumXNode> failovers,
required Coin coin,
TorService? torService, TorService? torService,
EventBus? globalEventBusForTesting, EventBus? globalEventBusForTesting,
}) { }) {
@ -175,6 +177,7 @@ class ElectrumXClient {
torService: torService, torService: torService,
failovers: failovers, failovers: failovers,
globalEventBusForTesting: globalEventBusForTesting, globalEventBusForTesting: globalEventBusForTesting,
coin: coin,
); );
} }
@ -186,7 +189,9 @@ class ElectrumXClient {
return true; return true;
} }
void _checkRpcClient() { Future<void> checkElectrumAdapter() async {
({InternetAddress host, int port})? proxyInfo;
// If we're supposed to use Tor... // If we're supposed to use Tor...
if (_prefs.useTor) { if (_prefs.useTor) {
// But Tor isn't running... // But Tor isn't running...
@ -195,65 +200,93 @@ class ElectrumXClient {
if (!_prefs.torKillSwitch) { if (!_prefs.torKillSwitch) {
// Then we'll just proceed and connect to ElectrumX through clearnet at the bottom of this function. // Then we'll just proceed and connect to ElectrumX through clearnet at the bottom of this function.
Logging.instance.log( Logging.instance.log(
"Tor preference set but Tor is not enabled, killswitch not set, connecting to ElectrumX through clearnet", "Tor preference set but Tor is not enabled, killswitch not set, connecting to Electrum adapter through clearnet",
level: LogLevel.Warning, level: LogLevel.Warning,
); );
} else { } else {
// ... But if the killswitch is set, then we throw an exception. // ... But if the killswitch is set, then we throw an exception.
throw Exception( throw Exception(
"Tor preference and killswitch set but Tor is not enabled, not connecting to ElectrumX"); "Tor preference and killswitch set but Tor is not enabled, not connecting to Electrum adapter");
// TODO [prio=low]: Try to start Tor. // TODO [prio=low]: Try to start Tor.
} }
} else { } else {
// Get the proxy info from the TorService. // Get the proxy info from the TorService.
final proxyInfo = _torService.getProxyInfo(); proxyInfo = _torService.getProxyInfo();
if (currentFailoverIndex == -1) {
_rpcClient ??= JsonRPC(
host: host,
port: port,
useSSL: useSSL,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
proxyInfo: proxyInfo,
);
} else {
_rpcClient ??= JsonRPC(
host: failovers![currentFailoverIndex].address,
port: failovers![currentFailoverIndex].port,
useSSL: failovers![currentFailoverIndex].useSSL,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
proxyInfo: proxyInfo,
);
}
if (_rpcClient!.proxyInfo != proxyInfo) {
_rpcClient!.proxyInfo = proxyInfo;
_rpcClient!.disconnect(
reason: "Tor proxyInfo does not match current info",
);
}
return;
} }
} }
if (currentFailoverIndex == -1) { // TODO [prio=med]: Add proxyInfo to StreamChannel (or add to wrapper).
_rpcClient ??= JsonRPC( // if (_electrumAdapter!.proxyInfo != proxyInfo) {
host: host, // _electrumAdapter!.proxyInfo = proxyInfo;
port: port, // _electrumAdapter!.disconnect(
useSSL: useSSL, // reason: "Tor proxyInfo does not match current info",
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, // );
proxyInfo: null, // }
);
} else { // If the current ElectrumAdapterClient is closed, create a new one.
_rpcClient ??= JsonRPC( if (_electrumAdapterClient != null &&
host: failovers![currentFailoverIndex].address, _electrumAdapterClient!.peer.isClosed) {
port: failovers![currentFailoverIndex].port, _electrumAdapterChannel = null;
useSSL: failovers![currentFailoverIndex].useSSL, _electrumAdapterClient = null;
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
proxyInfo: null,
);
} }
if (currentFailoverIndex == -1) {
_electrumAdapterChannel ??= await electrum_adapter.connect(
host,
port: port,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
aliveTimerDuration: connectionTimeoutForSpecialCaseJsonRPCClients,
acceptUnverified: true,
useSSL: useSSL,
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 {
_electrumAdapterChannel ??= await electrum_adapter.connect(
failovers![currentFailoverIndex].address,
port: failovers![currentFailoverIndex].port,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
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 /// Send raw rpc command
@ -269,32 +302,22 @@ class ElectrumXClient {
} }
if (_requireMutex) { if (_requireMutex) {
await _torConnectingLock.protect(() async => _checkRpcClient()); await _torConnectingLock
.protect(() async => await checkElectrumAdapter());
} else { } else {
_checkRpcClient(); await checkElectrumAdapter();
} }
try { try {
final requestId = requestID ?? const Uuid().v1(); final response = await _electrumAdapterClient!.request(
final jsonArgs = json.encode(args); command,
final jsonRequestString = '{"jsonrpc": "2.0", ' args,
'"id": "$requestId",'
'"method": "$command",'
'"params": $jsonArgs}';
// Logging.instance.log("ElectrumX jsonRequestString: $jsonRequestString");
final response = await _rpcClient!.request(
jsonRequestString,
requestTimeout,
); );
if (response.exception != null) { if (response is Map &&
throw response.exception!; response.keys.contains("error") &&
} response["error"] != null) {
if (response["error"]
if (response.data is Map && response.data["error"] != null) {
if (response.data["error"]
.toString() .toString()
.contains("No such mempool or blockchain transaction")) { .contains("No such mempool or blockchain transaction")) {
throw NoSuchTransactionException( throw NoSuchTransactionException(
@ -306,13 +329,19 @@ class ElectrumXClient {
throw Exception( throw Exception(
"JSONRPC response\n" "JSONRPC response\n"
" command: $command\n" " command: $command\n"
" error: ${response.data}" " error: ${response["error"]}\n"
" args: $args\n", " args: $args\n",
); );
} }
currentFailoverIndex = -1; currentFailoverIndex = -1;
return response.data;
// If the command is a ping, a good return should always be null.
if (command.contains("ping")) {
return true;
}
return response;
} on WifiOnlyException { } on WifiOnlyException {
rethrow; rethrow;
} on SocketException { } on SocketException {
@ -348,7 +377,7 @@ class ElectrumXClient {
/// map of <request id string : arguments list> /// map of <request id string : arguments list>
/// ///
/// returns a list of json response objects if no errors were found /// returns a list of json response objects if no errors were found
Future<List<Map<String, dynamic>>> batchRequest({ Future<List<dynamic>> batchRequest({
required String command, required String command,
required Map<String, List<dynamic>> args, required Map<String, List<dynamic>> args,
Duration requestTimeout = const Duration(seconds: 60), Duration requestTimeout = const Duration(seconds: 60),
@ -359,62 +388,34 @@ class ElectrumXClient {
} }
if (_requireMutex) { if (_requireMutex) {
await _torConnectingLock.protect(() async => _checkRpcClient()); await _torConnectingLock
.protect(() async => await checkElectrumAdapter());
} else { } else {
_checkRpcClient(); await checkElectrumAdapter();
} }
try { try {
final List<String> requestStrings = []; var futures = <Future<dynamic>>[];
List? response;
for (final entry in args.entries) { _electrumAdapterClient!.peer.withBatch(() {
final jsonArgs = json.encode(entry.value); for (final entry in args.entries) {
requestStrings.add( futures.add(_electrumAdapterClient!.request(command, entry.value));
'{"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<dynamic> 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;
} }
} catch (_) { });
throw Exception( response = await Future.wait(futures);
"Expected json list or map but got a ${jsonRpcResponse.data.runtimeType}: ${jsonRpcResponse.data}",
);
}
// check for errors, format and throw if there are any // check for errors, format and throw if there are any
final List<String> errors = []; final List<String> errors = [];
for (int i = 0; i < response.length; i++) { for (int i = 0; i < response.length; i++) {
final result = response[i]; var result = response[i];
if (result["error"] != null || result["result"] == null) {
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()); errors.add(result.toString());
} }
} }
@ -428,7 +429,7 @@ class ElectrumXClient {
} }
currentFailoverIndex = -1; currentFailoverIndex = -1;
return List<Map<String, dynamic>>.from(response, growable: false); return response;
} on WifiOnlyException { } on WifiOnlyException {
rethrow; rethrow;
} on SocketException { } on SocketException {
@ -463,13 +464,23 @@ class ElectrumXClient {
/// Returns true if ping succeeded /// Returns true if ping succeeded
Future<bool> ping({String? requestID, int retryCount = 1}) async { Future<bool> ping({String? requestID, int retryCount = 1}) async {
try { try {
final response = await request( // This doesn't work because electrum_adapter only returns the result:
// (which is always `null`).
// await checkElectrumAdapter();
// final response = await electrumAdapterClient!
// .ping()
// .timeout(const Duration(seconds: 2));
// return (response as Map<String, dynamic>).isNotEmpty;
// Because request() has been updated to use electrum_adapter, and because
// electrum_adapter returns the result of the request, request() has been
// updated to return a bool on a server.ping command as a special case.
return await request(
requestID: requestID, requestID: requestID,
command: 'server.ping', command: 'server.ping',
requestTimeout: const Duration(seconds: 2), requestTimeout: const Duration(seconds: 2),
retries: retryCount, retries: retryCount,
).timeout(const Duration(seconds: 2)) as Map<String, dynamic>; ).timeout(const Duration(seconds: 2)) as bool;
return response.keys.contains("result") && response["result"] == null;
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
@ -490,14 +501,14 @@ class ElectrumXClient {
requestID: requestID, requestID: requestID,
command: 'blockchain.headers.subscribe', command: 'blockchain.headers.subscribe',
); );
if (response["result"] == null) { if (response == null) {
Logging.instance.log( Logging.instance.log(
"getBlockHeadTip returned null response", "getBlockHeadTip returned null response",
level: LogLevel.Error, level: LogLevel.Error,
); );
throw 'getBlockHeadTip returned null response'; throw 'getBlockHeadTip returned null response';
} }
return Map<String, dynamic>.from(response["result"] as Map); return Map<String, dynamic>.from(response as Map);
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
@ -522,7 +533,7 @@ class ElectrumXClient {
requestID: requestID, requestID: requestID,
command: 'server.features', command: 'server.features',
); );
return Map<String, dynamic>.from(response["result"] as Map); return Map<String, dynamic>.from(response as Map);
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
@ -543,7 +554,7 @@ class ElectrumXClient {
rawTx, rawTx,
], ],
); );
return response["result"] as String; return response as String;
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
@ -570,7 +581,7 @@ class ElectrumXClient {
scripthash, scripthash,
], ],
); );
return Map<String, dynamic>.from(response["result"] as Map); return Map<String, dynamic>.from(response as Map);
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
@ -607,7 +618,7 @@ class ElectrumXClient {
scripthash, scripthash,
], ],
); );
result = response["result"]; result = response;
retryCount--; retryCount--;
} }
@ -617,17 +628,16 @@ class ElectrumXClient {
} }
} }
Future<Map<String, List<Map<String, dynamic>>>> getBatchHistory( Future<Map<int, List<Map<String, dynamic>>>> getBatchHistory(
{required Map<String, List<dynamic>> args}) async { {required Map<String, List<dynamic>> args}) async {
try { try {
final response = await batchRequest( final response = await batchRequest(
command: 'blockchain.scripthash.get_history', command: 'blockchain.scripthash.get_history',
args: args, args: args,
); );
final Map<String, List<Map<String, dynamic>>> result = {}; final Map<int, List<Map<String, dynamic>>> result = {};
for (int i = 0; i < response.length; i++) { for (int i = 0; i < response.length; i++) {
result[response[i]["id"] as String] = result[i] = List<Map<String, dynamic>>.from(response[i] as List);
List<Map<String, dynamic>>.from(response[i]["result"] as List);
} }
return result; return result;
} catch (e) { } catch (e) {
@ -665,23 +675,33 @@ class ElectrumXClient {
scripthash, scripthash,
], ],
); );
return List<Map<String, dynamic>>.from(response["result"] as List); return List<Map<String, dynamic>>.from(response as List);
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
} }
Future<Map<String, List<Map<String, dynamic>>>> getBatchUTXOs( Future<Map<int, List<Map<String, dynamic>>>> getBatchUTXOs(
{required Map<String, List<dynamic>> args}) async { {required Map<String, List<dynamic>> args}) async {
try { try {
final response = await batchRequest( final response = await batchRequest(
command: 'blockchain.scripthash.listunspent', command: 'blockchain.scripthash.listunspent',
args: args, args: args,
); );
final Map<String, List<Map<String, dynamic>>> result = {}; final Map<int, List<Map<String, dynamic>>> result = {};
for (int i = 0; i < response.length; i++) { for (int i = 0; i < response.length; i++) {
result[response[i]["id"] as String] = if ((response[i] as List).isNotEmpty) {
List<Map<String, dynamic>>.from(response[i]["result"] as List); try {
// result[i] = response[i] as List<Map<String, dynamic>>;
result[i] = List<Map<String, dynamic>>.from(response[i] as List);
} catch (e) {
print(response[i]);
Logging.instance.log(
"getBatchUTXOs failed to parse response",
level: LogLevel.Error,
);
}
}
} }
return result; return result;
} catch (e) { } catch (e) {
@ -741,41 +761,18 @@ class ElectrumXClient {
bool verbose = true, bool verbose = true,
String? requestID, String? requestID,
}) async { }) async {
dynamic response; Logging.instance.log("attempting to fetch blockchain.transaction.get...",
try { level: LogLevel.Info);
response = await request( await checkElectrumAdapter();
requestID: requestID, dynamic response = await _electrumAdapterClient!.getTransaction(txHash);
command: 'blockchain.transaction.get', Logging.instance.log("Fetching blockchain.transaction.get finished",
args: [ level: LogLevel.Info);
txHash,
verbose,
],
);
if (!verbose) {
return {"rawtx": response["result"] as String};
}
if (response is! Map) { if (!verbose) {
final String msg = "getTransaction($txHash) returned a non-Map response" return {"rawtx": response as String};
" of type ${response.runtimeType}.\nResponse: $response";
Logging.instance.log(msg, level: LogLevel.Fatal);
throw Exception(msg);
}
if (response["result"] == null) {
final String msg = "getTransaction($txHash) returned null result."
"\nResponse: $response";
Logging.instance.log(msg, level: LogLevel.Fatal);
throw Exception(msg);
}
return Map<String, dynamic>.from(response["result"] as Map);
} catch (e, s) {
Logging.instance.log(
"getTransaction($txHash) response: $response"
"\nError: $e\nStack trace: $s",
level: LogLevel.Error);
rethrow;
} }
return Map<String, dynamic>.from(response as Map);
} }
/// Returns the whole Lelantus anonymity set for denomination in the groupId. /// Returns the whole Lelantus anonymity set for denomination in the groupId.
@ -797,23 +794,15 @@ class ElectrumXClient {
String blockhash = "", String blockhash = "",
String? requestID, String? requestID,
}) async { }) async {
try { Logging.instance.log("attempting to fetch lelantus.getanonymityset...",
Logging.instance.log("attempting to fetch lelantus.getanonymityset...", level: LogLevel.Info);
level: LogLevel.Info); await checkElectrumAdapter();
final response = await request( Map<String, dynamic> response =
requestID: requestID, await (_electrumAdapterClient as FiroElectrumClient)!
command: 'lelantus.getanonymityset', .getLelantusAnonymitySet(groupId: groupId, blockHash: blockhash);
args: [ Logging.instance.log("Fetching lelantus.getanonymityset finished",
groupId, level: LogLevel.Info);
blockhash, return response;
],
);
Logging.instance.log("Fetching lelantus.getanonymityset finished",
level: LogLevel.Info);
return Map<String, dynamic>.from(response["result"] as Map);
} catch (e) {
rethrow;
}
} }
//TODO add example to docs //TODO add example to docs
@ -824,18 +813,14 @@ class ElectrumXClient {
dynamic mints, dynamic mints,
String? requestID, String? requestID,
}) async { }) async {
try { Logging.instance.log("attempting to fetch lelantus.getmintmetadata...",
final response = await request( level: LogLevel.Info);
requestID: requestID, await checkElectrumAdapter();
command: 'lelantus.getmintmetadata', dynamic response = await (_electrumAdapterClient as FiroElectrumClient)!
args: [ .getLelantusMintData(mints: mints);
mints, Logging.instance.log("Fetching lelantus.getmintmetadata finished",
], level: LogLevel.Info);
); return response;
return response["result"];
} catch (e) {
rethrow;
}
} }
//TODO add example to docs //TODO add example to docs
@ -844,45 +829,38 @@ class ElectrumXClient {
String? requestID, String? requestID,
required int startNumber, required int startNumber,
}) async { }) async {
try { Logging.instance.log("attempting to fetch lelantus.getusedcoinserials...",
int retryCount = 3; level: LogLevel.Info);
dynamic result; await checkElectrumAdapter();
while (retryCount > 0 && result is! List) { int retryCount = 3;
final response = await request( dynamic response;
requestID: requestID,
command: 'lelantus.getusedcoinserials',
args: [
"$startNumber",
],
requestTimeout: const Duration(minutes: 2),
);
result = response["result"]; while (retryCount > 0 && response is! List) {
retryCount--; response = await (_electrumAdapterClient as FiroElectrumClient)!
} .getLelantusUsedCoinSerials(startNumber: startNumber);
// TODO add 2 minute timeout.
Logging.instance.log("Fetching lelantus.getusedcoinserials finished",
level: LogLevel.Info);
return Map<String, dynamic>.from(result as Map); retryCount--;
} catch (e) {
Logging.instance.log(e, level: LogLevel.Error);
rethrow;
} }
return Map<String, dynamic>.from(response as Map);
} }
/// Returns the latest Lelantus set id /// Returns the latest Lelantus set id
/// ///
/// ex: 1 /// ex: 1
Future<int> getLelantusLatestCoinId({String? requestID}) async { Future<int> getLelantusLatestCoinId({String? requestID}) async {
try { Logging.instance.log("attempting to fetch lelantus.getlatestcoinid...",
final response = await request( level: LogLevel.Info);
requestID: requestID, await checkElectrumAdapter();
command: 'lelantus.getlatestcoinid', int response =
); await (_electrumAdapterClient as FiroElectrumClient).getLatestCoinId();
return response["result"] as int; Logging.instance.log("Fetching lelantus.getlatestcoinid finished",
} catch (e) { level: LogLevel.Info);
Logging.instance.log(e, level: LogLevel.Error); return response;
rethrow;
}
} }
// ============== Spark ====================================================== // ============== Spark ======================================================
@ -908,17 +886,14 @@ class ElectrumXClient {
try { try {
Logging.instance.log("attempting to fetch spark.getsparkanonymityset...", Logging.instance.log("attempting to fetch spark.getsparkanonymityset...",
level: LogLevel.Info); level: LogLevel.Info);
final response = await request( await checkElectrumAdapter();
requestID: requestID, Map<String, dynamic> response =
command: 'spark.getsparkanonymityset', await (_electrumAdapterClient as FiroElectrumClient)
args: [ .getSparkAnonymitySet(
coinGroupId, coinGroupId: coinGroupId, startBlockHash: startBlockHash);
startBlockHash,
],
);
Logging.instance.log("Fetching spark.getsparkanonymityset finished", Logging.instance.log("Fetching spark.getsparkanonymityset finished",
level: LogLevel.Info); level: LogLevel.Info);
return Map<String, dynamic>.from(response["result"] as Map); return response;
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
@ -931,15 +906,17 @@ class ElectrumXClient {
required int startNumber, required int startNumber,
}) async { }) async {
try { try {
final response = await request( // Use electrum_adapter package's getSparkUsedCoinsTags method.
requestID: requestID, Logging.instance.log("attempting to fetch spark.getusedcoinstags...",
command: 'spark.getusedcoinstags', level: LogLevel.Info);
args: [ await checkElectrumAdapter();
"$startNumber", Map<String, dynamic> response =
], await (_electrumAdapterClient as FiroElectrumClient)
requestTimeout: const Duration(minutes: 2), .getUsedCoinsTags(startNumber: startNumber);
); // TODO: Add 2 minute timeout.
final map = Map<String, dynamic>.from(response["result"] as Map); Logging.instance.log("Fetching spark.getusedcoinstags finished",
level: LogLevel.Info);
final map = Map<String, dynamic>.from(response);
final set = Set<String>.from(map["tags"] as List); final set = Set<String>.from(map["tags"] as List);
return await compute(_ffiHashTagsComputeWrapper, set); return await compute(_ffiHashTagsComputeWrapper, set);
} catch (e) { } catch (e) {
@ -963,16 +940,15 @@ class ElectrumXClient {
required List<String> sparkCoinHashes, required List<String> sparkCoinHashes,
}) async { }) async {
try { try {
final response = await request( Logging.instance.log("attempting to fetch spark.getsparkmintmetadata...",
requestID: requestID, level: LogLevel.Info);
command: 'spark.getsparkmintmetadata', await checkElectrumAdapter();
args: [ List<dynamic> response =
{ await (_electrumAdapterClient as FiroElectrumClient)
"coinHashes": sparkCoinHashes, .getSparkMintMetaData(sparkCoinHashes: sparkCoinHashes);
}, Logging.instance.log("Fetching spark.getsparkmintmetadata finished",
], level: LogLevel.Info);
); return List<Map<String, dynamic>>.from(response);
return List<Map<String, dynamic>>.from(response["result"] as List);
} catch (e) { } catch (e) {
Logging.instance.log(e, level: LogLevel.Error); Logging.instance.log(e, level: LogLevel.Error);
rethrow; rethrow;
@ -986,11 +962,14 @@ class ElectrumXClient {
String? requestID, String? requestID,
}) async { }) async {
try { try {
final response = await request( Logging.instance.log("attempting to fetch spark.getsparklatestcoinid...",
requestID: requestID, level: LogLevel.Info);
command: 'spark.getsparklatestcoinid', await checkElectrumAdapter();
); int response = await (_electrumAdapterClient as FiroElectrumClient)
return response["result"] as int; .getSparkLatestCoinId();
Logging.instance.log("Fetching spark.getsparklatestcoinid finished",
level: LogLevel.Info);
return response;
} catch (e) { } catch (e) {
Logging.instance.log(e, level: LogLevel.Error); Logging.instance.log(e, level: LogLevel.Error);
rethrow; rethrow;
@ -1007,15 +986,8 @@ class ElectrumXClient {
/// "rate": 1000, /// "rate": 1000,
/// } /// }
Future<Map<String, dynamic>> getFeeRate({String? requestID}) async { Future<Map<String, dynamic>> getFeeRate({String? requestID}) async {
try { await checkElectrumAdapter();
final response = await request( return await _electrumAdapterClient!.getFeeRate();
requestID: requestID,
command: 'blockchain.getfeerate',
);
return Map<String, dynamic>.from(response["result"] as Map);
} catch (e) {
rethrow;
}
} }
/// Return the estimated transaction fee per kilobyte for a transaction to be confirmed within a certain number of [blocks]. /// Return the estimated transaction fee per kilobyte for a transaction to be confirmed within a certain number of [blocks].
@ -1033,10 +1005,10 @@ class ElectrumXClient {
], ],
); );
try { try {
return Decimal.parse(response["result"].toString()); return Decimal.parse(response.toString());
} catch (e, s) { } catch (e, s) {
final String msg = "Error parsing fee rate. Response: $response" 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); Logging.instance.log(msg, level: LogLevel.Fatal);
throw Exception(msg); throw Exception(msg);
} }
@ -1056,7 +1028,7 @@ class ElectrumXClient {
requestID: requestID, requestID: requestID,
command: 'blockchain.relayfee', command: 'blockchain.relayfee',
); );
return Decimal.parse(response["result"].toString()); return Decimal.parse(response.toString());
} catch (e) { } catch (e) {
rethrow; rethrow;
} }

File diff suppressed because it is too large Load diff

View file

@ -177,6 +177,7 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
useSSL: formData.useSSL!, useSSL: formData.useSSL!,
failovers: [], failovers: [],
prefs: ref.read(prefsChangeNotifierProvider), prefs: ref.read(prefsChangeNotifierProvider),
coin: coin,
); );
try { try {

View file

@ -154,6 +154,7 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
useSSL: node.useSSL, useSSL: node.useSSL,
failovers: [], failovers: [],
prefs: ref.read(prefsChangeNotifierProvider), prefs: ref.read(prefsChangeNotifierProvider),
coin: coin,
); );
try { try {

View file

@ -146,6 +146,7 @@ class NotificationsService extends ChangeNotifier {
node: eNode, node: eNode,
failovers: failovers, failovers: failovers,
prefs: prefs, prefs: prefs,
coin: coin,
); );
final tx = await client.getTransaction(txHash: txid); final tx = await client.getTransaction(txHash: txid);

View file

@ -4,40 +4,59 @@ import 'dart:math';
import 'package:bip47/src/util.dart'; import 'package:bip47/src/util.dart';
import 'package:bitcoindart/bitcoindart.dart' as bitcoindart; import 'package:bitcoindart/bitcoindart.dart' as bitcoindart;
import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; 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:isar/isar.dart';
import 'package:mutex/mutex.dart';
import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart';
import 'package:stackwallet/electrumx_rpc/electrumx_chain_height_service.dart'; import 'package:stackwallet/electrumx_rpc/electrumx_chain_height_service.dart';
import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; import 'package:stackwallet/electrumx_rpc/electrumx_client.dart';
import 'package:stackwallet/electrumx_rpc/subscribable_electrumx_client.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/models/signing_data.dart'; import 'package:stackwallet/models/signing_data.dart';
import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/tor_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/derive_path_type_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/enums/fee_rate_type_enum.dart';
import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/paynym_is_api.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/coins/firo.dart';
import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_hd_currency.dart'; import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_hd_currency.dart';
import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/models/tx_data.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_wallet.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/intermediate/bip39_hd_wallet.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/paynym_interface.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<T extends Bip39HDCurrency> on Bip39HDWallet<T> { mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
late ElectrumXClient electrumXClient; late ElectrumXClient electrumXClient;
late StreamChannel electrumAdapterChannel;
late ElectrumClient electrumAdapterClient;
late CachedElectrumXClient electrumXCachedClient; late CachedElectrumXClient electrumXCachedClient;
late SubscribableElectrumXClient subscribableElectrumXClient; // late SubscribableElectrumXClient subscribableElectrumXClient;
int? get maximumFeerate => null; int? get maximumFeerate => null;
int? _latestHeight; int? _latestHeight;
late Prefs _prefs;
late TorService _torService;
StreamSubscription<TorPreferenceChangedEvent>? _torPreferenceListener;
StreamSubscription<TorConnectionStatusChangedEvent>? _torStatusListener;
final Mutex _torConnectingLock = Mutex();
bool _requireMutex = false;
Timer? _aliveTimer;
static const Duration _keepAlive = Duration(minutes: 1);
bool _isConnected = false;
static const _kServerBatchCutoffVersion = [1, 6]; static const _kServerBatchCutoffVersion = [1, 6];
List<int>? _serverVersion; List<int>? _serverVersion;
bool get serverCanBatch { bool get serverCanBatch {
@ -812,38 +831,94 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
// Make sure we only complete once. // Make sure we only complete once.
final isFirstResponse = _latestHeight == null; final isFirstResponse = _latestHeight == null;
// Subscribe to block headers. // Check Electrum and update internal and cached versions if necessary.
final subscription = await electrumXClient.checkElectrumAdapter();
subscribableElectrumXClient.subscribeToBlockHeaders(); if (electrumAdapterChannel != electrumXClient.electrumAdapterChannel &&
electrumXClient.electrumAdapterChannel != null) {
electrumAdapterChannel = electrumXClient.electrumAdapterChannel!;
}
if (electrumAdapterClient != electrumXClient.electrumAdapterClient &&
electrumXClient.electrumAdapterClient != null) {
electrumAdapterClient = electrumXClient.electrumAdapterClient!;
}
// electrumXCachedClient.electrumAdapterChannel = electrumAdapterChannel;
if (electrumXCachedClient.electrumAdapterClient !=
electrumAdapterClient) {
electrumXCachedClient.electrumAdapterClient = electrumAdapterClient;
}
// set stream subscription // Subscribe to and listen for new block headers.
final stream = electrumAdapterClient.subscribeHeaders();
ElectrumxChainHeightService.subscriptions[cryptoCurrency.coin] = ElectrumxChainHeightService.subscriptions[cryptoCurrency.coin] =
subscription.responseStream.asBroadcastStream().listen((event) { stream.asBroadcastStream().listen((response) {
final response = event; final int chainHeight = response.height;
if (response != null && // print("Current chain height: $chainHeight");
response is Map &&
response.containsKey('height')) {
final int chainHeight = response['height'] as int;
// print("Current chain height: $chainHeight");
_latestHeight = chainHeight; _latestHeight = chainHeight;
if (isFirstResponse && !completer.isCompleted) { if (isFirstResponse && !completer.isCompleted) {
// Return the chain height. // Return the chain height.
completer.complete(chainHeight); completer.complete(chainHeight);
}
} else {
Logging.instance.log(
"blockchain.headers.subscribe returned malformed response\n"
"Response: $response",
level: LogLevel.Error);
} }
}); });
return _latestHeight ?? await completer.future; // If we're testing, use the global event bus for testing.
} // final bus = globalEventBusForTesting ?? GlobalEventBus.instance;
// Don't set a stream subscription if one already exists. // No constructors for mixins, so no globalEventBusForTesting is passed in.
else { final bus = GlobalEventBus.instance;
// Listen to global event bus for Tor status changes.
_torStatusListener ??= bus.on<TorConnectionStatusChangedEvent>().listen(
(event) async {
try {
switch (event.newStatus) {
case TorConnectionStatus.connecting:
// If Tor is connecting, we need to wait.
await _torConnectingLock.acquire();
_requireMutex = true;
break;
case TorConnectionStatus.connected:
case TorConnectionStatus.disconnected:
// If Tor is connected or disconnected, we can release the lock.
if (_torConnectingLock.isLocked) {
_torConnectingLock.release();
}
_requireMutex = false;
break;
}
} finally {
// Ensure the lock is released.
if (_torConnectingLock.isLocked) {
_torConnectingLock.release();
}
}
},
);
// Listen to global event bus for Tor preference changes.
_torPreferenceListener ??= bus.on<TorPreferenceChangedEvent>().listen(
(event) async {
// Close any open subscriptions.
for (final coinSub
in ElectrumxChainHeightService.subscriptions.entries) {
await coinSub.value?.cancel();
}
// Cancel alive timer
_aliveTimer?.cancel();
},
);
// Set a timer to check if the subscription is still alive.
_aliveTimer?.cancel();
_aliveTimer = Timer.periodic(
_keepAlive,
(_) async => _updateConnectionStatus(await electrumXClient.ping()),
);
} else {
// Don't set a stream subscription if one already exists.
// Check if the stream subscription is paused. // Check if the stream subscription is paused.
if (ElectrumxChainHeightService if (ElectrumxChainHeightService
.subscriptions[cryptoCurrency.coin]!.isPaused) { .subscriptions[cryptoCurrency.coin]!.isPaused) {
@ -852,19 +927,6 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
.resume(); .resume();
} }
// Causes synchronization to stall.
// // Check if the stream subscription is active by pinging it.
// if (!(await subscribableElectrumXClient.ping())) {
// // If it's not active, reconnect it.
// final node = await getCurrentElectrumXNode();
//
// await subscribableElectrumXClient.connect(
// host: node.address, port: node.port);
//
// // Wait for first response.
// return completer.future;
// }
if (_latestHeight != null) { if (_latestHeight != null) {
return _latestHeight!; return _latestHeight!;
} }
@ -889,7 +951,7 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
return transactions.length; return transactions.length;
} }
Future<Map<String, int>> fetchTxCountBatched({ Future<Map<int, int>> fetchTxCountBatched({
required Map<String, String> addresses, required Map<String, String> addresses,
}) async { }) async {
try { try {
@ -901,7 +963,7 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
} }
final response = await electrumXClient.getBatchHistory(args: args); final response = await electrumXClient.getBatchHistory(args: args);
final Map<String, int> result = {}; final Map<int, int> result = {};
for (final entry in response.entries) { for (final entry in response.entries) {
result[entry.key] = entry.value.length; result[entry.key] = entry.value.length;
} }
@ -939,21 +1001,73 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
.toList(); .toList();
final newNode = await getCurrentElectrumXNode(); final newNode = await getCurrentElectrumXNode();
try {
await electrumXClient.electrumAdapterClient?.close();
} catch (e, s) {
if (e.toString().contains("initialized")) {
// Ignore. This should happen every first time the wallet is opened.
} else {
Logging.instance
.log("Error closing electrumXClient: $e", level: LogLevel.Error);
}
}
electrumXClient = ElectrumXClient.from( electrumXClient = ElectrumXClient.from(
node: newNode, node: newNode,
prefs: prefs, prefs: prefs,
failovers: failovers, 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( electrumXCachedClient = CachedElectrumXClient.from(
electrumXClient: electrumXClient, electrumXClient: electrumXClient,
electrumAdapterClient: electrumAdapterClient,
electrumAdapterUpdateCallback: updateClient,
); );
subscribableElectrumXClient = SubscribableElectrumXClient.from( // Replaced using electrum_adapters' SubscribableClient in fetchChainHeight.
node: newNode, // subscribableElectrumXClient = SubscribableElectrumXClient.from(
prefs: prefs, // node: newNode,
failovers: failovers, // prefs: prefs,
); // failovers: failovers,
await subscribableElectrumXClient.connect( // );
host: newNode.address, port: newNode.port); // await subscribableElectrumXClient.connect(
// host: newNode.address, port: newNode.port);
}
/// Update the connection status and call the onConnectionStatusChanged callback if it exists.
void _updateConnectionStatus(bool connectionStatus) {
// TODO [prio=low]: Set onConnectionStatusChanged callback.
// if (_isConnected != connectionStatus && onConnectionStatusChanged != null) {
// onConnectionStatusChanged!(connectionStatus);
// }
_isConnected = connectionStatus;
} }
//============================================================================ //============================================================================
@ -1115,21 +1229,22 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
List<Map<String, dynamic>> allTxHashes = []; List<Map<String, dynamic>> allTxHashes = [];
if (serverCanBatch) { if (serverCanBatch) {
final Map<int, Map<String, List<dynamic>>> batches = {}; final Map<String, Map<String, List<dynamic>>> batches = {};
final Map<String, String> requestIdToAddressMap = {}; final Map<int, String> requestIdToAddressMap = {};
const batchSizeMax = 100; const batchSizeMax = 100;
int batchNumber = 0; int batchNumber = 0;
for (int i = 0; i < allAddresses.length; i++) { for (int i = 0; i < allAddresses.length; i++) {
if (batches[batchNumber] == null) { if (batches["$batchNumber"] == null) {
batches[batchNumber] = {}; batches["$batchNumber"] = {};
} }
final scriptHash = cryptoCurrency.addressToScriptHash( final scriptHash = cryptoCurrency.addressToScriptHash(
address: allAddresses.elementAt(i), address: allAddresses.elementAt(i),
); );
final id = Logger.isTestEnv ? "$i" : const Uuid().v1(); // final id = Logger.isTestEnv ? "$i" : const Uuid().v1();
requestIdToAddressMap[id] = allAddresses.elementAt(i); // TODO [prio=???]: Pass request IDs to electrum_adapter.
batches[batchNumber]!.addAll({ requestIdToAddressMap[i] = allAddresses.elementAt(i);
id: [scriptHash] batches["$batchNumber"]!.addAll({
"$i": [scriptHash]
}); });
if (i % batchSizeMax == batchSizeMax - 1) { if (i % batchSizeMax == batchSizeMax - 1) {
batchNumber++; batchNumber++;
@ -1138,7 +1253,7 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
for (int i = 0; i < batches.length; i++) { for (int i = 0; i < batches.length; i++) {
final response = final response =
await electrumXClient.getBatchHistory(args: batches[i]!); await electrumXClient.getBatchHistory(args: batches["$i"]!);
for (final entry in response.entries) { for (final entry in response.entries) {
for (int j = 0; j < entry.value.length; j++) { for (int j = 0; j < entry.value.length; j++) {
entry.value[j]["address"] = requestIdToAddressMap[entry.key]; entry.value[j]["address"] = requestIdToAddressMap[entry.key];
@ -1186,6 +1301,8 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
coin: cryptoCurrency.coin, coin: cryptoCurrency.coin,
); );
print("txn: $txn");
final vout = jsonUTXO["tx_pos"] as int; final vout = jsonUTXO["tx_pos"] as int;
final outputs = txn["vout"] as List; final outputs = txn["vout"] as List;
@ -1254,6 +1371,13 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
await updateElectrumX(newNode: node); await updateElectrumX(newNode: node);
} }
Future<ElectrumClient> updateClient() async {
Logging.instance.log("Updating electrum node and ElectrumAdapterClient.",
level: LogLevel.Info);
await updateNode();
return electrumAdapterClient;
}
FeeObject? _cachedFees; FeeObject? _cachedFees;
@override @override

View file

@ -175,6 +175,7 @@ class _NodeCardState extends ConsumerState<NodeCard> {
useSSL: node.useSSL, useSSL: node.useSSL,
failovers: [], failovers: [],
prefs: ref.read(prefsChangeNotifierProvider), prefs: ref.read(prefsChangeNotifierProvider),
coin: widget.coin,
); );
try { try {

View file

@ -158,6 +158,7 @@ class NodeOptionsSheet extends ConsumerWidget {
failovers: [], failovers: [],
prefs: ref.read(prefsChangeNotifierProvider), prefs: ref.read(prefsChangeNotifierProvider),
torService: ref.read(pTorService), torService: ref.read(pTorService),
coin: coin,
); );
try { try {

View file

@ -524,6 +524,15 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.2" version: "1.0.2"
electrum_adapter:
dependency: "direct main"
description:
path: "."
ref: "51b7a60e07b0409b361e31da65d98178ee235bed"
resolved-ref: "51b7a60e07b0409b361e31da65d98178ee235bed"
url: "https://github.com/cypherstack/electrum_adapter.git"
source: git
version: "3.0.0"
emojis: emojis:
dependency: "direct main" dependency: "direct main"
description: description:
@ -674,10 +683,10 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_lints name: flutter_lints
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.3" version: "3.0.1"
flutter_local_notifications: flutter_local_notifications:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1038,10 +1047,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: lints name: lints
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "3.0.0"
local_auth: local_auth:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1594,7 +1603,7 @@ packages:
source: hosted source: hosted
version: "1.5.3" version: "1.5.3"
stream_channel: stream_channel:
dependency: transitive dependency: "direct main"
description: description:
name: stream_channel name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7

View file

@ -173,6 +173,11 @@ dependencies:
url: https://github.com/cypherstack/coinlib.git url: https://github.com/cypherstack/coinlib.git
path: coinlib_flutter path: coinlib_flutter
ref: 376d520b4516d4eb7c3f0bd4b1522f7769f3f2a7 ref: 376d520b4516d4eb7c3f0bd4b1522f7769f3f2a7
electrum_adapter:
git:
url: https://github.com/cypherstack/electrum_adapter.git
ref: 51b7a60e07b0409b361e31da65d98178ee235bed
stream_channel: ^2.1.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -189,7 +194,7 @@ dev_dependencies:
# lint: ^1.10.0 # lint: ^1.10.0
analyzer: ^5.13.0 analyzer: ^5.13.0
import_sorter: ^4.6.0 import_sorter: ^4.6.0
flutter_lints: ^2.0.1 flutter_lints: ^3.0.1
isar_generator: 3.0.5 isar_generator: 3.0.5
flutter_launcher_icons: flutter_launcher_icons: