mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 12:46: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;
|
||||
|
||||
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 com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.PdfUtils;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.scene.control.*;
|
||||
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 {
|
||||
private static final Logger log = LoggerFactory.getLogger(DescriptorQRDisplayDialog.class);
|
||||
|
||||
public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur) {
|
||||
super(ur);
|
||||
|
||||
|
@ -31,43 +16,11 @@ public class DescriptorQRDisplayDialog extends QRDisplayDialog {
|
|||
dialogPane.getButtonTypes().add(pdfButtonType);
|
||||
|
||||
Button pdfButton = (Button)dialogPane.lookupButton(pdfButtonType);
|
||||
pdfButton.setGraphicTextGap(5);
|
||||
pdfButton.setGraphic(getGlyph(FontAwesome5.Glyph.FILE_PDF));
|
||||
pdfButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
savePdf(walletName, outputDescriptor, ur);
|
||||
PdfUtils.saveOutputDescriptor(walletName, outputDescriptor, ur);
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
|
||||
glyph.setFontSize(11);
|
||||
return glyph;
|
||||
}
|
||||
protected static Glyph getGlyph(FontAwesome5.Glyph glyphName) {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
|
||||
glyph.setFontSize(11);
|
||||
return glyph;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ public class FontAwesome5 extends GlyphFont {
|
|||
FEATHER_ALT('\uf56b'),
|
||||
FILE_CSV('\uf6dd'),
|
||||
FILE_IMPORT('\uf56f'),
|
||||
FILE_PDF('\uf1c1'),
|
||||
HAND_HOLDING('\uf4bd'),
|
||||
HAND_HOLDING_MEDICAL('\ue05c'),
|
||||
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 -> {
|
||||
revert.setDisable(true);
|
||||
apply.setDisable(true);
|
||||
boolean addressChange = ((SettingsWalletForm)walletForm).isAddressChange();
|
||||
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());
|
||||
|
@ -325,18 +334,8 @@ public class SettingsController extends WalletFormController implements Initiali
|
|||
}
|
||||
|
||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(walletForm.getWallet(), KeyPurpose.DEFAULT_PURPOSES, null);
|
||||
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 {
|
||||
CryptoOutput cryptoOutput = getCryptoOutput(walletForm.getWallet());
|
||||
if(cryptoOutput == null) {
|
||||
AppServices.showErrorDialog("Unsupported Wallet Policy", "Cannot show a descriptor for this wallet.");
|
||||
return;
|
||||
}
|
||||
|
@ -346,6 +345,23 @@ public class SettingsController extends WalletFormController implements Initiali
|
|||
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) {
|
||||
if(scriptType == ScriptType.P2PK) {
|
||||
return List.of(ScriptExpression.PUBLIC_KEY);
|
||||
|
|
Loading…
Reference in a new issue