support retrieving silent payments spscan keys via connected devices

This commit is contained in:
Craig Raw
2026-04-27 08:13:35 +02:00
parent 0459f4ca45
commit f5ee7bf277
6 changed files with 90 additions and 3 deletions
+1 -1
Submodule lark updated: a6371b74df...45e7a2f97e
@@ -12,6 +12,7 @@ import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentScanAddress;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
@@ -730,13 +731,21 @@ public class DevicePane extends TitledDescriptionPane {
}
}
importXpub(derivation);
importKey(derivation);
});
enumerateService.setOnFailed(workerStateEvent -> {
setError("Error", enumerateService.getException().getMessage());
importButton.setDisable(false);
});
enumerateService.start();
} else {
importKey(derivation);
}
}
private void importKey(List<ChildNumber> derivation) {
if(wallet != null && wallet.getPolicyType() == PolicyType.SINGLE_SP) {
importSpscan(derivation);
} else {
importXpub(derivation);
}
@@ -771,6 +780,35 @@ public class DevicePane extends TitledDescriptionPane {
getXpubService.start();
}
private void importSpscan(List<ChildNumber> derivation) {
String derivationPath = KeyDerivation.writePath(derivation);
Hwi.GetSpscanService getSpscanService = new Hwi.GetSpscanService(device, passphrase.get(), derivationPath);
getSpscanService.setOnSucceeded(workerStateEvent -> {
SilentPaymentScanAddress spscan = getSpscanService.getValue();
try {
Keystore keystore = new Keystore();
keystore.setLabel(device.getModel().toDisplayString());
keystore.setSource(KeystoreSource.HW_USB);
keystore.setWalletModel(device.getModel());
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
keystore.setSilentPaymentScanAddress(spscan);
importKeystore(derivation, keystore);
} catch(Exception e) {
setError("Could not retrieve spscan", e.getMessage());
}
});
getSpscanService.setOnFailed(workerStateEvent -> {
setError("Could not retrieve spscan", getSpscanService.getException().getMessage());
importButton.setDisable(false);
});
setDescription("Importing...");
showHideLink.setVisible(false);
getSpscanService.start();
}
private void importKeystore(List<ChildNumber> derivation, Keystore keystore) {
if(wallet.getScriptType() == null) {
ScriptType scriptType = Arrays.stream(ScriptType.ADDRESSABLE_TYPES).filter(type -> type.getDefaultDerivation().get(0).equals(derivation.get(0))).findFirst().orElse(ScriptType.P2PKH);
@@ -1179,7 +1217,7 @@ public class DevicePane extends TitledDescriptionPane {
showHideLink.setVisible(true);
setExpanded(false);
List<ChildNumber> importDerivation = KeyDerivation.parsePath(derivationField.getText());
importXpub(importDerivation);
importKey(importDerivation);
});
derivationField.textProperty().addListener((observable, oldValue, newValue) -> {
@@ -6,6 +6,7 @@ import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentScanAddress;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.lark.DeviceException;
@@ -167,6 +168,20 @@ public class Hwi {
}
}
public SilentPaymentScanAddress getSpscan(Device device, String passphrase, String derivationPath) throws ImportException {
try {
Lark lark = getLark(passphrase);
SilentPaymentScanAddress spscan = lark.getSpscanAtPath(device.getType(), device.getPath(), derivationPath);
isPromptActive = false;
return spscan;
} catch(DeviceException e) {
throw new ImportException(e.getMessage(), e);
} catch(RuntimeException e) {
log.error("Error retrieving spscan", e);
throw e;
}
}
public String displayAddress(Device device, String passphrase, ScriptType scriptType, OutputDescriptor addressDescriptor,
OutputDescriptor walletDescriptor, String walletName, byte[] walletRegistration) throws DisplayAddressException {
try {
@@ -443,6 +458,28 @@ public class Hwi {
}
}
public static class GetSpscanService extends Service<SilentPaymentScanAddress> {
private final Device device;
private final String passphrase;
private final String derivationPath;
public GetSpscanService(Device device, String passphrase, String derivationPath) {
this.device = device;
this.passphrase = passphrase;
this.derivationPath = derivationPath;
}
@Override
protected Task<SilentPaymentScanAddress> createTask() {
return new Task<>() {
protected SilentPaymentScanAddress call() throws ImportException {
Hwi hwi = new Hwi();
return hwi.getSpscan(device, passphrase, derivationPath);
}
};
}
}
public static class GetXpubsService extends Service<Map<WalletType, String>> {
private final Device device;
private final String passphrase;
@@ -54,6 +54,10 @@ public class Tapsigner implements KeystoreCardImport {
@Override
public Keystore getKeystore(PolicyType policyType, String pin, List<ChildNumber> derivation, StringProperty messageProperty) throws ImportException {
if(policyType == PolicyType.SINGLE_SP) {
throw new ImportException(getName() + " does not support receiving silent payments");
}
if(pin.length() < 6) {
throw new ImportException("PIN too short.");
}
@@ -54,6 +54,10 @@ public class Keycard implements KeystoreCardImport {
@Override
public Keystore getKeystore(PolicyType policyType, String pin, List<ChildNumber> derivation, StringProperty messageProperty) throws ImportException {
if(policyType == PolicyType.SINGLE_SP) {
throw new ImportException(getName() + " does not support receiving silent payments");
}
if(!StringUtils.isNumeric(pin)) {
throw new ImportException("PIN must be all digits.");
}
@@ -53,6 +53,10 @@ public class Satochip implements KeystoreCardImport {
@Override
public Keystore getKeystore(PolicyType policyType, String pin, List<ChildNumber> derivation, StringProperty messageProperty) throws ImportException {
if(policyType == PolicyType.SINGLE_SP) {
throw new ImportException(getName() + " does not support receiving silent payments");
}
if(pin.length() < 4) {
throw new ImportException("PIN too short.");
}