implement silent payments receive ui

This commit is contained in:
Craig Raw
2026-04-23 13:46:40 +02:00
parent c67008840d
commit 1c5602aa9d
5 changed files with 193 additions and 18 deletions
@@ -14,7 +14,7 @@ import java.util.Map;
public class QRCodeDialog extends DialogWindow {
public QRCodeDialog(String data) throws WriterException {
super(data);
super(data.length() > 70 ? data.substring(0, 70) + "..." : data);
setHints(List.of(Hint.CENTERED));
@@ -4,6 +4,7 @@ import com.google.common.eventbus.Subscribe;
import com.googlecode.lanterna.gui2.*;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.WalletNode;
@@ -38,19 +39,28 @@ public class ReceiveDialog extends WalletDialog {
Panel mainPanel = new Panel(new GridLayout(2).setHorizontalSpacing(2).setVerticalSpacing(1).setTopMarginSize(1));
boolean isSilentPayments = walletForm.getWallet().getPolicyType() == PolicyType.SINGLE_SP;
mainPanel.addComponent(new Label("Address"));
address = new Label("").addTo(mainPanel);
mainPanel.addComponent(new Label("Derivation"));
derivation = new Label("").addTo(mainPanel);
if(!isSilentPayments) {
mainPanel.addComponent(new Label("Derivation"));
derivation = new Label("").addTo(mainPanel);
mainPanel.addComponent(new Label("Last Used"));
lastUsed = new Label("").addTo(mainPanel);
mainPanel.addComponent(new Label("Last Used"));
lastUsed = new Label("").addTo(mainPanel);
} else {
derivation = new Label("");
lastUsed = new Label("");
}
Panel buttonPanel = new Panel();
buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1));
buttonPanel.addComponent(new Button("Back", () -> onBack(Function.RECEIVE)));
buttonPanel.addComponent(new Button("Get Fresh Address", this::refreshAddress).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, true, false)));
if(!isSilentPayments) {
buttonPanel.addComponent(new Button("Get Fresh Address", this::refreshAddress).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, true, false)));
}
mainPanel.addComponent(new Button("Show QR", this::showQR));
@@ -61,12 +71,17 @@ public class ReceiveDialog extends WalletDialog {
}
public void showQR() {
if(currentEntry == null) {
return;
}
try {
QRCodeDialog qrCodeDialog = new QRCodeDialog(currentEntry.getAddress().toString());
String qrAddress;
if(getWalletForm().getWallet().getPolicyType() == PolicyType.SINGLE_SP) {
qrAddress = getWalletForm().getWallet().getKeystores().getFirst().getSilentPaymentScanAddress().getAddress();
} else if(currentEntry != null) {
qrAddress = currentEntry.getAddress().toString();
} else {
return;
}
QRCodeDialog qrCodeDialog = new QRCodeDialog(qrAddress);
qrCodeDialog.showDialog(SparrowTerminal.get().getGui());
} catch(Exception e) {
log.error("Error creating QR", e);
@@ -76,6 +91,12 @@ public class ReceiveDialog extends WalletDialog {
public void refreshAddress() {
SparrowTerminal.get().getGuiThread().invokeLater(() -> {
if(getWalletForm().getWallet().getPolicyType() == PolicyType.SINGLE_SP) {
String silentPaymentAddress = getWalletForm().getWallet().getKeystores().getFirst().getSilentPaymentScanAddress().getAddress();
address.setText(silentPaymentAddress);
return;
}
NodeEntry freshEntry = getWalletForm().getFreshNodeEntry(KeyPurpose.RECEIVE, currentEntry);
setNodeEntry(freshEntry);
});
@@ -109,6 +130,10 @@ public class ReceiveDialog extends WalletDialog {
}
private void updateLastUsed() {
if(currentEntry == null) {
return;
}
SparrowTerminal.get().getGuiThread().invokeLater(() -> {
Set<BlockTransactionHashIndex> currentOutputs = currentEntry.getNode().getTransactionOutputs();
if(AppServices.onlineProperty().get() && currentOutputs.isEmpty()) {
@@ -9,6 +9,7 @@ import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
@@ -20,6 +21,8 @@ import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Device;
import com.sparrowwallet.sparrow.io.Hwi;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
@@ -27,9 +30,14 @@ import javafx.fxml.Initializable;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.layout.AnchorPane;
import javafx.util.Duration;
import org.controlsfx.glyphfont.Glyph;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tornadofx.control.Form;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -44,6 +52,9 @@ public class ReceiveController extends WalletFormController implements Initializ
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
@FXML
private Form receiveForm;
@FXML
private CopyableTextField address;
@@ -56,18 +67,45 @@ public class ReceiveController extends WalletFormController implements Initializ
@FXML
private Label lastUsed;
@FXML
private AnchorPane qrCodePane;
@FXML
private ImageView qrCode;
@FXML
private Form spReceiveForm;
@FXML
private AnchorPane spCopyPane;
@FXML
private TextArea spAddress;
@FXML
private Form scriptPubKeyForm;
@FXML
private ScriptArea scriptPubKeyArea;
@FXML
private Form outputDescriptorForm;
@FXML
private SelectableCodeArea outputDescriptor;
@FXML
private Form spQrForm;
@FXML
private ImageView spQrCode;
@FXML
private Button displayAddress;
@FXML
private Button nextAddress;
private NodeEntry currentEntry;
private QRDisplayDialog addressQrDialog;
@@ -80,12 +118,39 @@ public class ReceiveController extends WalletFormController implements Initializ
@Override
public void initializeView() {
address.setSkin(new AddressTextFieldSkin(address));
receiveForm.managedProperty().bind(receiveForm.visibleProperty());
qrCodePane.managedProperty().bind(qrCodePane.visibleProperty());
spReceiveForm.managedProperty().bind(spReceiveForm.visibleProperty());
spCopyPane.managedProperty().bind(spCopyPane.visibleProperty());
scriptPubKeyForm.managedProperty().bind(scriptPubKeyForm.visibleProperty());
outputDescriptorForm.managedProperty().bind(outputDescriptorForm.visibleProperty());
spQrForm.managedProperty().bind(spQrForm.visibleProperty());
qrCodePane.visibleProperty().bind(receiveForm.visibleProperty());
spReceiveForm.visibleProperty().bind(receiveForm.visibleProperty().not());
spCopyPane.visibleProperty().bind(receiveForm.visibleProperty().not());
scriptPubKeyForm.visibleProperty().bind(receiveForm.visibleProperty());
outputDescriptorForm.visibleProperty().bind(receiveForm.visibleProperty());
spQrForm.visibleProperty().bind(receiveForm.visibleProperty().not());
updateFromWalletPolicy();
initializeScriptField(scriptPubKeyArea);
displayAddress.managedProperty().bind(displayAddress.visibleProperty());
displayAddress.setVisible(false);
qrCode.setOnMouseClicked(event -> {
nextAddress.managedProperty().bind(nextAddress.visibleProperty());
spAddress.setOnMouseClicked(event -> {
copySilentPaymentsAddress(null);
Tooltip tooltip = new Tooltip("Copied!");
tooltip.show(spAddress, event.getScreenX(), event.getScreenY());
Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(1), e -> tooltip.hide()));
timeline.play();
});
qrCode.setOnMouseClicked(_ -> {
if(currentEntry != null && addressQrDialog == null) {
addressQrDialog = new QRDisplayDialog(currentEntry.getAddress().toString());
addressQrDialog.initOwner(address.getScene().getWindow());
@@ -94,9 +159,16 @@ public class ReceiveController extends WalletFormController implements Initializ
}
});
spQrCode.setOnMouseClicked(_ -> copySilentPaymentsAddress(null));
refreshAddress();
}
public void updateFromWalletPolicy() {
receiveForm.setVisible(walletForm.getWallet().getPolicyType() != PolicyType.SINGLE_SP);
nextAddress.setVisible(walletForm.getWallet().getPolicyType() != PolicyType.SINGLE_SP);
}
public void setNodeEntry(NodeEntry nodeEntry) {
if(currentEntry != null) {
label.textProperty().unbindBidirectional(currentEntry.labelProperty());
@@ -128,6 +200,10 @@ public class ReceiveController extends WalletFormController implements Initializ
}
private void updateLastUsed() {
if(currentEntry == null) {
return;
}
Set<BlockTransactionHashIndex> currentOutputs = currentEntry.getNode().getTransactionOutputs();
if(AppServices.isConnected() && currentOutputs.isEmpty()) {
lastUsed.setText("Never");
@@ -192,6 +268,31 @@ public class ReceiveController extends WalletFormController implements Initializ
return null;
}
public void copySilentPaymentsAddress(ActionEvent actionEvent) {
if(walletForm.getWallet().getPolicyType() == PolicyType.SINGLE_SP) {
ClipboardContent content = new ClipboardContent();
content.putString(walletForm.getWallet().getKeystores().getFirst().getSilentPaymentScanAddress().getAddress());
Clipboard.getSystemClipboard().setContent(content);
}
}
private Image getSilentPaymentsQrCode(String address) {
try {
QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix qrMatrix = qrCodeWriter.encode(address, BarcodeFormat.QR_CODE, 400, 400, Map.of(EncodeHintType.MARGIN, 2));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(qrMatrix, "PNG", baos, new MatrixToImageConfig());
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
return new Image(bais);
} catch(Exception e) {
log.error("Error generating QR", e);
}
return null;
}
public void getNewAddress(ActionEvent event) {
refreshAddress();
if(currentEntry != null) {
@@ -200,6 +301,16 @@ public class ReceiveController extends WalletFormController implements Initializ
}
public void refreshAddress() {
if(walletForm.getWallet().getPolicyType() == PolicyType.SINGLE_SP) {
String silentPaymentAddress = walletForm.getWallet().getKeystores().getFirst().getSilentPaymentScanAddress().getAddress();
spAddress.setText(silentPaymentAddress);
Image qrImage = getSilentPaymentsQrCode(silentPaymentAddress);
if(qrImage != null) {
spQrCode.setImage(qrImage);
}
return;
}
NodeEntry freshEntry = getWalletForm().getFreshNodeEntry(KeyPurpose.RECEIVE, currentEntry);
while(freshEntry.getLabel() != null && !freshEntry.getLabel().isEmpty()) {
freshEntry = getWalletForm().getFreshNodeEntry(KeyPurpose.RECEIVE, freshEntry);
@@ -315,6 +426,7 @@ public class ReceiveController extends WalletFormController implements Initializ
@Subscribe
public void walletAddressesChanged(WalletAddressesChangedEvent event) {
updateFromWalletPolicy();
displayAddress.setUserData(null);
}