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()) {
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() {

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 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<ChildNumber> 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<ChildNumber> 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<ChildNumber> 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<ChildNumber> 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<ChildNumber> 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<ChildNumber> 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();

View file

@ -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));
}
}
}

View file

@ -95,7 +95,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
List<Device> 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)));

View file

@ -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'),

View file

@ -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);
}

View file

@ -51,10 +51,6 @@ public class WalletBackupAndKey implements Comparable<WalletBackupAndKey> {
@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);
}
}

View file

@ -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()));

View file

@ -13,8 +13,10 @@ public class HwUsbDevicesController extends KeystoreImportDetailController {
public void initializeView(List<Device> 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);
}
}
}
}

View file

@ -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;
}
}

View file

@ -26,6 +26,10 @@ public class KeystoreImportDialog extends Dialog<Keystore> {
}
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<Keystore> {
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);

View file

@ -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) {

View file

@ -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<Keystore> 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);
}
}

View file

@ -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<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) {
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());
}
}

View file

@ -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<UtxoEntry> utxoEntries) {

View file

@ -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();

View file

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