diff --git a/drongo b/drongo index 286e04ad..4e68815f 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 286e04ad25de8f5739a70ac353ee073eace5e873 +Subproject commit 4e68815fa977a45a7caddead35e5d0f90f5e8fd6 diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java index bddf0748..46305d46 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java @@ -727,7 +727,7 @@ public class TransactionDiagram extends GridPane { recipientLabel.getStyleClass().add("output-label"); recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label"); Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment); - WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null; + WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null; Wallet toBip47Wallet = getBip47SendWallet(payment); DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment); Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagramLabel.java b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagramLabel.java index 76432d7b..825bd2e5 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagramLabel.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagramLabel.java @@ -90,20 +90,20 @@ public class TransactionDiagramLabel extends HBox { outputLabels.add(mixOutputLabel); } } else if(walletTx.getPayments().size() >= 5 && walletTx.getPayments().stream().mapToLong(Payment::getAmount).distinct().count() <= 1 && walletTx.getWallet() != null - && walletTx.getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && walletTx.getPayments().stream().anyMatch(walletTx::isConsolidationSend)) { + && walletTx.getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && !walletTx.getWalletNodePayments().isEmpty()) { OutputLabel remixOutputLabel = getRemixOutputLabel(transactionDiagram, walletTx.getPayments()); if(remixOutputLabel != null) { outputLabels.add(remixOutputLabel); } } else { - List payments = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT && !walletTx.isConsolidationSend(payment)).collect(Collectors.toList()); + List payments = walletTx.getExternalPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).collect(Collectors.toList()); List paymentLabels = payments.stream().map(payment -> getOutputLabel(transactionDiagram, payment)).collect(Collectors.toList()); if(walletTx.getSelectedUtxos().values().stream().allMatch(Objects::isNull)) { paymentLabels.sort(Comparator.comparingInt(paymentLabel -> (paymentLabel.text.startsWith("Receive") ? 0 : 1))); } outputLabels.addAll(paymentLabels); - List consolidations = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT && walletTx.isConsolidationSend(payment)).collect(Collectors.toList()); + List consolidations = walletTx.getWalletNodePayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).collect(Collectors.toList()); outputLabels.addAll(consolidations.stream().map(consolidation -> getOutputLabel(transactionDiagram, consolidation)).collect(Collectors.toList())); List mixes = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.MIX || payment.getType() == Payment.Type.FAKE_MIX).collect(Collectors.toList()); @@ -203,7 +203,7 @@ public class TransactionDiagramLabel extends HBox { private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Payment payment) { WalletTransaction walletTx = transactionDiagram.getWalletTransaction(); Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment); - WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null; + WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null; Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment); String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + payment; diff --git a/src/main/java/com/sparrowwallet/sparrow/glyphfont/GlyphUtils.java b/src/main/java/com/sparrowwallet/sparrow/glyphfont/GlyphUtils.java index ef263c85..ac29d816 100644 --- a/src/main/java/com/sparrowwallet/sparrow/glyphfont/GlyphUtils.java +++ b/src/main/java/com/sparrowwallet/sparrow/glyphfont/GlyphUtils.java @@ -1,6 +1,7 @@ package com.sparrowwallet.sparrow.glyphfont; import com.sparrowwallet.drongo.wallet.Payment; +import com.sparrowwallet.drongo.wallet.WalletNodePayment; import com.sparrowwallet.drongo.wallet.WalletTransaction; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.control.TransactionDiagram; @@ -15,7 +16,7 @@ public class GlyphUtils { return getFakeMixGlyph(); } else if(payment.getType().equals(Payment.Type.ANCHOR)) { return getAnchorGlyph(); - } else if(walletTx.isConsolidationSend(payment)) { + } else if(payment instanceof WalletNodePayment) { return getConsolidationGlyph(); } else if(walletTx.isPremixSend(payment)) { return getPremixGlyph(); diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index 5fa7e9c4..0e87444c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java @@ -643,19 +643,20 @@ public class HeadersController extends TransactionFormController implements Init List payments = new ArrayList<>(); List outputs = new ArrayList<>(); Map changeMap = new LinkedHashMap<>(); + Map receiveOutputScripts = wallet.getWalletOutputScripts(KeyPurpose.RECEIVE); Map changeOutputScripts = wallet.getWalletOutputScripts(wallet.getChangeKeyPurpose()); for(TransactionOutput txOutput : headersForm.getTransaction().getOutputs()) { WalletNode changeNode = changeOutputScripts.get(txOutput.getScript()); if(changeNode != null) { if(headersForm.getTransaction().getOutputs().size() == 4 && headersForm.getTransaction().getOutputs().stream().anyMatch(txo -> txo != txOutput && txo.getValue() == txOutput.getValue())) { if(selectedTxos.values().stream().allMatch(Objects::nonNull)) { - payments.add(new Payment(txOutput.getScript().getToAddress(), ".." + changeNode + " (Fake Mix)", txOutput.getValue(), false, Payment.Type.FAKE_MIX)); + payments.add(new WalletNodePayment(changeNode, ".." + changeNode + " (Fake Mix)", txOutput.getValue(), false, Payment.Type.FAKE_MIX)); } else { - payments.add(new Payment(txOutput.getScript().getToAddress(), ".." + changeNode + " (Mix)", txOutput.getValue(), false, Payment.Type.MIX)); + payments.add(new WalletNodePayment(changeNode, ".." + changeNode + " (Mix)", txOutput.getValue(), false, Payment.Type.MIX)); } } else { if(changeMap.containsKey(changeNode)) { - payments.add(new Payment(txOutput.getScript().getToAddress(), headersForm.getName(), txOutput.getValue(), false, Payment.Type.DEFAULT)); + payments.add(new WalletNodePayment(changeNode, headersForm.getName(), txOutput.getValue(), false, Payment.Type.DEFAULT)); } else { changeMap.put(changeNode, txOutput.getValue()); } @@ -672,12 +673,18 @@ public class HeadersController extends TransactionFormController implements Init BlockTransactionHashIndex receivedTxo = walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txOutput.getHash()) && txo.getIndex() == txOutput.getIndex()).findFirst().orElse(null); String label = headersForm.getName() == null || (headersForm.getName().startsWith("[") && headersForm.getName().endsWith("]") && headersForm.getName().length() == 8) ? null : headersForm.getName(); Address address = txOutput.getScript().getToAddress(); + WalletNode receiveNode = receiveOutputScripts.get(txOutput.getScript()); SilentPaymentAddress silentPaymentAddress = headersForm.getSilentPaymentAddress(txOutput); label = receivedTxo != null ? receivedTxo.getLabel() : label; if(address != null || silentPaymentAddress != null) { - Payment payment = (silentPaymentAddress == null ? - new Payment(address, label, txOutput.getValue(), false, paymentType) : - new SilentPayment(silentPaymentAddress, address, label, txOutput.getValue(), false)); + Payment payment; + if(silentPaymentAddress != null) { + payment = new SilentPayment(silentPaymentAddress, address, label, txOutput.getValue(), false); + } else if(receiveNode != null) { + payment = new WalletNodePayment(receiveNode, label, txOutput.getValue(), false, paymentType); + } else { + payment = new Payment(address, label, txOutput.getValue(), false, paymentType); + } WalletTransaction createdTx = AppServices.get().getCreatedTransaction(selectedTxos.keySet()); if(createdTx != null) { Optional optLabel = createdTx.getPayments().stream() @@ -689,8 +696,13 @@ public class HeadersController extends TransactionFormController implements Init } } payments.add(payment); - outputs.add(payment instanceof SilentPayment silentPayment ? new WalletTransaction.SilentPaymentOutput(txOutput, silentPayment) : - new WalletTransaction.PaymentOutput(txOutput, payment)); + if(payment instanceof SilentPayment silentPayment) { + outputs.add(new WalletTransaction.SilentPaymentOutput(txOutput, silentPayment)); + } else if(payment instanceof WalletNodePayment walletNodePayment) { + outputs.add(new WalletTransaction.ConsolidationOutput(txOutput, walletNodePayment, walletNodePayment.getAmount())); + } else { + outputs.add(new WalletTransaction.PaymentOutput(txOutput, payment)); + } } else { outputs.add(new WalletTransaction.NonAddressOutput(txOutput)); } diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/OutputController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/OutputController.java index b4cabac2..831ccc6c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/OutputController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/OutputController.java @@ -126,13 +126,14 @@ public class OutputController extends TransactionFormController implements Initi WalletTransaction.Output output = outputs.get(outputForm.getIndex()); if(output instanceof WalletTransaction.NonAddressOutput) { outputFieldset.setText(baseText); - } else if(output instanceof WalletTransaction.SilentPaymentOutput silentPaymentOutput) { + } else if(output instanceof WalletTransaction.SilentPaymentOutput) { outputFieldset.setText(baseText + " - Silent Payment"); + } else if(output instanceof WalletTransaction.ConsolidationOutput) { + outputFieldset.setText(baseText + " - Consolidation"); } else if(output instanceof WalletTransaction.PaymentOutput paymentOutput) { Payment payment = paymentOutput.getPayment(); Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment); - WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null; - outputFieldset.setText(baseText + (toWallet == null ? (toNode != null ? " - Consolidation" : " - Payment") : " - Received to " + toWallet.getFullDisplayName())); + outputFieldset.setText(baseText + (toWallet == null ? " - Payment" : " - Received to " + toWallet.getFullDisplayName())); } else if(output instanceof WalletTransaction.ChangeOutput changeOutput) { outputFieldset.setText(baseText + " - Change to " + changeOutput.getWalletNode().toString()); } else { diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/OutputForm.java b/src/main/java/com/sparrowwallet/sparrow/transaction/OutputForm.java index fee07185..95a451db 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/OutputForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/OutputForm.java @@ -91,6 +91,10 @@ public class OutputForm extends IndexedTransactionForm { Payment payment = paymentOutput.getPayment(); return new Label(payment.getLabel() != null && payment.getType() != Payment.Type.FAKE_MIX && payment.getType() != Payment.Type.MIX ? payment.getLabel() : payment.toString(), GlyphUtils.getOutputGlyph(getWalletTransaction(), payment)); + } else if(output instanceof WalletTransaction.ConsolidationOutput consolidationOutput) { + Payment payment = consolidationOutput.getWalletNodePayment(); + return new Label(payment.getLabel() != null && payment.getType() != Payment.Type.FAKE_MIX && payment.getType() != Payment.Type.MIX ? payment.getLabel() : payment.toString(), + GlyphUtils.getOutputGlyph(getWalletTransaction(), payment)); } else if(output instanceof WalletTransaction.ChangeOutput changeOutput) { return new Label("Change", GlyphUtils.getChangeGlyph()); } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java index 7d8f30ba..d15e905a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java @@ -143,6 +143,8 @@ public class PaymentController extends WalletFormController implements Initializ } }; + private final ObjectProperty consolidationNodeProperty = new SimpleObjectProperty<>(); + private final ObjectProperty payNymProperty = new SimpleObjectProperty<>(); private final ObjectProperty silentPaymentAddressProperty = new SimpleObjectProperty<>(); @@ -168,6 +170,10 @@ public class PaymentController extends WalletFormController implements Initializ public void changed(ObservableValue observable, String oldValue, String newValue) { address.leftProperty().set(null); + if(consolidationNodeProperty.get() != null && !newValue.equals(consolidationNodeProperty.get().getAddress().toString())) { + consolidationNodeProperty.set(null); + } + if(payNymProperty.get() != null && !newValue.equals(payNymProperty.get().nymName())) { payNymProperty.set(null); } @@ -259,6 +265,17 @@ public class PaymentController extends WalletFormController implements Initializ //ignore, not a silent payment address } + try { + Address toAddress = Address.fromString(newValue); + WalletNode walletNode = sendController.getWalletNode(toAddress); + if(walletNode != null) { + consolidationNodeProperty.set(walletNode); + } + label.requestFocus(); + } catch(Exception e) { + //ignore, not an address + } + revalidateAmount(); maxButton.setDisable(!isMaxButtonEnabled()); sendController.updateTransaction(); @@ -658,8 +675,11 @@ public class PaymentController extends WalletFormController implements Initializ if(!label.getText().isEmpty() && value != null && value >= getRecipientDustThreshold()) { Payment payment; SilentPaymentAddress silentPaymentAddress = silentPaymentAddressProperty.get(); + WalletNode consolidationNode = consolidationNodeProperty.get(); if(silentPaymentAddress != null) { payment = new SilentPayment(silentPaymentAddress, label.getText(), value, sendAll); + } else if(consolidationNode != null) { + payment = new WalletNodePayment(consolidationNode, label.getText(), value, sendAll); } else { payment = new Payment(recipientAddress, label.getText(), value, sendAll); } @@ -718,6 +738,7 @@ public class PaymentController extends WalletFormController implements Initializ setSendMax(false); dustAmountProperty.set(false); + consolidationNodeProperty.set(null); payNymProperty.set(null); dnsPaymentProperty.set(null); silentPaymentAddressProperty.set(null); @@ -728,8 +749,7 @@ public class PaymentController extends WalletFormController implements Initializ if(utxoSelector == null) { MaxUtxoSelector maxUtxoSelector = new MaxUtxoSelector(); sendController.utxoSelectorProperty().set(maxUtxoSelector); - } else if(utxoSelector instanceof PresetUtxoSelector && !isValidAddressAndLabel() && sendController.getPaymentTabs().getTabs().size() == 1) { - PresetUtxoSelector presetUtxoSelector = (PresetUtxoSelector)utxoSelector; + } else if(utxoSelector instanceof PresetUtxoSelector presetUtxoSelector && !isValidAddressAndLabel() && sendController.getPaymentTabs().getTabs().size() == 1) { Payment payment = new Payment(null, null, presetUtxoSelector.getPresetUtxos().stream().mapToLong(BlockTransactionHashIndex::getValue).sum(), true); setPayment(payment); return; diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index 21720f17..435b5018 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -172,7 +172,7 @@ public class SendController extends WalletFormController implements Initializabl private final Set excludedChangeNodes = new HashSet<>(); - private final Map> addressNodeMap = new HashMap<>(); + private final Map walletAddresses = new HashMap<>(); private final ChangeListener feeListener = new ChangeListener<>() { @Override @@ -619,7 +619,7 @@ public class SendController extends WalletFormController implements Initializabl boolean allowRbf = (replacedTransaction == null || replacedTransaction.getTransaction().isReplaceByFee()) && payments.stream().noneMatch(payment -> payment instanceof SilentPayment); - walletTransactionService = new WalletTransactionService(addressNodeMap, wallet, getUtxoSelectors(payments), getTxoFilters(), + walletTransactionService = new WalletTransactionService(wallet, getUtxoSelectors(payments), getTxoFilters(), payments, opReturnsList, excludedChangeNodes, feeRate, getMinimumFeeRate(), minRelayFeeRate, userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs, replacedTransaction, allowRbf); @@ -684,7 +684,6 @@ public class SendController extends WalletFormController implements Initializabl } private static class WalletTransactionService extends Service { - private final Map> addressNodeMap; private final Wallet wallet; private final List utxoSelectors; private final List txoFilters; @@ -702,13 +701,11 @@ public class SendController extends WalletFormController implements Initializabl private final boolean allowRbf; private boolean ignoreResult; - public WalletTransactionService(Map> addressNodeMap, - Wallet wallet, List utxoSelectors, List txoFilters, + public WalletTransactionService(Wallet wallet, List utxoSelectors, List txoFilters, List payments, List opReturns, Set excludedChangeNodes, double feeRate, double longTermFeeRate, double minRelayFeeRate, Long fee, Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs, BlockTransaction replacedTransaction, boolean allowRbf) { - this.addressNodeMap = addressNodeMap; this.wallet = wallet; this.utxoSelectors = utxoSelectors; this.txoFilters = txoFilters; @@ -759,11 +756,8 @@ public class SendController extends WalletFormController implements Initializabl private WalletTransaction getWalletTransaction() throws InsufficientFundsException { try { updateMessage("Selecting UTXOs..."); - WalletTransaction walletTransaction = wallet.createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, excludedChangeNodes, + return wallet.createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, excludedChangeNodes, feeRate, longTermFeeRate, minRelayFeeRate, fee, currentBlockHeight, groupByAddress, includeMempoolOutputs, allowRbf); - updateMessage("Deriving keys..."); - walletTransaction.updateAddressNodeMap(addressNodeMap, walletTransaction.getWallet()); - return walletTransaction; } finally { updateMessage(""); } @@ -1131,7 +1125,7 @@ public class SendController extends WalletFormController implements Initializabl paymentCodeProperty.set(null); - addressNodeMap.clear(); + walletAddresses.clear(); } public UtxoSelector getUtxoSelector() { @@ -1209,13 +1203,20 @@ public class SendController extends WalletFormController implements Initializabl WalletTransaction walletTransaction = walletTransactionProperty.get(); Set nodes = new LinkedHashSet<>(walletTransaction.getSelectedUtxos().values()); nodes.addAll(walletTransaction.getChangeMap().keySet()); - Map addressNodeMap = walletTransaction.getAddressNodeMap(); - nodes.addAll(addressNodeMap.values().stream().filter(Objects::nonNull).collect(Collectors.toList())); + nodes.addAll(walletTransaction.getWalletNodePayments().stream().map(WalletNodePayment::getWalletNode).collect(Collectors.toList())); //All wallet nodes applicable to this transaction are stored so when the subscription status for one is updated, the history for all can be fetched in one atomic update walletForm.addWalletTransactionNodes(nodes); } + public WalletNode getWalletNode(Address address) { + if(walletAddresses.isEmpty()) { + walletAddresses.putAll(getWalletForm().getWallet().getWalletAddresses()); + } + + return walletAddresses.get(address); + } + public void broadcastNotification(ActionEvent event) { Wallet wallet = getWalletForm().getWallet(); Storage storage = AppServices.get().getOpenWallets().get(wallet); @@ -1667,12 +1668,12 @@ public class SendController extends WalletFormController implements Initializabl public PrivacyAnalysisTooltip(WalletTransaction walletTransaction) { List payments = walletTransaction.getPayments(); List userPayments = payments.stream().filter(payment -> payment.getType() != Payment.Type.FAKE_MIX).collect(Collectors.toList()); - Map walletAddresses = walletTransaction.getAddressNodeMap(); + List walletNodePayments = walletTransaction.getWalletNodePayments(); OptimizationStrategy optimizationStrategy = getPreferredOptimizationStrategy(); boolean fakeMixPresent = payments.stream().anyMatch(payment -> payment.getType() == Payment.Type.FAKE_MIX); boolean roundPaymentAmounts = userPayments.stream().anyMatch(payment -> payment.getAmount() % 100 == 0); boolean mixedAddressTypes = userPayments.stream().anyMatch(payment -> payment.getAddress().getScriptType() != getWalletForm().getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress().getScriptType()); - boolean addressReuse = userPayments.stream().anyMatch(payment -> walletAddresses.get(payment.getAddress()) != null && !walletAddresses.get(payment.getAddress()).getTransactionOutputs().isEmpty()); + boolean addressReuse = walletNodePayments.stream().anyMatch(walletNodePayment -> !walletNodePayment.getWalletNode().getTransactionOutputs().isEmpty()); boolean payjoinPresent = userPayments.stream().anyMatch(payment -> AppServices.getPayjoinURI(payment.getAddress()) != null); if(optimizationStrategy == OptimizationStrategy.PRIVACY) {