diff --git a/build.gradle b/build.gradle index 00d03b9d..c3c3e22a 100644 --- a/build.gradle +++ b/build.gradle @@ -69,7 +69,7 @@ dependencies { exclude group: 'org.openjfx', module: 'javafx-web' exclude group: 'org.openjfx', module: 'javafx-media' } - implementation('dev.bwt:bwt-jni:0.1.5') + implementation('dev.bwt:bwt-jni:0.1.6') testImplementation('junit:junit:4.12') } diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index d39e5904..fe3aebbc 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -23,6 +23,7 @@ import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.io.*; import com.sparrowwallet.sparrow.net.ElectrumServer; +import com.sparrowwallet.sparrow.net.ServerType; import com.sparrowwallet.sparrow.preferences.PreferencesDialog; import com.sparrowwallet.sparrow.transaction.TransactionController; import com.sparrowwallet.sparrow.transaction.TransactionData; @@ -223,6 +224,7 @@ public class AppController implements Initializable { showTxHex.setSelected(Config.get().isShowTransactionHex()); exportWallet.setDisable(true); + setServerType(Config.get().getServerType()); serverToggle.setSelected(isConnected()); onlineProperty().bindBidirectional(serverToggle.selectedProperty()); onlineProperty().addListener((observable, oldValue, newValue) -> { @@ -1042,6 +1044,14 @@ public class AppController implements Initializable { return contextMenu; } + public void setServerType(ServerType serverType) { + if(serverType == ServerType.BITCOIN_CORE && !serverToggle.getStyleClass().contains("core-server")) { + serverToggle.getStyleClass().add("core-server"); + } else { + serverToggle.getStyleClass().remove("core-server"); + } + } + public void setTheme(ActionEvent event) { Theme selectedTheme = (Theme)theme.getSelectedToggle().getUserData(); if(Config.get().getTheme() != selectedTheme) { @@ -1063,6 +1073,11 @@ public class AppController implements Initializable { } } + @Subscribe + public void serverTypeChanged(ServerTypeChangedEvent event) { + setServerType(event.getServerType()); + } + @Subscribe public void tabSelected(TabSelectedEvent event) { if(tabs.getTabs().contains(event.getTab())) { diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WelcomeDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WelcomeDialog.java index 44192217..78056030 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WelcomeDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WelcomeDialog.java @@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.Mode; +import com.sparrowwallet.sparrow.net.ServerType; import javafx.application.HostServices; import javafx.geometry.Insets; import javafx.scene.control.*; @@ -13,13 +14,10 @@ import org.controlsfx.control.StatusBar; import org.controlsfx.control.ToggleSwitch; public class WelcomeDialog extends Dialog { - private static final String[] ELECTRUM_SERVERS = new String[]{ - "ElectrumX (Recommended)", "https://github.com/spesmilo/electrumx", - "electrs", "https://github.com/romanz/electrs", - "esplora-electrs", "https://github.com/Blockstream/electrs"}; - private final HostServices hostServices; + private ServerType serverType = ServerType.ELECTRUM_SERVER; + public WelcomeDialog(HostServices services) { this.hostServices = services; @@ -30,7 +28,7 @@ public class WelcomeDialog extends Dialog { dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm()); AppServices.setStageIcon(dialogPane.getScene().getWindow()); dialogPane.setPrefWidth(600); - dialogPane.setPrefHeight(480); + dialogPane.setPrefHeight(520); Image image = new Image("image/sparrow-small.png", 50, 50, false, false); if (!image.isError()) { @@ -46,16 +44,10 @@ public class WelcomeDialog extends Dialog { final VBox content = new VBox(20); content.setPadding(new Insets(20, 20, 20, 20)); - content.getChildren().add(createParagraph("Sparrow can operate in both an online and offline mode. In the online mode it connects to your Electrum server to display transaction history. In the offline mode it is useful as a transaction editor and as an airgapped multisig coordinator.")); - content.getChildren().add(createParagraph("For privacy and security reasons it is not recommended to use a public Electrum server. Install an Electrum server that connects to your full node to index the blockchain and provide full privacy. Examples include:")); - - VBox linkBox = new VBox(); - for(int i = 0; i < ELECTRUM_SERVERS.length; i+=2) { - linkBox.getChildren().add(createBulletedLink(ELECTRUM_SERVERS[i], ELECTRUM_SERVERS[i+1])); - } - content.getChildren().add(linkBox); - - content.getChildren().add(createParagraph("You can change your mode at any time using the toggle in the status bar:")); + content.getChildren().add(createParagraph("Sparrow can operate in both an online and offline mode. In the online mode it connects to your Bitcoin Core node or Electrum server to display transaction history. In the offline mode it is useful as a transaction editor and as an airgapped multisig coordinator.")); + content.getChildren().add(createParagraph("Connecting Sparrow to your Bitcoin Core node ensures your privacy, while connecting Sparrow to your own Electrum server ensures wallets load quicker, you have access to a full blockchain explorer, and your public keys are always encrypted on disk. Examples of Electrum servers include ElectrumX and electrs.")); + content.getChildren().add(createParagraph("It's also possible to connect Sparrow to a public Electrum server (such as blockstream.info:700) but this is not recommended as you will share your public key information with that server.")); + content.getChildren().add(createParagraph("You can change your mode at any time using the toggle in the status bar. A blue toggle indicates you are connected to an Electrum server, while a green toggle indicates you are connected to a Bitcoin Code node.")); content.getChildren().add(createStatusBar(onlineButtonType, offlineButtonType)); dialogPane.setContent(content); @@ -70,16 +62,6 @@ public class WelcomeDialog extends Dialog { return label; } - private HyperlinkLabel createBulletedLink(String name, String url) { - String[] nameParts = name.split(" "); - HyperlinkLabel label = new HyperlinkLabel(" \u2022 [" + nameParts[0] + "] " + (nameParts.length > 1 ? nameParts[1] : "")); - label.setOnAction(event -> { - hostServices.showDocument(url); - }); - - return label; - } - private StatusBar createStatusBar(ButtonType onlineButtonType, ButtonType offlineButtonType) { StatusBar statusBar = new StatusBar(); statusBar.setText("Online Mode"); @@ -97,7 +79,18 @@ public class WelcomeDialog extends Dialog { onlineButton.setDefaultButton(newValue); Button offlineButton = (Button) getDialogPane().lookupButton(offlineButtonType); offlineButton.setDefaultButton(!newValue); - statusBar.setText(newValue ? "Online Mode" : "Offline Mode"); + + if(!newValue) { + serverType = (serverType == ServerType.BITCOIN_CORE ? ServerType.ELECTRUM_SERVER : ServerType.BITCOIN_CORE); + + if(serverType == ServerType.BITCOIN_CORE && !toggleSwitch.getStyleClass().contains("core-server")) { + toggleSwitch.getStyleClass().add("core-server"); + } else { + toggleSwitch.getStyleClass().remove("core-server"); + } + } + + statusBar.setText(newValue ? "Online Mode: " + serverType.getName() : "Offline Mode"); }); toggleSwitch.setSelected(true); diff --git a/src/main/java/com/sparrowwallet/sparrow/event/BwtReadyStatusEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/BwtReadyStatusEvent.java index 7598ec43..97760f6a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/BwtReadyStatusEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/BwtReadyStatusEvent.java @@ -1,14 +1,7 @@ package com.sparrowwallet.sparrow.event; public class BwtReadyStatusEvent extends BwtStatusEvent { - private final long shutdownPtr; - - public BwtReadyStatusEvent(String status, long shutdownPtr) { + public BwtReadyStatusEvent(String status) { super(status); - this.shutdownPtr = shutdownPtr; - } - - public long getShutdownPtr() { - return shutdownPtr; } } diff --git a/src/main/java/com/sparrowwallet/sparrow/event/BwtSyncStatusEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/BwtSyncStatusEvent.java index e2056c55..426cecce 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/BwtSyncStatusEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/BwtSyncStatusEvent.java @@ -1,10 +1,12 @@ package com.sparrowwallet.sparrow.event; +import java.util.Date; + public class BwtSyncStatusEvent extends BwtStatusEvent { private final int progress; - private final int tip; + private final Date tip; - public BwtSyncStatusEvent(String status, int progress, int tip) { + public BwtSyncStatusEvent(String status, int progress, Date tip) { super(status); this.progress = progress; this.tip = tip; @@ -18,7 +20,7 @@ public class BwtSyncStatusEvent extends BwtStatusEvent { return progress == 100; } - public int getTip() { + public Date getTip() { return tip; } } diff --git a/src/main/java/com/sparrowwallet/sparrow/event/ServerTypeChangedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/ServerTypeChangedEvent.java new file mode 100644 index 00000000..f98173e9 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/ServerTypeChangedEvent.java @@ -0,0 +1,15 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.sparrow.net.ServerType; + +public class ServerTypeChangedEvent { + private final ServerType serverType; + + public ServerTypeChangedEvent(ServerType serverType) { + this.serverType = serverType; + } + + public ServerType getServerType() { + return serverType; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java b/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java index d24d6151..ee36dadf 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java @@ -204,7 +204,8 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc { } try { - return new RetryLogic>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(batchRequest::execute); + //The server may return an error if the transaction has not yet been broadcasted - this is a valid state so only try once + return new RetryLogic>(1, RETRY_DELAY, IllegalStateException.class).getResult(batchRequest::execute); } catch(JsonRpcBatchException e) { log.warn("Some errors retrieving transactions: " + e.getErrors()); return (Map)e.getSuccesses(); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/Bwt.java b/src/main/java/com/sparrowwallet/sparrow/net/Bwt.java index aa717735..33d52704 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/Bwt.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/Bwt.java @@ -23,6 +23,7 @@ import java.util.*; public class Bwt { private static final Logger log = LoggerFactory.getLogger(Bwt.class); + private static final int IMPORT_BATCH_SIZE = 350; private Long shutdownPtr; private boolean terminating; @@ -85,7 +86,8 @@ public class Bwt { bwtConfig.descriptors = outputDescriptors; bwtConfig.rescanSince = (rescanSince == null || rescanSince < 0 ? "now" : rescanSince); bwtConfig.forceRescan = forceRescan; - bwtConfig.gapLimit = gapLimit; + //bwtConfig.initialImportSize = IMPORT_BATCH_SIZE; + bwtConfig.gapLimit = IMPORT_BATCH_SIZE; } else { bwtConfig.requireAddresses = false; } @@ -220,9 +222,14 @@ public class Bwt { protected Void call() { CallbackNotifier notifier = new CallbackNotifier() { @Override - public void onBooting() { + public void onBooting(long shutdownPtr) { log.debug("Booting bwt"); - if(!terminating) { + + Bwt.this.shutdownPtr = shutdownPtr; + if(terminating) { + Bwt.this.shutdown(); + terminating = false; + } else { Platform.runLater(() -> EventManager.get().post(new BwtBootStatusEvent("Connecting to Bitcoin Core node at " + Config.get().getCoreServer() + "..."))); } } @@ -230,9 +237,10 @@ public class Bwt { @Override public void onSyncProgress(float progress, int tip) { int percent = (int) (progress * 100.0); + Date tipDate = new Date((long)tip * 1000); log.debug("Syncing " + percent + "%"); if(!terminating) { - Platform.runLater(() -> EventManager.get().post(new BwtSyncStatusEvent("Syncing" + (percent < 100 ? " (" + percent + "%)" : ""), percent, tip))); + Platform.runLater(() -> EventManager.get().post(new BwtSyncStatusEvent("Syncing" + (percent < 100 ? " (" + percent + "%)" : ""), percent, tipDate))); } } @@ -260,14 +268,10 @@ public class Bwt { } @Override - public void onReady(long shutdownPtr) { + public void onReady() { log.debug("Bwt ready"); - Bwt.this.shutdownPtr = shutdownPtr; - if(terminating) { - Bwt.this.shutdown(); - terminating = false; - } else { - Platform.runLater(() -> EventManager.get().post(new BwtReadyStatusEvent("Server ready", shutdownPtr))); + if(!terminating) { + Platform.runLater(() -> EventManager.get().post(new BwtReadyStatusEvent("Server ready"))); } } }; diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 8f6a48c9..128635cc 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -54,7 +54,7 @@ public class ElectrumServer { if(Config.get().getServerType() == ServerType.BITCOIN_CORE) { if(bwtElectrumServer == null) { - throw new ServerException("BWT server not started"); + throw new ServerException("Could not connect to Bitcoin Core RPC"); } electrumServer = bwtElectrumServer; } else if(Config.get().getServerType() == ServerType.ELECTRUM_SERVER) { @@ -928,14 +928,16 @@ public class ElectrumServer { } private void shutdownBwt() { - Bwt.DisconnectionService disconnectionService = bwt.getDisconnectionService(); - disconnectionService.setOnSucceeded(workerStateEvent -> { - ElectrumServer.bwtElectrumServer = null; - }); - disconnectionService.setOnFailed(workerStateEvent -> { - log.error("Failed to stop BWT", workerStateEvent.getSource().getException()); - }); - Platform.runLater(disconnectionService::start); + if(Config.get().getServerType() == ServerType.BITCOIN_CORE) { + Bwt.DisconnectionService disconnectionService = bwt.getDisconnectionService(); + disconnectionService.setOnSucceeded(workerStateEvent -> { + ElectrumServer.bwtElectrumServer = null; + }); + disconnectionService.setOnFailed(workerStateEvent -> { + log.error("Failed to stop BWT", workerStateEvent.getSource().getException()); + }); + Platform.runLater(disconnectionService::start); + } } @Override diff --git a/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java b/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java index af77b4b6..5bbd5884 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java @@ -181,7 +181,8 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc { Map result = new LinkedHashMap<>(); for(String txid : txids) { try { - VerboseTransaction verboseTransaction = new RetryLogic(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(() -> + //The server may return an error if the transaction has not yet been broadcasted - this is a valid state so only try once + VerboseTransaction verboseTransaction = new RetryLogic(1, RETRY_DELAY, IllegalStateException.class).getResult(() -> client.createRequest().returnAs(VerboseTransaction.class).method("blockchain.transaction.get").id(idCounter.incrementAndGet()).params(txid, true).execute()); result.put(txid, verboseTransaction); } catch(Exception e) { diff --git a/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java b/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java index 36116f12..efbed275 100644 --- a/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java +++ b/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java @@ -6,10 +6,7 @@ import com.sparrowwallet.drongo.Network; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.control.TextFieldValidator; import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch; -import com.sparrowwallet.sparrow.event.BwtStatusEvent; -import com.sparrowwallet.sparrow.event.BwtSyncStatusEvent; -import com.sparrowwallet.sparrow.event.ConnectionEvent; -import com.sparrowwallet.sparrow.event.RequestDisconnectEvent; +import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.net.*; @@ -38,6 +35,8 @@ import javax.net.ssl.SSLHandshakeException; import java.io.File; import java.io.FileInputStream; import java.security.cert.CertificateFactory; +import java.text.DateFormat; +import java.text.SimpleDateFormat; import java.util.List; public class ServerPreferencesController extends PreferencesDetailController { @@ -141,6 +140,7 @@ public class ServerPreferencesController extends PreferencesDetailController { config.setServerType(serverType); testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.QUESTION_CIRCLE, "")); testResults.clear(); + EventManager.get().post(new ServerTypeChangedEvent(serverType)); } else if(oldValue != null) { oldValue.setSelected(true); } @@ -626,7 +626,9 @@ public class ServerPreferencesController extends PreferencesDetailController { @Subscribe public void bwtSyncStatus(BwtSyncStatusEvent event) { if(connectionService != null && connectionService.isRunning() && event.getProgress() < 100) { - testResults.appendText("\nThe connection to the Bitcoin Core node was successful, but it is still syncing to the the blockchain tip at " + event.getTip() + " blocks (" + event.getProgress() + "% completed)"); + DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm"); + testResults.appendText("\nThe connection to the Bitcoin Core node was successful, but it is still syncing and cannot be used yet."); + testResults.appendText("\nCurrently " + event.getProgress() + "% completed to date " + dateFormat.format(event.getTip())); testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.QUESTION_CIRCLE, null)); connectionService.cancel(); } diff --git a/src/main/resources/com/sparrowwallet/sparrow/general.css b/src/main/resources/com/sparrowwallet/sparrow/general.css index 8a10fdfa..9aab5b42 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/general.css +++ b/src/main/resources/com/sparrowwallet/sparrow/general.css @@ -159,3 +159,8 @@ .root .header-panel { -fx-background-color: -fx-box-border, derive(-fx-background, 10%); } + +.core-server.toggle-switch:selected .thumb-area { + -fx-background-color: linear-gradient(to bottom, derive(-fx-text-box-border, -20%), derive(-fx-text-box-border, -30%)), linear-gradient(to bottom, derive(#50a14f, 30%), #50a14f); + -fx-background-insets: 0, 1; +} \ No newline at end of file diff --git a/src/main/resources/native/osx/x64/libbwt_jni.dylib b/src/main/resources/native/osx/x64/libbwt_jni.dylib index dbbdcc35..87354d38 100755 Binary files a/src/main/resources/native/osx/x64/libbwt_jni.dylib and b/src/main/resources/native/osx/x64/libbwt_jni.dylib differ