stack_wallet/lib/electrumx_rpc/electrumx_client.dart

1302 lines
39 KiB
Dart
Raw Permalink Normal View History

2023-05-26 21:21:16 +00:00
/*
* This file is part of Stack Wallet.
*
* Copyright (c) 2023 Cypher Stack
* All Rights Reserved.
* The code is distributed under GPLv3 license, see LICENSE file for details.
* Generated by Cypher Stack on 2023-05-26
*
*/
import 'dart:async';
2022-08-26 08:11:35 +00:00
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:decimal/decimal.dart';
import 'package:electrum_adapter/electrum_adapter.dart' as electrum_adapter;
import 'package:electrum_adapter/electrum_adapter.dart';
import 'package:electrum_adapter/methods/specific/firo.dart';
import 'package:event_bus/event_bus.dart';
2023-09-08 16:37:50 +00:00
import 'package:mutex/mutex.dart';
2024-05-29 19:29:45 +00:00
import 'package:stream_channel/stream_channel.dart';
import '../exceptions/electrumx/no_such_transaction.dart';
import '../services/event_bus/events/global/tor_connection_status_changed_event.dart';
import '../services/event_bus/events/global/tor_status_changed_event.dart';
import '../services/event_bus/global_event_bus.dart';
import '../services/tor_service.dart';
import '../utilities/amount/amount.dart';
2024-06-14 19:33:27 +00:00
import '../utilities/extensions/impl/string.dart';
import '../utilities/logger.dart';
import '../utilities/prefs.dart';
import '../utilities/tor_plain_net_option_enum.dart';
import '../wallets/crypto_currency/crypto_currency.dart';
import '../wallets/crypto_currency/interfaces/electrumx_currency_interface.dart';
2024-05-29 19:29:45 +00:00
import 'client_manager.dart';
2022-08-26 08:11:35 +00:00
2024-06-14 19:33:27 +00:00
typedef SparkMempoolData = ({
String txid,
List<String> serialContext,
List<String> lTags,
List<String> coins,
});
2022-08-26 08:11:35 +00:00
class WifiOnlyException implements Exception {}
class TorOnlyException implements Exception {}
class ClearnetOnlyException implements Exception {}
2022-08-26 08:11:35 +00:00
class ElectrumXNode {
ElectrumXNode({
required this.address,
required this.port,
required this.name,
required this.id,
required this.useSSL,
required this.torEnabled,
2024-11-26 15:18:35 +00:00
required this.clearnetEnabled,
2022-08-26 08:11:35 +00:00
});
final String address;
final int port;
final String name;
final String id;
final bool useSSL;
final bool torEnabled;
2024-11-26 15:18:35 +00:00
final bool clearnetEnabled;
2022-08-26 08:11:35 +00:00
factory ElectrumXNode.from(ElectrumXNode node) {
return ElectrumXNode(
address: node.address,
port: node.port,
name: node.name,
id: node.id,
useSSL: node.useSSL,
torEnabled: node.torEnabled,
2024-11-26 15:18:35 +00:00
clearnetEnabled: node.clearnetEnabled,
2022-08-26 08:11:35 +00:00
);
}
@override
String toString() {
return "ElectrumXNode: {address: $address, port: $port, name: $name, useSSL: $useSSL}";
}
}
2023-11-14 20:31:53 +00:00
class ElectrumXClient {
2024-04-18 23:17:45 +00:00
final CryptoCurrency cryptoCurrency;
final TorPlainNetworkOption netType;
2024-04-18 23:17:45 +00:00
2022-08-26 08:11:35 +00:00
String get host => _host;
late String _host;
int get port => _port;
late int _port;
bool get useSSL => _useSSL;
late bool _useSSL;
2024-04-18 23:17:45 +00:00
// StreamChannel<dynamic>? get electrumAdapterChannel => _electrumAdapterChannel;
StreamChannel<dynamic>? _electrumAdapterChannel;
2024-04-18 23:17:45 +00:00
ElectrumClient? getElectrumAdapter() =>
ClientManager.sharedInstance.getClient(
cryptoCurrency: cryptoCurrency,
netType: netType,
2024-04-18 23:17:45 +00:00
);
2022-08-26 08:11:35 +00:00
late Prefs _prefs;
late TorService _torService;
2022-08-26 08:11:35 +00:00
List<ElectrumXNode>? failovers;
int currentFailoverIndex = -1;
2023-07-27 20:39:48 +00:00
final Duration connectionTimeoutForSpecialCaseJsonRPCClients;
// add finalizer to cancel stream subscription when all references to an
// instance of ElectrumX becomes inaccessible
2023-11-14 20:31:53 +00:00
static final Finalizer<ElectrumXClient> _finalizer = Finalizer(
2023-09-08 16:37:50 +00:00
(p0) {
p0._torPreferenceListener?.cancel();
p0._torStatusListener?.cancel();
},
);
2023-09-08 16:37:50 +00:00
StreamSubscription<TorPreferenceChangedEvent>? _torPreferenceListener;
StreamSubscription<TorConnectionStatusChangedEvent>? _torStatusListener;
final Mutex _torConnectingLock = Mutex();
2023-09-08 16:54:31 +00:00
bool _requireMutex = false;
2023-11-14 20:31:53 +00:00
ElectrumXClient({
2023-07-27 20:39:48 +00:00
required String host,
required int port,
required bool useSSL,
required Prefs prefs,
required this.netType,
2023-07-27 20:39:48 +00:00
required List<ElectrumXNode> failovers,
2024-04-18 23:17:45 +00:00
required this.cryptoCurrency,
2023-07-27 20:39:48 +00:00
this.connectionTimeoutForSpecialCaseJsonRPCClients =
const Duration(seconds: 60),
TorService? torService,
EventBus? globalEventBusForTesting,
2023-07-27 20:39:48 +00:00
}) {
2022-08-26 08:11:35 +00:00
_prefs = prefs;
_torService = torService ?? TorService.sharedInstance;
2022-08-26 08:11:35 +00:00
_host = host;
_port = port;
_useSSL = useSSL;
final bus = globalEventBusForTesting ?? GlobalEventBus.instance;
// Listen for tor status changes.
2023-09-08 16:37:50 +00:00
_torStatusListener = bus.on<TorConnectionStatusChangedEvent>().listen(
(event) async {
switch (event.newStatus) {
case TorConnectionStatus.connecting:
await _torConnectingLock.acquire();
2023-09-08 16:54:31 +00:00
_requireMutex = true;
2023-09-08 16:37:50 +00:00
break;
case TorConnectionStatus.connected:
case TorConnectionStatus.disconnected:
2023-09-08 18:59:54 +00:00
if (_torConnectingLock.isLocked) {
_torConnectingLock.release();
}
2023-09-08 16:54:31 +00:00
_requireMutex = false;
2023-09-08 16:37:50 +00:00
break;
}
},
);
// Listen for tor preference changes.
2023-09-08 16:37:50 +00:00
_torPreferenceListener = bus.on<TorPreferenceChangedEvent>().listen(
(event) async {
// not sure if we need to do anything specific here
// switch (event.status) {
// case TorStatus.enabled:
// case TorStatus.disabled:
// }
// setting to null should force the creation of a new json rpc client
// on the next request sent through this electrumx instance
_electrumAdapterChannel = null;
2024-04-18 23:17:45 +00:00
await (await ClientManager.sharedInstance
.remove(cryptoCurrency: cryptoCurrency))
.$1
2024-04-18 23:17:45 +00:00
?.close();
// Also close any chain height services that are currently open.
2024-04-18 23:17:45 +00:00
// await ChainHeightServiceManager.dispose();
},
);
2022-08-26 08:11:35 +00:00
}
2023-11-14 20:31:53 +00:00
factory ElectrumXClient.from({
2022-08-26 08:11:35 +00:00
required ElectrumXNode node,
required Prefs prefs,
required List<ElectrumXNode> failovers,
2024-04-18 23:17:45 +00:00
required CryptoCurrency cryptoCurrency,
TorService? torService,
EventBus? globalEventBusForTesting,
}) {
2023-11-14 20:31:53 +00:00
return ElectrumXClient(
host: node.address,
port: node.port,
useSSL: node.useSSL,
prefs: prefs,
torService: torService,
failovers: failovers,
globalEventBusForTesting: globalEventBusForTesting,
2024-04-18 23:17:45 +00:00
cryptoCurrency: cryptoCurrency,
netType: TorPlainNetworkOption.fromNodeData(
node.torEnabled,
2024-11-26 15:18:35 +00:00
node.clearnetEnabled,
),
);
}
2022-08-26 08:11:35 +00:00
Future<bool> _allow() async {
if (_prefs.wifiOnly) {
return (await Connectivity().checkConnectivity()) ==
ConnectivityResult.wifi;
}
return true;
}
2024-04-18 23:17:45 +00:00
Future<void> closeAdapter() async {
await getElectrumAdapter()?.close();
}
2024-06-24 20:23:39 +00:00
Future<void> checkElectrumAdapter() async {
({InternetAddress host, int port})? proxyInfo;
// If we're supposed to use Tor...
if (_prefs.useTor) {
// But Tor isn't running...
if (_torService.status != TorConnectionStatus.connected) {
// And the killswitch isn't set...
if (!_prefs.torKillSwitch) {
2024-05-09 18:44:56 +00:00
// Then we'll just proceed and connect to ElectrumX through
// clearnet at the bottom of this function.
Logging.instance.log(
2024-05-09 18:44:56 +00:00
"Tor preference set but Tor is not enabled, killswitch not set,"
" connecting to Electrum adapter through clearnet",
level: LogLevel.Warning,
);
} else {
// ... But if the killswitch is set, then we throw an exception.
throw Exception(
2024-05-09 18:44:56 +00:00
"Tor preference and killswitch set but Tor is not enabled, "
"not connecting to Electrum adapter",
);
// TODO [prio=low]: Try to start Tor.
}
} else {
// Get the proxy info from the TorService.
proxyInfo = _torService.getProxyInfo();
}
if (netType == TorPlainNetworkOption.clear) {
_electrumAdapterChannel = null;
await ClientManager.sharedInstance
.remove(cryptoCurrency: cryptoCurrency);
}
} else {
if (netType == TorPlainNetworkOption.tor) {
_electrumAdapterChannel = null;
await ClientManager.sharedInstance
.remove(cryptoCurrency: cryptoCurrency);
}
}
// If the current ElectrumAdapterClient is closed, create a new one.
2024-04-18 23:17:45 +00:00
if (getElectrumAdapter() != null && getElectrumAdapter()!.peer.isClosed) {
_electrumAdapterChannel = null;
2024-04-23 22:04:29 +00:00
await ClientManager.sharedInstance.remove(cryptoCurrency: cryptoCurrency);
}
2024-04-18 23:17:45 +00:00
final String useHost;
final int usePort;
final bool useUseSSL;
if (currentFailoverIndex == -1) {
2024-04-18 23:17:45 +00:00
useHost = host;
usePort = port;
useUseSSL = useSSL;
2022-08-26 08:11:35 +00:00
} else {
2024-04-18 23:17:45 +00:00
useHost = failovers![currentFailoverIndex].address;
usePort = failovers![currentFailoverIndex].port;
useUseSSL = failovers![currentFailoverIndex].useSSL;
}
_electrumAdapterChannel ??= await electrum_adapter.connect(
useHost,
port: usePort,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
aliveTimerDuration: connectionTimeoutForSpecialCaseJsonRPCClients,
acceptUnverified: true,
useSSL: useUseSSL,
proxyInfo: proxyInfo,
);
if (getElectrumAdapter() == null) {
final ElectrumClient newClient;
if (cryptoCurrency is Firo) {
newClient = FiroElectrumClient(
_electrumAdapterChannel!,
2024-04-18 23:17:45 +00:00
useHost,
usePort,
useUseSSL,
proxyInfo,
);
} else {
2024-04-18 23:17:45 +00:00
newClient = ElectrumClient(
_electrumAdapterChannel!,
2024-04-18 23:17:45 +00:00
useHost,
usePort,
useUseSSL,
proxyInfo,
);
}
2024-04-18 23:17:45 +00:00
await ClientManager.sharedInstance.addClient(
2024-04-18 23:17:45 +00:00
newClient,
cryptoCurrency: cryptoCurrency,
netType: netType,
2024-04-18 23:17:45 +00:00
);
2022-08-26 08:11:35 +00:00
}
return;
}
/// Send raw rpc command
Future<dynamic> request({
required String command,
List<dynamic> args = const [],
String? requestID,
int retries = 2,
Duration requestTimeout = const Duration(seconds: 60),
}) async {
if (!(await _allow())) {
throw WifiOnlyException();
}
2023-09-08 16:54:31 +00:00
if (_requireMutex) {
await _torConnectingLock
2024-06-24 20:23:39 +00:00
.protect(() async => await checkElectrumAdapter());
2023-09-08 16:54:31 +00:00
} else {
2024-06-24 20:23:39 +00:00
await checkElectrumAdapter();
2023-09-08 16:54:31 +00:00
}
2022-08-26 08:11:35 +00:00
try {
2024-04-18 23:17:45 +00:00
final response = await getElectrumAdapter()!.request(
command,
args,
2023-07-27 20:39:48 +00:00
);
2022-08-26 08:11:35 +00:00
if (response is Map &&
response.keys.contains("error") &&
response["error"] != null) {
if (response["error"]
2023-01-27 18:22:30 +00:00
.toString()
.contains("No such mempool or blockchain transaction")) {
throw NoSuchTransactionException(
"No such mempool or blockchain transaction",
args.first.toString(),
);
2023-01-27 18:22:30 +00:00
}
throw Exception(
2023-05-29 15:16:25 +00:00
"JSONRPC response\n"
" command: $command\n"
" error: ${response["error"]}\n"
" args: $args\n",
2023-05-29 15:16:25 +00:00
);
2022-08-26 08:11:35 +00:00
}
currentFailoverIndex = -1;
2024-02-16 22:55:24 +00:00
// If the command is a ping, a good return should always be null.
if (command.contains("ping")) {
return true;
}
return response;
2022-08-26 08:11:35 +00:00
} on WifiOnlyException {
rethrow;
} on ClearnetOnlyException {
rethrow;
} on TorOnlyException {
rethrow;
2022-08-26 08:11:35 +00:00
} on SocketException {
// likely timed out so then retry
if (retries > 0) {
return request(
command: command,
args: args,
2023-07-27 20:39:48 +00:00
requestTimeout: requestTimeout,
2022-08-26 08:11:35 +00:00
requestID: requestID,
retries: retries - 1,
);
} else {
rethrow;
}
} catch (e) {
if (failovers != null && currentFailoverIndex < failovers!.length - 1) {
currentFailoverIndex++;
return request(
command: command,
args: args,
2023-07-27 20:39:48 +00:00
requestTimeout: requestTimeout,
2022-08-26 08:11:35 +00:00
requestID: requestID,
);
} else {
currentFailoverIndex = -1;
rethrow;
}
}
}
/// send a batch request with [command] where [args] is a
/// map of <request id string : arguments list>
///
/// returns a list of json response objects if no errors were found
Future<List<dynamic>> batchRequest({
2022-08-26 08:11:35 +00:00
required String command,
required List<dynamic> args,
2023-07-27 20:39:48 +00:00
Duration requestTimeout = const Duration(seconds: 60),
2022-08-26 08:11:35 +00:00
int retries = 2,
}) async {
if (!(await _allow())) {
throw WifiOnlyException();
}
2023-09-08 16:54:31 +00:00
if (_requireMutex) {
await _torConnectingLock
2024-06-24 20:23:39 +00:00
.protect(() async => await checkElectrumAdapter());
2023-09-08 16:54:31 +00:00
} else {
2024-06-24 20:23:39 +00:00
await checkElectrumAdapter();
2023-09-08 16:54:31 +00:00
}
2022-08-26 08:11:35 +00:00
try {
2024-05-09 18:44:56 +00:00
final futures = <Future<dynamic>>[];
2024-04-18 23:17:45 +00:00
getElectrumAdapter()!.peer.withBatch(() {
for (final arg in args) {
2024-04-18 23:17:45 +00:00
futures.add(getElectrumAdapter()!.request(command, arg));
}
});
final response = await Future.wait(futures);
// We cannot modify the response list as the order and length are related
// to the order and length of the batched requests!
//
// // check for errors, format and throw if there are any
// final List<String> errors = [];
// for (int i = 0; i < response.length; i++) {
// var result = response[i];
//
// if (result == null || (result is List && result.isEmpty)) {
// continue;
// // TODO [prio=extreme]: Figure out if this is actually an issue.
// }
// result = result[0]; // Unwrap the list.
// if ((result is Map && result.keys.contains("error")) ||
// result == null) {
// errors.add(result.toString());
// }
// }
// if (errors.isNotEmpty) {
// String error = "[\n";
// for (int i = 0; i < errors.length; i++) {
// error += "${errors[i]}\n";
// }
// error += "]";
// throw Exception("JSONRPC response error: $error");
// }
2022-08-26 08:11:35 +00:00
currentFailoverIndex = -1;
return response;
2022-08-26 08:11:35 +00:00
} on WifiOnlyException {
rethrow;
} on ClearnetOnlyException {
rethrow;
} on TorOnlyException {
rethrow;
2022-08-26 08:11:35 +00:00
} on SocketException {
// likely timed out so then retry
if (retries > 0) {
return batchRequest(
command: command,
args: args,
2023-07-27 20:39:48 +00:00
requestTimeout: requestTimeout,
2022-08-26 08:11:35 +00:00
retries: retries - 1,
);
} else {
rethrow;
}
} catch (e) {
if (failovers != null && currentFailoverIndex < failovers!.length - 1) {
currentFailoverIndex++;
return batchRequest(
command: command,
args: args,
2023-07-27 20:39:48 +00:00
requestTimeout: requestTimeout,
2022-08-26 08:11:35 +00:00
);
} else {
currentFailoverIndex = -1;
rethrow;
}
}
}
/// Ping the server to ensure it is responding
///
/// Returns true if ping succeeded
Future<bool> ping({String? requestID, int retryCount = 1}) async {
try {
2024-02-16 22:55:24 +00:00
// This doesn't work because electrum_adapter only returns the result:
2024-02-16 22:46:24 +00:00
// (which is always `null`).
// await checkElectrumAdapter();
// final response = await electrumAdapterClient!
// .ping()
// .timeout(const Duration(seconds: 2));
// return (response as Map<String, dynamic>).isNotEmpty;
2024-02-16 22:55:24 +00:00
// 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(
2022-08-26 08:11:35 +00:00
requestID: requestID,
command: 'server.ping',
requestTimeout: const Duration(seconds: 3),
2022-08-26 08:11:35 +00:00
retries: retryCount,
2024-10-01 22:13:36 +00:00
).timeout(
const Duration(seconds: 3),
2024-10-01 22:13:36 +00:00
onTimeout: () {
Logging.instance.log(
"ElectrumxClient.ping timed out with retryCount=$retryCount, host=$_host",
level: LogLevel.Debug,
);
},
) as bool;
2022-08-26 08:11:35 +00:00
} catch (e) {
rethrow;
}
}
/// Get most recent block header.
///
/// Returns a map with keys 'height' and 'hex' corresponding to the block height
/// and the binary header as a hexadecimal string.
/// Ex:
/// {
2023-12-14 14:31:15 +00:00
/// "height": 520481,
/// "hex": "00000020890208a0ae3a3892aa047c5468725846577cfcd9b512b50000000000000000005dc2b02f2d297a9064ee103036c14d678f9afc7e3d9409cf53fd58b82e938e8ecbeca05a2d2103188ce804c4"
/// }
2022-08-26 08:11:35 +00:00
Future<Map<String, dynamic>> getBlockHeadTip({String? requestID}) async {
try {
final response = await request(
requestID: requestID,
command: 'blockchain.headers.subscribe',
);
if (response == null) {
Logging.instance.log(
"getBlockHeadTip returned null response",
level: LogLevel.Error,
);
throw 'getBlockHeadTip returned null response';
}
return Map<String, dynamic>.from(response as Map);
2022-08-26 08:11:35 +00:00
} catch (e) {
rethrow;
}
}
/// Get server info
///
/// Returns a map with server information
/// Ex:
2023-12-14 14:31:15 +00:00
/// {
/// "genesis_hash": "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943",
/// "hosts": {"14.3.140.101": {"tcp_port": 51001, "ssl_port": 51002}},
/// "protocol_max": "1.0",
/// "protocol_min": "1.0",
/// "pruning": null,
/// "server_version": "ElectrumX 1.0.17",
/// "hash_function": "sha256"
/// }
2022-08-26 08:11:35 +00:00
Future<Map<String, dynamic>> getServerFeatures({String? requestID}) async {
try {
final response = await request(
requestID: requestID,
command: 'server.features',
);
return Map<String, dynamic>.from(response as Map);
2022-08-26 08:11:35 +00:00
} catch (e) {
rethrow;
}
}
/// Broadcast a transaction to the network.
///
/// The transaction hash as a hexadecimal string.
Future<String> broadcastTransaction({
required String rawTx,
String? requestID,
}) async {
try {
final response = await request(
requestID: requestID,
command: 'blockchain.transaction.broadcast',
args: [
rawTx,
],
);
return response as String;
2022-08-26 08:11:35 +00:00
} catch (e) {
rethrow;
}
}
/// Return the confirmed and unconfirmed balances for the scripthash of a given scripthash
///
/// Returns a map with keys confirmed and unconfirmed. The value of each is
/// the appropriate balance in minimum coin units (satoshis).
/// Ex:
/// {
/// "confirmed": 103873966,
/// "unconfirmed": 23684400
/// }
Future<Map<String, dynamic>> getBalance({
required String scripthash,
String? requestID,
}) async {
try {
final response = await request(
requestID: requestID,
command: 'blockchain.scripthash.get_balance',
args: [
scripthash,
],
);
return Map<String, dynamic>.from(response as Map);
2022-08-26 08:11:35 +00:00
} catch (e) {
rethrow;
}
}
/// Return the confirmed and unconfirmed history for the given scripthash.
///
/// Returns a list of maps that contain the tx_hash and height of the tx.
/// Ex:
/// [
2023-12-14 14:31:15 +00:00
/// {
/// "height": 200004,
/// "tx_hash": "acc3758bd2a26f869fcc67d48ff30b96464d476bca82c1cd6656e7d506816412"
/// },
/// {
/// "height": 215008,
/// "tx_hash": "f3e1bf48975b8d6060a9de8884296abb80be618dc00ae3cb2f6cee3085e09403"
/// }
/// ]
2022-08-26 08:11:35 +00:00
Future<List<Map<String, dynamic>>> getHistory({
required String scripthash,
String? requestID,
}) async {
try {
int retryCount = 3;
dynamic result;
while (retryCount > 0 && result is! List) {
final response = await request(
requestID: requestID,
command: 'blockchain.scripthash.get_history',
requestTimeout: const Duration(minutes: 5),
args: [
scripthash,
],
);
result = response;
retryCount--;
}
return List<Map<String, dynamic>>.from(result as List);
2022-08-26 08:11:35 +00:00
} catch (e) {
rethrow;
}
}
Future<List<List<Map<String, dynamic>>>> getBatchHistory({
required List<dynamic> args,
}) async {
2022-08-26 08:11:35 +00:00
try {
final response = await batchRequest(
command: 'blockchain.scripthash.get_history',
args: args,
);
final List<List<Map<String, dynamic>>> result = [];
2022-08-26 08:11:35 +00:00
for (int i = 0; i < response.length; i++) {
result.add(List<Map<String, dynamic>>.from(response[i] as List));
2022-08-26 08:11:35 +00:00
}
return result;
} catch (e) {
rethrow;
}
}
/// Return an ordered list of UTXOs sent to a script hash of the given scripthash.
///
/// Returns a list of maps.
/// Ex:
/// [
2023-12-14 14:31:15 +00:00
/// {
/// "tx_pos": 0,
/// "value": 45318048,
/// "tx_hash": "9f2c45a12db0144909b5db269415f7319179105982ac70ed80d76ea79d923ebf",
/// "height": 437146
/// },
/// {
/// "tx_pos": 0,
/// "value": 919195,
/// "tx_hash": "3d2290c93436a3e964cfc2f0950174d8847b1fbe3946432c4784e168da0f019f",
/// "height": 441696
/// }
/// ]
2022-08-26 08:11:35 +00:00
Future<List<Map<String, dynamic>>> getUTXOs({
required String scripthash,
String? requestID,
}) async {
try {
final response = await request(
requestID: requestID,
command: 'blockchain.scripthash.listunspent',
args: [
scripthash,
],
);
return List<Map<String, dynamic>>.from(response as List);
2022-08-26 08:11:35 +00:00
} catch (e) {
rethrow;
}
}
Future<List<List<Map<String, dynamic>>>> getBatchUTXOs({
required List<dynamic> args,
}) async {
2022-08-26 08:11:35 +00:00
try {
final response = await batchRequest(
command: 'blockchain.scripthash.listunspent',
args: args,
);
final List<List<Map<String, dynamic>>> result = [];
2022-08-26 08:11:35 +00:00
for (int i = 0; i < response.length; i++) {
if ((response[i] as List).isNotEmpty) {
try {
final data = List<Map<String, dynamic>>.from(response[i] as List);
result.add(data);
} catch (e) {
// to ensure we keep same length of responses as requests/args
// add empty list on error
result.add([]);
Logging.instance.log(
"getBatchUTXOs failed to parse response=${response[i]}: $e",
level: LogLevel.Error,
);
}
}
2022-08-26 08:11:35 +00:00
}
return result;
} catch (e) {
rethrow;
}
}
/// Returns a raw transaction given the tx_hash.
///
/// Returns a list of maps.
/// Ex when verbose=false:
/// "01000000015bb9142c960a838329694d3fe9ba08c2a6421c5158d8f7044cb7c48006c1b48"
/// "4000000006a4730440220229ea5359a63c2b83a713fcc20d8c41b20d48fe639a639d2a824"
/// "6a137f29d0fc02201de12de9c056912a4e581a62d12fb5f43ee6c08ed0238c32a1ee76921"
/// "3ca8b8b412103bcf9a004f1f7a9a8d8acce7b51c983233d107329ff7c4fb53e44c855dbe1"
/// "f6a4feffffff02c6b68200000000001976a9141041fb024bd7a1338ef1959026bbba86006"
/// "4fe5f88ac50a8cf00000000001976a91445dac110239a7a3814535c15858b939211f85298"
/// "88ac61ee0700"
///
///
/// Ex when verbose=true:
/// {
/// "blockhash": "0000000000000000015a4f37ece911e5e3549f988e855548ce7494a0a08b2ad6",
/// "blocktime": 1520074861,
/// "confirmations": 679,
/// "hash": "36a3692a41a8ac60b73f7f41ee23f5c917413e5b2fad9e44b34865bd0d601a3d",
/// "hex": "01000000015bb9142c960a838329694d3fe9ba08c2a6421c5158d8f7044cb7c48006c1b484000000006a4730440220229ea5359a63c2b83a713fcc20d8c41b20d48fe639a639d2a8246a137f29d0fc02201de12de9c056912a4e581a62d12fb5f43ee6c08ed0238c32a1ee769213ca8b8b412103bcf9a004f1f7a9a8d8acce7b51c983233d107329ff7c4fb53e44c855dbe1f6a4feffffff02c6b68200000000001976a9141041fb024bd7a1338ef1959026bbba860064fe5f88ac50a8cf00000000001976a91445dac110239a7a3814535c15858b939211f8529888ac61ee0700",
/// "locktime": 519777,
/// "size": 225,
/// "time": 1520074861,
/// "txid": "36a3692a41a8ac60b73f7f41ee23f5c917413e5b2fad9e44b34865bd0d601a3d",
/// "version": 1,
/// "vin": [ {
/// "scriptSig": {
/// "asm": "30440220229ea5359a63c2b83a713fcc20d8c41b20d48fe639a639d2a8246a137f29d0fc02201de12de9c056912a4e581a62d12fb5f43ee6c08ed0238c32a1ee769213ca8b8b[ALL|FORKID] 03bcf9a004f1f7a9a8d8acce7b51c983233d107329ff7c4fb53e44c855dbe1f6a4",
/// "hex": "4730440220229ea5359a63c2b83a713fcc20d8c41b20d48fe639a639d2a8246a137f29d0fc02201de12de9c056912a4e581a62d12fb5f43ee6c08ed0238c32a1ee769213ca8b8b412103bcf9a004f1f7a9a8d8acce7b51c983233d107329ff7c4fb53e44c855dbe1f6a4"
/// },
/// "sequence": 4294967294,
/// "txid": "84b4c10680c4b74c04f7d858511c42a6c208bae93f4d692983830a962c14b95b",
/// "vout": 0}],
/// "vout": [ { "n": 0,
/// "scriptPubKey": { "addresses": [ "12UxrUZ6tyTLoR1rT1N4nuCgS9DDURTJgP"],
/// "asm": "OP_DUP OP_HASH160 1041fb024bd7a1338ef1959026bbba860064fe5f OP_EQUALVERIFY OP_CHECKSIG",
/// "hex": "76a9141041fb024bd7a1338ef1959026bbba860064fe5f88ac",
/// "reqSigs": 1,
/// "type": "pubkeyhash"},
/// "value": 0.0856647},
/// { "n": 1,
/// "scriptPubKey": { "addresses": [ "17NMgYPrguizvpJmB1Sz62ZHeeFydBYbZJ"],
/// "asm": "OP_DUP OP_HASH160 45dac110239a7a3814535c15858b939211f85298 OP_EQUALVERIFY OP_CHECKSIG",
/// "hex": "76a91445dac110239a7a3814535c15858b939211f8529888ac",
/// "reqSigs": 1,
/// "type": "pubkeyhash"},
/// "value": 0.1360904}]}
Future<Map<String, dynamic>> getTransaction({
required String txHash,
bool verbose = true,
String? requestID,
}) async {
2024-05-09 18:44:56 +00:00
Logging.instance.log(
"attempting to fetch blockchain.transaction.get...",
level: LogLevel.Info,
);
2024-06-24 20:23:39 +00:00
await checkElectrumAdapter();
2024-05-09 18:44:56 +00:00
final dynamic response = await getElectrumAdapter()!.getTransaction(txHash);
Logging.instance.log(
"Fetching blockchain.transaction.get finished",
level: LogLevel.Info,
);
if (!verbose) {
return {"rawtx": response as String};
2022-08-26 08:11:35 +00:00
}
return Map<String, dynamic>.from(response as Map);
2022-08-26 08:11:35 +00:00
}
2023-11-16 18:15:28 +00:00
/// Returns the whole Lelantus anonymity set for denomination in the groupId.
2022-08-26 08:11:35 +00:00
///
/// ex:
/// {
/// "blockHash": "37effb57352693f4efcb1710bf68e3a0d79ff6b8f1605529de3e0706d9ca21da",
/// "setHash": "aae1a64f19f5ccce1c242dfe331d8db2883a9508d998efa3def8a64844170fe4",
/// "coins": [
/// [dynamic list of length 4],
/// [dynamic list of length 4],
/// ....
/// [dynamic list of length 4],
/// [dynamic list of length 4],
/// ]
/// }
2023-11-16 18:15:28 +00:00
Future<Map<String, dynamic>> getLelantusAnonymitySet({
2022-08-26 08:11:35 +00:00
String groupId = "1",
String blockhash = "",
String? requestID,
}) async {
2024-05-09 18:44:56 +00:00
Logging.instance.log(
"attempting to fetch lelantus.getanonymityset...",
level: LogLevel.Info,
);
2024-06-24 20:23:39 +00:00
await checkElectrumAdapter();
2024-05-09 18:44:56 +00:00
final Map<String, dynamic> response =
2024-04-18 23:17:45 +00:00
await (getElectrumAdapter() as FiroElectrumClient)
.getLelantusAnonymitySet(groupId: groupId, blockHash: blockhash);
2024-05-09 18:44:56 +00:00
Logging.instance.log(
"Fetching lelantus.getanonymityset finished",
level: LogLevel.Info,
);
return response;
2022-08-26 08:11:35 +00:00
}
//TODO add example to docs
///
///
2023-11-16 18:15:28 +00:00
/// Returns the block height and groupId of a Lelantus pubcoin.
Future<dynamic> getLelantusMintData({
dynamic mints,
String? requestID,
}) async {
2024-05-09 18:44:56 +00:00
Logging.instance.log(
"attempting to fetch lelantus.getmintmetadata...",
level: LogLevel.Info,
);
2024-06-24 20:23:39 +00:00
await checkElectrumAdapter();
2024-05-09 18:44:56 +00:00
final dynamic response = await (getElectrumAdapter() as FiroElectrumClient)
.getLelantusMintData(mints: mints);
2024-05-09 18:44:56 +00:00
Logging.instance.log(
"Fetching lelantus.getmintmetadata finished",
level: LogLevel.Info,
);
return response;
2022-08-26 08:11:35 +00:00
}
//TODO add example to docs
2023-11-16 18:15:28 +00:00
/// Returns the whole set of the used Lelantus coin serials.
Future<Map<String, dynamic>> getLelantusUsedCoinSerials({
2022-08-26 08:11:35 +00:00
String? requestID,
required int startNumber,
}) async {
2024-05-09 18:44:56 +00:00
Logging.instance.log(
"attempting to fetch lelantus.getusedcoinserials...",
level: LogLevel.Info,
);
2024-06-24 20:23:39 +00:00
await checkElectrumAdapter();
2024-02-13 21:34:45 +00:00
int retryCount = 3;
dynamic response;
while (retryCount > 0 && response is! List) {
2024-04-18 23:17:45 +00:00
response = await (getElectrumAdapter() as FiroElectrumClient)
.getLelantusUsedCoinSerials(startNumber: startNumber);
// TODO add 2 minute timeout.
2024-05-09 18:44:56 +00:00
Logging.instance.log(
"Fetching lelantus.getusedcoinserials finished",
level: LogLevel.Info,
);
retryCount--;
2022-08-26 08:11:35 +00:00
}
return Map<String, dynamic>.from(response as Map);
2022-08-26 08:11:35 +00:00
}
2023-11-16 18:15:28 +00:00
/// Returns the latest Lelantus set id
2022-08-26 08:11:35 +00:00
///
/// ex: 1
2023-11-16 18:15:28 +00:00
Future<int> getLelantusLatestCoinId({String? requestID}) async {
2024-05-09 18:44:56 +00:00
Logging.instance.log(
"attempting to fetch lelantus.getlatestcoinid...",
level: LogLevel.Info,
);
2024-06-24 20:23:39 +00:00
await checkElectrumAdapter();
2024-05-09 18:44:56 +00:00
final int response =
2024-04-18 23:17:45 +00:00
await (getElectrumAdapter() as FiroElectrumClient).getLatestCoinId();
2024-05-09 18:44:56 +00:00
Logging.instance.log(
"Fetching lelantus.getlatestcoinid finished",
level: LogLevel.Info,
);
return response;
2022-08-26 08:11:35 +00:00
}
2023-11-16 18:15:28 +00:00
// ============== Spark ======================================================
// New Spark ElectrumX methods:
// > Functions provided by ElectrumX servers
// > // >
/// Returns the whole Spark anonymity set for denomination in the groupId.
///
/// Takes [coinGroupId] and [startBlockHash], if the last is empty it returns full set,
/// otherwise returns mint after that block, we need to call this to keep our
/// anonymity set data up to date.
///
/// Returns blockHash (last block hash),
/// setHash (hash of current set)
2023-12-07 21:30:35 +00:00
/// and coins (the list of pairs serialized coin and tx hash)
2023-11-16 18:15:28 +00:00
Future<Map<String, dynamic>> getSparkAnonymitySet({
String coinGroupId = "1",
String startBlockHash = "",
String? requestID,
}) async {
try {
2024-05-29 19:29:45 +00:00
final start = DateTime.now();
2024-06-24 20:23:39 +00:00
await checkElectrumAdapter();
2024-05-09 18:44:56 +00:00
final Map<String, dynamic> response =
2024-04-18 23:17:45 +00:00
await (getElectrumAdapter() as FiroElectrumClient)
.getSparkAnonymitySet(
2024-05-09 18:44:56 +00:00
coinGroupId: coinGroupId,
startBlockHash: startBlockHash,
);
Logging.instance.log(
2024-05-29 19:29:45 +00:00
"Finished ElectrumXClient.getSparkAnonymitySet(coinGroupId"
"=$coinGroupId, startBlockHash=$startBlockHash). "
"coins.length: ${(response["coins"] as List?)?.length}"
2024-05-29 19:29:45 +00:00
"Duration=${DateTime.now().difference(start)}",
2024-05-09 18:44:56 +00:00
level: LogLevel.Info,
);
return response;
2023-11-16 18:15:28 +00:00
} catch (e) {
rethrow;
}
}
/// NOT USED. See [getSparkUnhashedUsedCoinsTagsWithTxHashes]
2023-11-16 18:15:28 +00:00
/// Takes [startNumber], if it is 0, we get the full set,
/// otherwise the used tags after that number
// Future<List<String>> getSparkUnhashedUsedCoinsTags({
// String? requestID,
// required int startNumber,
// }) async {
// try {
// final start = DateTime.now();
// await _checkElectrumAdapter();
// final Map<String, dynamic> response =
// await (getElectrumAdapter() as FiroElectrumClient)
// .getUsedCoinsTags(startNumber: startNumber);
// // TODO: Add 2 minute timeout.
// // Why 2 minutes?
// Logging.instance.log(
// "Fetching spark.getusedcoinstags finished",
// level: LogLevel.Info,
// );
// final map = Map<String, dynamic>.from(response);
// final tags = List<String>.from(map["tags"] as List);
//
// Logging.instance.log(
// "Finished ElectrumXClient.getSparkUnhashedUsedCoinsTags(startNumber"
// "=$startNumber). # of tags fetched=${tags.length}, "
// "Duration=${DateTime.now().difference(start)}",
// level: LogLevel.Info,
// );
//
// return tags;
// } catch (e) {
// Logging.instance.log(e, level: LogLevel.Error);
// rethrow;
// }
// }
2023-11-16 18:15:28 +00:00
2023-11-17 15:23:16 +00:00
/// Takes a list of [sparkCoinHashes] and returns the set id and block height
2023-11-16 18:15:28 +00:00
/// for each coin
2023-11-16 20:49:35 +00:00
///
2023-11-17 15:23:16 +00:00
/// arg:
2023-11-16 20:49:35 +00:00
/// {
2023-11-17 15:23:16 +00:00
/// "coinHashes": [
/// "b476ed2b374bb081ea51d111f68f0136252521214e213d119b8dc67b92f5a390",
/// "b476ed2b374bb081ea51d111f68f0136252521214e213d119b8dc67b92f5a390",
2023-11-16 20:49:35 +00:00
/// ]
/// }
2023-11-17 15:23:16 +00:00
Future<List<Map<String, dynamic>>> getSparkMintMetaData({
2023-11-16 18:15:28 +00:00
String? requestID,
2023-11-17 15:23:16 +00:00
required List<String> sparkCoinHashes,
2023-11-16 18:15:28 +00:00
}) async {
try {
2024-05-09 18:44:56 +00:00
Logging.instance.log(
"attempting to fetch spark.getsparkmintmetadata...",
level: LogLevel.Info,
);
2024-06-24 20:23:39 +00:00
await checkElectrumAdapter();
2024-05-09 18:44:56 +00:00
final List<dynamic> response =
2024-04-18 23:17:45 +00:00
await (getElectrumAdapter() as FiroElectrumClient)
.getSparkMintMetaData(sparkCoinHashes: sparkCoinHashes);
2024-05-09 18:44:56 +00:00
Logging.instance.log(
"Fetching spark.getsparkmintmetadata finished",
level: LogLevel.Info,
);
return List<Map<String, dynamic>>.from(response);
2023-11-16 18:15:28 +00:00
} catch (e) {
Logging.instance.log(e, level: LogLevel.Error);
rethrow;
}
}
/// Returns the latest Spark set id
///
/// ex: 1
Future<int> getSparkLatestCoinId({
String? requestID,
}) async {
try {
2024-05-09 18:44:56 +00:00
Logging.instance.log(
"attempting to fetch spark.getsparklatestcoinid...",
level: LogLevel.Info,
);
2024-06-24 20:23:39 +00:00
await checkElectrumAdapter();
2024-05-09 18:44:56 +00:00
final int response = await (getElectrumAdapter() as FiroElectrumClient)
.getSparkLatestCoinId();
2024-05-09 18:44:56 +00:00
Logging.instance.log(
"Fetching spark.getsparklatestcoinid finished",
level: LogLevel.Info,
);
return response;
2023-11-16 18:15:28 +00:00
} catch (e) {
Logging.instance.log(e, level: LogLevel.Error);
rethrow;
}
}
2024-06-05 16:51:18 +00:00
/// Returns the txids of the current transactions found in the mempool
Future<Set<String>> getMempoolTxids({
String? requestID,
}) async {
try {
final start = DateTime.now();
final response = await request(
requestID: requestID,
command: "spark.getmempoolsparktxids",
2024-06-05 16:51:18 +00:00
);
2024-06-14 19:33:27 +00:00
final txids = List<String>.from(response as List)
.map((e) => e.toHexReversedFromBase64)
.toSet();
2024-06-05 16:51:18 +00:00
Logging.instance.log(
"Finished ElectrumXClient.getMempoolTxids(). "
"Duration=${DateTime.now().difference(start)}",
level: LogLevel.Info,
);
return txids;
} catch (e) {
Logging.instance.log(e, level: LogLevel.Error);
rethrow;
}
}
/// Returns the txids of the current transactions found in the mempool
2024-06-14 19:33:27 +00:00
Future<List<SparkMempoolData>> getMempoolSparkData({
2024-06-05 16:51:18 +00:00
String? requestID,
required List<String> txids,
}) async {
try {
final start = DateTime.now();
final response = await request(
requestID: requestID,
command: "spark.getmempoolsparktxs",
2024-06-14 19:33:27 +00:00
args: [
{
"txids": txids,
},
],
2024-06-05 16:51:18 +00:00
);
final map = Map<String, dynamic>.from(response as Map);
2024-06-14 19:33:27 +00:00
final List<SparkMempoolData> result = [];
for (final entry in map.entries) {
result.add(
(
txid: entry.key,
serialContext:
2024-06-25 21:31:18 +00:00
List<String>.from(entry.value["serial_context"] as List),
2024-06-14 19:33:27 +00:00
// the space after lTags is required lol
lTags: List<String>.from(entry.value["lTags "] as List),
2024-06-25 21:31:18 +00:00
coins: List<String>.from(entry.value["coins"] as List),
2024-06-14 19:33:27 +00:00
),
);
}
2024-06-05 16:51:18 +00:00
Logging.instance.log(
"Finished ElectrumXClient.getMempoolSparkData(txids: $txids). "
"Duration=${DateTime.now().difference(start)}",
level: LogLevel.Info,
);
2024-06-14 19:33:27 +00:00
return result;
} catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Error);
2024-06-05 16:51:18 +00:00
rethrow;
}
}
/// Takes [startNumber], if it is 0, we get the full set,
/// otherwise the used tags and txids after that number
Future<List<List<dynamic>>> getSparkUnhashedUsedCoinsTagsWithTxHashes({
String? requestID,
required int startNumber,
}) async {
try {
final start = DateTime.now();
final response = await request(
requestID: requestID,
command: "spark.getusedcoinstagstxhashes",
args: [
"$startNumber",
],
);
final map = Map<String, dynamic>.from(response as Map);
final tags = List<List<dynamic>>.from(map["tagsandtxids"] as List);
Logging.instance.log(
"Finished ElectrumXClient.getSparkUnhashedUsedCoinsTagsWithTxHashes("
"startNumber=$startNumber). # of tags fetched=${tags.length}, "
"Duration=${DateTime.now().difference(start)}",
level: LogLevel.Info,
);
return tags;
} catch (e) {
Logging.instance.log(e, level: LogLevel.Error);
rethrow;
}
}
2023-11-16 18:15:28 +00:00
// ===========================================================================
2022-08-26 08:11:35 +00:00
2024-06-25 17:59:55 +00:00
Future<bool> isMasterNodeCollateral({
String? requestID,
required String txid,
required int index,
}) async {
try {
final start = DateTime.now();
final response = await request(
requestID: requestID,
command: "blockchain.checkifmncollateral",
args: [
txid,
index.toString(),
],
);
Logging.instance.log(
"Finished ElectrumXClient.isMasterNodeCollateral, "
"response: $response, "
"Duration=${DateTime.now().difference(start)}",
level: LogLevel.Info,
);
return response as bool;
2024-06-25 17:59:55 +00:00
} catch (e) {
Logging.instance.log(e, level: LogLevel.Error);
rethrow;
}
}
// ===========================================================================
2022-08-26 08:11:35 +00:00
/// Get the current fee rate.
///
/// Returns a map with the kay "rate" that corresponds to the free rate in satoshis
/// Ex:
/// {
2023-12-14 14:31:15 +00:00
/// "rate": 1000,
/// }
2022-08-26 08:11:35 +00:00
Future<Map<String, dynamic>> getFeeRate({String? requestID}) async {
2024-06-24 20:23:39 +00:00
await checkElectrumAdapter();
2024-04-18 23:17:45 +00:00
return await getElectrumAdapter()!.getFeeRate();
2022-08-26 08:11:35 +00:00
}
2024-05-09 18:44:56 +00:00
/// Return the estimated transaction fee per kilobyte for a transaction to be
/// confirmed within a certain number of [blocks].
2022-08-26 08:11:35 +00:00
///
/// Returns a Decimal fee rate
/// Ex:
/// 0.00001000
Future<Decimal> estimateFee({String? requestID, required int blocks}) async {
try {
final response = await request(
requestID: requestID,
command: 'blockchain.estimatefee',
args: [
blocks,
],
);
2024-02-10 01:10:37 +00:00
try {
if (response == null ||
response == -1 ||
Decimal.parse(response.toString()) == Decimal.parse("-1")) {
if (cryptoCurrency is BitcoinFrost) {
final rate = Amount(
rawValue: (cryptoCurrency as BitcoinFrost).defaultFeeRate,
fractionDigits: cryptoCurrency.fractionDigits,
);
return rate.decimal;
} else if (cryptoCurrency is ElectrumXCurrencyInterface) {
final rate = Amount(
rawValue:
(cryptoCurrency as ElectrumXCurrencyInterface).defaultFeeRate,
fractionDigits: cryptoCurrency.fractionDigits,
);
return rate.decimal;
} else {
throw Exception("Unexpected cryptoCurrency found!");
}
}
return Decimal.parse(response.toString());
2024-02-10 01:10:37 +00:00
} catch (e, s) {
final String msg = "Error parsing fee rate. Response: $response"
2024-04-18 23:17:45 +00:00
"\nResult: $response\nError: $e\nStack trace: $s";
2024-02-10 01:10:37 +00:00
Logging.instance.log(msg, level: LogLevel.Fatal);
throw Exception(msg);
}
2022-08-26 08:11:35 +00:00
} catch (e) {
rethrow;
}
}
/// Return the minimum fee a low-priority transaction must pay in order to be accepted to the daemons memory pool.
///
/// Returns a Decimal fee rate
/// Ex:
/// 0.00001000
Future<Decimal> relayFee({String? requestID}) async {
try {
final response = await request(
requestID: requestID,
command: 'blockchain.relayfee',
);
return Decimal.parse(response.toString());
2022-08-26 08:11:35 +00:00
} catch (e) {
rethrow;
}
}
}