add silent payment consolidation output and dust threshold change check

This commit is contained in:
Craig Raw 2026-05-07 09:37:46 +02:00
parent 3fbad787a4
commit c87d5cc3c2
5 changed files with 53 additions and 20 deletions

View File

@ -16,8 +16,16 @@ public class SilentPayment extends Payment {
this(silentPaymentAddress, getDummyAddress(), label, amount, sendMax);
}
public SilentPayment(SilentPaymentAddress silentPaymentAddress, String label, long amount, boolean sendMax, Type type) {
this(silentPaymentAddress, getDummyAddress(), label, amount, sendMax, type);
}
public SilentPayment(SilentPaymentAddress silentPaymentAddress, Address address, String label, long amount, boolean sendMax) {
super(address == null ? getDummyAddress() : address, label, amount, sendMax, Type.DEFAULT);
this(silentPaymentAddress, address, label, amount, sendMax, Type.DEFAULT);
}
public SilentPayment(SilentPaymentAddress silentPaymentAddress, Address address, String label, long amount, boolean sendMax, Type type) {
super(address == null ? getDummyAddress() : address, label, amount, sendMax, type);
this.silentPaymentAddress = silentPaymentAddress;
}

View File

@ -67,7 +67,7 @@ public class SilentPaymentAddress {
public String toAbbreviatedString() {
String address = toString();
return address.substring(0, 50) + "...";
return address.substring(0, 24) + "..." + address.substring(address.length() - 24);
}
@Override

View File

@ -5,6 +5,10 @@ import com.sparrowwallet.drongo.address.P2AAddress;
import com.sparrowwallet.drongo.dns.DnsPayment;
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
import java.util.Arrays;
import java.util.Locale;
import java.util.stream.Collectors;
public class Payment {
private Address address;
private String label;
@ -66,6 +70,12 @@ public class Payment {
public enum Type {
DEFAULT, WHIRLPOOL_FEE, FAKE_MIX, MIX, ANCHOR;
public String toDisplayString() {
return Arrays.stream(this.toString().toLowerCase(Locale.ROOT).split("_"))
.map(w -> Character.toUpperCase(w.charAt(0)) + w.substring(1))
.collect(Collectors.joining(" "));
}
}
public String getDisplayAddress() {

View File

@ -1104,15 +1104,15 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
for(int i = 1; i < numSets; i+=2) {
Payment fakeMixPayment;
Payment.Type type = Payment.Type.FAKE_MIX;
if(policyType == PolicyType.SINGLE_SP) {
SilentPaymentAddress spChangeAddress = getSilentPaymentScanAddress().getChangeAddress().getSilentPaymentAddress();
fakeMixPayment = new SilentPayment(spChangeAddress, "(Fake Mix)", totalPaymentAmount, false);
fakeMixPayment = new SilentPayment(spChangeAddress, "(" + type.toDisplayString() + ")", totalPaymentAmount, false, type);
} else {
WalletNode mixNode = getFreshNode(getChangeKeyPurpose());
txExcludedChangeNodes.add(mixNode);
fakeMixPayment = new WalletNodePayment(mixNode, ".." + mixNode + " (Fake Mix)", totalPaymentAmount, false);
fakeMixPayment = new WalletNodePayment(mixNode, ".." + mixNode + " (" + type.toDisplayString() + ")", totalPaymentAmount, false, type);
}
fakeMixPayment.setType(Payment.Type.FAKE_MIX);
txPayments.add(fakeMixPayment);
}
@ -1120,7 +1120,11 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
for(Payment payment : txPayments) {
if(payment instanceof SilentPayment silentPayment) {
TransactionOutput output = transaction.addOutput(payment.getAmount(), new Script(new byte[0]));
outputs.add(new WalletTransaction.SilentPaymentOutput(output, silentPayment));
if(policyType == PolicyType.SINGLE_SP && getSilentPaymentScanAddress().getSilentPaymentAddress().equals(silentPayment.getSilentPaymentAddress())) {
outputs.add(new WalletTransaction.SilentPaymentConsolidationOutput(output, silentPayment));
} else {
outputs.add(new WalletTransaction.SilentPaymentOutput(output, silentPayment));
}
} else if(payment instanceof WalletNodePayment walletNodePayment) {
TransactionOutput output = transaction.addOutput(payment.getAmount(), payment.getAddress());
outputs.add(new WalletTransaction.ConsolidationOutput(output, walletNodePayment, payment.getAmount()));
@ -1172,13 +1176,15 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
continue;
}
//Determine if a change output is required by checking if its value is greater than its dust threshold
//Determine if a change output is required by checking if its value exceeds both the cost of change and the relay dust threshold
List<Long> setChangeAmts = getSetChangeAmounts(selectedUtxoSets, totalPaymentAmount, noChangeFeeRequiredAmt);
double noChangeFeeRate = (params.fee() == null ? params.feeRate() : noChangeFeeRequiredAmt / transaction.getVirtualSize());
TransactionOutput changeOutput = new TransactionOutput(transaction, setChangeAmts.getFirst(), getNode(KeyPurpose.CHANGE).getOutputScript());
long costOfChangeAmt = getCostOfChange(noChangeFeeRate, params.longTermFeeRate());
if(setChangeAmts.stream().allMatch(amt -> amt > costOfChangeAmt) || (numSets > 1 && differenceAmt / transaction.getVirtualSize() > noChangeFeeRate * 2)) {
long dustThresholdAmt = getDustThreshold(changeOutput, Transaction.DUST_RELAY_TX_FEE);
long minChangeAmt = Math.max(costOfChangeAmt, dustThresholdAmt);
if(setChangeAmts.stream().allMatch(amt -> amt > minChangeAmt) || (numSets > 1 && differenceAmt / transaction.getVirtualSize() > noChangeFeeRate * 2)) {
//Change output is required, determine new fee once change output has been added
TransactionOutput changeOutput = new TransactionOutput(transaction, setChangeAmts.getFirst(), getNode(KeyPurpose.CHANGE).getOutputScript());
double changeVSize = noChangeVSize + changeOutput.getLength() * numSets;
long changeFeeRequiredAmt = params.getRequiredFeeAmount(changeVSize);
if(params.isMinRelayRate()) {
@ -1211,7 +1217,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
}
}
if(setChangeAmts.stream().anyMatch(amt -> amt < costOfChangeAmt)) {
if(setChangeAmts.stream().anyMatch(amt -> amt < minChangeAmt)) {
//The new fee has meant that one of the change outputs is now dust. We pay too high a fee without change, but change is dust when added.
if(numSets > 1 && differenceAmt / transaction.getVirtualSize() < noChangeFeeRate * 2) {
//Maximize privacy. Pay a higher fee to keep multiple output sets.

View File

@ -3,6 +3,7 @@ package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.dns.DnsPayment;
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
@ -170,7 +171,7 @@ public class WalletTransaction {
}
if(payment.getType() == Payment.Type.WHIRLPOOL_FEE) {
return "Whirlpool fee";
return payment.getType().toDisplayString();
} else if(isPremixSend(payment)) {
int premixIndex = getOutputIndex(payment.getAddress(), payment.getAmount(), Collections.emptySet()) - 2;
return "Premix #" + premixIndex;
@ -191,9 +192,14 @@ public class WalletTransaction {
public Wallet getToWallet(Collection<Wallet> wallets, Payment payment) {
for(Wallet openWallet : wallets) {
if(openWallet != getWallet() && openWallet.isValid()) {
WalletNode addressNode = openWallet.getWalletAddresses().get(payment.getAddress());
if(addressNode != null) {
return addressNode.getWallet();
if(openWallet.getPolicyType() == PolicyType.SINGLE_SP && payment instanceof SilentPayment silentPayment
&& silentPayment.getSilentPaymentAddress().equals(openWallet.getSilentPaymentScanAddress().getSilentPaymentAddress())) {
return openWallet;
} else {
WalletNode addressNode = openWallet.getWalletAddresses().get(payment.getAddress());
if(addressNode != null) {
return addressNode.getWallet();
}
}
}
}
@ -206,18 +212,15 @@ public class WalletTransaction {
.anyMatch(p -> payment.getAddress() != null && payment.getAddress().equals(p.getAddress()));
}
public List<Payment> getExternalPayments() {
return payments.stream().filter(payment -> !(payment instanceof WalletNodePayment)).collect(Collectors.toList());
public boolean isConsolidation(Payment payment) {
return payment instanceof WalletNodePayment || (wallet != null && wallet.getPolicyType() == PolicyType.SINGLE_SP
&& payment instanceof SilentPayment silentPayment && wallet.getSilentPaymentScanAddress().getSilentPaymentAddress().equals(silentPayment.getSilentPaymentAddress()));
}
public List<WalletNodePayment> getWalletNodePayments() {
return payments.stream().filter(payment -> payment instanceof WalletNodePayment).map(payment -> (WalletNodePayment)payment).collect(Collectors.toList());
}
public List<SilentPaymentChangeOutput> getSilentPaymentChangeOutputs() {
return outputs.stream().filter(o -> o instanceof SilentPaymentChangeOutput).map(o -> (SilentPaymentChangeOutput)o).collect(Collectors.toList());
}
public static class Output {
private final TransactionOutput transactionOutput;
@ -282,6 +285,12 @@ public class WalletTransaction {
}
}
public static class SilentPaymentConsolidationOutput extends SilentPaymentOutput {
public SilentPaymentConsolidationOutput(TransactionOutput transactionOutput, SilentPayment silentPayment) {
super(transactionOutput, silentPayment);
}
}
public static class WalletNodeOutput extends Output {
private final WalletNode walletNode;
private final Long value;