mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-23 20:36:44 +00:00
add caravan import/export, minor ui fixes
This commit is contained in:
parent
4a3ad9f4ff
commit
ced4d4d337
11 changed files with 274 additions and 12 deletions
2
drongo
2
drongo
|
@ -1 +1 @@
|
|||
Subproject commit c71979966bbdbff58b1f63946549b15f54890ab5
|
||||
Subproject commit db081695e84f38a184567b00cad1af0f0e7e1b67
|
|
@ -3,10 +3,7 @@ package com.sparrowwallet.sparrow;
|
|||
import com.google.common.base.Charsets;
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.google.common.io.ByteSource;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.*;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.crypto.EncryptionType;
|
||||
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
||||
|
@ -801,7 +798,10 @@ public class AppController implements Initializable {
|
|||
} catch(StorageException e) {
|
||||
showErrorDialog("Error Opening Wallet", e.getMessage());
|
||||
} catch(Exception e) {
|
||||
if(!attemptImportWallet(file, null)) {
|
||||
if(e instanceof IOException && e.getMessage().startsWith("The process cannot access the file because another process has locked")) {
|
||||
log.error("Error opening wallet", e);
|
||||
showErrorDialog("Error Opening Wallet", "The wallet file is locked. Is another instance of " + MainApp.APP_NAME + " already running?");
|
||||
} else if(!attemptImportWallet(file, null)) {
|
||||
log.error("Error opening wallet", e);
|
||||
showErrorDialog("Error Opening Wallet", e.getMessage() == null ? "Unsupported file format" : e.getMessage());
|
||||
}
|
||||
|
@ -897,7 +897,8 @@ public class AppController implements Initializable {
|
|||
new SpecterDesktop(),
|
||||
new CoboVaultSinglesig(), new CoboVaultMultisig(),
|
||||
new PassportSinglesig(),
|
||||
new KeystoneSinglesig(), new KeystoneMultisig());
|
||||
new KeystoneSinglesig(), new KeystoneMultisig(),
|
||||
new CaravanMultisig());
|
||||
for(WalletImport importer : walletImporters) {
|
||||
try(FileInputStream inputStream = new FileInputStream(file)) {
|
||||
if(importer.isEncrypted(file) && password == null) {
|
||||
|
|
|
@ -28,6 +28,7 @@ import java.util.stream.Collectors;
|
|||
public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||
private static final DateFormat dateFormatter = new SimpleDateFormat("HH:mm");
|
||||
public static final int MAX_PERIOD_HOURS = 2;
|
||||
private static final double Y_VALUE_BREAK_MVB = 3.0;
|
||||
|
||||
private Tooltip tooltip;
|
||||
|
||||
|
@ -108,7 +109,7 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
|||
@Override
|
||||
public String toString(Number object) {
|
||||
long vSizeBytes = object.longValue();
|
||||
if(maxMvB > 1.0) {
|
||||
if(maxMvB > Y_VALUE_BREAK_MVB) {
|
||||
return (vSizeBytes / (1000 * 1000)) + " MvB";
|
||||
} else {
|
||||
return (vSizeBytes / (1000)) + " kvB";
|
||||
|
@ -197,8 +198,8 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
|||
if(data.getXValue().equals(category)) {
|
||||
double kvb = data.getYValue().doubleValue() / 1000;
|
||||
double mvb = kvb / 1000;
|
||||
if(mvb >= 0.01 || (maxMvB < 1.0 && mvb > 0.001)) {
|
||||
String amount = (maxMvB < 1.0 ? (int)kvb + " kvB" : String.format("%.2f", mvb) + " MvB");
|
||||
if(mvb >= 0.01 || (maxMvB < Y_VALUE_BREAK_MVB && mvb > 0.001)) {
|
||||
String amount = (maxMvB < Y_VALUE_BREAK_MVB ? (int)kvb + " kvB" : String.format("%.2f", mvb) + " MvB");
|
||||
Label label = new Label(series.getName() + ": " + amount);
|
||||
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CIRCLE);
|
||||
if(i < 8) {
|
||||
|
|
|
@ -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 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());
|
||||
} 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 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());
|
||||
for(WalletImport importer : walletImporters) {
|
||||
FileWalletImportPane importPane = new FileWalletImportPane(importer);
|
||||
importAccordion.getPanes().add(importPane);
|
||||
|
|
180
src/main/java/com/sparrowwallet/sparrow/io/CaravanMultisig.java
Normal file
180
src/main/java/com/sparrowwallet/sparrow/io/CaravanMultisig.java
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
BIN
src/main/resources/image/caravan.png
Normal file
BIN
src/main/resources/image/caravan.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
BIN
src/main/resources/image/caravan@2x.png
Normal file
BIN
src/main/resources/image/caravan@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.1 KiB |
BIN
src/main/resources/image/caravan@3x.png
Normal file
BIN
src/main/resources/image/caravan@3x.png
Normal file
Binary file not shown.
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 a new issue