diff --git a/drongo b/drongo index aa459d00..311afd04 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit aa459d0084b3cc72c49c8922d571338c4f5efaf4 +Subproject commit 311afd0409b7cb442e5ee1efebdc3c7dd628346a diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java index ac1908e1..b98f97f2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java @@ -42,9 +42,9 @@ public class WalletExportDialog extends Dialog { List exporters; if(wallet.getPolicyType() == PolicyType.SINGLE) { - exporters = List.of(new Electrum(), new Descriptor(), new SpecterDesktop(), new Sparrow()); + exporters = List.of(new Electrum(), new ElectrumPersonalServer(), new Descriptor(), new SpecterDesktop(), new Sparrow()); } else if(wallet.getPolicyType() == PolicyType.MULTI) { - exporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new Descriptor(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow()); + exporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new ElectrumPersonalServer(), new KeystoneMultisig(), new Descriptor(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow()); } else { throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType()); } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ElectrumPersonalServer.java b/src/main/java/com/sparrowwallet/sparrow/io/ElectrumPersonalServer.java new file mode 100644 index 00000000..ca299cc1 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/ElectrumPersonalServer.java @@ -0,0 +1,114 @@ +package com.sparrowwallet.sparrow.io; + +import com.sparrowwallet.drongo.ExtendedKey; +import com.sparrowwallet.drongo.KeyPurpose; +import com.sparrowwallet.drongo.policy.PolicyType; +import com.sparrowwallet.drongo.protocol.ScriptType; +import com.sparrowwallet.drongo.wallet.Keystore; +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.drongo.wallet.WalletModel; +import com.sparrowwallet.drongo.wallet.WalletNode; + +import java.io.BufferedWriter; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.util.Iterator; + +public class ElectrumPersonalServer implements WalletExport { + @Override + public String getName() { + return "Electrum Personal Server"; + } + + @Override + public WalletModel getWalletModel() { + return WalletModel.EPS; + } + + @Override + public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException { + if(wallet.getScriptType() == ScriptType.P2TR) { + throw new ExportException(getName() + " does not support Taproot wallets."); + } + + try { + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)); + writer.write("# Electrum Personal Server configuration file fragments\n"); + writer.write("# Copy the lines below into the relevant sections in your EPS config.ini file\n\n"); + writer.write("# Copy into [master-public-keys] section\n"); + writer.write(wallet.getFullName().replace(' ', '_') + " = "); + + ExtendedKey.Header xpubHeader = ExtendedKey.Header.fromScriptType(wallet.getScriptType(), false); + if(wallet.getPolicyType() == PolicyType.MULTI) { + writer.write(wallet.getDefaultPolicy().getNumSignaturesRequired() + " "); + } + + for(Iterator iter = wallet.getKeystores().iterator(); iter.hasNext(); ) { + Keystore keystore = iter.next(); + writer.write(keystore.getExtendedPublicKey().toString(xpubHeader)); + + if(iter.hasNext()) { + writer.write(" "); + } + } + + writer.newLine(); + + if(wallet.hasPaymentCode()) { + writer.write("\n# Copy into [watch-only-addresses] section\n"); + WalletNode notificationNode = wallet.getNotificationWallet().getNode(KeyPurpose.NOTIFICATION); + writer.write(wallet.getFullName().replace(' ', '_') + "-notification_addr = " + notificationNode.getAddress().toString() + "\n"); + + for(Wallet childWallet : wallet.getChildWallets()) { + if(childWallet.isBip47()) { + writer.write(childWallet.getFullName().replace(' ', '_') + " = "); + for(Iterator purposeIterator = KeyPurpose.DEFAULT_PURPOSES.iterator(); purposeIterator.hasNext(); ) { + KeyPurpose keyPurpose = purposeIterator.next(); + for(Iterator iter = childWallet.getNode(keyPurpose).getChildren().iterator(); iter.hasNext(); ) { + WalletNode receiveNode = iter.next(); + writer.write(receiveNode.getAddress().toString()); + + if(iter.hasNext()) { + writer.write(" "); + } + } + + if(purposeIterator.hasNext()) { + writer.write(" "); + } + } + + writer.newLine(); + } + } + + writer.write("\n# Important: If this wallet receives any BIP47 payments, redo this export"); + } + + writer.flush(); + } catch(Exception e) { + throw new ExportException("Could not export wallet", e); + } + } + + @Override + public String getWalletExportDescription() { + return "Export this wallet as a configuration file fragment to copy into your Electrum Personal Server (EPS) config.ini file."; + } + + @Override + public String getExportFileExtension(Wallet wallet) { + return "ini"; + } + + @Override + public boolean isWalletExportScannable() { + return false; + } + + @Override + public boolean walletExportRequiresDecryption() { + return false; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 9998c05c..2b742123 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -364,7 +364,7 @@ public class ElectrumServer { } private int getGapLimitSize(Wallet wallet, Map> nodeTransactionMap) { - int highestIndex = nodeTransactionMap.keySet().stream().map(WalletNode::getIndex).max(Comparator.comparing(Integer::valueOf)).orElse(-1); + int highestIndex = nodeTransactionMap.keySet().stream().filter(node -> node.getDerivation().size() > 1).map(WalletNode::getIndex).max(Comparator.comparing(Integer::valueOf)).orElse(-1); return highestIndex + wallet.getGapLimit() + 1; } diff --git a/src/main/resources/image/eps.png b/src/main/resources/image/eps.png new file mode 100644 index 00000000..4b1f6578 Binary files /dev/null and b/src/main/resources/image/eps.png differ diff --git a/src/main/resources/image/eps@2x.png b/src/main/resources/image/eps@2x.png new file mode 100644 index 00000000..047c483f Binary files /dev/null and b/src/main/resources/image/eps@2x.png differ diff --git a/src/main/resources/image/eps@3x.png b/src/main/resources/image/eps@3x.png new file mode 100644 index 00000000..d8277e40 Binary files /dev/null and b/src/main/resources/image/eps@3x.png differ