diff --git a/drongo b/drongo index caed93ca..d0da764a 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit caed93ca6d3e29433c13ed9db705e34e057caf43 +Subproject commit d0da764aadfc9edc2a7fb35540eca0ee4c20ba0f diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index cca315f8..a558fe57 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -1073,7 +1073,18 @@ public class AppController implements Initializable { Optional optionalWallet = dlg.showAndWait(); if(optionalWallet.isPresent()) { Wallet wallet = optionalWallet.get(); - if(selectedWalletForms.isEmpty() || wallet != selectedWalletForms.get(0).getWallet()) { + + List walletTabData = getOpenWalletTabData(); + List xpubs = wallet.getKeystores().stream().map(Keystore::getExtendedPublicKey).collect(Collectors.toList()); + Optional optNewWalletForm = walletTabData.stream() + .map(WalletTabData::getWalletForm) + .filter(wf -> wf.getSettingsWalletForm() != null && wf.getSettingsWalletForm().getWallet().getPolicyType() == PolicyType.MULTI && + wf.getSettingsWalletForm().getWallet().getScriptType() == wallet.getScriptType() && !wf.getSettingsWalletForm().getWallet().isValid() && + wf.getSettingsWalletForm().getWallet().getKeystores().stream().map(Keystore::getExtendedPublicKey).anyMatch(xpubs::contains)).findFirst(); + if(optNewWalletForm.isPresent()) { + EventManager.get().post(new ExistingWalletImportedEvent(optNewWalletForm.get().getWalletId(), wallet)); + selectTab(optNewWalletForm.get().getWallet()); + } else if(selectedWalletForms.isEmpty() || wallet != selectedWalletForms.get(0).getWallet()) { addImportedWallet(wallet); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java index 9226e555..0504759a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java @@ -92,9 +92,11 @@ public class FileWalletExportPane extends TitledDescriptionPane { FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Export " + exporter.getWalletModel().toDisplayString() + " File"); String extension = exporter.getExportFileExtension(wallet); - String fileName = wallet.getFullName() + "-" + exporter.getWalletModel().toDisplayString().toLowerCase(Locale.ROOT).replace(" ", ""); + String walletModel = exporter.getWalletModel().toDisplayString().toLowerCase(Locale.ROOT).replace(" ", ""); + String postfix = walletModel.equals(extension) ? "" : "-" + walletModel; + String fileName = wallet.getFullName() + postfix; if(exporter.exportsAllWallets()) { - fileName = wallet.getMasterName(); + fileName = wallet.getMasterName() + postfix; } fileChooser.setInitialFileName(fileName + (extension == null || extension.isEmpty() ? "" : "." + extension)); @@ -148,7 +150,7 @@ public class FileWalletExportPane extends TitledDescriptionPane { QRDisplayDialog qrDisplayDialog; if(exporter instanceof CoboVaultMultisig) { qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true); - } else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig || exporter instanceof JadeMultisig) { + } else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig || exporter instanceof JadeMultisig || exporter instanceof Bip129) { qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), false); } else { qrDisplayDialog = new QRDisplayDialog(outputStream.toString(StandardCharsets.UTF_8)); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java index a160e976..d87feb3f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java @@ -44,7 +44,7 @@ public class WalletExportDialog extends Dialog { if(wallet.getPolicyType() == PolicyType.SINGLE) { exporters = List.of(new Electrum(), new ElectrumPersonalServer(), new Descriptor(), new SpecterDesktop(), new Sparrow(), new WalletLabels()); } else if(wallet.getPolicyType() == PolicyType.MULTI) { - exporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new ElectrumPersonalServer(), new KeystoneMultisig(), new Descriptor(), new JadeMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow(), new WalletLabels()); + exporters = List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new ElectrumPersonalServer(), new KeystoneMultisig(), new Descriptor(), new JadeMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow(), new WalletLabels()); } 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 0b18ee5c..ba213cd2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java @@ -59,7 +59,7 @@ public class WalletImportDialog extends Dialog { } } - List walletImporters = new ArrayList<>(List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new Descriptor(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow())); + List walletImporters = new ArrayList<>(List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new Descriptor(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow())); if(!selectedWalletForms.isEmpty()) { walletImporters.add(new WalletLabels(selectedWalletForms)); } diff --git a/src/main/java/com/sparrowwallet/sparrow/event/ExistingWalletImportedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/ExistingWalletImportedEvent.java new file mode 100644 index 00000000..fd5a3639 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/ExistingWalletImportedEvent.java @@ -0,0 +1,21 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.drongo.wallet.Wallet; + +public class ExistingWalletImportedEvent { + private final String existingWalletId; + private final Wallet importedWallet; + + public ExistingWalletImportedEvent(String existingWalletId, Wallet importedWallet) { + this.existingWalletId = existingWalletId; + this.importedWallet = importedWallet; + } + + public String getExistingWalletId() { + return existingWalletId; + } + + public Wallet getImportedWallet() { + return importedWallet; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Bip129.java b/src/main/java/com/sparrowwallet/sparrow/io/Bip129.java index 8317b80e..7f3b9244 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Bip129.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Bip129.java @@ -1,6 +1,7 @@ package com.sparrowwallet.sparrow.io; import com.google.common.io.CharStreams; +import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.OutputDescriptor; import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.crypto.Pbkdf2KeyDeriver; @@ -20,7 +21,7 @@ import java.security.SignatureException; import java.util.Arrays; import java.util.List; -public class Bip129 implements KeystoreFileExport, KeystoreFileImport { +public class Bip129 implements KeystoreFileExport, KeystoreFileImport, WalletExport, WalletImport { @Override public String getName() { return "BSMS"; @@ -28,7 +29,7 @@ public class Bip129 implements KeystoreFileExport, KeystoreFileImport { @Override public WalletModel getWalletModel() { - return WalletModel.SEED; + return WalletModel.BSMS; } @Override @@ -186,4 +187,63 @@ public class Bip129 implements KeystoreFileExport, KeystoreFileImport { public boolean isKeystoreImportScannable() { return true; } + + @Override + public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException { + try { + String record = "BSMS 1.0\n" + + OutputDescriptor.getOutputDescriptor(wallet) + + "\n/0/*,/1/*\n" + + wallet.getNode(KeyPurpose.RECEIVE).getChildren().iterator().next().getAddress(); + outputStream.write(record.getBytes(StandardCharsets.UTF_8)); + } catch(Exception e) { + throw new ExportException("Error exporting BSMS format", e); + } + } + + @Override + public String getWalletExportDescription() { + return "Exports a multisig wallet in the Bitcoin Secure Multisig Setup (BSMS) format for import by other signers in the quorum."; + } + + @Override + public String getExportFileExtension(Wallet wallet) { + return "bsms"; + } + + @Override + public boolean isWalletExportScannable() { + return true; + } + + @Override + public boolean walletExportRequiresDecryption() { + return false; + } + + @Override + public String getWalletImportDescription() { + return "Imports a multisig wallet in the Bitcoin Secure Multisig Setup (BSMS) format that has been created by another signer in the quorum."; + } + + @Override + public Wallet importWallet(InputStream inputStream, String password) throws ImportException { + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + String header = reader.readLine(); + String descriptor = reader.readLine(); + String paths = reader.readLine(); + String address = reader.readLine(); + + OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(descriptor); + return outputDescriptor.toWallet(); + } catch(Exception e) { + throw new ImportException("Error importing BSMS format", e); + } + } + + @Override + public boolean isWalletImportScannable() { + return true; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java index d147c766..d846d21a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java @@ -141,7 +141,7 @@ public class KeystoreController extends WalletFormController implements Initiali displayXpubQR.managedProperty().bind(displayXpubQR.visibleProperty()); displayXpubQR.visibleProperty().bind(scanXpubQR.visibleProperty().not()); - updateType(false); + updateType(keystore.isValid() && !getWalletForm().getWallet().isValid()); label.setText(keystore.getLabel()); @@ -354,7 +354,7 @@ public class KeystoreController extends WalletFormController implements Initiali } private void launchImportDialog(KeystoreSource initialSource) { - boolean restrictSource = keystoreSourceToggleGroup.getToggles().stream().anyMatch(toggle -> ((ToggleButton)toggle).isDisabled()); + boolean restrictSource = keystore.getSource() != KeystoreSource.SW_WATCH && keystoreSourceToggleGroup.getToggles().stream().anyMatch(toggle -> ((ToggleButton)toggle).isDisabled()); KeyDerivation requiredDerivation = restrictSource ? keystore.getKeyDerivation() : null; WalletModel requiredModel = restrictSource ? keystore.getWalletModel() : null; KeystoreImportDialog dlg = new KeystoreImportDialog(getWalletForm().getWallet(), initialSource, requiredDerivation, requiredModel, restrictSource); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java index 47e12410..f114f2e3 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java @@ -416,24 +416,28 @@ public class SettingsController extends WalletFormController implements Initiali try { OutputDescriptor editedOutputDescriptor = OutputDescriptor.getOutputDescriptor(text.trim().replace("\\", "")); Wallet editedWallet = editedOutputDescriptor.toWallet(); - - editedWallet.setName(getWalletForm().getWallet().getName()); - editedWallet.setBirthDate(getWalletForm().getWallet().getBirthDate()); - editedWallet.setGapLimit(getWalletForm().getWallet().getGapLimit()); - editedWallet.setWatchLast(getWalletForm().getWallet().getWatchLast()); - keystoreTabs.getTabs().removeAll(keystoreTabs.getTabs()); - totalKeystores.unbind(); - totalKeystores.setValue(0); - walletForm.setWallet(editedWallet); - initialising = true; - setFieldsFromWallet(editedWallet); - - EventManager.get().post(new SettingsChangedEvent(editedWallet, SettingsChangedEvent.Type.POLICY)); + replaceWallet(editedWallet); } catch(Exception e) { AppServices.showErrorDialog("Invalid output descriptor", e.getMessage()); } } + private void replaceWallet(Wallet editedWallet) { + editedWallet.setName(getWalletForm().getWallet().getName()); + editedWallet.setBirthDate(getWalletForm().getWallet().getBirthDate()); + editedWallet.setGapLimit(getWalletForm().getWallet().getGapLimit()); + editedWallet.setWatchLast(getWalletForm().getWallet().getWatchLast()); + editedWallet.setMasterWallet(getWalletForm().getWallet().getMasterWallet()); + keystoreTabs.getTabs().removeAll(keystoreTabs.getTabs()); + totalKeystores.unbind(); + totalKeystores.setValue(0); + walletForm.setWallet(editedWallet); + initialising = true; + setFieldsFromWallet(editedWallet); + + EventManager.get().post(new SettingsChangedEvent(editedWallet, SettingsChangedEvent.Type.POLICY)); + } + public void showDescriptor(ActionEvent event) { OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(walletForm.getWallet(), KeyPurpose.DEFAULT_PURPOSES, null); String outputDescriptorString = outputDescriptor.toString(walletForm.getWallet().isValid()); @@ -726,6 +730,24 @@ public class SettingsController extends WalletFormController implements Initiali } } + @Subscribe + public void existingWalletImported(ExistingWalletImportedEvent event) { + if(event.getExistingWalletId().equals(getWalletForm().getWalletId())) { + List importedKeystores = event.getImportedWallet().getKeystores(); + List nonWatchKeystores = walletForm.getWallet().getKeystores().stream().filter(k -> k.isValid() && k.getSource() != KeystoreSource.SW_WATCH).collect(Collectors.toList()); + for(Keystore nonWatchKeystore : nonWatchKeystores) { + Optional optReplacedKeystore = importedKeystores.stream().filter(k -> nonWatchKeystore.getExtendedPublicKey().equals(k.getExtendedPublicKey())).findFirst(); + if(optReplacedKeystore.isPresent()) { + int index = importedKeystores.indexOf(optReplacedKeystore.get()); + importedKeystores.remove(index); + importedKeystores.add(index, nonWatchKeystore); + } + } + + replaceWallet(event.getImportedWallet()); + } + } + private void saveWallet(boolean changePassword, boolean suggestChangePassword) { ECKey existingPubKey = walletForm.getStorage().getEncryptionPubKey(); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java index 0c8297a6..a5a891ad 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java @@ -103,6 +103,7 @@ public class WalletController extends WalletFormController implements Initializa WalletForm walletForm = getWalletForm(); if(function.equals(Function.SETTINGS)) { walletForm = new SettingsWalletForm(getWalletForm().getStorage(), getWalletForm().getWallet()); + getWalletForm().setSettingsWalletForm(walletForm); } controller.setWalletForm(walletForm); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java index b6d30980..6510d633 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java @@ -41,6 +41,7 @@ public class WalletForm { private final PublishSubject refreshNodesSubject; + private WalletForm settingsWalletForm; private final List nestedWalletForms = new ArrayList<>(); private WalletTransactionsEntry walletTransactionsEntry; @@ -96,6 +97,14 @@ public class WalletForm { throw new UnsupportedOperationException("Only SettingsWalletForm supports setWallet"); } + public WalletForm getSettingsWalletForm() { + return settingsWalletForm; + } + + void setSettingsWalletForm(WalletForm settingsWalletForm) { + this.settingsWalletForm = settingsWalletForm; + } + public List getNestedWalletForms() { return nestedWalletForms; } diff --git a/src/main/resources/image/bsms.png b/src/main/resources/image/bsms.png new file mode 100644 index 00000000..999f237f Binary files /dev/null and b/src/main/resources/image/bsms.png differ diff --git a/src/main/resources/image/bsms@2x.png b/src/main/resources/image/bsms@2x.png new file mode 100644 index 00000000..899753e0 Binary files /dev/null and b/src/main/resources/image/bsms@2x.png differ diff --git a/src/main/resources/image/bsms@3x.png b/src/main/resources/image/bsms@3x.png new file mode 100644 index 00000000..4aa9cd60 Binary files /dev/null and b/src/main/resources/image/bsms@3x.png differ diff --git a/src/main/resources/image/seed.png b/src/main/resources/image/seed.png index 12392122..ce3709a1 100644 Binary files a/src/main/resources/image/seed.png and b/src/main/resources/image/seed.png differ diff --git a/src/main/resources/image/seed@2x.png b/src/main/resources/image/seed@2x.png index c540bf27..e0521b7a 100644 Binary files a/src/main/resources/image/seed@2x.png and b/src/main/resources/image/seed@2x.png differ diff --git a/src/main/resources/image/seed@3x.png b/src/main/resources/image/seed@3x.png index 589361f1..03e4471c 100644 Binary files a/src/main/resources/image/seed@3x.png and b/src/main/resources/image/seed@3x.png differ