mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 12:46:45 +00:00
handle connection errors and incoming notifications
This commit is contained in:
parent
3a65261326
commit
50ef1c1a07
5 changed files with 251 additions and 47 deletions
|
@ -24,10 +24,7 @@ import com.sparrowwallet.sparrow.transaction.TransactionController;
|
||||||
import com.sparrowwallet.sparrow.wallet.WalletController;
|
import com.sparrowwallet.sparrow.wallet.WalletController;
|
||||||
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||||
import de.codecentric.centerdevice.MenuToolkit;
|
import de.codecentric.centerdevice.MenuToolkit;
|
||||||
import javafx.animation.Animation;
|
import javafx.animation.*;
|
||||||
import javafx.animation.KeyFrame;
|
|
||||||
import javafx.animation.KeyValue;
|
|
||||||
import javafx.animation.Timeline;
|
|
||||||
import javafx.concurrent.Worker;
|
import javafx.concurrent.Worker;
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
|
@ -53,7 +50,6 @@ public class AppController implements Initializable {
|
||||||
private static final int SERVER_PING_PERIOD = 10 * 1000;
|
private static final int SERVER_PING_PERIOD = 10 * 1000;
|
||||||
private static final int ENUMERATE_HW_PERIOD = 30 * 1000;
|
private static final int ENUMERATE_HW_PERIOD = 30 * 1000;
|
||||||
|
|
||||||
private static final String TRANSACTION_TAB_TYPE = "transaction";
|
|
||||||
public static final String DRAG_OVER_CLASS = "drag-over";
|
public static final String DRAG_OVER_CLASS = "drag-over";
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
|
@ -77,9 +73,12 @@ public class AppController implements Initializable {
|
||||||
@FXML
|
@FXML
|
||||||
private UnlabeledToggleSwitch serverToggle;
|
private UnlabeledToggleSwitch serverToggle;
|
||||||
|
|
||||||
|
//Determines if a change in serverToggle changes the offline/online mode
|
||||||
|
private boolean changeMode = true;
|
||||||
|
|
||||||
private Timeline statusTimeline;
|
private Timeline statusTimeline;
|
||||||
|
|
||||||
private ElectrumServer.PingService pingService;
|
private ElectrumServer.ConnectionService connectionService;
|
||||||
|
|
||||||
public static boolean showTxHexProperty;
|
public static boolean showTxHexProperty;
|
||||||
|
|
||||||
|
@ -89,7 +88,7 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
void initializeView() {
|
void initializeView() {
|
||||||
setOsxApplicationMenu();
|
//setOsxApplicationMenu();
|
||||||
|
|
||||||
rootStack.setOnDragOver(event -> {
|
rootStack.setOnDragOver(event -> {
|
||||||
if(event.getGestureSource() != rootStack && event.getDragboard().hasFiles()) {
|
if(event.getGestureSource() != rootStack && event.getDragboard().hasFiles()) {
|
||||||
|
@ -140,44 +139,52 @@ public class AppController implements Initializable {
|
||||||
exportWallet.setDisable(true);
|
exportWallet.setDisable(true);
|
||||||
|
|
||||||
serverToggle.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
serverToggle.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
Config.get().setMode(newValue ? Mode.ONLINE : Mode.OFFLINE);
|
if(changeMode) {
|
||||||
if(newValue) {
|
Config.get().setMode(newValue ? Mode.ONLINE : Mode.OFFLINE);
|
||||||
if(pingService.getState() == Worker.State.CANCELLED) {
|
if(newValue) {
|
||||||
pingService.reset();
|
if(connectionService.getState() == Worker.State.CANCELLED) {
|
||||||
}
|
connectionService.reset();
|
||||||
|
}
|
||||||
|
|
||||||
if(!pingService.isRunning()) {
|
if(!connectionService.isRunning()) {
|
||||||
pingService.start();
|
connectionService.start();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
connectionService.cancel();
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
pingService.cancel();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
pingService = createPingService();
|
connectionService = createConnectionService();
|
||||||
Config config = Config.get();
|
Config config = Config.get();
|
||||||
if(config.getMode() == Mode.ONLINE && config.getElectrumServer() != null && !config.getElectrumServer().isEmpty()) {
|
if(config.getMode() == Mode.ONLINE && config.getElectrumServer() != null && !config.getElectrumServer().isEmpty()) {
|
||||||
pingService.start();
|
connectionService.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ElectrumServer.PingService createPingService() {
|
private ElectrumServer.ConnectionService createConnectionService() {
|
||||||
ElectrumServer.PingService pingService = new ElectrumServer.PingService();
|
ElectrumServer.ConnectionService connectionService = new ElectrumServer.ConnectionService();
|
||||||
pingService.setPeriod(new Duration(SERVER_PING_PERIOD));
|
connectionService.setPeriod(new Duration(SERVER_PING_PERIOD));
|
||||||
pingService.setOnSucceeded(successEvent -> {
|
connectionService.setRestartOnFailure(true);
|
||||||
|
|
||||||
|
connectionService.setOnSucceeded(successEvent -> {
|
||||||
|
changeMode = false;
|
||||||
serverToggle.setSelected(true);
|
serverToggle.setSelected(true);
|
||||||
if(pingService.getValue() != null) {
|
changeMode = true;
|
||||||
statusBar.setText("Connected: " + pingService.getValue().split(System.lineSeparator(), 2)[0]);
|
|
||||||
} else {
|
if(connectionService.getValue() != null) {
|
||||||
statusBar.setText("");
|
EventManager.get().post(connectionService.getValue());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
pingService.setOnFailed(failEvent -> {
|
connectionService.setOnFailed(failEvent -> {
|
||||||
|
changeMode = false;
|
||||||
serverToggle.setSelected(false);
|
serverToggle.setSelected(false);
|
||||||
statusBar.setText(failEvent.getSource().getException().getMessage());
|
changeMode = true;
|
||||||
|
|
||||||
|
EventManager.get().post(new ConnectionFailedEvent(failEvent.getSource().getException()));
|
||||||
});
|
});
|
||||||
|
|
||||||
return pingService;
|
return connectionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setOsxApplicationMenu() {
|
private void setOsxApplicationMenu() {
|
||||||
|
@ -594,6 +601,19 @@ public class AppController implements Initializable {
|
||||||
exportWallet.setDisable(!event.getWallet().isValid());
|
exportWallet.setDisable(!event.getWallet().isValid());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void statusUpdated(StatusEvent event) {
|
||||||
|
statusBar.setText(event.getStatus());
|
||||||
|
|
||||||
|
PauseTransition wait = new PauseTransition(Duration.seconds(10));
|
||||||
|
wait.setOnFinished((e) -> {
|
||||||
|
if(statusBar.getText().equals(event.getStatus())) {
|
||||||
|
statusBar.setText("");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
wait.play();
|
||||||
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void timedWorker(TimedEvent event) {
|
public void timedWorker(TimedEvent event) {
|
||||||
if(event.getTimeMills() == 0) {
|
if(event.getTimeMills() == 0) {
|
||||||
|
@ -643,4 +663,18 @@ public class AppController implements Initializable {
|
||||||
usbStatus.setDevices(event.getDevices());
|
usbStatus.setDevices(event.getDevices());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void newConnection(ConnectionEvent event) {
|
||||||
|
String banner = event.getServerBanner();
|
||||||
|
String status = "Connected: " + (banner == null ? "Server" : banner.split(System.lineSeparator(), 2)[0]) + " at height " + event.getBlockHeight();
|
||||||
|
EventManager.get().post(new StatusEvent(status));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void connectionFailed(ConnectionFailedEvent event) {
|
||||||
|
String reason = event.getException().getCause() != null ? event.getException().getCause().getMessage() : event.getException().getMessage();
|
||||||
|
String status = "Connection error: " + reason;
|
||||||
|
EventManager.get().post(new StatusEvent(status));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.protocol.BlockHeader;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ConnectionEvent {
|
||||||
|
private final List<String> serverVersion;
|
||||||
|
private final String serverBanner;
|
||||||
|
private final int blockHeight;
|
||||||
|
private final BlockHeader blockHeader;
|
||||||
|
|
||||||
|
public ConnectionEvent(List<String> serverVersion, String serverBanner, int blockHeight, BlockHeader blockHeader) {
|
||||||
|
this.serverVersion = serverVersion;
|
||||||
|
this.serverBanner = serverBanner;
|
||||||
|
this.blockHeight = blockHeight;
|
||||||
|
this.blockHeader = blockHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getServerVersion() {
|
||||||
|
return serverVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getServerBanner() {
|
||||||
|
return serverBanner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBlockHeight() {
|
||||||
|
return blockHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlockHeader getBlockHeader() {
|
||||||
|
return blockHeader;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
|
public class ConnectionFailedEvent {
|
||||||
|
private final Throwable exception;
|
||||||
|
|
||||||
|
public ConnectionFailedEvent(Throwable exception) {
|
||||||
|
this.exception = exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Throwable getException() {
|
||||||
|
return exception;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
|
public class StatusEvent {
|
||||||
|
private final String status;
|
||||||
|
|
||||||
|
public StatusEvent(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import com.sparrowwallet.drongo.KeyPurpose;
|
||||||
import com.sparrowwallet.drongo.Utils;
|
import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.drongo.protocol.*;
|
import com.sparrowwallet.drongo.protocol.*;
|
||||||
import com.sparrowwallet.drongo.wallet.*;
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
|
import com.sparrowwallet.sparrow.event.ConnectionEvent;
|
||||||
import javafx.concurrent.ScheduledService;
|
import javafx.concurrent.ScheduledService;
|
||||||
import javafx.concurrent.Service;
|
import javafx.concurrent.Service;
|
||||||
import javafx.concurrent.Task;
|
import javafx.concurrent.Task;
|
||||||
|
@ -24,6 +25,7 @@ import java.security.cert.CertificateException;
|
||||||
import java.security.cert.CertificateFactory;
|
import java.security.cert.CertificateFactory;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class ElectrumServer {
|
public class ElectrumServer {
|
||||||
|
@ -75,6 +77,11 @@ public class ElectrumServer {
|
||||||
return transport;
|
return transport;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void connect() throws ServerException {
|
||||||
|
TcpTransport tcpTransport = (TcpTransport)getTransport();
|
||||||
|
tcpTransport.connect();
|
||||||
|
}
|
||||||
|
|
||||||
public void ping() throws ServerException {
|
public void ping() throws ServerException {
|
||||||
JsonRpcClient client = new JsonRpcClient(getTransport());
|
JsonRpcClient client = new JsonRpcClient(getTransport());
|
||||||
client.createRequest().method("server.ping").id(1).executeNullable();
|
client.createRequest().method("server.ping").id(1).executeNullable();
|
||||||
|
@ -90,6 +97,11 @@ public class ElectrumServer {
|
||||||
return client.createRequest().returnAs(String.class).method("server.banner").id(1).execute();
|
return client.createRequest().returnAs(String.class).method("server.banner").id(1).execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BlockHeaderTip subscribeBlockHeaders() throws ServerException {
|
||||||
|
JsonRpcClient client = new JsonRpcClient(getTransport());
|
||||||
|
return client.createRequest().returnAs(BlockHeaderTip.class).method("blockchain.headers.subscribe").id(1).execute();
|
||||||
|
}
|
||||||
|
|
||||||
public static synchronized void closeActiveConnection() throws ServerException {
|
public static synchronized void closeActiveConnection() throws ServerException {
|
||||||
try {
|
try {
|
||||||
if(transport != null) {
|
if(transport != null) {
|
||||||
|
@ -368,6 +380,16 @@ public class ElectrumServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class BlockHeaderTip {
|
||||||
|
public int height;
|
||||||
|
public String hex;
|
||||||
|
|
||||||
|
public BlockHeader getBlockHeader() {
|
||||||
|
byte[] blockHeaderBytes = Utils.hexToBytes(hex);
|
||||||
|
return new BlockHeader(blockHeaderBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static class TcpTransport implements Transport, Closeable {
|
public static class TcpTransport implements Transport, Closeable {
|
||||||
public static final int DEFAULT_PORT = 50001;
|
public static final int DEFAULT_PORT = 50001;
|
||||||
|
|
||||||
|
@ -376,6 +398,12 @@ public class ElectrumServer {
|
||||||
|
|
||||||
private Socket socket;
|
private Socket socket;
|
||||||
|
|
||||||
|
private String response;
|
||||||
|
|
||||||
|
private final ReentrantLock clientRequestLock = new ReentrantLock();
|
||||||
|
private boolean running = false;
|
||||||
|
private boolean reading = true;
|
||||||
|
|
||||||
public TcpTransport(HostAndPort server) {
|
public TcpTransport(HostAndPort server) {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.socketFactory = SocketFactory.getDefault();
|
this.socketFactory = SocketFactory.getDefault();
|
||||||
|
@ -383,27 +411,62 @@ public class ElectrumServer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull String pass(@NotNull String request) throws IOException {
|
public @NotNull String pass(@NotNull String request) throws IOException {
|
||||||
if(socket == null) {
|
clientRequestLock.lock();
|
||||||
socket = createSocket();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
writeRequest(socket, request);
|
writeRequest(request);
|
||||||
} catch (IOException e) {
|
return readResponse();
|
||||||
socket = createSocket();
|
} finally {
|
||||||
writeRequest(socket, request);
|
clientRequestLock.unlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
return readResponse(socket);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writeRequest(Socket socket, String request) throws IOException {
|
private void writeRequest(String request) throws IOException {
|
||||||
PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())));
|
PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())));
|
||||||
out.println(request);
|
out.println(request);
|
||||||
out.flush();
|
out.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String readResponse(Socket socket) throws IOException {
|
private synchronized String readResponse() {
|
||||||
|
while(reading) {
|
||||||
|
try {
|
||||||
|
wait();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
//Restore interrupt status and continue
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reading = true;
|
||||||
|
|
||||||
|
notifyAll();
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void readInputLoop() throws ServerException {
|
||||||
|
while(running) {
|
||||||
|
try {
|
||||||
|
String received = readInputStream();
|
||||||
|
if(received.contains("method")) {
|
||||||
|
//Handle notification
|
||||||
|
System.out.println("Notification: " + received);
|
||||||
|
} else {
|
||||||
|
response = received;
|
||||||
|
reading = false;
|
||||||
|
notifyAll();
|
||||||
|
wait();
|
||||||
|
}
|
||||||
|
} catch(InterruptedException e) {
|
||||||
|
//Restore interrupt status and continue
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
} catch(IOException e) {
|
||||||
|
if(running) {
|
||||||
|
throw new ServerException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String readInputStream() throws IOException {
|
||||||
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||||
String response = in.readLine();
|
String response = in.readLine();
|
||||||
|
|
||||||
|
@ -414,6 +477,15 @@ public class ElectrumServer {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void connect() throws ServerException {
|
||||||
|
try {
|
||||||
|
socket = createSocket();
|
||||||
|
running = true;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ServerException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected Socket createSocket() throws IOException {
|
protected Socket createSocket() throws IOException {
|
||||||
return socketFactory.createSocket(server.getHost(), server.getPortOrDefault(DEFAULT_PORT));
|
return socketFactory.createSocket(server.getHost(), server.getPortOrDefault(DEFAULT_PORT));
|
||||||
}
|
}
|
||||||
|
@ -421,6 +493,7 @@ public class ElectrumServer {
|
||||||
@Override
|
@Override
|
||||||
public void close() throws IOException {
|
public void close() throws IOException {
|
||||||
if(socket != null) {
|
if(socket != null) {
|
||||||
|
running = false;
|
||||||
socket.close();
|
socket.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -527,20 +600,38 @@ public class ElectrumServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class PingService extends ScheduledService<String> {
|
public static class ConnectionService extends ScheduledService<ConnectionEvent> implements Thread.UncaughtExceptionHandler {
|
||||||
private boolean firstCall = true;
|
private boolean firstCall = true;
|
||||||
|
private Thread reader;
|
||||||
|
private Throwable lastReaderException;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Task<String> createTask() {
|
protected Task<ConnectionEvent> createTask() {
|
||||||
return new Task<>() {
|
return new Task<>() {
|
||||||
protected String call() throws ServerException {
|
protected ConnectionEvent call() throws ServerException {
|
||||||
ElectrumServer electrumServer = new ElectrumServer();
|
ElectrumServer electrumServer = new ElectrumServer();
|
||||||
if(firstCall) {
|
if(firstCall) {
|
||||||
electrumServer.getServerVersion();
|
electrumServer.connect();
|
||||||
|
|
||||||
|
reader = new Thread(new ReadRunnable());
|
||||||
|
reader.setDaemon(true);
|
||||||
|
reader.setUncaughtExceptionHandler(ConnectionService.this);
|
||||||
|
reader.start();
|
||||||
|
|
||||||
|
List<String> serverVersion = electrumServer.getServerVersion();
|
||||||
firstCall = false;
|
firstCall = false;
|
||||||
return electrumServer.getServerBanner();
|
|
||||||
|
BlockHeaderTip tip = electrumServer.subscribeBlockHeaders();
|
||||||
|
String banner = electrumServer.getServerBanner();
|
||||||
|
|
||||||
|
return new ConnectionEvent(serverVersion, banner, tip.height, tip.getBlockHeader());
|
||||||
} else {
|
} else {
|
||||||
electrumServer.ping();
|
if(reader.isAlive()) {
|
||||||
|
electrumServer.ping();
|
||||||
|
} else {
|
||||||
|
firstCall = true;
|
||||||
|
throw new ServerException("Connection to server failed", lastReaderException);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -563,6 +654,24 @@ public class ElectrumServer {
|
||||||
public void reset() {
|
public void reset() {
|
||||||
super.reset();
|
super.reset();
|
||||||
firstCall = true;
|
firstCall = true;
|
||||||
|
lastReaderException = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void uncaughtException(Thread t, Throwable e) {
|
||||||
|
this.lastReaderException = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ReadRunnable implements Runnable {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
TcpTransport tcpTransport = (TcpTransport)getTransport();
|
||||||
|
tcpTransport.readInputLoop();
|
||||||
|
} catch (ServerException e) {
|
||||||
|
throw new RuntimeException(e.getCause() != null ? e.getCause() : e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue