diff --git a/drongo b/drongo index 08e8df08..075707f1 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 08e8df0807a01b2716ac80e9d6d9e2c113025848 +Subproject commit 075707f1ad36813889b2b1ed5f59e2854ddc7dc9 diff --git a/src/main/java/com/sparrowwallet/sparrow/event/WalletChangedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/WalletChangedEvent.java new file mode 100644 index 00000000..5814aae6 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/WalletChangedEvent.java @@ -0,0 +1,15 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.drongo.wallet.Wallet; + +public class WalletChangedEvent { + private Wallet wallet; + + public WalletChangedEvent(Wallet wallet) { + this.wallet = wallet; + } + + public Wallet getWallet() { + return wallet; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java new file mode 100644 index 00000000..238175c9 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java @@ -0,0 +1,102 @@ +package com.sparrowwallet.sparrow.wallet; + +import com.sparrowwallet.drongo.ExtendedPublicKey; +import com.sparrowwallet.drongo.KeyDerivation; +import com.sparrowwallet.drongo.Utils; +import com.sparrowwallet.drongo.wallet.Keystore; +import javafx.application.Platform; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.Control; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; +import org.controlsfx.validation.ValidationResult; +import org.controlsfx.validation.ValidationSupport; +import org.controlsfx.validation.Validator; +import org.controlsfx.validation.decoration.StyleClassValidationDecoration; + +import java.net.URL; +import java.util.ResourceBundle; +import java.util.stream.Collectors; + +public class KeystoreController extends WalletFormController implements Initializable { + private Keystore keystore; + + @FXML + private TextField label; + + @FXML + private TextArea xpub; + + @FXML + private TextField derivation; + + @FXML + private TextField fingerprint; + + private ValidationSupport validationSupport; + + @Override + public void initialize(URL location, ResourceBundle resources) { + + } + + public void setKeystore(WalletForm walletForm, Keystore keystore) { + this.keystore = keystore; + setWalletForm(walletForm); + } + + @Override + public void initializeView() { + Platform.runLater(this::setupValidation); + + label.setText(keystore.getLabel()); + + if(keystore.getExtendedPublicKey() != null) { + xpub.setText(keystore.getExtendedPublicKey().toString()); + } + + if(keystore.getKeyDerivation() != null) { + derivation.setText(keystore.getKeyDerivation().getDerivationPath()); + fingerprint.setText(keystore.getKeyDerivation().getMasterFingerprint()); + } + + label.textProperty().addListener((observable, oldValue, newValue) -> keystore.setLabel(newValue)); + fingerprint.textProperty().addListener((observable, oldValue, newValue) -> keystore.setKeyDerivation(new KeyDerivation(newValue, keystore.getKeyDerivation().getDerivationPath()))); + derivation.textProperty().addListener((observable, oldValue, newValue) -> keystore.setKeyDerivation(new KeyDerivation(keystore.getKeyDerivation().getMasterFingerprint(), newValue))); + xpub.textProperty().addListener((observable, oldValue, newValue) -> keystore.setExtendedPublicKey(ExtendedPublicKey.fromDescriptor(newValue, null))); + } + + public TextField getLabel() { + return label; + } + + private void setupValidation() { + validationSupport = new ValidationSupport(); + + validationSupport.registerValidator(label, Validator.combine( + Validator.createEmptyValidator("Label is required"), + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Label is not unique", walletForm.getWallet().getKeystores().stream().filter(k -> k != keystore).map(Keystore::getLabel).collect(Collectors.toList()).contains(newValue)), + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Label is too long", newValue.length() > 16) + )); + + validationSupport.registerValidator(xpub, Validator.combine( + Validator.createEmptyValidator("xPub is required"), + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "xPub is invalid", !ExtendedPublicKey.isValid(newValue)) + )); + + validationSupport.registerValidator(derivation, Validator.combine( + Validator.createEmptyValidator("Derivation is required"), + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Derivation is invalid", !KeyDerivation.isValid(newValue)) + )); + + validationSupport.registerValidator(fingerprint, Validator.combine( + Validator.createEmptyValidator("Master fingerprint is required"), + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Master fingerprint is invalid", (newValue.length() != 8 || !Utils.isHex(newValue))) + )); + + validationSupport.setValidationDecorator(new StyleClassValidationDecoration()); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java index fa145a54..a0f421b6 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java @@ -1,17 +1,30 @@ package com.sparrowwallet.sparrow.wallet; +import com.google.common.eventbus.Subscribe; +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.Wallet; +import com.sparrowwallet.sparrow.AppController; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.control.CopyableLabel; +import com.sparrowwallet.sparrow.event.WalletChangedEvent; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.collections.FXCollections; import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; import javafx.scene.control.*; +import javafx.scene.layout.StackPane; import org.controlsfx.control.RangeSlider; +import org.controlsfx.tools.Borders; import tornadofx.control.Fieldset; +import java.io.IOException; import java.net.URL; import java.util.ResourceBundle; +import java.util.stream.Collectors; public class SettingsController extends WalletFormController implements Initializable { @@ -19,7 +32,7 @@ public class SettingsController extends WalletFormController implements Initiali private ComboBox policyType; @FXML - private TextField policy; + private TextField spendingMiniscript; @FXML private ComboBox scriptType; @@ -37,9 +50,11 @@ public class SettingsController extends WalletFormController implements Initiali private CopyableLabel multisigHighLabel; @FXML + private StackPane keystoreTabsPane; + private TabPane keystoreTabs; - @FXML ComboBox testType; + private final SimpleIntegerProperty totalKeystores = new SimpleIntegerProperty(0); @Override public void initialize(URL location, ResourceBundle resources) { @@ -48,22 +63,113 @@ public class SettingsController extends WalletFormController implements Initiali @Override public void initializeView() { + Wallet wallet = walletForm.getWallet(); + + keystoreTabs = new TabPane(); + keystoreTabsPane.getChildren().add(Borders.wrap(keystoreTabs).etchedBorder().outerPadding(10, 5, 0 ,0).innerPadding(0).raised().buildAll()); + policyType.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, policyType) -> { + wallet.setPolicyType(policyType); + scriptType.setItems(FXCollections.observableArrayList(ScriptType.getScriptTypesForPolicyType(policyType))); scriptType.getSelectionModel().select(policyType.getDefaultScriptType()); multisigFieldset.setVisible(policyType.equals(PolicyType.MULTI)); + if(policyType.equals(PolicyType.MULTI)) { + totalKeystores.bind(multisigControl.highValueProperty()); + } else { + totalKeystores.unbind(); + totalKeystores.set(1); + } + }); + + scriptType.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, scriptType) -> { + int threshold = wallet.getPolicyType().equals(PolicyType.MULTI) ? (int)multisigControl.lowValueProperty().get() : 1; + wallet.setDefaultPolicy(Policy.getPolicy(wallet.getPolicyType(), scriptType, wallet.getKeystores(), threshold)); + EventManager.get().post(new WalletChangedEvent(wallet)); }); multisigLowLabel.textProperty().bind(multisigControl.lowValueProperty().asString("%.0f") ); multisigHighLabel.textProperty().bind(multisigControl.highValueProperty().asString("%.0f")); + multisigControl.lowValueProperty().addListener((observable, oldValue, threshold) -> { + wallet.setDefaultPolicy(Policy.getPolicy(wallet.getPolicyType(), wallet.getScriptType(), wallet.getKeystores(), threshold.intValue())); + EventManager.get().post(new WalletChangedEvent(wallet)); + }); + multisigFieldset.managedProperty().bind(multisigFieldset.visibleProperty()); - if(walletForm.getWallet().getPolicyType() != null) { + totalKeystores.addListener((observable, oldValue, numCosigners) -> { + int keystoreCount = wallet.getKeystores().size(); + int keystoreNameCount = keystoreCount; + while(keystoreCount < numCosigners.intValue()) { + keystoreCount++; + String name = "Keystore " + keystoreNameCount; + while(wallet.getKeystores().stream().map(Keystore::getLabel).collect(Collectors.toList()).contains(name)) { + name = "Keystore " + (++keystoreNameCount); + } + wallet.getKeystores().add(new Keystore(name)); + } + wallet.setKeystores(wallet.getKeystores().subList(0, numCosigners.intValue())); + + for(int i = 0; i < wallet.getKeystores().size(); i++) { + Keystore keystore = wallet.getKeystores().get(i); + if(keystoreTabs.getTabs().size() == i) { + Tab tab = getKeystoreTab(wallet, keystore); + keystoreTabs.getTabs().add(tab); + } + } + while(keystoreTabs.getTabs().size() > wallet.getKeystores().size()) { + keystoreTabs.getTabs().remove(keystoreTabs.getTabs().size() - 1); + } + + if(wallet.getPolicyType().equals(PolicyType.MULTI)) { + wallet.setDefaultPolicy(Policy.getPolicy(wallet.getPolicyType(), wallet.getScriptType(), wallet.getKeystores(), wallet.getDefaultPolicy().getNumSignaturesRequired())); + EventManager.get().post(new WalletChangedEvent(wallet)); + } + }); + + if(wallet.getPolicyType() == null) { + wallet.setPolicyType(PolicyType.SINGLE); + wallet.setScriptType(ScriptType.P2WPKH); + wallet.getKeystores().add(new Keystore("Keystore 1")); + wallet.setDefaultPolicy(Policy.getPolicy(wallet.getPolicyType(), wallet.getScriptType(), wallet.getKeystores(), 1)); + } + + if(wallet.getPolicyType().equals(PolicyType.SINGLE)) { + totalKeystores.setValue(wallet.getKeystores().size()); + } else if(wallet.getPolicyType().equals(PolicyType.MULTI)) { + multisigControl.highValueProperty().set(wallet.getKeystores().size()); + } + + if(wallet.getPolicyType() != null) { policyType.getSelectionModel().select(walletForm.getWallet().getPolicyType()); } else { policyType.getSelectionModel().select(0); } + if(wallet.getScriptType() != null) { + scriptType.getSelectionModel().select(walletForm.getWallet().getScriptType()); + } + } + private Tab getKeystoreTab(Wallet wallet, Keystore keystore) { + Tab tab = new Tab(keystore.getLabel()); + tab.setClosable(false); + + try { + FXMLLoader keystoreLoader = new FXMLLoader(AppController.class.getResource("wallet/keystore.fxml")); + tab.setContent(keystoreLoader.load()); + KeystoreController controller = keystoreLoader.getController(); + controller.setKeystore(getWalletForm(), keystore); + tab.textProperty().bind(controller.getLabel().textProperty()); + + return tab; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Subscribe + public void updateMiniscript(WalletChangedEvent event) { + spendingMiniscript.setText(event.getWallet().getDefaultPolicy().getMiniscript().getScript()); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java index 9dee0f27..0554a883 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java @@ -1,6 +1,5 @@ package com.sparrowwallet.sparrow.wallet; -import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.AppController; import com.sparrowwallet.sparrow.EventManager; import javafx.fxml.FXML; diff --git a/src/main/resources/com/sparrowwallet/sparrow/general.css b/src/main/resources/com/sparrowwallet/sparrow/general.css index 18cc6de8..3e03cad5 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/general.css +++ b/src/main/resources/com/sparrowwallet/sparrow/general.css @@ -34,4 +34,11 @@ -fx-translate-x: -20px; } +.error { + -fx-effect: dropshadow(three-pass-box, darkred, 7, 0, 0, 0); +} + +.warning { + -fx-effect: dropshadow(three-pass-box, gold, 14, 0, 0, 0); +} diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.css b/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.css new file mode 100644 index 00000000..e268adc2 --- /dev/null +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.css @@ -0,0 +1,7 @@ +.form .fieldset:horizontal .label-container { + -fx-pref-width: 140px; +} + +#fingerprint, #derivation, #xpub { + -fx-font-family: Courier; +} \ No newline at end of file diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.fxml b/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.fxml new file mode 100644 index 00000000..b7f1fb7b --- /dev/null +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.fxml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + +