add initial support for sp wallets

This commit is contained in:
Craig Raw 2026-04-22 14:57:30 +02:00
parent dd50f6973b
commit 17a04510fd
25 changed files with 314 additions and 111 deletions

2
drongo

@ -1 +1 @@
Subproject commit 15e5aaa4b9846c7d410723a7785198c62ef66248
Subproject commit bc526c90e1a4fa2988df1538d6925ea7fdceb5c3

View File

@ -1473,7 +1473,7 @@ public class AppController implements Initializable {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) {
Wallet wallet = selectedWalletForm.getWallet();
if(wallet.getKeystores().size() == 1) {
if(wallet.getPolicyType() == PolicyType.SINGLE_HD) {
//Can sign and verify
messageSignDialog = new MessageSignDialog(wallet);
}

View File

@ -1212,6 +1212,7 @@ public class AppServices {
public static boolean isWhirlpoolCompatible(Wallet wallet) {
return WHIRLPOOL_NETWORKS.contains(Network.get())
&& wallet.getPolicyType() == PolicyType.SINGLE_HD
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
&& wallet.getKeystores().size() == 1
&& wallet.getKeystores().get(0).hasSeed()
@ -1222,6 +1223,7 @@ public class AppServices {
public static boolean isWhirlpoolPostmixCompatible(Wallet wallet) {
return WHIRLPOOL_NETWORKS.contains(Network.get())
&& wallet.getPolicyType() == PolicyType.SINGLE_HD
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
&& wallet.getKeystores().size() == 1
&& wallet.getKeystores().getFirst().getWalletModel() != WalletModel.BITBOX_02; //BitBox02 does not support high account numbers

View File

@ -55,8 +55,7 @@ public abstract class BaseController {
descriptorArea.setMouseOverTextDelay(Duration.ofMillis(150));
descriptorArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_BEGIN, e -> {
TwoDimensional.Position position = descriptorArea.getParagraph(0).getStyleSpans().offsetToPosition(e.getCharacterIndex(), Backward);
int index = descriptorArea.getWallet().getPolicyType() == PolicyType.SINGLE || descriptorArea.getWallet().getPolicyType() == PolicyType.SINGLE_SILENT_PAYMENTS ?
position.getMajor() - 1 : ((position.getMajor() - 1) / 2);
int index = descriptorArea.getWallet().getPolicyType() == PolicyType.SINGLE_HD || descriptorArea.getWallet().getPolicyType() == PolicyType.SINGLE_SP ? position.getMajor() - 1 : ((position.getMajor() - 1) / 2);
if(position.getMajor() > 0 && index >= 0 && index < descriptorArea.getWallet().getKeystores().size()) {
Keystore hoverKeystore = descriptorArea.getWallet().getKeystores().get(index);
Point2D pos = e.getScreenPosition();

View File

@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.Wallet;
@ -68,7 +69,7 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
final ButtonType discoverButtonType = new javafx.scene.control.ButtonType("Discover", ButtonBar.ButtonData.LEFT);
if(!availableAccounts.isEmpty() && (masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)
|| (masterWallet.getKeystores().size() == 1 && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)))) {
|| (masterWallet.getPolicyType() == PolicyType.SINGLE_HD && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)))) {
dialogPane.getButtonTypes().add(discoverButtonType);
Button discoverButton = (Button)dialogPane.lookupButton(discoverButtonType);
discoverButton.disableProperty().bind(AppServices.onlineProperty().not());

View File

@ -80,6 +80,7 @@ public class AddressTreeTable extends CoinTreeTable {
contextMenu.getItems().add(showCountItem);
getColumns().forEach(col -> col.setContextMenu(contextMenu));
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
setEditable(true);
setupColumnWidths();

View File

@ -14,8 +14,7 @@ import org.fxmisc.richtext.CodeArea;
import java.util.List;
import static com.sparrowwallet.drongo.policy.PolicyType.MULTI;
import static com.sparrowwallet.drongo.policy.PolicyType.SINGLE;
import static com.sparrowwallet.drongo.policy.PolicyType.*;
import static com.sparrowwallet.drongo.protocol.ScriptType.MULTISIG;
public class DescriptorArea extends CodeArea {
@ -33,13 +32,13 @@ public class DescriptorArea extends CodeArea {
List<Keystore> keystores = wallet.getKeystores();
int threshold = wallet.getDefaultPolicy().getNumSignaturesRequired();
if(SINGLE.equals(policyType)) {
if(SINGLE_HD.equals(policyType)) {
append(scriptType.getDescriptor(), "descriptor-text");
replace(getLength(), getLength(), keystores.get(0).getScriptName(), List.of(keystores.get(0).isValid() ? "descriptor-text" : "descriptor-error", keystores.get(0).getScriptName()));
append(scriptType.getCloseDescriptor(), "descriptor-text");
}
if(MULTI.equals(policyType)) {
if(MULTI_HD.equals(policyType)) {
append(scriptType.getDescriptor(), "descriptor-text");
append(MULTISIG.getDescriptor(), "descriptor-text");
append(Integer.toString(threshold), "descriptor-text");
@ -52,6 +51,12 @@ public class DescriptorArea extends CodeArea {
append(MULTISIG.getCloseDescriptor(), "descriptor-text");
append(scriptType.getCloseDescriptor(), "descriptor-text");
}
if(SINGLE_SP.equals(policyType)) {
append("sp(", "descriptor-text");
replace(getLength(), getLength(), keystores.get(0).getScriptName(), List.of(keystores.get(0).isValid() ? "descriptor-text" : "descriptor-error", keystores.get(0).getScriptName()));
append(")", "descriptor-text");
}
}
public Wallet getWallet() {

View File

@ -4,6 +4,7 @@ import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
@ -408,7 +409,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
private static boolean canSignMessage(WalletNode walletNode) {
Wallet wallet = walletNode.getWallet();
return wallet.getKeystores().size() == 1 && (!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
return wallet.getPolicyType() == PolicyType.SINGLE_HD && (!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
}
private static boolean containsWalletOutputs(TransactionEntry transactionEntry) {

View File

@ -5,7 +5,7 @@ import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.hummingbird.registry.CryptoOutput;
import com.sparrowwallet.hummingbird.registry.RegistryItem;
import com.sparrowwallet.hummingbird.registry.RegistryType;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
@ -32,7 +32,7 @@ import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.Optional;
import static com.sparrowwallet.sparrow.wallet.SettingsController.getCryptoOutput;
import static com.sparrowwallet.sparrow.wallet.SettingsController.getUROutputDescriptor;
public class FileWalletExportPane extends TitledDescriptionPane {
private final Wallet wallet;
@ -176,9 +176,9 @@ public class FileWalletExportPane extends TitledDescriptionPane {
boolean addBbqrOption = exportWallet.getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().showBbqr());
QREncoding encoding = exportWallet.getKeystores().stream().allMatch(keystore -> keystore.getWalletModel().selectBbqr()) ? QREncoding.BBQR : QREncoding.UR;
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(exportWallet, KeyPurpose.DEFAULT_PURPOSES, null);
CryptoOutput cryptoOutput = getCryptoOutput(exportWallet);
RegistryItem registryItem = getUROutputDescriptor(exportWallet);
BBQR bbqr = addBbqrOption ? new BBQR(BBQRType.UNICODE, outputDescriptor.toString(true).getBytes(StandardCharsets.UTF_8)) : null;
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), cryptoOutput.toUR(), bbqr, encoding);
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), registryItem.toUR(), bbqr, encoding);
} else if(exporter.getClass().equals(ColdcardMultisig.class)) {
UR ur = UR.fromBytes(outputStream.toByteArray());
BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray());

View File

@ -14,6 +14,8 @@ import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.psbt.PSBTProofException;
import com.sparrowwallet.drongo.silentpayments.*;
import com.sparrowwallet.drongo.wallet.Payment;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.AppServices;
@ -66,6 +68,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
private final ComboBox<Wallet> toWallet;
private final FeeRangeSlider feeRange;
private final CopyableLabel feeRate;
private SilentPaymentAddress silentPaymentAddress;
public PrivateKeySweepDialog(Wallet wallet) {
final DialogPane dialogPane = getDialogPane();
@ -204,18 +207,31 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
});
toAddress.textProperty().addListener((observable, oldValue, newValue) -> {
try {
silentPaymentAddress = SilentPaymentAddress.from(newValue);
} catch(Exception e) {
silentPaymentAddress = null;
}
createButton.setDisable(!isValidKey() || !isValidToAddress());
});
toWallet.valueProperty().addListener((observable, oldValue, selectedWallet) -> {
if(selectedWallet != null) {
toAddress.setText(selectedWallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
if(selectedWallet.getPolicyType() == PolicyType.SINGLE_SP) {
toAddress.setText(selectedWallet.getKeystores().getFirst().getSilentPaymentScanAddress().getSilentPaymentAddress().getAddress());
} else {
toAddress.setText(selectedWallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
}
}
});
keyScriptType.setValue(ScriptType.P2PKH);
if(wallet != null) {
toAddress.setText(wallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
if(wallet.getPolicyType() == PolicyType.SINGLE_SP) {
toAddress.setText(wallet.getKeystores().getFirst().getSilentPaymentScanAddress().getSilentPaymentAddress().getAddress());
} else {
toAddress.setText(wallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
}
}
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(null));
@ -272,10 +288,13 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
}
private boolean isValidToAddress() {
try {
Address address = getToAddress();
if(silentPaymentAddress != null) {
return true;
} catch (InvalidAddressException e) {
}
try {
getToAddress();
return true;
} catch(InvalidAddressException e) {
return false;
}
}
@ -347,7 +366,8 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
DumpedPrivateKey privateKey = getPrivateKey();
ScriptType scriptType = keyScriptType.getValue();
Address fromAddress = scriptType.getAddress(PolicyType.SINGLE_HD, privateKey.getKey());
Address destAddress = getToAddress();
Payment payment = silentPaymentAddress != null ? new SilentPayment(silentPaymentAddress, null, 0, true)
: new Payment(getToAddress(), null, 0, true);
Date since = null;
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
@ -363,7 +383,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
ElectrumServer.AddressUtxosService addressUtxosService = new ElectrumServer.AddressUtxosService(fromAddress, since);
addressUtxosService.setOnSucceeded(successEvent -> {
createTransaction(privateKey.getKey(), scriptType, addressUtxosService.getValue(), destAddress);
createTransaction(privateKey.getKey(), scriptType, addressUtxosService.getValue(), payment);
});
addressUtxosService.setOnFailed(failedEvent -> {
Throwable rootCause = Throwables.getRootCause(failedEvent.getSource().getException());
@ -383,7 +403,8 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
}
}
private void createTransaction(ECKey privKey, ScriptType scriptType, List<TransactionOutput> txOutputs, Address destAddress) {
private void createTransaction(ECKey privKey, ScriptType scriptType, List<TransactionOutput> txOutputs, Payment payment) {
Address destAddress = payment instanceof SilentPayment silentPayment ? computeSilentPaymentAddress(privKey, scriptType, txOutputs, silentPayment) : payment.getAddress();
ECKey pubKey = ECKey.fromPublicOnly(privKey);
Transaction noFeeTransaction = new Transaction();
@ -468,6 +489,29 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
}
}
private Address computeSilentPaymentAddress(ECKey privKey, ScriptType scriptType, List<TransactionOutput> txOutputs, SilentPayment silentPayment) {
ECKey summedPrivateKey = scriptType.getOutputKey(PolicyType.SINGLE_HD, privKey);
if(scriptType == P2TR && summedPrivateKey.hasOddYCoord()) {
summedPrivateKey = summedPrivateKey.negatePrivate();
}
Set<HashIndex> outpoints = new LinkedHashSet<>();
for(TransactionOutput txOutput : txOutputs) {
outpoints.add(new HashIndex(txOutput.getHash(), txOutput.getIndex()));
}
try {
SilentPaymentUtils.computeOutputAddresses(List.of(silentPayment), summedPrivateKey, outpoints);
if(!silentPayment.isAddressComputed()) {
throw new IllegalStateException("Failed to compute silent payment address");
}
return silentPayment.getAddress();
} catch(InvalidSilentPaymentException e) {
throw new IllegalStateException("Failed to compute silent payment address", e);
}
}
public Glyph getGlyph(FontAwesome5.Glyph glyphEnum) {
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphEnum);
glyph.setFontSize(12);

View File

@ -51,6 +51,7 @@ import org.openpnp.capture.CaptureDevice;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharsetDecoder;
@ -582,6 +583,14 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
}
}
private ECKey getKey(CryptoHDKey cryptoHDKey) {
if(cryptoHDKey.isPrivateKey()) {
return ECKey.fromPrivate(new BigInteger(1, cryptoHDKey.getKey()));
} else {
return ECKey.fromPublicOnly(cryptoHDKey.getKey());
}
}
private OutputDescriptor getOutputDescriptor(CryptoOutput cryptoOutput) {
ScriptType scriptType = getScriptType(cryptoOutput.getScriptExpressions());
@ -679,11 +688,16 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
for(int i = 0; i < keys.size(); i++) {
RegistryItem key = keys.get(i);
if(key instanceof URHDKey urhdKey) {
ExtendedKey extendedKey = getExtendedKey(urhdKey);
KeyDerivation keyDerivation = getKeyDerivation(urhdKey.getOrigin());
source = source.replaceAll("@" + i, OutputDescriptor.writeKey(extendedKey, keyDerivation, null, true, true));
if(urhdKey.getName() != null) {
mapExtendedPublicKeyLabels.put(extendedKey, urhdKey.getName());
if(urhdKey.getChainCode() == null) {
ECKey ecKey = getKey(urhdKey);
source = source.replaceAll("@" + i, OutputDescriptor.writeKey(ecKey, keyDerivation, true));
} else {
ExtendedKey extendedKey = getExtendedKey(urhdKey);
source = source.replaceAll("@" + i, OutputDescriptor.writeKey(extendedKey, keyDerivation, null, true, true));
if(urhdKey.getName() != null) {
mapExtendedPublicKeyLabels.put(extendedKey, urhdKey.getName());
}
}
} else {
throw new IllegalArgumentException("Only extended HD keys are supported in output descriptors");

View File

@ -44,13 +44,13 @@ public class WalletExportDialog extends Dialog<Wallet> {
AnchorPane.setRightAnchor(scrollPane, 0.0);
List<WalletExport> exporters;
if(wallet.getPolicyType() == PolicyType.SINGLE) {
if(wallet.getPolicyType() == PolicyType.SINGLE_HD) {
exporters = List.of(new Electrum(), new ElectrumPersonalServer(), new Descriptor(), new SpecterDesktop(), new Sparrow(), new WalletLabels(allWalletForms), new WalletTransactions(selectedWalletForm));
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
} else if(wallet.getPolicyType() == PolicyType.MULTI_HD) {
exporters = List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new ElectrumPersonalServer(), new KeystoneMultisig(),
new Descriptor(), new JadeMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow(), new WalletLabels(allWalletForms), new WalletTransactions(selectedWalletForm));
} else if(wallet.getPolicyType() == PolicyType.SINGLE_SILENT_PAYMENTS) {
exporters = List.of(new Sparrow(), new WalletLabels(allWalletForms), new WalletTransactions(selectedWalletForm));
} else if(wallet.getPolicyType() == PolicyType.SINGLE_SP) {
exporters = List.of(new Descriptor(), new Sparrow(), new WalletLabels(allWalletForms), new WalletTransactions(selectedWalletForm));
} else {
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
}

View File

@ -20,6 +20,6 @@ public class SettingsChangedEvent {
}
public enum Type {
POLICY, SCRIPT_TYPE, MUTLISIG_THRESHOLD, MULTISIG_TOTAL, KEYSTORE_LABEL, KEYSTORE_FINGERPRINT, KEYSTORE_DERIVATION, KEYSTORE_XPUB, GAP_LIMIT, BIRTH_DATE, WATCH_LAST;
POLICY, SCRIPT_TYPE, MUTLISIG_THRESHOLD, MULTISIG_TOTAL, KEYSTORE_LABEL, KEYSTORE_FINGERPRINT, KEYSTORE_DERIVATION, KEYSTORE_XPUB, KEYSTORE_SP_SCAN, GAP_LIMIT, BIRTH_DATE, WATCH_LAST;
}
}

View File

@ -3,10 +3,10 @@ package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.wallet.KeystoreController;
import java.io.*;
import java.nio.charset.StandardCharsets;
@ -29,26 +29,32 @@ public class Descriptor implements WalletImport, WalletExport {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
try {
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
bufferedWriter.write("# Receive and change descriptor (BIP389):");
bufferedWriter.newLine();
if(wallet.getPolicyType() == PolicyType.SINGLE_SP) {
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet);
bufferedWriter.write(outputDescriptor.toString(true));
bufferedWriter.newLine();
} else {
bufferedWriter.write("# Receive and change descriptor:");
bufferedWriter.newLine();
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.DEFAULT_PURPOSES, null);
bufferedWriter.write(outputDescriptor.toString(true));
bufferedWriter.newLine();
bufferedWriter.newLine();
bufferedWriter.newLine();
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.DEFAULT_PURPOSES, null);
bufferedWriter.write(outputDescriptor.toString(true));
bufferedWriter.newLine();
bufferedWriter.newLine();
bufferedWriter.newLine();
bufferedWriter.write("# Receive descriptor (Bitcoin Core):");
bufferedWriter.newLine();
OutputDescriptor receiveDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.RECEIVE, null);
bufferedWriter.write(receiveDescriptor.toString(true));
bufferedWriter.newLine();
bufferedWriter.newLine();
bufferedWriter.write("# Change descriptor (Bitcoin Core):");
bufferedWriter.newLine();
OutputDescriptor changeDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.CHANGE, null);
bufferedWriter.write(changeDescriptor.toString(true));
bufferedWriter.newLine();
bufferedWriter.write("# Receive descriptor:");
bufferedWriter.newLine();
OutputDescriptor receiveDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.RECEIVE, null);
bufferedWriter.write(receiveDescriptor.toString(true));
bufferedWriter.newLine();
bufferedWriter.newLine();
bufferedWriter.write("# Change descriptor:");
bufferedWriter.newLine();
OutputDescriptor changeDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.CHANGE, null);
bufferedWriter.write(changeDescriptor.toString(true));
bufferedWriter.newLine();
}
bufferedWriter.flush();
} catch(Exception e) {

View File

@ -188,7 +188,7 @@ public class Storage {
keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey());
keystore.getSeed().setPassphrase(copyKeystore.getSeed().getPassphrase());
keystore.setBip47ExtendedPrivateKey(derivedKeystore.getBip47ExtendedPrivateKey());
keystore.setSilentPaymentScanAddress(wallet.getPolicyType() == PolicyType.SINGLE_SP ? derivedKeystore.getSilentPaymentScanAddress() : null);
keystore.setSilentPaymentScanAddress(derivedKeystore.getSilentPaymentScanAddress());
copyKeystore.getSeed().clear();
} else if(keystore.hasMasterPrivateExtendedKey()) {
Keystore copyKeystore = copy.getKeystores().get(i);
@ -196,7 +196,7 @@ public class Storage {
keystore.setKeyDerivation(derivedKeystore.getKeyDerivation());
keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey());
keystore.setBip47ExtendedPrivateKey(derivedKeystore.getBip47ExtendedPrivateKey());
keystore.setSilentPaymentScanAddress(wallet.getPolicyType() == PolicyType.SINGLE_SP ? derivedKeystore.getSilentPaymentScanAddress() : null);
keystore.setSilentPaymentScanAddress(derivedKeystore.getSilentPaymentScanAddress());
copyKeystore.getMasterPrivateKey().clear();
}
}

View File

@ -71,7 +71,7 @@ public interface KeystoreDao {
long id = insert(truncate(keystore.getLabel()), keystore.getSource().ordinal(), keystore.getWalletModel().ordinal(),
keystore.hasMasterPrivateKey() || wallet.isBip47() ? null : keystore.getKeyDerivation().getMasterFingerprint(),
keystore.getKeyDerivation().getDerivationPath(),
keystore.hasMasterPrivateKey() || wallet.isBip47() ? null : keystore.getExtendedPublicKey().toString(),
keystore.hasMasterPrivateKey() || wallet.isBip47() || keystore.getExtendedPublicKey() == null ? null : keystore.getExtendedPublicKey().toString(),
keystore.getExternalPaymentCode() == null ? null : keystore.getExternalPaymentCode().toString(),
keystore.getSilentPaymentScanAddress() == null ? null : keystore.getSilentPaymentScanAddress().toBytes(),
keystore.getDeviceRegistration(),

View File

@ -112,7 +112,7 @@ public interface WalletDao {
wallet.getKeystores().addAll(createKeystoreDao().getForWalletId(wallet.getId()));
List<WalletNode> walletNodes = createWalletNodeDao().getForWalletId(wallet.getScriptType().ordinal(), wallet.getId());
wallet.getPurposeNodes().addAll(walletNodes.stream().filter(walletNode -> walletNode.getDerivation().size() == 1).collect(Collectors.toList()));
wallet.getPurposeNodes().addAll(walletNodes.stream().filter(WalletNode::isPurposeNode).collect(Collectors.toList()));
wallet.getPurposeNodes().forEach(walletNode -> walletNode.setWallet(wallet));
Map<Sha256Hash, BlockTransaction> blockTransactions = createBlockTransactionDao().getForWalletId(wallet.getId());

View File

@ -16,7 +16,7 @@ public class WalletNodeMapper implements RowMapper<WalletNode> {
walletNode.setLabel(rs.getString("walletNode.label"));
byte[] addressData = rs.getBytes("walletNode.addressData");
if(addressData != null) {
ScriptType scriptType = ScriptType.values()[rs.getInt(6)];
ScriptType scriptType = ScriptType.values()[rs.getInt(7)];
walletNode.setAddress(scriptType.getAddress(addressData));
}
walletNode.setSilentPaymentTweak(rs.getBytes("walletNode.silentPaymentTweak"));

View File

@ -75,7 +75,7 @@ public class SettingsDialog extends WalletDialog {
Panel leftButtonPanel = new Panel();
leftButtonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1));
leftButtonPanel.addComponent(new Button("Add Account", this::showAddAccount));
if(getWalletForm().getWallet().getPolicyType() == PolicyType.SINGLE || getWalletForm().getWallet().getPolicyType() == PolicyType.SINGLE_SILENT_PAYMENTS) {
if(getWalletForm().getWallet().getPolicyType() == PolicyType.SINGLE_HD || getWalletForm().getWallet().getPolicyType() == PolicyType.SINGLE_SP) {
leftButtonPanel.addComponent(new Button("Show Seed", this::showSeed));
} else {
leftButtonPanel.addComponent(new EmptySpace(TerminalSize.ZERO));

View File

@ -4,6 +4,7 @@ import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.*;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentScanAddress;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
@ -86,6 +87,12 @@ public class KeystoreController extends WalletFormController implements Initiali
@FXML
private TextArea xpub;
@FXML
private Field spScanField;
@FXML
private TextArea spScan;
@FXML
private TextField derivation;
@ -152,12 +159,20 @@ public class KeystoreController extends WalletFormController implements Initiali
derivation.setPromptText(getWalletForm().getWallet().getScriptType().getDefaultDerivationPath());
if(keystore.getExtendedPublicKey() != null) {
xpubField.managedProperty().bind(xpubField.visibleProperty());
spScanField.managedProperty().bind(spScanField.visibleProperty());
spScanField.visibleProperty().bind(xpubField.visibleProperty().not());
if(getWalletForm().getWallet().getPolicyType() == PolicyType.SINGLE_SP && keystore.getSilentPaymentScanAddress() != null) {
spScan.setText(keystore.getSilentPaymentScanAddress().toKeyString());
setSpScanContext(keystore.getSilentPaymentScanAddress());
} else if(keystore.getExtendedPublicKey() != null) {
xpub.setText(keystore.getExtendedPublicKey().toString());
setXpubContext(keystore.getExtendedPublicKey());
} else {
switchXpubHeader.setDisable(true);
xpubField.setText(Network.get().getXpubHeader().getDisplayName() + ":");
spScanField.setText(Network.get().getSilentPaymentsScanKeyHrp() + ":");
}
if(keystore.getKeyDerivation() != null) {
@ -210,6 +225,17 @@ public class KeystoreController extends WalletFormController implements Initiali
}
scanXpubQR.setVisible(!valid);
});
spScan.textProperty().addListener((observable, oldValue, newValue) -> {
boolean valid = SilentPaymentScanAddress.isValid(newValue);
if(valid) {
SilentPaymentScanAddress silentPaymentScanAddress = SilentPaymentScanAddress.fromKeyString(newValue);
setSpScanContext(silentPaymentScanAddress);
if(!silentPaymentScanAddress.equals(keystore.getSilentPaymentScanAddress())) {
keystore.setSilentPaymentScanAddress(silentPaymentScanAddress);
EventManager.get().post(new SettingsChangedEvent(walletForm.getWallet(), SettingsChangedEvent.Type.KEYSTORE_SP_SCAN));
}
}
});
if(keystore.getSource() != KeystoreSource.SW_WATCH && (!walletForm.getWallet().isMasterWallet() || !walletForm.getWallet().getChildWallets().isEmpty())) {
setInputFieldsDisabled(true);
@ -252,6 +278,21 @@ public class KeystoreController extends WalletFormController implements Initiali
scanXpubQR.setVisible(false);
}
private void setSpScanContext(SilentPaymentScanAddress silentPaymentScanAddress) {
ContextMenu contextMenu = new ContextMenu();
MenuItem copySpScan = new MenuItem("Copy " + Network.get().getSilentPaymentsScanKeyHrp());
copySpScan.setOnAction(AE -> {
contextMenu.hide();
ClipboardContent content = new ClipboardContent();
content.putString(silentPaymentScanAddress.toKeyString());
Clipboard.getSystemClipboard().setContent(content);
});
contextMenu.getItems().add(copySpScan);
spScanField.setText(Network.get().getSilentPaymentsScanKeyHrp() + ":");
spScan.setContextMenu(contextMenu);
}
public void selectSource(ActionEvent event) {
keystoreSourceToggleGroup.selectToggle(null);
ToggleButton sourceButton = (ToggleButton)event.getSource();
@ -260,7 +301,8 @@ public class KeystoreController extends WalletFormController implements Initiali
launchImportDialog(keystoreSource);
} else {
fingerprint.setText(KeyDerivation.DEFAULT_WATCH_ONLY_FINGERPRINT);
derivation.setText(getWalletForm().getWallet().getScriptType().getDefaultDerivationPath());
derivation.setText(getWalletForm().getWallet().getPolicyType() == PolicyType.SINGLE_SP ? KeyDerivation.writePath(KeyDerivation.getBip352Derivation(0))
: getWalletForm().getWallet().getScriptType().getDefaultDerivationPath());
selectSourcePane.setVisible(false);
}
}
@ -283,12 +325,19 @@ public class KeystoreController extends WalletFormController implements Initiali
));
validationSupport.registerValidator(xpub, Validator.combine(
Validator.createEmptyValidator(Network.get().getXpubHeader().getDisplayName() + " is required"),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, Network.get().getXpubHeader().getDisplayName() + " is invalid", !ExtendedKey.isValid(newValue)),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Extended key is not unique", ExtendedKey.isValid(newValue) &&
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, Network.get().getXpubHeader().getDisplayName() + " is required", getWalletForm().getWallet().getPolicyType() != PolicyType.SINGLE_SP && newValue.trim().isEmpty()),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, Network.get().getXpubHeader().getDisplayName() + " is invalid", getWalletForm().getWallet().getPolicyType() != PolicyType.SINGLE_SP && !ExtendedKey.isValid(newValue)),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Extended key is not unique", ExtendedKey.isValid(newValue) && getWalletForm().getWallet().getPolicyType() != PolicyType.SINGLE_SP &&
walletForm.getWallet().getKeystores().stream().filter(k -> k != keystore && k.getExtendedPublicKey() != null).map(Keystore::getExtendedPublicKey).collect(Collectors.toList()).contains(ExtendedKey.fromDescriptor(newValue)))
));
validationSupport.registerValidator(spScan, Validator.combine(
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, Network.get().getSilentPaymentsScanKeyHrp() + " is required", getWalletForm().getWallet().getPolicyType() == PolicyType.SINGLE_SP && newValue.trim().isEmpty()),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, Network.get().getSilentPaymentsScanKeyHrp() + " is invalid", getWalletForm().getWallet().getPolicyType() == PolicyType.SINGLE_SP && !SilentPaymentScanAddress.isValid(newValue)),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, Network.get().getSilentPaymentsScanKeyHrp() + " is not unique", SilentPaymentScanAddress.isValid(newValue) && getWalletForm().getWallet().getPolicyType() == PolicyType.SINGLE_SP &&
walletForm.getWallet().getKeystores().stream().filter(k -> k != keystore && k.getSilentPaymentScanAddress() != null).map(Keystore::getSilentPaymentScanAddress).collect(Collectors.toList()).contains(SilentPaymentScanAddress.fromKeyString(newValue)))
));
validationSupport.registerValidator(derivation, Validator.combine(
Validator.createEmptyValidator("Derivation is required"),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Derivation is invalid", !KeyDerivation.isValid(newValue)),
@ -305,7 +354,7 @@ public class KeystoreController extends WalletFormController implements Initiali
private void updateType(boolean showExport) {
type.setText(getTypeLabel(keystore));
type.setGraphic(getTypeIcon(keystore));
exportButton.setVisible(showExport && getWalletForm().getWallet().getPolicyType() == PolicyType.MULTI);
exportButton.setVisible(showExport && getWalletForm().getWallet().getPolicyType() == PolicyType.MULTI_HD);
viewSeedButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasSeed());
viewKeyButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasMasterPrivateExtendedKey());
cardServiceButtons.setVisible(keystore.getWalletModel().isCard());
@ -319,6 +368,9 @@ public class KeystoreController extends WalletFormController implements Initiali
setEditable(derivation, editable);
setEditable(xpub, editable);
scanXpubQR.setVisible(editable);
setEditable(spScan, editable);
xpubField.setVisible(getWalletForm().getWallet().getPolicyType() != PolicyType.SINGLE_SP);
}
private void setEditable(TextInputControl textInputControl, boolean editable) {
@ -374,6 +426,9 @@ public class KeystoreController extends WalletFormController implements Initiali
private void launchImportDialog(KeystoreSource initialSource) {
boolean restrictImport = keystore.getSource() != KeystoreSource.SW_WATCH && keystoreSourceToggleGroup.getToggles().stream().anyMatch(toggle -> ((ToggleButton)toggle).isDisabled());
KeyDerivation currentDerivation = keystore.getKeyDerivation();
if((currentDerivation == null || currentDerivation.getDerivation().isEmpty()) && getWalletForm().getWallet().getPolicyType() == PolicyType.SINGLE_SP) {
currentDerivation = new KeyDerivation(KeyDerivation.DEFAULT_WATCH_ONLY_FINGERPRINT, KeyDerivation.getBip352Derivation(0));
}
WalletModel currentModel = keystore.getWalletModel();
String currentLabel = keystore.getLabel();
KeystoreImportDialog dlg = new KeystoreImportDialog(getWalletForm().getWallet(), initialSource, currentDerivation, currentModel, currentLabel, restrictImport);
@ -405,17 +460,22 @@ public class KeystoreController extends WalletFormController implements Initiali
keystore.setMasterPrivateExtendedKey(importedKeystore.getMasterPrivateExtendedKey());
keystore.setSeed(importedKeystore.getSeed());
keystore.setBip47ExtendedPrivateKey(importedKeystore.getBip47ExtendedPrivateKey());
keystore.setSilentPaymentScanAddress(importedKeystore.getSilentPaymentScanAddress());
updateType(keystore.isValid());
label.setText(keystore.getLabel());
fingerprint.setText(keystore.getKeyDerivation().getMasterFingerprint());
derivation.setText(keystore.getKeyDerivation().getDerivationPath());
if(keystore.getExtendedPublicKey() != null) {
if(getWalletForm().getWallet().getPolicyType() == PolicyType.SINGLE_SP && keystore.getSilentPaymentScanAddress() != null) {
spScan.setText(keystore.getSilentPaymentScanAddress().toKeyString());
setSpScanContext(keystore.getSilentPaymentScanAddress());
} else if(keystore.getExtendedPublicKey() != null) {
xpub.setText(keystore.getExtendedPublicKey().toString());
setXpubContext(keystore.getExtendedPublicKey());
} else {
xpub.setText("");
spScan.setText("");
}
}
}
@ -651,6 +711,7 @@ public class KeystoreController extends WalletFormController implements Initiali
setEditable(fingerprint, !disabled);
setEditable(derivation, !disabled);
setEditable(xpub, !disabled);
setEditable(spScan, !disabled);
importButton.setDisable(disabled);
}
@ -674,9 +735,12 @@ public class KeystoreController extends WalletFormController implements Initiali
derivation.setText(derivationPath + " ");
derivation.setText(derivationPath);
}
if(keystore.getExtendedPublicKey() != null) {
if(keystore.getExtendedPublicKey() != null && walletForm.getWallet().getPolicyType() != PolicyType.SINGLE_SP) {
setXpubContext(keystore.getExtendedPublicKey());
}
if(keystore.getSilentPaymentScanAddress() != null && walletForm.getWallet().getPolicyType() == PolicyType.SINGLE_SP) {
setSpScanContext(keystore.getSilentPaymentScanAddress());
}
} else if(event.getType().equals(SettingsChangedEvent.Type.KEYSTORE_LABEL)) {
if(!keystore.getLabel().equals(label.getText())) {
label.setText(keystore.getLabel());
@ -686,7 +750,7 @@ public class KeystoreController extends WalletFormController implements Initiali
if(event.getType().equals(SettingsChangedEvent.Type.KEYSTORE_LABEL) || event.getType().equals(SettingsChangedEvent.Type.KEYSTORE_FINGERPRINT) ||
event.getType().equals(SettingsChangedEvent.Type.KEYSTORE_DERIVATION) || event.getType().equals(SettingsChangedEvent.Type.KEYSTORE_XPUB)) {
if(keystore.getSource() == KeystoreSource.SW_WATCH) {
exportButton.setVisible(keystore.isValid() && getWalletForm().getWallet().getPolicyType() == PolicyType.MULTI);
exportButton.setVisible(keystore.isValid() && getWalletForm().getWallet().getPolicyType() == PolicyType.MULTI_HD);
}
}
}
@ -696,7 +760,8 @@ public class KeystoreController extends WalletFormController implements Initiali
public void keystoreLabelsChanged(KeystoreLabelsChangedEvent event) {
if(event.getWalletId().equals(walletForm.getWalletId())) {
for(Keystore changedKeystore : event.getChangedKeystores()) {
if(xpub.getText().trim().equals(changedKeystore.getExtendedPublicKey().toString()) && !label.getText().equals(changedKeystore.getLabel())) {
if(xpub.getText().trim().equals(changedKeystore.getExtendedPublicKey().toString()) && !label.getText().equals(changedKeystore.getLabel())
|| spScan.getText().trim().equals(changedKeystore.getSilentPaymentScanAddress().toKeyString()) && !label.getText().equals(changedKeystore.getLabel())) {
label.textProperty().removeListener(labelChangeListener);
label.setText(changedKeystore.getLabel());
keystore.setLabel(changedKeystore.getLabel());

View File

@ -11,6 +11,7 @@ import com.sparrowwallet.drongo.bip47.InvalidPaymentCodeException;
import com.sparrowwallet.drongo.bip47.PaymentCode;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
@ -314,14 +315,18 @@ public class PaymentController extends WalletFormController implements Initializ
label.requestFocus();
}
} else if(newValue != null) {
List<Address> existingAddresses = getOtherAddresses();
WalletNode freshNode = newValue.getFreshNode(KeyPurpose.RECEIVE);
Address freshAddress = freshNode.getAddress();
while(existingAddresses.contains(freshAddress) || (freshNode.getLabel() != null && !freshNode.getLabel().isEmpty())) {
freshNode = newValue.getFreshNode(KeyPurpose.RECEIVE, freshNode);
freshAddress = freshNode.getAddress();
if(newValue.getPolicyType() == PolicyType.SINGLE_SP) {
address.setText(newValue.getKeystores().getFirst().getSilentPaymentScanAddress().getSilentPaymentAddress().getAddress());
} else {
List<Address> existingAddresses = getOtherAddresses();
WalletNode freshNode = newValue.getFreshNode(KeyPurpose.RECEIVE);
Address freshAddress = freshNode.getAddress();
while(existingAddresses.contains(freshAddress) || (freshNode.getLabel() != null && !freshNode.getLabel().isEmpty())) {
freshNode = newValue.getFreshNode(KeyPurpose.RECEIVE, freshNode);
freshAddress = freshNode.getAddress();
}
address.setText(freshAddress.toString());
}
address.setText(freshAddress.toString());
label.requestFocus();
}
});

View File

@ -6,6 +6,7 @@ import com.sparrowwallet.drongo.crypto.*;
import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentScanAddress;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.hummingbird.registry.*;
@ -118,6 +119,8 @@ public class SettingsController extends WalletFormController implements Initiali
keystoreTabs = new TabPane();
keystoreTabsPane.getChildren().add(keystoreTabs);
policyType.setButtonCell(new PolicyTypeButtonCell());
policyType.setCellFactory(_ -> new PolicyTypeListCell());
policyType.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, policyType) -> {
walletForm.getWallet().setPolicyType(policyType);
@ -131,8 +134,8 @@ public class SettingsController extends WalletFormController implements Initiali
}
initialising = false;
multisigFieldset.setVisible(policyType.equals(PolicyType.MULTI));
if(policyType.equals(PolicyType.MULTI)) {
multisigFieldset.setVisible(policyType.equals(PolicyType.MULTI_HD));
if(policyType.equals(PolicyType.MULTI_HD)) {
totalKeystores.bind(multisigControl.highValueProperty());
} else {
totalKeystores.set(1);
@ -167,7 +170,7 @@ public class SettingsController extends WalletFormController implements Initiali
return;
} else if(optType.get() == ButtonType.YES) {
clearKeystoreTabs();
if(walletForm.getWallet().getPolicyType() == PolicyType.MULTI) {
if(walletForm.getWallet().getPolicyType() == PolicyType.MULTI_HD) {
totalKeystores.bind(multisigControl.highValueProperty());
} else {
totalKeystores.set(1);
@ -225,7 +228,7 @@ public class SettingsController extends WalletFormController implements Initiali
keystoreTabs.getTabs().remove(keystoreTabs.getTabs().size() - 1);
}
if(walletForm.getWallet().getPolicyType().equals(PolicyType.MULTI)) {
if(walletForm.getWallet().getPolicyType().equals(PolicyType.MULTI_HD)) {
EventManager.get().post(new SettingsChangedEvent(walletForm.getWallet(), SettingsChangedEvent.Type.MULTISIG_TOTAL));
}
});
@ -261,10 +264,10 @@ public class SettingsController extends WalletFormController implements Initiali
saveWallet(false, false);
Wallet wallet = walletForm.getWallet();
if(wallet.getPolicyType() == PolicyType.MULTI && wallet.getDefaultPolicy().getNumSignaturesRequired() < wallet.getKeystores().size() && addressChange) {
if(wallet.getPolicyType() == PolicyType.MULTI_HD && wallet.getDefaultPolicy().getNumSignaturesRequired() < wallet.getKeystores().size() && addressChange) {
String outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.DEFAULT_PURPOSES, null).toString(true);
CryptoOutput cryptoOutput = getCryptoOutput(wallet);
MultisigBackupDialog dialog = new MultisigBackupDialog(wallet, outputDescriptor, cryptoOutput.toUR());
RegistryItem registryItem = getUROutputDescriptor(wallet);
MultisigBackupDialog dialog = new MultisigBackupDialog(wallet, outputDescriptor, registryItem.toUR());
dialog.initOwner(apply.getScene().getWindow());
dialog.showAndWait();
}
@ -281,7 +284,7 @@ public class SettingsController extends WalletFormController implements Initiali
private void setFieldsFromWallet(Wallet wallet) {
if(wallet.getPolicyType() == null) {
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setPolicyType(PolicyType.SINGLE_HD);
wallet.setScriptType(ScriptType.P2WPKH);
Keystore keystore = new Keystore("Keystore 1");
keystore.setSource(KeystoreSource.SW_WATCH);
@ -290,9 +293,9 @@ public class SettingsController extends WalletFormController implements Initiali
wallet.setDefaultPolicy(Policy.getPolicy(wallet.getPolicyType(), wallet.getScriptType(), wallet.getKeystores(), 1));
}
if(wallet.getPolicyType().equals(PolicyType.SINGLE)) {
if(wallet.getPolicyType().equals(PolicyType.SINGLE_HD) || wallet.getPolicyType().equals(PolicyType.SINGLE_SP)) {
totalKeystores.setValue(1);
} else if(wallet.getPolicyType().equals(PolicyType.MULTI)) {
} else if(wallet.getPolicyType().equals(PolicyType.MULTI_HD)) {
multisigControl.setMax(Math.max(multisigControl.getMax(), wallet.getKeystores().size()));
multisigControl.highValueProperty().set(wallet.getKeystores().size());
multisigControl.lowValueProperty().set(wallet.getDefaultPolicy().getNumSignaturesRequired());
@ -376,8 +379,8 @@ public class SettingsController extends WalletFormController implements Initiali
}
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(walletForm.getWallet(), KeyPurpose.DEFAULT_PURPOSES, null);
CryptoOutput cryptoOutput = getCryptoOutput(walletForm.getWallet());
if(cryptoOutput == null) {
RegistryItem registryItem = getUROutputDescriptor(walletForm.getWallet());
if(registryItem == null) {
AppServices.showErrorDialog("Unsupported Wallet Policy", "Cannot show a descriptor for this wallet.");
return;
}
@ -385,32 +388,45 @@ public class SettingsController extends WalletFormController implements Initiali
boolean addBbqrOption = walletForm.getWallet().getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().showBbqr());
QREncoding encoding = walletForm.getWallet().getKeystores().stream().allMatch(keystore -> keystore.getWalletModel().selectBbqr()) ? QREncoding.BBQR : QREncoding.UR;
UR cryptoOutputUR = cryptoOutput.toUR();
UR cryptoOutputUR = registryItem.toUR();
BBQR bbqr = addBbqrOption ? new BBQR(BBQRType.UNICODE, outputDescriptor.toString(true).getBytes(StandardCharsets.UTF_8)) : null;
QRDisplayDialog qrDisplayDialog = new DescriptorQRDisplayDialog(walletForm.getWallet().getFullDisplayName(), outputDescriptor.toString(true), cryptoOutputUR, bbqr, encoding);
qrDisplayDialog.initOwner(showDescriptorQR.getScene().getWindow());
qrDisplayDialog.showAndWait();
}
public static CryptoOutput getCryptoOutput(Wallet wallet) {
public static RegistryItem getUROutputDescriptor(Wallet wallet) {
List<ScriptExpression> scriptExpressions = getScriptExpressions(wallet.getScriptType());
CryptoOutput cryptoOutput = null;
if(wallet.getPolicyType() == PolicyType.SINGLE) {
cryptoOutput = new CryptoOutput(scriptExpressions, getCryptoHDKey(wallet.getKeystores().get(0)));
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
RegistryItem registryItem = null;
if(wallet.getPolicyType() == PolicyType.SINGLE_HD) {
Keystore keystore = wallet.getKeystores().getFirst();
KeyDerivation keyDerivation = keystore.getKeyDerivation();
registryItem = new CryptoOutput(scriptExpressions, getCryptoHDKey(keyDerivation.getMasterFingerprint(), keyDerivation.getDerivation(), keystore.getExtendedPublicKey(), keystore.getLabel()));
} else if(wallet.getPolicyType() == PolicyType.MULTI_HD) {
WalletNode firstReceive = new WalletNode(wallet, KeyPurpose.RECEIVE, 0);
Utils.LexicographicByteArrayComparator lexicographicByteArrayComparator = new Utils.LexicographicByteArrayComparator();
List<CryptoHDKey> cryptoHDKeys = wallet.getKeystores().stream().sorted((keystore1, keystore2) -> {
return lexicographicByteArrayComparator.compare(keystore1.getPubKey(firstReceive).getPubKey(), keystore2.getPubKey(firstReceive).getPubKey());
}).map(SettingsController::getCryptoHDKey).collect(Collectors.toList());
}).map(keystore -> {
KeyDerivation keyDerivation = keystore.getKeyDerivation();
return getCryptoHDKey(keyDerivation.getMasterFingerprint(), keyDerivation.getDerivation(), keystore.getExtendedPublicKey(), keystore.getLabel());
}).collect(Collectors.toList());
MultiKey multiKey = new MultiKey(wallet.getDefaultPolicy().getNumSignaturesRequired(), null, cryptoHDKeys);
List<ScriptExpression> multiScriptExpressions = new ArrayList<>(scriptExpressions);
multiScriptExpressions.add(ScriptExpression.SORTED_MULTISIG);
cryptoOutput = new CryptoOutput(multiScriptExpressions, multiKey);
registryItem = new CryptoOutput(multiScriptExpressions, multiKey);
} else if(wallet.getPolicyType() == PolicyType.SINGLE_SP) {
Keystore keystore = wallet.getKeystores().getFirst();
KeyDerivation keyDerivation = keystore.getKeyDerivation();
SilentPaymentScanAddress spScanAddress = keystore.getSilentPaymentScanAddress();
URHDKey scanKey = getURHDKey(keyDerivation.getMasterFingerprint(), KeyDerivation.getBip352ScanDerivation(keyDerivation.getDerivation()), spScanAddress.getScanKey(), keystore.getLabel());
URHDKey spendKey = getURHDKey(keyDerivation.getMasterFingerprint(), KeyDerivation.getBip352SpendDerivation(keyDerivation.getDerivation()), spScanAddress.getSpendKey(), keystore.getLabel());
String annotations = wallet.getBirthHeight() != null ? "?" + OutputDescriptor.ANNOTATION_BLOCK_HEIGHT + "=" + wallet.getBirthHeight() : "";
registryItem = new UROutputDescriptor("sp(@0,@1)" + annotations, List.of(scanKey, spendKey), wallet.getFullDisplayName(), null);
}
return cryptoOutput;
return registryItem;
}
private static List<ScriptExpression> getScriptExpressions(ScriptType scriptType) {
@ -435,12 +451,18 @@ public class SettingsController extends WalletFormController implements Initiali
throw new IllegalArgumentException("Unknown script type of " + scriptType);
}
private static CryptoHDKey getCryptoHDKey(Keystore keystore) {
ExtendedKey extendedKey = keystore.getExtendedPublicKey();
private static CryptoHDKey getCryptoHDKey(String masterFingerprint, List<ChildNumber> derivation, ExtendedKey extendedKey, String label) {
CryptoCoinInfo cryptoCoinInfo = new CryptoCoinInfo(CryptoCoinInfo.Type.BITCOIN.ordinal(), Network.get() == Network.MAINNET ? CryptoCoinInfo.Network.MAINNET.ordinal() : CryptoCoinInfo.Network.TESTNET.ordinal());
List<PathComponent> pathComponents = keystore.getKeyDerivation().getDerivation().stream().map(cNum -> new IndexPathComponent(cNum.num(), cNum.isHardened())).collect(Collectors.toList());
CryptoKeypath cryptoKeypath = new CryptoKeypath(pathComponents, Utils.hexToBytes(keystore.getKeyDerivation().getMasterFingerprint()), pathComponents.size());
return new CryptoHDKey(false, extendedKey.getKey().getPubKey(), extendedKey.getKey().getChainCode(), cryptoCoinInfo, cryptoKeypath, null, extendedKey.getParentFingerprint(), keystore.getLabel(), null);
List<PathComponent> pathComponents = derivation.stream().map(cNum -> new IndexPathComponent(cNum.num(), cNum.isHardened())).collect(Collectors.toList());
CryptoKeypath cryptoKeypath = new CryptoKeypath(pathComponents, Utils.hexToBytes(masterFingerprint), pathComponents.size());
return new CryptoHDKey(false, extendedKey.getKey().getPubKey(), extendedKey.getKey().getChainCode(), cryptoCoinInfo, cryptoKeypath, null, extendedKey.getParentFingerprint(), label, null);
}
private static URHDKey getURHDKey(String masterFingerprint, List<ChildNumber> derivation, ECKey key, String label) {
URCoinInfo cryptoCoinInfo = new URCoinInfo(URCoinInfo.Type.BITCOIN.ordinal(), Network.get() == Network.MAINNET ? URCoinInfo.Network.MAINNET.ordinal() : URCoinInfo.Network.TESTNET.ordinal());
List<PathComponent> pathComponents = derivation.stream().map(cNum -> new IndexPathComponent(cNum.num(), cNum.isHardened())).collect(Collectors.toList());
URKeypath cryptoKeypath = new URKeypath(pathComponents, Utils.hexToBytes(masterFingerprint), pathComponents.size());
return new URHDKey(key.hasPrivKey(), key.hasPrivKey() ? key.getPrivKeyBytes() : key.getPubKey(), null, cryptoCoinInfo, cryptoKeypath, null, null, label, null);
}
public void editDescriptor(ActionEvent event) {
@ -451,7 +473,7 @@ public class SettingsController extends WalletFormController implements Initiali
dialog.initOwner(editDescriptor.getScene().getWindow());
dialog.setTitle("Edit wallet output descriptor");
dialog.getDialogPane().setHeaderText("The wallet configuration is specified in the output descriptor.\nChanges to the output descriptor will modify the wallet configuration." +
(walletForm.getWallet().getPolicyType() == PolicyType.MULTI ? "\nKey expressions are shown in canonical order." : ""));
(walletForm.getWallet().getPolicyType() == PolicyType.MULTI_HD ? "\nKey expressions are shown in canonical order." : ""));
Optional<String> text = dialog.showAndWait();
if(text.isPresent() && !text.get().isEmpty() && !text.get().equals(outputDescriptorString)) {
if(text.get().contains("(multi(")) {
@ -514,6 +536,7 @@ public class SettingsController extends WalletFormController implements Initiali
keystore.setWalletModel(existing.getWalletModel());
if(existing.getKeyDerivation().getDerivation().equals(keystore.getKeyDerivation().getDerivation())) {
keystore.setExtendedPublicKey(existing.getExtendedPublicKey());
keystore.setSilentPaymentScanAddress(existing.getSilentPaymentScanAddress());
} else {
rederive = true;
}
@ -597,7 +620,7 @@ public class SettingsController extends WalletFormController implements Initiali
dialog.initOwner(showDescriptor.getScene().getWindow());
dialog.setTitle("Show wallet output descriptor");
dialog.getDialogPane().setHeaderText("The wallet configuration is specified in the output descriptor.\nThis wallet is no longer editable - create a new wallet to change the descriptor." +
(walletForm.getWallet().getPolicyType() == PolicyType.MULTI ? "\nKey expressions are shown in canonical order." : ""));
(walletForm.getWallet().getPolicyType() == PolicyType.MULTI_HD ? "\nKey expressions are shown in canonical order." : ""));
dialog.showAndWait();
}
@ -724,7 +747,7 @@ public class SettingsController extends WalletFormController implements Initiali
}
}
} else {
if(discoverAccounts && masterWallet.getKeystores().size() == 1 && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)) {
if(discoverAccounts && masterWallet.getPolicyType() == PolicyType.SINGLE_HD && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)) {
String fingerprint = masterWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint();
DeviceKeystoreDiscoverDialog deviceKeystoreDiscoverDialog = new DeviceKeystoreDiscoverDialog(List.of(fingerprint), masterWallet, standardAccounts);
deviceKeystoreDiscoverDialog.initOwner(addAccount.getScene().getWindow());
@ -827,9 +850,9 @@ public class SettingsController extends WalletFormController implements Initiali
public void update(SettingsChangedEvent event) {
Wallet wallet = event.getWallet();
if(walletForm.getWallet().equals(wallet)) {
if(wallet.getPolicyType() == PolicyType.SINGLE) {
if(wallet.getPolicyType() == PolicyType.SINGLE_HD || wallet.getPolicyType() == PolicyType.SINGLE_SP) {
wallet.setDefaultPolicy(Policy.getPolicy(wallet.getPolicyType(), wallet.getScriptType(), wallet.getKeystores(), 1));
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
} else if(wallet.getPolicyType() == PolicyType.MULTI_HD) {
wallet.setDefaultPolicy(Policy.getPolicy(wallet.getPolicyType(), wallet.getScriptType(), wallet.getKeystores(), (int)multisigControl.getLowValue()));
}
@ -1040,4 +1063,34 @@ public class SettingsController extends WalletFormController implements Initiali
apply.setDisable(false);
}
}
private static class PolicyTypeButtonCell extends ListCell<PolicyType> {
@Override
protected void updateItem(PolicyType policyType, boolean empty) {
super.updateItem(policyType, empty);
if(policyType == null || empty) {
setText("");
setGraphic(null);
} else {
setText(policyType.getName());
setGraphic(null);
setGraphicTextGap(8.0d);
}
}
}
private static class PolicyTypeListCell extends ListCell<PolicyType> {
@Override
protected void updateItem(PolicyType policyType, boolean empty) {
super.updateItem(policyType, empty);
if(policyType == null || empty) {
setText("");
setGraphic(null);
} else {
setText(policyType.getDescription());
setGraphic(null);
setGraphicTextGap(8.0d);
}
}
}
}

View File

@ -13,7 +13,7 @@
-fx-pref-width: 140px;
}
#fingerprint, #derivation, #xpub {
#fingerprint, #derivation, #xpub, #spScan {
-fx-font-size: 13px;
-fx-font-family: 'Fragment Mono Regular';
}

View File

@ -109,6 +109,9 @@
</Button>
</VBox>
</Field>
<Field fx:id="spScanField" text="spscan:">
<TextArea fx:id="spScan" wrapText="true" prefRowCount="2" maxHeight="52" />
</Field>
</Fieldset>
</Form>
<StackPane fx:id="selectSourcePane">
@ -141,9 +144,13 @@
<KeystoreSource fx:constant="SW_SEED"/>
</userData>
</ToggleButton>
<ToggleButton text="xPub / Watch Only Wallet" contentDisplay="TOP" wrapText="true" textAlignment="CENTER" toggleGroup="$keystoreSourceToggleGroup" onAction="#selectSource">
<ToggleButton contentDisplay="CENTER" toggleGroup="$keystoreSourceToggleGroup" onAction="#selectSource">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="EYE" />
<Label text="Watch Only Wallet" wrapText="true" textAlignment="CENTER" maxWidth="90" contentDisplay="TOP">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="EYE" />
</graphic>
</Label>
</graphic>
<userData>
<KeystoreSource fx:constant="SW_WATCH"/>

View File

@ -32,12 +32,12 @@
<Form GridPane.columnIndex="0" GridPane.rowIndex="0">
<Fieldset inputGrow="SOMETIMES" text="Settings" styleClass="header">
<Field text="Policy Type:">
<ComboBox fx:id="policyType">
<ComboBox fx:id="policyType" prefWidth="160">
<items>
<FXCollections fx:factory="observableArrayList">
<PolicyType fx:constant="SINGLE" />
<PolicyType fx:constant="MULTI" />
<!-- <PolicyType fx:constant="CUSTOM" /> -->
<PolicyType fx:constant="SINGLE_HD" />
<PolicyType fx:constant="MULTI_HD" />
<PolicyType fx:constant="SINGLE_SP" />
</FXCollections>
</items>
</ComboBox>