bip129 round 2 support (wallet import and export)
2
drongo
|
@ -1 +1 @@
|
|||
Subproject commit caed93ca6d3e29433c13ed9db705e34e057caf43
|
||||
Subproject commit d0da764aadfc9edc2a7fb35540eca0ee4c20ba0f
|
|
@ -1073,7 +1073,18 @@ public class AppController implements Initializable {
|
|||
Optional<Wallet> optionalWallet = dlg.showAndWait();
|
||||
if(optionalWallet.isPresent()) {
|
||||
Wallet wallet = optionalWallet.get();
|
||||
if(selectedWalletForms.isEmpty() || wallet != selectedWalletForms.get(0).getWallet()) {
|
||||
|
||||
List<WalletTabData> walletTabData = getOpenWalletTabData();
|
||||
List<ExtendedKey> xpubs = wallet.getKeystores().stream().map(Keystore::getExtendedPublicKey).collect(Collectors.toList());
|
||||
Optional<WalletForm> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -44,7 +44,7 @@ public class WalletExportDialog extends Dialog<Wallet> {
|
|||
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());
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
|||
}
|
||||
}
|
||||
|
||||
List<WalletImport> walletImporters = new ArrayList<>(List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new Descriptor(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow()));
|
||||
List<WalletImport> 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));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<Keystore> importedKeystores = event.getImportedWallet().getKeystores();
|
||||
List<Keystore> nonWatchKeystores = walletForm.getWallet().getKeystores().stream().filter(k -> k.isValid() && k.getSource() != KeystoreSource.SW_WATCH).collect(Collectors.toList());
|
||||
for(Keystore nonWatchKeystore : nonWatchKeystores) {
|
||||
Optional<Keystore> 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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -41,6 +41,7 @@ public class WalletForm {
|
|||
|
||||
private final PublishSubject<WalletNode> refreshNodesSubject;
|
||||
|
||||
private WalletForm settingsWalletForm;
|
||||
private final List<WalletForm> 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<WalletForm> getNestedWalletForms() {
|
||||
return nestedWalletForms;
|
||||
}
|
||||
|
|
BIN
src/main/resources/image/bsms.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
src/main/resources/image/bsms@2x.png
Normal file
After Width: | Height: | Size: 4 KiB |
BIN
src/main/resources/image/bsms@3x.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 6.6 KiB |