bip129 round 2 support (wallet import and export)

This commit is contained in:
Craig Raw 2023-02-23 12:02:06 +02:00
parent 2a7f14a4ed
commit fc5d48de6f
17 changed files with 150 additions and 24 deletions

2
drongo

@ -1 +1 @@
Subproject commit caed93ca6d3e29433c13ed9db705e34e057caf43
Subproject commit d0da764aadfc9edc2a7fb35540eca0ee4c20ba0f

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -416,11 +416,18 @@ public class SettingsController extends WalletFormController implements Initiali
try {
OutputDescriptor editedOutputDescriptor = OutputDescriptor.getOutputDescriptor(text.trim().replace("\\", ""));
Wallet editedWallet = editedOutputDescriptor.toWallet();
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);
@ -429,9 +436,6 @@ public class SettingsController extends WalletFormController implements Initiali
setFieldsFromWallet(editedWallet);
EventManager.get().post(new SettingsChangedEvent(editedWallet, SettingsChangedEvent.Type.POLICY));
} catch(Exception e) {
AppServices.showErrorDialog("Invalid output descriptor", e.getMessage());
}
}
public void showDescriptor(ActionEvent event) {
@ -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();

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB