diff --git a/build.gradle b/build.gradle index e79a349b..00d03b9d 100644 --- a/build.gradle +++ b/build.gradle @@ -69,7 +69,7 @@ dependencies { exclude group: 'org.openjfx', module: 'javafx-web' exclude group: 'org.openjfx', module: 'javafx-media' } - implementation('dev.bwt:bwt-jni:0.1.4') + implementation('dev.bwt:bwt-jni:0.1.5') testImplementation('junit:junit:4.12') } diff --git a/drongo b/drongo index 6ad3f537..4da2c024 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 6ad3f5373119b65d17b857738b8411ee88cea993 +Subproject commit 4da2c024d4fbe3ecbca9772a0761def6d5269c38 diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 5d22fe05..0ca26117 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -110,6 +110,8 @@ public class AppController implements Initializable { @FXML private UnlabeledToggleSwitch serverToggle; + private PauseTransition wait; + private Timeline statusTimeline; @Override @@ -170,7 +172,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 +196,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,7 +223,7 @@ public class AppController implements Initializable { showTxHex.setSelected(Config.get().isShowTransactionHex()); exportWallet.setDisable(true); - serverToggle.setSelected(isOnline()); + serverToggle.setSelected(isConnected()); onlineProperty().bindBidirectional(serverToggle.selectedProperty()); onlineProperty().addListener((observable, oldValue, newValue) -> { Platform.runLater(() -> setServerToggleTooltip(getCurrentBlockHeight())); @@ -451,21 +453,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,7 +532,7 @@ public class AppController implements Initializable { } private void setServerToggleTooltip(Integer currentBlockHeight) { - serverToggle.setTooltip(new Tooltip(AppServices.isOnline() ? "Connected to " + Config.get().getServerAddress() + (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) { @@ -845,6 +856,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); @@ -1162,7 +1175,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(""); @@ -1237,14 +1253,14 @@ public class AppController implements Initializable { @Subscribe public void bwtSyncStatus(BwtSyncStatusEvent event) { - if(AppServices.isOnline()) { + if((AppServices.isConnecting() || AppServices.isConnected()) && !event.isCompleted()) { statusUpdated(new StatusEvent(event.getStatus())); } } @Subscribe public void bwtScanStatus(BwtScanStatusEvent event) { - if(AppServices.isOnline()) { + if((AppServices.isConnecting() || AppServices.isConnected()) && !event.isCompleted()) { statusUpdated(new StatusEvent(event.getStatus())); } } @@ -1302,7 +1318,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 73fbc0c4..5cba587e 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; @@ -57,7 +54,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); @@ -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)); @@ -267,24 +278,22 @@ public class AppServices { public Map getOpenWallets() { Map openWallets = new LinkedHashMap<>(); - for(Map walletStorageMap : walletWindows.values()) { - openWallets.putAll(walletStorageMap); + for(List walletTabDataList : walletWindows.values()) { + for(WalletTabData walletTabData : walletTabDataList) { + openWallets.put(walletTabData.getWallet(), walletTabData.getStorage()); + } } return openWallets; } - public Map getOpenWallets(Window window) { - return walletWindows.get(window); - } - 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); } @@ -292,8 +301,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 onlineProperty.get() && get().connectionService.isConnecting(); + } + + public static boolean isConnected() { + return onlineProperty.get() && get().connectionService.isConnected(); } public static BooleanProperty onlineProperty() { @@ -440,25 +453,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; @@ -494,4 +507,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 ad545a55..db6e9a10 100644 --- a/src/main/java/com/sparrowwallet/sparrow/MainApp.java +++ b/src/main/java/com/sparrowwallet/sparrow/MainApp.java @@ -96,6 +96,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 175032ac..8b79a5da 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WalletNameDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletNameDialog.java @@ -42,7 +42,7 @@ public class WalletNameDialog extends Dialog dialogPane.setHeaderText("Enter a name for this wallet:"); dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm()); dialogPane.getButtonTypes().addAll(ButtonType.CANCEL); - dialogPane.setPrefWidth(420); + dialogPane.setPrefWidth(460); dialogPane.setPrefHeight(requestBirthDate ? 250 : 200); Glyph wallet = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WALLET); @@ -60,12 +60,20 @@ public class WalletNameDialog extends Dialog 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"); diff --git a/src/main/java/com/sparrowwallet/sparrow/event/BwtScanStatusEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/BwtScanStatusEvent.java index ce2cb1fb..0c41adbf 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/BwtScanStatusEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/BwtScanStatusEvent.java @@ -16,6 +16,10 @@ public class BwtScanStatusEvent extends BwtStatusEvent { 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..57f0351f --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/BwtShutdownEvent.java @@ -0,0 +1,8 @@ +package com.sparrowwallet.sparrow.event; + +/** + * Empty class used to notify the bwt has shut down. + */ +public class BwtShutdownEvent { + +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/BwtSyncStatusEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/BwtSyncStatusEvent.java index 9cd16123..e2056c55 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/BwtSyncStatusEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/BwtSyncStatusEvent.java @@ -14,6 +14,10 @@ public class BwtSyncStatusEvent extends BwtStatusEvent { return progress; } + public boolean isCompleted() { + return progress == 100; + } + public int getTip() { return tip; } 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/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 2aa4aa98..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/net/BatchedElectrumServerRpc.java b/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java index ad00fe7b..d24d6151 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); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/Bwt.java b/src/main/java/com/sparrowwallet/sparrow/net/Bwt.java index d2b2b8cb..cf2de53a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/Bwt.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/Bwt.java @@ -5,12 +5,13 @@ 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.daemon.CallbackNotifier; -import dev.bwt.daemon.NativeBwtDaemon; +import dev.bwt.libbwt.daemon.CallbackNotifier; +import dev.bwt.libbwt.daemon.NativeBwtDaemon; import javafx.application.Platform; import javafx.concurrent.Service; import javafx.concurrent.Task; @@ -18,20 +19,18 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.List; +import java.util.*; public class Bwt { private static final Logger log = LoggerFactory.getLogger(Bwt.class); private Long shutdownPtr; + private boolean terminating; 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.dylib"); + NativeUtils.loadLibraryFromJar("/native/osx/x64/libbwt_jni.dylib"); } else if(platform == org.controlsfx.tools.Platform.WINDOWS) { NativeUtils.loadLibraryFromJar("/native/windows/x64/bwt.dll"); } else { @@ -43,9 +42,7 @@ public class Bwt { } private void start(CallbackNotifier callback) { - List descriptors = List.of("pkh(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)"); - Date now = new Date(); - start(descriptors, (int)(now.getTime() / 1000), null, callback); + start(Collections.emptyList(), null, null, null, callback); } private void start(Collection wallets, CallbackNotifier callback) { @@ -57,10 +54,18 @@ public class Bwt { outputDescriptors.add(changeOutputDescriptor.toString(false, false)); } - int rescanSince = wallets.stream().filter(wallet -> wallet.getBirthDate() != null).mapToInt(wallet -> (int)(wallet.getBirthDate().getTime() / 1000)).min().orElse(0); + 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); - start(outputDescriptors, rescanSince, gapLimit, callback); + 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); } /** @@ -72,13 +77,24 @@ public class Bwt { * @param gapLimit desired gap limit beyond last used address * @param callback object receiving notifications */ - private void start(List outputDescriptors, Integer rescanSince, Integer gapLimit, CallbackNotifier callback) { + 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(); - bwtConfig.descriptors = outputDescriptors; - bwtConfig.rescanSince = rescanSince; - bwtConfig.gapLimit = gapLimit; + + 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; @@ -95,6 +111,7 @@ public class Bwt { Gson gson = new Gson(); String jsonConfig = gson.toJson(bwtConfig); + log.debug("Configuring bwt: " + jsonConfig); NativeBwtDaemon.start(jsonConfig, callback); } @@ -102,16 +119,26 @@ public class Bwt { /** * Shut down the BWT daemon * - * @param shutdownPtr the pointer provided on startup */ - private void shutdown(long shutdownPtr) { + private void shutdown() { + if(shutdownPtr == null) { + terminating = true; + return; + } + NativeBwtDaemon.shutdown(shutdownPtr); + this.shutdownPtr = null; + Platform.runLater(() -> EventManager.get().post(new BwtShutdownEvent())); } public boolean isRunning() { return shutdownPtr != null; } + public boolean isTerminating() { + return terminating; + } + public ConnectionService getConnectionService(Collection wallets) { return wallets != null ? new ConnectionService(wallets) : new ConnectionService(); } @@ -146,11 +173,17 @@ public class Bwt { public String xpubs; @SerializedName("rescan_since") - public Integer rescanSince; + 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; @@ -159,6 +192,12 @@ public class Bwt { @SerializedName("electrum_skip_merkle") public Boolean electrumSkipMerkle; + + @SerializedName("require_addresses") + public Boolean requireAddresses; + + @SerializedName("setup_logger") + public Boolean setupLogger; } public final class ConnectionService extends Service { @@ -179,25 +218,37 @@ public class Bwt { CallbackNotifier notifier = new CallbackNotifier() { @Override public void onBooting() { - Platform.runLater(() -> EventManager.get().post(new BwtStatusEvent("Starting bwt"))); + log.debug("Booting bwt"); + if(!terminating) { + Platform.runLater(() -> EventManager.get().post(new BwtStatusEvent("Starting bwt"))); + } } @Override public void onSyncProgress(float progress, int tip) { int percent = (int) (progress * 100.0); - Platform.runLater(() -> EventManager.get().post(new BwtSyncStatusEvent("Syncing (" + percent + "%)", percent, tip))); + log.debug("Syncing " + percent + "%"); + if(!terminating) { + Platform.runLater(() -> EventManager.get().post(new BwtSyncStatusEvent("Syncing" + (percent < 100 ? " (" + percent + "%)" : ""), percent, tip))); + } } @Override public void onScanProgress(float progress, int eta) { int percent = (int) (progress * 100.0); Date date = new Date((long) eta * 1000); - Platform.runLater(() -> EventManager.get().post(new BwtScanStatusEvent("Scanning (" + percent + "%)", percent, date))); + 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) { - Platform.runLater(() -> EventManager.get().post(new BwtElectrumReadyStatusEvent("Electrum server ready", addr))); + log.debug("Electrum ready"); + if(!terminating) { + Platform.runLater(() -> EventManager.get().post(new BwtElectrumReadyStatusEvent("Electrum server ready", addr))); + } } @Override @@ -207,8 +258,14 @@ public class Bwt { @Override public void onReady(long shutdownPtr) { + log.debug("Bwt ready"); Bwt.this.shutdownPtr = shutdownPtr; - Platform.runLater(() -> EventManager.get().post(new BwtReadyStatusEvent("Server ready", shutdownPtr))); + if(terminating) { + Bwt.this.shutdown(); + terminating = false; + } else { + Platform.runLater(() -> EventManager.get().post(new BwtReadyStatusEvent("Server ready", shutdownPtr))); + } } }; @@ -229,12 +286,7 @@ public class Bwt { protected Task createTask() { return new Task<>() { protected Void call() { - if(shutdownPtr == null) { - throw new IllegalStateException("Bwt has not been started"); - } - - Bwt.this.shutdown(shutdownPtr); - shutdownPtr = null; + Bwt.this.shutdown(); return null; } }; diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 4e8aaf12..8f6a48c9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -795,6 +795,18 @@ public class ElectrumServer { ElectrumServer electrumServer = new ElectrumServer(); if(Config.get().getServerType() == ServerType.BITCOIN_CORE) { + if(bwt.isTerminating()) { + try { + bwtStartLock.lock(); + bwtStartCondition.await(); + } catch(InterruptedException e) { + Thread.currentThread().interrupt(); + return null; + } finally { + bwtStartLock.unlock(); + } + } + if(!bwt.isRunning()) { Bwt.ConnectionService bwtConnectionService = bwt.getConnectionService(subscribe ? AppServices.get().getOpenWallets().keySet() : null); bwtConnectionService.setOnFailed(workerStateEvent -> { @@ -817,6 +829,7 @@ public class ElectrumServer { } } catch(InterruptedException e) { Thread.currentThread().interrupt(); + return null; } finally { bwtStartLock.unlock(); } @@ -894,6 +907,14 @@ public class ElectrumServer { } } + public boolean isConnecting() { + return isRunning() && Config.get().getServerType() == ServerType.BITCOIN_CORE && !bwt.isRunning(); + } + + public boolean isConnected() { + return isRunning() && (Config.get().getServerType() != ServerType.BITCOIN_CORE || bwt.isRunning()); + } + @Override public boolean cancel() { try { @@ -907,16 +928,14 @@ public class ElectrumServer { } private void shutdownBwt() { - if(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); - } + Bwt.DisconnectionService disconnectionService = bwt.getDisconnectionService(); + disconnectionService.setOnSucceeded(workerStateEvent -> { + ElectrumServer.bwtElectrumServer = null; + }); + disconnectionService.setOnFailed(workerStateEvent -> { + log.error("Failed to stop BWT", workerStateEvent.getSource().getException()); + }); + Platform.runLater(disconnectionService::start); } @Override @@ -954,6 +973,16 @@ public class ElectrumServer { } } + @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/SimpleElectrumServerRpc.java b/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java index 963a1bb1..639152d3 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()); diff --git a/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java b/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java index aa4416bd..36116f12 100644 --- a/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java +++ b/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java @@ -7,6 +7,7 @@ import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.control.TextFieldValidator; import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch; import com.sparrowwallet.sparrow.event.BwtStatusEvent; +import com.sparrowwallet.sparrow.event.BwtSyncStatusEvent; import com.sparrowwallet.sparrow.event.ConnectionEvent; import com.sparrowwallet.sparrow.event.RequestDisconnectEvent; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; @@ -119,6 +120,8 @@ 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); @@ -343,7 +346,11 @@ public class ServerPreferencesController extends PreferencesDetailController { } private void startElectrumConnection() { - ElectrumServer.ConnectionService connectionService = new ElectrumServer.ConnectionService(false); + 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) -> { @@ -365,11 +372,11 @@ public class ServerPreferencesController extends PreferencesDetailController { } private void setFieldsEditable(boolean editable) { + serverTypeToggleGroup.getToggles().forEach(toggle -> ((ToggleButton)toggle).setDisable(!editable)); + coreHost.setEditable(editable); corePort.setEditable(editable); - for(Toggle toggle : coreAuthToggleGroup.getToggles()) { - ((ToggleButton)toggle).setDisable(!editable); - } + coreAuthToggleGroup.getToggles().forEach(toggle -> ((ToggleButton)toggle).setDisable(!editable)); coreDataDir.setEditable(editable); coreDataDirSelect.setDisable(!editable); coreUser.setEditable(editable); @@ -611,6 +618,17 @@ public class ServerPreferencesController extends PreferencesDetailController { @Subscribe public void bwtStatus(BwtStatusEvent event) { - testResults.appendText("\n" + event.getStatus()); + if(!(event instanceof BwtSyncStatusEvent)) { + testResults.appendText("\n" + event.getStatus()); + } + } + + @Subscribe + public void bwtSyncStatus(BwtSyncStatusEvent event) { + if(connectionService != null && connectionService.isRunning() && event.getProgress() < 100) { + testResults.appendText("\nThe connection to the Bitcoin Core node was successful, but it is still syncing to the the blockchain tip at " + event.getTip() + " blocks (" + event.getProgress() + "% completed)"); + testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.QUESTION_CIRCLE, null)); + connectionService.cancel(); + } } } 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 f58cea56..6beb32ab 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/AdvancedController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/AdvancedController.java @@ -2,6 +2,7 @@ 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; @@ -27,12 +28,14 @@ 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)); } }); 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..906c3d86 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(); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsWalletForm.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsWalletForm.java index 6cb0db88..aeafb55d 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,7 +51,7 @@ 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; } @@ -86,6 +87,10 @@ public class SettingsWalletForm extends WalletForm { return true; } + if(!Objects.equals(original.getBirthDate(), changed.getBirthDate())) { + return true; + } + 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 a6eb9b4c..d46abff1 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionsController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionsController.java @@ -130,12 +130,17 @@ public class TransactionsController extends WalletFormController implements Init @Subscribe public void bwtSyncStatus(BwtSyncStatusEvent event) { - walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), false, event.getStatus())); + walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), true, event.getStatus())); } @Subscribe public void bwtScanStatus(BwtScanStatusEvent event) { - walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), false, event.getStatus())); + walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), true, event.getStatus())); + } + + @Subscribe + public void bwtShutdown(BwtShutdownEvent event) { + walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), false)); } @Subscribe diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/UtxosController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/UtxosController.java index b32bcabd..e76960d8 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/UtxosController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/UtxosController.java @@ -136,12 +136,17 @@ public class UtxosController extends WalletFormController implements Initializab @Subscribe public void bwtSyncStatus(BwtSyncStatusEvent event) { - walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), false, event.getStatus())); + walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), true, event.getStatus())); } @Subscribe public void bwtScanStatus(BwtScanStatusEvent event) { - walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), false, event.getStatus())); + walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), true, event.getStatus())); + } + + @Subscribe + public void bwtShutdown(BwtShutdownEvent event) { + walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), false)); } @Subscribe 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/resources/com/sparrowwallet/sparrow/darktheme.css b/src/main/resources/com/sparrowwallet/sparrow/darktheme.css index 4d487f9f..70b0a847 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/darktheme.css +++ b/src/main/resources/com/sparrowwallet/sparrow/darktheme.css @@ -168,3 +168,7 @@ -fx-border-width: 1px 0px 1px 0px; -fx-border-color: derive(-fx-background, -10%); } + +.root .placeholder .hyperlink { + -fx-text-fill: derive(#1e88cf, 20%); +} diff --git a/src/main/resources/native/osx/x64/libbwt.dylib b/src/main/resources/native/osx/x64/libbwt_jni.dylib old mode 100644 new mode 100755 similarity index 50% rename from src/main/resources/native/osx/x64/libbwt.dylib rename to src/main/resources/native/osx/x64/libbwt_jni.dylib index 49d7ac13..595ebc6f Binary files a/src/main/resources/native/osx/x64/libbwt.dylib and b/src/main/resources/native/osx/x64/libbwt_jni.dylib differ