mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-25 13:16:44 +00:00
use txo filters for all wallet transaction output filtering, fixing overselection of inputs during rbf
This commit is contained in:
parent
c9d6bb350d
commit
ebfdfc0c9f
10 changed files with 90 additions and 102 deletions
2
drongo
2
drongo
|
@ -1 +1 @@
|
||||||
Subproject commit 6a7d2aac285e9300b5897d1b64d0e5baa25e428d
|
Subproject commit 8484dd397b95d200cf3c363cd48e5751550b3bcb
|
|
@ -1298,7 +1298,7 @@ public class AppController implements Initializable {
|
||||||
Optional<List<Payment>> optPayments = sendToManyDialog.showAndWait();
|
Optional<List<Payment>> optPayments = sendToManyDialog.showAndWait();
|
||||||
optPayments.ifPresent(payments -> {
|
optPayments.ifPresent(payments -> {
|
||||||
if(!payments.isEmpty()) {
|
if(!payments.isEmpty()) {
|
||||||
EventManager.get().post(new SendActionEvent(wallet, new ArrayList<>(wallet.getWalletUtxos().keySet())));
|
EventManager.get().post(new SendActionEvent(wallet, new ArrayList<>(wallet.getSpendableUtxos().keySet())));
|
||||||
Platform.runLater(() -> EventManager.get().post(new SendPaymentsEvent(wallet, payments)));
|
Platform.runLater(() -> EventManager.get().post(new SendPaymentsEvent(wallet, payments)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -909,7 +909,7 @@ public class AppServices {
|
||||||
|
|
||||||
if(wallet != null) {
|
if(wallet != null) {
|
||||||
final Wallet sendingWallet = wallet;
|
final Wallet sendingWallet = wallet;
|
||||||
EventManager.get().post(new SendActionEvent(sendingWallet, new ArrayList<>(sendingWallet.getWalletUtxos().keySet()), true));
|
EventManager.get().post(new SendActionEvent(sendingWallet, new ArrayList<>(sendingWallet.getSpendableUtxos().keySet()), true));
|
||||||
Platform.runLater(() -> EventManager.get().post(new SendPaymentsEvent(sendingWallet, List.of(bitcoinURI.toPayment()))));
|
Platform.runLater(() -> EventManager.get().post(new SendPaymentsEvent(sendingWallet, List.of(bitcoinURI.toPayment()))));
|
||||||
}
|
}
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
|
|
|
@ -244,9 +244,9 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
Transaction tx = blockTransaction.getTransaction();
|
Transaction tx = blockTransaction.getTransaction();
|
||||||
double vSize = tx.getVirtualSize();
|
double vSize = tx.getVirtualSize();
|
||||||
int inputSize = tx.getInputs().get(0).getLength() + (tx.getInputs().get(0).hasWitness() ? tx.getInputs().get(0).getWitness().getLength() / Transaction.WITNESS_SCALE_FACTOR : 0);
|
int inputSize = tx.getInputs().get(0).getLength() + (tx.getInputs().get(0).hasWitness() ? tx.getInputs().get(0).getWitness().getLength() / Transaction.WITNESS_SCALE_FACTOR : 0);
|
||||||
List<BlockTransactionHashIndex> walletUtxos = new ArrayList<>(transactionEntry.getWallet().getWalletUtxos().keySet());
|
List<BlockTransactionHashIndex> walletUtxos = new ArrayList<>(transactionEntry.getWallet().getSpendableUtxos(blockTransaction).keySet());
|
||||||
//Remove any UTXOs that are frozen or created by the transaction that is to be replaced
|
//Remove the UTXOs we are respending
|
||||||
walletUtxos.removeIf(utxo -> utxo.getStatus() == Status.FROZEN || ourOutputs.stream().anyMatch(output -> output.getHash().equals(utxo.getHash()) && output.getIndex() == utxo.getIndex()));
|
walletUtxos.removeAll(utxos);
|
||||||
Collections.shuffle(walletUtxos);
|
Collections.shuffle(walletUtxos);
|
||||||
while((double)changeTotal / vSize < getMaxFeeRate() && !walletUtxos.isEmpty() && !cancelTransaction) {
|
while((double)changeTotal / vSize < getMaxFeeRate() && !walletUtxos.isEmpty() && !cancelTransaction) {
|
||||||
//If there is insufficient change output, include another random UTXO so the fee can be increased
|
//If there is insufficient change output, include another random UTXO so the fee can be increased
|
||||||
|
@ -319,7 +319,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
}
|
}
|
||||||
|
|
||||||
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
||||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, blockTransaction.getFee(), true)));
|
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, blockTransaction.getFee(), true, blockTransaction)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Double getMaxFeeRate() {
|
private static Double getMaxFeeRate() {
|
||||||
|
@ -350,9 +350,9 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
int inputSize = freshAddress.getScriptType().getInputVbytes();
|
int inputSize = freshAddress.getScriptType().getInputVbytes();
|
||||||
long vSize = inputSize + txOutput.getLength();
|
long vSize = inputSize + txOutput.getLength();
|
||||||
|
|
||||||
List<BlockTransactionHashIndex> walletUtxos = new ArrayList<>(transactionEntry.getWallet().getWalletUtxos().keySet());
|
List<BlockTransactionHashIndex> walletUtxos = new ArrayList<>(transactionEntry.getWallet().getSpendableUtxos().keySet());
|
||||||
//Remove any UTXOs that are frozen or that we are already spending
|
//Remove the UTXO we are already spending
|
||||||
walletUtxos.removeIf(utxo -> utxo.getStatus() == Status.FROZEN || utxo.equals(cpfpUtxo));
|
walletUtxos.remove(cpfpUtxo);
|
||||||
Collections.shuffle(walletUtxos);
|
Collections.shuffle(walletUtxos);
|
||||||
|
|
||||||
List<BlockTransactionHashIndex> utxos = new ArrayList<>();
|
List<BlockTransactionHashIndex> utxos = new ArrayList<>();
|
||||||
|
@ -371,7 +371,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
Payment payment = new Payment(freshAddress, label, inputTotal, true);
|
Payment payment = new Payment(freshAddress, label, inputTotal, true);
|
||||||
|
|
||||||
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
||||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), false)));
|
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), true, null)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean canSignMessage(WalletNode walletNode) {
|
private static boolean canSignMessage(WalletNode walletNode) {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
||||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||||
|
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||||
import com.sparrowwallet.drongo.wallet.Payment;
|
import com.sparrowwallet.drongo.wallet.Payment;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
@ -14,21 +15,23 @@ public class SpendUtxoEvent {
|
||||||
private final List<Payment> payments;
|
private final List<Payment> payments;
|
||||||
private final List<byte[]> opReturns;
|
private final List<byte[]> opReturns;
|
||||||
private final Long fee;
|
private final Long fee;
|
||||||
private final boolean includeSpentMempoolOutputs;
|
private final boolean requireAllUtxos;
|
||||||
|
private final BlockTransaction replacedTransaction;
|
||||||
private final Pool pool;
|
private final Pool pool;
|
||||||
private final PaymentCode paymentCode;
|
private final PaymentCode paymentCode;
|
||||||
|
|
||||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
|
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
|
||||||
this(wallet, utxos, null, null, null, false);
|
this(wallet, utxos, null, null, null, false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, boolean includeSpentMempoolOutputs) {
|
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, boolean requireAllUtxos, BlockTransaction replacedTransaction) {
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.utxos = utxos;
|
this.utxos = utxos;
|
||||||
this.payments = payments;
|
this.payments = payments;
|
||||||
this.opReturns = opReturns;
|
this.opReturns = opReturns;
|
||||||
this.fee = fee;
|
this.fee = fee;
|
||||||
this.includeSpentMempoolOutputs = includeSpentMempoolOutputs;
|
this.requireAllUtxos = requireAllUtxos;
|
||||||
|
this.replacedTransaction = replacedTransaction;
|
||||||
this.pool = null;
|
this.pool = null;
|
||||||
this.paymentCode = null;
|
this.paymentCode = null;
|
||||||
}
|
}
|
||||||
|
@ -39,7 +42,8 @@ public class SpendUtxoEvent {
|
||||||
this.payments = payments;
|
this.payments = payments;
|
||||||
this.opReturns = opReturns;
|
this.opReturns = opReturns;
|
||||||
this.fee = fee;
|
this.fee = fee;
|
||||||
this.includeSpentMempoolOutputs = false;
|
this.requireAllUtxos = false;
|
||||||
|
this.replacedTransaction = null;
|
||||||
this.pool = pool;
|
this.pool = pool;
|
||||||
this.paymentCode = null;
|
this.paymentCode = null;
|
||||||
}
|
}
|
||||||
|
@ -50,7 +54,8 @@ public class SpendUtxoEvent {
|
||||||
this.payments = payments;
|
this.payments = payments;
|
||||||
this.opReturns = opReturns;
|
this.opReturns = opReturns;
|
||||||
this.fee = null;
|
this.fee = null;
|
||||||
this.includeSpentMempoolOutputs = false;
|
this.requireAllUtxos = false;
|
||||||
|
this.replacedTransaction = null;
|
||||||
this.pool = null;
|
this.pool = null;
|
||||||
this.paymentCode = paymentCode;
|
this.paymentCode = paymentCode;
|
||||||
}
|
}
|
||||||
|
@ -75,8 +80,12 @@ public class SpendUtxoEvent {
|
||||||
return fee;
|
return fee;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isIncludeSpentMempoolOutputs() {
|
public boolean isRequireAllUtxos() {
|
||||||
return includeSpentMempoolOutputs;
|
return requireAllUtxos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlockTransaction getReplacedTransaction() {
|
||||||
|
return replacedTransaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Pool getPool() {
|
public Pool getPool() {
|
||||||
|
|
|
@ -465,7 +465,7 @@ public class PayNymController {
|
||||||
PaymentCode paymentCode = payNym.paymentCode();
|
PaymentCode paymentCode = payNym.paymentCode();
|
||||||
Payment payment = new Payment(paymentCode.getNotificationAddress(), "Link " + payNym.nymName(), MINIMUM_P2PKH_OUTPUT_SATS, false);
|
Payment payment = new Payment(paymentCode.getNotificationAddress(), "Link " + payNym.nymName(), MINIMUM_P2PKH_OUTPUT_SATS, false);
|
||||||
Wallet wallet = AppServices.get().getWallet(walletId);
|
Wallet wallet = AppServices.get().getWallet(walletId);
|
||||||
EventManager.get().post(new SendActionEvent(wallet, new ArrayList<>(wallet.getWalletUtxos().keySet())));
|
EventManager.get().post(new SendActionEvent(wallet, new ArrayList<>(wallet.getSpendableUtxos().keySet())));
|
||||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(wallet, List.of(payment), List.of(new byte[80]), paymentCode)));
|
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(wallet, List.of(payment), List.of(new byte[80]), paymentCode)));
|
||||||
closeProperty.set(true);
|
closeProperty.set(true);
|
||||||
} else {
|
} else {
|
||||||
|
@ -604,10 +604,10 @@ public class PayNymController {
|
||||||
boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs();
|
boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs();
|
||||||
|
|
||||||
long noInputsFee = getMasterWallet().getNoInputsFee(payments, feeRate);
|
long noInputsFee = getMasterWallet().getNoInputsFee(payments, feeRate);
|
||||||
List<UtxoSelector> utxoSelectors = List.of(utxos == null ? new KnapsackUtxoSelector(noInputsFee) : new PresetUtxoSelector(utxos, true));
|
List<UtxoSelector> utxoSelectors = List.of(utxos == null ? new KnapsackUtxoSelector(noInputsFee) : new PresetUtxoSelector(utxos, true, false));
|
||||||
List<UtxoFilter> utxoFilters = List.of(new FrozenUtxoFilter(), new CoinbaseUtxoFilter(wallet));
|
List<TxoFilter> txoFilters = List.of(new SpentTxoFilter(), new FrozenTxoFilter(), new CoinbaseTxoFilter(wallet));
|
||||||
|
|
||||||
return wallet.createWalletTransaction(utxoSelectors, utxoFilters, payments, opReturns, Collections.emptySet(), feeRate, minimumFeeRate, null, AppServices.getCurrentBlockHeight(), groupByAddress, includeMempoolOutputs, false);
|
return wallet.createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, Collections.emptySet(), feeRate, minimumFeeRate, null, AppServices.getCurrentBlockHeight(), groupByAddress, includeMempoolOutputs);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<BlockTransaction, WalletNode> getNotificationTransaction(PaymentCode externalPaymentCode) {
|
private Map<BlockTransaction, WalletNode> getNotificationTransaction(PaymentCode externalPaymentCode) {
|
||||||
|
|
|
@ -294,12 +294,10 @@ public class CounterpartyController extends SorobanController {
|
||||||
sorobanProgressLabel.setText("Creating mix transaction...");
|
sorobanProgressLabel.setText("Creating mix transaction...");
|
||||||
|
|
||||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||||
Map<BlockTransactionHashIndex, WalletNode> walletUtxos = wallet.getWalletUtxos();
|
Map<BlockTransactionHashIndex, WalletNode> walletUtxos = wallet.getSpendableUtxos();
|
||||||
for(Map.Entry<BlockTransactionHashIndex, WalletNode> entry : walletUtxos.entrySet()) {
|
for(Map.Entry<BlockTransactionHashIndex, WalletNode> entry : walletUtxos.entrySet()) {
|
||||||
if(entry.getKey().getStatus() != Status.FROZEN) {
|
|
||||||
counterpartyCahootsWallet.addUtxo(entry.getValue(), wallet.getWalletTransaction(entry.getKey().getHash()), (int)entry.getKey().getIndex());
|
counterpartyCahootsWallet.addUtxo(entry.getValue(), wallet.getWalletTransaction(entry.getKey().getHash()), (int)entry.getKey().getIndex());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
SorobanCahootsService sorobanCahootsService = soroban.getSorobanCahootsService(counterpartyCahootsWallet);
|
SorobanCahootsService sorobanCahootsService = soroban.getSorobanCahootsService(counterpartyCahootsWallet);
|
||||||
|
|
|
@ -466,12 +466,10 @@ public class InitiatorController extends SorobanController {
|
||||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||||
|
|
||||||
Payment payment = walletTransaction.getPayments().get(0);
|
Payment payment = walletTransaction.getPayments().get(0);
|
||||||
Map<BlockTransactionHashIndex, WalletNode> firstSetUtxos = walletTransaction.isCoinControlUsed() ? walletTransaction.getSelectedUtxoSets().get(0) : wallet.getWalletUtxos();
|
Map<BlockTransactionHashIndex, WalletNode> firstSetUtxos = walletTransaction.isCoinControlUsed() ? walletTransaction.getSelectedUtxoSets().get(0) : wallet.getSpendableUtxos();
|
||||||
for(Map.Entry<BlockTransactionHashIndex, WalletNode> entry : firstSetUtxos.entrySet()) {
|
for(Map.Entry<BlockTransactionHashIndex, WalletNode> entry : firstSetUtxos.entrySet()) {
|
||||||
if(entry.getKey().getStatus() != Status.FROZEN) {
|
|
||||||
initiatorCahootsWallet.addUtxo(entry.getValue(), wallet.getWalletTransaction(entry.getKey().getHash()), (int)entry.getKey().getIndex());
|
initiatorCahootsWallet.addUtxo(entry.getValue(), wallet.getWalletTransaction(entry.getKey().getHash()), (int)entry.getKey().getIndex());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
SorobanCahootsService sorobanCahootsService = soroban.getSorobanCahootsService(initiatorCahootsWallet);
|
SorobanCahootsService sorobanCahootsService = soroban.getSorobanCahootsService(initiatorCahootsWallet);
|
||||||
CahootsContext cahootsContext = cahootsType == CahootsType.STONEWALLX2 ?
|
CahootsContext cahootsContext = cahootsType == CahootsType.STONEWALLX2 ?
|
||||||
|
|
|
@ -158,7 +158,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
|
|
||||||
private final ObjectProperty<UtxoSelector> utxoSelectorProperty = new SimpleObjectProperty<>(null);
|
private final ObjectProperty<UtxoSelector> utxoSelectorProperty = new SimpleObjectProperty<>(null);
|
||||||
|
|
||||||
private final ObjectProperty<UtxoFilter> utxoFilterProperty = new SimpleObjectProperty<>(null);
|
private final ObjectProperty<TxoFilter> txoFilterProperty = new SimpleObjectProperty<>(null);
|
||||||
|
|
||||||
private final ObjectProperty<Pool> whirlpoolProperty = new SimpleObjectProperty<>(null);
|
private final ObjectProperty<Pool> whirlpoolProperty = new SimpleObjectProperty<>(null);
|
||||||
|
|
||||||
|
@ -170,7 +170,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
|
|
||||||
private final StringProperty utxoLabelSelectionProperty = new SimpleStringProperty("");
|
private final StringProperty utxoLabelSelectionProperty = new SimpleStringProperty("");
|
||||||
|
|
||||||
private final BooleanProperty includeSpentMempoolOutputsProperty = new SimpleBooleanProperty(false);
|
private final ObjectProperty<BlockTransaction> replacedTransactionProperty = new SimpleObjectProperty<>(null);
|
||||||
|
|
||||||
private final List<byte[]> opReturnsList = new ArrayList<>();
|
private final List<byte[]> opReturnsList = new ArrayList<>();
|
||||||
|
|
||||||
|
@ -385,11 +385,11 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
});
|
});
|
||||||
|
|
||||||
utxoSelectorProperty.addListener((observable, oldValue, utxoSelector) -> {
|
utxoSelectorProperty.addListener((observable, oldValue, utxoSelector) -> {
|
||||||
updateMaxClearButtons(utxoSelector, utxoFilterProperty.get());
|
updateMaxClearButtons(utxoSelector, txoFilterProperty.get());
|
||||||
});
|
});
|
||||||
|
|
||||||
utxoFilterProperty.addListener((observable, oldValue, utxoFilter) -> {
|
txoFilterProperty.addListener((observable, oldValue, txoFilter) -> {
|
||||||
updateMaxClearButtons(utxoSelectorProperty.get(), utxoFilter);
|
updateMaxClearButtons(utxoSelectorProperty.get(), txoFilter);
|
||||||
});
|
});
|
||||||
|
|
||||||
walletTransactionProperty.addListener((observable, oldValue, walletTransaction) -> {
|
walletTransactionProperty.addListener((observable, oldValue, walletTransaction) -> {
|
||||||
|
@ -590,11 +590,11 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
Integer currentBlockHeight = AppServices.getCurrentBlockHeight();
|
Integer currentBlockHeight = AppServices.getCurrentBlockHeight();
|
||||||
boolean groupByAddress = Config.get().isGroupByAddress();
|
boolean groupByAddress = Config.get().isGroupByAddress();
|
||||||
boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs();
|
boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs();
|
||||||
boolean includeSpentMempoolOutputs = includeSpentMempoolOutputsProperty.get();
|
BlockTransaction replacedTransaction = replacedTransactionProperty.get();
|
||||||
|
|
||||||
walletTransactionService = new WalletTransactionService(addressNodeMap, wallet, getUtxoSelectors(payments), getUtxoFilters(),
|
walletTransactionService = new WalletTransactionService(addressNodeMap, wallet, getUtxoSelectors(payments), getTxoFilters(),
|
||||||
payments, opReturnsList, excludedChangeNodes,
|
payments, opReturnsList, excludedChangeNodes,
|
||||||
feeRate, getMinimumFeeRate(), userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs, includeSpentMempoolOutputs);
|
feeRate, getMinimumFeeRate(), userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs, replacedTransaction);
|
||||||
walletTransactionService.setOnSucceeded(event -> {
|
walletTransactionService.setOnSucceeded(event -> {
|
||||||
if(!walletTransactionService.isIgnoreResult()) {
|
if(!walletTransactionService.isIgnoreResult()) {
|
||||||
walletTransactionProperty.setValue(walletTransactionService.getValue());
|
walletTransactionProperty.setValue(walletTransactionService.getValue());
|
||||||
|
@ -660,7 +660,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
private final Map<Wallet, Map<Address, WalletNode>> addressNodeMap;
|
private final Map<Wallet, Map<Address, WalletNode>> addressNodeMap;
|
||||||
private final Wallet wallet;
|
private final Wallet wallet;
|
||||||
private final List<UtxoSelector> utxoSelectors;
|
private final List<UtxoSelector> utxoSelectors;
|
||||||
private final List<UtxoFilter> utxoFilters;
|
private final List<TxoFilter> txoFilters;
|
||||||
private final List<Payment> payments;
|
private final List<Payment> payments;
|
||||||
private final List<byte[]> opReturns;
|
private final List<byte[]> opReturns;
|
||||||
private final Set<WalletNode> excludedChangeNodes;
|
private final Set<WalletNode> excludedChangeNodes;
|
||||||
|
@ -670,17 +670,17 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
private final Integer currentBlockHeight;
|
private final Integer currentBlockHeight;
|
||||||
private final boolean groupByAddress;
|
private final boolean groupByAddress;
|
||||||
private final boolean includeMempoolOutputs;
|
private final boolean includeMempoolOutputs;
|
||||||
private final boolean includeSpentMempoolOutputs;
|
private final BlockTransaction replacedTransaction;
|
||||||
private boolean ignoreResult;
|
private boolean ignoreResult;
|
||||||
|
|
||||||
public WalletTransactionService(Map<Wallet, Map<Address, WalletNode>> addressNodeMap,
|
public WalletTransactionService(Map<Wallet, Map<Address, WalletNode>> addressNodeMap,
|
||||||
Wallet wallet, List<UtxoSelector> utxoSelectors, List<UtxoFilter> utxoFilters,
|
Wallet wallet, List<UtxoSelector> utxoSelectors, List<TxoFilter> txoFilters,
|
||||||
List<Payment> payments, List<byte[]> opReturns, Set<WalletNode> excludedChangeNodes,
|
List<Payment> payments, List<byte[]> opReturns, Set<WalletNode> excludedChangeNodes,
|
||||||
double feeRate, double longTermFeeRate, Long fee, Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs, boolean includeSpentMempoolOutputs) {
|
double feeRate, double longTermFeeRate, Long fee, Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs, BlockTransaction replacedTransaction) {
|
||||||
this.addressNodeMap = addressNodeMap;
|
this.addressNodeMap = addressNodeMap;
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.utxoSelectors = utxoSelectors;
|
this.utxoSelectors = utxoSelectors;
|
||||||
this.utxoFilters = utxoFilters;
|
this.txoFilters = txoFilters;
|
||||||
this.payments = payments;
|
this.payments = payments;
|
||||||
this.opReturns = opReturns;
|
this.opReturns = opReturns;
|
||||||
this.excludedChangeNodes = excludedChangeNodes;
|
this.excludedChangeNodes = excludedChangeNodes;
|
||||||
|
@ -690,7 +690,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
this.currentBlockHeight = currentBlockHeight;
|
this.currentBlockHeight = currentBlockHeight;
|
||||||
this.groupByAddress = groupByAddress;
|
this.groupByAddress = groupByAddress;
|
||||||
this.includeMempoolOutputs = includeMempoolOutputs;
|
this.includeMempoolOutputs = includeMempoolOutputs;
|
||||||
this.includeSpentMempoolOutputs = includeSpentMempoolOutputs;
|
this.replacedTransaction = replacedTransaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -700,21 +700,9 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
try {
|
try {
|
||||||
return getWalletTransaction();
|
return getWalletTransaction();
|
||||||
} catch(InsufficientFundsException e) {
|
} catch(InsufficientFundsException e) {
|
||||||
if(e.getTargetValue() != null && includeSpentMempoolOutputs && utxoSelectors.size() == 1 && utxoSelectors.get(0) instanceof PresetUtxoSelector presetUtxoSelector) {
|
if(e.getTargetValue() != null && replacedTransaction != null && utxoSelectors.size() == 1 && utxoSelectors.get(0) instanceof PresetUtxoSelector presetUtxoSelector) {
|
||||||
Optional<BlockTransaction> optBlkTx = wallet.getWalletTransactions().values().stream().filter(blkTx -> {
|
|
||||||
return blkTx.getTransaction().getInputs().stream().anyMatch(txInput -> {
|
|
||||||
return presetUtxoSelector.getPresetUtxos().stream().anyMatch(ref -> {
|
|
||||||
return txInput.getOutpoint().getHash().equals(ref.getHash()) && txInput.getOutpoint().getIndex() == ref.getIndex();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}).findFirst();
|
|
||||||
|
|
||||||
if(optBlkTx.isPresent()) {
|
|
||||||
//Creating RBF transaction - include additional UTXOs if available to pay desired fee
|
//Creating RBF transaction - include additional UTXOs if available to pay desired fee
|
||||||
Transaction rbfTx = optBlkTx.get().getTransaction();
|
List<BlockTransactionHashIndex> walletUtxos = new ArrayList<>(wallet.getWalletTxos(txoFilters).keySet());
|
||||||
List<BlockTransactionHashIndex> walletUtxos = new ArrayList<>(wallet.getWalletUtxos().keySet());
|
|
||||||
//Remove any UTXOs that are frozen or created by the transaction that is to be replaced
|
|
||||||
walletUtxos.removeIf(utxo -> utxo.getStatus() == Status.FROZEN || rbfTx.getOutputs().stream().anyMatch(output -> output.getHash().equals(utxo.getHash()) && output.getIndex() == utxo.getIndex()));
|
|
||||||
//Remove any UTXOs that have already been added or previously excluded
|
//Remove any UTXOs that have already been added or previously excluded
|
||||||
walletUtxos.removeAll(presetUtxoSelector.getPresetUtxos());
|
walletUtxos.removeAll(presetUtxoSelector.getPresetUtxos());
|
||||||
walletUtxos.removeAll(presetUtxoSelector.getExcludedUtxos());
|
walletUtxos.removeAll(presetUtxoSelector.getExcludedUtxos());
|
||||||
|
@ -725,7 +713,6 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
|
|
||||||
return getWalletTransaction();
|
return getWalletTransaction();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
@ -733,8 +720,8 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
|
|
||||||
private WalletTransaction getWalletTransaction() throws InsufficientFundsException {
|
private WalletTransaction getWalletTransaction() throws InsufficientFundsException {
|
||||||
updateMessage("Selecting UTXOs...");
|
updateMessage("Selecting UTXOs...");
|
||||||
WalletTransaction walletTransaction = wallet.createWalletTransaction(utxoSelectors, utxoFilters, payments, opReturns, excludedChangeNodes,
|
WalletTransaction walletTransaction = wallet.createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, excludedChangeNodes,
|
||||||
feeRate, longTermFeeRate, fee, currentBlockHeight, groupByAddress, includeMempoolOutputs, includeSpentMempoolOutputs);
|
feeRate, longTermFeeRate, fee, currentBlockHeight, groupByAddress, includeMempoolOutputs);
|
||||||
updateMessage("Deriving keys...");
|
updateMessage("Deriving keys...");
|
||||||
walletTransaction.updateAddressNodeMap(addressNodeMap, walletTransaction.getWallet());
|
walletTransaction.updateAddressNodeMap(addressNodeMap, walletTransaction.getWallet());
|
||||||
return walletTransaction;
|
return walletTransaction;
|
||||||
|
@ -751,13 +738,15 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<UtxoFilter> getUtxoFilters() {
|
private List<TxoFilter> getTxoFilters() {
|
||||||
UtxoFilter utxoFilter = utxoFilterProperty.get();
|
SpentTxoFilter spentTxoFilter = new SpentTxoFilter(replacedTransactionProperty.get() == null ? null : replacedTransactionProperty.get().getHash());
|
||||||
if(utxoFilter != null) {
|
|
||||||
return List.of(utxoFilter, new FrozenUtxoFilter(), new CoinbaseUtxoFilter(getWalletForm().getWallet()));
|
TxoFilter txoFilter = txoFilterProperty.get();
|
||||||
|
if(txoFilter != null) {
|
||||||
|
return List.of(txoFilter, spentTxoFilter, new FrozenTxoFilter(), new CoinbaseTxoFilter(getWalletForm().getWallet()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return List.of(new FrozenUtxoFilter(), new CoinbaseUtxoFilter(getWalletForm().getWallet()));
|
return List.of(spentTxoFilter, new FrozenTxoFilter(), new CoinbaseTxoFilter(getWalletForm().getWallet()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateFeeRateSelection(FeeRatesSelection feeRatesSelection) {
|
private void updateFeeRateSelection(FeeRatesSelection feeRatesSelection) {
|
||||||
|
@ -1029,15 +1018,13 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
return (int)Math.round(index * 10.0);
|
return (int)Math.round(index * 10.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateMaxClearButtons(UtxoSelector utxoSelector, UtxoFilter utxoFilter) {
|
private void updateMaxClearButtons(UtxoSelector utxoSelector, TxoFilter txoFilter) {
|
||||||
if(utxoSelector instanceof PresetUtxoSelector) {
|
if(utxoSelector instanceof PresetUtxoSelector presetUtxoSelector) {
|
||||||
PresetUtxoSelector presetUtxoSelector = (PresetUtxoSelector)utxoSelector;
|
|
||||||
int num = presetUtxoSelector.getPresetUtxos().size();
|
int num = presetUtxoSelector.getPresetUtxos().size();
|
||||||
String selection = " (" + num + " UTXO" + (num != 1 ? "s" : "") + " selected)";
|
String selection = " (" + num + " UTXO" + (num != 1 ? "s" : "") + " selected)";
|
||||||
utxoLabelSelectionProperty.set(selection);
|
utxoLabelSelectionProperty.set(selection);
|
||||||
} else if(utxoFilter instanceof ExcludeUtxoFilter) {
|
} else if(txoFilter instanceof ExcludeTxoFilter excludeTxoFilter) {
|
||||||
ExcludeUtxoFilter excludeUtxoFilter = (ExcludeUtxoFilter)utxoFilter;
|
int num = excludeTxoFilter.getExcludedTxos().size();
|
||||||
int num = excludeUtxoFilter.getExcludedUtxos().size();
|
|
||||||
String exclusion = " (" + num + " UTXO" + (num != 1 ? "s" : "") + " excluded)";
|
String exclusion = " (" + num + " UTXO" + (num != 1 ? "s" : "") + " excluded)";
|
||||||
utxoLabelSelectionProperty.set(exclusion);
|
utxoLabelSelectionProperty.set(exclusion);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1130,8 +1117,8 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
userFeeSet.set(false);
|
userFeeSet.set(false);
|
||||||
setDefaultFeeRate();
|
setDefaultFeeRate();
|
||||||
utxoSelectorProperty.setValue(null);
|
utxoSelectorProperty.setValue(null);
|
||||||
utxoFilterProperty.setValue(null);
|
txoFilterProperty.setValue(null);
|
||||||
includeSpentMempoolOutputsProperty.set(false);
|
replacedTransactionProperty.setValue(null);
|
||||||
opReturnsList.clear();
|
opReturnsList.clear();
|
||||||
excludedChangeNodes.clear();
|
excludedChangeNodes.clear();
|
||||||
walletTransactionProperty.setValue(null);
|
walletTransactionProperty.setValue(null);
|
||||||
|
@ -1320,15 +1307,14 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
byte[] blindingMask = PaymentCode.getMask(secretPoint.ECDHSecretAsBytes(), input0Outpoint.bitcoinSerialize());
|
byte[] blindingMask = PaymentCode.getMask(secretPoint.ECDHSecretAsBytes(), input0Outpoint.bitcoinSerialize());
|
||||||
byte[] blindedPaymentCode = PaymentCode.blind(paymentCode.getPayload(), blindingMask);
|
byte[] blindedPaymentCode = PaymentCode.blind(paymentCode.getPayload(), blindingMask);
|
||||||
|
|
||||||
List<UtxoSelector> utxoSelectors = List.of(new PresetUtxoSelector(walletTransaction.getSelectedUtxos().keySet(), true));
|
List<UtxoSelector> utxoSelectors = List.of(new PresetUtxoSelector(walletTransaction.getSelectedUtxos().keySet(), true, false));
|
||||||
Long userFee = userFeeSet.get() ? getFeeValueSats() : null;
|
Long userFee = userFeeSet.get() ? getFeeValueSats() : null;
|
||||||
double feeRate = getUserFeeRate();
|
double feeRate = getUserFeeRate();
|
||||||
Integer currentBlockHeight = AppServices.getCurrentBlockHeight();
|
Integer currentBlockHeight = AppServices.getCurrentBlockHeight();
|
||||||
boolean groupByAddress = Config.get().isGroupByAddress();
|
boolean groupByAddress = Config.get().isGroupByAddress();
|
||||||
boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs();
|
boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs();
|
||||||
boolean includeSpentMempoolOutputs = includeSpentMempoolOutputsProperty.get();
|
|
||||||
|
|
||||||
WalletTransaction finalWalletTx = decryptedWallet.createWalletTransaction(utxoSelectors, getUtxoFilters(), walletTransaction.getPayments(), List.of(blindedPaymentCode), excludedChangeNodes, feeRate, getMinimumFeeRate(), userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs, includeSpentMempoolOutputs);
|
WalletTransaction finalWalletTx = decryptedWallet.createWalletTransaction(utxoSelectors, getTxoFilters(), walletTransaction.getPayments(), List.of(blindedPaymentCode), excludedChangeNodes, feeRate, getMinimumFeeRate(), userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs);
|
||||||
PSBT psbt = finalWalletTx.createPSBT();
|
PSBT psbt = finalWalletTx.createPSBT();
|
||||||
decryptedWallet.sign(psbt);
|
decryptedWallet.sign(psbt);
|
||||||
decryptedWallet.finalise(psbt);
|
decryptedWallet.finalise(psbt);
|
||||||
|
@ -1553,14 +1539,14 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
userFeeSet.set(true);
|
userFeeSet.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
includeSpentMempoolOutputsProperty.set(event.isIncludeSpentMempoolOutputs());
|
replacedTransactionProperty.set(event.getReplacedTransaction());
|
||||||
|
|
||||||
if(event.getUtxos() != null) {
|
if(event.getUtxos() != null) {
|
||||||
List<BlockTransactionHashIndex> utxos = event.getUtxos();
|
List<BlockTransactionHashIndex> utxos = event.getUtxos();
|
||||||
utxoSelectorProperty.set(new PresetUtxoSelector(utxos));
|
utxoSelectorProperty.set(new PresetUtxoSelector(utxos, false, event.isRequireAllUtxos()));
|
||||||
}
|
}
|
||||||
|
|
||||||
utxoFilterProperty.set(null);
|
txoFilterProperty.set(null);
|
||||||
whirlpoolProperty.set(event.getPool());
|
whirlpoolProperty.set(event.getPool());
|
||||||
paymentCodeProperty.set(event.getPaymentCode());
|
paymentCodeProperty.set(event.getPaymentCode());
|
||||||
updateTransaction(event.getPayments() == null || event.getPayments().stream().anyMatch(Payment::isSendMax));
|
updateTransaction(event.getPayments() == null || event.getPayments().stream().anyMatch(Payment::isSendMax));
|
||||||
|
@ -1628,11 +1614,8 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
if(event.getWalletTransaction() == walletTransactionProperty.get()) {
|
if(event.getWalletTransaction() == walletTransactionProperty.get()) {
|
||||||
UtxoSelector utxoSelector = utxoSelectorProperty.get();
|
UtxoSelector utxoSelector = utxoSelectorProperty.get();
|
||||||
if(utxoSelector instanceof MaxUtxoSelector) {
|
if(utxoSelector instanceof MaxUtxoSelector) {
|
||||||
Collection<BlockTransactionHashIndex> utxos = walletForm.getWallet().getWalletUtxos().keySet();
|
Collection<BlockTransactionHashIndex> utxos = event.getWalletTransaction().getSelectedUtxos().keySet();
|
||||||
utxos.remove(event.getUtxo());
|
utxos.remove(event.getUtxo());
|
||||||
if(utxoFilterProperty.get() instanceof ExcludeUtxoFilter existingUtxoFilter) {
|
|
||||||
utxos.removeAll(existingUtxoFilter.getExcludedUtxos());
|
|
||||||
}
|
|
||||||
PresetUtxoSelector presetUtxoSelector = new PresetUtxoSelector(utxos);
|
PresetUtxoSelector presetUtxoSelector = new PresetUtxoSelector(utxos);
|
||||||
presetUtxoSelector.getExcludedUtxos().add(event.getUtxo());
|
presetUtxoSelector.getExcludedUtxos().add(event.getUtxo());
|
||||||
utxoSelectorProperty.set(presetUtxoSelector);
|
utxoSelectorProperty.set(presetUtxoSelector);
|
||||||
|
@ -1642,15 +1625,15 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
presetUtxoSelector.getPresetUtxos().remove(event.getUtxo());
|
presetUtxoSelector.getPresetUtxos().remove(event.getUtxo());
|
||||||
presetUtxoSelector.getExcludedUtxos().add(event.getUtxo());
|
presetUtxoSelector.getExcludedUtxos().add(event.getUtxo());
|
||||||
utxoSelectorProperty.set(presetUtxoSelector);
|
utxoSelectorProperty.set(presetUtxoSelector);
|
||||||
updateTransaction(!includeSpentMempoolOutputsProperty.get());
|
updateTransaction(replacedTransactionProperty.get() == null);
|
||||||
} else {
|
} else {
|
||||||
ExcludeUtxoFilter utxoFilter = new ExcludeUtxoFilter();
|
ExcludeTxoFilter excludeTxoFilter = new ExcludeTxoFilter();
|
||||||
if(utxoFilterProperty.get() instanceof ExcludeUtxoFilter existingUtxoFilter) {
|
if(txoFilterProperty.get() instanceof ExcludeTxoFilter existingTxoFilter) {
|
||||||
utxoFilter.getExcludedUtxos().addAll(existingUtxoFilter.getExcludedUtxos());
|
excludeTxoFilter.getExcludedTxos().addAll(existingTxoFilter.getExcludedTxos());
|
||||||
}
|
}
|
||||||
|
|
||||||
utxoFilter.getExcludedUtxos().add(event.getUtxo());
|
excludeTxoFilter.getExcludedTxos().add(event.getUtxo());
|
||||||
utxoFilterProperty.set(utxoFilter);
|
txoFilterProperty.set(excludeTxoFilter);
|
||||||
updateTransaction();
|
updateTransaction();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,9 +105,9 @@ public class SparrowDataSource extends WalletResponseDataSource {
|
||||||
address.n_tx = walletTransactions.size();
|
address.n_tx = walletTransactions.size();
|
||||||
addresses.add(address);
|
addresses.add(address);
|
||||||
|
|
||||||
for(Map.Entry<BlockTransactionHashIndex, WalletNode> utxo : wallet.getWalletUtxos().entrySet()) {
|
for(Map.Entry<BlockTransactionHashIndex, WalletNode> utxo : wallet.getSpendableUtxos().entrySet()) {
|
||||||
BlockTransaction blockTransaction = wallet.getWalletTransaction(utxo.getKey().getHash());
|
BlockTransaction blockTransaction = wallet.getWalletTransaction(utxo.getKey().getHash());
|
||||||
if(blockTransaction != null && utxo.getKey().getStatus() != Status.FROZEN) {
|
if(blockTransaction != null) {
|
||||||
unspentOutputs.add(Whirlpool.getUnspentOutput(utxo.getValue(), blockTransaction, (int)utxo.getKey().getIndex()));
|
unspentOutputs.add(Whirlpool.getUnspentOutput(utxo.getValue(), blockTransaction, (int)utxo.getKey().getIndex()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue