From 2dfdbd6d7876153cd5da95b213fdc3201aeddc4f Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Tue, 13 Apr 2021 17:27:42 +0200 Subject: [PATCH] support ur:crypto-output scan and display of wallet output descriptor --- build.gradle | 2 +- .../sparrow/control/QRScanDialog.java | 7 +- .../sparrow/control/TextAreaDialog.java | 61 +++++++++++++- .../sparrow/wallet/SettingsController.java | 82 ++++++++++++++++++- .../sparrow/wallet/settings.fxml | 10 ++- 5 files changed, 156 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index c5268e13..b41399b2 100644 --- a/build.gradle +++ b/build.gradle @@ -51,7 +51,7 @@ dependencies { implementation('com.github.arteam:simple-json-rpc-server:1.0') { exclude group: 'org.slf4j' } - implementation('com.sparrowwallet:hummingbird:1.5.5') + implementation('com.sparrowwallet:hummingbird:1.6.0') implementation('com.nativelibs4java:bridj:0.7-20140918-3') { exclude group: 'com.google.android.tools', module: 'dx' } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java index d01212bb..6f6fe80f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java @@ -465,7 +465,12 @@ public class QRScanDialog extends Dialog { return wallets; } - private ScriptType getScriptType(List expressions) { + private ScriptType getScriptType(List scriptExpressions) { + List expressions = new ArrayList<>(scriptExpressions); + if(expressions.get(expressions.size() - 1) == ScriptExpression.MULTISIG || expressions.get(expressions.size() - 1) == ScriptExpression.SORTED_MULTISIG) { + expressions.remove(expressions.size() - 1); + } + if(List.of(ScriptExpression.PUBLIC_KEY_HASH).equals(expressions)) { return ScriptType.P2PKH; } else if(List.of(ScriptExpression.SCRIPT_HASH, ScriptExpression.WITNESS_PUBLIC_KEY_HASH).equals(expressions)) { diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TextAreaDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/TextAreaDialog.java index abbc047e..b19998e9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/TextAreaDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/TextAreaDialog.java @@ -1,13 +1,19 @@ package com.sparrowwallet.sparrow.control; +import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.sparrow.AppServices; +import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import javafx.application.Platform; import javafx.beans.NamedArg; +import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; +import org.controlsfx.glyphfont.Glyph; + +import java.util.Optional; public class TextAreaDialog extends Dialog { private final TextArea textArea; @@ -18,7 +24,8 @@ public class TextAreaDialog extends Dialog { } public TextAreaDialog(@NamedArg("defaultValue") String defaultValue) { - final DialogPane dialogPane = getDialogPane(); + final DialogPane dialogPane = new TextAreaDialogPane(); + setDialogPane(dialogPane); Image image = new Image("/image/sparrow-small.png"); dialogPane.setGraphic(new ImageView(image)); @@ -40,6 +47,9 @@ public class TextAreaDialog extends Dialog { dialogPane.getStyleClass().add("text-input-dialog"); dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + final ButtonType scanButtonType = new javafx.scene.control.ButtonType("Scan QR", ButtonBar.ButtonData.LEFT); + dialogPane.getButtonTypes().add(scanButtonType); + Platform.runLater(textArea::requestFocus); setResultConverter((dialogButton) -> { @@ -58,4 +68,53 @@ public class TextAreaDialog extends Dialog { public final String getDefaultValue() { return defaultValue; } + + private class TextAreaDialogPane extends DialogPane { + @Override + protected Node createButton(ButtonType buttonType) { + Node button; + if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) { + Button scanButton = new Button(buttonType.getText()); + scanButton.setGraphicTextGap(5); + scanButton.setGraphic(getGlyph(FontAwesome5.Glyph.CAMERA)); + + final ButtonBar.ButtonData buttonData = buttonType.getButtonData(); + ButtonBar.setButtonData(scanButton, buttonData); + scanButton.setOnAction(event -> { + QRScanDialog qrScanDialog = new QRScanDialog(); + Optional optionalResult = qrScanDialog.showAndWait(); + if(optionalResult.isPresent()) { + QRScanDialog.Result result = optionalResult.get(); + if(result.payload != null) { + textArea.setText(result.payload); + } else if(result.psbt != null) { + textArea.setText(result.psbt.toBase64String()); + } else if(result.transaction != null) { + textArea.setText(Utils.bytesToHex(result.transaction.bitcoinSerialize())); + } else if(result.uri != null) { + textArea.setText(result.uri.toString()); + } else if(result.extendedKey != null) { + textArea.setText(result.extendedKey.getExtendedKey()); + } else if(result.outputDescriptor != null) { + textArea.setText(result.outputDescriptor.toString(true)); + } else if(result.exception != null) { + AppServices.showErrorDialog("Error scanning QR", result.exception.getMessage()); + } + } + }); + + button = scanButton; + } else { + button = super.createButton(buttonType); + } + + return button; + } + + private Glyph getGlyph(FontAwesome5.Glyph glyphName) { + Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName); + glyph.setFontSize(11); + return glyph; + } + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java index 61f6281c..334e54cc 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java @@ -1,8 +1,7 @@ package com.sparrowwallet.sparrow.wallet; import com.google.common.eventbus.Subscribe; -import com.sparrowwallet.drongo.OutputDescriptor; -import com.sparrowwallet.drongo.SecureString; +import com.sparrowwallet.drongo.*; import com.sparrowwallet.drongo.crypto.*; import com.sparrowwallet.drongo.policy.Policy; import com.sparrowwallet.drongo.policy.PolicyType; @@ -11,6 +10,8 @@ import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.KeystoreSource; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.WalletModel; +import com.sparrowwallet.hummingbird.UR; +import com.sparrowwallet.hummingbird.registry.*; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.control.*; @@ -45,6 +46,12 @@ public class SettingsController extends WalletFormController implements Initiali @FXML private DescriptorArea descriptor; + @FXML + private Button scanDescriptorQR; + + @FXML + private Button showDescriptorQR; + @FXML private ComboBox scriptType; @@ -172,6 +179,11 @@ public class SettingsController extends WalletFormController implements Initiali }); initializeDescriptorField(descriptor); + scanDescriptorQR.managedProperty().bind(scanDescriptorQR.visibleProperty()); + scanDescriptorQR.prefHeightProperty().bind(descriptor.prefHeightProperty()); + showDescriptorQR.managedProperty().bind(showDescriptorQR.visibleProperty()); + showDescriptorQR.prefHeightProperty().bind(descriptor.prefHeightProperty()); + showDescriptorQR.visibleProperty().bind(scanDescriptorQR.visibleProperty().not()); revert.setOnAction(event -> { keystoreTabs.getTabs().removeAll(keystoreTabs.getTabs()); @@ -223,6 +235,7 @@ public class SettingsController extends WalletFormController implements Initiali scriptType.getSelectionModel().select(walletForm.getWallet().getScriptType()); } + scanDescriptorQR.setVisible(!walletForm.getWallet().isValid()); export.setDisable(!walletForm.getWallet().isValid()); revert.setDisable(true); apply.setDisable(true); @@ -265,6 +278,14 @@ public class SettingsController extends WalletFormController implements Initiali QRScanDialog.Result result = optionalResult.get(); if(result.outputDescriptor != null) { setDescriptorText(result.outputDescriptor.toString()); + } else if(result.wallets != null) { + for(Wallet wallet : result.wallets) { + if(scriptType.getValue().equals(wallet.getScriptType()) && !wallet.getKeystores().isEmpty()) { + OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(walletForm.getWallet()); + setDescriptorText(outputDescriptor.toString()); + break; + } + } } else if(result.payload != null && !result.payload.isEmpty()) { setDescriptorText(result.payload); } else if(result.exception != null) { @@ -273,6 +294,61 @@ public class SettingsController extends WalletFormController implements Initiali } } + public void showDescriptorQR(ActionEvent event) { + if(!walletForm.getWallet().isValid()) { + AppServices.showErrorDialog("Wallet Invalid", "Cannot show a descriptor for an invalid wallet."); + return; + } + + List scriptExpressions = getScriptExpressions(walletForm.getWallet().getScriptType()); + + CryptoOutput cryptoOutput; + if(walletForm.getWallet().getPolicyType() == PolicyType.SINGLE) { + cryptoOutput = new CryptoOutput(scriptExpressions, getCryptoHDKey(walletForm.getWallet().getKeystores().get(0))); + } else if(walletForm.getWallet().getPolicyType() == PolicyType.MULTI) { + List cryptoHDKeys = walletForm.getWallet().getKeystores().stream().map(this::getCryptoHDKey).collect(Collectors.toList()); + MultiKey multiKey = new MultiKey(walletForm.getWallet().getDefaultPolicy().getNumSignaturesRequired(), null, cryptoHDKeys); + List multiScriptExpressions = new ArrayList<>(scriptExpressions); + multiScriptExpressions.add(ScriptExpression.SORTED_MULTISIG); + cryptoOutput = new CryptoOutput(multiScriptExpressions, multiKey); + } else { + AppServices.showErrorDialog("Unsupported Wallet Policy", "Cannot show a descriptor for this wallet."); + return; + } + + UR cryptoOutputUR = cryptoOutput.toUR(); + QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoOutputUR); + qrDisplayDialog.showAndWait(); + } + + private List getScriptExpressions(ScriptType scriptType) { + if(scriptType == ScriptType.P2PK) { + return List.of(ScriptExpression.PUBLIC_KEY); + } else if(scriptType == ScriptType.P2PKH) { + return List.of(ScriptExpression.PUBLIC_KEY_HASH); + } else if(scriptType == ScriptType.P2SH_P2WPKH) { + return List.of(ScriptExpression.SCRIPT_HASH, ScriptExpression.WITNESS_PUBLIC_KEY_HASH); + } else if(scriptType == ScriptType.P2WPKH) { + return List.of(ScriptExpression.WITNESS_PUBLIC_KEY_HASH); + } else if(scriptType == ScriptType.P2SH) { + return List.of(ScriptExpression.SCRIPT_HASH); + } else if(scriptType == ScriptType.P2SH_P2WSH) { + return List.of(ScriptExpression.SCRIPT_HASH, ScriptExpression.WITNESS_SCRIPT_HASH); + } else if(scriptType == ScriptType.P2WSH) { + return List.of(ScriptExpression.WITNESS_SCRIPT_HASH); + } + + throw new IllegalArgumentException("Unknown script type of " + scriptType); + } + + private CryptoHDKey getCryptoHDKey(Keystore keystore) { + ExtendedKey extendedKey = keystore.getExtendedPublicKey(); + CryptoCoinInfo cryptoCoinInfo = new CryptoCoinInfo(CryptoCoinInfo.Type.BITCOIN.ordinal(), Network.get() == Network.MAINNET ? CryptoCoinInfo.Network.MAINNET.ordinal() : CryptoCoinInfo.Network.TESTNET.ordinal()); + List pathComponents = keystore.getKeyDerivation().getDerivation().stream().map(cNum -> new PathComponent(cNum.num(), cNum.isHardened())).collect(Collectors.toList()); + CryptoKeypath cryptoKeypath = new CryptoKeypath(pathComponents, Utils.hexToBytes(keystore.getKeyDerivation().getMasterFingerprint()), pathComponents.size()); + return new CryptoHDKey(false, extendedKey.getKey().getPubKey(), extendedKey.getKey().getChainCode(), cryptoCoinInfo, cryptoKeypath, null, extendedKey.getParentFingerprint()); + } + public void editDescriptor(ActionEvent event) { OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(walletForm.getWallet()); String outputDescriptorString = outputDescriptor.toString(walletForm.getWallet().isValid()); @@ -359,6 +435,7 @@ public class SettingsController extends WalletFormController implements Initiali revert.setDisable(false); apply.setDisable(!wallet.isValid()); export.setDisable(true); + scanDescriptorQR.setVisible(!wallet.isValid()); } } @@ -366,6 +443,7 @@ public class SettingsController extends WalletFormController implements Initiali public void walletSettingsChanged(WalletSettingsChangedEvent event) { if(event.getWalletFile().equals(walletForm.getWalletFile())) { export.setDisable(!event.getWallet().isValid()); + scanDescriptorQR.setVisible(!event.getWallet().isValid()); } } diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/settings.fxml b/src/main/resources/com/sparrowwallet/sparrow/wallet/settings.fxml index 603b48a3..d8d2e293 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/settings.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/settings.fxml @@ -83,7 +83,7 @@ - +