diff --git a/drongo b/drongo index c9e57fad..9a9a1b92 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit c9e57fad018750a150a563cd17c8872d7cda48f0 +Subproject commit 9a9a1b925461ee2a8ae7f6885309b0babd4b6767 diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index b8a287cd..70245ebc 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -1232,7 +1232,7 @@ public class AppController implements Initializable { if(walletTabData.getWallet() == wallet.getMasterWallet()) { TabPane subTabs = (TabPane)walletTab.getContent(); addWalletSubTab(subTabs, storage, wallet, backupWallet); - Tab masterTab = subTabs.getTabs().get(0); + Tab masterTab = subTabs.getTabs().stream().filter(tab -> ((WalletTabData)tab.getUserData()).getWallet().isMasterWallet()).findFirst().orElse(subTabs.getTabs().get(0)); Label masterLabel = (Label)masterTab.getGraphic(); masterLabel.setText(getAutomaticName(wallet.getMasterWallet())); Platform.runLater(() -> { @@ -1271,6 +1271,11 @@ public class AppController implements Initializable { subTab.setUserData(tabData); subTabs.getTabs().add(subTab); + subTabs.getTabs().sort((o1, o2) -> { + WalletTabData tabData1 = (WalletTabData) o1.getUserData(); + WalletTabData tabData2 = (WalletTabData) o2.getUserData(); + return tabData1.getWallet().compareTo(tabData2.getWallet()); + }); subTabs.getSelectionModel().select(subTab); return walletForm; @@ -1298,7 +1303,7 @@ public class AppController implements Initializable { private String getAutomaticName(Wallet wallet) { int account = wallet.getAccountIndex(); - return account < 0 ? wallet.getName() : (!wallet.isWhirlpoolMasterWallet() || account > 1 ? "Account #" + account : "Deposit"); + return (account < 0 || account > 9) ? wallet.getName() : (!wallet.isWhirlpoolMasterWallet() || account > 1 ? "Account #" + account : "Deposit"); } public WalletForm getSelectedWalletForm() { diff --git a/src/main/java/com/sparrowwallet/sparrow/control/AddAccountDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/AddAccountDialog.java new file mode 100644 index 00000000..cb297090 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/AddAccountDialog.java @@ -0,0 +1,91 @@ +package com.sparrowwallet.sparrow.control; + +import com.sparrowwallet.drongo.wallet.StandardAccount; +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.AppServices; +import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; +import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices; +import javafx.collections.FXCollections; +import javafx.scene.control.*; +import javafx.scene.layout.VBox; +import javafx.util.StringConverter; +import org.controlsfx.glyphfont.Glyph; + +import java.util.ArrayList; +import java.util.List; + +public class AddAccountDialog extends Dialog { + private final ComboBox standardAccountCombo; + + public AddAccountDialog(Wallet wallet) { + final DialogPane dialogPane = getDialogPane(); + setTitle("Add Account"); + dialogPane.setHeaderText("Choose an account to add:"); + dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm()); + AppServices.setStageIcon(dialogPane.getScene().getWindow()); + dialogPane.getButtonTypes().addAll(ButtonType.CANCEL, ButtonType.OK); + dialogPane.setPrefWidth(380); + dialogPane.setPrefHeight(200); + AppServices.moveToActiveWindowScreen(this); + + Glyph key = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SORT_NUMERIC_DOWN); + key.setFontSize(50); + dialogPane.setGraphic(key); + + final VBox content = new VBox(10); + content.setPrefHeight(50); + + standardAccountCombo = new ComboBox<>(); + standardAccountCombo.setMaxWidth(Double.MAX_VALUE); + + List existingIndexes = new ArrayList<>(); + Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet(); + existingIndexes.add(masterWallet.getAccountIndex()); + for(Wallet childWallet : masterWallet.getChildWallets()) { + existingIndexes.add(childWallet.getAccountIndex()); + } + + List availableAccounts = new ArrayList<>(); + for(StandardAccount standardAccount : StandardAccount.values()) { + if(!existingIndexes.contains(standardAccount.getAccountNumber()) && !StandardAccount.WHIRLPOOL_ACCOUNTS.contains(standardAccount)) { + availableAccounts.add(standardAccount); + } + } + + if(WhirlpoolServices.canWalletMix(masterWallet) && !masterWallet.isWhirlpoolMasterWallet()) { + availableAccounts.add(StandardAccount.WHIRLPOOL_PREMIX); + } + + standardAccountCombo.setItems(FXCollections.observableList(availableAccounts)); + standardAccountCombo.setConverter(new StringConverter<>() { + @Override + public String toString(StandardAccount account) { + if(account == null) { + return "None Available"; + } + + if(StandardAccount.WHIRLPOOL_ACCOUNTS.contains(account)) { + return "Whirlpool Accounts"; + } + + return account.getName(); + } + + @Override + public StandardAccount fromString(String string) { + return null; + } + }); + + if(standardAccountCombo.getItems().isEmpty()) { + Button okButton = (Button) dialogPane.lookupButton(ButtonType.OK); + okButton.setDisable(true); + } else { + standardAccountCombo.getSelectionModel().select(0); + } + content.getChildren().add(standardAccountCombo); + + dialogPane.setContent(content); + setResultConverter(dialogButton -> dialogButton == ButtonType.OK ? standardAccountCombo.getValue() : null); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java index 0d815697..792aebab 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java @@ -52,7 +52,7 @@ public class DevicePane extends TitledDescriptionPane { private Button unlockButton; private Button enterPinButton; private Button setPassphraseButton; - private SplitMenuButton importButton; + private ButtonBase importButton; private Button signButton; private Button displayAddressButton; private Button signMessageButton; @@ -61,13 +61,13 @@ public class DevicePane extends TitledDescriptionPane { private boolean defaultDevice; - public DevicePane(Wallet wallet, Device device, boolean defaultDevice) { + public DevicePane(Wallet wallet, Device device, boolean defaultDevice, KeyDerivation requiredDerivation) { super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png"); this.deviceOperation = DeviceOperation.IMPORT; this.wallet = wallet; this.psbt = null; this.outputDescriptor = null; - this.keyDerivation = null; + this.keyDerivation = requiredDerivation; this.message = null; this.device = device; this.defaultDevice = defaultDevice; @@ -199,35 +199,39 @@ public class DevicePane extends TitledDescriptionPane { } private void createImportButton() { - importButton = new SplitMenuButton(); + importButton = keyDerivation == null ? new SplitMenuButton() : new Button(); importButton.setAlignment(Pos.CENTER_RIGHT); importButton.setText("Import Keystore"); importButton.setOnAction(event -> { importButton.setDisable(true); - importKeystore(wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation()); + List defaultDerivation = wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation(); + importKeystore(keyDerivation == null ? defaultDerivation : keyDerivation.getDerivation()); }); - if(wallet.getScriptType() == null) { - ScriptType[] scriptTypes = new ScriptType[] {ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH}; - for(ScriptType scriptType : scriptTypes) { - MenuItem item = new MenuItem(scriptType.getDescription()); - final List derivation = scriptType.getDefaultDerivation(); - item.setOnAction(event -> { - importButton.setDisable(true); - importKeystore(derivation); - }); - importButton.getItems().add(item); - } - } else { - String[] accounts = new String[] {"Default Account #0", "Account #1", "Account #2", "Account #3", "Account #4", "Account #5", "Account #6", "Account #7", "Account #8", "Account #9"}; - int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length; - for(int i = 0; i < scriptAccountsLength; i++) { - MenuItem item = new MenuItem(accounts[i]); - final List derivation = wallet.getScriptType().getDefaultDerivation(i); - item.setOnAction(event -> { - importButton.setDisable(true); - importKeystore(derivation); - }); - importButton.getItems().add(item); + + if(importButton instanceof SplitMenuButton importMenuButton) { + if(wallet.getScriptType() == null) { + ScriptType[] scriptTypes = new ScriptType[] {ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH}; + for(ScriptType scriptType : scriptTypes) { + MenuItem item = new MenuItem(scriptType.getDescription()); + final List derivation = scriptType.getDefaultDerivation(); + item.setOnAction(event -> { + importMenuButton.setDisable(true); + importKeystore(derivation); + }); + importMenuButton.getItems().add(item); + } + } else { + String[] accounts = new String[] {"Default Account #0", "Account #1", "Account #2", "Account #3", "Account #4", "Account #5", "Account #6", "Account #7", "Account #8", "Account #9"}; + int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length; + for(int i = 0; i < scriptAccountsLength; i++) { + MenuItem item = new MenuItem(accounts[i]); + final List derivation = wallet.getScriptType().getDefaultDerivation(i); + item.setOnAction(event -> { + importMenuButton.setDisable(true); + importKeystore(derivation); + }); + importMenuButton.getItems().add(item); + } } } importButton.managedProperty().bind(importButton.visibleProperty()); @@ -430,7 +434,9 @@ public class DevicePane extends TitledDescriptionPane { setExpanded(true); } else { showOperationButton(); - setContent(getTogglePassphraseOn()); + if(!deviceOperation.equals(DeviceOperation.IMPORT)) { + setContent(getTogglePassphraseOn()); + } } } else { setError("Incorrect PIN", null); @@ -622,7 +628,8 @@ public class DevicePane extends TitledDescriptionPane { importButton.setVisible(true); showHideLink.setText("Show derivation..."); showHideLink.setVisible(true); - setContent(getDerivationEntry(wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation())); + List defaultDerivation = wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation(); + setContent(getDerivationEntry(keyDerivation == null ? defaultDerivation : keyDerivation.getDerivation())); } else if(deviceOperation.equals(DeviceOperation.SIGN)) { signButton.setDefaultButton(defaultDevice); signButton.setVisible(true); @@ -642,6 +649,7 @@ public class DevicePane extends TitledDescriptionPane { TextField derivationField = new TextField(); derivationField.setPromptText("Derivation path"); derivationField.setText(KeyDerivation.writePath(derivation)); + derivationField.setDisable(keyDerivation != null); HBox.setHgrow(derivationField, Priority.ALWAYS); ValidationSupport validationSupport = new ValidationSupport(); @@ -651,7 +659,8 @@ public class DevicePane extends TitledDescriptionPane { (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue)) )); - Button importDerivationButton = new Button("Import"); + Button importDerivationButton = new Button("Import Custom Derivation"); + importDerivationButton.setDisable(true); importDerivationButton.setOnAction(event -> { showHideLink.setVisible(true); setExpanded(false); @@ -660,7 +669,8 @@ public class DevicePane extends TitledDescriptionPane { }); derivationField.textProperty().addListener((observable, oldValue, newValue) -> { - importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue)); + importButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || !KeyDerivation.parsePath(newValue).equals(derivation)); + importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || KeyDerivation.parsePath(newValue).equals(derivation)); }); HBox contentBox = new HBox(); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/FileKeystoreImportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/FileKeystoreImportPane.java index a01f41a5..3006eb6d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/FileKeystoreImportPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/FileKeystoreImportPane.java @@ -1,5 +1,6 @@ package com.sparrowwallet.sparrow.control; +import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.EventManager; @@ -12,11 +13,13 @@ import java.io.*; public class FileKeystoreImportPane extends FileImportPane { protected final Wallet wallet; private final KeystoreFileImport importer; + private final KeyDerivation requiredDerivation; - public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer) { + public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer, KeyDerivation requiredDerivation) { super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable()); this.wallet = wallet; this.importer = importer; + this.requiredDerivation = requiredDerivation; } protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException { @@ -25,6 +28,10 @@ public class FileKeystoreImportPane extends FileImportPane { keystore = importer.getKeystore(wallet.getScriptType(), inputStream, password); } - EventManager.get().post(new KeystoreImportEvent(keystore)); + if(requiredDerivation != null && !requiredDerivation.getDerivation().equals(keystore.getKeyDerivation().getDerivation())) { + setError("Incorrect derivation", "This account requires a derivation of " + requiredDerivation.getDerivationPath() + ", but the imported keystore has a derivation of " + keystore.getKeyDerivation().getDerivationPath() + "."); + } else { + EventManager.get().post(new KeystoreImportEvent(keystore)); + } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java index 4eb87fe9..3b91c4c0 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java @@ -95,7 +95,7 @@ public class WalletImportDialog extends Dialog { List devices = enumerateService.getValue(); importAccordion.getPanes().removeIf(titledPane -> titledPane instanceof DevicePane); for(Device device : devices) { - DevicePane devicePane = new DevicePane(new Wallet(), device, devices.size() == 1); + DevicePane devicePane = new DevicePane(new Wallet(), device, devices.size() == 1, null); importAccordion.getPanes().add(0, devicePane); } Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices))); diff --git a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java index af17d16b..b4221203 100644 --- a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java +++ b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java @@ -62,6 +62,7 @@ public class FontAwesome5 extends GlyphFont { SIGN_OUT_ALT('\uf2f5'), SQUARE('\uf0c8'), SNOWFLAKE('\uf2dc'), + SORT_NUMERIC_DOWN('\uf162'), SUN('\uf185'), THEATER_MASKS('\uf630'), TIMES_CIRCLE('\uf057'), diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ColdcardMultisig.java b/src/main/java/com/sparrowwallet/sparrow/io/ColdcardMultisig.java index 75c9795c..b1807c69 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ColdcardMultisig.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ColdcardMultisig.java @@ -42,23 +42,27 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle keystore.setSource(KeystoreSource.HW_AIRGAPPED); keystore.setWalletModel(WalletModel.COLDCARD); - if(cck.xpub != null && cck.path != null) { - ExtendedKey.Header header = ExtendedKey.Header.fromExtendedKey(cck.xpub); - if(header.getDefaultScriptType() != scriptType) { - throw new ImportException("This wallet's script type (" + scriptType + ") does not match the " + getName() + " script type (" + header.getDefaultScriptType() + ")"); + try { + if(cck.xpub != null && cck.path != null) { + ExtendedKey.Header header = ExtendedKey.Header.fromExtendedKey(cck.xpub); + if(header.getDefaultScriptType() != scriptType) { + throw new ImportException("This wallet's script type (" + scriptType + ") does not match the " + getName() + " script type (" + header.getDefaultScriptType() + ")"); + } + keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.path)); + keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.xpub)); + } else if(scriptType.equals(ScriptType.P2SH)) { + keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2sh_deriv)); + keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2sh)); + } else if(scriptType.equals(ScriptType.P2SH_P2WSH)) { + keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_p2sh_deriv != null ? cck.p2wsh_p2sh_deriv : cck.p2sh_p2wsh_deriv)); + keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh_p2sh != null ? cck.p2wsh_p2sh : cck.p2sh_p2wsh)); + } else if(scriptType.equals(ScriptType.P2WSH)) { + keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_deriv)); + keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh)); + } else { + throw new ImportException("Correct derivation not found for script type: " + scriptType); } - keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.path)); - keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.xpub)); - } else if(scriptType.equals(ScriptType.P2SH)) { - keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2sh_deriv)); - keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2sh)); - } else if(scriptType.equals(ScriptType.P2SH_P2WSH)) { - keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_p2sh_deriv != null ? cck.p2wsh_p2sh_deriv : cck.p2sh_p2wsh_deriv)); - keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh_p2sh != null ? cck.p2wsh_p2sh : cck.p2sh_p2wsh)); - } else if(scriptType.equals(ScriptType.P2WSH)) { - keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_deriv)); - keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh)); - } else { + } catch(NullPointerException e) { throw new ImportException("Correct derivation not found for script type: " + scriptType); } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/WalletBackupAndKey.java b/src/main/java/com/sparrowwallet/sparrow/io/WalletBackupAndKey.java index c3e4a06c..9bb8506f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/WalletBackupAndKey.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/WalletBackupAndKey.java @@ -51,10 +51,6 @@ public class WalletBackupAndKey implements Comparable { @Override public int compareTo(WalletBackupAndKey other) { - if(wallet.getStandardAccountType() != null && other.wallet.getStandardAccountType() != null) { - return wallet.getStandardAccountType().ordinal() - other.wallet.getStandardAccountType().ordinal(); - } - - return wallet.getAccountIndex() - other.wallet.getAccountIndex(); + return wallet.compareTo(other.wallet); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/HwAirgappedController.java b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/HwAirgappedController.java index ca540a21..c35b7342 100644 --- a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/HwAirgappedController.java +++ b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/HwAirgappedController.java @@ -23,9 +23,11 @@ public class HwAirgappedController extends KeystoreImportDetailController { importers = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new SpecterDIY()); } - for(KeystoreImport importer : importers) { - FileKeystoreImportPane importPane = new FileKeystoreImportPane(getMasterController().getWallet(), (KeystoreFileImport)importer);; - importAccordion.getPanes().add(importPane); + for(KeystoreFileImport importer : importers) { + FileKeystoreImportPane importPane = new FileKeystoreImportPane(getMasterController().getWallet(), importer, getMasterController().getRequiredDerivation()); + if(getMasterController().getRequiredModel() == null || getMasterController().getRequiredModel() == importer.getWalletModel()) { + importAccordion.getPanes().add(importPane); + } } importAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle())); diff --git a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/HwUsbDevicesController.java b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/HwUsbDevicesController.java index c2d7f9c4..c99f9a9b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/HwUsbDevicesController.java +++ b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/HwUsbDevicesController.java @@ -13,8 +13,10 @@ public class HwUsbDevicesController extends KeystoreImportDetailController { public void initializeView(List devices) { for(Device device : devices) { - DevicePane devicePane = new DevicePane(getMasterController().getWallet(), device, devices.size() == 1); - deviceAccordion.getPanes().add(devicePane); + DevicePane devicePane = new DevicePane(getMasterController().getWallet(), device, devices.size() == 1, getMasterController().getRequiredDerivation()); + if(getMasterController().getRequiredModel() == null || getMasterController().getRequiredModel() == device.getModel()) { + deviceAccordion.getPanes().add(devicePane); + } } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportController.java b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportController.java index b17d620a..209c534b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportController.java +++ b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportController.java @@ -1,7 +1,9 @@ package com.sparrowwallet.sparrow.keystoreimport; +import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.wallet.KeystoreSource; import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.drongo.wallet.WalletModel; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.io.Device; import javafx.application.Platform; @@ -10,6 +12,7 @@ import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; import javafx.scene.Node; import javafx.scene.control.Toggle; +import javafx.scene.control.ToggleButton; import javafx.scene.control.ToggleGroup; import javafx.scene.layout.StackPane; @@ -27,6 +30,9 @@ public class KeystoreImportController implements Initializable { @FXML private StackPane importPane; + private KeyDerivation requiredDerivation; + private WalletModel requiredModel; + @Override public void initialize(URL location, ResourceBundle resources) { @@ -53,11 +59,12 @@ public class KeystoreImportController implements Initializable { }); } - public void selectSource(KeystoreSource keystoreSource) { + public void selectSource(KeystoreSource keystoreSource, boolean required) { for(Toggle toggle : importMenu.getToggles()) { if(toggle.getUserData().equals(keystoreSource)) { Platform.runLater(() -> importMenu.selectToggle(toggle)); - return; + } else if(required) { + ((ToggleButton)toggle).setDisable(true); } } } @@ -96,4 +103,20 @@ public class KeystoreImportController implements Initializable { throw new IllegalStateException("Can't find pane", e); } } + + public KeyDerivation getRequiredDerivation() { + return requiredDerivation; + } + + public void setRequiredDerivation(KeyDerivation requiredDerivation) { + this.requiredDerivation = requiredDerivation; + } + + public WalletModel getRequiredModel() { + return requiredModel; + } + + public void setRequiredModel(WalletModel requiredModel) { + this.requiredModel = requiredModel; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportDialog.java b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportDialog.java index 617a3044..62822d5c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportDialog.java @@ -26,6 +26,10 @@ public class KeystoreImportDialog extends Dialog { } public KeystoreImportDialog(Wallet wallet, KeystoreSource initialSource) { + this(wallet, initialSource, null, null, false); + } + + public KeystoreImportDialog(Wallet wallet, KeystoreSource initialSource, KeyDerivation requiredDerivation, WalletModel requiredModel, boolean restrictSource) { EventManager.get().register(this); setOnCloseRequest(event -> { EventManager.get().unregister(this); @@ -39,11 +43,16 @@ public class KeystoreImportDialog extends Dialog { dialogPane.setContent(Borders.wrap(ksiLoader.load()).emptyBorder().buildAll()); keystoreImportController = ksiLoader.getController(); keystoreImportController.initializeView(wallet); - keystoreImportController.selectSource(initialSource); + keystoreImportController.selectSource(initialSource, restrictSource); + keystoreImportController.setRequiredDerivation(requiredDerivation); + keystoreImportController.setRequiredModel(requiredModel); final ButtonType watchOnlyButtonType = new javafx.scene.control.ButtonType(Network.get().getXpubHeader().getDisplayName() + " / Watch Only Wallet", ButtonBar.ButtonData.LEFT); + if(!restrictSource) { + dialogPane.getButtonTypes().add(watchOnlyButtonType); + } final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); - dialogPane.getButtonTypes().addAll(watchOnlyButtonType, cancelButtonType); + dialogPane.getButtonTypes().add(cancelButtonType); dialogPane.setPrefWidth(650); dialogPane.setPrefHeight(690); AppServices.moveToActiveWindowScreen(this); diff --git a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/SwController.java b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/SwController.java index b7ed6b66..4ede87a6 100644 --- a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/SwController.java +++ b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/SwController.java @@ -21,7 +21,7 @@ public class SwController extends KeystoreImportDetailController { TitledDescriptionPane importPane = null; if(importer instanceof KeystoreFileImport) { - importPane = new FileKeystoreImportPane(getMasterController().getWallet(), (KeystoreFileImport)importer); + importPane = new FileKeystoreImportPane(getMasterController().getWallet(), (KeystoreFileImport)importer, getMasterController().getRequiredDerivation()); } else if(importer instanceof KeystoreMnemonicImport) { importPane = new MnemonicKeystoreImportPane(getMasterController().getWallet(), (KeystoreMnemonicImport)importer); } else if(importer instanceof KeystoreXprvImport) { diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java index 92ec4c97..89b5989a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java @@ -5,17 +5,15 @@ import com.sparrowwallet.drongo.*; import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.KeystoreSource; import com.sparrowwallet.drongo.wallet.Wallet; +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.ChildWalletAddedEvent; -import com.sparrowwallet.sparrow.event.StorageEvent; -import com.sparrowwallet.sparrow.event.TimedEvent; +import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands; import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.keystoreimport.KeystoreImportDialog; -import com.sparrowwallet.sparrow.event.SettingsChangedEvent; import javafx.application.Platform; import javafx.event.ActionEvent; import javafx.fxml.FXML; @@ -104,6 +102,14 @@ public class KeystoreController extends WalletFormController implements Initiali selectSourcePane.managedProperty().bind(selectSourcePane.visibleProperty()); if(keystore.isValid() || keystore.getExtendedPublicKey() != null) { selectSourcePane.setVisible(false); + } else if(!getWalletForm().getWallet().isMasterWallet() && keystore.getKeyDerivation() != null) { + Wallet masterWallet = getWalletForm().getWallet().getMasterWallet(); + int keystoreIndex = getWalletForm().getWallet().getKeystores().indexOf(keystore); + KeystoreSource keystoreSource = masterWallet.getKeystores().get(keystoreIndex).getSource(); + for(Toggle toggle : keystoreSourceToggleGroup.getToggles()) { + ToggleButton toggleButton = (ToggleButton)toggle; + toggleButton.setDisable(toggleButton.getUserData() != keystoreSource); + } } viewSeedButton.managedProperty().bind(viewSeedButton.visibleProperty()); @@ -163,7 +169,7 @@ public class KeystoreController extends WalletFormController implements Initiali scanXpubQR.setVisible(!valid); }); - setInputFieldsDisabled(!walletForm.getWallet().isMasterWallet() || !walletForm.getWallet().getChildWallets().isEmpty()); + setInputFieldsDisabled(keystore.getSource() != KeystoreSource.SW_WATCH && (!walletForm.getWallet().isMasterWallet() || !walletForm.getWallet().getChildWallets().isEmpty())); } private void setXpubContext(ExtendedKey extendedKey) { @@ -310,7 +316,10 @@ public class KeystoreController extends WalletFormController implements Initiali } private void launchImportDialog(KeystoreSource initialSource) { - KeystoreImportDialog dlg = new KeystoreImportDialog(getWalletForm().getWallet(), initialSource); + boolean restrictSource = keystoreSourceToggleGroup.getToggles().stream().anyMatch(toggle -> ((ToggleButton)toggle).isDisabled()); + KeyDerivation requiredDerivation = restrictSource ? keystore.getKeyDerivation() : null; + WalletModel requiredModel = restrictSource ? keystore.getWalletModel() : null; + KeystoreImportDialog dlg = new KeystoreImportDialog(getWalletForm().getWallet(), initialSource, requiredDerivation, requiredModel, restrictSource); Optional result = dlg.showAndWait(); if(result.isPresent()) { selectSourcePane.setVisible(false); @@ -421,7 +430,7 @@ public class KeystoreController extends WalletFormController implements Initiali @Subscribe public void childWalletAdded(ChildWalletAddedEvent event) { if(event.getMasterWalletId().equals(walletForm.getWalletId())) { - setInputFieldsDisabled(true); + setInputFieldsDisabled(keystore.getSource() != KeystoreSource.SW_WATCH); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java index 6bc166f1..271d50dd 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java @@ -6,10 +6,7 @@ import com.sparrowwallet.drongo.crypto.*; import com.sparrowwallet.drongo.policy.Policy; import com.sparrowwallet.drongo.policy.PolicyType; import com.sparrowwallet.drongo.protocol.ScriptType; -import com.sparrowwallet.drongo.wallet.Keystore; -import com.sparrowwallet.drongo.wallet.KeystoreSource; -import com.sparrowwallet.drongo.wallet.Wallet; -import com.sparrowwallet.drongo.wallet.WalletModel; +import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.hummingbird.UR; import com.sparrowwallet.hummingbird.registry.*; import com.sparrowwallet.sparrow.AppServices; @@ -18,6 +15,7 @@ import com.sparrowwallet.sparrow.control.*; 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.beans.property.SimpleIntegerProperty; import javafx.collections.FXCollections; import javafx.event.ActionEvent; @@ -32,7 +30,6 @@ 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.*; @@ -79,7 +76,11 @@ public class SettingsController extends WalletFormController implements Initiali private TabPane keystoreTabs; - @FXML Button export; + @FXML + private Button export; + + @FXML + private Button addAccount; @FXML private Button apply; @@ -254,6 +255,7 @@ public class SettingsController extends WalletFormController implements Initiali scanDescriptorQR.setVisible(!walletForm.getWallet().isValid()); export.setDisable(!walletForm.getWallet().isValid()); + addAccount.setDisable(!walletForm.getWallet().isValid()); revert.setDisable(true); apply.setDisable(true); } @@ -442,6 +444,79 @@ public class SettingsController extends WalletFormController implements Initiali } } + public void addAccount(ActionEvent event) { + Wallet openWallet = AppServices.get().getOpenWallets().entrySet().stream().filter(entry -> walletForm.getWalletFile().equals(entry.getValue().getWalletFile())).map(Map.Entry::getKey).findFirst().orElseThrow(); + Wallet masterWallet = openWallet.isMasterWallet() ? openWallet : openWallet.getMasterWallet(); + + AddAccountDialog addAccountDialog = new AddAccountDialog(masterWallet); + Optional optAccount = addAccountDialog.showAndWait(); + if(optAccount.isPresent()) { + StandardAccount standardAccount = optAccount.get(); + + if(masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)) { + if(masterWallet.isEncrypted()) { + String walletId = walletForm.getWalletId(); + WalletPasswordDialog dlg = new WalletPasswordDialog(masterWallet.getName(), WalletPasswordDialog.PasswordRequirement.LOAD); + Optional password = dlg.showAndWait(); + if(password.isPresent()) { + Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(walletForm.getStorage(), password.get()); + keyDerivationService.setOnSucceeded(workerStateEvent -> { + EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done")); + ECKey encryptionFullKey = keyDerivationService.getValue(); + Key key = new Key(encryptionFullKey.getPrivKeyBytes(), walletForm.getStorage().getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2); + masterWallet.decrypt(key); + + try { + addAndSaveAccount(masterWallet, standardAccount); + } finally { + masterWallet.encrypt(key); + for(Wallet childWallet : masterWallet.getChildWallets()) { + if(!childWallet.isEncrypted()) { + childWallet.encrypt(key); + } + } + key.clear(); + encryptionFullKey.clear(); + password.get().clear(); + } + }); + keyDerivationService.setOnFailed(workerStateEvent -> { + EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed")); + AppServices.showErrorDialog("Incorrect Password", keyDerivationService.getException().getMessage()); + }); + EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet...")); + keyDerivationService.start(); + } + } else { + addAndSaveAccount(masterWallet, standardAccount); + } + } else { + Wallet childWallet = masterWallet.addChildWallet(standardAccount); + EventManager.get().post(new ChildWalletAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet)); + } + } + } + + private void addAndSaveAccount(Wallet masterWallet, StandardAccount standardAccount) { + if(StandardAccount.WHIRLPOOL_ACCOUNTS.contains(standardAccount)) { + WhirlpoolServices.prepareWhirlpoolWallet(masterWallet, getWalletForm().getWalletId(), getWalletForm().getStorage()); + } else { + Wallet childWallet = masterWallet.addChildWallet(standardAccount); + EventManager.get().post(new ChildWalletAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet)); + } + + for(Wallet childWallet : masterWallet.getChildWallets()) { + Storage storage = AppServices.get().getOpenWallets().get(childWallet); + if(!storage.isPersisted(childWallet)) { + try { + storage.saveWallet(childWallet); + } catch(Exception e) { + AppServices.showErrorDialog("Error saving wallet " + childWallet.getName(), e.getMessage()); + } + } + } + } + private void setInputFieldsDisabled(boolean disabled) { policyType.setDisable(disabled); scriptType.setDisable(disabled); @@ -479,6 +554,7 @@ public class SettingsController extends WalletFormController implements Initiali revert.setDisable(false); apply.setDisable(!wallet.isValid()); export.setDisable(true); + addAccount.setDisable(true); scanDescriptorQR.setVisible(!wallet.isValid()); } } @@ -487,6 +563,7 @@ public class SettingsController extends WalletFormController implements Initiali public void walletSettingsChanged(WalletSettingsChangedEvent event) { if(event.getWalletId().equals(walletForm.getWalletId())) { export.setDisable(!event.getWallet().isValid()); + addAccount.setDisable(!event.getWallet().isValid()); scanDescriptorQR.setVisible(!event.getWallet().isValid()); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/UtxosController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/UtxosController.java index 00d9dfd1..2ddcdbc9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/UtxosController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/UtxosController.java @@ -21,6 +21,7 @@ import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.whirlpool.Whirlpool; import com.sparrowwallet.sparrow.whirlpool.WhirlpoolDialog; +import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.WeakChangeListener; @@ -141,11 +142,7 @@ public class UtxosController extends WalletFormController implements Initializab } private boolean canWalletMix() { - return Whirlpool.WHIRLPOOL_NETWORKS.contains(Network.get()) - && getWalletForm().getWallet().getKeystores().size() == 1 - && getWalletForm().getWallet().getKeystores().get(0).hasSeed() - && getWalletForm().getWallet().getKeystores().get(0).getSeed().getType() == DeterministicSeed.Type.BIP39 - && !getWalletForm().getWallet().isWhirlpoolMixWallet(); + return WhirlpoolServices.canWalletMix(getWalletForm().getWallet()); } private void updateButtons(BitcoinUnit unit) { @@ -262,16 +259,7 @@ public class UtxosController extends WalletFormController implements Initializab } private void prepareWhirlpoolWallet(Wallet decryptedWallet) { - Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getWalletId()); - whirlpool.setScode(decryptedWallet.getMasterMixConfig().getScode()); - whirlpool.setHDWallet(getWalletForm().getWalletId(), decryptedWallet); - - for(StandardAccount whirlpoolAccount : StandardAccount.WHIRLPOOL_ACCOUNTS) { - if(decryptedWallet.getChildWallet(whirlpoolAccount) == null) { - Wallet childWallet = decryptedWallet.addChildWallet(whirlpoolAccount); - EventManager.get().post(new ChildWalletAddedEvent(getWalletForm().getStorage(), decryptedWallet, childWallet)); - } - } + WhirlpoolServices.prepareWhirlpoolWallet(decryptedWallet, getWalletForm().getWalletId(), getWalletForm().getStorage()); } private void previewPremix(Wallet wallet, Tx0Preview tx0Preview, List utxoEntries) { diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolServices.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolServices.java index 24d01d9a..b13e0fb2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolServices.java @@ -4,9 +4,12 @@ import com.google.common.eventbus.Subscribe; import com.google.common.net.HostAndPort; import com.samourai.whirlpool.client.wallet.WhirlpoolEventService; import com.sparrowwallet.drongo.Network; +import com.sparrowwallet.drongo.wallet.DeterministicSeed; import com.sparrowwallet.drongo.wallet.MixConfig; +import com.sparrowwallet.drongo.wallet.StandardAccount; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.AppServices; +import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.WalletTabData; import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.io.Config; @@ -123,6 +126,27 @@ public class WhirlpoolServices { return whirlpoolMap.values().stream().filter(whirlpool -> walletId.equals(whirlpool.getMixToWalletId())).findFirst().orElse(null); } + public static boolean canWalletMix(Wallet wallet) { + return Whirlpool.WHIRLPOOL_NETWORKS.contains(Network.get()) + && wallet.getKeystores().size() == 1 + && wallet.getKeystores().get(0).hasSeed() + && wallet.getKeystores().get(0).getSeed().getType() == DeterministicSeed.Type.BIP39 + && !wallet.isWhirlpoolMixWallet(); + } + + public static void prepareWhirlpoolWallet(Wallet decryptedWallet, String walletId, Storage storage) { + Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(walletId); + whirlpool.setScode(decryptedWallet.getMasterMixConfig().getScode()); + whirlpool.setHDWallet(walletId, decryptedWallet); + + for(StandardAccount whirlpoolAccount : StandardAccount.WHIRLPOOL_ACCOUNTS) { + if(decryptedWallet.getChildWallet(whirlpoolAccount) == null) { + Wallet childWallet = decryptedWallet.addChildWallet(whirlpoolAccount); + EventManager.get().post(new ChildWalletAddedEvent(storage, decryptedWallet, childWallet)); + } + } + } + @Subscribe public void newConnection(ConnectionEvent event) { startAllWhirlpool(); diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/settings.fxml b/src/main/resources/com/sparrowwallet/sparrow/wallet/settings.fxml index 5a8c3fe9..22d7cb79 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/settings.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/settings.fxml @@ -123,6 +123,7 @@