add utility buttons to import and export xpubs with qr codes, show slip132 versions

This commit is contained in:
Craig Raw 2020-10-05 17:32:30 +02:00
parent 3a885b3a28
commit eb8d66bf25
7 changed files with 165 additions and 13 deletions

2
drongo

@ -1 +1 @@
Subproject commit fee042679938316f034ceee137cfa056be566ffd Subproject commit 3642ddc9581c4485b13d4d0fffee6290703a5768

View file

@ -76,6 +76,28 @@ public class QRDisplayDialog extends Dialog<UR> {
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? ur : null); setResultConverter(dialogButton -> dialogButton != cancelButtonType ? ur : null);
} }
public QRDisplayDialog(String data) {
this.ur = null;
this.encoder = null;
final DialogPane dialogPane = getDialogPane();
AppController.setStageIcon(dialogPane.getScene().getWindow());
StackPane stackPane = new StackPane();
qrImageView = new ImageView();
stackPane.getChildren().add(qrImageView);
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
qrImageView.setImage(getQrCode(data));
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType);
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(550);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? ur : null);
}
private void nextPart() { private void nextPart() {
String fragment = encoder.nextPart(); String fragment = encoder.nextPart();
currentPart = fragment.toUpperCase(); currentPart = fragment.toUpperCase();

View file

@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.control; package com.sparrowwallet.sparrow.control;
import com.github.sarxos.webcam.WebcamResolution; import com.github.sarxos.webcam.WebcamResolution;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.Base43; import com.sparrowwallet.drongo.protocol.Base43;
@ -106,6 +107,15 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
Transaction transaction; Transaction transaction;
BitcoinURI bitcoinURI; BitcoinURI bitcoinURI;
Address address; Address address;
ExtendedKey extendedKey;
try {
extendedKey = ExtendedKey.fromDescriptor(qrtext);
result = new Result(extendedKey);
return;
} catch(Exception e) {
//Ignore, not a valid xpub
}
try { try {
bitcoinURI = new BitcoinURI(qrtext); bitcoinURI = new BitcoinURI(qrtext);
result = new Result(bitcoinURI); result = new Result(bitcoinURI);
@ -180,6 +190,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
public final Transaction transaction; public final Transaction transaction;
public final PSBT psbt; public final PSBT psbt;
public final BitcoinURI uri; public final BitcoinURI uri;
public final ExtendedKey extendedKey;
public final String error; public final String error;
public final Throwable exception; public final Throwable exception;
@ -187,6 +198,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.transaction = transaction; this.transaction = transaction;
this.psbt = null; this.psbt = null;
this.uri = null; this.uri = null;
this.extendedKey = null;
this.error = null; this.error = null;
this.exception = null; this.exception = null;
} }
@ -195,6 +207,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.transaction = null; this.transaction = null;
this.psbt = psbt; this.psbt = psbt;
this.uri = null; this.uri = null;
this.extendedKey = null;
this.error = null; this.error = null;
this.exception = null; this.exception = null;
} }
@ -203,6 +216,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.transaction = null; this.transaction = null;
this.psbt = null; this.psbt = null;
this.uri = uri; this.uri = uri;
this.extendedKey = null;
this.error = null; this.error = null;
this.exception = null; this.exception = null;
} }
@ -211,6 +225,16 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.transaction = null; this.transaction = null;
this.psbt = null; this.psbt = null;
this.uri = BitcoinURI.fromAddress(address); this.uri = BitcoinURI.fromAddress(address);
this.extendedKey = null;
this.error = null;
this.exception = null;
}
public Result(ExtendedKey extendedKey) {
this.transaction = null;
this.psbt = null;
this.uri = null;
this.extendedKey = extendedKey;
this.error = null; this.error = null;
this.exception = null; this.exception = null;
} }
@ -219,6 +243,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.transaction = null; this.transaction = null;
this.psbt = null; this.psbt = null;
this.uri = null; this.uri = null;
this.extendedKey = null;
this.error = error; this.error = error;
this.exception = null; this.exception = null;
} }
@ -227,6 +252,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.transaction = null; this.transaction = null;
this.psbt = null; this.psbt = null;
this.uri = null; this.uri = null;
this.extendedKey = null;
this.error = null; this.error = null;
this.exception = exception; this.exception = exception;
} }

View file

@ -23,6 +23,7 @@ public class FontAwesome5 extends GlyphFont {
CHECK_CIRCLE('\uf058'), CHECK_CIRCLE('\uf058'),
CIRCLE('\uf111'), CIRCLE('\uf111'),
COINS('\uf51e'), COINS('\uf51e'),
EXCHANGE_ALT('\uf362'),
EXCLAMATION_CIRCLE('\uf06a'), EXCLAMATION_CIRCLE('\uf06a'),
EXCLAMATION_TRIANGLE('\uf071'), EXCLAMATION_TRIANGLE('\uf071'),
EXTERNAL_LINK_ALT('\uf35d'), EXTERNAL_LINK_ALT('\uf35d'),

View file

@ -7,6 +7,8 @@ import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.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 com.sparrowwallet.sparrow.control.QRDisplayDialog;
import com.sparrowwallet.sparrow.control.QRScanDialog;
import com.sparrowwallet.sparrow.control.SeedDisplayDialog; import com.sparrowwallet.sparrow.control.SeedDisplayDialog;
import com.sparrowwallet.sparrow.control.WalletPasswordDialog; import com.sparrowwallet.sparrow.control.WalletPasswordDialog;
import com.sparrowwallet.sparrow.event.StorageEvent; import com.sparrowwallet.sparrow.event.StorageEvent;
@ -29,6 +31,9 @@ import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport; import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator; import org.controlsfx.validation.Validator;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration; import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tornadofx.control.Field;
import java.net.URL; import java.net.URL;
import java.util.Optional; import java.util.Optional;
@ -36,6 +41,8 @@ import java.util.ResourceBundle;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class KeystoreController extends WalletFormController implements Initializable { public class KeystoreController extends WalletFormController implements Initializable {
private static final Logger log = LoggerFactory.getLogger(KeystoreController.class);
private Keystore keystore; private Keystore keystore;
@FXML @FXML
@ -56,6 +63,9 @@ public class KeystoreController extends WalletFormController implements Initiali
@FXML @FXML
private TextField label; private TextField label;
@FXML
private Field xpubField;
@FXML @FXML
private TextArea xpub; private TextArea xpub;
@ -65,6 +75,15 @@ public class KeystoreController extends WalletFormController implements Initiali
@FXML @FXML
private TextField fingerprint; private TextField fingerprint;
@FXML
private Button scanXpubQR;
@FXML
private Button displayXpubQR;
@FXML
private Button switchXpubHeader;
private final ValidationSupport validationSupport = new ValidationSupport(); private final ValidationSupport validationSupport = new ValidationSupport();
@Override @Override
@ -87,6 +106,9 @@ public class KeystoreController extends WalletFormController implements Initiali
} }
viewSeedButton.managedProperty().bind(viewSeedButton.visibleProperty()); viewSeedButton.managedProperty().bind(viewSeedButton.visibleProperty());
scanXpubQR.managedProperty().bind(scanXpubQR.visibleProperty());
displayXpubQR.managedProperty().bind(displayXpubQR.visibleProperty());
displayXpubQR.visibleProperty().bind(scanXpubQR.visibleProperty().not());
updateType(); updateType();
@ -97,6 +119,8 @@ public class KeystoreController extends WalletFormController implements Initiali
if(keystore.getExtendedPublicKey() != null) { if(keystore.getExtendedPublicKey() != null) {
xpub.setText(keystore.getExtendedPublicKey().toString()); xpub.setText(keystore.getExtendedPublicKey().toString());
setXpubContext(keystore.getExtendedPublicKey()); setXpubContext(keystore.getExtendedPublicKey());
} else {
switchXpubHeader.setDisable(true);
} }
if(keystore.getKeyDerivation() != null) { if(keystore.getKeyDerivation() != null) {
@ -121,15 +145,19 @@ public class KeystoreController extends WalletFormController implements Initiali
} }
}); });
xpub.textProperty().addListener((observable, oldValue, newValue) -> { xpub.textProperty().addListener((observable, oldValue, newValue) -> {
if(ExtendedKey.isValid(newValue)) { boolean valid = ExtendedKey.isValid(newValue);
if(valid) {
ExtendedKey extendedKey = ExtendedKey.fromDescriptor(newValue); ExtendedKey extendedKey = ExtendedKey.fromDescriptor(newValue);
setXpubContext(extendedKey); setXpubContext(extendedKey);
if(!extendedKey.equals(keystore.getExtendedPublicKey()) && extendedKey.getKey().isPubKeyOnly()) {
keystore.setExtendedPublicKey(extendedKey); keystore.setExtendedPublicKey(extendedKey);
EventManager.get().post(new SettingsChangedEvent(walletForm.getWallet(), SettingsChangedEvent.Type.KEYSTORE_XPUB)); EventManager.get().post(new SettingsChangedEvent(walletForm.getWallet(), SettingsChangedEvent.Type.KEYSTORE_XPUB));
} else {
xpub.setTooltip(null);
xpub.setContextMenu(null);
} }
} else {
xpub.setContextMenu(null);
switchXpubHeader.setDisable(true);
}
scanXpubQR.setVisible(!valid);
}); });
} }
@ -148,7 +176,7 @@ public class KeystoreController extends WalletFormController implements Initiali
if(header != Network.get().getXpubHeader()) { if(header != Network.get().getXpubHeader()) {
String otherPub = extendedKey.getExtendedKey(header); String otherPub = extendedKey.getExtendedKey(header);
MenuItem copyOtherPub = new MenuItem("Copy " + header.getName().replace('p', 'P')); MenuItem copyOtherPub = new MenuItem("Copy " + header.getDisplayName());
copyOtherPub.setOnAction(AE -> { copyOtherPub.setOnAction(AE -> {
contextMenu.hide(); contextMenu.hide();
ClipboardContent content = new ClipboardContent(); ClipboardContent content = new ClipboardContent();
@ -157,13 +185,16 @@ public class KeystoreController extends WalletFormController implements Initiali
}); });
contextMenu.getItems().add(copyOtherPub); contextMenu.getItems().add(copyOtherPub);
Tooltip tooltip = new Tooltip(otherPub); xpubField.setText("xPub / " + header.getDisplayName() + ":");
xpub.setTooltip(tooltip); switchXpubHeader.setDisable(false);
switchXpubHeader.setTooltip(new Tooltip("Show as " + header.getDisplayName()));
} else { } else {
xpub.setTooltip(null); xpubField.setText("xPub:");
switchXpubHeader.setDisable(true);
} }
xpub.setContextMenu(contextMenu); xpub.setContextMenu(contextMenu);
scanXpubQR.setVisible(false);
} }
public void selectSource(ActionEvent event) { public void selectSource(ActionEvent event) {
@ -223,6 +254,7 @@ public class KeystoreController extends WalletFormController implements Initiali
fingerprint.setEditable(editable); fingerprint.setEditable(editable);
derivation.setEditable(editable); derivation.setEditable(editable);
xpub.setEditable(editable); xpub.setEditable(editable);
scanXpubQR.setVisible(editable);
} }
private String getTypeLabel(Keystore keystore) { private String getTypeLabel(Keystore keystore) {
@ -284,6 +316,7 @@ public class KeystoreController extends WalletFormController implements Initiali
if(keystore.getExtendedPublicKey() != null) { if(keystore.getExtendedPublicKey() != null) {
xpub.setText(keystore.getExtendedPublicKey().toString()); xpub.setText(keystore.getExtendedPublicKey().toString());
setXpubContext(keystore.getExtendedPublicKey());
} else { } else {
xpub.setText(""); xpub.setText("");
} }
@ -321,6 +354,44 @@ public class KeystoreController extends WalletFormController implements Initiali
dlg.showAndWait(); dlg.showAndWait();
} }
public void scanXpubQR(ActionEvent event) {
QRScanDialog qrScanDialog = new QRScanDialog();
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
if(optionalResult.isPresent()) {
QRScanDialog.Result result = optionalResult.get();
if(result.extendedKey != null && result.extendedKey.getKey().isPubKeyOnly()) {
xpub.setText(result.extendedKey.getExtendedKey());
} else if(result.error != null) {
AppController.showErrorDialog("Invalid QR Code", result.error);
} else if(result.exception != null) {
log.error("Error opening webcam", result.exception);
AppController.showErrorDialog("Error opening webcam", result.exception.getMessage());
} else {
AppController.showErrorDialog("Invalid QR Code", "QR Code did not contain a valid xPub");
}
}
}
public void displayXpubQR(ActionEvent event) {
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(xpub.getText());
qrDisplayDialog.showAndWait();
}
public void switchXpubHeader(ActionEvent event) {
if(keystore.getExtendedPublicKey() != null) {
ExtendedKey.Header header = ExtendedKey.Header.fromScriptType(walletForm.getWallet().getScriptType(), false);
if(!xpub.getText().startsWith(header.getName())) {
String otherPub = keystore.getExtendedPublicKey().getExtendedKey(header);
xpub.setText(otherPub);
switchXpubHeader.setTooltip(new Tooltip("Show as xPub"));
} else {
String xPub = keystore.getExtendedPublicKey().getExtendedKey();
xpub.setText(xPub);
switchXpubHeader.setTooltip(new Tooltip("Show as " + header.getDisplayName()));
}
}
}
@Subscribe @Subscribe
public void update(SettingsChangedEvent event) { public void update(SettingsChangedEvent event) {
if(walletForm.getWallet().equals(event.getWallet()) && event.getType().equals(SettingsChangedEvent.Type.SCRIPT_TYPE)) { if(walletForm.getWallet().equals(event.getWallet()) && event.getType().equals(SettingsChangedEvent.Type.SCRIPT_TYPE)) {

View file

@ -21,3 +21,12 @@
#type { #type {
-fx-padding: 0 17 0 0; -fx-padding: 0 17 0 0;
} }
.xpub-buttons {
-fx-max-width: 40;
-fx-padding: 0 0 0 3;
}
.xpub-buttons .button {
-fx-pref-width: 35;
}

View file

@ -41,13 +41,36 @@
<TextField fx:id="label" maxWidth="160"/> <TextField fx:id="label" maxWidth="160"/>
</Field> </Field>
<Field text="Master fingerprint:"> <Field text="Master fingerprint:">
<TextField fx:id="fingerprint" maxWidth="80" promptText="ffffffff"/> <HelpLabel helpText="A master fingerprint is the first 4 bytes of the master public key hash. It is safe to use any valid value (ffffffff) for Watch Only Wallets." /> <TextField fx:id="fingerprint" maxWidth="80" promptText="00000000"/> <HelpLabel helpText="A master fingerprint is the first 4 bytes of the master public key hash. It is safe to use any valid value (00000000) for Watch Only Wallets." />
</Field> </Field>
<Field text="Derivation:"> <Field text="Derivation:">
<TextField fx:id="derivation" maxWidth="200"/> <TextField fx:id="derivation" maxWidth="200"/> <HelpLabel helpText="The derivation path to the xPub from the master private key. For safety, derivations that match defaults for other script types are not valid." />
</Field> </Field>
<Field text="xPub:"> <Field fx:id="xpubField" text="xPub:">
<TextArea fx:id="xpub" wrapText="true" prefRowCount="2" maxHeight="50" /> <TextArea fx:id="xpub" wrapText="true" prefRowCount="2" maxHeight="52" />
<VBox styleClass="xpub-buttons" HBox.hgrow="NEVER">
<Button fx:id="scanXpubQR" onAction="#scanXpubQR">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="CAMERA" />
</graphic>
<tooltip>
<Tooltip text="Scan a QR code" />
</tooltip>
</Button>
<Button fx:id="displayXpubQR" onAction="#displayXpubQR">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="QRCODE" />
</graphic>
<tooltip>
<Tooltip text="Display as a QR code" />
</tooltip>
</Button>
<Button fx:id="switchXpubHeader" onAction="#switchXpubHeader">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCHANGE_ALT" />
</graphic>
</Button>
</VBox>
</Field> </Field>
</Fieldset> </Fieldset>
</Form> </Form>