Craig Raw
4 years ago
17 changed files with 366 additions and 35 deletions
@ -1 +1 @@ |
|||||
Subproject commit 1aeaacaf59484c76d5bf485dabb4a632c5230032 |
Subproject commit 85e8b97a8c8d21bfbb76096285eec95d28384090 |
@ -0,0 +1,48 @@ |
|||||
|
package com.sparrowwallet.sparrow.control; |
||||
|
|
||||
|
import com.sparrowwallet.drongo.wallet.Keystore; |
||||
|
import com.sparrowwallet.sparrow.AppServices; |
||||
|
import javafx.application.Platform; |
||||
|
import javafx.scene.control.*; |
||||
|
import javafx.scene.layout.AnchorPane; |
||||
|
import javafx.scene.layout.StackPane; |
||||
|
|
||||
|
public class MasterKeyDisplayDialog extends Dialog<Void> { |
||||
|
public MasterKeyDisplayDialog(Keystore decryptedKeystore) { |
||||
|
final DialogPane dialogPane = getDialogPane(); |
||||
|
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm()); |
||||
|
AppServices.setStageIcon(dialogPane.getScene().getWindow()); |
||||
|
|
||||
|
StackPane stackPane = new StackPane(); |
||||
|
dialogPane.setContent(stackPane); |
||||
|
|
||||
|
AnchorPane anchorPane = new AnchorPane(); |
||||
|
ScrollPane scrollPane = new ScrollPane(); |
||||
|
scrollPane.getStyleClass().add("edge-to-edge"); |
||||
|
scrollPane.setPrefHeight(200); |
||||
|
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); |
||||
|
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); |
||||
|
anchorPane.getChildren().add(scrollPane); |
||||
|
scrollPane.setFitToWidth(true); |
||||
|
AnchorPane.setLeftAnchor(scrollPane, 0.0); |
||||
|
AnchorPane.setRightAnchor(scrollPane, 0.0); |
||||
|
|
||||
|
Accordion keystoreAccordion = new Accordion(); |
||||
|
scrollPane.setContent(keystoreAccordion); |
||||
|
|
||||
|
XprvKeystoreImportPane keystorePane = new XprvKeystoreImportPane(decryptedKeystore); |
||||
|
keystorePane.setAnimated(false); |
||||
|
keystoreAccordion.getPanes().add(keystorePane); |
||||
|
|
||||
|
stackPane.getChildren().addAll(anchorPane); |
||||
|
|
||||
|
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); |
||||
|
dialogPane.getButtonTypes().addAll(cancelButtonType); |
||||
|
|
||||
|
dialogPane.setPrefWidth(500); |
||||
|
dialogPane.setPrefHeight(260); |
||||
|
|
||||
|
Platform.runLater(() -> keystoreAccordion.setExpandedPane(keystorePane)); |
||||
|
} |
||||
|
} |
||||
|
|
@ -0,0 +1,209 @@ |
|||||
|
package com.sparrowwallet.sparrow.control; |
||||
|
|
||||
|
import com.sparrowwallet.drongo.ExtendedKey; |
||||
|
import com.sparrowwallet.drongo.KeyDerivation; |
||||
|
import com.sparrowwallet.drongo.crypto.ChildNumber; |
||||
|
import com.sparrowwallet.drongo.protocol.ScriptType; |
||||
|
import com.sparrowwallet.drongo.wallet.Keystore; |
||||
|
import com.sparrowwallet.drongo.wallet.MnemonicException; |
||||
|
import com.sparrowwallet.drongo.wallet.Wallet; |
||||
|
import com.sparrowwallet.drongo.wallet.WalletModel; |
||||
|
import com.sparrowwallet.sparrow.EventManager; |
||||
|
import com.sparrowwallet.sparrow.event.KeystoreImportEvent; |
||||
|
import com.sparrowwallet.sparrow.io.ImportException; |
||||
|
import com.sparrowwallet.sparrow.io.KeystoreXprvImport; |
||||
|
import javafx.geometry.Insets; |
||||
|
import javafx.geometry.Pos; |
||||
|
import javafx.scene.Node; |
||||
|
import javafx.scene.control.*; |
||||
|
import javafx.scene.layout.HBox; |
||||
|
import javafx.scene.layout.Priority; |
||||
|
import org.controlsfx.validation.ValidationResult; |
||||
|
import org.controlsfx.validation.ValidationSupport; |
||||
|
import org.controlsfx.validation.Validator; |
||||
|
import org.controlsfx.validation.decoration.StyleClassValidationDecoration; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
public class XprvKeystoreImportPane extends TitledDescriptionPane { |
||||
|
protected final Wallet wallet; |
||||
|
protected final KeystoreXprvImport importer; |
||||
|
|
||||
|
private Button enterXprvButton; |
||||
|
private SplitMenuButton importButton; |
||||
|
|
||||
|
private ExtendedKey xprv; |
||||
|
|
||||
|
public XprvKeystoreImportPane(Wallet wallet, KeystoreXprvImport importer) { |
||||
|
super(importer.getName(), "Extended key import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png"); |
||||
|
this.wallet = wallet; |
||||
|
this.importer = importer; |
||||
|
|
||||
|
createImportButton(); |
||||
|
buttonBox.getChildren().add(importButton); |
||||
|
} |
||||
|
|
||||
|
public XprvKeystoreImportPane(Keystore keystore) { |
||||
|
super("Master Private Key", "BIP32 key", "", "image/" + WalletModel.SEED.getType() + ".png"); |
||||
|
this.wallet = null; |
||||
|
this.importer = null; |
||||
|
|
||||
|
try { |
||||
|
this.xprv = keystore.getExtendedMasterPrivateKey(); |
||||
|
} catch(MnemonicException e) { |
||||
|
//can't happen
|
||||
|
} |
||||
|
|
||||
|
showHideLink.setVisible(false); |
||||
|
buttonBox.getChildren().clear(); |
||||
|
setContent(getXprvEntry(true)); |
||||
|
setExpanded(true); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected Control createButton() { |
||||
|
enterXprvButton = new Button("Enter Private Key"); |
||||
|
enterXprvButton.managedProperty().bind(enterXprvButton.visibleProperty()); |
||||
|
enterXprvButton.setOnAction(event -> { |
||||
|
enterXprvButton.setDisable(true); |
||||
|
enterXprv(); |
||||
|
}); |
||||
|
|
||||
|
return enterXprvButton; |
||||
|
} |
||||
|
|
||||
|
private void createImportButton() { |
||||
|
importButton = new SplitMenuButton(); |
||||
|
importButton.setAlignment(Pos.CENTER_RIGHT); |
||||
|
importButton.setText("Import Keystore"); |
||||
|
importButton.getStyleClass().add("default-button"); |
||||
|
importButton.setOnAction(event -> { |
||||
|
importButton.setDisable(true); |
||||
|
importKeystore(wallet.getScriptType().getDefaultDerivation()); |
||||
|
}); |
||||
|
String[] accounts = new String[] {"Import Default Account #0", "Import Account #1", "Import Account #2", "Import Account #3", "Import Account #4", "Import Account #5", "Import Account #6", "Import Account #7", "Import Account #8", "Import 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); |
||||
|
} |
||||
|
|
||||
|
importButton.managedProperty().bind(importButton.visibleProperty()); |
||||
|
importButton.setVisible(false); |
||||
|
} |
||||
|
|
||||
|
private void enterXprv() { |
||||
|
setDescription("Enter master private key"); |
||||
|
showHideLink.setVisible(false); |
||||
|
setContent(getXprvEntry(false)); |
||||
|
setExpanded(true); |
||||
|
} |
||||
|
|
||||
|
private void importKeystore(List<ChildNumber> derivation) { |
||||
|
importButton.setDisable(true); |
||||
|
try { |
||||
|
Keystore keystore = importer.getKeystore(derivation, xprv); |
||||
|
EventManager.get().post(new KeystoreImportEvent(keystore)); |
||||
|
} catch (ImportException e) { |
||||
|
String errorMessage = e.getMessage(); |
||||
|
if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) { |
||||
|
errorMessage = e.getCause().getMessage(); |
||||
|
} |
||||
|
setError("Import Error", errorMessage); |
||||
|
importButton.setDisable(false); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private Node getXprvEntry(boolean displayOnly) { |
||||
|
TextArea xprvField = new TextArea(); |
||||
|
xprvField.setPrefRowCount(2); |
||||
|
xprvField.setWrapText(true); |
||||
|
xprvField.getStyleClass().add("fixed-width"); |
||||
|
xprvField.setPromptText(ExtendedKey.Header.fromScriptType(ScriptType.P2PKH, true).getName() + (wallet != null ? "/" + ExtendedKey.Header.fromScriptType(wallet.getScriptType(), true).getName() : "") + "..."); |
||||
|
HBox.setHgrow(xprvField, Priority.ALWAYS); |
||||
|
|
||||
|
if(xprv != null) { |
||||
|
xprvField.setText(xprv.toString()); |
||||
|
} |
||||
|
if(displayOnly) { |
||||
|
xprvField.setEditable(false); |
||||
|
} |
||||
|
|
||||
|
ValidationSupport validationSupport = new ValidationSupport(); |
||||
|
validationSupport.registerValidator(xprvField, Validator.combine( |
||||
|
Validator.createEmptyValidator("xprv is required"), |
||||
|
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid private key", !ExtendedKey.isValid(newValue) || ExtendedKey.fromDescriptor(newValue).getKey().isPubKeyOnly()) |
||||
|
)); |
||||
|
validationSupport.setValidationDecorator(new StyleClassValidationDecoration()); |
||||
|
|
||||
|
Button importXprvButton = new Button("Import"); |
||||
|
importXprvButton.setMinWidth(80); |
||||
|
importXprvButton.setDisable(true); |
||||
|
importXprvButton.setOnAction(event -> { |
||||
|
enterXprvButton.setVisible(false); |
||||
|
importButton.setVisible(true); |
||||
|
setDescription("Ready to import"); |
||||
|
xprv = ExtendedKey.fromDescriptor(xprvField.getText()); |
||||
|
setContent(getDerivationEntry(wallet.getScriptType().getDefaultDerivation())); |
||||
|
}); |
||||
|
|
||||
|
xprvField.textProperty().addListener((observable, oldValue, newValue) -> { |
||||
|
importXprvButton.setDisable(newValue.isEmpty() || !ExtendedKey.isValid(newValue) || ExtendedKey.fromDescriptor(newValue).getKey().isPubKeyOnly()); |
||||
|
}); |
||||
|
|
||||
|
HBox contentBox = new HBox(); |
||||
|
contentBox.setAlignment(Pos.TOP_RIGHT); |
||||
|
contentBox.setSpacing(20); |
||||
|
contentBox.getChildren().add(xprvField); |
||||
|
if(!displayOnly) { |
||||
|
contentBox.getChildren().add(importXprvButton); |
||||
|
} |
||||
|
contentBox.setPadding(new Insets(10, 30, 10, 30)); |
||||
|
contentBox.setPrefHeight(100); |
||||
|
|
||||
|
return contentBox; |
||||
|
} |
||||
|
|
||||
|
private Node getDerivationEntry(List<ChildNumber> derivation) { |
||||
|
TextField derivationField = new TextField(); |
||||
|
derivationField.setPromptText("Derivation path"); |
||||
|
derivationField.setText(KeyDerivation.writePath(derivation)); |
||||
|
HBox.setHgrow(derivationField, Priority.ALWAYS); |
||||
|
|
||||
|
ValidationSupport validationSupport = new ValidationSupport(); |
||||
|
validationSupport.registerValidator(derivationField, Validator.combine( |
||||
|
Validator.createEmptyValidator("Derivation is required"), |
||||
|
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue)) |
||||
|
)); |
||||
|
validationSupport.setValidationDecorator(new StyleClassValidationDecoration()); |
||||
|
|
||||
|
Button importDerivationButton = new Button("Import Custom Derivation Keystore"); |
||||
|
importDerivationButton.setDisable(true); |
||||
|
importDerivationButton.setOnAction(event -> { |
||||
|
showHideLink.setVisible(true); |
||||
|
setExpanded(false); |
||||
|
List<ChildNumber> importDerivation = KeyDerivation.parsePath(derivationField.getText()); |
||||
|
importKeystore(importDerivation); |
||||
|
}); |
||||
|
|
||||
|
derivationField.textProperty().addListener((observable, oldValue, 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(); |
||||
|
contentBox.setAlignment(Pos.TOP_RIGHT); |
||||
|
contentBox.setSpacing(20); |
||||
|
contentBox.getChildren().add(derivationField); |
||||
|
contentBox.getChildren().add(importDerivationButton); |
||||
|
contentBox.setPadding(new Insets(10, 30, 10, 30)); |
||||
|
contentBox.setPrefHeight(60); |
||||
|
|
||||
|
return contentBox; |
||||
|
} |
||||
|
} |
@ -0,0 +1,36 @@ |
|||||
|
package com.sparrowwallet.sparrow.io; |
||||
|
|
||||
|
import com.sparrowwallet.drongo.ExtendedKey; |
||||
|
import com.sparrowwallet.drongo.crypto.ChildNumber; |
||||
|
import com.sparrowwallet.drongo.wallet.Keystore; |
||||
|
import com.sparrowwallet.drongo.wallet.MasterPrivateExtendedKey; |
||||
|
import com.sparrowwallet.drongo.wallet.WalletModel; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
public class Bip32 implements KeystoreXprvImport { |
||||
|
@Override |
||||
|
public String getName() { |
||||
|
return "Master Private Key (BIP32)"; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public WalletModel getWalletModel() { |
||||
|
return WalletModel.SEED; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getKeystoreImportDescription() { |
||||
|
return "Import an extended master private key (BIP 32 xprv)"; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Keystore getKeystore(List<ChildNumber> derivation, ExtendedKey xprv) throws ImportException { |
||||
|
try { |
||||
|
MasterPrivateExtendedKey masterPrivateExtendedKey = new MasterPrivateExtendedKey(xprv.getKey().getPrivKeyBytes(), xprv.getKey().getChainCode()); |
||||
|
return Keystore.fromMasterPrivateExtendedKey(masterPrivateExtendedKey, derivation); |
||||
|
} catch(Exception e) { |
||||
|
throw new ImportException(e); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,11 @@ |
|||||
|
package com.sparrowwallet.sparrow.io; |
||||
|
|
||||
|
import com.sparrowwallet.drongo.ExtendedKey; |
||||
|
import com.sparrowwallet.drongo.crypto.ChildNumber; |
||||
|
import com.sparrowwallet.drongo.wallet.Keystore; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
public interface KeystoreXprvImport extends KeystoreImport { |
||||
|
Keystore getKeystore(List<ChildNumber> derivation, ExtendedKey xprv) throws ImportException; |
||||
|
} |
Loading…
Reference in new issue