diff --git a/build.gradle b/build.gradle index d0383d33..e79a349b 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.4') testImplementation('junit:junit:4.12') } diff --git a/drongo b/drongo index 05674097..6ad3f537 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 05674097428d25de043310f8ecddf06d998b3943 +Subproject commit 6ad3f5373119b65d17b857738b8411ee88cea993 diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 32c8f3ec..5d22fe05 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -521,16 +521,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.isOnline() ? "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 +696,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"); @@ -1225,6 +1235,20 @@ public class AppController implements Initializable { } } + @Subscribe + public void bwtSyncStatus(BwtSyncStatusEvent event) { + if(AppServices.isOnline()) { + statusUpdated(new StatusEvent(event.getStatus())); + } + } + + @Subscribe + public void bwtScanStatus(BwtScanStatusEvent event) { + if(AppServices.isOnline()) { + statusUpdated(new StatusEvent(event.getStatus())); + } + } + @Subscribe public void newBlock(NewBlockEvent event) { setServerToggleTooltip(event.getHeight()); diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index 8a317331..73fbc0c4 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -127,7 +127,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(); } @@ -265,6 +265,15 @@ public class AppServices { return application; } + public Map getOpenWallets() { + Map openWallets = new LinkedHashMap<>(); + for(Map walletStorageMap : walletWindows.values()) { + openWallets.putAll(walletStorageMap); + } + + return openWallets; + } + public Map getOpenWallets(Window window) { return walletWindows.get(window); } @@ -365,7 +374,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)); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WalletNameDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WalletNameDialog.java index 708d3082..175032ac 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,66 @@ 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(420); + 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.setEditable(false); + existingPicker.setPrefWidth(130); + existingPicker.managedProperty().bind(existingPicker.visibleProperty()); + existingPicker.setVisible(false); + existingBox.getChildren().add(existingPicker); + + 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 +88,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/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..7598ec43 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/BwtReadyStatusEvent.java @@ -0,0 +1,14 @@ +package com.sparrowwallet.sparrow.event; + +public class BwtReadyStatusEvent extends BwtStatusEvent { + private final long shutdownPtr; + + public BwtReadyStatusEvent(String status, long shutdownPtr) { + super(status); + this.shutdownPtr = shutdownPtr; + } + + public long getShutdownPtr() { + return shutdownPtr; + } +} 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..ce2cb1fb --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/BwtScanStatusEvent.java @@ -0,0 +1,22 @@ +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 Date getEta() { + return eta; + } +} 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..9cd16123 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/BwtSyncStatusEvent.java @@ -0,0 +1,20 @@ +package com.sparrowwallet.sparrow.event; + +public class BwtSyncStatusEvent extends BwtStatusEvent { + private final int progress; + private final int tip; + + public BwtSyncStatusEvent(String status, int progress, int tip) { + super(status); + this.progress = progress; + this.tip = tip; + } + + public int getProgress() { + return progress; + } + + public int getTip() { + return tip; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/WalletHistoryStatusEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/WalletHistoryStatusEvent.java index a40771bb..2aa4aa98 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/WalletHistoryStatusEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/WalletHistoryStatusEvent.java @@ -15,7 +15,7 @@ public class WalletHistoryStatusEvent { this.errorMessage = null; } - public WalletHistoryStatusEvent(Wallet wallet,boolean loaded, String statusMessage) { + public WalletHistoryStatusEvent(Wallet wallet, boolean loaded, String statusMessage) { this.wallet = wallet; this.loaded = false; this.statusMessage = statusMessage; diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Config.java b/src/main/java/com/sparrowwallet/sparrow/io/Config.java index d5eee569..ec539792 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/Bwt.java b/src/main/java/com/sparrowwallet/sparrow/net/Bwt.java new file mode 100644 index 00000000..d2b2b8cb --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/Bwt.java @@ -0,0 +1,243 @@ +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.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 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.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; + +public class Bwt { + private static final Logger log = LoggerFactory.getLogger(Bwt.class); + private Long shutdownPtr; + + 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"); + } else if(platform == org.controlsfx.tools.Platform.WINDOWS) { + NativeUtils.loadLibraryFromJar("/native/windows/x64/bwt.dll"); + } else { + NativeUtils.loadLibraryFromJar("/native/linux/x64/libbwt.so"); + } + } catch(IOException e) { + log.error("Error loading bwt library", e); + } + } + + private void start(CallbackNotifier callback) { + List descriptors = List.of("pkh(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)"); + Date now = new Date(); + start(descriptors, (int)(now.getTime() / 1000), 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(0); + int gapLimit = wallets.stream().filter(wallet -> wallet.getGapLimit() > 0).mapToInt(Wallet::getGapLimit).max().orElse(Wallet.DEFAULT_LOOKAHEAD); + + start(outputDescriptors, rescanSince, 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, 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; + bwtConfig.verbose = log.isDebugEnabled() ? 2 : 0; + 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); + + NativeBwtDaemon.start(jsonConfig, callback); + } + + /** + * Shut down the BWT daemon + * + * @param shutdownPtr the pointer provided on startup + */ + private void shutdown(long shutdownPtr) { + NativeBwtDaemon.shutdown(shutdownPtr); + } + + public boolean isRunning() { + return shutdownPtr != null; + } + + 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 Integer rescanSince; + + @SerializedName("gap_limit") + public Integer gapLimit; + + @SerializedName("verbose") + public Integer verbose; + + @SerializedName("electrum_addr") + public String electrumAddr; + + @SerializedName("electrum_skip_merkle") + public Boolean electrumSkipMerkle; + } + + 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() { + 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))); + } + + @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))); + } + + @Override + public void onElectrumReady(String addr) { + 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(long shutdownPtr) { + Bwt.this.shutdownPtr = shutdownPtr; + Platform.runLater(() -> EventManager.get().post(new BwtReadyStatusEvent("Server ready", shutdownPtr))); + } + }; + + 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() { + if(shutdownPtr == null) { + throw new IllegalStateException("Bwt has not been started"); + } + + Bwt.this.shutdown(shutdownPtr); + shutdownPtr = null; + 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..4e8aaf12 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -9,11 +9,11 @@ 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.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 +26,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,12 +43,25 @@ 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 ServerException("BWT server not started"); + } + 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"); @@ -760,7 +775,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 +793,36 @@ 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(); + } finally { + bwtStartLock.unlock(); + } + } + } + if(firstCall) { electrumServer.connect(); @@ -839,6 +887,7 @@ public class ElectrumServer { public void resetConnection() { try { closeActiveConnection(); + shutdownBwt(); firstCall = true; } catch (ServerException e) { log.error("Error closing connection during connection reset", e); @@ -849,6 +898,7 @@ public class ElectrumServer { public boolean cancel() { try { closeActiveConnection(); + shutdownBwt(); } catch (ServerException e) { log.error("Error closing connection", e); } @@ -856,6 +906,19 @@ public class ElectrumServer { return super.cancel(); } + 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); + } + } + @Override public void reset() { super.reset(); @@ -872,6 +935,25 @@ 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(); + } + } + } + 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/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/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..83f513f3 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,8 @@ public class PreferencesController implements Initializable { @FXML private StackPane preferencesPane; + private final BooleanProperty closing = new SimpleBooleanProperty(false); + @Override public void initialize(URL location, ResourceBundle resources) { @@ -56,6 +60,10 @@ public class PreferencesController implements Initializable { } } + BooleanProperty closingProperty() { + return closing; + } + 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..c59d6c7a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/preferences/PreferencesDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/preferences/PreferencesDialog.java @@ -49,10 +49,11 @@ public class PreferencesDialog extends Dialog { } dialogPane.setPrefWidth(650); - dialogPane.setPrefHeight(550); + dialogPane.setPrefHeight(600); existingConnection = ElectrumServer.isConnected(); setOnCloseRequest(event -> { + preferencesController.closingProperty().set(true); if(existingConnection && !ElectrumServer.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..aa4416bd 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.EventManager; import com.sparrowwallet.sparrow.control.TextFieldValidator; import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch; +import com.sparrowwallet.sparrow.event.BwtStatusEvent; import com.sparrowwallet.sparrow.event.ConnectionEvent; import com.sparrowwallet.sparrow.event.RequestDisconnectEvent; 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,6 +30,8 @@ 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; @@ -41,19 +43,61 @@ 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; @@ -77,33 +121,100 @@ public class ServerPreferencesController extends PreferencesDetailController { @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 serverType = (ServerType)newValue.getUserData(); + electrumForm.setVisible(serverType == ServerType.ELECTRUM_SERVER); + config.setServerType(serverType); + testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.QUESTION_CIRCLE, "")); + testResults.clear(); + } 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 +226,7 @@ public class ServerPreferencesController extends PreferencesDetailController { File file = fileChooser.showOpenDialog(window); if(file != null) { - certificate.setText(file.getAbsolutePath()); + electrumCertificate.setText(file.getAbsolutePath()); } }); @@ -127,10 +238,10 @@ 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); } }); @@ -139,29 +250,11 @@ public class ServerPreferencesController extends PreferencesDetailController { 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()); @@ -173,27 +266,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 +328,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,12 +342,46 @@ public class ServerPreferencesController extends PreferencesDetailController { } } + private void startElectrumConnection() { + ElectrumServer.ConnectionService 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()); + 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); + coreHost.setEditable(editable); + corePort.setEditable(editable); + for(Toggle toggle : coreAuthToggleGroup.getToggles()) { + ((ToggleButton)toggle).setDisable(!editable); + } + coreDataDir.setEditable(editable); + coreDataDirSelect.setDisable(!editable); + coreUser.setEditable(editable); + corePass.setEditable(editable); + coreMultiWallet.setDisable(!editable); + coreWallet.setEditable(editable); + + electrumHost.setEditable(editable); + electrumPort.setEditable(editable); + electrumUseSsl.setDisable(!editable); + electrumCertificate.setEditable(editable); + electrumCertificateSelect.setDisable(!editable); useProxy.setDisable(!editable); proxyHost.setEditable(editable); proxyPort.setEditable(editable); @@ -252,12 +413,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 +454,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 +500,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 +527,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 +546,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 +586,31 @@ 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) { + testResults.appendText("\n" + event.getStatus()); + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/AdvancedController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/AdvancedController.java index d27c0e6a..f58cea56 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/AdvancedController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/AdvancedController.java @@ -5,13 +5,19 @@ import com.sparrowwallet.sparrow.EventManager; 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 +27,15 @@ public class AdvancedController implements Initializable { } public void initializeView(Wallet wallet) { + 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())); + } + }); + 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/TransactionsController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionsController.java index b418b9e6..a6eb9b4c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionsController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionsController.java @@ -128,6 +128,16 @@ public class TransactionsController extends WalletFormController implements Init transactionsTable.updateHistoryStatus(event); } + @Subscribe + public void bwtSyncStatus(BwtSyncStatusEvent event) { + walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), false, event.getStatus())); + } + + @Subscribe + public void bwtScanStatus(BwtScanStatusEvent event) { + walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), false, event.getStatus())); + } + @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..b32bcabd 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/UtxosController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/UtxosController.java @@ -133,6 +133,17 @@ public class UtxosController extends WalletFormController implements Initializab utxosTable.updateHistoryStatus(event); } + + @Subscribe + public void bwtSyncStatus(BwtSyncStatusEvent event) { + walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), false, event.getStatus())); + } + + @Subscribe + public void bwtScanStatus(BwtScanStatusEvent event) { + walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), false, event.getStatus())); + } + @Subscribe public void walletUtxoStatusChanged(WalletUtxoStatusChangedEvent event) { if(event.getWallet().equals(getWalletForm().getWallet())) { 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/general.css b/src/main/resources/com/sparrowwallet/sparrow/general.css index f3cbd059..8a10fdfa 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/general.css +++ b/src/main/resources/com/sparrowwallet/sparrow/general.css @@ -40,6 +40,10 @@ -fx-padding: -20 0 0 0; } +.form .field .toggle-switch { + -fx-padding: 5 0 2 0; +} + .tab-error > .tab-container { -fx-effect: dropshadow(three-pass-box, rgba(202, 18, 67, .6), 7, 0, 0, 0); } diff --git a/src/main/resources/com/sparrowwallet/sparrow/preferences/server.fxml b/src/main/resources/com/sparrowwallet/sparrow/preferences/server.fxml index 25d3e6b2..8ddcf24b 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/preferences/server.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/preferences/server.fxml @@ -12,6 +12,10 @@ + + + + @@ -24,25 +28,92 @@
-
+
+ + + + + + + + + + + + + + + + + + + +
+ + +
+
- - + + - - + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + +
+
-
+
+
+ + + + + + + + + + + @@ -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/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.so b/src/main/resources/native/linux/x64/libbwt.so new file mode 100755 index 00000000..25be5988 Binary files /dev/null and b/src/main/resources/native/linux/x64/libbwt.so differ diff --git a/src/main/resources/native/osx/x64/libbwt.dylib b/src/main/resources/native/osx/x64/libbwt.dylib new file mode 100644 index 00000000..49d7ac13 Binary files /dev/null and b/src/main/resources/native/osx/x64/libbwt.dylib differ diff --git a/src/main/resources/native/windows/x64/bwt.dll b/src/main/resources/native/windows/x64/bwt.dll new file mode 100755 index 00000000..6f52ce7c Binary files /dev/null and b/src/main/resources/native/windows/x64/bwt.dll differ