diff --git a/src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java index 01ad2199..be014589 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java @@ -2,44 +2,81 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.drongo.SecureString; import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.hummingbird.registry.RegistryType; +import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.event.StorageEvent; import com.sparrowwallet.sparrow.event.TimedEvent; import com.sparrowwallet.sparrow.event.WalletExportEvent; +import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; +import com.sparrowwallet.sparrow.io.CoboVaultMultisig; import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.io.WalletExport; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.Control; +import javafx.scene.control.ToggleButton; import javafx.stage.FileChooser; import javafx.stage.Stage; +import org.controlsfx.control.SegmentedButton; +import org.controlsfx.glyphfont.Glyph; -import java.io.File; -import java.io.FileOutputStream; -import java.io.OutputStream; +import java.io.*; +import java.nio.charset.StandardCharsets; import java.util.Optional; public class FileWalletExportPane extends TitledDescriptionPane { private final Wallet wallet; private final WalletExport exporter; + private final boolean scannable; public FileWalletExportPane(Wallet wallet, WalletExport exporter) { super(exporter.getName(), "Wallet file export", exporter.getWalletExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png"); this.wallet = wallet; this.exporter = exporter; + this.scannable = exporter.isWalletExportScannable(); + + buttonBox.getChildren().clear(); + buttonBox.getChildren().add(createButton()); } @Override protected Control createButton() { - Button exportButton = new Button("Export Wallet..."); - exportButton.setAlignment(Pos.CENTER_RIGHT); - exportButton.setOnAction(event -> { - exportWallet(); - }); - return exportButton; + if(scannable) { + ToggleButton showButton = new ToggleButton("Show..."); + Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA); + cameraGlyph.setFontSize(12); + showButton.setGraphic(cameraGlyph); + showButton.setOnAction(event -> { + showButton.setSelected(false); + exportQR(); + }); + + ToggleButton fileButton = new ToggleButton("Export File..."); + fileButton.setAlignment(Pos.CENTER_RIGHT); + fileButton.setOnAction(event -> { + fileButton.setSelected(false); + exportFile(); + }); + + SegmentedButton segmentedButton = new SegmentedButton(); + segmentedButton.getButtons().addAll(showButton, fileButton); + return segmentedButton; + } else { + Button exportButton = new Button("Export File..."); + exportButton.setAlignment(Pos.CENTER_RIGHT); + exportButton.setOnAction(event -> { + exportFile(); + }); + return exportButton; + } } - private void exportWallet() { + private void exportQR() { + exportWallet(null); + } + + private void exportFile() { Stage window = new Stage(); FileChooser fileChooser = new FileChooser(); @@ -59,17 +96,18 @@ public class FileWalletExportPane extends TitledDescriptionPane { WalletPasswordDialog dlg = new WalletPasswordDialog(WalletPasswordDialog.PasswordRequirement.LOAD); Optional password = dlg.showAndWait(); if(password.isPresent()) { + final File walletFile = AppServices.get().getOpenWallets().get(wallet).getWalletFile(); Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get()); decryptWalletService.setOnSucceeded(workerStateEvent -> { - EventManager.get().post(new StorageEvent(file, TimedEvent.Action.END, "Done")); + EventManager.get().post(new StorageEvent(walletFile, TimedEvent.Action.END, "Done")); Wallet decryptedWallet = decryptWalletService.getValue(); exportWallet(file, decryptedWallet); }); decryptWalletService.setOnFailed(workerStateEvent -> { - EventManager.get().post(new StorageEvent(file, TimedEvent.Action.END, "Failed")); + EventManager.get().post(new StorageEvent(walletFile, TimedEvent.Action.END, "Failed")); setError("Export Error", decryptWalletService.getException().getMessage()); }); - EventManager.get().post(new StorageEvent(file, TimedEvent.Action.START, "Decrypting wallet...")); + EventManager.get().post(new StorageEvent(walletFile, TimedEvent.Action.START, "Decrypting wallet...")); decryptWalletService.start(); } } else { @@ -79,9 +117,21 @@ public class FileWalletExportPane extends TitledDescriptionPane { private void exportWallet(File file, Wallet decryptedWallet) { try { - OutputStream outputStream = new FileOutputStream(file); - exporter.exportWallet(decryptedWallet, outputStream); - EventManager.get().post(new WalletExportEvent(decryptedWallet)); + if(file != null) { + OutputStream outputStream = new FileOutputStream(file); + exporter.exportWallet(decryptedWallet, outputStream); + EventManager.get().post(new WalletExportEvent(decryptedWallet)); + } else { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + exporter.exportWallet(decryptedWallet, outputStream); + QRDisplayDialog qrDisplayDialog; + if(exporter instanceof CoboVaultMultisig) { + qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true); + } else { + qrDisplayDialog = new QRDisplayDialog(outputStream.toString(StandardCharsets.UTF_8)); + } + qrDisplayDialog.showAndWait(); + } } catch(Exception e) { String errorMessage = e.getMessage(); if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) { diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java index 7eb197ec..14e98d4f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java @@ -43,7 +43,7 @@ public class WalletExportDialog extends Dialog { if(wallet.getPolicyType() == PolicyType.SINGLE) { exporters = List.of(new Electrum(), new SpecterDesktop()); } else if(wallet.getPolicyType() == PolicyType.MULTI) { - exporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new SpecterDesktop()); + exporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new SpecterDesktop(), new BlueWalletMultisig()); } else { throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType()); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java index 596a44f9..f431b11e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java @@ -45,7 +45,7 @@ public class WalletImportDialog extends Dialog { importAccordion.getPanes().add(importPane); } - List walletImporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new SpecterDesktop()); + List walletImporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new SpecterDesktop(), new BlueWalletMultisig()); for(WalletImport importer : walletImporters) { FileWalletImportPane importPane = new FileWalletImportPane(importer); importAccordion.getPanes().add(importPane); diff --git a/src/main/java/com/sparrowwallet/sparrow/io/BlueWalletMultisig.java b/src/main/java/com/sparrowwallet/sparrow/io/BlueWalletMultisig.java new file mode 100644 index 00000000..0ec7b312 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/BlueWalletMultisig.java @@ -0,0 +1,50 @@ +package com.sparrowwallet.sparrow.io; + +import com.sparrowwallet.drongo.wallet.Keystore; +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.drongo.wallet.WalletModel; + +import java.io.InputStream; + +public class BlueWalletMultisig extends ColdcardMultisig { + @Override + public String getName() { + return "Blue Wallet Vault Multisig"; + } + + @Override + public WalletModel getWalletModel() { + return WalletModel.BLUE_WALLET; + } + + @Override + public Wallet importWallet(InputStream inputStream, String password) throws ImportException { + Wallet wallet = super.importWallet(inputStream, password); + for(Keystore keystore : wallet.getKeystores()) { + keystore.setLabel(keystore.getLabel().replace("Coldcard", "Blue Wallet")); + keystore.setWalletModel(WalletModel.BLUE_WALLET); + } + + return wallet; + } + + @Override + public String getWalletImportDescription() { + return "Import file or QR created by using the Wallet > Export Coordination Setup feature on your Blue Wallet Vault wallet."; + } + + @Override + public String getWalletExportDescription() { + return "Export file that can be read by Blue Wallet using the Add Wallet > Vault > Import wallet feature."; + } + + @Override + public boolean isWalletImportScannable() { + return true; + } + + @Override + public boolean isWalletExportScannable() { + return true; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/CoboVaultMultisig.java b/src/main/java/com/sparrowwallet/sparrow/io/CoboVaultMultisig.java index 81b50734..084c199a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/CoboVaultMultisig.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/CoboVaultMultisig.java @@ -62,4 +62,9 @@ public class CoboVaultMultisig extends ColdcardMultisig { public boolean isKeystoreImportScannable() { return true; } + + @Override + public boolean isWalletExportScannable() { + return true; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ColdcardMultisig.java b/src/main/java/com/sparrowwallet/sparrow/io/ColdcardMultisig.java index 75a5c8c6..9f027ce0 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ColdcardMultisig.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ColdcardMultisig.java @@ -222,4 +222,9 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle public boolean isKeystoreImportScannable() { return false; } + + @Override + public boolean isWalletExportScannable() { + return false; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Electrum.java b/src/main/java/com/sparrowwallet/sparrow/io/Electrum.java index bd18c0e0..f91ded5e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Electrum.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Electrum.java @@ -359,6 +359,11 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport return false; } + @Override + public boolean isWalletExportScannable() { + return false; + } + @Override public String getWalletExportDescription() { return "Export this wallet as an Electrum wallet file."; diff --git a/src/main/java/com/sparrowwallet/sparrow/io/SpecterDesktop.java b/src/main/java/com/sparrowwallet/sparrow/io/SpecterDesktop.java index fbc17eec..48c95cc7 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/SpecterDesktop.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/SpecterDesktop.java @@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.io; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.sparrowwallet.drongo.OutputDescriptor; +import com.sparrowwallet.drongo.wallet.BlockTransactionHash; import com.sparrowwallet.drongo.wallet.InvalidWalletException; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.WalletModel; @@ -19,7 +20,7 @@ public class SpecterDesktop implements WalletImport, WalletExport { try { SpecterWallet specterWallet = new SpecterWallet(); specterWallet.label = wallet.getName(); - specterWallet.blockheight = wallet.getStoredBlockHeight(); + specterWallet.blockheight = wallet.getTransactions().values().stream().mapToInt(BlockTransactionHash::getHeight).min().orElse(wallet.getStoredBlockHeight()); specterWallet.descriptor = OutputDescriptor.getOutputDescriptor(wallet).toString(true); Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); @@ -83,6 +84,11 @@ public class SpecterDesktop implements WalletImport, WalletExport { return true; } + @Override + public boolean isWalletExportScannable() { + return true; + } + @Override public String getName() { return "Specter Desktop"; diff --git a/src/main/java/com/sparrowwallet/sparrow/io/WalletExport.java b/src/main/java/com/sparrowwallet/sparrow/io/WalletExport.java index 95f34344..75bf5009 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/WalletExport.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/WalletExport.java @@ -8,4 +8,5 @@ public interface WalletExport extends Export { void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException; String getWalletExportDescription(); String getExportFileExtension(); + boolean isWalletExportScannable(); } diff --git a/src/main/resources/image/bluewallet.png b/src/main/resources/image/bluewallet.png new file mode 100644 index 00000000..746172d4 Binary files /dev/null and b/src/main/resources/image/bluewallet.png differ diff --git a/src/main/resources/image/bluewallet@2x.png b/src/main/resources/image/bluewallet@2x.png new file mode 100644 index 00000000..2be8239e Binary files /dev/null and b/src/main/resources/image/bluewallet@2x.png differ diff --git a/src/main/resources/image/bluewallet@3x.png b/src/main/resources/image/bluewallet@3x.png new file mode 100644 index 00000000..79bad672 Binary files /dev/null and b/src/main/resources/image/bluewallet@3x.png differ