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
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
2022-08-26 08:11:35 +00:00
|
|
|
import 'dart:async';
|
|
|
|
import 'dart:convert';
|
|
|
|
import 'dart:io';
|
|
|
|
|
2023-05-29 15:16:25 +00:00
|
|
|
import 'package:flutter/foundation.dart';
|
2023-05-25 22:34:03 +00:00
|
|
|
import 'package:mutex/mutex.dart';
|
2023-08-10 16:40:12 +00:00
|
|
|
import 'package:stackwallet/networking/socks5.dart';
|
2023-08-08 21:01:41 +00:00
|
|
|
import 'package:stackwallet/networking/tor_service.dart';
|
2022-08-26 08:11:35 +00:00
|
|
|
import 'package:stackwallet/utilities/logger.dart';
|
2023-08-08 21:01:41 +00:00
|
|
|
import 'package:stackwallet/utilities/prefs.dart';
|
2022-08-26 08:11:35 +00:00
|
|
|
|
2023-05-29 15:16:25 +00:00
|
|
|
// Json RPC class to handle connecting to electrumx servers
|
2022-08-26 08:11:35 +00:00
|
|
|
class JsonRPC {
|
|
|
|
JsonRPC({
|
|
|
|
required this.host,
|
|
|
|
required this.port,
|
|
|
|
this.useSSL = false,
|
|
|
|
this.connectionTimeout = const Duration(seconds: 60),
|
2023-08-08 21:01:41 +00:00
|
|
|
required ({String host, int port})? proxyInfo,
|
2022-08-26 08:11:35 +00:00
|
|
|
});
|
2023-05-24 18:12:54 +00:00
|
|
|
final bool useSSL;
|
|
|
|
final String host;
|
|
|
|
final int port;
|
|
|
|
final Duration connectionTimeout;
|
2023-08-08 21:01:41 +00:00
|
|
|
({String host, int port})? proxyInfo;
|
2022-08-26 08:11:35 +00:00
|
|
|
|
2023-05-25 22:34:03 +00:00
|
|
|
final _requestMutex = Mutex();
|
2023-05-25 20:52:07 +00:00
|
|
|
final _JsonRPCRequestQueue _requestQueue = _JsonRPCRequestQueue();
|
2023-08-09 17:50:12 +00:00
|
|
|
Socket? _socket;
|
|
|
|
SOCKSSocket? _socksSocket;
|
2023-05-24 18:27:19 +00:00
|
|
|
StreamSubscription<Uint8List>? _subscription;
|
|
|
|
|
2023-05-25 20:52:07 +00:00
|
|
|
void _dataHandler(List<int> data) {
|
2023-05-29 15:16:25 +00:00
|
|
|
_requestQueue.nextIncompleteReq.then((req) {
|
|
|
|
if (req != null) {
|
|
|
|
req.appendDataAndCheckIfComplete(data);
|
2023-05-24 18:39:21 +00:00
|
|
|
|
2023-05-29 15:16:25 +00:00
|
|
|
if (req.isComplete) {
|
|
|
|
_onReqCompleted(req);
|
2022-08-26 08:11:35 +00:00
|
|
|
}
|
2023-05-29 15:16:25 +00:00
|
|
|
} else {
|
|
|
|
Logging.instance.log(
|
|
|
|
"_dataHandler found a null req!",
|
|
|
|
level: LogLevel.Warning,
|
|
|
|
);
|
2022-08-26 08:11:35 +00:00
|
|
|
}
|
2023-05-29 15:16:25 +00:00
|
|
|
});
|
2023-05-25 20:52:07 +00:00
|
|
|
}
|
2022-08-26 08:11:35 +00:00
|
|
|
|
2023-05-25 20:52:07 +00:00
|
|
|
void _errorHandler(Object error, StackTrace trace) {
|
2023-05-29 15:16:25 +00:00
|
|
|
_requestQueue.nextIncompleteReq.then((req) {
|
|
|
|
if (req != null) {
|
|
|
|
req.completer.completeError(error, trace);
|
|
|
|
_onReqCompleted(req);
|
|
|
|
}
|
|
|
|
});
|
2023-05-25 20:52:07 +00:00
|
|
|
}
|
2022-08-26 08:11:35 +00:00
|
|
|
|
2023-05-25 20:52:07 +00:00
|
|
|
void _doneHandler() {
|
2023-05-29 15:16:25 +00:00
|
|
|
disconnect(reason: "JsonRPC _doneHandler() called");
|
2023-05-25 20:52:07 +00:00
|
|
|
}
|
2022-08-26 08:11:35 +00:00
|
|
|
|
2023-05-25 21:40:30 +00:00
|
|
|
void _onReqCompleted(_JsonRPCRequest req) {
|
2023-05-29 15:16:25 +00:00
|
|
|
_requestQueue.remove(req).then((_) {
|
|
|
|
// attempt to send next request
|
2023-05-25 20:52:07 +00:00
|
|
|
_sendNextAvailableRequest();
|
2023-05-29 15:16:25 +00:00
|
|
|
});
|
2023-05-25 20:52:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void _sendNextAvailableRequest() {
|
2023-05-29 15:16:25 +00:00
|
|
|
_requestQueue.nextIncompleteReq.then((req) {
|
|
|
|
if (req != null) {
|
|
|
|
// \r\n required by electrumx server
|
2023-08-09 17:50:12 +00:00
|
|
|
if (_socket != null) {
|
|
|
|
_socket!.write('${req.jsonRequest}\r\n');
|
|
|
|
}
|
|
|
|
if (_socksSocket != null) {
|
|
|
|
_socksSocket!.write('${req.jsonRequest}\r\n');
|
|
|
|
}
|
2023-05-29 15:16:25 +00:00
|
|
|
|
|
|
|
// TODO different timeout length?
|
|
|
|
req.initiateTimeout(
|
|
|
|
onTimedOut: () {
|
|
|
|
_requestQueue.remove(req);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|
2023-05-25 20:52:07 +00:00
|
|
|
}
|
2023-05-24 18:39:21 +00:00
|
|
|
|
2023-07-27 20:39:48 +00:00
|
|
|
Future<JsonRPCResponse> request(
|
|
|
|
String jsonRpcRequest,
|
|
|
|
Duration requestTimeout,
|
|
|
|
) async {
|
2023-05-25 22:34:03 +00:00
|
|
|
await _requestMutex.protect(() async {
|
2023-08-09 17:50:12 +00:00
|
|
|
if (!Prefs.instance.useTor) {
|
|
|
|
if (_socket == null) {
|
|
|
|
Logging.instance.log(
|
2023-08-10 16:40:12 +00:00
|
|
|
"JsonRPC request: opening socket $host:$port",
|
2023-08-09 17:50:12 +00:00
|
|
|
level: LogLevel.Info,
|
|
|
|
);
|
2023-08-09 23:28:19 +00:00
|
|
|
await connect();
|
2023-08-09 17:50:12 +00:00
|
|
|
}
|
|
|
|
} else {
|
2023-08-09 23:28:19 +00:00
|
|
|
if (_socksSocket == null) {
|
2023-08-09 17:50:12 +00:00
|
|
|
Logging.instance.log(
|
2023-08-09 23:28:19 +00:00
|
|
|
"JsonRPC request: opening SOCKS socket to $host:$port",
|
2023-08-09 17:50:12 +00:00
|
|
|
level: LogLevel.Info,
|
|
|
|
);
|
|
|
|
await connect();
|
|
|
|
}
|
2023-05-25 22:34:03 +00:00
|
|
|
}
|
|
|
|
});
|
2023-05-24 20:55:24 +00:00
|
|
|
|
2023-05-25 20:52:07 +00:00
|
|
|
final req = _JsonRPCRequest(
|
|
|
|
jsonRequest: jsonRpcRequest,
|
2023-07-27 20:39:48 +00:00
|
|
|
requestTimeout: requestTimeout,
|
2023-05-29 15:16:25 +00:00
|
|
|
completer: Completer<JsonRPCResponse>(),
|
2023-05-25 20:52:07 +00:00
|
|
|
);
|
|
|
|
|
2023-05-29 15:16:25 +00:00
|
|
|
final future = req.completer.future.onError(
|
|
|
|
(error, stackTrace) async {
|
|
|
|
await disconnect(
|
|
|
|
reason: "return req.completer.future.onError: $error\n$stackTrace",
|
|
|
|
);
|
|
|
|
return JsonRPCResponse(
|
|
|
|
exception: error is Exception
|
|
|
|
? error
|
|
|
|
: Exception(
|
|
|
|
"req.completer.future.onError: $error\n$stackTrace",
|
|
|
|
),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
2023-05-24 18:15:10 +00:00
|
|
|
|
2023-05-25 20:52:07 +00:00
|
|
|
// if this is the only/first request then send it right away
|
2023-05-29 15:16:25 +00:00
|
|
|
await _requestQueue.add(
|
|
|
|
req,
|
|
|
|
onInitialRequestAdded: _sendNextAvailableRequest,
|
2023-05-26 21:28:25 +00:00
|
|
|
);
|
2023-05-29 15:16:25 +00:00
|
|
|
|
|
|
|
return future;
|
2023-05-25 20:52:07 +00:00
|
|
|
}
|
|
|
|
|
2023-05-29 15:16:25 +00:00
|
|
|
Future<void> disconnect({required String reason}) async {
|
|
|
|
await _requestMutex.protect(() async {
|
|
|
|
await _subscription?.cancel();
|
|
|
|
_subscription = null;
|
|
|
|
_socket?.destroy();
|
|
|
|
_socket = null;
|
2023-08-10 16:40:12 +00:00
|
|
|
unawaited(_socksSocket?.close(keepOpen: false));
|
|
|
|
// TODO check that it's ok to not await this
|
2023-08-09 17:50:12 +00:00
|
|
|
_socksSocket = null;
|
2023-05-29 15:16:25 +00:00
|
|
|
|
|
|
|
// clean up remaining queue
|
|
|
|
await _requestQueue.completeRemainingWithError(
|
|
|
|
"JsonRPC disconnect() called with reason: \"$reason\"",
|
|
|
|
);
|
|
|
|
});
|
2023-05-25 20:52:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> connect() async {
|
2023-08-09 17:50:12 +00:00
|
|
|
if (!Prefs.instance.useTor) {
|
|
|
|
if (useSSL) {
|
|
|
|
_socket = await SecureSocket.connect(
|
|
|
|
host,
|
|
|
|
port,
|
|
|
|
timeout: connectionTimeout,
|
|
|
|
onBadCertificate: (_) => true,
|
|
|
|
); // TODO do not automatically trust bad certificates
|
|
|
|
} else {
|
|
|
|
_socket = await Socket.connect(
|
|
|
|
host,
|
|
|
|
port,
|
|
|
|
timeout: connectionTimeout,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} else {
|
2023-08-09 23:28:19 +00:00
|
|
|
if (proxyInfo == null) {
|
|
|
|
// TODO await tor / make sure it's running
|
|
|
|
proxyInfo = (
|
|
|
|
host: InternetAddress.loopbackIPv4.address,
|
|
|
|
port: TorService.sharedInstance.port
|
2023-08-09 17:13:26 +00:00
|
|
|
);
|
2023-08-09 23:28:19 +00:00
|
|
|
Logging.instance.log(
|
|
|
|
"ElectrumX.connect(): no tor proxy info, read $proxyInfo",
|
|
|
|
level: LogLevel.Warning);
|
2023-08-10 16:40:12 +00:00
|
|
|
}
|
2023-08-09 23:28:19 +00:00
|
|
|
// TODO connect to proxy socket...
|
|
|
|
|
|
|
|
// TODO implement ssl over tor
|
|
|
|
// if (useSSL) {
|
|
|
|
// _socket = await SecureSocket.connect(
|
|
|
|
// host,
|
|
|
|
// port,
|
|
|
|
// timeout: connectionTimeout,
|
|
|
|
// onBadCertificate: (_) => true,
|
|
|
|
// ); // TODO do not automatically trust bad certificates
|
|
|
|
// final _client = SocksSocket.protected(_socket, type);
|
2023-08-10 16:40:12 +00:00
|
|
|
// } else {
|
2023-08-09 23:28:19 +00:00
|
|
|
final sock = await RawSocket.connect(
|
|
|
|
InternetAddress.loopbackIPv4, proxyInfo!.port);
|
2023-08-09 23:28:19 +00:00
|
|
|
|
2023-08-09 23:28:19 +00:00
|
|
|
if (_socksSocket == null) {
|
|
|
|
Logging.instance.log(
|
|
|
|
"JsonRPC.connect(): creating SOCKS socket at $proxyInfo",
|
|
|
|
level: LogLevel.Info);
|
|
|
|
_socksSocket = SOCKSSocket(sock);
|
|
|
|
if (_socksSocket == null) {
|
|
|
|
Logging.instance.log(
|
|
|
|
"JsonRPC.connect(): failed to create SOCKS socket at $proxyInfo",
|
|
|
|
level: LogLevel.Error);
|
|
|
|
throw Exception(
|
|
|
|
"JsonRPC.connect(): failed to create SOCKS socket at $proxyInfo");
|
|
|
|
} else {
|
|
|
|
Logging.instance.log(
|
|
|
|
"JsonRPC.connect(): created SOCKS socket at $proxyInfo",
|
|
|
|
level: LogLevel.Info);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// TODO also check if sock == previous sock, eg. if RawSocket is different
|
|
|
|
Logging.instance.log(
|
|
|
|
"JsonRPC.connect(): using pre-existing SOCKS socket at $proxyInfo",
|
|
|
|
level: LogLevel.Info);
|
|
|
|
}
|
2023-08-10 16:40:12 +00:00
|
|
|
|
2023-08-09 23:28:19 +00:00
|
|
|
try {
|
|
|
|
Logging.instance.log(
|
|
|
|
"JsonRPC.connect(): connecting to $host:$port over SOCKS socket at $proxyInfo...",
|
|
|
|
level: LogLevel.Info);
|
|
|
|
if (!isIpAddress(host)) {
|
|
|
|
await _socksSocket!.connect("$host:$port");
|
|
|
|
} else {
|
|
|
|
await _socksSocket!.connectIp(InternetAddress(host), port);
|
|
|
|
}
|
|
|
|
Logging.instance.log(
|
|
|
|
"JsonRPC.connect(): connected to $host:$port over SOCKS socket at $proxyInfo",
|
|
|
|
level: LogLevel.Info);
|
|
|
|
} catch (e) {
|
|
|
|
Logging.instance.log(
|
|
|
|
"JsonRPC.connect(): failed to connect to $host over tor proxy at $proxyInfo, $e",
|
|
|
|
level: LogLevel.Error);
|
|
|
|
throw Exception(
|
|
|
|
"JsonRPC.connect(): failed to connect to tor proxy, $e");
|
|
|
|
}
|
2023-08-08 21:27:38 +00:00
|
|
|
}
|
2023-08-10 16:40:12 +00:00
|
|
|
|
2023-08-09 23:28:19 +00:00
|
|
|
_subscription = _socket!.listen(
|
|
|
|
_dataHandler,
|
|
|
|
onError: _errorHandler,
|
|
|
|
onDone: _doneHandler,
|
|
|
|
cancelOnError: true,
|
|
|
|
);
|
2023-08-10 16:40:12 +00:00
|
|
|
}
|
2023-05-25 20:52:07 +00:00
|
|
|
}
|
2022-08-26 08:11:35 +00:00
|
|
|
|
2023-05-25 20:52:07 +00:00
|
|
|
class _JsonRPCRequestQueue {
|
2023-05-29 15:16:25 +00:00
|
|
|
final _lock = Mutex();
|
2023-05-25 20:52:07 +00:00
|
|
|
final List<_JsonRPCRequest> _rq = [];
|
2022-08-26 08:11:35 +00:00
|
|
|
|
2023-05-29 15:16:25 +00:00
|
|
|
Future<void> add(
|
|
|
|
_JsonRPCRequest req, {
|
|
|
|
VoidCallback? onInitialRequestAdded,
|
|
|
|
}) async {
|
|
|
|
return await _lock.protect(() async {
|
|
|
|
_rq.add(req);
|
|
|
|
if (_rq.length == 1) {
|
|
|
|
onInitialRequestAdded?.call();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2022-08-26 08:11:35 +00:00
|
|
|
|
2023-05-29 15:16:25 +00:00
|
|
|
Future<bool> remove(_JsonRPCRequest req) async {
|
|
|
|
return await _lock.protect(() async {
|
|
|
|
final result = _rq.remove(req);
|
|
|
|
return result;
|
|
|
|
});
|
2022-08-26 08:11:35 +00:00
|
|
|
}
|
2023-05-29 15:16:25 +00:00
|
|
|
|
|
|
|
Future<_JsonRPCRequest?> get nextIncompleteReq async {
|
|
|
|
return await _lock.protect(() async {
|
|
|
|
int removeCount = 0;
|
|
|
|
_JsonRPCRequest? returnValue;
|
|
|
|
for (final req in _rq) {
|
|
|
|
if (req.isComplete) {
|
|
|
|
removeCount++;
|
|
|
|
} else {
|
|
|
|
returnValue = req;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2023-05-25 20:52:07 +00:00
|
|
|
|
2023-05-29 15:16:25 +00:00
|
|
|
_rq.removeRange(0, removeCount);
|
2023-05-25 21:29:23 +00:00
|
|
|
|
2023-05-29 15:16:25 +00:00
|
|
|
return returnValue;
|
|
|
|
});
|
|
|
|
}
|
2023-05-25 21:29:23 +00:00
|
|
|
|
2023-05-29 15:16:25 +00:00
|
|
|
Future<void> completeRemainingWithError(
|
|
|
|
String error, {
|
|
|
|
StackTrace? stackTrace,
|
|
|
|
}) async {
|
|
|
|
await _lock.protect(() async {
|
|
|
|
for (final req in _rq) {
|
|
|
|
if (!req.isComplete) {
|
|
|
|
req.completer.completeError(Exception(error), stackTrace);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_rq.clear();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<bool> get isEmpty async {
|
|
|
|
return await _lock.protect(() async {
|
|
|
|
return _rq.isEmpty;
|
|
|
|
});
|
|
|
|
}
|
2023-05-25 20:52:07 +00:00
|
|
|
}
|
2023-05-24 20:55:55 +00:00
|
|
|
|
2023-05-25 20:52:07 +00:00
|
|
|
class _JsonRPCRequest {
|
2023-05-29 15:16:25 +00:00
|
|
|
// 0x0A is newline
|
|
|
|
// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-basics.html
|
|
|
|
static const int separatorByte = 0x0A;
|
|
|
|
|
2023-05-25 20:52:07 +00:00
|
|
|
final String jsonRequest;
|
2023-05-29 15:16:25 +00:00
|
|
|
final Completer<JsonRPCResponse> completer;
|
2023-07-27 20:39:48 +00:00
|
|
|
final Duration requestTimeout;
|
2023-05-25 20:52:07 +00:00
|
|
|
final List<int> _responseData = [];
|
|
|
|
|
2023-07-27 20:39:48 +00:00
|
|
|
_JsonRPCRequest({
|
|
|
|
required this.jsonRequest,
|
|
|
|
required this.completer,
|
|
|
|
required this.requestTimeout,
|
|
|
|
});
|
2023-05-25 20:52:07 +00:00
|
|
|
|
|
|
|
void appendDataAndCheckIfComplete(List<int> data) {
|
|
|
|
_responseData.addAll(data);
|
2023-05-29 15:16:25 +00:00
|
|
|
if (data.last == separatorByte) {
|
2023-05-25 20:52:07 +00:00
|
|
|
try {
|
|
|
|
final response = json.decode(String.fromCharCodes(_responseData));
|
2023-05-29 15:16:25 +00:00
|
|
|
completer.complete(JsonRPCResponse(data: response));
|
2023-05-25 20:52:07 +00:00
|
|
|
} catch (e, s) {
|
|
|
|
Logging.instance.log(
|
|
|
|
"JsonRPC json.decode: $e\n$s",
|
|
|
|
level: LogLevel.Error,
|
|
|
|
);
|
|
|
|
completer.completeError(e, s);
|
|
|
|
}
|
|
|
|
}
|
2022-08-26 08:11:35 +00:00
|
|
|
}
|
2023-05-25 20:52:07 +00:00
|
|
|
|
2023-07-27 20:39:48 +00:00
|
|
|
void initiateTimeout({
|
2023-05-29 15:16:25 +00:00
|
|
|
VoidCallback? onTimedOut,
|
|
|
|
}) {
|
2023-07-27 20:39:48 +00:00
|
|
|
Future<void>.delayed(requestTimeout).then((_) {
|
2023-05-26 21:34:09 +00:00
|
|
|
if (!isComplete) {
|
|
|
|
try {
|
|
|
|
throw Exception("_JsonRPCRequest timed out: $jsonRequest");
|
|
|
|
} catch (e, s) {
|
|
|
|
completer.completeError(e, s);
|
2023-05-29 15:16:25 +00:00
|
|
|
onTimedOut?.call();
|
2023-05-26 21:34:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-05-25 20:52:07 +00:00
|
|
|
bool get isComplete => completer.isCompleted;
|
2022-08-26 08:11:35 +00:00
|
|
|
}
|
2023-05-29 15:16:25 +00:00
|
|
|
|
|
|
|
class JsonRPCResponse {
|
|
|
|
final dynamic data;
|
|
|
|
final Exception? exception;
|
|
|
|
|
|
|
|
JsonRPCResponse({this.data, this.exception});
|
2022-08-26 08:11:35 +00:00
|
|
|
}
|
2023-08-09 17:50:12 +00:00
|
|
|
|
|
|
|
bool isIpAddress(String host) {
|
|
|
|
try {
|
|
|
|
// if the string can be parsed into an InternetAddress, it's an IP.
|
|
|
|
InternetAddress(host);
|
|
|
|
return true;
|
|
|
|
} catch (e) {
|
|
|
|
// if parsing fails, it's not an IP.
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|