add blue wallet vault import and export and add wallet export via qr

This commit is contained in:
Craig Raw 2021-02-08 16:29:59 +02:00
parent c27a576b3d
commit 622b681109
12 changed files with 141 additions and 19 deletions

View file

@ -2,44 +2,81 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.SecureString; import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.wallet.Wallet; 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.EventManager;
import com.sparrowwallet.sparrow.event.StorageEvent; import com.sparrowwallet.sparrow.event.StorageEvent;
import com.sparrowwallet.sparrow.event.TimedEvent; import com.sparrowwallet.sparrow.event.TimedEvent;
import com.sparrowwallet.sparrow.event.WalletExportEvent; 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.Storage;
import com.sparrowwallet.sparrow.io.WalletExport; import com.sparrowwallet.sparrow.io.WalletExport;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.Control; import javafx.scene.control.Control;
import javafx.scene.control.ToggleButton;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.controlsfx.control.SegmentedButton;
import org.controlsfx.glyphfont.Glyph;
import java.io.File; import java.io.*;
import java.io.FileOutputStream; import java.nio.charset.StandardCharsets;
import java.io.OutputStream;
import java.util.Optional; import java.util.Optional;
public class FileWalletExportPane extends TitledDescriptionPane { public class FileWalletExportPane extends TitledDescriptionPane {
private final Wallet wallet; private final Wallet wallet;
private final WalletExport exporter; private final WalletExport exporter;
private final boolean scannable;
public FileWalletExportPane(Wallet wallet, WalletExport exporter) { public FileWalletExportPane(Wallet wallet, WalletExport exporter) {
super(exporter.getName(), "Wallet file export", exporter.getWalletExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png"); super(exporter.getName(), "Wallet file export", exporter.getWalletExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png");
this.wallet = wallet; this.wallet = wallet;
this.exporter = exporter; this.exporter = exporter;
this.scannable = exporter.isWalletExportScannable();
buttonBox.getChildren().clear();
buttonBox.getChildren().add(createButton());
} }
@Override @Override
protected Control createButton() { protected Control createButton() {
Button exportButton = new Button("Export Wallet..."); if(scannable) {
exportButton.setAlignment(Pos.CENTER_RIGHT); ToggleButton showButton = new ToggleButton("Show...");
exportButton.setOnAction(event -> { Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
exportWallet(); cameraGlyph.setFontSize(12);
}); showButton.setGraphic(cameraGlyph);
return exportButton; 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(); Stage window = new Stage();
FileChooser fileChooser = new FileChooser(); FileChooser fileChooser = new FileChooser();
@ -59,17 +96,18 @@ public class FileWalletExportPane extends TitledDescriptionPane {
WalletPasswordDialog dlg = new WalletPasswordDialog(WalletPasswordDialog.PasswordRequirement.LOAD); WalletPasswordDialog dlg = new WalletPasswordDialog(WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<SecureString> password = dlg.showAndWait(); Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) { if(password.isPresent()) {
final File walletFile = AppServices.get().getOpenWallets().get(wallet).getWalletFile();
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get()); Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get());
decryptWalletService.setOnSucceeded(workerStateEvent -> { 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(); Wallet decryptedWallet = decryptWalletService.getValue();
exportWallet(file, decryptedWallet); exportWallet(file, decryptedWallet);
}); });
decryptWalletService.setOnFailed(workerStateEvent -> { 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()); 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(); decryptWalletService.start();
} }
} else { } else {
@ -79,9 +117,21 @@ public class FileWalletExportPane extends TitledDescriptionPane {
private void exportWallet(File file, Wallet decryptedWallet) { private void exportWallet(File file, Wallet decryptedWallet) {
try { try {
OutputStream outputStream = new FileOutputStream(file); if(file != null) {
exporter.exportWallet(decryptedWallet, outputStream); OutputStream outputStream = new FileOutputStream(file);
EventManager.get().post(new WalletExportEvent(decryptedWallet)); 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) { } catch(Exception e) {
String errorMessage = e.getMessage(); String errorMessage = e.getMessage();
if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) { if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {

View file

@ -43,7 +43,7 @@ public class WalletExportDialog extends Dialog<Wallet> {
if(wallet.getPolicyType() == PolicyType.SINGLE) { if(wallet.getPolicyType() == PolicyType.SINGLE) {
exporters = List.of(new Electrum(), new SpecterDesktop()); exporters = List.of(new Electrum(), new SpecterDesktop());
} else if(wallet.getPolicyType() == PolicyType.MULTI) { } 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 { } else {
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType()); throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
} }

View file

@ -45,7 +45,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
importAccordion.getPanes().add(importPane); importAccordion.getPanes().add(importPane);
} }
List<WalletImport> walletImporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new SpecterDesktop()); List<WalletImport> walletImporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new SpecterDesktop(), new BlueWalletMultisig());
for(WalletImport importer : walletImporters) { for(WalletImport importer : walletImporters) {
FileWalletImportPane importPane = new FileWalletImportPane(importer); FileWalletImportPane importPane = new FileWalletImportPane(importer);
importAccordion.getPanes().add(importPane); importAccordion.getPanes().add(importPane);

View file

@ -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;
}
}

View file

@ -62,4 +62,9 @@ public class CoboVaultMultisig extends ColdcardMultisig {
public boolean isKeystoreImportScannable() { public boolean isKeystoreImportScannable() {
return true; return true;
} }
@Override
public boolean isWalletExportScannable() {
return true;
}
} }

View file

@ -222,4 +222,9 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
public boolean isKeystoreImportScannable() { public boolean isKeystoreImportScannable() {
return false; return false;
} }
@Override
public boolean isWalletExportScannable() {
return false;
}
} }

View file

@ -359,6 +359,11 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
return false; return false;
} }
@Override
public boolean isWalletExportScannable() {
return false;
}
@Override @Override
public String getWalletExportDescription() { public String getWalletExportDescription() {
return "Export this wallet as an Electrum wallet file."; return "Export this wallet as an Electrum wallet file.";

View file

@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.io;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.sparrowwallet.drongo.OutputDescriptor; import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
import com.sparrowwallet.drongo.wallet.InvalidWalletException; import com.sparrowwallet.drongo.wallet.InvalidWalletException;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel; import com.sparrowwallet.drongo.wallet.WalletModel;
@ -19,7 +20,7 @@ public class SpecterDesktop implements WalletImport, WalletExport {
try { try {
SpecterWallet specterWallet = new SpecterWallet(); SpecterWallet specterWallet = new SpecterWallet();
specterWallet.label = wallet.getName(); 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); specterWallet.descriptor = OutputDescriptor.getOutputDescriptor(wallet).toString(true);
Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
@ -83,6 +84,11 @@ public class SpecterDesktop implements WalletImport, WalletExport {
return true; return true;
} }
@Override
public boolean isWalletExportScannable() {
return true;
}
@Override @Override
public String getName() { public String getName() {
return "Specter Desktop"; return "Specter Desktop";

View file

@ -8,4 +8,5 @@ public interface WalletExport extends Export {
void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException; void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException;
String getWalletExportDescription(); String getWalletExportDescription();
String getExportFileExtension(); String getExportFileExtension();
boolean isWalletExportScannable();
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB