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), useSSL: node.useSSL),
prefs: prefs, prefs: prefs,
failovers: failovers, failovers: failovers,
coin: Coin.firo,
); );
try { try {

View file

@ -12,29 +12,32 @@ 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' 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/services/tor_service.dart';
import 'package:stackwallet/utilities/enums/coin_enum.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:string_validator/string_validator.dart'; import 'package:string_validator/string_validator.dart';
class CachedElectrumXClient { class CachedElectrumXClient {
final ElectrumXClient electrumXClient; final ElectrumXClient electrumXClient;
final ElectrumClient electrumAdapterClient;
static const minCacheConfirms = 30; static const minCacheConfirms = 30;
const CachedElectrumXClient({ const CachedElectrumXClient({
required this.electrumXClient, required this.electrumXClient,
required this.electrumAdapterClient,
}); });
factory CachedElectrumXClient.from({ factory CachedElectrumXClient.from({
required ElectrumXClient electrumXClient, required ElectrumXClient electrumXClient,
required ElectrumClient electrumAdapterClient,
}) => }) =>
CachedElectrumXClient( CachedElectrumXClient(
electrumXClient: electrumXClient, electrumXClient: electrumXClient,
); electrumAdapterClient: electrumAdapterClient);
Future<Map<String, dynamic>> getAnonymitySet({ Future<Map<String, dynamic>> getAnonymitySet({
required String groupId, required String groupId,
@ -59,9 +62,10 @@ class CachedElectrumXClient {
set = Map<String, dynamic>.from(cachedSet); set = Map<String, dynamic>.from(cachedSet);
} }
final newSet = await electrumXClient.getLelantusAnonymitySet( 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
@ -85,7 +89,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)
@ -133,7 +137,8 @@ class CachedElectrumXClient {
set = Map<String, dynamic>.from(cachedSet); set = Map<String, dynamic>.from(cachedSet);
} }
final newSet = await electrumXClient.getSparkAnonymitySet( final newSet = await (electrumAdapterClient as FiroElectrumClient)
.getSparkAnonymitySet(
coinGroupId: groupId, coinGroupId: groupId,
startBlockHash: set["blockHash"] as String, startBlockHash: set["blockHash"] as String,
); );
@ -191,15 +196,8 @@ class CachedElectrumXClient {
final cachedTx = box.get(txHash) as Map?; final cachedTx = box.get(txHash) as Map?;
if (cachedTx == null) { if (cachedTx == null) {
var channel = await electrum_adapter.connect(electrumXClient.host, final Map<String, dynamic> result =
port: electrumXClient.port, await electrumAdapterClient.getTransaction(txHash);
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);
result.remove("hex"); result.remove("hex");
result.remove("lelantusData"); result.remove("lelantusData");
@ -241,7 +239,8 @@ class CachedElectrumXClient {
cachedSerials.length - 100, // 100 being some arbitrary buffer cachedSerials.length - 100, // 100 being some arbitrary buffer
); );
final serials = await electrumXClient.getLelantusUsedCoinSerials( final serials = await (electrumAdapterClient as FiroElectrumClient)
.getLelantusUsedCoinSerials(
startNumber: startNumber, startNumber: startNumber,
); );
@ -289,7 +288,8 @@ class CachedElectrumXClient {
cachedTags.length - 100, // 100 being some arbitrary buffer cachedTags.length - 100, // 100 being some arbitrary buffer
); );
final tags = await electrumXClient.getSparkUsedCoinsTags( final tags =
await (electrumAdapterClient as FiroElectrumClient).getUsedCoinsTags(
startNumber: startNumber, startNumber: startNumber,
); );
@ -297,12 +297,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,12 +9,12 @@
*/ */
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' as electrum_adapter;
import 'package:electrum_adapter/electrum_adapter.dart';
import 'package:electrum_adapter/methods/specific/firo.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';
@ -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/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 {}
@ -75,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;
@ -83,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(
@ -103,6 +113,7 @@ class ElectrumXClient {
required bool useSSL, required bool useSSL,
required Prefs prefs, required Prefs prefs,
required List<ElectrumXNode> failovers, required List<ElectrumXNode> failovers,
required Coin coin,
JsonRPC? client, JsonRPC? client,
this.connectionTimeoutForSpecialCaseJsonRPCClients = this.connectionTimeoutForSpecialCaseJsonRPCClients =
const Duration(seconds: 60), const Duration(seconds: 60),
@ -115,6 +126,7 @@ class ElectrumXClient {
_port = port; _port = port;
_useSSL = useSSL; _useSSL = useSSL;
_rpcClient = client; _rpcClient = client;
_coin = coin;
final bus = globalEventBusForTesting ?? GlobalEventBus.instance; final bus = globalEventBusForTesting ?? GlobalEventBus.instance;
_torStatusListener = bus.on<TorConnectionStatusChangedEvent>().listen( _torStatusListener = bus.on<TorConnectionStatusChangedEvent>().listen(
@ -154,6 +166,8 @@ class ElectrumXClient {
// 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; _rpcClient = null;
_electrumAdapterChannel = null;
_electrumAdapterClient = null;
await temp?.disconnect( await temp?.disconnect(
reason: "Tor status changed to \"${event.status}\"", reason: "Tor status changed to \"${event.status}\"",
@ -166,6 +180,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,
}) { }) {
@ -177,6 +192,7 @@ class ElectrumXClient {
torService: torService, torService: torService,
failovers: failovers, failovers: failovers,
globalEventBusForTesting: globalEventBusForTesting, globalEventBusForTesting: globalEventBusForTesting,
coin: coin,
); );
} }
@ -238,24 +254,99 @@ class ElectrumXClient {
return; return;
} }
} }
}
if (currentFailoverIndex == -1) { Future<void> _checkElectrumAdapter() async {
_rpcClient ??= JsonRPC( ({InternetAddress host, int port})? proxyInfo;
host: host,
port: port, // If we're supposed to use Tor...
useSSL: useSSL, if (_prefs.useTor) {
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, // But Tor isn't running...
proxyInfo: null, 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 { } else {
_rpcClient ??= JsonRPC( // ... But if the killswitch is set, then we throw an exception.
host: failovers![currentFailoverIndex].address, throw Exception(
port: failovers![currentFailoverIndex].port, "Tor preference and killswitch set but Tor is not enabled, not connecting to Electrum adapter");
useSSL: failovers![currentFailoverIndex].useSSL, // 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) {
_electrumAdapterChannel ??= await electrum_adapter.connect(
host,
port: port,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
proxyInfo: null, 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
@ -271,32 +362,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(
@ -308,13 +389,13 @@ 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; return response;
} on WifiOnlyException { } on WifiOnlyException {
rethrow; rethrow;
} on SocketException { } on SocketException {
@ -350,7 +431,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),
@ -361,62 +442,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;
_electrumAdapterClient!.peer.withBatch(() {
for (final entry in args.entries) { for (final entry in args.entries) {
final jsonArgs = json.encode(entry.value); futures.add(_electrumAdapterClient!.request(command, 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;
}
} 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 // 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());
} }
} }
@ -430,7 +483,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 {
@ -471,7 +524,7 @@ class ElectrumXClient {
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 Map<String, dynamic>;
return response.keys.contains("result") && response["result"] == null; return response.isNotEmpty; // TODO [prio=extreme]: Fix this.
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
@ -492,14 +545,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;
} }
@ -524,7 +577,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;
} }
@ -545,7 +598,7 @@ class ElectrumXClient {
rawTx, rawTx,
], ],
); );
return response["result"] as String; return response as String;
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
@ -572,7 +625,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;
} }
@ -609,7 +662,7 @@ class ElectrumXClient {
scripthash, scripthash,
], ],
); );
result = response["result"]; result = response;
retryCount--; 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 { {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) {
@ -667,23 +719,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) {
@ -745,14 +807,8 @@ class ElectrumXClient {
}) async { }) async {
Logging.instance.log("attempting to fetch blockchain.transaction.get...", Logging.instance.log("attempting to fetch blockchain.transaction.get...",
level: LogLevel.Info); level: LogLevel.Info);
var channel = await electrum_adapter.connect(host, await _checkElectrumAdapter();
port: port, dynamic response = await _electrumAdapterClient!.getTransaction(txHash);
useSSL: useSSL,
proxyInfo: Prefs.instance.useTor
? TorService.sharedInstance.getProxyInfo()
: null);
var client = electrum_adapter.ElectrumClient(channel, host, port);
dynamic response = await client.getTransaction(txHash);
Logging.instance.log("Fetching blockchain.transaction.get finished", Logging.instance.log("Fetching blockchain.transaction.get finished",
level: LogLevel.Info); level: LogLevel.Info);
@ -784,18 +840,13 @@ class ElectrumXClient {
}) async { }) async {
Logging.instance.log("attempting to fetch lelantus.getanonymityset...", Logging.instance.log("attempting to fetch lelantus.getanonymityset...",
level: LogLevel.Info); level: LogLevel.Info);
var channel = await electrum_adapter.connect(host, await _checkElectrumAdapter();
port: port, Map<String, dynamic> response =
useSSL: useSSL, await (_electrumAdapterClient as FiroElectrumClient)!
proxyInfo: Prefs.instance.useTor .getLelantusAnonymitySet(groupId: groupId, blockHash: blockhash);
? TorService.sharedInstance.getProxyInfo()
: null);
var client = electrum_adapter.FiroElectrumClient(channel);
Map<String, dynamic> anonymitySet = await client.getLelantusAnonymitySet(
groupId: groupId, blockHash: blockhash);
Logging.instance.log("Fetching lelantus.getanonymityset finished", Logging.instance.log("Fetching lelantus.getanonymityset finished",
level: LogLevel.Info); level: LogLevel.Info);
return anonymitySet; return response;
} }
//TODO add example to docs //TODO add example to docs
@ -808,17 +859,12 @@ class ElectrumXClient {
}) async { }) async {
Logging.instance.log("attempting to fetch lelantus.getmintmetadata...", Logging.instance.log("attempting to fetch lelantus.getmintmetadata...",
level: LogLevel.Info); level: LogLevel.Info);
var channel = await electrum_adapter.connect(host, await _checkElectrumAdapter();
port: port, dynamic response = await (_electrumAdapterClient as FiroElectrumClient)!
useSSL: useSSL, .getLelantusMintData(mints: mints);
proxyInfo: Prefs.instance.useTor
? TorService.sharedInstance.getProxyInfo()
: null);
var client = electrum_adapter.FiroElectrumClient(channel);
dynamic mintData = await client.getLelantusMintData(mints: mints);
Logging.instance.log("Fetching lelantus.getmintmetadata finished", Logging.instance.log("Fetching lelantus.getmintmetadata finished",
level: LogLevel.Info); level: LogLevel.Info);
return mintData; return response;
} }
//TODO add example to docs //TODO add example to docs
@ -829,20 +875,14 @@ class ElectrumXClient {
}) async { }) async {
Logging.instance.log("attempting to fetch lelantus.getusedcoinserials...", Logging.instance.log("attempting to fetch lelantus.getusedcoinserials...",
level: LogLevel.Info); level: LogLevel.Info);
var channel = await electrum_adapter.connect(host, await _checkElectrumAdapter();
port: port,
useSSL: useSSL,
proxyInfo: Prefs.instance.useTor
? TorService.sharedInstance.getProxyInfo()
: null);
var client = electrum_adapter.FiroElectrumClient(channel);
int retryCount = 3; int retryCount = 3;
dynamic usedCoinSerials; dynamic response;
while (retryCount > 0 && usedCoinSerials is! List) { while (retryCount > 0 && response is! List) {
usedCoinSerials = response = await (_electrumAdapterClient as FiroElectrumClient)!
await client.getLelantusUsedCoinSerials(startNumber: startNumber); .getLelantusUsedCoinSerials(startNumber: startNumber);
// TODO add 2 minute timeout. // TODO add 2 minute timeout.
Logging.instance.log("Fetching lelantus.getusedcoinserials finished", Logging.instance.log("Fetching lelantus.getusedcoinserials finished",
level: LogLevel.Info); level: LogLevel.Info);
@ -850,7 +890,7 @@ class ElectrumXClient {
retryCount--; retryCount--;
} }
return Map<String, dynamic>.from(usedCoinSerials as Map); return Map<String, dynamic>.from(response as Map);
} }
/// Returns the latest Lelantus set id /// Returns the latest Lelantus set id
@ -859,17 +899,12 @@ class ElectrumXClient {
Future<int> getLelantusLatestCoinId({String? requestID}) async { Future<int> getLelantusLatestCoinId({String? requestID}) async {
Logging.instance.log("attempting to fetch lelantus.getlatestcoinid...", Logging.instance.log("attempting to fetch lelantus.getlatestcoinid...",
level: LogLevel.Info); level: LogLevel.Info);
var channel = await electrum_adapter.connect(host, await _checkElectrumAdapter();
port: port, int response =
useSSL: useSSL, await (_electrumAdapterClient as FiroElectrumClient).getLatestCoinId();
proxyInfo: Prefs.instance.useTor
? TorService.sharedInstance.getProxyInfo()
: null);
var client = electrum_adapter.FiroElectrumClient(channel);
int latestCoinId = await client.getLatestCoinId();
Logging.instance.log("Fetching lelantus.getlatestcoinid finished", Logging.instance.log("Fetching lelantus.getlatestcoinid finished",
level: LogLevel.Info); level: LogLevel.Info);
return latestCoinId; return response;
} }
// ============== Spark ====================================================== // ============== Spark ======================================================
@ -895,18 +930,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);
var channel = await electrum_adapter.connect(host, await _checkElectrumAdapter();
port: port, Map<String, dynamic> response =
useSSL: useSSL, await (_electrumAdapterClient as FiroElectrumClient)
proxyInfo: Prefs.instance.useTor .getSparkAnonymitySet(
? TorService.sharedInstance.getProxyInfo()
: null);
var client = electrum_adapter.FiroElectrumClient(channel);
Map<String, dynamic> anonymitySet = await client.getSparkAnonymitySet(
coinGroupId: coinGroupId, startBlockHash: startBlockHash); coinGroupId: coinGroupId, startBlockHash: startBlockHash);
Logging.instance.log("Fetching spark.getsparkanonymityset finished", Logging.instance.log("Fetching spark.getsparkanonymityset finished",
level: LogLevel.Info); level: LogLevel.Info);
return anonymitySet; return response;
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
@ -922,19 +953,14 @@ class ElectrumXClient {
// Use electrum_adapter package's getSparkUsedCoinsTags method. // Use electrum_adapter package's getSparkUsedCoinsTags method.
Logging.instance.log("attempting to fetch spark.getusedcoinstags...", Logging.instance.log("attempting to fetch spark.getusedcoinstags...",
level: LogLevel.Info); level: LogLevel.Info);
var channel = await electrum_adapter.connect(host, await _checkElectrumAdapter();
port: port, Map<String, dynamic> response =
useSSL: useSSL, await (_electrumAdapterClient as FiroElectrumClient)
proxyInfo: Prefs.instance.useTor .getUsedCoinsTags(startNumber: startNumber);
? TorService.sharedInstance.getProxyInfo()
: null);
var client = electrum_adapter.FiroElectrumClient(channel);
Map<String, dynamic> usedCoinsTags =
await client.getUsedCoinsTags(startNumber: startNumber);
// TODO: Add 2 minute timeout. // TODO: Add 2 minute timeout.
Logging.instance.log("Fetching spark.getusedcoinstags finished", Logging.instance.log("Fetching spark.getusedcoinstags finished",
level: LogLevel.Info); 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); final set = Set<String>.from(map["tags"] as List);
return await compute(_ffiHashTagsComputeWrapper, set); return await compute(_ffiHashTagsComputeWrapper, set);
} catch (e) { } catch (e) {
@ -960,18 +986,13 @@ class ElectrumXClient {
try { try {
Logging.instance.log("attempting to fetch spark.getsparkmintmetadata...", Logging.instance.log("attempting to fetch spark.getsparkmintmetadata...",
level: LogLevel.Info); level: LogLevel.Info);
var channel = await electrum_adapter.connect(host, await _checkElectrumAdapter();
port: port, List<dynamic> response =
useSSL: useSSL, await (_electrumAdapterClient as FiroElectrumClient)
proxyInfo: Prefs.instance.useTor .getSparkMintMetaData(sparkCoinHashes: sparkCoinHashes);
? TorService.sharedInstance.getProxyInfo()
: null);
var client = electrum_adapter.FiroElectrumClient(channel);
List<dynamic> mintMetaData =
await client.getSparkMintMetaData(sparkCoinHashes: sparkCoinHashes);
Logging.instance.log("Fetching spark.getsparkmintmetadata finished", Logging.instance.log("Fetching spark.getsparkmintmetadata finished",
level: LogLevel.Info); level: LogLevel.Info);
return List<Map<String, dynamic>>.from(mintMetaData); return List<Map<String, dynamic>>.from(response);
} catch (e) { } catch (e) {
Logging.instance.log(e, level: LogLevel.Error); Logging.instance.log(e, level: LogLevel.Error);
rethrow; rethrow;
@ -987,17 +1008,12 @@ class ElectrumXClient {
try { try {
Logging.instance.log("attempting to fetch spark.getsparklatestcoinid...", Logging.instance.log("attempting to fetch spark.getsparklatestcoinid...",
level: LogLevel.Info); level: LogLevel.Info);
var channel = await electrum_adapter.connect(host, await _checkElectrumAdapter();
port: port, int response = await (_electrumAdapterClient as FiroElectrumClient)
useSSL: useSSL, .getSparkLatestCoinId();
proxyInfo: Prefs.instance.useTor
? TorService.sharedInstance.getProxyInfo()
: null);
var client = electrum_adapter.FiroElectrumClient(channel);
int latestCoinId = await client.getSparkLatestCoinId();
Logging.instance.log("Fetching spark.getsparklatestcoinid finished", Logging.instance.log("Fetching spark.getsparklatestcoinid finished",
level: LogLevel.Info); level: LogLevel.Info);
return latestCoinId; return response;
} catch (e) { } catch (e) {
Logging.instance.log(e, level: LogLevel.Error); Logging.instance.log(e, level: LogLevel.Error);
rethrow; rethrow;
@ -1014,14 +1030,8 @@ class ElectrumXClient {
/// "rate": 1000, /// "rate": 1000,
/// } /// }
Future<Map<String, dynamic>> getFeeRate({String? requestID}) async { Future<Map<String, dynamic>> getFeeRate({String? requestID}) async {
var channel = await electrum_adapter.connect(host, await _checkElectrumAdapter();
port: port, return await _electrumAdapterClient!.getFeeRate();
useSSL: useSSL,
proxyInfo: Prefs.instance.useTor
? TorService.sharedInstance.getProxyInfo()
: null);
var client = electrum_adapter.FiroElectrumClient(channel);
return await client.getFeeRate();
} }
/// 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].
@ -1039,10 +1049,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);
} }
@ -1062,7 +1072,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;
} }

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,6 +4,8 @@ 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: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';
@ -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/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/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;
@ -889,7 +895,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 +907,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;
} }
@ -943,9 +949,40 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
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,
); );
subscribableElectrumXClient = SubscribableElectrumXClient.from( subscribableElectrumXClient = SubscribableElectrumXClient.from(
node: newNode, node: newNode,
@ -1115,21 +1152,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 +1176,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];

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

@ -528,8 +528,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: dd83940d73429d917f9e50b3a765adbf5e06df6d ref: "51b7a60e07b0409b361e31da65d98178ee235bed"
resolved-ref: dd83940d73429d917f9e50b3a765adbf5e06df6d resolved-ref: "51b7a60e07b0409b361e31da65d98178ee235bed"
url: "https://github.com/cypherstack/electrum_adapter.git" url: "https://github.com/cypherstack/electrum_adapter.git"
source: git source: git
version: "3.0.0" version: "3.0.0"
@ -1603,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

@ -176,7 +176,8 @@ dependencies:
electrum_adapter: electrum_adapter:
git: git:
url: https://github.com/cypherstack/electrum_adapter.git url: https://github.com/cypherstack/electrum_adapter.git
ref: dd83940d73429d917f9e50b3a765adbf5e06df6d ref: 51b7a60e07b0409b361e31da65d98178ee235bed
stream_channel: ^2.1.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: