initial commit of bwt integration

This commit is contained in:
Craig Raw 2020-12-20 12:34:36 +02:00
parent 18bc7bf302
commit a1c65cff75
34 changed files with 1230 additions and 115 deletions

View file

@ -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')
}

2
drongo

@ -1 +1 @@
Subproject commit 05674097428d25de043310f8ecddf06d998b3943
Subproject commit 6ad3f5373119b65d17b857738b8411ee88cea993

View file

@ -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<String> walletName = dlg.showAndWait();
if(walletName.isPresent()) {
File walletFile = Storage.getWalletFile(walletName.get());
Optional<WalletNameDialog.NameAndBirthDate> 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<WalletNameDialog.NameAndBirthDate> 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());

View file

@ -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<Wallet, Storage> getOpenWallets() {
Map<Wallet, Storage> openWallets = new LinkedHashMap<>();
for(Map<Wallet, Storage> walletStorageMap : walletWindows.values()) {
openWallets.putAll(walletStorageMap);
}
return openWallets;
}
public Map<Wallet, Storage> 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));
}

View file

@ -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<String> {
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
public class WalletNameDialog extends Dialog<WalletNameDialog.NameAndBirthDate> {
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<String> {
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;
}
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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<File> 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;
}

View file

@ -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<String> descriptors = List.of("pkh(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)");
Date now = new Date();
start(descriptors, (int)(now.getTime() / 1000), null, callback);
}
private void start(Collection<Wallet> wallets, CallbackNotifier callback) {
List<String> 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<String> 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<Wallet> 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<String> 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<Void> {
private final Collection<Wallet> wallets;
public ConnectionService() {
this.wallets = null;
}
public ConnectionService(Collection<Wallet> wallets) {
this.wallets = wallets;
}
@Override
protected Task<Void> 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<Void> {
@Override
protected Task<Void> 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;
}
};
}
}
}

View file

@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow.net;
public enum CoreAuthType {
COOKIE, USERPASS;
}

View file

@ -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;
}

View file

@ -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<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
return Collections.emptyMap();

View file

@ -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 <a href="http://adamheinrich.com/blog/2012/how-to-load-native-jni-library-from-jar">http://adamheinrich.com/blog/2012/how-to-load-native-jni-library-from-jar</a>
* @see <a href="https://github.com/adamheinrich/native-utils">https://github.com/adamheinrich/native-utils</a>
*
*/
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;
}
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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());

View file

@ -49,10 +49,11 @@ public class PreferencesDialog extends Dialog<Boolean> {
}
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());
}

View file

@ -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,12 +250,101 @@ 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));
testResults.setText("Connecting to " + config.getServerAddress() + "...");
startElectrumConnection();
});
editConnection.managedProperty().bind(editConnection.visibleProperty());
editConnection.setVisible(isConnected);
editConnection.setOnAction(event -> {
EventManager.get().post(new RequestDisconnectEvent());
setFieldsEditable(true);
editConnection.setVisible(false);
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);
electrumUseSsl.setSelected(ssl);
electrumCertificate.setDisable(!ssl);
electrumCertificateSelect.setDisable(!ssl);
HostAndPort server = protocol.getServerHostAndPort(electrumServer);
electrumHost.setText(server.getHost());
if(server.hasPort()) {
electrumPort.setText(Integer.toString(server.getPort()));
}
}
}
File certificateFile = config.getElectrumServerCert();
if(certificateFile != null) {
electrumCertificate.setText(certificateFile.getAbsolutePath());
}
useProxy.setSelected(config.isUseProxy());
proxyHost.setDisable(!config.isUseProxy());
proxyPort.setDisable(!config.isUseProxy());
if(config.isUseProxy()) {
electrumUseSsl.setSelected(true);
electrumUseSsl.setDisable(true);
}
String proxyServer = config.getProxyServer();
if(proxyServer != null) {
HostAndPort server = HostAndPort.fromString(proxyServer);
proxyHost.setText(server.getHost());
if(server.hasPort()) {
proxyPort.setText(Integer.toString(server.getPort()));
}
}
}
private void startElectrumConnection() {
ElectrumServer.ConnectionService connectionService = new ElectrumServer.ConnectionService(false);
connectionService.setPeriod(Duration.ZERO);
connectionService.setPeriod(Duration.hours(1));
EventManager.get().register(connectionService);
connectionService.statusProperty().addListener((observable, oldValue, newValue) -> {
testResults.setText(testResults.getText() + "\n" + newValue);
@ -162,65 +362,26 @@ public class ServerPreferencesController extends PreferencesDetailController {
connectionService.cancel();
});
connectionService.start();
});
editConnection.managedProperty().bind(editConnection.visibleProperty());
editConnection.setVisible(isConnected);
editConnection.setOnAction(event -> {
EventManager.get().post(new RequestDisconnectEvent());
setFieldsEditable(true);
editConnection.setVisible(false);
testConnection.setVisible(true);
});
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);
HostAndPort server = protocol.getServerHostAndPort(electrumServer);
host.setText(server.getHost());
if(server.hasPort()) {
port.setText(Integer.toString(server.getPort()));
}
}
}
File certificateFile = config.getElectrumServerCert();
if(certificateFile != null) {
certificate.setText(certificateFile.getAbsolutePath());
}
useProxy.setSelected(config.isUseProxy());
proxyHost.setDisable(!config.isUseProxy());
proxyPort.setDisable(!config.isUseProxy());
if(config.isUseProxy()) {
useSsl.setSelected(true);
useSsl.setDisable(true);
}
String proxyServer = config.getProxyServer();
if(proxyServer != null) {
HostAndPort server = HostAndPort.fromString(proxyServer);
proxyHost.setText(server.getHost());
if(server.hasPort()) {
proxyPort.setText(Integer.toString(server.getPort()));
}
}
}
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<String> 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<String> getBitcoinAuthListener(Config config) {
return (observable, oldValue, newValue) -> {
config.setCoreAuth(coreUser.getText() + ":" + corePass.getText());
};
}
@NotNull
private ChangeListener<String> getBitcoinWalletListener(Config config) {
return (observable, oldValue, newValue) -> {
config.setCoreWallet(coreWallet.getText());
};
}
@NotNull
private ChangeListener<String> 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());
}
}

View file

@ -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<Integer> 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);

View file

@ -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())) {

View file

@ -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())) {

View file

@ -25,4 +25,5 @@ open module com.sparrowwallet.sparrow {
requires centerdevice.nsmenufx;
requires jcommander;
requires slf4j.api;
requires bwt.jni;
}

View file

@ -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);
}

View file

@ -12,6 +12,10 @@
<?import tornadofx.control.Field?>
<?import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch?>
<?import org.controlsfx.glyphfont.Glyph?>
<?import org.controlsfx.control.SegmentedButton?>
<?import com.sparrowwallet.sparrow.net.ServerType?>
<?import com.sparrowwallet.sparrow.net.CoreAuthType?>
<?import com.sparrowwallet.sparrow.control.HelpLabel?>
<GridPane hgap="10.0" vgap="10.0" stylesheets="@preferences.css, @../general.css" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.preferences.ServerPreferencesController">
<padding>
<Insets left="25.0" right="25.0" top="25.0" />
@ -24,25 +28,92 @@
</rowConstraints>
<Form GridPane.columnIndex="0" GridPane.rowIndex="0">
<Fieldset inputGrow="SOMETIMES" text="Electrum Server">
<Fieldset inputGrow="SOMETIMES" text="Server">
<Field text="Type:">
<SegmentedButton>
<toggleGroup>
<ToggleGroup fx:id="serverTypeToggleGroup" />
</toggleGroup>
<buttons>
<ToggleButton text="Bitcoin Core" toggleGroup="$serverTypeToggleGroup">
<userData>
<ServerType fx:constant="BITCOIN_CORE"/>
</userData>
</ToggleButton>
<ToggleButton text="Electrum Server" toggleGroup="$serverTypeToggleGroup">
<userData>
<ServerType fx:constant="ELECTRUM_SERVER"/>
</userData>
</ToggleButton>
</buttons>
</SegmentedButton>
</Field>
</Fieldset>
</Form>
<Form fx:id="coreForm" GridPane.columnIndex="0" GridPane.rowIndex="1">
<Fieldset inputGrow="SOMETIMES" text="Bitcoin Core RPC">
<Field text="URL:">
<TextField fx:id="host" promptText="e.g. 127.0.0.1"/>
<TextField fx:id="port" promptText="e.g. 50002" prefWidth="80" />
<TextField fx:id="coreHost" promptText="e.g. 127.0.0.1"/>
<TextField fx:id="corePort" promptText="e.g. 8332" prefWidth="80" />
</Field>
<Field text="Use SSL:">
<UnlabeledToggleSwitch fx:id="useSsl"/>
<Field text="Authentication:">
<SegmentedButton>
<toggleGroup>
<ToggleGroup fx:id="coreAuthToggleGroup" />
</toggleGroup>
<buttons>
<ToggleButton text="Default" toggleGroup="$coreAuthToggleGroup">
<userData>
<CoreAuthType fx:constant="COOKIE"/>
</userData>
</ToggleButton>
<ToggleButton text="User / Pass" toggleGroup="$coreAuthToggleGroup">
<userData>
<CoreAuthType fx:constant="USERPASS"/>
</userData>
</ToggleButton>
</buttons>
</SegmentedButton>
</Field>
<Field text="Certificate:" styleClass="label-button">
<TextField fx:id="certificate" promptText="Optional server certificate (.crt)"/>
<Button fx:id="certificateSelect" maxWidth="25" minWidth="-Infinity" prefWidth="30" text="Ed">
<Field fx:id="coreDataDirField" text="Data Folder:" styleClass="label-button">
<TextField fx:id="coreDataDir"/>
<Button fx:id="coreDataDirSelect" maxWidth="35" minWidth="-Infinity" prefWidth="35">
<graphic>
<Glyph fontFamily="FontAwesome" icon="EDIT" prefWidth="15" />
<Glyph fontFamily="FontAwesome" icon="EDIT" fontSize="13" />
</graphic>
</Button>
</Field>
<Field fx:id="coreUserPassField" text="User / Pass:" styleClass="label-button">
<TextField fx:id="coreUser"/>
<PasswordField fx:id="corePass"/>
</Field>
<Field text="Multi-Wallet:">
<UnlabeledToggleSwitch fx:id="coreMultiWallet"/> <HelpLabel helpText="Enable this if using multiple Bitcoin Core wallets" />
</Field>
<Field text="Wallet Name:" styleClass="label-button">
<TextField fx:id="coreWallet"/>
</Field>
</Fieldset>
</Form>
<Fieldset inputGrow="SOMETIMES" text="Proxy">
<Form fx:id="electrumForm" GridPane.columnIndex="0" GridPane.rowIndex="1">
<Fieldset inputGrow="SOMETIMES" text="Electrum Server">
<Field text="URL:">
<TextField fx:id="electrumHost" promptText="e.g. 127.0.0.1"/>
<TextField fx:id="electrumPort" promptText="e.g. 50002" prefWidth="80" />
</Field>
<Field text="Use SSL:">
<UnlabeledToggleSwitch fx:id="electrumUseSsl"/>
</Field>
<Field text="Certificate:" styleClass="label-button">
<TextField fx:id="electrumCertificate" promptText="Optional server certificate (.crt)"/>
<Button fx:id="electrumCertificateSelect" maxWidth="35" minWidth="-Infinity" prefWidth="35">
<graphic>
<Glyph fontFamily="FontAwesome" icon="EDIT" fontSize="13" />
</graphic>
</Button>
</Field>
<Field text="Use Proxy:">
<UnlabeledToggleSwitch fx:id="useProxy"/>
</Field>
@ -53,7 +124,7 @@
</Fieldset>
</Form>
<StackPane GridPane.columnIndex="0" GridPane.rowIndex="1">
<StackPane GridPane.columnIndex="0" GridPane.rowIndex="2">
<Button fx:id="testConnection" graphicTextGap="5" text="Test Connection">
<graphic>
<Glyph fontFamily="FontAwesome" icon="QUESTION_CIRCLE" prefWidth="13" />
@ -66,7 +137,7 @@
</Button>
</StackPane>
<StackPane GridPane.columnIndex="0" GridPane.rowIndex="2">
<StackPane GridPane.columnIndex="0" GridPane.rowIndex="3">
<padding>
<Insets top="10.0" bottom="20.0"/>
</padding>

View file

@ -25,7 +25,11 @@
</rowConstraints>
<Form GridPane.columnIndex="0" GridPane.rowIndex="0">
<Fieldset inputGrow="SOMETIMES" text="Advanced Settings" styleClass="wideLabelFieldSet">
<Fieldset inputGrow="SOMETIMES" text="Advanced Settings">
<Field text="Birth date:">
<DatePicker editable="false" fx:id="birthDate" prefWidth="140" />
<HelpLabel helpText="The date of the earliest transaction (used to avoid scanning the entire blockchain)."/>
</Field>
<Field text="Gap limit:">
<Spinner fx:id="gapLimit" editable="true" prefWidth="90" />
<HelpLabel helpText="Change how far ahead to look for additional transactions beyond the highest derivation with previous transaction outputs."/>

View file

@ -22,7 +22,7 @@
</encoder>
</appender>
<root level="info">
<root level="debug">
<appender-ref ref="FILE" />
<appender-ref ref="STDOUT" />
</root>

Binary file not shown.

Binary file not shown.

Binary file not shown.