mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-12-26 01:56:44 +00:00
utxo grouping, filtering and zero conf handling
This commit is contained in:
parent
832ca8f257
commit
f00754b157
5 changed files with 136 additions and 32 deletions
|
@ -32,20 +32,20 @@ public class BlockTransaction extends BlockTransactionHash implements Comparable
|
|||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(BlockTransaction blockchainTransaction) {
|
||||
if(getHeight() != blockchainTransaction.getHeight()) {
|
||||
return getHeight() - blockchainTransaction.getHeight();
|
||||
public int compareTo(BlockTransaction blkTx) {
|
||||
if(getHeight() != blkTx.getHeight()) {
|
||||
return (getHeight() > 0 ? getHeight() : Integer.MAX_VALUE) - (blkTx.getHeight() > 0 ? blkTx.getHeight() : Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
if(getReferencedOutpoints(this).removeAll(getOutputs(blockchainTransaction))) {
|
||||
if(getReferencedOutpoints(this).removeAll(getOutputs(blkTx))) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if(getReferencedOutpoints(blockchainTransaction).removeAll(getOutputs(this))) {
|
||||
if(getReferencedOutpoints(blkTx).removeAll(getOutputs(this))) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return super.compareTo(blockchainTransaction);
|
||||
return super.compareTo(blkTx);
|
||||
}
|
||||
|
||||
private static List<HashIndex> getReferencedOutpoints(BlockTransaction blockchainTransaction) {
|
||||
|
|
|
@ -67,9 +67,8 @@ public abstract class BlockTransactionHash {
|
|||
}
|
||||
|
||||
public int compareTo(BlockTransactionHash reference) {
|
||||
int heightDiff = height - reference.height;
|
||||
if(heightDiff != 0) {
|
||||
return heightDiff;
|
||||
if(height != reference.height) {
|
||||
return (height > 0 ? height : Integer.MAX_VALUE) - (reference.height > 0 ? reference.height : Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
return hash.compareTo(reference.hash);
|
||||
|
|
|
@ -7,6 +7,7 @@ import static com.sparrowwallet.drongo.protocol.Transaction.WITNESS_SCALE_FACTOR
|
|||
|
||||
public class OutputGroup {
|
||||
private final List<BlockTransactionHashIndex> utxos = new ArrayList<>();
|
||||
private final int walletBlockHeight;
|
||||
private final long inputWeightUnits;
|
||||
private final double feeRate;
|
||||
private final double longTermFeeRate;
|
||||
|
@ -14,26 +15,24 @@ public class OutputGroup {
|
|||
private long effectiveValue = 0;
|
||||
private long fee = 0;
|
||||
private long longTermFee = 0;
|
||||
private int depth = Integer.MAX_VALUE;
|
||||
private boolean allInputsFromWallet = true;
|
||||
|
||||
public OutputGroup(long inputWeightUnits, double feeRate, double longTermFeeRate) {
|
||||
public OutputGroup(int walletBlockHeight, long inputWeightUnits, double feeRate, double longTermFeeRate) {
|
||||
this.walletBlockHeight = walletBlockHeight;
|
||||
this.inputWeightUnits = inputWeightUnits;
|
||||
this.feeRate = feeRate;
|
||||
this.longTermFeeRate = longTermFeeRate;
|
||||
}
|
||||
|
||||
public OutputGroup(long inputWeightUnits, double feeRate, double longTermFeeRate, BlockTransactionHashIndex utxo) {
|
||||
this.inputWeightUnits = inputWeightUnits;
|
||||
this.feeRate = feeRate;
|
||||
this.longTermFeeRate = longTermFeeRate;
|
||||
add(utxo);
|
||||
}
|
||||
|
||||
public void add(BlockTransactionHashIndex utxo) {
|
||||
public void add(BlockTransactionHashIndex utxo, boolean allInputsFromWallet) {
|
||||
utxos.add(utxo);
|
||||
value += utxo.getValue();
|
||||
effectiveValue += utxo.getValue() - (long)(inputWeightUnits * feeRate / WITNESS_SCALE_FACTOR);
|
||||
fee += (long)(inputWeightUnits * feeRate / WITNESS_SCALE_FACTOR);
|
||||
longTermFee += (long)(inputWeightUnits * longTermFeeRate / WITNESS_SCALE_FACTOR);
|
||||
depth = utxo.getHeight() <= 0 ? 0 : Math.min(depth, walletBlockHeight - utxo.getHeight() + 1);
|
||||
this.allInputsFromWallet &= allInputsFromWallet;
|
||||
}
|
||||
|
||||
public void remove(BlockTransactionHashIndex utxo) {
|
||||
|
@ -64,4 +63,30 @@ public class OutputGroup {
|
|||
public long getLongTermFee() {
|
||||
return longTermFee;
|
||||
}
|
||||
|
||||
public int getDepth() {
|
||||
return depth;
|
||||
}
|
||||
|
||||
public boolean isAllInputsFromWallet() {
|
||||
return allInputsFromWallet;
|
||||
}
|
||||
|
||||
public static class Filter {
|
||||
private final int minWalletConfirmations;
|
||||
private final int minExternalConfirmations;
|
||||
|
||||
public Filter(int minWalletConfirmations, int minExternalConfirmations) {
|
||||
this.minWalletConfirmations = minWalletConfirmations;
|
||||
this.minExternalConfirmations = minExternalConfirmations;
|
||||
}
|
||||
|
||||
public boolean isEligible(OutputGroup outputGroup) {
|
||||
if(outputGroup.isAllInputsFromWallet()) {
|
||||
return outputGroup.getDepth() >= minWalletConfirmations;
|
||||
}
|
||||
|
||||
return outputGroup.getDepth() >= minExternalConfirmations;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ public class PriorityUtxoSelector implements UtxoSelector {
|
|||
public Collection<BlockTransactionHashIndex> select(long targetValue, Collection<OutputGroup> candidates) {
|
||||
List<BlockTransactionHashIndex> selected = new ArrayList<>();
|
||||
|
||||
List<BlockTransactionHashIndex> sorted = candidates.stream().flatMap(outputGroup -> outputGroup.getUtxos().stream()).filter(ref -> ref.getHeight() != 0).collect(Collectors.toList());
|
||||
List<BlockTransactionHashIndex> sorted = candidates.stream().flatMap(outputGroup -> outputGroup.getUtxos().stream()).filter(ref -> ref.getHeight() > 0).collect(Collectors.toList());
|
||||
sort(sorted);
|
||||
|
||||
//Testing only: remove
|
||||
|
|
|
@ -233,12 +233,29 @@ public class Wallet {
|
|||
}
|
||||
}
|
||||
|
||||
public boolean isWalletTxo(BlockTransactionHashIndex txo) {
|
||||
return getWalletTxos().containsKey(txo);
|
||||
}
|
||||
|
||||
public Map<BlockTransactionHashIndex, WalletNode> getWalletTxos() {
|
||||
Map<BlockTransactionHashIndex, WalletNode> walletTxos = new TreeMap<>();
|
||||
getWalletTxos(walletTxos, getNode(KeyPurpose.RECEIVE));
|
||||
getWalletTxos(walletTxos, getNode(KeyPurpose.CHANGE));
|
||||
return walletTxos;
|
||||
}
|
||||
|
||||
private void getWalletTxos(Map<BlockTransactionHashIndex, WalletNode> walletTxos, WalletNode purposeNode) {
|
||||
for(WalletNode addressNode : purposeNode.getChildren()) {
|
||||
for(BlockTransactionHashIndex txo : addressNode.getTransactionOutputs()) {
|
||||
walletTxos.put(txo, addressNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Map<BlockTransactionHashIndex, WalletNode> getWalletUtxos() {
|
||||
Map<BlockTransactionHashIndex, WalletNode> walletUtxos = new TreeMap<>();
|
||||
|
||||
getWalletUtxos(walletUtxos, getNode(KeyPurpose.RECEIVE));
|
||||
getWalletUtxos(walletUtxos, getNode(KeyPurpose.CHANGE));
|
||||
|
||||
return walletUtxos;
|
||||
}
|
||||
|
||||
|
@ -357,11 +374,11 @@ public class Wallet {
|
|||
return getFee(changeOutput, feeRate, longTermFeeRate);
|
||||
}
|
||||
|
||||
public WalletTransaction createWalletTransaction(List<UtxoSelector> utxoSelectors, Address recipientAddress, long recipientAmount, double feeRate, double longTermFeeRate, Long fee, boolean sendAll) throws InsufficientFundsException {
|
||||
public WalletTransaction createWalletTransaction(List<UtxoSelector> utxoSelectors, Address recipientAddress, long recipientAmount, double feeRate, double longTermFeeRate, Long fee, boolean sendAll, boolean groupByAddress, boolean includeMempoolChange) throws InsufficientFundsException {
|
||||
long valueRequiredAmt = recipientAmount;
|
||||
|
||||
while(true) {
|
||||
Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = selectInputs(utxoSelectors, valueRequiredAmt, feeRate, longTermFeeRate);
|
||||
Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = selectInputs(utxoSelectors, valueRequiredAmt, feeRate, longTermFeeRate, groupByAddress, includeMempoolChange);
|
||||
long totalSelectedAmt = selectedUtxos.keySet().stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
|
||||
|
||||
//Add inputs
|
||||
|
@ -434,22 +451,85 @@ public class Wallet {
|
|||
}
|
||||
}
|
||||
|
||||
private Map<BlockTransactionHashIndex, WalletNode> selectInputs(List<UtxoSelector> utxoSelectors, Long targetValue, double feeRate, double longTermFeeRate) throws InsufficientFundsException {
|
||||
Map<BlockTransactionHashIndex, WalletNode> utxos = getWalletUtxos();
|
||||
private Map<BlockTransactionHashIndex, WalletNode> selectInputs(List<UtxoSelector> utxoSelectors, Long targetValue, double feeRate, double longTermFeeRate, boolean groupByAddress, boolean includeMempoolChange) throws InsufficientFundsException {
|
||||
List<OutputGroup> utxoPool = getGroupedUtxos(feeRate, longTermFeeRate, groupByAddress);
|
||||
|
||||
for(UtxoSelector utxoSelector : utxoSelectors) {
|
||||
List<OutputGroup> utxoPool = utxos.keySet().stream().map(utxo -> new OutputGroup(getInputWeightUnits(), feeRate, longTermFeeRate, utxo)).collect(Collectors.toList());
|
||||
Collection<BlockTransactionHashIndex> selectedInputs = utxoSelector.select(targetValue, utxoPool);
|
||||
long total = selectedInputs.stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
|
||||
if(total > targetValue) {
|
||||
utxos.keySet().retainAll(selectedInputs);
|
||||
return utxos;
|
||||
List<OutputGroup.Filter> filters = new ArrayList<>();
|
||||
filters.add(new OutputGroup.Filter(1, 6));
|
||||
filters.add(new OutputGroup.Filter(1, 1));
|
||||
if(includeMempoolChange) {
|
||||
filters.add(new OutputGroup.Filter(0, 1));
|
||||
}
|
||||
|
||||
for(OutputGroup.Filter filter : filters) {
|
||||
List<OutputGroup> filteredPool = utxoPool.stream().filter(filter::isEligible).collect(Collectors.toList());
|
||||
|
||||
for(UtxoSelector utxoSelector : utxoSelectors) {
|
||||
Collection<BlockTransactionHashIndex> selectedInputs = utxoSelector.select(targetValue, filteredPool);
|
||||
long total = selectedInputs.stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
|
||||
if(total > targetValue) {
|
||||
Map<BlockTransactionHashIndex, WalletNode> utxos = getWalletUtxos();
|
||||
utxos.keySet().retainAll(selectedInputs);
|
||||
return utxos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new InsufficientFundsException("Not enough combined value in UTXOs for output value " + targetValue);
|
||||
}
|
||||
|
||||
private List<OutputGroup> getGroupedUtxos(double feeRate, double longTermFeeRate, boolean groupByAddress) {
|
||||
List<OutputGroup> outputGroups = new ArrayList<>();
|
||||
getGroupedUtxos(outputGroups, getNode(KeyPurpose.RECEIVE), feeRate, longTermFeeRate, groupByAddress);
|
||||
getGroupedUtxos(outputGroups, getNode(KeyPurpose.CHANGE), feeRate, longTermFeeRate, groupByAddress);
|
||||
return outputGroups;
|
||||
}
|
||||
|
||||
private void getGroupedUtxos(List<OutputGroup> outputGroups, WalletNode purposeNode, double feeRate, double longTermFeeRate, boolean groupByAddress) {
|
||||
for(WalletNode addressNode : purposeNode.getChildren()) {
|
||||
OutputGroup outputGroup = null;
|
||||
for(BlockTransactionHashIndex utxo : addressNode.getUnspentTransactionOutputs()) {
|
||||
if(outputGroup == null || !groupByAddress) {
|
||||
outputGroup = new OutputGroup(getStoredBlockHeight(), getInputWeightUnits(), feeRate, longTermFeeRate);
|
||||
outputGroups.add(outputGroup);
|
||||
}
|
||||
|
||||
outputGroup.add(utxo, allInputsFromWallet(utxo.getHash()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the provided wallet transaction was created from a purely internal transaction
|
||||
*
|
||||
* @param txId The txid
|
||||
* @return Whether the transaction was created entirely from inputs that reference outputs that belong to this wallet
|
||||
*/
|
||||
private boolean allInputsFromWallet(Sha256Hash txId) {
|
||||
BlockTransaction utxoBlkTx = getTransactions().get(txId);
|
||||
if(utxoBlkTx == null) {
|
||||
throw new IllegalArgumentException("Provided txId was not a wallet transaction");
|
||||
}
|
||||
|
||||
for(int i = 0; i < utxoBlkTx.getTransaction().getInputs().size(); i++) {
|
||||
TransactionInput utxoTxInput = utxoBlkTx.getTransaction().getInputs().get(i);
|
||||
BlockTransaction prevBlkTx = getTransactions().get(utxoTxInput.getOutpoint().getHash());
|
||||
if(prevBlkTx == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int index = (int)utxoTxInput.getOutpoint().getIndex();
|
||||
TransactionOutput prevTxOut = prevBlkTx.getTransaction().getOutputs().get(index);
|
||||
BlockTransactionHashIndex spendingTXI = new BlockTransactionHashIndex(utxoBlkTx.getHash(), utxoBlkTx.getHeight(), utxoBlkTx.getDate(), utxoBlkTx.getFee(), i, prevTxOut.getValue());
|
||||
BlockTransactionHashIndex spentTXO = new BlockTransactionHashIndex(prevBlkTx.getHash(), prevBlkTx.getHeight(), prevBlkTx.getDate(), prevBlkTx.getFee(), index, prevTxOut.getValue(), spendingTXI);
|
||||
if(!isWalletTxo(spentTXO)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public BitcoinUnit getAutoUnit() {
|
||||
for(KeyPurpose keyPurpose : KeyPurpose.values()) {
|
||||
for(WalletNode addressNode : getNode(keyPurpose).getChildren()) {
|
||||
|
|
Loading…
Reference in a new issue