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
|
|
|
|
|
*
|
|
|
|
|
*/
|
|
|
|
|
|
2023-09-07 18:07:05 +00:00
|
|
|
|
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';
|
2024-02-12 22:30:45 +00:00
|
|
|
|
import 'package:electrum_adapter/electrum_adapter.dart' as electrum_adapter;
|
2024-02-15 01:47:26 +00:00
|
|
|
|
import 'package:electrum_adapter/electrum_adapter.dart';
|
2024-02-13 20:25:03 +00:00
|
|
|
|
import 'package:electrum_adapter/methods/specific/firo.dart';
|
2023-09-07 18:07:05 +00:00
|
|
|
|
import 'package:event_bus/event_bus.dart';
|
2023-12-20 23:46:48 +00:00
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
|
import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart';
|
2023-09-08 16:37:50 +00:00
|
|
|
|
import 'package:mutex/mutex.dart';
|
2024-02-19 20:32:43 +00:00
|
|
|
|
import 'package:stackwallet/electrumx_rpc/electrumx_chain_height_service.dart';
|
2022-08-26 08:11:35 +00:00
|
|
|
|
import 'package:stackwallet/electrumx_rpc/rpc.dart';
|
2023-01-27 18:22:30 +00:00
|
|
|
|
import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart';
|
2023-09-08 16:37:50 +00:00
|
|
|
|
import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart';
|
2023-09-07 18:07:05 +00:00
|
|
|
|
import 'package:stackwallet/services/event_bus/events/global/tor_status_changed_event.dart';
|
|
|
|
|
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
|
2023-09-07 21:27:00 +00:00
|
|
|
|
import 'package:stackwallet/services/tor_service.dart';
|
2024-02-15 01:47:26 +00:00
|
|
|
|
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
2022-08-26 08:11:35 +00:00
|
|
|
|
import 'package:stackwallet/utilities/logger.dart';
|
|
|
|
|
import 'package:stackwallet/utilities/prefs.dart';
|
2024-02-15 01:47:26 +00:00
|
|
|
|
import 'package:stream_channel/stream_channel.dart';
|
2022-08-26 08:11:35 +00:00
|
|
|
|
|
|
|
|
|
class WifiOnlyException implements Exception {}
|
|
|
|
|
|
|
|
|
|
class ElectrumXNode {
|
|
|
|
|
ElectrumXNode({
|
|
|
|
|
required this.address,
|
|
|
|
|
required this.port,
|
|
|
|
|
required this.name,
|
|
|
|
|
required this.id,
|
|
|
|
|
required this.useSSL,
|
|
|
|
|
});
|
|
|
|
|
final String address;
|
|
|
|
|
final int port;
|
|
|
|
|
final String name;
|
|
|
|
|
final String id;
|
|
|
|
|
final bool useSSL;
|
|
|
|
|
|
|
|
|
|
factory ElectrumXNode.from(ElectrumXNode node) {
|
|
|
|
|
return ElectrumXNode(
|
|
|
|
|
address: node.address,
|
|
|
|
|
port: node.port,
|
|
|
|
|
name: node.name,
|
|
|
|
|
id: node.id,
|
|
|
|
|
useSSL: node.useSSL,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
String toString() {
|
|
|
|
|
return "ElectrumXNode: {address: $address, port: $port, name: $name, useSSL: $useSSL}";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-14 20:31:53 +00:00
|
|
|
|
class ElectrumXClient {
|
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;
|
|
|
|
|
|
|
|
|
|
JsonRPC? get rpcClient => _rpcClient;
|
|
|
|
|
JsonRPC? _rpcClient;
|
|
|
|
|
|
2024-02-15 01:47:26 +00:00
|
|
|
|
StreamChannel<dynamic>? get electrumAdapterChannel => _electrumAdapterChannel;
|
|
|
|
|
StreamChannel<dynamic>? _electrumAdapterChannel;
|
|
|
|
|
|
|
|
|
|
ElectrumClient? get electrumAdapterClient => _electrumAdapterClient;
|
|
|
|
|
ElectrumClient? _electrumAdapterClient;
|
|
|
|
|
|
2022-08-26 08:11:35 +00:00
|
|
|
|
late Prefs _prefs;
|
2023-09-07 18:07:05 +00:00
|
|
|
|
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;
|
|
|
|
|
|
2024-02-15 02:04:26 +00:00
|
|
|
|
Coin? get coin => _coin;
|
|
|
|
|
late Coin? _coin;
|
2024-02-15 01:47:26 +00:00
|
|
|
|
|
2023-09-07 18:27:19 +00:00
|
|
|
|
// 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-07 18:27:19 +00:00
|
|
|
|
);
|
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-08-16 03:56:41 +00:00
|
|
|
|
|
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 List<ElectrumXNode> failovers,
|
2024-02-15 02:04:26 +00:00
|
|
|
|
Coin? coin,
|
2023-07-27 20:39:48 +00:00
|
|
|
|
this.connectionTimeoutForSpecialCaseJsonRPCClients =
|
|
|
|
|
const Duration(seconds: 60),
|
2023-09-07 18:07:05 +00:00
|
|
|
|
TorService? torService,
|
|
|
|
|
EventBus? globalEventBusForTesting,
|
2023-07-27 20:39:48 +00:00
|
|
|
|
}) {
|
2022-08-26 08:11:35 +00:00
|
|
|
|
_prefs = prefs;
|
2023-09-07 18:07:05 +00:00
|
|
|
|
_torService = torService ?? TorService.sharedInstance;
|
2022-08-26 08:11:35 +00:00
|
|
|
|
_host = host;
|
|
|
|
|
_port = port;
|
|
|
|
|
_useSSL = useSSL;
|
2024-02-15 01:47:26 +00:00
|
|
|
|
_coin = coin;
|
2023-09-07 18:07:05 +00:00
|
|
|
|
|
|
|
|
|
final bus = globalEventBusForTesting ?? GlobalEventBus.instance;
|
2024-02-19 20:32:43 +00:00
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
);
|
2024-02-19 20:32:43 +00:00
|
|
|
|
|
|
|
|
|
// Listen for tor preference changes.
|
2023-09-08 16:37:50 +00:00
|
|
|
|
_torPreferenceListener = bus.on<TorPreferenceChangedEvent>().listen(
|
2023-09-07 18:07:05 +00:00
|
|
|
|
(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
|
2024-02-15 01:47:26 +00:00
|
|
|
|
_electrumAdapterChannel = null;
|
|
|
|
|
_electrumAdapterClient = null;
|
2024-02-19 20:32:43 +00:00
|
|
|
|
|
|
|
|
|
// Also close any chain height services that are currently open.
|
|
|
|
|
await ChainHeightServiceManager.dispose();
|
2023-09-07 18:07:05 +00:00
|
|
|
|
},
|
|
|
|
|
);
|
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-02-15 01:47:26 +00:00
|
|
|
|
required Coin coin,
|
2023-09-07 18:07:05 +00:00
|
|
|
|
TorService? torService,
|
|
|
|
|
EventBus? globalEventBusForTesting,
|
2023-08-16 03:56:41 +00:00
|
|
|
|
}) {
|
2023-11-14 20:31:53 +00:00
|
|
|
|
return ElectrumXClient(
|
2023-09-07 18:07:05 +00:00
|
|
|
|
host: node.address,
|
|
|
|
|
port: node.port,
|
|
|
|
|
useSSL: node.useSSL,
|
|
|
|
|
prefs: prefs,
|
|
|
|
|
torService: torService,
|
|
|
|
|
failovers: failovers,
|
|
|
|
|
globalEventBusForTesting: globalEventBusForTesting,
|
2024-02-15 01:47:26 +00:00
|
|
|
|
coin: coin,
|
2023-09-07 18:07:05 +00:00
|
|
|
|
);
|
2023-08-16 03:56:41 +00:00
|
|
|
|
}
|
2022-08-26 08:11:35 +00:00
|
|
|
|
|
|
|
|
|
Future<bool> _allow() async {
|
|
|
|
|
if (_prefs.wifiOnly) {
|
|
|
|
|
return (await Connectivity().checkConnectivity()) ==
|
|
|
|
|
ConnectivityResult.wifi;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-15 23:53:39 +00:00
|
|
|
|
Future<void> checkElectrumAdapter() async {
|
2024-02-15 01:47:26 +00:00
|
|
|
|
({InternetAddress host, int port})? proxyInfo;
|
|
|
|
|
|
|
|
|
|
// If we're supposed to use Tor...
|
|
|
|
|
if (_prefs.useTor) {
|
|
|
|
|
// But Tor isn't running...
|
|
|
|
|
if (_torService.status != TorConnectionStatus.connected) {
|
|
|
|
|
// And the killswitch isn't set...
|
|
|
|
|
if (!_prefs.torKillSwitch) {
|
|
|
|
|
// Then we'll just proceed and connect to ElectrumX through clearnet at the bottom of this function.
|
|
|
|
|
Logging.instance.log(
|
|
|
|
|
"Tor preference set but Tor is not enabled, killswitch not set, connecting to Electrum adapter through clearnet",
|
|
|
|
|
level: LogLevel.Warning,
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
// ... But if the killswitch is set, then we throw an exception.
|
|
|
|
|
throw Exception(
|
|
|
|
|
"Tor preference and killswitch set but Tor is not enabled, not connecting to Electrum adapter");
|
|
|
|
|
// TODO [prio=low]: Try to start Tor.
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Get the proxy info from the TorService.
|
|
|
|
|
proxyInfo = _torService.getProxyInfo();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO [prio=med]: Add proxyInfo to StreamChannel (or add to wrapper).
|
|
|
|
|
// if (_electrumAdapter!.proxyInfo != proxyInfo) {
|
|
|
|
|
// _electrumAdapter!.proxyInfo = proxyInfo;
|
|
|
|
|
// _electrumAdapter!.disconnect(
|
|
|
|
|
// reason: "Tor proxyInfo does not match current info",
|
|
|
|
|
// );
|
|
|
|
|
// }
|
2023-09-07 18:07:05 +00:00
|
|
|
|
|
2024-02-15 21:43:47 +00:00
|
|
|
|
// If the current ElectrumAdapterClient is closed, create a new one.
|
|
|
|
|
if (_electrumAdapterClient != null &&
|
|
|
|
|
_electrumAdapterClient!.peer.isClosed) {
|
|
|
|
|
_electrumAdapterChannel = null;
|
|
|
|
|
_electrumAdapterClient = null;
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-08 15:50:50 +00:00
|
|
|
|
if (currentFailoverIndex == -1) {
|
2024-02-15 01:47:26 +00:00
|
|
|
|
_electrumAdapterChannel ??= await electrum_adapter.connect(
|
|
|
|
|
host,
|
2023-09-08 15:50:50 +00:00
|
|
|
|
port: port,
|
|
|
|
|
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
|
2024-02-15 01:47:26 +00:00
|
|
|
|
aliveTimerDuration: connectionTimeoutForSpecialCaseJsonRPCClients,
|
|
|
|
|
acceptUnverified: true,
|
|
|
|
|
useSSL: useSSL,
|
|
|
|
|
proxyInfo: proxyInfo,
|
2023-09-08 15:50:50 +00:00
|
|
|
|
);
|
2024-02-15 01:47:26 +00:00
|
|
|
|
if (_coin == Coin.firo || _coin == Coin.firoTestNet) {
|
|
|
|
|
_electrumAdapterClient ??= FiroElectrumClient(
|
|
|
|
|
_electrumAdapterChannel!,
|
|
|
|
|
host,
|
|
|
|
|
port,
|
|
|
|
|
useSSL,
|
|
|
|
|
proxyInfo,
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
_electrumAdapterClient ??= ElectrumClient(
|
|
|
|
|
_electrumAdapterChannel!,
|
|
|
|
|
host,
|
|
|
|
|
port,
|
|
|
|
|
useSSL,
|
|
|
|
|
proxyInfo,
|
|
|
|
|
);
|
|
|
|
|
}
|
2022-08-26 08:11:35 +00:00
|
|
|
|
} else {
|
2024-02-15 01:47:26 +00:00
|
|
|
|
_electrumAdapterChannel ??= await electrum_adapter.connect(
|
|
|
|
|
failovers![currentFailoverIndex].address,
|
2023-09-08 15:50:50 +00:00
|
|
|
|
port: failovers![currentFailoverIndex].port,
|
|
|
|
|
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
|
2024-02-15 01:47:26 +00:00
|
|
|
|
aliveTimerDuration: connectionTimeoutForSpecialCaseJsonRPCClients,
|
|
|
|
|
acceptUnverified: true,
|
|
|
|
|
useSSL: failovers![currentFailoverIndex].useSSL,
|
|
|
|
|
proxyInfo: proxyInfo,
|
2023-09-08 15:50:50 +00:00
|
|
|
|
);
|
2024-02-15 01:47:26 +00:00
|
|
|
|
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,
|
|
|
|
|
);
|
|
|
|
|
}
|
2022-08-26 08:11:35 +00:00
|
|
|
|
}
|
2024-02-15 01:47:26 +00:00
|
|
|
|
|
|
|
|
|
return;
|
2023-09-07 18:07:05 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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) {
|
2024-02-15 01:47:26 +00:00
|
|
|
|
await _torConnectingLock
|
2024-02-15 23:53:39 +00:00
|
|
|
|
.protect(() async => await checkElectrumAdapter());
|
2023-09-08 16:54:31 +00:00
|
|
|
|
} else {
|
2024-02-15 23:53:39 +00:00
|
|
|
|
await checkElectrumAdapter();
|
2023-09-08 16:54:31 +00:00
|
|
|
|
}
|
2022-08-26 08:11:35 +00:00
|
|
|
|
|
|
|
|
|
try {
|
2024-02-15 01:47:26 +00:00
|
|
|
|
final response = await _electrumAdapterClient!.request(
|
|
|
|
|
command,
|
|
|
|
|
args,
|
2023-07-27 20:39:48 +00:00
|
|
|
|
);
|
2022-08-26 08:11:35 +00:00
|
|
|
|
|
2024-02-15 01:47:26 +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(
|
2023-01-27 18:49:25 +00:00
|
|
|
|
"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"
|
2024-02-15 01:47:26 +00:00
|
|
|
|
" error: ${response["error"]}\n"
|
2023-07-25 18:06:06 +00:00
|
|
|
|
" 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;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-15 01:47:26 +00:00
|
|
|
|
return response;
|
2022-08-26 08:11:35 +00:00
|
|
|
|
} on WifiOnlyException {
|
|
|
|
|
rethrow;
|
|
|
|
|
} 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
|
2024-02-15 01:47:26 +00:00
|
|
|
|
Future<List<dynamic>> batchRequest({
|
2022-08-26 08:11:35 +00:00
|
|
|
|
required String command,
|
2024-02-22 05:16:53 +00:00
|
|
|
|
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) {
|
2024-02-15 01:47:26 +00:00
|
|
|
|
await _torConnectingLock
|
2024-02-15 23:53:39 +00:00
|
|
|
|
.protect(() async => await checkElectrumAdapter());
|
2023-09-08 16:54:31 +00:00
|
|
|
|
} else {
|
2024-02-15 23:53:39 +00:00
|
|
|
|
await checkElectrumAdapter();
|
2023-09-08 16:54:31 +00:00
|
|
|
|
}
|
2022-08-26 08:11:35 +00:00
|
|
|
|
|
|
|
|
|
try {
|
2024-02-15 01:47:26 +00:00
|
|
|
|
var futures = <Future<dynamic>>[];
|
|
|
|
|
_electrumAdapterClient!.peer.withBatch(() {
|
2024-02-22 05:16:53 +00:00
|
|
|
|
for (final arg in args) {
|
|
|
|
|
futures.add(_electrumAdapterClient!.request(command, arg));
|
2024-02-03 01:01:26 +00:00
|
|
|
|
}
|
2024-02-15 01:47:26 +00:00
|
|
|
|
});
|
2024-02-22 05:16:53 +00:00
|
|
|
|
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;
|
2024-02-15 01:47:26 +00:00
|
|
|
|
return response;
|
2022-08-26 08:11:35 +00:00
|
|
|
|
} on WifiOnlyException {
|
|
|
|
|
rethrow;
|
|
|
|
|
} 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',
|
2023-07-27 20:39:48 +00:00
|
|
|
|
requestTimeout: const Duration(seconds: 2),
|
2022-08-26 08:11:35 +00:00
|
|
|
|
retries: retryCount,
|
2024-02-16 22:55:24 +00:00
|
|
|
|
).timeout(const Duration(seconds: 2)) 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',
|
|
|
|
|
);
|
2024-02-15 01:47:26 +00:00
|
|
|
|
if (response == null) {
|
2023-05-24 21:05:59 +00:00
|
|
|
|
Logging.instance.log(
|
|
|
|
|
"getBlockHeadTip returned null response",
|
|
|
|
|
level: LogLevel.Error,
|
|
|
|
|
);
|
|
|
|
|
throw 'getBlockHeadTip returned null response';
|
|
|
|
|
}
|
2024-02-15 01:47:26 +00:00
|
|
|
|
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',
|
|
|
|
|
);
|
2024-02-15 01:47:26 +00:00
|
|
|
|
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,
|
|
|
|
|
],
|
|
|
|
|
);
|
2024-02-15 01:47:26 +00:00
|
|
|
|
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,
|
|
|
|
|
],
|
|
|
|
|
);
|
2024-02-15 01:47:26 +00:00
|
|
|
|
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 {
|
2023-11-24 19:25:32 +00:00
|
|
|
|
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,
|
|
|
|
|
],
|
|
|
|
|
);
|
2024-02-15 01:47:26 +00:00
|
|
|
|
result = response;
|
2023-11-24 19:25:32 +00:00
|
|
|
|
retryCount--;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return List<Map<String, dynamic>>.from(result as List);
|
2022-08-26 08:11:35 +00:00
|
|
|
|
} catch (e) {
|
|
|
|
|
rethrow;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-22 05:16:53 +00:00
|
|
|
|
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,
|
|
|
|
|
);
|
2024-02-22 05:16:53 +00:00
|
|
|
|
final List<List<Map<String, dynamic>>> result = [];
|
2022-08-26 08:11:35 +00:00
|
|
|
|
for (int i = 0; i < response.length; i++) {
|
2024-02-22 05:16:53 +00:00
|
|
|
|
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,
|
|
|
|
|
],
|
|
|
|
|
);
|
2024-02-15 01:47:26 +00:00
|
|
|
|
return List<Map<String, dynamic>>.from(response as List);
|
2022-08-26 08:11:35 +00:00
|
|
|
|
} catch (e) {
|
|
|
|
|
rethrow;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-22 05:16:53 +00:00
|
|
|
|
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,
|
|
|
|
|
);
|
2024-02-22 05:16:53 +00:00
|
|
|
|
final List<List<Map<String, dynamic>>> result = [];
|
2022-08-26 08:11:35 +00:00
|
|
|
|
for (int i = 0; i < response.length; i++) {
|
2024-02-15 01:47:26 +00:00
|
|
|
|
if ((response[i] as List).isNotEmpty) {
|
|
|
|
|
try {
|
2024-02-22 05:16:53 +00:00
|
|
|
|
final data = List<Map<String, dynamic>>.from(response[i] as List);
|
|
|
|
|
result.add(data);
|
2024-02-15 01:47:26 +00:00
|
|
|
|
} catch (e) {
|
2024-02-22 05:16:53 +00:00
|
|
|
|
// to ensure we keep same length of responses as requests/args
|
|
|
|
|
// add empty list on error
|
|
|
|
|
result.add([]);
|
|
|
|
|
|
2024-02-15 01:47:26 +00:00
|
|
|
|
Logging.instance.log(
|
2024-02-22 05:16:53 +00:00
|
|
|
|
"getBatchUTXOs failed to parse response=${response[i]}: $e",
|
2024-02-15 01:47:26 +00:00
|
|
|
|
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-02-14 00:28:20 +00:00
|
|
|
|
Logging.instance.log("attempting to fetch blockchain.transaction.get...",
|
|
|
|
|
level: LogLevel.Info);
|
2024-02-15 23:53:39 +00:00
|
|
|
|
await checkElectrumAdapter();
|
2024-02-15 01:47:26 +00:00
|
|
|
|
dynamic response = await _electrumAdapterClient!.getTransaction(txHash);
|
2024-02-14 00:28:20 +00:00
|
|
|
|
Logging.instance.log("Fetching blockchain.transaction.get finished",
|
|
|
|
|
level: LogLevel.Info);
|
2024-02-06 18:31:42 +00:00
|
|
|
|
|
2024-02-14 01:47:26 +00:00
|
|
|
|
if (!verbose) {
|
|
|
|
|
return {"rawtx": response as String};
|
2022-08-26 08:11:35 +00:00
|
|
|
|
}
|
2024-02-14 01:47:26 +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-02-13 21:34:45 +00:00
|
|
|
|
Logging.instance.log("attempting to fetch lelantus.getanonymityset...",
|
|
|
|
|
level: LogLevel.Info);
|
2024-02-15 23:53:39 +00:00
|
|
|
|
await checkElectrumAdapter();
|
2024-02-15 01:47:26 +00:00
|
|
|
|
Map<String, dynamic> response =
|
|
|
|
|
await (_electrumAdapterClient as FiroElectrumClient)!
|
|
|
|
|
.getLelantusAnonymitySet(groupId: groupId, blockHash: blockhash);
|
2024-02-13 21:34:45 +00:00
|
|
|
|
Logging.instance.log("Fetching lelantus.getanonymityset finished",
|
|
|
|
|
level: LogLevel.Info);
|
2024-02-15 01:47:26 +00:00
|
|
|
|
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-02-13 21:34:45 +00:00
|
|
|
|
Logging.instance.log("attempting to fetch lelantus.getmintmetadata...",
|
|
|
|
|
level: LogLevel.Info);
|
2024-02-15 23:53:39 +00:00
|
|
|
|
await checkElectrumAdapter();
|
2024-02-15 01:47:26 +00:00
|
|
|
|
dynamic response = await (_electrumAdapterClient as FiroElectrumClient)!
|
|
|
|
|
.getLelantusMintData(mints: mints);
|
2024-02-13 21:34:45 +00:00
|
|
|
|
Logging.instance.log("Fetching lelantus.getmintmetadata finished",
|
|
|
|
|
level: LogLevel.Info);
|
2024-02-15 01:47:26 +00:00
|
|
|
|
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-02-13 21:34:45 +00:00
|
|
|
|
Logging.instance.log("attempting to fetch lelantus.getusedcoinserials...",
|
|
|
|
|
level: LogLevel.Info);
|
2024-02-15 23:53:39 +00:00
|
|
|
|
await checkElectrumAdapter();
|
2024-02-13 21:34:45 +00:00
|
|
|
|
|
2024-02-14 01:47:26 +00:00
|
|
|
|
int retryCount = 3;
|
2024-02-15 01:47:26 +00:00
|
|
|
|
dynamic response;
|
2023-11-24 19:25:32 +00:00
|
|
|
|
|
2024-02-15 01:47:26 +00:00
|
|
|
|
while (retryCount > 0 && response is! List) {
|
|
|
|
|
response = await (_electrumAdapterClient as FiroElectrumClient)!
|
|
|
|
|
.getLelantusUsedCoinSerials(startNumber: startNumber);
|
2024-02-14 01:47:26 +00:00
|
|
|
|
// TODO add 2 minute timeout.
|
|
|
|
|
Logging.instance.log("Fetching lelantus.getusedcoinserials finished",
|
|
|
|
|
level: LogLevel.Info);
|
2023-11-24 19:25:32 +00:00
|
|
|
|
|
2024-02-14 01:47:26 +00:00
|
|
|
|
retryCount--;
|
2022-08-26 08:11:35 +00:00
|
|
|
|
}
|
2024-02-14 01:47:26 +00:00
|
|
|
|
|
2024-02-15 01:47:26 +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-02-13 21:34:45 +00:00
|
|
|
|
Logging.instance.log("attempting to fetch lelantus.getlatestcoinid...",
|
|
|
|
|
level: LogLevel.Info);
|
2024-02-15 23:53:39 +00:00
|
|
|
|
await checkElectrumAdapter();
|
2024-02-15 01:47:26 +00:00
|
|
|
|
int response =
|
|
|
|
|
await (_electrumAdapterClient as FiroElectrumClient).getLatestCoinId();
|
2024-02-13 21:34:45 +00:00
|
|
|
|
Logging.instance.log("Fetching lelantus.getlatestcoinid finished",
|
|
|
|
|
level: LogLevel.Info);
|
2024-02-15 01:47:26 +00:00
|
|
|
|
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-02-12 22:30:45 +00:00
|
|
|
|
Logging.instance.log("attempting to fetch spark.getsparkanonymityset...",
|
|
|
|
|
level: LogLevel.Info);
|
2024-02-15 23:53:39 +00:00
|
|
|
|
await checkElectrumAdapter();
|
2024-02-15 01:47:26 +00:00
|
|
|
|
Map<String, dynamic> response =
|
|
|
|
|
await (_electrumAdapterClient as FiroElectrumClient)
|
|
|
|
|
.getSparkAnonymitySet(
|
|
|
|
|
coinGroupId: coinGroupId, startBlockHash: startBlockHash);
|
2024-02-12 22:30:45 +00:00
|
|
|
|
Logging.instance.log("Fetching spark.getsparkanonymityset finished",
|
|
|
|
|
level: LogLevel.Info);
|
2024-02-15 01:47:26 +00:00
|
|
|
|
return response;
|
2023-11-16 18:15:28 +00:00
|
|
|
|
} catch (e) {
|
|
|
|
|
rethrow;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Takes [startNumber], if it is 0, we get the full set,
|
|
|
|
|
/// otherwise the used tags after that number
|
2023-12-13 22:15:59 +00:00
|
|
|
|
Future<Set<String>> getSparkUsedCoinsTags({
|
2023-11-16 18:15:28 +00:00
|
|
|
|
String? requestID,
|
|
|
|
|
required int startNumber,
|
|
|
|
|
}) async {
|
|
|
|
|
try {
|
2024-02-12 22:30:45 +00:00
|
|
|
|
// Use electrum_adapter package's getSparkUsedCoinsTags method.
|
|
|
|
|
Logging.instance.log("attempting to fetch spark.getusedcoinstags...",
|
|
|
|
|
level: LogLevel.Info);
|
2024-02-15 23:53:39 +00:00
|
|
|
|
await checkElectrumAdapter();
|
2024-02-15 01:47:26 +00:00
|
|
|
|
Map<String, dynamic> response =
|
|
|
|
|
await (_electrumAdapterClient as FiroElectrumClient)
|
|
|
|
|
.getUsedCoinsTags(startNumber: startNumber);
|
2024-02-14 01:47:26 +00:00
|
|
|
|
// TODO: Add 2 minute timeout.
|
2024-02-12 22:30:45 +00:00
|
|
|
|
Logging.instance.log("Fetching spark.getusedcoinstags finished",
|
|
|
|
|
level: LogLevel.Info);
|
2024-02-15 01:47:26 +00:00
|
|
|
|
final map = Map<String, dynamic>.from(response);
|
2024-02-12 22:30:45 +00:00
|
|
|
|
final set = Set<String>.from(map["tags"] as List);
|
|
|
|
|
return await compute(_ffiHashTagsComputeWrapper, set);
|
2023-11-16 18:15:28 +00:00
|
|
|
|
} catch (e) {
|
|
|
|
|
Logging.instance.log(e, level: LogLevel.Error);
|
|
|
|
|
rethrow;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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-02-12 22:30:45 +00:00
|
|
|
|
Logging.instance.log("attempting to fetch spark.getsparkmintmetadata...",
|
|
|
|
|
level: LogLevel.Info);
|
2024-02-15 23:53:39 +00:00
|
|
|
|
await checkElectrumAdapter();
|
2024-02-15 01:47:26 +00:00
|
|
|
|
List<dynamic> response =
|
|
|
|
|
await (_electrumAdapterClient as FiroElectrumClient)
|
|
|
|
|
.getSparkMintMetaData(sparkCoinHashes: sparkCoinHashes);
|
2024-02-12 22:30:45 +00:00
|
|
|
|
Logging.instance.log("Fetching spark.getsparkmintmetadata finished",
|
|
|
|
|
level: LogLevel.Info);
|
2024-02-15 01:47:26 +00:00
|
|
|
|
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-02-12 22:30:45 +00:00
|
|
|
|
Logging.instance.log("attempting to fetch spark.getsparklatestcoinid...",
|
|
|
|
|
level: LogLevel.Info);
|
2024-02-15 23:53:39 +00:00
|
|
|
|
await checkElectrumAdapter();
|
2024-02-15 01:47:26 +00:00
|
|
|
|
int response = await (_electrumAdapterClient as FiroElectrumClient)
|
|
|
|
|
.getSparkLatestCoinId();
|
2024-02-12 22:30:45 +00:00
|
|
|
|
Logging.instance.log("Fetching spark.getsparklatestcoinid finished",
|
|
|
|
|
level: LogLevel.Info);
|
2024-02-15 01:47:26 +00:00
|
|
|
|
return response;
|
2023-11-16 18:15:28 +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-02-15 23:53:39 +00:00
|
|
|
|
await checkElectrumAdapter();
|
2024-02-15 01:47:26 +00:00
|
|
|
|
return await _electrumAdapterClient!.getFeeRate();
|
2022-08-26 08:11:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Return the estimated transaction fee per kilobyte for a transaction to be confirmed within a certain number of [blocks].
|
|
|
|
|
///
|
|
|
|
|
/// 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 {
|
2024-02-27 19:48:48 +00:00
|
|
|
|
// If the response is -1 or null, return a temporary hardcoded value for
|
|
|
|
|
// Dogecoin. This is a temporary fix until the fee estimation is fixed.
|
|
|
|
|
if (coin == Coin.dogecoin &&
|
|
|
|
|
(response == null ||
|
|
|
|
|
response == -1 ||
|
|
|
|
|
Decimal.parse(response.toString()) == Decimal.parse("-1"))) {
|
|
|
|
|
// Return 0.05 for slow, 0.2 for average, and 1 for fast txs.
|
|
|
|
|
// These numbers produce tx fees in line with txs in the wild on
|
|
|
|
|
// https://dogechain.info/
|
|
|
|
|
return Decimal.parse((1 / blocks).toString());
|
|
|
|
|
// TODO [prio=med]: Fix fee estimation.
|
|
|
|
|
}
|
2024-02-15 01:47:26 +00:00
|
|
|
|
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-02-15 01:47:26 +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 daemon’s 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',
|
|
|
|
|
);
|
2024-02-15 01:47:26 +00:00
|
|
|
|
return Decimal.parse(response.toString());
|
2022-08-26 08:11:35 +00:00
|
|
|
|
} catch (e) {
|
|
|
|
|
rethrow;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-20 23:46:48 +00:00
|
|
|
|
|
|
|
|
|
Set<String> _ffiHashTagsComputeWrapper(Set<String> base64Tags) {
|
|
|
|
|
return LibSpark.hashTags(base64Tags: base64Tags);
|
|
|
|
|
}
|