add lock wallet functionality

This commit is contained in:
Craig Raw 2021-10-01 15:47:01 +02:00
parent 8e0b9a3ea0
commit ea03dece72
13 changed files with 307 additions and 25 deletions

View file

@ -137,6 +137,9 @@ public class AppController implements Initializable {
@FXML @FXML
private MenuItem minimizeToTray; private MenuItem minimizeToTray;
@FXML
private MenuItem lockWallet;
@FXML @FXML
private MenuItem refreshWallet; private MenuItem refreshWallet;
@ -283,6 +286,7 @@ public class AppController implements Initializable {
savePSBT.visibleProperty().bind(saveTransaction.visibleProperty().not()); savePSBT.visibleProperty().bind(saveTransaction.visibleProperty().not());
savePSBTBinary.disableProperty().bind(saveTransaction.visibleProperty()); savePSBTBinary.disableProperty().bind(saveTransaction.visibleProperty());
exportWallet.setDisable(true); exportWallet.setDisable(true);
lockWallet.setDisable(true);
refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not()))); refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not())));
sendToMany.disableProperty().bind(exportWallet.disableProperty()); sendToMany.disableProperty().bind(exportWallet.disableProperty());
@ -1140,6 +1144,13 @@ public class AppController implements Initializable {
AppServices.get().minimizeStage((Stage)tabs.getScene().getWindow()); AppServices.get().minimizeStage((Stage)tabs.getScene().getWindow());
} }
public void lockWallet(ActionEvent event) {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) {
EventManager.get().post(new WalletLockEvent(selectedWalletForm.getMasterWallet()));
}
}
public void refreshWallet(ActionEvent event) { public void refreshWallet(ActionEvent event) {
WalletForm selectedWalletForm = getSelectedWalletForm(); WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) { if(selectedWalletForm != null) {
@ -1189,7 +1200,6 @@ public class AppController implements Initializable {
tabLabel.setGraphic(glyph); tabLabel.setGraphic(glyph);
tabLabel.setGraphicTextGap(5.0); tabLabel.setGraphicTextGap(5.0);
tab.setGraphic(tabLabel); tab.setGraphic(tabLabel);
tab.setContextMenu(getTabContextMenu(tab));
tab.setClosable(true); tab.setClosable(true);
tab.setOnCloseRequest(event -> { tab.setOnCloseRequest(event -> {
if(AppServices.getWhirlpoolServices().getWhirlpoolForMixToWallet(((WalletTabData)tab.getUserData()).getWalletForm().getWalletId()) != null) { if(AppServices.getWhirlpoolServices().getWhirlpoolForMixToWallet(((WalletTabData)tab.getUserData()).getWalletForm().getWalletId()) != null) {
@ -1202,13 +1212,17 @@ public class AppController implements Initializable {
TabPane subTabs = new TabPane(); TabPane subTabs = new TabPane();
subTabs.setSide(Side.RIGHT); subTabs.setSide(Side.RIGHT);
subTabs.getStyleClass().add("master-only"); setSubTabsVisible(subTabs, false);
subTabs.rotateGraphicProperty().set(true); subTabs.rotateGraphicProperty().set(true);
tab.setContent(subTabs); tab.setContent(subTabs);
WalletForm walletForm = addWalletSubTab(subTabs, storage, wallet, backupWallet); WalletForm walletForm = addWalletSubTab(subTabs, storage, wallet, backupWallet);
TabData tabData = new WalletTabData(TabData.TabType.WALLET, walletForm); TabData tabData = new WalletTabData(TabData.TabType.WALLET, walletForm);
tab.setUserData(tabData); tab.setUserData(tabData);
tab.setContextMenu(getTabContextMenu(tab));
walletForm.lockedProperty().addListener((observable, oldValue, newValue) -> {
setSubTabsVisible(subTabs, !newValue && subTabs.getTabs().size() > 1);
});
subTabs.getSelectionModel().selectedItemProperty().addListener((observable, old_val, selectedTab) -> { subTabs.getSelectionModel().selectedItemProperty().addListener((observable, old_val, selectedTab) -> {
if(selectedTab != null) { if(selectedTab != null) {
@ -1236,8 +1250,7 @@ public class AppController implements Initializable {
Label masterLabel = (Label)masterTab.getGraphic(); Label masterLabel = (Label)masterTab.getGraphic();
masterLabel.setText(getAutomaticName(wallet.getMasterWallet())); masterLabel.setText(getAutomaticName(wallet.getMasterWallet()));
Platform.runLater(() -> { Platform.runLater(() -> {
subTabs.getStyleClass().remove("master-only"); setSubTabsVisible(subTabs, true);
subTabs.getStyleClass().add("wallet-subtabs");
}); });
} }
} }
@ -1247,6 +1260,20 @@ public class AppController implements Initializable {
EventManager.get().post(new WalletOpenedEvent(storage, wallet)); EventManager.get().post(new WalletOpenedEvent(storage, wallet));
} }
private void setSubTabsVisible(TabPane subTabs, boolean visible) {
if(visible) {
subTabs.getStyleClass().remove("master-only");
if(!subTabs.getStyleClass().contains("wallet-subtabs")) {
subTabs.getStyleClass().add("wallet-subtabs");
}
} else {
if(!subTabs.getStyleClass().contains("master-only")) {
subTabs.getStyleClass().add("master-only");
}
subTabs.getStyleClass().remove("wallet-subtabs");
}
}
public WalletForm addWalletSubTab(TabPane subTabs, Storage storage, Wallet wallet, Wallet backupWallet) { public WalletForm addWalletSubTab(TabPane subTabs, Storage storage, Wallet wallet, Wallet backupWallet) {
try { try {
Tab subTab = new Tab(); Tab subTab = new Tab();
@ -1485,6 +1512,18 @@ public class AppController implements Initializable {
private ContextMenu getTabContextMenu(Tab tab) { private ContextMenu getTabContextMenu(Tab tab) {
ContextMenu contextMenu = new ContextMenu(); ContextMenu contextMenu = new ContextMenu();
if(tab.getUserData() instanceof WalletTabData walletTabData) {
MenuItem lock = new MenuItem("Lock");
Glyph lockGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.LOCK);
lockGlyph.setFontSize(12);
lock.setGraphic(lockGlyph);
lock.disableProperty().bind(walletTabData.getWalletForm().lockedProperty());
lock.setOnAction(event -> {
EventManager.get().post(new WalletLockEvent(walletTabData.getWallet()));
});
contextMenu.getItems().addAll(lock);
}
MenuItem close = new MenuItem("Close"); MenuItem close = new MenuItem("Close");
close.setOnAction(event -> { close.setOnAction(event -> {
tabs.getTabs().remove(tab); tabs.getTabs().remove(tab);
@ -1670,6 +1709,7 @@ public class AppController implements Initializable {
} else { } else {
saveTransaction.setVisible(false); saveTransaction.setVisible(false);
} }
lockWallet.setDisable(true);
exportWallet.setDisable(true); exportWallet.setDisable(true);
showLoadingLog.setDisable(true); showLoadingLog.setDisable(true);
showUtxosChart.setDisable(true); showUtxosChart.setDisable(true);
@ -1679,6 +1719,7 @@ public class AppController implements Initializable {
WalletTabData walletTabData = walletTabEvent.getWalletTabData(); WalletTabData walletTabData = walletTabEvent.getWalletTabData();
saveTransaction.setVisible(true); saveTransaction.setVisible(true);
saveTransaction.setDisable(true); saveTransaction.setDisable(true);
lockWallet.setDisable(walletTabData.getWalletForm().lockedProperty().get());
exportWallet.setDisable(walletTabData.getWallet() == null || !walletTabData.getWallet().isValid()); exportWallet.setDisable(walletTabData.getWallet() == null || !walletTabData.getWallet().isValid());
showLoadingLog.setDisable(false); showLoadingLog.setDisable(false);
showUtxosChart.setDisable(false); showUtxosChart.setDisable(false);
@ -1714,6 +1755,20 @@ public class AppController implements Initializable {
@Subscribe @Subscribe
public void newWalletTransactions(NewWalletTransactionsEvent event) { public void newWalletTransactions(NewWalletTransactionsEvent event) {
if(Config.get().isNotifyNewTransactions() && getOpenWallets().containsKey(event.getWallet())) { if(Config.get().isNotifyNewTransactions() && getOpenWallets().containsKey(event.getWallet())) {
for(Tab tab : tabs.getTabs()) {
if(tab.getUserData() instanceof WalletTabData) {
TabPane subTabs = (TabPane)tab.getContent();
for(Tab subTab : subTabs.getTabs()) {
TabData tabData = (TabData)subTab.getUserData();
if(tabData instanceof WalletTabData walletTabData) {
if(walletTabData.getWallet().equals(event.getWallet()) && walletTabData.getWalletForm().lockedProperty().get()) {
return;
}
}
}
}
}
List<BlockTransaction> blockTransactions = new ArrayList<>(event.getBlockTransactions()); List<BlockTransaction> blockTransactions = new ArrayList<>(event.getBlockTransactions());
List<BlockTransaction> whirlpoolTransactions = event.getWhirlpoolMixTransactions(); List<BlockTransaction> whirlpoolTransactions = event.getWhirlpoolMixTransactions();
blockTransactions.removeAll(whirlpoolTransactions); blockTransactions.removeAll(whirlpoolTransactions);
@ -2172,4 +2227,20 @@ public class AppController implements Initializable {
addWalletTab(storage, event.getChildWallet(), null); addWalletTab(storage, event.getChildWallet(), null);
} }
@Subscribe
public void walletLock(WalletLockEvent event) {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null && selectedWalletForm.getMasterWallet().equals(event.getWallet())) {
lockWallet.setDisable(true);
}
}
@Subscribe
public void walletUnlock(WalletUnlockEvent event) {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null && selectedWalletForm.getMasterWallet().equals(event.getWallet())) {
lockWallet.setDisable(false);
}
}
} }

View file

@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class WalletLockEvent {
private final Wallet wallet;
public WalletLockEvent(Wallet wallet) {
this.wallet = wallet;
}
public Wallet getWallet() {
return wallet;
}
}

View file

@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class WalletUnlockEvent {
private final Wallet wallet;
public WalletUnlockEvent(Wallet wallet) {
this.wallet = wallet;
}
public Wallet getWallet() {
return wallet;
}
}

View file

@ -62,6 +62,10 @@ public class Storage {
} }
public boolean isEncrypted() throws IOException { public boolean isEncrypted() throws IOException {
if(!walletFile.exists()) {
return false;
}
return persistence.isEncrypted(walletFile); return persistence.isEncrypted(walletFile);
} }
@ -531,10 +535,18 @@ public class Storage {
public static class KeyDerivationService extends Service<ECKey> { public static class KeyDerivationService extends Service<ECKey> {
private final Storage storage; private final Storage storage;
private final SecureString password; private final SecureString password;
private final boolean verifyPassword;
public KeyDerivationService(Storage storage, SecureString password) { public KeyDerivationService(Storage storage, SecureString password) {
this.storage = storage; this.storage = storage;
this.password = password; this.password = password;
this.verifyPassword = false;
}
public KeyDerivationService(Storage storage, SecureString password, boolean verifyPassword) {
this.storage = storage;
this.password = password;
this.verifyPassword = verifyPassword;
} }
@Override @Override
@ -542,7 +554,12 @@ public class Storage {
return new Task<>() { return new Task<>() {
protected ECKey call() throws IOException, StorageException { protected ECKey call() throws IOException, StorageException {
try { try {
return storage.getEncryptionKey(password); ECKey encryptionFullKey = storage.getEncryptionKey(password);
if(verifyPassword && !ECKey.fromPublicOnly(encryptionFullKey).equals(storage.getEncryptionPubKey())) {
throw new InvalidPasswordException("Derived pubkey does not match stored pubkey");
}
return encryptionFullKey;
} finally { } finally {
password.clear(); password.clear();
} }

View file

@ -1,5 +1,5 @@
package com.sparrowwallet.sparrow.wallet; package com.sparrowwallet.sparrow.wallet;
public enum Function { public enum Function {
TRANSACTIONS, SEND, RECEIVE, ADDRESSES, UTXOS, SETTINGS; TRANSACTIONS, SEND, RECEIVE, ADDRESSES, UTXOS, SETTINGS, LOCK;
} }

View file

@ -16,6 +16,7 @@ import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.io.StorageException; import com.sparrowwallet.sparrow.io.StorageException;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices; import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
import javafx.application.Platform;
import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
@ -35,6 +36,8 @@ import java.net.URL;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
public class SettingsController extends WalletFormController implements Initializable { public class SettingsController extends WalletFormController implements Initializable {
private static final Logger log = LoggerFactory.getLogger(SettingsController.class); private static final Logger log = LoggerFactory.getLogger(SettingsController.class);
@ -459,7 +462,7 @@ public class SettingsController extends WalletFormController implements Initiali
WalletPasswordDialog dlg = new WalletPasswordDialog(masterWallet.getName(), WalletPasswordDialog.PasswordRequirement.LOAD); WalletPasswordDialog dlg = new WalletPasswordDialog(masterWallet.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<SecureString> password = dlg.showAndWait(); Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) { if(password.isPresent()) {
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(walletForm.getStorage(), password.get()); Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(walletForm.getStorage(), password.get(), true);
keyDerivationService.setOnSucceeded(workerStateEvent -> { keyDerivationService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done")); EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
ECKey encryptionFullKey = keyDerivationService.getValue(); ECKey encryptionFullKey = keyDerivationService.getValue();
@ -482,7 +485,14 @@ public class SettingsController extends WalletFormController implements Initiali
}); });
keyDerivationService.setOnFailed(workerStateEvent -> { keyDerivationService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed")); EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
AppServices.showErrorDialog("Incorrect Password", keyDerivationService.getException().getMessage()); if(keyDerivationService.getException() instanceof InvalidPasswordException) {
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
Platform.runLater(() -> addAccount(null));
}
} else {
log.error("Error deriving wallet key", keyDerivationService.getException());
}
}); });
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet...")); EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet..."));
keyDerivationService.start(); keyDerivationService.start();

View file

@ -30,6 +30,7 @@ import javafx.event.ActionEvent;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Tooltip; import javafx.scene.control.Tooltip;
import javafx.scene.control.TreeItem; import javafx.scene.control.TreeItem;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
@ -47,6 +48,8 @@ import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
public class UtxosController extends WalletFormController implements Initializable { public class UtxosController extends WalletFormController implements Initializable {
private static final Logger log = LoggerFactory.getLogger(UtxosController.class); private static final Logger log = LoggerFactory.getLogger(UtxosController.class);
@ -234,7 +237,7 @@ public class UtxosController extends WalletFormController implements Initializab
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD); WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<SecureString> password = dlg.showAndWait(); Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) { if(password.isPresent()) {
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(walletForm.getStorage(), password.get()); Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(walletForm.getStorage(), password.get(), true);
keyDerivationService.setOnSucceeded(workerStateEvent -> { keyDerivationService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done")); EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
ECKey encryptionFullKey = keyDerivationService.getValue(); ECKey encryptionFullKey = keyDerivationService.getValue();
@ -259,7 +262,14 @@ public class UtxosController extends WalletFormController implements Initializab
}); });
keyDerivationService.setOnFailed(workerStateEvent -> { keyDerivationService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed")); EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
AppServices.showErrorDialog("Incorrect Password", keyDerivationService.getException().getMessage()); if(keyDerivationService.getException() instanceof InvalidPasswordException) {
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
Platform.runLater(() -> previewPremix(tx0Preview, utxoEntries));
}
} else {
log.error("Error deriving wallet key", keyDerivationService.getException());
}
}); });
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet...")); EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet..."));
keyDerivationService.start(); keyDerivationService.start();

View file

@ -1,33 +1,54 @@
package com.sparrowwallet.sparrow.wallet; package com.sparrowwallet.sparrow.wallet;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.ReceiveActionEvent; import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.event.SendActionEvent; import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.event.WalletAddressesChangedEvent;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.Toggle; import javafx.scene.control.*;
import javafx.scene.control.ToggleButton; import javafx.scene.layout.BorderPane;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.controlsfx.control.textfield.CustomPasswordField;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.FontAwesome;
import org.controlsfx.glyphfont.Glyph;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.util.ResourceBundle; import java.util.ResourceBundle;
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
public class WalletController extends WalletFormController implements Initializable { public class WalletController extends WalletFormController implements Initializable {
private static final Logger log = LoggerFactory.getLogger(WalletController.class);
@FXML @FXML
private StackPane walletPane; private StackPane walletPane;
@FXML
private VBox walletMenuBox;
@FXML @FXML
private ToggleGroup walletMenu; private ToggleGroup walletMenu;
private BorderPane lockPane;
private final BooleanProperty walletEncryptedProperty = new SimpleBooleanProperty(false);
@Override @Override
public void initialize(URL location, ResourceBundle resources) { public void initialize(URL location, ResourceBundle resources) {
EventManager.get().register(this); EventManager.get().register(this);
@ -47,7 +68,7 @@ public class WalletController extends WalletFormController implements Initializa
if(walletFunction.getUserData().equals(function)) { if(walletFunction.getUserData().equals(function)) {
existing = true; existing = true;
walletFunction.setViewOrder(0); walletFunction.setViewOrder(0);
} else { } else if(function != Function.LOCK) {
walletFunction.setViewOrder(1); walletFunction.setViewOrder(1);
} }
} }
@ -78,6 +99,9 @@ public class WalletController extends WalletFormController implements Initializa
toggleButton.managedProperty().bind(toggleButton.visibleProperty()); toggleButton.managedProperty().bind(toggleButton.visibleProperty());
} }
walletMenuBox.managedProperty().bind(walletMenuBox.visibleProperty());
walletMenuBox.visibleProperty().bind(getWalletForm().lockedProperty().not());
configure(walletForm.getWallet()); configure(walletForm.getWallet());
} }
@ -111,6 +135,76 @@ public class WalletController extends WalletFormController implements Initializa
}); });
} }
private void initializeLockScreen() {
lockPane = new BorderPane();
lockPane.setUserData(Function.LOCK);
lockPane.getStyleClass().add("wallet-pane");
VBox vBox = new VBox(20);
vBox.setAlignment(Pos.CENTER);
Glyph lock = new Glyph("FontAwesome", FontAwesome.Glyph.LOCK);
lock.setFontSize(80);
vBox.getChildren().add(lock);
Label label = new Label("Enter password to unlock:");
label.managedProperty().bind(label.visibleProperty());
label.visibleProperty().bind(walletEncryptedProperty);
CustomPasswordField passwordField = (CustomPasswordField)TextFields.createClearablePasswordField();
passwordField.setMaxWidth(300);
passwordField.managedProperty().bind(passwordField.visibleProperty());
passwordField.visibleProperty().bind(walletEncryptedProperty);
passwordField.setOnAction(event -> {
unlockWallet(passwordField);
});
Button unlockButton = new Button("Unlock");
unlockButton.setPrefWidth(300);
unlockButton.setOnAction(event -> {
unlockWallet(passwordField);
});
vBox.getChildren().addAll(label, passwordField, unlockButton);
StackPane stackPane = new StackPane();
stackPane.getChildren().add(vBox);
lockPane.setCenter(stackPane);
walletPane.getChildren().add(lockPane);
}
private void unlockWallet(CustomPasswordField passwordField) {
if(walletEncryptedProperty.get()) {
String walletId = walletForm.getWalletId();
SecureString password = new SecureString(passwordField.getText());
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(walletForm.getStorage(), password, true);
keyDerivationService.setOnSucceeded(workerStateEvent -> {
passwordField.clear();
password.clear();
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
unlockWallet();
});
keyDerivationService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
if(keyDerivationService.getException() instanceof InvalidPasswordException) {
showErrorDialog("Invalid Password", "The wallet password was invalid.");
} else {
log.error("Error deriving wallet key", keyDerivationService.getException());
}
});
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet..."));
keyDerivationService.start();
} else {
unlockWallet();
}
}
private void unlockWallet() {
Wallet masterWallet = getWalletForm().getWallet().isMasterWallet() ? getWalletForm().getWallet() : getWalletForm().getWallet().getMasterWallet();
EventManager.get().post(new WalletUnlockEvent(masterWallet));
}
private void updateWalletEncryptedStatus() {
try {
walletEncryptedProperty.set(getWalletForm().getStorage().isEncrypted());
} catch(IOException e) {
log.warn("Error determining if wallet is locked", e);
}
}
@Subscribe @Subscribe
public void walletAddressesChanged(WalletAddressesChangedEvent event) { public void walletAddressesChanged(WalletAddressesChangedEvent event) {
if(event.getWalletId().equals(walletForm.getWalletId())) { if(event.getWalletId().equals(walletForm.getWalletId())) {
@ -118,6 +212,13 @@ public class WalletController extends WalletFormController implements Initializa
} }
} }
@Subscribe
public void walletSettingsChanged(WalletSettingsChangedEvent event) {
if(event.getWalletId().equals(walletForm.getWalletId())) {
Platform.runLater(this::updateWalletEncryptedStatus);
}
}
@Subscribe @Subscribe
public void receiveAction(ReceiveActionEvent event) { public void receiveAction(ReceiveActionEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) { if(event.getWallet().equals(walletForm.getWallet())) {
@ -131,4 +232,27 @@ public class WalletController extends WalletFormController implements Initializa
selectFunction(Function.SEND); selectFunction(Function.SEND);
} }
} }
@Subscribe
public void walletLock(WalletLockEvent event) {
if(event.getWallet().equals(walletForm.getMasterWallet())) {
if(lockPane == null) {
updateWalletEncryptedStatus();
initializeLockScreen();
}
getWalletForm().setLocked(true);
lockPane.setViewOrder(-1);
}
}
@Subscribe
public void walletUnlock(WalletUnlockEvent event) {
if(event.getWallet().equals(walletForm.getMasterWallet())) {
getWalletForm().setLocked(false);
if(lockPane != null) {
lockPane.setViewOrder(2);
}
}
}
} }

View file

@ -14,6 +14,8 @@ import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.ServerType; import com.sparrowwallet.sparrow.net.ServerType;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.util.Duration; import javafx.util.Duration;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -37,6 +39,8 @@ public class WalletForm {
private ElectrumServer.TransactionMempoolService transactionMempoolService; private ElectrumServer.TransactionMempoolService transactionMempoolService;
private final BooleanProperty lockedProperty = new SimpleBooleanProperty(false);
public WalletForm(Storage storage, Wallet currentWallet, Wallet backupWallet) { public WalletForm(Storage storage, Wallet currentWallet, Wallet backupWallet) {
this(storage, currentWallet, backupWallet, true); this(storage, currentWallet, backupWallet, true);
} }
@ -58,6 +62,10 @@ public class WalletForm {
return wallet; return wallet;
} }
public Wallet getMasterWallet() {
return wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
}
public Storage getStorage() { public Storage getStorage() {
return storage; return storage;
} }
@ -298,6 +306,14 @@ public class WalletForm {
return walletUtxosEntry; return walletUtxosEntry;
} }
public BooleanProperty lockedProperty() {
return lockedProperty;
}
public void setLocked(boolean locked) {
this.lockedProperty.set(locked);
}
@Subscribe @Subscribe
public void walletDataChanged(WalletDataChangedEvent event) { public void walletDataChanged(WalletDataChangedEvent event) {
if(event.getWallet().equals(wallet)) { if(event.getWallet().equals(wallet)) {

View file

@ -177,12 +177,15 @@ public class SparrowDataSource extends WalletResponseDataSource {
static Wallet getWallet(String zpub) { static Wallet getWallet(String zpub) {
return AppServices.get().getOpenWallets().keySet().stream() return AppServices.get().getOpenWallets().keySet().stream()
.filter(wallet -> { .filter(wallet -> {
List<ExtendedKey.Header> headers = ExtendedKey.Header.getHeaders(Network.get()); try {
ExtendedKey.Header header = headers.stream().filter(head -> head.getDefaultScriptType().equals(wallet.getScriptType()) && !head.isPrivateKey()).findFirst().orElse(ExtendedKey.Header.xpub); List<ExtendedKey.Header> headers = ExtendedKey.Header.getHeaders(Network.get());
ExtendedKey extPubKey = wallet.getKeystores().get(0).getExtendedPublicKey(); ExtendedKey.Header header = headers.stream().filter(head -> head.getDefaultScriptType().equals(wallet.getScriptType()) && !head.isPrivateKey()).findFirst().orElse(ExtendedKey.Header.xpub);
return extPubKey.toString(header).equals(zpub); ExtendedKey extPubKey = wallet.getKeystores().get(0).getExtendedPublicKey();
return extPubKey.toString(header).equals(zpub);
} catch(Exception e) {
return false;
}
}) })
.filter(Wallet::isValid)
.findFirst() .findFirst()
.orElse(null); .orElse(null);
} }

View file

@ -100,6 +100,7 @@
<CheckMenuItem fx:id="showTxHex" mnemonicParsing="false" text="Show Transaction Hex" onAction="#showTxHex"/> <CheckMenuItem fx:id="showTxHex" mnemonicParsing="false" text="Show Transaction Hex" onAction="#showTxHex"/>
<SeparatorMenuItem /> <SeparatorMenuItem />
<MenuItem fx:id="minimizeToTray" mnemonicParsing="false" text="Minimize to System Tray" accelerator="Shortcut+Y" onAction="#minimizeToTray"/> <MenuItem fx:id="minimizeToTray" mnemonicParsing="false" text="Minimize to System Tray" accelerator="Shortcut+Y" onAction="#minimizeToTray"/>
<MenuItem fx:id="lockWallet" mnemonicParsing="false" text="Lock Wallet" accelerator="Shortcut+L" onAction="#lockWallet"/>
<MenuItem fx:id="refreshWallet" mnemonicParsing="false" text="Refresh Wallet" accelerator="Shortcut+R" onAction="#refreshWallet"/> <MenuItem fx:id="refreshWallet" mnemonicParsing="false" text="Refresh Wallet" accelerator="Shortcut+R" onAction="#refreshWallet"/>
</items> </items>
</Menu> </Menu>

View file

@ -88,7 +88,7 @@
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_TRIANGLE" styleClass="future-warning" /> <Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_TRIANGLE" styleClass="future-warning" />
</graphic> </graphic>
<tooltip> <tooltip>
<Tooltip text="Future block specified - transaction will not be mined until this block"/> <Tooltip text="Future block specified - transaction cannot be broadcast until this block height"/>
</tooltip> </tooltip>
</Label> </Label>
</Field> </Field>
@ -99,7 +99,7 @@
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_TRIANGLE" styleClass="future-warning" /> <Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_TRIANGLE" styleClass="future-warning" />
</graphic> </graphic>
<tooltip> <tooltip>
<Tooltip text="Future date specified - transaction will not be mined until this date"/> <Tooltip text="Future date specified - transaction cannot be broadcast until this date"/>
</tooltip> </tooltip>
</Label> </Label>
</Field> </Field>

View file

@ -10,7 +10,7 @@
<BorderPane stylesheets="@wallet.css, @../general.css" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.wallet.WalletController"> <BorderPane stylesheets="@wallet.css, @../general.css" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.wallet.WalletController">
<left> <left>
<VBox styleClass="list-menu"> <VBox fx:id="walletMenuBox" styleClass="list-menu">
<ToggleButton VBox.vgrow="ALWAYS" text="Transactions" contentDisplay="TOP" styleClass="list-item" maxHeight="Infinity"> <ToggleButton VBox.vgrow="ALWAYS" text="Transactions" contentDisplay="TOP" styleClass="list-item" maxHeight="Infinity">
<toggleGroup> <toggleGroup>
<ToggleGroup fx:id="walletMenu" /> <ToggleGroup fx:id="walletMenu" />