add cobo vault support with legacy qr scanning

This commit is contained in:
Craig Raw 2020-11-17 13:32:07 +02:00
parent bf6fbebd9e
commit 8e23bd64c7
5 changed files with 179 additions and 31 deletions

View file

@ -51,7 +51,7 @@ dependencies {
implementation('com.github.arteam:simple-json-rpc-server:1.0') { implementation('com.github.arteam:simple-json-rpc-server:1.0') {
exclude group: 'org.slf4j' exclude group: 'org.slf4j'
} }
implementation('com.sparrowwallet:hummingbird:1.4') implementation('com.sparrowwallet:hummingbird:1.5.2')
implementation('com.nativelibs4java:bridj:0.7-20140918-3') { implementation('com.nativelibs4java:bridj:0.7-20140918-3') {
exclude group: 'com.google.android.tools', module: 'dx' exclude group: 'com.google.android.tools', module: 'dx'
} }

View file

@ -17,6 +17,7 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.psbt.PSBTParseException; import com.sparrowwallet.drongo.psbt.PSBTParseException;
import com.sparrowwallet.drongo.uri.BitcoinURI; import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
@ -1137,14 +1138,31 @@ public class AppController implements Initializable {
//If an exact match bytewise of an existing tab, return that tab //If an exact match bytewise of an existing tab, return that tab
if(Arrays.equals(transactionTabData.getTransaction().bitcoinSerialize(), transaction.bitcoinSerialize())) { if(Arrays.equals(transactionTabData.getTransaction().bitcoinSerialize(), transaction.bitcoinSerialize())) {
if(transactionTabData.getPsbt() != null && psbt != null && !transactionTabData.getPsbt().isFinalized()) {
if(!psbt.isFinalized()) {
//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()) {
transactionTabData.getPsbt().combine(psbt); transactionTabData.getPsbt().combine(psbt);
if(name != null && !name.isEmpty()) { if(name != null && !name.isEmpty()) {
tab.setText(name); tab.setText(name);
} }
EventManager.get().post(new PSBTCombinedEvent(transactionTabData.getPsbt())); EventManager.get().post(new PSBTCombinedEvent(transactionTabData.getPsbt()));
} else {
//If the new PSBT is finalized, copy the finalized fields to the existing unfinalized PSBT
for(int i = 0; i < transactionTabData.getPsbt().getPsbtInputs().size(); i++) {
PSBTInput existingInput = transactionTabData.getPsbt().getPsbtInputs().get(i);
PSBTInput finalizedInput = psbt.getPsbtInputs().get(i);
existingInput.setFinalScriptSig(finalizedInput.getFinalScriptSig());
existingInput.setFinalScriptWitness(finalizedInput.getFinalScriptWitness());
existingInput.clearNonFinalFields();
}
if(name != null && !name.isEmpty()) {
tab.setText(name);
}
EventManager.get().post(new PSBTFinalizedEvent(transactionTabData.getPsbt()));
}
} }
return tab; return tab;

View file

@ -5,20 +5,22 @@ import com.google.zxing.client.j2se.MatrixToImageConfig;
import com.google.zxing.client.j2se.MatrixToImageWriter; import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix; import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter; import com.google.zxing.qrcode.QRCodeWriter;
import com.sparrowwallet.hummingbird.LegacyUREncoder;
import com.sparrowwallet.hummingbird.registry.RegistryType;
import com.sparrowwallet.sparrow.AppController; import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
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 javafx.concurrent.ScheduledService; import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import javafx.scene.control.ButtonBar; import javafx.scene.Node;
import javafx.scene.control.ButtonType; import javafx.scene.control.*;
import javafx.scene.control.Dialog;
import javafx.scene.control.DialogPane;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.util.Duration; import javafx.util.Duration;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Borders; import org.controlsfx.tools.Borders;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -26,30 +28,41 @@ import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@SuppressWarnings("deprecation")
public class QRDisplayDialog extends Dialog<UR> { public class QRDisplayDialog extends Dialog<UR> {
private static final Logger log = LoggerFactory.getLogger(QRDisplayDialog.class); private static final Logger log = LoggerFactory.getLogger(QRDisplayDialog.class);
private static final int MIN_FRAGMENT_LENGTH = 10; private static final int MIN_FRAGMENT_LENGTH = 10;
private static final int MAX_FRAGMENT_LENGTH = 100; private static final int MAX_FRAGMENT_LENGTH = 100;
private static final int ANIMATION_PERIOD_MILLIS = 200; private static final int ANIMATION_PERIOD_MILLIS = 400;
private final UR ur; private final UR ur;
private final UREncoder encoder; private final UREncoder encoder;
private final ImageView qrImageView; private final ImageView qrImageView;
private AnimateQRService animateQRService;
private String currentPart; private String currentPart;
public QRDisplayDialog(String type, byte[] data) throws UR.URException { private boolean useLegacyEncoding;
this(UR.fromBytes(type, data)); private String[] legacyParts;
private int legacyPartIndex;
public QRDisplayDialog(String type, byte[] data, boolean addLegacyEncodingOption) throws UR.URException {
this(UR.fromBytes(type, data), addLegacyEncodingOption);
} }
public QRDisplayDialog(UR ur) { public QRDisplayDialog(UR ur) {
this(ur, false);
}
public QRDisplayDialog(UR ur, boolean addLegacyEncodingOption) {
this.ur = ur; this.ur = ur;
this.encoder = new UREncoder(ur, MAX_FRAGMENT_LENGTH, MIN_FRAGMENT_LENGTH, 0); this.encoder = new UREncoder(ur, MAX_FRAGMENT_LENGTH, MIN_FRAGMENT_LENGTH, 0);
final DialogPane dialogPane = getDialogPane(); final DialogPane dialogPane = new QRDisplayDialogPane();
setDialogPane(dialogPane);
AppController.setStageIcon(dialogPane.getScene().getWindow()); AppController.setStageIcon(dialogPane.getScene().getWindow());
StackPane stackPane = new StackPane(); StackPane stackPane = new StackPane();
@ -62,7 +75,7 @@ public class QRDisplayDialog extends Dialog<UR> {
if(encoder.isSinglePart()) { if(encoder.isSinglePart()) {
qrImageView.setImage(getQrCode(currentPart)); qrImageView.setImage(getQrCode(currentPart));
} else { } else {
AnimateQRService animateQRService = new AnimateQRService(); animateQRService = new AnimateQRService();
animateQRService.setPeriod(Duration.millis(ANIMATION_PERIOD_MILLIS)); animateQRService.setPeriod(Duration.millis(ANIMATION_PERIOD_MILLIS));
animateQRService.start(); animateQRService.start();
setOnCloseRequest(event -> { setOnCloseRequest(event -> {
@ -71,7 +84,13 @@ public class QRDisplayDialog extends Dialog<UR> {
} }
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType); dialogPane.getButtonTypes().add(cancelButtonType);
if(addLegacyEncodingOption) {
final ButtonType legacyEncodingButtonType = new javafx.scene.control.ButtonType("Use Legacy Encoding (Cobo Vault)", ButtonBar.ButtonData.LEFT);
dialogPane.getButtonTypes().add(legacyEncodingButtonType);
}
dialogPane.setPrefWidth(500); dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(550); dialogPane.setPrefHeight(550);
@ -101,8 +120,16 @@ public class QRDisplayDialog extends Dialog<UR> {
} }
private void nextPart() { private void nextPart() {
if(!useLegacyEncoding) {
String fragment = encoder.nextPart(); String fragment = encoder.nextPart();
currentPart = fragment.toUpperCase(); currentPart = fragment.toUpperCase();
} else {
currentPart = legacyParts[legacyPartIndex];
legacyPartIndex++;
if(legacyPartIndex > legacyParts.length - 1) {
legacyPartIndex = 0;
}
}
} }
private Image getQrCode(String fragment) { private Image getQrCode(String fragment) {
@ -122,6 +149,44 @@ public class QRDisplayDialog extends Dialog<UR> {
return null; return null;
} }
private void setUseLegacyEncoding(boolean useLegacyEncoding) {
if(useLegacyEncoding) {
try {
//Force to be bytes type for legacy encoding
LegacyUREncoder legacyEncoder = new LegacyUREncoder(new UR(RegistryType.BYTES.toString(), ur.getCborBytes()));
this.legacyParts = legacyEncoder.encode();
this.useLegacyEncoding = true;
if(legacyParts.length == 1) {
if(animateQRService != null) {
animateQRService.cancel();
}
nextPart();
qrImageView.setImage(getQrCode(currentPart));
} else if(!animateQRService.isRunning()) {
animateQRService.reset();
animateQRService.start();
}
} catch(UR.InvalidTypeException e) {
//Can't happen
}
} else {
this.useLegacyEncoding = false;
if(encoder.isSinglePart()) {
if(animateQRService != null) {
animateQRService.cancel();
}
qrImageView.setImage(getQrCode(currentPart));
} else if(!animateQRService.isRunning()) {
animateQRService.reset();
animateQRService.start();
}
}
}
private class AnimateQRService extends ScheduledService<Boolean> { private class AnimateQRService extends ScheduledService<Boolean> {
@Override @Override
protected Task<Boolean> createTask() { protected Task<Boolean> createTask() {
@ -136,4 +201,44 @@ public class QRDisplayDialog extends Dialog<UR> {
}; };
} }
} }
private class QRDisplayDialogPane extends DialogPane {
@Override
protected Node createButton(ButtonType buttonType) {
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
ToggleButton legacy = new ToggleButton(buttonType.getText());
legacy.setGraphicTextGap(5);
setLegacyGraphic(legacy, false);
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
ButtonBar.setButtonData(legacy, buttonData);
legacy.selectedProperty().addListener((observable, oldValue, newValue) -> {
setUseLegacyEncoding(newValue);
setLegacyGraphic(legacy, newValue);
});
return legacy;
}
return super.createButton(buttonType);
}
private void setLegacyGraphic(ToggleButton legacy, boolean useLegacyEncoding) {
if(useLegacyEncoding) {
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE, "success"));
} else {
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.QUESTION_CIRCLE, null));
}
}
private Glyph getGlyph(FontAwesome5.Glyph glyphName, String styleClass) {
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
glyph.setFontSize(12);
if(styleClass != null) {
glyph.getStyleClass().add(styleClass);
}
return glyph;
}
}
} }

View file

@ -17,6 +17,7 @@ import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.uri.BitcoinURI; import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.hummingbird.LegacyURDecoder;
import com.sparrowwallet.hummingbird.registry.*; import com.sparrowwallet.hummingbird.registry.*;
import com.sparrowwallet.sparrow.AppController; import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.hummingbird.ResultType; import com.sparrowwallet.hummingbird.ResultType;
@ -39,10 +40,12 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.IntStream; import java.util.stream.IntStream;
@SuppressWarnings("deprecation")
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 decoder;
private final LegacyURDecoder legacyDecoder;
private final WebcamService webcamService; private final WebcamService webcamService;
private List<String> parts; private List<String> parts;
@ -53,6 +56,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
public QRScanDialog() { public QRScanDialog() {
this.decoder = new URDecoder(); this.decoder = new URDecoder();
this.legacyDecoder = new LegacyURDecoder();
this.webcamService = new WebcamService(WebcamResolution.VGA); this.webcamService = new WebcamService(WebcamResolution.VGA);
WebcamView webcamView = new WebcamView(webcamService); WebcamView webcamView = new WebcamView(webcamService);
@ -95,6 +99,19 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
if(isUr || qrtext.toLowerCase().startsWith(UR.UR_PREFIX)) { if(isUr || qrtext.toLowerCase().startsWith(UR.UR_PREFIX)) {
isUr = true; isUr = true;
if(LegacyURDecoder.isLegacyURFragment(qrtext)) {
legacyDecoder.receivePart(qrtext.toLowerCase());
if(legacyDecoder.isComplete()) {
try {
UR ur = legacyDecoder.decode();
result = extractResultFromUR(ur);
} catch(Exception e) {
result = new Result(new URException(e.getMessage()));
}
}
} else {
decoder.receivePart(qrtext); decoder.receivePart(qrtext);
if(decoder.getResult() != null) { if(decoder.getResult() != null) {
@ -105,6 +122,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
result = new Result(new URException(urResult.error)); result = new Result(new URException(urResult.error));
} }
} }
}
} 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));
@ -313,8 +331,8 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
lastChild = new ChildNumber(lastComponent.getIndex(), lastComponent.isHardened()); lastChild = new ChildNumber(lastComponent.getIndex(), lastComponent.isHardened());
depth = cryptoHDKey.getOrigin().getComponents().size(); depth = cryptoHDKey.getOrigin().getComponents().size();
} }
if(cryptoHDKey.getOrigin().getParentFingerprint() != null) { if(cryptoHDKey.getParentFingerprint() != null) {
parentFingerprint = cryptoHDKey.getOrigin().getParentFingerprint(); parentFingerprint = cryptoHDKey.getParentFingerprint();
} }
} }
DeterministicKey pubKey = new DeterministicKey(List.of(lastChild), cryptoHDKey.getChainCode(), cryptoHDKey.getKey(), depth, parentFingerprint); DeterministicKey pubKey = new DeterministicKey(List.of(lastChild), cryptoHDKey.getChainCode(), cryptoHDKey.getKey(), depth, parentFingerprint);
@ -387,7 +405,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
private KeyDerivation getKeyDerivation(CryptoKeypath cryptoKeypath) { private KeyDerivation getKeyDerivation(CryptoKeypath cryptoKeypath) {
if(cryptoKeypath != null) { if(cryptoKeypath != null) {
return new KeyDerivation(Utils.bytesToHex(cryptoKeypath.getParentFingerprint()), cryptoKeypath.getPath()); return new KeyDerivation(Utils.bytesToHex(cryptoKeypath.getSourceFingerprint()), cryptoKeypath.getPath());
} }
return null; return null;

View file

@ -620,8 +620,11 @@ 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
boolean addLegacyEncodingOption = headersForm.getSigningWallet().getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().equals(WalletModel.COBO_VAULT));
try { try {
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(RegistryType.CRYPTO_PSBT.toString(), headersForm.getPsbt().serialize()); QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(RegistryType.CRYPTO_PSBT.toString(), headersForm.getPsbt().serialize(), addLegacyEncodingOption);
qrDisplayDialog.show(); qrDisplayDialog.show();
} catch(UR.URException e) { } catch(UR.URException e) {
log.error("Error creating PSBT UR", e); log.error("Error creating PSBT UR", e);
@ -981,6 +984,10 @@ public class HeadersController extends TransactionFormController implements Init
@Subscribe @Subscribe
public void psbtFinalized(PSBTFinalizedEvent event) { public void psbtFinalized(PSBTFinalizedEvent event) {
if(event.getPsbt().equals(headersForm.getPsbt())) { if(event.getPsbt().equals(headersForm.getPsbt())) {
if(headersForm.getSigningWallet() != null) {
updateSignedKeystores(headersForm.getSigningWallet());
}
signButtonBox.setVisible(false); signButtonBox.setVisible(false);
broadcastButtonBox.setVisible(true); broadcastButtonBox.setVisible(true);
} }