11 changed files with 274 additions and 12 deletions
@ -1 +1 @@ |
|||
Subproject commit c71979966bbdbff58b1f63946549b15f54890ab5 |
|||
Subproject commit db081695e84f38a184567b00cad1af0f0e7e1b67 |
@ -0,0 +1,180 @@ |
|||
package com.sparrowwallet.sparrow.io; |
|||
|
|||
import com.google.gson.Gson; |
|||
import com.google.gson.GsonBuilder; |
|||
import com.sparrowwallet.drongo.ExtendedKey; |
|||
import com.sparrowwallet.drongo.KeyDerivation; |
|||
import com.sparrowwallet.drongo.Network; |
|||
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.File; |
|||
import java.io.InputStream; |
|||
import java.io.InputStreamReader; |
|||
import java.io.OutputStream; |
|||
import java.nio.charset.StandardCharsets; |
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
public class CaravanMultisig implements WalletImport, WalletExport { |
|||
private static final Logger log = LoggerFactory.getLogger(ColdcardMultisig.class); |
|||
|
|||
@Override |
|||
public String getName() { |
|||
return "Caravan Multisig"; |
|||
} |
|||
|
|||
@Override |
|||
public WalletModel getWalletModel() { |
|||
return WalletModel.CARAVAN; |
|||
} |
|||
|
|||
@Override |
|||
public String getWalletImportDescription() { |
|||
return "Import the file created via the Download Wallet Details button in Caravan."; |
|||
} |
|||
|
|||
@Override |
|||
public Wallet importWallet(InputStream inputStream, String password) throws ImportException { |
|||
try { |
|||
InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8); |
|||
CaravanFile cf = JsonPersistence.getGson().fromJson(reader, CaravanFile.class); |
|||
|
|||
Wallet wallet = new Wallet(); |
|||
wallet.setName(cf.name); |
|||
wallet.setPolicyType(PolicyType.MULTI); |
|||
|
|||
for(ExtPublicKey extKey : cf.extendedPublicKeys) { |
|||
Keystore keystore = new Keystore(extKey.name); |
|||
keystore.setKeyDerivation(new KeyDerivation(extKey.xfp, extKey.bip32Path)); |
|||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(extKey.xpub)); |
|||
|
|||
WalletModel walletModel = WalletModel.fromType(extKey.method); |
|||
if(walletModel == null) { |
|||
keystore.setWalletModel(WalletModel.SPARROW); |
|||
keystore.setSource(KeystoreSource.SW_WATCH); |
|||
} else { |
|||
keystore.setWalletModel(walletModel); |
|||
keystore.setSource(KeystoreSource.HW_USB); |
|||
} |
|||
wallet.getKeystores().add(keystore); |
|||
} |
|||
|
|||
ScriptType scriptType = ScriptType.valueOf(cf.addressType); |
|||
wallet.setScriptType(scriptType); |
|||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, scriptType, wallet.getKeystores(), cf.quorum.requiredSigners)); |
|||
|
|||
return wallet; |
|||
} catch(Exception e) { |
|||
log.error("Error importing " + getName() + " wallet", e); |
|||
throw new ImportException("Error importing " + getName() + " wallet", e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public boolean isWalletImportScannable() { |
|||
return false; |
|||
} |
|||
|
|||
@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"); |
|||
} |
|||
|
|||
try { |
|||
CaravanFile cf = new CaravanFile(); |
|||
cf.name = wallet.getName(); |
|||
cf.addressType = wallet.getScriptType().toString().replace('-', '_'); |
|||
cf.network = Network.get().getName(); |
|||
cf.client = new Client(); |
|||
|
|||
Quorum quorum = new Quorum(); |
|||
quorum.requiredSigners = wallet.getDefaultPolicy().getNumSignaturesRequired(); |
|||
quorum.totalSigners = wallet.getKeystores().size(); |
|||
cf.quorum = quorum; |
|||
|
|||
cf.extendedPublicKeys = new ArrayList<>(); |
|||
for(Keystore keystore : wallet.getKeystores()) { |
|||
ExtPublicKey extKey = new ExtPublicKey(); |
|||
extKey.name = keystore.getLabel(); |
|||
extKey.bip32Path = keystore.getKeyDerivation().getDerivationPath(); |
|||
extKey.xpub = keystore.getExtendedPublicKey().toString(); |
|||
extKey.xfp = keystore.getKeyDerivation().getMasterFingerprint(); |
|||
extKey.method = keystore.getWalletModel().getType(); |
|||
cf.extendedPublicKeys.add(extKey); |
|||
} |
|||
|
|||
Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); |
|||
String json = gson.toJson(cf); |
|||
outputStream.write(json.getBytes(StandardCharsets.UTF_8)); |
|||
outputStream.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 a file that can be imported via the Import Wallet Configuration button in Caravan."; |
|||
} |
|||
|
|||
@Override |
|||
public String getExportFileExtension(Wallet wallet) { |
|||
return "json"; |
|||
} |
|||
|
|||
@Override |
|||
public boolean isWalletExportScannable() { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean walletExportRequiresDecryption() { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean isEncrypted(File file) { |
|||
return false; |
|||
} |
|||
|
|||
private static final class CaravanFile { |
|||
public String name; |
|||
public String addressType; |
|||
public String network; |
|||
public Client client; |
|||
public Quorum quorum; |
|||
public List<ExtPublicKey> extendedPublicKeys; |
|||
public int startingAddressIndex = 0; |
|||
} |
|||
|
|||
private static final class Client { |
|||
public String type = "public"; |
|||
} |
|||
|
|||
private static final class Quorum { |
|||
public int requiredSigners; |
|||
public int totalSigners; |
|||
} |
|||
|
|||
private static final class ExtPublicKey { |
|||
public String name; |
|||
public String bip32Path; |
|||
public String xpub; |
|||
public String xfp; |
|||
public String method; |
|||
} |
|||
} |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 5.1 KiB |
After Width: | Height: | Size: 9.5 KiB |
@ -0,0 +1,44 @@ |
|||
package com.sparrowwallet.sparrow.io; |
|||
|
|||
import com.google.common.io.ByteStreams; |
|||
import com.sparrowwallet.drongo.policy.PolicyType; |
|||
import com.sparrowwallet.drongo.protocol.ScriptType; |
|||
import com.sparrowwallet.drongo.wallet.Wallet; |
|||
import com.sparrowwallet.drongo.wallet.WalletModel; |
|||
import org.junit.Assert; |
|||
import org.junit.Test; |
|||
|
|||
import java.io.ByteArrayInputStream; |
|||
import java.io.ByteArrayOutputStream; |
|||
import java.io.IOException; |
|||
|
|||
public class CaravanMultisigTest extends IoTest { |
|||
@Test |
|||
public void importWallet1() throws ImportException { |
|||
CaravanMultisig ccMultisig = new CaravanMultisig(); |
|||
Wallet wallet = ccMultisig.importWallet(getInputStream("caravan-multisig-export-1.json"), null); |
|||
Assert.assertEquals("Test Wallet", wallet.getName()); |
|||
Assert.assertEquals(PolicyType.MULTI, wallet.getPolicyType()); |
|||
Assert.assertEquals(ScriptType.P2WSH, wallet.getScriptType()); |
|||
Assert.assertEquals(2, wallet.getDefaultPolicy().getNumSignaturesRequired()); |
|||
Assert.assertEquals("wsh(sortedmulti(2,mercury,venus,earth))", wallet.getDefaultPolicy().getMiniscript().getScript().toLowerCase()); |
|||
Assert.assertTrue(wallet.isValid()); |
|||
Assert.assertEquals("8188029f", wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint()); |
|||
Assert.assertEquals("m/48'/0'/0'/2'", wallet.getKeystores().get(0).getKeyDerivation().getDerivationPath()); |
|||
Assert.assertEquals(WalletModel.TREZOR_1, wallet.getKeystores().get(0).getWalletModel()); |
|||
Assert.assertEquals("xpub6EMVvcTUbaABdaPLaVWE72CjcN72URa5pKK1knrKLz1hKaDwUkgddc3832a8MHEpLyuow7MfjMRomt2iMtwPH4pWrFLft4JsquHjeZfKsYp", wallet.getKeystores().get(0).getExtendedPublicKey().toString()); |
|||
} |
|||
|
|||
@Test |
|||
public void exportWallet1() throws ImportException, ExportException, IOException { |
|||
CaravanMultisig ccMultisig = new CaravanMultisig(); |
|||
byte[] walletBytes = ByteStreams.toByteArray(getInputStream("caravan-multisig-export-1.json")); |
|||
Wallet wallet = ccMultisig.importWallet(new ByteArrayInputStream(walletBytes), null); |
|||
ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
|||
ccMultisig.exportWallet(wallet, baos); |
|||
byte[] exportedBytes = baos.toByteArray(); |
|||
String original = new String(walletBytes); |
|||
String exported = new String(exportedBytes); |
|||
Assert.assertEquals(original, exported); |
|||
} |
|||
} |
@ -0,0 +1,36 @@ |
|||
{ |
|||
"name": "Test Wallet", |
|||
"addressType": "P2WSH", |
|||
"network": "mainnet", |
|||
"client": { |
|||
"type": "public" |
|||
}, |
|||
"quorum": { |
|||
"requiredSigners": 2, |
|||
"totalSigners": 3 |
|||
}, |
|||
"extendedPublicKeys": [ |
|||
{ |
|||
"name": "Mercury", |
|||
"bip32Path": "m/48'/0'/0'/2'", |
|||
"xpub": "xpub6EMVvcTUbaABdaPLaVWE72CjcN72URa5pKK1knrKLz1hKaDwUkgddc3832a8MHEpLyuow7MfjMRomt2iMtwPH4pWrFLft4JsquHjeZfKsYp", |
|||
"xfp": "8188029f", |
|||
"method": "trezor" |
|||
}, |
|||
{ |
|||
"name": "Venus", |
|||
"bip32Path": "m/48'/0'/0'/2'", |
|||
"xpub": "xpub6EbTHAgvzZtVppjLPXia2prt6T2w7DD2VQ8gysgV6ECzmKkA6zt4WR3hX4bgciTDrnaneZoJbA19tYKBMrWuA89SbgYAdbMNWmtzrgLhqco", |
|||
"xfp": "a15e8133", |
|||
"method": "ledger" |
|||
}, |
|||
{ |
|||
"name": "Earth", |
|||
"bip32Path": "m/48'/0'/0'/2'", |
|||
"xpub": "xpub6Ea6qWGr6BWpCSyLo8ggypvXg1MwadiAGZxAHvrBUwvT2ki6pNVG61bg3YEdxkj7FRJ1cLFK7Vvqd9rcDvwCLX7VfEMfJJfdbA1NGsFazvz", |
|||
"xfp": "d31cf7f9", |
|||
"method": "coldcard" |
|||
} |
|||
], |
|||
"startingAddressIndex": 0 |
|||
} |
Loading…
Reference in new issue