WIP use and reuse electrum adapter channel

This commit is contained in:
sneurlax 2024-02-14 19:47:26 -06:00
parent 2339b33798
commit b8987c73c0
11 changed files with 293 additions and 232 deletions

View file

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

View file

@ -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<Map<String, dynamic>> getAnonymitySet({
required String groupId,
@ -59,9 +62,10 @@ class CachedElectrumXClient {
set = Map<String, dynamic>.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<String, dynamic>.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<String, dynamic> result = await client.getTransaction(txHash);
final Map<String, dynamic> 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<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
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(
"tags",

View file

@ -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<dynamic>? get electrumAdapterChannel => _electrumAdapterChannel;
StreamChannel<dynamic>? _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<ElectrumXClient> _finalizer = Finalizer(
@ -103,6 +113,7 @@ class ElectrumXClient {
required bool useSSL,
required Prefs prefs,
required List<ElectrumXNode> 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<TorConnectionStatusChangedEvent>().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<ElectrumXNode> 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<void> _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 <request id string : arguments list>
///
/// 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 Map<String, List<dynamic>> 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<String> 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<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;
var futures = <Future<dynamic>>[];
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<String> 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<Map<String, dynamic>>.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<String, dynamic>;
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<String, dynamic>.from(response["result"] as Map);
return Map<String, dynamic>.from(response as Map);
} catch (e) {
rethrow;
}
@ -524,7 +577,7 @@ class ElectrumXClient {
requestID: requestID,
command: 'server.features',
);
return Map<String, dynamic>.from(response["result"] as Map);
return Map<String, dynamic>.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<String, dynamic>.from(response["result"] as Map);
return Map<String, dynamic>.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<Map<String, List<Map<String, dynamic>>>> getBatchHistory(
Future<Map<int, List<Map<String, dynamic>>>> getBatchHistory(
{required Map<String, List<dynamic>> args}) async {
try {
final response = await batchRequest(
command: 'blockchain.scripthash.get_history',
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++) {
result[response[i]["id"] as String] =
List<Map<String, dynamic>>.from(response[i]["result"] as List);
result[i] = List<Map<String, dynamic>>.from(response[i] as List);
}
return result;
} catch (e) {
@ -667,23 +719,33 @@ class ElectrumXClient {
scripthash,
],
);
return List<Map<String, dynamic>>.from(response["result"] as List);
return List<Map<String, dynamic>>.from(response as List);
} catch (e) {
rethrow;
}
}
Future<Map<String, List<Map<String, dynamic>>>> getBatchUTXOs(
Future<Map<int, List<Map<String, dynamic>>>> getBatchUTXOs(
{required Map<String, List<dynamic>> args}) async {
try {
final response = await batchRequest(
command: 'blockchain.scripthash.listunspent',
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++) {
result[response[i]["id"] as String] =
List<Map<String, dynamic>>.from(response[i]["result"] as List);
if ((response[i] as List).isNotEmpty) {
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;
} 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<String, dynamic> anonymitySet = await client.getLelantusAnonymitySet(
groupId: groupId, blockHash: blockhash);
await _checkElectrumAdapter();
Map<String, dynamic> 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<String, dynamic>.from(usedCoinSerials as Map);
return Map<String, dynamic>.from(response as Map);
}
/// Returns the latest Lelantus set id
@ -859,17 +899,12 @@ class ElectrumXClient {
Future<int> 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<String, dynamic> anonymitySet = await client.getSparkAnonymitySet(
coinGroupId: coinGroupId, startBlockHash: startBlockHash);
await _checkElectrumAdapter();
Map<String, dynamic> 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<String, dynamic> usedCoinsTags =
await client.getUsedCoinsTags(startNumber: startNumber);
await _checkElectrumAdapter();
Map<String, dynamic> 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<String, dynamic>.from(usedCoinsTags);
final map = Map<String, dynamic>.from(response);
final set = Set<String>.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<dynamic> mintMetaData =
await client.getSparkMintMetaData(sparkCoinHashes: sparkCoinHashes);
await _checkElectrumAdapter();
List<dynamic> response =
await (_electrumAdapterClient as FiroElectrumClient)
.getSparkMintMetaData(sparkCoinHashes: sparkCoinHashes);
Logging.instance.log("Fetching spark.getsparkmintmetadata finished",
level: LogLevel.Info);
return List<Map<String, dynamic>>.from(mintMetaData);
return List<Map<String, dynamic>>.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<Map<String, dynamic>> 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;
}

View file

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

View file

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

View file

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

View file

@ -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<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
late ElectrumXClient electrumXClient;
late StreamChannel electrumAdapterChannel;
late ElectrumClient electrumAdapterClient;
late CachedElectrumXClient electrumXCachedClient;
late SubscribableElectrumXClient subscribableElectrumXClient;
@ -889,7 +895,7 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
return transactions.length;
}
Future<Map<String, int>> fetchTxCountBatched({
Future<Map<int, int>> fetchTxCountBatched({
required Map<String, String> addresses,
}) async {
try {
@ -901,7 +907,7 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
}
final response = await electrumXClient.getBatchHistory(args: args);
final Map<String, int> result = {};
final Map<int, int> result = {};
for (final entry in response.entries) {
result[entry.key] = entry.value.length;
}
@ -943,9 +949,40 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
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<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
List<Map<String, dynamic>> allTxHashes = [];
if (serverCanBatch) {
final Map<int, Map<String, List<dynamic>>> batches = {};
final Map<String, String> requestIdToAddressMap = {};
final Map<String, Map<String, List<dynamic>>> batches = {};
final Map<int, String> 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<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
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];

View file

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

View file

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

View file

@ -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

View file

@ -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: