diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/BnBUtxoSelector.java b/src/main/java/com/sparrowwallet/drongo/wallet/BnBUtxoSelector.java index 2bfda88..c32ff8d 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/BnBUtxoSelector.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/BnBUtxoSelector.java @@ -4,19 +4,15 @@ import com.sparrowwallet.drongo.protocol.Transaction; import java.util.*; -import static com.sparrowwallet.drongo.protocol.Transaction.WITNESS_SCALE_FACTOR; - public class BnBUtxoSelector implements UtxoSelector { private static final int TOTAL_TRIES = 100000; - private final int noInputsWeightUnits; - private final Double feeRate; + private final long noInputsFee; private final long costOfChangeValue; - public BnBUtxoSelector(Wallet wallet, int noInputsWeightUnits, Double feeRate, Double longTermFeeRate) { - this.noInputsWeightUnits = noInputsWeightUnits; - this.feeRate = feeRate; - this.costOfChangeValue = wallet.getCostOfChange(feeRate, longTermFeeRate); + public BnBUtxoSelector(long noInputsFee, long costOfChangeValue) { + this.noInputsFee = noInputsFee; + this.costOfChangeValue = costOfChangeValue; } @Override @@ -26,7 +22,7 @@ public class BnBUtxoSelector implements UtxoSelector { long currentValue = 0; ArrayDeque currentSelection = new ArrayDeque<>(utxoPool.size()); - long actualTargetValue = targetValue + (long)(noInputsWeightUnits * feeRate / WITNESS_SCALE_FACTOR); + long actualTargetValue = targetValue + noInputsFee; System.out.println("Selected must be: " + actualTargetValue + " < x < " + (actualTargetValue + costOfChangeValue)); long currentAvailableValue = utxoPool.stream().mapToLong(OutputGroup::getEffectiveValue).sum(); @@ -131,7 +127,6 @@ public class BnBUtxoSelector implements UtxoSelector { } private void printCurrentUtxoSet(List utxoPool, ArrayDeque currentSelection, long currentValue) { - long noInputsFee = (long)(noInputsWeightUnits * feeRate / WITNESS_SCALE_FACTOR); long inputsFee = 0; StringJoiner joiner = new StringJoiner(" + "); int i = 0; diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/KnapsackUtxoSelector.java b/src/main/java/com/sparrowwallet/drongo/wallet/KnapsackUtxoSelector.java index 3e5ac3e..7e14521 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/KnapsackUtxoSelector.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/KnapsackUtxoSelector.java @@ -8,8 +8,16 @@ import java.util.stream.Collectors; public class KnapsackUtxoSelector implements UtxoSelector { private static final long MIN_CHANGE = Transaction.SATOSHIS_PER_BITCOIN / 100; + private final long noInputsFee; + + public KnapsackUtxoSelector(long noInputsFee) { + this.noInputsFee = noInputsFee; + } + @Override public Collection select(long targetValue, Collection candidates) { + long actualTargetValue = targetValue + noInputsFee; + List shuffled = new ArrayList<>(candidates); Collections.shuffle(shuffled); @@ -18,9 +26,9 @@ public class KnapsackUtxoSelector implements UtxoSelector { long totalLower = 0; for(OutputGroup outputGroup : shuffled) { - if(outputGroup.getEffectiveValue() == targetValue) { + if(outputGroup.getEffectiveValue() == actualTargetValue) { return new ArrayList<>(outputGroup.getUtxos()); - } else if(outputGroup.getEffectiveValue() < targetValue + MIN_CHANGE) { + } else if(outputGroup.getEffectiveValue() < actualTargetValue + MIN_CHANGE) { applicableGroups.add(outputGroup); totalLower += outputGroup.getEffectiveValue(); } else if(lowestLarger == null || outputGroup.getEffectiveValue() < lowestLarger.getEffectiveValue()) { @@ -28,32 +36,32 @@ public class KnapsackUtxoSelector implements UtxoSelector { } } - if(totalLower == targetValue) { + if(totalLower == actualTargetValue) { return applicableGroups.stream().flatMap(outputGroup -> outputGroup.getUtxos().stream()).collect(Collectors.toList()); } - if(totalLower < targetValue) { + if(totalLower < actualTargetValue) { if(lowestLarger == null) { return Collections.emptyList(); } return lowestLarger.getUtxos(); } - //We now have a list of UTXOs that are all smaller than the target + MIN_CHANGE, but together sum to greater than targetValue + //We now have a list of UTXOs that are all smaller than the target + MIN_CHANGE, but together sum to greater than actualTargetValue // Solve subset sum by stochastic approximation applicableGroups.sort((a, b) -> (int)(b.getEffectiveValue() - a.getEffectiveValue())); boolean[] bestSelection = new boolean[applicableGroups.size()]; - long bestValue = findApproximateBestSubset(applicableGroups, totalLower, targetValue, bestSelection); - if(bestValue != targetValue && totalLower >= targetValue + MIN_CHANGE) { - bestValue = findApproximateBestSubset(applicableGroups, totalLower, targetValue + MIN_CHANGE, bestSelection); + long bestValue = findApproximateBestSubset(applicableGroups, totalLower, actualTargetValue, bestSelection); + if(bestValue != actualTargetValue && totalLower >= actualTargetValue + MIN_CHANGE) { + bestValue = findApproximateBestSubset(applicableGroups, totalLower, actualTargetValue + MIN_CHANGE, bestSelection); } // If we have a bigger coin and (either the stochastic approximation didn't find a good solution, // or the next bigger coin is closer), return the bigger coin - if(lowestLarger != null && ((bestValue != targetValue && bestValue < targetValue + MIN_CHANGE) || lowestLarger.getEffectiveValue() <= bestValue)) { + if(lowestLarger != null && ((bestValue != actualTargetValue && bestValue < actualTargetValue + MIN_CHANGE) || lowestLarger.getEffectiveValue() <= bestValue)) { return lowestLarger.getUtxos(); } else { List utxos = new ArrayList<>(); @@ -66,7 +74,7 @@ public class KnapsackUtxoSelector implements UtxoSelector { } } - private long findApproximateBestSubset(List groups, long totalLower, long targetValue, boolean[] bestSelection) { + private long findApproximateBestSubset(List groups, long totalLower, long actualTargetValue, boolean[] bestSelection) { int iterations = 1000; boolean[] includedSelection; @@ -76,7 +84,7 @@ public class KnapsackUtxoSelector implements UtxoSelector { Random random = new Random(); - for(int rep = 0; rep < iterations && bestValue != targetValue; rep++) { + for(int rep = 0; rep < iterations && bestValue != actualTargetValue; rep++) { includedSelection = new boolean[groups.size()]; Arrays.fill(includedSelection, false); long total = 0; @@ -94,7 +102,7 @@ public class KnapsackUtxoSelector implements UtxoSelector { if(pass == 0 ? random.nextBoolean() : !includedSelection[i]) { total += groups.get(i).getEffectiveValue(); includedSelection[i] = true; - if(total >= targetValue) { + if(total >= actualTargetValue) { reachedTarget = true; if(total < bestValue) { bestValue = total; diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java b/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java index 94af18f..5d158d5 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java @@ -281,6 +281,16 @@ public class Wallet { return (long)(feeRate * outputVbytes + longTermFeeRate * inputVbytes); } + /** + * Determines the fee for a transaction from this wallet that has one output and no inputs + * + * @param recipientAddress The address to create the output to send to + * @return The determined fee + */ + public long getNoInputsFee(Address recipientAddress, Double feeRate) { + return (long)Math.ceil((double)getNoInputsWeightUnits(recipientAddress) * feeRate / (double)WITNESS_SCALE_FACTOR); + } + /** * Determines the weight units for a transaction from this wallet that has one output and no inputs *