mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-11-02 12:26:45 +00:00
add qr scanning support
This commit is contained in:
parent
709c65ec20
commit
c9c00cc74e
14 changed files with 448 additions and 6 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
|
@ -12,6 +12,8 @@ sourceCompatibility = 1.9
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
maven { url 'https://oss.sonatype.org/content/groups/public' }
|
||||||
|
maven { url 'https://mymavenrepo.com/repo/29EACwkkGcoOKnbx3bxN/' }
|
||||||
}
|
}
|
||||||
|
|
||||||
javafx {
|
javafx {
|
||||||
|
@ -38,6 +40,10 @@ dependencies {
|
||||||
exclude group: 'org.slf4j'
|
exclude group: 'org.slf4j'
|
||||||
}
|
}
|
||||||
implementation('co.nstant.in:cbor:0.9')
|
implementation('co.nstant.in:cbor:0.9')
|
||||||
|
implementation('com.nativelibs4java:bridj:0.7-20200803')
|
||||||
|
implementation('com.github.sarxos:webcam-capture:0.3.13-SNAPSHOT') {
|
||||||
|
exclude group: 'com.nativelibs4java', module: 'bridj'
|
||||||
|
}
|
||||||
implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
|
implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
|
||||||
implementation('org.controlsfx:controlsfx:11.0.1' ) {
|
implementation('org.controlsfx:controlsfx:11.0.1' ) {
|
||||||
exclude group: 'org.openjfx', module: 'javafx-base'
|
exclude group: 'org.openjfx', module: 'javafx-base'
|
||||||
|
|
2
drongo
2
drongo
|
@ -1 +1 @@
|
||||||
Subproject commit 8d49cebcaca6ccb2ea699fe8141554d1470d0a97
|
Subproject commit 97cf49276a5c87425682a3fd0e48ffe081ff71bb
|
|
@ -409,6 +409,28 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void openTransactionFromQR(ActionEvent event) {
|
||||||
|
QRScanDialog qrScanDialog = new QRScanDialog();
|
||||||
|
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
|
||||||
|
if(optionalResult.isPresent()) {
|
||||||
|
QRScanDialog.Result result = optionalResult.get();
|
||||||
|
if(result.transaction != null) {
|
||||||
|
Tab tab = addTransactionTab(null, result.transaction);
|
||||||
|
tabs.getSelectionModel().select(tab);
|
||||||
|
}
|
||||||
|
if(result.psbt != null) {
|
||||||
|
Tab tab = addTransactionTab(null, result.psbt);
|
||||||
|
tabs.getSelectionModel().select(tab);
|
||||||
|
}
|
||||||
|
if(result.error != null) {
|
||||||
|
showErrorDialog("Invalid QR Code", result.error);
|
||||||
|
}
|
||||||
|
if(result.exception != null) {
|
||||||
|
showErrorDialog("Error opening webcam", result.exception.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void saveTransaction(ActionEvent event) {
|
public void saveTransaction(ActionEvent event) {
|
||||||
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
||||||
TabData tabData = (TabData)selectedTab.getUserData();
|
TabData tabData = (TabData)selectedTab.getUserData();
|
||||||
|
@ -784,7 +806,10 @@ public class AppController implements Initializable {
|
||||||
//As per BIP174, combine PSBTs with matching transactions so long as they are not yet finalized
|
//As per BIP174, combine PSBTs with matching transactions so long as they are not yet finalized
|
||||||
if(transactionTabData.getPsbt() != null && psbt != null && !transactionTabData.getPsbt().isFinalized() && !psbt.isFinalized()) {
|
if(transactionTabData.getPsbt() != null && psbt != null && !transactionTabData.getPsbt().isFinalized() && !psbt.isFinalized()) {
|
||||||
transactionTabData.getPsbt().combine(psbt);
|
transactionTabData.getPsbt().combine(psbt);
|
||||||
tab.setText(name);
|
if(name != null && !name.isEmpty()) {
|
||||||
|
tab.setText(name);
|
||||||
|
}
|
||||||
|
|
||||||
EventManager.get().post(new PSBTCombinedEvent(transactionTabData.getPsbt()));
|
EventManager.get().post(new PSBTCombinedEvent(transactionTabData.getPsbt()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1012,4 +1037,9 @@ public class AppController implements Initializable {
|
||||||
public void requestTransactionOpen(RequestTransactionOpenEvent event) {
|
public void requestTransactionOpen(RequestTransactionOpenEvent event) {
|
||||||
openTransactionFromFile(null);
|
openTransactionFromFile(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void requestQRScan(RequestQRScanEvent event) {
|
||||||
|
openTransactionFromQR(null);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,191 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.github.sarxos.webcam.WebcamResolution;
|
||||||
|
import com.sparrowwallet.drongo.Utils;
|
||||||
|
import com.sparrowwallet.drongo.protocol.Base43;
|
||||||
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
|
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||||
|
import com.sparrowwallet.sparrow.ur.ResultType;
|
||||||
|
import com.sparrowwallet.sparrow.ur.UR;
|
||||||
|
import com.sparrowwallet.sparrow.ur.URDecoder;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.value.ChangeListener;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import javafx.scene.control.ButtonBar;
|
||||||
|
import javafx.scene.control.ButtonType;
|
||||||
|
import javafx.scene.control.Dialog;
|
||||||
|
import javafx.scene.control.DialogPane;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
import org.controlsfx.tools.Borders;
|
||||||
|
|
||||||
|
public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
|
private final URDecoder decoder;
|
||||||
|
private final WebcamService webcamService;
|
||||||
|
|
||||||
|
private boolean isUr;
|
||||||
|
private QRScanDialog.Result result;
|
||||||
|
|
||||||
|
public QRScanDialog() {
|
||||||
|
this.decoder = new URDecoder();
|
||||||
|
|
||||||
|
this.webcamService = new WebcamService(WebcamResolution.VGA);
|
||||||
|
WebcamView webcamView = new WebcamView(webcamService);
|
||||||
|
|
||||||
|
final DialogPane dialogPane = getDialogPane();
|
||||||
|
|
||||||
|
StackPane stackPane = new StackPane();
|
||||||
|
stackPane.getChildren().add(webcamView.getView());
|
||||||
|
|
||||||
|
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().outerPadding(0).innerPadding(0).buildAll());
|
||||||
|
|
||||||
|
webcamService.resultProperty().addListener(new QRResultListener());
|
||||||
|
webcamService.setOnFailed(failedEvent -> {
|
||||||
|
Platform.runLater(() -> setResult(new Result(failedEvent.getSource().getException())));
|
||||||
|
});
|
||||||
|
webcamService.start();
|
||||||
|
setOnCloseRequest(event -> {
|
||||||
|
Platform.runLater(webcamService::cancel);
|
||||||
|
});
|
||||||
|
|
||||||
|
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||||
|
dialogPane.getButtonTypes().addAll(cancelButtonType);
|
||||||
|
dialogPane.setPrefWidth(660);
|
||||||
|
dialogPane.setPrefHeight(550);
|
||||||
|
|
||||||
|
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? result : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class QRResultListener implements ChangeListener<com.google.zxing.Result> {
|
||||||
|
@Override
|
||||||
|
public void changed(ObservableValue<? extends com.google.zxing.Result> observable, com.google.zxing.Result oldValue, com.google.zxing.Result qrResult) {
|
||||||
|
if(result != null) {
|
||||||
|
Platform.runLater(() -> setResult(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
//Try text first
|
||||||
|
String qrtext = qrResult.getText();
|
||||||
|
if(isUr || qrtext.toLowerCase().startsWith(UR.UR_PREFIX)) {
|
||||||
|
isUr = true;
|
||||||
|
decoder.receivePart(qrtext);
|
||||||
|
|
||||||
|
if(decoder.getResult() != null) {
|
||||||
|
URDecoder.Result urResult = decoder.getResult();
|
||||||
|
if(urResult.type == ResultType.SUCCESS) {
|
||||||
|
if(urResult.ur.getType().equals(UR.BYTES_TYPE)) {
|
||||||
|
try {
|
||||||
|
PSBT psbt = new PSBT(urResult.ur.toBytes());
|
||||||
|
result = new Result(psbt);
|
||||||
|
return;
|
||||||
|
} catch(Exception e) {
|
||||||
|
//ignore, bytes not parsable as PSBT
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Transaction transaction = new Transaction(urResult.ur.toBytes());
|
||||||
|
result = new Result(transaction);
|
||||||
|
return;
|
||||||
|
} catch(Exception e) {
|
||||||
|
//ignore, bytes not parsable as tx
|
||||||
|
}
|
||||||
|
|
||||||
|
result = new Result("Parsed UR of type " + urResult.ur.getType() + " was not a PSBT or transaction");
|
||||||
|
} else {
|
||||||
|
result = new Result("Cannot parse UR type of " + urResult.ur.getType());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = new Result(urResult.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
PSBT psbt;
|
||||||
|
Transaction transaction;
|
||||||
|
try {
|
||||||
|
psbt = PSBT.fromString(qrtext);
|
||||||
|
result = new Result(psbt);
|
||||||
|
return;
|
||||||
|
} catch(Exception e) {
|
||||||
|
//Ignore, not parseable as Base64 or hex
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
psbt = new PSBT(qrResult.getRawBytes());
|
||||||
|
result = new Result(psbt);
|
||||||
|
return;
|
||||||
|
} catch(Exception e) {
|
||||||
|
//Ignore, not parseable as raw bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
transaction = new Transaction(Utils.hexToBytes(qrtext));
|
||||||
|
result = new Result(transaction);
|
||||||
|
return;
|
||||||
|
} catch(Exception e) {
|
||||||
|
//Ignore, not parseable as hex
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
transaction = new Transaction(qrResult.getRawBytes());
|
||||||
|
result = new Result(transaction);
|
||||||
|
return;
|
||||||
|
} catch(Exception e) {
|
||||||
|
//Ignore, not parseable as raw bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
//Try Base43 used by Electrum
|
||||||
|
byte[] base43 = Base43.decode(qrResult.getText());
|
||||||
|
try {
|
||||||
|
psbt = new PSBT(base43);
|
||||||
|
result = new Result(psbt);
|
||||||
|
return;
|
||||||
|
} catch(Exception e) {
|
||||||
|
//Ignore, not parseable as base43 decoded bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
transaction = new Transaction(base43);
|
||||||
|
result = new Result(transaction);
|
||||||
|
return;
|
||||||
|
} catch(Exception e) {
|
||||||
|
//Ignore, not parseable as base43 decoded bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
result = new Result("Cannot parse QR code into a PSBT or transaction");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Result {
|
||||||
|
public final Transaction transaction;
|
||||||
|
public final PSBT psbt;
|
||||||
|
public final String error;
|
||||||
|
public final Throwable exception;
|
||||||
|
|
||||||
|
public Result(Transaction transaction) {
|
||||||
|
this.transaction = transaction;
|
||||||
|
this.psbt = null;
|
||||||
|
this.error = null;
|
||||||
|
this.exception = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result(PSBT psbt) {
|
||||||
|
this.transaction = null;
|
||||||
|
this.psbt = psbt;
|
||||||
|
this.error = null;
|
||||||
|
this.exception = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result(String error) {
|
||||||
|
this.transaction = null;
|
||||||
|
this.psbt = null;
|
||||||
|
this.error = error;
|
||||||
|
this.exception = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result(Throwable exception) {
|
||||||
|
this.transaction = null;
|
||||||
|
this.psbt = null;
|
||||||
|
this.error = null;
|
||||||
|
this.exception = exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.github.sarxos.webcam.Webcam;
|
||||||
|
import com.github.sarxos.webcam.WebcamResolution;
|
||||||
|
import com.google.zxing.*;
|
||||||
|
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
|
||||||
|
import com.google.zxing.common.HybridBinarizer;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.concurrent.Service;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
|
import javafx.embed.swing.SwingFXUtils;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class WebcamService extends Service<Image> {
|
||||||
|
private final WebcamResolution resolution ;
|
||||||
|
|
||||||
|
private final ObjectProperty<Result> resultProperty = new SimpleObjectProperty<>(null);
|
||||||
|
|
||||||
|
public WebcamService(WebcamResolution resolution) {
|
||||||
|
this.resolution = resolution;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Task<Image> createTask() {
|
||||||
|
return new Task<Image>() {
|
||||||
|
@Override
|
||||||
|
protected Image call() throws Exception {
|
||||||
|
Webcam cam = Webcam.getWebcams(1, TimeUnit.MINUTES).get(0);
|
||||||
|
try {
|
||||||
|
cam.setCustomViewSizes(resolution.getSize());
|
||||||
|
cam.setViewSize(resolution.getSize());
|
||||||
|
|
||||||
|
cam.open();
|
||||||
|
while(!isCancelled()) {
|
||||||
|
if(cam.isImageNew()) {
|
||||||
|
BufferedImage bimg = cam.getImage();
|
||||||
|
updateValue(SwingFXUtils.toFXImage(bimg, null));
|
||||||
|
readQR(bimg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cam.close();
|
||||||
|
return getValue();
|
||||||
|
} finally {
|
||||||
|
cam.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void readQR(BufferedImage bufferedImage) {
|
||||||
|
LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
|
||||||
|
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||||
|
|
||||||
|
try {
|
||||||
|
Result result = new MultiFormatReader().decode(bitmap);
|
||||||
|
resultProperty.set(result);
|
||||||
|
} catch(NotFoundException e) {
|
||||||
|
// fall thru, it means there is no QR code in image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result getResult() {
|
||||||
|
return resultProperty.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectProperty<Result> resultProperty() {
|
||||||
|
return resultProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCamWidth() {
|
||||||
|
return resolution.getSize().width;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCamHeight() {
|
||||||
|
return resolution.getSize().height;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.layout.Region;
|
||||||
|
|
||||||
|
public class WebcamView {
|
||||||
|
private final ImageView imageView;
|
||||||
|
private final WebcamService service;
|
||||||
|
private final Region view;
|
||||||
|
|
||||||
|
private final Label statusPlaceholder ;
|
||||||
|
|
||||||
|
public WebcamView(WebcamService service) {
|
||||||
|
this.service = service ;
|
||||||
|
this.imageView = new ImageView();
|
||||||
|
imageView.setPreserveRatio(true);
|
||||||
|
// make the cam behave like a mirror:
|
||||||
|
imageView.setScaleX(-1);
|
||||||
|
|
||||||
|
this.statusPlaceholder = new Label();
|
||||||
|
this.view = new Region() {
|
||||||
|
{
|
||||||
|
service.stateProperty().addListener((obs, oldState, newState) -> {
|
||||||
|
switch (newState) {
|
||||||
|
case READY:
|
||||||
|
statusPlaceholder.setText("Initializing");
|
||||||
|
getChildren().setAll(statusPlaceholder);
|
||||||
|
break ;
|
||||||
|
case SCHEDULED:
|
||||||
|
statusPlaceholder.setText("Waiting");
|
||||||
|
getChildren().setAll(statusPlaceholder);
|
||||||
|
break ;
|
||||||
|
case RUNNING:
|
||||||
|
imageView.imageProperty().unbind();
|
||||||
|
imageView.imageProperty().bind(service.valueProperty());
|
||||||
|
getChildren().setAll(imageView);
|
||||||
|
break ;
|
||||||
|
case CANCELLED:
|
||||||
|
imageView.imageProperty().unbind();
|
||||||
|
imageView.setImage(null);
|
||||||
|
statusPlaceholder.setText("Stopped");
|
||||||
|
getChildren().setAll(statusPlaceholder);
|
||||||
|
break;
|
||||||
|
case FAILED:
|
||||||
|
imageView.imageProperty().unbind();
|
||||||
|
statusPlaceholder.setText("Error");
|
||||||
|
getChildren().setAll(statusPlaceholder);
|
||||||
|
service.getException().printStackTrace();
|
||||||
|
break;
|
||||||
|
case SUCCEEDED:
|
||||||
|
// unreachable...
|
||||||
|
imageView.imageProperty().unbind();
|
||||||
|
statusPlaceholder.setText("");
|
||||||
|
getChildren().clear();
|
||||||
|
}
|
||||||
|
requestLayout();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void layoutChildren() {
|
||||||
|
super.layoutChildren();
|
||||||
|
double w = getWidth();
|
||||||
|
double h = getHeight();
|
||||||
|
if (service.isRunning()) {
|
||||||
|
imageView.setFitWidth(w);
|
||||||
|
imageView.setFitHeight(h);
|
||||||
|
imageView.resizeRelocate(0, 0, w, h);
|
||||||
|
} else {
|
||||||
|
double labelHeight = statusPlaceholder.prefHeight(w);
|
||||||
|
double labelWidth = statusPlaceholder.prefWidth(labelHeight);
|
||||||
|
statusPlaceholder.resizeRelocate((w - labelWidth)/2, (h-labelHeight)/2, labelWidth, labelHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected double computePrefWidth(double height) {
|
||||||
|
return service.getCamWidth();
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
protected double computePrefHeight(double width) {
|
||||||
|
return service.getCamHeight();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public WebcamService getService() {
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Node getView() {
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
|
public class RequestQRScanEvent {
|
||||||
|
//Empty event class used to request the QRScanDialog is opened
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
package com.sparrowwallet.sparrow.event;
|
package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
public class RequestTransactionOpenEvent {
|
public class RequestTransactionOpenEvent {
|
||||||
//Empty event class used to request the transaction open dialog
|
//Empty event class used to request the transaction open file dialog
|
||||||
}
|
}
|
||||||
|
|
|
@ -537,6 +537,8 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
public void scanPSBT(ActionEvent event) {
|
public void scanPSBT(ActionEvent event) {
|
||||||
ToggleButton toggleButton = (ToggleButton)event.getSource();
|
ToggleButton toggleButton = (ToggleButton)event.getSource();
|
||||||
toggleButton.setSelected(false);
|
toggleButton.setSelected(false);
|
||||||
|
|
||||||
|
EventManager.get().post(new RequestQRScanEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void savePSBT(ActionEvent event) {
|
public void savePSBT(ActionEvent event) {
|
||||||
|
|
|
@ -1,12 +1,25 @@
|
||||||
package com.sparrowwallet.sparrow.ur;
|
package com.sparrowwallet.sparrow.ur;
|
||||||
|
|
||||||
|
import co.nstant.in.cbor.CborBuilder;
|
||||||
|
import co.nstant.in.cbor.CborDecoder;
|
||||||
|
import co.nstant.in.cbor.CborEncoder;
|
||||||
|
import co.nstant.in.cbor.CborException;
|
||||||
|
import co.nstant.in.cbor.model.ByteString;
|
||||||
|
import co.nstant.in.cbor.model.DataItem;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ported from https://github.com/BlockchainCommons/URKit
|
* Ported from https://github.com/BlockchainCommons/URKit
|
||||||
*/
|
*/
|
||||||
public class UR {
|
public class UR {
|
||||||
|
public static final String UR_PREFIX = "ur";
|
||||||
|
public static final String BYTES_TYPE = "bytes";
|
||||||
|
|
||||||
private final String type;
|
private final String type;
|
||||||
private final byte[] data;
|
private final byte[] data;
|
||||||
|
|
||||||
|
@ -27,6 +40,16 @@ public class UR {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] toBytes() throws InvalidTypeException, CborException {
|
||||||
|
if(!BYTES_TYPE.equals(getType())) {
|
||||||
|
throw new InvalidTypeException("Not a " + BYTES_TYPE + " type");
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteArrayInputStream bais = new ByteArrayInputStream(getCbor());
|
||||||
|
List<DataItem> dataItems = new CborDecoder(bais).decode();
|
||||||
|
return ((ByteString)dataItems.get(0)).getBytes();
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isURType(String type) {
|
public static boolean isURType(String type) {
|
||||||
for(char c : type.toCharArray()) {
|
for(char c : type.toCharArray()) {
|
||||||
if('a' <= c && c <= 'z') {
|
if('a' <= c && c <= 'z') {
|
||||||
|
@ -45,8 +68,14 @@ public class UR {
|
||||||
|
|
||||||
public static UR fromBytes(byte[] data) {
|
public static UR fromBytes(byte[] data) {
|
||||||
try {
|
try {
|
||||||
return new UR("bytes", data);
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
} catch(UR.InvalidTypeException e) {
|
new CborEncoder(baos).encode(new CborBuilder()
|
||||||
|
.add(data)
|
||||||
|
.build());
|
||||||
|
byte[] cbor = baos.toByteArray();
|
||||||
|
|
||||||
|
return new UR("bytes", cbor);
|
||||||
|
} catch(InvalidTypeException | CborException e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ public class UREncoder {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String encodeUR(String... pathComponents) {
|
private static String encodeUR(String... pathComponents) {
|
||||||
return encodeURI("ur", pathComponents);
|
return encodeURI(UR.UR_PREFIX, pathComponents);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String encodeURI(String scheme, String... pathComponents) {
|
private static String encodeURI(String scheme, String... pathComponents) {
|
||||||
|
|
|
@ -18,6 +18,7 @@ open module com.sparrowwallet.sparrow {
|
||||||
requires org.jetbrains.annotations;
|
requires org.jetbrains.annotations;
|
||||||
requires com.fasterxml.jackson.databind;
|
requires com.fasterxml.jackson.databind;
|
||||||
requires cbor;
|
requires cbor;
|
||||||
|
requires webcam.capture;
|
||||||
requires centerdevice.nsmenufx;
|
requires centerdevice.nsmenufx;
|
||||||
requires javafx.swing;
|
requires javafx.swing;
|
||||||
}
|
}
|
|
@ -21,6 +21,7 @@
|
||||||
<MenuItem text="File..." onAction="#openTransactionFromFile"/>
|
<MenuItem text="File..." onAction="#openTransactionFromFile"/>
|
||||||
<MenuItem fx:id="openTransactionIdItem" text="From ID..." onAction="#openTransactionFromId"/>
|
<MenuItem fx:id="openTransactionIdItem" text="From ID..." onAction="#openTransactionFromId"/>
|
||||||
<MenuItem text="From Text..." onAction="#openTransactionFromText"/>
|
<MenuItem text="From Text..." onAction="#openTransactionFromText"/>
|
||||||
|
<MenuItem text="From QR..." onAction="#openTransactionFromQR"/>
|
||||||
<MenuItem text="Examples" onAction="#openExamples"/>
|
<MenuItem text="Examples" onAction="#openExamples"/>
|
||||||
</items>
|
</items>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
Loading…
Reference in a new issue