adapt to use declarative style to for consolidation payments

This commit is contained in:
Craig Raw 2025-10-17 10:27:20 +02:00
parent 0974918cff
commit 092267339a
9 changed files with 74 additions and 35 deletions

2
drongo

@ -1 +1 @@
Subproject commit 286e04ad25de8f5739a70ac353ee073eace5e873
Subproject commit 4e68815fa977a45a7caddead35e5d0f90f5e8fd6

View file

@ -727,7 +727,7 @@ public class TransactionDiagram extends GridPane {
recipientLabel.getStyleClass().add("output-label");
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null;
Wallet toBip47Wallet = getBip47SendWallet(payment);
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment);
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ")

View file

@ -90,20 +90,20 @@ public class TransactionDiagramLabel extends HBox {
outputLabels.add(mixOutputLabel);
}
} else if(walletTx.getPayments().size() >= 5 && walletTx.getPayments().stream().mapToLong(Payment::getAmount).distinct().count() <= 1 && walletTx.getWallet() != null
&& walletTx.getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && walletTx.getPayments().stream().anyMatch(walletTx::isConsolidationSend)) {
&& walletTx.getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && !walletTx.getWalletNodePayments().isEmpty()) {
OutputLabel remixOutputLabel = getRemixOutputLabel(transactionDiagram, walletTx.getPayments());
if(remixOutputLabel != null) {
outputLabels.add(remixOutputLabel);
}
} else {
List<Payment> payments = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT && !walletTx.isConsolidationSend(payment)).collect(Collectors.toList());
List<Payment> payments = walletTx.getExternalPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).collect(Collectors.toList());
List<OutputLabel> paymentLabels = payments.stream().map(payment -> getOutputLabel(transactionDiagram, payment)).collect(Collectors.toList());
if(walletTx.getSelectedUtxos().values().stream().allMatch(Objects::isNull)) {
paymentLabels.sort(Comparator.comparingInt(paymentLabel -> (paymentLabel.text.startsWith("Receive") ? 0 : 1)));
}
outputLabels.addAll(paymentLabels);
List<Payment> consolidations = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT && walletTx.isConsolidationSend(payment)).collect(Collectors.toList());
List<Payment> consolidations = walletTx.getWalletNodePayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).collect(Collectors.toList());
outputLabels.addAll(consolidations.stream().map(consolidation -> getOutputLabel(transactionDiagram, consolidation)).collect(Collectors.toList()));
List<Payment> mixes = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.MIX || payment.getType() == Payment.Type.FAKE_MIX).collect(Collectors.toList());
@ -203,7 +203,7 @@ public class TransactionDiagramLabel extends HBox {
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Payment payment) {
WalletTransaction walletTx = transactionDiagram.getWalletTransaction();
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null;
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment);
String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + payment;

View file

@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.glyphfont;
import com.sparrowwallet.drongo.wallet.Payment;
import com.sparrowwallet.drongo.wallet.WalletNodePayment;
import com.sparrowwallet.drongo.wallet.WalletTransaction;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.control.TransactionDiagram;
@ -15,7 +16,7 @@ public class GlyphUtils {
return getFakeMixGlyph();
} else if(payment.getType().equals(Payment.Type.ANCHOR)) {
return getAnchorGlyph();
} else if(walletTx.isConsolidationSend(payment)) {
} else if(payment instanceof WalletNodePayment) {
return getConsolidationGlyph();
} else if(walletTx.isPremixSend(payment)) {
return getPremixGlyph();

View file

@ -643,19 +643,20 @@ public class HeadersController extends TransactionFormController implements Init
List<Payment> payments = new ArrayList<>();
List<WalletTransaction.Output> outputs = new ArrayList<>();
Map<WalletNode, Long> changeMap = new LinkedHashMap<>();
Map<Script, WalletNode> receiveOutputScripts = wallet.getWalletOutputScripts(KeyPurpose.RECEIVE);
Map<Script, WalletNode> changeOutputScripts = wallet.getWalletOutputScripts(wallet.getChangeKeyPurpose());
for(TransactionOutput txOutput : headersForm.getTransaction().getOutputs()) {
WalletNode changeNode = changeOutputScripts.get(txOutput.getScript());
if(changeNode != null) {
if(headersForm.getTransaction().getOutputs().size() == 4 && headersForm.getTransaction().getOutputs().stream().anyMatch(txo -> txo != txOutput && txo.getValue() == txOutput.getValue())) {
if(selectedTxos.values().stream().allMatch(Objects::nonNull)) {
payments.add(new Payment(txOutput.getScript().getToAddress(), ".." + changeNode + " (Fake Mix)", txOutput.getValue(), false, Payment.Type.FAKE_MIX));
payments.add(new WalletNodePayment(changeNode, ".." + changeNode + " (Fake Mix)", txOutput.getValue(), false, Payment.Type.FAKE_MIX));
} else {
payments.add(new Payment(txOutput.getScript().getToAddress(), ".." + changeNode + " (Mix)", txOutput.getValue(), false, Payment.Type.MIX));
payments.add(new WalletNodePayment(changeNode, ".." + changeNode + " (Mix)", txOutput.getValue(), false, Payment.Type.MIX));
}
} else {
if(changeMap.containsKey(changeNode)) {
payments.add(new Payment(txOutput.getScript().getToAddress(), headersForm.getName(), txOutput.getValue(), false, Payment.Type.DEFAULT));
payments.add(new WalletNodePayment(changeNode, headersForm.getName(), txOutput.getValue(), false, Payment.Type.DEFAULT));
} else {
changeMap.put(changeNode, txOutput.getValue());
}
@ -672,12 +673,18 @@ public class HeadersController extends TransactionFormController implements Init
BlockTransactionHashIndex receivedTxo = walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txOutput.getHash()) && txo.getIndex() == txOutput.getIndex()).findFirst().orElse(null);
String label = headersForm.getName() == null || (headersForm.getName().startsWith("[") && headersForm.getName().endsWith("]") && headersForm.getName().length() == 8) ? null : headersForm.getName();
Address address = txOutput.getScript().getToAddress();
WalletNode receiveNode = receiveOutputScripts.get(txOutput.getScript());
SilentPaymentAddress silentPaymentAddress = headersForm.getSilentPaymentAddress(txOutput);
label = receivedTxo != null ? receivedTxo.getLabel() : label;
if(address != null || silentPaymentAddress != null) {
Payment payment = (silentPaymentAddress == null ?
new Payment(address, label, txOutput.getValue(), false, paymentType) :
new SilentPayment(silentPaymentAddress, address, label, txOutput.getValue(), false));
Payment payment;
if(silentPaymentAddress != null) {
payment = new SilentPayment(silentPaymentAddress, address, label, txOutput.getValue(), false);
} else if(receiveNode != null) {
payment = new WalletNodePayment(receiveNode, label, txOutput.getValue(), false, paymentType);
} else {
payment = new Payment(address, label, txOutput.getValue(), false, paymentType);
}
WalletTransaction createdTx = AppServices.get().getCreatedTransaction(selectedTxos.keySet());
if(createdTx != null) {
Optional<String> optLabel = createdTx.getPayments().stream()
@ -689,8 +696,13 @@ public class HeadersController extends TransactionFormController implements Init
}
}
payments.add(payment);
outputs.add(payment instanceof SilentPayment silentPayment ? new WalletTransaction.SilentPaymentOutput(txOutput, silentPayment) :
new WalletTransaction.PaymentOutput(txOutput, payment));
if(payment instanceof SilentPayment silentPayment) {
outputs.add(new WalletTransaction.SilentPaymentOutput(txOutput, silentPayment));
} else if(payment instanceof WalletNodePayment walletNodePayment) {
outputs.add(new WalletTransaction.ConsolidationOutput(txOutput, walletNodePayment, walletNodePayment.getAmount()));
} else {
outputs.add(new WalletTransaction.PaymentOutput(txOutput, payment));
}
} else {
outputs.add(new WalletTransaction.NonAddressOutput(txOutput));
}

View file

@ -126,13 +126,14 @@ public class OutputController extends TransactionFormController implements Initi
WalletTransaction.Output output = outputs.get(outputForm.getIndex());
if(output instanceof WalletTransaction.NonAddressOutput) {
outputFieldset.setText(baseText);
} else if(output instanceof WalletTransaction.SilentPaymentOutput silentPaymentOutput) {
} else if(output instanceof WalletTransaction.SilentPaymentOutput) {
outputFieldset.setText(baseText + " - Silent Payment");
} else if(output instanceof WalletTransaction.ConsolidationOutput) {
outputFieldset.setText(baseText + " - Consolidation");
} else if(output instanceof WalletTransaction.PaymentOutput paymentOutput) {
Payment payment = paymentOutput.getPayment();
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
outputFieldset.setText(baseText + (toWallet == null ? (toNode != null ? " - Consolidation" : " - Payment") : " - Received to " + toWallet.getFullDisplayName()));
outputFieldset.setText(baseText + (toWallet == null ? " - Payment" : " - Received to " + toWallet.getFullDisplayName()));
} else if(output instanceof WalletTransaction.ChangeOutput changeOutput) {
outputFieldset.setText(baseText + " - Change to " + changeOutput.getWalletNode().toString());
} else {

View file

@ -91,6 +91,10 @@ public class OutputForm extends IndexedTransactionForm {
Payment payment = paymentOutput.getPayment();
return new Label(payment.getLabel() != null && payment.getType() != Payment.Type.FAKE_MIX && payment.getType() != Payment.Type.MIX ? payment.getLabel() : payment.toString(),
GlyphUtils.getOutputGlyph(getWalletTransaction(), payment));
} else if(output instanceof WalletTransaction.ConsolidationOutput consolidationOutput) {
Payment payment = consolidationOutput.getWalletNodePayment();
return new Label(payment.getLabel() != null && payment.getType() != Payment.Type.FAKE_MIX && payment.getType() != Payment.Type.MIX ? payment.getLabel() : payment.toString(),
GlyphUtils.getOutputGlyph(getWalletTransaction(), payment));
} else if(output instanceof WalletTransaction.ChangeOutput changeOutput) {
return new Label("Change", GlyphUtils.getChangeGlyph());
}

View file

@ -143,6 +143,8 @@ public class PaymentController extends WalletFormController implements Initializ
}
};
private final ObjectProperty<WalletNode> consolidationNodeProperty = new SimpleObjectProperty<>();
private final ObjectProperty<PayNym> payNymProperty = new SimpleObjectProperty<>();
private final ObjectProperty<SilentPaymentAddress> silentPaymentAddressProperty = new SimpleObjectProperty<>();
@ -168,6 +170,10 @@ public class PaymentController extends WalletFormController implements Initializ
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
address.leftProperty().set(null);
if(consolidationNodeProperty.get() != null && !newValue.equals(consolidationNodeProperty.get().getAddress().toString())) {
consolidationNodeProperty.set(null);
}
if(payNymProperty.get() != null && !newValue.equals(payNymProperty.get().nymName())) {
payNymProperty.set(null);
}
@ -259,6 +265,17 @@ public class PaymentController extends WalletFormController implements Initializ
//ignore, not a silent payment address
}
try {
Address toAddress = Address.fromString(newValue);
WalletNode walletNode = sendController.getWalletNode(toAddress);
if(walletNode != null) {
consolidationNodeProperty.set(walletNode);
}
label.requestFocus();
} catch(Exception e) {
//ignore, not an address
}
revalidateAmount();
maxButton.setDisable(!isMaxButtonEnabled());
sendController.updateTransaction();
@ -658,8 +675,11 @@ public class PaymentController extends WalletFormController implements Initializ
if(!label.getText().isEmpty() && value != null && value >= getRecipientDustThreshold()) {
Payment payment;
SilentPaymentAddress silentPaymentAddress = silentPaymentAddressProperty.get();
WalletNode consolidationNode = consolidationNodeProperty.get();
if(silentPaymentAddress != null) {
payment = new SilentPayment(silentPaymentAddress, label.getText(), value, sendAll);
} else if(consolidationNode != null) {
payment = new WalletNodePayment(consolidationNode, label.getText(), value, sendAll);
} else {
payment = new Payment(recipientAddress, label.getText(), value, sendAll);
}
@ -718,6 +738,7 @@ public class PaymentController extends WalletFormController implements Initializ
setSendMax(false);
dustAmountProperty.set(false);
consolidationNodeProperty.set(null);
payNymProperty.set(null);
dnsPaymentProperty.set(null);
silentPaymentAddressProperty.set(null);
@ -728,8 +749,7 @@ public class PaymentController extends WalletFormController implements Initializ
if(utxoSelector == null) {
MaxUtxoSelector maxUtxoSelector = new MaxUtxoSelector();
sendController.utxoSelectorProperty().set(maxUtxoSelector);
} else if(utxoSelector instanceof PresetUtxoSelector && !isValidAddressAndLabel() && sendController.getPaymentTabs().getTabs().size() == 1) {
PresetUtxoSelector presetUtxoSelector = (PresetUtxoSelector)utxoSelector;
} else if(utxoSelector instanceof PresetUtxoSelector presetUtxoSelector && !isValidAddressAndLabel() && sendController.getPaymentTabs().getTabs().size() == 1) {
Payment payment = new Payment(null, null, presetUtxoSelector.getPresetUtxos().stream().mapToLong(BlockTransactionHashIndex::getValue).sum(), true);
setPayment(payment);
return;

View file

@ -172,7 +172,7 @@ public class SendController extends WalletFormController implements Initializabl
private final Set<WalletNode> excludedChangeNodes = new HashSet<>();
private final Map<Wallet, Map<Address, WalletNode>> addressNodeMap = new HashMap<>();
private final Map<Address, WalletNode> walletAddresses = new HashMap<>();
private final ChangeListener<String> feeListener = new ChangeListener<>() {
@Override
@ -619,7 +619,7 @@ public class SendController extends WalletFormController implements Initializabl
boolean allowRbf = (replacedTransaction == null || replacedTransaction.getTransaction().isReplaceByFee())
&& payments.stream().noneMatch(payment -> payment instanceof SilentPayment);
walletTransactionService = new WalletTransactionService(addressNodeMap, wallet, getUtxoSelectors(payments), getTxoFilters(),
walletTransactionService = new WalletTransactionService(wallet, getUtxoSelectors(payments), getTxoFilters(),
payments, opReturnsList, excludedChangeNodes,
feeRate, getMinimumFeeRate(), minRelayFeeRate, userFee,
currentBlockHeight, groupByAddress, includeMempoolOutputs, replacedTransaction, allowRbf);
@ -684,7 +684,6 @@ public class SendController extends WalletFormController implements Initializabl
}
private static class WalletTransactionService extends Service<WalletTransaction> {
private final Map<Wallet, Map<Address, WalletNode>> addressNodeMap;
private final Wallet wallet;
private final List<UtxoSelector> utxoSelectors;
private final List<TxoFilter> txoFilters;
@ -702,13 +701,11 @@ public class SendController extends WalletFormController implements Initializabl
private final boolean allowRbf;
private boolean ignoreResult;
public WalletTransactionService(Map<Wallet, Map<Address, WalletNode>> addressNodeMap,
Wallet wallet, List<UtxoSelector> utxoSelectors, List<TxoFilter> txoFilters,
public WalletTransactionService(Wallet wallet, List<UtxoSelector> utxoSelectors, List<TxoFilter> txoFilters,
List<Payment> payments, List<byte[]> opReturns, Set<WalletNode> excludedChangeNodes,
double feeRate, double longTermFeeRate, double minRelayFeeRate, Long fee,
Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs,
BlockTransaction replacedTransaction, boolean allowRbf) {
this.addressNodeMap = addressNodeMap;
this.wallet = wallet;
this.utxoSelectors = utxoSelectors;
this.txoFilters = txoFilters;
@ -759,11 +756,8 @@ public class SendController extends WalletFormController implements Initializabl
private WalletTransaction getWalletTransaction() throws InsufficientFundsException {
try {
updateMessage("Selecting UTXOs...");
WalletTransaction walletTransaction = wallet.createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, excludedChangeNodes,
return wallet.createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, excludedChangeNodes,
feeRate, longTermFeeRate, minRelayFeeRate, fee, currentBlockHeight, groupByAddress, includeMempoolOutputs, allowRbf);
updateMessage("Deriving keys...");
walletTransaction.updateAddressNodeMap(addressNodeMap, walletTransaction.getWallet());
return walletTransaction;
} finally {
updateMessage("");
}
@ -1131,7 +1125,7 @@ public class SendController extends WalletFormController implements Initializabl
paymentCodeProperty.set(null);
addressNodeMap.clear();
walletAddresses.clear();
}
public UtxoSelector getUtxoSelector() {
@ -1209,13 +1203,20 @@ public class SendController extends WalletFormController implements Initializabl
WalletTransaction walletTransaction = walletTransactionProperty.get();
Set<WalletNode> nodes = new LinkedHashSet<>(walletTransaction.getSelectedUtxos().values());
nodes.addAll(walletTransaction.getChangeMap().keySet());
Map<Address, WalletNode> addressNodeMap = walletTransaction.getAddressNodeMap();
nodes.addAll(addressNodeMap.values().stream().filter(Objects::nonNull).collect(Collectors.toList()));
nodes.addAll(walletTransaction.getWalletNodePayments().stream().map(WalletNodePayment::getWalletNode).collect(Collectors.toList()));
//All wallet nodes applicable to this transaction are stored so when the subscription status for one is updated, the history for all can be fetched in one atomic update
walletForm.addWalletTransactionNodes(nodes);
}
public WalletNode getWalletNode(Address address) {
if(walletAddresses.isEmpty()) {
walletAddresses.putAll(getWalletForm().getWallet().getWalletAddresses());
}
return walletAddresses.get(address);
}
public void broadcastNotification(ActionEvent event) {
Wallet wallet = getWalletForm().getWallet();
Storage storage = AppServices.get().getOpenWallets().get(wallet);
@ -1667,12 +1668,12 @@ public class SendController extends WalletFormController implements Initializabl
public PrivacyAnalysisTooltip(WalletTransaction walletTransaction) {
List<Payment> payments = walletTransaction.getPayments();
List<Payment> userPayments = payments.stream().filter(payment -> payment.getType() != Payment.Type.FAKE_MIX).collect(Collectors.toList());
Map<Address, WalletNode> walletAddresses = walletTransaction.getAddressNodeMap();
List<WalletNodePayment> walletNodePayments = walletTransaction.getWalletNodePayments();
OptimizationStrategy optimizationStrategy = getPreferredOptimizationStrategy();
boolean fakeMixPresent = payments.stream().anyMatch(payment -> payment.getType() == Payment.Type.FAKE_MIX);
boolean roundPaymentAmounts = userPayments.stream().anyMatch(payment -> payment.getAmount() % 100 == 0);
boolean mixedAddressTypes = userPayments.stream().anyMatch(payment -> payment.getAddress().getScriptType() != getWalletForm().getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress().getScriptType());
boolean addressReuse = userPayments.stream().anyMatch(payment -> walletAddresses.get(payment.getAddress()) != null && !walletAddresses.get(payment.getAddress()).getTransactionOutputs().isEmpty());
boolean addressReuse = walletNodePayments.stream().anyMatch(walletNodePayment -> !walletNodePayment.getWalletNode().getTransactionOutputs().isEmpty());
boolean payjoinPresent = userPayments.stream().anyMatch(payment -> AppServices.getPayjoinURI(payment.getAddress()) != null);
if(optimizationStrategy == OptimizationStrategy.PRIVACY) {