diff --git a/build.gradle b/build.gradle index ac3896d8..b5f77732 100644 --- a/build.gradle +++ b/build.gradle @@ -51,7 +51,7 @@ dependencies { implementation('com.github.arteam:simple-json-rpc-server:1.0') { 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') { exclude group: 'com.google.android.tools', module: 'dx' } diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 1bcd501b..afc149fd 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -17,6 +17,7 @@ import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.psbt.PSBT; +import com.sparrowwallet.drongo.psbt.PSBTInput; import com.sparrowwallet.drongo.psbt.PSBTParseException; import com.sparrowwallet.drongo.uri.BitcoinURI; 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(Arrays.equals(transactionTabData.getTransaction().bitcoinSerialize(), transaction.bitcoinSerialize())) { - //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); - if(name != null && !name.isEmpty()) { - tab.setText(name); - } + 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 + transactionTabData.getPsbt().combine(psbt); + if(name != null && !name.isEmpty()) { + 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; diff --git a/src/main/java/com/sparrowwallet/sparrow/control/QRDisplayDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/QRDisplayDialog.java index b94e22c9..cf663b01 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/QRDisplayDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/QRDisplayDialog.java @@ -5,20 +5,22 @@ import com.google.zxing.client.j2se.MatrixToImageConfig; import com.google.zxing.client.j2se.MatrixToImageWriter; import com.google.zxing.common.BitMatrix; import com.google.zxing.qrcode.QRCodeWriter; +import com.sparrowwallet.hummingbird.LegacyUREncoder; +import com.sparrowwallet.hummingbird.registry.RegistryType; import com.sparrowwallet.sparrow.AppController; +import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.io.ImportException; import com.sparrowwallet.hummingbird.UR; import com.sparrowwallet.hummingbird.UREncoder; import javafx.concurrent.ScheduledService; import javafx.concurrent.Task; -import javafx.scene.control.ButtonBar; -import javafx.scene.control.ButtonType; -import javafx.scene.control.Dialog; -import javafx.scene.control.DialogPane; +import javafx.scene.Node; +import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.StackPane; import javafx.util.Duration; +import org.controlsfx.glyphfont.Glyph; import org.controlsfx.tools.Borders; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,30 +28,41 @@ import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +@SuppressWarnings("deprecation") public class QRDisplayDialog extends Dialog { private static final Logger log = LoggerFactory.getLogger(QRDisplayDialog.class); private static final int MIN_FRAGMENT_LENGTH = 10; 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 UREncoder encoder; private final ImageView qrImageView; + private AnimateQRService animateQRService; private String currentPart; - public QRDisplayDialog(String type, byte[] data) throws UR.URException { - this(UR.fromBytes(type, data)); + private boolean useLegacyEncoding; + 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) { + this(ur, false); + } + + public QRDisplayDialog(UR ur, boolean addLegacyEncodingOption) { this.ur = ur; 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()); StackPane stackPane = new StackPane(); @@ -62,7 +75,7 @@ public class QRDisplayDialog extends Dialog { if(encoder.isSinglePart()) { qrImageView.setImage(getQrCode(currentPart)); } else { - AnimateQRService animateQRService = new AnimateQRService(); + animateQRService = new AnimateQRService(); animateQRService.setPeriod(Duration.millis(ANIMATION_PERIOD_MILLIS)); animateQRService.start(); setOnCloseRequest(event -> { @@ -71,7 +84,13 @@ public class QRDisplayDialog extends Dialog { } 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.setPrefHeight(550); @@ -101,8 +120,16 @@ public class QRDisplayDialog extends Dialog { } private void nextPart() { - String fragment = encoder.nextPart(); - currentPart = fragment.toUpperCase(); + if(!useLegacyEncoding) { + String fragment = encoder.nextPart(); + currentPart = fragment.toUpperCase(); + } else { + currentPart = legacyParts[legacyPartIndex]; + legacyPartIndex++; + if(legacyPartIndex > legacyParts.length - 1) { + legacyPartIndex = 0; + } + } } private Image getQrCode(String fragment) { @@ -122,6 +149,44 @@ public class QRDisplayDialog extends Dialog { 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 { @Override protected Task createTask() { @@ -136,4 +201,44 @@ public class QRDisplayDialog extends Dialog { }; } } + + 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; + } + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java index aa7a3928..e67ec4dc 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java @@ -17,6 +17,7 @@ import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.uri.BitcoinURI; import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.hummingbird.LegacyURDecoder; import com.sparrowwallet.hummingbird.registry.*; import com.sparrowwallet.sparrow.AppController; import com.sparrowwallet.hummingbird.ResultType; @@ -39,10 +40,12 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.IntStream; +@SuppressWarnings("deprecation") public class QRScanDialog extends Dialog { private static final Logger log = LoggerFactory.getLogger(QRScanDialog.class); private final URDecoder decoder; + private final LegacyURDecoder legacyDecoder; private final WebcamService webcamService; private List parts; @@ -53,6 +56,7 @@ public class QRScanDialog extends Dialog { public QRScanDialog() { this.decoder = new URDecoder(); + this.legacyDecoder = new LegacyURDecoder(); this.webcamService = new WebcamService(WebcamResolution.VGA); WebcamView webcamView = new WebcamView(webcamService); @@ -95,14 +99,28 @@ public class QRScanDialog extends Dialog { 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) { - result = extractResultFromUR(urResult.ur); - } else { - result = new Result(new URException(urResult.error)); + 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); + + if(decoder.getResult() != null) { + URDecoder.Result urResult = decoder.getResult(); + if(urResult.type == ResultType.SUCCESS) { + result = extractResultFromUR(urResult.ur); + } else { + result = new Result(new URException(urResult.error)); + } } } } else if(partMatcher.matches()) { @@ -313,8 +331,8 @@ public class QRScanDialog extends Dialog { lastChild = new ChildNumber(lastComponent.getIndex(), lastComponent.isHardened()); depth = cryptoHDKey.getOrigin().getComponents().size(); } - if(cryptoHDKey.getOrigin().getParentFingerprint() != null) { - parentFingerprint = cryptoHDKey.getOrigin().getParentFingerprint(); + if(cryptoHDKey.getParentFingerprint() != null) { + parentFingerprint = cryptoHDKey.getParentFingerprint(); } } DeterministicKey pubKey = new DeterministicKey(List.of(lastChild), cryptoHDKey.getChainCode(), cryptoHDKey.getKey(), depth, parentFingerprint); @@ -387,7 +405,7 @@ public class QRScanDialog extends Dialog { private KeyDerivation getKeyDerivation(CryptoKeypath cryptoKeypath) { if(cryptoKeypath != null) { - return new KeyDerivation(Utils.bytesToHex(cryptoKeypath.getParentFingerprint()), cryptoKeypath.getPath()); + return new KeyDerivation(Utils.bytesToHex(cryptoKeypath.getSourceFingerprint()), cryptoKeypath.getPath()); } return null; diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index 043bc38a..c78fb386 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java @@ -620,8 +620,11 @@ public class HeadersController extends TransactionFormController implements Init ToggleButton toggleButton = (ToggleButton)event.getSource(); 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 { - 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(); } catch(UR.URException e) { log.error("Error creating PSBT UR", e); @@ -981,6 +984,10 @@ public class HeadersController extends TransactionFormController implements Init @Subscribe public void psbtFinalized(PSBTFinalizedEvent event) { if(event.getPsbt().equals(headersForm.getPsbt())) { + if(headersForm.getSigningWallet() != null) { + updateSignedKeystores(headersForm.getSigningWallet()); + } + signButtonBox.setVisible(false); broadcastButtonBox.setVisible(true); }