bwt threading and wallet rescanning

This commit is contained in:
Craig Raw 2021-01-07 13:04:42 +02:00
parent a1c65cff75
commit 3c4b25ecee
35 changed files with 536 additions and 142 deletions

View file

@ -69,7 +69,7 @@ dependencies {
exclude group: 'org.openjfx', module: 'javafx-web'
exclude group: 'org.openjfx', module: 'javafx-media'
}
implementation('dev.bwt:bwt-jni:0.1.4')
implementation('dev.bwt:bwt-jni:0.1.5')
testImplementation('junit:junit:4.12')
}

2
drongo

@ -1 +1 @@
Subproject commit 6ad3f5373119b65d17b857738b8411ee88cea993
Subproject commit 4da2c024d4fbe3ecbca9772a0761def6d5269c38

View file

@ -110,6 +110,8 @@ public class AppController implements Initializable {
@FXML
private UnlabeledToggleSwitch serverToggle;
private PauseTransition wait;
private Timeline statusTimeline;
@Override
@ -170,7 +172,7 @@ public class AppController implements Initializable {
boolean walletAdded = c.getAddedSubList().stream().anyMatch(tab -> ((TabData)tab.getUserData()).getType() == TabData.TabType.WALLET);
boolean walletRemoved = c.getRemoved().stream().anyMatch(tab -> ((TabData)tab.getUserData()).getType() == TabData.TabType.WALLET);
if(walletAdded || walletRemoved) {
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), getOpenWallets()));
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), getOpenWalletTabData()));
}
List<WalletTabData> closedWalletTabs = c.getRemoved().stream().map(tab -> (TabData)tab.getUserData())
@ -194,7 +196,7 @@ public class AppController implements Initializable {
tabs.getScene().getWindow().setOnCloseRequest(event -> {
EventManager.get().unregister(this);
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), Collections.emptyMap()));
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), Collections.emptyList()));
});
BitcoinUnit unit = Config.get().getBitcoinUnit();
@ -221,7 +223,7 @@ public class AppController implements Initializable {
showTxHex.setSelected(Config.get().isShowTransactionHex());
exportWallet.setDisable(true);
serverToggle.setSelected(isOnline());
serverToggle.setSelected(isConnected());
onlineProperty().bindBidirectional(serverToggle.selectedProperty());
onlineProperty().addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> setServerToggleTooltip(getCurrentBlockHeight()));
@ -451,21 +453,30 @@ public class AppController implements Initializable {
}
}
public Map<Wallet, Storage> getOpenWallets() {
Map<Wallet, Storage> openWallets = new LinkedHashMap<>();
public List<WalletTabData> getOpenWalletTabData() {
List<WalletTabData> openWalletTabData = new ArrayList<>();
for(Tab tab : tabs.getTabs()) {
TabData tabData = (TabData)tab.getUserData();
if(tabData.getType() == TabData.TabType.WALLET) {
WalletTabData walletTabData = (WalletTabData) tabData;
openWallets.put(walletTabData.getWallet(), walletTabData.getStorage());
openWalletTabData.add((WalletTabData)tabData);
}
}
return openWalletTabData;
}
public Map<Wallet, Storage> getOpenWallets() {
Map<Wallet, Storage> openWallets = new LinkedHashMap<>();
for(WalletTabData walletTabData : getOpenWalletTabData()){
openWallets.put(walletTabData.getWallet(), walletTabData.getStorage());
}
return openWallets;
}
public void selectTab(Wallet wallet) {
for(Tab tab : tabs.getTabs()) {
TabData tabData = (TabData) tab.getUserData();
@ -521,7 +532,7 @@ public class AppController implements Initializable {
}
private void setServerToggleTooltip(Integer currentBlockHeight) {
serverToggle.setTooltip(new Tooltip(AppServices.isOnline() ? "Connected to " + Config.get().getServerAddress() + (currentBlockHeight != null ? " at height " + currentBlockHeight : "") : "Disconnected"));
serverToggle.setTooltip(new Tooltip(AppServices.isConnected() ? "Connected to " + Config.get().getServerAddress() + (currentBlockHeight != null ? " at height " + currentBlockHeight : "") : "Disconnected"));
}
public void newWallet(ActionEvent event) {
@ -845,6 +856,8 @@ public class AppController implements Initializable {
tab.setContent(walletLoader.load());
WalletController controller = walletLoader.getController();
EventManager.get().post(new WalletOpeningEvent(storage, wallet));
//Note that only one WalletForm is created per wallet tab, and registered to listen for events. All wallet controllers (except SettingsController) share this instance.
WalletForm walletForm = new WalletForm(storage, wallet);
EventManager.get().register(walletForm);
@ -1162,7 +1175,10 @@ public class AppController implements Initializable {
public void statusUpdated(StatusEvent event) {
statusBar.setText(event.getStatus());
PauseTransition wait = new PauseTransition(Duration.seconds(20));
if(wait != null && wait.getStatus() == Animation.Status.RUNNING) {
wait.stop();
}
wait = new PauseTransition(Duration.seconds(20));
wait.setOnFinished((e) -> {
if(statusBar.getText().equals(event.getStatus())) {
statusBar.setText("");
@ -1237,14 +1253,14 @@ public class AppController implements Initializable {
@Subscribe
public void bwtSyncStatus(BwtSyncStatusEvent event) {
if(AppServices.isOnline()) {
if((AppServices.isConnecting() || AppServices.isConnected()) && !event.isCompleted()) {
statusUpdated(new StatusEvent(event.getStatus()));
}
}
@Subscribe
public void bwtScanStatus(BwtScanStatusEvent event) {
if(AppServices.isOnline()) {
if((AppServices.isConnecting() || AppServices.isConnected()) && !event.isCompleted()) {
statusUpdated(new StatusEvent(event.getStatus()));
}
}
@ -1302,7 +1318,7 @@ public class AppController implements Initializable {
@Subscribe
public void requestOpenWallets(RequestOpenWalletsEvent event) {
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), getOpenWallets()));
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), getOpenWalletTabData()));
}
@Subscribe

View file

@ -12,10 +12,7 @@ import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Device;
import com.sparrowwallet.sparrow.io.Hwi;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.net.ExchangeSource;
import com.sparrowwallet.sparrow.net.MempoolRateSize;
import com.sparrowwallet.sparrow.net.VersionCheckService;
import com.sparrowwallet.sparrow.net.*;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
@ -57,7 +54,7 @@ public class AppServices {
private final MainApp application;
private final Map<Window, Map<Wallet, Storage>> walletWindows = new LinkedHashMap<>();
private final Map<Window, List<WalletTabData>> walletWindows = new LinkedHashMap<>();
private static final BooleanProperty onlineProperty = new SimpleBooleanProperty(false);
@ -146,6 +143,20 @@ public class AppServices {
onlineProperty.addListener(onlineServicesListener);
}
public void stop() {
if(connectionService != null) {
connectionService.cancel();
}
if(ratesService != null) {
ratesService.cancel();
}
if(versionCheckService != null) {
versionCheckService.cancel();
}
}
private ElectrumServer.ConnectionService createConnectionService() {
ElectrumServer.ConnectionService connectionService = new ElectrumServer.ConnectionService();
connectionService.setPeriod(new Duration(SERVER_PING_PERIOD));
@ -267,24 +278,22 @@ public class AppServices {
public Map<Wallet, Storage> getOpenWallets() {
Map<Wallet, Storage> openWallets = new LinkedHashMap<>();
for(Map<Wallet, Storage> walletStorageMap : walletWindows.values()) {
openWallets.putAll(walletStorageMap);
for(List<WalletTabData> walletTabDataList : walletWindows.values()) {
for(WalletTabData walletTabData : walletTabDataList) {
openWallets.put(walletTabData.getWallet(), walletTabData.getStorage());
}
}
return openWallets;
}
public Map<Wallet, Storage> getOpenWallets(Window window) {
return walletWindows.get(window);
}
public Window getWindowForWallet(Storage storage) {
Optional<Window> optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().values().stream().anyMatch(storage1 -> storage1.getWalletFile().equals(storage.getWalletFile()))).map(Map.Entry::getKey).findFirst();
Optional<Window> optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().stream().anyMatch(walletTabData -> walletTabData.getStorage().getWalletFile().equals(storage.getWalletFile()))).map(Map.Entry::getKey).findFirst();
return optWindow.orElse(null);
}
public Window getWindowForPSBT(PSBT psbt) {
Optional<Window> optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().keySet().stream().anyMatch(wallet -> wallet.canSign(psbt))).map(Map.Entry::getKey).findFirst();
Optional<Window> optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().stream().anyMatch(walletTabData -> walletTabData.getWallet().canSign(psbt))).map(Map.Entry::getKey).findFirst();
return optWindow.orElse(null);
}
@ -292,8 +301,12 @@ public class AppServices {
return walletWindows.keySet().stream().mapToDouble(Window::getX).max().orElse(0d);
}
public static boolean isOnline() {
return onlineProperty.get();
public static boolean isConnecting() {
return onlineProperty.get() && get().connectionService.isConnecting();
}
public static boolean isConnected() {
return onlineProperty.get() && get().connectionService.isConnected();
}
public static BooleanProperty onlineProperty() {
@ -440,25 +453,25 @@ public class AppServices {
@Subscribe
public void openWallets(OpenWalletsEvent event) {
if(event.getWalletsMap().isEmpty()) {
if(event.getWalletTabDataList().isEmpty()) {
walletWindows.remove(event.getWindow());
} else {
walletWindows.put(event.getWindow(), event.getWalletsMap());
walletWindows.put(event.getWindow(), event.getWalletTabDataList());
}
List<Map.Entry<Wallet, Storage>> allWallets = walletWindows.values().stream().flatMap(map -> map.entrySet().stream()).collect(Collectors.toList());
List<WalletTabData> allWallets = walletWindows.values().stream().flatMap(Collection::stream).collect(Collectors.toList());
Platform.runLater(() -> {
if(!Window.getWindows().isEmpty()) {
List<File> walletFiles = allWallets.stream().map(entry -> entry.getValue().getWalletFile()).collect(Collectors.toList());
List<File> walletFiles = allWallets.stream().map(walletTabData -> walletTabData.getStorage().getWalletFile()).collect(Collectors.toList());
Config.get().setRecentWalletFiles(walletFiles);
}
});
boolean usbWallet = false;
for(Map.Entry<Wallet, Storage> entry : allWallets) {
Wallet wallet = entry.getKey();
Storage storage = entry.getValue();
for(WalletTabData walletTabData : allWallets) {
Wallet wallet = walletTabData.getWallet();
Storage storage = walletTabData.getStorage();
if(!storage.getWalletFile().exists() || wallet.containsSource(KeystoreSource.HW_USB)) {
usbWallet = true;
@ -494,4 +507,28 @@ public class AppServices {
public void requestDisconnect(RequestDisconnectEvent event) {
onlineProperty.set(false);
}
@Subscribe
public void walletSettingsChanged(WalletSettingsChangedEvent event) {
restartBwt(event.getWallet());
}
@Subscribe
public void walletOpening(WalletOpeningEvent event) {
restartBwt(event.getWallet());
}
private void restartBwt(Wallet wallet) {
if(Config.get().getServerType() == ServerType.BITCOIN_CORE && isConnected() && wallet.isValid()) {
connectionService.cancel();
}
}
@Subscribe
public void bwtShutdown(BwtShutdownEvent event) {
if(onlineProperty().get() && !connectionService.isRunning()) {
connectionService.reset();
connectionService.start();
}
}
}

View file

@ -96,6 +96,7 @@ public class MainApp extends Application {
@Override
public void stop() throws Exception {
AppServices.get().stop();
mainStage.close();
}

View file

@ -2,12 +2,27 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletDataChangedEvent;
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
import com.sparrowwallet.sparrow.event.WalletSettingsChangedEvent;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.ServerType;
import com.sparrowwallet.sparrow.wallet.Entry;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.TreeTableView;
import javafx.scene.layout.StackPane;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Optional;
public class CoinTreeTable extends TreeTableView<Entry> {
private BitcoinUnit bitcoinUnit;
@ -51,10 +66,46 @@ public class CoinTreeTable extends TreeTableView<Entry> {
setPlaceholder(new Label("Loading transactions..."));
}
} else {
setPlaceholder(new Label("No transactions"));
setPlaceholder(getDefaultPlaceholder(event.getWallet()));
}
});
}
}
}
protected Node getDefaultPlaceholder(Wallet wallet) {
StackPane stackPane = new StackPane();
stackPane.getChildren().add(AppServices.isConnecting() ? new Label("Loading transactions...") : new Label("No transactions"));
if(Config.get().getServerType() == ServerType.BITCOIN_CORE && !AppServices.isConnecting()) {
Hyperlink hyperlink = new Hyperlink();
hyperlink.setTranslateY(30);
hyperlink.setOnAction(event -> {
WalletBirthDateDialog dlg = new WalletBirthDateDialog(wallet.getBirthDate());
Optional<Date> optDate = dlg.showAndWait();
if(optDate.isPresent()) {
wallet.setBirthDate(optDate.get());
Storage storage = AppServices.get().getOpenWallets().get(wallet);
if(storage != null) {
//Trigger background save of birthdate
EventManager.get().post(new WalletDataChangedEvent(wallet));
//Trigger full wallet rescan
wallet.clearHistory();
EventManager.get().post(new WalletSettingsChangedEvent(wallet, storage.getWalletFile()));
}
}
});
if(wallet.getBirthDate() == null) {
hyperlink.setText("Scan for previous transactions?");
} else {
DateFormat dateFormat = new SimpleDateFormat(DateStringConverter.FORMAT_PATTERN);
hyperlink.setText("Scan for transactions earlier than " + dateFormat.format(wallet.getBirthDate()) + "?");
}
stackPane.getChildren().add(hyperlink);
}
stackPane.setAlignment(Pos.CENTER);
return stackPane;
}
}

View file

@ -0,0 +1,29 @@
package com.sparrowwallet.sparrow.control;
import javafx.util.StringConverter;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class DateStringConverter extends StringConverter<LocalDate> {
public static final String FORMAT_PATTERN = "yyyy/MM/dd";
public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(FORMAT_PATTERN);
@Override
public String toString(LocalDate date) {
if (date != null) {
return DATE_FORMATTER.format(date);
} else {
return "";
}
}
@Override
public LocalDate fromString(String string) {
if (string != null && !string.isEmpty()) {
return LocalDate.parse(string, DATE_FORMATTER);
} else {
return null;
}
}
}

View file

@ -5,7 +5,6 @@ import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.scene.control.Label;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
@ -51,7 +50,7 @@ public class TransactionsTreeTable extends CoinTreeTable {
balanceCol.setSortable(true);
getColumns().add(balanceCol);
setPlaceholder(new Label("No transactions"));
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
setEditable(true);
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
dateCol.setSortType(TreeTableColumn.SortType.DESCENDING);

View file

@ -3,7 +3,6 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.wallet.*;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.scene.control.Label;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
@ -69,7 +68,7 @@ public class UtxosTreeTable extends CoinTreeTable {
getColumns().add(amountCol);
setTreeColumn(amountCol);
setPlaceholder(new Label("No unspent outputs"));
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
setEditable(true);
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
amountCol.setSortType(TreeTableColumn.SortType.DESCENDING);

View file

@ -0,0 +1,68 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
public class WalletBirthDateDialog extends Dialog<Date> {
private final DatePicker birthDatePicker;
public WalletBirthDateDialog(Date birthDate) {
final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow());
setTitle("Wallet Birth Date");
dialogPane.setHeaderText("Select an approximate date earlier than the first wallet transaction:");
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
dialogPane.setPrefWidth(420);
dialogPane.setPrefHeight(200);
Glyph wallet = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HISTORY);
wallet.setFontSize(50);
dialogPane.setGraphic(wallet);
HBox datePickerBox = new HBox(10);
Label label = new Label("Start scanning from:");
label.setPadding(new Insets(5, 0, 0, 8));
datePickerBox.getChildren().add(label);
birthDatePicker = birthDate == null ? new DatePicker() : new DatePicker(birthDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
birthDatePicker.setEditable(false);
birthDatePicker.setConverter(new DateStringConverter());
datePickerBox.getChildren().add(birthDatePicker);
dialogPane.setContent(datePickerBox);
ValidationSupport validationSupport = new ValidationSupport();
Platform.runLater( () -> {
validationSupport.registerValidator(birthDatePicker, Validator.combine(
(Control c, LocalDate newValue) -> ValidationResult.fromErrorIf( c, "Birth date not specified", newValue == null)
));
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
});
final ButtonType okButtonType = new javafx.scene.control.ButtonType("Rescan Wallet", ButtonBar.ButtonData.OK_DONE);
dialogPane.getButtonTypes().addAll(okButtonType);
Button okButton = (Button) dialogPane.lookupButton(okButtonType);
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> birthDatePicker.getValue() == null, birthDatePicker.valueProperty());
okButton.disableProperty().bind(isInvalid);
setResultConverter(dialogButton -> dialogButton == okButtonType ? Date.from(birthDatePicker.getValue().atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()) : null);
}
}

View file

@ -42,7 +42,7 @@ public class WalletNameDialog extends Dialog<WalletNameDialog.NameAndBirthDate>
dialogPane.setHeaderText("Enter a name for this wallet:");
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
dialogPane.setPrefWidth(420);
dialogPane.setPrefWidth(460);
dialogPane.setPrefHeight(requestBirthDate ? 250 : 200);
Glyph wallet = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WALLET);
@ -60,12 +60,20 @@ public class WalletNameDialog extends Dialog<WalletNameDialog.NameAndBirthDate>
existingBox.getChildren().add(existingCheck);
existingPicker = new DatePicker();
existingPicker.setConverter(new DateStringConverter());
existingPicker.setEditable(false);
existingPicker.setPrefWidth(130);
existingPicker.managedProperty().bind(existingPicker.visibleProperty());
existingPicker.setVisible(false);
existingBox.getChildren().add(existingPicker);
HelpLabel helpLabel = new HelpLabel();
helpLabel.setHelpText("Select an approximate date earlier than the first wallet transaction.");
helpLabel.setTranslateY(5);
helpLabel.managedProperty().bind(helpLabel.visibleProperty());
helpLabel.visibleProperty().bind(existingPicker.visibleProperty());
existingBox.getChildren().add(helpLabel);
existingCheck.selectedProperty().addListener((observable, oldValue, newValue) -> {
if(newValue) {
existingCheck.setText("Has existing transactions starting from");

View file

@ -16,6 +16,10 @@ public class BwtScanStatusEvent extends BwtStatusEvent {
return progress;
}
public boolean isCompleted() {
return progress == 100;
}
public Date getEta() {
return eta;
}

View file

@ -0,0 +1,8 @@
package com.sparrowwallet.sparrow.event;
/**
* Empty class used to notify the bwt has shut down.
*/
public class BwtShutdownEvent {
}

View file

@ -14,6 +14,10 @@ public class BwtSyncStatusEvent extends BwtStatusEvent {
return progress;
}
public boolean isCompleted() {
return progress == 100;
}
public int getTip() {
return tip;
}

View file

@ -1,35 +1,47 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.WalletTabData;
import com.sparrowwallet.sparrow.io.Storage;
import javafx.stage.Window;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class OpenWalletsEvent {
private final Window window;
private final Map<Wallet, Storage> walletsMap;
private final List<WalletTabData> walletTabDataList;
public OpenWalletsEvent(Window window, Map<Wallet, Storage> walletsMap) {
public OpenWalletsEvent(Window window, List<WalletTabData> walletTabDataList) {
this.window = window;
this.walletsMap = walletsMap;
this.walletTabDataList = walletTabDataList;
}
public Window getWindow() {
return window;
}
public List<Wallet> getWallets() {
return new ArrayList<>(walletsMap.keySet());
}
public Storage getStorage(Wallet wallet) {
return walletsMap.get(wallet);
public List<WalletTabData> getWalletTabDataList() {
return walletTabDataList;
}
public Map<Wallet, Storage> getWalletsMap() {
return walletsMap;
Map<Wallet, Storage> openWallets = new LinkedHashMap<>();
for(WalletTabData walletTabData : walletTabDataList){
openWallets.put(walletTabData.getWallet(), walletTabData.getStorage());
}
return openWallets;
}
public List<Wallet> getWallets() {
return new ArrayList<>(getWalletsMap().keySet());
}
public Storage getStorage(Wallet wallet) {
return getWalletsMap().get(wallet);
}
}

View file

@ -20,6 +20,6 @@ public class SettingsChangedEvent {
}
public enum Type {
POLICY, SCRIPT_TYPE, MUTLISIG_THRESHOLD, MULTISIG_TOTAL, KEYSTORE_LABEL, KEYSTORE_FINGERPRINT, KEYSTORE_DERIVATION, KEYSTORE_XPUB, GAP_LIMIT;
POLICY, SCRIPT_TYPE, MUTLISIG_THRESHOLD, MULTISIG_TOTAL, KEYSTORE_LABEL, KEYSTORE_FINGERPRINT, KEYSTORE_DERIVATION, KEYSTORE_XPUB, GAP_LIMIT, BIRTH_DATE;
}
}

View file

@ -3,7 +3,9 @@ package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.io.Storage;
import java.io.File;
import java.util.List;
import java.util.stream.Collectors;
@ -12,13 +14,19 @@ import java.util.stream.Collectors;
*
*/
public class WalletHistoryChangedEvent extends WalletChangedEvent {
private final Storage storage;
private final List<WalletNode> historyChangedNodes;
public WalletHistoryChangedEvent(Wallet wallet, List<WalletNode> historyChangedNodes) {
public WalletHistoryChangedEvent(Wallet wallet, Storage storage, List<WalletNode> historyChangedNodes) {
super(wallet);
this.storage = storage;
this.historyChangedNodes = historyChangedNodes;
}
public File getWalletFile() {
return storage.getWalletFile();
}
public List<WalletNode> getHistoryChangedNodes() {
return historyChangedNodes;
}

View file

@ -4,27 +4,27 @@ import com.sparrowwallet.drongo.wallet.Wallet;
public class WalletHistoryStatusEvent {
private final Wallet wallet;
private final boolean loaded;
private final boolean loading;
private final String statusMessage;
private final String errorMessage;
public WalletHistoryStatusEvent(Wallet wallet, boolean loaded) {
public WalletHistoryStatusEvent(Wallet wallet, boolean loading) {
this.wallet = wallet;
this.loaded = loaded;
this.loading = loading;
this.statusMessage = null;
this.errorMessage = null;
}
public WalletHistoryStatusEvent(Wallet wallet, boolean loaded, String statusMessage) {
public WalletHistoryStatusEvent(Wallet wallet, boolean loading, String statusMessage) {
this.wallet = wallet;
this.loaded = false;
this.loading = loading;
this.statusMessage = statusMessage;
this.errorMessage = null;
}
public WalletHistoryStatusEvent(Wallet wallet,String errorMessage) {
public WalletHistoryStatusEvent(Wallet wallet, String errorMessage) {
this.wallet = wallet;
this.loaded = false;
this.loading = true;
this.statusMessage = null;
this.errorMessage = errorMessage;
}
@ -34,11 +34,7 @@ public class WalletHistoryStatusEvent {
}
public boolean isLoading() {
return !loaded;
}
public boolean isLoaded() {
return loaded;
return loading;
}
public String getStatusMessage() {

View file

@ -0,0 +1,22 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.io.Storage;
public class WalletOpeningEvent {
private final Storage storage;
private final Wallet wallet;
public WalletOpeningEvent(Storage storage, Wallet wallet) {
this.storage = storage;
this.wallet = wallet;
}
public Storage getStorage() {
return storage;
}
public Wallet getWallet() {
return wallet;
}
}

View file

@ -31,6 +31,7 @@ public class FontAwesome5 extends GlyphFont {
EYE('\uf06e'),
HAND_HOLDING('\uf4bd'),
HAND_HOLDING_MEDICAL('\ue05c'),
HISTORY('\uf1da'),
KEY('\uf084'),
LAPTOP('\uf109'),
LOCK('\uf023'),

View file

@ -74,7 +74,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
public Map<String, ScriptHashTx[]> getScriptHashHistory(Transport transport, Wallet wallet, Map<String, String> pathScriptHashes, boolean failOnError) {
JsonRpcClient client = new JsonRpcClient(transport);
BatchRequestBuilder<String, ScriptHashTx[]> batchRequest = client.createBatchRequest().keysType(String.class).returnType(ScriptHashTx[].class);
EventManager.get().post(new WalletHistoryStatusEvent(wallet, false, "Loading transactions"));
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Loading transactions"));
for(String path : pathScriptHashes.keySet()) {
batchRequest.add(path, "blockchain.scripthash.get_history", pathScriptHashes.get(path));
@ -130,7 +130,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
public Map<String, String> subscribeScriptHashes(Transport transport, Wallet wallet, Map<String, String> pathScriptHashes) {
JsonRpcClient client = new JsonRpcClient(transport);
BatchRequestBuilder<String, String> batchRequest = client.createBatchRequest().keysType(String.class).returnType(String.class);
EventManager.get().post(new WalletHistoryStatusEvent(wallet, false, "Finding transactions"));
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Finding transactions"));
for(String path : pathScriptHashes.keySet()) {
batchRequest.add(path, "blockchain.scripthash.subscribe", pathScriptHashes.get(path));
@ -151,7 +151,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
public Map<Integer, String> getBlockHeaders(Transport transport, Wallet wallet, Set<Integer> blockHeights) {
JsonRpcClient client = new JsonRpcClient(transport);
BatchRequestBuilder<Integer, String> batchRequest = client.createBatchRequest().keysType(Integer.class).returnType(String.class);
EventManager.get().post(new WalletHistoryStatusEvent(wallet, false, "Retrieving blocks"));
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Retrieving blocks"));
for(Integer height : blockHeights) {
batchRequest.add(height, "blockchain.block.header", height);
@ -171,7 +171,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
public Map<String, String> getTransactions(Transport transport, Wallet wallet, Set<String> txids) {
JsonRpcClient client = new JsonRpcClient(transport);
BatchRequestBuilder<String, String> batchRequest = client.createBatchRequest().keysType(String.class).returnType(String.class);
EventManager.get().post(new WalletHistoryStatusEvent(wallet, false, "Retrieving transactions"));
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Retrieving transactions"));
for(String txid : txids) {
batchRequest.add(txid, "blockchain.transaction.get", txid);

View file

@ -5,12 +5,13 @@ import com.google.gson.annotations.SerializedName;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Config;
import dev.bwt.daemon.CallbackNotifier;
import dev.bwt.daemon.NativeBwtDaemon;
import dev.bwt.libbwt.daemon.CallbackNotifier;
import dev.bwt.libbwt.daemon.NativeBwtDaemon;
import javafx.application.Platform;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
@ -18,20 +19,18 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.*;
public class Bwt {
private static final Logger log = LoggerFactory.getLogger(Bwt.class);
private Long shutdownPtr;
private boolean terminating;
static {
try {
org.controlsfx.tools.Platform platform = org.controlsfx.tools.Platform.getCurrent();
if(platform == org.controlsfx.tools.Platform.OSX) {
NativeUtils.loadLibraryFromJar("/native/osx/x64/libbwt.dylib");
NativeUtils.loadLibraryFromJar("/native/osx/x64/libbwt_jni.dylib");
} else if(platform == org.controlsfx.tools.Platform.WINDOWS) {
NativeUtils.loadLibraryFromJar("/native/windows/x64/bwt.dll");
} else {
@ -43,9 +42,7 @@ public class Bwt {
}
private void start(CallbackNotifier callback) {
List<String> descriptors = List.of("pkh(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)");
Date now = new Date();
start(descriptors, (int)(now.getTime() / 1000), null, callback);
start(Collections.emptyList(), null, null, null, callback);
}
private void start(Collection<Wallet> wallets, CallbackNotifier callback) {
@ -57,10 +54,18 @@ public class Bwt {
outputDescriptors.add(changeOutputDescriptor.toString(false, false));
}
int rescanSince = wallets.stream().filter(wallet -> wallet.getBirthDate() != null).mapToInt(wallet -> (int)(wallet.getBirthDate().getTime() / 1000)).min().orElse(0);
int rescanSince = wallets.stream().filter(wallet -> wallet.getBirthDate() != null).mapToInt(wallet -> (int)(wallet.getBirthDate().getTime() / 1000)).min().orElse(-1);
int gapLimit = wallets.stream().filter(wallet -> wallet.getGapLimit() > 0).mapToInt(Wallet::getGapLimit).max().orElse(Wallet.DEFAULT_LOOKAHEAD);
start(outputDescriptors, rescanSince, gapLimit, callback);
boolean forceRescan = false;
for(Wallet wallet :wallets) {
Date txBirthDate = wallet.getTransactions().values().stream().map(BlockTransactionHash::getDate).filter(Objects::nonNull).min(Date::compareTo).orElse(null);
if((wallet.getBirthDate() != null && txBirthDate != null && wallet.getBirthDate().before(txBirthDate)) || (txBirthDate == null && wallet.getStoredBlockHeight() == 0)) {
forceRescan = true;
}
}
start(outputDescriptors, rescanSince, forceRescan, gapLimit, callback);
}
/**
@ -72,13 +77,24 @@ public class Bwt {
* @param gapLimit desired gap limit beyond last used address
* @param callback object receiving notifications
*/
private void start(List<String> outputDescriptors, Integer rescanSince, Integer gapLimit, CallbackNotifier callback) {
private void start(List<String> outputDescriptors, Integer rescanSince, Boolean forceRescan, Integer gapLimit, CallbackNotifier callback) {
BwtConfig bwtConfig = new BwtConfig();
bwtConfig.network = Network.get() == Network.MAINNET ? "bitcoin" : Network.get().getName();
bwtConfig.descriptors = outputDescriptors;
bwtConfig.rescanSince = rescanSince;
bwtConfig.gapLimit = gapLimit;
if(!outputDescriptors.isEmpty()) {
bwtConfig.descriptors = outputDescriptors;
bwtConfig.rescanSince = (rescanSince == null || rescanSince < 0 ? "now" : rescanSince);
bwtConfig.forceRescan = forceRescan;
bwtConfig.gapLimit = gapLimit;
} else {
bwtConfig.requireAddresses = false;
}
bwtConfig.verbose = log.isDebugEnabled() ? 2 : 0;
if(!log.isInfoEnabled()) {
bwtConfig.setupLogger = false;
}
bwtConfig.electrumAddr = "127.0.0.1:0";
bwtConfig.electrumSkipMerkle = true;
@ -95,6 +111,7 @@ public class Bwt {
Gson gson = new Gson();
String jsonConfig = gson.toJson(bwtConfig);
log.debug("Configuring bwt: " + jsonConfig);
NativeBwtDaemon.start(jsonConfig, callback);
}
@ -102,16 +119,26 @@ public class Bwt {
/**
* Shut down the BWT daemon
*
* @param shutdownPtr the pointer provided on startup
*/
private void shutdown(long shutdownPtr) {
private void shutdown() {
if(shutdownPtr == null) {
terminating = true;
return;
}
NativeBwtDaemon.shutdown(shutdownPtr);
this.shutdownPtr = null;
Platform.runLater(() -> EventManager.get().post(new BwtShutdownEvent()));
}
public boolean isRunning() {
return shutdownPtr != null;
}
public boolean isTerminating() {
return terminating;
}
public ConnectionService getConnectionService(Collection<Wallet> wallets) {
return wallets != null ? new ConnectionService(wallets) : new ConnectionService();
}
@ -146,11 +173,17 @@ public class Bwt {
public String xpubs;
@SerializedName("rescan_since")
public Integer rescanSince;
public Object rescanSince;
@SerializedName("force_rescan")
public Boolean forceRescan;
@SerializedName("gap_limit")
public Integer gapLimit;
@SerializedName("initial_import_size")
public Integer initialImportSize;
@SerializedName("verbose")
public Integer verbose;
@ -159,6 +192,12 @@ public class Bwt {
@SerializedName("electrum_skip_merkle")
public Boolean electrumSkipMerkle;
@SerializedName("require_addresses")
public Boolean requireAddresses;
@SerializedName("setup_logger")
public Boolean setupLogger;
}
public final class ConnectionService extends Service<Void> {
@ -179,25 +218,37 @@ public class Bwt {
CallbackNotifier notifier = new CallbackNotifier() {
@Override
public void onBooting() {
Platform.runLater(() -> EventManager.get().post(new BwtStatusEvent("Starting bwt")));
log.debug("Booting bwt");
if(!terminating) {
Platform.runLater(() -> EventManager.get().post(new BwtStatusEvent("Starting bwt")));
}
}
@Override
public void onSyncProgress(float progress, int tip) {
int percent = (int) (progress * 100.0);
Platform.runLater(() -> EventManager.get().post(new BwtSyncStatusEvent("Syncing (" + percent + "%)", percent, tip)));
log.debug("Syncing " + percent + "%");
if(!terminating) {
Platform.runLater(() -> EventManager.get().post(new BwtSyncStatusEvent("Syncing" + (percent < 100 ? " (" + percent + "%)" : ""), percent, tip)));
}
}
@Override
public void onScanProgress(float progress, int eta) {
int percent = (int) (progress * 100.0);
Date date = new Date((long) eta * 1000);
Platform.runLater(() -> EventManager.get().post(new BwtScanStatusEvent("Scanning (" + percent + "%)", percent, date)));
log.debug("Scanning " + percent + "%");
if(!terminating) {
Platform.runLater(() -> EventManager.get().post(new BwtScanStatusEvent("Scanning" + (percent < 100 ? " (" + percent + "%)" : ""), percent, date)));
}
}
@Override
public void onElectrumReady(String addr) {
Platform.runLater(() -> EventManager.get().post(new BwtElectrumReadyStatusEvent("Electrum server ready", addr)));
log.debug("Electrum ready");
if(!terminating) {
Platform.runLater(() -> EventManager.get().post(new BwtElectrumReadyStatusEvent("Electrum server ready", addr)));
}
}
@Override
@ -207,8 +258,14 @@ public class Bwt {
@Override
public void onReady(long shutdownPtr) {
log.debug("Bwt ready");
Bwt.this.shutdownPtr = shutdownPtr;
Platform.runLater(() -> EventManager.get().post(new BwtReadyStatusEvent("Server ready", shutdownPtr)));
if(terminating) {
Bwt.this.shutdown();
terminating = false;
} else {
Platform.runLater(() -> EventManager.get().post(new BwtReadyStatusEvent("Server ready", shutdownPtr)));
}
}
};
@ -229,12 +286,7 @@ public class Bwt {
protected Task<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;
Bwt.this.shutdown();
return null;
}
};

View file

@ -795,6 +795,18 @@ public class ElectrumServer {
ElectrumServer electrumServer = new ElectrumServer();
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
if(bwt.isTerminating()) {
try {
bwtStartLock.lock();
bwtStartCondition.await();
} catch(InterruptedException e) {
Thread.currentThread().interrupt();
return null;
} finally {
bwtStartLock.unlock();
}
}
if(!bwt.isRunning()) {
Bwt.ConnectionService bwtConnectionService = bwt.getConnectionService(subscribe ? AppServices.get().getOpenWallets().keySet() : null);
bwtConnectionService.setOnFailed(workerStateEvent -> {
@ -817,6 +829,7 @@ public class ElectrumServer {
}
} catch(InterruptedException e) {
Thread.currentThread().interrupt();
return null;
} finally {
bwtStartLock.unlock();
}
@ -894,6 +907,14 @@ public class ElectrumServer {
}
}
public boolean isConnecting() {
return isRunning() && Config.get().getServerType() == ServerType.BITCOIN_CORE && !bwt.isRunning();
}
public boolean isConnected() {
return isRunning() && (Config.get().getServerType() != ServerType.BITCOIN_CORE || bwt.isRunning());
}
@Override
public boolean cancel() {
try {
@ -907,16 +928,14 @@ public class ElectrumServer {
}
private void shutdownBwt() {
if(bwt.isRunning()) {
Bwt.DisconnectionService disconnectionService = bwt.getDisconnectionService();
disconnectionService.setOnSucceeded(workerStateEvent -> {
ElectrumServer.bwtElectrumServer = null;
});
disconnectionService.setOnFailed(workerStateEvent -> {
log.error("Failed to stop BWT", workerStateEvent.getSource().getException());
});
Platform.runLater(disconnectionService::start);
}
Bwt.DisconnectionService disconnectionService = bwt.getDisconnectionService();
disconnectionService.setOnSucceeded(workerStateEvent -> {
ElectrumServer.bwtElectrumServer = null;
});
disconnectionService.setOnFailed(workerStateEvent -> {
log.error("Failed to stop BWT", workerStateEvent.getSource().getException());
});
Platform.runLater(disconnectionService::start);
}
@Override
@ -954,6 +973,16 @@ public class ElectrumServer {
}
}
@Subscribe
public void bwtShutdown(BwtShutdownEvent event) {
try {
bwtStartLock.lock();
bwtStartCondition.signal();
} finally {
bwtStartLock.unlock();
}
}
public StringProperty statusProperty() {
return statusProperty;
}

View file

@ -75,7 +75,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
Map<String, ScriptHashTx[]> result = new LinkedHashMap<>();
for(String path : pathScriptHashes.keySet()) {
EventManager.get().post(new WalletHistoryStatusEvent(wallet, false, "Loading transactions for " + path));
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Loading transactions for " + path));
try {
ScriptHashTx[] scriptHashTxes = new RetryLogic<ScriptHashTx[]>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() ->
client.createRequest().returnAs(ScriptHashTx[].class).method("blockchain.scripthash.get_history").id(path + "-" + idCounter.incrementAndGet()).params(pathScriptHashes.get(path)).execute());
@ -120,7 +120,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
Map<String, String> result = new LinkedHashMap<>();
for(String path : pathScriptHashes.keySet()) {
EventManager.get().post(new WalletHistoryStatusEvent(wallet, false, "Finding transactions for " + path));
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Finding transactions for " + path));
try {
String scriptHash = new RetryLogic<String>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() ->
client.createRequest().returnAs(String.class).method("blockchain.scripthash.subscribe").id(path + "-" + idCounter.incrementAndGet()).params(pathScriptHashes.get(path)).executeNullable());
@ -140,7 +140,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
Map<Integer, String> result = new LinkedHashMap<>();
for(Integer blockHeight : blockHeights) {
EventManager.get().post(new WalletHistoryStatusEvent(wallet, false, "Retrieving block at height " + blockHeight));
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Retrieving block at height " + blockHeight));
try {
String blockHeader = new RetryLogic<String>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() ->
client.createRequest().returnAs(String.class).method("blockchain.block.header").id(idCounter.incrementAndGet()).params(blockHeight).execute());
@ -161,7 +161,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
Map<String, String> result = new LinkedHashMap<>();
for(String txid : txids) {
EventManager.get().post(new WalletHistoryStatusEvent(wallet, false, "Retrieving transaction [" + txid.substring(0, 6) + "]"));
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Retrieving transaction [" + txid.substring(0, 6) + "]"));
try {
String rawTxHex = new RetryLogic<String>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() ->
client.createRequest().returnAs(String.class).method("blockchain.transaction.get").id(idCounter.incrementAndGet()).params(txid).execute());

View file

@ -7,6 +7,7 @@ import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.TextFieldValidator;
import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch;
import com.sparrowwallet.sparrow.event.BwtStatusEvent;
import com.sparrowwallet.sparrow.event.BwtSyncStatusEvent;
import com.sparrowwallet.sparrow.event.ConnectionEvent;
import com.sparrowwallet.sparrow.event.RequestDisconnectEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
@ -119,6 +120,8 @@ public class ServerPreferencesController extends PreferencesDetailController {
private final ValidationSupport validationSupport = new ValidationSupport();
private ElectrumServer.ConnectionService connectionService;
@Override
public void initializeView(Config config) {
EventManager.get().register(this);
@ -343,7 +346,11 @@ public class ServerPreferencesController extends PreferencesDetailController {
}
private void startElectrumConnection() {
ElectrumServer.ConnectionService connectionService = new ElectrumServer.ConnectionService(false);
if(connectionService != null && connectionService.isRunning()) {
connectionService.cancel();
}
connectionService = new ElectrumServer.ConnectionService(false);
connectionService.setPeriod(Duration.hours(1));
EventManager.get().register(connectionService);
connectionService.statusProperty().addListener((observable, oldValue, newValue) -> {
@ -365,11 +372,11 @@ public class ServerPreferencesController extends PreferencesDetailController {
}
private void setFieldsEditable(boolean editable) {
serverTypeToggleGroup.getToggles().forEach(toggle -> ((ToggleButton)toggle).setDisable(!editable));
coreHost.setEditable(editable);
corePort.setEditable(editable);
for(Toggle toggle : coreAuthToggleGroup.getToggles()) {
((ToggleButton)toggle).setDisable(!editable);
}
coreAuthToggleGroup.getToggles().forEach(toggle -> ((ToggleButton)toggle).setDisable(!editable));
coreDataDir.setEditable(editable);
coreDataDirSelect.setDisable(!editable);
coreUser.setEditable(editable);
@ -611,6 +618,17 @@ public class ServerPreferencesController extends PreferencesDetailController {
@Subscribe
public void bwtStatus(BwtStatusEvent event) {
testResults.appendText("\n" + event.getStatus());
if(!(event instanceof BwtSyncStatusEvent)) {
testResults.appendText("\n" + event.getStatus());
}
}
@Subscribe
public void bwtSyncStatus(BwtSyncStatusEvent event) {
if(connectionService != null && connectionService.isRunning() && event.getProgress() < 100) {
testResults.appendText("\nThe connection to the Bitcoin Core node was successful, but it is still syncing to the the blockchain tip at " + event.getTip() + " blocks (" + event.getProgress() + "% completed)");
testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.QUESTION_CIRCLE, null));
connectionService.cancel();
}
}
}

View file

@ -326,7 +326,7 @@ public class TransactionController implements Initializable {
}
private void fetchThisAndInputBlockTransactions(int indexStart, int indexEnd) {
if(AppServices.isOnline() && indexStart < getTransaction().getInputs().size()) {
if(AppServices.isConnected() && indexStart < getTransaction().getInputs().size()) {
Set<Sha256Hash> references = new HashSet<>();
if(getPSBT() == null) {
references.add(getTransaction().getTxId());
@ -378,7 +378,7 @@ public class TransactionController implements Initializable {
}
private void fetchOutputBlockTransactions(int indexStart, int indexEnd) {
if(AppServices.isOnline() && getPSBT() == null && indexStart < getTransaction().getOutputs().size()) {
if(AppServices.isConnected() && getPSBT() == null && indexStart < getTransaction().getOutputs().size()) {
int maxIndex = Math.min(getTransaction().getOutputs().size(), indexEnd);
ElectrumServer.TransactionOutputsReferenceService transactionOutputsReferenceService = new ElectrumServer.TransactionOutputsReferenceService(getTransaction(), indexStart, maxIndex);
transactionOutputsReferenceService.setOnSucceeded(successEvent -> {

View file

@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.wallet;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.DateStringConverter;
import com.sparrowwallet.sparrow.event.SettingsChangedEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
@ -27,12 +28,14 @@ public class AdvancedController implements Initializable {
}
public void initializeView(Wallet wallet) {
birthDate.setConverter(new DateStringConverter());
if(wallet.getBirthDate() != null) {
birthDate.setValue(wallet.getBirthDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
}
birthDate.valueProperty().addListener((observable, oldValue, newValue) -> {
if(newValue != null) {
wallet.setBirthDate(Date.from(newValue.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()));
EventManager.get().post(new SettingsChangedEvent(wallet, SettingsChangedEvent.Type.BIRTH_DATE));
}
});

View file

@ -138,7 +138,7 @@ public class ReceiveController extends WalletFormController implements Initializ
private void updateLastUsed() {
Set<BlockTransactionHashIndex> currentOutputs = currentEntry.getNode().getTransactionOutputs();
if(AppServices.isOnline() && currentOutputs.isEmpty()) {
if(AppServices.isConnected() && currentOutputs.isEmpty()) {
lastUsed.setText("Never");
lastUsed.setGraphic(getUnusedGlyph());
} else if(!currentOutputs.isEmpty()) {

View file

@ -14,10 +14,7 @@ import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.RequestOpenWalletsEvent;
import com.sparrowwallet.sparrow.event.SettingsChangedEvent;
import com.sparrowwallet.sparrow.event.StorageEvent;
import com.sparrowwallet.sparrow.event.TimedEvent;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Storage;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
@ -28,17 +25,14 @@ import javafx.fxml.Initializable;
import javafx.scene.control.*;
import javafx.scene.layout.StackPane;
import org.controlsfx.control.RangeSlider;
import org.controlsfx.tools.Borders;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tornadofx.control.Fieldset;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.*;
import java.util.stream.Collectors;
public class SettingsController extends WalletFormController implements Initializable {
@ -329,6 +323,22 @@ public class SettingsController extends WalletFormController implements Initiali
}
}
@Subscribe
public void walletSettingsChanged(WalletSettingsChangedEvent event) {
updateBirthDate(event.getWalletFile(), event.getWallet());
}
@Subscribe
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
updateBirthDate(event.getWalletFile(), event.getWallet());
}
private void updateBirthDate(File walletFile, Wallet wallet) {
if(walletFile.equals(walletForm.getWalletFile()) && !Objects.equals(wallet.getBirthDate(), walletForm.getWallet().getBirthDate())) {
walletForm.getWallet().setBirthDate(wallet.getBirthDate());
}
}
private void saveWallet(boolean changePassword) {
ECKey existingPubKey = walletForm.getStorage().getEncryptionPubKey();

View file

@ -7,6 +7,7 @@ import com.sparrowwallet.sparrow.event.WalletSettingsChangedEvent;
import com.sparrowwallet.sparrow.io.Storage;
import java.io.IOException;
import java.util.Objects;
/**
* This class extends WalletForm to allow rollback of wallet changes. It is used exclusively by SettingsController for this purpose.
@ -37,7 +38,7 @@ public class SettingsWalletForm extends WalletForm {
@Override
public void saveAndRefresh() throws IOException {
boolean refreshAll = changesScriptHashes(wallet, walletCopy);
boolean refreshAll = isRefreshNecessary(wallet, walletCopy);
if(refreshAll) {
walletCopy.clearNodes();
}
@ -50,7 +51,7 @@ public class SettingsWalletForm extends WalletForm {
}
}
private boolean changesScriptHashes(Wallet original, Wallet changed) {
private boolean isRefreshNecessary(Wallet original, Wallet changed) {
if(!original.isValid() || !changed.isValid()) {
return true;
}
@ -86,6 +87,10 @@ public class SettingsWalletForm extends WalletForm {
return true;
}
if(!Objects.equals(original.getBirthDate(), changed.getBirthDate())) {
return true;
}
return false;
}
}

View file

@ -130,12 +130,17 @@ public class TransactionsController extends WalletFormController implements Init
@Subscribe
public void bwtSyncStatus(BwtSyncStatusEvent event) {
walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), false, event.getStatus()));
walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), true, event.getStatus()));
}
@Subscribe
public void bwtScanStatus(BwtScanStatusEvent event) {
walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), false, event.getStatus()));
walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), true, event.getStatus()));
}
@Subscribe
public void bwtShutdown(BwtShutdownEvent event) {
walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), false));
}
@Subscribe

View file

@ -136,12 +136,17 @@ public class UtxosController extends WalletFormController implements Initializab
@Subscribe
public void bwtSyncStatus(BwtSyncStatusEvent event) {
walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), false, event.getStatus()));
walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), true, event.getStatus()));
}
@Subscribe
public void bwtScanStatus(BwtScanStatusEvent event) {
walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), false, event.getStatus()));
walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), true, event.getStatus()));
}
@Subscribe
public void bwtShutdown(BwtShutdownEvent event) {
walletHistoryStatus(new WalletHistoryStatusEvent(walletForm.getWallet(), false));
}
@Subscribe

View file

@ -75,18 +75,18 @@ public class WalletForm {
public void refreshHistory(Integer blockHeight, WalletNode node) {
Wallet previousWallet = wallet.copy();
if(wallet.isValid() && AppServices.isOnline()) {
if(wallet.isValid() && AppServices.isConnected()) {
log.debug(node == null ? wallet.getName() + " refreshing full wallet history" : wallet.getName() + " requesting node wallet history for " + node.getDerivationPath());
ElectrumServer.TransactionHistoryService historyService = new ElectrumServer.TransactionHistoryService(wallet, getWalletTransactionNodes(node));
historyService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true));
EventManager.get().post(new WalletHistoryStatusEvent(wallet, false));
updateWallet(previousWallet, blockHeight);
});
historyService.setOnFailed(workerStateEvent -> {
log.error("Error retrieving wallet history", workerStateEvent.getSource().getException());
EventManager.get().post(new WalletHistoryStatusEvent(wallet, workerStateEvent.getSource().getException().getMessage()));
});
EventManager.get().post(new WalletHistoryStatusEvent(wallet, false));
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true));
historyService.start();
}
}
@ -106,7 +106,7 @@ public class WalletForm {
boolean changed = false;
if(!historyChangedNodes.isEmpty()) {
Platform.runLater(() -> EventManager.get().post(new WalletHistoryChangedEvent(wallet, historyChangedNodes)));
Platform.runLater(() -> EventManager.get().post(new WalletHistoryChangedEvent(wallet, storage, historyChangedNodes)));
changed = true;
}

View file

@ -168,3 +168,7 @@
-fx-border-width: 1px 0px 1px 0px;
-fx-border-color: derive(-fx-background, -10%);
}
.root .placeholder .hyperlink {
-fx-text-fill: derive(#1e88cf, 20%);
}