mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-12-26 18:16:45 +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
|
@Override
|
||||||
public int compareTo(BlockTransaction blockchainTransaction) {
|
public int compareTo(BlockTransaction blkTx) {
|
||||||
if(getHeight() != blockchainTransaction.getHeight()) {
|
if(getHeight() != blkTx.getHeight()) {
|
||||||
return getHeight() - blockchainTransaction.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;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(getReferencedOutpoints(blockchainTransaction).removeAll(getOutputs(this))) {
|
if(getReferencedOutpoints(blkTx).removeAll(getOutputs(this))) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.compareTo(blockchainTransaction);
|
return super.compareTo(blkTx);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<HashIndex> getReferencedOutpoints(BlockTransaction blockchainTransaction) {
|
private static List<HashIndex> getReferencedOutpoints(BlockTransaction blockchainTransaction) {
|
||||||
|
|
|
@ -67,9 +67,8 @@ public abstract class BlockTransactionHash {
|
||||||
}
|
}
|
||||||
|
|
||||||
public int compareTo(BlockTransactionHash reference) {
|
public int compareTo(BlockTransactionHash reference) {
|
||||||
int heightDiff = height - reference.height;
|
if(height != reference.height) {
|
||||||
if(heightDiff != 0) {
|
return (height > 0 ? height : Integer.MAX_VALUE) - (reference.height > 0 ? reference.height : Integer.MAX_VALUE);
|
||||||
return heightDiff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return hash.compareTo(reference.hash);
|
return hash.compareTo(reference.hash);
|
||||||
|
|
|
@ -7,6 +7,7 @@ import static com.sparrowwallet.drongo.protocol.Transaction.WITNESS_SCALE_FACTOR
|
||||||
|
|
||||||
public class OutputGroup {
|
public class OutputGroup {
|
||||||
private final List<BlockTransactionHashIndex> utxos = new ArrayList<>();
|
private final List<BlockTransactionHashIndex> utxos = new ArrayList<>();
|
||||||
|
private final int walletBlockHeight;
|
||||||
private final long inputWeightUnits;
|
private final long inputWeightUnits;
|
||||||
private final double feeRate;
|
private final double feeRate;
|
||||||
private final double longTermFeeRate;
|
private final double longTermFeeRate;
|
||||||
|
@ -14,26 +15,24 @@ public class OutputGroup {
|
||||||
private long effectiveValue = 0;
|
private long effectiveValue = 0;
|
||||||
private long fee = 0;
|
private long fee = 0;
|
||||||
private long longTermFee = 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.inputWeightUnits = inputWeightUnits;
|
||||||
this.feeRate = feeRate;
|
this.feeRate = feeRate;
|
||||||
this.longTermFeeRate = longTermFeeRate;
|
this.longTermFeeRate = longTermFeeRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public OutputGroup(long inputWeightUnits, double feeRate, double longTermFeeRate, BlockTransactionHashIndex utxo) {
|
public void add(BlockTransactionHashIndex utxo, boolean allInputsFromWallet) {
|
||||||
this.inputWeightUnits = inputWeightUnits;
|
|
||||||
this.feeRate = feeRate;
|
|
||||||
this.longTermFeeRate = longTermFeeRate;
|
|
||||||
add(utxo);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void add(BlockTransactionHashIndex utxo) {
|
|
||||||
utxos.add(utxo);
|
utxos.add(utxo);
|
||||||
value += utxo.getValue();
|
value += utxo.getValue();
|
||||||
effectiveValue += utxo.getValue() - (long)(inputWeightUnits * feeRate / WITNESS_SCALE_FACTOR);
|
effectiveValue += utxo.getValue() - (long)(inputWeightUnits * feeRate / WITNESS_SCALE_FACTOR);
|
||||||
fee += (long)(inputWeightUnits * feeRate / WITNESS_SCALE_FACTOR);
|
fee += (long)(inputWeightUnits * feeRate / WITNESS_SCALE_FACTOR);
|
||||||
longTermFee += (long)(inputWeightUnits * longTermFeeRate / 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) {
|
public void remove(BlockTransactionHashIndex utxo) {
|
||||||
|
@ -64,4 +63,30 @@ public class OutputGroup {
|
||||||
public long getLongTermFee() {
|
public long getLongTermFee() {
|
||||||
return longTermFee;
|
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) {
|
public Collection<BlockTransactionHashIndex> select(long targetValue, Collection<OutputGroup> candidates) {
|
||||||
List<BlockTransactionHashIndex> selected = new ArrayList<>();
|
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);
|
sort(sorted);
|
||||||
|
|
||||||
//Testing only: remove
|
//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() {
|
public Map<BlockTransactionHashIndex, WalletNode> getWalletUtxos() {
|
||||||
Map<BlockTransactionHashIndex, WalletNode> walletUtxos = new TreeMap<>();
|
Map<BlockTransactionHashIndex, WalletNode> walletUtxos = new TreeMap<>();
|
||||||
|
|
||||||
getWalletUtxos(walletUtxos, getNode(KeyPurpose.RECEIVE));
|
getWalletUtxos(walletUtxos, getNode(KeyPurpose.RECEIVE));
|
||||||
getWalletUtxos(walletUtxos, getNode(KeyPurpose.CHANGE));
|
getWalletUtxos(walletUtxos, getNode(KeyPurpose.CHANGE));
|
||||||
|
|
||||||
return walletUtxos;
|
return walletUtxos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -357,11 +374,11 @@ public class Wallet {
|
||||||
return getFee(changeOutput, feeRate, longTermFeeRate);
|
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;
|
long valueRequiredAmt = recipientAmount;
|
||||||
|
|
||||||
while(true) {
|
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();
|
long totalSelectedAmt = selectedUtxos.keySet().stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
|
||||||
|
|
||||||
//Add inputs
|
//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 {
|
private Map<BlockTransactionHashIndex, WalletNode> selectInputs(List<UtxoSelector> utxoSelectors, Long targetValue, double feeRate, double longTermFeeRate, boolean groupByAddress, boolean includeMempoolChange) throws InsufficientFundsException {
|
||||||
Map<BlockTransactionHashIndex, WalletNode> utxos = getWalletUtxos();
|
List<OutputGroup> utxoPool = getGroupedUtxos(feeRate, longTermFeeRate, groupByAddress);
|
||||||
|
|
||||||
for(UtxoSelector utxoSelector : utxoSelectors) {
|
List<OutputGroup.Filter> filters = new ArrayList<>();
|
||||||
List<OutputGroup> utxoPool = utxos.keySet().stream().map(utxo -> new OutputGroup(getInputWeightUnits(), feeRate, longTermFeeRate, utxo)).collect(Collectors.toList());
|
filters.add(new OutputGroup.Filter(1, 6));
|
||||||
Collection<BlockTransactionHashIndex> selectedInputs = utxoSelector.select(targetValue, utxoPool);
|
filters.add(new OutputGroup.Filter(1, 1));
|
||||||
long total = selectedInputs.stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
|
if(includeMempoolChange) {
|
||||||
if(total > targetValue) {
|
filters.add(new OutputGroup.Filter(0, 1));
|
||||||
utxos.keySet().retainAll(selectedInputs);
|
}
|
||||||
return utxos;
|
|
||||||
|
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);
|
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() {
|
public BitcoinUnit getAutoUnit() {
|
||||||
for(KeyPurpose keyPurpose : KeyPurpose.values()) {
|
for(KeyPurpose keyPurpose : KeyPurpose.values()) {
|
||||||
for(WalletNode addressNode : getNode(keyPurpose).getChildren()) {
|
for(WalletNode addressNode : getNode(keyPurpose).getChildren()) {
|
||||||
|
|
Loading…
Reference in a new issue