diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java index befe1983..6e5afda8 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java @@ -305,15 +305,34 @@ public class DevicePane extends TitledDescriptionPane { if(importButton instanceof SplitMenuButton importMenuButton) { if(wallet.getScriptType() == null) { - ScriptType[] scriptTypes = new ScriptType[] {ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH, ScriptType.P2TR}; - for(ScriptType scriptType : scriptTypes) { - MenuItem item = new MenuItem(scriptType.getDescription()); - final List derivation = scriptType.getDefaultDerivation(); - item.setOnAction(event -> { - importMenuButton.setDisable(true); - importKeystore(derivation); - }); - importMenuButton.getItems().add(item); + if(wallet.getPolicyType() == null) { + List types = new ArrayList<>(); + for(PolicyType policyType : List.of(PolicyType.SINGLE_HD, PolicyType.SINGLE_SP)) { + for(ScriptType scriptType : ScriptType.getAddressableScriptTypes(policyType)) { + types.add(new PolicyAndScriptType(policyType, scriptType)); + } + } + for(PolicyAndScriptType type : types) { + MenuItem item = new MenuItem(type.getDescription()); + final List derivation = type.scriptType().getDefaultDerivation(); + item.setOnAction(event -> { + importMenuButton.setDisable(true); + wallet.setPolicyType(type.policyType()); + importKeystore(derivation); + }); + importMenuButton.getItems().add(item); + } + } else { + List scriptTypes = ScriptType.getScriptTypesForPolicyType(wallet.getPolicyType()); + for(ScriptType scriptType : scriptTypes) { + MenuItem item = new MenuItem(scriptType.getDescription()); + final List derivation = scriptType.getDefaultDerivation(); + item.setOnAction(event -> { + importMenuButton.setDisable(true); + importKeystore(derivation); + }); + importMenuButton.getItems().add(item); + } } importMenuButton.getItems().add(new SeparatorMenuItem()); MenuItem discoverItem = new MenuItem("Discover Wallet..."); @@ -811,12 +830,13 @@ public class DevicePane extends TitledDescriptionPane { private void importKeystore(List derivation, Keystore keystore) { if(wallet.getScriptType() == null) { - ScriptType scriptType = Arrays.stream(ScriptType.ADDRESSABLE_TYPES).filter(type -> type.getDefaultDerivation().get(0).equals(derivation.get(0))).findFirst().orElse(ScriptType.P2PKH); + ScriptType scriptType = Arrays.stream(ScriptType.ADDRESSABLE_TYPES).filter(type -> type.getDefaultDerivation().getFirst().equals(derivation.getFirst())).findFirst().orElse(ScriptType.P2PKH); + PolicyType policyType = wallet.getPolicyType() != null ? wallet.getPolicyType() : PolicyType.SINGLE_HD; wallet.setName(device.getModel().toDisplayString()); - wallet.setPolicyType(PolicyType.SINGLE_HD); + wallet.setPolicyType(policyType); wallet.setScriptType(scriptType); wallet.getKeystores().add(keystore); - wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, scriptType, wallet.getKeystores(), null)); + wallet.setDefaultPolicy(Policy.getPolicy(policyType, scriptType, wallet.getKeystores(), null)); EventManager.get().post(new WalletImportEvent(wallet)); } else { @@ -1020,7 +1040,7 @@ public class DevicePane extends TitledDescriptionPane { AppServices.showErrorDialog("No existing wallet found", Config.get().getServerType() == ServerType.BITCOIN_CORE ? "The configured server type is Bitcoin Core, which does not support wallet discovery.\n\n" + "You can however import the " + device.getModel().toDisplayString() + " and scan the blockchain by supplying a start date." : - "Could not find a wallet with existing transactions using the " + device.getModel().toDisplayString() + "."); + "Could not find an HD wallet with existing transactions using the " + device.getModel().toDisplayString() + "."); setDefaultStatus(); importButton.setDisable(false); } @@ -1393,4 +1413,10 @@ public class DevicePane extends TitledDescriptionPane { public enum DeviceOperation { IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE, DISCOVER_KEYSTORES, GET_PRIVATE_KEY, GET_ADDRESS; } + + protected record PolicyAndScriptType(PolicyType policyType, ScriptType scriptType) { + public String getDescription() { + return scriptType.getDescription() + (policyType == PolicyType.SINGLE_SP ? " SP" : " HD"); + } + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/FileWalletKeystoreImportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/FileWalletKeystoreImportPane.java index f8732290..b938875b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/FileWalletKeystoreImportPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/FileWalletKeystoreImportPane.java @@ -30,8 +30,8 @@ import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; public class FileWalletKeystoreImportPane extends FileImportPane { private static final Logger log = LoggerFactory.getLogger(FileWalletKeystoreImportPane.class); @@ -50,19 +50,27 @@ public class FileWalletKeystoreImportPane extends FileImportPane { this.fileName = fileName; this.password = password; - List scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE_HD); + List types = new ArrayList<>(); + for(PolicyType policyType : List.of(PolicyType.SINGLE_HD, PolicyType.SINGLE_SP)) { + for(ScriptType scriptType : ScriptType.getAddressableScriptTypes(policyType)) { + types.add(new PolicyAndScriptType(policyType, scriptType)); + } + } + if(wallets != null && !wallets.isEmpty()) { - if(wallets.size() == 1 && scriptTypes.contains(wallets.get(0).getScriptType())) { - Wallet wallet = wallets.get(0); - wallet.setPolicyType(PolicyType.SINGLE_HD); - wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, wallet.getScriptType(), wallet.getKeystores(), null)); + wallets.stream().filter(w -> w.getPolicyType() == null).forEach(w -> w.setPolicyType(PolicyType.SINGLE_HD)); + List walletTypes = wallets.stream().map(w -> new PolicyAndScriptType(w.getPolicyType(), w.getScriptType())).toList(); + types.retainAll(walletTypes); + if(types.isEmpty()) { + throw new ImportException("No singlesig script types present in QR code"); + } + + if(types.size() == 1) { + Wallet wallet = wallets.stream().filter(w -> w.getPolicyType() == types.getFirst().policyType() && w.getScriptType() == types.getFirst().scriptType()).findFirst().orElseThrow(ImportException::new); + wallet.setDefaultPolicy(Policy.getPolicy(wallet.getPolicyType(), wallet.getScriptType(), wallet.getKeystores(), null)); wallet.setName(importer.getName()); - EventManager.get().post(new WalletImportEvent(wallets.get(0))); - } else { - scriptTypes.retainAll(wallets.stream().map(Wallet::getScriptType).collect(Collectors.toList())); - if(scriptTypes.isEmpty()) { - throw new ImportException("No singlesig script types present in QR code"); - } + EventManager.get().post(new WalletImportEvent(wallet)); + return; } } else { try { @@ -72,58 +80,61 @@ public class FileWalletKeystoreImportPane extends FileImportPane { } } - setContent(getScriptTypeEntry(scriptTypes)); + setContent(getScriptTypeEntry(types)); setExpanded(true); importButton.setDisable(true); } - private void importWallet(ScriptType scriptType) throws ImportException { + private void importWallet(PolicyAndScriptType type) throws ImportException { + PolicyType policyType = type.policyType(); + ScriptType scriptType = type.scriptType(); + if(wallets != null && !wallets.isEmpty()) { - Wallet wallet = wallets.stream().filter(wallet1 -> wallet1.getScriptType() == scriptType).findFirst().orElseThrow(ImportException::new); + Wallet wallet = wallets.stream().filter(w -> w.getPolicyType() == policyType && w.getScriptType() == scriptType).findFirst().orElseThrow(ImportException::new); wallet.setName(importer.getName()); - wallet.setPolicyType(PolicyType.SINGLE_HD); - wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, wallet.getScriptType(), wallet.getKeystores(), null)); + wallet.setDefaultPolicy(Policy.getPolicy(policyType, scriptType, wallet.getKeystores(), null)); EventManager.get().post(new WalletImportEvent(wallet)); } else { ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes); - Keystore keystore = importer.getKeystore(PolicyType.SINGLE_HD, scriptType, bais, password); + Keystore keystore = importer.getKeystore(policyType, scriptType, bais, password); Wallet wallet = new Wallet(); wallet.setName(Files.getNameWithoutExtension(fileName)); - wallet.setPolicyType(PolicyType.SINGLE_HD); + wallet.setPolicyType(policyType); wallet.setScriptType(scriptType); wallet.getKeystores().add(keystore); - wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, scriptType, wallet.getKeystores(), null)); + wallet.setDefaultPolicy(Policy.getPolicy(policyType, scriptType, wallet.getKeystores(), null)); EventManager.get().post(new WalletImportEvent(wallet)); } } - private Node getScriptTypeEntry(List scriptTypes) { - Label label = new Label("Script Type:"); + private Node getScriptTypeEntry(List types) { + Label label = new Label("Type:"); HBox fieldBox = new HBox(5); fieldBox.setAlignment(Pos.CENTER_RIGHT); - ComboBox scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(scriptTypes)); - if(scriptTypes.contains(ScriptType.P2WPKH)) { - scriptTypeComboBox.setValue(ScriptType.P2WPKH); + ComboBox comboBox = new ComboBox<>(FXCollections.observableArrayList(types)); + PolicyAndScriptType defaultType = new PolicyAndScriptType(PolicyType.SINGLE_HD, ScriptType.P2WPKH); + if(types.contains(defaultType)) { + comboBox.setValue(defaultType); } - scriptTypeComboBox.setConverter(new StringConverter<>() { + comboBox.setConverter(new StringConverter<>() { @Override - public String toString(ScriptType scriptType) { - return scriptType == null ? "" : scriptType.getDescription(); + public String toString(PolicyAndScriptType type) { + return type == null ? "" : type.getDescription(); } @Override - public ScriptType fromString(String string) { + public PolicyAndScriptType fromString(String string) { return null; } }); - scriptTypeComboBox.setMaxWidth(170); + comboBox.setMaxWidth(220); HelpLabel helpLabel = new HelpLabel(); - helpLabel.setHelpText("P2WPKH is a Native Segwit type and is usually the best choice for new wallets.\nP2SH-P2WPKH is a Wrapped Segwit type and is a reasonable choice for the widest compatibility.\nP2PKH is a Legacy type and should be avoided for new wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing."); - fieldBox.getChildren().addAll(scriptTypeComboBox, helpLabel); + helpLabel.setHelpText("Native Segwit is usually the best choice for new wallets.\nTaproot is newer and supports both HD and SP (silent payments) wallets.\nNested Segwit and Legacy are useful for recovering older wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing."); + fieldBox.getChildren().addAll(comboBox, helpLabel); Region region = new Region(); HBox.setHgrow(region, Priority.SOMETIMES); @@ -133,7 +144,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane { showHideLink.setVisible(true); setExpanded(false); try { - importWallet(scriptTypeComboBox.getValue()); + importWallet(comboBox.getValue()); } catch(ImportException e) { log.error("Error importing file", e); String errorMessage = e.getMessage(); @@ -154,8 +165,14 @@ public class FileWalletKeystoreImportPane extends FileImportPane { contentBox.setPadding(new Insets(10, 30, 10, 30)); contentBox.setPrefHeight(60); - Platform.runLater(scriptTypeComboBox::requestFocus); + Platform.runLater(comboBox::requestFocus); return contentBox; } + + protected record PolicyAndScriptType(PolicyType policyType, ScriptType scriptType) { + public String getDescription() { + return scriptType.getDescription() + (policyType == PolicyType.SINGLE_SP ? " SP" : " HD"); + } + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MnemonicWalletKeystoreImportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicWalletKeystoreImportPane.java index bdb8cf7b..8426d2b1 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/MnemonicWalletKeystoreImportPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicWalletKeystoreImportPane.java @@ -115,7 +115,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane { for(ScriptType scriptType : ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE_HD)) { for(List derivation : derivations) { try { - Wallet wallet = getWallet(scriptType, derivation); + Wallet wallet = getWallet(PolicyType.SINGLE_HD, scriptType, derivation); wallets.add(wallet); } catch(ImportException e) { String errorMessage = e.getMessage(); @@ -148,7 +148,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane { Optional optButtonType = AppServices.showErrorDialog("No existing wallet found", Config.get().getServerType() == ServerType.BITCOIN_CORE ? "The configured server type is Bitcoin Core, which does not support wallet discovery.\n\n" + "You can however import this wallet and scan the blockchain by supplying a start date. Do you want to import this wallet?" : - "Could not find a wallet with existing transactions using this mnemonic. Import this wallet anyway?", ButtonType.NO, ButtonType.YES); + "Could not find an HD wallet with existing transactions using this mnemonic. Import this wallet anyway?", ButtonType.NO, ButtonType.YES); if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) { setContent(getScriptTypeEntry()); setExpanded(true); @@ -163,41 +163,49 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane { walletDiscoveryService.start(); } - private Wallet getWallet(ScriptType scriptType, List derivation) throws ImportException { + private Wallet getWallet(PolicyType policyType, ScriptType scriptType, List derivation) throws ImportException { Wallet wallet = new Wallet(""); - wallet.setPolicyType(PolicyType.SINGLE_HD); + wallet.setPolicyType(policyType); wallet.setScriptType(scriptType); - Keystore keystore = importer.getKeystore(PolicyType.SINGLE_HD, derivation, wordEntriesProperty.get(), passphraseProperty.get()); + Keystore keystore = importer.getKeystore(policyType, derivation, wordEntriesProperty.get(), passphraseProperty.get()); wallet.getKeystores().add(keystore); - wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, scriptType, wallet.getKeystores(), 1)); + wallet.setDefaultPolicy(Policy.getPolicy(policyType, scriptType, wallet.getKeystores(), 1)); return wallet; } private Node getScriptTypeEntry() { - Label label = new Label("Script Type:"); + Label label = new Label("Type:"); + + List types = new ArrayList<>(); + for(PolicyType policyType : List.of(PolicyType.SINGLE_HD, PolicyType.SINGLE_SP)) { + for(ScriptType scriptType : ScriptType.getAddressableScriptTypes(policyType)) { + types.add(new PolicyAndScriptType(policyType, scriptType)); + } + } HBox fieldBox = new HBox(5); fieldBox.setAlignment(Pos.CENTER_RIGHT); - ComboBox scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE_HD))); - if(scriptTypeComboBox.getItems().contains(ScriptType.P2WPKH)) { - scriptTypeComboBox.setValue(ScriptType.P2WPKH); + ComboBox comboBox = new ComboBox<>(FXCollections.observableArrayList(types)); + PolicyAndScriptType defaultType = new PolicyAndScriptType(PolicyType.SINGLE_HD, ScriptType.P2WPKH); + if(types.contains(defaultType)) { + comboBox.setValue(defaultType); } - scriptTypeComboBox.setConverter(new StringConverter<>() { + comboBox.setConverter(new StringConverter<>() { @Override - public String toString(ScriptType scriptType) { - return scriptType == null ? "" : scriptType.getDescription(); + public String toString(PolicyAndScriptType type) { + return type == null ? "" : type.getDescription(); } @Override - public ScriptType fromString(String string) { + public PolicyAndScriptType fromString(String string) { return null; } }); - scriptTypeComboBox.setMaxWidth(170); + comboBox.setMaxWidth(220); HelpLabel helpLabel = new HelpLabel(); - helpLabel.setHelpText("Native Segwit is usually the best choice for new wallets.\nTaproot is a new type useful for specific needs.\nNested Segwit and Legacy are useful for recovering older wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing."); - fieldBox.getChildren().addAll(scriptTypeComboBox, helpLabel); + helpLabel.setHelpText("Native Segwit is usually the best choice for new wallets.\nTaproot is a new type useful for specific needs.\nTaproot Silent Payments creates a silent payment wallet.\nNested Segwit and Legacy are useful for recovering older wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing."); + fieldBox.getChildren().addAll(comboBox, helpLabel); Region region = new Region(); HBox.setHgrow(region, Priority.SOMETIMES); @@ -208,8 +216,8 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane { showHideLink.setVisible(true); setExpanded(false); try { - ScriptType scriptType = scriptTypeComboBox.getValue(); - Wallet wallet = getWallet(scriptType, scriptType.getDefaultDerivation()); + PolicyAndScriptType type = comboBox.getValue(); + Wallet wallet = getWallet(type.policyType(), type.scriptType(), type.scriptType().getDefaultDerivation()); EventManager.get().post(new WalletImportEvent(wallet)); } catch(ImportException e) { log.error("Error importing mnemonic", e); @@ -231,4 +239,10 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane { return contentBox; } + + protected record PolicyAndScriptType(PolicyType policyType, ScriptType scriptType) { + public String getDescription() { + return scriptType.getDescription() + (policyType == PolicyType.SINGLE_SP ? " SP" : " HD"); + } + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/CoboVaultSinglesig.java b/src/main/java/com/sparrowwallet/sparrow/io/CoboVaultSinglesig.java index ad1ea3f5..5819c431 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/CoboVaultSinglesig.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/CoboVaultSinglesig.java @@ -36,6 +36,10 @@ public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport { @Override public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException { + if(policyType == PolicyType.SINGLE_SP) { + throw new ImportException(getName() + " does not support receiving silent payments"); + } + try { Gson gson = new Gson(); CoboVaultSinglesigKeystore coboKeystore = gson.fromJson(new InputStreamReader(inputStream, StandardCharsets.UTF_8), CoboVaultSinglesigKeystore.class); diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ColdcardSinglesig.java b/src/main/java/com/sparrowwallet/sparrow/io/ColdcardSinglesig.java index af639734..d17e16d0 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ColdcardSinglesig.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ColdcardSinglesig.java @@ -62,8 +62,8 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport { }.getType(); Map map = gson.fromJson(new InputStreamReader(inputStream, StandardCharsets.UTF_8), stringStringMap); - if (map.get("xfp") == null) { - throw new ImportException("File was not a valid " + getName() + " wallet export"); + if(map.get("xfp") == null) { + throw new ImportException("Export was not a valid " + getName() + " wallet export"); } String masterFingerprint = map.get("xfp").getAsString(); @@ -71,7 +71,7 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport { if(policyType == PolicyType.SINGLE_SP) { JsonElement bip352Element = map.get("bip352"); if(bip352Element == null) { - throw new ImportException("File does not contain an export for silent payments"); + throw new ImportException("Export does not contain the spscan value for silent payments"); } ColdcardKeystore ck = gson.fromJson(bip352Element, ColdcardKeystore.class); @@ -84,8 +84,8 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport { return keystore; } - for (String key : map.keySet()) { - if (key.startsWith("bip")) { + for(String key : map.keySet()) { + if(key.startsWith("bip")) { ColdcardKeystore ck = gson.fromJson(map.get(key), ColdcardKeystore.class); if(ck.name != null) { @@ -103,7 +103,7 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport { } } } - } catch (Exception e) { + } catch(Exception e) { throw new ImportException("Error getting " + getName() + " keystore", e); } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/KeystoneSinglesig.java b/src/main/java/com/sparrowwallet/sparrow/io/KeystoneSinglesig.java index c47e8be4..485ccb02 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/KeystoneSinglesig.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/KeystoneSinglesig.java @@ -45,24 +45,22 @@ public class KeystoneSinglesig implements KeystoreFileImport, WalletImport { throw new IllegalArgumentException("Output descriptor describes a multisig wallet"); } + if(policyType == PolicyType.SINGLE_SP && !descriptor.isSilentPayments()) { + throw new IllegalArgumentException("Export does not contain the spscan value for silent payments"); + } + if(descriptor.getScriptType() != scriptType) { throw new IllegalArgumentException("Output descriptor describes a " + descriptor.getScriptType().getDescription() + " wallet"); } - ExtendedKey xpub = descriptor.getSingletonExtendedPublicKey(); - KeyDerivation keyDerivation = descriptor.getKeyDerivation(xpub); - - Keystore keystore = new Keystore(); + Wallet wallet = descriptor.toWallet(); + Keystore keystore = wallet.getKeystores().getFirst(); keystore.setLabel(getName()); keystore.setSource(KeystoreSource.HW_AIRGAPPED); keystore.setWalletModel(getWalletModel()); - keystore.setKeyDerivation(keyDerivation); - keystore.setExtendedPublicKey(xpub); return keystore; - } catch (IllegalArgumentException e) { - throw new ImportException("Error getting " + getName() + " keystore - not an output descriptor", e); - } catch (Exception e) { + } catch(Exception e) { throw new ImportException("Error getting " + getName() + " keystore", e); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Samourai.java b/src/main/java/com/sparrowwallet/sparrow/io/Samourai.java index 6b7e2a9e..8742c0e7 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Samourai.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Samourai.java @@ -27,6 +27,10 @@ public class Samourai implements KeystoreFileImport { @Override public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException { + if(policyType == PolicyType.SINGLE_SP) { + throw new ImportException(getName() + " does not support receiving silent payments"); + } + try { String input = CharStreams.toString(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); diff --git a/src/main/java/com/sparrowwallet/sparrow/io/SpecterDIY.java b/src/main/java/com/sparrowwallet/sparrow/io/SpecterDIY.java index ead4e5da..30db6b38 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/SpecterDIY.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/SpecterDIY.java @@ -21,7 +21,7 @@ public class SpecterDIY implements KeystoreFileImport, WalletExport { public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException { try { String text = CharStreams.toString(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); - String outputDesc = "sh(" + text + ")"; + String outputDesc = policyType == PolicyType.SINGLE_SP ? "sp(" + text + ")" : "sh(" + text + ")"; OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(outputDesc); Wallet wallet = outputDescriptor.toWallet(); diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/Bip39Dialog.java b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/Bip39Dialog.java index a1826c5c..610bd1a6 100644 --- a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/Bip39Dialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/Bip39Dialog.java @@ -28,7 +28,7 @@ public class Bip39Dialog extends NewWalletDialog { private final Bip39 importer = new Bip39(); - private final ComboBox scriptType; + private final ComboBox scriptType; private final TextBox seedWords; private final TextBox passphrase; private final Button createWallet; @@ -68,13 +68,17 @@ public class Bip39Dialog extends NewWalletDialog { buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER,false,false)).addTo(mainPanel); setComponent(mainPanel); - ScriptType.getAddressableScriptTypes(PolicyType.SINGLE_HD).stream().map(DisplayScriptType::new).forEach(scriptType::addItem); - scriptType.setSelectedItem(new DisplayScriptType(ScriptType.P2WPKH)); + for(PolicyType policyType : List.of(PolicyType.SINGLE_HD, PolicyType.SINGLE_SP)) { + for(ScriptType scriptType : ScriptType.getAddressableScriptTypes(policyType)) { + this.scriptType.addItem(new PolicyAndScriptType(policyType, scriptType)); + } + } + scriptType.setSelectedItem(new PolicyAndScriptType(PolicyType.SINGLE_HD, ScriptType.P2WPKH)); seedWords.setTextChangeListener((newText, changedByUserInteraction) -> { try { String[] words = newText.split("[ \n]"); - importer.getKeystore(PolicyType.SINGLE_HD, scriptType.getSelectedItem().scriptType.getDefaultDerivation(), Arrays.asList(words), passphrase.getText()); + importer.getKeystore(PolicyType.SINGLE_HD, scriptType.getSelectedItem().scriptType().getDefaultDerivation(), Arrays.asList(words), passphrase.getText()); createWallet.setEnabled(true); } catch(ImportException e) { createWallet.setEnabled(false); @@ -149,44 +153,20 @@ public class Bip39Dialog extends NewWalletDialog { @Override protected List getWallets() throws ImportException { + PolicyAndScriptType type = scriptType.getSelectedItem(); Wallet wallet = new Wallet(walletName); - wallet.setPolicyType(PolicyType.SINGLE_HD); - wallet.setScriptType(scriptType.getSelectedItem().scriptType); - Keystore keystore = importer.getKeystore(PolicyType.SINGLE_HD, wallet.getScriptType().getDefaultDerivation(), getWords(), passphrase.getText()); + wallet.setPolicyType(type.policyType()); + wallet.setScriptType(type.scriptType()); + Keystore keystore = importer.getKeystore(type.policyType(), wallet.getScriptType().getDefaultDerivation(), getWords(), passphrase.getText()); wallet.getKeystores().add(keystore); - wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, wallet.getScriptType(), wallet.getKeystores(), 1)); + wallet.setDefaultPolicy(Policy.getPolicy(type.policyType(), wallet.getScriptType(), wallet.getKeystores(), 1)); return List.of(wallet); } - private static final class DisplayScriptType { - private final ScriptType scriptType; - - public DisplayScriptType(ScriptType scriptType) { - this.scriptType = scriptType; - } - + private record PolicyAndScriptType(PolicyType policyType, ScriptType scriptType) { @Override public String toString() { - return scriptType.getDescription(); - } - - @Override - public boolean equals(Object o) { - if(this == o) { - return true; - } - if(o == null || getClass() != o.getClass()) { - return false; - } - - DisplayScriptType that = (DisplayScriptType) o; - - return scriptType == that.scriptType; - } - - @Override - public int hashCode() { - return scriptType.hashCode(); + return scriptType.getDescription() + (policyType == PolicyType.SINGLE_SP ? " SP" : " HD"); } } diff --git a/src/test/java/com/sparrowwallet/sparrow/io/KeystoneSinglesigTest.java b/src/test/java/com/sparrowwallet/sparrow/io/KeystoneSinglesigTest.java index 55cd0897..8cf878bb 100644 --- a/src/test/java/com/sparrowwallet/sparrow/io/KeystoneSinglesigTest.java +++ b/src/test/java/com/sparrowwallet/sparrow/io/KeystoneSinglesigTest.java @@ -1,9 +1,12 @@ package com.sparrowwallet.sparrow.io; import com.sparrowwallet.drongo.ExtendedKey; +import com.sparrowwallet.drongo.Network; import com.sparrowwallet.drongo.policy.PolicyType; import com.sparrowwallet.drongo.protocol.ScriptType; +import com.sparrowwallet.drongo.silentpayments.SilentPaymentScanAddress; import com.sparrowwallet.drongo.wallet.Keystore; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -25,4 +28,25 @@ public class KeystoneSinglesigTest extends IoTest { KeystoneSinglesig keystoneSingleSig = new KeystoneSinglesig(); Assertions.assertThrows(ImportException.class, () -> keystoneSingleSig.getKeystore(PolicyType.SINGLE_HD, ScriptType.P2SH_P2WPKH, getInputStream("keystone-singlesig-keystore-1.txt"), null)); } + + @Test + public void testImportSilentPayments() throws ImportException { + Network.set(Network.TESTNET); + KeystoneSinglesig keystoneSingleSig = new KeystoneSinglesig(); + Keystore keystore = keystoneSingleSig.getKeystore(PolicyType.SINGLE_SP, ScriptType.P2TR, getInputStream("keystone-singlesig-sp-keystore-1.txt"), null); + + Assertions.assertEquals("Keystone", keystore.getLabel()); + Assertions.assertEquals("m/352'/1'/0'", keystore.getKeyDerivation().getDerivationPath()); + Assertions.assertEquals("0f056943", keystore.getKeyDerivation().getMasterFingerprint()); + Assertions.assertNull(keystore.getExtendedPublicKey()); + Assertions.assertNotNull(keystore.getSilentPaymentScanAddress()); + Assertions.assertEquals(SilentPaymentScanAddress.fromKeyString("tspscan1q05wxw5wc7wqmkf8cnfc6ry76qej8vhr3a3mmxmwgv35s0tlw24fs82k0npv2hv6p97s8sd9t7vpf44kluka9w863zjwxzfrym2ay9ccfzt06c4"), + keystore.getSilentPaymentScanAddress()); + Assertions.assertTrue(keystore.isValid()); + } + + @AfterEach + public void tearDown() throws Exception { + Network.set(null); + } } diff --git a/src/test/java/com/sparrowwallet/sparrow/io/SpecterDIYTest.java b/src/test/java/com/sparrowwallet/sparrow/io/SpecterDIYTest.java index 7afeeff2..5fcc1f02 100644 --- a/src/test/java/com/sparrowwallet/sparrow/io/SpecterDIYTest.java +++ b/src/test/java/com/sparrowwallet/sparrow/io/SpecterDIYTest.java @@ -6,6 +6,7 @@ import com.sparrowwallet.drongo.Network; import com.sparrowwallet.drongo.OutputDescriptor; import com.sparrowwallet.drongo.policy.PolicyType; import com.sparrowwallet.drongo.protocol.ScriptType; +import com.sparrowwallet.drongo.silentpayments.SilentPaymentScanAddress; import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Wallet; import org.junit.jupiter.api.Assertions; @@ -29,6 +30,23 @@ public class SpecterDIYTest extends IoTest { Network.set(Network.MAINNET); } + @Test + public void testImportSilentPayments() throws ImportException { + Network.set(Network.TESTNET); + SpecterDIY specterDIY = new SpecterDIY(); + Keystore keystore = specterDIY.getKeystore(PolicyType.SINGLE_SP, ScriptType.P2TR, getInputStream("specter-diy-sp-keystore.txt"), null); + + Assertions.assertEquals("Specter DIY", keystore.getLabel()); + Assertions.assertEquals("m/352'/1'/0'", keystore.getKeyDerivation().getDerivationPath()); + Assertions.assertEquals("0f056943", keystore.getKeyDerivation().getMasterFingerprint()); + Assertions.assertNull(keystore.getExtendedPublicKey()); + Assertions.assertNotNull(keystore.getSilentPaymentScanAddress()); + Assertions.assertEquals(SilentPaymentScanAddress.fromKeyString("tspscan1q05wxw5wc7wqmkf8cnfc6ry76qej8vhr3a3mmxmwgv35s0tlw24fs82k0npv2hv6p97s8sd9t7vpf44kluka9w863zjwxzfrym2ay9ccfzt06c4"), + keystore.getSilentPaymentScanAddress()); + Assertions.assertTrue(keystore.isValid()); + Network.set(Network.MAINNET); + } + @Test public void testExport() throws ExportException, IOException { OutputDescriptor walletDescriptor = OutputDescriptor.getOutputDescriptor("wsh(sortedmulti(2,[7fd1bbf4/48h/0h/0h/2h]xpub6DnVFCXjZKhSAJw1oGzksdc1CtMxHxqG6DgNSjZHsymMSgcNEb2c3bz5N2bBMEEUFos98CeAWbh1pTMBcJrsKW63icdAQNGT6Aqv1WWrkxg,[8ff26349/48h/0h/0h/2h]xpub6ErPooPdSeBoXVZocBe8EWF9GXjFuV52kme35p4MtrP2SAFdUmgTJM1urrJzSuA44izrEuiQNNdmWEVRaBJcBDcPpnLBR8tP2Pcu2EiyeHu,[ff3305c2/48h/0h/0h/2h]xpub6Dpndp2xurqbfSGhxKVXzk3nJZgah3PdD3qD11KyPicYYBatRxfxqoN7s9tnWKXaz7zhyVqcvnJyak7BVKonW2wTXHd1zNDxJAu8jcxF59j))"); diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/keystone-singlesig-sp-keystore-1.txt b/src/test/resources/com/sparrowwallet/sparrow/io/keystone-singlesig-sp-keystore-1.txt new file mode 100644 index 00000000..cbbac1ae --- /dev/null +++ b/src/test/resources/com/sparrowwallet/sparrow/io/keystone-singlesig-sp-keystore-1.txt @@ -0,0 +1 @@ +sp([0f056943/352h/1h/0h]tspscan1q05wxw5wc7wqmkf8cnfc6ry76qej8vhr3a3mmxmwgv35s0tlw24fs82k0npv2hv6p97s8sd9t7vpf44kluka9w863zjwxzfrym2ay9ccfzt06c4) \ No newline at end of file diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/specter-diy-sp-keystore.txt b/src/test/resources/com/sparrowwallet/sparrow/io/specter-diy-sp-keystore.txt new file mode 100644 index 00000000..452d539b --- /dev/null +++ b/src/test/resources/com/sparrowwallet/sparrow/io/specter-diy-sp-keystore.txt @@ -0,0 +1 @@ +[0f056943/352h/1h/0h]tspscan1q05wxw5wc7wqmkf8cnfc6ry76qej8vhr3a3mmxmwgv35s0tlw24fs82k0npv2hv6p97s8sd9t7vpf44kluka9w863zjwxzfrym2ay9ccfzt06c4 \ No newline at end of file