mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-25 05:06:45 +00:00
Merge branch 'sparrowwallet:master' into master
This commit is contained in:
commit
6cdbba4bb3
18 changed files with 730 additions and 68 deletions
|
@ -134,6 +134,7 @@ dependencies {
|
||||||
implementation('net.coobird:thumbnailator:0.4.18')
|
implementation('net.coobird:thumbnailator:0.4.18')
|
||||||
implementation('com.github.hervegirod:fxsvgimage:1.0b2')
|
implementation('com.github.hervegirod:fxsvgimage:1.0b2')
|
||||||
implementation('com.sparrowwallet:toucan:0.9.0')
|
implementation('com.sparrowwallet:toucan:0.9.0')
|
||||||
|
implementation('com.jcraft:jzlib:1.1.3')
|
||||||
testImplementation('org.junit.jupiter:junit-jupiter-api:5.10.0')
|
testImplementation('org.junit.jupiter:junit-jupiter-api:5.10.0')
|
||||||
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.10.0')
|
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.10.0')
|
||||||
testRuntimeOnly('org.junit.platform:junit-platform-launcher')
|
testRuntimeOnly('org.junit.platform:junit-platform-launcher')
|
||||||
|
@ -715,4 +716,7 @@ extraJavaModuleInfo {
|
||||||
requires('org.bouncycastle.pg')
|
requires('org.bouncycastle.pg')
|
||||||
requires('org.slf4j')
|
requires('org.slf4j')
|
||||||
}
|
}
|
||||||
|
module('jzlib-1.1.3.jar', 'com.jcraft.jzlib', '1.1.3') {
|
||||||
|
exports('com.jcraft.jzlib')
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -100,7 +100,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
releaseLink.setOnAction(event -> {
|
releaseLink.setOnAction(event -> {
|
||||||
if(release.get() != null && release.get().exists()) {
|
if(release.get() != null && release.get().exists()) {
|
||||||
if(release.get().getName().toLowerCase(Locale.ROOT).startsWith("sparrow")) {
|
if(release.get().getName().toLowerCase(Locale.ROOT).startsWith("sparrow")) {
|
||||||
Optional<ButtonType> optType = AppServices.showAlertDialog("Close Sparrow?", "Close Sparrow before installing?", Alert.AlertType.CONFIRMATION, ButtonType.NO, ButtonType.YES);
|
Optional<ButtonType> optType = AppServices.showAlertDialog("Exit Sparrow?", "Sparrow must be closed before installation. Exit?", Alert.AlertType.CONFIRMATION, ButtonType.NO, ButtonType.YES);
|
||||||
if(optType.isPresent() && optType.get() == ButtonType.YES) {
|
if(optType.isPresent() && optType.get() == ButtonType.YES) {
|
||||||
javafx.application.Platform.exit();
|
javafx.application.Platform.exit();
|
||||||
AppServices.get().getApplication().getHostServices().showDocument("file://" + release.get().getAbsolutePath());
|
AppServices.get().getApplication().getHostServices().showDocument("file://" + release.get().getAbsolutePath());
|
||||||
|
|
|
@ -1,22 +1,28 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
public enum QRDensity {
|
public enum QRDensity {
|
||||||
NORMAL("Normal", 400),
|
NORMAL("Normal", 400, 2000),
|
||||||
LOW("Low", 80);
|
LOW("Low", 80, 1000);
|
||||||
|
|
||||||
private final String name;
|
private final String name;
|
||||||
private final int maxFragmentLength;
|
private final int maxUrFragmentLength;
|
||||||
|
private final int maxBbqrFragmentLength;
|
||||||
|
|
||||||
QRDensity(String name, int maxFragmentLength) {
|
QRDensity(String name, int maxUrFragmentLength, int maxBbqrFragmentLength) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.maxFragmentLength = maxFragmentLength;
|
this.maxUrFragmentLength = maxUrFragmentLength;
|
||||||
|
this.maxBbqrFragmentLength = maxBbqrFragmentLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getMaxFragmentLength() {
|
public int getMaxUrFragmentLength() {
|
||||||
return maxFragmentLength;
|
return maxUrFragmentLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxBbqrFragmentLength() {
|
||||||
|
return maxBbqrFragmentLength;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,9 @@ import com.sparrowwallet.sparrow.io.Config;
|
||||||
import com.sparrowwallet.sparrow.io.ImportException;
|
import com.sparrowwallet.sparrow.io.ImportException;
|
||||||
import com.sparrowwallet.hummingbird.UR;
|
import com.sparrowwallet.hummingbird.UR;
|
||||||
import com.sparrowwallet.hummingbird.UREncoder;
|
import com.sparrowwallet.hummingbird.UREncoder;
|
||||||
|
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
|
||||||
|
import com.sparrowwallet.sparrow.io.bbqr.BBQREncoder;
|
||||||
|
import com.sparrowwallet.sparrow.io.bbqr.BBQREncoding;
|
||||||
import javafx.concurrent.ScheduledService;
|
import javafx.concurrent.ScheduledService;
|
||||||
import javafx.concurrent.Task;
|
import javafx.concurrent.Task;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
|
@ -44,10 +47,16 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||||
private static final int DEFAULT_QR_SIZE = 580;
|
private static final int DEFAULT_QR_SIZE = 580;
|
||||||
private static final int REDUCED_QR_SIZE = 520;
|
private static final int REDUCED_QR_SIZE = 520;
|
||||||
|
|
||||||
|
private static final BBQREncoding DEFAULT_BBQR_ENCODING = BBQREncoding.ZLIB;
|
||||||
|
|
||||||
private final int qrSize = getQRSize();
|
private final int qrSize = getQRSize();
|
||||||
|
|
||||||
private final UR ur;
|
private final UR ur;
|
||||||
private UREncoder encoder;
|
private UREncoder urEncoder;
|
||||||
|
|
||||||
|
private final BBQR bbqr;
|
||||||
|
private BBQREncoder bbqrEncoder;
|
||||||
|
private boolean useBbqrEncoding;
|
||||||
|
|
||||||
private final ImageView qrImageView;
|
private final ImageView qrImageView;
|
||||||
|
|
||||||
|
@ -62,17 +71,26 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||||
private static boolean initialDensityChange;
|
private static boolean initialDensityChange;
|
||||||
|
|
||||||
public QRDisplayDialog(String type, byte[] data, boolean addLegacyEncodingOption) throws UR.URException {
|
public QRDisplayDialog(String type, byte[] data, boolean addLegacyEncodingOption) throws UR.URException {
|
||||||
this(UR.fromBytes(type, data), addLegacyEncodingOption, false);
|
this(UR.fromBytes(type, data), null, addLegacyEncodingOption, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public QRDisplayDialog(UR ur) {
|
public QRDisplayDialog(UR ur) {
|
||||||
this(ur, false, false);
|
this(ur, null, false, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public QRDisplayDialog(UR ur, boolean addLegacyEncodingOption, boolean addScanButton) {
|
public QRDisplayDialog(UR ur, BBQR bbqr, boolean addLegacyEncodingOption, boolean addScanButton, boolean selectBbqrButton) {
|
||||||
this.ur = ur;
|
this.ur = ur;
|
||||||
this.addLegacyEncodingOption = addLegacyEncodingOption;
|
this.bbqr = bbqr;
|
||||||
this.encoder = new UREncoder(ur, Config.get().getQrDensity().getMaxFragmentLength(), MIN_FRAGMENT_LENGTH, 0);
|
this.addLegacyEncodingOption = bbqr == null && addLegacyEncodingOption;
|
||||||
|
|
||||||
|
this.urEncoder = new UREncoder(ur, Config.get().getQrDensity().getMaxUrFragmentLength(), MIN_FRAGMENT_LENGTH, 0);
|
||||||
|
|
||||||
|
if(bbqr != null) {
|
||||||
|
this.bbqrEncoder = new BBQREncoder(bbqr.type(), DEFAULT_BBQR_ENCODING, bbqr.data(), Config.get().getQrDensity().getMaxBbqrFragmentLength(), 0);
|
||||||
|
if(selectBbqrButton) {
|
||||||
|
useBbqrEncoding = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final DialogPane dialogPane = new QRDisplayDialogPane();
|
final DialogPane dialogPane = new QRDisplayDialogPane();
|
||||||
setDialogPane(dialogPane);
|
setDialogPane(dialogPane);
|
||||||
|
@ -85,7 +103,7 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||||
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
|
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
|
||||||
|
|
||||||
nextPart();
|
nextPart();
|
||||||
if(encoder.isSinglePart()) {
|
if(isSinglePart()) {
|
||||||
qrImageView.setImage(getQrCode(currentPart));
|
qrImageView.setImage(getQrCode(currentPart));
|
||||||
} else {
|
} else {
|
||||||
createAnimateQRService();
|
createAnimateQRService();
|
||||||
|
@ -94,7 +112,7 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.CANCEL_CLOSE);
|
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||||
dialogPane.getButtonTypes().add(cancelButtonType);
|
dialogPane.getButtonTypes().add(cancelButtonType);
|
||||||
|
|
||||||
if(addLegacyEncodingOption) {
|
if(this.addLegacyEncodingOption) {
|
||||||
final ButtonType legacyEncodingButtonType = new javafx.scene.control.ButtonType("Use Legacy Encoding (Cobo Vault)", ButtonBar.ButtonData.LEFT);
|
final ButtonType legacyEncodingButtonType = new javafx.scene.control.ButtonType("Use Legacy Encoding (Cobo Vault)", ButtonBar.ButtonData.LEFT);
|
||||||
dialogPane.getButtonTypes().add(legacyEncodingButtonType);
|
dialogPane.getButtonTypes().add(legacyEncodingButtonType);
|
||||||
} else {
|
} else {
|
||||||
|
@ -102,8 +120,13 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||||
dialogPane.getButtonTypes().add(densityButtonType);
|
dialogPane.getButtonTypes().add(densityButtonType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(bbqr != null) {
|
||||||
|
final ButtonType bbqrButtonType = new javafx.scene.control.ButtonType("Show BBQr", ButtonBar.ButtonData.BACK_PREVIOUS);
|
||||||
|
dialogPane.getButtonTypes().add(bbqrButtonType);
|
||||||
|
}
|
||||||
|
|
||||||
if(addScanButton) {
|
if(addScanButton) {
|
||||||
final ButtonType scanButtonType = new javafx.scene.control.ButtonType("Scan QR", ButtonBar.ButtonData.NEXT_FORWARD);
|
final ButtonType scanButtonType = new javafx.scene.control.ButtonType("Scan QR", ButtonBar.ButtonData.OK_DONE);
|
||||||
dialogPane.getButtonTypes().add(scanButtonType);
|
dialogPane.getButtonTypes().add(scanButtonType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,7 +144,9 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||||
|
|
||||||
public QRDisplayDialog(String data, boolean addScanButton) {
|
public QRDisplayDialog(String data, boolean addScanButton) {
|
||||||
this.ur = null;
|
this.ur = null;
|
||||||
this.encoder = null;
|
this.bbqr = null;
|
||||||
|
this.urEncoder = null;
|
||||||
|
this.bbqrEncoder = null;
|
||||||
|
|
||||||
final DialogPane dialogPane = new QRDisplayDialogPane();
|
final DialogPane dialogPane = new QRDisplayDialogPane();
|
||||||
setDialogPane(dialogPane);
|
setDialogPane(dialogPane);
|
||||||
|
@ -138,7 +163,7 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||||
dialogPane.getButtonTypes().addAll(cancelButtonType);
|
dialogPane.getButtonTypes().addAll(cancelButtonType);
|
||||||
|
|
||||||
if(addScanButton) {
|
if(addScanButton) {
|
||||||
final ButtonType scanButtonType = new javafx.scene.control.ButtonType("Scan QR", ButtonBar.ButtonData.NEXT_FORWARD);
|
final ButtonType scanButtonType = new javafx.scene.control.ButtonType("Scan QR", ButtonBar.ButtonData.OK_DONE);
|
||||||
dialogPane.getButtonTypes().add(scanButtonType);
|
dialogPane.getButtonTypes().add(scanButtonType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,9 +188,22 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isSinglePart() {
|
||||||
|
if(useBbqrEncoding) {
|
||||||
|
return bbqrEncoder.isSinglePart();
|
||||||
|
} else if(!useLegacyEncoding) {
|
||||||
|
return urEncoder.isSinglePart();
|
||||||
|
} else {
|
||||||
|
return legacyParts.length == 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void nextPart() {
|
private void nextPart() {
|
||||||
if(!useLegacyEncoding) {
|
if(useBbqrEncoding) {
|
||||||
String fragment = encoder.nextPart();
|
String fragment = bbqrEncoder.nextPart();
|
||||||
|
currentPart = fragment.toUpperCase(Locale.ROOT);
|
||||||
|
} else if(!useLegacyEncoding) {
|
||||||
|
String fragment = urEncoder.nextPart();
|
||||||
currentPart = fragment.toUpperCase(Locale.ROOT);
|
currentPart = fragment.toUpperCase(Locale.ROOT);
|
||||||
} else {
|
} else {
|
||||||
currentPart = legacyParts[legacyPartIndex];
|
currentPart = legacyParts[legacyPartIndex];
|
||||||
|
@ -201,37 +239,23 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||||
this.legacyParts = legacyEncoder.encode();
|
this.legacyParts = legacyEncoder.encode();
|
||||||
this.useLegacyEncoding = true;
|
this.useLegacyEncoding = true;
|
||||||
|
|
||||||
if(legacyParts.length == 1) {
|
restartAnimation();
|
||||||
if(animateQRService != null) {
|
|
||||||
animateQRService.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
nextPart();
|
|
||||||
qrImageView.setImage(getQrCode(currentPart));
|
|
||||||
} else if(animateQRService == null) {
|
|
||||||
createAnimateQRService();
|
|
||||||
} else if(!animateQRService.isRunning()) {
|
|
||||||
animateQRService.reset();
|
|
||||||
animateQRService.start();
|
|
||||||
}
|
|
||||||
} catch(UR.InvalidTypeException e) {
|
} catch(UR.InvalidTypeException e) {
|
||||||
//Can't happen
|
//Can't happen
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.useLegacyEncoding = false;
|
this.useLegacyEncoding = false;
|
||||||
|
restartAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(encoder.isSinglePart()) {
|
private void setUseBbqrEncoding(boolean useBbqrEncoding) {
|
||||||
if(animateQRService != null) {
|
if(useBbqrEncoding) {
|
||||||
animateQRService.cancel();
|
this.useBbqrEncoding = true;
|
||||||
}
|
restartAnimation();
|
||||||
|
} else {
|
||||||
qrImageView.setImage(getQrCode(currentPart));
|
this.useBbqrEncoding = false;
|
||||||
} else if(animateQRService == null) {
|
restartAnimation();
|
||||||
createAnimateQRService();
|
|
||||||
} else if(!animateQRService.isRunning()) {
|
|
||||||
animateQRService.reset();
|
|
||||||
animateQRService.start();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,12 +264,28 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||||
animateQRService.cancel();
|
animateQRService.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.encoder = new UREncoder(ur, Config.get().getQrDensity().getMaxFragmentLength(), MIN_FRAGMENT_LENGTH, 0);
|
if(bbqr != null) {
|
||||||
nextPart();
|
this.bbqrEncoder = new BBQREncoder(bbqr.type(), DEFAULT_BBQR_ENCODING, bbqr.data(), Config.get().getQrDensity().getMaxBbqrFragmentLength(), 0);
|
||||||
if(encoder.isSinglePart()) {
|
}
|
||||||
|
|
||||||
|
this.urEncoder = new UREncoder(ur, Config.get().getQrDensity().getMaxUrFragmentLength(), MIN_FRAGMENT_LENGTH, 0);
|
||||||
|
|
||||||
|
restartAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void restartAnimation() {
|
||||||
|
if(isSinglePart()) {
|
||||||
|
if(animateQRService != null) {
|
||||||
|
animateQRService.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPart();
|
||||||
qrImageView.setImage(getQrCode(currentPart));
|
qrImageView.setImage(getQrCode(currentPart));
|
||||||
} else {
|
} else if(animateQRService == null) {
|
||||||
createAnimateQRService();
|
createAnimateQRService();
|
||||||
|
} else if(!animateQRService.isRunning()) {
|
||||||
|
animateQRService.reset();
|
||||||
|
animateQRService.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -290,7 +330,7 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||||
ButtonBar.setButtonData(density, buttonData);
|
ButtonBar.setButtonData(density, buttonData);
|
||||||
density.setOnAction(event -> {
|
density.setOnAction(event -> {
|
||||||
if(!initialDensityChange && !encoder.isSinglePart()) {
|
if(!initialDensityChange && !isSinglePart()) {
|
||||||
Optional<ButtonType> optButtonType = AppServices.showWarningDialog("Discard progress?", "Changing the QR code density means any progress on the receiving device must be discarded. Proceed?", ButtonType.NO, ButtonType.YES);
|
Optional<ButtonType> optButtonType = AppServices.showWarningDialog("Discard progress?", "Changing the QR code density means any progress on the receiving device must be discarded. Proceed?", ButtonType.NO, ButtonType.YES);
|
||||||
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
|
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
|
||||||
initialDensityChange = true;
|
initialDensityChange = true;
|
||||||
|
@ -306,12 +346,25 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||||
|
|
||||||
return density;
|
return density;
|
||||||
}
|
}
|
||||||
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.NEXT_FORWARD) {
|
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.OK_DONE) {
|
||||||
Button scanButton = (Button)super.createButton(buttonType);
|
Button scanButton = (Button)super.createButton(buttonType);
|
||||||
scanButton.setGraphicTextGap(5);
|
scanButton.setGraphicTextGap(5);
|
||||||
scanButton.setGraphic(getGlyph(FontAwesome5.Glyph.CAMERA));
|
scanButton.setGraphic(getGlyph(FontAwesome5.Glyph.CAMERA));
|
||||||
|
|
||||||
return scanButton;
|
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);
|
||||||
|
|
||||||
|
bbqr.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
setUseBbqrEncoding(newValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
return bbqr;
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.createButton(buttonType);
|
return super.createButton(buttonType);
|
||||||
|
|
|
@ -30,6 +30,8 @@ import com.sparrowwallet.hummingbird.registry.pathcomponent.PathComponent;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
import com.sparrowwallet.sparrow.io.bbqr.BBQRDecoder;
|
||||||
|
import com.sparrowwallet.sparrow.io.bbqr.BBQRException;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.DoubleProperty;
|
import javafx.beans.property.DoubleProperty;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
@ -63,8 +65,9 @@ import java.util.stream.IntStream;
|
||||||
public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
private static final Logger log = LoggerFactory.getLogger(QRScanDialog.class);
|
private static final Logger log = LoggerFactory.getLogger(QRScanDialog.class);
|
||||||
|
|
||||||
private final URDecoder decoder;
|
private final URDecoder urDecoder;
|
||||||
private final LegacyURDecoder legacyDecoder;
|
private final LegacyURDecoder legacyUrDecoder;
|
||||||
|
private final BBQRDecoder bbqrDecoder;
|
||||||
private final WebcamService webcamService;
|
private final WebcamService webcamService;
|
||||||
private List<String> parts;
|
private List<String> parts;
|
||||||
|
|
||||||
|
@ -80,8 +83,9 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
private final ObjectProperty<WebcamDevice> webcamDeviceProperty = new SimpleObjectProperty<>();
|
private final ObjectProperty<WebcamDevice> webcamDeviceProperty = new SimpleObjectProperty<>();
|
||||||
|
|
||||||
public QRScanDialog() {
|
public QRScanDialog() {
|
||||||
this.decoder = new URDecoder();
|
this.urDecoder = new URDecoder();
|
||||||
this.legacyDecoder = new LegacyURDecoder();
|
this.legacyUrDecoder = new LegacyURDecoder();
|
||||||
|
this.bbqrDecoder = new BBQRDecoder();
|
||||||
|
|
||||||
if(Config.get().isHdCapture()) {
|
if(Config.get().isHdCapture()) {
|
||||||
webcamResolutionProperty.set(WebcamResolution.HD);
|
webcamResolutionProperty.set(WebcamResolution.HD);
|
||||||
|
@ -192,23 +196,23 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
|
|
||||||
if(qrtext.toLowerCase(Locale.ROOT).startsWith(UR.UR_PREFIX)) {
|
if(qrtext.toLowerCase(Locale.ROOT).startsWith(UR.UR_PREFIX)) {
|
||||||
if(LegacyURDecoder.isLegacyURFragment(qrtext)) {
|
if(LegacyURDecoder.isLegacyURFragment(qrtext)) {
|
||||||
legacyDecoder.receivePart(qrtext);
|
legacyUrDecoder.receivePart(qrtext);
|
||||||
Platform.runLater(() -> percentComplete.setValue(legacyDecoder.getPercentComplete()));
|
Platform.runLater(() -> percentComplete.setValue(legacyUrDecoder.getPercentComplete()));
|
||||||
|
|
||||||
if(legacyDecoder.isComplete()) {
|
if(legacyUrDecoder.isComplete()) {
|
||||||
try {
|
try {
|
||||||
UR ur = legacyDecoder.decode();
|
UR ur = legacyUrDecoder.decode();
|
||||||
result = extractResultFromUR(ur);
|
result = extractResultFromUR(ur);
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
result = new Result(new URException(e.getMessage()));
|
result = new Result(new URException(e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
decoder.receivePart(qrtext);
|
urDecoder.receivePart(qrtext);
|
||||||
Platform.runLater(() -> percentComplete.setValue(decoder.getProcessedPartsCount() > 0 ? decoder.getEstimatedPercentComplete() : 0));
|
Platform.runLater(() -> percentComplete.setValue(urDecoder.getProcessedPartsCount() > 0 ? urDecoder.getEstimatedPercentComplete() : 0));
|
||||||
|
|
||||||
if(decoder.getResult() != null) {
|
if(urDecoder.getResult() != null) {
|
||||||
URDecoder.Result urResult = decoder.getResult();
|
URDecoder.Result urResult = urDecoder.getResult();
|
||||||
if(urResult.type == ResultType.SUCCESS) {
|
if(urResult.type == ResultType.SUCCESS) {
|
||||||
result = extractResultFromUR(urResult.ur);
|
result = extractResultFromUR(urResult.ur);
|
||||||
Platform.runLater(() -> setResult(result));
|
Platform.runLater(() -> setResult(result));
|
||||||
|
@ -217,6 +221,19 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if(BBQRDecoder.isBBQRFragment(qrtext)) {
|
||||||
|
bbqrDecoder.receivePart(qrtext);
|
||||||
|
Platform.runLater(() -> percentComplete.setValue(bbqrDecoder.getPercentComplete()));
|
||||||
|
|
||||||
|
if(bbqrDecoder.getResult() != null) {
|
||||||
|
BBQRDecoder.Result bbqrResult = bbqrDecoder.getResult();
|
||||||
|
if(bbqrResult.getResultType() == BBQRDecoder.ResultType.SUCCESS) {
|
||||||
|
result = extractResultFromBBQR(bbqrResult);
|
||||||
|
Platform.runLater(() -> setResult(result));
|
||||||
|
} else {
|
||||||
|
result = new Result(new BBQRException(bbqrResult.getError()));
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if(partMatcher.matches()) {
|
} else if(partMatcher.matches()) {
|
||||||
int m = Integer.parseInt(partMatcher.group(1));
|
int m = Integer.parseInt(partMatcher.group(1));
|
||||||
int n = Integer.parseInt(partMatcher.group(2));
|
int n = Integer.parseInt(partMatcher.group(2));
|
||||||
|
@ -651,6 +668,19 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
|
|
||||||
return wallets;
|
return wallets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Result extractResultFromBBQR(BBQRDecoder.Result result) {
|
||||||
|
if(result.getPsbt() != null) {
|
||||||
|
return new Result(result.getPsbt());
|
||||||
|
} else if(result.getTransaction() != null) {
|
||||||
|
return new Result(result.getTransaction());
|
||||||
|
} else if(result.toString() != null) {
|
||||||
|
return new Result(result.toString());
|
||||||
|
} else {
|
||||||
|
log.error("Unsupported BBQR type " + result.getBbqrType());
|
||||||
|
return new Result(new URException("BBQR type " + result.getBbqrType() + " is not supported"));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class QRScanListener implements WebcamListener {
|
private class QRScanListener implements WebcamListener {
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
package com.sparrowwallet.sparrow.io.bbqr;
|
||||||
|
|
||||||
|
public record BBQR(BBQRType type, byte[] data) {}
|
180
src/main/java/com/sparrowwallet/sparrow/io/bbqr/BBQRDecoder.java
Normal file
180
src/main/java/com/sparrowwallet/sparrow/io/bbqr/BBQRDecoder.java
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
package com.sparrowwallet.sparrow.io.bbqr;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
|
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
public class BBQRDecoder {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(BBQRDecoder.class);
|
||||||
|
|
||||||
|
private final Map<Integer, byte[]> receivedParts = new TreeMap<>();
|
||||||
|
private int totalParts;
|
||||||
|
|
||||||
|
private BBQRType type;
|
||||||
|
private Result result;
|
||||||
|
|
||||||
|
public static boolean isBBQRFragment(String part) {
|
||||||
|
try {
|
||||||
|
BBQRHeader.fromString(part);
|
||||||
|
return true;
|
||||||
|
} catch(Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void receivePart(String part) {
|
||||||
|
try {
|
||||||
|
BBQRHeader header = BBQRHeader.fromString(part);
|
||||||
|
totalParts = header.seqTotal();
|
||||||
|
type = header.type();
|
||||||
|
byte[] partData = header.decode(part);
|
||||||
|
receivedParts.put(header.seqNumber(), partData);
|
||||||
|
|
||||||
|
if(receivedParts.size() == totalParts) {
|
||||||
|
byte[] data = concatParts();
|
||||||
|
|
||||||
|
if(type == BBQRType.PSBT) {
|
||||||
|
result = new Result(new PSBT(data));
|
||||||
|
} else if(type == BBQRType.TXN) {
|
||||||
|
result = new Result(new Transaction(data));
|
||||||
|
} else if(type == BBQRType.JSON || type == BBQRType.UNICODE) {
|
||||||
|
result = new Result(type, new String(data, StandardCharsets.UTF_8));
|
||||||
|
} else {
|
||||||
|
result = new Result(type, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error("Could not parse received QR of type " + type, e);
|
||||||
|
result = new Result(ResultType.FAILURE, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] concatParts() {
|
||||||
|
int totalLength = 0;
|
||||||
|
for(byte[] part : receivedParts.values()) {
|
||||||
|
totalLength += part.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] data = new byte[totalLength];
|
||||||
|
int index = 0;
|
||||||
|
for(byte[] part : receivedParts.values()) {
|
||||||
|
System.arraycopy(part, 0, data, index, part.length);
|
||||||
|
index += part.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getProcessedPartsCount() {
|
||||||
|
return receivedParts.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getPercentComplete() {
|
||||||
|
if(totalParts == 0) {
|
||||||
|
return 0d;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (double)getProcessedPartsCount() / totalParts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result getResult() {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Result {
|
||||||
|
private final ResultType resultType;
|
||||||
|
private final BBQRType bbqrType;
|
||||||
|
private final PSBT psbt;
|
||||||
|
private final Transaction transaction;
|
||||||
|
private final String strData;
|
||||||
|
private final byte[] data;
|
||||||
|
private final String error;
|
||||||
|
|
||||||
|
public Result(ResultType resultType, String error) {
|
||||||
|
this.resultType = resultType;
|
||||||
|
this.bbqrType = null;
|
||||||
|
this.psbt = null;
|
||||||
|
this.transaction = null;
|
||||||
|
this.strData = null;
|
||||||
|
this.data = null;
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result(PSBT psbt) {
|
||||||
|
this.resultType = ResultType.SUCCESS;
|
||||||
|
this.bbqrType = BBQRType.PSBT;
|
||||||
|
this.psbt = psbt;
|
||||||
|
this.transaction = null;
|
||||||
|
this.strData = null;
|
||||||
|
this.data = null;
|
||||||
|
this.error = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result(Transaction transaction) {
|
||||||
|
this.resultType = ResultType.SUCCESS;
|
||||||
|
this.bbqrType = BBQRType.TXN;
|
||||||
|
this.psbt = null;
|
||||||
|
this.transaction = transaction;
|
||||||
|
this.strData = null;
|
||||||
|
this.data = null;
|
||||||
|
this.error = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result(BBQRType bbqrType, String strData) {
|
||||||
|
this.resultType = ResultType.SUCCESS;
|
||||||
|
this.bbqrType = bbqrType;
|
||||||
|
this.psbt = null;
|
||||||
|
this.transaction = null;
|
||||||
|
this.strData = strData;
|
||||||
|
this.data = null;
|
||||||
|
this.error = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result(BBQRType bbqrType, byte[] data) {
|
||||||
|
this.resultType = ResultType.SUCCESS;
|
||||||
|
this.bbqrType = bbqrType;
|
||||||
|
this.psbt = null;
|
||||||
|
this.transaction = null;
|
||||||
|
this.strData = null;
|
||||||
|
this.data = data;
|
||||||
|
this.error = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResultType getResultType() {
|
||||||
|
return resultType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BBQRType getBbqrType() {
|
||||||
|
return bbqrType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PSBT getPsbt() {
|
||||||
|
return psbt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Transaction getTransaction() {
|
||||||
|
return transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getData() {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
return strData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getError() {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ResultType {
|
||||||
|
SUCCESS, FAILURE;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
package com.sparrowwallet.sparrow.io.bbqr;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class BBQREncoder {
|
||||||
|
private final String[] parts;
|
||||||
|
private int partIndex;
|
||||||
|
|
||||||
|
public BBQREncoder(BBQRType bbqrType, BBQREncoding bbqrEncoding, byte[] data, int maxFragmentLength, int firstSeqNum) {
|
||||||
|
this.parts = encode(bbqrType, bbqrEncoding, data, maxFragmentLength).toArray(new String[0]);
|
||||||
|
this.partIndex = firstSeqNum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSinglePart() {
|
||||||
|
return parts.length == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String nextPart() {
|
||||||
|
String currentPart = parts[partIndex];
|
||||||
|
partIndex++;
|
||||||
|
if(partIndex > parts.length - 1) {
|
||||||
|
partIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentPart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getNumParts() {
|
||||||
|
return parts.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> encode(BBQRType type, BBQREncoding desiredEncoding, byte[] data, int desiredChunkSize) {
|
||||||
|
String encoded;
|
||||||
|
BBQREncoding encoding = desiredEncoding;
|
||||||
|
|
||||||
|
try {
|
||||||
|
encoded = encoding.encode(data);
|
||||||
|
if(encoding == BBQREncoding.ZLIB) {
|
||||||
|
String uncompressed = BBQREncoding.BASE32.encode(data);
|
||||||
|
if(encoded.length() > uncompressed.length()) {
|
||||||
|
throw new BBQREncodingException("Compressed data was larger than uncompressed data");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(BBQREncodingException e) {
|
||||||
|
encoding = BBQREncoding.BASE32;
|
||||||
|
encoded = BBQREncoding.BASE32.encode(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
int inputLength = encoded.length();
|
||||||
|
int numChunks = (inputLength + desiredChunkSize - 1) / desiredChunkSize;
|
||||||
|
int chunkSize = numChunks == 1 ? desiredChunkSize : (int)Math.ceil((double)inputLength / numChunks);
|
||||||
|
|
||||||
|
int modulo = chunkSize % encoding.getPartModulo();
|
||||||
|
if(modulo > 0) {
|
||||||
|
chunkSize += (encoding.getPartModulo() - modulo);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> chunks = new ArrayList<>();
|
||||||
|
int startIndex = 0;
|
||||||
|
for(int i = 0; i < numChunks; i ++) {
|
||||||
|
int endIndex = Math.min(startIndex + chunkSize, encoded.length());
|
||||||
|
BBQRHeader bbqrHeader = new BBQRHeader(encoding, type, numChunks, i);
|
||||||
|
String chunk = bbqrHeader + encoded.substring(startIndex, endIndex);
|
||||||
|
startIndex = endIndex;
|
||||||
|
chunks.add(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
package com.sparrowwallet.sparrow.io.bbqr;
|
||||||
|
|
||||||
|
import com.google.common.io.BaseEncoding;
|
||||||
|
import com.jcraft.jzlib.*;
|
||||||
|
import com.sparrowwallet.drongo.Utils;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public enum BBQREncoding {
|
||||||
|
HEX("H") {
|
||||||
|
@Override
|
||||||
|
public String encode(byte[] data) throws BBQREncodingException {
|
||||||
|
return Utils.bytesToHex(data).toUpperCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] decode(String part) throws BBQREncodingException {
|
||||||
|
return Utils.hexToBytes(part);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPartModulo() {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}, BASE32("2") {
|
||||||
|
@Override
|
||||||
|
public String encode(byte[] data) throws BBQREncodingException {
|
||||||
|
return BaseEncoding.base32().encode(data).replaceAll("=+$", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] decode(String part) throws BBQREncodingException {
|
||||||
|
return BaseEncoding.base32().decode(part);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPartModulo() {
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
}, ZLIB("Z") {
|
||||||
|
@Override
|
||||||
|
public String encode(byte[] data) throws BBQREncodingException {
|
||||||
|
try {
|
||||||
|
Deflater deflater = new Deflater(JZlib.Z_BEST_COMPRESSION, 10, true);
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
DeflaterOutputStream zOut = new DeflaterOutputStream(out, deflater);
|
||||||
|
zOut.write(data);
|
||||||
|
zOut.close();
|
||||||
|
|
||||||
|
return BASE32.encode(out.toByteArray());
|
||||||
|
} catch(Exception e) {
|
||||||
|
throw new BBQREncodingException("Error deflating with zlib", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] decode(String part) throws BBQREncodingException {
|
||||||
|
try {
|
||||||
|
Inflater inflater = new Inflater(10, true);
|
||||||
|
ByteArrayInputStream in = new ByteArrayInputStream(BASE32.decode(part));
|
||||||
|
InflaterInputStream zIn = new InflaterInputStream(in, inflater);
|
||||||
|
byte[] decoded = zIn.readAllBytes();
|
||||||
|
zIn.close();
|
||||||
|
|
||||||
|
return decoded;
|
||||||
|
} catch(Exception e) {
|
||||||
|
throw new BBQREncodingException("Error inflating with zlib", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPartModulo() {
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static BBQREncoding fromString(String code) {
|
||||||
|
for(BBQREncoding encoding : values()) {
|
||||||
|
if(encoding.getCode().equals(code)) {
|
||||||
|
return encoding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("Could not find encoding for code " + code);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
BBQREncoding(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract String encode(byte[] data) throws BBQREncodingException;
|
||||||
|
|
||||||
|
public abstract byte[] decode(String part) throws BBQREncodingException;
|
||||||
|
|
||||||
|
public abstract int getPartModulo();
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.sparrowwallet.sparrow.io.bbqr;
|
||||||
|
|
||||||
|
public class BBQREncodingException extends BBQRException {
|
||||||
|
public BBQREncodingException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BBQREncodingException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.sparrowwallet.sparrow.io.bbqr;
|
||||||
|
|
||||||
|
public class BBQRException extends RuntimeException {
|
||||||
|
public BBQRException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BBQRException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package com.sparrowwallet.sparrow.io.bbqr;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
public record BBQRHeader(BBQREncoding encoding, BBQRType type, int seqTotal, int seqNumber) {
|
||||||
|
public static final String HEADER = "B$";
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
return HEADER + encoding.getCode() + type.getCode() + encodeToBase36(seqTotal) + encodeToBase36(seqNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] decode(String part) {
|
||||||
|
return encoding.decode(part.substring(8));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BBQRHeader fromString(String part) {
|
||||||
|
if(part.length() < 8) {
|
||||||
|
throw new IllegalArgumentException("Part too short");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!HEADER.equals(part.substring(0, 2))) {
|
||||||
|
throw new IllegalArgumentException("Part does not start with " + HEADER);
|
||||||
|
}
|
||||||
|
|
||||||
|
BBQREncoding e = BBQREncoding.fromString(part.substring(2, 3));
|
||||||
|
BBQRType t = BBQRType.fromString(part.substring(3, 4));
|
||||||
|
|
||||||
|
return new BBQRHeader(e, t, decodeFromBase36(part.substring(4, 6)), decodeFromBase36(part.substring(6, 8)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String encodeToBase36(int number) {
|
||||||
|
String base36Encoded = new BigInteger(String.valueOf(number)).toString(36);
|
||||||
|
return String.format("%2s", base36Encoded).replace(' ', '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int decodeFromBase36(String base36) {
|
||||||
|
BigInteger bigInteger = new BigInteger(base36, 36);
|
||||||
|
return bigInteger.intValue();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package com.sparrowwallet.sparrow.io.bbqr;
|
||||||
|
|
||||||
|
public enum BBQRType {
|
||||||
|
PSBT("P"), TXN("T"), JSON("J"), CBOR("C"), UNICODE("U"), BINARY("B"), EXECUTABLE("X");
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
BBQRType(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BBQRType fromString(String code) {
|
||||||
|
for(BBQRType type : values()) {
|
||||||
|
if(type.getCode().equals(code)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("Could not find type for code " + code);
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,6 +19,8 @@ import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
import com.sparrowwallet.sparrow.io.Device;
|
import com.sparrowwallet.sparrow.io.Device;
|
||||||
|
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
|
||||||
|
import com.sparrowwallet.sparrow.io.bbqr.BBQRType;
|
||||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||||
import com.sparrowwallet.sparrow.io.Storage;
|
import com.sparrowwallet.sparrow.io.Storage;
|
||||||
import com.sparrowwallet.sparrow.payjoin.Payjoin;
|
import com.sparrowwallet.sparrow.payjoin.Payjoin;
|
||||||
|
@ -902,16 +904,21 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
ToggleButton toggleButton = (ToggleButton)event.getSource();
|
ToggleButton toggleButton = (ToggleButton)event.getSource();
|
||||||
toggleButton.setSelected(false);
|
toggleButton.setSelected(false);
|
||||||
|
|
||||||
//TODO: Remove once Cobo Vault has upgraded to UR2.0
|
//TODO: Remove once Cobo Vault support has been removed
|
||||||
boolean addLegacyEncodingOption = headersForm.getSigningWallet().getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().equals(WalletModel.COBO_VAULT));
|
boolean addLegacyEncodingOption = headersForm.getSigningWallet().getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().equals(WalletModel.COBO_VAULT));
|
||||||
|
boolean addBbqrOption = headersForm.getSigningWallet().getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().equals(WalletModel.COLDCARD) || keystore.getSource().equals(KeystoreSource.SW_WATCH));
|
||||||
|
boolean selectBbqrOption = headersForm.getSigningWallet().getKeystores().stream().allMatch(keystore -> keystore.getWalletModel().equals(WalletModel.COLDCARD));
|
||||||
|
|
||||||
//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
|
//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());
|
boolean includeNonWitnessUtxos = !Arrays.asList(ScriptType.WITNESS_TYPES).contains(headersForm.getSigningWallet().getScriptType());
|
||||||
CryptoPSBT cryptoPSBT = new CryptoPSBT(headersForm.getPsbt().serialize(true, includeNonWitnessUtxos));
|
byte[] psbtBytes = headersForm.getPsbt().serialize(true, includeNonWitnessUtxos);
|
||||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), addLegacyEncodingOption, true);
|
|
||||||
|
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.initOwner(toggleButton.getScene().getWindow());
|
qrDisplayDialog.initOwner(toggleButton.getScene().getWindow());
|
||||||
Optional<ButtonType> optButtonType = qrDisplayDialog.showAndWait();
|
Optional<ButtonType> optButtonType = qrDisplayDialog.showAndWait();
|
||||||
if(optButtonType.isPresent() && optButtonType.get().getButtonData() == ButtonBar.ButtonData.NEXT_FORWARD) {
|
if(optButtonType.isPresent() && optButtonType.get().getButtonData() == ButtonBar.ButtonData.OK_DONE) {
|
||||||
scanPSBT(event);
|
scanPSBT(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,4 +63,5 @@ open module com.sparrowwallet.sparrow {
|
||||||
requires com.github.hervegirod;
|
requires com.github.hervegirod;
|
||||||
requires com.sparrowwallet.toucan;
|
requires com.sparrowwallet.toucan;
|
||||||
requires java.smartcardio;
|
requires java.smartcardio;
|
||||||
|
requires com.jcraft.jzlib;
|
||||||
}
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package com.sparrowwallet.sparrow.io.bbqr;
|
||||||
|
|
||||||
|
import com.sparrowwallet.sparrow.control.QRDensity;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class BBQRDecoderTest {
|
||||||
|
@Test
|
||||||
|
public void testDecoder() {
|
||||||
|
Random random = new Random();
|
||||||
|
|
||||||
|
for(QRDensity qrDensity : QRDensity.values()) {
|
||||||
|
for(BBQREncoding encoding : BBQREncoding.values()) {
|
||||||
|
for(int length = qrDensity.getMaxBbqrFragmentLength() / 2; length < qrDensity.getMaxBbqrFragmentLength() * 2.5d; length++) {
|
||||||
|
byte[] data = new byte[length];
|
||||||
|
random.nextBytes(data);
|
||||||
|
|
||||||
|
BBQREncoder encoder = new BBQREncoder(BBQRType.BINARY, encoding, data, qrDensity.getMaxBbqrFragmentLength() + 13, 0);
|
||||||
|
BBQRDecoder decoder = new BBQRDecoder();
|
||||||
|
|
||||||
|
while(decoder.getPercentComplete() < 1d) {
|
||||||
|
String part = encoder.nextPart();
|
||||||
|
Assertions.assertTrue(BBQRDecoder.isBBQRFragment(part));
|
||||||
|
if(random.nextDouble() < 0.7) {
|
||||||
|
decoder.receivePart(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assertions.assertNotNull(decoder.getResult(), "Result was null for encoding " + encoding + " " + qrDensity + " " + length);
|
||||||
|
Assertions.assertArrayEquals(data, decoder.getResult().getData());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package com.sparrowwallet.sparrow.io.bbqr;
|
||||||
|
|
||||||
|
import com.sparrowwallet.sparrow.control.QRDensity;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class BBQREncoderTest {
|
||||||
|
@Test
|
||||||
|
public void testEncoding() {
|
||||||
|
Random random = new Random();
|
||||||
|
for(QRDensity qrDensity : QRDensity.values()) {
|
||||||
|
for(BBQREncoding encoding : BBQREncoding.values()) {
|
||||||
|
for(int length = qrDensity.getMaxBbqrFragmentLength() / 2; length < qrDensity.getMaxBbqrFragmentLength() * 2.5d; length++) {
|
||||||
|
byte[] data = new byte[length];
|
||||||
|
random.nextBytes(data);
|
||||||
|
|
||||||
|
BBQREncoder encoder = new BBQREncoder(BBQRType.BINARY, encoding, data, qrDensity.getMaxBbqrFragmentLength(), 0);
|
||||||
|
int partLength = encoder.nextPart().length();
|
||||||
|
for(int i = 1; i < encoder.getNumParts(); i++) {
|
||||||
|
int nextPartLength = encoder.nextPart().length();
|
||||||
|
if(i < encoder.getNumParts() - 1) {
|
||||||
|
Assertions.assertEquals(0, nextPartLength % encoding.getPartModulo(), "Modulo test failed for " + length + " in encoding " + encoding + " on part " + (i+1) + " of " + encoder.getNumParts());
|
||||||
|
Assertions.assertEquals(partLength, nextPartLength);
|
||||||
|
} else {
|
||||||
|
Assertions.assertTrue(nextPartLength <= partLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package com.sparrowwallet.sparrow.io.bbqr;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class BBQREncodingTest {
|
||||||
|
@Test
|
||||||
|
public void testHex() {
|
||||||
|
byte[] data = new byte[1000];
|
||||||
|
Random random = new Random();
|
||||||
|
random.nextBytes(data);
|
||||||
|
|
||||||
|
String deflated = BBQREncoding.HEX.encode(data);
|
||||||
|
byte[] inflated = BBQREncoding.HEX.decode(deflated);
|
||||||
|
|
||||||
|
Assertions.assertArrayEquals(data, inflated);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBase32() {
|
||||||
|
byte[] data = new byte[1000];
|
||||||
|
Random random = new Random();
|
||||||
|
random.nextBytes(data);
|
||||||
|
|
||||||
|
String deflated = BBQREncoding.BASE32.encode(data);
|
||||||
|
byte[] inflated = BBQREncoding.BASE32.decode(deflated);
|
||||||
|
|
||||||
|
Assertions.assertArrayEquals(data, inflated);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testZlib() {
|
||||||
|
byte[] data = new byte[1000];
|
||||||
|
Random random = new Random();
|
||||||
|
random.nextBytes(data);
|
||||||
|
|
||||||
|
String deflated = BBQREncoding.ZLIB.encode(data);
|
||||||
|
byte[] inflated = BBQREncoding.ZLIB.decode(deflated);
|
||||||
|
|
||||||
|
Assertions.assertArrayEquals(data, inflated);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue