add multiple account functionality

This commit is contained in:
Craig Raw 2021-09-29 10:11:51 +02:00
parent 429b733140
commit 58e3b9dcdd
19 changed files with 345 additions and 96 deletions

2
drongo

@ -1 +1 @@
Subproject commit c9e57fad018750a150a563cd17c8872d7cda48f0 Subproject commit 9a9a1b925461ee2a8ae7f6885309b0babd4b6767

View file

@ -1232,7 +1232,7 @@ public class AppController implements Initializable {
if(walletTabData.getWallet() == wallet.getMasterWallet()) { if(walletTabData.getWallet() == wallet.getMasterWallet()) {
TabPane subTabs = (TabPane)walletTab.getContent(); TabPane subTabs = (TabPane)walletTab.getContent();
addWalletSubTab(subTabs, storage, wallet, backupWallet); 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(); Label masterLabel = (Label)masterTab.getGraphic();
masterLabel.setText(getAutomaticName(wallet.getMasterWallet())); masterLabel.setText(getAutomaticName(wallet.getMasterWallet()));
Platform.runLater(() -> { Platform.runLater(() -> {
@ -1271,6 +1271,11 @@ public class AppController implements Initializable {
subTab.setUserData(tabData); subTab.setUserData(tabData);
subTabs.getTabs().add(subTab); 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); subTabs.getSelectionModel().select(subTab);
return walletForm; return walletForm;
@ -1298,7 +1303,7 @@ public class AppController implements Initializable {
private String getAutomaticName(Wallet wallet) { private String getAutomaticName(Wallet wallet) {
int account = wallet.getAccountIndex(); 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() { public WalletForm getSelectedWalletForm() {

View file

@ -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<StandardAccount> {
private final ComboBox<StandardAccount> 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<Integer> existingIndexes = new ArrayList<>();
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
existingIndexes.add(masterWallet.getAccountIndex());
for(Wallet childWallet : masterWallet.getChildWallets()) {
existingIndexes.add(childWallet.getAccountIndex());
}
List<StandardAccount> 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);
}
}

View file

@ -52,7 +52,7 @@ public class DevicePane extends TitledDescriptionPane {
private Button unlockButton; private Button unlockButton;
private Button enterPinButton; private Button enterPinButton;
private Button setPassphraseButton; private Button setPassphraseButton;
private SplitMenuButton importButton; private ButtonBase importButton;
private Button signButton; private Button signButton;
private Button displayAddressButton; private Button displayAddressButton;
private Button signMessageButton; private Button signMessageButton;
@ -61,13 +61,13 @@ public class DevicePane extends TitledDescriptionPane {
private boolean defaultDevice; 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"); super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.IMPORT; this.deviceOperation = DeviceOperation.IMPORT;
this.wallet = wallet; this.wallet = wallet;
this.psbt = null; this.psbt = null;
this.outputDescriptor = null; this.outputDescriptor = null;
this.keyDerivation = null; this.keyDerivation = requiredDerivation;
this.message = null; this.message = null;
this.device = device; this.device = device;
this.defaultDevice = defaultDevice; this.defaultDevice = defaultDevice;
@ -199,23 +199,26 @@ public class DevicePane extends TitledDescriptionPane {
} }
private void createImportButton() { private void createImportButton() {
importButton = new SplitMenuButton(); importButton = keyDerivation == null ? new SplitMenuButton() : new Button();
importButton.setAlignment(Pos.CENTER_RIGHT); importButton.setAlignment(Pos.CENTER_RIGHT);
importButton.setText("Import Keystore"); importButton.setText("Import Keystore");
importButton.setOnAction(event -> { importButton.setOnAction(event -> {
importButton.setDisable(true); importButton.setDisable(true);
importKeystore(wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation()); List<ChildNumber> defaultDerivation = wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
importKeystore(keyDerivation == null ? defaultDerivation : keyDerivation.getDerivation());
}); });
if(importButton instanceof SplitMenuButton importMenuButton) {
if(wallet.getScriptType() == null) { if(wallet.getScriptType() == null) {
ScriptType[] scriptTypes = new ScriptType[] {ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH}; ScriptType[] scriptTypes = new ScriptType[] {ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH};
for(ScriptType scriptType : scriptTypes) { for(ScriptType scriptType : scriptTypes) {
MenuItem item = new MenuItem(scriptType.getDescription()); MenuItem item = new MenuItem(scriptType.getDescription());
final List<ChildNumber> derivation = scriptType.getDefaultDerivation(); final List<ChildNumber> derivation = scriptType.getDefaultDerivation();
item.setOnAction(event -> { item.setOnAction(event -> {
importButton.setDisable(true); importMenuButton.setDisable(true);
importKeystore(derivation); importKeystore(derivation);
}); });
importButton.getItems().add(item); importMenuButton.getItems().add(item);
} }
} else { } 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"}; 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"};
@ -224,10 +227,11 @@ public class DevicePane extends TitledDescriptionPane {
MenuItem item = new MenuItem(accounts[i]); MenuItem item = new MenuItem(accounts[i]);
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i); final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
item.setOnAction(event -> { item.setOnAction(event -> {
importButton.setDisable(true); importMenuButton.setDisable(true);
importKeystore(derivation); importKeystore(derivation);
}); });
importButton.getItems().add(item); importMenuButton.getItems().add(item);
}
} }
} }
importButton.managedProperty().bind(importButton.visibleProperty()); importButton.managedProperty().bind(importButton.visibleProperty());
@ -430,8 +434,10 @@ public class DevicePane extends TitledDescriptionPane {
setExpanded(true); setExpanded(true);
} else { } else {
showOperationButton(); showOperationButton();
if(!deviceOperation.equals(DeviceOperation.IMPORT)) {
setContent(getTogglePassphraseOn()); setContent(getTogglePassphraseOn());
} }
}
} else { } else {
setError("Incorrect PIN", null); setError("Incorrect PIN", null);
unlockButton.setDisable(false); unlockButton.setDisable(false);
@ -622,7 +628,8 @@ public class DevicePane extends TitledDescriptionPane {
importButton.setVisible(true); importButton.setVisible(true);
showHideLink.setText("Show derivation..."); showHideLink.setText("Show derivation...");
showHideLink.setVisible(true); showHideLink.setVisible(true);
setContent(getDerivationEntry(wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation())); List<ChildNumber> defaultDerivation = wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
setContent(getDerivationEntry(keyDerivation == null ? defaultDerivation : keyDerivation.getDerivation()));
} else if(deviceOperation.equals(DeviceOperation.SIGN)) { } else if(deviceOperation.equals(DeviceOperation.SIGN)) {
signButton.setDefaultButton(defaultDevice); signButton.setDefaultButton(defaultDevice);
signButton.setVisible(true); signButton.setVisible(true);
@ -642,6 +649,7 @@ public class DevicePane extends TitledDescriptionPane {
TextField derivationField = new TextField(); TextField derivationField = new TextField();
derivationField.setPromptText("Derivation path"); derivationField.setPromptText("Derivation path");
derivationField.setText(KeyDerivation.writePath(derivation)); derivationField.setText(KeyDerivation.writePath(derivation));
derivationField.setDisable(keyDerivation != null);
HBox.setHgrow(derivationField, Priority.ALWAYS); HBox.setHgrow(derivationField, Priority.ALWAYS);
ValidationSupport validationSupport = new ValidationSupport(); 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)) (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 -> { importDerivationButton.setOnAction(event -> {
showHideLink.setVisible(true); showHideLink.setVisible(true);
setExpanded(false); setExpanded(false);
@ -660,7 +669,8 @@ public class DevicePane extends TitledDescriptionPane {
}); });
derivationField.textProperty().addListener((observable, oldValue, newValue) -> { 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(); HBox contentBox = new HBox();

View file

@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control; package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
@ -12,11 +13,13 @@ import java.io.*;
public class FileKeystoreImportPane extends FileImportPane { public class FileKeystoreImportPane extends FileImportPane {
protected final Wallet wallet; protected final Wallet wallet;
private final KeystoreFileImport importer; 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()); super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable());
this.wallet = wallet; this.wallet = wallet;
this.importer = importer; this.importer = importer;
this.requiredDerivation = requiredDerivation;
} }
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException { 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); keystore = importer.getKeystore(wallet.getScriptType(), inputStream, password);
} }
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)); EventManager.get().post(new KeystoreImportEvent(keystore));
} }
}
} }

View file

@ -95,7 +95,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
List<Device> devices = enumerateService.getValue(); List<Device> devices = enumerateService.getValue();
importAccordion.getPanes().removeIf(titledPane -> titledPane instanceof DevicePane); importAccordion.getPanes().removeIf(titledPane -> titledPane instanceof DevicePane);
for(Device device : devices) { 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); importAccordion.getPanes().add(0, devicePane);
} }
Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices))); Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices)));

View file

@ -62,6 +62,7 @@ public class FontAwesome5 extends GlyphFont {
SIGN_OUT_ALT('\uf2f5'), SIGN_OUT_ALT('\uf2f5'),
SQUARE('\uf0c8'), SQUARE('\uf0c8'),
SNOWFLAKE('\uf2dc'), SNOWFLAKE('\uf2dc'),
SORT_NUMERIC_DOWN('\uf162'),
SUN('\uf185'), SUN('\uf185'),
THEATER_MASKS('\uf630'), THEATER_MASKS('\uf630'),
TIMES_CIRCLE('\uf057'), TIMES_CIRCLE('\uf057'),

View file

@ -42,6 +42,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
keystore.setSource(KeystoreSource.HW_AIRGAPPED); keystore.setSource(KeystoreSource.HW_AIRGAPPED);
keystore.setWalletModel(WalletModel.COLDCARD); keystore.setWalletModel(WalletModel.COLDCARD);
try {
if(cck.xpub != null && cck.path != null) { if(cck.xpub != null && cck.path != null) {
ExtendedKey.Header header = ExtendedKey.Header.fromExtendedKey(cck.xpub); ExtendedKey.Header header = ExtendedKey.Header.fromExtendedKey(cck.xpub);
if(header.getDefaultScriptType() != scriptType) { if(header.getDefaultScriptType() != scriptType) {
@ -61,6 +62,9 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
} else { } else {
throw new ImportException("Correct derivation not found for script type: " + scriptType); throw new ImportException("Correct derivation not found for script type: " + scriptType);
} }
} catch(NullPointerException e) {
throw new ImportException("Correct derivation not found for script type: " + scriptType);
}
return keystore; return keystore;
} }

View file

@ -51,10 +51,6 @@ public class WalletBackupAndKey implements Comparable<WalletBackupAndKey> {
@Override @Override
public int compareTo(WalletBackupAndKey other) { public int compareTo(WalletBackupAndKey other) {
if(wallet.getStandardAccountType() != null && other.wallet.getStandardAccountType() != null) { return wallet.compareTo(other.wallet);
return wallet.getStandardAccountType().ordinal() - other.wallet.getStandardAccountType().ordinal();
}
return wallet.getAccountIndex() - other.wallet.getAccountIndex();
} }
} }

View file

@ -23,10 +23,12 @@ public class HwAirgappedController extends KeystoreImportDetailController {
importers = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new SpecterDIY()); importers = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new SpecterDIY());
} }
for(KeystoreImport importer : importers) { for(KeystoreFileImport importer : importers) {
FileKeystoreImportPane importPane = new FileKeystoreImportPane(getMasterController().getWallet(), (KeystoreFileImport)importer);; FileKeystoreImportPane importPane = new FileKeystoreImportPane(getMasterController().getWallet(), importer, getMasterController().getRequiredDerivation());
if(getMasterController().getRequiredModel() == null || getMasterController().getRequiredModel() == importer.getWalletModel()) {
importAccordion.getPanes().add(importPane); importAccordion.getPanes().add(importPane);
} }
}
importAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle())); importAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle()));
} }

View file

@ -13,8 +13,10 @@ public class HwUsbDevicesController extends KeystoreImportDetailController {
public void initializeView(List<Device> devices) { public void initializeView(List<Device> devices) {
for(Device device : devices) { for(Device device : devices) {
DevicePane devicePane = new DevicePane(getMasterController().getWallet(), device, devices.size() == 1); DevicePane devicePane = new DevicePane(getMasterController().getWallet(), device, devices.size() == 1, getMasterController().getRequiredDerivation());
if(getMasterController().getRequiredModel() == null || getMasterController().getRequiredModel() == device.getModel()) {
deviceAccordion.getPanes().add(devicePane); deviceAccordion.getPanes().add(devicePane);
} }
} }
}
} }

View file

@ -1,7 +1,9 @@
package com.sparrowwallet.sparrow.keystoreimport; package com.sparrowwallet.sparrow.keystoreimport;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.wallet.KeystoreSource; import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.io.Device; import com.sparrowwallet.sparrow.io.Device;
import javafx.application.Platform; import javafx.application.Platform;
@ -10,6 +12,7 @@ import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.Toggle; import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup; import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
@ -27,6 +30,9 @@ public class KeystoreImportController implements Initializable {
@FXML @FXML
private StackPane importPane; private StackPane importPane;
private KeyDerivation requiredDerivation;
private WalletModel requiredModel;
@Override @Override
public void initialize(URL location, ResourceBundle resources) { 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()) { for(Toggle toggle : importMenu.getToggles()) {
if(toggle.getUserData().equals(keystoreSource)) { if(toggle.getUserData().equals(keystoreSource)) {
Platform.runLater(() -> importMenu.selectToggle(toggle)); 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); 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;
}
} }

View file

@ -26,6 +26,10 @@ public class KeystoreImportDialog extends Dialog<Keystore> {
} }
public KeystoreImportDialog(Wallet wallet, KeystoreSource initialSource) { 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); EventManager.get().register(this);
setOnCloseRequest(event -> { setOnCloseRequest(event -> {
EventManager.get().unregister(this); EventManager.get().unregister(this);
@ -39,11 +43,16 @@ public class KeystoreImportDialog extends Dialog<Keystore> {
dialogPane.setContent(Borders.wrap(ksiLoader.load()).emptyBorder().buildAll()); dialogPane.setContent(Borders.wrap(ksiLoader.load()).emptyBorder().buildAll());
keystoreImportController = ksiLoader.getController(); keystoreImportController = ksiLoader.getController();
keystoreImportController.initializeView(wallet); 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); 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); 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.setPrefWidth(650);
dialogPane.setPrefHeight(690); dialogPane.setPrefHeight(690);
AppServices.moveToActiveWindowScreen(this); AppServices.moveToActiveWindowScreen(this);

View file

@ -21,7 +21,7 @@ public class SwController extends KeystoreImportDetailController {
TitledDescriptionPane importPane = null; TitledDescriptionPane importPane = null;
if(importer instanceof KeystoreFileImport) { 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) { } else if(importer instanceof KeystoreMnemonicImport) {
importPane = new MnemonicKeystoreImportPane(getMasterController().getWallet(), (KeystoreMnemonicImport)importer); importPane = new MnemonicKeystoreImportPane(getMasterController().getWallet(), (KeystoreMnemonicImport)importer);
} else if(importer instanceof KeystoreXprvImport) { } else if(importer instanceof KeystoreXprvImport) {

View file

@ -5,17 +5,15 @@ import com.sparrowwallet.drongo.*;
import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource; import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.ChildWalletAddedEvent; import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.event.StorageEvent;
import com.sparrowwallet.sparrow.event.TimedEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.keystoreimport.KeystoreImportDialog; import com.sparrowwallet.sparrow.keystoreimport.KeystoreImportDialog;
import com.sparrowwallet.sparrow.event.SettingsChangedEvent;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
import javafx.fxml.FXML; import javafx.fxml.FXML;
@ -104,6 +102,14 @@ public class KeystoreController extends WalletFormController implements Initiali
selectSourcePane.managedProperty().bind(selectSourcePane.visibleProperty()); selectSourcePane.managedProperty().bind(selectSourcePane.visibleProperty());
if(keystore.isValid() || keystore.getExtendedPublicKey() != null) { if(keystore.isValid() || keystore.getExtendedPublicKey() != null) {
selectSourcePane.setVisible(false); 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()); viewSeedButton.managedProperty().bind(viewSeedButton.visibleProperty());
@ -163,7 +169,7 @@ public class KeystoreController extends WalletFormController implements Initiali
scanXpubQR.setVisible(!valid); 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) { private void setXpubContext(ExtendedKey extendedKey) {
@ -310,7 +316,10 @@ public class KeystoreController extends WalletFormController implements Initiali
} }
private void launchImportDialog(KeystoreSource initialSource) { 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<Keystore> result = dlg.showAndWait(); Optional<Keystore> result = dlg.showAndWait();
if(result.isPresent()) { if(result.isPresent()) {
selectSourcePane.setVisible(false); selectSourcePane.setVisible(false);
@ -421,7 +430,7 @@ public class KeystoreController extends WalletFormController implements Initiali
@Subscribe @Subscribe
public void childWalletAdded(ChildWalletAddedEvent event) { public void childWalletAdded(ChildWalletAddedEvent event) {
if(event.getMasterWalletId().equals(walletForm.getWalletId())) { if(event.getMasterWalletId().equals(walletForm.getWalletId())) {
setInputFieldsDisabled(true); setInputFieldsDisabled(keystore.getSource() != KeystoreSource.SW_WATCH);
} }
} }

View file

@ -6,10 +6,7 @@ import com.sparrowwallet.drongo.crypto.*;
import com.sparrowwallet.drongo.policy.Policy; import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType; import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.hummingbird.UR; import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.hummingbird.registry.*; import com.sparrowwallet.hummingbird.registry.*;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
@ -18,6 +15,7 @@ import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*; 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 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;
@ -32,7 +30,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import tornadofx.control.Fieldset; import tornadofx.control.Fieldset;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.util.*; import java.util.*;
@ -79,7 +76,11 @@ public class SettingsController extends WalletFormController implements Initiali
private TabPane keystoreTabs; private TabPane keystoreTabs;
@FXML Button export; @FXML
private Button export;
@FXML
private Button addAccount;
@FXML @FXML
private Button apply; private Button apply;
@ -254,6 +255,7 @@ public class SettingsController extends WalletFormController implements Initiali
scanDescriptorQR.setVisible(!walletForm.getWallet().isValid()); scanDescriptorQR.setVisible(!walletForm.getWallet().isValid());
export.setDisable(!walletForm.getWallet().isValid()); export.setDisable(!walletForm.getWallet().isValid());
addAccount.setDisable(!walletForm.getWallet().isValid());
revert.setDisable(true); revert.setDisable(true);
apply.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<StandardAccount> 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<SecureString> 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) { private void setInputFieldsDisabled(boolean disabled) {
policyType.setDisable(disabled); policyType.setDisable(disabled);
scriptType.setDisable(disabled); scriptType.setDisable(disabled);
@ -479,6 +554,7 @@ public class SettingsController extends WalletFormController implements Initiali
revert.setDisable(false); revert.setDisable(false);
apply.setDisable(!wallet.isValid()); apply.setDisable(!wallet.isValid());
export.setDisable(true); export.setDisable(true);
addAccount.setDisable(true);
scanDescriptorQR.setVisible(!wallet.isValid()); scanDescriptorQR.setVisible(!wallet.isValid());
} }
} }
@ -487,6 +563,7 @@ public class SettingsController extends WalletFormController implements Initiali
public void walletSettingsChanged(WalletSettingsChangedEvent event) { public void walletSettingsChanged(WalletSettingsChangedEvent event) {
if(event.getWalletId().equals(walletForm.getWalletId())) { if(event.getWalletId().equals(walletForm.getWalletId())) {
export.setDisable(!event.getWallet().isValid()); export.setDisable(!event.getWallet().isValid());
addAccount.setDisable(!event.getWallet().isValid());
scanDescriptorQR.setVisible(!event.getWallet().isValid()); scanDescriptorQR.setVisible(!event.getWallet().isValid());
} }
} }

View file

@ -21,6 +21,7 @@ import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool; import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolDialog; import com.sparrowwallet.sparrow.whirlpool.WhirlpoolDialog;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
import javafx.beans.value.WeakChangeListener; import javafx.beans.value.WeakChangeListener;
@ -141,11 +142,7 @@ public class UtxosController extends WalletFormController implements Initializab
} }
private boolean canWalletMix() { private boolean canWalletMix() {
return Whirlpool.WHIRLPOOL_NETWORKS.contains(Network.get()) return WhirlpoolServices.canWalletMix(getWalletForm().getWallet());
&& getWalletForm().getWallet().getKeystores().size() == 1
&& getWalletForm().getWallet().getKeystores().get(0).hasSeed()
&& getWalletForm().getWallet().getKeystores().get(0).getSeed().getType() == DeterministicSeed.Type.BIP39
&& !getWalletForm().getWallet().isWhirlpoolMixWallet();
} }
private void updateButtons(BitcoinUnit unit) { private void updateButtons(BitcoinUnit unit) {
@ -262,16 +259,7 @@ public class UtxosController extends WalletFormController implements Initializab
} }
private void prepareWhirlpoolWallet(Wallet decryptedWallet) { private void prepareWhirlpoolWallet(Wallet decryptedWallet) {
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getWalletId()); WhirlpoolServices.prepareWhirlpoolWallet(decryptedWallet, getWalletForm().getWalletId(), getWalletForm().getStorage());
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));
}
}
} }
private void previewPremix(Wallet wallet, Tx0Preview tx0Preview, List<UtxoEntry> utxoEntries) { private void previewPremix(Wallet wallet, Tx0Preview tx0Preview, List<UtxoEntry> utxoEntries) {

View file

@ -4,9 +4,12 @@ import com.google.common.eventbus.Subscribe;
import com.google.common.net.HostAndPort; import com.google.common.net.HostAndPort;
import com.samourai.whirlpool.client.wallet.WhirlpoolEventService; import com.samourai.whirlpool.client.wallet.WhirlpoolEventService;
import com.sparrowwallet.drongo.Network; import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
import com.sparrowwallet.drongo.wallet.MixConfig; import com.sparrowwallet.drongo.wallet.MixConfig;
import com.sparrowwallet.drongo.wallet.StandardAccount;
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.WalletTabData; import com.sparrowwallet.sparrow.WalletTabData;
import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Config; 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); 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 @Subscribe
public void newConnection(ConnectionEvent event) { public void newConnection(ConnectionEvent event) {
startAllWhirlpool(); startAllWhirlpool();

View file

@ -123,6 +123,7 @@
</padding> </padding>
<HBox AnchorPane.leftAnchor="0" spacing="20"> <HBox AnchorPane.leftAnchor="0" spacing="20">
<Button fx:id="export" text="Export..." onAction="#exportWallet" /> <Button fx:id="export" text="Export..." onAction="#exportWallet" />
<Button fx:id="addAccount" text="Add Account..." onAction="#addAccount" />
</HBox> </HBox>
<HBox AnchorPane.rightAnchor="10" spacing="20"> <HBox AnchorPane.rightAnchor="10" spacing="20">
<Button text="Advanced..." onAction="#showAdvanced" /> <Button text="Advanced..." onAction="#showAdvanced" />