From 0a469a380bd11424a96214499fd0ced13e213212 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Tue, 11 Jul 2023 11:25:29 +0200 Subject: [PATCH] when constructing rbf and cpfp transactions, add any additional utxos by output group if effective fee is sufficient --- drongo | 2 +- .../sparrow/control/EntryCell.java | 52 +++++++++++-------- .../sparrow/wallet/SendController.java | 18 ++++--- 3 files changed, 41 insertions(+), 31 deletions(-) diff --git a/drongo b/drongo index 8484dd39..38b04b8e 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 8484dd397b95d200cf3c363cd48e5751550b3bcb +Subproject commit 38b04b8e0b802f6cd43b4e88730d4d3ed31227fc diff --git a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java index f0d34e01..88b9bc42 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java @@ -249,16 +249,19 @@ public class EntryCell extends TreeTableCell implements Confirmati vSize += changeOutput.getLength(); } double inputSize = tx.getInputs().get(0).getLength() + (tx.getInputs().get(0).hasWitness() ? (double)tx.getInputs().get(0).getWitness().getLength() / Transaction.WITNESS_SCALE_FACTOR : 0); - 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 - BlockTransactionHashIndex utxo = walletUtxos.remove(0); - utxos.add(utxo); - changeTotal += utxo.getValue(); - vSize += inputSize; + List txoFilters = List.of(new ExcludeTxoFilter(utxos), new SpentTxoFilter(blockTransaction.getHash()), new FrozenTxoFilter(), new CoinbaseTxoFilter(transactionEntry.getWallet())); + double feeRate = blockTransaction.getFeeRate() == null ? AppServices.getMinimumRelayFeeRate() : blockTransaction.getFeeRate(); + List outputGroups = transactionEntry.getWallet().getGroupedUtxos(txoFilters, feeRate, AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress()) + .stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList()); + Collections.shuffle(outputGroups); + while((double)changeTotal / vSize < getMaxFeeRate() && !outputGroups.isEmpty() && !cancelTransaction) { + //If there is insufficient change output, include another random output group so the fee can be increased + OutputGroup outputGroup = outputGroups.remove(0); + for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) { + utxos.add(utxo); + changeTotal += utxo.getValue(); + vSize += inputSize; + } } Long fee = blockTransaction.getFee(); @@ -336,7 +339,7 @@ public class EntryCell extends TreeTableCell implements Confirmati private static Double getMaxFeeRate() { if(AppServices.getTargetBlockFeeRates() == null || AppServices.getTargetBlockFeeRates().isEmpty()) { - return 1.0; + return 100.0; } return AppServices.getTargetBlockFeeRates().values().iterator().next(); @@ -359,23 +362,26 @@ public class EntryCell extends TreeTableCell implements Confirmati Address freshAddress = transactionEntry.getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress(); TransactionOutput txOutput = new TransactionOutput(new Transaction(), cpfpUtxo.getValue(), freshAddress.getOutputScript()); long dustThreshold = freshAddress.getScriptType().getDustThreshold(txOutput, Transaction.DUST_RELAY_TX_FEE); - int inputSize = freshAddress.getScriptType().getInputVbytes(); - long vSize = inputSize + txOutput.getLength(); + double inputSize = freshAddress.getScriptType().getInputVbytes(); + double vSize = inputSize + txOutput.getLength(); - List walletUtxos = new ArrayList<>(transactionEntry.getWallet().getSpendableUtxos().keySet()); - //Remove the UTXO we are already spending - walletUtxos.remove(cpfpUtxo); - Collections.shuffle(walletUtxos); + List txoFilters = List.of(new ExcludeTxoFilter(List.of(cpfpUtxo)), new SpentTxoFilter(), new FrozenTxoFilter(), new CoinbaseTxoFilter(transactionEntry.getWallet())); + double feeRate = blockTransaction.getFeeRate() == null ? AppServices.getMinimumRelayFeeRate() : blockTransaction.getFeeRate(); + List outputGroups = transactionEntry.getWallet().getGroupedUtxos(txoFilters, feeRate, AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress()) + .stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList()); + Collections.shuffle(outputGroups); List utxos = new ArrayList<>(); utxos.add(cpfpUtxo); long inputTotal = cpfpUtxo.getValue(); - while((inputTotal - (long)(getMaxFeeRate() * vSize)) < dustThreshold && !walletUtxos.isEmpty()) { - //If there is insufficient input value, include another random UTXO so the fee can be increased - BlockTransactionHashIndex utxo = walletUtxos.remove(0); - utxos.add(utxo); - inputTotal += utxo.getValue(); - vSize += inputSize; + while((inputTotal - (long)(getMaxFeeRate() * vSize)) < dustThreshold && !outputGroups.isEmpty()) { + //If there is insufficient input value, include another random output group so the fee can be increased + OutputGroup outputGroup = outputGroups.remove(0); + for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) { + utxos.add(utxo); + inputTotal += utxo.getValue(); + vSize += inputSize; + } } String label = transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel(); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index 7443019b..eed3fb3e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -702,13 +702,17 @@ public class SendController extends WalletFormController implements Initializabl } catch(InsufficientFundsException e) { 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)); + List filters = new ArrayList<>(txoFilters); + filters.add(presetUtxoSelector.asExcludeTxoFilter()); + List outputGroups = wallet.getGroupedUtxos(filters, feeRate, AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress()) + .stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList()); + Collections.shuffle(outputGroups); + + while(!outputGroups.isEmpty() && presetUtxoSelector.getPresetUtxos().stream().mapToLong(BlockTransactionHashIndex::getValue).sum() < e.getTargetValue()) { + OutputGroup outputGroup = outputGroups.remove(0); + for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) { + presetUtxoSelector.getPresetUtxos().add(utxo); + } } return getWalletTransaction();