mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2025-11-05 11:56:37 +00:00
add initial sending to silent payments support
This commit is contained in:
parent
6240667478
commit
efb1eb1051
20 changed files with 439 additions and 149 deletions
2
drongo
2
drongo
|
|
@ -1 +1 @@
|
||||||
Subproject commit d30cc4432cec39ff44ad6c11ab324200a9629a8c
|
Subproject commit a896809286f6f4393202110a15a4dd525f14cf73
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 -> {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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())) {
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue