diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DescriptorQRDisplayDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/DescriptorQRDisplayDialog.java index c846ca0d..e844e923 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DescriptorQRDisplayDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DescriptorQRDisplayDialog.java @@ -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()); - } - } - } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/QRDisplayDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/QRDisplayDialog.java index 4fb7ba40..5107cb37 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/QRDisplayDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/QRDisplayDialog.java @@ -244,11 +244,11 @@ public class QRDisplayDialog extends Dialog { 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; } } diff --git a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java index f2510803..434f3f31 100644 --- a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java +++ b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java @@ -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'), diff --git a/src/main/java/com/sparrowwallet/sparrow/io/PdfUtils.java b/src/main/java/com/sparrowwallet/sparrow/io/PdfUtils.java new file mode 100644 index 00000000..c64ae8e2 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/PdfUtils.java @@ -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); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/MultisigBackupDialog.java b/src/main/java/com/sparrowwallet/sparrow/wallet/MultisigBackupDialog.java new file mode 100644 index 00000000..2adc1837 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/MultisigBackupDialog.java @@ -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 { + 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; + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java index 7a962a7e..dc08ceee 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java @@ -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 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 cryptoHDKeys = walletForm.getWallet().getKeystores().stream().map(this::getCryptoHDKey).collect(Collectors.toList()); - MultiKey multiKey = new MultiKey(walletForm.getWallet().getDefaultPolicy().getNumSignaturesRequired(), null, cryptoHDKeys); - List 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 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 cryptoHDKeys = wallet.getKeystores().stream().map(this::getCryptoHDKey).collect(Collectors.toList()); + MultiKey multiKey = new MultiKey(wallet.getDefaultPolicy().getNumSignaturesRequired(), null, cryptoHDKeys); + List multiScriptExpressions = new ArrayList<>(scriptExpressions); + multiScriptExpressions.add(ScriptExpression.SORTED_MULTISIG); + cryptoOutput = new CryptoOutput(multiScriptExpressions, multiKey); + } + + return cryptoOutput; + } + private List getScriptExpressions(ScriptType scriptType) { if(scriptType == ScriptType.P2PK) { return List.of(ScriptExpression.PUBLIC_KEY);