From ebfdfc0c9f60955d0965cccb4ff112f242630e4c Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Tue, 11 Jul 2023 09:08:40 +0200 Subject: [PATCH] use txo filters for all wallet transaction output filtering, fixing overselection of inputs during rbf --- drongo | 2 +- .../sparrowwallet/sparrow/AppController.java | 2 +- .../sparrowwallet/sparrow/AppServices.java | 2 +- .../sparrow/control/EntryCell.java | 16 +-- .../sparrow/event/SpendUtxoEvent.java | 25 ++-- .../sparrow/paynym/PayNymController.java | 8 +- .../soroban/CounterpartyController.java | 6 +- .../sparrow/soroban/InitiatorController.java | 6 +- .../sparrow/wallet/SendController.java | 121 ++++++++---------- .../dataSource/SparrowDataSource.java | 4 +- 10 files changed, 90 insertions(+), 102 deletions(-) diff --git a/drongo b/drongo index 6a7d2aac..8484dd39 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 6a7d2aac285e9300b5897d1b64d0e5baa25e428d +Subproject commit 8484dd397b95d200cf3c363cd48e5751550b3bcb diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 05e282ad..df5935d0 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -1298,7 +1298,7 @@ public class AppController implements Initializable { Optional> optPayments = sendToManyDialog.showAndWait(); optPayments.ifPresent(payments -> { if(!payments.isEmpty()) { - EventManager.get().post(new SendActionEvent(wallet, new ArrayList<>(wallet.getWalletUtxos().keySet()))); + EventManager.get().post(new SendActionEvent(wallet, new ArrayList<>(wallet.getSpendableUtxos().keySet()))); Platform.runLater(() -> EventManager.get().post(new SendPaymentsEvent(wallet, payments))); } }); diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index 3fad7221..e01a9cbc 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -909,7 +909,7 @@ public class AppServices { if(wallet != null) { final Wallet sendingWallet = wallet; - EventManager.get().post(new SendActionEvent(sendingWallet, new ArrayList<>(sendingWallet.getWalletUtxos().keySet()), true)); + EventManager.get().post(new SendActionEvent(sendingWallet, new ArrayList<>(sendingWallet.getSpendableUtxos().keySet()), true)); Platform.runLater(() -> EventManager.get().post(new SendPaymentsEvent(sendingWallet, List.of(bitcoinURI.toPayment())))); } } catch(Exception e) { diff --git a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java index 97c5f28c..6b72f4bc 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java @@ -244,9 +244,9 @@ public class EntryCell extends TreeTableCell implements Confirmati Transaction tx = blockTransaction.getTransaction(); double vSize = tx.getVirtualSize(); int inputSize = tx.getInputs().get(0).getLength() + (tx.getInputs().get(0).hasWitness() ? tx.getInputs().get(0).getWitness().getLength() / Transaction.WITNESS_SCALE_FACTOR : 0); - List walletUtxos = new ArrayList<>(transactionEntry.getWallet().getWalletUtxos().keySet()); - //Remove any UTXOs that are frozen or created by the transaction that is to be replaced - walletUtxos.removeIf(utxo -> utxo.getStatus() == Status.FROZEN || ourOutputs.stream().anyMatch(output -> output.getHash().equals(utxo.getHash()) && output.getIndex() == utxo.getIndex())); + List walletUtxos = new ArrayList<>(transactionEntry.getWallet().getSpendableUtxos(blockTransaction).keySet()); + //Remove the UTXOs we are respending + walletUtxos.removeAll(utxos); Collections.shuffle(walletUtxos); while((double)changeTotal / vSize < getMaxFeeRate() && !walletUtxos.isEmpty() && !cancelTransaction) { //If there is insufficient change output, include another random UTXO so the fee can be increased @@ -319,7 +319,7 @@ public class EntryCell extends TreeTableCell implements Confirmati } EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos)); - Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, blockTransaction.getFee(), true))); + Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, blockTransaction.getFee(), true, blockTransaction))); } private static Double getMaxFeeRate() { @@ -350,9 +350,9 @@ public class EntryCell extends TreeTableCell implements Confirmati int inputSize = freshAddress.getScriptType().getInputVbytes(); long vSize = inputSize + txOutput.getLength(); - List walletUtxos = new ArrayList<>(transactionEntry.getWallet().getWalletUtxos().keySet()); - //Remove any UTXOs that are frozen or that we are already spending - walletUtxos.removeIf(utxo -> utxo.getStatus() == Status.FROZEN || utxo.equals(cpfpUtxo)); + List walletUtxos = new ArrayList<>(transactionEntry.getWallet().getSpendableUtxos().keySet()); + //Remove the UTXO we are already spending + walletUtxos.remove(cpfpUtxo); Collections.shuffle(walletUtxos); List utxos = new ArrayList<>(); @@ -371,7 +371,7 @@ public class EntryCell extends TreeTableCell implements Confirmati Payment payment = new Payment(freshAddress, label, inputTotal, true); EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos)); - Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), false))); + Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), true, null))); } private static boolean canSignMessage(WalletNode walletNode) { diff --git a/src/main/java/com/sparrowwallet/sparrow/event/SpendUtxoEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/SpendUtxoEvent.java index b0be72fb..8d73103b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/SpendUtxoEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/SpendUtxoEvent.java @@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.event; import com.samourai.whirlpool.client.whirlpool.beans.Pool; import com.sparrowwallet.drongo.bip47.PaymentCode; +import com.sparrowwallet.drongo.wallet.BlockTransaction; import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex; import com.sparrowwallet.drongo.wallet.Payment; import com.sparrowwallet.drongo.wallet.Wallet; @@ -14,21 +15,23 @@ public class SpendUtxoEvent { private final List payments; private final List opReturns; private final Long fee; - private final boolean includeSpentMempoolOutputs; + private final boolean requireAllUtxos; + private final BlockTransaction replacedTransaction; private final Pool pool; private final PaymentCode paymentCode; public SpendUtxoEvent(Wallet wallet, List utxos) { - this(wallet, utxos, null, null, null, false); + this(wallet, utxos, null, null, null, false, null); } - public SpendUtxoEvent(Wallet wallet, List utxos, List payments, List opReturns, Long fee, boolean includeSpentMempoolOutputs) { + public SpendUtxoEvent(Wallet wallet, List utxos, List payments, List opReturns, Long fee, boolean requireAllUtxos, BlockTransaction replacedTransaction) { this.wallet = wallet; this.utxos = utxos; this.payments = payments; this.opReturns = opReturns; this.fee = fee; - this.includeSpentMempoolOutputs = includeSpentMempoolOutputs; + this.requireAllUtxos = requireAllUtxos; + this.replacedTransaction = replacedTransaction; this.pool = null; this.paymentCode = null; } @@ -39,7 +42,8 @@ public class SpendUtxoEvent { this.payments = payments; this.opReturns = opReturns; this.fee = fee; - this.includeSpentMempoolOutputs = false; + this.requireAllUtxos = false; + this.replacedTransaction = null; this.pool = pool; this.paymentCode = null; } @@ -50,7 +54,8 @@ public class SpendUtxoEvent { this.payments = payments; this.opReturns = opReturns; this.fee = null; - this.includeSpentMempoolOutputs = false; + this.requireAllUtxos = false; + this.replacedTransaction = null; this.pool = null; this.paymentCode = paymentCode; } @@ -75,8 +80,12 @@ public class SpendUtxoEvent { return fee; } - public boolean isIncludeSpentMempoolOutputs() { - return includeSpentMempoolOutputs; + public boolean isRequireAllUtxos() { + return requireAllUtxos; + } + + public BlockTransaction getReplacedTransaction() { + return replacedTransaction; } public Pool getPool() { diff --git a/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java index 3269d841..6dbd9cd8 100644 --- a/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java +++ b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java @@ -465,7 +465,7 @@ public class PayNymController { PaymentCode paymentCode = payNym.paymentCode(); Payment payment = new Payment(paymentCode.getNotificationAddress(), "Link " + payNym.nymName(), MINIMUM_P2PKH_OUTPUT_SATS, false); Wallet wallet = AppServices.get().getWallet(walletId); - EventManager.get().post(new SendActionEvent(wallet, new ArrayList<>(wallet.getWalletUtxos().keySet()))); + EventManager.get().post(new SendActionEvent(wallet, new ArrayList<>(wallet.getSpendableUtxos().keySet()))); Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(wallet, List.of(payment), List.of(new byte[80]), paymentCode))); closeProperty.set(true); } else { @@ -604,10 +604,10 @@ public class PayNymController { boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs(); long noInputsFee = getMasterWallet().getNoInputsFee(payments, feeRate); - List utxoSelectors = List.of(utxos == null ? new KnapsackUtxoSelector(noInputsFee) : new PresetUtxoSelector(utxos, true)); - List utxoFilters = List.of(new FrozenUtxoFilter(), new CoinbaseUtxoFilter(wallet)); + List utxoSelectors = List.of(utxos == null ? new KnapsackUtxoSelector(noInputsFee) : new PresetUtxoSelector(utxos, true, false)); + List txoFilters = List.of(new SpentTxoFilter(), new FrozenTxoFilter(), new CoinbaseTxoFilter(wallet)); - return wallet.createWalletTransaction(utxoSelectors, utxoFilters, payments, opReturns, Collections.emptySet(), feeRate, minimumFeeRate, null, AppServices.getCurrentBlockHeight(), groupByAddress, includeMempoolOutputs, false); + return wallet.createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, Collections.emptySet(), feeRate, minimumFeeRate, null, AppServices.getCurrentBlockHeight(), groupByAddress, includeMempoolOutputs); } private Map getNotificationTransaction(PaymentCode externalPaymentCode) { diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java b/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java index f99dbdc9..e653fd7a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java @@ -294,11 +294,9 @@ public class CounterpartyController extends SorobanController { sorobanProgressLabel.setText("Creating mix transaction..."); Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); - Map walletUtxos = wallet.getWalletUtxos(); + Map walletUtxos = wallet.getSpendableUtxos(); for(Map.Entry entry : walletUtxos.entrySet()) { - if(entry.getKey().getStatus() != Status.FROZEN) { - counterpartyCahootsWallet.addUtxo(entry.getValue(), wallet.getWalletTransaction(entry.getKey().getHash()), (int)entry.getKey().getIndex()); - } + counterpartyCahootsWallet.addUtxo(entry.getValue(), wallet.getWalletTransaction(entry.getKey().getHash()), (int)entry.getKey().getIndex()); } try { diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java b/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java index ac294af5..b384f153 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java @@ -466,11 +466,9 @@ public class InitiatorController extends SorobanController { Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); Payment payment = walletTransaction.getPayments().get(0); - Map firstSetUtxos = walletTransaction.isCoinControlUsed() ? walletTransaction.getSelectedUtxoSets().get(0) : wallet.getWalletUtxos(); + Map firstSetUtxos = walletTransaction.isCoinControlUsed() ? walletTransaction.getSelectedUtxoSets().get(0) : wallet.getSpendableUtxos(); for(Map.Entry entry : firstSetUtxos.entrySet()) { - if(entry.getKey().getStatus() != Status.FROZEN) { - initiatorCahootsWallet.addUtxo(entry.getValue(), wallet.getWalletTransaction(entry.getKey().getHash()), (int)entry.getKey().getIndex()); - } + initiatorCahootsWallet.addUtxo(entry.getValue(), wallet.getWalletTransaction(entry.getKey().getHash()), (int)entry.getKey().getIndex()); } SorobanCahootsService sorobanCahootsService = soroban.getSorobanCahootsService(initiatorCahootsWallet); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index 4fe86dd6..7443019b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -158,7 +158,7 @@ public class SendController extends WalletFormController implements Initializabl private final ObjectProperty utxoSelectorProperty = new SimpleObjectProperty<>(null); - private final ObjectProperty utxoFilterProperty = new SimpleObjectProperty<>(null); + private final ObjectProperty txoFilterProperty = new SimpleObjectProperty<>(null); private final ObjectProperty whirlpoolProperty = new SimpleObjectProperty<>(null); @@ -170,7 +170,7 @@ public class SendController extends WalletFormController implements Initializabl private final StringProperty utxoLabelSelectionProperty = new SimpleStringProperty(""); - private final BooleanProperty includeSpentMempoolOutputsProperty = new SimpleBooleanProperty(false); + private final ObjectProperty replacedTransactionProperty = new SimpleObjectProperty<>(null); private final List opReturnsList = new ArrayList<>(); @@ -385,11 +385,11 @@ public class SendController extends WalletFormController implements Initializabl }); utxoSelectorProperty.addListener((observable, oldValue, utxoSelector) -> { - updateMaxClearButtons(utxoSelector, utxoFilterProperty.get()); + updateMaxClearButtons(utxoSelector, txoFilterProperty.get()); }); - utxoFilterProperty.addListener((observable, oldValue, utxoFilter) -> { - updateMaxClearButtons(utxoSelectorProperty.get(), utxoFilter); + txoFilterProperty.addListener((observable, oldValue, txoFilter) -> { + updateMaxClearButtons(utxoSelectorProperty.get(), txoFilter); }); walletTransactionProperty.addListener((observable, oldValue, walletTransaction) -> { @@ -590,11 +590,11 @@ public class SendController extends WalletFormController implements Initializabl Integer currentBlockHeight = AppServices.getCurrentBlockHeight(); boolean groupByAddress = Config.get().isGroupByAddress(); boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs(); - boolean includeSpentMempoolOutputs = includeSpentMempoolOutputsProperty.get(); + BlockTransaction replacedTransaction = replacedTransactionProperty.get(); - walletTransactionService = new WalletTransactionService(addressNodeMap, wallet, getUtxoSelectors(payments), getUtxoFilters(), + walletTransactionService = new WalletTransactionService(addressNodeMap, wallet, getUtxoSelectors(payments), getTxoFilters(), payments, opReturnsList, excludedChangeNodes, - feeRate, getMinimumFeeRate(), userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs, includeSpentMempoolOutputs); + feeRate, getMinimumFeeRate(), userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs, replacedTransaction); walletTransactionService.setOnSucceeded(event -> { if(!walletTransactionService.isIgnoreResult()) { walletTransactionProperty.setValue(walletTransactionService.getValue()); @@ -660,7 +660,7 @@ public class SendController extends WalletFormController implements Initializabl private final Map> addressNodeMap; private final Wallet wallet; private final List utxoSelectors; - private final List utxoFilters; + private final List txoFilters; private final List payments; private final List opReturns; private final Set excludedChangeNodes; @@ -670,17 +670,17 @@ public class SendController extends WalletFormController implements Initializabl private final Integer currentBlockHeight; private final boolean groupByAddress; private final boolean includeMempoolOutputs; - private final boolean includeSpentMempoolOutputs; + private final BlockTransaction replacedTransaction; private boolean ignoreResult; public WalletTransactionService(Map> addressNodeMap, - Wallet wallet, List utxoSelectors, List utxoFilters, + Wallet wallet, List utxoSelectors, List txoFilters, List payments, List opReturns, Set excludedChangeNodes, - double feeRate, double longTermFeeRate, Long fee, Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs, boolean includeSpentMempoolOutputs) { + double feeRate, double longTermFeeRate, Long fee, Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs, BlockTransaction replacedTransaction) { this.addressNodeMap = addressNodeMap; this.wallet = wallet; this.utxoSelectors = utxoSelectors; - this.utxoFilters = utxoFilters; + this.txoFilters = txoFilters; this.payments = payments; this.opReturns = opReturns; this.excludedChangeNodes = excludedChangeNodes; @@ -690,7 +690,7 @@ public class SendController extends WalletFormController implements Initializabl this.currentBlockHeight = currentBlockHeight; this.groupByAddress = groupByAddress; this.includeMempoolOutputs = includeMempoolOutputs; - this.includeSpentMempoolOutputs = includeSpentMempoolOutputs; + this.replacedTransaction = replacedTransaction; } @Override @@ -700,31 +700,18 @@ public class SendController extends WalletFormController implements Initializabl try { return getWalletTransaction(); } catch(InsufficientFundsException e) { - if(e.getTargetValue() != null && includeSpentMempoolOutputs && utxoSelectors.size() == 1 && utxoSelectors.get(0) instanceof PresetUtxoSelector presetUtxoSelector) { - Optional optBlkTx = wallet.getWalletTransactions().values().stream().filter(blkTx -> { - return blkTx.getTransaction().getInputs().stream().anyMatch(txInput -> { - return presetUtxoSelector.getPresetUtxos().stream().anyMatch(ref -> { - return txInput.getOutpoint().getHash().equals(ref.getHash()) && txInput.getOutpoint().getIndex() == ref.getIndex(); - }); - }); - }).findFirst(); - - if(optBlkTx.isPresent()) { - //Creating RBF transaction - include additional UTXOs if available to pay desired fee - Transaction rbfTx = optBlkTx.get().getTransaction(); - List walletUtxos = new ArrayList<>(wallet.getWalletUtxos().keySet()); - //Remove any UTXOs that are frozen or created by the transaction that is to be replaced - walletUtxos.removeIf(utxo -> utxo.getStatus() == Status.FROZEN || rbfTx.getOutputs().stream().anyMatch(output -> output.getHash().equals(utxo.getHash()) && output.getIndex() == utxo.getIndex())); - //Remove any UTXOs that have already been added or previously excluded - walletUtxos.removeAll(presetUtxoSelector.getPresetUtxos()); - walletUtxos.removeAll(presetUtxoSelector.getExcludedUtxos()); - Collections.shuffle(walletUtxos); - while(!walletUtxos.isEmpty() && presetUtxoSelector.getPresetUtxos().stream().mapToLong(BlockTransactionHashIndex::getValue).sum() < e.getTargetValue()) { - presetUtxoSelector.getPresetUtxos().add(walletUtxos.remove(0)); - } - - return getWalletTransaction(); + if(e.getTargetValue() != null && replacedTransaction != null && utxoSelectors.size() == 1 && utxoSelectors.get(0) instanceof PresetUtxoSelector presetUtxoSelector) { + //Creating RBF transaction - include additional UTXOs if available to pay desired fee + List walletUtxos = new ArrayList<>(wallet.getWalletTxos(txoFilters).keySet()); + //Remove any UTXOs that have already been added or previously excluded + walletUtxos.removeAll(presetUtxoSelector.getPresetUtxos()); + walletUtxos.removeAll(presetUtxoSelector.getExcludedUtxos()); + Collections.shuffle(walletUtxos); + while(!walletUtxos.isEmpty() && presetUtxoSelector.getPresetUtxos().stream().mapToLong(BlockTransactionHashIndex::getValue).sum() < e.getTargetValue()) { + presetUtxoSelector.getPresetUtxos().add(walletUtxos.remove(0)); } + + return getWalletTransaction(); } throw e; @@ -733,8 +720,8 @@ public class SendController extends WalletFormController implements Initializabl private WalletTransaction getWalletTransaction() throws InsufficientFundsException { updateMessage("Selecting UTXOs..."); - WalletTransaction walletTransaction = wallet.createWalletTransaction(utxoSelectors, utxoFilters, payments, opReturns, excludedChangeNodes, - feeRate, longTermFeeRate, fee, currentBlockHeight, groupByAddress, includeMempoolOutputs, includeSpentMempoolOutputs); + WalletTransaction walletTransaction = wallet.createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, excludedChangeNodes, + feeRate, longTermFeeRate, fee, currentBlockHeight, groupByAddress, includeMempoolOutputs); updateMessage("Deriving keys..."); walletTransaction.updateAddressNodeMap(addressNodeMap, walletTransaction.getWallet()); return walletTransaction; @@ -751,13 +738,15 @@ public class SendController extends WalletFormController implements Initializabl } } - private List getUtxoFilters() { - UtxoFilter utxoFilter = utxoFilterProperty.get(); - if(utxoFilter != null) { - return List.of(utxoFilter, new FrozenUtxoFilter(), new CoinbaseUtxoFilter(getWalletForm().getWallet())); + private List getTxoFilters() { + SpentTxoFilter spentTxoFilter = new SpentTxoFilter(replacedTransactionProperty.get() == null ? null : replacedTransactionProperty.get().getHash()); + + TxoFilter txoFilter = txoFilterProperty.get(); + if(txoFilter != null) { + return List.of(txoFilter, spentTxoFilter, new FrozenTxoFilter(), new CoinbaseTxoFilter(getWalletForm().getWallet())); } - return List.of(new FrozenUtxoFilter(), new CoinbaseUtxoFilter(getWalletForm().getWallet())); + return List.of(spentTxoFilter, new FrozenTxoFilter(), new CoinbaseTxoFilter(getWalletForm().getWallet())); } private void updateFeeRateSelection(FeeRatesSelection feeRatesSelection) { @@ -1029,15 +1018,13 @@ public class SendController extends WalletFormController implements Initializabl return (int)Math.round(index * 10.0); } - private void updateMaxClearButtons(UtxoSelector utxoSelector, UtxoFilter utxoFilter) { - if(utxoSelector instanceof PresetUtxoSelector) { - PresetUtxoSelector presetUtxoSelector = (PresetUtxoSelector)utxoSelector; + private void updateMaxClearButtons(UtxoSelector utxoSelector, TxoFilter txoFilter) { + if(utxoSelector instanceof PresetUtxoSelector presetUtxoSelector) { int num = presetUtxoSelector.getPresetUtxos().size(); String selection = " (" + num + " UTXO" + (num != 1 ? "s" : "") + " selected)"; utxoLabelSelectionProperty.set(selection); - } else if(utxoFilter instanceof ExcludeUtxoFilter) { - ExcludeUtxoFilter excludeUtxoFilter = (ExcludeUtxoFilter)utxoFilter; - int num = excludeUtxoFilter.getExcludedUtxos().size(); + } else if(txoFilter instanceof ExcludeTxoFilter excludeTxoFilter) { + int num = excludeTxoFilter.getExcludedTxos().size(); String exclusion = " (" + num + " UTXO" + (num != 1 ? "s" : "") + " excluded)"; utxoLabelSelectionProperty.set(exclusion); } else { @@ -1130,8 +1117,8 @@ public class SendController extends WalletFormController implements Initializabl userFeeSet.set(false); setDefaultFeeRate(); utxoSelectorProperty.setValue(null); - utxoFilterProperty.setValue(null); - includeSpentMempoolOutputsProperty.set(false); + txoFilterProperty.setValue(null); + replacedTransactionProperty.setValue(null); opReturnsList.clear(); excludedChangeNodes.clear(); walletTransactionProperty.setValue(null); @@ -1320,15 +1307,14 @@ public class SendController extends WalletFormController implements Initializabl byte[] blindingMask = PaymentCode.getMask(secretPoint.ECDHSecretAsBytes(), input0Outpoint.bitcoinSerialize()); byte[] blindedPaymentCode = PaymentCode.blind(paymentCode.getPayload(), blindingMask); - List utxoSelectors = List.of(new PresetUtxoSelector(walletTransaction.getSelectedUtxos().keySet(), true)); + List utxoSelectors = List.of(new PresetUtxoSelector(walletTransaction.getSelectedUtxos().keySet(), true, false)); Long userFee = userFeeSet.get() ? getFeeValueSats() : null; double feeRate = getUserFeeRate(); Integer currentBlockHeight = AppServices.getCurrentBlockHeight(); boolean groupByAddress = Config.get().isGroupByAddress(); boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs(); - boolean includeSpentMempoolOutputs = includeSpentMempoolOutputsProperty.get(); - WalletTransaction finalWalletTx = decryptedWallet.createWalletTransaction(utxoSelectors, getUtxoFilters(), walletTransaction.getPayments(), List.of(blindedPaymentCode), excludedChangeNodes, feeRate, getMinimumFeeRate(), userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs, includeSpentMempoolOutputs); + WalletTransaction finalWalletTx = decryptedWallet.createWalletTransaction(utxoSelectors, getTxoFilters(), walletTransaction.getPayments(), List.of(blindedPaymentCode), excludedChangeNodes, feeRate, getMinimumFeeRate(), userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs); PSBT psbt = finalWalletTx.createPSBT(); decryptedWallet.sign(psbt); decryptedWallet.finalise(psbt); @@ -1553,14 +1539,14 @@ public class SendController extends WalletFormController implements Initializabl userFeeSet.set(true); } - includeSpentMempoolOutputsProperty.set(event.isIncludeSpentMempoolOutputs()); + replacedTransactionProperty.set(event.getReplacedTransaction()); if(event.getUtxos() != null) { List utxos = event.getUtxos(); - utxoSelectorProperty.set(new PresetUtxoSelector(utxos)); + utxoSelectorProperty.set(new PresetUtxoSelector(utxos, false, event.isRequireAllUtxos())); } - utxoFilterProperty.set(null); + txoFilterProperty.set(null); whirlpoolProperty.set(event.getPool()); paymentCodeProperty.set(event.getPaymentCode()); updateTransaction(event.getPayments() == null || event.getPayments().stream().anyMatch(Payment::isSendMax)); @@ -1628,11 +1614,8 @@ public class SendController extends WalletFormController implements Initializabl if(event.getWalletTransaction() == walletTransactionProperty.get()) { UtxoSelector utxoSelector = utxoSelectorProperty.get(); if(utxoSelector instanceof MaxUtxoSelector) { - Collection utxos = walletForm.getWallet().getWalletUtxos().keySet(); + Collection utxos = event.getWalletTransaction().getSelectedUtxos().keySet(); utxos.remove(event.getUtxo()); - if(utxoFilterProperty.get() instanceof ExcludeUtxoFilter existingUtxoFilter) { - utxos.removeAll(existingUtxoFilter.getExcludedUtxos()); - } PresetUtxoSelector presetUtxoSelector = new PresetUtxoSelector(utxos); presetUtxoSelector.getExcludedUtxos().add(event.getUtxo()); utxoSelectorProperty.set(presetUtxoSelector); @@ -1642,15 +1625,15 @@ public class SendController extends WalletFormController implements Initializabl presetUtxoSelector.getPresetUtxos().remove(event.getUtxo()); presetUtxoSelector.getExcludedUtxos().add(event.getUtxo()); utxoSelectorProperty.set(presetUtxoSelector); - updateTransaction(!includeSpentMempoolOutputsProperty.get()); + updateTransaction(replacedTransactionProperty.get() == null); } else { - ExcludeUtxoFilter utxoFilter = new ExcludeUtxoFilter(); - if(utxoFilterProperty.get() instanceof ExcludeUtxoFilter existingUtxoFilter) { - utxoFilter.getExcludedUtxos().addAll(existingUtxoFilter.getExcludedUtxos()); + ExcludeTxoFilter excludeTxoFilter = new ExcludeTxoFilter(); + if(txoFilterProperty.get() instanceof ExcludeTxoFilter existingTxoFilter) { + excludeTxoFilter.getExcludedTxos().addAll(existingTxoFilter.getExcludedTxos()); } - utxoFilter.getExcludedUtxos().add(event.getUtxo()); - utxoFilterProperty.set(utxoFilter); + excludeTxoFilter.getExcludedTxos().add(event.getUtxo()); + txoFilterProperty.set(excludeTxoFilter); updateTransaction(); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowDataSource.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowDataSource.java index 022d1179..6504ee17 100644 --- a/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowDataSource.java +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowDataSource.java @@ -105,9 +105,9 @@ public class SparrowDataSource extends WalletResponseDataSource { address.n_tx = walletTransactions.size(); addresses.add(address); - for(Map.Entry utxo : wallet.getWalletUtxos().entrySet()) { + for(Map.Entry utxo : wallet.getSpendableUtxos().entrySet()) { BlockTransaction blockTransaction = wallet.getWalletTransaction(utxo.getKey().getHash()); - if(blockTransaction != null && utxo.getKey().getStatus() != Status.FROZEN) { + if(blockTransaction != null) { unspentOutputs.add(Whirlpool.getUnspentOutput(utxo.getValue(), blockTransaction, (int)utxo.getKey().getIndex())); } }