diff --git a/drongo b/drongo index e912e8a5..fee04267 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit e912e8a5121f06b4a2ac913b74e51c0f7dcbb940 +Subproject commit fee042679938316f034ceee137cfa056be566ffd diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 10b0c389..f6c7ce6f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -773,6 +773,9 @@ public class AppController implements Initializable { Wallet wallet = storage.loadWallet(); checkWalletNetwork(wallet); restorePublicKeysFromSeed(wallet, null); + if(!wallet.isValid()) { + throw new IllegalStateException("Wallet file is not valid."); + } Tab tab = addWalletTab(storage, wallet); tabs.getSelectionModel().select(tab); } else if(FileType.BINARY.equals(fileType)) { @@ -804,8 +807,11 @@ public class AppController implements Initializable { if(exception instanceof InvalidPasswordException) { showErrorDialog("Invalid Password", "The wallet password was invalid."); } else { - log.error("Error Opening Wallet", exception); - showErrorDialog("Error Opening Wallet", exception.getMessage()); + if(!attemptImportWallet(file, password)) { + log.error("Error Opening Wallet", exception); + showErrorDialog("Error Opening Wallet", exception.getMessage()); + } + password.clear(); } }); EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.START, "Decrypting wallet...")); @@ -814,8 +820,10 @@ public class AppController implements Initializable { throw new IOException("Unsupported file type"); } } catch(Exception e) { - log.error("Error opening wallet", e); - showErrorDialog("Error Opening Wallet", e.getMessage()); + if(!attemptImportWallet(file, null)) { + log.error("Error opening wallet", e); + showErrorDialog("Error Opening Wallet", e.getMessage()); + } } } @@ -872,35 +880,65 @@ public class AppController implements Initializable { Optional optionalWallet = dlg.showAndWait(); if(optionalWallet.isPresent()) { Wallet wallet = optionalWallet.get(); - File walletFile = Storage.getWalletFile(wallet.getName()); + addImportedWallet(wallet); + } + } - if(walletFile.exists()) { - Alert alert = new Alert(Alert.AlertType.CONFIRMATION); - alert.setTitle("Existing wallet found"); - alert.setHeaderText("Replace existing wallet?"); - alert.setContentText("Wallet file " + wallet.getName() + " already exists"); - Optional result = alert.showAndWait(); - if(result.isPresent() && result.get() == ButtonType.CANCEL) { - return; + private boolean attemptImportWallet(File file, SecureString password) { + List walletImporters = List.of(new ColdcardSinglesig(), new ColdcardMultisig(), new Electrum(), new Specter()); + for(WalletImport importer : walletImporters) { + try(FileInputStream inputStream = new FileInputStream(file)) { + if(importer.isEncrypted(file) && password == null) { + WalletPasswordDialog dlg = new WalletPasswordDialog(file.getName(), WalletPasswordDialog.PasswordRequirement.LOAD); + Optional optionalPassword = dlg.showAndWait(); + if(optionalPassword.isPresent()) { + password = optionalPassword.get(); + } } - //Close existing wallet first if open - for(Iterator iter = tabs.getTabs().iterator(); iter.hasNext(); ) { - Tab tab = iter.next(); - TabData tabData = (TabData)tab.getUserData(); - if(tabData.getType() == TabData.TabType.WALLET) { - WalletTabData walletTabData = (WalletTabData) tabData; - if(walletTabData.getStorage().getWalletFile().equals(walletFile)) { - iter.remove(); - } + Wallet wallet = importer.importWallet(inputStream, password == null ? null: password.asString()); + if(wallet.getName() == null) { + wallet.setName(file.getName()); + } + addImportedWallet(wallet); + return true; + } catch(Exception e) { + //ignore + } + } + + return false; + } + + private void addImportedWallet(Wallet wallet) { + File walletFile = Storage.getWalletFile(wallet.getName()); + + if(walletFile.exists()) { + Alert alert = new Alert(Alert.AlertType.CONFIRMATION); + alert.setTitle("Existing wallet found"); + alert.setHeaderText("Replace existing wallet?"); + alert.setContentText("Wallet file " + wallet.getName() + " already exists"); + Optional result = alert.showAndWait(); + if(result.isPresent() && result.get() == ButtonType.CANCEL) { + return; + } + + //Close existing wallet first if open + for(Iterator iter = tabs.getTabs().iterator(); iter.hasNext(); ) { + Tab tab = iter.next(); + TabData tabData = (TabData)tab.getUserData(); + if(tabData.getType() == TabData.TabType.WALLET) { + WalletTabData walletTabData = (WalletTabData) tabData; + if(walletTabData.getStorage().getWalletFile().equals(walletFile)) { + iter.remove(); } } } - - Storage storage = new Storage(walletFile); - Tab tab = addWalletTab(storage, wallet); - tabs.getSelectionModel().select(tab); } + + Storage storage = new Storage(walletFile); + Tab tab = addWalletTab(storage, wallet); + tabs.getSelectionModel().select(tab); } public void exportWallet(ActionEvent event) { diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ColdcardSinglesig.java b/src/main/java/com/sparrowwallet/sparrow/io/ColdcardSinglesig.java index f016b066..4588c3e4 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ColdcardSinglesig.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ColdcardSinglesig.java @@ -5,9 +5,12 @@ import com.google.gson.JsonElement; import com.google.gson.reflect.TypeToken; import com.sparrowwallet.drongo.ExtendedKey; import com.sparrowwallet.drongo.KeyDerivation; +import com.sparrowwallet.drongo.policy.Policy; +import com.sparrowwallet.drongo.policy.PolicyType; import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.KeystoreSource; +import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.WalletModel; import java.io.File; @@ -16,7 +19,7 @@ import java.io.InputStreamReader; import java.lang.reflect.Type; import java.util.Map; -public class ColdcardSinglesig implements KeystoreFileImport { +public class ColdcardSinglesig implements KeystoreFileImport, WalletImport { @Override public String getName() { return "Coldcard"; @@ -77,6 +80,29 @@ public class ColdcardSinglesig implements KeystoreFileImport { throw new ImportException("Correct derivation not found for script type: " + scriptType); } + @Override + public String getWalletImportDescription() { + return getKeystoreImportDescription(); + } + + @Override + public Wallet importWallet(InputStream inputStream, String password) throws ImportException { + //Use default of P2WPKH + Keystore keystore = getKeystore(ScriptType.P2WPKH, inputStream, ""); + + Wallet wallet = new Wallet(); + wallet.setPolicyType(PolicyType.SINGLE); + wallet.setScriptType(ScriptType.P2WPKH); + wallet.getKeystores().add(keystore); + wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2WPKH, wallet.getKeystores(), null)); + + if(!wallet.isValid()) { + throw new ImportException("Wallet is in an inconsistent state."); + } + + return wallet; + } + private static class ColdcardKeystore { public String deriv; public String name; diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java index dca88db5..939a34dd 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java @@ -81,7 +81,7 @@ public class Storage { return gsonBuilder.setPrettyPrinting().disableHtmlEscaping().create(); } - public Wallet loadWallet() throws IOException, MnemonicException { + public Wallet loadWallet() throws IOException { Reader reader = new FileReader(walletFile); Wallet wallet = gson.fromJson(reader, Wallet.class); reader.close(); @@ -90,7 +90,7 @@ public class Storage { return wallet; } - public WalletAndKey loadWallet(CharSequence password) throws IOException, MnemonicException, StorageException { + public WalletAndKey loadWallet(CharSequence password) throws IOException, StorageException { InputStream fileStream = new FileInputStream(walletFile); ECKey encryptionKey = getEncryptionKey(password, fileStream); @@ -461,11 +461,9 @@ public class Storage { protected Task createTask() { return new Task<>() { protected WalletAndKey call() throws IOException, StorageException, MnemonicException { - try { - return storage.loadWallet(password); - } finally { - password.clear(); - } + WalletAndKey walletAndKey = storage.loadWallet(password); + password.clear(); + return walletAndKey; } }; } diff --git a/src/test/java/com/sparrowwallet/sparrow/io/SpecterTest.java b/src/test/java/com/sparrowwallet/sparrow/io/SpecterTest.java new file mode 100644 index 00000000..b50b390c --- /dev/null +++ b/src/test/java/com/sparrowwallet/sparrow/io/SpecterTest.java @@ -0,0 +1,39 @@ +package com.sparrowwallet.sparrow.io; + +import com.sparrowwallet.drongo.policy.PolicyType; +import com.sparrowwallet.drongo.protocol.ScriptType; +import com.sparrowwallet.drongo.wallet.Wallet; +import org.junit.Assert; +import org.junit.Test; + +public class SpecterTest extends IoTest { + @Test + public void testImport() throws ImportException { + Specter specter = new Specter(); + Wallet wallet = specter.importWallet(getInputStream("specter-wallet.json"), null); + + Assert.assertEquals(PolicyType.SINGLE, wallet.getPolicyType()); + Assert.assertEquals(ScriptType.P2SH_P2WPKH, wallet.getScriptType()); + Assert.assertEquals(1, wallet.getDefaultPolicy().getNumSignaturesRequired()); + Assert.assertEquals("sh(wpkh(keystore1))", wallet.getDefaultPolicy().getMiniscript().getScript()); + Assert.assertEquals("4df18faa", wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint()); + Assert.assertEquals("m/49'/0'/0'", wallet.getKeystores().get(0).getKeyDerivation().getDerivationPath()); + Assert.assertEquals("xpub6BgwyseZdeGJj2vB3FPHSGPxR1LLkr8AsAJqedrgjwBXKXXVWkH31fhwtQXgrM7uMrWjLwXhuDhhenNAh5eBdUSjrHkrKfaXutcJdAfgQ8D", wallet.getKeystores().get(0).getExtendedPublicKey().toString()); + Assert.assertTrue(wallet.isValid()); + } + + @Test + public void testMultisigImport() throws ImportException { + Specter specter = new Specter(); + Wallet wallet = specter.importWallet(getInputStream("specter-multisig-wallet.json"), null); + + Assert.assertEquals(PolicyType.MULTI, wallet.getPolicyType()); + Assert.assertEquals(ScriptType.P2WSH, wallet.getScriptType()); + Assert.assertEquals(3, wallet.getDefaultPolicy().getNumSignaturesRequired()); + Assert.assertEquals("wsh(sortedmulti(3,keystore1,keystore2,keystore3,keystore4))", wallet.getDefaultPolicy().getMiniscript().getScript()); + Assert.assertEquals("ca9a2b19", wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint()); + Assert.assertEquals("m/48'/0'/0'/2'", wallet.getKeystores().get(0).getKeyDerivation().getDerivationPath()); + Assert.assertEquals("xpub6EhbRDNhmMX863W8RujJyAMw1vtM4MHXnsk14paK1ZBEH75k44gWqfaraXCrzg6w9pzC2yLc28vAdUfpB9ShuEB1HA9xMs6BjmRi4PKbt1K", wallet.getKeystores().get(0).getExtendedPublicKey().toString()); + Assert.assertTrue(wallet.isValid()); + } +} diff --git a/src/test/resources/.DS_Store b/src/test/resources/.DS_Store deleted file mode 100644 index 8c4069f8..00000000 Binary files a/src/test/resources/.DS_Store and /dev/null differ diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/specter-multisig-wallet.json b/src/test/resources/com/sparrowwallet/sparrow/io/specter-multisig-wallet.json new file mode 100644 index 00000000..24f13205 --- /dev/null +++ b/src/test/resources/com/sparrowwallet/sparrow/io/specter-multisig-wallet.json @@ -0,0 +1,5 @@ +{ + "label": "specter-multisig", + "blockheight": 646246, + "descriptor": "wsh(multi(3,[ca9a2b19/48'/0'/0'/2']xpub6EhbRDNhmMX863W8RujJyAMw1vtM4MHXnsk14paK1ZBEH75k44gWqfaraXCrzg6w9pzC2yLc28vAdUfpB9ShuEB1HA9xMs6BjmRi4PKbt1K,[046b9450/48'/0'/0'/2']xpub6Ej6a311KAYh6jUAnD4zHhgtgemM4DfMksQUNbAzXCQEkjKf5Ep2WLmMtJkmR4MHm8aEy648kzZo4cphwYhMpfwfYvHRQY4u2e4ijWzohwE,[747b698e/48'/0'/0'/2']xpub6Eb6Z1xtmWRiWKgRpHf6dHiEagGd6FLiBXrnma1nFK4PGRYqSVqVyJaxna5Mb8etSP4ATKVAvKnXG1a9HZauoAawuSDJT5RgH2HqEVHZVHY,[7bb026be/48'/0'/0'/2']xpub6FMGmJccz6dqLo9TMmXYPpZ7HUDm71RHHSTXqTUgkyP9TZmF2uexoB7qttEBtHaotopPwfAVfKfwmdEjCGabVpND1m7ix2AW2LxPuNfLZhi))" +} \ No newline at end of file diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/specter-wallet.json b/src/test/resources/com/sparrowwallet/sparrow/io/specter-wallet.json new file mode 100644 index 00000000..586bd98e --- /dev/null +++ b/src/test/resources/com/sparrowwallet/sparrow/io/specter-wallet.json @@ -0,0 +1,5 @@ +{ + "label": "specter-wallet", + "blockheight": 647153, + "descriptor": "sh(wpkh([4df18faa/49'/0'/0']xpub6BgwyseZdeGJj2vB3FPHSGPxR1LLkr8AsAJqedrgjwBXKXXVWkH31fhwtQXgrM7uMrWjLwXhuDhhenNAh5eBdUSjrHkrKfaXutcJdAfgQ8D))" +} \ No newline at end of file