mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-12-26 01:56:44 +00:00
use txo filters for all wallet transaction output filtering
This commit is contained in:
parent
6a7d2aac28
commit
8484dd397b
9 changed files with 106 additions and 75 deletions
|
@ -2,10 +2,10 @@ package com.sparrowwallet.drongo.wallet;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
|
|
||||||
public class CoinbaseUtxoFilter implements UtxoFilter {
|
public class CoinbaseTxoFilter implements TxoFilter {
|
||||||
private final Wallet wallet;
|
private final Wallet wallet;
|
||||||
|
|
||||||
public CoinbaseUtxoFilter(Wallet wallet) {
|
public CoinbaseTxoFilter(Wallet wallet) {
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
package com.sparrowwallet.drongo.wallet;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
public class ExcludeTxoFilter implements TxoFilter {
|
||||||
|
private final Collection<BlockTransactionHashIndex> excludedTxos;
|
||||||
|
|
||||||
|
public ExcludeTxoFilter() {
|
||||||
|
this.excludedTxos = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExcludeTxoFilter(Collection<BlockTransactionHashIndex> excludedTxos) {
|
||||||
|
this.excludedTxos = new ArrayList<>(excludedTxos);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEligible(BlockTransactionHashIndex candidate) {
|
||||||
|
for(BlockTransactionHashIndex excludedTxo : excludedTxos) {
|
||||||
|
if(candidate.getHash().equals(excludedTxo.getHash()) && candidate.getIndex() == excludedTxo.getIndex()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<BlockTransactionHashIndex> getExcludedTxos() {
|
||||||
|
return excludedTxos;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,31 +0,0 @@
|
||||||
package com.sparrowwallet.drongo.wallet;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
|
|
||||||
public class ExcludeUtxoFilter implements UtxoFilter {
|
|
||||||
private final Collection<BlockTransactionHashIndex> excludedUtxos;
|
|
||||||
|
|
||||||
public ExcludeUtxoFilter() {
|
|
||||||
this.excludedUtxos = new ArrayList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ExcludeUtxoFilter(Collection<BlockTransactionHashIndex> excludedUtxos) {
|
|
||||||
this.excludedUtxos = new ArrayList<>(excludedUtxos);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isEligible(BlockTransactionHashIndex candidate) {
|
|
||||||
for(BlockTransactionHashIndex excludedUtxo : excludedUtxos) {
|
|
||||||
if(candidate.getHash().equals(excludedUtxo.getHash()) && candidate.getIndex() == excludedUtxo.getIndex()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Collection<BlockTransactionHashIndex> getExcludedUtxos() {
|
|
||||||
return excludedUtxos;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
package com.sparrowwallet.drongo.wallet;
|
package com.sparrowwallet.drongo.wallet;
|
||||||
|
|
||||||
public class FrozenUtxoFilter implements UtxoFilter {
|
public class FrozenTxoFilter implements TxoFilter {
|
||||||
@Override
|
@Override
|
||||||
public boolean isEligible(BlockTransactionHashIndex candidate) {
|
public boolean isEligible(BlockTransactionHashIndex candidate) {
|
||||||
return candidate.getStatus() == null || candidate.getStatus() != Status.FROZEN;
|
return candidate.getStatus() == null || candidate.getStatus() != Status.FROZEN;
|
|
@ -1,14 +1,13 @@
|
||||||
package com.sparrowwallet.drongo.wallet;
|
package com.sparrowwallet.drongo.wallet;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class PresetUtxoSelector extends SingleSetUtxoSelector {
|
public class PresetUtxoSelector extends SingleSetUtxoSelector {
|
||||||
private final Collection<BlockTransactionHashIndex> presetUtxos;
|
private final Collection<BlockTransactionHashIndex> presetUtxos;
|
||||||
private final Collection<BlockTransactionHashIndex> excludedUtxos;
|
private final Collection<BlockTransactionHashIndex> excludedUtxos;
|
||||||
private final boolean maintainOrder;
|
private final boolean maintainOrder;
|
||||||
|
private final boolean requireAll;
|
||||||
|
|
||||||
public PresetUtxoSelector(Collection<BlockTransactionHashIndex> presetUtxos) {
|
public PresetUtxoSelector(Collection<BlockTransactionHashIndex> presetUtxos) {
|
||||||
this(presetUtxos, new ArrayList<>());
|
this(presetUtxos, new ArrayList<>());
|
||||||
|
@ -18,12 +17,14 @@ public class PresetUtxoSelector extends SingleSetUtxoSelector {
|
||||||
this.presetUtxos = presetUtxos;
|
this.presetUtxos = presetUtxos;
|
||||||
this.excludedUtxos = excludedUtxos;
|
this.excludedUtxos = excludedUtxos;
|
||||||
this.maintainOrder = false;
|
this.maintainOrder = false;
|
||||||
|
this.requireAll = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PresetUtxoSelector(Collection<BlockTransactionHashIndex> presetUtxos, boolean maintainOrder) {
|
public PresetUtxoSelector(Collection<BlockTransactionHashIndex> presetUtxos, boolean maintainOrder, boolean requireAll) {
|
||||||
this.presetUtxos = presetUtxos;
|
this.presetUtxos = presetUtxos;
|
||||||
this.excludedUtxos = new ArrayList<>();
|
this.excludedUtxos = new ArrayList<>();
|
||||||
this.maintainOrder = maintainOrder;
|
this.maintainOrder = maintainOrder;
|
||||||
|
this.requireAll = requireAll;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -40,8 +41,11 @@ public class PresetUtxoSelector extends SingleSetUtxoSelector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(maintainOrder && utxos.containsAll(presetUtxos)) {
|
Set<BlockTransactionHashIndex> utxosSet = new HashSet<>(utxos);
|
||||||
|
if(maintainOrder && utxosSet.containsAll(presetUtxos)) {
|
||||||
return presetUtxos;
|
return presetUtxos;
|
||||||
|
} else if(requireAll && !utxosSet.containsAll(presetUtxos)) {
|
||||||
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
return utxos;
|
return utxos;
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.sparrowwallet.drongo.wallet;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||||
|
|
||||||
|
public class SpentTxoFilter implements TxoFilter {
|
||||||
|
private final Sha256Hash replacedTxid;
|
||||||
|
|
||||||
|
public SpentTxoFilter() {
|
||||||
|
replacedTxid = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SpentTxoFilter(Sha256Hash replacedTxid) {
|
||||||
|
this.replacedTxid = replacedTxid;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEligible(BlockTransactionHashIndex candidate) {
|
||||||
|
return !isSpentOrReplaced(candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSpentOrReplaced(BlockTransactionHashIndex candidate) {
|
||||||
|
return candidate.getHash().equals(replacedTxid) || (candidate.isSpent() && !candidate.getSpentBy().getHash().equals(replacedTxid));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
package com.sparrowwallet.drongo.wallet;
|
package com.sparrowwallet.drongo.wallet;
|
||||||
|
|
||||||
public interface UtxoFilter {
|
public interface TxoFilter {
|
||||||
boolean isEligible(BlockTransactionHashIndex candidate);
|
boolean isEligible(BlockTransactionHashIndex candidate);
|
||||||
}
|
}
|
|
@ -799,30 +799,38 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<BlockTransactionHashIndex, WalletNode> getWalletUtxos() {
|
public Map<BlockTransactionHashIndex, WalletNode> getWalletUtxos() {
|
||||||
return getWalletUtxos(false);
|
return getWalletTxos(List.of(new SpentTxoFilter()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<BlockTransactionHashIndex, WalletNode> getWalletUtxos(boolean includeSpentMempoolOutputs) {
|
public Map<BlockTransactionHashIndex, WalletNode> getSpendableUtxos() {
|
||||||
Map<BlockTransactionHashIndex, WalletNode> walletUtxos = new TreeMap<>();
|
return getWalletTxos(List.of(new SpentTxoFilter(), new FrozenTxoFilter(), new CoinbaseTxoFilter(this)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<BlockTransactionHashIndex, WalletNode> getSpendableUtxos(BlockTransaction replacedTransaction) {
|
||||||
|
return getWalletTxos(List.of(new SpentTxoFilter(replacedTransaction == null ? null : replacedTransaction.getHash()), new FrozenTxoFilter(), new CoinbaseTxoFilter(this)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<BlockTransactionHashIndex, WalletNode> getWalletTxos(Collection<TxoFilter> txoFilters) {
|
||||||
|
Map<BlockTransactionHashIndex, WalletNode> walletTxos = new TreeMap<>();
|
||||||
for(KeyPurpose keyPurpose : getWalletKeyPurposes()) {
|
for(KeyPurpose keyPurpose : getWalletKeyPurposes()) {
|
||||||
getWalletUtxos(walletUtxos, getNode(keyPurpose), includeSpentMempoolOutputs);
|
getWalletTxos(walletTxos, getNode(keyPurpose), txoFilters);
|
||||||
}
|
}
|
||||||
|
|
||||||
for(Wallet childWallet : getChildWallets()) {
|
for(Wallet childWallet : getChildWallets()) {
|
||||||
if(childWallet.isNested()) {
|
if(childWallet.isNested()) {
|
||||||
for(KeyPurpose keyPurpose : childWallet.getWalletKeyPurposes()) {
|
for(KeyPurpose keyPurpose : childWallet.getWalletKeyPurposes()) {
|
||||||
getWalletUtxos(walletUtxos, childWallet.getNode(keyPurpose), includeSpentMempoolOutputs);
|
getWalletTxos(walletTxos, childWallet.getNode(keyPurpose), txoFilters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return walletUtxos;
|
return walletTxos;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void getWalletUtxos(Map<BlockTransactionHashIndex, WalletNode> walletUtxos, WalletNode purposeNode, boolean includeSpentMempoolOutputs) {
|
private void getWalletTxos(Map<BlockTransactionHashIndex, WalletNode> walletTxos, WalletNode purposeNode, Collection<TxoFilter> txoFilters) {
|
||||||
for(WalletNode addressNode : purposeNode.getChildren()) {
|
for(WalletNode addressNode : purposeNode.getChildren()) {
|
||||||
for(BlockTransactionHashIndex utxo : addressNode.getUnspentTransactionOutputs(includeSpentMempoolOutputs)) {
|
for(BlockTransactionHashIndex utxo : addressNode.getTransactionOutputs(txoFilters)) {
|
||||||
walletUtxos.put(utxo, addressNode);
|
walletTxos.put(utxo, addressNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -981,29 +989,30 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||||
return getFee(changeOutput, feeRate, longTermFeeRate);
|
return getFee(changeOutput, feeRate, longTermFeeRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
public WalletTransaction createWalletTransaction(List<UtxoSelector> utxoSelectors, List<UtxoFilter> utxoFilters, List<Payment> payments, List<byte[]> opReturns, Set<WalletNode> excludedChangeNodes, double feeRate, double longTermFeeRate, Long fee, Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs, boolean includeSpentMempoolOutputs) throws InsufficientFundsException {
|
public WalletTransaction createWalletTransaction(List<UtxoSelector> utxoSelectors, List<TxoFilter> txoFilters, List<Payment> payments, List<byte[]> opReturns, Set<WalletNode> excludedChangeNodes, double feeRate, double longTermFeeRate, Long fee, Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs) throws InsufficientFundsException {
|
||||||
boolean sendMax = payments.stream().anyMatch(Payment::isSendMax);
|
boolean sendMax = payments.stream().anyMatch(Payment::isSendMax);
|
||||||
long totalPaymentAmount = payments.stream().map(Payment::getAmount).mapToLong(v -> v).sum();
|
long totalPaymentAmount = payments.stream().map(Payment::getAmount).mapToLong(v -> v).sum();
|
||||||
long totalUtxoValue = getWalletUtxos().keySet().stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
|
Map<BlockTransactionHashIndex, WalletNode> availableTxos = getWalletTxos(txoFilters);
|
||||||
|
long totalAvailableValue = availableTxos.keySet().stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
|
||||||
|
|
||||||
if(fee != null && feeRate != Transaction.DEFAULT_MIN_RELAY_FEE) {
|
if(fee != null && feeRate != Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||||
throw new IllegalArgumentException("Use an input fee rate of 1 sat/vB when using a defined fee amount so UTXO selectors overestimate effective value");
|
throw new IllegalArgumentException("Use an input fee rate of 1 sat/vB when using a defined fee amount so UTXO selectors overestimate effective value");
|
||||||
}
|
}
|
||||||
|
|
||||||
long maxSpendableAmt = getMaxSpendable(payments.stream().map(Payment::getAddress).collect(Collectors.toList()), feeRate, includeSpentMempoolOutputs);
|
long maxSpendableAmt = getMaxSpendable(payments.stream().map(Payment::getAddress).collect(Collectors.toList()), feeRate, availableTxos);
|
||||||
if(maxSpendableAmt < 0) {
|
if(maxSpendableAmt < 0) {
|
||||||
throw new InsufficientFundsException("Not enough combined value in all available UTXOs to send a transaction to the provided addresses at this fee rate");
|
throw new InsufficientFundsException("Not enough combined value in all available UTXOs to send a transaction to the provided addresses at this fee rate");
|
||||||
}
|
}
|
||||||
|
|
||||||
//When a user fee is set, we can calculate the fees to spend all UTXOs because we assume all UTXOs are spendable at a fee rate of 1 sat/vB
|
//When a user fee is set, we can calculate the fees to spend all UTXOs because we assume all UTXOs are spendable at a fee rate of 1 sat/vB
|
||||||
//We can then add the user set fee less this amount as a "phantom payment amount" to the value required to find (which cannot include transaction fees)
|
//We can then add the user set fee less this amount as a "phantom payment amount" to the value required to find (which cannot include transaction fees)
|
||||||
long valueRequiredAmt = totalPaymentAmount + (fee != null ? fee - (totalUtxoValue - maxSpendableAmt) : 0);
|
long valueRequiredAmt = totalPaymentAmount + (fee != null ? fee - (totalAvailableValue - maxSpendableAmt) : 0);
|
||||||
if(maxSpendableAmt < valueRequiredAmt) {
|
if(maxSpendableAmt < valueRequiredAmt) {
|
||||||
throw new InsufficientFundsException("Not enough combined value in all available UTXOs to send a transaction to send the provided payments at the user set fee" + (fee == null ? " rate" : ""));
|
throw new InsufficientFundsException("Not enough combined value in all available UTXOs to send a transaction to send the provided payments at the user set fee" + (fee == null ? " rate" : ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
while(true) {
|
while(true) {
|
||||||
List<Map<BlockTransactionHashIndex, WalletNode>> selectedUtxoSets = selectInputSets(utxoSelectors, utxoFilters, valueRequiredAmt, feeRate, longTermFeeRate, groupByAddress, includeMempoolOutputs, includeSpentMempoolOutputs, sendMax);
|
List<Map<BlockTransactionHashIndex, WalletNode>> selectedUtxoSets = selectInputSets(availableTxos, utxoSelectors, txoFilters, valueRequiredAmt, feeRate, longTermFeeRate, groupByAddress, includeMempoolOutputs, sendMax);
|
||||||
Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = new LinkedHashMap<>();
|
Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = new LinkedHashMap<>();
|
||||||
selectedUtxoSets.forEach(selectedUtxos::putAll);
|
selectedUtxoSets.forEach(selectedUtxos::putAll);
|
||||||
long totalSelectedAmt = selectedUtxos.keySet().stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
|
long totalSelectedAmt = selectedUtxos.keySet().stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
|
||||||
|
@ -1076,8 +1085,8 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||||
if(differenceAmt < noChangeFeeRequiredAmt) {
|
if(differenceAmt < noChangeFeeRequiredAmt) {
|
||||||
valueRequiredAmt = totalSelectedAmt + 1;
|
valueRequiredAmt = totalSelectedAmt + 1;
|
||||||
//If we haven't selected all UTXOs yet, don't require more than the max spendable amount
|
//If we haven't selected all UTXOs yet, don't require more than the max spendable amount
|
||||||
if(valueRequiredAmt > maxSpendableAmt && transaction.getInputs().size() < getWalletUtxos().size()) {
|
if(valueRequiredAmt > maxSpendableAmt && transaction.getInputs().size() < availableTxos.size()) {
|
||||||
valueRequiredAmt = maxSpendableAmt;
|
valueRequiredAmt = maxSpendableAmt;
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
|
@ -1180,8 +1189,8 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Map<BlockTransactionHashIndex, WalletNode>> selectInputSets(List<UtxoSelector> utxoSelectors, List<UtxoFilter> utxoFilters, Long targetValue, double feeRate, double longTermFeeRate, boolean groupByAddress, boolean includeMempoolOutputs, boolean includeSpentMempoolOutputs, boolean sendMax) throws InsufficientFundsException {
|
private List<Map<BlockTransactionHashIndex, WalletNode>> selectInputSets(Map<BlockTransactionHashIndex, WalletNode> availableTxos, List<UtxoSelector> utxoSelectors, List<TxoFilter> txoFilters, Long targetValue, double feeRate, double longTermFeeRate, boolean groupByAddress, boolean includeMempoolOutputs, boolean sendMax) throws InsufficientFundsException {
|
||||||
List<OutputGroup> utxoPool = getGroupedUtxos(utxoFilters, feeRate, longTermFeeRate, groupByAddress, includeSpentMempoolOutputs);
|
List<OutputGroup> utxoPool = getGroupedUtxos(txoFilters, feeRate, longTermFeeRate, groupByAddress);
|
||||||
|
|
||||||
List<OutputGroup.Filter> filters = new ArrayList<>();
|
List<OutputGroup.Filter> filters = new ArrayList<>();
|
||||||
filters.add(new OutputGroup.Filter(1, 6, false));
|
filters.add(new OutputGroup.Filter(1, 6, false));
|
||||||
|
@ -1204,7 +1213,6 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||||
List<Collection<BlockTransactionHashIndex>> selectedInputSets = utxoSelector.selectSets(targetValue, filteredPool);
|
List<Collection<BlockTransactionHashIndex>> selectedInputSets = utxoSelector.selectSets(targetValue, filteredPool);
|
||||||
List<Map<BlockTransactionHashIndex, WalletNode>> selectedInputSetsList = new ArrayList<>();
|
List<Map<BlockTransactionHashIndex, WalletNode>> selectedInputSetsList = new ArrayList<>();
|
||||||
long total = 0;
|
long total = 0;
|
||||||
Map<BlockTransactionHashIndex, WalletNode> utxos = getWalletUtxos(includeSpentMempoolOutputs);
|
|
||||||
for(Collection<BlockTransactionHashIndex> selectedInputs : selectedInputSets) {
|
for(Collection<BlockTransactionHashIndex> selectedInputs : selectedInputSets) {
|
||||||
total += selectedInputs.stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
|
total += selectedInputs.stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
|
||||||
Map<BlockTransactionHashIndex, WalletNode> selectedInputsMap = new LinkedHashMap<>();
|
Map<BlockTransactionHashIndex, WalletNode> selectedInputsMap = new LinkedHashMap<>();
|
||||||
|
@ -1213,7 +1221,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||||
Collections.shuffle(shuffledInputs);
|
Collections.shuffle(shuffledInputs);
|
||||||
}
|
}
|
||||||
for(BlockTransactionHashIndex shuffledInput : shuffledInputs) {
|
for(BlockTransactionHashIndex shuffledInput : shuffledInputs) {
|
||||||
selectedInputsMap.put(shuffledInput, utxos.get(shuffledInput));
|
selectedInputsMap.put(shuffledInput, availableTxos.get(shuffledInput));
|
||||||
}
|
}
|
||||||
selectedInputSetsList.add(selectedInputsMap);
|
selectedInputSetsList.add(selectedInputsMap);
|
||||||
}
|
}
|
||||||
|
@ -1227,18 +1235,18 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||||
throw new InsufficientFundsException("Not enough combined value in UTXOs for output value " + targetValue, targetValue);
|
throw new InsufficientFundsException("Not enough combined value in UTXOs for output value " + targetValue, targetValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<OutputGroup> getGroupedUtxos(List<UtxoFilter> utxoFilters, double feeRate, double longTermFeeRate, boolean groupByAddress, boolean includeSpentMempoolOutputs) {
|
public List<OutputGroup> getGroupedUtxos(List<TxoFilter> txoFilters, double feeRate, double longTermFeeRate, boolean groupByAddress) {
|
||||||
List<OutputGroup> outputGroups = new ArrayList<>();
|
List<OutputGroup> outputGroups = new ArrayList<>();
|
||||||
Map<Sha256Hash, BlockTransaction> walletTransactions = getWalletTransactions();
|
Map<Sha256Hash, BlockTransaction> walletTransactions = getWalletTransactions();
|
||||||
Map<BlockTransactionHashIndex, WalletNode> walletTxos = getWalletTxos();
|
Map<BlockTransactionHashIndex, WalletNode> walletTxos = getWalletTxos();
|
||||||
for(KeyPurpose keyPurpose : getWalletKeyPurposes()) {
|
for(KeyPurpose keyPurpose : getWalletKeyPurposes()) {
|
||||||
getGroupedUtxos(outputGroups, getNode(keyPurpose), utxoFilters, walletTransactions, walletTxos, feeRate, longTermFeeRate, groupByAddress, includeSpentMempoolOutputs);
|
getGroupedUtxos(outputGroups, getNode(keyPurpose), txoFilters, walletTransactions, walletTxos, feeRate, longTermFeeRate, groupByAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
for(Wallet childWallet : getChildWallets()) {
|
for(Wallet childWallet : getChildWallets()) {
|
||||||
if(childWallet.isNested()) {
|
if(childWallet.isNested()) {
|
||||||
for(KeyPurpose keyPurpose : childWallet.getWalletKeyPurposes()) {
|
for(KeyPurpose keyPurpose : childWallet.getWalletKeyPurposes()) {
|
||||||
childWallet.getGroupedUtxos(outputGroups, childWallet.getNode(keyPurpose), utxoFilters, walletTransactions, walletTxos, feeRate, longTermFeeRate, groupByAddress, includeSpentMempoolOutputs);
|
childWallet.getGroupedUtxos(outputGroups, childWallet.getNode(keyPurpose), txoFilters, walletTransactions, walletTxos, feeRate, longTermFeeRate, groupByAddress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1246,16 +1254,11 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||||
return outputGroups;
|
return outputGroups;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void getGroupedUtxos(List<OutputGroup> outputGroups, WalletNode purposeNode, List<UtxoFilter> utxoFilters, Map<Sha256Hash, BlockTransaction> walletTransactions, Map<BlockTransactionHashIndex, WalletNode> walletTxos, double feeRate, double longTermFeeRate, boolean groupByAddress, boolean includeSpentMempoolOutputs) {
|
private void getGroupedUtxos(List<OutputGroup> outputGroups, WalletNode purposeNode, List<TxoFilter> txoFilters, Map<Sha256Hash, BlockTransaction> walletTransactions, Map<BlockTransactionHashIndex, WalletNode> walletTxos, double feeRate, double longTermFeeRate, boolean groupByAddress) {
|
||||||
int inputWeightUnits = getInputWeightUnits();
|
int inputWeightUnits = getInputWeightUnits();
|
||||||
for(WalletNode addressNode : purposeNode.getChildren()) {
|
for(WalletNode addressNode : purposeNode.getChildren()) {
|
||||||
OutputGroup outputGroup = null;
|
OutputGroup outputGroup = null;
|
||||||
for(BlockTransactionHashIndex utxo : addressNode.getUnspentTransactionOutputs(includeSpentMempoolOutputs)) {
|
for(BlockTransactionHashIndex utxo : addressNode.getTransactionOutputs(txoFilters)) {
|
||||||
Optional<UtxoFilter> matchedFilter = utxoFilters.stream().filter(utxoFilter -> !utxoFilter.isEligible(utxo)).findAny();
|
|
||||||
if(matchedFilter.isPresent()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(outputGroup == null || !groupByAddress) {
|
if(outputGroup == null || !groupByAddress) {
|
||||||
outputGroup = new OutputGroup(addressNode.getWallet().getScriptType(), getStoredBlockHeight(), inputWeightUnits, feeRate, longTermFeeRate);
|
outputGroup = new OutputGroup(addressNode.getWallet().getScriptType(), getStoredBlockHeight(), inputWeightUnits, feeRate, longTermFeeRate);
|
||||||
outputGroups.add(outputGroup);
|
outputGroups.add(outputGroup);
|
||||||
|
@ -1323,12 +1326,12 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||||
* @param feeRate the fee rate in sats/vB
|
* @param feeRate the fee rate in sats/vB
|
||||||
* @return the maximum spendable amount (can be negative if the fee is higher than the combined UTXO value)
|
* @return the maximum spendable amount (can be negative if the fee is higher than the combined UTXO value)
|
||||||
*/
|
*/
|
||||||
public long getMaxSpendable(List<Address> paymentAddresses, double feeRate, boolean includeSpentMempoolOutputs) {
|
public long getMaxSpendable(List<Address> paymentAddresses, double feeRate, Map<BlockTransactionHashIndex, WalletNode> availableTxos) {
|
||||||
long maxInputValue = 0;
|
long maxInputValue = 0;
|
||||||
|
|
||||||
Map<Wallet, Integer> cachedInputWeightUnits = new HashMap<>();
|
Map<Wallet, Integer> cachedInputWeightUnits = new HashMap<>();
|
||||||
Transaction transaction = new Transaction();
|
Transaction transaction = new Transaction();
|
||||||
for(Map.Entry<BlockTransactionHashIndex, WalletNode> utxo : getWalletUtxos(includeSpentMempoolOutputs).entrySet()) {
|
for(Map.Entry<BlockTransactionHashIndex, WalletNode> utxo : availableTxos.entrySet()) {
|
||||||
int inputWeightUnits = cachedInputWeightUnits.computeIfAbsent(utxo.getValue().getWallet(), Wallet::getInputWeightUnits);
|
int inputWeightUnits = cachedInputWeightUnits.computeIfAbsent(utxo.getValue().getWallet(), Wallet::getInputWeightUnits);
|
||||||
long minInputValue = (long)Math.ceil(feeRate * inputWeightUnits / WITNESS_SCALE_FACTOR);
|
long minInputValue = (long)Math.ceil(feeRate * inputWeightUnits / WITNESS_SCALE_FACTOR);
|
||||||
if(utxo.getKey().getValue() > minInputValue) {
|
if(utxo.getKey().getValue() > minInputValue) {
|
||||||
|
|
|
@ -168,16 +168,16 @@ public class WalletNode extends Persistable implements Comparable<WalletNode> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<BlockTransactionHashIndex> getUnspentTransactionOutputs() {
|
public Set<BlockTransactionHashIndex> getUnspentTransactionOutputs() {
|
||||||
return getUnspentTransactionOutputs(false);
|
return getTransactionOutputs(List.of(new SpentTxoFilter()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<BlockTransactionHashIndex> getUnspentTransactionOutputs(boolean includeSpentMempoolOutputs) {
|
public Set<BlockTransactionHashIndex> getTransactionOutputs(Collection<TxoFilter> txoFilters) {
|
||||||
if(transactionOutputs.isEmpty()) {
|
if(transactionOutputs.isEmpty()) {
|
||||||
return Collections.emptySet();
|
return Collections.emptySet();
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<BlockTransactionHashIndex> unspentTXOs = new TreeSet<>(transactionOutputs);
|
Set<BlockTransactionHashIndex> unspentTXOs = new TreeSet<>(transactionOutputs);
|
||||||
unspentTXOs.removeIf(txo -> txo.isSpent() && (!includeSpentMempoolOutputs || txo.getSpentBy().getHeight() > 0));
|
unspentTXOs.removeIf(txo -> !txoFilters.stream().allMatch(txoFilter -> txoFilter.isEligible(txo)));
|
||||||
return unspentTXOs;
|
return unspentTXOs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue