support ur:crypto-output scan and display of wallet output descriptor

This commit is contained in:
Craig Raw 2021-04-13 17:27:42 +02:00
parent 2e86840e92
commit 2dfdbd6d78
5 changed files with 156 additions and 6 deletions

View file

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

View file

@ -465,7 +465,12 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
return wallets;
}
private ScriptType getScriptType(List<ScriptExpression> expressions) {
private ScriptType getScriptType(List<ScriptExpression> scriptExpressions) {
List<ScriptExpression> 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)) {

View file

@ -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<String> {
private final TextArea textArea;
@ -18,7 +24,8 @@ public class TextAreaDialog extends Dialog<String> {
}
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<String> {
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<String> {
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<QRScanDialog.Result> 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;
}
}
}

View file

@ -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> 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<ScriptExpression> 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<CryptoHDKey> cryptoHDKeys = walletForm.getWallet().getKeystores().stream().map(this::getCryptoHDKey).collect(Collectors.toList());
MultiKey multiKey = new MultiKey(walletForm.getWallet().getDefaultPolicy().getNumSignaturesRequired(), null, cryptoHDKeys);
List<ScriptExpression> 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<ScriptExpression> 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<PathComponent> 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());
}
}

View file

@ -83,7 +83,7 @@
<DescriptorArea fx:id="descriptor" editable="false" styleClass="uneditable-codearea" prefHeight="27" maxHeight="27" />
</content>
</VirtualizedScrollPane>
<Button onAction="#scanDescriptorQR">
<Button fx:id="scanDescriptorQR" onAction="#scanDescriptorQR">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="CAMERA" />
</graphic>
@ -91,6 +91,14 @@
<Tooltip text="Scan a QR code" />
</tooltip>
</Button>
<Button fx:id="showDescriptorQR" onAction="#showDescriptorQR">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="QRCODE" />
</graphic>
<tooltip>
<Tooltip text="Show as QR code" />
</tooltip>
</Button>
<Button text="Edit..." onAction="#editDescriptor" />
</Field>
</Fieldset>