diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 42f4c916..e816e592 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -771,7 +771,8 @@ public class AppController implements Initializable { byte[] txBytes = transaction.bitcoinSerialize(); UR ur = UR.fromBytes(txBytes); BBQR bbqr = new BBQR(BBQRType.TXN, txBytes); - QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, false); + String raw = Utils.bytesToHex(txBytes); + QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(ur, bbqr, raw, false, false, QREncoding.UR); qrDisplayDialog.initOwner(rootStack.getScene().getWindow()); qrDisplayDialog.showAndWait(); } catch(Exception e) { @@ -873,7 +874,7 @@ public class AppController implements Initializable { byte[] psbtBytes = transactionTabData.getPsbt().getForExport().serialize(); CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes); BBQR bbqr = new BBQR(BBQRType.PSBT, psbtBytes); - QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, false); + QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, QREncoding.UR); qrDisplayDialog.initOwner(rootStack.getScene().getWindow()); qrDisplayDialog.show(); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DescriptorQRDisplayDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/DescriptorQRDisplayDialog.java index ab1d2c24..db4bf1c0 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DescriptorQRDisplayDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DescriptorQRDisplayDialog.java @@ -9,8 +9,8 @@ import javafx.scene.control.*; import javafx.scene.control.Button; public class DescriptorQRDisplayDialog extends QRDisplayDialog { - public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur, BBQR bbqr, boolean selectBbqrButton) { - super(ur, bbqr, false, false, selectBbqrButton); + public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur, BBQR bbqr, QREncoding encoding) { + super(ur, bbqr, false, false, encoding); DialogPane dialogPane = getDialogPane(); final ButtonType pdfButtonType = new javafx.scene.control.ButtonType("Save PDF...", ButtonBar.ButtonData.HELP_2); @@ -20,7 +20,7 @@ public class DescriptorQRDisplayDialog extends QRDisplayDialog { pdfButton.setGraphicTextGap(5); pdfButton.setGraphic(getGlyph(FontAwesome5.Glyph.FILE_PDF)); pdfButton.addEventFilter(ActionEvent.ACTION, event -> { - PdfUtils.saveOutputDescriptor(walletName, outputDescriptor, ur, isUseBbqrEncoding() ? bbqr : null); + PdfUtils.saveOutputDescriptor(walletName, outputDescriptor, ur, getEncoding() == QREncoding.BBQR ? bbqr : null); event.consume(); }); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/FileKeystoreExportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/FileKeystoreExportPane.java index 6eecf0f3..7340bea6 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/FileKeystoreExportPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/FileKeystoreExportPane.java @@ -157,7 +157,7 @@ public class FileKeystoreExportPane extends TitledDescriptionPane { if(exporter instanceof Bip129) { UR ur = UR.fromBytes(baos.toByteArray()); BBQR bbqr = new BBQR(BBQRType.UNICODE, baos.toByteArray()); - qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, true, false); + qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, true, QREncoding.UR); } else { qrDisplayDialog = new QRDisplayDialog(baos.toString(StandardCharsets.UTF_8)); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java index 4980de8b..81ffd20c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java @@ -171,18 +171,18 @@ public class FileWalletExportPane extends TitledDescriptionPane { } else if(exporter instanceof Bip129 || exporter instanceof WalletLabels) { UR ur = UR.fromBytes(outputStream.toByteArray()); BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray()); - qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, false); + qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, QREncoding.UR); } else if(exporter instanceof Descriptor) { boolean addBbqrOption = exportWallet.getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().showBbqr()); - boolean selectBbqrOption = exportWallet.getKeystores().stream().allMatch(keystore -> keystore.getWalletModel().selectBbqr()); + QREncoding encoding = exportWallet.getKeystores().stream().allMatch(keystore -> keystore.getWalletModel().selectBbqr()) ? QREncoding.BBQR : QREncoding.UR; OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(exportWallet, KeyPurpose.DEFAULT_PURPOSES, null); CryptoOutput cryptoOutput = getCryptoOutput(exportWallet); BBQR bbqr = addBbqrOption ? new BBQR(BBQRType.UNICODE, outputDescriptor.toString(true).getBytes(StandardCharsets.UTF_8)) : null; - qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), cryptoOutput.toUR(), bbqr, selectBbqrOption); + qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), cryptoOutput.toUR(), bbqr, encoding); } else if(exporter.getClass().equals(ColdcardMultisig.class)) { UR ur = UR.fromBytes(outputStream.toByteArray()); BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray()); - qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, true); + qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, QREncoding.BBQR); } else { qrDisplayDialog = new QRDisplayDialog(outputStream.toString(StandardCharsets.UTF_8)); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/QRDisplayDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/QRDisplayDialog.java index c6137cc9..29636a8c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/QRDisplayDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/QRDisplayDialog.java @@ -57,7 +57,11 @@ public class QRDisplayDialog extends Dialog { private final BBQR bbqr; private BBQREncoder bbqrEncoder; - private boolean useBbqrEncoding; + + private final String raw; + private final boolean rawEncodable; + + private QREncoding encoding = QREncoding.UR; private final ImageView qrImageView; @@ -72,14 +76,18 @@ public class QRDisplayDialog extends Dialog { private static boolean initialDensityChange; public QRDisplayDialog(String type, byte[] data, boolean addLegacyEncodingOption) throws UR.URException { - this(UR.fromBytes(type, data), null, addLegacyEncodingOption, false, false); + this(UR.fromBytes(type, data), null, addLegacyEncodingOption, false, QREncoding.UR); } public QRDisplayDialog(UR ur) { - this(ur, null, false, false, false); + this(ur, null, false, false, QREncoding.UR); } - public QRDisplayDialog(UR ur, BBQR bbqr, boolean addLegacyEncodingOption, boolean addScanButton, boolean selectBbqrButton) { + public QRDisplayDialog(UR ur, BBQR bbqr, boolean addLegacyEncodingOption, boolean addScanButton, QREncoding defaultEncoding) { + this(ur, bbqr, null, addLegacyEncodingOption, addScanButton, defaultEncoding); + } + + public QRDisplayDialog(UR ur, BBQR bbqr, String raw, boolean addLegacyEncodingOption, boolean addScanButton, QREncoding defaultEncoding) { this.ur = ur; this.bbqr = bbqr; this.addLegacyEncodingOption = bbqr == null && addLegacyEncodingOption; @@ -88,11 +96,14 @@ public class QRDisplayDialog extends Dialog { if(bbqr != null) { this.bbqrEncoder = new BBQREncoder(bbqr.type(), DEFAULT_BBQR_ENCODING, bbqr.data(), Config.get().getQrDensity().getMaxBbqrFragmentLength(), 0); - if(selectBbqrButton) { - useBbqrEncoding = true; + if(defaultEncoding == QREncoding.BBQR || Config.get().getQrEncoding() == QREncoding.BBQR) { + encoding = QREncoding.BBQR; } } + this.raw = raw; + this.rawEncodable = raw != null && getQrCode(raw, true) != null; + final DialogPane dialogPane = new QRDisplayDialogPane(); setDialogPane(dialogPane); AppServices.setStageIcon(dialogPane.getScene().getWindow()); @@ -156,8 +167,10 @@ public class QRDisplayDialog extends Dialog { public QRDisplayDialog(String data, boolean addScanButton) { this.ur = null; this.bbqr = null; + this.raw = data; this.urEncoder = null; this.bbqrEncoder = null; + this.rawEncodable = true; final DialogPane dialogPane = new QRDisplayDialogPane(); setDialogPane(dialogPane); @@ -205,7 +218,9 @@ public class QRDisplayDialog extends Dialog { } private boolean isSinglePart() { - if(useBbqrEncoding) { + if(encoding == QREncoding.RAW) { + return true; + } else if(encoding == QREncoding.BBQR) { return bbqrEncoder.isSinglePart(); } else if(!useLegacyEncoding) { return urEncoder.isSinglePart(); @@ -215,7 +230,9 @@ public class QRDisplayDialog extends Dialog { } private void nextPart() { - if(useBbqrEncoding) { + if(encoding == QREncoding.RAW) { + currentPart = raw; + } else if(encoding == QREncoding.BBQR) { String fragment = bbqrEncoder.nextPart(); currentPart = fragment.toUpperCase(Locale.ROOT); } else if(!useLegacyEncoding) { @@ -231,6 +248,10 @@ public class QRDisplayDialog extends Dialog { } protected Image getQrCode(String fragment) { + return getQrCode(fragment, false); + } + + protected Image getQrCode(String fragment, boolean trial) { try { QRCodeWriter qrCodeWriter = new QRCodeWriter(); BitMatrix qrMatrix = qrCodeWriter.encode(fragment, BarcodeFormat.QR_CODE, qrSize, qrSize, Map.of(EncodeHintType.MARGIN, "2")); @@ -241,7 +262,9 @@ public class QRDisplayDialog extends Dialog { ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); return new Image(bais); } catch(Exception e) { - log.error("Error generating QR", e); + if(!trial) { + log.error("Error generating QR", e); + } } return null; @@ -265,18 +288,13 @@ public class QRDisplayDialog extends Dialog { } } - public boolean isUseBbqrEncoding() { - return useBbqrEncoding; + public QREncoding getEncoding() { + return encoding; } - private void setUseBbqrEncoding(boolean useBbqrEncoding) { - if(useBbqrEncoding) { - this.useBbqrEncoding = true; - restartAnimation(); - } else { - this.useBbqrEncoding = false; - restartAnimation(); - } + private void setEncoding(QREncoding encoding) { + this.encoding = encoding; + restartAnimation(); } private void changeQRDensity() { @@ -373,18 +391,29 @@ public class QRDisplayDialog extends Dialog { return scanButton; } else if(buttonType.getButtonData() == ButtonBar.ButtonData.BACK_PREVIOUS) { - ToggleButton bbqr = new ToggleButton(buttonType.getText()); - bbqr.setGraphicTextGap(5); - bbqr.setGraphic(getGlyph(FontAwesome5.Glyph.QRCODE)); - bbqr.setSelected(useBbqrEncoding); - final ButtonBar.ButtonData buttonData = buttonType.getButtonData(); - ButtonBar.setButtonData(bbqr, buttonData); + ComboBox encodingComboBox = new ComboBox<>(); + if(ur != null) { + encodingComboBox.getItems().add(QREncoding.UR); + } + if(bbqr != null) { + encodingComboBox.getItems().add(QREncoding.BBQR); + } + if(raw != null) { + encodingComboBox.getItems().add(QREncoding.RAW); + } - bbqr.selectedProperty().addListener((observable, oldValue, newValue) -> { - setUseBbqrEncoding(newValue); + encodingComboBox.setCellFactory(_ -> new QREncodingListCell()); + encodingComboBox.setButtonCell(new QREncodingButtonCell()); + encodingComboBox.setValue(encoding); + encodingComboBox.setOnAction(_ -> { + setEncoding(encodingComboBox.getValue()); + Config.get().setQrEncoding(encodingComboBox.getValue()); }); - return bbqr; + final ButtonBar.ButtonData buttonData = buttonType.getButtonData(); + ButtonBar.setButtonData(encodingComboBox, buttonData); + + return encodingComboBox; } return super.createButton(buttonType); @@ -413,4 +442,38 @@ public class QRDisplayDialog extends Dialog { glyph.setFontSize(11); return glyph; } + + private class QREncodingListCell extends ListCell { + @Override + protected void updateItem(QREncoding item, boolean empty) { + super.updateItem(item, empty); + if(empty || item == null) { + setText(null); + setGraphic(null); + setDisable(false); + setOpacity(1.0); + } else { + setText(item.getName() + " Encoding"); + setGraphic(item.getSVGImage()); + setGraphicTextGap(8.0d); + setDisable(item == QREncoding.RAW && !rawEncodable); + setOpacity(isDisabled() ? 0.5 : 1.0); + } + } + } + + private static class QREncodingButtonCell extends ListCell { + @Override + protected void updateItem(QREncoding item, boolean empty) { + super.updateItem(item, empty); + if(item == null || empty) { + setText(""); + setGraphic(null); + } else { + setText(item.getName()); + setGraphic(item.getSVGImage()); + setGraphicTextGap(8.0d); + } + } + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/QREncoding.java b/src/main/java/com/sparrowwallet/sparrow/control/QREncoding.java new file mode 100644 index 00000000..b309ec85 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/QREncoding.java @@ -0,0 +1,49 @@ +package com.sparrowwallet.sparrow.control; + +import com.sparrowwallet.sparrow.AppServices; +import com.sparrowwallet.sparrow.Theme; +import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; +import com.sparrowwallet.sparrow.io.Config; +import javafx.geometry.Insets; +import javafx.scene.Node; +import org.controlsfx.glyphfont.Glyph; +import org.girod.javafx.svgimage.SVGLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URL; +import java.util.Locale; + +public enum QREncoding { + UR("UR"), BBQR("BBQr"), RAW("Raw"); + + private static final Logger log = LoggerFactory.getLogger(QREncoding.class); + + QREncoding(String name) { + this.name = name; + } + + private final String name; + + public String getName() { + return name; + } + + public Node getSVGImage() { + try { + URL url = AppServices.class.getResource("/image/qrencoding/" + getName().toLowerCase(Locale.ROOT) + "-icon" + (Config.get().getTheme() == Theme.DARK ? "-invert" : "") + ".svg"); + if(url != null) { + return SVGLoader.load(url); + } else { + Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.QRCODE); + glyph.setFontSize(12); + glyph.setPadding(new Insets(0, 2, 0, 0)); + return glyph; + } + } catch(Exception e) { + log.error("Could not load QR encoding source image for " + name); + } + + return null; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Config.java b/src/main/java/com/sparrowwallet/sparrow/io/Config.java index 06843efb..06dda954 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Config.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Config.java @@ -7,6 +7,7 @@ import com.sparrowwallet.sparrow.UnitFormat; import com.sparrowwallet.sparrow.Mode; import com.sparrowwallet.sparrow.Theme; import com.sparrowwallet.sparrow.control.QRDensity; +import com.sparrowwallet.sparrow.control.QREncoding; import com.sparrowwallet.sparrow.control.WebcamResolution; import com.sparrowwallet.sparrow.net.*; import com.sparrowwallet.sparrow.wallet.FeeRatesSelection; @@ -62,6 +63,7 @@ public class Config { private long dustAttackThreshold = DUST_ATTACK_THRESHOLD_SATS; private int enumerateHwPeriod = ENUMERATE_HW_PERIOD_SECS; private QRDensity qrDensity; + private QREncoding qrEncoding; private WebcamResolution webcamResolution; private boolean mirrorCapture = true; private boolean useZbar = true; @@ -429,6 +431,15 @@ public class Config { flush(); } + public QREncoding getQrEncoding() { + return qrEncoding; + } + + public void setQrEncoding(QREncoding qrEncoding) { + this.qrEncoding = qrEncoding; + flush(); + } + public WebcamResolution getWebcamResolution() { return webcamResolution; } diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index 314991f1..ce2bc472 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java @@ -982,7 +982,7 @@ public class HeadersController extends TransactionFormController implements Init //TODO: Remove once Cobo Vault support has been removed boolean addLegacyEncodingOption = headersForm.getSigningWallet().getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().showLegacyQR()); boolean addBbqrOption = headersForm.getSigningWallet().getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().showBbqr()); - boolean selectBbqrOption = headersForm.getSigningWallet().getKeystores().stream().allMatch(keystore -> keystore.getWalletModel().selectBbqr()); + QREncoding encoding = headersForm.getSigningWallet().getKeystores().stream().allMatch(keystore -> keystore.getWalletModel().selectBbqr()) ? QREncoding.BBQR : QREncoding.UR; //Don't include non witness utxo fields for segwit wallets when displaying the PSBT as a QR - it can add greatly to the time required for scanning boolean includeNonWitnessUtxos = !Arrays.asList(ScriptType.WITNESS_TYPES).contains(headersForm.getSigningWallet().getScriptType()); @@ -990,7 +990,7 @@ public class HeadersController extends TransactionFormController implements Init CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes); BBQR bbqr = addBbqrOption ? new BBQR(BBQRType.PSBT, psbtBytes) : null; - QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, addLegacyEncodingOption, true, selectBbqrOption); + QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, addLegacyEncodingOption, true, encoding); qrDisplayDialog.initOwner(toggleButton.getScene().getWindow()); Optional optButtonType = qrDisplayDialog.showAndWait(); if(optButtonType.isPresent() && optButtonType.get().getButtonData() == ButtonBar.ButtonData.OK_DONE) { @@ -1360,7 +1360,8 @@ public class HeadersController extends TransactionFormController implements Init byte[] txBytes = transaction.bitcoinSerialize(); UR ur = UR.fromBytes(txBytes); BBQR bbqr = new BBQR(BBQRType.TXN, txBytes); - QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, false); + String raw = Utils.bytesToHex(txBytes); + QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(ur, bbqr, raw, false, false, QREncoding.UR); qrDisplayDialog.initOwner(showTransactionButton.getScene().getWindow()); qrDisplayDialog.showAndWait(); } catch (Exception exception) { diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java index adc9713b..158515ac 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java @@ -383,11 +383,11 @@ public class SettingsController extends WalletFormController implements Initiali } boolean addBbqrOption = walletForm.getWallet().getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().showBbqr()); - boolean selectBbqrOption = walletForm.getWallet().getKeystores().stream().allMatch(keystore -> keystore.getWalletModel().selectBbqr()); + QREncoding encoding = walletForm.getWallet().getKeystores().stream().allMatch(keystore -> keystore.getWalletModel().selectBbqr()) ? QREncoding.BBQR : QREncoding.UR; UR cryptoOutputUR = cryptoOutput.toUR(); BBQR bbqr = addBbqrOption ? new BBQR(BBQRType.UNICODE, outputDescriptor.toString(true).getBytes(StandardCharsets.UTF_8)) : null; - QRDisplayDialog qrDisplayDialog = new DescriptorQRDisplayDialog(walletForm.getWallet().getFullDisplayName(), outputDescriptor.toString(true), cryptoOutputUR, bbqr, selectBbqrOption); + QRDisplayDialog qrDisplayDialog = new DescriptorQRDisplayDialog(walletForm.getWallet().getFullDisplayName(), outputDescriptor.toString(true), cryptoOutputUR, bbqr, encoding); qrDisplayDialog.initOwner(showDescriptorQR.getScene().getWindow()); qrDisplayDialog.showAndWait(); } diff --git a/src/main/resources/image/qrencoding/bbqr-icon-invert.svg b/src/main/resources/image/qrencoding/bbqr-icon-invert.svg new file mode 100644 index 00000000..6eb6df3a --- /dev/null +++ b/src/main/resources/image/qrencoding/bbqr-icon-invert.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/main/resources/image/qrencoding/bbqr-icon.svg b/src/main/resources/image/qrencoding/bbqr-icon.svg new file mode 100644 index 00000000..cfd63741 --- /dev/null +++ b/src/main/resources/image/qrencoding/bbqr-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/main/resources/image/qrencoding/ur-icon-invert.svg b/src/main/resources/image/qrencoding/ur-icon-invert.svg new file mode 100644 index 00000000..89148e76 --- /dev/null +++ b/src/main/resources/image/qrencoding/ur-icon-invert.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/image/qrencoding/ur-icon.svg b/src/main/resources/image/qrencoding/ur-icon.svg new file mode 100644 index 00000000..2876b079 --- /dev/null +++ b/src/main/resources/image/qrencoding/ur-icon.svg @@ -0,0 +1,8 @@ + + + + + + + +