mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-25 05:06:45 +00:00
recommend backup of output descriptor when saving new multisig wallets
This commit is contained in:
parent
1f67692727
commit
2b4d3fac6c
6 changed files with 204 additions and 69 deletions
|
@ -1,28 +1,13 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.lowagie.text.*;
|
|
||||||
import com.lowagie.text.Font;
|
|
||||||
import com.lowagie.text.Image;
|
|
||||||
import com.lowagie.text.pdf.PdfWriter;
|
|
||||||
import com.sparrowwallet.hummingbird.UR;
|
import com.sparrowwallet.hummingbird.UR;
|
||||||
import com.sparrowwallet.hummingbird.UREncoder;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.io.PdfUtils;
|
||||||
import javafx.embed.swing.SwingFXUtils;
|
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.stage.FileChooser;
|
|
||||||
import javafx.stage.Stage;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.awt.*;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
|
|
||||||
public class DescriptorQRDisplayDialog extends QRDisplayDialog {
|
public class DescriptorQRDisplayDialog extends QRDisplayDialog {
|
||||||
private static final Logger log = LoggerFactory.getLogger(DescriptorQRDisplayDialog.class);
|
|
||||||
|
|
||||||
public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur) {
|
public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur) {
|
||||||
super(ur);
|
super(ur);
|
||||||
|
|
||||||
|
@ -31,43 +16,11 @@ public class DescriptorQRDisplayDialog extends QRDisplayDialog {
|
||||||
dialogPane.getButtonTypes().add(pdfButtonType);
|
dialogPane.getButtonTypes().add(pdfButtonType);
|
||||||
|
|
||||||
Button pdfButton = (Button)dialogPane.lookupButton(pdfButtonType);
|
Button pdfButton = (Button)dialogPane.lookupButton(pdfButtonType);
|
||||||
|
pdfButton.setGraphicTextGap(5);
|
||||||
|
pdfButton.setGraphic(getGlyph(FontAwesome5.Glyph.FILE_PDF));
|
||||||
pdfButton.addEventFilter(ActionEvent.ACTION, event -> {
|
pdfButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||||
savePdf(walletName, outputDescriptor, ur);
|
PdfUtils.saveOutputDescriptor(walletName, outputDescriptor, ur);
|
||||||
event.consume();
|
event.consume();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void savePdf(String walletName, String outputDescriptor, UR ur) {
|
|
||||||
Stage window = new Stage();
|
|
||||||
FileChooser fileChooser = new FileChooser();
|
|
||||||
fileChooser.setTitle("Save PDF");
|
|
||||||
fileChooser.setInitialFileName(walletName + ".pdf");
|
|
||||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
|
||||||
File file = fileChooser.showSaveDialog(window);
|
|
||||||
if(file != null) {
|
|
||||||
try(Document document = new Document()) {
|
|
||||||
document.setMargins(36, 36, 48, 36);
|
|
||||||
PdfWriter.getInstance(document, new FileOutputStream(file));
|
|
||||||
document.open();
|
|
||||||
|
|
||||||
Font titleFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 16, Color.BLACK);
|
|
||||||
Chunk title = new Chunk("Output descriptor for " + walletName, titleFont);
|
|
||||||
document.add(title);
|
|
||||||
|
|
||||||
UREncoder urEncoder = new UREncoder(ur, 2000, 10, 0);
|
|
||||||
String fragment = urEncoder.nextPart();
|
|
||||||
if(urEncoder.isSinglePart()) {
|
|
||||||
Image image = Image.getInstance(SwingFXUtils.fromFXImage(getQrCode(fragment), null), Color.WHITE);
|
|
||||||
document.add(image);
|
|
||||||
}
|
|
||||||
|
|
||||||
Font descriptorFont = FontFactory.getFont(FontFactory.COURIER, 14, Color.BLACK);
|
|
||||||
Paragraph descriptor = new Paragraph(outputDescriptor, descriptorFont);
|
|
||||||
document.add(descriptor);
|
|
||||||
} catch(Exception e) {
|
|
||||||
log.error("Error creating descriptor PDF", e);
|
|
||||||
AppServices.showErrorDialog("Error creating PDF", e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -244,11 +244,11 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||||
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.BAN));
|
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.BAN));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
|
protected static Glyph getGlyph(FontAwesome5.Glyph glyphName) {
|
||||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
|
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
|
||||||
glyph.setFontSize(11);
|
glyph.setFontSize(11);
|
||||||
return glyph;
|
return glyph;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@ public class FontAwesome5 extends GlyphFont {
|
||||||
FEATHER_ALT('\uf56b'),
|
FEATHER_ALT('\uf56b'),
|
||||||
FILE_CSV('\uf6dd'),
|
FILE_CSV('\uf6dd'),
|
||||||
FILE_IMPORT('\uf56f'),
|
FILE_IMPORT('\uf56f'),
|
||||||
|
FILE_PDF('\uf1c1'),
|
||||||
HAND_HOLDING('\uf4bd'),
|
HAND_HOLDING('\uf4bd'),
|
||||||
HAND_HOLDING_MEDICAL('\ue05c'),
|
HAND_HOLDING_MEDICAL('\ue05c'),
|
||||||
HAND_HOLDING_WATER('\uf4c1'),
|
HAND_HOLDING_WATER('\uf4c1'),
|
||||||
|
|
75
src/main/java/com/sparrowwallet/sparrow/io/PdfUtils.java
Normal file
75
src/main/java/com/sparrowwallet/sparrow/io/PdfUtils.java
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
package com.sparrowwallet.sparrow.io;
|
||||||
|
|
||||||
|
import com.google.zxing.BarcodeFormat;
|
||||||
|
import com.google.zxing.WriterException;
|
||||||
|
import com.google.zxing.client.j2se.MatrixToImageConfig;
|
||||||
|
import com.google.zxing.client.j2se.MatrixToImageWriter;
|
||||||
|
import com.google.zxing.common.BitMatrix;
|
||||||
|
import com.google.zxing.qrcode.QRCodeWriter;
|
||||||
|
import com.lowagie.text.*;
|
||||||
|
import com.lowagie.text.Font;
|
||||||
|
import com.lowagie.text.Image;
|
||||||
|
import com.lowagie.text.pdf.PdfWriter;
|
||||||
|
import com.sparrowwallet.hummingbird.UR;
|
||||||
|
import com.sparrowwallet.hummingbird.UREncoder;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import javafx.embed.swing.SwingFXUtils;
|
||||||
|
import javafx.stage.FileChooser;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.io.*;
|
||||||
|
|
||||||
|
public class PdfUtils {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(PdfUtils.class);
|
||||||
|
|
||||||
|
private static final int QR_WIDTH = 480;
|
||||||
|
private static final int QR_HEIGHT = 480;
|
||||||
|
|
||||||
|
public static void saveOutputDescriptor(String walletName, String outputDescriptor, UR ur) {
|
||||||
|
Stage window = new Stage();
|
||||||
|
FileChooser fileChooser = new FileChooser();
|
||||||
|
fileChooser.setTitle("Save PDF");
|
||||||
|
fileChooser.setInitialFileName(walletName + ".pdf");
|
||||||
|
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||||
|
File file = fileChooser.showSaveDialog(window);
|
||||||
|
if(file != null) {
|
||||||
|
try(Document document = new Document()) {
|
||||||
|
document.setMargins(36, 36, 48, 36);
|
||||||
|
PdfWriter.getInstance(document, new FileOutputStream(file));
|
||||||
|
document.open();
|
||||||
|
|
||||||
|
Font titleFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 16, Color.BLACK);
|
||||||
|
Chunk title = new Chunk("Output descriptor for " + walletName, titleFont);
|
||||||
|
document.add(title);
|
||||||
|
|
||||||
|
UREncoder urEncoder = new UREncoder(ur, 2000, 10, 0);
|
||||||
|
String fragment = urEncoder.nextPart();
|
||||||
|
if(urEncoder.isSinglePart()) {
|
||||||
|
Image image = Image.getInstance(SwingFXUtils.fromFXImage(getQrCode(fragment), null), Color.WHITE);
|
||||||
|
document.add(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
Font descriptorFont = FontFactory.getFont(FontFactory.COURIER, 14, Color.BLACK);
|
||||||
|
Paragraph descriptor = new Paragraph(outputDescriptor, descriptorFont);
|
||||||
|
document.add(descriptor);
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error("Error creating descriptor PDF", e);
|
||||||
|
AppServices.showErrorDialog("Error creating PDF", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static javafx.scene.image.Image getQrCode(String fragment) throws IOException, WriterException {
|
||||||
|
QRCodeWriter qrCodeWriter = new QRCodeWriter();
|
||||||
|
BitMatrix qrMatrix = qrCodeWriter.encode(fragment, BarcodeFormat.QR_CODE, QR_WIDTH, QR_HEIGHT);
|
||||||
|
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
MatrixToImageWriter.writeToStream(qrMatrix, "PNG", baos, new MatrixToImageConfig());
|
||||||
|
|
||||||
|
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
|
||||||
|
return new javafx.scene.image.Image(bais);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
package com.sparrowwallet.sparrow.wallet;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.hummingbird.UR;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
|
import com.sparrowwallet.sparrow.io.PdfUtils;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.Priority;
|
||||||
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
|
||||||
|
public class MultisigBackupDialog extends Dialog<String> {
|
||||||
|
private final Wallet wallet;
|
||||||
|
private final String descriptor;
|
||||||
|
private final UR ur;
|
||||||
|
|
||||||
|
private final TextArea textArea;
|
||||||
|
|
||||||
|
public MultisigBackupDialog(Wallet wallet, String descriptor, UR ur) {
|
||||||
|
this.wallet = wallet;
|
||||||
|
this.descriptor = descriptor;
|
||||||
|
this.ur = ur;
|
||||||
|
|
||||||
|
setTitle("Backup Multisig Wallet?");
|
||||||
|
|
||||||
|
DialogPane dialogPane = new MultisigBackupDialogPane();
|
||||||
|
dialogPane.setHeaderText("To restore this multisig wallet, you need at least " + wallet.getDefaultPolicy().getNumSignaturesRequired() + " seeds and ALL of the xpubs!\n" +
|
||||||
|
"It is recommended to backup either this wallet file, or the wallet output descriptor.\n\n" +
|
||||||
|
"The wallet output descriptor contains all the xpubs and is shown below.\n" +
|
||||||
|
"Alternatively, use the Export button below to export the Sparrow wallet file.");
|
||||||
|
setDialogPane(dialogPane);
|
||||||
|
|
||||||
|
dialogPane.getStyleClass().addAll("alert", "warning");
|
||||||
|
|
||||||
|
HBox hbox = new HBox();
|
||||||
|
this.textArea = new TextArea(descriptor);
|
||||||
|
this.textArea.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
this.textArea.setWrapText(true);
|
||||||
|
this.textArea.getStyleClass().add("fixed-width");
|
||||||
|
this.textArea.setEditable(false);
|
||||||
|
hbox.getChildren().add(textArea);
|
||||||
|
HBox.setHgrow(this.textArea, Priority.ALWAYS);
|
||||||
|
|
||||||
|
dialogPane.setContent(hbox);
|
||||||
|
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||||
|
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||||
|
|
||||||
|
dialogPane.getStyleClass().add("text-input-dialog");
|
||||||
|
dialogPane.getButtonTypes().add(ButtonType.OK);
|
||||||
|
|
||||||
|
final ButtonType qrButtonType = new javafx.scene.control.ButtonType("Save PDF...", ButtonBar.ButtonData.LEFT);
|
||||||
|
dialogPane.getButtonTypes().add(qrButtonType);
|
||||||
|
|
||||||
|
dialogPane.setPrefWidth(700);
|
||||||
|
dialogPane.setPrefHeight(350);
|
||||||
|
AppServices.moveToActiveWindowScreen(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MultisigBackupDialogPane extends DialogPane {
|
||||||
|
@Override
|
||||||
|
protected Node createButton(ButtonType buttonType) {
|
||||||
|
Node button;
|
||||||
|
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||||
|
Button pdfButton = new Button(buttonType.getText());
|
||||||
|
pdfButton.setGraphicTextGap(5);
|
||||||
|
pdfButton.setGraphic(getGlyph(FontAwesome5.Glyph.FILE_PDF));
|
||||||
|
|
||||||
|
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||||
|
ButtonBar.setButtonData(pdfButton, buttonData);
|
||||||
|
pdfButton.setOnAction(event -> {
|
||||||
|
PdfUtils.saveOutputDescriptor(wallet.getFullDisplayName(), descriptor, ur);
|
||||||
|
});
|
||||||
|
|
||||||
|
button = pdfButton;
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -217,7 +217,16 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||||
apply.setOnAction(event -> {
|
apply.setOnAction(event -> {
|
||||||
revert.setDisable(true);
|
revert.setDisable(true);
|
||||||
apply.setDisable(true);
|
apply.setDisable(true);
|
||||||
|
boolean addressChange = ((SettingsWalletForm)walletForm).isAddressChange();
|
||||||
saveWallet(false, false);
|
saveWallet(false, false);
|
||||||
|
|
||||||
|
Wallet wallet = walletForm.getWallet();
|
||||||
|
if(wallet.getPolicyType() == PolicyType.MULTI && wallet.getDefaultPolicy().getNumSignaturesRequired() < wallet.getKeystores().size() && addressChange) {
|
||||||
|
String outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.DEFAULT_PURPOSES, null).toString(true);
|
||||||
|
CryptoOutput cryptoOutput = getCryptoOutput(wallet);
|
||||||
|
MultisigBackupDialog dialog = new MultisigBackupDialog(wallet, outputDescriptor, cryptoOutput.toUR());
|
||||||
|
dialog.showAndWait();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setFieldsFromWallet(walletForm.getWallet());
|
setFieldsFromWallet(walletForm.getWallet());
|
||||||
|
@ -325,18 +334,8 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||||
}
|
}
|
||||||
|
|
||||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(walletForm.getWallet(), KeyPurpose.DEFAULT_PURPOSES, null);
|
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(walletForm.getWallet(), KeyPurpose.DEFAULT_PURPOSES, null);
|
||||||
List<ScriptExpression> scriptExpressions = getScriptExpressions(walletForm.getWallet().getScriptType());
|
CryptoOutput cryptoOutput = getCryptoOutput(walletForm.getWallet());
|
||||||
|
if(cryptoOutput == null) {
|
||||||
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.");
|
AppServices.showErrorDialog("Unsupported Wallet Policy", "Cannot show a descriptor for this wallet.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -346,6 +345,23 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||||
qrDisplayDialog.showAndWait();
|
qrDisplayDialog.showAndWait();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private CryptoOutput getCryptoOutput(Wallet wallet) {
|
||||||
|
List<ScriptExpression> scriptExpressions = getScriptExpressions(wallet.getScriptType());
|
||||||
|
|
||||||
|
CryptoOutput cryptoOutput = null;
|
||||||
|
if(wallet.getPolicyType() == PolicyType.SINGLE) {
|
||||||
|
cryptoOutput = new CryptoOutput(scriptExpressions, getCryptoHDKey(wallet.getKeystores().get(0)));
|
||||||
|
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
|
||||||
|
List<CryptoHDKey> cryptoHDKeys = wallet.getKeystores().stream().map(this::getCryptoHDKey).collect(Collectors.toList());
|
||||||
|
MultiKey multiKey = new MultiKey(wallet.getDefaultPolicy().getNumSignaturesRequired(), null, cryptoHDKeys);
|
||||||
|
List<ScriptExpression> multiScriptExpressions = new ArrayList<>(scriptExpressions);
|
||||||
|
multiScriptExpressions.add(ScriptExpression.SORTED_MULTISIG);
|
||||||
|
cryptoOutput = new CryptoOutput(multiScriptExpressions, multiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cryptoOutput;
|
||||||
|
}
|
||||||
|
|
||||||
private List<ScriptExpression> getScriptExpressions(ScriptType scriptType) {
|
private List<ScriptExpression> getScriptExpressions(ScriptType scriptType) {
|
||||||
if(scriptType == ScriptType.P2PK) {
|
if(scriptType == ScriptType.P2PK) {
|
||||||
return List.of(ScriptExpression.PUBLIC_KEY);
|
return List.of(ScriptExpression.PUBLIC_KEY);
|
||||||
|
|
Loading…
Reference in a new issue