add increase fee functionality for rbf transactions

This commit is contained in:
Craig Raw 2020-11-24 20:34:51 +02:00
parent 94960567b5
commit 2ca8b91283
15 changed files with 240 additions and 58 deletions

2
drongo

@ -1 +1 @@
Subproject commit 49799fc0c8b5245a7931d0437a68172f9b6efbbc Subproject commit 6b20c6558ab7cef6f582461692232a7687fe26c8

View file

@ -2,8 +2,11 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.wallet.BlockTransaction; import com.sparrowwallet.drongo.protocol.NonStandardScriptException;
import com.sparrowwallet.drongo.wallet.KeystoreSource; import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
@ -19,7 +22,7 @@ import org.controlsfx.glyphfont.Glyph;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.List; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
class EntryCell extends TreeTableCell<Entry, Entry> { class EntryCell extends TreeTableCell<Entry, Entry> {
@ -46,10 +49,10 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
TransactionEntry transactionEntry = (TransactionEntry)entry; TransactionEntry transactionEntry = (TransactionEntry)entry;
if(transactionEntry.getBlockTransaction().getHeight() == -1) { if(transactionEntry.getBlockTransaction().getHeight() == -1) {
setText("Unconfirmed Parent"); setText("Unconfirmed Parent");
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry.getBlockTransaction())); setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry));
} else if(transactionEntry.getBlockTransaction().getHeight() == 0) { } else if(transactionEntry.getBlockTransaction().getHeight() == 0) {
setText("Unconfirmed"); setText("Unconfirmed");
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry.getBlockTransaction())); setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry));
} else { } else {
String date = DATE_FORMAT.format(transactionEntry.getBlockTransaction().getDate()); String date = DATE_FORMAT.format(transactionEntry.getBlockTransaction().getDate());
setText(date); setText(date);
@ -60,6 +63,7 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
tooltip.setText(transactionEntry.getBlockTransaction().getHash().toString()); tooltip.setText(transactionEntry.getBlockTransaction().getHash().toString());
setTooltip(tooltip); setTooltip(tooltip);
HBox actionBox = new HBox();
Button viewTransactionButton = new Button(""); Button viewTransactionButton = new Button("");
Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH); Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH);
searchGlyph.setFontSize(12); searchGlyph.setFontSize(12);
@ -67,7 +71,21 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
viewTransactionButton.setOnAction(event -> { viewTransactionButton.setOnAction(event -> {
EventManager.get().post(new ViewTransactionEvent(transactionEntry.getBlockTransaction())); EventManager.get().post(new ViewTransactionEvent(transactionEntry.getBlockTransaction()));
}); });
setGraphic(viewTransactionButton); actionBox.getChildren().add(viewTransactionButton);
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
if(blockTransaction.getHeight() <= 0 && blockTransaction.getTransaction().isReplaceByFee() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
Button increaseFeeButton = new Button("");
Glyph increaseFeeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING_MEDICAL);
increaseFeeGlyph.setFontSize(12);
increaseFeeButton.setGraphic(increaseFeeGlyph);
increaseFeeButton.setOnAction(event -> {
increaseFee(transactionEntry);
});
actionBox.getChildren().add(increaseFeeButton);
}
setGraphic(actionBox);
} else if(entry instanceof NodeEntry) { } else if(entry instanceof NodeEntry) {
NodeEntry nodeEntry = (NodeEntry)entry; NodeEntry nodeEntry = (NodeEntry)entry;
Address address = nodeEntry.getAddress(); Address address = nodeEntry.getAddress();
@ -139,9 +157,9 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
utxoEntries = List.of(hashIndexEntry); utxoEntries = List.of(hashIndexEntry);
} }
final List<HashIndexEntry> spendingUtxoEntries = utxoEntries; final List<BlockTransactionHashIndex> spendingUtxos = utxoEntries.stream().map(HashIndexEntry::getHashIndex).collect(Collectors.toList());
EventManager.get().post(new SendActionEvent(utxoEntries)); EventManager.get().post(new SendActionEvent(hashIndexEntry.getWallet(), spendingUtxos));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(spendingUtxoEntries))); Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(hashIndexEntry.getWallet(), spendingUtxos)));
}); });
actionBox.getChildren().add(spendUtxoButton); actionBox.getChildren().add(spendUtxoButton);
} }
@ -151,8 +169,63 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
} }
} }
private static void increaseFee(TransactionEntry transactionEntry) {
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
Map<BlockTransactionHashIndex, WalletNode> walletTxos = transactionEntry.getWallet().getWalletTxos();
List<BlockTransactionHashIndex> utxos = transactionEntry.getChildren().stream()
.filter(e -> e instanceof HashIndexEntry)
.map(e -> (HashIndexEntry)e)
.filter(e -> e.getType().equals(HashIndexEntry.Type.INPUT) && e.isSpendable())
.map(e -> blockTransaction.getTransaction().getInputs().get((int)e.getHashIndex().getIndex()))
.map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get())
.collect(Collectors.toList());
List<TransactionOutput> ourOutputs = transactionEntry.getChildren().stream()
.filter(e -> e instanceof HashIndexEntry)
.map(e -> (HashIndexEntry)e)
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT))
.map(e -> e.getBlockTransaction().getTransaction().getOutputs().get((int)e.getHashIndex().getIndex()))
.collect(Collectors.toList());
long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
Transaction tx = blockTransaction.getTransaction();
int 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);
List<BlockTransactionHashIndex> walletUtxos = new ArrayList<>(transactionEntry.getWallet().getWalletUtxos().keySet());
Collections.shuffle(walletUtxos);
while((double)changeTotal / vSize < getMaxFeeRate() && !walletUtxos.isEmpty()) {
//If there is insufficent change output, include another random UTXO so the fee can be increased
BlockTransactionHashIndex utxo = walletUtxos.remove(0);
utxos.add(utxo);
changeTotal += utxo.getValue();
vSize += inputSize;
}
List<TransactionOutput> externalOutputs = new ArrayList<>(blockTransaction.getTransaction().getOutputs());
externalOutputs.removeAll(ourOutputs);
List<Payment> payments = externalOutputs.stream().map(txOutput -> {
try {
return new Payment(txOutput.getScript().getToAddresses()[0], transactionEntry.getLabel(), txOutput.getValue(), false);
} catch(Exception e) {
return null;
}
}).filter(Objects::nonNull).collect(Collectors.toList());
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, blockTransaction.getFee(), true)));
}
private static Double getMaxFeeRate() {
if(AppController.getTargetBlockFeeRates().isEmpty()) {
return 100.0;
}
return AppController.getTargetBlockFeeRates().values().iterator().next();
}
private static class UnconfirmedTransactionContextMenu extends ContextMenu { private static class UnconfirmedTransactionContextMenu extends ContextMenu {
public UnconfirmedTransactionContextMenu(BlockTransaction blockTransaction) { public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) {
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
MenuItem copyTxid = new MenuItem("Copy Transaction ID"); MenuItem copyTxid = new MenuItem("Copy Transaction ID");
copyTxid.setOnAction(AE -> { copyTxid.setOnAction(AE -> {
hide(); hide();
@ -161,7 +234,17 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
Clipboard.getSystemClipboard().setContent(content); Clipboard.getSystemClipboard().setContent(content);
}); });
getItems().addAll(copyTxid); getItems().add(copyTxid);
if(blockTransaction.getTransaction().isReplaceByFee() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
MenuItem increaseFee = new MenuItem("Increase Fee");
increaseFee.setOnAction(AE -> {
hide();
increaseFee(transactionEntry);
});
getItems().add(increaseFee);
}
} }
} }

View file

@ -174,7 +174,9 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
if(mvb >= 0.01) { if(mvb >= 0.01) {
Label label = new Label(series.getName() + ": " + String.format("%.2f", mvb) + " MvB"); Label label = new Label(series.getName() + ": " + String.format("%.2f", mvb) + " MvB");
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CIRCLE); Glyph circle = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CIRCLE);
circle.setStyle("-fx-text-fill: CHART_COLOR_" + (i+1)); if(i < 8) {
circle.setStyle("-fx-text-fill: CHART_COLOR_" + (i+1));
}
label.setGraphic(circle); label.setGraphic(circle);
getChildren().add(label); getChildren().add(label);
} }

View file

@ -1,17 +1,24 @@
package com.sparrowwallet.sparrow.event; package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.sparrow.wallet.HashIndexEntry; import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Wallet;
import java.util.List; import java.util.List;
public class SendActionEvent { public class SendActionEvent {
private final List<HashIndexEntry> utxoEntries; private final Wallet wallet;
private final List<BlockTransactionHashIndex> utxos;
public SendActionEvent(List<HashIndexEntry> utxoEntries) { public SendActionEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
this.utxoEntries = utxoEntries; this.wallet = wallet;
this.utxos = utxos;
} }
public List<HashIndexEntry> getUtxoEntries() { public Wallet getWallet() {
return utxoEntries; return wallet;
}
public List<BlockTransactionHashIndex> getUtxos() {
return utxos;
} }
} }

View file

@ -1,17 +1,47 @@
package com.sparrowwallet.sparrow.event; package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.sparrow.wallet.HashIndexEntry; import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Payment;
import com.sparrowwallet.drongo.wallet.Wallet;
import java.util.List; import java.util.List;
public class SpendUtxoEvent { public class SpendUtxoEvent {
private final List<HashIndexEntry> utxoEntries; private final Wallet wallet;
private final List<BlockTransactionHashIndex> utxos;
private final List<Payment> payments;
private final Long fee;
private final boolean includeMempoolInputs;
public SpendUtxoEvent(List<HashIndexEntry> utxoEntries) { public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
this.utxoEntries = utxoEntries; this(wallet, utxos, null, null, false);
} }
public List<HashIndexEntry> getUtxoEntries() { public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, Long fee, boolean includeMempoolInputs) {
return utxoEntries; this.wallet = wallet;
this.utxos = utxos;
this.payments = payments;
this.fee = fee;
this.includeMempoolInputs = includeMempoolInputs;
}
public Wallet getWallet() {
return wallet;
}
public List<BlockTransactionHashIndex> getUtxos() {
return utxos;
}
public List<Payment> getPayments() {
return payments;
}
public Long getFee() {
return fee;
}
public boolean isIncludeMempoolInputs() {
return includeMempoolInputs;
} }
} }

View file

@ -30,6 +30,7 @@ public class FontAwesome5 extends GlyphFont {
ELLIPSIS_H('\uf141'), ELLIPSIS_H('\uf141'),
EYE('\uf06e'), EYE('\uf06e'),
HAND_HOLDING('\uf4bd'), HAND_HOLDING('\uf4bd'),
HAND_HOLDING_MEDICAL('\ue05c'),
KEY('\uf084'), KEY('\uf084'),
LAPTOP('\uf109'), LAPTOP('\uf109'),
LOCK('\uf023'), LOCK('\uf023'),

View file

@ -138,7 +138,16 @@ public class ElectrumServer {
public Map<WalletNode, Set<BlockTransactionHash>> getHistory(Wallet wallet, Collection<WalletNode> nodes) throws ServerException { public Map<WalletNode, Set<BlockTransactionHash>> getHistory(Wallet wallet, Collection<WalletNode> nodes) throws ServerException {
Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap = new TreeMap<>(); Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap = new TreeMap<>();
subscribeWalletNodes(wallet, nodes, nodeTransactionMap, 0);
Set<WalletNode> historyNodes = new HashSet<>(nodes);
//Add any nodes with mempool transactions in case these have been replaced
Set<WalletNode> mempoolNodes = wallet.getWalletTxos().entrySet().stream()
.filter(entry -> entry.getKey().getHeight() <= 0 || (entry.getKey().getSpentBy() != null && entry.getKey().getSpentBy().getHeight() <= 0))
.map(Map.Entry::getValue)
.collect(Collectors.toSet());
historyNodes.addAll(mempoolNodes);
subscribeWalletNodes(wallet, historyNodes, nodeTransactionMap, 0);
getReferences(wallet, nodeTransactionMap.keySet(), nodeTransactionMap, 0); getReferences(wallet, nodeTransactionMap.keySet(), nodeTransactionMap, 0);
Set<BlockTransactionHash> newReferences = nodeTransactionMap.values().stream().flatMap(Collection::stream).filter(ref -> !wallet.getTransactions().containsKey(ref.getHash())).collect(Collectors.toSet()); Set<BlockTransactionHash> newReferences = nodeTransactionMap.values().stream().flatMap(Collection::stream).filter(ref -> !wallet.getTransactions().containsKey(ref.getHash())).collect(Collectors.toSet());
getReferencedTransactions(wallet, nodeTransactionMap); getReferencedTransactions(wallet, nodeTransactionMap);
@ -152,7 +161,7 @@ public class ElectrumServer {
BlockTransaction blockTransaction = wallet.getTransactions().get(reference.getHash()); BlockTransaction blockTransaction = wallet.getTransactions().get(reference.getHash());
for(TransactionOutput txOutput : blockTransaction.getTransaction().getOutputs()) { for(TransactionOutput txOutput : blockTransaction.getTransaction().getOutputs()) {
WalletNode node = walletScriptHashes.get(getScriptHash(txOutput)); WalletNode node = walletScriptHashes.get(getScriptHash(txOutput));
if(node != null && !nodes.contains(node)) { if(node != null && !historyNodes.contains(node)) {
additionalNodes.add(node); additionalNodes.add(node);
} }
} }
@ -162,7 +171,7 @@ public class ElectrumServer {
if(inputBlockTransaction != null) { if(inputBlockTransaction != null) {
TransactionOutput txOutput = inputBlockTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex()); TransactionOutput txOutput = inputBlockTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex());
WalletNode node = walletScriptHashes.get(getScriptHash(txOutput)); WalletNode node = walletScriptHashes.get(getScriptHash(txOutput));
if(node != null && !nodes.contains(node)) { if(node != null && !historyNodes.contains(node)) {
additionalNodes.add(node); additionalNodes.add(node);
} }
} }

View file

@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.net; package com.sparrowwallet.sparrow.net;
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcMethod; import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcMethod;
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcOptional;
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcParam; import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcParam;
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcService; import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcService;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
@ -23,7 +24,12 @@ public class SubscriptionService {
} }
@JsonRpcMethod("blockchain.scripthash.subscribe") @JsonRpcMethod("blockchain.scripthash.subscribe")
public void scriptHashStatusUpdated(@JsonRpcParam("scripthash") final String scriptHash, @JsonRpcParam("status") final String status) { public void scriptHashStatusUpdated(@JsonRpcParam("scripthash") final String scriptHash, @JsonRpcOptional @JsonRpcParam("status") final String status) {
if(status == null) {
//Mempool transaction was replaced returning change/consolidation script hash status to null, ignore this update
return;
}
Set<String> existingStatuses = ElectrumServer.getSubscribedScriptHashes().get(scriptHash); Set<String> existingStatuses = ElectrumServer.getSubscribedScriptHashes().get(scriptHash);
if(existingStatuses == null) { if(existingStatuses == null) {
log.warn("Received script hash status update for unsubscribed script hash: " + scriptHash); log.warn("Received script hash status update for unsubscribed script hash: " + scriptHash);

View file

@ -255,6 +255,10 @@ public class PaymentController extends WalletFormController implements Initializ
public void setPayment(Payment payment) { public void setPayment(Payment payment) {
if(getRecipientValueSats() == null || payment.getAmount() != getRecipientValueSats()) { if(getRecipientValueSats() == null || payment.getAmount() != getRecipientValueSats()) {
address.setText(payment.getAddress().toString());
if(payment.getLabel() != null) {
label.setText(payment.getLabel());
}
setRecipientValueSats(payment.getAmount()); setRecipientValueSats(payment.getAmount());
setFiatAmount(AppController.getFiatCurrencyExchangeRate(), payment.getAmount()); setFiatAmount(AppController.getFiatCurrencyExchangeRate(), payment.getAmount());
} }

View file

@ -119,6 +119,8 @@ public class SendController extends WalletFormController implements Initializabl
private final StringProperty utxoLabelSelectionProperty = new SimpleStringProperty(""); private final StringProperty utxoLabelSelectionProperty = new SimpleStringProperty("");
private final BooleanProperty includeMempoolInputsProperty = new SimpleBooleanProperty(false);
private final ChangeListener<String> feeListener = new ChangeListener<>() { private final ChangeListener<String> feeListener = new ChangeListener<>() {
@Override @Override
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) { public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
@ -147,7 +149,7 @@ public class SendController extends WalletFormController implements Initializabl
feeRate.setText("Unknown"); feeRate.setText("Unknown");
} }
Tooltip tooltip = new Tooltip("Target confirmation within " + target + " blocks"); Tooltip tooltip = new Tooltip("Target inclusion within " + target + " blocks");
targetBlocks.setTooltip(tooltip); targetBlocks.setTooltip(tooltip);
userFeeSet.set(false); userFeeSet.set(false);
@ -271,6 +273,7 @@ public class SendController extends WalletFormController implements Initializabl
FeeRatesSelection feeRatesSelection = Config.get().getFeeRatesSelection(); FeeRatesSelection feeRatesSelection = Config.get().getFeeRatesSelection();
feeRatesSelection = (feeRatesSelection == null ? FeeRatesSelection.BLOCK_TARGET : feeRatesSelection); feeRatesSelection = (feeRatesSelection == null ? FeeRatesSelection.BLOCK_TARGET : feeRatesSelection);
setDefaultFeeRate();
updateFeeRateSelection(feeRatesSelection); updateFeeRateSelection(feeRatesSelection);
feeSelectionToggleGroup.selectToggle(feeRatesSelection == FeeRatesSelection.BLOCK_TARGET ? targetBlocksToggle : mempoolSizeToggle); feeSelectionToggleGroup.selectToggle(feeRatesSelection == FeeRatesSelection.BLOCK_TARGET ? targetBlocksToggle : mempoolSizeToggle);
feeSelectionToggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> { feeSelectionToggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
@ -318,15 +321,12 @@ public class SendController extends WalletFormController implements Initializabl
walletTransactionProperty.addListener((observable, oldValue, walletTransaction) -> { walletTransactionProperty.addListener((observable, oldValue, walletTransaction) -> {
if(walletTransaction != null) { if(walletTransaction != null) {
for(int i = 0; i < paymentTabs.getTabs().size(); i++) { setPayments(walletTransaction.getPayments());
Payment payment = walletTransaction.getPayments().get(i);
PaymentController controller = (PaymentController)paymentTabs.getTabs().get(i).getUserData();
controller.setPayment(payment);
}
double feeRate = walletTransaction.getFeeRate(); double feeRate = walletTransaction.getFeeRate();
if(userFeeSet.get()) { if(userFeeSet.get()) {
setTargetBlocks(getTargetBlocks(feeRate)); setTargetBlocks(getTargetBlocks(feeRate));
setFeeRangeRate(feeRate);
} else { } else {
setFeeValueSats(walletTransaction.getFee()); setFeeValueSats(walletTransaction.getFee());
} }
@ -386,7 +386,8 @@ public class SendController extends WalletFormController implements Initializabl
} }
public Tab getPaymentTab() { public Tab getPaymentTab() {
Tab tab = new Tab(Integer.toString(paymentTabs.getTabs().size() + 1)); OptionalInt highestTabNo = paymentTabs.getTabs().stream().mapToInt(tab -> Integer.parseInt(tab.getText())).max();
Tab tab = new Tab(Integer.toString(highestTabNo.isPresent() ? highestTabNo.getAsInt() + 1 : 1));
try { try {
FXMLLoader paymentLoader = new FXMLLoader(AppController.class.getResource("wallet/payment.fxml")); FXMLLoader paymentLoader = new FXMLLoader(AppController.class.getResource("wallet/payment.fxml"));
@ -411,6 +412,22 @@ public class SendController extends WalletFormController implements Initializabl
return payments; return payments;
} }
public void setPayments(List<Payment> payments) {
while(paymentTabs.getTabs().size() < payments.size()) {
addPaymentTab();
}
while(paymentTabs.getTabs().size() > payments.size()) {
paymentTabs.getTabs().remove(paymentTabs.getTabs().size() - 1);
}
for(int i = 0; i < paymentTabs.getTabs().size(); i++) {
Payment payment = payments.get(i);
PaymentController controller = (PaymentController)paymentTabs.getTabs().get(i).getUserData();
controller.setPayment(payment);
}
}
public void updateTransaction() { public void updateTransaction() {
updateTransaction(null); updateTransaction(null);
} }
@ -437,7 +454,8 @@ public class SendController extends WalletFormController implements Initializabl
Integer currentBlockHeight = AppController.getCurrentBlockHeight(); Integer currentBlockHeight = AppController.getCurrentBlockHeight();
boolean groupByAddress = Config.get().isGroupByAddress(); boolean groupByAddress = Config.get().isGroupByAddress();
boolean includeMempoolChange = Config.get().isIncludeMempoolChange(); boolean includeMempoolChange = Config.get().isIncludeMempoolChange();
WalletTransaction walletTransaction = wallet.createWalletTransaction(getUtxoSelectors(), getUtxoFilters(), payments, getFeeRate(), getMinimumFeeRate(), userFee, currentBlockHeight, groupByAddress, includeMempoolChange); boolean includeMempoolInputs = includeMempoolInputsProperty.get();
WalletTransaction walletTransaction = wallet.createWalletTransaction(getUtxoSelectors(), getUtxoFilters(), payments, getFeeRate(), getMinimumFeeRate(), userFee, currentBlockHeight, groupByAddress, includeMempoolChange, includeMempoolInputs);
walletTransactionProperty.setValue(walletTransaction); walletTransactionProperty.setValue(walletTransaction);
insufficientInputsProperty.set(false); insufficientInputsProperty.set(false);
@ -477,7 +495,11 @@ public class SendController extends WalletFormController implements Initializabl
boolean blockTargetSelection = (feeRatesSelection == FeeRatesSelection.BLOCK_TARGET); boolean blockTargetSelection = (feeRatesSelection == FeeRatesSelection.BLOCK_TARGET);
targetBlocksField.setVisible(blockTargetSelection); targetBlocksField.setVisible(blockTargetSelection);
blockTargetFeeRatesChart.setVisible(blockTargetSelection); blockTargetFeeRatesChart.setVisible(blockTargetSelection);
setDefaultFeeRate(); if(blockTargetSelection) {
setTargetBlocks(getTargetBlocks(getFeeRangeRate()));
} else {
setFeeRangeRate(getTargetBlocksFeeRates().get(getTargetBlocks()));
}
updateTransaction(); updateTransaction();
} }
@ -485,14 +507,10 @@ public class SendController extends WalletFormController implements Initializabl
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1); int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
int index = TARGET_BLOCKS_RANGE.indexOf(defaultTarget); int index = TARGET_BLOCKS_RANGE.indexOf(defaultTarget);
Double defaultRate = getTargetBlocksFeeRates().get(defaultTarget); Double defaultRate = getTargetBlocksFeeRates().get(defaultTarget);
if(targetBlocksField.isVisible()) { targetBlocks.setValue(index);
targetBlocks.setValue(index); blockTargetFeeRatesChart.select(defaultTarget);
blockTargetFeeRatesChart.select(defaultTarget); setFeeRangeRate(defaultRate);
setFeeRate(defaultRate); setFeeRate(getFeeRangeRate());
} else {
feeRange.setValue(Math.log(defaultRate) / Math.log(2));
setFeeRate(getFeeRangeRate());
}
} }
private Long getFeeValueSats() { private Long getFeeValueSats() {
@ -528,7 +546,7 @@ public class SendController extends WalletFormController implements Initializabl
for(Integer targetBlocks : targetBlocksFeeRates.keySet()) { for(Integer targetBlocks : targetBlocksFeeRates.keySet()) {
maxTargetBlocks = Math.max(maxTargetBlocks, targetBlocks); maxTargetBlocks = Math.max(maxTargetBlocks, targetBlocks);
Double candidate = targetBlocksFeeRates.get(targetBlocks); Double candidate = targetBlocksFeeRates.get(targetBlocks);
if(feeRate > candidate) { if(Math.round(feeRate) >= Math.round(candidate)) {
return targetBlocks; return targetBlocks;
} }
} }
@ -541,6 +559,8 @@ public class SendController extends WalletFormController implements Initializabl
int index = TARGET_BLOCKS_RANGE.indexOf(target); int index = TARGET_BLOCKS_RANGE.indexOf(target);
targetBlocks.setValue(index); targetBlocks.setValue(index);
blockTargetFeeRatesChart.select(target); blockTargetFeeRatesChart.select(target);
Tooltip tooltip = new Tooltip("Target inclusion within " + target + " blocks");
targetBlocks.setTooltip(tooltip);
targetBlocks.valueProperty().addListener(targetBlocksListener); targetBlocks.valueProperty().addListener(targetBlocksListener);
} }
@ -559,6 +579,12 @@ public class SendController extends WalletFormController implements Initializabl
return Math.pow(2.0, feeRange.getValue()); return Math.pow(2.0, feeRange.getValue());
} }
private void setFeeRangeRate(Double feeRate) {
feeRange.valueProperty().removeListener(feeRangeListener);
feeRange.setValue(Math.log(feeRate) / Math.log(2));
feeRange.valueProperty().addListener(feeRangeListener);
}
public Double getFeeRate() { public Double getFeeRate() {
if(targetBlocksField.isVisible()) { if(targetBlocksField.isVisible()) {
return getTargetBlocksFeeRates().get(getTargetBlocks()); return getTargetBlocksFeeRates().get(getTargetBlocks());
@ -631,9 +657,10 @@ public class SendController extends WalletFormController implements Initializabl
fiatFeeAmount.setText(""); fiatFeeAmount.setText("");
userFeeSet.set(false); userFeeSet.set(false);
targetBlocks.setValue(4); setDefaultFeeRate();
utxoSelectorProperty.setValue(null); utxoSelectorProperty.setValue(null);
utxoFilterProperty.setValue(null); utxoFilterProperty.setValue(null);
includeMempoolInputsProperty.set(false);
walletTransactionProperty.setValue(null); walletTransactionProperty.setValue(null);
createdWalletTransactionProperty.set(null); createdWalletTransactionProperty.set(null);
@ -779,11 +806,23 @@ public class SendController extends WalletFormController implements Initializabl
@Subscribe @Subscribe
public void spendUtxos(SpendUtxoEvent event) { public void spendUtxos(SpendUtxoEvent event) {
if(!event.getUtxoEntries().isEmpty() && event.getUtxoEntries().get(0).getWallet().equals(getWalletForm().getWallet())) { if(!event.getUtxos().isEmpty() && event.getWallet().equals(getWalletForm().getWallet())) {
List<BlockTransactionHashIndex> utxos = event.getUtxoEntries().stream().map(HashIndexEntry::getHashIndex).collect(Collectors.toList()); if(event.getPayments() != null) {
clear(null);
setPayments(event.getPayments());
}
if(event.getFee() != null) {
setFeeValueSats(event.getFee());
userFeeSet.set(true);
}
includeMempoolInputsProperty.set(event.isIncludeMempoolInputs());
List<BlockTransactionHashIndex> utxos = event.getUtxos();
utxoSelectorProperty.set(new PresetUtxoSelector(utxos)); utxoSelectorProperty.set(new PresetUtxoSelector(utxos));
utxoFilterProperty.set(null); utxoFilterProperty.set(null);
updateTransaction(true); updateTransaction(event.getPayments() == null);
} }
} }

View file

@ -138,9 +138,7 @@ public class TransactionEntry extends Entry implements Comparable<TransactionEnt
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
TransactionEntry that = (TransactionEntry) o; TransactionEntry that = (TransactionEntry) o;
// Even though the txid identifies a transaction, receiving an incomplete set of script hash notifications can result in some inputs/outputs for a tx being missing. return getWallet().equals(that.getWallet()) && blockTransaction.equals(that.blockTransaction);
// To resolve this we check the number of children, but not the children themselves (since we don't care here when they are spent)
return getWallet().equals(that.getWallet()) && blockTransaction.equals(that.blockTransaction) && getChildren().size() == that.getChildren().size();
} }
@Override @Override

View file

@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.wallet;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.BitcoinUnit; import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.CoinLabel; import com.sparrowwallet.sparrow.control.CoinLabel;
import com.sparrowwallet.sparrow.control.UtxosChart; import com.sparrowwallet.sparrow.control.UtxosChart;
@ -81,8 +82,9 @@ public class UtxosController extends WalletFormController implements Initializab
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT) && e.isSpendable()) .filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT) && e.isSpendable())
.collect(Collectors.toList()); .collect(Collectors.toList());
EventManager.get().post(new SendActionEvent(utxoEntries)); final List<BlockTransactionHashIndex> spendingUtxos = utxoEntries.stream().map(HashIndexEntry::getHashIndex).collect(Collectors.toList());
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(utxoEntries))); EventManager.get().post(new SendActionEvent(getWalletForm().getWallet(), spendingUtxos));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(getWalletForm().getWallet(), spendingUtxos)));
} }
public void clear(ActionEvent event) { public void clear(ActionEvent event) {

View file

@ -121,7 +121,7 @@ public class WalletController extends WalletFormController implements Initializa
@Subscribe @Subscribe
public void sendAction(SendActionEvent event) { public void sendAction(SendActionEvent event) {
if(!event.getUtxoEntries().isEmpty() && event.getUtxoEntries().get(0).getWallet().equals(walletForm.getWallet())) { if(!event.getUtxos().isEmpty() && event.getWallet().equals(walletForm.getWallet())) {
selectFunction(Function.SEND); selectFunction(Function.SEND);
} }
} }

View file

@ -76,7 +76,8 @@ public class WalletTransactionsEntry extends Entry {
entriesAdded.removeAll(entriesComplete); entriesAdded.removeAll(entriesComplete);
for(Entry entry : entriesAdded) { for(Entry entry : entriesAdded) {
TransactionEntry txEntry = (TransactionEntry)entry; TransactionEntry txEntry = (TransactionEntry)entry;
log.warn("Not notifying for incomplete entry " + ((TransactionEntry)entry).getBlockTransaction().getHashAsString() + " value " + txEntry.getValue()); getChildren().remove(txEntry);
log.warn("Removing and not notifying incomplete entry " + ((TransactionEntry)entry).getBlockTransaction().getHashAsString() + " value " + txEntry.getValue());
} }
} }
} }