Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add nthKey support #182

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/main/java/com/sparrowwallet/sparrow/AppController.java
Original file line number Diff line number Diff line change
Expand Up @@ -938,7 +938,8 @@ private boolean attemptImportWallet(File file, SecureString password) {
new CoboVaultSinglesig(), new CoboVaultMultisig(),
new PassportSinglesig(),
new KeystoneSinglesig(), new KeystoneMultisig(),
new CaravanMultisig());
new CaravanMultisig(),
new NthKeyMultisig());
for(WalletImport importer : walletImporters) {
try(FileInputStream inputStream = new FileInputStream(file)) {
if(importer.isEncrypted(file) && password == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public WalletExportDialog(Wallet wallet) {
if(wallet.getPolicyType() == PolicyType.SINGLE) {
exporters = List.of(new Electrum(), new SpecterDesktop(), new Sparrow());
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
exporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow());
exporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow(), new NthKeyMultisig());
} else {
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public WalletImportDialog() {
importAccordion.getPanes().add(importPane);
}

List<WalletImport> walletImporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow());
List<WalletImport> walletImporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow(), new NthKeyMultisig());
for(WalletImport importer : walletImporters) {
FileWalletImportPane importPane = new FileWalletImportPane(importer);
importAccordion.getPanes().add(importPane);
Expand Down
242 changes: 242 additions & 0 deletions src/main/java/com/sparrowwallet/sparrow/io/NthKeyMultisig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
package com.sparrowwallet.sparrow.io;

import com.google.common.io.CharStreams;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.Utils;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class NthKeyMultisig implements WalletImport, KeystoreFileImport, WalletExport {
private static final Logger log = LoggerFactory.getLogger(NthKeyMultisig.class);

@Override
public String getName() {
return "nthKey Multisig";
}

@Override
public WalletModel getWalletModel() {
return WalletModel.NTHKEY;
}

@Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
NthKeyKeystore cck = JsonPersistence.getGson().fromJson(reader, NthKeyKeystore.class);

Keystore keystore = new Keystore("NthKey");
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
keystore.setWalletModel(WalletModel.NTHKEY);

if(cck.xpub != null && cck.path != null) {
ExtendedKey.Header header = ExtendedKey.Header.fromExtendedKey(cck.xpub);
if(header.getDefaultScriptType() != scriptType) {
throw new ImportException("This wallet's script type (" + scriptType + ") does not match the " + getName() + " script type (" + header.getDefaultScriptType() + ")");
}
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.path));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.xpub));
} else if(scriptType.equals(ScriptType.P2SH)) {
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2sh_deriv));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2sh));
} else if(scriptType.equals(ScriptType.P2SH_P2WSH)) {
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_p2sh_deriv != null ? cck.p2wsh_p2sh_deriv : cck.p2sh_p2wsh_deriv));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh_p2sh != null ? cck.p2wsh_p2sh : cck.p2sh_p2wsh));
} else if(scriptType.equals(ScriptType.P2WSH)) {
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_deriv));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh));
} else {
throw new ImportException("Correct derivation not found for script type: " + scriptType);
}

return keystore;
}

private static class NthKeyKeystore {
public String p2sh_deriv;
public String p2sh;
public String p2wsh_p2sh_deriv;
public String p2wsh_p2sh;
public String p2sh_p2wsh_deriv;
public String p2sh_p2wsh;
public String p2wsh_deriv;
public String p2wsh;
public String xpub;
public String path;
public String xfp;
}

@Override
public String getKeystoreImportDescription() {
return "Import file created by using the Settings > Announce feature in nthKey.";
}

@Override
public String getExportFileExtension(Wallet wallet) {
return "txt";
}

@Override
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
Wallet wallet = new Wallet();
wallet.setPolicyType(PolicyType.MULTI);

int threshold = 2;
ScriptType scriptType = ScriptType.P2SH;
String derivation = null;

try {
List<String> lines = CharStreams.readLines(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
for (String line : lines) {
line = line.trim();
if (line.isEmpty()) {
continue;
}

String[] keyValue = line.split(":");
if (keyValue.length == 2) {
String key = keyValue[0].trim();
String value = keyValue[1].trim();

switch (key) {
case "Name":
wallet.setName(value.trim());
break;
case "Policy":
threshold = Integer.parseInt(value.split(" ")[0]);
break;
case "Derivation":
case "# derivation":
derivation = value;
break;
case "Format":
scriptType = ScriptType.valueOf(value.replace("P2SH_P2WSH"));
break;
default:
if (key.length() == 8 && Utils.isHex(key)) {
Keystore keystore = new Keystore("NthKey");
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
keystore.setWalletModel(WalletModel.NTHKEY);
keystore.setKeyDerivation(new KeyDerivation(key, derivation));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(value));
wallet.makeLabelsUnique(keystore);
wallet.getKeystores().add(keystore);
}
}
}
}


Policy policy = Policy.getPolicy(PolicyType.MULTI, scriptType, wallet.getKeystores(), threshold);
wallet.setDefaultPolicy(policy);
wallet.setScriptType(scriptType);

if(!wallet.isValid()) {
throw new IllegalStateException("This file does not describe a valid wallet. " + getKeystoreImportDescription());
}

return wallet;
} catch(Exception e) {
log.error("Error importing " + getName() + " wallet", e);
throw new ImportException("Error importing " + getName() + " wallet", e);
}
}

@Override
public String getWalletImportDescription() {
return "Import file created by using the Settings > Announce -> Save as JSON Mainnet in nthKey.";
}

@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
if(!wallet.isValid()) {
throw new ExportException("Cannot export an incomplete wallet");
}

if(!wallet.getPolicyType().equals(PolicyType.MULTI)) {
throw new ExportException(getName() + " import requires a multisig wallet");
}

boolean multipleDerivations = false;
Set<String> derivationSet = new HashSet<>();
for(Keystore keystore : wallet.getKeystores()) {
derivationSet.add(keystore.getKeyDerivation().getDerivationPath());
}
if(derivationSet.size() > 1) {
multipleDerivations = true;
}

try {
// TODO: should produce a JSON file like Specter does

// BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
// writer.append("# " + getName() + " setup file (created by Sparrow)\n");
// writer.append("#\n");
// writer.append("Name: ").append(wallet.getFullName()).append("\n");
// writer.append("Policy: ").append(Integer.toString(wallet.getDefaultPolicy().getNumSignaturesRequired())).append(" of ").append(Integer.toString(wallet.getKeystores().size())).append("\n");
// if(!multipleDerivations) {
// writer.append("Derivation: ").append(wallet.getKeystores().get(0).getKeyDerivation().getDerivationPath()).append("\n");
// }
// writer.append("Format: ").append(wallet.getScriptType().toString().replace("P2SH-P2WSH", "P2WSH-P2SH")).append("\n");
// writer.append("\n");
//
// for(Keystore keystore : wallet.getKeystores()) {
// if(multipleDerivations) {
// writer.append("# derivation: ").append(keystore.getKeyDerivation().getDerivationPath()).append("\n");
// }
// writer.append(keystore.getKeyDerivation().getMasterFingerprint().toUpperCase()).append(": ").append(keystore.getExtendedPublicKey().toString()).append("\n");
// if(multipleDerivations) {
// writer.append("\n");
// }
// }
//
// writer.flush();
} catch(Exception e) {
log.error("Error exporting " + getName() + " wallet", e);
throw new ExportException("Error exporting " + getName() + " wallet", e);
}
}

@Override
public String getWalletExportDescription() {
return "Export file that can be read by nthKey using the Settings > Import wallet > Import wallet JSON.";
}

@Override
public boolean isEncrypted(File file) {
return false;
}

@Override
public boolean isWalletImportScannable() {
return false;
}

@Override
public boolean isKeystoreImportScannable() {
return true;
}

@Override
public boolean isWalletExportScannable() {
return true;
}

@Override
public boolean walletExportRequiresDecryption() {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public Wallet importWallet(InputStream inputStream, String password) throws Impo
keystore.setWalletModel(walletModel);
if(walletModel == WalletModel.TREZOR_1 || walletModel == WalletModel.TREZOR_T || walletModel == WalletModel.KEEPKEY ||
walletModel == WalletModel.LEDGER_NANO_S || walletModel == WalletModel.LEDGER_NANO_X ||
walletModel == WalletModel.BITBOX_02 || walletModel == WalletModel.COLDCARD) {
walletModel == WalletModel.BITBOX_02 || walletModel == WalletModel.COLDCARD || walletModel == WalletModel.NTHKEY) {
keystore.setSource(KeystoreSource.HW_USB);
} else if(walletModel == WalletModel.BITCOIN_CORE) {
keystore.setSource(KeystoreSource.SW_WATCH);
Expand Down
Binary file added src/main/resources/image/nthkey-orig.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/main/resources/image/nthkey.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/main/resources/image/nthkey@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/main/resources/image/nthkey@3x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.