add caravan import/export, minor ui fixes

This commit is contained in:
Craig Raw
2021-07-15 11:09:39 +02:00
parent 4a3ad9f4ff
commit ced4d4d337
11 changed files with 274 additions and 12 deletions
@@ -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);
@@ -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;
}
}