mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 12:46:45 +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') {
|
implementation('com.github.arteam:simple-json-rpc-server:1.0') {
|
||||||
exclude group: 'org.slf4j'
|
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') {
|
implementation('com.nativelibs4java:bridj:0.7-20140918-3') {
|
||||||
exclude group: 'com.google.android.tools', module: 'dx'
|
exclude group: 'com.google.android.tools', module: 'dx'
|
||||||
}
|
}
|
||||||
|
|
|
@ -465,7 +465,12 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
return wallets;
|
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)) {
|
if(List.of(ScriptExpression.PUBLIC_KEY_HASH).equals(expressions)) {
|
||||||
return ScriptType.P2PKH;
|
return ScriptType.P2PKH;
|
||||||
} else if(List.of(ScriptExpression.SCRIPT_HASH, ScriptExpression.WITNESS_PUBLIC_KEY_HASH).equals(expressions)) {
|
} else if(List.of(ScriptExpression.SCRIPT_HASH, ScriptExpression.WITNESS_PUBLIC_KEY_HASH).equals(expressions)) {
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.NamedArg;
|
import javafx.beans.NamedArg;
|
||||||
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
import javafx.scene.image.ImageView;
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public class TextAreaDialog extends Dialog<String> {
|
public class TextAreaDialog extends Dialog<String> {
|
||||||
private final TextArea textArea;
|
private final TextArea textArea;
|
||||||
|
@ -18,7 +24,8 @@ public class TextAreaDialog extends Dialog<String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public TextAreaDialog(@NamedArg("defaultValue") String defaultValue) {
|
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");
|
Image image = new Image("/image/sparrow-small.png");
|
||||||
dialogPane.setGraphic(new ImageView(image));
|
dialogPane.setGraphic(new ImageView(image));
|
||||||
|
@ -40,6 +47,9 @@ public class TextAreaDialog extends Dialog<String> {
|
||||||
dialogPane.getStyleClass().add("text-input-dialog");
|
dialogPane.getStyleClass().add("text-input-dialog");
|
||||||
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
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);
|
Platform.runLater(textArea::requestFocus);
|
||||||
|
|
||||||
setResultConverter((dialogButton) -> {
|
setResultConverter((dialogButton) -> {
|
||||||
|
@ -58,4 +68,53 @@ public class TextAreaDialog extends Dialog<String> {
|
||||||
public final String getDefaultValue() {
|
public final String getDefaultValue() {
|
||||||
return defaultValue;
|
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;
|
package com.sparrowwallet.sparrow.wallet;
|
||||||
|
|
||||||
import com.google.common.eventbus.Subscribe;
|
import com.google.common.eventbus.Subscribe;
|
||||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
import com.sparrowwallet.drongo.*;
|
||||||
import com.sparrowwallet.drongo.SecureString;
|
|
||||||
import com.sparrowwallet.drongo.crypto.*;
|
import com.sparrowwallet.drongo.crypto.*;
|
||||||
import com.sparrowwallet.drongo.policy.Policy;
|
import com.sparrowwallet.drongo.policy.Policy;
|
||||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
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.KeystoreSource;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
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.AppServices;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.control.*;
|
import com.sparrowwallet.sparrow.control.*;
|
||||||
|
@ -45,6 +46,12 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||||
@FXML
|
@FXML
|
||||||
private DescriptorArea descriptor;
|
private DescriptorArea descriptor;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button scanDescriptorQR;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button showDescriptorQR;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private ComboBox<ScriptType> scriptType;
|
private ComboBox<ScriptType> scriptType;
|
||||||
|
|
||||||
|
@ -172,6 +179,11 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||||
});
|
});
|
||||||
|
|
||||||
initializeDescriptorField(descriptor);
|
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 -> {
|
revert.setOnAction(event -> {
|
||||||
keystoreTabs.getTabs().removeAll(keystoreTabs.getTabs());
|
keystoreTabs.getTabs().removeAll(keystoreTabs.getTabs());
|
||||||
|
@ -223,6 +235,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||||
scriptType.getSelectionModel().select(walletForm.getWallet().getScriptType());
|
scriptType.getSelectionModel().select(walletForm.getWallet().getScriptType());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scanDescriptorQR.setVisible(!walletForm.getWallet().isValid());
|
||||||
export.setDisable(!walletForm.getWallet().isValid());
|
export.setDisable(!walletForm.getWallet().isValid());
|
||||||
revert.setDisable(true);
|
revert.setDisable(true);
|
||||||
apply.setDisable(true);
|
apply.setDisable(true);
|
||||||
|
@ -265,6 +278,14 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||||
QRScanDialog.Result result = optionalResult.get();
|
QRScanDialog.Result result = optionalResult.get();
|
||||||
if(result.outputDescriptor != null) {
|
if(result.outputDescriptor != null) {
|
||||||
setDescriptorText(result.outputDescriptor.toString());
|
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()) {
|
} else if(result.payload != null && !result.payload.isEmpty()) {
|
||||||
setDescriptorText(result.payload);
|
setDescriptorText(result.payload);
|
||||||
} else if(result.exception != null) {
|
} 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) {
|
public void editDescriptor(ActionEvent event) {
|
||||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(walletForm.getWallet());
|
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(walletForm.getWallet());
|
||||||
String outputDescriptorString = outputDescriptor.toString(walletForm.getWallet().isValid());
|
String outputDescriptorString = outputDescriptor.toString(walletForm.getWallet().isValid());
|
||||||
|
@ -359,6 +435,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||||
revert.setDisable(false);
|
revert.setDisable(false);
|
||||||
apply.setDisable(!wallet.isValid());
|
apply.setDisable(!wallet.isValid());
|
||||||
export.setDisable(true);
|
export.setDisable(true);
|
||||||
|
scanDescriptorQR.setVisible(!wallet.isValid());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -366,6 +443,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||||
public void walletSettingsChanged(WalletSettingsChangedEvent event) {
|
public void walletSettingsChanged(WalletSettingsChangedEvent event) {
|
||||||
if(event.getWalletFile().equals(walletForm.getWalletFile())) {
|
if(event.getWalletFile().equals(walletForm.getWalletFile())) {
|
||||||
export.setDisable(!event.getWallet().isValid());
|
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" />
|
<DescriptorArea fx:id="descriptor" editable="false" styleClass="uneditable-codearea" prefHeight="27" maxHeight="27" />
|
||||||
</content>
|
</content>
|
||||||
</VirtualizedScrollPane>
|
</VirtualizedScrollPane>
|
||||||
<Button onAction="#scanDescriptorQR">
|
<Button fx:id="scanDescriptorQR" onAction="#scanDescriptorQR">
|
||||||
<graphic>
|
<graphic>
|
||||||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="CAMERA" />
|
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="CAMERA" />
|
||||||
</graphic>
|
</graphic>
|
||||||
|
@ -91,6 +91,14 @@
|
||||||
<Tooltip text="Scan a QR code" />
|
<Tooltip text="Scan a QR code" />
|
||||||
</tooltip>
|
</tooltip>
|
||||||
</Button>
|
</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" />
|
<Button text="Edit..." onAction="#editDescriptor" />
|
||||||
</Field>
|
</Field>
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
|
|
Loading…
Reference in a new issue