mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-27 05:56:45 +00:00
Add nthKey support
Keys can be imported using JSON or QR. Wallet can be exported using JSON or QR. Device supports testnet, signet and mainnet. Only sh-wpkh (native Segwit) is supported. Only multisig is supported. Thumbnails generated with: convert nthkey-orig.png -channel RGB -negate -resize 300x300 -background white -gravity center -extent 200x200 -type grayscale nthkey@3x.png convert nthkey-orig.png -channel RGB -negate -resize 150x150 -background white -gravity center -extent 100x100 -type grayscale nthkey@2x.png convert nthkey-orig.png -channel RGB -negate -resize 75x75 -background white -gravity center -extent 50x50 -type grayscale nthkey.png
This commit is contained in:
parent
b22e891b7d
commit
15d1183cce
9 changed files with 247 additions and 4 deletions
|
@ -938,7 +938,8 @@ public class AppController implements Initializable {
|
|||
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) {
|
||||
|
|
|
@ -44,7 +44,7 @@ public class WalletExportDialog extends Dialog<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());
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
|||
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);
|
||||
|
|
242
src/main/java/com/sparrowwallet/sparrow/io/NthKeyMultisig.java
Normal file
242
src/main/java/com/sparrowwallet/sparrow/io/NthKeyMultisig.java
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -73,7 +73,7 @@ public class SpecterDesktop implements WalletImport, WalletExport {
|
|||
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);
|
||||
|
|
BIN
src/main/resources/image/nthkey-orig.png
Normal file
BIN
src/main/resources/image/nthkey-orig.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 644 KiB |
BIN
src/main/resources/image/nthkey.png
Normal file
BIN
src/main/resources/image/nthkey.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
BIN
src/main/resources/image/nthkey@2x.png
Normal file
BIN
src/main/resources/image/nthkey@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
BIN
src/main/resources/image/nthkey@3x.png
Normal file
BIN
src/main/resources/image/nthkey@3x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
Loading…
Reference in a new issue