diff --git a/build.gradle b/build.gradle index d0383d33..c3c3e22a 100644 --- a/build.gradle +++ b/build.gradle @@ -69,6 +69,7 @@ dependencies { exclude group: 'org.openjfx', module: 'javafx-web' exclude group: 'org.openjfx', module: 'javafx-media' } + implementation('dev.bwt:bwt-jni:0.1.6') testImplementation('junit:junit:4.12') } diff --git a/src/main/java/com/sparrowwallet/sparrow/AboutController.java b/src/main/java/com/sparrowwallet/sparrow/AboutController.java index 07472f54..4ae2d8d3 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AboutController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AboutController.java @@ -22,4 +22,8 @@ public class AboutController { public void close(ActionEvent event) { stage.close(); } + + public void openDonate(ActionEvent event) { + AppServices.get().getApplication().getHostServices().showDocument("https://sparrowwallet.com/donate"); + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 32c8f3ec..b396bbe1 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; @@ -110,6 +111,8 @@ public class AppController implements Initializable { @FXML private UnlabeledToggleSwitch serverToggle; + private PauseTransition wait; + private Timeline statusTimeline; @Override @@ -170,7 +173,7 @@ public class AppController implements Initializable { boolean walletAdded = c.getAddedSubList().stream().anyMatch(tab -> ((TabData)tab.getUserData()).getType() == TabData.TabType.WALLET); boolean walletRemoved = c.getRemoved().stream().anyMatch(tab -> ((TabData)tab.getUserData()).getType() == TabData.TabType.WALLET); if(walletAdded || walletRemoved) { - EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), getOpenWallets())); + EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), getOpenWalletTabData())); } List closedWalletTabs = c.getRemoved().stream().map(tab -> (TabData)tab.getUserData()) @@ -194,7 +197,7 @@ public class AppController implements Initializable { tabs.getScene().getWindow().setOnCloseRequest(event -> { EventManager.get().unregister(this); - EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), Collections.emptyMap())); + EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), Collections.emptyList())); }); BitcoinUnit unit = Config.get().getBitcoinUnit(); @@ -221,11 +224,15 @@ public class AppController implements Initializable { showTxHex.setSelected(Config.get().isShowTransactionHex()); exportWallet.setDisable(true); - serverToggle.setSelected(isOnline()); + setServerType(Config.get().getServerType()); + serverToggle.setSelected(isConnected()); onlineProperty().bindBidirectional(serverToggle.selectedProperty()); onlineProperty().addListener((observable, oldValue, newValue) -> { Platform.runLater(() -> setServerToggleTooltip(getCurrentBlockHeight())); }); + serverToggle.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> { + Config.get().setMode(serverToggle.isSelected() ? Mode.ONLINE : Mode.OFFLINE); + }); openTransactionIdItem.disableProperty().bind(onlineProperty().not()); } @@ -451,21 +458,30 @@ public class AppController implements Initializable { } } - public Map getOpenWallets() { - Map openWallets = new LinkedHashMap<>(); + + public List getOpenWalletTabData() { + List openWalletTabData = new ArrayList<>(); for(Tab tab : tabs.getTabs()) { TabData tabData = (TabData)tab.getUserData(); if(tabData.getType() == TabData.TabType.WALLET) { - WalletTabData walletTabData = (WalletTabData) tabData; - openWallets.put(walletTabData.getWallet(), walletTabData.getStorage()); + openWalletTabData.add((WalletTabData)tabData); } } + return openWalletTabData; + } + + public Map getOpenWallets() { + Map openWallets = new LinkedHashMap<>(); + + for(WalletTabData walletTabData : getOpenWalletTabData()){ + openWallets.put(walletTabData.getWallet(), walletTabData.getStorage()); + } + return openWallets; } - public void selectTab(Wallet wallet) { for(Tab tab : tabs.getTabs()) { TabData tabData = (TabData) tab.getUserData(); @@ -521,16 +537,17 @@ public class AppController implements Initializable { } private void setServerToggleTooltip(Integer currentBlockHeight) { - serverToggle.setTooltip(new Tooltip(AppServices.isOnline() ? "Connected to " + Config.get().getElectrumServer() + (currentBlockHeight != null ? " at height " + currentBlockHeight : "") : "Disconnected")); + serverToggle.setTooltip(new Tooltip(AppServices.isConnected() ? "Connected to " + Config.get().getServerAddress() + (currentBlockHeight != null ? " at height " + currentBlockHeight : "") : "Disconnected")); } public void newWallet(ActionEvent event) { WalletNameDialog dlg = new WalletNameDialog(); - Optional walletName = dlg.showAndWait(); - if(walletName.isPresent()) { - File walletFile = Storage.getWalletFile(walletName.get()); + Optional optNameAndBirthDate = dlg.showAndWait(); + if(optNameAndBirthDate.isPresent()) { + WalletNameDialog.NameAndBirthDate nameAndBirthDate = optNameAndBirthDate.get(); + File walletFile = Storage.getWalletFile(nameAndBirthDate.getName()); Storage storage = new Storage(walletFile); - Wallet wallet = new Wallet(walletName.get(), PolicyType.SINGLE, ScriptType.P2WPKH); + Wallet wallet = new Wallet(nameAndBirthDate.getName(), PolicyType.SINGLE, ScriptType.P2WPKH, nameAndBirthDate.getBirthDate()); addWalletTabOrWindow(storage, wallet, false); } } @@ -695,8 +712,17 @@ public class AppController implements Initializable { } private void addImportedWallet(Wallet wallet) { - File walletFile = Storage.getExistingWallet(wallet.getName()); + WalletNameDialog nameDlg = new WalletNameDialog(wallet.getName()); + Optional optNameAndBirthDate = nameDlg.showAndWait(); + if(optNameAndBirthDate.isPresent()) { + WalletNameDialog.NameAndBirthDate nameAndBirthDate = optNameAndBirthDate.get(); + wallet.setName(nameAndBirthDate.getName()); + wallet.setBirthDate(nameAndBirthDate.getBirthDate()); + } else { + return; + } + File walletFile = Storage.getExistingWallet(wallet.getName()); if(walletFile != null) { Alert alert = new Alert(Alert.AlertType.CONFIRMATION); alert.setTitle("Existing wallet found"); @@ -835,6 +861,8 @@ public class AppController implements Initializable { tab.setContent(walletLoader.load()); WalletController controller = walletLoader.getController(); + EventManager.get().post(new WalletOpeningEvent(storage, wallet)); + //Note that only one WalletForm is created per wallet tab, and registered to listen for events. All wallet controllers (except SettingsController) share this instance. WalletForm walletForm = new WalletForm(storage, wallet); EventManager.get().register(walletForm); @@ -1019,6 +1047,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) { @@ -1040,6 +1076,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())) { @@ -1152,7 +1193,10 @@ public class AppController implements Initializable { public void statusUpdated(StatusEvent event) { statusBar.setText(event.getStatus()); - PauseTransition wait = new PauseTransition(Duration.seconds(20)); + if(wait != null && wait.getStatus() == Animation.Status.RUNNING) { + wait.stop(); + } + wait = new PauseTransition(Duration.seconds(20)); wait.setOnFinished((e) -> { if(statusBar.getText().equals(event.getStatus())) { statusBar.setText(""); @@ -1225,6 +1269,41 @@ public class AppController implements Initializable { } } + @Subscribe + public void bwtBootStatus(BwtBootStatusEvent event) { + serverToggle.setDisable(true); + statusUpdated(new StatusEvent(event.getStatus())); + } + + @Subscribe + public void bwtSyncStatus(BwtSyncStatusEvent event) { + serverToggle.setDisable(false); + if((AppServices.isConnecting() || AppServices.isConnected()) && !event.isCompleted()) { + statusUpdated(new StatusEvent(event.getStatus())); + } + } + + @Subscribe + public void bwtScanStatus(BwtScanStatusEvent event) { + serverToggle.setDisable(true); + if((AppServices.isConnecting() || AppServices.isConnected()) && !event.isCompleted()) { + statusUpdated(new StatusEvent(event.getStatus())); + } + } + + @Subscribe + public void bwtReadyStatus(BwtReadyStatusEvent event) { + serverToggle.setDisable(false); + } + + @Subscribe + public void disconnection(DisconnectionEvent event) { + serverToggle.setDisable(false); + if(!AppServices.isConnecting() && !AppServices.isConnected() && !statusBar.getText().startsWith("Connection error")) { + statusUpdated(new StatusEvent("Disconnected")); + } + } + @Subscribe public void newBlock(NewBlockEvent event) { setServerToggleTooltip(event.getHeight()); @@ -1278,7 +1357,7 @@ public class AppController implements Initializable { @Subscribe public void requestOpenWallets(RequestOpenWalletsEvent event) { - EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), getOpenWallets())); + EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), getOpenWalletTabData())); } @Subscribe diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index 8a317331..5f75fdbb 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -12,10 +12,7 @@ import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Device; import com.sparrowwallet.sparrow.io.Hwi; import com.sparrowwallet.sparrow.io.Storage; -import com.sparrowwallet.sparrow.net.ElectrumServer; -import com.sparrowwallet.sparrow.net.ExchangeSource; -import com.sparrowwallet.sparrow.net.MempoolRateSize; -import com.sparrowwallet.sparrow.net.VersionCheckService; +import com.sparrowwallet.sparrow.net.*; import javafx.application.Platform; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; @@ -27,6 +24,7 @@ import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; import javafx.scene.image.Image; import javafx.scene.text.Font; import javafx.stage.Stage; @@ -57,7 +55,7 @@ public class AppServices { private final MainApp application; - private final Map> walletWindows = new LinkedHashMap<>(); + private final Map> walletWindows = new LinkedHashMap<>(); private static final BooleanProperty onlineProperty = new SimpleBooleanProperty(false); @@ -86,7 +84,6 @@ public class AppServices { private final ChangeListener onlineServicesListener = new ChangeListener<>() { @Override public void changed(ObservableValue observable, Boolean oldValue, Boolean online) { - Config.get().setMode(online ? Mode.ONLINE : Mode.OFFLINE); if(online) { restartService(connectionService); @@ -110,7 +107,7 @@ public class AppServices { service.cancel(); } - if(service.getState() == Worker.State.CANCELLED) { + if(service.getState() == Worker.State.CANCELLED || service.getState() == Worker.State.FAILED) { service.reset(); } @@ -127,7 +124,7 @@ public class AppServices { public void start() { Config config = Config.get(); connectionService = createConnectionService(); - if(config.getMode() == Mode.ONLINE && config.getElectrumServer() != null && !config.getElectrumServer().isEmpty()) { + if(config.getMode() == Mode.ONLINE && config.getServerAddress() != null && !config.getServerAddress().isEmpty()) { connectionService.start(); } @@ -146,6 +143,20 @@ public class AppServices { onlineProperty.addListener(onlineServicesListener); } + public void stop() { + if(connectionService != null) { + connectionService.cancel(); + } + + if(ratesService != null) { + ratesService.cancel(); + } + + if(versionCheckService != null) { + versionCheckService.cancel(); + } + } + private ElectrumServer.ConnectionService createConnectionService() { ElectrumServer.ConnectionService connectionService = new ElectrumServer.ConnectionService(); connectionService.setPeriod(new Duration(SERVER_PING_PERIOD)); @@ -159,6 +170,8 @@ public class AppServices { }); connectionService.setOnSucceeded(successEvent -> { + connectionService.setRestartOnFailure(true); + onlineProperty.removeListener(onlineServicesListener); onlineProperty.setValue(true); onlineProperty.addListener(onlineServicesListener); @@ -171,6 +184,10 @@ public class AppServices { //Close connection here to create a new transport next time we try connectionService.resetConnection(); + if(failEvent.getSource().getException() instanceof ServerConfigException) { + connectionService.setRestartOnFailure(false); + } + onlineProperty.removeListener(onlineServicesListener); onlineProperty.setValue(false); onlineProperty.addListener(onlineServicesListener); @@ -265,17 +282,24 @@ public class AppServices { return application; } - public Map getOpenWallets(Window window) { - return walletWindows.get(window); + public Map getOpenWallets() { + Map openWallets = new LinkedHashMap<>(); + for(List walletTabDataList : walletWindows.values()) { + for(WalletTabData walletTabData : walletTabDataList) { + openWallets.put(walletTabData.getWallet(), walletTabData.getStorage()); + } + } + + return openWallets; } public Window getWindowForWallet(Storage storage) { - Optional optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().values().stream().anyMatch(storage1 -> storage1.getWalletFile().equals(storage.getWalletFile()))).map(Map.Entry::getKey).findFirst(); + Optional optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().stream().anyMatch(walletTabData -> walletTabData.getStorage().getWalletFile().equals(storage.getWalletFile()))).map(Map.Entry::getKey).findFirst(); return optWindow.orElse(null); } public Window getWindowForPSBT(PSBT psbt) { - Optional optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().keySet().stream().anyMatch(wallet -> wallet.canSign(psbt))).map(Map.Entry::getKey).findFirst(); + Optional optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().stream().anyMatch(walletTabData -> walletTabData.getWallet().canSign(psbt))).map(Map.Entry::getKey).findFirst(); return optWindow.orElse(null); } @@ -283,8 +307,12 @@ public class AppServices { return walletWindows.keySet().stream().mapToDouble(Window::getX).max().orElse(0d); } - public static boolean isOnline() { - return onlineProperty.get(); + public static boolean isConnecting() { + return get().connectionService != null && get().connectionService.isConnecting(); + } + + public static boolean isConnected() { + return onlineProperty.get() && get().connectionService.isConnected(); } public static BooleanProperty onlineProperty() { @@ -335,14 +363,21 @@ public class AppServices { payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI); } - public static void showErrorDialog(String title, String content) { - Alert alert = new Alert(Alert.AlertType.ERROR); + public static Optional showWarningDialog(String title, String content, ButtonType... buttons) { + return showAlertDialog(title, content, Alert.AlertType.WARNING, buttons); + } + + public static Optional showErrorDialog(String title, String content, ButtonType... buttons) { + return showAlertDialog(title, content, Alert.AlertType.ERROR, buttons); + } + + public static Optional showAlertDialog(String title, String content, Alert.AlertType alertType, ButtonType... buttons) { + Alert alert = new Alert(alertType, content, buttons); setStageIcon(alert.getDialogPane().getScene().getWindow()); alert.getDialogPane().getScene().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm()); alert.setTitle(title); alert.setHeaderText(title); - alert.setContentText(content); - alert.showAndWait(); + return alert.showAndWait(); } public static void setStageIcon(Window window) { @@ -365,7 +400,7 @@ public class AppServices { addMempoolRateSizes(event.getMempoolRateSizes()); minimumRelayFeeRate = event.getMinimumRelayFeeRate(); String banner = event.getServerBanner(); - String status = "Connected to " + Config.get().getElectrumServer() + " at height " + event.getBlockHeight(); + String status = "Connected to " + Config.get().getServerAddress() + " at height " + event.getBlockHeight(); EventManager.get().post(new StatusEvent(status)); } @@ -431,25 +466,25 @@ public class AppServices { @Subscribe public void openWallets(OpenWalletsEvent event) { - if(event.getWalletsMap().isEmpty()) { + if(event.getWalletTabDataList().isEmpty()) { walletWindows.remove(event.getWindow()); } else { - walletWindows.put(event.getWindow(), event.getWalletsMap()); + walletWindows.put(event.getWindow(), event.getWalletTabDataList()); } - List> allWallets = walletWindows.values().stream().flatMap(map -> map.entrySet().stream()).collect(Collectors.toList()); + List allWallets = walletWindows.values().stream().flatMap(Collection::stream).collect(Collectors.toList()); Platform.runLater(() -> { if(!Window.getWindows().isEmpty()) { - List walletFiles = allWallets.stream().map(entry -> entry.getValue().getWalletFile()).collect(Collectors.toList()); + List walletFiles = allWallets.stream().map(walletTabData -> walletTabData.getStorage().getWalletFile()).collect(Collectors.toList()); Config.get().setRecentWalletFiles(walletFiles); } }); boolean usbWallet = false; - for(Map.Entry entry : allWallets) { - Wallet wallet = entry.getKey(); - Storage storage = entry.getValue(); + for(WalletTabData walletTabData : allWallets) { + Wallet wallet = walletTabData.getWallet(); + Storage storage = walletTabData.getStorage(); if(!storage.getWalletFile().exists() || wallet.containsSource(KeystoreSource.HW_USB)) { usbWallet = true; @@ -485,4 +520,28 @@ public class AppServices { public void requestDisconnect(RequestDisconnectEvent event) { onlineProperty.set(false); } + + @Subscribe + public void walletSettingsChanged(WalletSettingsChangedEvent event) { + restartBwt(event.getWallet()); + } + + @Subscribe + public void walletOpening(WalletOpeningEvent event) { + restartBwt(event.getWallet()); + } + + private void restartBwt(Wallet wallet) { + if(Config.get().getServerType() == ServerType.BITCOIN_CORE && isConnected() && wallet.isValid()) { + connectionService.cancel(); + } + } + + @Subscribe + public void bwtShutdown(BwtShutdownEvent event) { + if(onlineProperty().get() && !connectionService.isRunning()) { + connectionService.reset(); + connectionService.start(); + } + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/MainApp.java b/src/main/java/com/sparrowwallet/sparrow/MainApp.java index 235c562b..83213f8e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/MainApp.java +++ b/src/main/java/com/sparrowwallet/sparrow/MainApp.java @@ -9,14 +9,10 @@ import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.FileType; import com.sparrowwallet.sparrow.io.IOUtils; import com.sparrowwallet.sparrow.io.Storage; +import com.sparrowwallet.sparrow.net.ServerType; import com.sparrowwallet.sparrow.preferences.PreferenceGroup; import com.sparrowwallet.sparrow.preferences.PreferencesDialog; import javafx.application.Application; -import javafx.application.Platform; -import javafx.fxml.FXMLLoader; -import javafx.scene.Parent; -import javafx.scene.Scene; -import javafx.scene.image.Image; import javafx.scene.text.Font; import javafx.stage.Stage; import org.controlsfx.glyphfont.GlyphFontRegistry; @@ -66,6 +62,10 @@ public class MainApp extends Application { } } + if(Config.get().getServerType() == null && Config.get().getCoreServer() == null && Config.get().getElectrumServer() != null) { + Config.get().setServerType(ServerType.ELECTRUM_SERVER); + } + AppServices.initialize(this); AppController appController = AppServices.newAppWindow(stage); @@ -94,6 +94,7 @@ public class MainApp extends Application { @Override public void stop() throws Exception { + AppServices.get().stop(); mainStage.close(); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java b/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java index ddc76df0..14d30a09 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java @@ -2,12 +2,27 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.drongo.BitcoinUnit; import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.AppServices; +import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.event.WalletDataChangedEvent; import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent; +import com.sparrowwallet.sparrow.event.WalletSettingsChangedEvent; import com.sparrowwallet.sparrow.io.Config; +import com.sparrowwallet.sparrow.io.Storage; +import com.sparrowwallet.sparrow.net.ServerType; import com.sparrowwallet.sparrow.wallet.Entry; import javafx.application.Platform; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.control.TreeTableView; +import javafx.scene.layout.StackPane; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Optional; public class CoinTreeTable extends TreeTableView { private BitcoinUnit bitcoinUnit; @@ -51,10 +66,46 @@ public class CoinTreeTable extends TreeTableView { setPlaceholder(new Label("Loading transactions...")); } } else { - setPlaceholder(new Label("No transactions")); + setPlaceholder(getDefaultPlaceholder(event.getWallet())); } }); } } } + + protected Node getDefaultPlaceholder(Wallet wallet) { + StackPane stackPane = new StackPane(); + stackPane.getChildren().add(AppServices.isConnecting() ? new Label("Loading transactions...") : new Label("No transactions")); + + if(Config.get().getServerType() == ServerType.BITCOIN_CORE && !AppServices.isConnecting()) { + Hyperlink hyperlink = new Hyperlink(); + hyperlink.setTranslateY(30); + hyperlink.setOnAction(event -> { + WalletBirthDateDialog dlg = new WalletBirthDateDialog(wallet.getBirthDate()); + Optional optDate = dlg.showAndWait(); + if(optDate.isPresent()) { + wallet.setBirthDate(optDate.get()); + Storage storage = AppServices.get().getOpenWallets().get(wallet); + if(storage != null) { + //Trigger background save of birthdate + EventManager.get().post(new WalletDataChangedEvent(wallet)); + //Trigger full wallet rescan + wallet.clearHistory(); + EventManager.get().post(new WalletSettingsChangedEvent(wallet, storage.getWalletFile())); + } + } + }); + if(wallet.getBirthDate() == null) { + hyperlink.setText("Scan for previous transactions?"); + } else { + DateFormat dateFormat = new SimpleDateFormat(DateStringConverter.FORMAT_PATTERN); + hyperlink.setText("Scan for transactions earlier than " + dateFormat.format(wallet.getBirthDate()) + "?"); + } + + stackPane.getChildren().add(hyperlink); + } + + stackPane.setAlignment(Pos.CENTER); + return stackPane; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DateStringConverter.java b/src/main/java/com/sparrowwallet/sparrow/control/DateStringConverter.java new file mode 100644 index 00000000..c993bb55 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/DateStringConverter.java @@ -0,0 +1,29 @@ +package com.sparrowwallet.sparrow.control; + +import javafx.util.StringConverter; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +public class DateStringConverter extends StringConverter { + public static final String FORMAT_PATTERN = "yyyy/MM/dd"; + public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(FORMAT_PATTERN); + + @Override + public String toString(LocalDate date) { + if (date != null) { + return DATE_FORMATTER.format(date); + } else { + return ""; + } + } + + @Override + public LocalDate fromString(String string) { + if (string != null && !string.isEmpty()) { + return LocalDate.parse(string, DATE_FORMATTER); + } else { + return null; + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TransactionsTreeTable.java b/src/main/java/com/sparrowwallet/sparrow/control/TransactionsTreeTable.java index e5ac147b..3409fa02 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/TransactionsTreeTable.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/TransactionsTreeTable.java @@ -5,7 +5,6 @@ import com.sparrowwallet.sparrow.wallet.Entry; import com.sparrowwallet.sparrow.wallet.TransactionEntry; import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry; import javafx.beans.property.ReadOnlyObjectWrapper; -import javafx.scene.control.Label; import javafx.scene.control.TreeTableColumn; import javafx.scene.control.TreeTableView; @@ -51,7 +50,7 @@ public class TransactionsTreeTable extends CoinTreeTable { balanceCol.setSortable(true); getColumns().add(balanceCol); - setPlaceholder(new Label("No transactions")); + setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet())); setEditable(true); setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY); dateCol.setSortType(TreeTableColumn.SortType.DESCENDING); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/UtxosTreeTable.java b/src/main/java/com/sparrowwallet/sparrow/control/UtxosTreeTable.java index 82d3e6f6..72ff859d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/UtxosTreeTable.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/UtxosTreeTable.java @@ -3,7 +3,6 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.drongo.wallet.WalletNode; import com.sparrowwallet.sparrow.wallet.*; import javafx.beans.property.ReadOnlyObjectWrapper; -import javafx.scene.control.Label; import javafx.scene.control.SelectionMode; import javafx.scene.control.TreeTableColumn; import javafx.scene.control.TreeTableView; @@ -69,7 +68,7 @@ public class UtxosTreeTable extends CoinTreeTable { getColumns().add(amountCol); setTreeColumn(amountCol); - setPlaceholder(new Label("No unspent outputs")); + setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet())); setEditable(true); setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY); amountCol.setSortType(TreeTableColumn.SortType.DESCENDING); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WalletBirthDateDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WalletBirthDateDialog.java new file mode 100644 index 00000000..fb5ec076 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletBirthDateDialog.java @@ -0,0 +1,68 @@ +package com.sparrowwallet.sparrow.control; + +import com.sparrowwallet.sparrow.AppServices; +import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; +import javafx.application.Platform; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.geometry.Insets; +import javafx.scene.control.*; +import javafx.scene.layout.HBox; +import org.controlsfx.glyphfont.Glyph; +import org.controlsfx.validation.ValidationResult; +import org.controlsfx.validation.ValidationSupport; +import org.controlsfx.validation.Validator; +import org.controlsfx.validation.decoration.StyleClassValidationDecoration; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Date; + +public class WalletBirthDateDialog extends Dialog { + private final DatePicker birthDatePicker; + + public WalletBirthDateDialog(Date birthDate) { + final DialogPane dialogPane = getDialogPane(); + AppServices.setStageIcon(dialogPane.getScene().getWindow()); + + setTitle("Wallet Birth Date"); + dialogPane.setHeaderText("Select an approximate date earlier than the first wallet transaction:"); + dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm()); + dialogPane.getButtonTypes().addAll(ButtonType.CANCEL); + dialogPane.setPrefWidth(420); + dialogPane.setPrefHeight(200); + + Glyph wallet = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HISTORY); + wallet.setFontSize(50); + dialogPane.setGraphic(wallet); + + HBox datePickerBox = new HBox(10); + Label label = new Label("Start scanning from:"); + label.setPadding(new Insets(5, 0, 0, 8)); + datePickerBox.getChildren().add(label); + + birthDatePicker = birthDate == null ? new DatePicker() : new DatePicker(birthDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()); + birthDatePicker.setEditable(false); + birthDatePicker.setConverter(new DateStringConverter()); + + datePickerBox.getChildren().add(birthDatePicker); + + dialogPane.setContent(datePickerBox); + + ValidationSupport validationSupport = new ValidationSupport(); + Platform.runLater( () -> { + validationSupport.registerValidator(birthDatePicker, Validator.combine( + (Control c, LocalDate newValue) -> ValidationResult.fromErrorIf( c, "Birth date not specified", newValue == null) + )); + validationSupport.setValidationDecorator(new StyleClassValidationDecoration()); + }); + + final ButtonType okButtonType = new javafx.scene.control.ButtonType("Rescan Wallet", ButtonBar.ButtonData.OK_DONE); + dialogPane.getButtonTypes().addAll(okButtonType); + Button okButton = (Button) dialogPane.lookupButton(okButtonType); + BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> birthDatePicker.getValue() == null, birthDatePicker.valueProperty()); + okButton.disableProperty().bind(isInvalid); + + setResultConverter(dialogButton -> dialogButton == okButtonType ? Date.from(birthDatePicker.getValue().atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()) : null); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WalletNameDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WalletNameDialog.java index 708d3082..8b79a5da 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WalletNameDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletNameDialog.java @@ -2,11 +2,15 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; +import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Storage; +import com.sparrowwallet.sparrow.net.ServerType; import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; +import javafx.geometry.Insets; import javafx.scene.control.*; +import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import org.controlsfx.control.textfield.CustomTextField; import org.controlsfx.control.textfield.TextFields; @@ -16,28 +20,74 @@ import org.controlsfx.validation.ValidationSupport; import org.controlsfx.validation.Validator; import org.controlsfx.validation.decoration.StyleClassValidationDecoration; -public class WalletNameDialog extends Dialog { +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Date; + +public class WalletNameDialog extends Dialog { private final CustomTextField name; + private final CheckBox existingCheck; + private final DatePicker existingPicker; public WalletNameDialog() { - this.name = (CustomTextField)TextFields.createClearableTextField(); + this(""); + } + + public WalletNameDialog(String initialName) { final DialogPane dialogPane = getDialogPane(); AppServices.setStageIcon(dialogPane.getScene().getWindow()); + boolean requestBirthDate = (Config.get().getServerType() == null || Config.get().getServerType() == ServerType.BITCOIN_CORE); setTitle("Wallet Name"); dialogPane.setHeaderText("Enter a name for this wallet:"); dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm()); dialogPane.getButtonTypes().addAll(ButtonType.CANCEL); - dialogPane.setPrefWidth(380); - dialogPane.setPrefHeight(200); + dialogPane.setPrefWidth(460); + dialogPane.setPrefHeight(requestBirthDate ? 250 : 200); Glyph wallet = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WALLET); wallet.setFontSize(50); dialogPane.setGraphic(wallet); - final VBox content = new VBox(10); + final VBox content = new VBox(20); + name = (CustomTextField)TextFields.createClearableTextField(); + name.setText(initialName); content.getChildren().add(name); + HBox existingBox = new HBox(10); + existingCheck = new CheckBox("Has existing transactions"); + existingCheck.setPadding(new Insets(5, 0, 0, 0)); + existingBox.getChildren().add(existingCheck); + + existingPicker = new DatePicker(); + existingPicker.setConverter(new DateStringConverter()); + existingPicker.setEditable(false); + existingPicker.setPrefWidth(130); + existingPicker.managedProperty().bind(existingPicker.visibleProperty()); + existingPicker.setVisible(false); + existingBox.getChildren().add(existingPicker); + + HelpLabel helpLabel = new HelpLabel(); + helpLabel.setHelpText("Select an approximate date earlier than the first wallet transaction."); + helpLabel.setTranslateY(5); + helpLabel.managedProperty().bind(helpLabel.visibleProperty()); + helpLabel.visibleProperty().bind(existingPicker.visibleProperty()); + existingBox.getChildren().add(helpLabel); + + existingCheck.selectedProperty().addListener((observable, oldValue, newValue) -> { + if(newValue) { + existingCheck.setText("Has existing transactions starting from"); + existingPicker.setVisible(true); + } else { + existingCheck.setText("Has existing transactions"); + existingPicker.setVisible(false); + } + }); + + if(requestBirthDate) { + content.getChildren().add(existingBox); + } + dialogPane.setContent(content); ValidationSupport validationSupport = new ValidationSupport(); @@ -46,18 +96,39 @@ public class WalletNameDialog extends Dialog { Validator.createEmptyValidator("Wallet name is required"), (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Wallet name is not unique", Storage.walletExists(newValue)) )); + validationSupport.registerValidator(existingPicker, Validator.combine( + (Control c, LocalDate newValue) -> ValidationResult.fromErrorIf( c, "Birth date not specified", existingCheck.isSelected() && newValue == null) + )); validationSupport.setValidationDecorator(new StyleClassValidationDecoration()); }); - final ButtonType okButtonType = new javafx.scene.control.ButtonType("New Wallet", ButtonBar.ButtonData.OK_DONE); + final ButtonType okButtonType = new javafx.scene.control.ButtonType("Create Wallet", ButtonBar.ButtonData.OK_DONE); dialogPane.getButtonTypes().addAll(okButtonType); Button okButton = (Button) dialogPane.lookupButton(okButtonType); BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> - name.getText().length() == 0 || Storage.walletExists(name.getText()), name.textProperty()); + name.getText().length() == 0 || Storage.walletExists(name.getText()) || (existingCheck.isSelected() && existingPicker.getValue() == null), name.textProperty(), existingCheck.selectedProperty(), existingPicker.valueProperty()); okButton.disableProperty().bind(isInvalid); name.setPromptText("Wallet Name"); Platform.runLater(name::requestFocus); - setResultConverter(dialogButton -> dialogButton == okButtonType ? name.getText() : null); + setResultConverter(dialogButton -> dialogButton == okButtonType ? new NameAndBirthDate(name.getText(), existingPicker.getValue()) : null); + } + + public static class NameAndBirthDate { + private final String name; + private final Date birthDate; + + public NameAndBirthDate(String name, LocalDate birthLocalDate) { + this.name = name; + this.birthDate = (birthLocalDate == null ? null : Date.from(birthLocalDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant())); + } + + public String getName() { + return name; + } + + public Date getBirthDate() { + return birthDate; + } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WelcomeDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WelcomeDialog.java index 44192217..ce7cec4b 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; @@ -27,10 +25,11 @@ public class WelcomeDialog extends Dialog { setTitle("Welcome to Sparrow"); dialogPane.setHeaderText("Welcome to Sparrow!"); + dialogPane.getStylesheets().add(AppServices.class.getResource("app.css").toExternalForm()); 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 +45,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 +63,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 +80,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/BwtBootStatusEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/BwtBootStatusEvent.java new file mode 100644 index 00000000..ef4be5be --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/BwtBootStatusEvent.java @@ -0,0 +1,7 @@ +package com.sparrowwallet.sparrow.event; + +public class BwtBootStatusEvent extends BwtStatusEvent { + public BwtBootStatusEvent(String status) { + super(status); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/BwtElectrumReadyStatusEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/BwtElectrumReadyStatusEvent.java new file mode 100644 index 00000000..8838ef4b --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/BwtElectrumReadyStatusEvent.java @@ -0,0 +1,14 @@ +package com.sparrowwallet.sparrow.event; + +public class BwtElectrumReadyStatusEvent extends BwtStatusEvent { + private final String electrumAddr; + + public BwtElectrumReadyStatusEvent(String status, String electrumAddr) { + super(status); + this.electrumAddr = electrumAddr; + } + + public String getElectrumAddr() { + return electrumAddr; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/BwtReadyStatusEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/BwtReadyStatusEvent.java new file mode 100644 index 00000000..97760f6a --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/BwtReadyStatusEvent.java @@ -0,0 +1,7 @@ +package com.sparrowwallet.sparrow.event; + +public class BwtReadyStatusEvent extends BwtStatusEvent { + public BwtReadyStatusEvent(String status) { + super(status); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/BwtScanStatusEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/BwtScanStatusEvent.java new file mode 100644 index 00000000..0c41adbf --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/BwtScanStatusEvent.java @@ -0,0 +1,26 @@ +package com.sparrowwallet.sparrow.event; + +import java.util.Date; + +public class BwtScanStatusEvent extends BwtStatusEvent { + private final int progress; + private final Date eta; + + public BwtScanStatusEvent(String status, int progress, Date eta) { + super(status); + this.progress = progress; + this.eta = eta; + } + + public int getProgress() { + return progress; + } + + public boolean isCompleted() { + return progress == 100; + } + + public Date getEta() { + return eta; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/BwtShutdownEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/BwtShutdownEvent.java new file mode 100644 index 00000000..d171613f --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/BwtShutdownEvent.java @@ -0,0 +1,9 @@ +package com.sparrowwallet.sparrow.event; + +/** + * Empty class used to notify the bwt has shut down. + * Note this extends from DisconnectionEvent, which is the more general event fired on any type of disconnection. + */ +public class BwtShutdownEvent extends DisconnectionEvent { + +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/BwtStatusEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/BwtStatusEvent.java new file mode 100644 index 00000000..fc37b6ba --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/BwtStatusEvent.java @@ -0,0 +1,13 @@ +package com.sparrowwallet.sparrow.event; + +public class BwtStatusEvent { + private final String status; + + public BwtStatusEvent(String status) { + this.status = status; + } + + public String getStatus() { + return status; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/BwtSyncStatusEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/BwtSyncStatusEvent.java new file mode 100644 index 00000000..426cecce --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/BwtSyncStatusEvent.java @@ -0,0 +1,26 @@ +package com.sparrowwallet.sparrow.event; + +import java.util.Date; + +public class BwtSyncStatusEvent extends BwtStatusEvent { + private final int progress; + private final Date tip; + + public BwtSyncStatusEvent(String status, int progress, Date tip) { + super(status); + this.progress = progress; + this.tip = tip; + } + + public int getProgress() { + return progress; + } + + public boolean isCompleted() { + return progress == 100; + } + + public Date getTip() { + return tip; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/DisconnectionEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/DisconnectionEvent.java new file mode 100644 index 00000000..ecf21f3d --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/DisconnectionEvent.java @@ -0,0 +1,8 @@ +package com.sparrowwallet.sparrow.event; + +/** + * Empty class used to signal that the server has been disconnected from. + */ +public class DisconnectionEvent { + +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/OpenWalletsEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/OpenWalletsEvent.java index 20205a2b..f64a6124 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/OpenWalletsEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/OpenWalletsEvent.java @@ -1,35 +1,47 @@ package com.sparrowwallet.sparrow.event; import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.WalletTabData; import com.sparrowwallet.sparrow.io.Storage; import javafx.stage.Window; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; public class OpenWalletsEvent { private final Window window; - private final Map walletsMap; + private final List walletTabDataList; - public OpenWalletsEvent(Window window, Map walletsMap) { + public OpenWalletsEvent(Window window, List walletTabDataList) { this.window = window; - this.walletsMap = walletsMap; + this.walletTabDataList = walletTabDataList; } public Window getWindow() { return window; } - public List getWallets() { - return new ArrayList<>(walletsMap.keySet()); - } - - public Storage getStorage(Wallet wallet) { - return walletsMap.get(wallet); + public List getWalletTabDataList() { + return walletTabDataList; } public Map getWalletsMap() { - return walletsMap; + Map openWallets = new LinkedHashMap<>(); + + for(WalletTabData walletTabData : walletTabDataList){ + openWallets.put(walletTabData.getWallet(), walletTabData.getStorage()); + } + + return openWallets; + } + + public List getWallets() { + return new ArrayList<>(getWalletsMap().keySet()); + } + + public Storage getStorage(Wallet wallet) { + return getWalletsMap().get(wallet); } } 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/event/SettingsChangedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/SettingsChangedEvent.java index 63c1a93b..8ed01c3a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/SettingsChangedEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/SettingsChangedEvent.java @@ -20,6 +20,6 @@ public class SettingsChangedEvent { } public enum Type { - POLICY, SCRIPT_TYPE, MUTLISIG_THRESHOLD, MULTISIG_TOTAL, KEYSTORE_LABEL, KEYSTORE_FINGERPRINT, KEYSTORE_DERIVATION, KEYSTORE_XPUB, GAP_LIMIT; + POLICY, SCRIPT_TYPE, MUTLISIG_THRESHOLD, MULTISIG_TOTAL, KEYSTORE_LABEL, KEYSTORE_FINGERPRINT, KEYSTORE_DERIVATION, KEYSTORE_XPUB, GAP_LIMIT, BIRTH_DATE; } } diff --git a/src/main/java/com/sparrowwallet/sparrow/event/WalletHistoryChangedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/WalletHistoryChangedEvent.java index 49ef4af9..af12bd7e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/WalletHistoryChangedEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/WalletHistoryChangedEvent.java @@ -3,7 +3,9 @@ package com.sparrowwallet.sparrow.event; import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.WalletNode; +import com.sparrowwallet.sparrow.io.Storage; +import java.io.File; import java.util.List; import java.util.stream.Collectors; @@ -12,13 +14,19 @@ import java.util.stream.Collectors; * */ public class WalletHistoryChangedEvent extends WalletChangedEvent { + private final Storage storage; private final List historyChangedNodes; - public WalletHistoryChangedEvent(Wallet wallet, List historyChangedNodes) { + public WalletHistoryChangedEvent(Wallet wallet, Storage storage, List historyChangedNodes) { super(wallet); + this.storage = storage; this.historyChangedNodes = historyChangedNodes; } + public File getWalletFile() { + return storage.getWalletFile(); + } + public List getHistoryChangedNodes() { return historyChangedNodes; } diff --git a/src/main/java/com/sparrowwallet/sparrow/event/WalletHistoryStatusEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/WalletHistoryStatusEvent.java index a40771bb..b433e133 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/WalletHistoryStatusEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/WalletHistoryStatusEvent.java @@ -4,27 +4,27 @@ import com.sparrowwallet.drongo.wallet.Wallet; public class WalletHistoryStatusEvent { private final Wallet wallet; - private final boolean loaded; + private final boolean loading; private final String statusMessage; private final String errorMessage; - public WalletHistoryStatusEvent(Wallet wallet, boolean loaded) { + public WalletHistoryStatusEvent(Wallet wallet, boolean loading) { this.wallet = wallet; - this.loaded = loaded; + this.loading = loading; this.statusMessage = null; this.errorMessage = null; } - public WalletHistoryStatusEvent(Wallet wallet,boolean loaded, String statusMessage) { + public WalletHistoryStatusEvent(Wallet wallet, boolean loading, String statusMessage) { this.wallet = wallet; - this.loaded = false; + this.loading = loading; this.statusMessage = statusMessage; this.errorMessage = null; } - public WalletHistoryStatusEvent(Wallet wallet,String errorMessage) { + public WalletHistoryStatusEvent(Wallet wallet, String errorMessage) { this.wallet = wallet; - this.loaded = false; + this.loading = true; this.statusMessage = null; this.errorMessage = errorMessage; } @@ -34,11 +34,7 @@ public class WalletHistoryStatusEvent { } public boolean isLoading() { - return !loaded; - } - - public boolean isLoaded() { - return loaded; + return loading; } public String getStatusMessage() { diff --git a/src/main/java/com/sparrowwallet/sparrow/event/WalletOpeningEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/WalletOpeningEvent.java new file mode 100644 index 00000000..0bbf4a0a --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/WalletOpeningEvent.java @@ -0,0 +1,22 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.io.Storage; + +public class WalletOpeningEvent { + private final Storage storage; + private final Wallet wallet; + + public WalletOpeningEvent(Storage storage, Wallet wallet) { + this.storage = storage; + this.wallet = wallet; + } + + public Storage getStorage() { + return storage; + } + + public Wallet getWallet() { + return wallet; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java index 6e693dcf..fad35703 100644 --- a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java +++ b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java @@ -31,6 +31,7 @@ public class FontAwesome5 extends GlyphFont { EYE('\uf06e'), HAND_HOLDING('\uf4bd'), HAND_HOLDING_MEDICAL('\ue05c'), + HISTORY('\uf1da'), KEY('\uf084'), LAPTOP('\uf109'), LOCK('\uf023'), diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ColdcardMultisig.java b/src/main/java/com/sparrowwallet/sparrow/io/ColdcardMultisig.java index f7d31e9f..172ae84d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ColdcardMultisig.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ColdcardMultisig.java @@ -48,8 +48,8 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2sh_deriv)); keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2sh)); } else if(scriptType.equals(ScriptType.P2SH_P2WSH)) { - keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_p2sh_deriv)); - keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh_p2sh)); + keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_p2sh_deriv != null ? cck.p2wsh_p2sh_deriv : cck.p2sh_p2wsh_deriv)); + keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh_p2sh != null ? cck.p2wsh_p2sh : cck.p2sh_p2wsh)); } else if(scriptType.equals(ScriptType.P2WSH)) { keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_deriv)); keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh)); @@ -65,6 +65,8 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle public String p2sh; public String p2wsh_p2sh_deriv; public String p2wsh_p2sh; + public String p2sh_p2wsh_deriv; + public String p2sh_p2wsh; public String p2wsh_deriv; public String p2wsh; public String xpub; diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ColdcardSinglesig.java b/src/main/java/com/sparrowwallet/sparrow/io/ColdcardSinglesig.java index a308e3b2..f7c1a450 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ColdcardSinglesig.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ColdcardSinglesig.java @@ -66,7 +66,7 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport { ColdcardKeystore ck = gson.fromJson(map.get(key), ColdcardKeystore.class); if(ck.name != null) { - ScriptType ckScriptType = ScriptType.valueOf(ck.name.replace("p2wpkh-p2sh", "p2sh_p2wpkh").toUpperCase()); + ScriptType ckScriptType = ScriptType.valueOf(ck.name.replace("p2wpkh-p2sh", "p2sh_p2wpkh").replace("p2sh-p2wpkh", "p2sh_p2wpkh").toUpperCase()); if(ckScriptType.equals(scriptType)) { Keystore keystore = new Keystore(); keystore.setLabel(getName()); diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Config.java b/src/main/java/com/sparrowwallet/sparrow/io/Config.java index 2ea4b518..db555c40 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Config.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Config.java @@ -4,8 +4,10 @@ import com.google.gson.*; import com.sparrowwallet.drongo.BitcoinUnit; import com.sparrowwallet.sparrow.Mode; import com.sparrowwallet.sparrow.Theme; +import com.sparrowwallet.sparrow.net.CoreAuthType; import com.sparrowwallet.sparrow.net.ExchangeSource; import com.sparrowwallet.sparrow.net.FeeRatesSource; +import com.sparrowwallet.sparrow.net.ServerType; import com.sparrowwallet.sparrow.wallet.FeeRatesSelection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,6 +39,12 @@ public class Config { private List recentWalletFiles; private Integer keyDerivationPeriod; private File hwi; + private ServerType serverType; + private String coreServer; + private CoreAuthType coreAuthType; + private File coreDataDir; + private String coreAuth; + private String coreWallet; private String electrumServer; private File electrumServerCert; private boolean useProxy; @@ -241,6 +249,64 @@ public class Config { flush(); } + public ServerType getServerType() { + return serverType; + } + + public void setServerType(ServerType serverType) { + this.serverType = serverType; + flush(); + } + + public String getServerAddress() { + return getServerType() == ServerType.BITCOIN_CORE ? getCoreServer() : getElectrumServer(); + } + + public String getCoreServer() { + return coreServer; + } + + public void setCoreServer(String coreServer) { + this.coreServer = coreServer; + flush(); + } + + public CoreAuthType getCoreAuthType() { + return coreAuthType; + } + + public void setCoreAuthType(CoreAuthType coreAuthType) { + this.coreAuthType = coreAuthType; + flush(); + } + + public File getCoreDataDir() { + return coreDataDir; + } + + public void setCoreDataDir(File coreDataDir) { + this.coreDataDir = coreDataDir; + flush(); + } + + public String getCoreAuth() { + return coreAuth; + } + + public void setCoreAuth(String coreAuth) { + this.coreAuth = coreAuth; + flush(); + } + + public String getCoreWallet() { + return coreWallet; + } + + public void setCoreWallet(String coreWallet) { + this.coreWallet = (coreWallet == null || coreWallet.isEmpty() ? null : coreWallet); + flush(); + } + public String getElectrumServer() { return electrumServer; } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java b/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java index ad00fe7b..ee36dadf 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java @@ -74,7 +74,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc { public Map getScriptHashHistory(Transport transport, Wallet wallet, Map pathScriptHashes, boolean failOnError) { JsonRpcClient client = new JsonRpcClient(transport); BatchRequestBuilder batchRequest = client.createBatchRequest().keysType(String.class).returnType(ScriptHashTx[].class); - EventManager.get().post(new WalletHistoryStatusEvent(wallet, false, "Loading transactions")); + EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Loading transactions")); for(String path : pathScriptHashes.keySet()) { batchRequest.add(path, "blockchain.scripthash.get_history", pathScriptHashes.get(path)); @@ -130,7 +130,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc { public Map subscribeScriptHashes(Transport transport, Wallet wallet, Map pathScriptHashes) { JsonRpcClient client = new JsonRpcClient(transport); BatchRequestBuilder batchRequest = client.createBatchRequest().keysType(String.class).returnType(String.class); - EventManager.get().post(new WalletHistoryStatusEvent(wallet, false, "Finding transactions")); + EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Finding transactions")); for(String path : pathScriptHashes.keySet()) { batchRequest.add(path, "blockchain.scripthash.subscribe", pathScriptHashes.get(path)); @@ -151,7 +151,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc { public Map getBlockHeaders(Transport transport, Wallet wallet, Set blockHeights) { JsonRpcClient client = new JsonRpcClient(transport); BatchRequestBuilder batchRequest = client.createBatchRequest().keysType(Integer.class).returnType(String.class); - EventManager.get().post(new WalletHistoryStatusEvent(wallet, false, "Retrieving blocks")); + EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Retrieving blocks")); for(Integer height : blockHeights) { batchRequest.add(height, "blockchain.block.header", height); @@ -171,7 +171,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc { public Map getTransactions(Transport transport, Wallet wallet, Set txids) { JsonRpcClient client = new JsonRpcClient(transport); BatchRequestBuilder batchRequest = client.createBatchRequest().keysType(String.class).returnType(String.class); - EventManager.get().post(new WalletHistoryStatusEvent(wallet, false, "Retrieving transactions")); + EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Retrieving transactions")); for(String txid : txids) { batchRequest.add(txid, "blockchain.transaction.get", txid); @@ -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 new file mode 100644 index 00000000..f1ee331b --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/Bwt.java @@ -0,0 +1,309 @@ +package com.sparrowwallet.sparrow.net; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; +import com.sparrowwallet.drongo.KeyPurpose; +import com.sparrowwallet.drongo.Network; +import com.sparrowwallet.drongo.OutputDescriptor; +import com.sparrowwallet.drongo.wallet.BlockTransactionHash; +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.event.*; +import com.sparrowwallet.sparrow.io.Config; +import dev.bwt.libbwt.daemon.CallbackNotifier; +import dev.bwt.libbwt.daemon.NativeBwtDaemon; +import javafx.application.Platform; +import javafx.concurrent.Service; +import javafx.concurrent.Task; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +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; + private boolean ready; + + static { + try { + org.controlsfx.tools.Platform platform = org.controlsfx.tools.Platform.getCurrent(); + if(platform == org.controlsfx.tools.Platform.OSX) { + NativeUtils.loadLibraryFromJar("/native/osx/x64/libbwt_jni.dylib"); + } else if(platform == org.controlsfx.tools.Platform.WINDOWS) { + NativeUtils.loadLibraryFromJar("/native/windows/x64/bwt_jni.dll"); + } else { + NativeUtils.loadLibraryFromJar("/native/linux/x64/libbwt_jni.so"); + } + } catch(IOException e) { + log.error("Error loading bwt library", e); + } + } + + private void start(CallbackNotifier callback) { + start(Collections.emptyList(), null, null, null, callback); + } + + private void start(Collection wallets, CallbackNotifier callback) { + List outputDescriptors = new ArrayList<>(); + for(Wallet wallet : wallets) { + OutputDescriptor receiveOutputDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.RECEIVE); + outputDescriptors.add(receiveOutputDescriptor.toString(false, false)); + OutputDescriptor changeOutputDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.CHANGE); + outputDescriptors.add(changeOutputDescriptor.toString(false, false)); + } + + int rescanSince = wallets.stream().filter(wallet -> wallet.getBirthDate() != null).mapToInt(wallet -> (int)(wallet.getBirthDate().getTime() / 1000)).min().orElse(-1); + int gapLimit = wallets.stream().filter(wallet -> wallet.getGapLimit() > 0).mapToInt(Wallet::getGapLimit).max().orElse(Wallet.DEFAULT_LOOKAHEAD); + + boolean forceRescan = false; + for(Wallet wallet :wallets) { + Date txBirthDate = wallet.getTransactions().values().stream().map(BlockTransactionHash::getDate).filter(Objects::nonNull).min(Date::compareTo).orElse(null); + if((wallet.getBirthDate() != null && txBirthDate != null && wallet.getBirthDate().before(txBirthDate)) || (txBirthDate == null && wallet.getStoredBlockHeight() == 0)) { + forceRescan = true; + } + } + + start(outputDescriptors, rescanSince, forceRescan, gapLimit, callback); + } + + /** + * Start the bwt daemon with the provided wallets + * Blocks until the daemon is shut down. + * + * @param outputDescriptors descriptors of keys to add to Bitcoin Core + * @param rescanSince seconds since epoch to start scanning keys + * @param gapLimit desired gap limit beyond last used address + * @param callback object receiving notifications + */ + private void start(List outputDescriptors, Integer rescanSince, Boolean forceRescan, Integer gapLimit, CallbackNotifier callback) { + BwtConfig bwtConfig = new BwtConfig(); + bwtConfig.network = Network.get() == Network.MAINNET ? "bitcoin" : Network.get().getName(); + + if(!outputDescriptors.isEmpty()) { + bwtConfig.descriptors = outputDescriptors; + bwtConfig.rescanSince = (rescanSince == null || rescanSince < 0 ? "now" : rescanSince); + bwtConfig.forceRescan = forceRescan; + bwtConfig.gapLimit = gapLimit; + } else { + bwtConfig.requireAddresses = false; + } + + bwtConfig.verbose = log.isDebugEnabled() ? 2 : 0; + if(!log.isInfoEnabled()) { + bwtConfig.setupLogger = false; + } + + bwtConfig.electrumAddr = "127.0.0.1:0"; + bwtConfig.electrumSkipMerkle = true; + + Config config = Config.get(); + bwtConfig.bitcoindUrl = config.getCoreServer(); + if(config.getCoreAuthType() == CoreAuthType.COOKIE) { + bwtConfig.bitcoindDir = config.getCoreDataDir().getAbsolutePath() + "/"; + } else { + bwtConfig.bitcoindAuth = config.getCoreAuth(); + } + if(config.getCoreWallet() != null && !config.getCoreWallet().isEmpty()) { + bwtConfig.bitcoindWallet = config.getCoreWallet(); + } + + Gson gson = new Gson(); + String jsonConfig = gson.toJson(bwtConfig); + log.debug("Configuring bwt: " + jsonConfig); + + NativeBwtDaemon.start(jsonConfig, callback); + } + + /** + * Shut down the BWT daemon + * + */ + private void shutdown() { + if(shutdownPtr == null) { + terminating = true; + return; + } + + NativeBwtDaemon.shutdown(shutdownPtr); + this.terminating = false; + this.ready = false; + this.shutdownPtr = null; + Platform.runLater(() -> EventManager.get().post(new BwtShutdownEvent())); + } + + public boolean isRunning() { + return shutdownPtr != null; + } + + public boolean isReady() { + return ready; + } + + public boolean isTerminating() { + return terminating; + } + + public ConnectionService getConnectionService(Collection wallets) { + return wallets != null ? new ConnectionService(wallets) : new ConnectionService(); + } + + public DisconnectionService getDisconnectionService() { + return new DisconnectionService(); + } + + private static class BwtConfig { + @SerializedName("network") + public String network; + + @SerializedName("bitcoind_url") + public String bitcoindUrl; + + @SerializedName("bitcoind_auth") + public String bitcoindAuth; + + @SerializedName("bitcoind_dir") + public String bitcoindDir; + + @SerializedName("bitcoind_cookie") + public String bitcoindCookie; + + @SerializedName("bitcoind_wallet") + public String bitcoindWallet; + + @SerializedName("descriptors") + public List descriptors; + + @SerializedName("xpubs") + public String xpubs; + + @SerializedName("rescan_since") + public Object rescanSince; + + @SerializedName("force_rescan") + public Boolean forceRescan; + + @SerializedName("gap_limit") + public Integer gapLimit; + + @SerializedName("initial_import_size") + public Integer initialImportSize; + + @SerializedName("verbose") + public Integer verbose; + + @SerializedName("electrum_addr") + public String electrumAddr; + + @SerializedName("electrum_skip_merkle") + public Boolean electrumSkipMerkle; + + @SerializedName("require_addresses") + public Boolean requireAddresses; + + @SerializedName("setup_logger") + public Boolean setupLogger; + + @SerializedName("http_addr") + public String httpAddr; + } + + public final class ConnectionService extends Service { + private final Collection wallets; + + public ConnectionService() { + this.wallets = null; + } + + public ConnectionService(Collection wallets) { + this.wallets = wallets; + } + + @Override + protected Task createTask() { + return new Task<>() { + protected Void call() { + CallbackNotifier notifier = new CallbackNotifier() { + @Override + public void onBooting(long shutdownPtr) { + log.debug("Booting bwt"); + + 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() + "..."))); + } + } + + @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, tipDate))); + } + } + + @Override + public void onScanProgress(float progress, int eta) { + int percent = (int) (progress * 100.0); + Date date = new Date((long) eta * 1000); + log.debug("Scanning " + percent + "%"); + if(!terminating) { + Platform.runLater(() -> EventManager.get().post(new BwtScanStatusEvent("Scanning" + (percent < 100 ? " (" + percent + "%)" : ""), percent, date))); + } + } + + @Override + public void onElectrumReady(String addr) { + log.debug("Electrum ready"); + if(!terminating) { + Platform.runLater(() -> EventManager.get().post(new BwtElectrumReadyStatusEvent("Electrum server ready", addr))); + } + } + + @Override + public void onHttpReady(String addr) { + log.info("http ready at " + addr); + } + + @Override + public void onReady() { + log.debug("Bwt ready"); + ready = true; + if(!terminating) { + Platform.runLater(() -> EventManager.get().post(new BwtReadyStatusEvent("Server ready"))); + } + } + }; + + if(wallets == null) { + Bwt.this.start(notifier); + } else { + Bwt.this.start(wallets, notifier); + } + + return null; + } + }; + } + } + + public final class DisconnectionService extends Service { + @Override + protected Task createTask() { + return new Task<>() { + protected Void call() { + Bwt.this.shutdown(); + return null; + } + }; + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/CoreAuthType.java b/src/main/java/com/sparrowwallet/sparrow/net/CoreAuthType.java new file mode 100644 index 00000000..c8a5efeb --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/CoreAuthType.java @@ -0,0 +1,5 @@ +package com.sparrowwallet.sparrow.net; + +public enum CoreAuthType { + COOKIE, USERPASS; +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 26547bc0..fd944c30 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -9,11 +9,12 @@ import com.sparrowwallet.drongo.Network; import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.protocol.*; import com.sparrowwallet.drongo.wallet.*; -import com.sparrowwallet.sparrow.event.ConnectionEvent; -import com.sparrowwallet.sparrow.event.FeeRatesUpdatedEvent; -import com.sparrowwallet.sparrow.event.TorStatusEvent; +import com.sparrowwallet.sparrow.AppServices; +import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.wallet.SendController; +import javafx.application.Platform; import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleStringProperty; @@ -26,6 +27,8 @@ import org.slf4j.LoggerFactory; import java.io.*; import java.util.*; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; public class ElectrumServer { @@ -41,24 +44,37 @@ public class ElectrumServer { private static ElectrumServerRpc electrumServerRpc = new SimpleElectrumServerRpc(); + private static String bwtElectrumServer; + private static synchronized Transport getTransport() throws ServerException { if(transport == null) { try { - String electrumServer = Config.get().getElectrumServer(); - File electrumServerCert = Config.get().getElectrumServerCert(); - String proxyServer = Config.get().getProxyServer(); + String electrumServer = null; + File electrumServerCert = null; + String proxyServer = null; + + if(Config.get().getServerType() == ServerType.BITCOIN_CORE) { + if(bwtElectrumServer == null) { + throw new ServerConfigException("Could not connect to Bitcoin Core RPC"); + } + electrumServer = bwtElectrumServer; + } else if(Config.get().getServerType() == ServerType.ELECTRUM_SERVER) { + electrumServer = Config.get().getElectrumServer(); + electrumServerCert = Config.get().getElectrumServerCert(); + proxyServer = Config.get().getProxyServer(); + } if(electrumServer == null) { - throw new ServerException("Electrum server URL not specified"); + throw new ServerConfigException("Electrum server URL not specified"); } if(electrumServerCert != null && !electrumServerCert.exists()) { - throw new ServerException("Electrum server certificate file not found"); + throw new ServerConfigException("Electrum server certificate file not found"); } Protocol protocol = Protocol.getProtocol(electrumServer); if(protocol == null) { - throw new ServerException("Electrum server URL must start with " + Protocol.TCP.toUrlString() + " or " + Protocol.SSL.toUrlString()); + throw new ServerConfigException("Electrum server URL must start with " + Protocol.TCP.toUrlString() + " or " + Protocol.SSL.toUrlString()); } HostAndPort server = protocol.getServerHostAndPort(electrumServer); @@ -78,7 +94,7 @@ public class ElectrumServer { } } } catch (Exception e) { - throw new ServerException(e); + throw new ServerConfigException(e); } } @@ -760,7 +776,10 @@ public class ElectrumServer { private boolean firstCall = true; private Thread reader; private long feeRatesRetrievedAt; - private StringProperty statusProperty = new SimpleStringProperty(); + private final Bwt bwt = new Bwt(); + private final ReentrantLock bwtStartLock = new ReentrantLock(); + private final Condition bwtStartCondition = bwtStartLock.newCondition(); + private final StringProperty statusProperty = new SimpleStringProperty(); public ConnectionService() { this(true); @@ -775,6 +794,37 @@ public class ElectrumServer { return new Task<>() { protected FeeRatesUpdatedEvent call() throws ServerException { ElectrumServer electrumServer = new ElectrumServer(); + + if(Config.get().getServerType() == ServerType.BITCOIN_CORE) { + if(!bwt.isRunning()) { + Bwt.ConnectionService bwtConnectionService = bwt.getConnectionService(subscribe ? AppServices.get().getOpenWallets().keySet() : null); + bwtConnectionService.setOnFailed(workerStateEvent -> { + log.error("Failed to start BWT", workerStateEvent.getSource().getException()); + try { + bwtStartLock.lock(); + bwtStartCondition.signal(); + } finally { + bwtStartLock.unlock(); + } + }); + Platform.runLater(bwtConnectionService::start); + + try { + bwtStartLock.lock(); + bwtStartCondition.await(); + + if(!bwt.isRunning()) { + throw new ServerException("Check if Bitcoin Core is running, and the authentication details are correct."); + } + } catch(InterruptedException e) { + Thread.currentThread().interrupt(); + return null; + } finally { + bwtStartLock.unlock(); + } + } + } + if(firstCall) { electrumServer.connect(); @@ -839,16 +889,26 @@ public class ElectrumServer { public void resetConnection() { try { closeActiveConnection(); + shutdown(); firstCall = true; } catch (ServerException e) { log.error("Error closing connection during connection reset", e); } } + public boolean isConnecting() { + return isRunning() && Config.get().getServerType() == ServerType.BITCOIN_CORE && bwt.isRunning() && !bwt.isReady(); + } + + public boolean isConnected() { + return isRunning() && (Config.get().getServerType() != ServerType.BITCOIN_CORE || (bwt.isRunning() && bwt.isReady())); + } + @Override public boolean cancel() { try { closeActiveConnection(); + shutdown(); } catch (ServerException e) { log.error("Error closing connection", e); } @@ -856,6 +916,21 @@ public class ElectrumServer { return super.cancel(); } + private void shutdown() { + if(Config.get().getServerType() == ServerType.BITCOIN_CORE && bwt.isRunning()) { + 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); + } else { + Platform.runLater(() -> EventManager.get().post(new DisconnectionEvent())); + } + } + @Override public void reset() { super.reset(); @@ -872,6 +947,35 @@ public class ElectrumServer { statusProperty.set(event.getStatus()); } + @Subscribe + public void bwtElectrumReadyStatus(BwtElectrumReadyStatusEvent event) { + if(this.isRunning()) { + ElectrumServer.bwtElectrumServer = Protocol.TCP.toUrlString(HostAndPort.fromString(event.getElectrumAddr())); + } + } + + @Subscribe + public void bwtReadyStatus(BwtReadyStatusEvent event) { + if(this.isRunning()) { + try { + bwtStartLock.lock(); + bwtStartCondition.signal(); + } finally { + bwtStartLock.unlock(); + } + } + } + + @Subscribe + public void bwtShutdown(BwtShutdownEvent event) { + try { + bwtStartLock.lock(); + bwtStartCondition.signal(); + } finally { + bwtStartLock.unlock(); + } + } + public StringProperty statusProperty() { return statusProperty; } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java b/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java index 2ff1a74f..46fadf3d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java @@ -18,7 +18,7 @@ import java.util.LinkedHashMap; import java.util.Map; public enum FeeRatesSource { - ELECTRUM_SERVER("Electrum Server") { + ELECTRUM_SERVER("Server") { @Override public Map getBlockTargetFeeRates(Map defaultblockTargetFeeRates) { return Collections.emptyMap(); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/NativeUtils.java b/src/main/java/com/sparrowwallet/sparrow/net/NativeUtils.java new file mode 100644 index 00000000..1f3c2dd7 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/NativeUtils.java @@ -0,0 +1,119 @@ +package com.sparrowwallet.sparrow.net; + +import java.io.*; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.ProviderNotFoundException; +import java.nio.file.StandardCopyOption; + +/** + * A simple library class which helps with loading dynamic libraries stored in the + * JAR archive. These libraries usually contain implementation of some methods in + * native code (using JNI - Java Native Interface). + * + * @see http://adamheinrich.com/blog/2012/how-to-load-native-jni-library-from-jar + * @see https://github.com/adamheinrich/native-utils + * + */ +public class NativeUtils { + + /** + * The minimum length a prefix for a file has to have according to {@link File#createTempFile(String, String)}}. + */ + private static final int MIN_PREFIX_LENGTH = 3; + public static final String NATIVE_FOLDER_PATH_PREFIX = "nativeutils"; + + /** + * Temporary directory which will contain the DLLs. + */ + private static File temporaryDir; + + /** + * Private constructor - this class will never be instanced + */ + private NativeUtils() { + } + + /** + * Loads library from current JAR archive + * + * The file from JAR is copied into system temporary directory and then loaded. The temporary file is deleted after + * exiting. + * Method uses String as filename because the pathname is "abstract", not system-dependent. + * + * @param path The path of file inside JAR as absolute path (beginning with '/'), e.g. /package/File.ext + * @throws IOException If temporary file creation or read/write operation fails + * @throws IllegalArgumentException If source file (param path) does not exist + * @throws IllegalArgumentException If the path is not absolute or if the filename is shorter than three characters + * (restriction of {@link File#createTempFile(java.lang.String, java.lang.String)}). + * @throws FileNotFoundException If the file could not be found inside the JAR. + */ + public static void loadLibraryFromJar(String path) throws IOException { + + if (null == path || !path.startsWith("/")) { + throw new IllegalArgumentException("The path has to be absolute (start with '/')."); + } + + // Obtain filename from path + String[] parts = path.split("/"); + String filename = (parts.length > 1) ? parts[parts.length - 1] : null; + + // Check if the filename is okay + if (filename == null || filename.length() < MIN_PREFIX_LENGTH) { + throw new IllegalArgumentException("The filename has to be at least 3 characters long."); + } + + // Prepare temporary file + if (temporaryDir == null) { + temporaryDir = createTempDirectory(NATIVE_FOLDER_PATH_PREFIX); + temporaryDir.deleteOnExit(); + } + + File temp = new File(temporaryDir, filename); + + try (InputStream is = NativeUtils.class.getResourceAsStream(path)) { + Files.copy(is, temp.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + temp.delete(); + throw e; + } catch (NullPointerException e) { + temp.delete(); + throw new FileNotFoundException("File " + path + " was not found inside JAR."); + } + + try { + System.load(temp.getAbsolutePath()); + } finally { + if (isPosixCompliant()) { + // Assume POSIX compliant file system, can be deleted after loading + temp.delete(); + } else { + // Assume non-POSIX, and don't delete until last file descriptor closed + temp.deleteOnExit(); + } + } + } + + private static boolean isPosixCompliant() { + try { + return FileSystems.getDefault() + .supportedFileAttributeViews() + .contains("posix"); + } catch (FileSystemNotFoundException + | ProviderNotFoundException + | SecurityException e) { + return false; + } + } + + private static File createTempDirectory(String prefix) throws IOException { + String tempDir = System.getProperty("java.io.tmpdir"); + File generatedDir = new File(tempDir, prefix + System.nanoTime()); + + if (!generatedDir.mkdir()) + throw new IOException("Failed to create temp directory " + generatedDir.getName()); + + return generatedDir; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/Protocol.java b/src/main/java/com/sparrowwallet/sparrow/net/Protocol.java index 1cbe3baf..bfe24dcb 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/Protocol.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/Protocol.java @@ -64,6 +64,27 @@ public enum Protocol { public Transport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { return new ProxyTcpOverTlsTransport(server, serverCert, proxy); } + }, + HTTP { + @Override + public Transport getTransport(HostAndPort server) throws KeyManagementException, NoSuchAlgorithmException { + throw new UnsupportedOperationException("No transport supported for HTTP"); + } + + @Override + public Transport getTransport(HostAndPort server, File serverCert) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + throw new UnsupportedOperationException("No transport supported for HTTP"); + } + + @Override + public Transport getTransport(HostAndPort server, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + throw new UnsupportedOperationException("No transport supported for HTTP"); + } + + @Override + public Transport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + throw new UnsupportedOperationException("No transport supported for HTTP"); + } }; public abstract Transport getTransport(HostAndPort server) throws KeyManagementException, NoSuchAlgorithmException; @@ -105,6 +126,9 @@ public enum Protocol { if(url.startsWith("ssl://")) { return SSL; } + if(url.startsWith("http://")) { + return HTTP; + } return null; } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ServerConfigException.java b/src/main/java/com/sparrowwallet/sparrow/net/ServerConfigException.java new file mode 100644 index 00000000..cf5d93d0 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/ServerConfigException.java @@ -0,0 +1,18 @@ +package com.sparrowwallet.sparrow.net; + +public class ServerConfigException extends ServerException { + public ServerConfigException() { + } + + public ServerConfigException(String message) { + super(message); + } + + public ServerConfigException(Throwable cause) { + super(cause); + } + + public ServerConfigException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ServerType.java b/src/main/java/com/sparrowwallet/sparrow/net/ServerType.java new file mode 100644 index 00000000..dab5b9a9 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/ServerType.java @@ -0,0 +1,15 @@ +package com.sparrowwallet.sparrow.net; + +public enum ServerType { + BITCOIN_CORE("Bitcoin Core"), ELECTRUM_SERVER("Electrum Server"); + + private final String name; + + ServerType(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java b/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java index 963a1bb1..5bbd5884 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java @@ -75,7 +75,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc { Map result = new LinkedHashMap<>(); for(String path : pathScriptHashes.keySet()) { - EventManager.get().post(new WalletHistoryStatusEvent(wallet, false, "Loading transactions for " + path)); + EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Loading transactions for " + path)); try { ScriptHashTx[] scriptHashTxes = new RetryLogic(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() -> client.createRequest().returnAs(ScriptHashTx[].class).method("blockchain.scripthash.get_history").id(path + "-" + idCounter.incrementAndGet()).params(pathScriptHashes.get(path)).execute()); @@ -120,7 +120,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc { Map result = new LinkedHashMap<>(); for(String path : pathScriptHashes.keySet()) { - EventManager.get().post(new WalletHistoryStatusEvent(wallet, false, "Finding transactions for " + path)); + EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Finding transactions for " + path)); try { String scriptHash = new RetryLogic(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() -> client.createRequest().returnAs(String.class).method("blockchain.scripthash.subscribe").id(path + "-" + idCounter.incrementAndGet()).params(pathScriptHashes.get(path)).executeNullable()); @@ -140,7 +140,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc { Map result = new LinkedHashMap<>(); for(Integer blockHeight : blockHeights) { - EventManager.get().post(new WalletHistoryStatusEvent(wallet, false, "Retrieving block at height " + blockHeight)); + EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Retrieving block at height " + blockHeight)); try { String blockHeader = new RetryLogic(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() -> client.createRequest().returnAs(String.class).method("blockchain.block.header").id(idCounter.incrementAndGet()).params(blockHeight).execute()); @@ -161,7 +161,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc { Map result = new LinkedHashMap<>(); for(String txid : txids) { - EventManager.get().post(new WalletHistoryStatusEvent(wallet, false, "Retrieving transaction [" + txid.substring(0, 6) + "]")); + EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Retrieving transaction [" + txid.substring(0, 6) + "]")); try { String rawTxHex = new RetryLogic(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() -> client.createRequest().returnAs(String.class).method("blockchain.transaction.get").id(idCounter.incrementAndGet()).params(txid).execute()); @@ -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) { @@ -213,7 +214,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc { verboseTransaction.blockhash = Sha256Hash.ZERO_HASH.toString(); result.put(txid, verboseTransaction); } catch(Exception ex) { - throw new ElectrumServerRpcException("Error retrieving transaction: ", ex); + //ignore } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TcpTransport.java b/src/main/java/com/sparrowwallet/sparrow/net/TcpTransport.java index 33b2ed96..5751e159 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/TcpTransport.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/TcpTransport.java @@ -149,7 +149,7 @@ public class TcpTransport implements Transport, Closeable { //Restore interrupt status and continue Thread.currentThread().interrupt(); } catch(Exception e) { - log.debug("Connection error while reading", e); + log.trace("Connection error while reading", e); if(running) { lastException = e; reading = false; @@ -177,7 +177,7 @@ public class TcpTransport implements Transport, Closeable { String response = in.readLine(); if(response == null) { - throw new IOException("Could not connect to server at " + Config.get().getElectrumServer()); + throw new IOException("Could not connect to server at " + Config.get().getServerAddress()); } return response; diff --git a/src/main/java/com/sparrowwallet/sparrow/preferences/PreferencesController.java b/src/main/java/com/sparrowwallet/sparrow/preferences/PreferencesController.java index 551ca2e3..2fe79206 100644 --- a/src/main/java/com/sparrowwallet/sparrow/preferences/PreferencesController.java +++ b/src/main/java/com/sparrowwallet/sparrow/preferences/PreferencesController.java @@ -3,6 +3,8 @@ package com.sparrowwallet.sparrow.preferences; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.io.Config; import javafx.application.Platform; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; @@ -24,6 +26,10 @@ public class PreferencesController implements Initializable { @FXML private StackPane preferencesPane; + private final BooleanProperty closing = new SimpleBooleanProperty(false); + + private final BooleanProperty reconnectOnClosing = new SimpleBooleanProperty(false); + @Override public void initialize(URL location, ResourceBundle resources) { @@ -56,6 +62,18 @@ public class PreferencesController implements Initializable { } } + BooleanProperty closingProperty() { + return closing; + } + + public boolean isReconnectOnClosing() { + return reconnectOnClosing.get(); + } + + public BooleanProperty reconnectOnClosingProperty() { + return reconnectOnClosing; + } + FXMLLoader setPreferencePane(String fxmlName) { preferencesPane.getChildren().removeAll(preferencesPane.getChildren()); diff --git a/src/main/java/com/sparrowwallet/sparrow/preferences/PreferencesDialog.java b/src/main/java/com/sparrowwallet/sparrow/preferences/PreferencesDialog.java index efb7efd2..9af84ec5 100644 --- a/src/main/java/com/sparrowwallet/sparrow/preferences/PreferencesDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/preferences/PreferencesDialog.java @@ -15,8 +15,6 @@ import org.controlsfx.tools.Borders; import java.io.IOException; public class PreferencesDialog extends Dialog { - private final boolean existingConnection; - public PreferencesDialog() { this(null); } @@ -49,11 +47,12 @@ public class PreferencesDialog extends Dialog { } dialogPane.setPrefWidth(650); - dialogPane.setPrefHeight(550); + dialogPane.setPrefHeight(600); - existingConnection = ElectrumServer.isConnected(); + preferencesController.reconnectOnClosingProperty().set(AppServices.isConnecting() || AppServices.isConnected()); setOnCloseRequest(event -> { - if(existingConnection && !ElectrumServer.isConnected()) { + preferencesController.closingProperty().set(true); + if(preferencesController.isReconnectOnClosing() && !(AppServices.isConnecting() || AppServices.isConnected())) { EventManager.get().post(new RequestConnectEvent()); } }); diff --git a/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java b/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java index bf38f015..c5af5d14 100644 --- a/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java +++ b/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java @@ -1,24 +1,24 @@ package com.sparrowwallet.sparrow.preferences; +import com.google.common.eventbus.Subscribe; import com.google.common.net.HostAndPort; +import com.sparrowwallet.drongo.Network; +import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.Mode; import com.sparrowwallet.sparrow.control.TextFieldValidator; import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch; -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.ElectrumServer; -import com.sparrowwallet.sparrow.net.Protocol; +import com.sparrowwallet.sparrow.net.*; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.concurrent.WorkerStateEvent; import javafx.fxml.FXML; -import javafx.scene.control.Button; -import javafx.scene.control.Control; -import javafx.scene.control.TextArea; -import javafx.scene.control.TextField; -import javafx.scene.paint.Color; +import javafx.scene.control.*; +import javafx.scene.text.Font; +import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; import javafx.stage.Stage; import javafx.util.Duration; @@ -30,30 +30,76 @@ import org.controlsfx.validation.decoration.StyleClassValidationDecoration; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import tornadofx.control.Field; +import tornadofx.control.Form; 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 { private static final Logger log = LoggerFactory.getLogger(ServerPreferencesController.class); @FXML - private TextField host; + private ToggleGroup serverTypeToggleGroup; @FXML - private TextField port; + private Form coreForm; @FXML - private UnlabeledToggleSwitch useSsl; + private TextField coreHost; @FXML - private TextField certificate; + private TextField corePort; @FXML - private Button certificateSelect; + private ToggleGroup coreAuthToggleGroup; + + @FXML + private Field coreDataDirField; + + @FXML + private TextField coreDataDir; + + @FXML + private Button coreDataDirSelect; + + @FXML + private Field coreUserPassField; + + @FXML + private TextField coreUser; + + @FXML + private PasswordField corePass; + + @FXML + private UnlabeledToggleSwitch coreMultiWallet; + + @FXML + private TextField coreWallet; + + @FXML + private Form electrumForm; + + @FXML + private TextField electrumHost; + + @FXML + private TextField electrumPort; + + @FXML + private UnlabeledToggleSwitch electrumUseSsl; + + @FXML + private TextField electrumCertificate; + + @FXML + private Button electrumCertificateSelect; @FXML private UnlabeledToggleSwitch useProxy; @@ -75,35 +121,108 @@ public class ServerPreferencesController extends PreferencesDetailController { private final ValidationSupport validationSupport = new ValidationSupport(); + private ElectrumServer.ConnectionService connectionService; + @Override public void initializeView(Config config) { + EventManager.get().register(this); + getMasterController().closingProperty().addListener((observable, oldValue, newValue) -> { + EventManager.get().unregister(this); + }); + Platform.runLater(this::setupValidation); - port.setTextFormatter(new TextFieldValidator(TextFieldValidator.ValidationModus.MAX_INTEGERS, 5).getFormatter()); + coreForm.managedProperty().bind(coreForm.visibleProperty()); + electrumForm.managedProperty().bind(electrumForm.visibleProperty()); + coreForm.visibleProperty().bind(electrumForm.visibleProperty().not()); + serverTypeToggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> { + if(serverTypeToggleGroup.getSelectedToggle() != null) { + ServerType existingType = config.getServerType(); + ServerType serverType = (ServerType)newValue.getUserData(); + electrumForm.setVisible(serverType == ServerType.ELECTRUM_SERVER); + config.setServerType(serverType); + testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.QUESTION_CIRCLE, "")); + testResults.clear(); + if(existingType != serverType) { + EventManager.get().post(new ServerTypeChangedEvent(serverType)); + } + } else if(oldValue != null) { + oldValue.setSelected(true); + } + }); + ServerType serverType = config.getServerType() != null ? config.getServerType() : (config.getCoreServer() == null && config.getElectrumServer() != null ? ServerType.ELECTRUM_SERVER : ServerType.BITCOIN_CORE); + serverTypeToggleGroup.selectToggle(serverTypeToggleGroup.getToggles().stream().filter(toggle -> toggle.getUserData() == serverType).findFirst().orElse(null)); + + corePort.setTextFormatter(new TextFieldValidator(TextFieldValidator.ValidationModus.MAX_INTEGERS, 5).getFormatter()); + electrumPort.setTextFormatter(new TextFieldValidator(TextFieldValidator.ValidationModus.MAX_INTEGERS, 5).getFormatter()); proxyPort.setTextFormatter(new TextFieldValidator(TextFieldValidator.ValidationModus.MAX_INTEGERS, 5).getFormatter()); - host.textProperty().addListener(getElectrumServerListener(config)); - port.textProperty().addListener(getElectrumServerListener(config)); + coreHost.textProperty().addListener(getBitcoinCoreListener(config)); + corePort.textProperty().addListener(getBitcoinCoreListener(config)); + + coreUser.textProperty().addListener(getBitcoinAuthListener(config)); + corePass.textProperty().addListener(getBitcoinAuthListener(config)); + + coreWallet.textProperty().addListener(getBitcoinWalletListener(config)); + + electrumHost.textProperty().addListener(getElectrumServerListener(config)); + electrumPort.textProperty().addListener(getElectrumServerListener(config)); proxyHost.textProperty().addListener(getProxyListener(config)); proxyPort.textProperty().addListener(getProxyListener(config)); - useSsl.selectedProperty().addListener((observable, oldValue, newValue) -> { - setElectrumServerInConfig(config); - certificate.setDisable(!newValue); - certificateSelect.setDisable(!newValue); + coreDataDirField.managedProperty().bind(coreDataDirField.visibleProperty()); + coreUserPassField.managedProperty().bind(coreUserPassField.visibleProperty()); + coreUserPassField.visibleProperty().bind(coreDataDirField.visibleProperty().not()); + coreAuthToggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> { + if(coreAuthToggleGroup.getSelectedToggle() != null) { + CoreAuthType coreAuthType = (CoreAuthType)newValue.getUserData(); + coreDataDirField.setVisible(coreAuthType == CoreAuthType.COOKIE); + config.setCoreAuthType(coreAuthType); + } else if(oldValue != null) { + oldValue.setSelected(true); + } + }); + CoreAuthType coreAuthType = config.getCoreAuthType() != null ? config.getCoreAuthType() : CoreAuthType.COOKIE; + coreAuthToggleGroup.selectToggle(coreAuthToggleGroup.getToggles().stream().filter(toggle -> toggle.getUserData() == coreAuthType).findFirst().orElse(null)); + + coreDataDir.textProperty().addListener((observable, oldValue, newValue) -> { + File dataDir = getDirectory(newValue); + config.setCoreDataDir(dataDir); }); - certificate.textProperty().addListener((observable, oldValue, newValue) -> { - File crtFile = getCertificate(newValue); - if(crtFile != null) { - config.setElectrumServerCert(crtFile); - } else { - config.setElectrumServerCert(null); + coreDataDirSelect.setOnAction(event -> { + Stage window = new Stage(); + + DirectoryChooser directorChooser = new DirectoryChooser(); + directorChooser.setTitle("Select Bitcoin Core Data Directory"); + directorChooser.setInitialDirectory(config.getCoreDataDir() != null ? config.getCoreDataDir() : new File(System.getProperty("user.home"))); + + File dataDir = directorChooser.showDialog(window); + if(dataDir != null) { + coreDataDir.setText(dataDir.getAbsolutePath()); } }); - certificateSelect.setOnAction(event -> { + coreMultiWallet.selectedProperty().addListener((observable, oldValue, newValue) -> { + coreWallet.setText(" "); + coreWallet.setText(""); + coreWallet.setDisable(!newValue); + coreWallet.setPromptText(newValue ? "" : "Default"); + }); + + electrumUseSsl.selectedProperty().addListener((observable, oldValue, newValue) -> { + setElectrumServerInConfig(config); + electrumCertificate.setDisable(!newValue); + electrumCertificateSelect.setDisable(!newValue); + }); + + electrumCertificate.textProperty().addListener((observable, oldValue, newValue) -> { + File crtFile = getCertificate(newValue); + config.setElectrumServerCert(crtFile); + }); + + electrumCertificateSelect.setOnAction(event -> { Stage window = new Stage(); FileChooser fileChooser = new FileChooser(); @@ -115,7 +234,7 @@ public class ServerPreferencesController extends PreferencesDetailController { File file = fileChooser.showOpenDialog(window); if(file != null) { - certificate.setText(file.getAbsolutePath()); + electrumCertificate.setText(file.getAbsolutePath()); } }); @@ -127,45 +246,32 @@ public class ServerPreferencesController extends PreferencesDetailController { proxyPort.setDisable(!newValue); if(newValue) { - useSsl.setSelected(true); - useSsl.setDisable(true); + electrumUseSsl.setSelected(true); + electrumUseSsl.setDisable(true); } else { - useSsl.setDisable(false); + electrumUseSsl.setDisable(false); } }); - boolean isConnected = ElectrumServer.isConnected(); + boolean isConnected = AppServices.isConnecting() || AppServices.isConnected(); setFieldsEditable(!isConnected); + if(AppServices.isConnecting()) { + testResults.appendText("Connecting to server, please wait..."); + } + testConnection.managedProperty().bind(testConnection.visibleProperty()); testConnection.setVisible(!isConnected); + setTestResultsFont(); testConnection.setOnAction(event -> { - testResults.setText("Connecting to " + config.getElectrumServer() + "..."); testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.ELLIPSIS_H, null)); - - ElectrumServer.ConnectionService connectionService = new ElectrumServer.ConnectionService(false); - connectionService.setPeriod(Duration.ZERO); - EventManager.get().register(connectionService); - connectionService.statusProperty().addListener((observable, oldValue, newValue) -> { - testResults.setText(testResults.getText() + "\n" + newValue); - }); - - connectionService.setOnSucceeded(successEvent -> { - EventManager.get().unregister(connectionService); - ConnectionEvent connectionEvent = (ConnectionEvent)connectionService.getValue(); - showConnectionSuccess(connectionEvent.getServerVersion(), connectionEvent.getServerBanner()); - connectionService.cancel(); - }); - connectionService.setOnFailed(workerStateEvent -> { - EventManager.get().unregister(connectionService); - showConnectionFailure(workerStateEvent); - connectionService.cancel(); - }); - connectionService.start(); + testResults.setText("Connecting to " + config.getServerAddress() + "..."); + startElectrumConnection(); }); editConnection.managedProperty().bind(editConnection.visibleProperty()); editConnection.setVisible(isConnected); + editConnection.setDisable(AppServices.isConnecting()); editConnection.setOnAction(event -> { EventManager.get().post(new RequestDisconnectEvent()); setFieldsEditable(true); @@ -173,27 +279,61 @@ public class ServerPreferencesController extends PreferencesDetailController { testConnection.setVisible(true); }); + String coreServer = config.getCoreServer(); + if(coreServer != null) { + Protocol protocol = Protocol.getProtocol(coreServer); + + if(protocol != null) { + HostAndPort server = protocol.getServerHostAndPort(coreServer); + coreHost.setText(server.getHost()); + if(server.hasPort()) { + corePort.setText(Integer.toString(server.getPort())); + } + } + } else { + coreHost.setText("127.0.0.1"); + corePort.setText(String.valueOf(Network.get().getDefaultPort())); + } + + coreDataDir.setText(config.getCoreDataDir() != null ? config.getCoreDataDir().getAbsolutePath() : getDefaultCoreDataDir().getAbsolutePath()); + + if(config.getCoreAuth() != null) { + String[] userPass = config.getCoreAuth().split(":"); + if(userPass.length > 0) { + coreUser.setText(userPass[0]); + } + if(userPass.length > 1) { + corePass.setText(userPass[1]); + } + } + + coreMultiWallet.setSelected(true); + coreMultiWallet.setSelected(config.getCoreWallet() != null); + if(config.getCoreWallet() != null) { + coreWallet.setText(config.getCoreWallet()); + } + String electrumServer = config.getElectrumServer(); if(electrumServer != null) { Protocol protocol = Protocol.getProtocol(electrumServer); if(protocol != null) { boolean ssl = protocol.equals(Protocol.SSL); - useSsl.setSelected(ssl); - certificate.setDisable(!ssl); - certificateSelect.setDisable(!ssl); + electrumUseSsl.setSelected(ssl); + electrumCertificate.setDisable(!ssl); + electrumCertificateSelect.setDisable(!ssl); HostAndPort server = protocol.getServerHostAndPort(electrumServer); - host.setText(server.getHost()); + electrumHost.setText(server.getHost()); if(server.hasPort()) { - port.setText(Integer.toString(server.getPort())); + electrumPort.setText(Integer.toString(server.getPort())); } } } File certificateFile = config.getElectrumServerCert(); if(certificateFile != null) { - certificate.setText(certificateFile.getAbsolutePath()); + electrumCertificate.setText(certificateFile.getAbsolutePath()); } useProxy.setSelected(config.isUseProxy()); @@ -201,8 +341,8 @@ public class ServerPreferencesController extends PreferencesDetailController { proxyPort.setDisable(!config.isUseProxy()); if(config.isUseProxy()) { - useSsl.setSelected(true); - useSsl.setDisable(true); + electrumUseSsl.setSelected(true); + electrumUseSsl.setDisable(true); } String proxyServer = config.getProxyServer(); @@ -215,15 +355,55 @@ public class ServerPreferencesController extends PreferencesDetailController { } } + private void startElectrumConnection() { + if(connectionService != null && connectionService.isRunning()) { + connectionService.cancel(); + } + + connectionService = new ElectrumServer.ConnectionService(false); + connectionService.setPeriod(Duration.hours(1)); + EventManager.get().register(connectionService); + connectionService.statusProperty().addListener((observable, oldValue, newValue) -> { + testResults.setText(testResults.getText() + "\n" + newValue); + }); + + connectionService.setOnSucceeded(successEvent -> { + EventManager.get().unregister(connectionService); + ConnectionEvent connectionEvent = (ConnectionEvent)connectionService.getValue(); + showConnectionSuccess(connectionEvent.getServerVersion(), connectionEvent.getServerBanner()); + getMasterController().reconnectOnClosingProperty().set(true); + Config.get().setMode(Mode.ONLINE); + connectionService.cancel(); + }); + connectionService.setOnFailed(workerStateEvent -> { + EventManager.get().unregister(connectionService); + showConnectionFailure(workerStateEvent); + connectionService.cancel(); + }); + connectionService.start(); + } + private void setFieldsEditable(boolean editable) { - host.setEditable(editable); - port.setEditable(editable); - useSsl.setDisable(!editable); - certificate.setEditable(editable); - certificateSelect.setDisable(!editable); + serverTypeToggleGroup.getToggles().forEach(toggle -> ((ToggleButton)toggle).setDisable(!editable)); + + coreHost.setDisable(!editable); + corePort.setDisable(!editable); + coreAuthToggleGroup.getToggles().forEach(toggle -> ((ToggleButton)toggle).setDisable(!editable)); + coreDataDir.setDisable(!editable); + coreDataDirSelect.setDisable(!editable); + coreUser.setDisable(!editable); + corePass.setDisable(!editable); + coreMultiWallet.setDisable(!editable); + coreWallet.setDisable(!editable); + + electrumHost.setDisable(!editable); + electrumPort.setDisable(!editable); + electrumUseSsl.setDisable(!editable); + electrumCertificate.setDisable(!editable); + electrumCertificateSelect.setDisable(!editable); useProxy.setDisable(!editable); - proxyHost.setEditable(editable); - proxyPort.setEditable(editable); + proxyHost.setDisable(!editable); + proxyPort.setDisable(!editable); } private void showConnectionSuccess(List serverVersion, String serverBanner) { @@ -252,12 +432,36 @@ public class ServerPreferencesController extends PreferencesDetailController { } private void setupValidation() { - validationSupport.registerValidator(host, Validator.combine( - (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid host name", getHost(newValue) == null) + validationSupport.registerValidator(coreHost, Validator.combine( + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid Core host", getHost(newValue) == null) )); - validationSupport.registerValidator(port, Validator.combine( - (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid port", !newValue.isEmpty() && !isValidPort(Integer.parseInt(newValue))) + validationSupport.registerValidator(corePort, Validator.combine( + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid Core port", !newValue.isEmpty() && !isValidPort(Integer.parseInt(newValue))) + )); + + validationSupport.registerValidator(coreDataDir, Validator.combine( + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Core Data Dir required", coreAuthToggleGroup.getSelectedToggle().getUserData() == CoreAuthType.COOKIE && (newValue.isEmpty() || getDirectory(newValue) == null)) + )); + + validationSupport.registerValidator(coreUser, Validator.combine( + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Core user required", coreAuthToggleGroup.getSelectedToggle().getUserData() == CoreAuthType.USERPASS && newValue.isEmpty()) + )); + + validationSupport.registerValidator(corePass, Validator.combine( + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Core pass required", coreAuthToggleGroup.getSelectedToggle().getUserData() == CoreAuthType.USERPASS && newValue.isEmpty()) + )); + + validationSupport.registerValidator(coreWallet, Validator.combine( + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Core wallet required", coreMultiWallet.isSelected() && newValue.isEmpty()) + )); + + validationSupport.registerValidator(electrumHost, Validator.combine( + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid Electrum host", getHost(newValue) == null) + )); + + validationSupport.registerValidator(electrumPort, Validator.combine( + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid Electrum port", !newValue.isEmpty() && !isValidPort(Integer.parseInt(newValue))) )); validationSupport.registerValidator(proxyHost, Validator.combine( @@ -269,13 +473,44 @@ public class ServerPreferencesController extends PreferencesDetailController { (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid proxy port", !newValue.isEmpty() && !isValidPort(Integer.parseInt(newValue))) )); - validationSupport.registerValidator(certificate, Validator.combine( + validationSupport.registerValidator(electrumCertificate, Validator.combine( (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid certificate file", newValue != null && !newValue.isEmpty() && getCertificate(newValue) == null) )); validationSupport.setValidationDecorator(new StyleClassValidationDecoration()); } + @NotNull + private ChangeListener getBitcoinCoreListener(Config config) { + return (observable, oldValue, newValue) -> { + setCoreServerInConfig(config); + }; + } + + private void setCoreServerInConfig(Config config) { + String hostAsString = getHost(coreHost.getText()); + Integer portAsInteger = getPort(corePort.getText()); + if(hostAsString != null && portAsInteger != null && isValidPort(portAsInteger)) { + config.setCoreServer(Protocol.HTTP.toUrlString(hostAsString, portAsInteger)); + } else if(hostAsString != null) { + config.setCoreServer(Protocol.HTTP.toUrlString(hostAsString)); + } + } + + @NotNull + private ChangeListener getBitcoinAuthListener(Config config) { + return (observable, oldValue, newValue) -> { + config.setCoreAuth(coreUser.getText() + ":" + corePass.getText()); + }; + } + + @NotNull + private ChangeListener getBitcoinWalletListener(Config config) { + return (observable, oldValue, newValue) -> { + config.setCoreWallet(coreWallet.getText()); + }; + } + @NotNull private ChangeListener getElectrumServerListener(Config config) { return (observable, oldValue, newValue) -> { @@ -284,8 +519,8 @@ public class ServerPreferencesController extends PreferencesDetailController { } private void setElectrumServerInConfig(Config config) { - String hostAsString = getHost(host.getText()); - Integer portAsInteger = getPort(port.getText()); + String hostAsString = getHost(electrumHost.getText()); + Integer portAsInteger = getPort(electrumPort.getText()); if(hostAsString != null && portAsInteger != null && isValidPort(portAsInteger)) { config.setElectrumServer(getProtocol().toUrlString(hostAsString, portAsInteger)); } else if(hostAsString != null) { @@ -311,7 +546,7 @@ public class ServerPreferencesController extends PreferencesDetailController { } private Protocol getProtocol() { - return (useSsl.isSelected() ? Protocol.SSL : Protocol.TCP); + return (electrumUseSsl.isSelected() ? Protocol.SSL : Protocol.TCP); } private String getHost(String text) { @@ -330,6 +565,19 @@ public class ServerPreferencesController extends PreferencesDetailController { } } + private File getDirectory(String dirLocation) { + try { + File dirFile = new File(dirLocation); + if(!dirFile.exists() || !dirFile.isDirectory()) { + return null; + } + + return dirFile; + } catch (Exception e) { + return null; + } + } + private File getCertificate(String crtFileLocation) { try { File crtFile = new File(crtFileLocation); @@ -357,4 +605,48 @@ public class ServerPreferencesController extends PreferencesDetailController { private static boolean isValidPort(int port) { return port >= 0 && port <= 65535; } + + private File getDefaultCoreDataDir() { + org.controlsfx.tools.Platform platform = org.controlsfx.tools.Platform.getCurrent(); + if(platform == org.controlsfx.tools.Platform.OSX) { + return new File(System.getProperty("user.home") + "/Library/Application Support/Bitcoin"); + } else if(platform == org.controlsfx.tools.Platform.WINDOWS) { + return new File(System.getenv("APPDATA") + "/Bitcoin"); + } else { + return new File(System.getProperty("user.home") + "/.bitcoin"); + } + } + + private void setTestResultsFont() { + org.controlsfx.tools.Platform platform = org.controlsfx.tools.Platform.getCurrent(); + if(platform == org.controlsfx.tools.Platform.OSX) { + testResults.setFont(Font.font("Monaco", 11)); + } else if(platform == org.controlsfx.tools.Platform.WINDOWS) { + testResults.setFont(Font.font("Lucida Console", 11)); + } else { + testResults.setFont(Font.font("monospace", 11)); + } + } + + @Subscribe + public void bwtStatus(BwtStatusEvent event) { + if(!(event instanceof BwtSyncStatusEvent)) { + testResults.appendText("\n" + event.getStatus()); + } + if(event instanceof BwtReadyStatusEvent) { + editConnection.setDisable(false); + } + } + + @Subscribe + public void bwtSyncStatus(BwtSyncStatusEvent event) { + editConnection.setDisable(false); + if(connectionService != null && connectionService.isRunning() && event.getProgress() < 100) { + 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/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index 07daaa95..8fac7034 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java @@ -25,6 +25,7 @@ import com.sparrowwallet.sparrow.wallet.HashIndexEntry; import com.sparrowwallet.sparrow.wallet.TransactionEntry; import javafx.application.Platform; import javafx.collections.FXCollections; +import javafx.collections.ObservableMap; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.Initializable; @@ -415,17 +416,17 @@ public class HeadersController extends TransactionFormController implements Init } }); - headersForm.signingWalletProperty().addListener((observable, oldValue, signingWallet) -> { - initializeSignButton(signingWallet); - updateSignedKeystores(signingWallet); - - int threshold = signingWallet.getDefaultPolicy().getNumSignaturesRequired(); - signaturesProgressBar.initialize(headersForm.getSignatureKeystoreMap(), threshold); - }); - Platform.runLater(this::requestOpenWallets); } + headersForm.signingWalletProperty().addListener((observable, oldValue, signingWallet) -> { + initializeSignButton(signingWallet); + updateSignedKeystores(signingWallet); + + int threshold = signingWallet.getDefaultPolicy().getNumSignaturesRequired(); + signaturesProgressBar.initialize(headersForm.getSignatureKeystoreMap(), threshold); + }); + blockchainForm.setDynamicUpdate(this); } @@ -756,7 +757,7 @@ public class HeadersController extends TransactionFormController implements Init } private void updateSignedKeystores(Wallet signingWallet) { - Map> signedKeystoresMap = signingWallet.getSignedKeystores(headersForm.getPsbt()); + Map> signedKeystoresMap = headersForm.getPsbt() == null ? signingWallet.getSignedKeystores(headersForm.getTransaction()) : signingWallet.getSignedKeystores(headersForm.getPsbt()); Optional> optSignedKeystores = signedKeystoresMap.values().stream().filter(map -> !map.isEmpty()).min(Comparator.comparingInt(Map::size)); optSignedKeystores.ifPresent(signedKeystores -> { headersForm.getSignatureKeystoreMap().keySet().retainAll(signedKeystores.keySet()); @@ -781,7 +782,9 @@ public class HeadersController extends TransactionFormController implements Init public void broadcastTransaction(ActionEvent event) { broadcastButton.setDisable(true); - extractTransaction(event); + if(headersForm.getPsbt() != null) { + extractTransaction(event); + } if(headersForm.getSigningWallet() instanceof FinalizingPSBTWallet) { //Ensure the script hashes of the UTXOs in FinalizingPSBTWallet are subscribed to @@ -818,6 +821,21 @@ public class HeadersController extends TransactionFormController implements Init } }); transactionMempoolService.start(); + } else { + Sha256Hash txid = headersForm.getTransaction().getTxId(); + ElectrumServer.TransactionReferenceService transactionReferenceService = new ElectrumServer.TransactionReferenceService(Set.of(txid)); + transactionReferenceService.setOnSucceeded(successEvent -> { + Map transactionMap = transactionReferenceService.getValue(); + BlockTransaction blockTransaction = transactionMap.get(txid); + if(blockTransaction != null) { + headersForm.setBlockTransaction(blockTransaction); + updateBlockchainForm(blockTransaction, AppServices.getCurrentBlockHeight()); + } + }); + transactionReferenceService.setOnFailed(failedEvent -> { + log.error("Error fetching broadcasted transaction", failedEvent.getSource().getException()); + }); + transactionReferenceService.start(); } }); broadcastTransactionService.setOnFailed(workerStateEvent -> { @@ -903,6 +921,43 @@ public class HeadersController extends TransactionFormController implements Init if(event.getTxId().equals(headersForm.getTransaction().getTxId())) { if(event.getBlockTransaction() != null && (!Sha256Hash.ZERO_HASH.equals(event.getBlockTransaction().getBlockHash()) || headersForm.getBlockTransaction() == null)) { updateBlockchainForm(event.getBlockTransaction(), AppServices.getCurrentBlockHeight()); + } else if(headersForm.getPsbt() == null) { + boolean isSigned = true; + ObservableMap signatureKeystoreMap = FXCollections.observableMap(new LinkedHashMap<>()); + for(TransactionInput txInput : headersForm.getTransaction().getInputs()) { + List signatures = txInput.hasWitness() ? txInput.getWitness().getSignatures() : txInput.getScriptSig().getSignatures(); + + if(signatures.isEmpty()) { + isSigned = false; + break; + } + + if(signatureKeystoreMap.isEmpty()) { + for(int i = 0; i < signatures.size(); i++) { + signatureKeystoreMap.put(signatures.get(i), new Keystore("Keystore " + (i+1))); + } + } + } + + if(isSigned) { + blockchainForm.setVisible(false); + signaturesForm.setVisible(true); + broadcastButtonBox.setVisible(true); + viewFinalButton.setDisable(true); + + if(headersForm.getSigningWallet() == null) { + for(Wallet wallet : AppServices.get().getOpenWallets().keySet()) { + if(wallet.canSign(headersForm.getTransaction())) { + headersForm.setSigningWallet(wallet); + break; + } + } + } + + if(headersForm.getSigningWallet() == null) { + signaturesProgressBar.initialize(signatureKeystoreMap, signatureKeystoreMap.size()); + } + } } Long feeAmt = calculateFee(event.getInputTransactions()); diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionController.java index 6a9917db..0aad9cb9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionController.java @@ -326,7 +326,7 @@ public class TransactionController implements Initializable { } private void fetchThisAndInputBlockTransactions(int indexStart, int indexEnd) { - if(AppServices.isOnline() && indexStart < getTransaction().getInputs().size()) { + if(AppServices.isConnected() && indexStart < getTransaction().getInputs().size()) { Set references = new HashSet<>(); if(getPSBT() == null) { references.add(getTransaction().getTxId()); @@ -378,7 +378,7 @@ public class TransactionController implements Initializable { } private void fetchOutputBlockTransactions(int indexStart, int indexEnd) { - if(AppServices.isOnline() && getPSBT() == null && indexStart < getTransaction().getOutputs().size()) { + if(AppServices.isConnected() && getPSBT() == null && indexStart < getTransaction().getOutputs().size()) { int maxIndex = Math.min(getTransaction().getOutputs().size(), indexEnd); ElectrumServer.TransactionOutputsReferenceService transactionOutputsReferenceService = new ElectrumServer.TransactionOutputsReferenceService(getTransaction(), indexStart, maxIndex); transactionOutputsReferenceService.setOnSucceeded(successEvent -> { diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/AdvancedController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/AdvancedController.java index d27c0e6a..6beb32ab 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/AdvancedController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/AdvancedController.java @@ -2,16 +2,23 @@ package com.sparrowwallet.sparrow.wallet; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.control.DateStringConverter; import com.sparrowwallet.sparrow.event.SettingsChangedEvent; import javafx.fxml.FXML; import javafx.fxml.Initializable; +import javafx.scene.control.DatePicker; import javafx.scene.control.Spinner; import javafx.scene.control.SpinnerValueFactory; import java.net.URL; +import java.time.ZoneId; +import java.util.Date; import java.util.ResourceBundle; public class AdvancedController implements Initializable { + @FXML + private DatePicker birthDate; + @FXML private Spinner gapLimit; @@ -21,6 +28,17 @@ public class AdvancedController implements Initializable { } public void initializeView(Wallet wallet) { + birthDate.setConverter(new DateStringConverter()); + if(wallet.getBirthDate() != null) { + birthDate.setValue(wallet.getBirthDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate()); + } + birthDate.valueProperty().addListener((observable, oldValue, newValue) -> { + if(newValue != null) { + wallet.setBirthDate(Date.from(newValue.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant())); + EventManager.get().post(new SettingsChangedEvent(wallet, SettingsChangedEvent.Type.BIRTH_DATE)); + } + }); + gapLimit.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(Wallet.DEFAULT_LOOKAHEAD, 10000, wallet.getGapLimit())); gapLimit.valueProperty().addListener((observable, oldValue, newValue) -> { wallet.setGapLimit(newValue); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/ReceiveController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/ReceiveController.java index 3a468dec..04e31a7d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/ReceiveController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/ReceiveController.java @@ -138,7 +138,7 @@ public class ReceiveController extends WalletFormController implements Initializ private void updateLastUsed() { Set currentOutputs = currentEntry.getNode().getTransactionOutputs(); - if(AppServices.isOnline() && currentOutputs.isEmpty()) { + if(AppServices.isConnected() && currentOutputs.isEmpty()) { lastUsed.setText("Never"); lastUsed.setGraphic(getUnusedGlyph()); } else if(!currentOutputs.isEmpty()) { diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java index aa0586f5..6ef1130e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java @@ -14,10 +14,7 @@ import com.sparrowwallet.drongo.wallet.WalletModel; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.control.*; -import com.sparrowwallet.sparrow.event.RequestOpenWalletsEvent; -import com.sparrowwallet.sparrow.event.SettingsChangedEvent; -import com.sparrowwallet.sparrow.event.StorageEvent; -import com.sparrowwallet.sparrow.event.TimedEvent; +import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.io.Storage; import javafx.beans.property.SimpleIntegerProperty; import javafx.collections.FXCollections; @@ -28,17 +25,14 @@ import javafx.fxml.Initializable; import javafx.scene.control.*; import javafx.scene.layout.StackPane; import org.controlsfx.control.RangeSlider; -import org.controlsfx.tools.Borders; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import tornadofx.control.Fieldset; +import java.io.File; import java.io.IOException; import java.net.URL; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.ResourceBundle; +import java.util.*; import java.util.stream.Collectors; public class SettingsController extends WalletFormController implements Initializable { @@ -329,6 +323,22 @@ public class SettingsController extends WalletFormController implements Initiali } } + @Subscribe + public void walletSettingsChanged(WalletSettingsChangedEvent event) { + updateBirthDate(event.getWalletFile(), event.getWallet()); + } + + @Subscribe + public void walletHistoryChanged(WalletHistoryChangedEvent event) { + updateBirthDate(event.getWalletFile(), event.getWallet()); + } + + private void updateBirthDate(File walletFile, Wallet wallet) { + if(walletFile.equals(walletForm.getWalletFile()) && !Objects.equals(wallet.getBirthDate(), walletForm.getWallet().getBirthDate())) { + walletForm.getWallet().setBirthDate(wallet.getBirthDate()); + } + } + private void saveWallet(boolean changePassword) { ECKey existingPubKey = walletForm.getStorage().getEncryptionPubKey(); @@ -345,6 +355,15 @@ public class SettingsController extends WalletFormController implements Initiali requirement = WalletPasswordDialog.PasswordRequirement.UPDATE_SET; } + if(!changePassword && ((SettingsWalletForm)walletForm).isAddressChange() && !walletForm.getWallet().getTransactions().isEmpty()) { + Optional optResponse = AppServices.showWarningDialog("Change Wallet Addresses?", "This wallet has existing transactions which will be replaced as the wallet addresses will change. Ok to proceed?", ButtonType.CANCEL, ButtonType.OK); + if(optResponse.isPresent() && optResponse.get().equals(ButtonType.CANCEL)) { + revert.setDisable(false); + apply.setDisable(false); + return; + } + } + WalletPasswordDialog dlg = new WalletPasswordDialog(requirement); Optional password = dlg.showAndWait(); if(password.isPresent()) { diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsWalletForm.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsWalletForm.java index 6cb0db88..17e07718 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsWalletForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsWalletForm.java @@ -7,6 +7,7 @@ import com.sparrowwallet.sparrow.event.WalletSettingsChangedEvent; import com.sparrowwallet.sparrow.io.Storage; import java.io.IOException; +import java.util.Objects; /** * This class extends WalletForm to allow rollback of wallet changes. It is used exclusively by SettingsController for this purpose. @@ -37,7 +38,7 @@ public class SettingsWalletForm extends WalletForm { @Override public void saveAndRefresh() throws IOException { - boolean refreshAll = changesScriptHashes(wallet, walletCopy); + boolean refreshAll = isRefreshNecessary(wallet, walletCopy); if(refreshAll) { walletCopy.clearNodes(); } @@ -50,11 +51,31 @@ public class SettingsWalletForm extends WalletForm { } } - private boolean changesScriptHashes(Wallet original, Wallet changed) { + private boolean isRefreshNecessary(Wallet original, Wallet changed) { if(!original.isValid() || !changed.isValid()) { return true; } + if(isAddressChange(original, changed)) { + return true; + } + + if(original.getGapLimit() != changed.getGapLimit()) { + return true; + } + + if(!Objects.equals(original.getBirthDate(), changed.getBirthDate())) { + return true; + } + + return false; + } + + protected boolean isAddressChange() { + return isAddressChange(wallet, walletCopy); + } + + private boolean isAddressChange(Wallet original, Wallet changed) { if(original.getPolicyType() != changed.getPolicyType()) { return true; } @@ -73,19 +94,15 @@ public class SettingsWalletForm extends WalletForm { Keystore originalKeystore = original.getKeystores().get(i); Keystore changedKeystore = changed.getKeystores().get(i); - if(!originalKeystore.getKeyDerivation().equals(changedKeystore.getKeyDerivation())) { + if(!Objects.equals(originalKeystore.getKeyDerivation(), changedKeystore.getKeyDerivation())) { return true; } - if(!originalKeystore.getExtendedPublicKey().equals(changedKeystore.getExtendedPublicKey())) { + if(!Objects.equals(originalKeystore.getExtendedPublicKey(), changedKeystore.getExtendedPublicKey())) { return true; } } - if(original.getGapLimit() != changed.getGapLimit()) { - return true; - } - return false; } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionEntry.java index ece8e2e5..e24449e7 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionEntry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionEntry.java @@ -14,11 +14,15 @@ import javafx.beans.property.IntegerProperty; import javafx.beans.property.IntegerPropertyBase; import javafx.beans.property.LongProperty; import javafx.beans.property.LongPropertyBase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.*; import java.util.stream.Collectors; public class TransactionEntry extends Entry implements Comparable { + private static final Logger log = LoggerFactory.getLogger(TransactionEntry.class); + private final BlockTransaction blockTransaction; public TransactionEntry(Wallet wallet, BlockTransaction blockTransaction, Map inputs, Map outputs) { @@ -86,6 +90,7 @@ public class TransactionEntry extends Entry implements Comparable ((HashIndexEntry)entry).getHashIndex().equals(optRef.get().getSpentBy()) && ((HashIndexEntry)entry).getType().equals(HashIndexEntry.Type.INPUT))) { + log.warn("TransactionEntry " + blockTransaction.getHash() + " for wallet " + getWallet().getName() + " missing child for input " + optRef.get().getSpentBy() + " on output " + optRef.get()); return false; } } @@ -95,12 +100,14 @@ public class TransactionEntry extends Entry implements Comparable ((HashIndexEntry)entry).getHashIndex().equals(optRef.get()) && ((HashIndexEntry)entry).getType().equals(HashIndexEntry.Type.OUTPUT))) { + log.warn("TransactionEntry " + blockTransaction.getHash() + " for wallet " + getWallet().getName() + " missing child for output " + optRef.get()); return false; } } } if(getChildren().size() != validEntries) { + log.warn("TransactionEntry " + blockTransaction.getHash() + " for wallet " + getWallet().getName() + " has incorrect number of children " + getChildren().size() + " (should be " + validEntries + ")"); return false; } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionsController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionsController.java index b418b9e6..ccdadb24 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionsController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionsController.java @@ -4,10 +4,7 @@ import com.google.common.eventbus.Subscribe; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.CurrencyRate; import com.sparrowwallet.sparrow.EventManager; -import com.sparrowwallet.sparrow.control.BalanceChart; -import com.sparrowwallet.sparrow.control.CoinLabel; -import com.sparrowwallet.sparrow.control.FiatLabel; -import com.sparrowwallet.sparrow.control.TransactionsTreeTable; +import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.net.ExchangeSource; import javafx.collections.ListChangeListener; @@ -29,6 +26,12 @@ public class TransactionsController extends WalletFormController implements Init @FXML private CoinLabel mempoolBalance; + @FXML + private FiatLabel fiatMempoolBalance; + + @FXML + private CopyableLabel transactionCount; + @FXML private TransactionsTreeTable transactionsTable; @@ -47,10 +50,14 @@ public class TransactionsController extends WalletFormController implements Init transactionsTable.initialize(walletTransactionsEntry); balance.valueProperty().addListener((observable, oldValue, newValue) -> { - setFiatBalance(AppServices.getFiatCurrencyExchangeRate(), newValue.longValue()); + setFiatBalance(fiatBalance, AppServices.getFiatCurrencyExchangeRate(), newValue.longValue()); }); balance.setValue(walletTransactionsEntry.getBalance()); + mempoolBalance.valueProperty().addListener((observable, oldValue, newValue) -> { + setFiatBalance(fiatMempoolBalance, AppServices.getFiatCurrencyExchangeRate(), newValue.longValue()); + }); mempoolBalance.setValue(walletTransactionsEntry.getMempoolBalance()); + setTransactionCount(walletTransactionsEntry); balanceChart.initialize(walletTransactionsEntry); transactionsTable.getSelectionModel().getSelectedIndices().addListener((ListChangeListener) c -> { @@ -61,12 +68,19 @@ public class TransactionsController extends WalletFormController implements Init }); } - private void setFiatBalance(CurrencyRate currencyRate, long balance) { - if(currencyRate != null && currencyRate.isAvailable()) { - fiatBalance.set(currencyRate, balance); + private void setFiatBalance(FiatLabel fiatLabel, CurrencyRate currencyRate, long balance) { + if(currencyRate != null && currencyRate.isAvailable() && balance > 0) { + fiatLabel.set(currencyRate, balance); + } else { + fiatLabel.setCurrency(null); + fiatLabel.setBtcRate(0.0); } } + private void setTransactionCount(WalletTransactionsEntry walletTransactionsEntry) { + transactionCount.setText(walletTransactionsEntry.getChildren() != null ? Integer.toString(walletTransactionsEntry.getChildren().size()) : "0"); + } + @Subscribe public void walletNodesChanged(WalletNodesChangedEvent event) { if(event.getWallet().equals(walletForm.getWallet())) { @@ -76,6 +90,7 @@ public class TransactionsController extends WalletFormController implements Init balance.setValue(walletTransactionsEntry.getBalance()); mempoolBalance.setValue(walletTransactionsEntry.getMempoolBalance()); balanceChart.update(walletTransactionsEntry); + setTransactionCount(walletTransactionsEntry); } } @@ -91,6 +106,7 @@ public class TransactionsController extends WalletFormController implements Init balance.setValue(walletTransactionsEntry.getBalance()); mempoolBalance.setValue(walletTransactionsEntry.getMempoolBalance()); balanceChart.update(walletTransactionsEntry); + setTransactionCount(walletTransactionsEntry); } } @@ -115,12 +131,15 @@ public class TransactionsController extends WalletFormController implements Init if(event.getExchangeSource() == ExchangeSource.NONE) { fiatBalance.setCurrency(null); fiatBalance.setBtcRate(0.0); + fiatMempoolBalance.setCurrency(null); + fiatMempoolBalance.setBtcRate(0.0); } } @Subscribe public void exchangeRatesUpdated(ExchangeRatesUpdatedEvent event) { - setFiatBalance(event.getCurrencyRate(), getWalletForm().getWalletTransactionsEntry().getBalance()); + setFiatBalance(fiatBalance, event.getCurrencyRate(), getWalletForm().getWalletTransactionsEntry().getBalance()); + setFiatBalance(fiatMempoolBalance, event.getCurrencyRate(), getWalletForm().getWalletTransactionsEntry().getMempoolBalance()); } @Subscribe @@ -128,6 +147,21 @@ public class TransactionsController extends WalletFormController implements Init transactionsTable.updateHistoryStatus(event); } + @Subscribe + public void bwtSyncStatus(BwtSyncStatusEvent event) { + walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), true, event.getStatus())); + } + + @Subscribe + public void bwtScanStatus(BwtScanStatusEvent event) { + walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), true, event.getStatus())); + } + + @Subscribe + public void bwtShutdown(BwtShutdownEvent event) { + walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), false)); + } + @Subscribe public void walletUtxoStatusChanged(WalletUtxoStatusChangedEvent event) { if(event.getWallet().equals(getWalletForm().getWallet())) { diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/UtxosController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/UtxosController.java index 2da0b76e..e76960d8 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/UtxosController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/UtxosController.java @@ -133,6 +133,22 @@ public class UtxosController extends WalletFormController implements Initializab utxosTable.updateHistoryStatus(event); } + + @Subscribe + public void bwtSyncStatus(BwtSyncStatusEvent event) { + walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), true, event.getStatus())); + } + + @Subscribe + public void bwtScanStatus(BwtScanStatusEvent event) { + walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), true, event.getStatus())); + } + + @Subscribe + public void bwtShutdown(BwtShutdownEvent event) { + walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), false)); + } + @Subscribe public void walletUtxoStatusChanged(WalletUtxoStatusChangedEvent event) { if(event.getWallet().equals(getWalletForm().getWallet())) { diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java index dc36c78f..01a2a014 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java @@ -75,18 +75,18 @@ public class WalletForm { public void refreshHistory(Integer blockHeight, WalletNode node) { Wallet previousWallet = wallet.copy(); - if(wallet.isValid() && AppServices.isOnline()) { + if(wallet.isValid() && AppServices.isConnected()) { log.debug(node == null ? wallet.getName() + " refreshing full wallet history" : wallet.getName() + " requesting node wallet history for " + node.getDerivationPath()); ElectrumServer.TransactionHistoryService historyService = new ElectrumServer.TransactionHistoryService(wallet, getWalletTransactionNodes(node)); historyService.setOnSucceeded(workerStateEvent -> { - EventManager.get().post(new WalletHistoryStatusEvent(wallet, true)); + EventManager.get().post(new WalletHistoryStatusEvent(wallet, false)); updateWallet(previousWallet, blockHeight); }); historyService.setOnFailed(workerStateEvent -> { log.error("Error retrieving wallet history", workerStateEvent.getSource().getException()); EventManager.get().post(new WalletHistoryStatusEvent(wallet, workerStateEvent.getSource().getException().getMessage())); }); - EventManager.get().post(new WalletHistoryStatusEvent(wallet, false)); + EventManager.get().post(new WalletHistoryStatusEvent(wallet, true)); historyService.start(); } } @@ -106,7 +106,7 @@ public class WalletForm { boolean changed = false; if(!historyChangedNodes.isEmpty()) { - Platform.runLater(() -> EventManager.get().post(new WalletHistoryChangedEvent(wallet, historyChangedNodes))); + Platform.runLater(() -> EventManager.get().post(new WalletHistoryChangedEvent(wallet, storage, historyChangedNodes))); changed = true; } diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 61ab145f..a6a4839d 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -25,4 +25,5 @@ open module com.sparrowwallet.sparrow { requires centerdevice.nsmenufx; requires jcommander; requires slf4j.api; + requires bwt.jni; } \ No newline at end of file diff --git a/src/main/resources/com/sparrowwallet/sparrow/about.fxml b/src/main/resources/com/sparrowwallet/sparrow/about.fxml index 7ba67d3a..ab280520 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/about.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/about.fxml @@ -8,7 +8,7 @@ - + @@ -21,8 +21,9 @@ + + + + + + + + + + + -
+
+
+ + + + + + + + + + + @@ -53,7 +124,7 @@
- + - + diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/advanced.fxml b/src/main/resources/com/sparrowwallet/sparrow/wallet/advanced.fxml index 655ecd54..92a456a6 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/advanced.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/advanced.fxml @@ -25,7 +25,11 @@
-
+
+ + + + diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/transactions.fxml b/src/main/resources/com/sparrowwallet/sparrow/wallet/transactions.fxml index 39ebf611..9bbdc27a 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/transactions.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/transactions.fxml @@ -14,6 +14,7 @@ +
@@ -32,13 +33,13 @@
- - - - + - + + + +
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 5abd8741..27e7c41b 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -22,7 +22,7 @@ - + diff --git a/src/main/resources/native/linux/x64/libbwt_jni.so b/src/main/resources/native/linux/x64/libbwt_jni.so new file mode 100755 index 00000000..351763d8 Binary files /dev/null and b/src/main/resources/native/linux/x64/libbwt_jni.so differ diff --git a/src/main/resources/native/osx/x64/libbwt_jni.dylib b/src/main/resources/native/osx/x64/libbwt_jni.dylib new file mode 100755 index 00000000..52c7bea1 Binary files /dev/null and b/src/main/resources/native/osx/x64/libbwt_jni.dylib differ diff --git a/src/main/resources/native/windows/x64/bwt_jni.dll b/src/main/resources/native/windows/x64/bwt_jni.dll new file mode 100755 index 00000000..60e6d618 Binary files /dev/null and b/src/main/resources/native/windows/x64/bwt_jni.dll differ