mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-23 20:36:44 +00:00
support ur:crypto-output scan and display of wallet output descriptor
This commit is contained in:
parent
2e86840e92
commit
2dfdbd6d78
5 changed files with 156 additions and 6 deletions
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue