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
private MenuItem minimizeToTray;
@FXML
private MenuItem lockWallet;
@FXML
private MenuItem refreshWallet;
@ -283,6 +286,7 @@ public class AppController implements Initializable {
savePSBT.visibleProperty().bind(saveTransaction.visibleProperty().not());
savePSBTBinary.disableProperty().bind(saveTransaction.visibleProperty());
exportWallet.setDisable(true);
lockWallet.setDisable(true);
refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not())));
sendToMany.disableProperty().bind(exportWallet.disableProperty());
@ -1140,6 +1144,13 @@ public class AppController implements Initializable {
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) {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) {
@ -1189,7 +1200,6 @@ public class AppController implements Initializable {
tabLabel.setGraphic(glyph);
tabLabel.setGraphicTextGap(5.0);
tab.setGraphic(tabLabel);
tab.setContextMenu(getTabContextMenu(tab));
tab.setClosable(true);
tab.setOnCloseRequest(event -> {
if(AppServices.getWhirlpoolServices().getWhirlpoolForMixToWallet(((WalletTabData)tab.getUserData()).getWalletForm().getWalletId()) != null) {
@ -1202,13 +1212,17 @@ public class AppController implements Initializable {
TabPane subTabs = new TabPane();
subTabs.setSide(Side.RIGHT);
subTabs.getStyleClass().add("master-only");
setSubTabsVisible(subTabs, false);
subTabs.rotateGraphicProperty().set(true);
tab.setContent(subTabs);
WalletForm walletForm = addWalletSubTab(subTabs, storage, wallet, backupWallet);
TabData tabData = new WalletTabData(TabData.TabType.WALLET, walletForm);
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) -> {
if(selectedTab != null) {
@ -1236,8 +1250,7 @@ public class AppController implements Initializable {
Label masterLabel = (Label)masterTab.getGraphic();
masterLabel.setText(getAutomaticName(wallet.getMasterWallet()));
Platform.runLater(() -> {
subTabs.getStyleClass().remove("master-only");
subTabs.getStyleClass().add("wallet-subtabs");
setSubTabsVisible(subTabs, true);
});
}
}
@ -1247,6 +1260,20 @@ public class AppController implements Initializable {
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) {
try {
Tab subTab = new Tab();
@ -1485,6 +1512,18 @@ public class AppController implements Initializable {
private ContextMenu getTabContextMenu(Tab tab) {
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");
close.setOnAction(event -> {
tabs.getTabs().remove(tab);
@ -1670,6 +1709,7 @@ public class AppController implements Initializable {
} else {
saveTransaction.setVisible(false);
}
lockWallet.setDisable(true);
exportWallet.setDisable(true);
showLoadingLog.setDisable(true);
showUtxosChart.setDisable(true);
@ -1679,6 +1719,7 @@ public class AppController implements Initializable {
WalletTabData walletTabData = walletTabEvent.getWalletTabData();
saveTransaction.setVisible(true);
saveTransaction.setDisable(true);
lockWallet.setDisable(walletTabData.getWalletForm().lockedProperty().get());
exportWallet.setDisable(walletTabData.getWallet() == null || !walletTabData.getWallet().isValid());
showLoadingLog.setDisable(false);
showUtxosChart.setDisable(false);
@ -1714,6 +1755,20 @@ public class AppController implements Initializable {
@Subscribe
public void newWalletTransactions(NewWalletTransactionsEvent event) {
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> whirlpoolTransactions = event.getWhirlpoolMixTransactions();
blockTransactions.removeAll(whirlpoolTransactions);
@ -2172,4 +2227,20 @@ public class AppController implements Initializable {
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 {
if(!walletFile.exists()) {
return false;
}
return persistence.isEncrypted(walletFile);
}
@ -531,10 +535,18 @@ public class Storage {
public static class KeyDerivationService extends Service<ECKey> {
private final Storage storage;
private final SecureString password;
private final boolean verifyPassword;
public KeyDerivationService(Storage storage, SecureString password) {
this.storage = storage;
this.password = password;
this.verifyPassword = false;
}
public KeyDerivationService(Storage storage, SecureString password, boolean verifyPassword) {
this.storage = storage;
this.password = password;
this.verifyPassword = verifyPassword;
}
@Override
@ -542,7 +554,12 @@ public class Storage {
return new Task<>() {
protected ECKey call() throws IOException, StorageException {
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 {
password.clear();
}

View file

@ -1,5 +1,5 @@
package com.sparrowwallet.sparrow.wallet;
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.StorageException;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
import javafx.application.Platform;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.event.ActionEvent;
@ -35,6 +36,8 @@ import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
public class SettingsController extends WalletFormController implements Initializable {
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);
Optional<SecureString> password = dlg.showAndWait();
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 -> {
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
ECKey encryptionFullKey = keyDerivationService.getValue();
@ -482,7 +485,14 @@ public class SettingsController extends WalletFormController implements Initiali
});
keyDerivationService.setOnFailed(workerStateEvent -> {
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..."));
keyDerivationService.start();

View file

@ -30,6 +30,7 @@ import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Tooltip;
import javafx.scene.control.TreeItem;
import javafx.scene.layout.HBox;
@ -47,6 +48,8 @@ import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
public class UtxosController extends WalletFormController implements Initializable {
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);
Optional<SecureString> password = dlg.showAndWait();
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 -> {
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
ECKey encryptionFullKey = keyDerivationService.getValue();
@ -259,7 +262,14 @@ public class UtxosController extends WalletFormController implements Initializab
});
keyDerivationService.setOnFailed(workerStateEvent -> {
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..."));
keyDerivationService.start();

View file

@ -1,33 +1,54 @@
package com.sparrowwallet.sparrow.wallet;
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.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.ReceiveActionEvent;
import com.sparrowwallet.sparrow.event.SendActionEvent;
import com.sparrowwallet.sparrow.event.WalletAddressesChangedEvent;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Storage;
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;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
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.net.URL;
import java.util.ResourceBundle;
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
public class WalletController extends WalletFormController implements Initializable {
private static final Logger log = LoggerFactory.getLogger(WalletController.class);
@FXML
private StackPane walletPane;
@FXML
private VBox walletMenuBox;
@FXML
private ToggleGroup walletMenu;
private BorderPane lockPane;
private final BooleanProperty walletEncryptedProperty = new SimpleBooleanProperty(false);
@Override
public void initialize(URL location, ResourceBundle resources) {
EventManager.get().register(this);
@ -47,7 +68,7 @@ public class WalletController extends WalletFormController implements Initializa
if(walletFunction.getUserData().equals(function)) {
existing = true;
walletFunction.setViewOrder(0);
} else {
} else if(function != Function.LOCK) {
walletFunction.setViewOrder(1);
}
}
@ -78,6 +99,9 @@ public class WalletController extends WalletFormController implements Initializa
toggleButton.managedProperty().bind(toggleButton.visibleProperty());
}
walletMenuBox.managedProperty().bind(walletMenuBox.visibleProperty());
walletMenuBox.visibleProperty().bind(getWalletForm().lockedProperty().not());
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
public void walletAddressesChanged(WalletAddressesChangedEvent event) {
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
public void receiveAction(ReceiveActionEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) {
@ -131,4 +232,27 @@ public class WalletController extends WalletFormController implements Initializa
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.net.ServerType;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.util.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -37,6 +39,8 @@ public class WalletForm {
private ElectrumServer.TransactionMempoolService transactionMempoolService;
private final BooleanProperty lockedProperty = new SimpleBooleanProperty(false);
public WalletForm(Storage storage, Wallet currentWallet, Wallet backupWallet) {
this(storage, currentWallet, backupWallet, true);
}
@ -58,6 +62,10 @@ public class WalletForm {
return wallet;
}
public Wallet getMasterWallet() {
return wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
}
public Storage getStorage() {
return storage;
}
@ -298,6 +306,14 @@ public class WalletForm {
return walletUtxosEntry;
}
public BooleanProperty lockedProperty() {
return lockedProperty;
}
public void setLocked(boolean locked) {
this.lockedProperty.set(locked);
}
@Subscribe
public void walletDataChanged(WalletDataChangedEvent event) {
if(event.getWallet().equals(wallet)) {

View file

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

View file

@ -100,6 +100,7 @@
<CheckMenuItem fx:id="showTxHex" mnemonicParsing="false" text="Show Transaction Hex" onAction="#showTxHex"/>
<SeparatorMenuItem />
<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"/>
</items>
</Menu>

View file

@ -88,7 +88,7 @@
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_TRIANGLE" styleClass="future-warning" />
</graphic>
<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>
</Label>
</Field>
@ -99,7 +99,7 @@
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_TRIANGLE" styleClass="future-warning" />
</graphic>
<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>
</Label>
</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">
<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">
<toggleGroup>
<ToggleGroup fx:id="walletMenu" />