What changed, and why it matters
This commit adds initial support for a new type of Bitcoin wallet called 'silent payments' (abbreviated as 'sp' or 'SINGLE_SP') to the Sparrow Wallet application. It updates many parts of the user interface and storage logic so that these wallets can be created, displayed, exported, and used alongside existing single-key and multi-key wallets. The changes are broad but appear to be a normal feature addition rather than a fix for a known security flaw.
Treat this as a feature commit, not a security patch. Reviewers should validate that silent payment address computation in PrivateKeySweepDialog correctly handles key negation for P2TR, that the new spScan key is validated and stored securely, and that the WalletNodeMapper column index change does not introduce data corruption. No immediate security response is indicated by the available evidence.
Security signals we found
Large feature commit touching 25 files with wallet policy type changes
New silent payment address handling in PrivateKeySweepDialog
QR/UR output descriptor generation updated for SINGLE_SP wallets
Database mapper column index fix in WalletNodeMapper (rs.getInt(6) -> rs.getInt(7))
KeystoreDao null-check added for extended public key before serialization
Evidence from the diff
The commit introduces PolicyType.SINGLE_SP (silent payments) support across the Sparrow codebase. It renames existing policy type references from SINGLE/MULTI to SINGLE_HD/MULTI_HD, adds UI fields for silent payment scan keys (spScan), updates descriptor export/import, QR code generation, private key sweeping to silent payment addresses, wallet storage/DB mapping, and validation logic. The drongo submodule is also updated. No explicit security bug fix or vulnerability disclosure is present in the commit message or diff.
Changed components
Sparrow Wallet desktop applicationWallet policy type handlingKeystore UI controller and FXMLDescriptor import/exportQR scanning and UR/CryptoOutput generationPrivate key sweep dialogWallet database storage layerdrongo submodule (wallet/silent payments primitives)Inspect captured patch +315 / −112
diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java
index 3f972f6..053418c 100644
--- a/src/main/java/com/sparrowwallet/sparrow/AppController.java
+++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java
@@ -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);
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java
index b0a111b..b5f2df9 100644
--- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java
+++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java
@@ -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
diff --git a/src/main/java/com/sparrowwallet/sparrow/BaseController.java b/src/main/java/com/sparrowwallet/sparrow/BaseController.java
index cee2d97..29e33e0 100644
--- a/src/main/java/com/sparrowwallet/sparrow/BaseController.java
+++ b/src/main/java/com/sparrowwallet/sparrow/BaseController.java
@@ -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();
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/AddAccountDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/AddAccountDialog.java
index fab81a5..964debb 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/AddAccountDialog.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/AddAccountDialog.java
@@ -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());
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/AddressTreeTable.java b/src/main/java/com/sparrowwallet/sparrow/control/AddressTreeTable.java
index 9f9ab8f..332ef7d 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/AddressTreeTable.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/AddressTreeTable.java
@@ -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();
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DescriptorArea.java b/src/main/java/com/sparrowwallet/sparrow/control/DescriptorArea.java
index fb3a7f0..f934632 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/DescriptorArea.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/DescriptorArea.java
@@ -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() {
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java
index 53ad4bd..7df69be 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java
@@ -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) {
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java
index 81ffd20..f95c948 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java
@@ -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());
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/PrivateKeySweepDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/PrivateKeySweepDialog.java
index 912d3a8..87cbc34 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/PrivateKeySweepDialog.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/PrivateKeySweepDialog.java
@@ -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() {
+ if(silentPaymentAddress != null) {
+ return true;
+ }
try {
- Address address = getToAddress();
+ getToAddress();
return true;
- } catch (InvalidAddressException e) {
+ } 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);
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java
index 4f4e290..9e384f8 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java
@@ -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");
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java
index 82d12a9..14be0ce 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java
@@ -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());
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/event/SettingsChangedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/SettingsChangedEvent.java
index ddfb099..199a15f 100644
--- a/src/main/java/com/sparrowwallet/sparrow/event/SettingsChangedEvent.java
+++ b/src/main/java/com/sparrowwallet/sparrow/event/SettingsChangedEvent.java
@@ -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;
}
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Descriptor.java b/src/main/java/com/sparrowwallet/sparrow/io/Descriptor.java
index ea8acb1..7ba96de 100644
--- a/src/main/java/com/sparrowwallet/sparrow/io/Descriptor.java
+++ b/src/main/java/com/sparrowwallet/sparrow/io/Descriptor.java
@@ -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();
-
- 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();
+ 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();
+
+ 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) {
diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java
index 71c2f1e..f461754 100644
--- a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java
+++ b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java
@@ -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();
}
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/io/db/KeystoreDao.java b/src/main/java/com/sparrowwallet/sparrow/io/db/KeystoreDao.java
index e417322..25fd20a 100644
--- a/src/main/java/com/sparrowwallet/sparrow/io/db/KeystoreDao.java
+++ b/src/main/java/com/sparrowwallet/sparrow/io/db/KeystoreDao.java
@@ -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(),
diff --git a/src/main/java/com/sparrowwallet/sparrow/io/db/WalletDao.java b/src/main/java/com/sparrowwallet/sparrow/io/db/WalletDao.java
index 0ee0281..36005bf 100644
--- a/src/main/java/com/sparrowwallet/sparrow/io/db/WalletDao.java
+++ b/src/main/java/com/sparrowwallet/sparrow/io/db/WalletDao.java
@@ -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());
diff --git a/src/main/java/com/sparrowwallet/sparrow/io/db/WalletNodeMapper.java b/src/main/java/com/sparrowwallet/sparrow/io/db/WalletNodeMapper.java
index a23b86b..b84a3f0 100644
--- a/src/main/java/com/sparrowwallet/sparrow/io/db/WalletNodeMapper.java
+++ b/src/main/java/com/sparrowwallet/sparrow/io/db/WalletNodeMapper.java
@@ -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"));
diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/SettingsDialog.java b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/SettingsDialog.java
index fb7f1ca..ca879b9 100644
--- a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/SettingsDialog.java
+++ b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/SettingsDialog.java
@@ -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));
diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java
index 3b547a6..5ac22c1 100644
--- a/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java
+++ b/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java
@@ -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());
diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java
index 11cd858..a8f98c9 100644
--- a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java
+++ b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java
@@ -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();
}
});
diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java
index 158515a..d3f4ba3 100644
--- a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java
+++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java
@@ -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);
+ }
+ }
+ }
}
diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.css b/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.css
index 501db4a..9a6e90a 100644
--- a/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.css
+++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.css
@@ -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';
}
diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.fxml b/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.fxml
index a690d1c..58bfd5b 100644
--- a/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.fxml
+++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.fxml
@@ -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"/>
diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/settings.fxml b/src/main/resources/com/sparrowwallet/sparrow/wallet/settings.fxml
index 22d7cb7..3623cd1 100644
--- a/src/main/resources/com/sparrowwallet/sparrow/wallet/settings.fxml
+++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/settings.fxml
@@ -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>
Why this scored 28/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.