From eb8d66bf25e303484bd9a5a369b5402b1ed17ec4 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Mon, 5 Oct 2020 17:32:30 +0200 Subject: [PATCH] add utility buttons to import and export xpubs with qr codes, show slip132 versions --- drongo | 2 +- .../sparrow/control/QRDisplayDialog.java | 22 +++++ .../sparrow/control/QRScanDialog.java | 26 ++++++ .../sparrow/glyphfont/FontAwesome5.java | 1 + .../sparrow/wallet/KeystoreController.java | 87 +++++++++++++++++-- .../sparrowwallet/sparrow/wallet/keystore.css | 9 ++ .../sparrow/wallet/keystore.fxml | 31 ++++++- 7 files changed, 165 insertions(+), 13 deletions(-) diff --git a/drongo b/drongo index fee04267..3642ddc9 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit fee042679938316f034ceee137cfa056be566ffd +Subproject commit 3642ddc9581c4485b13d4d0fffee6290703a5768 diff --git a/src/main/java/com/sparrowwallet/sparrow/control/QRDisplayDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/QRDisplayDialog.java index e067386a..ef3931bb 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/QRDisplayDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/QRDisplayDialog.java @@ -76,6 +76,28 @@ public class QRDisplayDialog extends Dialog { 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() { String fragment = encoder.nextPart(); currentPart = fragment.toUpperCase(); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java index 0c1baa57..3d3a3d64 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java @@ -1,6 +1,7 @@ package com.sparrowwallet.sparrow.control; import com.github.sarxos.webcam.WebcamResolution; +import com.sparrowwallet.drongo.ExtendedKey; import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.protocol.Base43; @@ -106,6 +107,15 @@ public class QRScanDialog extends Dialog { Transaction transaction; BitcoinURI bitcoinURI; Address address; + ExtendedKey extendedKey; + try { + extendedKey = ExtendedKey.fromDescriptor(qrtext); + result = new Result(extendedKey); + return; + } catch(Exception e) { + //Ignore, not a valid xpub + } + try { bitcoinURI = new BitcoinURI(qrtext); result = new Result(bitcoinURI); @@ -180,6 +190,7 @@ public class QRScanDialog extends Dialog { public final Transaction transaction; public final PSBT psbt; public final BitcoinURI uri; + public final ExtendedKey extendedKey; public final String error; public final Throwable exception; @@ -187,6 +198,7 @@ public class QRScanDialog extends Dialog { this.transaction = transaction; this.psbt = null; this.uri = null; + this.extendedKey = null; this.error = null; this.exception = null; } @@ -195,6 +207,7 @@ public class QRScanDialog extends Dialog { this.transaction = null; this.psbt = psbt; this.uri = null; + this.extendedKey = null; this.error = null; this.exception = null; } @@ -203,6 +216,7 @@ public class QRScanDialog extends Dialog { this.transaction = null; this.psbt = null; this.uri = uri; + this.extendedKey = null; this.error = null; this.exception = null; } @@ -211,6 +225,16 @@ public class QRScanDialog extends Dialog { this.transaction = null; this.psbt = null; 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.exception = null; } @@ -219,6 +243,7 @@ public class QRScanDialog extends Dialog { this.transaction = null; this.psbt = null; this.uri = null; + this.extendedKey = null; this.error = error; this.exception = null; } @@ -227,6 +252,7 @@ public class QRScanDialog extends Dialog { this.transaction = null; this.psbt = null; this.uri = null; + this.extendedKey = null; this.error = null; this.exception = exception; } diff --git a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java index 64deb7f9..115bf7c4 100644 --- a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java +++ b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java @@ -23,6 +23,7 @@ public class FontAwesome5 extends GlyphFont { CHECK_CIRCLE('\uf058'), CIRCLE('\uf111'), COINS('\uf51e'), + EXCHANGE_ALT('\uf362'), EXCLAMATION_CIRCLE('\uf06a'), EXCLAMATION_TRIANGLE('\uf071'), EXTERNAL_LINK_ALT('\uf35d'), diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java index 1229d3fe..8be7e4a5 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java @@ -7,6 +7,8 @@ import com.sparrowwallet.drongo.wallet.KeystoreSource; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.AppController; 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.WalletPasswordDialog; import com.sparrowwallet.sparrow.event.StorageEvent; @@ -29,6 +31,9 @@ import org.controlsfx.validation.ValidationResult; import org.controlsfx.validation.ValidationSupport; import org.controlsfx.validation.Validator; import org.controlsfx.validation.decoration.StyleClassValidationDecoration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import tornadofx.control.Field; import java.net.URL; import java.util.Optional; @@ -36,6 +41,8 @@ import java.util.ResourceBundle; import java.util.stream.Collectors; public class KeystoreController extends WalletFormController implements Initializable { + private static final Logger log = LoggerFactory.getLogger(KeystoreController.class); + private Keystore keystore; @FXML @@ -56,6 +63,9 @@ public class KeystoreController extends WalletFormController implements Initiali @FXML private TextField label; + @FXML + private Field xpubField; + @FXML private TextArea xpub; @@ -65,6 +75,15 @@ public class KeystoreController extends WalletFormController implements Initiali @FXML private TextField fingerprint; + @FXML + private Button scanXpubQR; + + @FXML + private Button displayXpubQR; + + @FXML + private Button switchXpubHeader; + private final ValidationSupport validationSupport = new ValidationSupport(); @Override @@ -87,6 +106,9 @@ public class KeystoreController extends WalletFormController implements Initiali } viewSeedButton.managedProperty().bind(viewSeedButton.visibleProperty()); + scanXpubQR.managedProperty().bind(scanXpubQR.visibleProperty()); + displayXpubQR.managedProperty().bind(displayXpubQR.visibleProperty()); + displayXpubQR.visibleProperty().bind(scanXpubQR.visibleProperty().not()); updateType(); @@ -97,6 +119,8 @@ public class KeystoreController extends WalletFormController implements Initiali if(keystore.getExtendedPublicKey() != null) { xpub.setText(keystore.getExtendedPublicKey().toString()); setXpubContext(keystore.getExtendedPublicKey()); + } else { + switchXpubHeader.setDisable(true); } if(keystore.getKeyDerivation() != null) { @@ -121,15 +145,19 @@ public class KeystoreController extends WalletFormController implements Initiali } }); xpub.textProperty().addListener((observable, oldValue, newValue) -> { - if(ExtendedKey.isValid(newValue)) { + boolean valid = ExtendedKey.isValid(newValue); + if(valid) { ExtendedKey extendedKey = ExtendedKey.fromDescriptor(newValue); setXpubContext(extendedKey); - keystore.setExtendedPublicKey(extendedKey); - EventManager.get().post(new SettingsChangedEvent(walletForm.getWallet(), SettingsChangedEvent.Type.KEYSTORE_XPUB)); + if(!extendedKey.equals(keystore.getExtendedPublicKey()) && extendedKey.getKey().isPubKeyOnly()) { + keystore.setExtendedPublicKey(extendedKey); + EventManager.get().post(new SettingsChangedEvent(walletForm.getWallet(), SettingsChangedEvent.Type.KEYSTORE_XPUB)); + } } else { - xpub.setTooltip(null); 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()) { 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 -> { contextMenu.hide(); ClipboardContent content = new ClipboardContent(); @@ -157,13 +185,16 @@ public class KeystoreController extends WalletFormController implements Initiali }); contextMenu.getItems().add(copyOtherPub); - Tooltip tooltip = new Tooltip(otherPub); - xpub.setTooltip(tooltip); + xpubField.setText("xPub / " + header.getDisplayName() + ":"); + switchXpubHeader.setDisable(false); + switchXpubHeader.setTooltip(new Tooltip("Show as " + header.getDisplayName())); } else { - xpub.setTooltip(null); + xpubField.setText("xPub:"); + switchXpubHeader.setDisable(true); } xpub.setContextMenu(contextMenu); + scanXpubQR.setVisible(false); } public void selectSource(ActionEvent event) { @@ -223,6 +254,7 @@ public class KeystoreController extends WalletFormController implements Initiali fingerprint.setEditable(editable); derivation.setEditable(editable); xpub.setEditable(editable); + scanXpubQR.setVisible(editable); } private String getTypeLabel(Keystore keystore) { @@ -284,6 +316,7 @@ public class KeystoreController extends WalletFormController implements Initiali if(keystore.getExtendedPublicKey() != null) { xpub.setText(keystore.getExtendedPublicKey().toString()); + setXpubContext(keystore.getExtendedPublicKey()); } else { xpub.setText(""); } @@ -321,6 +354,44 @@ public class KeystoreController extends WalletFormController implements Initiali dlg.showAndWait(); } + public void scanXpubQR(ActionEvent event) { + QRScanDialog qrScanDialog = new QRScanDialog(); + Optional 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 public void update(SettingsChangedEvent event) { if(walletForm.getWallet().equals(event.getWallet()) && event.getType().equals(SettingsChangedEvent.Type.SCRIPT_TYPE)) { diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.css b/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.css index 93a9c4a8..ddb8a2e1 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.css +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.css @@ -20,4 +20,13 @@ #type { -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; } \ 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 index 16702565..7e5dff7f 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.fxml @@ -41,13 +41,36 @@ - + - + - -