wallet settings and keystores

This commit is contained in:
Craig Raw 2020-04-18 10:43:32 +02:00
parent 17b03a6750
commit 8da6226545
12 changed files with 293 additions and 14 deletions

2
drongo

@ -1 +1 @@
Subproject commit 08e8df0807a01b2716ac80e9d6d9e2c113025848 Subproject commit 075707f1ad36813889b2b1ed5f59e2854ddc7dc9

View file

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

View file

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

View file

@ -1,17 +1,30 @@
package com.sparrowwallet.sparrow.wallet; 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.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.Wallet;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.CopyableLabel; 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.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.layout.StackPane;
import org.controlsfx.control.RangeSlider; import org.controlsfx.control.RangeSlider;
import org.controlsfx.tools.Borders;
import tornadofx.control.Fieldset; import tornadofx.control.Fieldset;
import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.util.ResourceBundle; import java.util.ResourceBundle;
import java.util.stream.Collectors;
public class SettingsController extends WalletFormController implements Initializable { public class SettingsController extends WalletFormController implements Initializable {
@ -19,7 +32,7 @@ public class SettingsController extends WalletFormController implements Initiali
private ComboBox<PolicyType> policyType; private ComboBox<PolicyType> policyType;
@FXML @FXML
private TextField policy; private TextField spendingMiniscript;
@FXML @FXML
private ComboBox<ScriptType> scriptType; private ComboBox<ScriptType> scriptType;
@ -37,9 +50,11 @@ public class SettingsController extends WalletFormController implements Initiali
private CopyableLabel multisigHighLabel; private CopyableLabel multisigHighLabel;
@FXML @FXML
private StackPane keystoreTabsPane;
private TabPane keystoreTabs; private TabPane keystoreTabs;
@FXML ComboBox testType; private final SimpleIntegerProperty totalKeystores = new SimpleIntegerProperty(0);
@Override @Override
public void initialize(URL location, ResourceBundle resources) { public void initialize(URL location, ResourceBundle resources) {
@ -48,22 +63,113 @@ public class SettingsController extends WalletFormController implements Initiali
@Override @Override
public void initializeView() { 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) -> { policyType.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, policyType) -> {
wallet.setPolicyType(policyType);
scriptType.setItems(FXCollections.observableArrayList(ScriptType.getScriptTypesForPolicyType(policyType)));
scriptType.getSelectionModel().select(policyType.getDefaultScriptType()); scriptType.getSelectionModel().select(policyType.getDefaultScriptType());
multisigFieldset.setVisible(policyType.equals(PolicyType.MULTI)); 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") ); multisigLowLabel.textProperty().bind(multisigControl.lowValueProperty().asString("%.0f") );
multisigHighLabel.textProperty().bind(multisigControl.highValueProperty().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()); 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()); policyType.getSelectionModel().select(walletForm.getWallet().getPolicyType());
} else { } else {
policyType.getSelectionModel().select(0); 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());
} }
} }

View file

@ -1,6 +1,5 @@
package com.sparrowwallet.sparrow.wallet; package com.sparrowwallet.sparrow.wallet;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppController; import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import javafx.fxml.FXML; import javafx.fxml.FXML;

View file

@ -34,4 +34,11 @@
-fx-translate-x: -20px; -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);
}

View file

@ -0,0 +1,7 @@
.form .fieldset:horizontal .label-container {
-fx-pref-width: 140px;
}
#fingerprint, #derivation, #xpub {
-fx-font-family: Courier;
}

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import tornadofx.control.Form?>
<?import tornadofx.control.Fieldset?>
<?import tornadofx.control.Field?>
<?import javafx.geometry.Insets?>
<StackPane stylesheets="@keystore.css, @settings.css, @../general.css" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.wallet.KeystoreController">
<padding>
<Insets left="25.0" right="25.0" />
</padding>
<Form GridPane.columnIndex="0" GridPane.rowIndex="0">
<Fieldset inputGrow="SOMETIMES" text="">
<Field text="Name:">
<TextField fx:id="label" maxWidth="160"/>
</Field>
<Field text="Master fingerprint:">
<TextField fx:id="fingerprint" maxWidth="80"/>
</Field>
<Field text="Derivation:">
<TextField fx:id="derivation" maxWidth="200"/>
</Field>
<Field text="xPub:">
<TextArea fx:id="xpub" wrapText="true" prefRowCount="2" maxHeight="40" />
</Field>
</Fieldset>
</Form>
</StackPane>

View file

@ -9,3 +9,8 @@
.form .fieldset:horizontal .input-container { .form .fieldset:horizontal .input-container {
-fx-alignment: center-left; -fx-alignment: center-left;
} }
#spendingMiniscript {
-fx-font-family: Courier;
}

View file

@ -35,7 +35,6 @@
</FXCollections> </FXCollections>
</items> </items>
</ComboBox> </ComboBox>
</Field> </Field>
<Field text="Script Type:"> <Field text="Script Type:">
<ComboBox fx:id="scriptType"> <ComboBox fx:id="scriptType">
@ -58,7 +57,7 @@
<Form GridPane.columnIndex="1" GridPane.rowIndex="0"> <Form GridPane.columnIndex="1" GridPane.rowIndex="0">
<Fieldset inputGrow="SOMETIMES" text="" fx:id="multisigFieldset"> <Fieldset inputGrow="SOMETIMES" text="" fx:id="multisigFieldset">
<Field text="Cosigners:"> <Field text="Cosigners:">
<RangeSlider fx:id="multisigControl" showTickMarks="true" showTickLabels="true" blockIncrement="1" min="2" max="10" lowValue="2" highValue="3" snapToTicks="true" majorTickUnit="1" minorTickCount="0" /> <RangeSlider fx:id="multisigControl" showTickMarks="true" showTickLabels="true" blockIncrement="1" min="2" max="9" lowValue="2" highValue="3" snapToTicks="true" majorTickUnit="1" minorTickCount="0" />
</Field> </Field>
<Field text="M of N:"> <Field text="M of N:">
<CopyableLabel fx:id="multisigLowLabel" /> <CopyableLabel fx:id="multisigLowLabel" />
@ -69,14 +68,16 @@
</Form> </Form>
<Form GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.rowIndex="1"> <Form GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.rowIndex="1">
<Fieldset inputGrow="SOMETIMES" text="Default Policy"> <Fieldset inputGrow="SOMETIMES" text="Spending Policy">
<Field text="Policy:"> <Field text="Miniscript:">
<TextField fx:id="policy" /> <TextField fx:id="spendingMiniscript" editable="false" />
</Field> </Field>
</Fieldset> </Fieldset>
</Form> </Form>
<TabPane fx:id="keystoreTabs" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.rowIndex="2"> <Form GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.rowIndex="2">
<Fieldset inputGrow="SOMETIMES" text="Keystores">
</TabPane> <StackPane fx:id="keystoreTabsPane" />
</Fieldset>
</Form>
</GridPane> </GridPane>

View file

@ -19,3 +19,7 @@
.list-item:selected { .list-item:selected {
-fx-background-color: #1e88cf; -fx-background-color: #1e88cf;
} }
#walletPane {
-fx-background-color: -fx-background;
}

View file

@ -48,7 +48,7 @@
</ToggleButton> </ToggleButton>
<ToggleButton VBox.vgrow="ALWAYS" text="Policies" contentDisplay="TOP" styleClass="list-item" maxHeight="Infinity" toggleGroup="$walletMenu"> <ToggleButton VBox.vgrow="ALWAYS" text="Policies" contentDisplay="TOP" styleClass="list-item" maxHeight="Infinity" toggleGroup="$walletMenu">
<graphic> <graphic>
<Glyph fontFamily="FontAwesome" icon="FILE_TEXT_ALT" /> <Glyph fontFamily="FontAwesome" icon="FILE_TEXT" />
</graphic> </graphic>
<userData> <userData>
<Function fx:constant="POLICIES"/> <Function fx:constant="POLICIES"/>