mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-11-18 02:07:43 +00:00
Merge pull request #760 from cypherstack/electrum_adapter
Use electrum_adapter package for Electrum calls
This commit is contained in:
commit
c75b819157
13 changed files with 1388 additions and 1228 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
19
pubspec.lock
19
pubspec.lock
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue