add initial sending to silent payments support

This commit is contained in:
Craig Raw 2025-09-29 08:37:07 +02:00
parent 6240667478
commit efb1eb1051
20 changed files with 439 additions and 149 deletions

2
drongo

@ -1 +1 @@
Subproject commit d30cc4432cec39ff44ad6c11ab324200a9629a8c Subproject commit a896809286f6f4393202110a15a4dd525f14cf73

View file

@ -10,6 +10,7 @@ import com.sparrowwallet.drongo.dns.DnsPaymentCache;
import com.sparrowwallet.drongo.policy.PolicyType; import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.*; import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.*; import com.sparrowwallet.drongo.psbt.*;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.hummingbird.UR; import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.hummingbird.registry.CryptoPSBT; import com.sparrowwallet.hummingbird.registry.CryptoPSBT;
@ -822,10 +823,10 @@ public class AppController implements Initializable {
try(FileOutputStream outputStream = new FileOutputStream(file)) { try(FileOutputStream outputStream = new FileOutputStream(file)) {
if(asText) { if(asText) {
PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)); PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
writer.print(transactionTabData.getPsbt().toBase64String(includeXpubs)); writer.print(transactionTabData.getPsbt().getForExport().toBase64String(includeXpubs));
writer.flush(); writer.flush();
} else { } else {
outputStream.write(transactionTabData.getPsbt().serialize(includeXpubs, true)); outputStream.write(transactionTabData.getPsbt().getForExport().serialize(includeXpubs, true));
} }
} catch(IOException e) { } catch(IOException e) {
log.error("Error saving PSBT", e); log.error("Error saving PSBT", e);
@ -848,7 +849,7 @@ public class AppController implements Initializable {
TabData tabData = (TabData)selectedTab.getUserData(); TabData tabData = (TabData)selectedTab.getUserData();
if(tabData.getType() == TabData.TabType.TRANSACTION) { if(tabData.getType() == TabData.TabType.TRANSACTION) {
TransactionTabData transactionTabData = (TransactionTabData)tabData; TransactionTabData transactionTabData = (TransactionTabData)tabData;
String data = asBase64 ? transactionTabData.getPsbt().toBase64String() : transactionTabData.getPsbt().toString(); String data = asBase64 ? transactionTabData.getPsbt().getForExport().toBase64String() : transactionTabData.getPsbt().getForExport().toString();
ClipboardContent content = new ClipboardContent(); ClipboardContent content = new ClipboardContent();
content.putString(data); content.putString(data);
@ -862,7 +863,7 @@ public class AppController implements Initializable {
if(tabData.getType() == TabData.TabType.TRANSACTION) { if(tabData.getType() == TabData.TabType.TRANSACTION) {
TransactionTabData transactionTabData = (TransactionTabData)tabData; TransactionTabData transactionTabData = (TransactionTabData)tabData;
byte[] psbtBytes = transactionTabData.getPsbt().serialize(); byte[] psbtBytes = transactionTabData.getPsbt().getForExport().serialize();
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes); CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
BBQR bbqr = new BBQR(BBQRType.PSBT, psbtBytes); BBQR bbqr = new BBQR(BBQRType.PSBT, psbtBytes);
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, false); QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, false);
@ -1897,6 +1898,11 @@ public class AppController implements Initializable {
} }
private void addTransactionTab(String name, File file, PSBT psbt) { private void addTransactionTab(String name, File file, PSBT psbt) {
//Convert to PSBTv0 first
if(psbt.getVersion() != null && psbt.getVersion() >= 2) {
psbt.convertVersion(0);
}
//Add any missing previous outputs if available in open wallets //Add any missing previous outputs if available in open wallets
for(PSBTInput psbtInput : psbt.getPsbtInputs()) { for(PSBTInput psbtInput : psbt.getPsbtInputs()) {
if(psbtInput.getUtxo() == null) { if(psbtInput.getUtxo() == null) {
@ -1920,13 +1926,32 @@ public class AppController implements Initializable {
for(PSBTOutput psbtOutput : psbt.getPsbtOutputs()) { for(PSBTOutput psbtOutput : psbt.getPsbtOutputs()) {
if(psbtOutput.getDnssecProof() != null && !psbtOutput.getDnssecProof().isEmpty() && psbtOutput.getScript() != null) { if(psbtOutput.getDnssecProof() != null && !psbtOutput.getDnssecProof().isEmpty() && psbtOutput.getScript() != null) {
Address address = psbtOutput.getScript().getToAddress(); Address address = psbtOutput.getScript().getToAddress();
if(address != null && DnsPaymentCache.getDnsPayment(address) == null) { if(address != null) {
try { Optional<SilentPaymentAddress> optSilentPaymentAddress = AppServices.get().getOpenWallets().keySet().stream()
Optional<DnsPayment> optDnsPayment = psbtOutput.getDnsPayment(); .map(wallet -> wallet.getSilentPaymentAddress(address)).filter(Objects::nonNull).findFirst();
optDnsPayment.ifPresent(dnsPayment -> DnsPaymentCache.putDnsPayment(address, dnsPayment)); optSilentPaymentAddress.ifPresentOrElse(silentPaymentAddress -> {
} catch(Exception e) { if(DnsPaymentCache.getDnsPayment(silentPaymentAddress) == null) {
log.debug("Error resolving DNS payment", e); try {
} Optional<DnsPayment> optDnsPayment = psbtOutput.getDnsPayment();
if(optDnsPayment.isPresent() && optDnsPayment.get().hasSilentPaymentAddress()) {
DnsPaymentCache.putDnsPayment(silentPaymentAddress, optDnsPayment.get());
}
} catch(Exception e) {
log.debug("Error resolving DNS payment", e);
}
}
}, () -> {
if(DnsPaymentCache.getDnsPayment(address) == null) {
try {
Optional<DnsPayment> optDnsPayment = psbtOutput.getDnsPayment();
if(optDnsPayment.isPresent() && optDnsPayment.get().hasAddress()) {
DnsPaymentCache.putDnsPayment(address, optDnsPayment.get());
}
} catch(Exception e) {
log.debug("Error resolving DNS payment", e);
}
}
});
} }
} }
} }

View file

@ -835,8 +835,8 @@ public class AppServices {
} }
public static void addPayjoinURI(BitcoinURI bitcoinURI) { public static void addPayjoinURI(BitcoinURI bitcoinURI) {
if(bitcoinURI.getPayjoinUrl() == null) { if(bitcoinURI.getPayjoinUrl() == null || bitcoinURI.getAddress() == null) {
throw new IllegalArgumentException("Not a payjoin URI"); throw new IllegalArgumentException("Not a valid payjoin URI");
} }
payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI); payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI);
} }

View file

@ -5,6 +5,8 @@ import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.*; import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
@ -66,8 +68,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
setText(null); setText(null);
setGraphic(null); setGraphic(null);
} else { } else {
if(entry instanceof TransactionEntry) { if(entry instanceof TransactionEntry transactionEntry) {
TransactionEntry transactionEntry = (TransactionEntry)entry;
if(transactionEntry.getBlockTransaction().getHeight() == -1) { if(transactionEntry.getBlockTransaction().getHeight() == -1) {
setText("Unconfirmed Parent"); setText("Unconfirmed Parent");
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry)); setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry));
@ -101,7 +102,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
actionBox.getChildren().add(viewTransactionButton); actionBox.getChildren().add(viewTransactionButton);
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction(); BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
if(blockTransaction.getHeight() <= 0 && canRBF(blockTransaction) && if(blockTransaction.getHeight() <= 0 && canRBF(blockTransaction, transactionEntry.getWallet()) &&
Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) { Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
Button increaseFeeButton = new Button(""); Button increaseFeeButton = new Button("");
increaseFeeButton.setGraphic(getIncreaseFeeRBFGlyph()); increaseFeeButton.setGraphic(getIncreaseFeeRBFGlyph());
@ -121,8 +122,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
} }
setGraphic(actionBox); setGraphic(actionBox);
} else if(entry instanceof NodeEntry) { } else if(entry instanceof NodeEntry nodeEntry) {
NodeEntry nodeEntry = (NodeEntry)entry;
Address address = nodeEntry.getAddress(); Address address = nodeEntry.getAddress();
setText(address.toString()); setText(address.toString());
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry, true, getTreeTableView())); setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry, true, getTreeTableView()));
@ -163,8 +163,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
setContextMenu(null); setContextMenu(null);
setGraphic(new HBox()); setGraphic(new HBox());
} }
} else if(entry instanceof HashIndexEntry) { } else if(entry instanceof HashIndexEntry hashIndexEntry) {
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
setText(hashIndexEntry.getDescription()); setText(hashIndexEntry.getDescription());
setContextMenu(getTreeTableView().getStyleClass().contains("bip47") ? null : new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry)); setContextMenu(getTreeTableView().getStyleClass().contains("bip47") ? null : new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
Tooltip tooltip = new Tooltip(); Tooltip tooltip = new Tooltip();
@ -212,13 +211,14 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
private static void increaseFee(TransactionEntry transactionEntry, boolean cancelTransaction) { private static void increaseFee(TransactionEntry transactionEntry, boolean cancelTransaction) {
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction(); BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
boolean silentPaymentTransaction = transactionEntry.getWallet().isSilentPaymentsTransaction(blockTransaction);
Map<BlockTransactionHashIndex, WalletNode> walletTxos = transactionEntry.getWallet().getWalletTxos(); Map<BlockTransactionHashIndex, WalletNode> walletTxos = transactionEntry.getWallet().getWalletTxos();
List<BlockTransactionHashIndex> utxos = transactionEntry.getChildren().stream() List<BlockTransactionHashIndex> utxos = transactionEntry.getChildren().stream()
.filter(e -> e instanceof HashIndexEntry) .filter(e -> e instanceof HashIndexEntry)
.map(e -> (HashIndexEntry)e) .map(e -> (HashIndexEntry)e)
.filter(e -> e.getType().equals(HashIndexEntry.Type.INPUT) && e.isSpendable()) .filter(e -> e.getType().equals(HashIndexEntry.Type.INPUT) && e.isSpendable())
.map(e -> blockTransaction.getTransaction().getInputs().get((int)e.getHashIndex().getIndex())) .map(e -> blockTransaction.getTransaction().getInputs().get((int)e.getHashIndex().getIndex()))
.filter(i -> Config.get().isMempoolFullRbf() || i.isReplaceByFeeEnabled()) .filter(i -> Config.get().isMempoolFullRbf() || i.isReplaceByFeeEnabled() || silentPaymentTransaction)
.map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get()) .map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get())
.collect(Collectors.toList()); .collect(Collectors.toList());
@ -243,6 +243,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
.collect(Collectors.toList()); .collect(Collectors.toList());
boolean consolidationTransaction = consolidationOutputs.size() == blockTransaction.getTransaction().getOutputs().size() && consolidationOutputs.size() == 1; boolean consolidationTransaction = consolidationOutputs.size() == blockTransaction.getTransaction().getOutputs().size() && consolidationOutputs.size() == 1;
boolean safeToAddInputsOrOutputs = transactionEntry.getWallet().isSafeToAddInputsOrOutputs(blockTransaction);
long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum() - consolidationOutputs.stream().mapToLong(TransactionOutput::getValue).sum(); long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum() - consolidationOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
Transaction tx = blockTransaction.getTransaction(); Transaction tx = blockTransaction.getTransaction();
double vSize = tx.getVirtualSize(); double vSize = tx.getVirtualSize();
@ -257,7 +258,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
List<OutputGroup> outputGroups = transactionEntry.getWallet().getGroupedUtxos(txoFilters, feeRate, AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress()) List<OutputGroup> outputGroups = transactionEntry.getWallet().getGroupedUtxos(txoFilters, feeRate, AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress())
.stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList()); .stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList());
Collections.shuffle(outputGroups); Collections.shuffle(outputGroups);
while((double)changeTotal / vSize < getMaxFeeRate() && !outputGroups.isEmpty() && !cancelTransaction && !consolidationTransaction) { while((double)changeTotal / vSize < getMaxFeeRate() && !outputGroups.isEmpty() && !cancelTransaction && !consolidationTransaction && safeToAddInputsOrOutputs) {
//If there is insufficient change output, include another random output group so the fee can be increased //If there is insufficient change output, include another random output group so the fee can be increased
OutputGroup outputGroup = outputGroups.remove(0); OutputGroup outputGroup = outputGroups.remove(0);
for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) { for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) {
@ -298,9 +299,13 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
label += " (Replaced By Fee)"; label += " (Replaced By Fee)";
} }
if(txOutput.getScript().getToAddress() != null) { Address address = txOutput.getScript().getToAddress();
if(address != null) {
long value = txOutput.getValue();
//Disable change creation by enabling max payment when there is only one output and no additional UTXOs included //Disable change creation by enabling max payment when there is only one output and no additional UTXOs included
return new Payment(txOutput.getScript().getToAddress(), label, txOutput.getValue(), blockTransaction.getTransaction().getOutputs().size() == 1 && rbfChange == 0); boolean sendMax = blockTransaction.getTransaction().getOutputs().size() == 1 && rbfChange == 0;
SilentPaymentAddress silentPaymentAddress = transactionEntry.getWallet().getSilentPaymentAddress(address);
return silentPaymentAddress == null ? new Payment(address, label, value, sendMax) : new SilentPayment(silentPaymentAddress, label, value, sendMax);
} }
return null; return null;
@ -337,7 +342,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
} }
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos)); EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, rbfFee, true, blockTransaction))); Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, rbfFee, true, blockTransaction, safeToAddInputsOrOutputs)));
} }
private static Double getMaxFeeRate() { private static Double getMaxFeeRate() {
@ -394,11 +399,11 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
Payment payment = new Payment(freshAddress, label, inputTotal, true); Payment payment = new Payment(freshAddress, label, inputTotal, true);
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos)); EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), true, null))); Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), true, null, true)));
} }
private static boolean canRBF(BlockTransaction blockTransaction) { private static boolean canRBF(BlockTransaction blockTransaction, Wallet wallet) {
return Config.get().isMempoolFullRbf() || blockTransaction.getTransaction().isReplaceByFee(); return Config.get().isMempoolFullRbf() || blockTransaction.getTransaction().isReplaceByFee() || wallet.isSilentPaymentsTransaction(blockTransaction);
} }
private static boolean canSignMessage(WalletNode walletNode) { private static boolean canSignMessage(WalletNode walletNode) {
@ -476,7 +481,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
tooltip += "\nFee rate: " + String.format("%.2f", feeRate) + " sats/vB"; tooltip += "\nFee rate: " + String.format("%.2f", feeRate) + " sats/vB";
} }
tooltip += "\nRBF: " + (canRBF(transactionEntry.getBlockTransaction()) ? "Enabled" : "Disabled"); tooltip += "\nRBF: " + (canRBF(transactionEntry.getBlockTransaction(), transactionEntry.getWallet()) ? "Enabled" : "Disabled");
} }
return tooltip; return tooltip;
@ -544,6 +549,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
private static class UnconfirmedTransactionContextMenu extends ContextMenu { private static class UnconfirmedTransactionContextMenu extends ContextMenu {
public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) { public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) {
Wallet wallet = transactionEntry.getWallet();
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction(); BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
MenuItem viewTransaction = new MenuItem("View Transaction"); MenuItem viewTransaction = new MenuItem("View Transaction");
viewTransaction.setGraphic(getViewTransactionGlyph()); viewTransaction.setGraphic(getViewTransactionGlyph());
@ -553,7 +559,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
}); });
getItems().add(viewTransaction); getItems().add(viewTransaction);
if(canRBF(blockTransaction) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) { if(canRBF(blockTransaction, wallet) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
MenuItem increaseFee = new MenuItem("Increase Fee (RBF)"); MenuItem increaseFee = new MenuItem("Increase Fee (RBF)");
increaseFee.setGraphic(getIncreaseFeeRBFGlyph()); increaseFee.setGraphic(getIncreaseFeeRBFGlyph());
increaseFee.setOnAction(AE -> { increaseFee.setOnAction(AE -> {
@ -564,7 +570,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
getItems().add(increaseFee); getItems().add(increaseFee);
} }
if(canRBF(blockTransaction) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) { if(canRBF(blockTransaction, wallet) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
MenuItem cancelTx = new MenuItem("Cancel Transaction (RBF)"); MenuItem cancelTx = new MenuItem("Cancel Transaction (RBF)");
cancelTx.setGraphic(getCancelTransactionRBFGlyph()); cancelTx.setGraphic(getCancelTransactionRBFGlyph());
cancelTx.setOnAction(AE -> { cancelTx.setOnAction(AE -> {

View file

@ -6,6 +6,8 @@ import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException; import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.wallet.Payment; import com.sparrowwallet.drongo.wallet.Payment;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
@ -30,7 +32,7 @@ import java.util.stream.IntStream;
public class SendToManyDialog extends Dialog<List<Payment>> { public class SendToManyDialog extends Dialog<List<Payment>> {
private final BitcoinUnit bitcoinUnit; private final BitcoinUnit bitcoinUnit;
private final SpreadsheetView spreadsheetView; private final SpreadsheetView spreadsheetView;
public static final AddressCellType ADDRESS = new AddressCellType(); public static final SendToAddressCellType SEND_TO_ADDRESS = new SendToAddressCellType();
public SendToManyDialog(BitcoinUnit bitcoinUnit, List<Payment> payments) { public SendToManyDialog(BitcoinUnit bitcoinUnit, List<Payment> payments) {
this.bitcoinUnit = bitcoinUnit; this.bitcoinUnit = bitcoinUnit;
@ -92,7 +94,8 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
for(int row = 0; row < grid.getRowCount(); ++row) { for(int row = 0; row < grid.getRowCount(); ++row) {
final ObservableList<SpreadsheetCell> list = FXCollections.observableArrayList(); final ObservableList<SpreadsheetCell> list = FXCollections.observableArrayList();
SpreadsheetCell addressCell = ADDRESS.createCell(row, 0, 1, 1, payments.get(row).getAddress()); SendToAddress sendToAddress = SendToAddress.fromPayment(payments.get(row));
SpreadsheetCell addressCell = SEND_TO_ADDRESS.createCell(row, 0, 1, 1, sendToAddress);
addressCell.getStyleClass().add("fixed-width"); addressCell.getStyleClass().add("fixed-width");
list.add(addressCell); list.add(addressCell);
@ -123,7 +126,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
String firstLabel = null; String firstLabel = null;
for(int row = 0; row < grid.getRowCount(); row++) { for(int row = 0; row < grid.getRowCount(); row++) {
ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row); ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row);
Address address = (Address)rowCells.get(0).getItem(); SendToAddress sendToAddress = (SendToAddress)rowCells.get(0).getItem();
Double value = (Double)rowCells.get(1).getItem(); Double value = (Double)rowCells.get(1).getItem();
String label = (String)rowCells.get(2).getItem(); String label = (String)rowCells.get(2).getItem();
if(firstLabel == null) { if(firstLabel == null) {
@ -133,12 +136,12 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
label = firstLabel; label = firstLabel;
} }
if(address != null && value != null) { if(sendToAddress != null && value != null) {
if(bitcoinUnit == BitcoinUnit.BTC) { if(bitcoinUnit == BitcoinUnit.BTC) {
value = value * Transaction.SATOSHIS_PER_BITCOIN; value = value * Transaction.SATOSHIS_PER_BITCOIN;
} }
payments.add(new Payment(address, label, value.longValue(), false)); payments.add(sendToAddress.toPayment(label, value.longValue(), false));
} }
} }
@ -183,9 +186,14 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
} else { } else {
amount = Long.parseLong(csvReader.get(1).replace(",", "")); amount = Long.parseLong(csvReader.get(1).replace(",", ""));
} }
Address address = Address.fromString(csvReader.get(0));
String label = csvReader.get(2); String label = csvReader.get(2);
csvPayments.add(new Payment(address, label, amount, false)); try {
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(csvReader.get(0));
csvPayments.add(new SilentPayment(silentPaymentAddress, label, amount, false));
} catch(Exception e) {
Address address = Address.fromString(csvReader.get(0));
csvPayments.add(new Payment(address, label, amount, false));
}
} catch(NumberFormatException e) { } catch(NumberFormatException e) {
//ignore and continue - probably a header line //ignore and continue - probably a header line
} catch(InvalidAddressException e) { } catch(InvalidAddressException e) {
@ -221,16 +229,16 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
} }
} }
public static class AddressCellType extends SpreadsheetCellType<Address> { public static class SendToAddressCellType extends SpreadsheetCellType<SendToAddress> {
public AddressCellType() { public SendToAddressCellType() {
this(new StringConverterWithFormat<>(new AddressStringConverter()) { this(new StringConverterWithFormat<>(new SendToAddressStringConverter()) {
@Override @Override
public String toString(Address item) { public String toString(SendToAddress item) {
return toStringFormat(item, ""); //$NON-NLS-1$ return toStringFormat(item, ""); //$NON-NLS-1$
} }
@Override @Override
public Address fromString(String str) { public SendToAddress fromString(String str) {
if(str == null || str.isEmpty()) { //$NON-NLS-1$ if(str == null || str.isEmpty()) { //$NON-NLS-1$
return null; return null;
} else { } else {
@ -239,7 +247,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
} }
@Override @Override
public String toStringFormat(Address item, String format) { public String toStringFormat(SendToAddress item, String format) {
try { try {
if(item == null) { if(item == null) {
return ""; //$NON-NLS-1$ return ""; //$NON-NLS-1$
@ -253,7 +261,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
}); });
} }
public AddressCellType(StringConverter<Address> converter) { public SendToAddressCellType(StringConverter<SendToAddress> converter) {
super(converter); super(converter);
} }
@ -263,7 +271,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
} }
public SpreadsheetCell createCell(final int row, final int column, final int rowSpan, final int columnSpan, public SpreadsheetCell createCell(final int row, final int column, final int rowSpan, final int columnSpan,
final Address value) { final SendToAddress value) {
SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this); SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this);
cell.setItem(value); cell.setItem(value);
return cell; return cell;
@ -276,7 +284,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
@Override @Override
public boolean match(Object value, Object... options) { public boolean match(Object value, Object... options) {
if(value instanceof Address) if(value instanceof SendToAddress)
return true; return true;
else { else {
try { try {
@ -289,9 +297,9 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
} }
@Override @Override
public Address convertValue(Object value) { public SendToAddress convertValue(Object value) {
if(value instanceof Address) if(value instanceof SendToAddress)
return (Address)value; return (SendToAddress)value;
else { else {
try { try {
return converter.fromString(value == null ? null : value.toString()); return converter.fromString(value == null ? null : value.toString());
@ -302,13 +310,64 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
} }
@Override @Override
public String toString(Address item) { public String toString(SendToAddress item) {
return converter.toString(item); return converter.toString(item);
} }
@Override @Override
public String toString(Address item, String format) { public String toString(SendToAddress item, String format) {
return ((StringConverterWithFormat<Address>)converter).toStringFormat(item, format); return ((StringConverterWithFormat<SendToAddress>)converter).toStringFormat(item, format);
} }
}; };
public static class SendToAddress {
private final Address address;
private final SilentPaymentAddress silentPaymentAddress;
public SendToAddress(Address address) {
this.address = address;
this.silentPaymentAddress = null;
}
public SendToAddress(SilentPaymentAddress silentPaymentAddress) {
this.address = null;
this.silentPaymentAddress = silentPaymentAddress;
}
public String toString() {
return silentPaymentAddress == null ? (address == null ? null : address.toString()) : silentPaymentAddress.toString();
}
public static SendToAddress fromPayment(Payment payment) {
return payment instanceof SilentPayment ? new SendToAddress(((SilentPayment)payment).getSilentPaymentAddress()) : new SendToAddress(payment.getAddress());
}
public Payment toPayment(String label, long value, boolean sendMax) {
if(silentPaymentAddress != null) {
return new SilentPayment(silentPaymentAddress, label, value, sendMax);
} else {
return new Payment(address, label, value, sendMax);
}
}
}
private static class SendToAddressStringConverter extends StringConverter<SendToAddress> {
private final AddressStringConverter addressStringConverter = new AddressStringConverter();
@Override
public SendToAddress fromString(String value) {
try {
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(value);
return new SendToAddress(silentPaymentAddress);
} catch(Exception e) {
Address address = addressStringConverter.fromString(value);
return address == null ? null : new SendToAddress(address);
}
}
@Override
public String toString(SendToAddress value) {
return value.toString();
}
}
} }

View file

@ -8,6 +8,8 @@ import com.sparrowwallet.drongo.dns.DnsPayment;
import com.sparrowwallet.drongo.dns.DnsPaymentCache; import com.sparrowwallet.drongo.dns.DnsPaymentCache;
import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.TransactionOutput; import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.uri.BitcoinURI; import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.*; import com.sparrowwallet.sparrow.*;
@ -202,7 +204,7 @@ public class TransactionDiagram extends GridPane {
VBox messagePane = new VBox(); VBox messagePane = new VBox();
messagePane.setPrefHeight(getDiagramHeight()); messagePane.setPrefHeight(getDiagramHeight());
messagePane.setPadding(new Insets(0, 10, 0, 280)); messagePane.setPadding(new Insets(0, 10, 0, 10));
messagePane.setAlignment(Pos.CENTER); messagePane.setAlignment(Pos.CENTER);
messagePane.getChildren().add(createSpacer()); messagePane.getChildren().add(createSpacer());
@ -676,7 +678,8 @@ public class TransactionDiagram extends GridPane {
double width = 140.0; double width = 140.0;
long sum = walletTx.getTotal(); long sum = walletTx.getTotal();
List<Long> values = walletTx.getTransaction().getOutputs().stream().filter(txo -> txo.getScript().getToAddress() != null).map(TransactionOutput::getValue).collect(Collectors.toList()); List<Long> values = walletTx.getOutputs().stream().filter(output -> !(output instanceof WalletTransaction.NonAddressOutput))
.map(output -> output.getTransactionOutput().getValue()).collect(Collectors.toList());
values.add(walletTx.getFee()); values.add(walletTx.getFee());
int numOutputs = displayedPayments.size() + walletTx.getChangeMap().size() + 1; int numOutputs = displayedPayments.size() + walletTx.getChangeMap().size() + 1;
for(int i = 1; i <= numOutputs; i++) { for(int i = 1; i <= numOutputs; i++) {
@ -720,16 +723,16 @@ public class TransactionDiagram extends GridPane {
for(Payment payment : displayedPayments) { for(Payment payment : displayedPayments) {
Glyph outputGlyph = GlyphUtils.getOutputGlyph(walletTx, payment); Glyph outputGlyph = GlyphUtils.getOutputGlyph(walletTx, payment);
boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon", "anchor-icon").contains(style)) || payment instanceof AdditionalPayment || payment.getLabel() != null; boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon", "anchor-icon").contains(style)) || payment instanceof AdditionalPayment || payment.getLabel() != null;
Label recipientLabel = new Label(payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX || payment.getType() == Payment.Type.MIX ? payment.getAddress().toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph); Label recipientLabel = new Label(payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX || payment.getType() == Payment.Type.MIX ? payment.toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph);
recipientLabel.getStyleClass().add("output-label"); recipientLabel.getStyleClass().add("output-label");
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label"); recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment); 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 = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
Wallet toBip47Wallet = getBip47SendWallet(payment); Wallet toBip47Wallet = getBip47SendWallet(payment);
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment.getAddress()); DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment);
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ")
+ getSatsValue(payment.getAmount()) + " sats to " + getSatsValue(payment.getAmount()) + " sats to "
+ (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toWallet == null ? (dnsPayment == null ? (payment.getLabel() == null ? (toNode != null ? toNode : (toBip47Wallet == null ? "external address" : toBip47Wallet.getDisplayName())) : payment.getLabel()) : dnsPayment.toString()) : toWallet.getFullDisplayName()) + "\n" + payment.getAddress().toString()) + (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toWallet == null ? (dnsPayment == null ? (payment.getLabel() == null ? (toNode != null ? toNode : (toBip47Wallet == null ? "external address" : toBip47Wallet.getDisplayName())) : payment.getLabel()) : dnsPayment.toString()) : toWallet.getFullDisplayName()) + "\n" + payment.getDisplayAddress())
+ (walletTx.isDuplicateAddress(payment) ? " (Duplicate)" : "")); + (walletTx.isDuplicateAddress(payment) ? " (Duplicate)" : ""));
recipientTooltip.getStyleClass().add("recipient-label"); recipientTooltip.getStyleClass().add("recipient-label");
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY)); recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
@ -754,9 +757,13 @@ public class TransactionDiagram extends GridPane {
paymentBox.getChildren().addAll(region, amountLabel); paymentBox.getChildren().addAll(region, amountLabel);
} }
Wallet bip47Wallet = toWallet != null && toWallet.isBip47() ? toWallet : (toBip47Wallet != null && toBip47Wallet.isBip47() ? toBip47Wallet : null); if(payment instanceof SilentPayment silentPayment) {
PaymentCode paymentCode = bip47Wallet == null ? null : bip47Wallet.getKeystores().getFirst().getExternalPaymentCode(); outputNodes.add(new OutputNode(paymentBox, silentPayment.isAddressComputed() ? silentPayment.getAddress() : null, payment.getAmount(), null, silentPayment.getSilentPaymentAddress()));
outputNodes.add(new OutputNode(paymentBox, payment.getAddress(), payment.getAmount(), paymentCode)); } else {
Wallet bip47Wallet = toWallet != null && toWallet.isBip47() ? toWallet : (toBip47Wallet != null && toBip47Wallet.isBip47() ? toBip47Wallet : null);
PaymentCode paymentCode = bip47Wallet == null ? null : bip47Wallet.getKeystores().getFirst().getExternalPaymentCode();
outputNodes.add(new OutputNode(paymentBox, payment.getAddress(), payment.getAmount(), paymentCode, null));
}
} }
Set<Integer> seenIndexes = new HashSet<>(); Set<Integer> seenIndexes = new HashSet<>();
@ -820,7 +827,7 @@ public class TransactionDiagram extends GridPane {
outputsBox.getChildren().add(outputNode.outputLabel); outputsBox.getChildren().add(outputNode.outputLabel);
outputsBox.getChildren().add(createSpacer()); outputsBox.getChildren().add(createSpacer());
ContextMenu contextMenu = new LabelContextMenu(outputNode.address, outputNode.amount, outputNode.paymentCode); ContextMenu contextMenu = new LabelContextMenu(outputNode.address, outputNode.amount, outputNode.paymentCode, outputNode.silentPaymentAddress);
if(!outputNode.outputLabel.getChildren().isEmpty() && outputNode.outputLabel.getChildren().get(0) instanceof Label outputLabelControl) { if(!outputNode.outputLabel.getChildren().isEmpty() && outputNode.outputLabel.getChildren().get(0) instanceof Label outputLabelControl) {
outputLabelControl.setContextMenu(contextMenu); outputLabelControl.setContextMenu(contextMenu);
} }
@ -995,8 +1002,11 @@ public class TransactionDiagram extends GridPane {
} }
private int getOutputIndex(Address address, long amount, Collection<Integer> seenIndexes) { private int getOutputIndex(Address address, long amount, Collection<Integer> seenIndexes) {
List<TransactionOutput> addressOutputs = walletTx.getTransaction().getOutputs().stream().filter(txOutput -> txOutput.getScript().getToAddress() != null).collect(Collectors.toList()); List<TransactionOutput> addressOutputs = walletTx.getOutputs().stream().filter(output -> !(output instanceof WalletTransaction.NonAddressOutput))
TransactionOutput output = addressOutputs.stream().filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount && !seenIndexes.contains(txOutput.getIndex())).findFirst().orElseThrow(); .map(WalletTransaction.Output::getTransactionOutput).collect(Collectors.toList());
TransactionOutput output = addressOutputs.stream()
.filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount && !seenIndexes.contains(txOutput.getIndex()))
.findFirst().orElseThrow();
return addressOutputs.indexOf(output); return addressOutputs.indexOf(output);
} }
@ -1146,7 +1156,7 @@ public class TransactionDiagram extends GridPane {
} }
public String toString() { public String toString() {
return additionalPayments.stream().map(payment -> payment.getAddress().toString()).collect(Collectors.joining("\n")); return additionalPayments.stream().map(Payment::toString).collect(Collectors.joining("\n"));
} }
} }
@ -1155,25 +1165,27 @@ public class TransactionDiagram extends GridPane {
public Address address; public Address address;
public long amount; public long amount;
public PaymentCode paymentCode; public PaymentCode paymentCode;
public SilentPaymentAddress silentPaymentAddress;
public OutputNode(Pane outputLabel, Address address, long amount) { public OutputNode(Pane outputLabel, Address address, long amount) {
this(outputLabel, address, amount, null); this(outputLabel, address, amount, null, null);
} }
public OutputNode(Pane outputLabel, Address address, long amount, PaymentCode paymentCode) { public OutputNode(Pane outputLabel, Address address, long amount, PaymentCode paymentCode, SilentPaymentAddress silentPaymentAddress) {
this.outputLabel = outputLabel; this.outputLabel = outputLabel;
this.address = address; this.address = address;
this.amount = amount; this.amount = amount;
this.paymentCode = paymentCode; this.paymentCode = paymentCode;
this.silentPaymentAddress = silentPaymentAddress;
} }
} }
private class LabelContextMenu extends ContextMenu { private class LabelContextMenu extends ContextMenu {
public LabelContextMenu(Address address, long value) { public LabelContextMenu(Address address, long value) {
this(address, value, null); this(address, value, null, null);
} }
public LabelContextMenu(Address address, long value, PaymentCode paymentCode) { public LabelContextMenu(Address address, long value, PaymentCode paymentCode, SilentPaymentAddress silentPaymentAddress) {
if(address != null) { if(address != null) {
MenuItem copyAddress = new MenuItem("Copy Address"); MenuItem copyAddress = new MenuItem("Copy Address");
copyAddress.setOnAction(event -> { copyAddress.setOnAction(event -> {
@ -1221,6 +1233,17 @@ public class TransactionDiagram extends GridPane {
}); });
getItems().add(copyPaymentCode); getItems().add(copyPaymentCode);
} }
if(silentPaymentAddress != null) {
MenuItem copySilentPaymentAddress = new MenuItem("Copy Silent Payment Address");
copySilentPaymentAddress.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(silentPaymentAddress.toString());
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copySilentPaymentAddress);
}
} }
} }
} }

View file

@ -1,7 +1,5 @@
package com.sparrowwallet.sparrow.control; package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.dns.DnsPayment;
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
@ -208,9 +206,7 @@ public class TransactionDiagramLabel extends HBox {
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null; WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment); Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment);
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment.getAddress()); String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + payment;
String recipient = dnsPayment == null ? payment.getAddress().toString() : dnsPayment.toString();
String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + recipient;
return getOutputLabel(glyph, text); return getOutputLabel(glyph, text);
} }

View file

@ -17,12 +17,13 @@ public class SpendUtxoEvent {
private final boolean requireAllUtxos; private final boolean requireAllUtxos;
private final BlockTransaction replacedTransaction; private final BlockTransaction replacedTransaction;
private final PaymentCode paymentCode; private final PaymentCode paymentCode;
private final boolean allowPaymentChanges;
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) { public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
this(wallet, utxos, null, null, null, false, null); this(wallet, utxos, null, null, null, false, null, true);
} }
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, boolean requireAllUtxos, BlockTransaction replacedTransaction) { public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, boolean requireAllUtxos, BlockTransaction replacedTransaction, boolean allowPaymentChanges) {
this.wallet = wallet; this.wallet = wallet;
this.utxos = utxos; this.utxos = utxos;
this.payments = payments; this.payments = payments;
@ -31,6 +32,7 @@ public class SpendUtxoEvent {
this.requireAllUtxos = requireAllUtxos; this.requireAllUtxos = requireAllUtxos;
this.replacedTransaction = replacedTransaction; this.replacedTransaction = replacedTransaction;
this.paymentCode = null; this.paymentCode = null;
this.allowPaymentChanges = allowPaymentChanges;
} }
public SpendUtxoEvent(Wallet wallet, List<Payment> payments, List<byte[]> opReturns, PaymentCode paymentCode) { public SpendUtxoEvent(Wallet wallet, List<Payment> payments, List<byte[]> opReturns, PaymentCode paymentCode) {
@ -42,6 +44,7 @@ public class SpendUtxoEvent {
this.requireAllUtxos = false; this.requireAllUtxos = false;
this.replacedTransaction = null; this.replacedTransaction = null;
this.paymentCode = paymentCode; this.paymentCode = paymentCode;
this.allowPaymentChanges = false;
} }
public Wallet getWallet() { public Wallet getWallet() {
@ -75,4 +78,8 @@ public class SpendUtxoEvent {
public PaymentCode getPaymentCode() { public PaymentCode getPaymentCode() {
return paymentCode; return paymentCode;
} }
public boolean allowPaymentChanges() {
return allowPaymentChanges;
}
} }

View file

@ -0,0 +1,9 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.protocol.Transaction;
public class TransactionOutputsChangedEvent extends TransactionChangedEvent {
public TransactionOutputsChangedEvent(Transaction transaction) {
super(transaction);
}
}

View file

@ -624,7 +624,8 @@ public class PayNymController {
List<UtxoSelector> utxoSelectors = List.of(utxos == null ? new KnapsackUtxoSelector(noInputsFee) : new PresetUtxoSelector(utxos, true, false)); List<UtxoSelector> utxoSelectors = List.of(utxos == null ? new KnapsackUtxoSelector(noInputsFee) : new PresetUtxoSelector(utxos, true, false));
List<TxoFilter> txoFilters = List.of(new SpentTxoFilter(), new FrozenTxoFilter(), new CoinbaseTxoFilter(wallet)); List<TxoFilter> txoFilters = List.of(new SpentTxoFilter(), new FrozenTxoFilter(), new CoinbaseTxoFilter(wallet));
return wallet.createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, Collections.emptySet(), feeRate, minimumFeeRate, minRelayFeeRate, null, AppServices.getCurrentBlockHeight(), groupByAddress, includeMempoolOutputs); return wallet.createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, Collections.emptySet(), feeRate, minimumFeeRate, minRelayFeeRate, null,
AppServices.getCurrentBlockHeight(), groupByAddress, includeMempoolOutputs, true);
} }
private Map<BlockTransaction, WalletNode> getNotificationTransaction(PaymentCode externalPaymentCode) { private Map<BlockTransaction, WalletNode> getNotificationTransaction(PaymentCode externalPaymentCode) {

View file

@ -7,6 +7,8 @@ import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.*; import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput; import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.uri.BitcoinURI; import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.hummingbird.UR; import com.sparrowwallet.hummingbird.UR;
@ -57,7 +59,6 @@ import tornadofx.control.Fieldset;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import tornadofx.control.Form; import tornadofx.control.Form;
import javax.swing.text.html.Option;
import java.io.*; import java.io.*;
import java.net.*; import java.net.*;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -640,6 +641,7 @@ public class HeadersController extends TransactionFormController implements Init
} }
List<Payment> payments = new ArrayList<>(); List<Payment> payments = new ArrayList<>();
List<WalletTransaction.Output> outputs = new ArrayList<>();
Map<WalletNode, Long> changeMap = new LinkedHashMap<>(); Map<WalletNode, Long> changeMap = new LinkedHashMap<>();
Map<Script, WalletNode> changeOutputScripts = wallet.getWalletOutputScripts(wallet.getChangeKeyPurpose()); Map<Script, WalletNode> changeOutputScripts = wallet.getWalletOutputScripts(wallet.getChangeKeyPurpose());
for(TransactionOutput txOutput : headersForm.getTransaction().getOutputs()) { for(TransactionOutput txOutput : headersForm.getTransaction().getOutputs()) {
@ -658,6 +660,7 @@ public class HeadersController extends TransactionFormController implements Init
changeMap.put(changeNode, txOutput.getValue()); changeMap.put(changeNode, txOutput.getValue());
} }
} }
outputs.add(new WalletTransaction.ChangeOutput(txOutput, changeNode, txOutput.getValue()));
} else { } else {
Payment.Type paymentType = Payment.Type.DEFAULT; Payment.Type paymentType = Payment.Type.DEFAULT;
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet(); Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
@ -668,24 +671,33 @@ 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); 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(); String label = headersForm.getName() == null || (headersForm.getName().startsWith("[") && headersForm.getName().endsWith("]") && headersForm.getName().length() == 8) ? null : headersForm.getName();
try { Address address = txOutput.getScript().getToAddress();
Payment payment = new Payment(txOutput.getScript().getToAddresses()[0], receivedTxo != null ? receivedTxo.getLabel() : label, txOutput.getValue(), false, paymentType); 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));
WalletTransaction createdTx = AppServices.get().getCreatedTransaction(selectedTxos.keySet()); WalletTransaction createdTx = AppServices.get().getCreatedTransaction(selectedTxos.keySet());
if(createdTx != null) { if(createdTx != null) {
Optional<String> optLabel = createdTx.getPayments().stream().filter(pymt -> pymt.getAddress().equals(payment.getAddress()) && pymt.getAmount() == payment.getAmount()).map(Payment::getLabel).findFirst(); Optional<String> optLabel = createdTx.getPayments().stream()
.filter(pymt -> (pymt instanceof SilentPayment silentPayment ? silentPayment.getSilentPaymentAddress().equals(silentPaymentAddress) :
pymt.getAddress().equals(payment.getAddress())) && pymt.getAmount() == payment.getAmount()).map(Payment::getLabel).findFirst();
if(optLabel.isPresent()) { if(optLabel.isPresent()) {
payment.setLabel(optLabel.get()); payment.setLabel(optLabel.get());
outputIndexLabels.put(txOutput.getIndex(), optLabel.get()); outputIndexLabels.put(txOutput.getIndex(), optLabel.get());
} }
} }
payments.add(payment); payments.add(payment);
} catch(Exception e) { outputs.add(payment instanceof SilentPayment silentPayment ? new WalletTransaction.SilentPaymentOutput(txOutput, silentPayment) :
//ignore new WalletTransaction.PaymentOutput(txOutput, payment));
} else {
outputs.add(new WalletTransaction.NonAddressOutput(txOutput));
} }
} }
} }
return new WalletTransaction(wallet, headersForm.getTransaction(), Collections.emptyList(), List.of(selectedTxos), payments, changeMap, fee.getValue(), walletInputTransactions); return new WalletTransaction(wallet, headersForm.getTransaction(), Collections.emptyList(), List.of(selectedTxos), payments, outputs, changeMap, fee.getValue(), walletInputTransactions);
} else { } else {
Map<BlockTransactionHashIndex, WalletNode> selectedTxos = headersForm.getTransaction().getInputs().stream() Map<BlockTransactionHashIndex, WalletNode> selectedTxos = headersForm.getTransaction().getInputs().stream()
.collect(Collectors.toMap(txInput -> getBlockTransactionInput(inputTransactions, txInput), .collect(Collectors.toMap(txInput -> getBlockTransactionInput(inputTransactions, txInput),
@ -695,16 +707,25 @@ public class HeadersController extends TransactionFormController implements Init
selectedTxos.entrySet().forEach(entry -> entry.setValue(null)); selectedTxos.entrySet().forEach(entry -> entry.setValue(null));
List<Payment> payments = new ArrayList<>(); List<Payment> payments = new ArrayList<>();
List<WalletTransaction.Output> outputs = new ArrayList<>();
for(TransactionOutput txOutput : headersForm.getTransaction().getOutputs()) { for(TransactionOutput txOutput : headersForm.getTransaction().getOutputs()) {
try { Address address = txOutput.getScript().getToAddress();
BlockTransactionHashIndex receivedTxo = getBlockTransactionOutput(txOutput); SilentPaymentAddress silentPaymentAddress = headersForm.getSilentPaymentAddress(txOutput);
payments.add(new Payment(txOutput.getScript().getToAddresses()[0], receivedTxo != null ? receivedTxo.getLabel() : null, txOutput.getValue(), false)); BlockTransactionHashIndex receivedTxo = getBlockTransactionOutput(txOutput);
} catch(Exception e) { String label = receivedTxo != null ? receivedTxo.getLabel() : null;
//ignore if(address != null || silentPaymentAddress != null) {
Payment payment = (silentPaymentAddress == null ?
new Payment(address, label, txOutput.getValue(), false) :
new SilentPayment(silentPaymentAddress, address, label, txOutput.getValue(), false));
payments.add(payment);
outputs.add(payment instanceof SilentPayment silentPayment ? new WalletTransaction.SilentPaymentOutput(txOutput, silentPayment) :
new WalletTransaction.PaymentOutput(txOutput, payment));
} else {
outputs.add(new WalletTransaction.NonAddressOutput(txOutput));
} }
} }
return new WalletTransaction(null, headersForm.getTransaction(), Collections.emptyList(), List.of(selectedTxos), payments, Collections.emptyMap(), fee.getValue(), inputTransactions); return new WalletTransaction(null, headersForm.getTransaction(), Collections.emptyList(), List.of(selectedTxos), payments, outputs, Collections.emptyMap(), fee.getValue(), inputTransactions);
} }
} }
@ -931,7 +952,7 @@ public class HeadersController extends TransactionFormController implements Init
//Don't include non witness utxo fields for segwit wallets when displaying the PSBT as a QR - it can add greatly to the time required for scanning //Don't include non witness utxo fields for segwit wallets when displaying the PSBT as a QR - it can add greatly to the time required for scanning
boolean includeNonWitnessUtxos = !Arrays.asList(ScriptType.WITNESS_TYPES).contains(headersForm.getSigningWallet().getScriptType()); boolean includeNonWitnessUtxos = !Arrays.asList(ScriptType.WITNESS_TYPES).contains(headersForm.getSigningWallet().getScriptType());
byte[] psbtBytes = headersForm.getPsbt().serialize(true, includeNonWitnessUtxos); byte[] psbtBytes = headersForm.getPsbt().getForExport().serialize(true, includeNonWitnessUtxos);
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes); CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
BBQR bbqr = addBbqrOption ? new BBQR(BBQRType.PSBT, psbtBytes) : null; BBQR bbqr = addBbqrOption ? new BBQR(BBQRType.PSBT, psbtBytes) : null;
@ -1014,7 +1035,7 @@ public class HeadersController extends TransactionFormController implements Init
} }
try(FileOutputStream outputStream = new FileOutputStream(file)) { try(FileOutputStream outputStream = new FileOutputStream(file)) {
outputStream.write(headersForm.getPsbt().serialize()); outputStream.write(headersForm.getPsbt().getForExport().serialize());
} catch(IOException e) { } catch(IOException e) {
log.error("Error saving PSBT", e); log.error("Error saving PSBT", e);
AppServices.showErrorDialog("Error saving PSBT", "Cannot write to " + file.getAbsolutePath()); AppServices.showErrorDialog("Error saving PSBT", "Cannot write to " + file.getAbsolutePath());
@ -1071,7 +1092,12 @@ public class HeadersController extends TransactionFormController implements Init
private void signUnencryptedKeystores(Wallet unencryptedWallet) { private void signUnencryptedKeystores(Wallet unencryptedWallet) {
try { try {
unencryptedWallet.sign(headersForm.getPsbt()); Map<PSBTInput, WalletNode> signingNodes = unencryptedWallet.getSigningNodes(headersForm.getPsbt());
List<SilentPayment> silentPayments = unencryptedWallet.computeSilentPaymentOutputs(headersForm.getPsbt(), signingNodes);
if(!silentPayments.isEmpty()) {
EventManager.get().post(new TransactionOutputsChangedEvent(headersForm.getTransaction()));
}
unencryptedWallet.sign(signingNodes);
updateSignedKeystores(headersForm.getSigningWallet()); updateSignedKeystores(headersForm.getSigningWallet());
} catch(Exception e) { } catch(Exception e) {
log.warn("Failed to Sign", e); log.warn("Failed to Sign", e);
@ -1599,6 +1625,13 @@ public class HeadersController extends TransactionFormController implements Init
} }
} }
@Subscribe
public void transactionOutputsChanged(TransactionOutputsChangedEvent event) {
if(event.getTransaction().equals(headersForm.getTransaction())) {
headersForm.setWalletTransaction(getWalletTransaction(headersForm.getInputTransactions()));
}
}
@Subscribe @Subscribe
public void transactionExtracted(TransactionExtractedEvent event) { public void transactionExtracted(TransactionExtractedEvent event) {
if(event.getPsbt().equals(headersForm.getPsbt())) { if(event.getPsbt().equals(headersForm.getPsbt())) {

View file

@ -337,7 +337,7 @@ public class InputController extends TransactionFormController implements Initia
} }
} else { } else {
if(txInput.isAbsoluteTimeLocked()) { if(txInput.isAbsoluteTimeLocked()) {
txInput.setSequenceNumber(TransactionInput.SEQUENCE_LOCKTIME_DISABLED - 1); txInput.setSequenceNumber(TransactionInput.SEQUENCE_RBF_DISABLED);
if(oldValue != null) { if(oldValue != null) {
EventManager.get().post(new TransactionChangedEvent(transaction)); EventManager.get().post(new TransactionChangedEvent(transaction));
} }
@ -389,7 +389,7 @@ public class InputController extends TransactionFormController implements Initia
if(rbf.selectedProperty().getValue()) { if(rbf.selectedProperty().getValue()) {
txInput.setSequenceNumber(TransactionInput.SEQUENCE_RBF_ENABLED); txInput.setSequenceNumber(TransactionInput.SEQUENCE_RBF_ENABLED);
} else { } else {
txInput.setSequenceNumber(TransactionInput.SEQUENCE_LOCKTIME_DISABLED - 1); txInput.setSequenceNumber(TransactionInput.SEQUENCE_RBF_DISABLED);
} }
if(old_toggle != null) { if(old_toggle != null) {
EventManager.get().post(new TransactionChangedEvent(transaction)); EventManager.get().post(new TransactionChangedEvent(transaction));

View file

@ -5,14 +5,12 @@ import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.NonStandardScriptException; import com.sparrowwallet.drongo.protocol.NonStandardScriptException;
import com.sparrowwallet.drongo.protocol.TransactionInput; import com.sparrowwallet.drongo.protocol.TransactionInput;
import com.sparrowwallet.drongo.protocol.TransactionOutput; import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.PSBTReorderedEvent; import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.event.UnitFormatChangedEvent;
import com.sparrowwallet.sparrow.event.BlockTransactionOutputsFetchedEvent;
import com.sparrowwallet.sparrow.event.ViewTransactionEvent;
import com.sparrowwallet.sparrow.net.ElectrumServer; import com.sparrowwallet.sparrow.net.ElectrumServer;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
@ -70,20 +68,7 @@ public class OutputController extends TransactionFormController implements Initi
updateOutputLegendFromWallet(txOutput, walletTransaction != null ? walletTransaction.getWallet() : null); updateOutputLegendFromWallet(txOutput, walletTransaction != null ? walletTransaction.getWallet() : null);
}); });
updateOutputLegendFromWallet(txOutput, outputForm.getWallet()); updateOutputLegendFromWallet(txOutput, outputForm.getWallet());
updateSends(txOutput);
value.setValue(txOutput.getValue());
to.setVisible(false);
try {
Address[] addresses = txOutput.getScript().getToAddresses();
to.setVisible(true);
if(addresses.length == 1) {
address.setAddress(addresses[0]);
} else {
address.setText("multiple addresses");
}
} catch(NonStandardScriptException e) {
//ignore
}
spentField.managedProperty().bind(spentField.visibleProperty()); spentField.managedProperty().bind(spentField.visibleProperty());
spentByField.managedProperty().bind(spentByField.visibleProperty()); spentByField.managedProperty().bind(spentByField.visibleProperty());
@ -98,6 +83,32 @@ public class OutputController extends TransactionFormController implements Initi
} }
initializeScriptField(scriptPubKeyArea); initializeScriptField(scriptPubKeyArea);
updateScriptPubKey(txOutput);
}
private void updateSends(TransactionOutput txOutput) {
value.setValue(txOutput.getValue());
to.setVisible(false);
Address toAddress = txOutput.getScript().getToAddress();
SilentPaymentAddress silentPaymentAddress = outputForm.getSilentPaymentAddress(txOutput);
if(toAddress != null) {
to.setVisible(true);
address.setAddress(toAddress);
} else if(silentPaymentAddress != null) {
to.setVisible(true);
address.setText(silentPaymentAddress.toAbbreviatedString());
} else {
try {
txOutput.getScript().getToAddresses();
to.setVisible(true);
address.setText("multiple addresses");
} catch(NonStandardScriptException e) {
//ignore
}
}
}
private void updateScriptPubKey(TransactionOutput txOutput) {
scriptPubKeyArea.clear(); scriptPubKeyArea.clear();
scriptPubKeyArea.appendScript(txOutput.getScript(), null, null); scriptPubKeyArea.appendScript(txOutput.getScript(), null, null);
} }
@ -115,6 +126,8 @@ public class OutputController extends TransactionFormController implements Initi
WalletTransaction.Output output = outputs.get(outputForm.getIndex()); WalletTransaction.Output output = outputs.get(outputForm.getIndex());
if(output instanceof WalletTransaction.NonAddressOutput) { if(output instanceof WalletTransaction.NonAddressOutput) {
outputFieldset.setText(baseText); outputFieldset.setText(baseText);
} else if(output instanceof WalletTransaction.SilentPaymentOutput silentPaymentOutput) {
outputFieldset.setText(baseText + " - Silent Payment");
} else if(output instanceof WalletTransaction.PaymentOutput paymentOutput) { } else if(output instanceof WalletTransaction.PaymentOutput paymentOutput) {
Payment payment = paymentOutput.getPayment(); Payment payment = paymentOutput.getPayment();
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment); Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
@ -206,4 +219,12 @@ public class OutputController extends TransactionFormController implements Initi
updateOutputLegendFromWallet(outputForm.getTransactionOutput(), null); updateOutputLegendFromWallet(outputForm.getTransactionOutput(), null);
} }
} }
@Subscribe
public void transactionOutputsChanged(TransactionOutputsChangedEvent event) {
if(event.getTransaction().equals(outputForm.getTransaction())) {
updateSends(outputForm.getTransactionOutput());
updateScriptPubKey(outputForm.getTransactionOutput());
}
}
} }

View file

@ -89,7 +89,7 @@ public class OutputForm extends IndexedTransactionForm {
} }
} else if(output instanceof WalletTransaction.PaymentOutput paymentOutput) { } else if(output instanceof WalletTransaction.PaymentOutput paymentOutput) {
Payment payment = paymentOutput.getPayment(); Payment payment = paymentOutput.getPayment();
return new Label(payment.getLabel() != null && payment.getType() != Payment.Type.FAKE_MIX && payment.getType() != Payment.Type.MIX ? payment.getLabel() : payment.getAddress().toString(), 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)); GlyphUtils.getOutputGlyph(getWalletTransaction(), payment));
} else if(output instanceof WalletTransaction.ChangeOutput changeOutput) { } else if(output instanceof WalletTransaction.ChangeOutput changeOutput) {
return new Label("Change", GlyphUtils.getChangeGlyph()); return new Label("Change", GlyphUtils.getChangeGlyph());

View file

@ -6,6 +6,7 @@ import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.CopyableCoinLabel; import com.sparrowwallet.sparrow.control.CopyableCoinLabel;
import com.sparrowwallet.sparrow.control.CopyableLabel; import com.sparrowwallet.sparrow.control.CopyableLabel;
import com.sparrowwallet.sparrow.event.TransactionOutputsChangedEvent;
import com.sparrowwallet.sparrow.event.UnitFormatChangedEvent; import com.sparrowwallet.sparrow.event.UnitFormatChangedEvent;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
@ -60,4 +61,11 @@ public class OutputsController extends TransactionFormController implements Init
public void unitFormatChanged(UnitFormatChangedEvent event) { public void unitFormatChanged(UnitFormatChangedEvent event) {
total.refresh(event.getUnitFormat(), event.getBitcoinUnit()); total.refresh(event.getUnitFormat(), event.getBitcoinUnit());
} }
@Subscribe
public void transactionOutputsChanged(TransactionOutputsChangedEvent event) {
if(event.getTransaction().equals(outputsForm.getTransaction())) {
updatePieData(outputsPie, outputsForm.getTransaction().getOutputs());
}
}
} }

View file

@ -3,6 +3,8 @@ package com.sparrowwallet.sparrow.transaction;
import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.protocol.*; import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTOutput;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.io.Storage;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
@ -193,4 +195,16 @@ public class TransactionData {
public Wallet getWallet() { public Wallet getWallet() {
return getSigningWallet() != null ? getSigningWallet() : (getWalletTransaction() != null ? getWalletTransaction().getWallet() : null); return getSigningWallet() != null ? getSigningWallet() : (getWalletTransaction() != null ? getWalletTransaction().getWallet() : null);
} }
protected SilentPaymentAddress getSilentPaymentAddress(TransactionOutput txOutput) {
if(getPsbt() != null && txOutput.getParent() != null) {
for(PSBTOutput psbtOutput : getPsbt().getPsbtOutputs()) {
if(psbtOutput.getOutput().getIndex() == txOutput.getIndex() && psbtOutput.getSilentPaymentAddress() != null) {
return psbtOutput.getSilentPaymentAddress();
}
}
}
return null;
}
} }

View file

@ -2,8 +2,10 @@ package com.sparrowwallet.sparrow.transaction;
import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.protocol.TransactionSignature; import com.sparrowwallet.drongo.protocol.TransactionSignature;
import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.io.Storage;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
@ -112,6 +114,10 @@ public abstract class TransactionForm {
return txdata.getWallet(); return txdata.getWallet();
} }
public SilentPaymentAddress getSilentPaymentAddress(TransactionOutput output) {
return txdata.getSilentPaymentAddress(output);
}
public boolean isEditable() { public boolean isEditable() {
if(getBlockTransaction() != null) { if(getBlockTransaction() != null) {
return false; return false;

View file

@ -5,6 +5,7 @@ import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.NonStandardScriptException; import com.sparrowwallet.drongo.protocol.NonStandardScriptException;
import com.sparrowwallet.drongo.protocol.TransactionOutput; import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.sparrow.UnitFormat; import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.BaseController; import com.sparrowwallet.sparrow.BaseController;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
@ -33,17 +34,7 @@ public abstract class TransactionFormController extends BaseController {
long totalAmt = 0; long totalAmt = 0;
for(int i = 0; i < outputs.size(); i++) { for(int i = 0; i < outputs.size(); i++) {
TransactionOutput output = outputs.get(i); TransactionOutput output = outputs.get(i);
String name = "#" + i; String name = getPieDataName(i, output);
try {
Address[] addresses = output.getScript().getToAddresses();
if(addresses.length == 1) {
name = name + " " + addresses[0].getAddress();
} else {
name = name + " [" + addresses[0].getAddress() + ",...]";
}
} catch(NonStandardScriptException e) {
//ignore
}
totalAmt += output.getValue(); totalAmt += output.getValue();
outputsPieData.add(new PieChart.Data(name, output.getValue())); outputsPieData.add(new PieChart.Data(name, output.getValue()));
@ -52,6 +43,34 @@ public abstract class TransactionFormController extends BaseController {
addPieData(pie, outputsPieData); addPieData(pie, outputsPieData);
} }
protected void updatePieData(PieChart pie, List<TransactionOutput> outputs) {
for(int i = 0; i < outputs.size(); i++) {
TransactionOutput output = outputs.get(i);
String name = getPieDataName(i, output);
pie.getData().get(i).setName(name);
}
}
private String getPieDataName(int i, TransactionOutput output) {
String name = "#" + i;
Address address = output.getScript().getToAddress();
SilentPaymentAddress silentPaymentAddress = getTransactionForm().getSilentPaymentAddress(output);
if(address != null) {
name = name + " " + address.getAddress();
} else if(silentPaymentAddress != null) {
name = name + " " + silentPaymentAddress.toAbbreviatedString();
} else {
try {
Address[] addresses = output.getScript().getToAddresses();
name = name + " [" + addresses[0].getAddress() + ",...]";
} catch(NonStandardScriptException e) {
//ignore
}
}
return name;
}
protected void addCoinbasePieData(PieChart pie, long value) { protected void addCoinbasePieData(PieChart pie, long value) {
ObservableList<PieChart.Data> outputsPieData = FXCollections.observableList(List.of(new PieChart.Data("Coinbase", value))); ObservableList<PieChart.Data> outputsPieData = FXCollections.observableList(List.of(new PieChart.Data("Coinbase", value)));
addPieData(pie, outputsPieData); addPieData(pie, outputsPieData);

View file

@ -14,6 +14,8 @@ import com.sparrowwallet.drongo.dns.DnsPaymentCache;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.protocol.TransactionOutput; import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.uri.BitcoinURI; import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.*; import com.sparrowwallet.sparrow.*;
@ -143,6 +145,8 @@ public class PaymentController extends WalletFormController implements Initializ
private final ObjectProperty<PayNym> payNymProperty = new SimpleObjectProperty<>(); private final ObjectProperty<PayNym> payNymProperty = new SimpleObjectProperty<>();
private final ObjectProperty<SilentPaymentAddress> silentPaymentAddressProperty = new SimpleObjectProperty<>();
private final ObjectProperty<DnsPayment> dnsPaymentProperty = new SimpleObjectProperty<>(); private final ObjectProperty<DnsPayment> dnsPaymentProperty = new SimpleObjectProperty<>();
private static final Wallet payNymWallet = new Wallet() { private static final Wallet payNymWallet = new Wallet() {
@ -172,6 +176,10 @@ public class PaymentController extends WalletFormController implements Initializ
dnsPaymentProperty.set(null); dnsPaymentProperty.set(null);
} }
if(silentPaymentAddressProperty.get() != null && !newValue.equals(silentPaymentAddressProperty.get().getAddress())) {
silentPaymentAddressProperty.set(null);
}
try { try {
BitcoinURI bitcoinURI = new BitcoinURI(newValue); BitcoinURI bitcoinURI = new BitcoinURI(newValue);
Platform.runLater(() -> updateFromURI(bitcoinURI)); Platform.runLater(() -> updateFromURI(bitcoinURI));
@ -237,6 +245,13 @@ public class PaymentController extends WalletFormController implements Initializ
} }
} }
try {
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(newValue);
setSilentPaymentAddress(silentPaymentAddress);
} catch(Exception e) {
//ignore, not a silent payment address
}
revalidateAmount(); revalidateAmount();
maxButton.setDisable(!isMaxButtonEnabled()); maxButton.setDisable(!isMaxButtonEnabled());
sendController.updateTransaction(); sendController.updateTransaction();
@ -312,6 +327,10 @@ public class PaymentController extends WalletFormController implements Initializ
revalidateAmount(); revalidateAmount();
}); });
silentPaymentAddressProperty.addListener((observable, oldValue, silentPaymentAddress) -> {
revalidateAmount();
});
dnsPaymentProperty.addListener((observable, oldValue, dnsPayment) -> { dnsPaymentProperty.addListener((observable, oldValue, dnsPayment) -> {
if(dnsPayment != null) { if(dnsPayment != null) {
MenuItem copyMenuItem = new MenuItem("Copy URI"); MenuItem copyMenuItem = new MenuItem("Copy URI");
@ -402,22 +421,36 @@ public class PaymentController extends WalletFormController implements Initializ
} }
public void setDnsPayment(DnsPayment dnsPayment) { public void setDnsPayment(DnsPayment dnsPayment) {
if(dnsPayment.bitcoinURI().getAddress() == null) { if(dnsPayment.hasAddress()) {
AppServices.showWarningDialog("No Address Provided", "The DNS payment instruction for " + dnsPayment.hrn() + " resolved correctly but did not contain a Bitcoin address."); DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getAddress(), dnsPayment);
} else if(dnsPayment.hasSilentPaymentAddress()) {
DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getSilentPaymentAddress(), dnsPayment);
setSilentPaymentAddress(dnsPayment.bitcoinURI().getSilentPaymentAddress());
} else {
AppServices.showWarningDialog("No Address Provided", "The DNS payment instruction for " + dnsPayment.hrn() + " resolved correctly but did not contain a bitcoin address.");
return; return;
} }
DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getAddress(), dnsPayment);
dnsPaymentProperty.set(dnsPayment); dnsPaymentProperty.set(dnsPayment);
address.setText(dnsPayment.hrn()); address.setText(dnsPayment.hrn());
revalidate(address, addressListener); revalidate(address, addressListener);
address.leftProperty().set(getBitcoinCharacter()); address.leftProperty().set(getBitcoinCharacter());
if(label.getText().isEmpty() || label.getText().startsWith("To ")) { if(label.getText().isEmpty() || (label.getText().startsWith("") && !label.getText().contains(" "))) {
label.setText("To " + dnsPayment); label.setText(dnsPayment.toString());
} }
label.requestFocus(); label.requestFocus();
} }
private void setSilentPaymentAddress(SilentPaymentAddress silentPaymentAddress) {
if(!sendController.getWalletForm().getWallet().canSendSilentPayments()) {
Platform.runLater(() -> AppServices.showErrorDialog("Silent Payments Unsupported", "This wallet does not support sending silent payments. Use a single signature software wallet."));
return;
}
silentPaymentAddressProperty.set(silentPaymentAddress);
label.requestFocus();
}
private void updateOpenWallets() { private void updateOpenWallets() {
updateOpenWallets(AppServices.get().getOpenWallets().keySet()); updateOpenWallets(AppServices.get().getOpenWallets().keySet());
} }
@ -489,8 +522,13 @@ public class PaymentController extends WalletFormController implements Initializ
} }
private Address getRecipientAddress() throws InvalidAddressException { private Address getRecipientAddress() throws InvalidAddressException {
SilentPaymentAddress silentPaymentAddress = silentPaymentAddressProperty.get();
if(silentPaymentAddress != null) {
return SilentPayment.getDummyAddress();
}
DnsPayment dnsPayment = dnsPaymentProperty.get(); DnsPayment dnsPayment = dnsPaymentProperty.get();
if(dnsPayment != null) { if(dnsPayment != null && dnsPayment.hasAddress()) {
return dnsPayment.bitcoinURI().getAddress(); return dnsPayment.bitcoinURI().getAddress();
} }
@ -630,7 +668,14 @@ public class PaymentController extends WalletFormController implements Initializ
Long value = sendAll ? Long.valueOf(getRecipientDustThreshold() + 1) : getRecipientValueSats(); Long value = sendAll ? Long.valueOf(getRecipientDustThreshold() + 1) : getRecipientValueSats();
if(!label.getText().isEmpty() && value != null && value >= getRecipientDustThreshold()) { if(!label.getText().isEmpty() && value != null && value >= getRecipientDustThreshold()) {
Payment payment = new Payment(recipientAddress, label.getText(), value, sendAll); Payment payment;
SilentPaymentAddress silentPaymentAddress = silentPaymentAddressProperty.get();
if(silentPaymentAddress != null) {
payment = new SilentPayment(silentPaymentAddress, label.getText(), value, sendAll);
} else {
payment = new Payment(recipientAddress, label.getText(), value, sendAll);
}
if(address.getUserData() != null) { if(address.getUserData() != null) {
payment.setType((Payment.Type)address.getUserData()); payment.setType((Payment.Type)address.getUserData());
} }
@ -647,7 +692,11 @@ public class PaymentController extends WalletFormController implements Initializ
public void setPayment(Payment payment) { public void setPayment(Payment payment) {
if(getRecipientValueSats() == null || payment.getAmount() != getRecipientValueSats()) { if(getRecipientValueSats() == null || payment.getAmount() != getRecipientValueSats()) {
if(payment.getAddress() != null) { if(payment.getAddress() != null) {
address.setText(payment.getAddress().toString()); if(payment instanceof SilentPayment silentPayment) {
address.setText(silentPayment.getSilentPaymentAddress().getAddress());
} else {
address.setText(payment.getAddress().toString());
}
address.setUserData(payment.getType()); address.setUserData(payment.getType());
} }
if(payment.getLabel() != null && !label.getText().equals(payment.getLabel())) { if(payment.getLabel() != null && !label.getText().equals(payment.getLabel())) {
@ -680,6 +729,7 @@ public class PaymentController extends WalletFormController implements Initializ
dustAmountProperty.set(false); dustAmountProperty.set(false);
payNymProperty.set(null); payNymProperty.set(null);
dnsPaymentProperty.set(null); dnsPaymentProperty.set(null);
silentPaymentAddressProperty.set(null);
} }
public void setMaxInput(ActionEvent event) { public void setMaxInput(ActionEvent event) {

View file

@ -6,12 +6,12 @@ import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Network; import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.SecureString; import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.bip47.PaymentCode; import com.sparrowwallet.drongo.bip47.PaymentCode;
import com.sparrowwallet.drongo.bip47.SecretPoint; import com.sparrowwallet.drongo.bip47.SecretPoint;
import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.*; import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.*; import com.sparrowwallet.sparrow.*;
import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.control.*;
@ -615,9 +615,14 @@ public class SendController extends WalletFormController implements Initializabl
boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs(); boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs();
BlockTransaction replacedTransaction = replacedTransactionProperty.get(); BlockTransaction replacedTransaction = replacedTransactionProperty.get();
//Disable RBF for silent payments, as we can't guarantee RBF won't be attempted on another device without knowledge to recompute the address if necessary
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(addressNodeMap, wallet, getUtxoSelectors(payments), getTxoFilters(),
payments, opReturnsList, excludedChangeNodes, payments, opReturnsList, excludedChangeNodes,
feeRate, getMinimumFeeRate(), minRelayFeeRate, userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs, replacedTransaction); feeRate, getMinimumFeeRate(), minRelayFeeRate, userFee,
currentBlockHeight, groupByAddress, includeMempoolOutputs, replacedTransaction, allowRbf);
walletTransactionService.setOnSucceeded(event -> { walletTransactionService.setOnSucceeded(event -> {
if(!walletTransactionService.isIgnoreResult()) { if(!walletTransactionService.isIgnoreResult()) {
walletTransactionProperty.setValue(walletTransactionService.getValue()); walletTransactionProperty.setValue(walletTransactionService.getValue());
@ -652,12 +657,12 @@ public class SendController extends WalletFormController implements Initializabl
walletTransactionService.start(); walletTransactionService.start();
} }
} catch(InvalidAddressException | IllegalStateException e) { } catch(IllegalStateException e) {
walletTransactionProperty.setValue(null); walletTransactionProperty.setValue(null);
} }
} }
private List<UtxoSelector> getUtxoSelectors(List<Payment> payments) throws InvalidAddressException { private List<UtxoSelector> getUtxoSelectors(List<Payment> payments) {
if(utxoSelectorProperty.get() != null) { if(utxoSelectorProperty.get() != null) {
return List.of(utxoSelectorProperty.get()); return List.of(utxoSelectorProperty.get());
} }
@ -694,13 +699,15 @@ public class SendController extends WalletFormController implements Initializabl
private final boolean groupByAddress; private final boolean groupByAddress;
private final boolean includeMempoolOutputs; private final boolean includeMempoolOutputs;
private final BlockTransaction replacedTransaction; private final BlockTransaction replacedTransaction;
private final boolean allowRbf;
private boolean ignoreResult; private boolean ignoreResult;
public WalletTransactionService(Map<Wallet, Map<Address, WalletNode>> addressNodeMap, public WalletTransactionService(Map<Wallet, Map<Address, WalletNode>> addressNodeMap,
Wallet wallet, List<UtxoSelector> utxoSelectors, List<TxoFilter> txoFilters, Wallet wallet, List<UtxoSelector> utxoSelectors, List<TxoFilter> txoFilters,
List<Payment> payments, List<byte[]> opReturns, Set<WalletNode> excludedChangeNodes, List<Payment> payments, List<byte[]> opReturns, Set<WalletNode> excludedChangeNodes,
double feeRate, double longTermFeeRate, double minRelayFeeRate, Long fee, double feeRate, double longTermFeeRate, double minRelayFeeRate, Long fee,
Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs, BlockTransaction replacedTransaction) { Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs,
BlockTransaction replacedTransaction, boolean allowRbf) {
this.addressNodeMap = addressNodeMap; this.addressNodeMap = addressNodeMap;
this.wallet = wallet; this.wallet = wallet;
this.utxoSelectors = utxoSelectors; this.utxoSelectors = utxoSelectors;
@ -716,6 +723,7 @@ public class SendController extends WalletFormController implements Initializabl
this.groupByAddress = groupByAddress; this.groupByAddress = groupByAddress;
this.includeMempoolOutputs = includeMempoolOutputs; this.includeMempoolOutputs = includeMempoolOutputs;
this.replacedTransaction = replacedTransaction; this.replacedTransaction = replacedTransaction;
this.allowRbf = allowRbf;
} }
@Override @Override
@ -725,7 +733,8 @@ public class SendController extends WalletFormController implements Initializabl
try { try {
return getWalletTransaction(); return getWalletTransaction();
} catch(InsufficientFundsException e) { } catch(InsufficientFundsException e) {
if(e.getTargetValue() != null && replacedTransaction != null && utxoSelectors.size() == 1 && utxoSelectors.get(0) instanceof PresetUtxoSelector presetUtxoSelector) { if(e.getTargetValue() != null && replacedTransaction != null && wallet.isSafeToAddInputsOrOutputs(replacedTransaction)
&& utxoSelectors.size() == 1 && utxoSelectors.getFirst() instanceof PresetUtxoSelector presetUtxoSelector) {
//Creating RBF transaction - include additional UTXOs if available to pay desired fee //Creating RBF transaction - include additional UTXOs if available to pay desired fee
List<TxoFilter> filters = new ArrayList<>(txoFilters); List<TxoFilter> filters = new ArrayList<>(txoFilters);
filters.add(presetUtxoSelector.asExcludeTxoFilter()); filters.add(presetUtxoSelector.asExcludeTxoFilter());
@ -734,7 +743,7 @@ public class SendController extends WalletFormController implements Initializabl
Collections.shuffle(outputGroups); Collections.shuffle(outputGroups);
while(!outputGroups.isEmpty() && presetUtxoSelector.getPresetUtxos().stream().mapToLong(BlockTransactionHashIndex::getValue).sum() < e.getTargetValue()) { while(!outputGroups.isEmpty() && presetUtxoSelector.getPresetUtxos().stream().mapToLong(BlockTransactionHashIndex::getValue).sum() < e.getTargetValue()) {
OutputGroup outputGroup = outputGroups.remove(0); OutputGroup outputGroup = outputGroups.removeFirst();
for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) { for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) {
presetUtxoSelector.getPresetUtxos().add(utxo); presetUtxoSelector.getPresetUtxos().add(utxo);
} }
@ -748,12 +757,16 @@ public class SendController extends WalletFormController implements Initializabl
} }
private WalletTransaction getWalletTransaction() throws InsufficientFundsException { private WalletTransaction getWalletTransaction() throws InsufficientFundsException {
updateMessage("Selecting UTXOs..."); try {
WalletTransaction walletTransaction = wallet.createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, excludedChangeNodes, updateMessage("Selecting UTXOs...");
feeRate, longTermFeeRate, minRelayFeeRate, fee, currentBlockHeight, groupByAddress, includeMempoolOutputs); WalletTransaction walletTransaction = wallet.createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, excludedChangeNodes,
updateMessage("Deriving keys..."); feeRate, longTermFeeRate, minRelayFeeRate, fee, currentBlockHeight, groupByAddress, includeMempoolOutputs, allowRbf);
walletTransaction.updateAddressNodeMap(addressNodeMap, walletTransaction.getWallet()); updateMessage("Deriving keys...");
return walletTransaction; walletTransaction.updateAddressNodeMap(addressNodeMap, walletTransaction.getWallet());
return walletTransaction;
} finally {
updateMessage("");
}
} }
}; };
} }
@ -1252,7 +1265,7 @@ public class SendController extends WalletFormController implements Initializabl
boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs(); boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs();
WalletTransaction finalWalletTx = decryptedWallet.createWalletTransaction(utxoSelectors, getTxoFilters(), walletTransaction.getPayments(), List.of(blindedPaymentCode), WalletTransaction finalWalletTx = decryptedWallet.createWalletTransaction(utxoSelectors, getTxoFilters(), walletTransaction.getPayments(), List.of(blindedPaymentCode),
excludedChangeNodes, feeRate, getMinimumFeeRate(), minRelayFeeRate, userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs); excludedChangeNodes, feeRate, getMinimumFeeRate(), minRelayFeeRate, userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs, true);
PSBT psbt = finalWalletTx.createPSBT(); PSBT psbt = finalWalletTx.createPSBT();
decryptedWallet.sign(psbt); decryptedWallet.sign(psbt);
decryptedWallet.finalise(psbt); decryptedWallet.finalise(psbt);
@ -1511,7 +1524,7 @@ public class SendController extends WalletFormController implements Initializabl
notificationButton.setVisible(isNotificationTransaction); notificationButton.setVisible(isNotificationTransaction);
notificationButton.setDefaultButton(isNotificationTransaction); notificationButton.setDefaultButton(isNotificationTransaction);
setInputFieldsDisabled(isNotificationTransaction, false); setInputFieldsDisabled(!event.allowPaymentChanges(), false);
} }
} }